mirror of
https://github.com/DYefremov/DemonEditor.git
synced 2026-05-09 09:37:23 +02:00
Compare commits
278 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d3a9f768c | ||
|
|
37bea3a93c | ||
|
|
33a22fdca7 | ||
|
|
d54e97b1b8 | ||
|
|
12c2a449ea | ||
|
|
7164f54773 | ||
|
|
84adeb994e | ||
|
|
69c3b4a6c1 | ||
|
|
cbec74c2a4 | ||
|
|
8d856bc989 | ||
|
|
a1ec7600da | ||
|
|
2fd71c3645 | ||
|
|
a0f6b1f651 | ||
|
|
671c2204bf | ||
|
|
d2a0419c06 | ||
|
|
fb48395d1e | ||
|
|
44fb760241 | ||
|
|
428a240416 | ||
|
|
8c58b9395b | ||
|
|
021b2b08cf | ||
|
|
0d68e43212 | ||
|
|
751e633a51 | ||
|
|
9ff96f2e1d | ||
|
|
3e85fa0149 | ||
|
|
ab5620f9d1 | ||
|
|
bceddc199b | ||
|
|
4f26855ec3 | ||
|
|
15b8483107 | ||
|
|
f4dac57d06 | ||
|
|
71f7b3a570 | ||
|
|
265fb59f0b | ||
|
|
5b090672d9 | ||
|
|
aa8f2a8df0 | ||
|
|
6782da5f83 | ||
|
|
26d7b22c3a | ||
|
|
69ec1f8359 | ||
|
|
c5aac859b0 | ||
|
|
8d0b241ca3 | ||
|
|
8943eab99d | ||
|
|
a5a4f267cc | ||
|
|
a69127f0cc | ||
|
|
858b2ae2d6 | ||
|
|
13a08c98de | ||
|
|
272e3786dc | ||
|
|
3b510e6935 | ||
|
|
f3a6d2bd9c | ||
|
|
ca20852bfe | ||
|
|
5e1bd8e1c9 | ||
|
|
0b87f4f143 | ||
|
|
00cbe43aa7 | ||
|
|
deb161a153 | ||
|
|
2a2611abde | ||
|
|
dbcfb71224 | ||
|
|
2441d3726b | ||
|
|
681b43b164 | ||
|
|
31025777a3 | ||
|
|
33e39d2f25 | ||
|
|
14e200f262 | ||
|
|
3cef75e765 | ||
|
|
a973f8e636 | ||
|
|
c6bea94ff5 | ||
|
|
736655542c | ||
|
|
073521de75 | ||
|
|
46748d3fc4 | ||
|
|
f2e571185d | ||
|
|
c2fd116252 | ||
|
|
c1c5e866ad | ||
|
|
d4a2e78a09 | ||
|
|
0f68d5b292 | ||
|
|
c3d9159822 | ||
|
|
b5a508ef54 | ||
|
|
14459b8e7e | ||
|
|
a9998b9d17 | ||
|
|
6d05a6ec20 | ||
|
|
b821fd54be | ||
|
|
c3bc3a1160 | ||
|
|
748b41e31c | ||
|
|
a4cbe00e96 | ||
|
|
9f0ad72d42 | ||
|
|
673a8547ff | ||
|
|
f6058dafb9 | ||
|
|
0140fb4eb4 | ||
|
|
97f04999c4 | ||
|
|
e879c8db18 | ||
|
|
d41ceb6bbc | ||
|
|
8e82d8562a | ||
|
|
4cba9a6754 | ||
|
|
d568a9429d | ||
|
|
8b39fedaed | ||
|
|
c3351b43cc | ||
|
|
58156dd4c1 | ||
|
|
d09c14518e | ||
|
|
a8739be31d | ||
|
|
67a75e5ffa | ||
|
|
7d31a050fe | ||
|
|
5f4cee759f | ||
|
|
f7ed1736c5 | ||
|
|
4bdbb511ee | ||
|
|
e49d32f931 | ||
|
|
42feb8d5f6 | ||
|
|
e408f88afd | ||
|
|
eaf0434e19 | ||
|
|
079cf6a482 | ||
|
|
d3ae2187c8 | ||
|
|
eac0cc47a9 | ||
|
|
1972374505 | ||
|
|
d8626e63cc | ||
|
|
528f59d990 | ||
|
|
28802957fc | ||
|
|
b763d9785d | ||
|
|
601a81beb9 | ||
|
|
ace38433a1 | ||
|
|
536b23a845 | ||
|
|
d71a1d5dac | ||
|
|
4a92084c75 | ||
|
|
e7b8412c11 | ||
|
|
ef53de1796 | ||
|
|
8e7a116db7 | ||
|
|
285014480f | ||
|
|
279c255ad0 | ||
|
|
90a3053192 | ||
|
|
19c6a5bef9 | ||
|
|
945ee13058 | ||
|
|
c65b6c540c | ||
|
|
62091dfa96 | ||
|
|
6ca06fd2cd | ||
|
|
4cab05fc09 | ||
|
|
81e714ebab | ||
|
|
147430d4f3 | ||
|
|
f8f209d288 | ||
|
|
bd0e08e90b | ||
|
|
57020423d7 | ||
|
|
1fd3e45dd3 | ||
|
|
ac1725b3ef | ||
|
|
39a592fd4d | ||
|
|
e40e0f2458 | ||
|
|
d5889cd96c | ||
|
|
77a8bfe2c6 | ||
|
|
470d2d843b | ||
|
|
a908845b4e | ||
|
|
8fee5033a4 | ||
|
|
a9b1f8b26c | ||
|
|
8fa306a9d1 | ||
|
|
383ea2b9b3 | ||
|
|
a40ba2ff68 | ||
|
|
421d9b1c96 | ||
|
|
7357939241 | ||
|
|
08ef7bc451 | ||
|
|
8b255ec824 | ||
|
|
c81084015d | ||
|
|
66c8e9e916 | ||
|
|
4b93ae6950 | ||
|
|
e2cafef113 | ||
|
|
c35be2aa24 | ||
|
|
852404bae6 | ||
|
|
f6d2765137 | ||
|
|
182c7a9cc7 | ||
|
|
271ea97040 | ||
|
|
364fb68743 | ||
|
|
85a9d5e67e | ||
|
|
50c0a0cf37 | ||
|
|
25fd6df967 | ||
|
|
6106e86d18 | ||
|
|
1c5f7fab11 | ||
|
|
024f90d23f | ||
|
|
ee3041174c | ||
|
|
6f28aae40c | ||
|
|
1b2de795a2 | ||
|
|
8d485a9993 | ||
|
|
1721567731 | ||
|
|
fd4325961c | ||
|
|
294d32c705 | ||
|
|
aa961030ce | ||
|
|
bc4c6746c9 | ||
|
|
61282b0cc8 | ||
|
|
07e855f99d | ||
|
|
0d0f19122b | ||
|
|
4789688efd | ||
|
|
b13c2c3be0 | ||
|
|
d72189abc4 | ||
|
|
35d194100b | ||
|
|
aa0b97b9ae | ||
|
|
5f54452ee2 | ||
|
|
580e8ca82c | ||
|
|
4ba2fb1a04 | ||
|
|
b4612c26cb | ||
|
|
9a8b1e871d | ||
|
|
3a53a95f86 | ||
|
|
24a94cfe9a | ||
|
|
0ca08e3a1d | ||
|
|
db4e9d2696 | ||
|
|
3be9b374c8 | ||
|
|
0b9fd37ee9 | ||
|
|
be90d9694a | ||
|
|
a5144e8e34 | ||
|
|
04e0a25956 | ||
|
|
dc24c899af | ||
|
|
a55495fd7c | ||
|
|
ed3aea42f5 | ||
|
|
a6904360f9 | ||
|
|
1e42d693cc | ||
|
|
5e64605be6 | ||
|
|
63c55ea2ed | ||
|
|
c7f85b027d | ||
|
|
b24910a9a5 | ||
|
|
1bdb4f123f | ||
|
|
8f8d7633b8 | ||
|
|
c997724300 | ||
|
|
727a3fa8a2 | ||
|
|
4009f5c2a2 | ||
|
|
ca5d648032 | ||
|
|
938fe297c5 | ||
|
|
db29b78fd7 | ||
|
|
e599ea04c7 | ||
|
|
50a2f66fc3 | ||
|
|
34056b1006 | ||
|
|
6188fecda9 | ||
|
|
eb41b9629e | ||
|
|
7694754919 | ||
|
|
11f240a81f | ||
|
|
1b75034317 | ||
|
|
5b6900cae7 | ||
|
|
eeeca881e8 | ||
|
|
ed16fb0195 | ||
|
|
833b386356 | ||
|
|
83424124d3 | ||
|
|
d77aa68a39 | ||
|
|
94266e13b8 | ||
|
|
8db6fb1b0b | ||
|
|
e07c5d4bf7 | ||
|
|
6f3090a7e1 | ||
|
|
b73c1d1118 | ||
|
|
df127c05f3 | ||
|
|
9b579af528 | ||
|
|
8290e723c9 | ||
|
|
b12c29be84 | ||
|
|
1780fbadbd | ||
|
|
4fe4e92442 | ||
|
|
bfad5cf9ac | ||
|
|
5d285f61c0 | ||
|
|
f61d9a1f61 | ||
|
|
8d115677d1 | ||
|
|
f7c6cd6908 | ||
|
|
2d2a90542c | ||
|
|
535c9c9102 | ||
|
|
866e18762d | ||
|
|
aef5027d23 | ||
|
|
3a142eca4a | ||
|
|
606bad7716 | ||
|
|
fa07f8bf85 | ||
|
|
92280162c6 | ||
|
|
5d285e88d8 | ||
|
|
0355714e92 | ||
|
|
b06e877a0c | ||
|
|
9b479b051d | ||
|
|
b953ee8762 | ||
|
|
0e7d6bec69 | ||
|
|
cb9824d404 | ||
|
|
c87adb256f | ||
|
|
899d05a186 | ||
|
|
bd4f86e91e | ||
|
|
42fb365b45 | ||
|
|
67d6ea861e | ||
|
|
d887a61636 | ||
|
|
9d9efb7577 | ||
|
|
b6d331a311 | ||
|
|
1060e169a1 | ||
|
|
562c1a5955 | ||
|
|
3c4dec323f | ||
|
|
3bafe08030 | ||
|
|
722f8df813 | ||
|
|
8f6984dbaf | ||
|
|
bf6e9617ec | ||
|
|
ec27c32d35 | ||
|
|
bfa3b1aa66 | ||
|
|
a1e32abd07 | ||
|
|
f93370293b | ||
|
|
79a2a034eb |
70
README.md
70
README.md
@@ -1,33 +1,32 @@
|
||||
# <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)
|
||||
|
||||
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).
|
||||
### Enigma2 channel and satellite list editor for GNU/Linux.
|
||||
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
|
||||
|
||||
## 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)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141680963-9b8eb6cc-c712-46b2-aefe-19769e21a7d5.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141680963-9b8eb6cc-c712-46b2-aefe-19769e21a7d5.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)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141681059-68bc1b55-6fab-436c-aa73-ef24e2e5113b.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681059-68bc1b55-6fab-436c-aa73-ef24e2e5113b.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)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141681104-ed9b5d35-25de-426f-b9bb-2a6e4db022bb.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681104-ed9b5d35-25de-426f-b9bb-2a6e4db022bb.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)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141681115-957c63a3-4113-422d-bb27-2d96b1463cd1.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681115-957c63a3-4113-422d-bb27-2d96b1463cd1.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)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141681075-28f18ea5-e456-4e84-bf64-1b7d9a95324d.png" width="262"/>](https://user-images.githubusercontent.com/7511379/141681075-28f18ea5-e456-4e84-bf64-1b7d9a95324d.png)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141681040-b1ad190a-6bc2-4741-bb42-1fb219a0fcab.png" width="250"/>](https://user-images.githubusercontent.com/7511379/141681040-b1ad190a-6bc2-4741-bb42-1fb219a0fcab.png)
|
||||
* Extended support of IPTV.
|
||||
* Import to bouquet(Neutrino WEBTV) from m3u.
|
||||
* Export of bouquets with IPTV services in m3u.
|
||||
* Assignment of EPG from DVB or XML for IPTV services (only Enigma2, experimental).
|
||||
* Preview (playback) of IPTV or other streams directly from the bouquet list.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/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)
|
||||
* Assignment of EPG from DVB or XML for IPTV services (Enigma2 only).
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141681187-fae4e784-c9e0-43df-b499-4d38e83d6560.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681187-fae4e784-c9e0-43df-b499-4d38e83d6560.png)
|
||||
* Playback of IPTV or other streams directly from the bouquet list.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141681129-98f78cdc-9a98-46ef-b738-618a327634d4.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681129-98f78cdc-9a98-46ef-b738-618a327634d4.png)
|
||||
* Control panel (via HTTP API).
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141684475-4511ea4f-b152-42d5-b9c8-f3e1e9a160d0.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141684475-4511ea4f-b152-42d5-b9c8-f3e1e9a160d0.png)
|
||||
* Ability to view EPG and manage timers (via HTTP API).
|
||||
* Simple FTP client (experimental).
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/118527372-e8bb0b80-b749-11eb-9653-4ad64c99a05a.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118527372-e8bb0b80-b749-11eb-9653-4ad64c99a05a.png)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141681165-5679c331-72e7-4044-b365-dcdb30b1433c.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681165-5679c331-72e7-4044-b365-dcdb30b1433c.png)
|
||||
|
||||
#### Keyboard shortcuts
|
||||
* **Ctrl + X** - only in bouquet list.
|
||||
@@ -37,7 +36,8 @@ Clipboard is **"rubber"**. There is an accumulation before the insertion!
|
||||
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 + R, F2** - rename.
|
||||
* **Ctrl + Alt + R** - rename for bouquet.
|
||||
* **Ctrl + S, T** in Satellites edit tool for create satellite or transponder.
|
||||
* **Ctrl + L** - parental lock.
|
||||
* **Ctrl + H** - hide/skip.
|
||||
@@ -51,8 +51,10 @@ Clipboard is **"rubber"**. There is an accumulation before the insertion!
|
||||
* **Ctrl + D** - load data from receiver.
|
||||
* **Ctrl + U/B** - upload data/bouquets to receiver.
|
||||
* **Ctrl + I** - extra info, details.
|
||||
* **Ctrl + F** - show/hide search bar.
|
||||
* **Ctrl + F** - show search bar.
|
||||
* **Ctrl + Shift + F** - show/hide filter bar.
|
||||
* **Ctrl + T** - show/hide built-in Telnet client.
|
||||
* **Ctrl + Shift + L** - show/hide logging panel.
|
||||
|
||||
For **multiple** selection with the mouse, press and hold the **Ctrl** key!
|
||||
|
||||
@@ -71,13 +73,33 @@ To create a simple **debian package**, you can use the *build-deb.sh.* You can a
|
||||
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!**
|
||||
**This program can be run on macOS.**
|
||||
To run the program on macOS, you need to install [brew](https://brew.sh/).
|
||||
Then install the required components via terminal:
|
||||
```brew install python3 gtk+3 pygobject3 adwaita-icon-theme```
|
||||
```pip3 install requests, pillow```
|
||||
|
||||
Launch is similar to Linux.
|
||||
|
||||
You can also download the ready-made package as a ***.dmg** file from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
|
||||
Recommended copy the package to the **Application** directory.
|
||||
Perhaps in the security settings it will be necessary to allow the launch of this application!
|
||||
|
||||
* ### MS Windows
|
||||
**Windows users can also run this program.**
|
||||
One way is to use the [MSYS2](https://www.msys2.org/) platform. You can use [this](https://github.com/DYefremov/DemonEditor/blob/master/build/BUILD_WIN.md) quick guide.
|
||||
In addition, you can download a ready-made build (**64-bit**) from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
|
||||
|
||||
**All builds may contain components distributed under the GPL [v3](http://www.gnu.org/licenses/gpl-3.0.html) or lower license.
|
||||
By downloading and using this packages you agree to the terms of this [license](http://www.gnu.org/licenses/gpl-3.0.html) and the possible inconvenience associated with this!**
|
||||
|
||||
THIS SOFTWARE COMES WITH ABSOLUTELY NO WARRANTY.
|
||||
AUTHOR IS NOT LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY CONNECTION WITH THIS SOFTWARE.
|
||||
|
||||
## 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!
|
||||
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!
|
||||
Support for DVB-T/T2 and DVB-C channels for Neutrino is not fully implemented and has an experimental status.
|
||||
|
||||
Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2.
|
||||
Main supported *lamedb* format is version **4**. Versions **3** and **5** has only **experimental** support! For version **3** is only read mode available. When saving, version **4** format is used instead.
|
||||
|
||||
When using the multiple import feature, from *lamedb* will be taken data **only for channels that are in the selected bouquets!**
|
||||
@@ -85,6 +107,8 @@ If you need full set of the data, including *[satellites, terrestrial, cables].x
|
||||
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.
|
||||
|
||||
**The built-in Telnet client does not support ANSI escape sequences!**
|
||||
|
||||
For streams playback, this app supports [VLC](https://www.videolan.org/vlc/), [MPV](https://mpv.io/) and [GStreamer](https://gstreamer.freedesktop.org/). Depending on your distro, you may need to install additional packages and libraries.
|
||||
#### Command line arguments:
|
||||
* **-l** - write logs to file.
|
||||
|
||||
@@ -6,16 +6,16 @@ from threading import Thread, Timer
|
||||
from gi.repository import GLib
|
||||
|
||||
_LOG_FILE = "demon-editor.log"
|
||||
_DATE_FORMAT = "%d-%m-%y %H:%M:%S"
|
||||
|
||||
LOG_DATE_FORMAT = "%d-%m-%y %H:%M:%S"
|
||||
LOGGER_NAME = "main_logger"
|
||||
LOG_FORMAT = "%(asctime)s %(message)s"
|
||||
|
||||
|
||||
def init_logger():
|
||||
logging.Logger(LOGGER_NAME)
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format="%(asctime)s %(message)s",
|
||||
datefmt=_DATE_FORMAT,
|
||||
format=LOG_FORMAT,
|
||||
datefmt=LOG_DATE_FORMAT,
|
||||
handlers=[logging.FileHandler(_LOG_FILE), logging.StreamHandler()])
|
||||
log("Logging is enabled.", level=logging.INFO)
|
||||
|
||||
@@ -77,6 +77,25 @@ def run_with_delay(timeout=5):
|
||||
return run_with
|
||||
|
||||
|
||||
def get_size_from_bytes(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 f"{b / kb:.1f} K"
|
||||
elif mb <= b < gb:
|
||||
return f"{b / mb:.1f} M"
|
||||
elif gb <= b:
|
||||
return f"{b / gb:.1f} G"
|
||||
|
||||
|
||||
class DefaultDict(defaultdict):
|
||||
""" Extended to support functions with params as default factory. """
|
||||
|
||||
|
||||
@@ -1,3 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
@@ -5,7 +33,7 @@ import time
|
||||
import urllib
|
||||
import xml.etree.ElementTree as ETree
|
||||
from enum import Enum
|
||||
from ftplib import FTP, CRLF, Error, error_perm
|
||||
from ftplib import FTP, CRLF, Error, all_errors
|
||||
from http.client import RemoteDisconnected
|
||||
from telnetlib import Telnet
|
||||
from urllib.error import HTTPError, URLError
|
||||
@@ -16,14 +44,15 @@ from urllib.request import (urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicA
|
||||
from app.commons import log, run_task
|
||||
from app.settings import SettingsType
|
||||
|
||||
BQ_FILES_LIST = ("tv", "radio", # enigma 2
|
||||
"services.xml", "myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino
|
||||
BQ_FILES_LIST = ("tv", "radio", # Enigma2.
|
||||
"services.xml", "myservices.xml", "bouquets.xml", "ubouquets.xml") # Neutrino.
|
||||
|
||||
DATA_FILES_LIST = ("lamedb", "lamedb5", "blacklist", "whitelist",)
|
||||
|
||||
STC_XML_FILE = ("satellites.xml", "terrestrial.xml", "cables.xml")
|
||||
WEB_TV_XML_FILE = ("webtv.xml",)
|
||||
PICONS_SUF = (".jpg", ".png")
|
||||
PICONS_MAX_NUM = 1000 # Maximum picon number for sending without compression.
|
||||
|
||||
|
||||
class DownloadType(Enum):
|
||||
@@ -83,20 +112,22 @@ class UtfFTP(FTP):
|
||||
|
||||
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)
|
||||
|
||||
msg = "Downloading file: {}. Status: {}"
|
||||
resp = self.download_binary(name, f)
|
||||
msg = msg.format(name, resp)
|
||||
callback(msg) if callback else log(msg.rstrip())
|
||||
|
||||
return resp
|
||||
|
||||
def download_binary(self, src, fo):
|
||||
try:
|
||||
resp = str(self.retrbinary(f"RETR {src}", fo.write))
|
||||
except all_errors as e:
|
||||
resp = str(e)
|
||||
log(f"Error. {e}")
|
||||
|
||||
return resp
|
||||
|
||||
def download_dir(self, path, save_path, callback=None):
|
||||
""" Downloads directory from FTP with all contents.
|
||||
|
||||
@@ -107,8 +138,8 @@ class UtfFTP(FTP):
|
||||
files = []
|
||||
self.dir(path, files.append)
|
||||
for f in files:
|
||||
f_data = f.split()
|
||||
f_path = os.path.join(path, " ".join(f_data[8:]))
|
||||
f_data = self.get_file_data(f)
|
||||
f_path = f_data[8]
|
||||
|
||||
if f_data[0][0] == "d":
|
||||
try:
|
||||
@@ -142,7 +173,7 @@ class UtfFTP(FTP):
|
||||
def download_picons(self, src, dest, callback, files_filter=None):
|
||||
try:
|
||||
self.cwd(src)
|
||||
except error_perm as e:
|
||||
except all_errors as e:
|
||||
callback(str(e))
|
||||
return
|
||||
|
||||
@@ -172,7 +203,7 @@ class UtfFTP(FTP):
|
||||
def upload_picons(self, src, dest, callback, files_filter=None):
|
||||
try:
|
||||
self.cwd(dest)
|
||||
except error_perm as e:
|
||||
except all_errors as e:
|
||||
if str(e).startswith("550"):
|
||||
self.mkd(dest) # if not exist
|
||||
self.cwd(dest)
|
||||
@@ -181,7 +212,7 @@ class UtfFTP(FTP):
|
||||
self.send_file(file_name, src, callback)
|
||||
|
||||
def remove_unused_bouquets(self, callback):
|
||||
bq_files = ("userbouquet.", "bouquets.xml", "ubouquets.xml")
|
||||
bq_files = ("userbouquet.", "subbouquet.", "bouquets.xml", "ubouquets.xml")
|
||||
|
||||
for file in filter(lambda f: f.startswith(bq_files), self.nlst()):
|
||||
self.delete_file(file, callback)
|
||||
@@ -195,18 +226,17 @@ class UtfFTP(FTP):
|
||||
return resp + " File not found."
|
||||
|
||||
with open(file_src, "rb") as f:
|
||||
msg = "Uploading file: {}. Status: {}\n"
|
||||
msg = "Uploading file: {}. Status: {}"
|
||||
try:
|
||||
resp = str(self.storbinary("STOR " + file_name, f))
|
||||
except Error as e:
|
||||
except all_errors as e:
|
||||
resp = str(e)
|
||||
msg = msg.format(file_name, resp)
|
||||
log(msg)
|
||||
else:
|
||||
msg = msg.format(file_name, resp)
|
||||
|
||||
if callback:
|
||||
callback(msg)
|
||||
if callback:
|
||||
callback(msg)
|
||||
|
||||
return resp
|
||||
|
||||
@@ -230,12 +260,12 @@ class UtfFTP(FTP):
|
||||
elif os.path.isdir(file):
|
||||
try:
|
||||
self.mkd(f)
|
||||
except Error:
|
||||
except all_errors:
|
||||
pass # NOP
|
||||
|
||||
try:
|
||||
self.cwd(f)
|
||||
except Error as e:
|
||||
except all_errors as e:
|
||||
resp = str(e)
|
||||
log(msg.format(f, resp))
|
||||
else:
|
||||
@@ -255,7 +285,7 @@ class UtfFTP(FTP):
|
||||
if dest:
|
||||
try:
|
||||
self.cwd(dest)
|
||||
except Error as e:
|
||||
except all_errors as e:
|
||||
callback(str(e))
|
||||
return
|
||||
|
||||
@@ -263,10 +293,10 @@ class UtfFTP(FTP):
|
||||
self.delete_file(file, callback)
|
||||
|
||||
def delete_file(self, file, callback=log):
|
||||
msg = "Deleting file: {}. Status: {}\n"
|
||||
msg = "Deleting file: {}. Status: {}"
|
||||
try:
|
||||
resp = self.delete(file)
|
||||
except Error as e:
|
||||
except all_errors as e:
|
||||
resp = str(e)
|
||||
msg = msg.format(file, resp)
|
||||
log(msg)
|
||||
@@ -282,19 +312,18 @@ class UtfFTP(FTP):
|
||||
files = []
|
||||
self.dir(path, files.append)
|
||||
for f in files:
|
||||
f_data = f.split()
|
||||
name = " ".join(f_data[8:])
|
||||
f_path = path + "/" + name
|
||||
f_data = self.get_file_data(f)
|
||||
f_path = f"{path}/{f_data[8]}"
|
||||
|
||||
if f_data[0][0] == "d":
|
||||
self.delete_dir(f_path, callback)
|
||||
else:
|
||||
self.delete_file(f_path, callback)
|
||||
|
||||
msg = "Remove directory {}. Status: {}\n"
|
||||
msg = "Remove directory {}. Status: {}"
|
||||
try:
|
||||
resp = self.rmd(path)
|
||||
except Error as e:
|
||||
except all_errors as e:
|
||||
msg = msg.format(path, e)
|
||||
log(msg)
|
||||
return "500"
|
||||
@@ -308,10 +337,10 @@ class UtfFTP(FTP):
|
||||
return resp
|
||||
|
||||
def rename_file(self, from_name, to_name, callback=None):
|
||||
msg = "File rename: {}. Status: {}\n"
|
||||
msg = "File rename: {}. Status: {}"
|
||||
try:
|
||||
resp = self.rename(from_name, to_name)
|
||||
except Error as e:
|
||||
except all_errors as e:
|
||||
resp = str(e)
|
||||
msg = msg.format(from_name, resp)
|
||||
log(msg)
|
||||
@@ -323,11 +352,20 @@ class UtfFTP(FTP):
|
||||
|
||||
return resp
|
||||
|
||||
@staticmethod
|
||||
def get_file_data(file):
|
||||
""" Returns a prepared list of file data from a file string. """
|
||||
f_data = file.split()
|
||||
# Ignoring space in file name.
|
||||
f_data = f_data[0:9]
|
||||
f_data[8] = file[file.index(f_data[8]):]
|
||||
return f_data
|
||||
|
||||
|
||||
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")
|
||||
callback("FTP OK.")
|
||||
save_path = settings.profile_data_path
|
||||
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||
# bouquets
|
||||
@@ -347,41 +385,35 @@ def download_data(*, settings, download_type=DownloadType.ALL, callback=log, fil
|
||||
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.get("epg_dat_stb_path", stb_path)
|
||||
save_path = epg_options.get("epg_dat_path", save_path)
|
||||
ftp.cwd(settings.epg_dat_path)
|
||||
ftp.download_files(f"{settings.profile_data_path}epg{os.sep}", "epg.dat", callback)
|
||||
|
||||
ftp.cwd(stb_path)
|
||||
ftp.download_files(save_path, "epg.dat", callback)
|
||||
|
||||
callback("\nDone.\n")
|
||||
callback("*** Done. ***")
|
||||
|
||||
|
||||
def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False,
|
||||
callback=log, done_callback=None, use_http=False, files_filter=None):
|
||||
def upload_data(*, settings, download_type=DownloadType.ALL, callback=log, done_callback=None,
|
||||
files_filter=None, ext_host=None):
|
||||
s_type = settings.setting_type
|
||||
use_http = s_type is SettingsType.ENIGMA_2 and settings.use_http
|
||||
data_path = settings.profile_data_path
|
||||
host, port, use_ssl = settings.host, settings.http_port, settings.http_use_ssl
|
||||
host, port, use_ssl = ext_host or settings.host, settings.http_port, settings.http_use_ssl
|
||||
user, password = settings.user, settings.password
|
||||
base_url = f"http{'s' if use_ssl else ''}://{host}:{port}"
|
||||
base = "web" if s_type is SettingsType.ENIGMA_2 else "control"
|
||||
url = f"{base_url}/{base}/"
|
||||
tn, ht = None, None # telnet, http
|
||||
tn, ht = None, None # Telnet, HTTP.
|
||||
|
||||
try:
|
||||
use_http = use_http and test_http(host, port, user, password, use_ssl=use_ssl, skip_message=True, s_type=s_type)
|
||||
except TestException:
|
||||
log("HTTP test failed.")
|
||||
use_http = False
|
||||
|
||||
try:
|
||||
if use_http:
|
||||
ht = http(settings.user, settings.password, base_url, callback, use_ssl, s_type)
|
||||
ht = http(user, password, base_url, callback, use_ssl, s_type)
|
||||
next(ht)
|
||||
message = ""
|
||||
if download_type is DownloadType.BOUQUETS:
|
||||
message = "User bouquets will be updated!"
|
||||
elif download_type is DownloadType.ALL:
|
||||
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!"
|
||||
message = get_upload_info_message(download_type)
|
||||
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
params = urlencode({"text": message, "type": 2, "timeout": 5})
|
||||
@@ -392,24 +424,22 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
|
||||
|
||||
if s_type is SettingsType.ENIGMA_2 and download_type is DownloadType.ALL:
|
||||
time.sleep(5)
|
||||
ht.send((f"{url}powerstate?newstate=0", "Toggle Standby "))
|
||||
if not settings.keep_power_mode:
|
||||
ht.send((f"{url}powerstate?newstate=0", "Toggle Standby "))
|
||||
time.sleep(2)
|
||||
else:
|
||||
if download_type is not DownloadType.PICONS:
|
||||
# Telnet
|
||||
tn = telnet(host=host,
|
||||
user=settings.user,
|
||||
password=settings.password,
|
||||
timeout=settings.telnet_timeout)
|
||||
tn = telnet(host=host, user=user, password=password, timeout=settings.telnet_timeout)
|
||||
next(tn)
|
||||
# Terminate Enigma2 or Neutrino.
|
||||
callback("Telnet initialization ...\n")
|
||||
callback("Telnet initialization ...")
|
||||
tn.send("init 4")
|
||||
callback("Stopping GUI...\n")
|
||||
callback("Stopping GUI...")
|
||||
|
||||
with UtfFTP(host=host, user=settings.user, passwd=settings.password) as ftp:
|
||||
with UtfFTP(host=host, user=user, passwd=password) as ftp:
|
||||
ftp.encoding = "utf-8"
|
||||
callback("FTP OK.\n")
|
||||
callback("FTP OK.")
|
||||
sat_xml_path = settings.satellites_xml_path
|
||||
services_path = settings.services_path
|
||||
|
||||
@@ -421,7 +451,7 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
|
||||
|
||||
if download_type is DownloadType.BOUQUETS:
|
||||
ftp.cwd(services_path)
|
||||
ftp.upload_bouquets(data_path, remove_unused, callback)
|
||||
ftp.upload_bouquets(data_path, settings.remove_unused_bouquets, callback)
|
||||
|
||||
if download_type is DownloadType.ALL:
|
||||
ftp.upload_xml(data_path, sat_xml_path, STC_XML_FILE, callback)
|
||||
@@ -429,23 +459,65 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
|
||||
ftp.upload_xml(data_path, sat_xml_path, WEB_TV_XML_FILE, callback)
|
||||
|
||||
ftp.cwd(services_path)
|
||||
ftp.upload_bouquets(data_path, remove_unused, callback)
|
||||
ftp.upload_bouquets(data_path, settings.remove_unused_bouquets, callback)
|
||||
ftp.upload_files(data_path, DATA_FILES_LIST, callback)
|
||||
|
||||
if download_type is DownloadType.PICONS:
|
||||
ftp.upload_picons(settings.profile_picons_path, settings.picons_path, callback, files_filter)
|
||||
p_src, p_dst = settings.profile_picons_path, settings.picons_path
|
||||
compress = all((settings.compress_picons, files_filter, len(files_filter) > PICONS_MAX_NUM))
|
||||
if compress:
|
||||
from zipfile import ZipFile
|
||||
|
||||
if tn and not use_http:
|
||||
z_name = "picons.zip"
|
||||
zip_file = f"{p_src}{z_name}"
|
||||
p_dst = os.path.abspath(os.path.join(p_dst, os.pardir))
|
||||
|
||||
if files_filter and z_name in files_filter:
|
||||
files_filter.remove(z_name)
|
||||
|
||||
if os.path.isfile(zip_file):
|
||||
try:
|
||||
os.unlink(zip_file)
|
||||
except OSError:
|
||||
pass # NOP
|
||||
|
||||
log("Compressing picons...")
|
||||
with ZipFile(zip_file, "w") as zf:
|
||||
list(map(lambda p: zf.write(os.path.join(p_src, p), arcname=p), files_filter))
|
||||
|
||||
files_filter = {z_name}
|
||||
|
||||
log("Uploading...")
|
||||
ftp.upload_picons(p_src, p_dst, callback, files_filter)
|
||||
|
||||
if compress:
|
||||
if not tn:
|
||||
callback("Telnet initialization...")
|
||||
tn = telnet(host=host, user=user, password=password, timeout=settings.telnet_timeout)
|
||||
next(tn)
|
||||
|
||||
callback("Extracting...")
|
||||
cmd = f"mkdir -p {settings.picons_path} && unzip -o -q {p_dst}/{z_name} -d {settings.picons_path}"
|
||||
tn.send(cmd)
|
||||
ftp.delete_file(z_name)
|
||||
|
||||
try:
|
||||
os.unlink(zip_file)
|
||||
except OSError:
|
||||
pass # NOP
|
||||
|
||||
if all((tn, download_type is not DownloadType.PICONS, not use_http)):
|
||||
# Resume Enigma2 or restart Neutrino.
|
||||
tn.send("init 3" if s_type is SettingsType.ENIGMA_2 else "init 6")
|
||||
callback("Starting...\n" if s_type is SettingsType.ENIGMA_2 else "Rebooting...\n")
|
||||
callback("Starting..." if s_type is SettingsType.ENIGMA_2 else "Rebooting...")
|
||||
elif ht and use_http:
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
if download_type is DownloadType.BOUQUETS:
|
||||
ht.send((f"{url}servicelistreload?mode=2", "Reloading Userbouquets."))
|
||||
elif download_type is DownloadType.ALL:
|
||||
ht.send((f"{url}servicelistreload?mode=0", "Reloading lamedb and Userbouquets."))
|
||||
ht.send((f"{url}powerstate?newstate=4", "Wakeup from Standby."))
|
||||
if not settings.keep_power_mode:
|
||||
ht.send((f"{url}powerstate?newstate=4", "Wakeup from Standby."))
|
||||
else:
|
||||
ht.send((f"{url}reloadchannels", "Reloading channels..."))
|
||||
|
||||
@@ -458,12 +530,24 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
|
||||
ht.close()
|
||||
|
||||
|
||||
def get_upload_info_message(download_type):
|
||||
if download_type is DownloadType.BOUQUETS:
|
||||
return "User bouquets will be updated!"
|
||||
elif download_type is DownloadType.ALL:
|
||||
return "All user data will be reloaded!"
|
||||
elif download_type is DownloadType.SATELLITES:
|
||||
return "Satellites.xml file will be updated!"
|
||||
elif download_type is DownloadType.PICONS:
|
||||
return "Picons will be updated!"
|
||||
return ""
|
||||
|
||||
|
||||
# ***************** Picons *******************#
|
||||
|
||||
def remove_picons(*, settings, callback, done_callback=None, files_filter=None):
|
||||
def remove_picons(*, settings, callback=log, 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")
|
||||
callback("FTP OK.")
|
||||
ftp.delete_picons(callback, settings.picons_path, files_filter)
|
||||
if done_callback:
|
||||
done_callback()
|
||||
@@ -483,7 +567,7 @@ def http(user, password, url, callback, use_ssl=False, s_type=SettingsType.ENIGM
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
resp = resp.get("e2statetext", None)
|
||||
|
||||
callback(f"HTTP: {message} {'Successful.' if resp and message else ''}\n")
|
||||
callback(f"HTTP: {message} {'Successful.' if resp and message else ''}")
|
||||
|
||||
|
||||
def telnet(host, port=23, user="", password="", timeout=5):
|
||||
@@ -502,19 +586,27 @@ def telnet(host, port=23, user="", password="", timeout=5):
|
||||
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"))
|
||||
time.sleep(timeout)
|
||||
|
||||
command = f"{command}\r\n".encode("utf-8")
|
||||
tn.write(command)
|
||||
|
||||
msg = tn.read_until(command, timeout)
|
||||
while msg.endswith(command) or not msg:
|
||||
time.sleep(timeout)
|
||||
msg = tn.read_until(command, timeout)
|
||||
|
||||
command = yield
|
||||
time.sleep(timeout)
|
||||
tn.write("{}\r\n".format(command).encode("utf-8"))
|
||||
tn.write(f"{command}\r\n".encode("utf-8"))
|
||||
time.sleep(timeout)
|
||||
yield
|
||||
|
||||
|
||||
# ***************** HTTP API *******************#
|
||||
# ***************** HTTP API ******************* #
|
||||
|
||||
class HttpAPI:
|
||||
__MAX_WORKERS = 4
|
||||
_MAX_WORKERS = 4
|
||||
_TIMEOUT = 10
|
||||
|
||||
class Request(str, Enum):
|
||||
ZAP = "zap?sRef="
|
||||
@@ -540,6 +632,8 @@ class HttpAPI:
|
||||
VOL = "vol?set=set"
|
||||
# EPG
|
||||
EPG = "epgservice?sRef="
|
||||
EPG_NOW = "epgnow?bRef="
|
||||
EPG_MULTI = "epgmulti?bRef="
|
||||
# Timer
|
||||
TIMER = ""
|
||||
TIMER_LIST = "timerlist"
|
||||
@@ -564,10 +658,16 @@ class HttpAPI:
|
||||
EXIT = "174"
|
||||
OK = "352"
|
||||
INFO = "358"
|
||||
TV = "377"
|
||||
RADIO = "385"
|
||||
AUDIO = "392"
|
||||
FAV = "393"
|
||||
RED = "398"
|
||||
GREEN = "399"
|
||||
YELLOW = "400"
|
||||
BLUE = "401"
|
||||
CH_UP = "402"
|
||||
CH_DOWN = "403"
|
||||
BACK = "412"
|
||||
|
||||
class Power(str, Enum):
|
||||
@@ -583,6 +683,8 @@ class HttpAPI:
|
||||
Request.POWER,
|
||||
Request.VOL,
|
||||
Request.EPG,
|
||||
Request.EPG_NOW,
|
||||
Request.EPG_MULTI,
|
||||
Request.TIMER,
|
||||
Request.RECORDINGS,
|
||||
Request.N_ZAP}
|
||||
@@ -594,7 +696,7 @@ class HttpAPI:
|
||||
|
||||
def __init__(self, settings):
|
||||
from concurrent.futures import ThreadPoolExecutor as PoolExecutor
|
||||
self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS)
|
||||
self._executor = PoolExecutor(max_workers=self._MAX_WORKERS)
|
||||
|
||||
self._settings = settings
|
||||
self._shutdown = False
|
||||
@@ -606,7 +708,7 @@ class HttpAPI:
|
||||
self._s_type = SettingsType.ENIGMA_2
|
||||
self.init()
|
||||
|
||||
def send(self, req_type, ref, callback=print, ref_prefix=""):
|
||||
def send(self, req_type, ref, callback=print, ref_prefix="", timeout=_TIMEOUT):
|
||||
if self._shutdown:
|
||||
return
|
||||
|
||||
@@ -626,7 +728,7 @@ class HttpAPI:
|
||||
def done_callback(f):
|
||||
callback(f.result())
|
||||
|
||||
future = self._executor.submit(self.get_response, req_type, url, data, self._s_type)
|
||||
future = self._executor.submit(self.get_response, req_type, url, data, self._s_type, timeout)
|
||||
future.add_done_callback(done_callback)
|
||||
|
||||
@run_task
|
||||
@@ -663,9 +765,9 @@ class HttpAPI:
|
||||
self._executor.shutdown()
|
||||
|
||||
@staticmethod
|
||||
def get_response(req_type, url, data=None, s_type=SettingsType.ENIGMA_2):
|
||||
def get_response(req_type, url, data=None, s_type=SettingsType.ENIGMA_2, timeout=_TIMEOUT):
|
||||
try:
|
||||
with urlopen(Request(url, data=data), timeout=10) as f:
|
||||
with urlopen(Request(url, data=data), timeout=timeout) as f:
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
return HttpAPI.get_e2_response_data(req_type, f)
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
@@ -676,7 +778,7 @@ class HttpAPI:
|
||||
if req_type is HttpAPI.Request.TEST:
|
||||
raise e
|
||||
return {"error_code": e.code}
|
||||
except (URLError, RemoteDisconnected, ConnectionResetError) as e:
|
||||
except OSError as e:
|
||||
if req_type is HttpAPI.Request.TEST:
|
||||
raise e
|
||||
except ETree.ParseError as e:
|
||||
@@ -696,7 +798,7 @@ class HttpAPI:
|
||||
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:
|
||||
elif req_type in (HttpAPI.Request.EPG, HttpAPI.Request.EPG_NOW, HttpAPI.Request.EPG_MULTI):
|
||||
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:
|
||||
@@ -755,7 +857,7 @@ def test_ftp(host, port, user, password, timeout=5):
|
||||
try:
|
||||
with FTP(host=host, user=user, passwd=password, timeout=timeout) as ftp:
|
||||
return ftp.getwelcome()
|
||||
except (error_perm, ConnectionRefusedError, OSError) as e:
|
||||
except all_errors as e:
|
||||
raise TestException(e)
|
||||
|
||||
|
||||
@@ -763,7 +865,7 @@ def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message
|
||||
t_msg = "Connection test!"
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
params = urlencode({"text": t_msg, "type": 2, "timeout": timeout})
|
||||
params = "statusinfo" if skip_message else f"message?{params}"
|
||||
params = "deviceinfo" if skip_message else f"message?{params}"
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
params = urlencode({"nmsg": t_msg, "timeout": 5}, quote_via=quote)
|
||||
params = "info" if skip_message else f"message?{params}"
|
||||
@@ -778,9 +880,11 @@ def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message
|
||||
data = HttpAPI.get_post_data(base_url, password, user) if s_type is SettingsType.ENIGMA_2 else None
|
||||
|
||||
try:
|
||||
log("Testing HTTP connection...")
|
||||
resp = HttpAPI.get_response(HttpAPI.Request.TEST, url, data, s_type)
|
||||
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
return resp.get("e2statetext", "")
|
||||
return resp.get("e2enigmaversion", "")
|
||||
return resp
|
||||
except (RemoteDisconnected, URLError, HTTPError) as e:
|
||||
raise TestException(e)
|
||||
|
||||
@@ -62,7 +62,7 @@ def get_bouquets(path, s_type):
|
||||
def write_bouquet(path, bq, s_type):
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
writer = BouquetsWriter(path, None)
|
||||
writer.write_bouquet(path + "userbouquet.{}.{}".format(bq.name, bq.type), bq.name, bq.services)
|
||||
writer.write_bouquet(f"{path}userbouquet.{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)
|
||||
|
||||
@@ -1,7 +1,37 @@
|
||||
""" Common elements module """
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Common elements module. """
|
||||
from collections import namedtuple
|
||||
from enum import Enum
|
||||
|
||||
from app.commons import log
|
||||
|
||||
Service = namedtuple("Service", ["flags_cas", "transponder_type", "coded", "service", "locked", "hide", "package",
|
||||
"service_type", "picon", "picon_id", "ssid", "freq", "rate", "pol", "fec",
|
||||
"system", "pos", "data_id", "fav_id", "transponder"])
|
||||
@@ -17,18 +47,28 @@ class BqServiceType(Enum):
|
||||
ALT = "ALT" # Service with alternatives
|
||||
BOUQUET = "BOUQUET" # Sub bouquet.
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.DEFAULT
|
||||
|
||||
|
||||
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"])
|
||||
|
||||
# ***************** Satellites *******************#
|
||||
# *************** *.xml [Satellites, Terrestrial, Cable] ***************** #
|
||||
|
||||
Satellite = namedtuple("Satellite", ["name", "flags", "position", "transponders"])
|
||||
Terrestrial = namedtuple("Terrestrial", ["name", "flags", "countrycode", "transponders"])
|
||||
Cable = namedtuple("Cable", ["name", "flags", "satfeed", "countrycode", "transponders"])
|
||||
|
||||
Transponder = namedtuple("Transponder", ["frequency", "symbol_rate", "polarization", "fec_inner",
|
||||
"system", "modulation", "pls_mode", "pls_code", "is_id"])
|
||||
Transponder = namedtuple("Transponder", ["frequency", "symbol_rate", "polarization", "fec_inner", "system",
|
||||
"modulation", "pls_mode", "pls_code", "is_id", "t2mi_plp_id"])
|
||||
TerTransponder = namedtuple("TerTransponder", ["centre_frequency", "system", "bandwidth", "constellation",
|
||||
"code_rate_hp", "code_rate_lp", "guard_interval", "transmission_mode",
|
||||
"hierarchy_information", "inversion", "plp_id"])
|
||||
CableTransponder = namedtuple("CableTransponder", ["frequency", "symbol_rate", "fec_inner", "modulation"])
|
||||
|
||||
|
||||
class TrType(Enum):
|
||||
@@ -161,6 +201,8 @@ SERVICE_TYPE = {"-2": "Data", "1": "TV", "2": "Radio", "3": "Data", "10": "Radio
|
||||
# Terrestrial
|
||||
BANDWIDTH = {"0": "8MHz", "1": "7MHz", "2": "6MHz", "3": "Auto", "4": "5MHz", "5": "1/712MHz", "6": "10MHz"}
|
||||
|
||||
CONSTELLATION = {"0": "QPSK", "1": "16-QAM", "2": "64-QAM", "3": "Auto"}
|
||||
|
||||
T_MODULATION = {"0": "QPSK", "1": "QAM16", "2": "QAM64", "3": "Auto", "4": "QAM256"}
|
||||
|
||||
TRANSMISSION_MODE = {"0": "2k", "1": "8k", "2": "Auto", "3": "4k", "4": "1k", "5": "16k", "6": "32k"}
|
||||
@@ -175,7 +217,7 @@ T_FEC = {"0": "1/2", "1": "2/3", "2": "3/4", "3": "5/6", "4": "7/8", "5": "Auto"
|
||||
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"}
|
||||
C_MODULATION = {"0": "QPSK", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM128", "5": "QAM256", "6": "Auto"}
|
||||
|
||||
# ATSC
|
||||
A_MODULATION = {"0": "Auto", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM128", "5": "QAM256", "6": "8VSB",
|
||||
@@ -206,23 +248,25 @@ def get_value_by_name(en, name):
|
||||
|
||||
|
||||
def is_transponder_valid(tr: Transponder):
|
||||
""" Checks transponder validity """
|
||||
""" Checks transponder validity. """
|
||||
try:
|
||||
int(tr.frequency)
|
||||
int(tr.symbol_rate)
|
||||
tr.pls_mode is None or int(tr.pls_mode)
|
||||
tr.pls_code is None or int(tr.pls_code)
|
||||
tr.is_id is None or int(tr.is_id)
|
||||
except TypeError:
|
||||
tr.t2mi_plp_id is None or int(tr.t2mi_plp_id)
|
||||
except (TypeError, ValueError) as e:
|
||||
log(f"Transponder validation error: {e}\n{tr}")
|
||||
return False
|
||||
|
||||
if tr.polarization not in POLARIZATION.values():
|
||||
if tr.polarization not in POLARIZATION:
|
||||
return False
|
||||
if tr.fec_inner not in FEC.values():
|
||||
if tr.fec_inner not in FEC:
|
||||
return False
|
||||
if tr.system not in SYSTEM.values():
|
||||
if tr.system not in SYSTEM:
|
||||
return False
|
||||
if tr.modulation not in MODULATION.values():
|
||||
if tr.modulation not in MODULATION:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 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
|
||||
@@ -60,14 +60,14 @@ class BouquetsWriter:
|
||||
self._marker_index = 1
|
||||
self._space_index = 0
|
||||
self._alt_names = set()
|
||||
self._NAME_PATTERN = re.compile("[^\\w_()]+")
|
||||
|
||||
def write(self):
|
||||
line = []
|
||||
pattern = re.compile("[^\\w_()]+")
|
||||
|
||||
for bqs in self._bouquets:
|
||||
line.clear()
|
||||
line.append("#NAME {}\n".format(bqs.name))
|
||||
line.append(f"#NAME {bqs.name}\n")
|
||||
bq_file_names = {b.file for b in bqs.bouquets}
|
||||
count = 1
|
||||
m_count = 0
|
||||
@@ -76,24 +76,29 @@ class BouquetsWriter:
|
||||
bq_name = bq.file
|
||||
if not bq_name:
|
||||
if self._force_bq_names:
|
||||
bq_name = re.sub(pattern, "_", bq.name)
|
||||
bq_name = re.sub(self._NAME_PATTERN, "_", bq.name)
|
||||
else:
|
||||
bq_name = "de{0:02d}".format(count)
|
||||
bq_name = f"de{count:02d}"
|
||||
while bq_name in bq_file_names:
|
||||
count += 1
|
||||
bq_name = "de{0:02d}".format(count)
|
||||
bq_name = f"de{count:02d}"
|
||||
bq_file_names.add(bq_name)
|
||||
|
||||
if BqType(bq.type) is BqType.MARKER:
|
||||
bq_type = BqType(bq.type)
|
||||
if bq_type is BqType.MARKER:
|
||||
m_data = bq.file.split(":") if bq.file else None
|
||||
b_name = m_data[-1].strip() if m_data else bq.name.lstrip(_MARKER_PREFIX)
|
||||
line.append(self._MARKER.format(m_count, b_name))
|
||||
m_count += 1
|
||||
else:
|
||||
line.append(self._SERVICE.format(2 if bq.type == BqType.RADIO.value else 1, bq_name, bq.type))
|
||||
self.write_bouquet(f"{self._path}userbouquet.{bq_name}.{bq.type}", bq.name, bq.services)
|
||||
if bq_type is BqType.BOUQUET:
|
||||
bq_name = re.sub(self._NAME_PATTERN, "_", bq.name)
|
||||
self.write_sub_bouquet(self._path, bq_name, bq, bqs.type)
|
||||
else:
|
||||
self.write_bouquet(f"{self._path}userbouquet.{bq_name}.{bqs.type}", bq.name, bq.services)
|
||||
line.append(self._SERVICE.format(2 if bqs.type == BqType.RADIO.value else 1, bq_name, bqs.type))
|
||||
|
||||
with open(self._path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file:
|
||||
with open(f"{self._path}bouquets.{bqs.type}", "w", encoding="utf-8", newline="\n") as file:
|
||||
file.writelines(line)
|
||||
|
||||
def write_bouquet(self, path, name, services):
|
||||
@@ -131,7 +136,19 @@ class BouquetsWriter:
|
||||
else:
|
||||
bouquet.append(f"#SERVICE {data}\n")
|
||||
|
||||
with open(path, "w", encoding="utf-8") as file:
|
||||
with open(path, "w", encoding="utf-8", newline="\n") as file:
|
||||
file.writelines(bouquet)
|
||||
|
||||
def write_sub_bouquet(self, path, file_name, bq, bq_type):
|
||||
bouquet = [f"#NAME {bq.name}\n"]
|
||||
sb_type = 2 if bq_type == BqType.RADIO.value else 1
|
||||
|
||||
for sb in bq.services:
|
||||
bq_name = f"subbouquet.{re.sub(self._NAME_PATTERN, '_', sb.name)}.{sb.type}"
|
||||
self.write_bouquet(f"{path}{bq_name}", sb.name, sb.services)
|
||||
bouquet.append(f"#SERVICE 1:7:{sb_type}:0:0:0:0:0:0:0:FROM BOUQUET \"{bq_name}\" ORDER BY bouquet\n")
|
||||
|
||||
with open(f"{self._path}userbouquet.{file_name}.{bq_type}", "w", encoding="utf-8", newline="\n") as file:
|
||||
file.writelines(bouquet)
|
||||
|
||||
|
||||
@@ -151,9 +168,9 @@ class ServiceType(Enum):
|
||||
|
||||
class BouquetsReader:
|
||||
""" Class for reading and parsing bouquets. """
|
||||
_ALT_PAT = re.compile(".*alternatives\\.+(.*)\\.([tv|radio]+).*")
|
||||
_BQ_PAT = re.compile(".*userbouquet\\.+(.*)\\.+[tv|radio].*")
|
||||
_SUB_BQ_PAT = re.compile(".*subbouquet\\.+(.*)\\.([tv|radio]+).*")
|
||||
_ALT_PAT = re.compile(r".*alternatives\.+(.*)\.([tv|radio]+).*")
|
||||
_BQ_PAT = re.compile(r".*\s+\W(.*bouquet)\.+(.*)\.+[tv|radio].*")
|
||||
_SUB_BQ_PAT = re.compile(r".*subbouquet\.+(.*)\.([tv|radio]+).*")
|
||||
_STREAM_TYPES = {"4097", "5001", "5002", "8193", "8739"}
|
||||
|
||||
__slots__ = ["_path"]
|
||||
@@ -181,15 +198,15 @@ class BouquetsReader:
|
||||
if "#SERVICE" in line:
|
||||
name = re.match(self._BQ_PAT, line)
|
||||
if name:
|
||||
b_name = name.group(1)
|
||||
prefix, b_name = name.group(1), name.group(2)
|
||||
if b_name in b_names:
|
||||
log(f"The list of bouquets contains duplicate [{b_name}] names!")
|
||||
else:
|
||||
b_names.add(b_name)
|
||||
|
||||
rb_name, services = self.get_bouquet(self._path, b_name, bq_type)
|
||||
rb_name, services = self.get_bouquet(self._path, b_name, bq_type, prefix)
|
||||
if rb_name in real_b_names:
|
||||
log(f"Bouquet file 'userbouquet.{b_name}.{bq_type}' has duplicate name: {rb_name}")
|
||||
log(f"Bouquet file '{prefix}.{b_name}.{bq_type}' has duplicate name: {rb_name}")
|
||||
real_b_names[rb_name] += 1
|
||||
rb_name = f"{rb_name} {real_b_names[rb_name]}"
|
||||
else:
|
||||
@@ -199,7 +216,7 @@ class BouquetsReader:
|
||||
else:
|
||||
s_data = line.split(":")
|
||||
if len(s_data) == 12 and s_data[1] == ServiceType.MARKER.value:
|
||||
b_name = "{}{}".format(_MARKER_PREFIX, s_data[-1].strip())
|
||||
b_name = f"{_MARKER_PREFIX}{s_data[-1].strip()}"
|
||||
bouquets[2].append(Bouquet(b_name, BqType.MARKER.value, [], None, None, line.strip()))
|
||||
else:
|
||||
log(f"Unsupported or invalid data format: [{line}].")
|
||||
@@ -211,14 +228,14 @@ class BouquetsReader:
|
||||
@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:
|
||||
with open(f"{path}{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
|
||||
log(f"Bouquet file 'userbouquet.{bq_name}.{bq_type}' is empty or wrong!")
|
||||
return f"{bq_name} [empty]", services
|
||||
|
||||
bq_name = srvs.pop(0)
|
||||
|
||||
@@ -226,7 +243,7 @@ class BouquetsReader:
|
||||
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))
|
||||
log(f"The bouquet [{bq_name}] service [{num}] has the wrong data format: [{srv}]")
|
||||
continue
|
||||
|
||||
s_type = ServiceType(srv_data[1])
|
||||
@@ -254,7 +271,7 @@ class BouquetsReader:
|
||||
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])
|
||||
fav_id = f"{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")
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -52,7 +52,7 @@ class LameDbReader:
|
||||
""" Lamedb parser class.
|
||||
|
||||
Reads and parses the Enigma2 lamedb[5] file.
|
||||
Supports versions 3, 4 and 5..
|
||||
Supports versions 3, 4 and 5.
|
||||
"""
|
||||
__slots__ = ["_path", "_fmt"]
|
||||
|
||||
@@ -99,7 +99,7 @@ class LameDbReader:
|
||||
try:
|
||||
data = str(file.read())
|
||||
except UnicodeDecodeError as e:
|
||||
log("lamedb parse error: " + str(e))
|
||||
log(f"lamedb parse error: {e}")
|
||||
else:
|
||||
return self.get_services_list(data)
|
||||
|
||||
@@ -115,7 +115,7 @@ class LameDbReader:
|
||||
for line in lns:
|
||||
if line.startswith("s:"):
|
||||
srv_data = line.strip("s:").split(",", 2)
|
||||
srv_data[1] = srv_data[1].strip("\"")
|
||||
srv_data[1] = srv_data[1].strip("\"\n")
|
||||
data_len = len(srv_data)
|
||||
if data_len == 3:
|
||||
srv_data[2] = srv_data[2].strip()
|
||||
@@ -129,7 +129,7 @@ class LameDbReader:
|
||||
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))
|
||||
log(f"Error while parsing transponder data [ver. 5] for line: {line}")
|
||||
|
||||
return self.parse_services(srvs, trs)
|
||||
|
||||
@@ -151,28 +151,28 @@ class LameDbReader:
|
||||
is_v3 = False
|
||||
if len(tid) < 4:
|
||||
is_v3 = True
|
||||
tid = "{:0>4}".format(tid)
|
||||
tid = f"{tid:0>4}"
|
||||
data[2] = tid
|
||||
if len(nid) < 4:
|
||||
is_v3 = True
|
||||
nid = "{:0>4}".format(nid)
|
||||
nid = f"{nid:0>4}"
|
||||
data[3] = nid
|
||||
if is_v3:
|
||||
data[0] = "{:0>4}".format(data[0])
|
||||
data[0] = f"{data[0]:0>4}"
|
||||
data_id = _SEP.join(data)
|
||||
|
||||
srv_type = int(data[4])
|
||||
transponder_id = "{}:{}:{}".format(data[1], tid, nid)
|
||||
transponder_id = f"{data[1]}:{tid}:{nid}"
|
||||
transponder = transponders.get(transponder_id, None)
|
||||
|
||||
tid = tid.lstrip(sp).upper()
|
||||
nid = nid.lstrip(sp).upper()
|
||||
# The tid and nid values can be 0.
|
||||
tid = tid.lstrip(sp).upper() or "0"
|
||||
nid = nid.lstrip(sp).upper() or "0"
|
||||
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)
|
||||
fav_id = f"{ssid}:{tid}:{nid}:{onid}"
|
||||
picon_id = f"1_0_{srv_type:X}_{ssid}_{tid}_{nid}_{onid}_0_0_0.png"
|
||||
s_id = f"1:0:{srv_type:X}:{ssid}:{tid}:{nid}:{onid}:0:0:0:"
|
||||
|
||||
all_flags = srv[2].split(",")
|
||||
coded = CODED_ICON if list(filter(lambda x: x.startswith("C:"), all_flags)) else None
|
||||
@@ -188,7 +188,7 @@ class LameDbReader:
|
||||
tr_type = TrType(tr_type)
|
||||
tr = tr.split(_SEP)
|
||||
service_type = SERVICE_TYPE.get(data[4], SERVICE_TYPE["-2"])
|
||||
# Removing all non printable symbols!
|
||||
# Removing all non-printable symbols!
|
||||
srv_name = "".join(c for c in srv[1] if c.isprintable())
|
||||
freq = tr[0]
|
||||
rate = tr[1]
|
||||
@@ -203,7 +203,7 @@ class LameDbReader:
|
||||
system = "DVB-S2" if len(tr) > 7 else "DVB-S"
|
||||
pos = tr[4]
|
||||
if tr_type is TrType.Terrestrial:
|
||||
system = T_SYSTEM.get(tr[10], None)
|
||||
system = T_SYSTEM.get(tr[10] if len(tr) > 10 else "0", None)
|
||||
pos = "T"
|
||||
fec = T_FEC.get(tr[3], None)
|
||||
elif tr_type is TrType.Cable:
|
||||
@@ -217,13 +217,13 @@ class LameDbReader:
|
||||
|
||||
# Formatting displayed values.
|
||||
try:
|
||||
freq = "{}".format(int(freq) // 1000)
|
||||
rate = "{}".format(int(rate) // 1000)
|
||||
freq = f"{int(freq) // 1000}"
|
||||
rate = f"{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")
|
||||
pos = f"{abs(pos / 10):0.1f}{'W' if pos < 0 else 'E'}"
|
||||
except ValueError as e:
|
||||
log("Parse error [parse_services]: {}".format(e))
|
||||
log(f"Parse error [parse_services]: {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)
|
||||
@@ -258,18 +258,17 @@ class LameDbReader:
|
||||
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])
|
||||
tr_id = f"{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_lines.append(f"{tr_id}\n\t{srv.transponder}\n/\n")
|
||||
tr_set.add(tr_id)
|
||||
# Services
|
||||
services_lines.append("{}\n{}\n{}\n".format(srv.data_id, srv.service, srv.flags_cas))
|
||||
services_lines.append(f"{srv.data_id}\n{srv.service}\n{srv.flags_cas}\n")
|
||||
|
||||
tr_lines.sort()
|
||||
lines.extend(tr_lines)
|
||||
lines.extend(services_lines)
|
||||
lines.append("end\n" + _END_LINE)
|
||||
lines.append(f"end\n{_END_LINE}")
|
||||
|
||||
return lines
|
||||
|
||||
@@ -311,7 +310,7 @@ class LameDbWriter:
|
||||
def write(self):
|
||||
if self._fmt == 4:
|
||||
# Writing lamedb file ver.4
|
||||
with open(self._path + _FILE_NAME, "w", encoding="utf-8") as file:
|
||||
with open(self._path + _FILE_NAME, "w", encoding="utf-8", newline="\n") as file:
|
||||
file.writelines(LameDbReader.get_services_lines(self._services))
|
||||
elif self._fmt == 5:
|
||||
self.write_to_lamedb5()
|
||||
@@ -324,19 +323,19 @@ class LameDbWriter:
|
||||
|
||||
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)))
|
||||
tr_id = f"{data_id[1]}:{data_id[2]}:{data_id[3]}"
|
||||
tr_set.add(f"t:{tr_id},{srv.transponder.replace(' ', ':', 1)}\n")
|
||||
# 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))
|
||||
services_lines.append(f"s:{srv.data_id},\"{srv.service}\"{flags}\n")
|
||||
|
||||
lines.extend(sorted(tr_set))
|
||||
lines.extend(services_lines)
|
||||
lines.append(_END_LINE)
|
||||
|
||||
with open(self._path + "lamedb5", "w", encoding="utf-8") as file:
|
||||
with open(self._path + "lamedb5", "w", encoding="utf-8", newline="\n") as file:
|
||||
file.writelines(lines)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 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
|
||||
@@ -40,6 +40,7 @@ from app.ui.uicommons import IPTV_ICON
|
||||
NEUTRINO_FAV_ID_FORMAT = "{}::{}::{}::{}::{}::{}::{}::{}::{}::{}"
|
||||
ENIGMA2_FAV_ID_FORMAT = " {}:{}:{}:{:X}:{:X}:{:X}:{:X}:0:0:0:{}:{}\n#DESCRIPTION: {}\n"
|
||||
MARKER_FORMAT = " 1:64:{}:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n"
|
||||
PICON_FORMAT = "1_{}_{:X}_{:X}_{:X}_{:X}_{:X}_0_0_0.png"
|
||||
|
||||
|
||||
class StreamType(Enum):
|
||||
@@ -49,6 +50,11 @@ class StreamType(Enum):
|
||||
NONE_REC_2 = "5002"
|
||||
E_SERVICE_URI = "8193"
|
||||
E_SERVICE_HLS = "8739"
|
||||
UNKNOWN = "0"
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.UNKNOWN
|
||||
|
||||
|
||||
def parse_m3u(path, s_type, detect_encoding=True, params=None):
|
||||
@@ -76,6 +82,7 @@ def parse_m3u(path, s_type, detect_encoding=True, params=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]
|
||||
m_name = BqServiceType.MARKER.name
|
||||
|
||||
for line in str(data, encoding=encoding, errors="ignore").splitlines():
|
||||
if line.startswith("#EXTINF"):
|
||||
@@ -88,36 +95,40 @@ def parse_m3u(path, s_type, detect_encoding=True, params=None):
|
||||
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)
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
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], m_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(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)
|
||||
mr = Service(None, None, None, grp_name, *aggr[0:3], m_name, *aggr, fav_id, None)
|
||||
services.append(mr)
|
||||
elif not line.startswith("#"):
|
||||
url = line.strip()
|
||||
params[0] = sid_counter
|
||||
sid_counter += 1
|
||||
fav_id = get_fav_id(url, name, s_type, params)
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
p_id = get_picon_id(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))
|
||||
log(f"*.m3u* parse error ['{path}']: name[{name}], url[{url}], fav id[{fav_id}]")
|
||||
|
||||
return services
|
||||
|
||||
|
||||
def export_to_m3u(path, bouquet, s_type):
|
||||
def export_to_m3u(path, bouquet, s_type, url=None):
|
||||
pattern = re.compile(".*:(http.*):.*") if s_type is SettingsType.ENIGMA_2 else re.compile("(http.*?)::::.*")
|
||||
lines = ["#EXTM3U\n"]
|
||||
current_grp = None
|
||||
@@ -128,15 +139,17 @@ def export_to_m3u(path, bouquet, s_type):
|
||||
res = re.match(pattern, s.data)
|
||||
if not res:
|
||||
continue
|
||||
data = res.group(1)
|
||||
lines.append("#EXTINF:-1,{}\n".format(s.name))
|
||||
if current_grp:
|
||||
lines.append(current_grp)
|
||||
lines.append("{}\n".format(unquote(data.strip())))
|
||||
lines.append(f"#EXTINF:-1,{s.name}\n")
|
||||
lines.append(current_grp) if current_grp else None
|
||||
lines.append(f"{unquote(res.group(1).strip())}\n")
|
||||
elif s_type is BqServiceType.MARKER:
|
||||
current_grp = "#EXTGRP:{}\n".format(s.name)
|
||||
current_grp = f"#EXTGRP:{s.name}\n"
|
||||
elif s_type is BqServiceType.DEFAULT and url:
|
||||
lines.append(f"#EXTINF:-1,{s.name}\n")
|
||||
lines.append(current_grp) if current_grp else None
|
||||
lines.append(f"{url}{s.data}\n")
|
||||
|
||||
with open(path + "{}.m3u".format(bouquet.name), "w", encoding="utf-8") as file:
|
||||
with open(f"{path}{bouquet.name}.m3u", "w", encoding="utf-8") as file:
|
||||
file.writelines(lines)
|
||||
|
||||
|
||||
@@ -150,5 +163,10 @@ def get_fav_id(url, name, settings_type, params=None, st_type=None, s_id=0, srv_
|
||||
return NEUTRINO_FAV_ID_FORMAT.format(url, "", 0, None, None, None, None, "", "", 1)
|
||||
|
||||
|
||||
def get_picon_id(params=None, s_id=0, srv_type=1):
|
||||
params = params or (0, 0, 0, 0)
|
||||
return PICON_FORMAT.format(s_id, srv_type, *params)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
@@ -1,120 +1,195 @@
|
||||
""" Module foe parsing Satellites.xml
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 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
|
||||
#
|
||||
|
||||
For more info see __COMMENT
|
||||
|
||||
""" Module for working with *.xml files.
|
||||
|
||||
For more info see comments.
|
||||
"""
|
||||
from xml.dom.minidom import parse, Document
|
||||
import xml.etree.ElementTree as ETree
|
||||
|
||||
from app.commons import log
|
||||
from .ecommons import POLARIZATION, FEC, SYSTEM, MODULATION, Transponder, Satellite, get_key_by_value
|
||||
from .ecommons import Satellite, Terrestrial, Cable, Transponder, TerTransponder, CableTransponder
|
||||
|
||||
__COMMENT = (" File was created in DemonEditor\n\n"
|
||||
"usable flags are\n"
|
||||
" 1: Network Scan\n"
|
||||
" 2: use BAT\n"
|
||||
" 4: use ONIT\n"
|
||||
" 8: skip NITs of known networks\n"
|
||||
" and combinations of this.\n\n"
|
||||
_SAT_COMMENT = ("\tFile was created in DemonEditor.\n\n"
|
||||
"Usable flags are:\n"
|
||||
" 1: Network Scan\n"
|
||||
" 2: use BAT\n"
|
||||
" 4: use ONIT\n"
|
||||
" 8: skip NITs of known networks\n"
|
||||
" This is a bitmap and combinations can be used.\n\n"
|
||||
"Transponder parameters:\n"
|
||||
"\tpolarization: 0 - Horizontal, 1 - Vertical, 2 - Left Circular, 3 - Right Circular\n"
|
||||
"\tfec_inner: 0 - Auto, 1 - 1/2, 2 - 2/3, 3 - 3/4, 4 - 5/6, 5 - 7/8, 6 - 8/9, 7 - 3/5,\n"
|
||||
"\t8 - 4/5, 9 - 9/10, 15 - None\n"
|
||||
"\tmodulation: 0 - Auto, 1 - QPSK, 2 - 8PSK, 4 - 16APSK, 5 - 32APSK\n"
|
||||
"\trolloff: 0 - 0.35, 1 - 0.25, 2 - 0.20, 3 - Auto\n"
|
||||
"\tpilot: 0 - Off, 1 - On, 2 - Auto\n"
|
||||
"\tinversion: 0 = Off, 1 = On, 2 = Auto (default)\n"
|
||||
"\tsystem: 0 = DVB-S, 1 = DVB-S2\n"
|
||||
"\tis_id: 0 - 255\n"
|
||||
"\tpls_mode: 0 - Root, 1 - Gold, 2 - Combo\n"
|
||||
"\tpls_code: 0 - 262142\n\n")
|
||||
|
||||
"transponder parameters:\n"
|
||||
"polarization: 0 - Horizontal, 1 - Vertical, 2 - Left Circular, 3 - Right Circular\n"
|
||||
"fec_inner: 0 - Auto, 1 - 1/2, 2 - 2/3, 3 - 3/4, 4 - 5/6, 5 - 7/8, 6 - 8/9, 7 - 3/5,\n"
|
||||
"8 - 4/5, 9 - 9/10, 15 - None\n"
|
||||
"modulation: 0 - Auto, 1 - QPSK, 2 - 8PSK, 4 - 16APSK, 5 - 32APSK\n"
|
||||
"rolloff: 0 - 0.35, 1 - 0.25, 2 - 0.20, 3 - Auto\n"
|
||||
"pilot: 0 - Off, 1 - On, 2 - Auto\n"
|
||||
"inversion: 0 = Off, 1 = On, 2 = Auto (default)\n"
|
||||
"system: 0 = DVB-S, 1 = DVB-S2\n"
|
||||
"is_id: 0 - 255\n"
|
||||
"pls_mode: 0 - Root, 1 - Gold, 2 - Combo\n"
|
||||
"pls_code: 0 - 262142\n\n")
|
||||
_TERRESTRIAL_COMMENT = ("\tFile was created in DemonEditor.\n\n"
|
||||
"Usable flags are:\n"
|
||||
" 1: Network Scan\n"
|
||||
" 2: use BAT\n"
|
||||
" 4: use ONIT\n"
|
||||
" 8: skip NITs of known networks\n"
|
||||
" This is a bitmap and combinations can be used.\n\n")
|
||||
|
||||
_CABLE_COMMENT = ("\tFile was created in DemonEditor.\n\n"
|
||||
"Transponder parameters:\n"
|
||||
"\tmodulation:\n"
|
||||
"\t3: QAM64\n"
|
||||
"\t5: QAM256\n")
|
||||
|
||||
|
||||
def get_satellites(path):
|
||||
return parse_satellites(path)
|
||||
""" Returns data [Satellite] list from *.xml. """
|
||||
return [Satellite(e.get("name", None),
|
||||
e.get("flags", None),
|
||||
e.get("position", None) or "0",
|
||||
get_sat_transponders(e)) for e in ETree.parse(path).iter("sat")]
|
||||
|
||||
|
||||
def write_satellites(satellites, data_path):
|
||||
""" Creation satellites.xml file """
|
||||
doc = Document()
|
||||
comment = doc.createComment(__COMMENT)
|
||||
doc.appendChild(comment)
|
||||
root = doc.createElement("satellites")
|
||||
doc.appendChild(root)
|
||||
|
||||
for sat in satellites:
|
||||
# Create Element
|
||||
sat_child = doc.createElement("sat")
|
||||
sat_child.setAttribute("name", sat.name)
|
||||
sat_child.setAttribute("flags", sat.flags)
|
||||
sat_child.setAttribute("position", sat.position)
|
||||
|
||||
for tr in sat.transponders:
|
||||
transponder_child = doc.createElement("transponder")
|
||||
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) 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:
|
||||
transponder_child.setAttribute("pls_code", tr.pls_code)
|
||||
if tr.is_id:
|
||||
transponder_child.setAttribute("is_id", tr.is_id)
|
||||
sat_child.appendChild(transponder_child)
|
||||
root.appendChild(sat_child)
|
||||
doc.writexml(open(data_path, "w"),
|
||||
# indent="",
|
||||
addindent=" ",
|
||||
newl='\n',
|
||||
encoding="iso-8859-1")
|
||||
doc.unlink()
|
||||
def get_sat_transponders(elem):
|
||||
""" Returns satellite transponders list. """
|
||||
return [Transponder(e.get("frequency", "0"),
|
||||
e.get("symbol_rate", "0"),
|
||||
e.get("polarization", None),
|
||||
e.get("fec_inner", None),
|
||||
e.get("system", None),
|
||||
e.get("modulation", None),
|
||||
e.get("pls_mode", None),
|
||||
e.get("pls_code", None),
|
||||
e.get("is_id", None),
|
||||
e.get("t2mi_plp_id", None)) for e in elem.iter("transponder")]
|
||||
|
||||
|
||||
def parse_transponders(elem, sat_name):
|
||||
""" Parsing satellite transponders """
|
||||
transponders = []
|
||||
for el in elem.getElementsByTagName("transponder"):
|
||||
if el.hasAttributes():
|
||||
atr = el.attributes
|
||||
try:
|
||||
tr = Transponder(atr["frequency"].value,
|
||||
atr["symbol_rate"].value,
|
||||
POLARIZATION[atr["polarization"].value],
|
||||
FEC[atr["fec_inner"].value],
|
||||
SYSTEM[atr["system"].value],
|
||||
MODULATION[atr["modulation"].value],
|
||||
atr["pls_mode"].value if "pls_mode" in atr else None,
|
||||
atr["pls_code"].value if "pls_code" in atr else None,
|
||||
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))
|
||||
log(message)
|
||||
else:
|
||||
transponders.append(tr)
|
||||
return transponders
|
||||
def get_terrestrial(path):
|
||||
""" Returns data [Terrestrial] list from *.xml. """
|
||||
return [Terrestrial(e.get("name", None),
|
||||
e.get("flags", None),
|
||||
e.get("countrycode", None),
|
||||
[get_ter_transponder(e) for e in e.iter("transponder")]
|
||||
) for e in ETree.parse(path).iter("terrestrial")]
|
||||
|
||||
|
||||
def parse_sat(elem):
|
||||
""" Parsing satellite """
|
||||
sat_name = elem.attributes["name"].value
|
||||
return Satellite(sat_name,
|
||||
elem.attributes["flags"].value,
|
||||
elem.attributes["position"].value,
|
||||
parse_transponders(elem, sat_name))
|
||||
def get_ter_transponder(elem):
|
||||
""" Returns terrestrial transponder. """
|
||||
return TerTransponder(elem.get("centre_frequency", "0"),
|
||||
elem.get("system", None),
|
||||
elem.get("bandwidth", None),
|
||||
elem.get("constellation", None),
|
||||
elem.get("code_rate_hp", None),
|
||||
elem.get("code_rate_lp", None),
|
||||
elem.get("guard_interval", None),
|
||||
elem.get("transmission_mode", None),
|
||||
elem.get("hierarchy_information", None),
|
||||
elem.get("inversion", None),
|
||||
elem.get("plp_id", None))
|
||||
|
||||
|
||||
def parse_satellites(path):
|
||||
""" Parsing satellites from xml"""
|
||||
dom = parse(path)
|
||||
satellites = []
|
||||
def get_cable(path):
|
||||
""" Returns data [Cable] list from *.xml. """
|
||||
return [Cable(e.get("name", None),
|
||||
e.get("flags", None),
|
||||
e.get("satfeed", None),
|
||||
e.get("countrycode", None),
|
||||
get_cable_transponders(e)) for e in ETree.parse(path).iter("cable")]
|
||||
|
||||
for elem in dom.getElementsByTagName("sat"):
|
||||
if elem.hasAttributes():
|
||||
satellites.append(parse_sat(elem))
|
||||
|
||||
return satellites
|
||||
def get_cable_transponders(elem):
|
||||
""" Returns cable transponders list. """
|
||||
return [CableTransponder(e.get("frequency", "0"),
|
||||
e.get("symbol_rate", "0"),
|
||||
e.get("fec_inner", None),
|
||||
e.get("modulation", None)) for e in elem.iter("transponder")]
|
||||
|
||||
|
||||
def write_satellites(satellites, data_path, encoding="UTF-8"):
|
||||
""" Creates satellites.xml file. """
|
||||
write_xml("satellites", "sat", satellites, data_path, _SAT_COMMENT, encoding)
|
||||
|
||||
|
||||
def write_terrestrial(terrestrial, data_path, encoding="UTF-8"):
|
||||
""" Creates terrestrial.xml file. """
|
||||
write_xml("locations", "terrestrial", terrestrial, data_path, _TERRESTRIAL_COMMENT, encoding)
|
||||
|
||||
|
||||
def write_cable(cables, data_path, encoding="UTF-8"):
|
||||
""" Creates cables.xml file. """
|
||||
write_xml("cables", "cable", cables, data_path, _CABLE_COMMENT, encoding)
|
||||
|
||||
|
||||
def write_xml(root_name, sub_name, data, data_path, comment="", encoding="UTF-8"):
|
||||
""" Creates *.xml files. """
|
||||
xml = ETree.Element(root_name)
|
||||
[write_element(sub_name, "transponder", t, xml) for t in data]
|
||||
|
||||
tree = ETree.ElementTree(xml)
|
||||
indent(tree.getroot())
|
||||
|
||||
with open(data_path, "wb") as f:
|
||||
# To put comment on top.
|
||||
f.write(f'<?xml version="1.0" encoding="{encoding}"?>\n<!--\n{comment}-->\n\n'.encode("utf-8"))
|
||||
tree.write(f, encoding=encoding)
|
||||
|
||||
|
||||
def write_element(e_name, ch_name, e_data, root):
|
||||
""" Writes element with sub elements.
|
||||
|
||||
@param e_name: Element name.
|
||||
@param ch_name: Child element name.
|
||||
@param e_data: Element data -> defaultdict
|
||||
@param root: Parent of the element.
|
||||
"""
|
||||
t = e_data._asdict()
|
||||
subs = t.pop("transponders")
|
||||
root_sub = ETree.SubElement(root, e_name, {k: v for k, v in t.items() if v})
|
||||
[ETree.SubElement(root_sub, ch_name, {k: v for k, v in tr._asdict().items() if v}) for tr in subs]
|
||||
|
||||
|
||||
def indent(elem, parent=None, index=-1, level=0, space=" "):
|
||||
""" Appends whitespace to the subtree to indent the tree visually.
|
||||
|
||||
Since the minimum supported version < 3.9, we will use our own implementation.
|
||||
"""
|
||||
for i, sub in enumerate(elem):
|
||||
indent(sub, elem, i, level + 1)
|
||||
if parent:
|
||||
if index == 0:
|
||||
parent.text = f"\n{space * level}"
|
||||
else:
|
||||
parent[index - 1].tail = f"\n{space * level}"
|
||||
|
||||
if index == len(parent) - 1:
|
||||
elem.tail = f"\n{space * (level - 1)}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
164
app/settings.py
164
app/settings.py
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -39,9 +39,9 @@ from textwrap import dedent
|
||||
|
||||
SEP = os.sep
|
||||
HOME_PATH = str(Path.home())
|
||||
CONFIG_PATH = HOME_PATH + "{}.config{}demon-editor{}".format(SEP, SEP, SEP)
|
||||
CONFIG_PATH = HOME_PATH + f"{SEP}.config{SEP}demon-editor{SEP}"
|
||||
CONFIG_FILE = CONFIG_PATH + "config.json"
|
||||
DATA_PATH = HOME_PATH + "{}DemonEditor{}".format(SEP, SEP)
|
||||
DATA_PATH = HOME_PATH + f"{SEP}DemonEditor{SEP}"
|
||||
GTK_PATH = os.environ.get("GTK_PATH", None)
|
||||
|
||||
IS_DARWIN = sys.platform == "darwin"
|
||||
@@ -61,6 +61,7 @@ class Defaults(Enum):
|
||||
# Enigma2.
|
||||
BOX_SERVICES_PATH = "/etc/enigma2/"
|
||||
BOX_SATELLITE_PATH = "/etc/tuxbox/"
|
||||
BOX_EPG_PATH = "/etc/enigma2/"
|
||||
BOX_PICON_PATH = "/usr/share/enigma2/picon/"
|
||||
BOX_PICON_PATHS = ("/usr/share/enigma2/picon/",
|
||||
"/media/hdd/picon/",
|
||||
@@ -73,8 +74,8 @@ class Defaults(Enum):
|
||||
NEUTRINO_BOX_PICON_PATH = "/usr/share/tuxbox/neutrino/icons/logo/"
|
||||
NEUTRINO_BOX_PICON_PATHS = ("/usr/share/tuxbox/neutrino/icons/logo/",)
|
||||
# Paths.
|
||||
BACKUP_PATH = "{}backup{}".format(DATA_PATH, SEP)
|
||||
PICON_PATH = "{}picons{}".format(DATA_PATH, SEP)
|
||||
BACKUP_PATH = f"{DATA_PATH}backup{SEP}"
|
||||
PICON_PATH = f"{DATA_PATH}picons{SEP}"
|
||||
|
||||
DEFAULT_PROFILE = "default"
|
||||
BACKUP_BEFORE_DOWNLOADING = True
|
||||
@@ -92,10 +93,11 @@ class Defaults(Enum):
|
||||
FAV_CLICK_MODE = 0
|
||||
PLAY_STREAMS_MODE = 1 if IS_DARWIN else 0
|
||||
STREAM_LIB = "mpv" if IS_WIN else "vlc"
|
||||
MAIN_LIST_PLAYBACK = False
|
||||
PROFILE_FOLDER_DEFAULT = False
|
||||
RECORDS_PATH = DATA_PATH + "records{}".format(SEP)
|
||||
RECORDINGS_PATH = f"{DATA_PATH}recordings{SEP}"
|
||||
ACTIVATE_TRANSCODING = False
|
||||
ACTIVE_TRANSCODING_PRESET = "720p TV{}device".format(SEP)
|
||||
ACTIVE_TRANSCODING_PRESET = f"720p TV{SEP}device"
|
||||
|
||||
|
||||
class SettingsType(IntEnum):
|
||||
@@ -109,12 +111,14 @@ class SettingsType(IntEnum):
|
||||
srv_path = Defaults.BOX_SERVICES_PATH.value
|
||||
sat_path = Defaults.BOX_SATELLITE_PATH.value
|
||||
picons_path = Defaults.BOX_PICON_PATH.value
|
||||
epg_path = Defaults.BOX_EPG_PATH.value
|
||||
http_timeout = 5
|
||||
telnet_timeout = 5
|
||||
else:
|
||||
srv_path = Defaults.NEUTRINO_BOX_SERVICES_PATH.value
|
||||
sat_path = Defaults.NEUTRINO_BOX_SATELLITE_PATH.value
|
||||
picons_path = Defaults.NEUTRINO_BOX_PICON_PATH.value
|
||||
epg_path = ""
|
||||
http_timeout = 2
|
||||
telnet_timeout = 1
|
||||
|
||||
@@ -132,6 +136,7 @@ class SettingsType(IntEnum):
|
||||
"services_path": srv_path,
|
||||
"user_bouquet_path": srv_path,
|
||||
"satellites_xml_path": sat_path,
|
||||
"epg_dat_path": epg_path,
|
||||
"picons_path": picons_path}
|
||||
|
||||
|
||||
@@ -150,6 +155,12 @@ class PlayStreamsMode(IntEnum):
|
||||
M3U = 2
|
||||
|
||||
|
||||
class EpgSource(IntEnum):
|
||||
HTTP = 0 # HTTP API -> WebIf
|
||||
DAT = 1 # epg.dat file
|
||||
XML = 2 # XML TV
|
||||
|
||||
|
||||
class Settings:
|
||||
__INSTANCE = None
|
||||
__VERSION = 2
|
||||
@@ -270,6 +281,14 @@ class Settings:
|
||||
def host(self, value):
|
||||
self._cp_settings["host"] = value
|
||||
|
||||
@property
|
||||
def hosts(self):
|
||||
return self._cp_settings.get("hosts", [self.host, ])
|
||||
|
||||
@hosts.setter
|
||||
def hosts(self, value):
|
||||
self._cp_settings["hosts"] = value
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
return self._cp_settings.get("port", self.get_default("port"))
|
||||
@@ -358,6 +377,14 @@ class Settings:
|
||||
def satellites_xml_path(self, value):
|
||||
self._cp_settings["satellites_xml_path"] = value
|
||||
|
||||
@property
|
||||
def epg_dat_path(self):
|
||||
return self._cp_settings.get("epg_dat_path", self.get_default("epg_dat_path"))
|
||||
|
||||
@epg_dat_path.setter
|
||||
def epg_dat_path(self, value):
|
||||
self._cp_settings["epg_dat_path"] = value
|
||||
|
||||
@property
|
||||
def picons_path(self):
|
||||
return self._cp_settings.get("picons_path", self.get_default("picons_path"))
|
||||
@@ -416,7 +443,7 @@ class Settings:
|
||||
|
||||
@property
|
||||
def profile_data_path(self):
|
||||
return "{}data{}{}{}".format(self.default_data_path, SEP, self._current_profile, SEP)
|
||||
return f"{self.default_data_path}data{SEP}{self._current_profile}{SEP}"
|
||||
|
||||
@profile_data_path.setter
|
||||
def profile_data_path(self, value):
|
||||
@@ -425,8 +452,8 @@ class Settings:
|
||||
@property
|
||||
def profile_picons_path(self):
|
||||
if self.profile_folder_is_default:
|
||||
return "{}picons{}".format(self.profile_data_path, SEP)
|
||||
return "{}{}{}".format(self.default_picon_path, self._current_profile, SEP)
|
||||
return f"{self.profile_data_path}picons{SEP}"
|
||||
return f"{self.default_picon_path}{self._current_profile}{SEP}"
|
||||
|
||||
@profile_picons_path.setter
|
||||
def profile_picons_path(self, value):
|
||||
@@ -435,20 +462,20 @@ class Settings:
|
||||
@property
|
||||
def profile_backup_path(self):
|
||||
if self.profile_folder_is_default:
|
||||
return "{}backup{}".format(self.profile_data_path, SEP)
|
||||
return "{}{}{}".format(self.default_backup_path, self._current_profile, SEP)
|
||||
return f"{self.profile_data_path}backup{SEP}"
|
||||
return f"{self.default_backup_path}{self._current_profile}{SEP}"
|
||||
|
||||
@profile_backup_path.setter
|
||||
def profile_backup_path(self, value):
|
||||
self._cp_settings["profile_backup_path"] = value
|
||||
|
||||
@property
|
||||
def records_path(self):
|
||||
return self._settings.get("records_path", Defaults.RECORDS_PATH.value)
|
||||
def recordings_path(self):
|
||||
return self._settings.get("recordings_path", Defaults.RECORDINGS_PATH.value)
|
||||
|
||||
@records_path.setter
|
||||
def records_path(self, value):
|
||||
self._settings["records_path"] = value
|
||||
@recordings_path.setter
|
||||
def recordings_path(self, value):
|
||||
self._settings["recordings_path"] = value
|
||||
|
||||
# ******** Streaming ********* #
|
||||
|
||||
@@ -492,6 +519,22 @@ class Settings:
|
||||
def stream_lib(self, value):
|
||||
self._settings["stream_lib"] = 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 main_list_playback(self):
|
||||
return self._settings.get("main_list_playback", Defaults.MAIN_LIST_PLAYBACK.value)
|
||||
|
||||
@main_list_playback.setter
|
||||
def main_list_playback(self, value):
|
||||
self._settings["main_list_playback"] = value
|
||||
|
||||
# *********** EPG ************ #
|
||||
|
||||
@property
|
||||
@@ -503,6 +546,40 @@ class Settings:
|
||||
def epg_options(self, value):
|
||||
self._cp_settings["epg_options"] = value
|
||||
|
||||
@property
|
||||
def epg_source(self):
|
||||
return EpgSource(self._cp_settings.get("epg_source", EpgSource.HTTP))
|
||||
|
||||
@epg_source.setter
|
||||
def epg_source(self, value):
|
||||
self._cp_settings["epg_source"] = value
|
||||
|
||||
@property
|
||||
def epg_update_interval(self):
|
||||
return self._cp_settings.get("epg_update_interval", 5)
|
||||
|
||||
@epg_update_interval.setter
|
||||
def epg_update_interval(self, value):
|
||||
self._cp_settings["epg_update_interval"] = value
|
||||
|
||||
@property
|
||||
def epg_xml_source(self):
|
||||
return self._cp_settings.get("epg_xml_source", "")
|
||||
|
||||
@epg_xml_source.setter
|
||||
def epg_xml_source(self, value):
|
||||
self._cp_settings["epg_xml_source"] = value
|
||||
|
||||
# *********** FTP ************ #
|
||||
|
||||
@property
|
||||
def ftp_bookmarks(self):
|
||||
return self._cp_settings.get("ftp_bookmarks", [])
|
||||
|
||||
@ftp_bookmarks.setter
|
||||
def ftp_bookmarks(self, value):
|
||||
self._cp_settings["ftp_bookmarks"] = value
|
||||
|
||||
# ***** Program settings ***** #
|
||||
|
||||
@property
|
||||
@@ -569,14 +646,6 @@ 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")
|
||||
@@ -676,6 +745,22 @@ class Settings:
|
||||
def dark_mode(self, value):
|
||||
self._settings["dark_mode"] = value
|
||||
|
||||
@property
|
||||
def display_picons(self):
|
||||
return self._settings.get("display_picons", True)
|
||||
|
||||
@display_picons.setter
|
||||
def display_picons(self, value):
|
||||
self._settings["display_picons"] = value
|
||||
|
||||
@property
|
||||
def display_epg(self):
|
||||
return self._settings.get("display_epg", False)
|
||||
|
||||
@display_epg.setter
|
||||
def display_epg(self, value):
|
||||
self._settings["display_epg"] = value
|
||||
|
||||
@property
|
||||
def alternate_layout(self):
|
||||
return self._settings.get("alternate_layout", IS_DARWIN)
|
||||
@@ -711,7 +796,7 @@ class Settings:
|
||||
@property
|
||||
@lru_cache(1)
|
||||
def themes_path(self):
|
||||
return "{}{}.themes{}".format(HOME_PATH, SEP, SEP)
|
||||
return f"{HOME_PATH}{SEP}.themes{SEP}"
|
||||
|
||||
@property
|
||||
def icon_theme(self):
|
||||
@@ -724,13 +809,13 @@ class Settings:
|
||||
@property
|
||||
@lru_cache(1)
|
||||
def icon_themes_path(self):
|
||||
return "{}{}.icons{}".format(HOME_PATH, SEP, SEP)
|
||||
return f"{HOME_PATH}{SEP}.icons{SEP}"
|
||||
|
||||
@property
|
||||
def is_darwin(self):
|
||||
return IS_DARWIN
|
||||
|
||||
# *********** Download dialog *********** #
|
||||
# ************* Download ************** #
|
||||
|
||||
@property
|
||||
def use_http(self):
|
||||
@@ -748,6 +833,22 @@ class Settings:
|
||||
def remove_unused_bouquets(self, value):
|
||||
self._settings["remove_unused_bouquets"] = value
|
||||
|
||||
@property
|
||||
def keep_power_mode(self):
|
||||
return self._settings.get("keep_power_mode", False)
|
||||
|
||||
@keep_power_mode.setter
|
||||
def keep_power_mode(self, value):
|
||||
self._settings["keep_power_mode"] = value
|
||||
|
||||
@property
|
||||
def compress_picons(self):
|
||||
return self._settings.get("compress_picons", False)
|
||||
|
||||
@compress_picons.setter
|
||||
def compress_picons(self, value):
|
||||
self._settings["compress_picons"] = value
|
||||
|
||||
# **************** Debug **************** #
|
||||
|
||||
@property
|
||||
@@ -777,7 +878,10 @@ class Settings:
|
||||
Settings.write_settings(Settings.get_default_settings())
|
||||
|
||||
with open(CONFIG_FILE, "r", encoding="utf-8") as config_file:
|
||||
return json.load(config_file)
|
||||
try:
|
||||
return json.load(config_file)
|
||||
except ValueError as e:
|
||||
raise SettingsReadException(e)
|
||||
|
||||
@staticmethod
|
||||
def get_default_settings(profile_name="default"):
|
||||
@@ -796,7 +900,7 @@ class Settings:
|
||||
"extra_color": Defaults.EXTRA_COLOR.value,
|
||||
"fav_click_mode": Defaults.FAV_CLICK_MODE.value,
|
||||
"profile_folder_is_default": Defaults.PROFILE_FOLDER_DEFAULT.value,
|
||||
"records_path": Defaults.RECORDS_PATH.value
|
||||
"records_path": Defaults.RECORDINGS_PATH.value
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
||||
382
app/tools/epg.py
382
app/tools/epg.py
@@ -1,45 +1,367 @@
|
||||
""" Module for working with epg.dat file """
|
||||
import struct
|
||||
from datetime import datetime
|
||||
from xml.dom.minidom import parse, Node, Document
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 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 epg.dat file. """
|
||||
import abc
|
||||
import os
|
||||
import shutil
|
||||
import struct
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from datetime import datetime, timezone
|
||||
from tempfile import NamedTemporaryFile
|
||||
from urllib.parse import urlparse
|
||||
from xml.dom.minidom import parse, Node, Document
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import requests
|
||||
|
||||
from app.commons import log
|
||||
from app.eparser.ecommons import BqServiceType, BouquetService
|
||||
from app.settings import IS_WIN
|
||||
|
||||
ENCODING = "utf-8"
|
||||
DETECT_ENCODING = False
|
||||
try:
|
||||
import chardet
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
else:
|
||||
DETECT_ENCODING = True
|
||||
|
||||
EpgEvent = namedtuple("EpgEvent", ["service_name", "title", "time", "desc", "event_data"])
|
||||
EpgEvent.__new__.__defaults__ = ("N/A", "N/A", "N/A", "N/A", None) # For Python3 < 3.7
|
||||
|
||||
|
||||
class Reader(metaclass=abc.ABCMeta):
|
||||
|
||||
@abc.abstractmethod
|
||||
def download(self, clb=None): pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_current_events(self, ids: set) -> dict: pass
|
||||
|
||||
|
||||
class EPG:
|
||||
""" Base EPG class. """
|
||||
# DVB/EPG count days with a 'modified Julian calendar' where day 1 is 17 November 1858.
|
||||
# datetime.datetime.toordinal(1858,11,17) => 678576
|
||||
ZERO_DAY = 678576
|
||||
|
||||
@staticmethod
|
||||
def get_epg_refs(path):
|
||||
""" The read algorithm was taken from the eEPGCache::load() function from this source:
|
||||
Event = namedtuple("EpgEvent", ["id", "data", "start", "duration", "title", "desc", "ext_desc"])
|
||||
|
||||
class EventData:
|
||||
""" Event data representation class. """
|
||||
__slots__ = ["raw_data", "crc", "size", "type"]
|
||||
|
||||
def __init__(self, size=0, e_type=0):
|
||||
self.raw_data = None
|
||||
self.crc = None
|
||||
self.size = size
|
||||
self.type = e_type
|
||||
|
||||
def get_event_id(self):
|
||||
return self.raw_data[0] << 8 | self.raw_data[1]
|
||||
|
||||
def get_start_time(self):
|
||||
""" Returns start time [sec.]. """
|
||||
# Date
|
||||
start_date = datetime.fromordinal((self.raw_data[2] << 8 | self.raw_data[3]) + EPG.ZERO_DAY).timestamp()
|
||||
# Time
|
||||
tm_hour = EPG.get_from_bcd(self.raw_data[4])
|
||||
tm_min = EPG.get_from_bcd(self.raw_data[5])
|
||||
tm_sec = EPG.get_from_bcd(self.raw_data[6])
|
||||
# UTC.
|
||||
s_time = start_date + tm_hour * 3600 + tm_min * 60 + tm_sec
|
||||
# Time zone correction.
|
||||
s_time += datetime.now(timezone.utc).astimezone().utcoffset().seconds
|
||||
|
||||
return s_time
|
||||
|
||||
def get_duration(self):
|
||||
""" Returns duration [sec.]."""
|
||||
return EPG.get_from_bcd(self.raw_data[7]) * 3600 + EPG.get_from_bcd(
|
||||
self.raw_data[8]) * 60 + EPG.get_from_bcd(self.raw_data[9])
|
||||
|
||||
class DatReader(Reader):
|
||||
""" The epd.dat file reading class.
|
||||
|
||||
The read algorithm was taken from the eEPGCache::load() function from this source:
|
||||
https://github.com/OpenPLi/enigma2/blob/44d9b92f5260c7de1b3b3a1b9a9cbe0f70ca4bf0/lib/dvb/epgcache.cpp#L1300
|
||||
"""
|
||||
refs = set()
|
||||
|
||||
with open(path, mode="rb") as f:
|
||||
crc = struct.unpack("<I", f.read(4))[0]
|
||||
if crc != int(0x98765432):
|
||||
raise ValueError("Epg file has incorrect byte order!")
|
||||
def __init__(self, path):
|
||||
self._path = path
|
||||
self._refs = {}
|
||||
self._desc = {}
|
||||
|
||||
header = f.read(13).decode()
|
||||
if header != "ENIGMA_EPG_V7":
|
||||
raise ValueError("Unsupported format of epd.dat file!")
|
||||
def download(self, clb=None):
|
||||
pass
|
||||
|
||||
channels_count = struct.unpack("<I", f.read(4))[0]
|
||||
def get_current_events(self, ids: set) -> dict:
|
||||
pass
|
||||
|
||||
for i in range(channels_count):
|
||||
sid, nid, tsid, events_size = struct.unpack("<IIII", f.read(16))
|
||||
service_id = "{:X}:{:X}:{:X}".format(sid, tsid, nid)
|
||||
def get_refs(self):
|
||||
return self._refs.keys()
|
||||
|
||||
for j in range(events_size):
|
||||
_type, _len = struct.unpack("<BB", f.read(2))
|
||||
f.read(10)
|
||||
n_crc = (_len - 10) // 4
|
||||
if n_crc > 0:
|
||||
[f.read(4) for n in range(n_crc)]
|
||||
def get_services(self):
|
||||
return self._refs
|
||||
|
||||
refs.add(service_id)
|
||||
def get_event(self, evd):
|
||||
title, desc, ext_desc = None, None, None
|
||||
e_id, start, duration = evd.get_event_id(), evd.get_start_time(), evd.get_duration()
|
||||
|
||||
return refs
|
||||
for c in evd.crc:
|
||||
data = self._desc.get(c, None)
|
||||
if not data:
|
||||
continue
|
||||
|
||||
encoding = ENCODING
|
||||
if DETECT_ENCODING:
|
||||
# May be slow.
|
||||
encoding = chardet.detect(data).get("encoding", "utf-8") or encoding
|
||||
|
||||
desc_type = data[0]
|
||||
if desc_type == 77: # Short event descriptor -> 0x4d -> 77
|
||||
size = data[6]
|
||||
txt = data[7:-1].decode(encoding, errors="ignore")
|
||||
t_len = len(txt)
|
||||
st = 0
|
||||
|
||||
if size and size < t_len:
|
||||
st = abs(size - t_len)
|
||||
|
||||
if size < 32:
|
||||
title = txt
|
||||
else:
|
||||
desc = txt[st:]
|
||||
elif desc_type == 78: # Extended event descriptor -> 0x4e -> 78
|
||||
ext_desc = data[9:].decode(encoding, errors="ignore") if data[7] and data[8] < 32 else None
|
||||
|
||||
return EPG.Event(e_id, evd, start, duration, title, desc, ext_desc)
|
||||
|
||||
def get_events(self, ref):
|
||||
return self._refs.get(ref, {})
|
||||
|
||||
def read(self):
|
||||
with open(self._path, mode="rb") as f:
|
||||
crc = struct.unpack("I", f.read(4))[0]
|
||||
if crc != int(0x98765432):
|
||||
raise ValueError("Epg file has incorrect byte order!")
|
||||
|
||||
header = f.read(13).decode()
|
||||
if header == "ENIGMA_EPG_V7":
|
||||
epg_ver = 7
|
||||
elif header == "ENIGMA_EPG_V8":
|
||||
epg_ver = 8
|
||||
else:
|
||||
raise ValueError("Unsupported format of epd.dat file!")
|
||||
|
||||
channels_count = struct.unpack("I", f.read(4))[0]
|
||||
_len_read_size = 3 if epg_ver == 8 else 2
|
||||
_type_read_str = f"{'H' if epg_ver == 8 else 'B'}B"
|
||||
|
||||
for i in range(channels_count):
|
||||
sid, nid, tsid, events_size = struct.unpack("IIII", f.read(16))
|
||||
service_id = f"{sid:X}:{tsid:X}:{nid:X}"
|
||||
events = {}
|
||||
|
||||
for j in range(events_size):
|
||||
_type, _len = struct.unpack(_type_read_str, f.read(_len_read_size))
|
||||
event = EPG.EventData(size=_len, e_type=_type)
|
||||
event.raw_data = f.read(10)
|
||||
|
||||
n_crc = (_len - 10) // 4
|
||||
if n_crc > 0:
|
||||
event.crc = [struct.unpack("I", f.read(4))[0] for n in range(n_crc)]
|
||||
events[event.get_event_id()] = event
|
||||
|
||||
self._refs[service_id] = events
|
||||
|
||||
for i in range(struct.unpack("I", f.read(4))[0]):
|
||||
_id, ref_count = struct.unpack("II", f.read(8))
|
||||
header = struct.unpack("BB", f.read(2))
|
||||
_bytes = header[1] + 2
|
||||
f.seek(-2, os.SEEK_CUR)
|
||||
self._desc[_id] = f.read(_bytes)
|
||||
|
||||
@staticmethod
|
||||
def get_from_bcd(value: int):
|
||||
""" Converts a BCD to an integer. """
|
||||
if ((value & 0xF0) >= 0xA0) or ((value & 0xF) >= 0xA):
|
||||
return -1
|
||||
return ((value & 0xF0) >> 4) * 10 + (value & 0xF)
|
||||
|
||||
|
||||
class XmlTvReader(Reader):
|
||||
PR_TAG = "programme"
|
||||
CH_TAG = "channel"
|
||||
DSP_NAME_TAG = "display-name"
|
||||
ICON_TAG = "icon"
|
||||
TITLE_TAG = "title"
|
||||
DESC_TAG = "desc"
|
||||
|
||||
TIME_FORMAT_STR = "%Y%m%d%H%M%S %z"
|
||||
|
||||
Service = namedtuple("Service", ["id", "names", "logo", "events"])
|
||||
Event = namedtuple("EpgEvent", ["start", "duration", "title", "desc"])
|
||||
|
||||
def __init__(self, path, url):
|
||||
self._path = path
|
||||
self._url = url
|
||||
self._ids = {}
|
||||
|
||||
def download(self, clb=None):
|
||||
""" Downloads an XMLTV file. """
|
||||
res = urlparse(self._url)
|
||||
if not all((res.scheme, res.netloc)):
|
||||
log(f"{self.__class__.__name__} [download] error: Invalid URL {self._url}")
|
||||
return
|
||||
|
||||
with requests.get(url=self._url, stream=True) as request:
|
||||
if request.reason == "OK":
|
||||
suf = self._url[self._url.rfind("."):]
|
||||
if suf not in (".gz", ".xz", ".lzma"):
|
||||
log(f"{self.__class__.__name__} [download] error: Unsupported file extension.")
|
||||
return
|
||||
|
||||
data_len = request.headers.get("content-length")
|
||||
|
||||
with NamedTemporaryFile(suffix=suf, delete=not IS_WIN) as tf:
|
||||
downloaded = 0
|
||||
data_len = int(data_len)
|
||||
log("Downloading XMLTV file...")
|
||||
for data in request.iter_content(chunk_size=1024):
|
||||
downloaded += len(data)
|
||||
tf.write(data)
|
||||
done = int(50 * downloaded / data_len)
|
||||
sys.stdout.write(f"\rDownloading XMLTV file [{'=' * done}{' ' * (50 - done)}]")
|
||||
sys.stdout.flush()
|
||||
tf.seek(0)
|
||||
sys.stdout.write("\n")
|
||||
|
||||
os.makedirs(os.path.dirname(self._path), exist_ok=True)
|
||||
|
||||
if suf.endswith(".gz"):
|
||||
try:
|
||||
shutil.copyfile(tf.name, self._path)
|
||||
except OSError as e:
|
||||
log(f"{self.__class__.__name__} [download *.gz] error: {e}")
|
||||
elif self._url.endswith((".xz", ".lzma")):
|
||||
import lzma
|
||||
|
||||
try:
|
||||
with lzma.open(tf, "rb") as lzf:
|
||||
shutil.copyfileobj(lzf, self._path)
|
||||
except (lzma.LZMAError, OSError) as e:
|
||||
log(f"{self.__class__.__name__} [download *.xz] error: {e}")
|
||||
|
||||
if IS_WIN and os.path.isfile(tf.name):
|
||||
tf.close()
|
||||
os.remove(tf.name)
|
||||
else:
|
||||
log(f"{self.__class__.__name__} [download] error: {request.reason}")
|
||||
|
||||
if clb:
|
||||
clb()
|
||||
|
||||
def get_current_events(self, names: set) -> dict:
|
||||
events = {}
|
||||
|
||||
dt = datetime.utcnow()
|
||||
utc = dt.timestamp()
|
||||
offset = datetime.now() - dt
|
||||
|
||||
for srv in filter(lambda s: any(name in names for name in s.names), self._ids.values()):
|
||||
ev = list(filter(lambda s: s.start < utc, srv.events))
|
||||
if ev:
|
||||
ev = ev[-1]
|
||||
start = datetime.fromtimestamp(ev.start) + offset
|
||||
end_time = datetime.fromtimestamp(ev.duration) + offset
|
||||
tm = f"{start.strftime('%H:%M')} - {end_time.strftime('%H:%M')}"
|
||||
for n in srv.names:
|
||||
events[n] = EpgEvent(n, ev.title, tm, ev.desc, ev)
|
||||
|
||||
return events
|
||||
|
||||
def parse(self):
|
||||
""" Parses XML. """
|
||||
try:
|
||||
import gzip
|
||||
|
||||
with gzip.open(self._path, "rb") as gzf:
|
||||
log("Processing XMLTV data...")
|
||||
list(map(self.process_node, ET.iterparse(gzf)))
|
||||
log("XMLTV data parsing is complete.")
|
||||
except OSError as e:
|
||||
log(f"{self.__class__.__name__} [parse] error: {e}")
|
||||
|
||||
def process_node(self, node):
|
||||
event, element = node
|
||||
if element.tag == self.CH_TAG:
|
||||
ch_id = element.get("id", None)
|
||||
logo = None # Currently not in use.
|
||||
# Since a service can have several names, we will store a set of names in the "names" field!
|
||||
self._ids[ch_id] = self.Service(ch_id, {c.text for c in element if c.tag == self.DSP_NAME_TAG}, logo, [])
|
||||
elif element.tag == self.PR_TAG:
|
||||
channel = self._ids.get(element.get(self.CH_TAG, None), None)
|
||||
if channel:
|
||||
events = channel[-1]
|
||||
start = element.get("start", None)
|
||||
if start:
|
||||
start = self.get_utc_time(start)
|
||||
|
||||
stop = element.get("stop", None)
|
||||
if stop:
|
||||
stop = self.get_utc_time(stop)
|
||||
|
||||
title, desc = None, None
|
||||
for c in element:
|
||||
if c.tag == self.TITLE_TAG:
|
||||
title = c.text
|
||||
elif c.tag == self.DESC_TAG:
|
||||
desc = c.text
|
||||
|
||||
if all((start, stop, title)):
|
||||
events.append(self.Event(start, stop, title, desc))
|
||||
|
||||
def to_epg_dat(self):
|
||||
""" Converts and saves imported data to 'epg.dat' file. """
|
||||
raise ValueError("Not implemented yet!")
|
||||
|
||||
@staticmethod
|
||||
def get_utc_time(time_str):
|
||||
""" Returns the UTC time in seconds. """
|
||||
t, sep, delta = time_str.partition(" ")
|
||||
t = datetime(*map(int, (t[:4], t[4:6], t[6:8], t[8:10], t[10:12], t[12:]))).timestamp()
|
||||
if delta:
|
||||
t -= (3600 * int(delta) // 100)
|
||||
return t
|
||||
|
||||
|
||||
class ChannelsParser:
|
||||
@@ -90,14 +412,14 @@ class ChannelsParser:
|
||||
srv_type = srv.type
|
||||
if srv_type is BqServiceType.IPTV:
|
||||
channel_child = doc.createElement("channel")
|
||||
channel_child.setAttribute("id", str(srv.num))
|
||||
channel_child.setAttribute("id", srv.name)
|
||||
data = srv.data.strip().split(":")
|
||||
channel_child.appendChild(doc.createTextNode(":".join(data[:10])))
|
||||
comment = doc.createComment(srv.name)
|
||||
lines.append("{} {}\n".format(str(channel_child.toxml()), str(comment.toxml())))
|
||||
lines.append(f"{channel_child.toxml()} {comment.toxml()}\n")
|
||||
elif srv_type is BqServiceType.MARKER:
|
||||
comment = doc.createComment(srv.name)
|
||||
lines.append("{}\n".format(str(comment.toxml())))
|
||||
lines.append(f"{comment.toxml()}\n")
|
||||
|
||||
lines.append("</channels>")
|
||||
doc.unlink()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -32,7 +32,8 @@ from datetime import datetime
|
||||
|
||||
from gi.repository import Gdk, Gtk, GObject
|
||||
|
||||
from app.commons import run_task, log, _DATE_FORMAT, run_with_delay
|
||||
from app.commons import run_task, log, LOG_DATE_FORMAT, run_with_delay
|
||||
from app.settings import IS_DARWIN, IS_LINUX, IS_WIN
|
||||
|
||||
|
||||
class Player(Gtk.DrawingArea):
|
||||
@@ -115,22 +116,21 @@ class Player(Gtk.DrawingArea):
|
||||
Based on gtkvlc.py[get_window_pointer] example from here:
|
||||
https://github.com/oaubert/python-vlc/tree/master/examples
|
||||
"""
|
||||
if sys.platform == "linux":
|
||||
if IS_LINUX:
|
||||
return self.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")
|
||||
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))
|
||||
log(f"{__class__.__name__}: Load library error: {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(self.get_window().__gpointer__, None)
|
||||
get_pointer = libgdk.gdk_quartz_window_get_nsview if is_darwin else libgdk.gdk_win32_window_get_handle
|
||||
get_pointer = 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]
|
||||
|
||||
@@ -171,7 +171,7 @@ class Player(Gtk.DrawingArea):
|
||||
elif name == "vlc":
|
||||
return VlcPlayer.get_instance(mode, widget)
|
||||
else:
|
||||
raise NameError("There is no such [{}] implementation.".format(name))
|
||||
raise NameError(f"There is no such [{name}] implementation.")
|
||||
|
||||
|
||||
class MpvPlayer(Player):
|
||||
@@ -191,7 +191,7 @@ class MpvPlayer(Player):
|
||||
input_cursor=False,
|
||||
cursor_autohide="no")
|
||||
except OSError as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
log(f"{__class__.__name__}: Load library error: {e}")
|
||||
raise ImportError("No libmpv is found. Check that it is installed!")
|
||||
else:
|
||||
self._mode = mode
|
||||
@@ -202,11 +202,22 @@ class MpvPlayer(Player):
|
||||
log("Starting playback...")
|
||||
self.emit("played", 0)
|
||||
|
||||
t_list = self._player._get_property("track-list")
|
||||
if t_list:
|
||||
# Audio tracks.
|
||||
a_tracks = filter(lambda t: t.get("type", "") == "audio", t_list)
|
||||
self.emit("audio-track", ((t.get("id", 1), t.get("lang", "Unknown")) for t in a_tracks))
|
||||
# Subtitle.
|
||||
sub_tracks = [(0, "no")]
|
||||
tracks = filter(lambda t: t.get("type", "") == "sub", t_list)
|
||||
[sub_tracks.append((t.get("id", 1), t.get("lang", "Unknown"))) for t in tracks]
|
||||
self.emit("subtitle-track", sub_tracks)
|
||||
|
||||
@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)))
|
||||
log(f"Stream playback error: {event.get('error', mpv.ErrorCode.GENERIC)}")
|
||||
self.emit("error", "Can't Playback!")
|
||||
|
||||
@classmethod
|
||||
@@ -243,6 +254,15 @@ class MpvPlayer(Player):
|
||||
def is_playing(self):
|
||||
return self._is_playing
|
||||
|
||||
def set_audio_track(self, track):
|
||||
self._player._set_property("aid", track)
|
||||
|
||||
def set_subtitle_track(self, track):
|
||||
self._player._set_property("sub", track)
|
||||
|
||||
def set_aspect_ratio(self, ratio):
|
||||
self._player._set_property("aspect", ratio or "-1.0")
|
||||
|
||||
|
||||
class GstPlayer(Player):
|
||||
""" Simple wrapper for GStreamer playbin. """
|
||||
@@ -260,7 +280,7 @@ class GstPlayer(Player):
|
||||
# Initialization of GStreamer.
|
||||
Gst.init(sys.argv)
|
||||
except (OSError, ValueError) as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
log(f"{__class__.__name__}: Load library error: {e}")
|
||||
raise ImportError("No GStreamer is found. Check that it is installed!")
|
||||
else:
|
||||
self.STATE = Gst.State
|
||||
@@ -293,11 +313,11 @@ class GstPlayer(Player):
|
||||
|
||||
self._player.set_property("uri", mrl)
|
||||
|
||||
log("Setting the URL for playback: {}".format(mrl))
|
||||
log(f"Setting the URL for playback: {mrl}")
|
||||
ret = self._player.set_state(self.STATE.PLAYING)
|
||||
|
||||
if ret == self.STAT_RETURN.FAILURE:
|
||||
msg = "ERROR: Unable to set the 'PLAYING' state for '{}'.".format(mrl)
|
||||
msg = f"ERROR: Unable to set the 'PLAYING' state for '{mrl}'."
|
||||
log(msg)
|
||||
self.emit("error", msg)
|
||||
else:
|
||||
@@ -356,7 +376,7 @@ class GstPlayer(Player):
|
||||
tags = self._player.emit("get-video-tags", i)
|
||||
if tags:
|
||||
_, cod = tags.get_string("video-codec")
|
||||
log("Video codec: {}".format(cod or "unknown"))
|
||||
log(f"Video codec: {cod or 'unknown'}")
|
||||
|
||||
nr_audio = self._player.get_property("n-audio")
|
||||
for i in range(nr_audio):
|
||||
@@ -364,7 +384,7 @@ class GstPlayer(Player):
|
||||
tags = self._player.emit("get-audio-tags", i)
|
||||
if tags:
|
||||
_, cod = tags.get_string("audio-codec")
|
||||
log("Audio codec: {}".format(cod or "unknown"))
|
||||
log(f"Audio codec: {cod or 'unknown'}")
|
||||
|
||||
|
||||
class VlcPlayer(Player):
|
||||
@@ -378,18 +398,18 @@ class VlcPlayer(Player):
|
||||
def __init__(self, mode, widget):
|
||||
super().__init__(mode, widget)
|
||||
try:
|
||||
if sys.platform == "win32":
|
||||
if IS_WIN:
|
||||
os.add_dll_directory(r"C:\Program Files\VideoLAN\VLC")
|
||||
|
||||
from app.tools import vlc
|
||||
from app.tools.vlc import EventType
|
||||
|
||||
args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib")
|
||||
args = f"--quiet {'' if IS_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))
|
||||
log(f"{__class__.__name__}: Load library error: {e}")
|
||||
raise ImportError("No VLC is found. Check that it is installed!")
|
||||
else:
|
||||
self._mode = mode
|
||||
@@ -457,17 +477,17 @@ class VlcPlayer(Player):
|
||||
|
||||
def on_playback_start(self, event):
|
||||
self.emit("played", self._player.get_media().get_duration())
|
||||
# Audio tracks
|
||||
# Audio tracks.
|
||||
a_desc = self._player.audio_get_track_description()
|
||||
self.emit("audio-track", [(t[0], t[1].decode(encoding="utf-8", errors="ignore")) for t in a_desc])
|
||||
# Subtitle
|
||||
# Subtitle.
|
||||
s_desc = self._player.video_get_spu_description()
|
||||
self.emit("subtitle-track", [(s[0], s[1].decode(encoding="utf-8", errors="ignore")) for s in s_desc])
|
||||
|
||||
def init_video_widget(self, widget):
|
||||
if sys.platform == "linux":
|
||||
if IS_LINUX:
|
||||
self._player.set_xwindow(self.get_window_handle())
|
||||
elif sys.platform == "darwin":
|
||||
elif IS_DARWIN:
|
||||
self._player.set_nsobject(self.get_window_handle())
|
||||
else:
|
||||
self._player.set_hwnd(self.get_window_handle())
|
||||
@@ -481,15 +501,18 @@ class Recorder:
|
||||
|
||||
def __init__(self, settings):
|
||||
try:
|
||||
if IS_WIN:
|
||||
os.add_dll_directory(r"C:\Program Files\VideoLAN\VLC")
|
||||
|
||||
from app.tools import vlc
|
||||
from app.tools.vlc import EventType
|
||||
except OSError as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
log(f"{__class__.__name__}: Load library error: {e}")
|
||||
raise ImportError
|
||||
else:
|
||||
self._settings = settings
|
||||
self._is_record = False
|
||||
args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib")
|
||||
args = f"--quiet {'' if IS_DARWIN else '--no-xlib'}"
|
||||
self._recorder = vlc.Instance(args).media_player_new()
|
||||
|
||||
@classmethod
|
||||
@@ -503,10 +526,11 @@ class Recorder:
|
||||
if self._recorder:
|
||||
self._recorder.stop()
|
||||
|
||||
path = self._settings.records_path
|
||||
path = self._settings.recordings_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(" ", "_"))
|
||||
d_now = datetime.now().strftime(LOG_DATE_FORMAT)
|
||||
d_now = d_now.replace(" ", "_").replace(":", "-") if IS_WIN else d_now.replace(" ", "_")
|
||||
path = f"{path}{name.replace(' ', '_')}_{d_now}"
|
||||
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()
|
||||
@@ -514,7 +538,7 @@ class Recorder:
|
||||
self._recorder.set_media(media)
|
||||
self._is_record = True
|
||||
self._recorder.play()
|
||||
log("Record started {}".format(d_now))
|
||||
log(f"Record started {d_now}")
|
||||
|
||||
@run_task
|
||||
def stop(self):
|
||||
@@ -536,7 +560,7 @@ class Recorder:
|
||||
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)
|
||||
return self._TR_CMD.format(",".join(f"{k}={v}" for k, v in prs.items()), path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 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
|
||||
@@ -57,7 +57,7 @@ class PiconsCzDownloader:
|
||||
_PERM_URL = "https://picon.cz/download/7337"
|
||||
_BASE_URL = "https://picon.cz/download/"
|
||||
_BASE_LOGO_URL = "https://picon.cz/picon/0/"
|
||||
_HEADER = {"User-Agent": "DemonEditor/2.0.0", "Referer": ""}
|
||||
_HEADER = {"User-Agent": "DemonEditor/3.0.0", "Referer": ""}
|
||||
_LINK_PATTERN = re.compile(r"((.*)-\d+x\d+)-(.*)_by_chocholousek.7z$")
|
||||
_FILE_PATTERN = re.compile(b"\\s+(1_.*\\.png).*")
|
||||
|
||||
@@ -116,11 +116,11 @@ class PiconsCzDownloader:
|
||||
with requests.get(url=provider.url, headers=self._HEADER, stream=True) as request:
|
||||
if request.reason == "OK":
|
||||
dest = f"{picons_path}{provider.on_id}.7z"
|
||||
self._appender(f"Downloading: {provider.url}\n")
|
||||
self._appender(f"Downloading: {provider.url}")
|
||||
with open(dest, mode="bw") as f:
|
||||
for data in request.iter_content(chunk_size=1024):
|
||||
f.write(data)
|
||||
self._appender(f"Extracting: {provider.on_id}\n")
|
||||
self._appender(f"Extracting: {provider.on_id}")
|
||||
self.extract(dest, picons_path, picon_ids)
|
||||
else:
|
||||
log(f"{self.__class__.__name__} [download] error: {request.reason}")
|
||||
@@ -130,13 +130,13 @@ class PiconsCzDownloader:
|
||||
# TODO: think about https://github.com/miurahr/py7zr
|
||||
exe = "7z"
|
||||
if IS_DARWIN and GTK_PATH:
|
||||
exe = "./7z"
|
||||
exe = "./7zr"
|
||||
|
||||
if IS_LINUX and not os.path.isfile(f"/usr/bin/{exe}"):
|
||||
raise PiconsError("7-zip [7z] archiver not found!")
|
||||
|
||||
if IS_WIN:
|
||||
exe = f"C:{os.sep}Program Files{os.sep}7-Zip{os.sep}{exe}.exe"
|
||||
exe = f"{exe}.exe" if GTK_PATH else f"C:{os.sep}Program Files{os.sep}7-Zip{os.sep}{exe}.exe"
|
||||
if not os.path.isfile(exe):
|
||||
raise PiconsError("7-Zip executable not found!")
|
||||
|
||||
@@ -219,7 +219,8 @@ class PiconsCzDownloader:
|
||||
"picontransparentdark": "td220",
|
||||
"piconoled": "o96",
|
||||
"piconblack80": "b50",
|
||||
"piconblack3d": "b50"
|
||||
"piconblack3d": "b50",
|
||||
"piconwin11": "win11220"
|
||||
}
|
||||
|
||||
def get_name_map(self):
|
||||
@@ -485,20 +486,16 @@ def parse_providers(url):
|
||||
return providers
|
||||
|
||||
|
||||
def download_picon(src_url, dest_path, callback):
|
||||
def download_picon(src_url, dest_path):
|
||||
""" 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)))
|
||||
log("Downloading: {}.".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:
|
||||
@@ -507,12 +504,10 @@ def download_picon(src_url, dest_path, callback):
|
||||
except OSError as e:
|
||||
err_msg = "Saving picon [{}] error: {}".format(dest_path, e)
|
||||
log(err_msg)
|
||||
if callback:
|
||||
callback(err_msg + "\n")
|
||||
|
||||
|
||||
@run_task
|
||||
def convert_to(src_path, dest_path, s_type, callback, done_callback):
|
||||
def convert_to(src_path, dest_path, s_type, done_callback):
|
||||
""" Converts names format of picons.
|
||||
|
||||
Copies resulting files from src to dest and writes state to callback.
|
||||
@@ -523,7 +518,7 @@ def convert_to(src_path, dest_path, s_type, callback, done_callback):
|
||||
pic_data = base_name.rstrip(".png").split("_")
|
||||
dest_file = _NEUTRINO_PICON_KEY.format(int(pic_data[4], 16), int(pic_data[5], 16), int(pic_data[3], 16))
|
||||
dest = "{}/{}".format(dest_path, dest_file)
|
||||
callback('Converting "{}" to "{}"\n'.format(base_name, dest_file))
|
||||
log('Converting "{}" to "{}"'.format(base_name, dest_file))
|
||||
shutil.copyfile(file, dest)
|
||||
|
||||
done_callback()
|
||||
|
||||
@@ -1,6 +1,34 @@
|
||||
""" Module for downloading satellites, transponders ans services from the web.
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 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
|
||||
#
|
||||
|
||||
Sources: www.flysat.com, www.lyngsat.com.
|
||||
|
||||
""" Module for downloading satellites, transponders and services from the Web.
|
||||
|
||||
Sources: www.flysat.com, www.lyngsat.com, www.kingofsat.net.
|
||||
Replaces or updates the current satellites.xml file.
|
||||
"""
|
||||
import re
|
||||
@@ -15,10 +43,11 @@ from app.eparser.ecommons import (PLS_MODE, get_key_by_value, FEC, SYSTEM, POLAR
|
||||
Service, CAS)
|
||||
|
||||
_HEADERS = {"User-Agent": "Mozilla/5.0 (Linux x86_64; rv:92.0) Gecko/20100101 Firefox/92.0"}
|
||||
_TIMEOUT = 10
|
||||
|
||||
|
||||
class SatelliteSource(Enum):
|
||||
FLYSAT = ("https://www.flysat.com/satlist.php",)
|
||||
FLYSAT = ("https://www.flysat.com/en/satellitelist",)
|
||||
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",)
|
||||
@@ -38,10 +67,10 @@ class Cell:
|
||||
self._img = img
|
||||
|
||||
def __repr__(self):
|
||||
return "Cell({}, {}, {})".format(self._text, self._url, self._img)
|
||||
return f"Cell({self._text}, {self._url}, {self._img})"
|
||||
|
||||
def __str__(self):
|
||||
return "<Cell(text={}, link={}, img={})>".format(self._text, self._url, self._img)
|
||||
return f"<Cell(text={self._text}, link={self._url}, img={self._img})>"
|
||||
|
||||
def __iter__(self):
|
||||
return (x for x in (self._text, self._url, self._img))
|
||||
@@ -93,6 +122,12 @@ class SatellitesParser(HTMLParser):
|
||||
self._rows = []
|
||||
self._source = source
|
||||
|
||||
self.PLS_MODES = {v: k for k, v in PLS_MODE.items()}
|
||||
self.POLARIZATION = {v: k for k, v in POLARIZATION.items()}
|
||||
self.FEC = {v: k for k, v in FEC.items()}
|
||||
self.SYSTEM = {v: k for k, v in SYSTEM.items()}
|
||||
self.MODULATION = {v: k for k, v in MODULATION.items()}
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == "td":
|
||||
self._is_td = True
|
||||
@@ -134,76 +169,114 @@ class SatellitesParser(HTMLParser):
|
||||
|
||||
for src in SatelliteSource.get_sources(self._source):
|
||||
try:
|
||||
request = requests.get(url=src, headers=_HEADERS)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
log(repr(e))
|
||||
return []
|
||||
resp = requests.get(url=src, headers=_HEADERS, timeout=_TIMEOUT)
|
||||
except requests.exceptions.RequestException as e:
|
||||
log(f"Getting satellite list error: {repr(e)}")
|
||||
else:
|
||||
reason = request.reason
|
||||
reason = resp.reason
|
||||
if reason == "OK":
|
||||
self.feed(request.text)
|
||||
self.feed(resp.text)
|
||||
else:
|
||||
log(reason)
|
||||
log(f"Getting satellite list error: {reason} -> {resp.url}")
|
||||
|
||||
if self._rows:
|
||||
if self._source is SatelliteSource.FLYSAT:
|
||||
def get_sat(r):
|
||||
return r[1], self.parse_position(r[2]), r[3], r[0], False
|
||||
|
||||
return list(map(get_sat, filter(lambda x: all(x) and len(x) == 5, self._rows)))
|
||||
return self.get_satellites_for_fly_sat()
|
||||
elif self._source is SatelliteSource.LYNGSAT:
|
||||
base_url = "https://www.lyngsat.com/"
|
||||
sats = []
|
||||
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
|
||||
return self.get_satellites_for_lyng_sat()
|
||||
elif source is SatelliteSource.KINGOFSAT:
|
||||
def get_sat(r):
|
||||
return r[3], self.parse_position(r[1]), None, r[2], False
|
||||
return self.get_satellites_for_king_of_sat()
|
||||
|
||||
return list(map(get_sat, filter(lambda x: len(x) == 17, self._rows)))
|
||||
return []
|
||||
|
||||
def get_satellite(self, sat):
|
||||
pos = sat[1]
|
||||
return Satellite(name="{} {}".format(pos, sat[0]),
|
||||
flags="0",
|
||||
return Satellite(name=f"{pos} {sat[0]}", flags="0",
|
||||
position=self.get_position(pos.replace(".", "")),
|
||||
transponders=self.get_transponders(sat[3]))
|
||||
|
||||
def get_satellites_for_fly_sat(self):
|
||||
sat_pat = re.compile(r"https://.*/satellite/.+")
|
||||
pos_pat = re.compile(r"https://.*/satellite/position/.+")
|
||||
names = []
|
||||
pos = ""
|
||||
pos_url = ""
|
||||
satellites = []
|
||||
|
||||
def normalize_pos(p):
|
||||
return f"{float(p[:-1])}{p[-1]}" if "." not in p else p
|
||||
|
||||
for row in filter(lambda x: len(x) > 6, self._rows):
|
||||
if re.match(sat_pat, row[1]):
|
||||
row.pop(0)
|
||||
|
||||
if re.match(sat_pat, row[0]) and row[-2]: # r[-2] -> skip EMPTY satellites!
|
||||
if re.match(pos_pat, row[0]):
|
||||
names.clear()
|
||||
pos_url = row[0]
|
||||
name = row[3]
|
||||
pos = normalize_pos(self.parse_position(row[-4]))
|
||||
names.append(name)
|
||||
satellites.append((name, pos, row[-2], row[2], False))
|
||||
|
||||
if len(row) == 7:
|
||||
single_pos = normalize_pos(self.parse_position(row[-4]))
|
||||
name = row[1]
|
||||
if pos == single_pos:
|
||||
names.append(name)
|
||||
else:
|
||||
# Uniting satellites in position.
|
||||
if len(names) > 1:
|
||||
satellites.append(("/".join(names), pos, None, pos_url, False))
|
||||
names.clear()
|
||||
satellites.append((name, single_pos, row[-2], row[0], False))
|
||||
|
||||
return satellites
|
||||
|
||||
def get_satellites_for_lyng_sat(self):
|
||||
base_url = "https://www.lyngsat.com/"
|
||||
sats = []
|
||||
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
|
||||
|
||||
def get_satellites_for_king_of_sat(self):
|
||||
def get_sat(r):
|
||||
return r[3], self.parse_position(r[1]), None, r[2], False
|
||||
|
||||
return list(map(get_sat, filter(lambda x: len(x) == 17, self._rows)))
|
||||
|
||||
@staticmethod
|
||||
def parse_position(pos_str):
|
||||
return "".join(c for c in pos_str if c.isdigit() or c.isalpha() or c == ".")
|
||||
|
||||
@staticmethod
|
||||
def get_position(pos):
|
||||
return "{}{}".format("-" if pos[-1] == "W" else "", pos[:-1])
|
||||
return f"{'-' if pos[-1] == 'W' else ''}{pos[:-1]}"
|
||||
|
||||
def get_transponders(self, sat_url):
|
||||
""" Getting transponders(sorted by frequency). """
|
||||
self._rows.clear()
|
||||
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
|
||||
if self._source is SatelliteSource.KINGOFSAT:
|
||||
sat_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))
|
||||
request = requests.get(url=sat_url, headers=_HEADERS, timeout=_TIMEOUT)
|
||||
except requests.exceptions.RequestException as e:
|
||||
log(f"Getting transponders error: {e}")
|
||||
else:
|
||||
if request.status_code == 200:
|
||||
self.feed(request.text)
|
||||
@@ -214,87 +287,99 @@ class SatellitesParser(HTMLParser):
|
||||
elif self._source is SatelliteSource.KINGOFSAT:
|
||||
self.get_transponders_for_king_of_sat(trs)
|
||||
else:
|
||||
log("SatellitesParser [get transponders] error: {} {}".format(url, request.reason))
|
||||
log(f"SatellitesParser [get transponders] error: {sat_url} {request.reason}")
|
||||
|
||||
return sorted(trs, key=lambda x: int(x.frequency))
|
||||
|
||||
def get_transponders_for_fly_sat(self, trs):
|
||||
""" Parsing transponders for FlySat """
|
||||
pls_pattern = re.compile("(PLS:)+ (Root|Gold|Combo)+ (\\d+)?")
|
||||
is_id_pattern = re.compile("(Stream) (\\d+)")
|
||||
pls_modes = {v: k for k, v in PLS_MODE.items()}
|
||||
""" Parsing transponders for FlySat. """
|
||||
frq_pol_pattern = re.compile(r"(\d{4,5})+\s+([RLHV]).*(DVB-S[2]?)/(.+PSK)?.*")
|
||||
pls_pattern = re.compile(r".*PLS\s+(Root|Gold|Combo)+\s(\d+)?")
|
||||
is_id_pattern = re.compile(r"Stream\s(\d+)")
|
||||
sr_fec_pattern = re.compile(r"(\d{4,5})+\s+(\d+/\d+).*")
|
||||
n_trs = []
|
||||
|
||||
if self._rows:
|
||||
zeros = "000"
|
||||
is_ids = []
|
||||
for r in self._rows:
|
||||
if len(r) == 1:
|
||||
row_len = len(r)
|
||||
if row_len == 1:
|
||||
is_ids.extend(re.findall(is_id_pattern, r[0]))
|
||||
continue
|
||||
if len(r) < 3:
|
||||
if row_len < 12:
|
||||
continue
|
||||
data = r[2].split(" ")
|
||||
if len(data) != 2:
|
||||
continue
|
||||
sr, fec = data
|
||||
data = r[1].split(" ")
|
||||
if len(data) < 3:
|
||||
continue
|
||||
freq, pol, sys = data[0], data[1], data[2]
|
||||
sys = sys.split("/")
|
||||
if len(sys) != 2:
|
||||
continue
|
||||
sys, mod = sys
|
||||
mod = "QPSK" if sys == "DVB-S" else mod
|
||||
|
||||
pls = re.findall(pls_pattern, r[1])
|
||||
freq = re.findall(frq_pol_pattern, r[2])
|
||||
if not freq:
|
||||
continue
|
||||
freq, pol, sys, mod = freq[0]
|
||||
|
||||
sr_fec = re.match(sr_fec_pattern, r[3])
|
||||
if not sr_fec:
|
||||
continue
|
||||
sr, fec = sr_fec.group(1), sr_fec.group(2)
|
||||
|
||||
pls = re.match(pls_pattern, r[2])
|
||||
pls_code = None
|
||||
pls_mode = None
|
||||
|
||||
if pls:
|
||||
pls_code = pls[0][2]
|
||||
pls_mode = pls_modes.get(pls[0][1], None)
|
||||
pls_mode = self.PLS_MODES.get(pls.group(1), None)
|
||||
pls_code = pls.group(2)
|
||||
|
||||
if is_ids:
|
||||
tr = trs.pop()
|
||||
for index, is_id in enumerate(is_ids):
|
||||
tr = tr._replace(is_id=is_id[1])
|
||||
tr = tr._replace(is_id=is_id)
|
||||
if is_transponder_valid(tr):
|
||||
n_trs.append(tr)
|
||||
else:
|
||||
tr = Transponder(freq + zeros, sr + zeros, pol, fec, sys, mod, pls_mode, pls_code, None)
|
||||
if is_transponder_valid(tr):
|
||||
trs.append(tr)
|
||||
|
||||
tr = Transponder(f"{freq}000", f"{sr}000",
|
||||
self.POLARIZATION.get(pol, None),
|
||||
self.FEC.get(fec, None),
|
||||
self.SYSTEM.get(sys, None),
|
||||
self.MODULATION.get(mod, None),
|
||||
pls_mode, pls_code, None, None)
|
||||
if is_transponder_valid(tr):
|
||||
trs.append(tr)
|
||||
|
||||
is_ids.clear()
|
||||
trs.extend(n_trs)
|
||||
|
||||
def get_transponders_for_lyng_sat(self, trs):
|
||||
""" Parsing transponders for LyngSat. """
|
||||
frq_pol_pattern = re.compile("(\\d{4,5})\\s+([RLHV]).*")
|
||||
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_mode, pls_code, pls_id = None, None, None
|
||||
frq_pol_pattern = re.compile(r"(\d{4,5})\s+([RLHV]).*")
|
||||
sr_fec_pattern = re.compile((r"(DVB-S[2]?)\s+(.+PSK)?.*?(\d+)\s+(\d/\d)\s?"
|
||||
r"(?:T2-MI\s+PLP\s+(\d+))?.*"
|
||||
r"?(?:PLS\s+(Root|Gold|Combo)\s+(\d+))?"
|
||||
r"(?:.*Stream\s+(\d+))?.*"))
|
||||
|
||||
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:
|
||||
for freq in row[1], row[2], row[3]:
|
||||
res = re.match(frq_pol_pattern, freq)
|
||||
if res:
|
||||
break
|
||||
if not freq:
|
||||
if not res:
|
||||
continue
|
||||
|
||||
frq, pol = freq.group(1), freq.group(2)
|
||||
srf = " ".join(row[3:5])
|
||||
sr_fec = re.search(sr_fec_pattern, srf)
|
||||
if not sr_fec:
|
||||
freq, pol = res.group(1), res.group(2)
|
||||
res = re.search(sr_fec_pattern, row[3])
|
||||
if not res:
|
||||
continue
|
||||
|
||||
sys, mod, sr, fec = sr_fec.group(1), sr_fec.group(2), sr_fec.group(3), sr_fec.group(4)
|
||||
sys, mod, sr, fec = res.group(1), res.group(2), res.group(3), res.group(4)
|
||||
mod = mod.strip() if mod else "Auto"
|
||||
pls_id = sr_fec.group(5)
|
||||
plp, pls_mode, pls_code, is_id = res.group(5), res.group(6), res.group(7), res.group(8)
|
||||
pls_mode = self.PLS_MODES.get(pls_mode, None)
|
||||
|
||||
tr = Transponder(frq + zeros, sr + zeros, pol, fec, sys, mod, pls_mode, pls_code, pls_id)
|
||||
if plp is not None:
|
||||
log(f"Detected T2-MI transponder! [{freq} {sr} {pol}] ")
|
||||
|
||||
tr = Transponder(f"{freq}000", f"{sr}000",
|
||||
self.POLARIZATION.get(pol, None),
|
||||
self.FEC.get(fec, None),
|
||||
self.SYSTEM.get(sys, None),
|
||||
self.MODULATION.get(mod, None),
|
||||
pls_mode, pls_code, is_id, None)
|
||||
if is_transponder_valid(tr):
|
||||
trs.append(tr)
|
||||
|
||||
@@ -303,19 +388,42 @@ class SatellitesParser(HTMLParser):
|
||||
|
||||
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+)$")
|
||||
sys_pat = re.compile(r"(DVB-S[2]?)\s?(?:T2-MI,\s+PLP\s+(\d+))?.*?(?:PLS:\s+(Root|Gold|Combo)\+(\d+))?")
|
||||
mod_pat = re.compile(r"(.*PSK).*?(?:.*Stream\s+(\d+))?.*")
|
||||
sr_fec_pattern = re.compile(r"(\d{4,5})+\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)
|
||||
freq, pol = row[2].replace(".", "0"), row[3]
|
||||
if not freq.isdigit() or pol not in "VHLR":
|
||||
continue
|
||||
|
||||
tr = Transponder(freq + zeros, sr + zeros, pol, fec, sys, mod, None, pls_code, pls_id)
|
||||
if is_transponder_valid(tr):
|
||||
trs.append(tr)
|
||||
res = re.match(sys_pat, row[8])
|
||||
if not res:
|
||||
continue
|
||||
sys, t2_mi, pls_id, pls_code = res.group(1), res.group(2), res.group(3), res.group(4)
|
||||
pls_id = self.PLS_MODES.get(pls_id, None)
|
||||
|
||||
res = re.match(mod_pat, row[9])
|
||||
if not res:
|
||||
continue
|
||||
mod, is_id = res.group(1), res.group(2)
|
||||
|
||||
res = re.match(sr_fec_pattern, row[10])
|
||||
if not res:
|
||||
continue
|
||||
sr, fec = res.group(1), res.group(2)
|
||||
|
||||
if t2_mi:
|
||||
log(f"Detected T2-MI transponder! [{freq} {sr} {pol}] ")
|
||||
|
||||
tr = Transponder(freq, f"{sr}000",
|
||||
self.POLARIZATION.get(pol, None),
|
||||
self.FEC.get(fec, None),
|
||||
self.SYSTEM.get(sys, None),
|
||||
self.MODULATION.get(mod, None),
|
||||
pls_id, pls_code, is_id, None)
|
||||
if is_transponder_valid(tr):
|
||||
trs.append(tr)
|
||||
|
||||
|
||||
class ServicesParser(HTMLParser):
|
||||
@@ -329,16 +437,27 @@ class ServicesParser(HTMLParser):
|
||||
"MPEG-4": "22", "HEVC SD": "22", "MPEG-4/HD": "25", "MPEG-4 HD": "25", "MPEG-4 HD 1080": "25",
|
||||
"MPEG-4 HD 720": "25", "HEVC HD": "25", "HEVC/HD": "25", "HEVC": "31", "HEVC/UHD": "31",
|
||||
"HEVC UHD": "31", "HEVC UHD 4K": "31", "3": "Data"}
|
||||
self._TR_PAT = re.compile(
|
||||
r".*?(\d+)\s+([RLHV]).*(DVB-S[2]?)/?(.*PSK)?\s(T2-MI)?\s?SR-FEC:\s(\d+)-(\d/\d)\s+.*ONID-TID:\s+(\d+)-(\d+).*")
|
||||
self._POS_PAT = re.compile(r".*?(\d+\.\d°[EW]).*")
|
||||
|
||||
self._TR = "s {}000:{}000:{}:{}:{}:{}:{}:{}"
|
||||
self._S2_TR = "{}:{}:{}:{}"
|
||||
|
||||
self._POS_PAT = re.compile(r".*?(\d+\.\d°[EW]).*")
|
||||
# LyngSat.
|
||||
self._TR_PAT = re.compile((r".*?(\d+)\.?\d?\s+([RLHV]).*(DVB-S[2]?)/?(.*PSK)?\s"
|
||||
r"?(T2-MI)?\s?(PLS\s+Multistream)?\s?"
|
||||
r"SR-FEC:\s(\d+)-(\d/\d)\s+.*ONID-TID:\s+(\d+)-(\d+).*"))
|
||||
|
||||
self._MULTI_PAT = re.compile(r"PLS\s+(Root|Gold|Combo)+\s(\d+)?\s+(?:Stream\s(\d+))")
|
||||
# KingOfSat.
|
||||
self._KING_TR_PAT = re.compile((r"(DVB-S[2]?)\s?(?:T2-MI,\s+PLP\s+(\d+))?.*"
|
||||
r"?(?:PLS:\s+(Root|Gold|Combo)\+(\d+))?"
|
||||
r"\s+(.*PSK).*?(?:.*Stream\s+(\d+))?.*"))
|
||||
|
||||
self._parse_html_entities = entities
|
||||
self._separator = separator
|
||||
self._is_td = False
|
||||
self._is_th = False
|
||||
self._is_mux_div = False
|
||||
self._current_row = []
|
||||
self._current_cell_text = []
|
||||
self._current_cell = Cell()
|
||||
@@ -346,6 +465,7 @@ class ServicesParser(HTMLParser):
|
||||
self._source = source
|
||||
self._t_url = ""
|
||||
self._use_short_names = True
|
||||
self._pls_modes = {v: k for k, v in PLS_MODE.items()}
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
@@ -379,9 +499,11 @@ class ServicesParser(HTMLParser):
|
||||
if a[0] != "title":
|
||||
continue
|
||||
txt = a[1]
|
||||
if txt and txt.startswith("Id: "):
|
||||
sep = "Id: "
|
||||
if txt and txt.startswith(sep):
|
||||
# Saving the 'short' name.
|
||||
self._current_cell.text = txt.lstrip("Id: ")
|
||||
_, sep, name = txt.partition(sep)
|
||||
self._current_cell.text = name
|
||||
elif tag == "img":
|
||||
img_link = attrs[0][1]
|
||||
if self._source is SatelliteSource.LYNGSAT:
|
||||
@@ -389,12 +511,18 @@ class ServicesParser(HTMLParser):
|
||||
self._current_cell.img = img_link
|
||||
elif self._source is SatelliteSource.KINGOFSAT:
|
||||
self._current_cell.img = img_link
|
||||
elif tag == "div" and self._source is SatelliteSource.LYNGSAT:
|
||||
self._is_mux_div = bool(list(filter(lambda at: at[-1] == "mux-header", attrs)))
|
||||
|
||||
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())
|
||||
|
||||
if self._is_mux_div:
|
||||
self._current_cell.url = data.strip()
|
||||
self._is_mux_div = False
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == "td":
|
||||
self._is_td = False
|
||||
@@ -414,21 +542,24 @@ class ServicesParser(HTMLParser):
|
||||
self._current_row = []
|
||||
|
||||
def error(self, message):
|
||||
log("ServicesParser error: {}".format(message))
|
||||
log(f"ServicesParser error: {message}")
|
||||
|
||||
def init_data(self, url):
|
||||
""" Initializes data for the given URL. """
|
||||
if self._source not in (SatelliteSource.LYNGSAT, SatelliteSource.KINGOFSAT):
|
||||
raise ValueError("Unsupported source: {}!".format(self._source.name))
|
||||
raise ValueError(f"Unsupported source: {self._source.name}!")
|
||||
|
||||
self._rows.clear()
|
||||
request = requests.get(url=url, headers=_HEADERS)
|
||||
reason = request.reason
|
||||
|
||||
if reason == "OK":
|
||||
self.feed(request.text)
|
||||
try:
|
||||
request = requests.get(url=url, headers=_HEADERS, timeout=_TIMEOUT)
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise ValueError(e)
|
||||
else:
|
||||
raise ValueError(reason)
|
||||
reason = request.reason
|
||||
if reason == "OK":
|
||||
self.feed(request.text)
|
||||
else:
|
||||
raise ValueError(reason)
|
||||
|
||||
def get_transponders_links(self, sat_url):
|
||||
""" Returns transponder links. """
|
||||
@@ -467,13 +598,13 @@ class ServicesParser(HTMLParser):
|
||||
self.init_data(tr_url)
|
||||
except ValueError as e:
|
||||
log(e)
|
||||
return []
|
||||
else:
|
||||
if self._source is SatelliteSource.LYNGSAT:
|
||||
return self.get_lyngsat_services(sat_position, use_pids)
|
||||
elif self._source is SatelliteSource.KINGOFSAT:
|
||||
return self.get_kingofsat_services(sat_position, use_pids)
|
||||
else:
|
||||
return []
|
||||
return []
|
||||
|
||||
def get_lyngsat_services(self, sat_position=None, use_pids=False):
|
||||
services = []
|
||||
@@ -481,7 +612,10 @@ class ServicesParser(HTMLParser):
|
||||
sys = "DVB-S"
|
||||
pos_found = False
|
||||
tr = None
|
||||
# Transponder
|
||||
# Multi-stream.
|
||||
multi_tr = None
|
||||
multi = False
|
||||
# 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)
|
||||
@@ -490,7 +624,6 @@ class ServicesParser(HTMLParser):
|
||||
|
||||
if not sat_position:
|
||||
pos = self.get_position(pos_tr.group(1))
|
||||
|
||||
pos_found = True
|
||||
|
||||
if pos_found:
|
||||
@@ -498,34 +631,46 @@ class ServicesParser(HTMLParser):
|
||||
td = re.match(self._TR_PAT, text)
|
||||
if td:
|
||||
freq, pol = int(td.group(1)), get_key_by_value(POLARIZATION, td.group(2))
|
||||
if td.group(5):
|
||||
log("Detected T2-MI transponder!")
|
||||
continue
|
||||
|
||||
sys, mod, sr, _fec, = td.group(3), td.group(4), td.group(6), td.group(7)
|
||||
nid, tid = td.group(8), td.group(9)
|
||||
sys, mod, sr, _fec, = td.group(3), td.group(4), td.group(7), td.group(8)
|
||||
nid, tid = td.group(9), td.group(10)
|
||||
sys, mod, fec, nsp, s2_flags, roll_off, pilot, inv = self.get_transponder_data(pos, _fec, sys, mod)
|
||||
nid, tid = int(nid), int(tid)
|
||||
|
||||
if td.group(5):
|
||||
log(f"Detected T2-MI transponder! [{freq} {sr} {pol}]")
|
||||
|
||||
if td.group(6):
|
||||
log(f"Detected multi-stream transponder! [{freq} {sr} {pol}]")
|
||||
multi = True
|
||||
|
||||
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
|
||||
|
||||
if not tr:
|
||||
er = f"Transponder [{freq}] not found or its type [T2-MI, etc] not supported yet."
|
||||
er = f"Transponder [{self._t_url}] not found or its type [T2-MI, etc] not supported yet."
|
||||
log(f"ServicesParser error [get transponder services]: {er}")
|
||||
return services
|
||||
|
||||
# Services
|
||||
for r in filter(lambda x: x and len(x) == 12 and (x[0].text.isdigit()), self._rows):
|
||||
sid, name, s_type, v_pid, a_pid, cas, pkg = r[0].text, r[2].text, r[4].text, r[
|
||||
5].text.strip(), r[6].text.split(), r[9].text, r[10].text.strip()
|
||||
try:
|
||||
s_type = self._S_TYPES.get(s_type, "3") # 3 = Data
|
||||
_s_type = SERVICE_TYPE.get(s_type, SERVICE_TYPE.get("3")) # str repr
|
||||
flags, sid, fav_id, picon_id, data_id = self.get_service_data(s_type, pkg, sid, tid, nid, nsp,
|
||||
v_pid, a_pid, cas, use_pids)
|
||||
services.append(Service(flags, "s", None, name, None, None, pkg, _s_type, r[1].img, picon_id,
|
||||
sid, freq, sr, pol, fec, sys, pos, data_id, fav_id, tr))
|
||||
except ValueError as e:
|
||||
log(f"ServicesParser error [get transponder services]: {e}")
|
||||
# Services.
|
||||
for r in filter(None, self._rows):
|
||||
if multi and r[0].url:
|
||||
res = re.match(self._MULTI_PAT, r[0].url)
|
||||
if res:
|
||||
pls_mode, is_code, is_id = self._pls_modes.get(res.group(1), None), res.group(2), res.group(3)
|
||||
multi_tr = f"{tr}:{is_id}:{is_code}:{pls_mode}" if all((pls_mode, is_code, is_id)) else None
|
||||
tid = int(is_id) if multi_tr else tid
|
||||
|
||||
if len(r) == 12 and r[0].text.isdigit():
|
||||
sid, name, s_type, v_pid, a_pid, cas, pkg = r[0].text, r[2].text, r[4].text, r[
|
||||
5].text.strip(), r[6].text.split(), r[9].text, r[10].text.strip()
|
||||
try:
|
||||
s_type = self._S_TYPES.get(s_type, "3") # 3 = Data
|
||||
_s_type = SERVICE_TYPE.get(s_type, SERVICE_TYPE.get("3")) # str repr
|
||||
flags, sid, fav_id, picon_id, data_id = self.get_service_data(s_type, pkg, sid, tid, nid, nsp,
|
||||
v_pid, a_pid, cas, use_pids)
|
||||
services.append(Service(flags, "s", None, name, None, None, pkg, _s_type, r[1].img, picon_id,
|
||||
sid, freq, sr, pol, fec, sys, pos, data_id, fav_id, multi_tr or tr))
|
||||
except ValueError as e:
|
||||
log(f"ServicesParser error [get transponder services]: {e}")
|
||||
|
||||
return services
|
||||
|
||||
@@ -537,40 +682,60 @@ class ServicesParser(HTMLParser):
|
||||
log(f"ServicesParser error [get transponder services]: Transponder [{self._t_url}] not found!")
|
||||
return services
|
||||
|
||||
tr = tr[0]
|
||||
s_pos, freq, pol, sys, mod, sr_fec = tr[0].text, tr[2].text, tr[3].text, tr[6].text, tr[7].text, tr[8].text
|
||||
tid, nid = tr[10].text, tr[11].text
|
||||
tr, multi_tr, tid, nid, nsp = None, None, None, None, None
|
||||
freq, sr, pol, fec, sys, pos = None, None, None, None, None, None
|
||||
|
||||
pos = sat_position
|
||||
if not sat_position:
|
||||
pos_tr = re.match(self._POS_PAT, s_pos)
|
||||
if pos_tr:
|
||||
pos = self.get_position(pos_tr.group(1))
|
||||
for r in filter(lambda x: len(x) > 12, self._rows):
|
||||
r_size = len(r)
|
||||
if r_size == 13 and r[4].url and r[4].url.startswith("tp.php?tp="):
|
||||
res = re.match(self._KING_TR_PAT, f"{r[6].text} {r[7].text}")
|
||||
if not res:
|
||||
continue
|
||||
|
||||
sr, fec = sr_fec.split()
|
||||
pol = get_key_by_value(POLARIZATION, pol)
|
||||
sys, mod, fec, nsp, s2_flags, roll_off, pilot, inv = self.get_transponder_data(pos, fec, sys, mod)
|
||||
freq, nid, tid = int(float(freq)), int(nid), int(tid)
|
||||
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
|
||||
sys, mod = res.group(1), res.group(5)
|
||||
s_pos, freq, pol, sr_fec = r[0].text, r[2].text, r[3].text, r[8].text
|
||||
nid, tid = r[10].text, r[11].text
|
||||
|
||||
for r in filter(lambda x: len(x) == 14 and not x[1].text and x[7].text and x[7].text.isdigit(), self._rows):
|
||||
if r[1].img == "/radio.gif":
|
||||
s_type = ""
|
||||
elif r[8].img == "/hd.gif":
|
||||
s_type = "HEVC HD"
|
||||
elif r[1].img == "/data.gif":
|
||||
s_type = "Data"
|
||||
else:
|
||||
s_type = "SD"
|
||||
pos = sat_position
|
||||
if not sat_position:
|
||||
pos_tr = re.match(self._POS_PAT, s_pos)
|
||||
if pos_tr:
|
||||
pos = self.get_position(pos_tr.group(1))
|
||||
|
||||
s_type = self._S_TYPES.get(s_type, "3")
|
||||
_s_type = SERVICE_TYPE.get(s_type, SERVICE_TYPE.get("3"))
|
||||
sr, fec = sr_fec.split()
|
||||
pol = get_key_by_value(POLARIZATION, pol)
|
||||
sys, mod, fec, nsp, s2_flags, roll_off, pilot, inv = self.get_transponder_data(pos, fec, sys, mod)
|
||||
freq, nid, tid = int(float(freq)), int(nid), int(tid)
|
||||
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
|
||||
|
||||
name, pkg, cas, sid, v_pid, a_pid = r[2].text, r[5].text, r[6].text, r[7].text, None, None
|
||||
flags, sid, fav_id, picon_id, data_id = self.get_service_data(s_type, pkg, sid, tid, nid, nsp,
|
||||
v_pid, a_pid, cas, use_pids)
|
||||
services.append(Service(flags, "s", None, name, None, None, pkg, _s_type, None, picon_id,
|
||||
sid, str(freq), sr, pol, fec, sys, pos, data_id, fav_id, tr))
|
||||
pls_mode, is_code, is_id = self._pls_modes.get(res.group(3), None), res.group(4), res.group(6)
|
||||
multi_tr = f"{tr}:{is_id}:{is_code}:{pls_mode}" if all((pls_mode, is_code, is_id)) else None
|
||||
tid = int(is_id) if multi_tr else tid
|
||||
|
||||
if res.group(2):
|
||||
log(f"Detected T2-MI transponder! [{freq} {sr}]")
|
||||
|
||||
if multi_tr:
|
||||
log(f"Detected multi-stream transponder! [{freq} {sr}]")
|
||||
|
||||
if tr and r_size == 14 and not r[1].text and r[7].text and r[7].text.isdigit():
|
||||
if r[1].img == "/radio.gif":
|
||||
s_type = ""
|
||||
elif r[8].img == "/hd.gif":
|
||||
s_type = "HEVC HD"
|
||||
elif r[1].img == "/data.gif":
|
||||
s_type = "Data"
|
||||
else:
|
||||
s_type = "SD"
|
||||
|
||||
s_type = self._S_TYPES.get(s_type, "3")
|
||||
_s_type = SERVICE_TYPE.get(s_type, SERVICE_TYPE.get("3"))
|
||||
|
||||
name, pkg, cas, sid, v_pid, a_pid = r[2].text, r[5].text, r[6].text, r[7].text, None, None
|
||||
flags, sid, fav_id, picon_id, data_id = self.get_service_data(s_type, pkg, sid, tid, nid, nsp,
|
||||
v_pid, a_pid, cas, use_pids)
|
||||
services.append(Service(flags, "s", None, name, None, None, pkg, _s_type, None, picon_id,
|
||||
sid, str(freq), sr, pol, fec, sys, pos, data_id, fav_id, multi_tr or tr))
|
||||
|
||||
return services
|
||||
|
||||
@@ -580,7 +745,7 @@ class ServicesParser(HTMLParser):
|
||||
mod = get_key_by_value(MODULATION, mod)
|
||||
fec = get_key_by_value(FEC, fec)
|
||||
# For negative (West) positions: 3600 - numeric position value!!!
|
||||
namespace = "{:04x}0000".format(3600 - pos if pos < 0 else pos)
|
||||
namespace = f"{3600 - pos if pos < 0 else pos:04x}0000"
|
||||
tr_flag = 1
|
||||
roll_off = 0 # 35% DVB-S2/DVB-S (default)
|
||||
pilot = 2 # Auto
|
||||
@@ -592,15 +757,15 @@ class ServicesParser(HTMLParser):
|
||||
@staticmethod
|
||||
def get_service_data(s_type, pkg, sid, tid, nid, namespace, v_pid, a_pid, cas, use_pids=False):
|
||||
sid = int(sid)
|
||||
data_id = "{:04x}:{}:{:04x}:{:04x}:{}:0:0".format(sid, namespace, tid, nid, s_type)
|
||||
fav_id = "{}:{}:{}:{}".format(sid, tid, nid, namespace)
|
||||
picon_id = "1_0_{:X}_{}_{}_{}_{}_0_0_0.png".format(int(s_type), sid, tid, nid, namespace)
|
||||
data_id = f"{sid:04x}:{namespace}:{tid:04x}:{nid:04x}:{s_type}:0:0"
|
||||
fav_id = f"{sid}:{tid}:{nid}:{namespace}"
|
||||
picon_id = f"1_0_{int(s_type):X}_{sid}_{tid}_{nid}_{namespace}_0_0_0.png"
|
||||
# Flags.
|
||||
flags = "p:{}".format(pkg)
|
||||
flags = f"p:{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
|
||||
v_pid = f"c:00{int(v_pid):04x}" if v_pid else None
|
||||
a_pid = ",".join([f"c:01{int(p):04x}" 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)))
|
||||
|
||||
186
app/tools/yt.py
186
app/tools/yt.py
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 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,7 +26,7 @@
|
||||
#
|
||||
|
||||
|
||||
""" Module for working with YouTube service """
|
||||
""" Module for working with YouTube service. """
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
@@ -35,20 +35,21 @@ import shutil
|
||||
import sys
|
||||
from html.parser import HTMLParser
|
||||
from json import JSONDecodeError
|
||||
from urllib import parse
|
||||
from urllib.error import URLError
|
||||
from urllib.parse import unquote
|
||||
from urllib.request import Request, urlopen, urlretrieve
|
||||
|
||||
from app.commons import log
|
||||
from app.settings import SEP
|
||||
from app.ui.uicommons import show_notification
|
||||
|
||||
_TIMEOUT = 5
|
||||
_HEADERS = {"User-Agent": "Mozilla/5.0 (Linux x86_64; rv:92.0) Gecko/20100101 Firefox/92.0",
|
||||
"DNT": "1",
|
||||
"Accept-Encoding": "gzip, deflate"}
|
||||
_YT_PATTERN = re.compile(r"https://www.youtube.com/.+(?:v=)([\w-]{11}).*")
|
||||
_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",
|
||||
"Accept-Encoding": "gzip, deflate"}
|
||||
|
||||
Quality = {137: "1080p", 136: "720p", 135: "480p", 134: "360p",
|
||||
133: "240p", 160: "144p", 0: "0p", 18: "360p", 22: "720p"}
|
||||
@@ -109,6 +110,8 @@ class YouTube:
|
||||
if self._settings.enable_yt_dl and url:
|
||||
if not self._yt_dl:
|
||||
self._yt_dl = YouTubeDL.get_instance(self._settings, self._callback)
|
||||
if not self._yt_dl:
|
||||
raise YouTubeException("youtube-dl initialization error.")
|
||||
return self._yt_dl.get_yt_link(url, skip_errors)
|
||||
|
||||
return self.get_yt_link_by_id(video_id)
|
||||
@@ -119,61 +122,117 @@ class YouTube:
|
||||
|
||||
Returns tuple from the video links dict and title.
|
||||
"""
|
||||
req = Request(YouTube._VIDEO_INFO_LINK.format(video_id), headers=_HEADERS)
|
||||
info = InnerTube().player(video_id)
|
||||
det = info.get("videoDetails", None)
|
||||
title = det.get("title", None) if det else None
|
||||
streaming_data = info.get("streamingData", None)
|
||||
fmts = streaming_data.get("formats", None) if streaming_data else None
|
||||
|
||||
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 fmts:
|
||||
links = {Quality[i["itag"]]: i["url"] for i in filter(
|
||||
lambda i: i.get("itag", -1) in Quality, fmts) if "url" in i}
|
||||
|
||||
if player_resp:
|
||||
try:
|
||||
resp = json.loads(player_resp)
|
||||
except JSONDecodeError as e:
|
||||
log("{}: Parsing player response error: {}".format(__class__.__name__, e))
|
||||
else:
|
||||
det = resp.get("videoDetails", None)
|
||||
title = det.get("title", None) if det else None
|
||||
streaming_data = resp.get("streamingData", None)
|
||||
fmts = streaming_data.get("formats", None) if streaming_data else None
|
||||
if links and title:
|
||||
return links, title.replace("+", " ")
|
||||
|
||||
if fmts:
|
||||
urls = {Quality[i["itag"]]: i["url"] for i in
|
||||
filter(lambda i: i.get("itag", -1) in Quality, fmts) if "url" in i}
|
||||
cause = None
|
||||
status = info.get("playabilityStatus", None)
|
||||
if status:
|
||||
cause = f"[{status.get('status', '')}] {status.get('reason', '')}"
|
||||
|
||||
if urls and title:
|
||||
return urls, title.replace("+", " ")
|
||||
log(f"{__class__.__name__}: Getting link to video with id '{video_id}' filed! Cause: {cause}")
|
||||
|
||||
stream_map = out.get("url_encoded_fmt_stream_map", None)
|
||||
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 = unquote(url) if url else "", title.replace("+", " ") if title else ""
|
||||
if url and title:
|
||||
return {Quality[0]: url}, title.replace("+", " ")
|
||||
|
||||
rsn = out.get("reason", None)
|
||||
rsn = rsn.replace("+", " ") if rsn else ""
|
||||
log("{}: Getting link to video with id {} filed! Cause: {}".format(__class__.__name__, video_id, rsn))
|
||||
|
||||
return None, rsn
|
||||
return None, cause
|
||||
|
||||
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:
|
||||
if not self._yt_dl:
|
||||
raise YouTubeException("youtube-dl is not initialized!")
|
||||
|
||||
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})
|
||||
if self._yt_dl:
|
||||
self._yt_dl.update_options({"noplaylist": True, "extract_flat": False})
|
||||
|
||||
return PlayListParser.get_yt_playlist(list_id)
|
||||
|
||||
|
||||
class InnerTube:
|
||||
""" Object for interacting with the innertube API.
|
||||
|
||||
Based on InnerTube class from pytube [https://github.com/pytube/pytube] project!
|
||||
"""
|
||||
_BASE_URI = "https://www.youtube.com/youtubei/v1"
|
||||
|
||||
_DEFAULT_CLIENTS = {
|
||||
"ANDROID": {
|
||||
"context": {"client": {"clientName": "ANDROID", "clientVersion": "16.20"}},
|
||||
"api_key": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
|
||||
},
|
||||
"ANDROID_EMBED": {
|
||||
"context": {"client": {"clientName": "ANDROID", "clientVersion": "16.20", "clientScreen": "EMBED"}},
|
||||
"api_key": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, client="ANDROID"):
|
||||
""" Initialize an InnerTube object.
|
||||
|
||||
@param client: Client to use for the object. Default to web because it returns the most playback types.
|
||||
"""
|
||||
self.context = self._DEFAULT_CLIENTS[client]["context"]
|
||||
self.api_key = self._DEFAULT_CLIENTS[client]["api_key"]
|
||||
|
||||
@property
|
||||
def base_data(self):
|
||||
"""Return the base json data to transmit to the innertube API."""
|
||||
return {"context": self.context}
|
||||
|
||||
@property
|
||||
def base_params(self):
|
||||
"""Return the base query parameters to transmit to the innertube API."""
|
||||
return {"key": self.api_key, "contentCheckOk": True, "racyCheckOk": True}
|
||||
|
||||
def player(self, video_id):
|
||||
""" Make a request to the player endpoint. Returns raw player info results. """
|
||||
endpoint = f"{self._BASE_URI}/player"
|
||||
query = {"videoId": video_id}
|
||||
query.update(self.base_params)
|
||||
return self._call_api(endpoint, query, self.base_data) or {}
|
||||
|
||||
@staticmethod
|
||||
def _call_api(endpoint, query, data):
|
||||
""" Make a request to a given endpoint with the provided query parameters and data."""
|
||||
headers = {"Content-Type": "application/json", }
|
||||
response = InnerTube._execute(f"{endpoint}?{parse.urlencode(query)}", "POST", headers=headers, data=data)
|
||||
|
||||
try:
|
||||
resp = json.loads(response.read())
|
||||
except JSONDecodeError as e:
|
||||
log(f"{__class__.__name__}: Parsing response error: {e}")
|
||||
else:
|
||||
return resp
|
||||
|
||||
@staticmethod
|
||||
def _execute(url, method=None, headers=None, data=None, timeout=_TIMEOUT):
|
||||
base_headers = {"User-Agent": "Mozilla/5.0", "accept-language": "en-US,en"}
|
||||
if headers:
|
||||
base_headers.update(headers)
|
||||
if data:
|
||||
# Encoding data for request.
|
||||
if not isinstance(data, bytes):
|
||||
data = bytes(json.dumps(data), encoding="utf-8")
|
||||
return urlopen(Request(url, headers=base_headers, method=method, data=data), timeout=timeout)
|
||||
|
||||
|
||||
class PlayListParser(HTMLParser):
|
||||
""" Very simple parser to handle YouTube playlist pages. """
|
||||
|
||||
@@ -200,7 +259,7 @@ class PlayListParser(HTMLParser):
|
||||
try:
|
||||
resp = json.loads(data)
|
||||
except JSONDecodeError as e:
|
||||
log("{}: Parsing data error: {}".format(__class__.__name__, e))
|
||||
log(f"{__class__.__name__}: Parsing data error: {e}")
|
||||
else:
|
||||
sb = resp.get("sidebar", None)
|
||||
if sb:
|
||||
@@ -218,7 +277,7 @@ class PlayListParser(HTMLParser):
|
||||
self._is_script = False
|
||||
|
||||
def error(self, message):
|
||||
log("{} Parsing error: {}".format(__class__.__name__, message))
|
||||
log(f"{__class__.__name__} Parsing error: {message}")
|
||||
|
||||
@property
|
||||
def header(self):
|
||||
@@ -234,9 +293,9 @@ class PlayListParser(HTMLParser):
|
||||
|
||||
returns tuple from the playlist header and list of tuples (title, video id)
|
||||
"""
|
||||
request = Request("https://www.youtube.com/playlist?list={}&hl=en".format(play_list_id), headers=_HEADERS)
|
||||
request = Request(f"https://www.youtube.com/playlist?list={play_list_id}&hl=en", headers=_HEADERS)
|
||||
|
||||
with urlopen(request, timeout=2) as resp:
|
||||
with urlopen(request, timeout=_TIMEOUT) as resp:
|
||||
data = gzip.decompress(resp.read()).decode("utf-8")
|
||||
parser = PlayListParser()
|
||||
parser.feed(data)
|
||||
@@ -259,7 +318,7 @@ class YouTubeDL:
|
||||
"cookiefile": "cookies.txt"} # File name where cookies should be read from and dumped to.
|
||||
|
||||
def __init__(self, settings, callback):
|
||||
self._path = "{}tools{}".format(settings.default_data_path, SEP)
|
||||
self._path = f"{settings.default_data_path}tools{SEP}"
|
||||
self._update = settings.enable_yt_dl_update
|
||||
self._supported = {"22", "18"}
|
||||
self._dl = None
|
||||
@@ -276,7 +335,7 @@ class YouTubeDL:
|
||||
return cls._DL_INSTANCE
|
||||
|
||||
def init(self):
|
||||
if not os.path.isfile("{}youtube_dl{}version.py".format(self._path, SEP)):
|
||||
if not os.path.isfile(f"{self._path}youtube_dl{SEP}version.py"):
|
||||
self.get_latest_release()
|
||||
|
||||
if self._path not in sys.path:
|
||||
@@ -288,17 +347,22 @@ class YouTubeDL:
|
||||
try:
|
||||
import youtube_dl
|
||||
except ModuleNotFoundError as e:
|
||||
log("YouTubeDLHelper error: {}".format(str(e)))
|
||||
log(f"YouTubeDLHelper error: {e}")
|
||||
raise YouTubeException(e)
|
||||
except ImportError as e:
|
||||
log("YouTubeDLHelper error: {}".format(str(e)))
|
||||
log(f"YouTubeDLHelper error: {e}")
|
||||
else:
|
||||
if self._path not in youtube_dl.__file__:
|
||||
msg = "Another version of youtube-dl was found on your system!"
|
||||
log(msg)
|
||||
raise YouTubeException(msg)
|
||||
|
||||
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)
|
||||
msg = f"youtube-dl has new release!\nCurrent: {cur_ver}. Last: {l_ver}."
|
||||
show_notification(msg)
|
||||
log(msg)
|
||||
self._callback(msg, False)
|
||||
@@ -318,7 +382,7 @@ class YouTubeDL:
|
||||
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))
|
||||
log(f"YouTubeDLHelper error [get last release id]: {e}")
|
||||
|
||||
def get_latest_release(self):
|
||||
try:
|
||||
@@ -329,6 +393,9 @@ class YouTubeDL:
|
||||
r = json.loads(resp.read().decode("utf-8"))
|
||||
zip_url = r.get("zipball_url", None)
|
||||
if zip_url:
|
||||
if os.path.isdir(self._path):
|
||||
shutil.rmtree(self._path)
|
||||
|
||||
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)
|
||||
@@ -336,25 +403,22 @@ class YouTubeDL:
|
||||
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.move(info.filename, f"{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
|
||||
|
||||
if os.path.isfile(zip_file):
|
||||
os.remove(zip_file)
|
||||
return True
|
||||
except URLError as e:
|
||||
log("YouTubeDLHelper error: {}".format(e))
|
||||
log(f"YouTubeDLHelper error: {e}")
|
||||
raise YouTubeException(e)
|
||||
finally:
|
||||
self._is_update_process = False
|
||||
@@ -377,10 +441,10 @@ class YouTubeDL:
|
||||
try:
|
||||
return self._dl.extract_info(url, download=False)
|
||||
except URLError as e:
|
||||
log(str(e))
|
||||
log(f"YouTubeDLHelper error [get info]: {e}")
|
||||
raise YouTubeException(e)
|
||||
except self._DownloadError as e:
|
||||
log(str(e))
|
||||
log(f"YouTubeDLHelper error [get info]: {e}")
|
||||
if not skip_errors:
|
||||
raise YouTubeException(e)
|
||||
|
||||
|
||||
@@ -58,10 +58,19 @@
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">FTP-transfer</attribute>
|
||||
<attribute name="action">app.on_download</attribute>
|
||||
</item>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Download from the receiver</attribute>
|
||||
<attribute name="action">app.on_receive</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Transfer to receiver</attribute>
|
||||
<attribute name="action">app.on_send</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
@@ -133,6 +142,21 @@
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Display picons</attribute>
|
||||
<attribute name="action">app.display_picons</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Display EPG in bouquet list</attribute>
|
||||
<attribute name="action">app.display_epg</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Alternate layout</attribute>
|
||||
<attribute name="action">app.set_alternate_layout</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu id="tools_menu">
|
||||
<attribute name="label" translatable="yes">Tools</attribute>
|
||||
@@ -260,10 +284,19 @@
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">FTP-transfer</attribute>
|
||||
<attribute name="action">app.on_download</attribute>
|
||||
</item>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Download from the receiver</attribute>
|
||||
<attribute name="action">app.on_receive</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Transfer to receiver</attribute>
|
||||
<attribute name="action">app.on_send</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
@@ -323,6 +356,21 @@
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Display picons</attribute>
|
||||
<attribute name="action">app.display_picons</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Display EPG in bouquet list</attribute>
|
||||
<attribute name="action">app.display_epg</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Alternate layout</attribute>
|
||||
<attribute name="action">app.set_alternate_layout</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Tools</attribute>
|
||||
@@ -368,7 +416,7 @@
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Export to m3u</attribute>
|
||||
<attribute name="action">app.on_export_to_m3u</attribute>
|
||||
<attribute name="action">app.on_export_iptv_to_m3u</attribute>
|
||||
</item>
|
||||
<section>
|
||||
<item>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -33,12 +33,13 @@ import time
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
from app.commons import run_idle
|
||||
from app.commons import run_idle, get_size_from_bytes
|
||||
from app.settings import SettingsType, SEP
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_builder
|
||||
from app.ui.main_helper import append_text_to_tview
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, IS_GNOME_SESSION
|
||||
|
||||
|
||||
class RestoreType(Enum):
|
||||
@@ -63,7 +64,7 @@ class BackupDialog:
|
||||
self._settings = settings
|
||||
self._s_type = settings.setting_type
|
||||
self._data_path = self._settings.profile_data_path
|
||||
self._backup_path = self._settings.profile_backup_path or "{}backup{}".format(self._data_path, os.sep)
|
||||
self._backup_path = self._settings.profile_backup_path or f"{self._data_path}backup{os.sep}"
|
||||
self._open_data_callback = callback
|
||||
self._dialog_window = builder.get_object("dialog_window")
|
||||
self._dialog_window.set_transient_for(transient)
|
||||
@@ -74,6 +75,25 @@ class BackupDialog:
|
||||
self._info_check_button = builder.get_object("info_check_button")
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("message_label")
|
||||
self._file_count_label = builder.get_object("file_count_label")
|
||||
|
||||
if IS_GNOME_SESSION:
|
||||
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True)
|
||||
self._dialog_window.set_titlebar(header_bar)
|
||||
|
||||
button_box = builder.get_object("main_button_box")
|
||||
button_box.set_margin_top(0)
|
||||
button_box.set_margin_bottom(0)
|
||||
button_box.set_margin_left(0)
|
||||
button_box.reparent(header_bar)
|
||||
|
||||
ch_button = builder.get_object("info_check_button")
|
||||
ch_button.set_margin_right(0)
|
||||
h_bar = builder.get_object("header_bar")
|
||||
h_bar.remove(ch_button)
|
||||
h_bar.set_visible(False)
|
||||
header_bar.pack_end(ch_button)
|
||||
|
||||
# Setting the last size of the dialog window if it was saved
|
||||
window_size = self._settings.get("backup_tool_window_size")
|
||||
if window_size:
|
||||
@@ -88,10 +108,14 @@ class BackupDialog:
|
||||
def init_data(self):
|
||||
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))
|
||||
p = Path(os.path.join(self._backup_path, file))
|
||||
if p.is_file():
|
||||
self._model.append((p.stem, get_size_from_bytes(p.stat().st_size)))
|
||||
else:
|
||||
os.makedirs(os.path.dirname(self._backup_path), exist_ok=True)
|
||||
|
||||
self._file_count_label.set_text(str(len(self._model)))
|
||||
|
||||
def on_restore_bouquets(self, item):
|
||||
self.restore(RestoreType.BOUQUETS)
|
||||
|
||||
@@ -111,13 +135,15 @@ class BackupDialog:
|
||||
try:
|
||||
for itr in map(model.get_iter, paths):
|
||||
file_name = model.get_value(itr, 0)
|
||||
os.remove("{}{}{}".format(self._backup_path, file_name, ".zip"))
|
||||
os.remove(f"{self._backup_path}{file_name}.zip")
|
||||
itrs_to_delete.append(itr)
|
||||
except FileNotFoundError as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
else:
|
||||
list(map(model.remove, itrs_to_delete))
|
||||
|
||||
self._file_count_label.set_text(str(len(self._model)))
|
||||
|
||||
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)
|
||||
@@ -147,7 +173,7 @@ class BackupDialog:
|
||||
file_name = self._backup_path + model.get_value(model.get_iter(paths[0]), 0) + ".zip"
|
||||
created = time.ctime(os.path.getctime(file_name))
|
||||
self._text_view.get_buffer().set_text(
|
||||
"Created: {}\n********** Files: **********\n".format(created))
|
||||
f"Created: {created}\n********** Files: **********\n")
|
||||
with zipfile.ZipFile(file_name) as zip_file:
|
||||
for name in zip_file.namelist():
|
||||
append_text_to_tview(name + "\n", self._text_view)
|
||||
@@ -216,15 +242,15 @@ def backup_data(path, backup_path, move=True):
|
||||
|
||||
Returns full path to the compressed file.
|
||||
"""
|
||||
backup_path = "{}{}{}".format(backup_path, datetime.now().strftime("%Y-%m-%d_%H-%M-%S"), SEP)
|
||||
backup_path = f"{backup_path}{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}{SEP}"
|
||||
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
# backup files in data dir(skipping dirs and satellites.xml)
|
||||
# Backup files in data dir(skipping dirs and satellites.xml).
|
||||
for file in filter(lambda f: f != "satellites.xml" and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
|
||||
src, dst = os.path.join(path, file), backup_path + file
|
||||
shutil.move(src, dst) if move else shutil.copy(src, dst)
|
||||
# compressing to zip and delete remaining files
|
||||
zip_file = shutil.make_archive(backup_path, "zip", backup_path)
|
||||
# Compressing to zip and delete remaining files.
|
||||
zip_file = shutil.make_archive(backup_path.rstrip(SEP), "zip", backup_path)
|
||||
shutil.rmtree(backup_path)
|
||||
|
||||
return zip_file
|
||||
|
||||
@@ -41,10 +41,10 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
<object class="GtkListStore" id="main_list_store">
|
||||
<columns>
|
||||
<!-- column-name date -->
|
||||
<!-- column-name name -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name size -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name selected -->
|
||||
<column type="gboolean"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkMenu" id="popup_menu">
|
||||
@@ -124,7 +124,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="header_bar">
|
||||
<property name="visible">True</property>
|
||||
@@ -199,16 +198,23 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="info_check_button">
|
||||
<property name="label" translatable="yes">Details</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Details</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_right">15</property>
|
||||
<property name="image">details_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="details_image1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">emblem-important-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
</child>
|
||||
<accelerator key="i" signal="clicked" modifiers="Primary"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -229,83 +235,170 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkPaned" id="main_paned">
|
||||
<object class="GtkFrame" id="main_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">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="wide_handle">True</property>
|
||||
<property name="label_xalign">0</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="main_view_scrolled_window">
|
||||
<object class="GtkPaned" id="main_paned">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</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="GtkTreeView" id="main_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="model">main_list_store</property>
|
||||
<property name="headers_visible">False</property>
|
||||
<property name="search_column">0</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<property name="activate_on_single_click">True</property>
|
||||
<signal name="button-press-event" handler="on_view_popup_menu" object="popup_menu" swapped="no"/>
|
||||
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
|
||||
<signal name="key-release-event" handler="on_key_release" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="main_view_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="backup_date_column">
|
||||
<property name="title" translatable="yes">Backup</property>
|
||||
<property name="clickable">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="date_render">
|
||||
<property name="xpad">10</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</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="text_view_scrolled_window">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="text_view">
|
||||
<object class="GtkBox" id="backups_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="pixels_above_lines">5</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="left_margin">10</property>
|
||||
<property name="right_margin">10</property>
|
||||
<property name="indent">10</property>
|
||||
<property name="cursor_visible">False</property>
|
||||
<property name="accepts_tab">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="main_view_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="main_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="model">main_list_store</property>
|
||||
<property name="search_column">0</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<property name="activate_on_single_click">True</property>
|
||||
<signal name="button-press-event" handler="on_view_popup_menu" object="popup_menu" swapped="no"/>
|
||||
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
|
||||
<signal name="key-release-event" handler="on_key_release" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="main_view_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="backup_name_column">
|
||||
<property name="min_width">75</property>
|
||||
<property name="title" translatable="yes">Name</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="clickable">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="name_renderer">
|
||||
<property name="xpad">10</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="backup_size_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="fixed_width">120</property>
|
||||
<property name="title" translatable="yes">Size</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="size_renderer">
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="status_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">2</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="count_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">document-properties</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="file_count_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
<property name="width_chars">4</property>
|
||||
<property name="xalign">0</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">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="text_view_scrolled_window">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="text_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="pixels_above_lines">5</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="left_margin">10</property>
|
||||
<property name="right_margin">10</property>
|
||||
<property name="indent">10</property>
|
||||
<property name="cursor_visible">False</property>
|
||||
<property name="accepts_tab">False</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child type="label_item">
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
|
||||
2737
app/ui/control.glade
2737
app/ui/control.glade
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 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
|
||||
@@ -28,766 +28,15 @@
|
||||
|
||||
""" Receiver control module via HTTP API. """
|
||||
import os
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from ftplib import all_errors
|
||||
from urllib.parse import quote
|
||||
import re
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from .dialogs import get_builder, show_dialog, DialogType, get_message
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Page, Column, KeyboardKey
|
||||
from .dialogs import get_builder, get_message
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH
|
||||
from ..commons import run_task, run_with_delay, log, run_idle
|
||||
from ..connections import HttpAPI, UtfFTP
|
||||
from ..eparser.ecommons import BqServiceType
|
||||
from ..settings import IS_DARWIN, PlayStreamsMode, IS_LINUX, IS_WIN
|
||||
|
||||
|
||||
class EpgTool(Gtk.Box):
|
||||
def __init__(self, app, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._app = app
|
||||
self._app.connect("fav-changed", self.on_service_changed)
|
||||
|
||||
handlers = {"on_epg_press": self.on_epg_press,
|
||||
"on_timer_add": self.on_timer_add,
|
||||
"on_epg_filter_changed": self.on_epg_filter_changed,
|
||||
"on_epg_filter_toggled": self.on_epg_filter_toggled}
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers,
|
||||
objects=("epg_frame", "epg_model", "epg_filter_model", "epg_sort_model"))
|
||||
self._view = builder.get_object("epg_view")
|
||||
self._model = builder.get_object("epg_model")
|
||||
self._filter_model = builder.get_object("epg_filter_model")
|
||||
self._filter_model.set_visible_func(self.epg_filter_function)
|
||||
self._filter_entry = builder.get_object("epg_filter_entry")
|
||||
builder.get_object("epg_filter_button").bind_property("active", self._filter_entry, "visible")
|
||||
self.pack_start(builder.get_object("epg_frame"), True, True, 0)
|
||||
self.show()
|
||||
|
||||
def on_timer_add(self, action=None, value=None):
|
||||
model, paths = self._view.get_selection().get_selected_rows()
|
||||
p_count = len(paths)
|
||||
|
||||
if p_count == 1:
|
||||
dialog = TimerTool.TimerDialog(self._app.app_window, TimerTool.TimerAction.EVENT, model[paths][-1])
|
||||
response = dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
gen = self.write_timers_list([dialog.get_request()])
|
||||
GLib.idle_add(lambda: next(gen, False))
|
||||
dialog.destroy()
|
||||
elif p_count > 1:
|
||||
if show_dialog(DialogType.QUESTION, self._app.app_window,
|
||||
"Add timers for selected events?") != Gtk.ResponseType.OK:
|
||||
return True
|
||||
|
||||
self.add_timers_list((model[p][-1] for p in paths))
|
||||
else:
|
||||
self._app.show_error_message("No selected item!")
|
||||
|
||||
def add_timers_list(self, paths):
|
||||
ref_str = "timeraddbyeventid?sRef={}&eventid={}&justplay=0"
|
||||
refs = [ref_str.format(ev.get("e2eventservicereference", ""), ev.get("e2eventid", "")) for ev in paths]
|
||||
|
||||
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._app.send_http_request(HttpAPI.Request.TIMER, ref, lambda x: tasks.pop())
|
||||
yield True
|
||||
|
||||
while tasks:
|
||||
yield True
|
||||
|
||||
self._app.emit("change-page", Page.TIMERS.value)
|
||||
|
||||
def on_epg_press(self, view, event):
|
||||
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(view.get_model()) > 0:
|
||||
self.on_timer_add()
|
||||
|
||||
def on_service_changed(self, app, ref):
|
||||
self._app.wait_dialog.show()
|
||||
self._app.send_http_request(HttpAPI.Request.EPG, quote(ref), self.update_epg_data)
|
||||
|
||||
@run_idle
|
||||
def update_epg_data(self, epg):
|
||||
self._model.clear()
|
||||
list(map(self._model.append, (self.get_event_row(e) for e in epg.get("event_list", []))))
|
||||
self._app.wait_dialog.hide()
|
||||
|
||||
def get_event_row(self, event):
|
||||
title = event.get("e2eventtitle", "") or ""
|
||||
desc = event.get("e2eventdescription", "") or ""
|
||||
|
||||
start = int(event.get("e2eventstart", "0"))
|
||||
start_time = datetime.fromtimestamp(start)
|
||||
end_time = datetime.fromtimestamp(start + int(event.get("e2eventduration", "0")))
|
||||
time = "{} - {}".format(start_time.strftime("%A, %H:%M"), end_time.strftime("%H:%M"))
|
||||
|
||||
return title, time, desc, event
|
||||
|
||||
def on_epg_filter_changed(self, entry):
|
||||
self._filter_model.refilter()
|
||||
|
||||
def on_epg_filter_toggled(self, button):
|
||||
if not button.get_active():
|
||||
self._filter_entry.set_text("")
|
||||
|
||||
def epg_filter_function(self, model, itr, data):
|
||||
txt = self._filter_entry.get_text().upper()
|
||||
return next((s for s in model.get(itr, 0, 1, 2) if txt in s.upper()), False)
|
||||
|
||||
|
||||
class TimerTool(Gtk.Box):
|
||||
TIME_STR = "%Y-%m-%d %H:%M"
|
||||
|
||||
ACTION = {"0": "Record", "1": "Zap"}
|
||||
|
||||
AFTER_EVENT = {"0": "Do Nothing",
|
||||
"1": "Standby",
|
||||
"2": "Shut down",
|
||||
"3": "Auto"}
|
||||
|
||||
class TimerAction(Enum):
|
||||
ADD = 0
|
||||
EVENT = 1
|
||||
CHANGE = 2
|
||||
|
||||
class TimerDialog(Gtk.Dialog):
|
||||
def __init__(self, parent, action=None, timer_data=None, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._action = action or TimerTool.TimerAction.ADD
|
||||
self._timer_data = timer_data or {}
|
||||
self._request = ""
|
||||
|
||||
handlers = {"on_timer_begins_set": self.on_timer_begins_set,
|
||||
"on_timer_ends_set": self.on_timer_ends_set}
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers,
|
||||
objects=("timer_dialog_frame", "timer_ends_popover", "end_hour_adjustment",
|
||||
"min_end_adjustment", "timer_begins_popover", "begins_hour_adjustment",
|
||||
"min_begins_adjustment"))
|
||||
|
||||
self.set_title(get_message("Timer"))
|
||||
self.set_modal(True)
|
||||
self.set_skip_pager_hint(True)
|
||||
self.set_skip_taskbar_hint(True)
|
||||
self.set_transient_for(parent)
|
||||
self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
|
||||
self.set_resizable(False)
|
||||
|
||||
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._days_buttons = (builder.get_object("timer_mo_check_button"),
|
||||
builder.get_object("timer_tu_check_button"),
|
||||
builder.get_object("timer_we_check_button"),
|
||||
builder.get_object("timer_th_check_button"),
|
||||
builder.get_object("timer_fr_check_button"),
|
||||
builder.get_object("timer_sa_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()
|
||||
|
||||
self.add_buttons(get_message("Cancel"), Gtk.ResponseType.CLOSE, get_message("Save"), Gtk.ResponseType.OK)
|
||||
self.get_content_area().pack_start(builder.get_object("timer_dialog_frame"), True, True, 5)
|
||||
|
||||
if self._action is TimerTool.TimerAction.ADD:
|
||||
self.set_timer_for_add()
|
||||
elif self._action is TimerTool.TimerAction.CHANGE:
|
||||
self.set_timer_for_edit()
|
||||
elif self._action is TimerTool.TimerAction.EVENT:
|
||||
self.set_timer_from_event_data()
|
||||
else:
|
||||
log("{} error: No action set for timer!".format(__class__.__name__))
|
||||
|
||||
@property
|
||||
def request(self):
|
||||
return self._request
|
||||
|
||||
def run(self):
|
||||
resp = super().run()
|
||||
if resp == Gtk.ResponseType.OK:
|
||||
self._request = self.get_request()
|
||||
return resp
|
||||
|
||||
def get_request(self):
|
||||
""" Constructs str representation of add/update request. """
|
||||
args = []
|
||||
t_data = self.get_timer_data()
|
||||
s_ref = quote(t_data.get("sRef", ""))
|
||||
|
||||
if self._action is TimerTool.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._action is TimerTool.TimerAction.ADD:
|
||||
args.append("timeradd?sRef={}".format(s_ref))
|
||||
args.append("deleteOldOnSave={}".format(0))
|
||||
elif self._action is TimerTool.TimerAction.CHANGE:
|
||||
args.append("timerchange?sRef={}".format(s_ref))
|
||||
args.append("channelOld={}".format(s_ref))
|
||||
args.append("beginOld={}".format(self._timer_data.get("e2timebegin", "0")))
|
||||
args.append("endOld={}".format(self._timer_data.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(TimerTool.get_repetition_flags(self._days_buttons)))
|
||||
|
||||
if self._timer_location_switch.get_active():
|
||||
args.append("dirname={}".format(self._timer_location_entry.get_text()))
|
||||
|
||||
return "&".join(args)
|
||||
|
||||
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_for_add(self):
|
||||
self._timer_service_entry.set_text(self._timer_data.get("e2servicename", ""))
|
||||
self._timer_service_ref_entry.set_text(self._timer_data.get("e2servicereference", ""))
|
||||
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)
|
||||
TimerTool.set_repetition_flags(0, self._days_buttons)
|
||||
|
||||
def set_timer_for_edit(self):
|
||||
self._timer_name_entry.set_text(self._timer_data.get("e2name", ""))
|
||||
self._timer_desc_entry.set_text(self._timer_data.get("e2description", "") or "")
|
||||
self._timer_service_entry.set_text(self._timer_data.get("e2servicename", "") or "")
|
||||
self._timer_service_ref_entry.set_text(self._timer_data.get("e2servicereference", ""))
|
||||
self._timer_event_id_entry.set_text(self._timer_data.get("e2eit", ""))
|
||||
self._timer_enabled_switch.set_active((self._timer_data.get("e2disabled", "0") == "0"))
|
||||
self._timer_action_combo_box.set_active_id(self._timer_data.get("e2justplay", "0"))
|
||||
self._timer_after_combo_box.set_active_id(self._timer_data.get("e2afterevent", "0"))
|
||||
self.set_time_data(int(self._timer_data.get("e2timebegin", "0")),
|
||||
int(self._timer_data.get("e2timeend", "0")))
|
||||
location = self._timer_data.get("e2location", "")
|
||||
self._timer_location_entry.set_text("" if location == "None" else location)
|
||||
TimerTool.set_repetition_flags(int(self._timer_data.get("e2repeated", "0")), self._days_buttons)
|
||||
|
||||
def set_timer_from_event_data(self):
|
||||
self._timer_name_entry.set_text(self._timer_data.get("e2eventtitle", ""))
|
||||
self._timer_desc_entry.set_text(self._timer_data.get("e2eventdescription", ""))
|
||||
self._timer_service_entry.set_text(self._timer_data.get("e2eventservicename", ""))
|
||||
self._timer_service_ref_entry.set_text(self._timer_data.get("e2eventservicereference", ""))
|
||||
self._timer_event_id_entry.set_text(self._timer_data.get("e2eventid", ""))
|
||||
self._timer_action_combo_box.set_active_id("1")
|
||||
self._timer_after_combo_box.set_active_id("3")
|
||||
start_time = int(self._timer_data.get("e2eventstart", "0"))
|
||||
self.set_time_data(start_time, start_time + int(self._timer_data.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(TimerTool.TIME_STR))
|
||||
self._timer_ends_entry.set_text(ev_time_end.strftime(TimerTool.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(), TimerTool.TIME_STR).timestamp()),
|
||||
"end": int(datetime.strptime(self._timer_ends_entry.get_text(), TimerTool.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": TimerTool.get_repetition_flags(self._days_buttons)}
|
||||
|
||||
def __init__(self, app, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._app = app
|
||||
self._app.connect("page-changed", self.update_timer_list)
|
||||
# Icon.
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
icon = "alarm-symbolic"
|
||||
self._icon = theme.load_icon(icon, 16, 0) if theme.lookup_icon(icon, 16, 0) else None
|
||||
|
||||
handlers = {"on_timer_add": self.on_timer_add,
|
||||
"on_timer_edit": self.on_timer_edit,
|
||||
"on_timer_remove": self.on_timer_remove,
|
||||
"on_timers_press": self.on_timers_press,
|
||||
"on_timers_key_press": self.on_timers_key_press,
|
||||
"on_timer_cursor_changed": self.on_timer_cursor_changed,
|
||||
"on_timers_drag_data_received": self.on_timers_drag_data_received}
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers, objects=("timers_frame", "timer_model"))
|
||||
self._view = builder.get_object("timer_view")
|
||||
self._remove_button = builder.get_object("timer_remove_button")
|
||||
self._remove_button.bind_property("sensitive", builder.get_object("timer_edit_button"), "sensitive")
|
||||
self._info_button = builder.get_object("timer_info_check_button")
|
||||
self._info_button.bind_property("active", builder.get_object("timer_info_frame"), "visible")
|
||||
self._info_enabled_switch = builder.get_object("timer_info_enabled_switch")
|
||||
self._ref_info_label = builder.get_object("timer_ref_value_label")
|
||||
self._event_id_info_label = builder.get_object("timer_event_id_value_label")
|
||||
self._begins_info_label = builder.get_object("timer_begins_value_label")
|
||||
self._ends_info_label = builder.get_object("timer_ends_value_label")
|
||||
self._action_info_label = builder.get_object("timer_action_value_label")
|
||||
self._after_info_label = builder.get_object("timer_after_value_label")
|
||||
self._timer_location_switch = builder.get_object("timer_location_switch")
|
||||
self._info_location_entry = builder.get_object("timer_info_location_entry")
|
||||
self._days_buttons = (builder.get_object("timer_info_mo_check_button"),
|
||||
builder.get_object("timer_info_tu_check_button"),
|
||||
builder.get_object("timer_info_we_check_button"),
|
||||
builder.get_object("timer_info_th_check_button"),
|
||||
builder.get_object("timer_info_fr_check_button"),
|
||||
builder.get_object("timer_info_sa_check_button"),
|
||||
builder.get_object("timer_info_su_check_button"))
|
||||
# Disable button presses.
|
||||
list(map(lambda b: b.connect("button-press-event", lambda bx, e: True), self._days_buttons))
|
||||
self._info_enabled_switch.connect("button-press-event", lambda b, e: True)
|
||||
# DnD initialization for the timer list.
|
||||
self._view.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
|
||||
self._view.drag_dest_add_text_targets()
|
||||
|
||||
self.pack_start(builder.get_object("timers_frame"), True, True, 0)
|
||||
self.show()
|
||||
|
||||
def update_timer_list(self, app, page):
|
||||
if page is Page.TIMERS:
|
||||
self._app.wait_dialog.show()
|
||||
self._app.send_http_request(HttpAPI.Request.TIMER_LIST, "", self.update_timers_data)
|
||||
|
||||
@run_idle
|
||||
def update_timers_data(self, timers):
|
||||
model = self._view.get_model()
|
||||
model.clear()
|
||||
list(map(model.append, (self.get_timer_row(t) for t in timers.get("timer_list", []))))
|
||||
self._remove_button.set_sensitive(len(model))
|
||||
self._app.wait_dialog.hide()
|
||||
|
||||
def get_timer_row(self, timer):
|
||||
disabled = self._icon if timer.get("e2disabled", "0") == "0" else None
|
||||
name = timer.get("e2name", "") or ""
|
||||
description = timer.get("e2description", "") or ""
|
||||
service = timer.get("e2servicename", "") or ""
|
||||
start_time = datetime.fromtimestamp(int(timer.get("e2timebegin", "0")))
|
||||
end_time = datetime.fromtimestamp(int(timer.get("e2timeend", "0")))
|
||||
time = "{} - {}".format(start_time.strftime("%A, %H:%M"), end_time.strftime("%H:%M"))
|
||||
|
||||
return disabled, name, service, time, description, timer
|
||||
|
||||
def on_timer_add(self, timer=None, value=None):
|
||||
model, paths = self._app.fav_view.get_selection().get_selected_rows()
|
||||
p_count = len(paths)
|
||||
|
||||
if p_count == 1:
|
||||
service = self._app.current_services.get(model[paths][Column.FAV_ID], None)
|
||||
if service:
|
||||
self.add_timer({"e2servicename": service.service,
|
||||
"e2servicereference": service.picon_id.rstrip(".png").replace("_", ":")})
|
||||
elif p_count > 1:
|
||||
self._app.show_error_message("Please, select only one item!")
|
||||
else:
|
||||
self._app.show_error_message("No selected item!")
|
||||
|
||||
def add_timer(self, timer_data):
|
||||
dialog = self.TimerDialog(self._app.app_window, self.TimerAction.ADD, timer_data)
|
||||
response = dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
self._app.send_http_request(HttpAPI.Request.TIMER, dialog.request, self.timer_add_edit_callback)
|
||||
dialog.destroy()
|
||||
|
||||
def on_timer_edit(self, action=None, value=None):
|
||||
model, paths = self._view.get_selection().get_selected_rows()
|
||||
if len(paths) > 1:
|
||||
self._app.show_error_message("Please, select only one item!")
|
||||
return
|
||||
|
||||
dialog = self.TimerDialog(self._app.app_window, self.TimerAction.CHANGE, model[paths][-1])
|
||||
response = dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
self._app.send_http_request(HttpAPI.Request.TIMER, dialog.request, self.timer_add_edit_callback)
|
||||
dialog.destroy()
|
||||
|
||||
@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_message(msg)
|
||||
log(msg)
|
||||
return
|
||||
|
||||
state = resp.get("e2state", None)
|
||||
if state == "False":
|
||||
msg = resp.get("e2statetext", "")
|
||||
self._app.show_error_message(msg)
|
||||
log(msg)
|
||||
if state == "True":
|
||||
msg = resp.get("e2statetext", "")
|
||||
log(msg)
|
||||
self._app.show_info_message(msg, Gtk.MessageType.INFO)
|
||||
self.update_timer_list(self._app, Page.TIMERS)
|
||||
else:
|
||||
log("Error getting timer status. No response!")
|
||||
|
||||
def on_timer_remove(self, action=None, value=None):
|
||||
model, paths = self._view.get_selection().get_selected_rows()
|
||||
if not paths or show_dialog(DialogType.QUESTION, self._app.app_window) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
refs = {}
|
||||
for path in paths:
|
||||
timer = model[path][-1]
|
||||
ref = "timerdelete?sRef={}&begin={}&end={}".format(quote(timer.get("e2servicereference", "")),
|
||||
timer.get("e2timebegin", ""),
|
||||
timer.get("e2timeend", ""))
|
||||
refs[ref] = model.get_iter(path)
|
||||
|
||||
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
|
||||
|
||||
model = self._view.get_model()
|
||||
list(map(model.remove, (refs[ref] for ref in refs if ref in removed)))
|
||||
self._app.wait_dialog.hide()
|
||||
self._remove_button.set_sensitive(len(model))
|
||||
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._app.send_http_request(HttpAPI.Request.TIMER, ref, callback)
|
||||
yield True
|
||||
|
||||
def on_timers_press(self, view, event):
|
||||
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(view.get_model()) > 0:
|
||||
self.on_timer_edit()
|
||||
|
||||
def on_timers_key_press(self, view, event):
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
return
|
||||
|
||||
key = KeyboardKey(key_code)
|
||||
if key is KeyboardKey.DELETE:
|
||||
self.on_timer_remove()
|
||||
|
||||
def on_timer_cursor_changed(self, view):
|
||||
path, column = view.get_cursor()
|
||||
if not path:
|
||||
return
|
||||
|
||||
timer = view.get_model()[path][-1]
|
||||
self._info_enabled_switch.set_active((timer.get("e2disabled", "0") == "0"))
|
||||
self._ref_info_label.set_text(timer.get("e2servicereference", ""))
|
||||
self._event_id_info_label.set_text(timer.get("e2eit", ""))
|
||||
self._action_info_label.set_text(get_message(self.ACTION.get(timer.get("e2justplay", "0"), "0")))
|
||||
self._after_info_label.set_text(get_message(self.AFTER_EVENT.get(timer.get("e2afterevent", "0"), "0")))
|
||||
self._begins_info_label.set_text(str(datetime.fromtimestamp(int(timer.get("e2timebegin", "0")))))
|
||||
self._ends_info_label.set_text(str(datetime.fromtimestamp(int(timer.get("e2timeend", "0")))))
|
||||
self.set_repetition_flags(int(timer.get("e2repeated", "0")), self._days_buttons)
|
||||
location = timer.get("e2location", "")
|
||||
self._info_location_entry.set_text("" if location == "None" else location)
|
||||
|
||||
@staticmethod
|
||||
def get_repetition_flags(boxes):
|
||||
""" Returns flags for repetition.
|
||||
|
||||
@param boxes: Buttons tuple for the days of the week.
|
||||
"""
|
||||
day_flags = 0
|
||||
for i, box in enumerate(boxes):
|
||||
if box.get_active():
|
||||
day_flags = day_flags | (1 << i)
|
||||
|
||||
return day_flags
|
||||
|
||||
@staticmethod
|
||||
def set_repetition_flags(flags, boxes):
|
||||
""" Sets flags for repetition.
|
||||
|
||||
@param flags: Flags value.
|
||||
@param boxes: Buttons tuple for the days of the week.
|
||||
"""
|
||||
for i, box in enumerate(boxes):
|
||||
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_message("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.add_timer({"e2servicename": service.service,
|
||||
"e2servicereference": service.picon_id.rstrip(".png").replace("_", ":")})
|
||||
|
||||
context.finish(True, False, time)
|
||||
|
||||
|
||||
class RecordingsTool(Gtk.Box):
|
||||
ROOT = ".."
|
||||
DEFAULT_PATH = "/hdd"
|
||||
|
||||
def __init__(self, app, settings, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._app = app
|
||||
self._app.connect("profile-changed", self.init)
|
||||
self._settings = settings
|
||||
self._ftp = None
|
||||
# Icon.
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
icon = "folder-symbolic" if IS_DARWIN else "folder"
|
||||
self._icon = theme.load_icon(icon, 24, 0) if theme.lookup_icon(icon, 24, 0) else None
|
||||
|
||||
handlers = {"on_path_press": self.on_path_press,
|
||||
"on_path_activated": self.on_path_activated,
|
||||
"on_recordings_activated": self.on_recordings_activated,
|
||||
"on_recording_remove": self.on_recording_remove}
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers,
|
||||
objects=("recordings_frame", "recordings_model", "rec_paths_model"))
|
||||
self._rec_view = builder.get_object("recordings_view")
|
||||
self._paths_view = builder.get_object("recordings_paths_view")
|
||||
self._paned = builder.get_object("recordings_paned")
|
||||
self.pack_start(builder.get_object("recordings_frame"), True, True, 0)
|
||||
|
||||
self.init()
|
||||
self.show()
|
||||
|
||||
def clear_data(self):
|
||||
self._rec_view.get_model().clear()
|
||||
self._paths_view.get_model().clear()
|
||||
|
||||
@run_task
|
||||
def init(self, app=None, arg=None):
|
||||
GLib.idle_add(self.clear_data)
|
||||
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"
|
||||
except all_errors:
|
||||
pass # NOP
|
||||
else:
|
||||
self.init_paths(self.DEFAULT_PATH)
|
||||
|
||||
@run_idle
|
||||
def init_paths(self, path=None):
|
||||
self.clear_data()
|
||||
if not self._ftp:
|
||||
return
|
||||
|
||||
if path:
|
||||
try:
|
||||
self._ftp.cwd(path)
|
||||
except all_errors as e:
|
||||
pass
|
||||
|
||||
files = []
|
||||
try:
|
||||
self._ftp.dir(files.append)
|
||||
except all_errors as e:
|
||||
log(e)
|
||||
else:
|
||||
self.append_paths(files)
|
||||
|
||||
@run_idle
|
||||
def append_paths(self, files):
|
||||
model = self._paths_view.get_model()
|
||||
model.clear()
|
||||
model.append((None, self.ROOT, self._ftp.pwd()))
|
||||
|
||||
for f in files:
|
||||
f_data = f.split()
|
||||
f_type = f_data[0][0]
|
||||
|
||||
if f_type == "d":
|
||||
model.append((self._icon, f_data[-1], self._ftp.pwd()))
|
||||
|
||||
def on_path_activated(self, view, path, column):
|
||||
row = view.get_model()[path][:]
|
||||
path = f"{row[-1]}/{row[1]}/"
|
||||
self._app.send_http_request(HttpAPI.Request.RECORDINGS, quote(path), self.update_recordings_data)
|
||||
|
||||
def on_path_press(self, view, event):
|
||||
target = view.get_path_at_pos(event.x, event.y)
|
||||
if not target or event.button != Gdk.BUTTON_PRIMARY:
|
||||
return
|
||||
|
||||
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS:
|
||||
self.init_paths(self._paths_view.get_model()[target[0]][1])
|
||||
|
||||
@run_idle
|
||||
def update_recordings_data(self, recordings):
|
||||
model = self._rec_view.get_model()
|
||||
model.clear()
|
||||
list(map(model.append, (self.get_recordings_row(r) for r in recordings.get("recordings", []))))
|
||||
|
||||
def get_recordings_row(self, rec):
|
||||
service = rec.get("e2servicename")
|
||||
title = rec.get("e2title", "")
|
||||
time = datetime.fromtimestamp(int(rec.get("e2time", "0"))).strftime("%A, %H:%M")
|
||||
length = rec.get("e2length", "0")
|
||||
file = rec.get("e2filename", "")
|
||||
desc = rec.get("e2description", "")
|
||||
|
||||
return service, title, time, length, file, desc, rec
|
||||
|
||||
def on_recordings_activated(self, view, path, column):
|
||||
rec = view.get_model()[path][-1]
|
||||
self._app.send_http_request(HttpAPI.Request.STREAM_TS, rec.get("e2filename", ""), self.on_play_recording)
|
||||
|
||||
def on_play_recording(self, m3u):
|
||||
url = self._app.get_url_from_m3u(m3u)
|
||||
if url:
|
||||
self._app.emit("play-recording", url)
|
||||
|
||||
def on_recording_remove(self, action, value=None):
|
||||
""" Removes recordings via FTP. """
|
||||
if show_dialog(DialogType.QUESTION, self._app.app_window) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
model, paths = self._rec_view.get_selection().get_selected_rows()
|
||||
if paths and self._ftp:
|
||||
for file, itr in ((model[p][-1].get("e2filename", ""), model.get_iter(p)) for p in paths):
|
||||
resp = self._ftp.delete_file(file)
|
||||
if resp.startswith("2"):
|
||||
GLib.idle_add(model.remove, itr)
|
||||
else:
|
||||
self._app.show_error_message(resp)
|
||||
break
|
||||
|
||||
def on_playback(self, box, state):
|
||||
""" Updates state of the UI elements for playback mode. """
|
||||
if self._settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
|
||||
self._paned.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
self.update_rec_columns_visibility(False)
|
||||
|
||||
def on_playback_close(self, box, state):
|
||||
""" Restores UI elements state after playback mode. """
|
||||
self._paned.set_orientation(Gtk.Orientation.HORIZONTAL)
|
||||
self.update_rec_columns_visibility(True)
|
||||
|
||||
def update_rec_columns_visibility(self, state):
|
||||
for c in (Column.REC_SERVICE, Column.REC_TIME, Column.REC_LEN, Column.REC_FILE, Column.REC_DESC):
|
||||
self._rec_view.get_column(c).set_visible(state)
|
||||
from ..connections import HttpAPI
|
||||
from ..settings import IS_DARWIN, IS_LINUX, IS_WIN
|
||||
|
||||
|
||||
class ControlTool(Gtk.Box):
|
||||
@@ -797,16 +46,18 @@ class ControlTool(Gtk.Box):
|
||||
|
||||
self._settings = settings
|
||||
self._app = app
|
||||
self._app.connect("layout-changed", self.on_layout_changed)
|
||||
self._pix = None
|
||||
|
||||
handlers = {"on_volume_changed": self.on_volume_changed,
|
||||
"on_screenshot_draw": self.on_screenshot_draw}
|
||||
"on_screenshot_draw": self.on_screenshot_draw,
|
||||
"on_network_toggled": self.on_network_toggled,
|
||||
"on_network_view_query_tooltip": self.on_network_view_query_tooltip}
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers,
|
||||
objects=("control_box", "volume_adjustment"))
|
||||
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers)
|
||||
|
||||
self.pack_start(builder.get_object("control_box"), True, True, 0)
|
||||
self._stack = builder.get_object("stack")
|
||||
self._remote_box = builder.get_object("remote_box")
|
||||
self._screenshot_area = builder.get_object("screenshot_area")
|
||||
self._screenshot_button_box = builder.get_object("screenshot_button_box")
|
||||
self._screenshot_check_button = builder.get_object("screenshot_check_button")
|
||||
@@ -818,7 +69,16 @@ class ControlTool(Gtk.Box):
|
||||
self._ber_level_bar = builder.get_object("ber_level_bar")
|
||||
self._agc_level_bar = builder.get_object("agc_level_bar")
|
||||
self._volume_button = builder.get_object("volume_button")
|
||||
self._header_box = builder.get_object("control_header_box")
|
||||
# Network.
|
||||
self._network_button = builder.get_object("control_network_button")
|
||||
self._network_model = builder.get_object("network_model")
|
||||
|
||||
self.init_actions(app)
|
||||
|
||||
if settings.alternate_layout:
|
||||
self.on_layout_changed(app, True)
|
||||
|
||||
self.show()
|
||||
|
||||
def init_actions(self, app):
|
||||
@@ -832,10 +92,16 @@ class ControlTool(Gtk.Box):
|
||||
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_ch_up", lambda a, v: self.on_remote_action(HttpAPI.Remote.CH_UP))
|
||||
app.set_action("on_ch_down", lambda a, v: self.on_remote_action(HttpAPI.Remote.CH_DOWN))
|
||||
app.set_action("on_red", lambda a, v: self.on_remote_action(HttpAPI.Remote.RED))
|
||||
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))
|
||||
app.set_action("on_audio", lambda a, v: self.on_remote_action(HttpAPI.Remote.AUDIO))
|
||||
app.set_action("on_tv", lambda a, v: self.on_remote_action(HttpAPI.Remote.TV))
|
||||
app.set_action("on_radio", lambda a, v: self.on_remote_action(HttpAPI.Remote.RADIO))
|
||||
app.set_action("on_fav", lambda a, v: self.on_remote_action(HttpAPI.Remote.FAV))
|
||||
# Playback.
|
||||
app.set_action("on_prev_media", lambda a, v: self.on_player_action(HttpAPI.Request.PLAYER_PREV))
|
||||
app.set_action("on_play_media", lambda a, v: self.on_player_action(HttpAPI.Request.PLAYER_PLAY))
|
||||
@@ -852,6 +118,13 @@ class ControlTool(Gtk.Box):
|
||||
app.set_action("on_screenshot_video", self.on_screenshot_video)
|
||||
app.set_action("on_screenshot_osd", self.on_screenshot_osd)
|
||||
|
||||
def on_layout_changed(self, app, alt_layout):
|
||||
children = self._remote_box.get_children()
|
||||
self._remote_box.reorder_child(children[0], len(children) - 1)
|
||||
self._remote_box.reorder_child(children[-1], 0)
|
||||
pack_type = Gtk.PackType.END if alt_layout else Gtk.PackType.START
|
||||
self._header_box.set_child_packing(self._network_button, False, False, 0, pack_type)
|
||||
|
||||
# ***************** Remote controller ********************* #
|
||||
|
||||
def on_remote(self, action, state=False):
|
||||
@@ -871,7 +144,7 @@ class ControlTool(Gtk.Box):
|
||||
|
||||
@run_with_delay(0.5)
|
||||
def on_volume_changed(self, button, value):
|
||||
self._app.send_http_request(HttpAPI.Request.VOL, "{:.0f}".format(value), self.on_response)
|
||||
self._app.send_http_request(HttpAPI.Request.VOL, f"{value:.0f}", self.on_response)
|
||||
|
||||
def update_volume(self, vol):
|
||||
if "error_code" in vol:
|
||||
@@ -980,3 +253,75 @@ class ControlTool(Gtk.Box):
|
||||
self._snr_level_bar.set_value(int(snr.strip("%N/A") or 0))
|
||||
self._agc_level_bar.set_value(int(acg.rstrip("%N/A") or 0))
|
||||
self._ber_level_bar.set_value(int(ber.rstrip("N/A") or 0))
|
||||
|
||||
# ***************** Network explorer ********************** #
|
||||
|
||||
def on_network_toggled(self, button):
|
||||
self._network_model.clear()
|
||||
if button.get_active():
|
||||
self.update_network()
|
||||
|
||||
@run_task
|
||||
def update_network(self):
|
||||
pattern = re.compile(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})')
|
||||
|
||||
ips = [match for match in re.findall(pattern, os.popen("arp -a").read())]
|
||||
for ip in ips:
|
||||
if not self._network_button.get_active():
|
||||
break
|
||||
|
||||
url = f"http://{ip}/web/{HttpAPI.Request.INFO.value}"
|
||||
try:
|
||||
resp = HttpAPI.get_response(HttpAPI.Request.INFO, url, timeout=5)
|
||||
except OSError as e:
|
||||
log(f"{ip} {e}")
|
||||
else:
|
||||
if resp.get("e2distroversion", None):
|
||||
log(f"Receiver found. Model: {resp.get('e2model', 'N/A')} [{ip} ]")
|
||||
self.append_box_data(resp)
|
||||
|
||||
@run_idle
|
||||
def append_box_data(self, data):
|
||||
ip = data.get('e2lanip', 'N/A')
|
||||
itr = self._network_model.append((data.get("e2model", "N/A"), ip, None, data, None))
|
||||
GLib.timeout_add_seconds(3, self.check_power_state, itr, priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def on_network_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
|
||||
result = view.get_dest_row_at_pos(x, y)
|
||||
if not result:
|
||||
return False
|
||||
|
||||
path, pos = result
|
||||
model = view.get_model()
|
||||
data = model[path][3]
|
||||
|
||||
dist = data.get("e2distroversion", "N/A")
|
||||
img = data.get("e2imageversion", "N/A")
|
||||
txt = f"Distro version: {dist}\nImage version: {img}"
|
||||
tooltip.set_text(txt)
|
||||
view.set_tooltip_row(tooltip, path)
|
||||
return True
|
||||
|
||||
def check_power_state(self, itr):
|
||||
active = self._network_button.get_active()
|
||||
if not active:
|
||||
return False
|
||||
|
||||
data = self._network_model.get_value(itr, 3)
|
||||
url = f"http://{data.get('e2lanip', 'N/A')}/web/powerstate"
|
||||
self.update_power_state(itr, url)
|
||||
return active
|
||||
|
||||
@run_task
|
||||
def update_power_state(self, itr, url):
|
||||
try:
|
||||
resp = HttpAPI.get_response(HttpAPI.Request.POWER, url, timeout=2)
|
||||
except OSError as e:
|
||||
log(e)
|
||||
else:
|
||||
state = get_message("On" if resp.get("e2instandby", "N/A").strip() == "false" else "Standby")
|
||||
GLib.idle_add(self._network_model.set_value, itr, 2, state)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2022 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. -->
|
||||
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkAboutDialog" id="about_dialog">
|
||||
<property name="can_focus">False</property>
|
||||
@@ -40,8 +40,8 @@ Author: Dmitriy Yefremov
|
||||
<property name="icon_name">system-help</property>
|
||||
<property name="type_hint">normal</property>
|
||||
<property name="program_name">DemonEditor</property>
|
||||
<property name="version">2.0.0 Alpha2</property>
|
||||
<property name="copyright">2018-2021 Dmitriy Yefremov
|
||||
<property name="version">3.1.0 Alpha</property>
|
||||
<property name="copyright">2018-2022 Dmitriy Yefremov
|
||||
</property>
|
||||
<property name="comments" translatable="yes">Enigma2 channel and satellite list editor.</property>
|
||||
<property name="website">https://dyefremov.github.io/DemonEditor/</property>
|
||||
@@ -91,51 +91,36 @@ Author: Dmitriy Yefremov
|
||||
<property name="type_hint">utility</property>
|
||||
<property name="skip_taskbar_hint">True</property>
|
||||
<property name="skip_pager_hint">True</property>
|
||||
<child type="titlebar">
|
||||
<placeholder/>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="input_dialog_cancel_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="input_dialog_ok_button">
|
||||
<property name="label" translatable="yes">OK</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
<accelerator key="Return" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">4</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="input_dialog_cancel_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="input_dialog_ok_button">
|
||||
<property name="label" translatable="yes">OK</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
<accelerator key="Return" signal="activate"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -147,10 +132,10 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkEntry" id="input_entry">
|
||||
<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="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_activatable">False</property>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 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
|
||||
@@ -28,10 +28,10 @@
|
||||
|
||||
""" Common module for showing dialogs """
|
||||
import gettext
|
||||
import xml.etree.ElementTree as ET
|
||||
from enum import Enum
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from app.commons import run_idle
|
||||
from app.settings import SEP, IS_WIN
|
||||
@@ -238,15 +238,16 @@ def get_builder(path, handlers=None, use_str=False, objects=None, tag="property"
|
||||
|
||||
|
||||
def translate_xml(path, tag="property"):
|
||||
"""
|
||||
Used to translate GUI from * .glade files in MS Windows.
|
||||
""" Used to translate GUI from * .glade files in MS Windows.
|
||||
|
||||
More info: https://gitlab.gnome.org/GNOME/gtk/-/issues/569
|
||||
"""
|
||||
et = ET.parse(path)
|
||||
root = et.getroot()
|
||||
for e in root.iter(tag):
|
||||
if e.attrib.get("translatable", None) == "yes":
|
||||
for e in root.iter():
|
||||
if e.tag == tag and e.attrib.get("translatable", None) == "yes":
|
||||
e.text = get_message(e.text)
|
||||
elif e.tag == "item" and e.attrib.get("translatable", None) == "yes":
|
||||
e.text = get_message(e.text)
|
||||
|
||||
return ET.tostring(root, encoding="unicode", method="xml")
|
||||
|
||||
@@ -1,542 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- 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 domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor. -->
|
||||
<!-- interface-copyright 2018 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkImage" id="download_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">network-receive-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="send_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">network-transmit-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkWindow" id="download_dialog_window">
|
||||
<property name="width_request">550</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">FTP-transfer</property>
|
||||
<property name="resizable">False</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window_position">center-on-parent</property>
|
||||
<property name="icon_name">mail-send-receive-symbolic</property>
|
||||
<property name="skip_taskbar_hint">True</property>
|
||||
<property name="skip_pager_hint">True</property>
|
||||
<child type="titlebar">
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_dialog_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="header_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="profile_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Profile:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="profile_combo_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="active">0</property>
|
||||
<property name="has_frame">False</property>
|
||||
<signal name="changed" handler="on_profile_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="options_button">
|
||||
<property name="label" translatable="yes">Options</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Options</property>
|
||||
<signal name="clicked" handler="on_settings" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="selection_data_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label10">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="all_radio_button">
|
||||
<property name="label" translatable="yes">All</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">satellites_radio_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="bouquets_radio_button">
|
||||
<property name="label" translatable="yes">Bouquets</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">satellites_radio_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="satellites_radio_button">
|
||||
<property name="label" translatable="yes">Satellites</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">all_radio_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="webtv_radio_button">
|
||||
<property name="label" translatable="yes">WebTV</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">all_radio_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="primary-toolbar"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="main_settings_box_frame">
|
||||
<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="label_xalign">0.019999999552965164</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_settings_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="margin_bottom">10</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkGrid" id="main_settings_bo">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="row_spacing">2</property>
|
||||
<property name="column_spacing">2</property>
|
||||
<property name="column_homogeneous">True</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ip_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Receiver IP:</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="host_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="max_width_chars">10</property>
|
||||
<property name="text">127.0.0.1</property>
|
||||
<property name="caps_lock_warning">False</property>
|
||||
<property name="primary_icon_name">network-transmit-receive-symbolic</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="data_path_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Current data path:</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="data_path_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="text">data/</property>
|
||||
<property name="caps_lock_warning">False</property>
|
||||
<property name="primary_icon_name">folder-open-symbolic</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">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="extra_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="remove_unused_check_button">
|
||||
<property name="label" translatable="yes">Remove unused bouquets</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">True</property>
|
||||
<signal name="toggled" handler="on_remove_unused_bouquets_toggled" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="use_http_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="use_http_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Use HTTP</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="use_http_switch">
|
||||
<property name="visible">True</property>
|
||||
<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>
|
||||
<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="pack_type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label_item">
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="button_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_left">20</property>
|
||||
<property name="margin_right">20</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="margin_bottom">10</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="layout_style">expand</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="receive_button">
|
||||
<property name="label" translatable="yes">Receive</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Receive</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="image">download_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_receive" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="send_button">
|
||||
<property name="label" translatable="yes">Send</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Send</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="image">send_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_send" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkExpander" id="expander">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="margin_left">1</property>
|
||||
<property name="margin_right">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<property name="resize_toplevel">True</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrolled_window">
|
||||
<property name="height_request">120</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="text_view">
|
||||
<property name="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>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="expander_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Extra:</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">7</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">1</property>
|
||||
<property name="margin_right">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<signal name="response" handler="on_info_bar_close" swapped="no"/>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="layout_style">expand</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">16</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="info_bar_message_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Info</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">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">8</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
@@ -1,197 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
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, get_builder
|
||||
from .uicommons import Gtk, UI_RESOURCES_PATH
|
||||
|
||||
|
||||
class DownloadDialog:
|
||||
def __init__(self, transient, settings, open_data_callback, update_settings_callback):
|
||||
self._s_type = settings.setting_type
|
||||
self._settings = settings
|
||||
self._open_data_callback = open_data_callback
|
||||
self._update_settings_callback = update_settings_callback
|
||||
|
||||
handlers = {"on_receive": self.on_receive,
|
||||
"on_send": self.on_send,
|
||||
"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 = get_builder(UI_RESOURCES_PATH + "download_dialog.glade", handlers)
|
||||
|
||||
self._dialog_window = builder.get_object("download_dialog_window")
|
||||
self._dialog_window.set_transient_for(transient)
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("info_bar_message_label")
|
||||
self._text_view = builder.get_object("text_view")
|
||||
self._expander = builder.get_object("expander")
|
||||
self._host_entry = builder.get_object("host_entry")
|
||||
self._data_path_entry = builder.get_object("data_path_entry")
|
||||
self._remove_unused_check_button = builder.get_object("remove_unused_check_button")
|
||||
self._all_radio_button = builder.get_object("all_radio_button")
|
||||
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._use_http_switch = builder.get_object("use_http_switch")
|
||||
self._http_radio_button = builder.get_object("http_radio_button")
|
||||
self._profile_combo_box = builder.get_object("profile_combo_box")
|
||||
|
||||
self.init_settings()
|
||||
|
||||
def show(self):
|
||||
self._dialog_window.show()
|
||||
|
||||
def init_settings(self):
|
||||
self.update_profiles()
|
||||
self.init_ui_settings()
|
||||
|
||||
def init_ui_settings(self):
|
||||
self._host_entry.set_text(self._settings.host)
|
||||
self._data_path_entry.set_text(self._settings.profile_data_path)
|
||||
is_enigma = self._s_type is SettingsType.ENIGMA_2
|
||||
self._webtv_radio_button.set_visible(not is_enigma)
|
||||
self._use_http_switch.set_active(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()
|
||||
for p in self._settings.profiles:
|
||||
self._profile_combo_box.append(p, p)
|
||||
self._profile_combo_box.set_active_id(self._settings.current_profile)
|
||||
|
||||
@run_idle
|
||||
def on_receive(self, item):
|
||||
self.download(True, self.get_download_type())
|
||||
|
||||
@run_idle
|
||||
def on_send(self, item):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog_window) != Gtk.ResponseType.CANCEL:
|
||||
self.download(False, self.get_download_type())
|
||||
|
||||
def get_download_type(self):
|
||||
download_type = DownloadType.ALL
|
||||
if self._bouquets_radio_button.get_active():
|
||||
download_type = DownloadType.BOUQUETS
|
||||
elif self._satellites_radio_button.get_active():
|
||||
download_type = DownloadType.SATELLITES
|
||||
elif self._webtv_radio_button.get_active():
|
||||
download_type = DownloadType.WEBTV
|
||||
return download_type
|
||||
|
||||
def destroy(self):
|
||||
self._dialog_window.destroy()
|
||||
|
||||
def on_settings(self, item):
|
||||
response = show_settings_dialog(self._dialog_window, self._settings)
|
||||
if response != Gtk.ResponseType.CANCEL:
|
||||
self._s_type = self._settings.setting_type
|
||||
self.update_profiles()
|
||||
gen = self._update_settings_callback()
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
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 """
|
||||
GLib.idle_add(self._expander.set_expanded, True)
|
||||
self.clear_output()
|
||||
backup, backup_src, data_path = self._settings.backup_before_downloading, None, None
|
||||
|
||||
try:
|
||||
if download:
|
||||
if backup and d_type is not DownloadType.SATELLITES:
|
||||
data_path = self._settings.profile_data_path or self._data_path_entry.get_text()
|
||||
os.makedirs(os.path.dirname(data_path), exist_ok=True)
|
||||
backup_path = self._settings.profile_backup_path or self._settings.default_backup_path
|
||||
backup_src = backup_data(data_path, backup_path, d_type is DownloadType.ALL)
|
||||
|
||||
download_data(settings=self._settings, download_type=d_type, callback=self.append_output)
|
||||
else:
|
||||
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
|
||||
upload_data(settings=self._settings,
|
||||
download_type=d_type,
|
||||
remove_unused=self._remove_unused_check_button.get_active(),
|
||||
callback=self.append_output,
|
||||
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:
|
||||
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:
|
||||
if download and d_type is not DownloadType.SATELLITES:
|
||||
GLib.idle_add(self._open_data_callback)
|
||||
|
||||
@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)
|
||||
|
||||
@run_idle
|
||||
def append_output(self, text):
|
||||
append_text_to_tview(text, self._text_view)
|
||||
|
||||
@run_idle
|
||||
def clear_output(self):
|
||||
self._text_view.get_buffer().set_text("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
0
app/ui/epg/__init__.py
Normal file
0
app/ui/epg/__init__.py
Normal file
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2020 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2022 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-2020 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkImage" id="apply_image">
|
||||
<property name="visible">True</property>
|
||||
@@ -158,8 +158,12 @@ Author: Dmitriy Yefremov
|
||||
<columns>
|
||||
<!-- column-name service -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name pos -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name service_id -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name picon_id -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkTreeModelFilter" id="services_filter_model">
|
||||
@@ -671,7 +675,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="image">filter_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="toggled" handler="on_filter_toggled" swapped="no"/>
|
||||
<accelerator key="f" signal="clicked" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/>
|
||||
<accelerator key="f" signal="clicked" modifiers="GDK_SHIFT_MASK | Primary"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -810,17 +814,61 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSearchBar" id="filter_bar">
|
||||
<object class="GtkBox" id="filter_bar">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">2</property>
|
||||
<property name="margin_bottom">2</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="filter_auto_switch">
|
||||
<property name="name">filter_switch</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Automatically set the name selected in the bouquet list.</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_right">15</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="filter_auto_label">
|
||||
<property name="name">filter_auto_label</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="label" translatable="yes">Auto</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>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child type="center">
|
||||
<object class="GtkSearchEntry" id="filter_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="primary_icon_name">edit-find-replace-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="primary_icon_sensitive">False</property>
|
||||
<signal name="search-changed" handler="on_filter_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -851,7 +899,6 @@ 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="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
@@ -864,26 +911,28 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">service_sort_model</property>
|
||||
<property name="search_column">0</property>
|
||||
<property name="fixed_height_mode">True</property>
|
||||
<property name="enable_grid_lines">both</property>
|
||||
<signal name="button-press-event" handler="on_popup_menu" object="source_popup_menu" swapped="no"/>
|
||||
<signal name="drag-begin" handler="on_drag_begin" swapped="no"/>
|
||||
<signal name="drag-data-get" handler="on_drag_data_get" swapped="no"/>
|
||||
<signal name="key-release-event" handler="on_key_release" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="service_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">autosize</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">50</property>
|
||||
<property name="title" translatable="yes">Service</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="source_service_cellrenderertext">
|
||||
<object class="GtkCellRendererText" id="source_service_renderertext">
|
||||
<property name="xpad">5</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
@@ -892,15 +941,15 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ref_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">autosize</property>
|
||||
<property name="min_width">50</property>
|
||||
<property name="title" translatable="yes">Reference</property>
|
||||
<property name="expand">True</property>
|
||||
<object class="GtkTreeViewColumn" id="pos_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">70</property>
|
||||
<property name="title" translatable="yes">Pos</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">1</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="source_reference_cellrenderertext">
|
||||
<object class="GtkCellRendererText" id="source_pos_renderertext">
|
||||
<property name="xpad">5</property>
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
</object>
|
||||
<attributes>
|
||||
@@ -909,6 +958,24 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ref_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">50</property>
|
||||
<property name="title" translatable="yes">Reference</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="source_reference_renderertext">
|
||||
<property name="xpad">10</property>
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
@@ -1039,7 +1106,6 @@ 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="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="fav_scrolled_window">
|
||||
@@ -1052,19 +1118,22 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">bouquet_list_store</property>
|
||||
<property name="search_column">2</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<property name="enable_grid_lines">both</property>
|
||||
<property name="tooltip_column">9</property>
|
||||
<signal name="button-press-event" handler="on_bouquet_popup_menu" object="bouquet_popup_menu" swapped="no"/>
|
||||
<signal name="cursor-changed" handler="on_bq_cursor_changed" swapped="no"/>
|
||||
<signal name="drag-data-received" handler="on_drag_data_received" swapped="no"/>
|
||||
<signal name="key-release-event" handler="on_key_release" swapped="no"/>
|
||||
<signal name="key-release-event" handler="on_key_press" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection">
|
||||
<object class="GtkTreeSelection" id="bouquet_view_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="num_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">30</property>
|
||||
<property name="title" translatable="yes">Num</property>
|
||||
<property name="alignment">0.5</property>
|
||||
@@ -1085,6 +1154,7 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="fav_service_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">50</property>
|
||||
<property name="title" translatable="yes">Service</property>
|
||||
<property name="expand">True</property>
|
||||
@@ -1141,7 +1211,7 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="fav_type_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">autosize</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">40</property>
|
||||
<property name="title" translatable="yes">Type</property>
|
||||
<property name="expand">True</property>
|
||||
@@ -1161,7 +1231,7 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkTreeViewColumn" id="fav_pos_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">autosize</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">10</property>
|
||||
<property name="title" translatable="yes">Pos</property>
|
||||
<child>
|
||||
@@ -1178,7 +1248,7 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="fav_id_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="sizing">autosize</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="title">fav_id</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="fav_id_cellrenderertext4"/>
|
||||
@@ -1191,7 +1261,7 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="fav_extra_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="sizing">autosize</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="title" translatable="yes">extra</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="fav_tooltip_cellrenderertext"/>
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 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,25 +26,30 @@
|
||||
#
|
||||
|
||||
|
||||
""" Module for working with EPG. """
|
||||
import gzip
|
||||
import locale
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import quote
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import run_idle, run_task
|
||||
from app.connections import download_data, DownloadType
|
||||
from app.commons import run_idle, run_task, run_with_delay
|
||||
from app.connections import download_data, DownloadType, HttpAPI
|
||||
from app.eparser.ecommons import BouquetService, BqServiceType
|
||||
from app.settings import SEP
|
||||
from app.tools.epg import EPG, ChannelsParser
|
||||
from app.settings import SEP, EpgSource
|
||||
from app.tools.epg import EPG, ChannelsParser, EpgEvent, XmlTvReader
|
||||
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, Column, EPG_ICON, KeyboardKey
|
||||
from app.ui.tasks import BGTaskWidget
|
||||
from app.ui.timers import TimerTool
|
||||
from ..main_helper import on_popup_menu, update_entry_data, scroll_to
|
||||
from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column, EPG_ICON, KeyboardKey, IS_GNOME_SESSION, Page
|
||||
|
||||
|
||||
class RefsSource(Enum):
|
||||
@@ -52,9 +57,315 @@ class RefsSource(Enum):
|
||||
XML = 1
|
||||
|
||||
|
||||
class EpgCache(dict):
|
||||
def __init__(self, app):
|
||||
super().__init__()
|
||||
self._current_bq = None
|
||||
self._reader = None
|
||||
self._canceled = False
|
||||
|
||||
self._settings = app.app_settings
|
||||
self._src = self._settings.epg_source
|
||||
self._app = app
|
||||
self._app.connect("bouquet-changed", self.on_bouquet_changed)
|
||||
self._app.connect("profile-changed", self.on_profile_changed)
|
||||
self._app.connect("task-canceled", self.on_xml_load_cancel)
|
||||
|
||||
self.init()
|
||||
|
||||
@run_idle
|
||||
def init(self):
|
||||
if self._src is EpgSource.XML:
|
||||
url = self._settings.epg_xml_source
|
||||
gz_file = f"{self._settings.profile_data_path}epg{os.sep}epg.gz"
|
||||
self._reader = XmlTvReader(gz_file, url)
|
||||
|
||||
def process_data():
|
||||
t = BGTaskWidget(self._app, "Processing XMLTV data...", self._reader.parse, )
|
||||
self._app.emit("add-background-task", t)
|
||||
|
||||
if os.path.isfile(gz_file):
|
||||
# Difference calculation between the current time and file modification.
|
||||
dif = datetime.now() - datetime.fromtimestamp(os.path.getmtime(gz_file))
|
||||
# We will update daily. -> Temporarily!!!
|
||||
if dif.days > 0 and not self._canceled:
|
||||
task = BGTaskWidget(self._app, "Downloading EPG...", self._reader.download, process_data, )
|
||||
self._app.emit("add-background-task", task)
|
||||
else:
|
||||
process_data()
|
||||
else:
|
||||
if not self._canceled:
|
||||
task = BGTaskWidget(self._app, "Downloading EPG...", self._reader.download, process_data, )
|
||||
self._app.emit("add-background-task", task)
|
||||
elif self._src is EpgSource.DAT:
|
||||
self._reader = EPG.DatReader(f"{self._settings.profile_data_path}epg{os.sep}epg.dat")
|
||||
self._reader.download()
|
||||
|
||||
GLib.timeout_add_seconds(self._settings.epg_update_interval, self.update_epg_data, priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def on_bouquet_changed(self, app, bq):
|
||||
self._current_bq = bq
|
||||
|
||||
def on_profile_changed(self, app, p):
|
||||
self.clear()
|
||||
|
||||
def on_xml_load_cancel(self, app, widget):
|
||||
self._canceled = True
|
||||
|
||||
def update_epg_data(self):
|
||||
if self._src is EpgSource.HTTP:
|
||||
api = self._app.http_api
|
||||
bq = self._app.current_bouquet_files.get(self._current_bq, None)
|
||||
|
||||
if bq and api:
|
||||
req = quote(f'FROM BOUQUET "userbouquet.{bq}.{self._current_bq.split(":")[-1]}"')
|
||||
api.send(HttpAPI.Request.EPG_NOW, f'1:7:1:0:0:0:0:0:0:0:{req}', self.update_http_data)
|
||||
elif self._src is EpgSource.XML:
|
||||
self.update_xml_data()
|
||||
|
||||
return self._app.display_epg
|
||||
|
||||
def update_http_data(self, epg):
|
||||
for e in (EpgTool.get_event(e, False) for e in epg.get("event_list", []) if e.get("e2eventid", "").isdigit()):
|
||||
self[e.event_data.get("e2eventservicename", "")] = e
|
||||
|
||||
@run_task
|
||||
def update_xml_data(self):
|
||||
services = self._app.current_services
|
||||
names = {services[s].service for s in self._app.current_bouquets.get(self._current_bq, [])}
|
||||
for name, e in self._reader.get_current_events(names).items():
|
||||
self[name] = e
|
||||
|
||||
def get_current_event(self, service_name):
|
||||
return self.get(service_name, EpgEvent())
|
||||
|
||||
|
||||
class EpgSettingsPopover(Gtk.Popover):
|
||||
|
||||
def __init__(self, app, **kwarg):
|
||||
super().__init__(**kwarg)
|
||||
self._app = app
|
||||
self._app.connect("profile-changed", self.on_profile_changed)
|
||||
|
||||
handlers = {"on_apply": self.on_apply,
|
||||
"on_close": lambda b: self.popdown()}
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}epg{SEP}settings.glade", handlers)
|
||||
self.add(builder.get_object("main_box"))
|
||||
|
||||
self._http_src_button = builder.get_object("http_src_button")
|
||||
self._xml_src_button = builder.get_object("xml_src_button")
|
||||
self._dat_src_button = builder.get_object("dat_src_button")
|
||||
self._interval_button = builder.get_object("interval_button")
|
||||
self._url_entry = builder.get_object("url_entry")
|
||||
self._dat_path_box = builder.get_object("dat_path_box")
|
||||
|
||||
self.init()
|
||||
|
||||
def init(self):
|
||||
settings = self._app.app_settings
|
||||
src = settings.epg_source
|
||||
if src is EpgSource.HTTP:
|
||||
self._http_src_button.set_active(True)
|
||||
elif src is EpgSource.XML:
|
||||
self._xml_src_button.set_active(True)
|
||||
else:
|
||||
self._dat_src_button.set_active(True)
|
||||
|
||||
self._interval_button.set_value(settings.epg_update_interval)
|
||||
self._url_entry.set_text(settings.epg_xml_source)
|
||||
self._dat_path_box.set_active_id(settings.epg_dat_path)
|
||||
|
||||
def on_apply(self, button):
|
||||
settings = self._app.app_settings
|
||||
if self._http_src_button.get_active():
|
||||
settings.epg_source = EpgSource.HTTP
|
||||
elif self._xml_src_button.get_active():
|
||||
settings.epg_source = EpgSource.XML
|
||||
else:
|
||||
settings.epg_source = EpgSource.DAT
|
||||
|
||||
settings.epg_update_interval = self._interval_button.get_value()
|
||||
settings.epg_xml_source = self._url_entry.get_text()
|
||||
settings.epg_dat_path = self._dat_path_box.get_active_id()
|
||||
self.popdown()
|
||||
|
||||
self._app.change_action_state("display_epg", GLib.Variant.new_boolean(True))
|
||||
|
||||
def on_profile_changed(self, app, p):
|
||||
self.init()
|
||||
|
||||
|
||||
class EpgTool(Gtk.Box):
|
||||
def __init__(self, app, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._current_bq = None
|
||||
self._app = app
|
||||
self._app.connect("fav-changed", self.on_service_changed)
|
||||
self._app.connect("bouquet-changed", self.on_bouquet_changed)
|
||||
|
||||
handlers = {"on_epg_press": self.on_epg_press,
|
||||
"on_timer_add": self.on_timer_add,
|
||||
"on_epg_filter_changed": self.on_epg_filter_changed,
|
||||
"on_epg_filter_toggled": self.on_epg_filter_toggled,
|
||||
"on_view_query_tooltip": self.on_view_query_tooltip,
|
||||
"on_multi_epg_toggled": self.on_multi_epg_toggled}
|
||||
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}epg{SEP}tab.glade", handlers)
|
||||
|
||||
self._view = builder.get_object("epg_view")
|
||||
self._model = builder.get_object("epg_model")
|
||||
self._filter_model = builder.get_object("epg_filter_model")
|
||||
self._filter_model.set_visible_func(self.epg_filter_function)
|
||||
self._filter_entry = builder.get_object("epg_filter_entry")
|
||||
self._multi_epg_button = builder.get_object("multi_epg_button")
|
||||
self._event_count_label = builder.get_object("event_count_label")
|
||||
self.pack_start(builder.get_object("epg_frame"), True, True, 0)
|
||||
# Custom sort function.
|
||||
self._view.get_model().set_sort_func(2, self.time_sort_func, 2)
|
||||
|
||||
self.show()
|
||||
|
||||
def on_timer_add(self, action=None, value=None):
|
||||
model, paths = self._view.get_selection().get_selected_rows()
|
||||
p_count = len(paths)
|
||||
|
||||
if p_count == 1:
|
||||
dialog = TimerTool.TimerDialog(self._app.app_window, TimerTool.TimerAction.EVENT, model[paths][-1])
|
||||
response = dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
gen = self.write_timers_list([dialog.get_request()])
|
||||
GLib.idle_add(lambda: next(gen, False))
|
||||
dialog.destroy()
|
||||
elif p_count > 1:
|
||||
if show_dialog(DialogType.QUESTION, self._app.app_window,
|
||||
"Add timers for selected events?") != Gtk.ResponseType.OK:
|
||||
return True
|
||||
|
||||
self.add_timers_list((model[p][-1] for p in paths))
|
||||
else:
|
||||
self._app.show_error_message("No selected item!")
|
||||
|
||||
def add_timers_list(self, paths):
|
||||
ref_str = "timeraddbyeventid?sRef={}&eventid={}&justplay=0"
|
||||
refs = [ref_str.format(quote(ev.get("e2eventservicereference", "")), ev.get("e2eventid", "")) for ev in paths]
|
||||
|
||||
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._app.send_http_request(HttpAPI.Request.TIMER, ref, lambda x: tasks.pop())
|
||||
yield True
|
||||
|
||||
while tasks:
|
||||
yield True
|
||||
|
||||
self._app.emit("change-page", Page.TIMERS.value)
|
||||
|
||||
def on_epg_press(self, view, event):
|
||||
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(view.get_model()) > 0:
|
||||
self.on_timer_add()
|
||||
|
||||
def on_service_changed(self, app, ref):
|
||||
if app.page is Page.EPG:
|
||||
if self._multi_epg_button.get_active():
|
||||
ref += ":"
|
||||
path = next((r.path for r in self._model if r[-1].get("e2eventservicereference", None) == ref), None)
|
||||
scroll_to(path, self._view) if path else None
|
||||
else:
|
||||
self._app.wait_dialog.show()
|
||||
self._app.send_http_request(HttpAPI.Request.EPG, quote(ref), self.update_epg_data)
|
||||
|
||||
@run_idle
|
||||
def update_epg_data(self, epg):
|
||||
self._event_count_label.set_text("0")
|
||||
self._model.clear()
|
||||
list(map(self._model.append, (self.get_event(e) for e in epg.get("event_list", [])
|
||||
if e.get("e2eventid", "").isdigit())))
|
||||
self._event_count_label.set_text(str(len(self._model)))
|
||||
self._app.wait_dialog.hide()
|
||||
|
||||
@staticmethod
|
||||
def get_event(event, show_day=True):
|
||||
t_str = f"{'%a, ' if show_day else ''}%x, %H:%M"
|
||||
s_name = event.get("e2eventservicename", "")
|
||||
title = event.get("e2eventtitle", "") or ""
|
||||
desc = event.get("e2eventdescription", "") or ""
|
||||
desc = desc.strip()
|
||||
|
||||
start = int(event.get("e2eventstart", "0"))
|
||||
start_time = datetime.fromtimestamp(start)
|
||||
end_time = datetime.fromtimestamp(start + int(event.get("e2eventduration", "0")))
|
||||
ev_time = f"{start_time.strftime(t_str)} - {end_time.strftime('%H:%M')}"
|
||||
|
||||
return EpgEvent(s_name, title, ev_time, desc, event)
|
||||
|
||||
def on_epg_filter_changed(self, entry):
|
||||
self._filter_model.refilter()
|
||||
|
||||
def on_epg_filter_toggled(self, button):
|
||||
if not button.get_active():
|
||||
self._filter_entry.set_text("")
|
||||
|
||||
def epg_filter_function(self, model, itr, data):
|
||||
txt = self._filter_entry.get_text().upper()
|
||||
return next((s for s in model.get(itr, 0, 1, 2, 3) if txt in s.upper()), False)
|
||||
|
||||
def time_sort_func(self, model, iter1, iter2, column):
|
||||
""" Custom sort function for time column. """
|
||||
event1 = model.get_value(iter1, 4)
|
||||
event2 = model.get_value(iter2, 4)
|
||||
|
||||
return int(event1.get("e2eventstart", "0")) - int(event2.get("e2eventstart", "0"))
|
||||
|
||||
def on_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
|
||||
dst = view.get_dest_row_at_pos(x, y)
|
||||
if not dst:
|
||||
return False
|
||||
|
||||
path, pos = dst
|
||||
model = view.get_model()
|
||||
data = model[path][-1]
|
||||
desc = data.get("e2eventdescription", "") or ""
|
||||
ext_desc = data.get("e2eventdescriptionextended", "") or ""
|
||||
|
||||
tooltip.set_text(ext_desc if ext_desc else desc)
|
||||
view.set_tooltip_row(tooltip, path)
|
||||
|
||||
return True
|
||||
|
||||
def on_multi_epg_toggled(self, button):
|
||||
self._model.clear()
|
||||
self._event_count_label.set_text("0")
|
||||
|
||||
if button.get_active():
|
||||
self.get_multi_epg()
|
||||
|
||||
def on_bouquet_changed(self, app, bq):
|
||||
self._current_bq = bq
|
||||
if app.page is Page.EPG and self._multi_epg_button.get_active():
|
||||
self.get_multi_epg()
|
||||
|
||||
def get_multi_epg(self):
|
||||
if not self._current_bq:
|
||||
return
|
||||
|
||||
self._app.wait_dialog.show()
|
||||
bq = self._app.current_bouquet_files.get(self._current_bq, None)
|
||||
api = self._app.http_api
|
||||
|
||||
if bq and api:
|
||||
tm = datetime.now().timestamp()
|
||||
req = quote(f'FROM BOUQUET "userbouquet.{bq}.{self._current_bq.split(":")[-1]}"&time={tm}')
|
||||
api.send(HttpAPI.Request.EPG_MULTI, f'1:7:1:0:0:0:0:0:0:0:{req}', self.update_epg_data, timeout=15)
|
||||
|
||||
|
||||
class EpgDialog:
|
||||
|
||||
def __init__(self, transient, settings, services, bouquet, fav_model, bouquet_name):
|
||||
def __init__(self, app, bouquet, bouquet_name):
|
||||
|
||||
handlers = {"on_close_dialog": self.on_close_dialog,
|
||||
"on_apply": self.on_apply,
|
||||
@@ -80,12 +391,14 @@ class EpgDialog:
|
||||
"on_enable_filtering_switch": self.on_enable_filtering_switch,
|
||||
"on_update_on_start_switch": self.on_update_on_start_switch,
|
||||
"on_field_icon_press": self.on_field_icon_press,
|
||||
"on_key_release": self.on_key_release}
|
||||
"on_key_press": self.on_key_press,
|
||||
"on_bq_cursor_changed": self.on_bq_cursor_changed}
|
||||
|
||||
self._app = app
|
||||
self._services = {}
|
||||
self._ex_services = services
|
||||
self._ex_fav_model = fav_model
|
||||
self._settings = settings
|
||||
self._ex_services = self._app.current_services
|
||||
self._ex_fav_model = self._app.fav_view.get_model()
|
||||
self._settings = self._app.app_settings
|
||||
self._bouquet = bouquet
|
||||
self._bouquet_name = bouquet_name
|
||||
self._current_ref = []
|
||||
@@ -93,13 +406,12 @@ class EpgDialog:
|
||||
self._use_web_source = False
|
||||
self._update_epg_data_on_start = False
|
||||
self._refs_source = RefsSource.SERVICES
|
||||
self._show_tooltips = True
|
||||
self._download_xml_is_active = False
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "epg.glade", handlers)
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}epg{SEP}dialog.glade", handlers)
|
||||
|
||||
self._dialog = builder.get_object("epg_dialog_window")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._dialog.set_transient_for(self._app.app_window)
|
||||
self._source_view = builder.get_object("source_view")
|
||||
self._bouquet_view = builder.get_object("bouquet_view")
|
||||
self._bouquet_model = builder.get_object("bouquet_list_store")
|
||||
@@ -107,12 +419,12 @@ class EpgDialog:
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("info_bar_message_label")
|
||||
self._assign_ref_popup_item = builder.get_object("bouquet_assign_ref_popup_item")
|
||||
self._left_header_box = builder.get_object("left_header_box")
|
||||
self._left_action_box = builder.get_object("left_action_box")
|
||||
self._xml_download_progress_bar = builder.get_object("xml_download_progress_bar")
|
||||
# Filter
|
||||
self._filter_bar = builder.get_object("filter_bar")
|
||||
self._filter_bar.bind_property("search-mode-enabled", self._filter_bar, "visible")
|
||||
self._filter_entry = builder.get_object("filter_entry")
|
||||
self._filter_auto_switch = builder.get_object("filter_auto_switch")
|
||||
self._services_filter_model = builder.get_object("services_filter_model")
|
||||
self._services_filter_model.set_visible_func(self.services_filter_function)
|
||||
# Info
|
||||
@@ -132,6 +444,19 @@ class EpgDialog:
|
||||
self._epg_dat_stb_path_entry = builder.get_object("epg_dat_stb_path_entry")
|
||||
self._update_on_start_switch = builder.get_object("update_on_start_switch")
|
||||
self._epg_dat_source_box = builder.get_object("epg_dat_source_box")
|
||||
|
||||
if IS_GNOME_SESSION:
|
||||
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True, title="EPG",
|
||||
subtitle=get_message("List configuration"))
|
||||
self._dialog.set_titlebar(header_bar)
|
||||
builder.get_object("left_action_box").reparent(header_bar)
|
||||
right_box = builder.get_object("right_action_box")
|
||||
builder.get_object("main_actions_box").remove(right_box)
|
||||
header_bar.pack_end(right_box)
|
||||
builder.get_object("toolbar_box").set_visible(False)
|
||||
|
||||
self._app.connect("epg-dat-downloaded", self.on_epg_dat_downloaded)
|
||||
|
||||
# Setting the last size of the dialog window
|
||||
window_size = self._settings.get("epg_tool_window_size")
|
||||
if window_size:
|
||||
@@ -153,6 +478,7 @@ class EpgDialog:
|
||||
|
||||
self._bouquet.clear()
|
||||
list(map(self._bouquet.append, [r[Column.FAV_ID] for r in self._bouquet_model]))
|
||||
p_ids = {r[Column.FAV_ID]: r[Column.FAV_POS] for r in self._bouquet_model}
|
||||
for index, row in enumerate(self._ex_fav_model):
|
||||
fav_id = self._bouquet[index]
|
||||
row[Column.FAV_ID] = fav_id
|
||||
@@ -160,15 +486,19 @@ class EpgDialog:
|
||||
old_fav_id = self._services[fav_id]
|
||||
srv = self._ex_services.pop(old_fav_id, None)
|
||||
if srv:
|
||||
self._ex_services[fav_id] = srv._replace(fav_id=fav_id)
|
||||
picon_id = p_ids.get(fav_id) or srv.picon_id
|
||||
self._ex_services[fav_id] = srv._replace(fav_id=fav_id, picon_id=picon_id)
|
||||
self._dialog.destroy()
|
||||
|
||||
@run_idle
|
||||
def on_update(self, item=None):
|
||||
self.clear_data()
|
||||
self.init_options()
|
||||
gen = self.init_data()
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
if self._update_epg_data_on_start:
|
||||
self.download_epg_from_stb()
|
||||
else:
|
||||
gen = self.init_data()
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def clear_data(self):
|
||||
self._services_model.clear()
|
||||
@@ -184,23 +514,17 @@ class EpgDialog:
|
||||
|
||||
refs = None
|
||||
if self._enable_dat_filter:
|
||||
if self._update_epg_data_on_start:
|
||||
try:
|
||||
self.download_epg_from_stb()
|
||||
except OSError as e:
|
||||
self.show_info_message("Download epg.dat file error: {}".format(e), Gtk.MessageType.ERROR)
|
||||
return
|
||||
yield True
|
||||
|
||||
try:
|
||||
refs = EPG.get_epg_refs(self._epg_dat_path_entry.get_text() + "epg.dat")
|
||||
except FileNotFoundError as e:
|
||||
self.show_info_message("Read data error: {}".format(e), Gtk.MessageType.ERROR)
|
||||
epg_reader = EPG.DatReader(f"{self._epg_dat_path_entry.get_text()}epg.dat")
|
||||
epg_reader.read()
|
||||
refs = epg_reader.get_refs()
|
||||
except (OSError, ValueError) as e:
|
||||
self.show_info_message(f"Read data error: {e}", Gtk.MessageType.ERROR)
|
||||
return
|
||||
yield True
|
||||
|
||||
if self._refs_source is RefsSource.SERVICES:
|
||||
self.init_lamedb_source(refs)
|
||||
yield from self.init_lamedb_source(refs)
|
||||
elif self._refs_source is RefsSource.XML:
|
||||
xml_gen = self.init_xml_source(refs)
|
||||
try:
|
||||
@@ -225,8 +549,15 @@ class EpgDialog:
|
||||
s_types = (BqServiceType.MARKER.value, BqServiceType.IPTV.value)
|
||||
filtered = filter(None, [srvs.get(ref) for ref in refs]) if refs else filter(
|
||||
lambda s: s.service_type not in s_types, self._ex_services.values())
|
||||
list(map(self._services_model.append, map(lambda s: (s.service, s.fav_id), filtered)))
|
||||
|
||||
factor = self._app.DEL_FACTOR / 4
|
||||
for index, srv in enumerate(filtered):
|
||||
self._services_model.append((srv.service, srv.pos, srv.fav_id, srv.picon_id))
|
||||
if index % factor == 0:
|
||||
yield True
|
||||
|
||||
self.update_source_count_info()
|
||||
yield True
|
||||
|
||||
def init_xml_source(self, refs):
|
||||
path = self._epg_dat_path_entry.get_text() if self._use_web_source else self._xml_chooser_button.get_filename()
|
||||
@@ -274,7 +605,7 @@ class EpgDialog:
|
||||
|
||||
path = tfp.name.rstrip(".gz")
|
||||
except (HTTPError, URLError) as e:
|
||||
raise ValueError("{} {}".format(get_message("Download XML file error."), e))
|
||||
raise ValueError(f"{get_message('Download XML file error.')} {e}")
|
||||
else:
|
||||
try:
|
||||
with open(path, "wb") as f_out:
|
||||
@@ -282,7 +613,7 @@ class EpgDialog:
|
||||
shutil.copyfileobj(f, f_out)
|
||||
os.remove(tfp.name)
|
||||
except Exception as e:
|
||||
raise ValueError("{} {}".format(get_message("Unpacking data error."), e))
|
||||
raise ValueError(f"{get_message('Unpacking data error.')} {e}")
|
||||
finally:
|
||||
self._download_xml_is_active = False
|
||||
self.update_active_header_elements(True)
|
||||
@@ -291,16 +622,22 @@ class EpgDialog:
|
||||
s_refs, info = ChannelsParser.get_refs_from_xml(path)
|
||||
yield True
|
||||
except Exception as e:
|
||||
raise ValueError("{} {}".format(get_message("XML parsing error:"), e))
|
||||
raise ValueError(f"{get_message('XML parsing error:')} {e}")
|
||||
else:
|
||||
if refs:
|
||||
s_refs = filter(lambda x: x.num in refs, s_refs)
|
||||
list(map(lambda s: self._services_model.append((s.name, s.data)), s_refs))
|
||||
|
||||
factor = self._app.DEL_FACTOR / 4
|
||||
for index, srv in enumerate(s_refs):
|
||||
self._services_model.append((srv.name, " ", srv.data, ""))
|
||||
if index % factor == 0:
|
||||
yield True
|
||||
|
||||
self.update_source_info(info)
|
||||
self.update_source_count_info()
|
||||
yield True
|
||||
|
||||
def on_key_release(self, view, event):
|
||||
def on_key_press(self, view, event):
|
||||
""" Handling keystrokes """
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
@@ -313,6 +650,13 @@ class EpgDialog:
|
||||
elif ctrl and key is KeyboardKey.V:
|
||||
self.on_assign_ref()
|
||||
|
||||
def on_bq_cursor_changed(self, view):
|
||||
if self._filter_bar.get_visible() and self._filter_auto_switch.get_active():
|
||||
path, column = view.get_cursor()
|
||||
model = view.get_model()
|
||||
if path:
|
||||
self._filter_entry.set_text(model[path][Column.FAV_SERVICE] or "")
|
||||
|
||||
@run_idle
|
||||
def on_save_to_xml(self, item):
|
||||
response = show_dialog(DialogType.CHOOSER, self._dialog, settings=self._settings)
|
||||
@@ -348,7 +692,7 @@ class EpgDialog:
|
||||
for row in self._services_model:
|
||||
name = re.sub("\\W+", "", str(row[0])).upper()
|
||||
name = name.translate(tr) if use_cyrillic else name
|
||||
source[name] = row[1]
|
||||
source[name] = row
|
||||
|
||||
success_count = 0
|
||||
not_founded = {}
|
||||
@@ -378,7 +722,11 @@ class EpgDialog:
|
||||
get_message("Count of successfully configured services:"),
|
||||
success_count), Gtk.MessageType.INFO)
|
||||
|
||||
def assign_data(self, row, ref, show_error=False):
|
||||
def assign_refs(self, model, paths, data):
|
||||
[self.assign_data(model[p], data) for p in paths]
|
||||
self.update_epg_count()
|
||||
|
||||
def assign_data(self, row, data, show_error=False):
|
||||
if row[Column.FAV_TYPE] != BqServiceType.IPTV.value:
|
||||
if not show_error:
|
||||
self.show_info_message(get_message("Not allowed in this context!"), Gtk.MessageType.ERROR)
|
||||
@@ -386,18 +734,28 @@ class EpgDialog:
|
||||
|
||||
fav_id = row[Column.FAV_ID]
|
||||
fav_id_data = fav_id.split(":")
|
||||
fav_id_data[3:7] = ref.split(":")
|
||||
fav_id_data[3:7] = data[-2].split(":")
|
||||
new_fav_id = ":".join(fav_id_data)
|
||||
service = self._services.pop(fav_id, None)
|
||||
if service:
|
||||
self._services[new_fav_id] = service
|
||||
row[Column.FAV_ID] = new_fav_id
|
||||
row[Column.FAV_LOCKED] = EPG_ICON
|
||||
row[Column.FAV_TOOLTIP] = ":".join(fav_id_data[:10]) if self._show_tooltips else None
|
||||
if data[-1]:
|
||||
row[Column.FAV_POS] = data[-1]
|
||||
p_data = data[-1].split("_")
|
||||
if p_data:
|
||||
fav_id_data[2] = p_data[2]
|
||||
pos = f"({data[1] if self._refs_source is RefsSource.SERVICES else 'XML'})"
|
||||
src = f"{get_message('EPG source')}: {(GLib.markup_escape_text(data[0] or ''))} {pos}"
|
||||
row[Column.FAV_TOOLTIP] = f"{get_message('Service reference')}: {':'.join(fav_id_data[:10])}\n{src}"
|
||||
|
||||
def on_filter_toggled(self, button: Gtk.ToggleButton):
|
||||
self._filter_bar.set_search_mode(button.get_active())
|
||||
def on_filter_toggled(self, button):
|
||||
self._filter_bar.set_visible(button.get_active())
|
||||
if not button.get_active():
|
||||
self._filter_entry.set_text("")
|
||||
|
||||
@run_with_delay(1)
|
||||
def on_filter_changed(self, entry):
|
||||
self._services_filter_model.refilter()
|
||||
|
||||
@@ -412,13 +770,12 @@ class EpgDialog:
|
||||
model, paths = self._source_view.get_selection().get_selected_rows()
|
||||
self._current_ref.clear()
|
||||
if paths:
|
||||
self._current_ref.append(model[paths][1])
|
||||
self._current_ref.append(model[paths][:])
|
||||
|
||||
def on_assign_ref(self, item=None):
|
||||
if self._current_ref:
|
||||
model, paths = self._bouquet_view.get_selection().get_selected_rows()
|
||||
self.assign_data(model[paths], self._current_ref.pop())
|
||||
self.update_epg_count()
|
||||
self.assign_refs(model, paths, self._current_ref.pop())
|
||||
|
||||
@run_idle
|
||||
def on_reset(self, item):
|
||||
@@ -466,7 +823,7 @@ class EpgDialog:
|
||||
|
||||
@run_idle
|
||||
def update_active_header_elements(self, state):
|
||||
self._left_header_box.set_sensitive(state)
|
||||
self._left_action_box.set_sensitive(state)
|
||||
self._xml_download_progress_bar.set_visible(not state)
|
||||
self._source_info_label.set_text("" if state else "Downloading XML:")
|
||||
|
||||
@@ -481,7 +838,7 @@ class EpgDialog:
|
||||
# ***************** Drag-and-drop *********************#
|
||||
|
||||
def init_drag_and_drop(self):
|
||||
""" Enable drag-and-drop """
|
||||
""" Enable drag-and-drop. """
|
||||
target = []
|
||||
self._source_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, target, Gdk.DragAction.COPY)
|
||||
self._source_view.drag_source_add_text_targets()
|
||||
@@ -494,17 +851,22 @@ class EpgDialog:
|
||||
if selection.count_selected_rows() > 1:
|
||||
view.do_toggle_cursor_row(view)
|
||||
|
||||
def on_drag_data_get(self, view: Gtk.TreeView, drag_context, data, info, time):
|
||||
def on_drag_data_get(self, view, drag_context, data, info, time):
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if paths:
|
||||
val = model.get_value(model.get_iter(paths), 1)
|
||||
data.set_text(val, -1)
|
||||
s_data = model[paths][:]
|
||||
if all(s_data[:-1]):
|
||||
data.set_text("::::".join(s_data), -1)
|
||||
else:
|
||||
self.show_info_message(get_message("Source error!"), Gtk.MessageType.ERROR)
|
||||
|
||||
def on_drag_data_received(self, view: Gtk.TreeView, drag_context, x, y, data, info, time):
|
||||
def on_drag_data_received(self, view, drag_context, x, y, data, info, time):
|
||||
path, pos = view.get_dest_row_at_pos(x, y)
|
||||
model = view.get_model()
|
||||
self.assign_data(model[path], data.get_text())
|
||||
self.update_epg_count()
|
||||
data = data.get_text()
|
||||
if data:
|
||||
data = data.split("::::")
|
||||
self.assign_refs(model, path, data)
|
||||
return False
|
||||
|
||||
# ***************** Options *********************#
|
||||
@@ -566,10 +928,19 @@ class EpgDialog:
|
||||
|
||||
# ***************** Downloads *********************#
|
||||
|
||||
def on_epg_dat_downloaded(self, app, value):
|
||||
gen = self.init_data()
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
@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)
|
||||
try:
|
||||
download_data(settings=self._settings, download_type=DownloadType.EPG, callback=print)
|
||||
except Exception as e:
|
||||
GLib.idle_add(self.show_info_message, f"Download epg.dat file error: {e}", Gtk.MessageType.ERROR)
|
||||
else:
|
||||
GLib.idle_add(self._app.emit, "epg-dat-downloaded", None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
356
app/ui/epg/settings.glade
Normal file
356
app/ui/epg/settings.glade
Normal file
@@ -0,0 +1,356 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkAdjustment" id="interval_adjustment">
|
||||
<property name="lower">3</property>
|
||||
<property name="upper">60</property>
|
||||
<property name="value">3</property>
|
||||
<property name="step_increment">1</property>
|
||||
<property name="page_increment">10</property>
|
||||
</object>
|
||||
<object class="GtkBox" id="main_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="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="label" translatable="yes">Source:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="src_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="source_selection_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="layout_style">expand</property>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="http_src_button">
|
||||
<property name="label" translatable="yes">Receiver</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">dat_src_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="xml_src_button">
|
||||
<property name="label" translatable="yes">XML TV</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">dat_src_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="dat_src_button">
|
||||
<property name="label" translatable="yes">*.dat file</property>
|
||||
<property name="visible">False</property>
|
||||
<property name="sensitive">False</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">http_src_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="interval_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="interval_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Update interval (sec):</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="interval_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="max_width_chars">4</property>
|
||||
<property name="adjustment">interval_adjustment</property>
|
||||
<property name="climb_rate">1</property>
|
||||
<property name="numeric">True</property>
|
||||
<property name="value">3</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">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="xml_source_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="sensitive" bind-source="xml_src_button" bind-property="active"/>
|
||||
<child>
|
||||
<object class="GtkLabel" id="url_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Url to *.xml.gz file:</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="url_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">network-transmit-receive-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="input_purpose">url</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="download_interval_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="download_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Update:</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="download_interval_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="active">0</property>
|
||||
<property name="active_id">daily</property>
|
||||
<items>
|
||||
<item id="daily" translatable="yes">Daily</item>
|
||||
</items>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</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="GtkBox" id="dat_source_box">
|
||||
<property name="visible">False</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="sensitive" bind-source="dat_src_button" bind-property="active"/>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">STB path:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="dat_path_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="active">0</property>
|
||||
<property name="active_id">/etc/enigma2</property>
|
||||
<items>
|
||||
<item id="/etc/enigma2/">/etc/enigma2/</item>
|
||||
<item id="/media/hdd/">/media/hdd/</item>
|
||||
<item id="/media/usb/">/media/usb/</item>
|
||||
<item id="/media/mmc/">/media/mmc/</item>
|
||||
<item id="/media/cf/">/media/cf/</item>
|
||||
</items>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="actions_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="layout_style">expand</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="apply_button">
|
||||
<property name="label" translatable="yes">Apply</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<signal name="clicked" handler="on_apply" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="close_button">
|
||||
<property name="label" translatable="yes">Close</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<signal name="clicked" handler="on_close" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">16</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
428
app/ui/epg/tab.glade
Normal file
428
app/ui/epg/tab.glade
Normal file
@@ -0,0 +1,428 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkListStore" id="epg_model">
|
||||
<columns>
|
||||
<!-- column-name service -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name title -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name time -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name description -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name data -->
|
||||
<column type="PyObject"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkTreeModelFilter" id="epg_filter_model">
|
||||
<property name="child_model">epg_model</property>
|
||||
</object>
|
||||
<object class="GtkTreeModelSort" id="epg_sort_model">
|
||||
<property name="model">epg_filter_model</property>
|
||||
</object>
|
||||
<object class="GtkFrame" id="epg_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0.49000000953674316</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="epg_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">2</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="epg_action_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="src_combo_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">EPG source</property>
|
||||
<property name="active">0</property>
|
||||
<property name="has_entry">True</property>
|
||||
<property name="active_id">0</property>
|
||||
<items>
|
||||
<item id="0" translatable="yes">Receiver</item>
|
||||
</items>
|
||||
<child internal-child="entry">
|
||||
<object class="GtkEntry">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="width_chars">10</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="GtkToggleButton" id="epg_filter_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Filter</property>
|
||||
<signal name="toggled" handler="on_epg_filter_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="epg_filter_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-find-replace-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="epg_add_timer_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Add timer</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_timer_add" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="add_timer_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">alarm-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child type="center">
|
||||
<object class="GtkToggleButton" id="multi_epg_button">
|
||||
<property name="label" translatable="yes">Multi EPG</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<signal name="toggled" handler="on_multi_epg_toggled" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="epg_fs_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="epg_filter_entry">
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">edit-find-replace-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="primary_icon_sensitive">False</property>
|
||||
<property name="visible" bind-source="epg_filter_button" bind-property="active"/>
|
||||
<signal name="search-changed" handler="on_epg_filter_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="fav_search_box">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="epg_search_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">edit-find-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="primary_icon_sensitive">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="epg_search_down_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<child>
|
||||
<object class="GtkArrow" id="epg_down_arrow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="arrow_type">down</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="epg_search_up_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<child>
|
||||
<object class="GtkArrow" id="epg_up_arrow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="arrow_type">up</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="group"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="epg_view_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="epg_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">epg_sort_model</property>
|
||||
<property name="rules_hint">True</property>
|
||||
<property name="fixed_height_mode">True</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<property name="enable_grid_lines">both</property>
|
||||
<property name="tooltip_column">3</property>
|
||||
<signal name="button-press-event" handler="on_epg_press" swapped="no"/>
|
||||
<signal name="query-tooltip" handler="on_view_query_tooltip" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="epg_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_service_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="fixed_width">100</property>
|
||||
<property name="min_width">40</property>
|
||||
<property name="title" translatable="yes">Service</property>
|
||||
<property name="alignment">0.49000000953674316</property>
|
||||
<property name="sort_column_id">0</property>
|
||||
<property name="visible" bind-source="multi_epg_button" bind-property="active"/>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_service_renderer">
|
||||
<property name="xpad">5</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_title_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="fixed_width">170</property>
|
||||
<property name="min_width">50</property>
|
||||
<property name="title" translatable="yes">Title</property>
|
||||
<property name="alignment">0.49000000953674316</property>
|
||||
<property name="sort_column_id">1</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_title_renderer">
|
||||
<property name="xpad">5</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_time_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="fixed_width">210</property>
|
||||
<property name="min_width">50</property>
|
||||
<property name="title" translatable="yes">Time</property>
|
||||
<property name="alignment">0.49000000953674316</property>
|
||||
<property name="sort_column_id">2</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_time_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_desc_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="fixed_width">100</property>
|
||||
<property name="min_width">50</property>
|
||||
<property name="title" translatable="yes">Description</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.49000000953674316</property>
|
||||
<property name="sort_column_id">3</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_desc_renderer">
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">3</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>
|
||||
<child>
|
||||
<object class="GtkBox" id="status_box">
|
||||
<property name="height_request">26</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="event_count_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">document-properties</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="event_count_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
<property name="width_chars">4</property>
|
||||
<property name="xalign">0</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">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="epg_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">EPG</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
1172
app/ui/ftp.glade
1172
app/ui/ftp.glade
File diff suppressed because it is too large
Load Diff
490
app/ui/ftp.py
490
app/ui/ftp.py
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -27,26 +27,189 @@
|
||||
|
||||
|
||||
""" Simple FTP client module. """
|
||||
import stat
|
||||
import subprocess
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from enum import IntEnum
|
||||
from ftplib import all_errors
|
||||
from io import TextIOWrapper, BytesIO
|
||||
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.commons import log, run_task, run_idle, get_size_from_bytes
|
||||
from app.connections import UtfFTP
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_builder
|
||||
from app.settings import IS_LINUX, IS_DARWIN, IS_WIN, SEP
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_builder, get_message
|
||||
from app.ui.main_helper import on_popup_menu
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, IS_GNOME_SESSION, Page
|
||||
|
||||
File = namedtuple("File", ["icon", "name", "size", "date", "attr", "extra"])
|
||||
|
||||
|
||||
class BaseDialog(Gtk.Dialog):
|
||||
""" Base class for additional FTP dialogs. """
|
||||
|
||||
def __init__(self, title, use_header_bar=0, *args, **kwargs):
|
||||
super().__init__(title=title, use_header_bar=use_header_bar, *args, **kwargs)
|
||||
|
||||
self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK)
|
||||
self.set_modal(True)
|
||||
self.set_skip_pager_hint(True)
|
||||
self.set_skip_taskbar_hint(True)
|
||||
self.set_position(Gtk.PositionType.BOTTOM)
|
||||
self.set_default_icon_name("document-properties-symbolic")
|
||||
|
||||
|
||||
class TextEditDialog(BaseDialog):
|
||||
""" Simple text edit dialog. """
|
||||
|
||||
def __init__(self, path, use_header_bar=0, *args, **kwargs):
|
||||
super().__init__(title=f"DemonEditor [{path}]", use_header_bar=use_header_bar, *args, **kwargs)
|
||||
|
||||
content_box = self.get_content_area()
|
||||
self._search_entry = Gtk.SearchEntry(visible=True, primary_icon_name="system-search-symbolic")
|
||||
self._search_entry.connect("search-changed", self.on_search_changed)
|
||||
|
||||
if use_header_bar:
|
||||
bar = self.get_header_bar()
|
||||
bar.pack_start(self._search_entry)
|
||||
bar.set_title("DemonEditor")
|
||||
bar.set_subtitle(path)
|
||||
else:
|
||||
search_bar = Gtk.SearchBar(visible=True)
|
||||
search_bar.add(self._search_entry)
|
||||
search_bar.set_search_mode(True)
|
||||
content_box.pack_start(search_bar, False, False, 0)
|
||||
|
||||
scrolled_window = Gtk.ScrolledWindow(hexpand=True, vexpand=True,
|
||||
min_content_width=720,
|
||||
min_content_height=320)
|
||||
content_box.pack_start(scrolled_window, True, True, 0)
|
||||
|
||||
try:
|
||||
import gi
|
||||
|
||||
gi.require_version("GtkSource", "3.0")
|
||||
from gi.repository import GtkSource
|
||||
except (ImportError, ValueError) as e:
|
||||
self._text_view = Gtk.TextView()
|
||||
self._buf = self._text_view.get_buffer()
|
||||
log(e)
|
||||
else:
|
||||
self._text_view = GtkSource.View(show_line_numbers=True, show_line_marks=True)
|
||||
self._buf = self._text_view.get_buffer()
|
||||
self._buf.set_highlight_syntax(True)
|
||||
self._buf.set_highlight_matching_brackets(True)
|
||||
lang_manager = GtkSource.LanguageManager.new()
|
||||
self._buf.set_language(lang_manager.guess_language(path))
|
||||
# Style
|
||||
self._buf.set_style_scheme(GtkSource.StyleSchemeManager().get_default().get_scheme("tango"))
|
||||
|
||||
self._tag_found = self._buf.create_tag("found", background="yellow")
|
||||
scrolled_window.add(self._text_view)
|
||||
|
||||
self.show_all()
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return self._buf.get_text(self._buf.get_start_iter(), self._buf.get_end_iter(), include_hidden_chars=True)
|
||||
|
||||
@text.setter
|
||||
def text(self, value):
|
||||
self._buf.set_text(value)
|
||||
|
||||
def on_search_changed(self, entry):
|
||||
self._buf.remove_tag(self._tag_found, self._buf.get_start_iter(), self._buf.get_end_iter())
|
||||
cursor_mark = self._buf.get_insert()
|
||||
start = self._buf.get_iter_at_mark(cursor_mark)
|
||||
if start.get_offset() == self._buf.get_char_count():
|
||||
start = self._buf.get_start_iter()
|
||||
|
||||
self.search_and_mark(entry.get_text(), start)
|
||||
|
||||
def search_and_mark(self, text, start, first=True):
|
||||
end = self._buf.get_end_iter()
|
||||
match = start.forward_search(text, 0, end)
|
||||
|
||||
if match is not None:
|
||||
match_start, match_end = match
|
||||
self._buf.apply_tag(self._tag_found, match_start, match_end)
|
||||
if first:
|
||||
self._text_view.scroll_to_iter(match_start, 0.0, False, 0.0, 0.0)
|
||||
GLib.idle_add(self.search_and_mark, text, match_end, False)
|
||||
|
||||
|
||||
class AttributesDialog(BaseDialog):
|
||||
""" Dialog for editing file attributes (permissions). """
|
||||
|
||||
def __init__(self, attrs, use_header_bar=0, *args, **kwargs):
|
||||
super().__init__(title=get_message("Permissions"), use_header_bar=use_header_bar, *args, **kwargs)
|
||||
|
||||
self.set_default_size(360, 100)
|
||||
self.set_resizable(False)
|
||||
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}ftp.glade", use_str=True, objects=("attributes_box",))
|
||||
content_box = self.get_content_area()
|
||||
content_box.pack_start(builder.get_object("attributes_box"), True, True, 0)
|
||||
self._num_value_entry = builder.get_object("num_value_entry")
|
||||
# Buttons.
|
||||
self._owner_read_button = builder.get_object("owner_read_button")
|
||||
self._group_read_button = builder.get_object("group_read_button")
|
||||
self._others_read_button = builder.get_object("others_read_button")
|
||||
self._owner_write_button = builder.get_object("owner_write_button")
|
||||
self._group_write_button = builder.get_object("group_write_button")
|
||||
self._others_write_button = builder.get_object("others_write_button")
|
||||
self._owner_exec_button = builder.get_object("owner_exec_button")
|
||||
self._group_exec_button = builder.get_object("group_exec_button")
|
||||
self._others_exec_button = builder.get_object("others_exec_button")
|
||||
self.init_attrs(attrs)
|
||||
|
||||
for b in (self._owner_read_button, self._group_read_button, self._others_read_button, self._owner_write_button,
|
||||
self._group_write_button, self._others_write_button, self._owner_exec_button, self._group_exec_button,
|
||||
self._others_exec_button):
|
||||
b.connect("toggled", self.update_num_value)
|
||||
|
||||
self.show_all()
|
||||
|
||||
@property
|
||||
def permissions(self):
|
||||
return self._num_value_entry.get_text()
|
||||
|
||||
def init_attrs(self, attrs):
|
||||
# Owner.
|
||||
self._owner_read_button.set_active(attrs[1] != "-")
|
||||
self._owner_write_button.set_active(attrs[2] != "-")
|
||||
self._owner_exec_button.set_active(attrs[3] != "-")
|
||||
# Group.
|
||||
self._group_read_button.set_active(attrs[4] != "-")
|
||||
self._group_write_button.set_active(attrs[5] != "-")
|
||||
self._group_exec_button.set_active(attrs[6] != "-")
|
||||
# Others.
|
||||
self._others_read_button.set_active(attrs[7] != "-")
|
||||
self._others_write_button.set_active(attrs[8] != "-")
|
||||
self._others_exec_button.set_active(attrs[9] != "-")
|
||||
|
||||
self.update_num_value()
|
||||
|
||||
def update_num_value(self, button=None):
|
||||
val = 0
|
||||
val |= stat.S_IRUSR if self._owner_read_button.get_active() else val
|
||||
val |= stat.S_IWUSR if self._owner_write_button.get_active() else val
|
||||
val |= stat.S_IXUSR if self._owner_exec_button.get_active() else val
|
||||
val |= stat.S_IRGRP if self._group_read_button.get_active() else val
|
||||
val |= stat.S_IWGRP if self._group_write_button.get_active() else val
|
||||
val |= stat.S_IXGRP if self._group_exec_button.get_active() else val
|
||||
val |= stat.S_IROTH if self._others_read_button.get_active() else val
|
||||
val |= stat.S_IWOTH if self._others_write_button.get_active() else val
|
||||
val |= stat.S_IXOTH if self._others_exec_button.get_active() else val
|
||||
|
||||
self._num_value_entry.set_text(f"{val:o}")
|
||||
|
||||
|
||||
class FtpClientBox(Gtk.HBox):
|
||||
""" Simple FTP client base class. """
|
||||
ROOT = ".."
|
||||
@@ -69,6 +232,8 @@ class FtpClientBox(Gtk.HBox):
|
||||
self.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
|
||||
self._app = app
|
||||
self._app.connect("data-receive", self.on_receive)
|
||||
self._app.connect("data-send", self.on_send)
|
||||
self._settings = settings
|
||||
self._ftp = None
|
||||
self._select_enabled = True
|
||||
@@ -77,12 +242,18 @@ class FtpClientBox(Gtk.HBox):
|
||||
"on_disconnect": self.on_disconnect,
|
||||
"on_ftp_row_activated": self.on_ftp_row_activated,
|
||||
"on_file_row_activated": self.on_file_row_activated,
|
||||
"on_bookmark_activated": self.on_bookmark_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_ftp_rename": self.on_ftp_rename,
|
||||
"on_ftp_renamed": self.on_ftp_renamed,
|
||||
"on_ftp_attr_change": self.on_ftp_attr_change,
|
||||
"on_ftp_copy": self.on_ftp_copy,
|
||||
"on_file_rename": self.on_file_rename,
|
||||
"on_file_renamed": self.on_file_renamed,
|
||||
"on_file_copy": self.on_file_copy,
|
||||
"on_file_remove": self.on_file_remove,
|
||||
"on_ftp_remove": self.on_ftp_file_remove,
|
||||
"on_bookmark_remove": self.on_bookmark_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,
|
||||
@@ -91,14 +262,16 @@ class FtpClientBox(Gtk.HBox):
|
||||
"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_bookmark_add": self.on_bookmark_add,
|
||||
"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}
|
||||
"on_view_release": self.on_view_release,
|
||||
"on_paned_size_allocate": self.on_paned_size_allocate}
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "ftp.glade", handlers)
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}ftp.glade", handlers)
|
||||
|
||||
self.add(builder.get_object("main_frame"))
|
||||
self.add(builder.get_object("main_ftp_box"))
|
||||
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")
|
||||
@@ -106,13 +279,20 @@ class FtpClientBox(Gtk.HBox):
|
||||
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")
|
||||
self._bookmark_view = builder.get_object("bookmarks_view")
|
||||
self._bookmark_model = builder.get_object("bookmarks_list_store")
|
||||
# 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_actions_box"), "sensitive")
|
||||
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_rename_menu_item"), "sensitive")
|
||||
disconnect_button.bind_property("visible", builder.get_object("ftp_remove_menu_item"), "sensitive")
|
||||
disconnect_button.bind_property("visible", builder.get_object("add_ftp_bookmark_button"), "sensitive")
|
||||
self._connect_button.bind_property("visible", builder.get_object("disconnect_button"), "visible", 4)
|
||||
self._bookmarks_button = builder.get_object("bookmarks_button")
|
||||
self._bookmarks_button.bind_property("active", builder.get_object("bookmarks_box"), "visible")
|
||||
# Force Ctrl
|
||||
self._ftp_view.connect("key-press-event", self._app.force_ctrl)
|
||||
self._file_view.connect("key-press-event", self._app.force_ctrl)
|
||||
@@ -128,8 +308,17 @@ class FtpClientBox(Gtk.HBox):
|
||||
self.init_file_data()
|
||||
self.show()
|
||||
|
||||
def on_receive(self, app, page):
|
||||
if page is Page.FTP:
|
||||
self.on_ftp_copy()
|
||||
|
||||
def on_send(self, app, page):
|
||||
if page is Page.FTP:
|
||||
self.on_file_copy()
|
||||
|
||||
@run_task
|
||||
def init_ftp(self):
|
||||
self.init_bookmarks()
|
||||
GLib.idle_add(self._ftp_model.clear)
|
||||
try:
|
||||
if self._ftp:
|
||||
@@ -193,7 +382,7 @@ class FtpClientBox(Gtk.HBox):
|
||||
r_size = self.LINK
|
||||
icon = self._link_icon
|
||||
else:
|
||||
r_size = self.get_size_from_bytes(size)
|
||||
r_size = get_size_from_bytes(size)
|
||||
|
||||
self._file_model.append(File(icon, p.name, r_size, date, str(p.resolve()), size))
|
||||
|
||||
@@ -203,7 +392,7 @@ class FtpClientBox(Gtk.HBox):
|
||||
self._ftp_model.append(File(None, self.ROOT, None, None, self._ftp.pwd(), "0"))
|
||||
|
||||
for f in files:
|
||||
f_data = f.split()
|
||||
f_data = self._ftp.get_file_data(f)
|
||||
f_type = f_data[0][0]
|
||||
is_dir = f_type == "d"
|
||||
is_link = f_type == "l"
|
||||
@@ -217,10 +406,10 @@ class FtpClientBox(Gtk.HBox):
|
||||
r_size = self.LINK
|
||||
icon = self._link_icon
|
||||
else:
|
||||
r_size = self.get_size_from_bytes(size)
|
||||
r_size = 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))
|
||||
date = f"{f_data[5]}, {f_data[6]} {f_data[7]}"
|
||||
self._ftp_model.append(File(icon, f_data[8], r_size, date, f_data[0], size))
|
||||
|
||||
def on_connect(self, item=None):
|
||||
self.init_ftp()
|
||||
@@ -238,6 +427,10 @@ class FtpClientBox(Gtk.HBox):
|
||||
|
||||
if size == self.FOLDER or f_path == self.ROOT:
|
||||
self.init_ftp_data(f_path)
|
||||
elif size == self.LINK:
|
||||
name, sep, f_path = f_path.partition("->")
|
||||
if f_path:
|
||||
self.init_ftp_data(f_path.strip())
|
||||
else:
|
||||
b_size = row[self.Column.EXTRA]
|
||||
if b_size.isdigit() and int(b_size) > self.MAX_SIZE:
|
||||
@@ -259,23 +452,22 @@ class FtpClientBox(Gtk.HBox):
|
||||
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()
|
||||
cmd = [self.get_open_file_cmd(), path]
|
||||
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=IS_WIN).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
|
||||
path = os.path.expanduser("~/Desktop") if not IS_LINUX else None
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="wb", dir=path, delete=not is_darwin) as tf:
|
||||
msg = "Downloading file: {}. Status: {}"
|
||||
with tempfile.NamedTemporaryFile(mode="wb", dir=path, delete=IS_LINUX) 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))
|
||||
@@ -283,12 +475,74 @@ class FtpClientBox(Gtk.HBox):
|
||||
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()
|
||||
|
||||
cmd = [self.get_open_file_cmd(), tf.name]
|
||||
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=IS_WIN).communicate()
|
||||
finally:
|
||||
GLib.idle_add(self._ftp_view.set_sensitive, True)
|
||||
|
||||
def on_ftp_edit(self, renderer):
|
||||
@staticmethod
|
||||
def get_open_file_cmd():
|
||||
if IS_DARWIN:
|
||||
return "open"
|
||||
elif IS_WIN:
|
||||
return "start"
|
||||
return "xdg-open"
|
||||
|
||||
@run_task
|
||||
def on_ftp_edit(self, item=None):
|
||||
path = self.get_ftp_edit_path()
|
||||
if path:
|
||||
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._app.show_error_message("Not allowed in this context!")
|
||||
else:
|
||||
b_size = row[self.Column.EXTRA]
|
||||
if b_size.isdigit() and int(b_size) > self.MAX_SIZE / 5:
|
||||
self._app.show_error_message("The file size is too large!")
|
||||
else:
|
||||
msg = "Retrieving file: {}. Status: {}"
|
||||
io = BytesIO()
|
||||
try:
|
||||
status = self._ftp.retrbinary("RETR " + f_path, io.write)
|
||||
self.update_ftp_info(msg.format(f_path, status))
|
||||
except all_errors as e:
|
||||
self.update_ftp_info(msg.format(f_path, e))
|
||||
else:
|
||||
io.seek(0)
|
||||
self.show_edit_dialog(f_path, TextIOWrapper(io, errors="ignore").read())
|
||||
|
||||
def on_ftp_edited(self, f_path, txt_data):
|
||||
buf = BytesIO()
|
||||
buf.write(txt_data.encode())
|
||||
buf.seek(0)
|
||||
|
||||
msg = "Uploading file: {}. Status: {}"
|
||||
try:
|
||||
status = self._ftp.storbinary(f"STOR {f_path}", buf)
|
||||
self.update_ftp_info(msg.format(f_path, status))
|
||||
except all_errors as e:
|
||||
self.update_ftp_info(msg.format(f_path, e))
|
||||
|
||||
@run_idle
|
||||
def show_edit_dialog(self, f_path, data):
|
||||
dialog = TextEditDialog(f_path, IS_GNOME_SESSION)
|
||||
dialog.text = data
|
||||
ok = Gtk.ResponseType.OK
|
||||
if dialog.run() == ok and show_dialog(DialogType.QUESTION, self._app.app_window) == ok:
|
||||
self.on_ftp_edited(f_path, dialog.text)
|
||||
dialog.destroy()
|
||||
|
||||
def on_ftp_rename(self, renderer):
|
||||
path = self.get_ftp_edit_path()
|
||||
if path:
|
||||
renderer.set_property("editable", True)
|
||||
self._ftp_view.set_cursor(path, self._ftp_view.get_column(0), True)
|
||||
|
||||
def get_ftp_edit_path(self):
|
||||
model, paths = self._ftp_view.get_selection().get_selected_rows()
|
||||
if not paths:
|
||||
return
|
||||
@@ -296,11 +550,9 @@ class FtpClientBox(Gtk.HBox):
|
||||
if len(paths) > 1:
|
||||
self._app.show_error_message("Please, select only one item!")
|
||||
return
|
||||
return paths
|
||||
|
||||
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):
|
||||
def on_ftp_renamed(self, renderer, path, new_value):
|
||||
renderer.set_property("editable", False)
|
||||
row = self._ftp_model[path]
|
||||
old_name = row[self.Column.NAME]
|
||||
@@ -308,11 +560,33 @@ class FtpClientBox(Gtk.HBox):
|
||||
return
|
||||
|
||||
resp = self._ftp.rename_file(old_name, new_value)
|
||||
self.update_ftp_info("{} Status: {}".format(old_name, resp))
|
||||
self.update_ftp_info(f"{old_name} Status: {resp}")
|
||||
if resp[0] == "2":
|
||||
row[self.Column.NAME] = new_value
|
||||
|
||||
def on_file_edit(self, renderer):
|
||||
def on_ftp_attr_change(self, item):
|
||||
path = self.get_ftp_edit_path()
|
||||
if path:
|
||||
row = self._ftp_model[path]
|
||||
file = row[self.Column.NAME]
|
||||
if file == self.ROOT:
|
||||
self._app.show_error_message("Not allowed in this context!")
|
||||
return
|
||||
|
||||
attrs = row[self.Column.ATTR]
|
||||
if len(attrs) != 10:
|
||||
log(f"Init attributes error [{attrs}]. Invalid length!")
|
||||
return
|
||||
|
||||
dialog = AttributesDialog(attrs, IS_GNOME_SESSION)
|
||||
ok = Gtk.ResponseType.OK
|
||||
if dialog.run() == ok and show_dialog(DialogType.QUESTION, self._app.app_window) == ok:
|
||||
log(self._ftp.sendcmd(f"SITE CHMOD {dialog.permissions} {file}"))
|
||||
f_data = self._ftp.sendcmd(f"STAT {file}").split()
|
||||
row[self.Column.ATTR] = f_data[2] if len(f_data) > 3 else attrs
|
||||
dialog.destroy()
|
||||
|
||||
def on_file_rename(self, renderer):
|
||||
model, paths = self._file_view.get_selection().get_selected_rows()
|
||||
if len(paths) > 1:
|
||||
self._app.show_error_message("Please, select only one item!")
|
||||
@@ -321,7 +595,7 @@ class FtpClientBox(Gtk.HBox):
|
||||
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):
|
||||
def on_file_renamed(self, renderer, path, new_value):
|
||||
renderer.set_property("editable", False)
|
||||
row = self._file_model[path]
|
||||
old_name = row[self.Column.NAME]
|
||||
@@ -331,7 +605,7 @@ class FtpClientBox(Gtk.HBox):
|
||||
path = Path(row[self.Column.ATTR])
|
||||
if path.exists():
|
||||
try:
|
||||
new_path = path.rename("{}/{}".format(path.parent, new_value))
|
||||
new_path = path.rename(f"{path.parent}/{new_value}")
|
||||
except ValueError as e:
|
||||
log(e)
|
||||
self._app.show_error_message(str(e))
|
||||
@@ -340,8 +614,16 @@ class FtpClientBox(Gtk.HBox):
|
||||
row[self.Column.NAME] = new_value
|
||||
row[self.Column.ATTR] = str(new_path.resolve())
|
||||
|
||||
def on_file_copy(self, item=None):
|
||||
uris = self.get_file_uris()
|
||||
self.copy_to_ftp(uris) if uris else None
|
||||
|
||||
def on_ftp_copy(self, item=None):
|
||||
uris = self.get_ftp_uris()
|
||||
self.copy_to_pc(uris) if uris else None
|
||||
|
||||
def on_file_remove(self, item=None):
|
||||
if show_dialog(DialogType.QUESTION, self._app._main_window) != Gtk.ResponseType.OK:
|
||||
if show_dialog(DialogType.QUESTION, self._app.app_window) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
model, paths = self._file_view.get_selection().get_selected_rows()
|
||||
@@ -359,7 +641,7 @@ class FtpClientBox(Gtk.HBox):
|
||||
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:
|
||||
if show_dialog(DialogType.QUESTION, self._app.app_window) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
model, paths = self._ftp_view.get_selection().get_selected_rows()
|
||||
@@ -385,7 +667,7 @@ class FtpClientBox(Gtk.HBox):
|
||||
|
||||
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))
|
||||
path = Path(f"{cur_path}/{name}")
|
||||
|
||||
try:
|
||||
path.mkdir()
|
||||
@@ -406,13 +688,13 @@ class FtpClientBox(Gtk.HBox):
|
||||
name = self.get_new_folder_name(self._ftp_model)
|
||||
|
||||
try:
|
||||
folder = "{}/{}".format(cur_path, name)
|
||||
folder = f"{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):
|
||||
if resp == f"{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)
|
||||
@@ -424,7 +706,7 @@ class FtpClientBox(Gtk.HBox):
|
||||
count = 0
|
||||
while name in names:
|
||||
count += 1
|
||||
name = "{}{}".format(name, count)
|
||||
name = f"{name}{count}"
|
||||
return name
|
||||
|
||||
# ***************** Drag-and-drop ********************* #
|
||||
@@ -453,30 +735,43 @@ class FtpClientBox(Gtk.HBox):
|
||||
return True
|
||||
|
||||
def on_ftp_drag_data_get(self, view, context, data, info, time):
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
uris = self.get_ftp_uris()
|
||||
data.set_uris(uris) if uris else None
|
||||
|
||||
def get_ftp_uris(self):
|
||||
""" Returns the selected paths in FTP view as a list containing uris string or None. """
|
||||
model, paths = self._ftp_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)])
|
||||
path = Path(f"/{r[self.Column.NAME]}:{r[self.Column.ATTR]}")
|
||||
uris.append(str(path.resolve()) if IS_WIN else path.as_uri())
|
||||
return [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
|
||||
|
||||
self.copy_to_ftp(data.get_uris())
|
||||
Gtk.drag_finish(context, True, False, time)
|
||||
return True
|
||||
|
||||
@run_task
|
||||
def copy_to_ftp(self, uris):
|
||||
resp = "2"
|
||||
try:
|
||||
GLib.idle_add(self._app._wait_dialog.show)
|
||||
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)
|
||||
if len(uris) == 1:
|
||||
uris = uris[0].split(self.URI_SEP if self._settings.is_darwin else "\n")
|
||||
|
||||
for uri in uris:
|
||||
uri = urlparse(unquote(uri)).path
|
||||
if IS_WIN:
|
||||
uri = uri.lstrip("/")
|
||||
|
||||
path = Path(uri)
|
||||
if path.is_dir():
|
||||
try:
|
||||
@@ -484,34 +779,40 @@ class FtpClientBox(Gtk.HBox):
|
||||
except all_errors as e:
|
||||
pass # NOP
|
||||
self._ftp.cwd(path.name)
|
||||
resp = self._ftp.upload_dir(str(path.resolve()) + "/", self.update_ftp_info)
|
||||
resp = self._ftp.upload_dir(str(path.resolve()) + SEP, self.update_ftp_info)
|
||||
else:
|
||||
resp = self._ftp.send_file(path.name, str(path.parent) + "/", callback=self.update_ftp_info)
|
||||
resp = self._ftp.send_file(path.name, str(path.parent) + SEP, callback=self.update_ftp_info)
|
||||
finally:
|
||||
GLib.idle_add(self._app._wait_dialog.hide)
|
||||
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))
|
||||
|
||||
def on_file_drag_data_get(self, view, context, data, info, time):
|
||||
uris = self.get_file_uris()
|
||||
data.set_uris(uris) if uris else None
|
||||
|
||||
def get_file_uris(self):
|
||||
""" Returns the selected paths in the file view as a list containing uris string or None. """
|
||||
model, paths = self._file_view.get_selection().get_selected_rows()
|
||||
if len(paths) > 0:
|
||||
sep = self.URI_SEP if self._settings.is_darwin else "\n"
|
||||
return [sep.join([Path(model[p][self.Column.ATTR]).as_uri() for p in paths])]
|
||||
|
||||
def on_file_drag_data_received(self, view, context, x, y, data, info, time):
|
||||
self.copy_to_pc(data.get_uris())
|
||||
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):
|
||||
def copy_to_pc(self, uris):
|
||||
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)
|
||||
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)
|
||||
if len(uris) == 1:
|
||||
uris = uris[0].split(self.URI_SEP if self._settings.is_darwin else "\n")
|
||||
|
||||
for uri in uris:
|
||||
name, sep, attr = unquote(Path(uri).name).partition(":")
|
||||
@@ -525,12 +826,9 @@ class FtpClientBox(Gtk.HBox):
|
||||
except OSError as e:
|
||||
log(e)
|
||||
finally:
|
||||
GLib.idle_add(self._app._wait_dialog.hide)
|
||||
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()
|
||||
@@ -541,6 +839,27 @@ class FtpClientBox(Gtk.HBox):
|
||||
self._ftp_info_label.set_text(message)
|
||||
self._ftp_info_label.set_tooltip_text(message)
|
||||
|
||||
# **************** Bookmarks ***************** #
|
||||
|
||||
@run_idle
|
||||
def init_bookmarks(self):
|
||||
self._bookmark_model.clear()
|
||||
list(map(lambda b: self._bookmark_model.append((b,)), self._settings.ftp_bookmarks))
|
||||
|
||||
def on_bookmark_activated(self, view, path, column):
|
||||
self.init_ftp_data(self._bookmark_model[path][0])
|
||||
|
||||
def on_bookmark_add(self, item=None):
|
||||
self._bookmarks_button.set_active(True)
|
||||
self._bookmark_model.append((self._ftp_model.get_value(self._ftp_model.get_iter_first(), 4),))
|
||||
self._settings.ftp_bookmarks = [r[0] for r in self._bookmark_model]
|
||||
|
||||
def on_bookmark_remove(self, item=None):
|
||||
model, paths = self._bookmark_view.get_selection().get_selected_rows()
|
||||
if paths and show_dialog(DialogType.QUESTION, self._app.app_window) == Gtk.ResponseType.OK:
|
||||
list(map(lambda p: model.remove(model.get_iter(p)), paths))
|
||||
self._settings.ftp_bookmarks = [r[0] for r in self._bookmark_model]
|
||||
|
||||
def on_view_key_press(self, view, event):
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
@@ -556,14 +875,28 @@ class FtpClientBox(Gtk.HBox):
|
||||
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)
|
||||
self.on_ftp_rename(self._ftp_name_renderer)
|
||||
elif self._file_view.is_focus():
|
||||
self.on_file_edit(self._file_name_renderer)
|
||||
self.on_file_rename(self._file_name_renderer)
|
||||
elif key is KeyboardKey.F4:
|
||||
if self._ftp_view.is_focus():
|
||||
self.on_ftp_edit()
|
||||
elif key is KeyboardKey.F5:
|
||||
if self._ftp_view.is_focus():
|
||||
self.on_ftp_copy()
|
||||
elif self._file_view.is_focus():
|
||||
self.on_file_copy()
|
||||
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()
|
||||
elif self._bookmark_view.is_focus():
|
||||
self.on_bookmark_remove()
|
||||
elif key is KeyboardKey.RETURN:
|
||||
path, column = view.get_cursor()
|
||||
if path:
|
||||
view.emit("row-activated", path, column)
|
||||
|
||||
def on_view_press(self, view, event):
|
||||
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_PRIMARY:
|
||||
@@ -577,24 +910,11 @@ class FtpClientBox(Gtk.HBox):
|
||||
# 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)
|
||||
@staticmethod
|
||||
def on_paned_size_allocate(paned, allocation):
|
||||
""" Sets default homogeneous sizes. """
|
||||
paned.set_position(0.5 * allocation.width)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
<!-- Generated with glade 3.38.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2020 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -27,25 +27,13 @@ Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellite list editor. -->
|
||||
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkImage" id="details_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">emblem-important-symbolic</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="import_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">document-revert-symbolic-rtl</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkListStore" id="main_list_store">
|
||||
<object class="GtkListStore" id="bq_list_store">
|
||||
<columns>
|
||||
<!-- column-name name -->
|
||||
<column type="gchararray"/>
|
||||
@@ -55,32 +43,72 @@ Author: Dmitriy Yefremov
|
||||
<column type="gboolean"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkImage" id="details_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">emblem-important-symbolic</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="import_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">document-revert-symbolic-rtl</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="remove_selection_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-undo</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-undo</property>
|
||||
</object>
|
||||
<object class="GtkMenu" id="popup_menu">
|
||||
<object class="GtkMenu" id="bq_popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="select_all_popup_item">
|
||||
<property name="label">gtk-select-all</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="activate" handler="on_select_all" object="main_view" swapped="no"/>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="use-stock">True</property>
|
||||
<signal name="activate" handler="on_select_all" object="bq_view" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="unselect_all_popup_item">
|
||||
<property name="label" translatable="yes">Remove selection</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="image">remove_selection_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_unselect_all" object="main_view" swapped="no"/>
|
||||
<property name="use-stock">False</property>
|
||||
<signal name="activate" handler="on_unselect_all" object="bq_view" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkImage" id="remove_services_selection_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-undo</property>
|
||||
</object>
|
||||
<object class="GtkMenu" id="services_popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="select_all_services_popup_item">
|
||||
<property name="label">gtk-select-all</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="use-underline">True</property>
|
||||
<property name="use-stock">True</property>
|
||||
<signal name="activate" handler="on_select_all" object="services_view" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="unselect_all_services_popup_item">
|
||||
<property name="label" translatable="yes">Remove selection</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="image">remove_services_selection_image</property>
|
||||
<property name="use-stock">False</property>
|
||||
<signal name="activate" handler="on_unselect_all" object="services_view" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
@@ -90,52 +118,56 @@ Author: Dmitriy Yefremov
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name type -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name selected -->
|
||||
<column type="gboolean"/>
|
||||
<!-- column-name background -->
|
||||
<column type="GdkRGBA"/>
|
||||
<!-- column-name id -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
<signal name="row-changed" handler="on_services_model_changed" swapped="no"/>
|
||||
</object>
|
||||
<object class="GtkWindow" id="dialog_window">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="title" translatable="yes">Import</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window_position">center-on-parent</property>
|
||||
<property name="default_width">480</property>
|
||||
<property name="default_height">320</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<property name="window-position">center-on-parent</property>
|
||||
<property name="default-width">480</property>
|
||||
<property name="default-height">320</property>
|
||||
<property name="destroy-with-parent">True</property>
|
||||
<property name="type-hint">dialog</property>
|
||||
<signal name="check-resize" handler="on_resize" swapped="no"/>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_box">
|
||||
<property name="width_request">480</property>
|
||||
<property name="width-request">480</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="toolbar_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="actions_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_left">15</property>
|
||||
<property name="margin_right">15</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="margin_bottom">10</property>
|
||||
<property name="layout_style">start</property>
|
||||
<property name="margin-left">15</property>
|
||||
<property name="margin-right">15</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">10</property>
|
||||
<property name="layout-style">start</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="import_button">
|
||||
<property name="label" translatable="yes">Import</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Bouquets and services</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Bouquets and services</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="image">import_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="clicked" handler="on_import" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -145,21 +177,19 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="info_check_button">
|
||||
<object class="GtkToggleButton" id="details_button">
|
||||
<property name="label" translatable="yes">Details</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Details</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="image">details_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<accelerator key="i" signal="clicked" modifiers="Primary"/>
|
||||
<property name="always-show-image">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">1</property>
|
||||
<property name="secondary">True</property>
|
||||
</packing>
|
||||
@@ -184,39 +214,38 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkPaned" id="main_paned">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="wide_handle">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="wide-handle">True</property>
|
||||
<child>
|
||||
<object class="GtkFrame" id="bouquets_box_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0.49000000953674316</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label-xalign">0.49000000953674316</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="bouquets_box">
|
||||
<property name="width-request">100</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="bouquets_screlled_window">
|
||||
<property name="width_request">200</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="main_view">
|
||||
<object class="GtkTreeView" id="bq_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">main_list_store</property>
|
||||
<property name="headers_clickable">False</property>
|
||||
<property name="search_column">0</property>
|
||||
<signal name="button-press-event" handler="on_popup_menu" object="popup_menu" swapped="no"/>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="model">bq_list_store</property>
|
||||
<property name="headers-clickable">False</property>
|
||||
<property name="search-column">0</property>
|
||||
<signal name="button-press-event" handler="on_popup_menu" object="bq_popup_menu" swapped="no"/>
|
||||
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
|
||||
<signal name="select-all" handler="on_select_all" swapped="no"/>
|
||||
@@ -246,7 +275,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="bq_type_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
<property name="xalign">0.5099999904632568</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
@@ -260,8 +289,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererToggle" id="bq_selected_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
<signal name="toggled" handler="on_selected_toggled" swapped="no"/>
|
||||
<signal name="toggled" handler="on_bq_selected_toggled" swapped="no"/>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="active">2</attribute>
|
||||
@@ -278,14 +306,56 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="bouquets_status_box">
|
||||
<property name="height-request">26</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="bouquets_count_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">document-properties</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="bouquets_count_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="label" translatable="yes">Bouquets</property>
|
||||
</object>
|
||||
</child>
|
||||
@@ -297,30 +367,35 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="services_box_frame">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0.49000000953674316</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label-xalign">0.49000000953674316</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="services_box">
|
||||
<property name="width-request">100</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="services_view_scrolled_window">
|
||||
<property name="width_request">150</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="services_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="model">services_list_store</property>
|
||||
<property name="headers_clickable">False</property>
|
||||
<property name="search_column">0</property>
|
||||
<property name="headers-clickable">False</property>
|
||||
<property name="search-column">0</property>
|
||||
<signal name="button-press-event" handler="on_popup_menu" object="services_popup_menu" swapped="no"/>
|
||||
<signal name="cursor-changed" handler="on_service_changed" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
|
||||
<signal name="realize" handler="on_services_view_realize" swapped="no"/>
|
||||
<signal name="select-all" handler="on_select_all" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection"/>
|
||||
</child>
|
||||
@@ -331,11 +406,12 @@ Author: Dmitriy Yefremov
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="info_name_renderer">
|
||||
<property name="xalign">0.02</property>
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
<attribute name="background-rgba">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
@@ -346,10 +422,26 @@ Author: Dmitriy Yefremov
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="info_type_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
<property name="xalign">0.5099999904632568</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
<attribute name="background-rgba">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="service_selected_column">
|
||||
<property name="title" translatable="yes">Selected</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererToggle" id="service_selected_renderer">
|
||||
<signal name="toggled" handler="on_service_selected_toggled" swapped="no"/>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="cell-background-rgba">3</attribute>
|
||||
<attribute name="active">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
@@ -363,14 +455,130 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="service_info_box_frame">
|
||||
<property name="visible" bind-source="details_button" bind-property="active">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="label-xalign">0</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="service_info_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="service_info_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">10</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label_item">
|
||||
<placeholder/>
|
||||
</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="services_status_box">
|
||||
<property name="height-request">26</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="services_count_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">document-properties</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="services_count_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="service_exists_frame">
|
||||
<property name="width-request">32</property>
|
||||
<property name="height-request">16</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="label-xalign">0</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child type="label_item">
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id=" service_exists_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Already exists</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="label" translatable="yes">Bouquet details</property>
|
||||
</object>
|
||||
</child>
|
||||
@@ -389,14 +597,14 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="show-close-button">True</property>
|
||||
<signal name="response" handler="on_info_bar_close" swapped="no"/>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout_style">end</property>
|
||||
<property name="layout-style">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -406,12 +614,12 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">16</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="message_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">message</property>
|
||||
<property name="wrap">True</property>
|
||||
</object>
|
||||
|
||||
@@ -1,3 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
from collections import defaultdict
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
|
||||
@@ -5,22 +34,25 @@ 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.neutrino.bouquets import parse_webtv, parse_bouquets as get_neutrino_bouquets
|
||||
from app.settings import SettingsType
|
||||
from app.settings import SettingsType, IS_DARWIN, SEP
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, get_message, get_builder
|
||||
from app.ui.main_helper import on_popup_menu
|
||||
from .uicommons import Gtk, UI_RESOURCES_PATH, KeyboardKey, Column
|
||||
from app.ui.main_helper import on_popup_menu, get_iptv_data
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, Column
|
||||
|
||||
|
||||
def import_bouquet(transient, model, path, settings, services, appender, file_path=None):
|
||||
def import_bouquet(app, model, path, appender, file_path=None):
|
||||
""" Import of single bouquet """
|
||||
itr = model.get_iter(path)
|
||||
bq_type = BqType(model.get(itr, Column.BQ_TYPE)[0])
|
||||
pattern, f_pattern = None, None
|
||||
settings = app.app_settings
|
||||
transient = app.app_window
|
||||
services = app.current_services
|
||||
profile = settings.setting_type
|
||||
|
||||
if profile is SettingsType.ENIGMA_2:
|
||||
pattern = ".{}".format(bq_type.value)
|
||||
f_pattern = "userbouquet.*{}".format(pattern)
|
||||
pattern = f".{bq_type.value}"
|
||||
f_pattern = f"{'' if IS_DARWIN else 'userbouquet.'}*{pattern}"
|
||||
elif profile is SettingsType.NEUTRINO_MP:
|
||||
pattern = "webtv.xml" if bq_type is BqType.WEBTV else "bouquets.xml"
|
||||
f_pattern = "bouquets.xml"
|
||||
@@ -38,6 +70,10 @@ def import_bouquet(transient, model, path, settings, services, appender, file_pa
|
||||
return
|
||||
|
||||
if profile is SettingsType.ENIGMA_2:
|
||||
if IS_DARWIN and file_path.rfind("userbouquet.") < 0:
|
||||
show_dialog(DialogType.ERROR, transient, text="Not allowed in this context!")
|
||||
return
|
||||
|
||||
bq = get_enigma2_bouquet(file_path)
|
||||
imported = list(filter(lambda x: x.data in services or x.type is BqServiceType.IPTV, bq.services))
|
||||
|
||||
@@ -55,8 +91,8 @@ def import_bouquet(transient, model, path, settings, services, appender, file_pa
|
||||
bqs = parse_webtv(file_path, "WEBTV", bq_type.value)
|
||||
else:
|
||||
bqs = get_neutrino_bouquets(file_path, "", bq_type.value)
|
||||
file_path = "{}/".format(Path(file_path).parent)
|
||||
ImportDialog(transient, file_path, settings, services.keys(), lambda b, s: appender(b), (bqs,)).show()
|
||||
file_path = f"{Path(file_path).parent}{SEP}"
|
||||
ImportDialog(app, file_path, lambda b, s: appender(b), (bqs,)).show()
|
||||
|
||||
|
||||
def get_enigma2_bouquet(path):
|
||||
@@ -68,37 +104,48 @@ def get_enigma2_bouquet(path):
|
||||
|
||||
|
||||
class ImportDialog:
|
||||
def __init__(self, transient, path, settings, service_ids, appender, bouquets=None):
|
||||
def __init__(self, app, path, appender, bouquets=None):
|
||||
handlers = {"on_import": self.on_import,
|
||||
"on_cursor_changed": self.on_cursor_changed,
|
||||
"on_selected_toggled": self.on_selected_toggled,
|
||||
"on_service_changed": self.on_service_changed,
|
||||
"on_bq_selected_toggled": self.on_bq_selected_toggled,
|
||||
"on_service_selected_toggled": self.on_service_selected_toggled,
|
||||
"on_services_model_changed": self.on_services_model_changed,
|
||||
"on_info_bar_close": self.on_info_bar_close,
|
||||
"on_select_all": self.on_select_all,
|
||||
"on_unselect_all": self.on_unselect_all,
|
||||
"on_services_view_realize": self.on_services_view_realize,
|
||||
"on_popup_menu": on_popup_menu,
|
||||
"on_resize": self.on_resize,
|
||||
"on_key_press": self.on_key_press}
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "imports.glade", handlers)
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}imports.glade", handlers)
|
||||
|
||||
self._app = app
|
||||
self._bq_services = {}
|
||||
self._services = {}
|
||||
self._service_ids = service_ids
|
||||
self._ids = self._app.current_services.keys()
|
||||
self._skip_import = defaultdict(set)
|
||||
self._append = appender
|
||||
self._profile = settings.setting_type
|
||||
self._settings = settings
|
||||
self._profile = app.app_settings.setting_type
|
||||
self._settings = app.app_settings
|
||||
self._bouquets = bouquets
|
||||
self._current_bq = None
|
||||
self._existing_srv_background = None
|
||||
|
||||
self._dialog_window = builder.get_object("dialog_window")
|
||||
self._dialog_window.set_transient_for(transient)
|
||||
self._main_model = builder.get_object("main_list_store")
|
||||
self._main_view = builder.get_object("main_view")
|
||||
self._dialog_window.set_transient_for(app.app_window)
|
||||
self._bq_model = builder.get_object("bq_list_store")
|
||||
self._bq_view = builder.get_object("bq_view")
|
||||
self._services_view = builder.get_object("services_view")
|
||||
self._services_model = builder.get_object("services_list_store")
|
||||
self._info_check_button = builder.get_object("info_check_button")
|
||||
self._info_check_button.bind_property("active", builder.get_object("services_box_frame"), "visible")
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("message_label")
|
||||
self._bouquets_count_label = builder.get_object("bouquets_count_label")
|
||||
self._services_count_label = builder.get_object("services_count_label")
|
||||
self._service_info_label = builder.get_object("service_info_label")
|
||||
self._service_exists_frame = builder.get_object("service_exists_frame")
|
||||
|
||||
window_size = self._settings.get("import_dialog_window_size")
|
||||
if window_size:
|
||||
self._dialog_window.resize(*window_size)
|
||||
@@ -110,7 +157,7 @@ class ImportDialog:
|
||||
|
||||
@run_idle
|
||||
def init_data(self, path):
|
||||
self._main_model.clear()
|
||||
self._bq_model.clear()
|
||||
self._services_model.clear()
|
||||
try:
|
||||
if not self._bouquets:
|
||||
@@ -118,8 +165,9 @@ class ImportDialog:
|
||||
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_model.append((bq.name, bq.type, True))
|
||||
self._bq_services[(bq.name, bq.type)] = bq.services
|
||||
self._bouquets_count_label.set_text(str(len(self._bq_model)))
|
||||
|
||||
if self._profile is SettingsType.ENIGMA_2:
|
||||
services = get_services(path, self._profile, 5 if self._settings.v5_support else 4)
|
||||
@@ -132,11 +180,11 @@ class ImportDialog:
|
||||
for srv in services:
|
||||
self._services[srv.fav_id] = srv
|
||||
except FileNotFoundError as e:
|
||||
log("Import error [init data]: {}".format(e))
|
||||
log(f"Import error [init data]: {e}")
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
|
||||
def on_import(self, item):
|
||||
if not any(r[-1] for r in self._main_model):
|
||||
if not any(r[-1] for r in self._bq_model):
|
||||
self.show_info_message(get_message("No selected item!"), Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
@@ -154,12 +202,13 @@ class ImportDialog:
|
||||
log("Importing data...")
|
||||
services = set()
|
||||
to_delete = set()
|
||||
for row in self._main_model:
|
||||
for row in self._bq_model:
|
||||
bq = (row[0], row[1])
|
||||
if row[-1]:
|
||||
skip = self._skip_import[bq]
|
||||
for bq_srv in self._bq_services.get(bq, []):
|
||||
srv = self._services.get(bq_srv.data, None)
|
||||
if srv:
|
||||
if srv and srv.fav_id not in skip:
|
||||
services.add(srv)
|
||||
else:
|
||||
to_delete.add(bq)
|
||||
@@ -168,35 +217,75 @@ class ImportDialog:
|
||||
for bq in bqs.bouquets:
|
||||
if (bq.name, bq.type) in to_delete:
|
||||
bqs_to_delete.append(bq)
|
||||
else:
|
||||
skip = self._skip_import[(bq.name, bq.type)]
|
||||
bq_services = [srv for srv in bq.services if srv.data not in skip]
|
||||
bq.services.clear()
|
||||
bq.services.extend(bq_services)
|
||||
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)))
|
||||
|
||||
services = list(filter(lambda s: s.fav_id not in self._ids, services))
|
||||
self._append(self._bouquets, services)
|
||||
self._dialog_window.destroy()
|
||||
|
||||
@run_idle
|
||||
def on_cursor_changed(self, view):
|
||||
if not self._info_check_button.get_active():
|
||||
return
|
||||
|
||||
self._services_model.clear()
|
||||
self._service_info_label.set_text("")
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if not paths:
|
||||
return
|
||||
|
||||
bq_services = self._bq_services.get(model.get(model.get_iter(paths[0]), 0, 1))
|
||||
self._current_bq = model.get(model.get_iter(paths[0]), 0, 1)
|
||||
bq_services = self._bq_services.get(self._current_bq)
|
||||
skip = self._skip_import[self._current_bq]
|
||||
|
||||
for bq_srv in bq_services:
|
||||
if bq_srv.type is BqServiceType.DEFAULT:
|
||||
srv = self._services.get(bq_srv.data, None)
|
||||
if srv:
|
||||
self._services_model.append((srv.service, srv.service_type))
|
||||
bg = self._existing_srv_background if srv.fav_id in self._ids else None
|
||||
srv = (srv.service, srv.service_type, srv.fav_id not in skip, bg, srv.fav_id)
|
||||
self._services_model.append(srv)
|
||||
else:
|
||||
self._services_model.append((bq_srv.name, bq_srv.type.value))
|
||||
bg = self._existing_srv_background if bq_srv.data in self._ids else None
|
||||
srv = (bq_srv.name, bq_srv.type.value, bq_srv.data not in skip, bg, bq_srv.data)
|
||||
self._services_model.append(srv)
|
||||
|
||||
def on_selected_toggled(self, toggle, path):
|
||||
self._main_model.set_value(self._main_model.get_iter(path), 2, not toggle.get_active())
|
||||
self._services_count_label.set_text(str(len(self._services_model)))
|
||||
|
||||
def on_service_changed(self, view):
|
||||
path, column = view.get_cursor()
|
||||
if path:
|
||||
row = self._services_model[path][:]
|
||||
if row[1] == "IPTV":
|
||||
ref, url = get_iptv_data(row[-1])
|
||||
ref = f"{get_message('Service reference')}: {ref}"
|
||||
info = f"{get_message('Name')}: {row[0]}\n{ref}\nURL: {url}"
|
||||
self._service_info_label.set_text(info)
|
||||
else:
|
||||
srv = self._services.get(row[-1], None)
|
||||
self._service_info_label.set_text(self._app.get_hint_for_fav_list(srv) if srv else "")
|
||||
|
||||
def on_bq_selected_toggled(self, toggle, path):
|
||||
self._bq_model.set_value(self._bq_model.get_iter(path), 2, not toggle.get_active())
|
||||
|
||||
def on_service_selected_toggled(self, toggle, path):
|
||||
self._services_model.set_value(self._services_model.get_iter(path), 2, not toggle.get_active())
|
||||
|
||||
def on_services_model_changed(self, model, path, itr):
|
||||
row = model[itr][:]
|
||||
fav_id = row[-1]
|
||||
skip = self._skip_import[self._current_bq]
|
||||
if row[2]:
|
||||
if fav_id in skip:
|
||||
skip.remove(fav_id)
|
||||
else:
|
||||
skip.add(fav_id)
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
@@ -217,6 +306,12 @@ class ImportDialog:
|
||||
def update_selection(self, view, select):
|
||||
view.get_model().foreach(lambda mod, path, itr: mod.set_value(itr, 2, select))
|
||||
|
||||
def on_services_view_realize(self, view):
|
||||
if self._settings.use_colors:
|
||||
background = Gdk.RGBA()
|
||||
self._existing_srv_background = background if background.parse(self._settings.extra_color) else None
|
||||
self._service_exists_frame.modify_bg(Gtk.StateType.NORMAL, background.to_color())
|
||||
|
||||
def on_resize(self, window):
|
||||
if self._settings:
|
||||
self._settings.add("import_dialog_window_size", window.get_size())
|
||||
@@ -229,10 +324,11 @@ class ImportDialog:
|
||||
key = KeyboardKey(key_code)
|
||||
|
||||
if key is KeyboardKey.SPACE:
|
||||
model = view.get_model()
|
||||
path, column = view.get_cursor()
|
||||
itr = self._main_model.get_iter(path)
|
||||
selected = self._main_model.get_value(itr, 2)
|
||||
self._main_model.set_value(itr, 2, not selected)
|
||||
itr = model.get_iter(path)
|
||||
selected = model.get_value(itr, 2)
|
||||
model.set_value(itr, 2, not selected)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?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
|
||||
Copyright (c) 2018-2022 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-2021 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkImage" id="remove_selection_image">
|
||||
<property name="visible">True</property>
|
||||
@@ -64,7 +64,6 @@ Author: Dmitriy Yefremov
|
||||
</object>
|
||||
<object class="GtkDialog" id="search_unavailable_streams_dialog">
|
||||
<property name="use-header-bar">1</property>
|
||||
<property name="width_request">320</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes"> </property>
|
||||
<property name="resizable">False</property>
|
||||
@@ -103,76 +102,98 @@ Author: Dmitriy Yefremov
|
||||
<property name="label_yalign">1</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkGrid">
|
||||
<object class="GtkBox" id="search_unavailable_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="column_spacing">10</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<object class="GtkGrid" id="search_unavailable_grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<object class="GtkBox" id="found_state_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Found</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Found</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="streams_rows_counter_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">unavailable streams.</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>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="streams_rows_counter_label">
|
||||
<object class="GtkLevelBar" id="unavailable_streams_level_bar">
|
||||
<property name="height_request">10</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
</attributes>
|
||||
<property name="valign">center</property>
|
||||
<property name="inverted">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">unavailable streams.</property>
|
||||
<property name="label" translatable="yes">Please wait, streams testing in progress...</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLevelBar" id="unavailable_streams_level_bar">
|
||||
<property name="height_request">10</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="inverted">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
@@ -181,36 +202,22 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Cancel</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="cancel_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-cancel</property>
|
||||
<property name="stock">gtk-close</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
<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="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Please wait, streams testing in progress...</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label_item">
|
||||
@@ -268,6 +275,33 @@ Author: Dmitriy Yefremov
|
||||
<property name="skip_taskbar_hint">True</property>
|
||||
<property name="skip_pager_hint">True</property>
|
||||
<signal name="response" handler="on_response" swapped="no"/>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="cancel_config_list_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="list_configuration_apply_button">
|
||||
<property name="label" translatable="yes">Apply</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</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" translatable="yes">OK</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox" id="iptv_list_configuration_dialog_box">
|
||||
<property name="can_focus">False</property>
|
||||
@@ -278,48 +312,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancel_config_list_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="list_configuration_apply_button">
|
||||
<property name="label" translatable="yes">Apply</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_apply" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="list_configuration_ok_button">
|
||||
<property name="label" translatable="yes">OK</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -433,7 +425,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="row_spacing">2</property>
|
||||
<property name="column_spacing">5</property>
|
||||
<property name="column_homogeneous">True</property>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="stream_type_list_combobox">
|
||||
<property name="visible">True</property>
|
||||
@@ -510,6 +501,7 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="list_namespace_entry">
|
||||
<property name="width_request">120</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
@@ -553,6 +545,7 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="list_nid_entry">
|
||||
<property name="width_request">75</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
@@ -596,6 +589,7 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="list_tid_entry">
|
||||
<property name="width_request">75</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
@@ -639,6 +633,7 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="list_sid_entry">
|
||||
<property name="width_request">75</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
@@ -682,6 +677,7 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="list_srv_id_entry">
|
||||
<property name="width_request">75</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
@@ -725,6 +721,7 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="list_srv_type_entry">
|
||||
<property name="width_request">75</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
@@ -873,6 +870,34 @@ Author: Dmitriy Yefremov
|
||||
<child type="titlebar">
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="iptv_dialog_cancel_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="iptv_dialog_add_button">
|
||||
<property name="label" translatable="yes">Add</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_save" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="iptv_dialog_save_button">
|
||||
<property name="label" translatable="yes">Save</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_save" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox" id="iptv_dialog_box">
|
||||
<property name="can_focus">False</property>
|
||||
@@ -882,49 +907,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="iptv_dialog_cancel_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="iptv_dialog_add_button">
|
||||
<property name="label" translatable="yes">Add</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_save" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="iptv_dialog_save_button">
|
||||
<property name="label" translatable="yes">Save</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_save" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -938,7 +920,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">2</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkFrame" id="iptv_dialog_service_data_frame">
|
||||
@@ -1284,6 +1265,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">5</property>
|
||||
<property name="max_width_chars">4</property>
|
||||
<property name="text">0</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
@@ -1340,60 +1322,60 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<signal name="response" handler="on_info_bar_close" swapped="no"/>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<signal name="response" handler="on_info_bar_close" swapped="no"/>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout_style">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">16</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="info_bar_message_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout_style">end</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">label</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">16</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="info_bar_message_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">label</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
117
app/ui/iptv.py
117
app/ui/iptv.py
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 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
|
||||
@@ -44,13 +44,14 @@ from app.eparser.iptv import (NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID
|
||||
from app.settings import SettingsType
|
||||
from app.tools.yt import YouTubeException, YouTube
|
||||
from app.ui.dialogs import Action, show_dialog, DialogType, get_message, get_builder
|
||||
from app.ui.main_helper import get_base_model, get_iptv_url, on_popup_menu, get_picon_pixbuf
|
||||
from app.ui.main_helper import get_iptv_url, on_popup_menu, get_picon_pixbuf
|
||||
from app.ui.uicommons import (Gtk, Gdk, UI_RESOURCES_PATH, IPTV_ICON, Column, KeyboardKey, get_yt_icon)
|
||||
|
||||
_DIGIT_ENTRY_NAME = "digit-entry"
|
||||
_ENIGMA2_REFERENCE = "{}:{}:{}:{:X}:{:X}:{:X}:{:X}:0:0:0"
|
||||
_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
|
||||
_UI_PATH = UI_RESOURCES_PATH + "iptv.glade"
|
||||
_PICON_FORMAT = "1_{}_{:X}_{:X}_{:X}_{:X}_{:X}_0_0_0.png"
|
||||
|
||||
|
||||
def is_data_correct(elems):
|
||||
@@ -77,7 +78,7 @@ def get_stream_type(box):
|
||||
|
||||
class IptvDialog:
|
||||
|
||||
def __init__(self, transient, view, services, bouquet, settings, action=Action.ADD):
|
||||
def __init__(self, app, view, bouquet=None, service=None, action=Action.ADD):
|
||||
handlers = {"on_response": self.on_response,
|
||||
"on_entry_changed": self.on_entry_changed,
|
||||
"on_url_changed": self.on_url_changed,
|
||||
@@ -86,11 +87,11 @@ class IptvDialog:
|
||||
"on_yt_quality_changed": self.on_yt_quality_changed,
|
||||
"on_info_bar_close": self.on_info_bar_close}
|
||||
|
||||
self._app = app
|
||||
self._action = action
|
||||
self._s_type = settings.setting_type
|
||||
self._settings = settings
|
||||
self._settings = app.app_settings
|
||||
self._s_type = self._settings.setting_type
|
||||
self._bouquet = bouquet
|
||||
self._services = services
|
||||
self._yt_links = None
|
||||
self._yt_dl = None
|
||||
|
||||
@@ -98,7 +99,7 @@ class IptvDialog:
|
||||
objects=("iptv_dialog", "stream_type_liststore", "yt_quality_liststore"))
|
||||
|
||||
self._dialog = builder.get_object("iptv_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._dialog.set_transient_for(app.app_window)
|
||||
self._name_entry = builder.get_object("name_entry")
|
||||
self._description_entry = builder.get_object("description_entry")
|
||||
self._url_entry = builder.get_object("url_entry")
|
||||
@@ -142,7 +143,7 @@ class IptvDialog:
|
||||
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][:]
|
||||
self._current_srv = service
|
||||
self.init_data(self._current_srv)
|
||||
|
||||
def show(self):
|
||||
@@ -167,8 +168,8 @@ class IptvDialog:
|
||||
self._dialog.destroy()
|
||||
|
||||
def init_data(self, srv):
|
||||
name, fav_id = srv[2], srv[7]
|
||||
self._name_entry.set_text(name)
|
||||
fav_id = srv.fav_id
|
||||
self._name_entry.set_text(srv.service)
|
||||
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):
|
||||
@@ -307,11 +308,12 @@ class IptvDialog:
|
||||
int(self._namespace_entry.get_text()),
|
||||
quote(self._url_entry.get_text()),
|
||||
name, name)
|
||||
|
||||
self.update_bouquet_data(name, fav_id)
|
||||
|
||||
def save_neutrino_data(self):
|
||||
if self._action is Action.EDIT:
|
||||
id_data = self._current_srv[7].split("::")
|
||||
id_data = self._current_srv.fav_id.split("::")
|
||||
else:
|
||||
id_data = ["", "", "0", None, None, None, None, "", "", "1"]
|
||||
id_data[0] = self._url_entry.get_text()
|
||||
@@ -320,20 +322,25 @@ class IptvDialog:
|
||||
self._dialog.destroy()
|
||||
|
||||
def update_bouquet_data(self, name, fav_id):
|
||||
picon_id = f"1_{'_'.join(self._reference_entry.get_text().split(':')[1:])}.png"
|
||||
|
||||
if self._action is Action.EDIT:
|
||||
old_srv = self._services.pop(self._current_srv[7])
|
||||
self._services[fav_id] = old_srv._replace(service=name, fav_id=fav_id)
|
||||
self._bouquet[self._paths[0][0]] = fav_id
|
||||
self._model.set(self._model.get_iter(self._paths), {Column.FAV_SERVICE: name, Column.FAV_ID: fav_id})
|
||||
services = self._app.current_services
|
||||
old_srv = services.pop(self._current_srv.fav_id)
|
||||
new_service = old_srv._replace(service=name, fav_id=fav_id, picon_id=picon_id)
|
||||
services[fav_id] = new_service
|
||||
self._app.emit("iptv-service-edited", (old_srv, new_service))
|
||||
else:
|
||||
aggr = [None] * 10
|
||||
aggr = [None] * 8
|
||||
s_type = BqServiceType.IPTV.name
|
||||
srv = (None, None, name, None, None, s_type, None, fav_id, *aggr[0:3])
|
||||
itr = self._model.insert_after(self._model.get_iter(self._paths[0]),
|
||||
srv) if self._paths else self._model.insert(0, srv)
|
||||
self._model.set_value(itr, 1, IPTV_ICON)
|
||||
self._bouquet.insert(self._model.get_path(itr)[0], fav_id)
|
||||
self._services[fav_id] = Service(None, None, IPTV_ICON, name, *aggr[0:3], s_type, *aggr, fav_id, None)
|
||||
service = Service(None, None, IPTV_ICON, name, *aggr[0:3], s_type, None, picon_id, *aggr, fav_id, None)
|
||||
self._app.current_services[fav_id] = service
|
||||
self._app.emit("iptv-service-added", (service,))
|
||||
|
||||
@run_idle
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
@@ -373,9 +380,13 @@ class SearchUnavailableDialog:
|
||||
|
||||
@run_task
|
||||
def do_search(self):
|
||||
import concurrent.futures
|
||||
import ssl
|
||||
import certifi
|
||||
|
||||
context = ssl.create_default_context(cafile=certifi.where())
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
|
||||
futures = {executor.submit(self.get_unavailable, row): row for row in self._iptv_rows}
|
||||
futures = {executor.submit(self.get_unavailable, row, context): row for row in self._iptv_rows}
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
if not self._download_task:
|
||||
executor.shutdown()
|
||||
@@ -384,13 +395,13 @@ class SearchUnavailableDialog:
|
||||
self._download_task = False
|
||||
self.on_close()
|
||||
|
||||
def get_unavailable(self, row):
|
||||
def get_unavailable(self, row, context):
|
||||
if not self._download_task:
|
||||
return
|
||||
try:
|
||||
req = Request(get_iptv_url(row, self._s_type))
|
||||
self.update_bar()
|
||||
urlopen(req, timeout=2)
|
||||
urlopen(req, context=context, timeout=2)
|
||||
except HTTPError as e:
|
||||
if e.code != 403:
|
||||
self.append_data(row)
|
||||
@@ -474,6 +485,8 @@ class IptvListDialog:
|
||||
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")
|
||||
self._ok_button.bind_property("visible", self._apply_button, "visible", 4)
|
||||
self._ok_button.bind_property("visible", self._cancel_button, "visible", 4)
|
||||
# Style
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
@@ -585,37 +598,44 @@ class IptvListConfigurationDialog(IptvListDialog):
|
||||
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()
|
||||
all_default = self.is_all_data_default()
|
||||
|
||||
st_type = get_stream_type(self._stream_type_combobox)
|
||||
s_id = 0 if id_default else int(self._list_srv_id_entry.get_text())
|
||||
s_id = "0" if id_default else self._list_srv_id_entry.get_text()
|
||||
srv_type = "1" if type_default else self._list_srv_type_entry.get_text()
|
||||
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()))
|
||||
namespace = "0" if namespace_default else "{:X}".format(int(self._list_namespace_entry.get_text()))
|
||||
sid = "0" if sid_auto else self._list_sid_entry.get_text()
|
||||
tid = "0" if tid_default else f"{int(self._list_tid_entry.get_text()):X}"
|
||||
nid = "0" if nid_default else f"{int(self._list_nid_entry.get_text()):X}"
|
||||
namespace = "0" if namespace_default else f"{int(self._list_namespace_entry.get_text()):X}"
|
||||
params = [int(el.get_text()) for el in self._digit_elems[2:]]
|
||||
|
||||
for index, row in enumerate(self._rows):
|
||||
fav_id = row[Column.FAV_ID]
|
||||
data, sep, desc = fav_id.partition("http")
|
||||
data = data.split(":")
|
||||
|
||||
if self.is_all_data_default():
|
||||
if all_default:
|
||||
data[1], data[2], data[3], data[4], data[5], data[6] = "010000"
|
||||
else:
|
||||
data[0], data[1], data[2], data[4], data[5], data[6] = st_type, s_id, srv_type, tid, nid, namespace
|
||||
data[3] = "{:X}".format(index) if sid_auto else "0"
|
||||
|
||||
data[3] = f"{index:X}" if sid_auto else sid
|
||||
if sid_auto:
|
||||
params[0] = index
|
||||
picon_id = _PICON_FORMAT.format(int(s_id), int(srv_type), *params)
|
||||
data = ":".join(data)
|
||||
new_fav_id = "{}{}{}".format(data, sep, desc)
|
||||
new_fav_id = f"{data}{sep}{desc}"
|
||||
row[Column.FAV_ID] = new_fav_id
|
||||
srv = self._services.pop(fav_id, None)
|
||||
|
||||
if srv:
|
||||
self._services[new_fav_id] = srv._replace(fav_id=new_fav_id)
|
||||
self._services[new_fav_id] = srv._replace(fav_id=new_fav_id, picon_id=picon_id)
|
||||
|
||||
self._bouquet.clear()
|
||||
list(map(lambda r: self._bouquet.append(r[Column.FAV_ID]), self._fav_model))
|
||||
|
||||
self._info_bar.set_visible(True)
|
||||
self._ok_button.set_visible(True)
|
||||
|
||||
|
||||
class M3uImportDialog(IptvListDialog):
|
||||
@@ -625,7 +645,7 @@ class M3uImportDialog(IptvListDialog):
|
||||
super().__init__(transient, s_type)
|
||||
|
||||
self._app = app
|
||||
self._picons = app._picons
|
||||
self._picons = app.picons
|
||||
self._pic_path = app._settings.profile_picons_path
|
||||
self._services = None
|
||||
self._url_count = 0
|
||||
@@ -636,8 +656,6 @@ class M3uImportDialog(IptvListDialog):
|
||||
self._dialog.set_title(get_message("Playlist import"))
|
||||
self._dialog.connect("delete-event", self.on_close)
|
||||
self._apply_button.set_label(get_message("Import"))
|
||||
self._ok_button.bind_property("visible", self._apply_button, "visible", 4)
|
||||
self._ok_button.bind_property("visible", self._cancel_button, "visible", 4)
|
||||
# Progress
|
||||
self._progress_bar = Gtk.ProgressBar(visible=False, valign="center")
|
||||
self._spinner = Gtk.Spinner(active=False)
|
||||
@@ -653,7 +671,7 @@ class M3uImportDialog(IptvListDialog):
|
||||
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 = Gtk.HBox(visible=True, sensitive=False, spacing=5)
|
||||
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
|
||||
@@ -662,7 +680,7 @@ class M3uImportDialog(IptvListDialog):
|
||||
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 = Gtk.Frame(visible=True, margin_bottom=5)
|
||||
frame.add(extra_box)
|
||||
self._data_box.add(frame)
|
||||
|
||||
@@ -678,7 +696,7 @@ class M3uImportDialog(IptvListDialog):
|
||||
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)
|
||||
msg = f"{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)
|
||||
|
||||
@@ -697,6 +715,8 @@ class M3uImportDialog(IptvListDialog):
|
||||
s_type = params[1]
|
||||
params = params[2:]
|
||||
st_type = get_stream_type(self._stream_type_combobox)
|
||||
sid_auto = self._sid_auto_check_button.get_active()
|
||||
sid = 0 if sid_auto else int(self._list_sid_entry.get_text())
|
||||
|
||||
for i, s in enumerate(self._services, start=params[0]):
|
||||
# Skipping markers.
|
||||
@@ -704,8 +724,8 @@ class M3uImportDialog(IptvListDialog):
|
||||
services.append(s)
|
||||
continue
|
||||
|
||||
params[0] = i
|
||||
picon_id = "{}_{}_{:X}_{:X}_{:X}_{:X}_{:X}_0_0_0.png".format(st_type, s_id, s_type, *params)
|
||||
params[0] = i if sid_auto else sid
|
||||
picon_id = _PICON_FORMAT.format(s_id, s_type, *params)
|
||||
fav_id = get_fav_id(s.data_id, s.service, self._s_type, params, st_type, s_id, s_type)
|
||||
if s.picon:
|
||||
picons[s.picon] = picon_id
|
||||
@@ -764,16 +784,16 @@ class M3uImportDialog(IptvListDialog):
|
||||
@run_idle
|
||||
def on_picon_load_done(self, data, user_data):
|
||||
try:
|
||||
self._info_label.set_text("Processing: {}".format(user_data))
|
||||
self._info_label.set_text(f"Processing: {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)
|
||||
path = f"{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))
|
||||
log(f"Loading picon [{user_data}] data error: {e}")
|
||||
else:
|
||||
self.update_progress()
|
||||
|
||||
@@ -789,15 +809,15 @@ class M3uImportDialog(IptvListDialog):
|
||||
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._info_label.set_text(f"Errors: {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
|
||||
services = self._app.current_services
|
||||
picons = self._app.picons
|
||||
model = self._app.fav_view.get_model()
|
||||
for r in model:
|
||||
s = services.get(r[Column.FAV_ID], None)
|
||||
@@ -828,7 +848,7 @@ class M3uImportDialog(IptvListDialog):
|
||||
|
||||
|
||||
class YtListImportDialog:
|
||||
def __init__(self, transient, settings, appender):
|
||||
def __init__(self, app):
|
||||
handlers = {"on_import": self.on_import,
|
||||
"on_receive": self.on_receive,
|
||||
"on_yt_url_entry_changed": self.on_url_entry_changed,
|
||||
@@ -840,12 +860,13 @@ class YtListImportDialog:
|
||||
"on_key_press": self.on_key_press,
|
||||
"on_close": self.on_close}
|
||||
|
||||
self.appender = appender
|
||||
self._s_type = settings.setting_type
|
||||
# self._main_window, self._settings, self.append_imported_services
|
||||
self.appender = app.append_imported_services
|
||||
self._settings = app.app_settings
|
||||
self._s_type = self._settings.setting_type
|
||||
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,
|
||||
@@ -854,7 +875,7 @@ class YtListImportDialog:
|
||||
"yt_import_image"))
|
||||
|
||||
self._dialog = builder.get_object("yt_import_dialog_window")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._dialog.set_transient_for(app.app_window)
|
||||
self._list_view_scrolled_window = builder.get_object("yt_list_view_scrolled_window")
|
||||
self._model = builder.get_object("yt_liststore")
|
||||
self._progress_bar = builder.get_object("yt_progress_bar")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -32,7 +32,7 @@ Author: Dmitriy Yefremov
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellite list editor. -->
|
||||
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkFrame" id="log_frame">
|
||||
<property name="visible">True</property>
|
||||
@@ -80,7 +80,27 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
<object class="GtkButton" id="close_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Close</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_close" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="close_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-close</property>
|
||||
</object>
|
||||
</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>
|
||||
</object>
|
||||
<packing>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -30,7 +30,7 @@ import logging
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import LOGGER_NAME
|
||||
from app.commons import LOGGER_NAME, LOG_FORMAT, LOG_DATE_FORMAT
|
||||
from app.ui.dialogs import get_builder
|
||||
from app.ui.main_helper import append_text_to_tview
|
||||
from app.ui.uicommons import Gtk, UI_RESOURCES_PATH
|
||||
@@ -43,15 +43,16 @@ class LogsClient(Gtk.Box):
|
||||
def __init__(self, view):
|
||||
logging.Handler.__init__(self)
|
||||
self._view = view
|
||||
self.setFormatter(logging.Formatter(fmt=LOG_FORMAT, datefmt=LOG_DATE_FORMAT))
|
||||
|
||||
def handle(self, rec):
|
||||
GLib.idle_add(append_text_to_tview, f"{rec.msg}\n", self._view)
|
||||
def handle(self, rec: logging.LogRecord):
|
||||
GLib.idle_add(append_text_to_tview, f"{self.format(rec)}\n", self._view)
|
||||
|
||||
def __init__(self, app, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._app = app
|
||||
|
||||
handlers = {"on_clear": self.on_clear}
|
||||
handlers = {"on_clear": self.on_clear, "on_close": self.on_close}
|
||||
builder = get_builder(UI_RESOURCES_PATH + "logs.glade", handlers)
|
||||
|
||||
self._log_view = builder.get_object("log_view")
|
||||
@@ -64,3 +65,10 @@ class LogsClient(Gtk.Box):
|
||||
|
||||
def on_clear(self, button):
|
||||
GLib.idle_add(self._log_view.get_buffer().set_text, "")
|
||||
|
||||
def on_close(self, button):
|
||||
self._app.change_action_state("on_logs_show", GLib.Variant.new_boolean(False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
@@ -1,18 +1,37 @@
|
||||
* {
|
||||
-GtkDialog-action-area-border: 5em;
|
||||
background-clip: padding-box;
|
||||
-GtkScrolledWindow-scrollbar-spacing: 0;
|
||||
-GtkToolItemGroup-expander-size: 11;
|
||||
-GtkWidget-text-handle-width: 20;
|
||||
-GtkWidget-text-handle-height: 20;
|
||||
-GtkDialog-button-spacing: 12;
|
||||
-GtkDialog-action-area-border: 6;
|
||||
}
|
||||
|
||||
entry {
|
||||
min-height: 2.0em;
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
entry > image {
|
||||
padding-left: 0.3em;
|
||||
padding-right: 0.3em;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 1.5em;
|
||||
min-width: 1em;
|
||||
padding-left: 0.4em;
|
||||
padding-right: 0.4em;
|
||||
padding-top: 0.1em;
|
||||
padding-bottom: 0.1em;
|
||||
min-height: 1.2em;
|
||||
min-width: 1.5em;
|
||||
padding-top: 0.3em;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
button:active, button:checked {
|
||||
color: @theme_selected_fg_color;
|
||||
background-image: linear-gradient(@theme_selected_bg_color, @theme_selected_bg_color);
|
||||
}
|
||||
|
||||
combobox {
|
||||
min-height: 2.2em;
|
||||
}
|
||||
|
||||
spinbutton {
|
||||
@@ -32,7 +51,21 @@ infobar {
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
revealer > box > button {
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
switch slider {
|
||||
min-height: 1.5em;
|
||||
min-width: 1.5em;
|
||||
}
|
||||
|
||||
.font > box {
|
||||
min-height: 1.5em;
|
||||
padding-top: 0.1em;
|
||||
padding-bottom: 0.1em;
|
||||
}
|
||||
|
||||
.dialog-action-area button {
|
||||
margin-bottom: 0.6em;
|
||||
}
|
||||
|
||||
2796
app/ui/main.glade
2796
app/ui/main.glade
File diff suppressed because it is too large
Load Diff
1673
app/ui/main.py
1673
app/ui/main.py
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 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,10 +26,21 @@
|
||||
#
|
||||
|
||||
|
||||
""" Helper module for the ui. """
|
||||
""" Helper module for the GUI. """
|
||||
|
||||
__all__ = ("insert_marker", "move_items", "rename", "ViewTarget", "set_flags", "locate_in_services",
|
||||
"scroll_to", "get_base_model", "copy_reference", "assign_picons", "remove_picon",
|
||||
"is_only_one_item_selected", "gen_bouquets", "BqGenType", "get_selection", "get_service_reference",
|
||||
"get_model_data", "remove_all_unused_picons", "get_picon_pixbuf", "get_base_itrs", "get_iptv_url",
|
||||
"get_iptv_data", "update_entry_data", "append_text_to_tview", "on_popup_menu", "get_picon_file_name")
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import unicodedata
|
||||
from collections import defaultdict
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote
|
||||
|
||||
from gi.repository import GdkPixbuf, GLib
|
||||
@@ -37,8 +48,8 @@ from gi.repository import GdkPixbuf, GLib
|
||||
from app.eparser import Service
|
||||
from app.eparser.ecommons import Flag, BouquetService, Bouquet, BqType
|
||||
from app.eparser.enigma.bouquets import BqServiceType, to_bouquet_id
|
||||
from app.settings import SettingsType, SEP, IS_WIN
|
||||
from .dialogs import show_dialog, DialogType, get_chooser_dialog
|
||||
from app.settings import SettingsType, SEP, IS_WIN, IS_DARWIN, IS_LINUX
|
||||
from .dialogs import show_dialog, DialogType, get_message
|
||||
from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON, KeyboardKey, Column
|
||||
|
||||
|
||||
@@ -57,7 +68,7 @@ def insert_marker(view, bouquets, selected_bouquet, services, parent_window, m_t
|
||||
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)
|
||||
fav_id = f"1:64:0:0:0:0:0:0:0:0::{response}\n#DESCRIPTION {response}\n"
|
||||
text = response
|
||||
|
||||
s_type = m_type.name
|
||||
@@ -103,8 +114,8 @@ def move_items(key, view: Gtk.TreeView):
|
||||
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))
|
||||
min_path = Gtk.TreePath.new_from_string(f"{parent_index}:{0}")
|
||||
max_path = Gtk.TreePath.new_from_string(f"{parent_index}:{children_num}")
|
||||
is_tree_store = True
|
||||
|
||||
if key is KeyboardKey.UP:
|
||||
@@ -274,7 +285,7 @@ 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}
|
||||
skip_type = {BqServiceType.MARKER.name, BqServiceType.SPACE.name, BqServiceType.ALT.name}
|
||||
|
||||
for path in paths:
|
||||
itr = model.get_iter(path)
|
||||
@@ -352,7 +363,7 @@ def has_locked_hide(model, paths, col_num):
|
||||
|
||||
# ***************** Location *******************#
|
||||
|
||||
def locate_in_services(fav_view, services_view, parent_window):
|
||||
def locate_in_services(fav_view, services_view, column, parent_window):
|
||||
""" Locating and scrolling to the service """
|
||||
model, paths = fav_view.get_selection().get_selected_rows()
|
||||
|
||||
@@ -364,7 +375,7 @@ def locate_in_services(fav_view, services_view, parent_window):
|
||||
|
||||
fav_id = model.get_value(model.get_iter(paths[0]), Column.FAV_ID)
|
||||
for index, row in enumerate(services_view.get_model()):
|
||||
if row[Column.SRV_FAV_ID] == fav_id:
|
||||
if row[column] == fav_id:
|
||||
scroll_to(index, services_view)
|
||||
break
|
||||
|
||||
@@ -379,26 +390,36 @@ def scroll_to(index, view, paths=None):
|
||||
selection.select_path(index)
|
||||
|
||||
|
||||
# ***************** Picons *********************#
|
||||
# ***************** Picons ********************* #
|
||||
|
||||
def update_picons_data(path, picons, size=32):
|
||||
if not os.path.exists(path):
|
||||
return
|
||||
def get_picon_dialog(transient, title, button_text, multiple=True):
|
||||
""" Returns a copy dialog with a preview of images [picons -> *.png]. """
|
||||
dialog = Gtk.FileChooserNative.new(title, transient, Gtk.FileChooserAction.OPEN, button_text)
|
||||
dialog.set_select_multiple(multiple)
|
||||
dialog.set_modal(True)
|
||||
# Filter.
|
||||
file_filter = Gtk.FileFilter()
|
||||
file_filter.set_name("*.png")
|
||||
file_filter.add_pattern("*.png")
|
||||
file_filter.add_mime_type("image/png") if IS_DARWIN else None
|
||||
dialog.add_filter(file_filter)
|
||||
|
||||
for file in os.listdir(path):
|
||||
pf = get_picon_pixbuf(path + file, size)
|
||||
if pf:
|
||||
picons[file] = pf
|
||||
if IS_LINUX:
|
||||
preview_image = Gtk.Image(margin_right=10)
|
||||
dialog.set_preview_widget(preview_image)
|
||||
|
||||
def update_preview_widget(dlg):
|
||||
path = dialog.get_preview_filename()
|
||||
if not path:
|
||||
return
|
||||
|
||||
def append_picons(picons, model):
|
||||
def append_picons_data(pcs, mod):
|
||||
for r in mod:
|
||||
mod.set_value(mod.get_iter(r.path), Column.SRV_PICON, pcs.get(r[Column.SRV_PICON_ID], None))
|
||||
yield True
|
||||
pix = get_picon_pixbuf(path, 220)
|
||||
preview_image.set_from_pixbuf(pix)
|
||||
dlg.set_preview_widget_active(bool(pix))
|
||||
|
||||
app = append_picons_data(picons, model)
|
||||
GLib.idle_add(lambda: next(app, False), priority=GLib.PRIORITY_LOW)
|
||||
dialog.connect("update-preview", update_preview_widget)
|
||||
|
||||
return dialog
|
||||
|
||||
|
||||
def assign_picons(target, srv_view, fav_view, transient, picons, settings, services, src_path=None, dst_path=None):
|
||||
@@ -408,10 +429,12 @@ def assign_picons(target, srv_view, fav_view, transient, picons, settings, servi
|
||||
picons_files = []
|
||||
|
||||
if not src_path:
|
||||
src_path = get_chooser_dialog(transient, settings, "*.png files", ("*.png",))
|
||||
if src_path == Gtk.ResponseType.CANCEL:
|
||||
dialog = get_picon_dialog(transient, get_message("Picon selection"), get_message("Open"), False)
|
||||
if dialog.run() in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT) or not dialog.get_filenames():
|
||||
return picons_files
|
||||
|
||||
src_path = dialog.get_filenames()[0]
|
||||
|
||||
if IS_WIN:
|
||||
src_path = src_path.lstrip("/")
|
||||
dst_path = dst_path.lstrip("/") if dst_path else dst_path
|
||||
@@ -443,7 +466,7 @@ def assign_picons(target, srv_view, fav_view, transient, picons, settings, servi
|
||||
pass # NOP
|
||||
else:
|
||||
picons_files.append(picon_file)
|
||||
picon = get_picon_pixbuf(picon_file)
|
||||
picon = get_picon_pixbuf(picon_file, settings.list_picon_size)
|
||||
picons[picon_id] = picon
|
||||
model.set_value(itr, p_pos, picon)
|
||||
if target is ViewTarget.SERVICES:
|
||||
@@ -503,36 +526,17 @@ def remove_picon(target, srv_view, fav_view, picons, settings):
|
||||
remove_picons(settings, picon_ids, picons)
|
||||
|
||||
|
||||
def copy_picon_reference(target, view, services, clipboard, transient):
|
||||
""" Copying picon id to clipboard """
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if not is_only_one_item_selected(paths, transient):
|
||||
return
|
||||
|
||||
if target is ViewTarget.SERVICES:
|
||||
picon_id = model.get_value(model.get_iter(paths), Column.SRV_PICON_ID)
|
||||
if picon_id:
|
||||
clipboard.set_text(picon_id.rstrip(".png"), -1)
|
||||
else:
|
||||
show_dialog(DialogType.ERROR, transient, "No reference is present!")
|
||||
elif target is ViewTarget.FAV:
|
||||
fav_id = model.get_value(model.get_iter(paths), Column.FAV_ID)
|
||||
srv = services.get(fav_id, None)
|
||||
if srv and srv.picon_id:
|
||||
clipboard.set_text(srv.picon_id.rstrip(".png"), -1)
|
||||
else:
|
||||
show_dialog(DialogType.ERROR, transient, "No reference is present!")
|
||||
|
||||
|
||||
def remove_all_unused_picons(settings, picons, services):
|
||||
def remove_all_unused_picons(settings, services):
|
||||
""" Removes picons from profile picons folder if there are no services for these picons. """
|
||||
ids = {s.picon_id for s in services}
|
||||
pcs = list(filter(lambda x: x not in ids, picons))
|
||||
remove_picons(settings, pcs, picons)
|
||||
for p in Path(settings.profile_picons_path).glob("*.png"):
|
||||
if p.name not in ids and p.is_file():
|
||||
p.unlink()
|
||||
|
||||
|
||||
def remove_picons(settings, picon_ids, picons):
|
||||
pions_path = settings.profile_picons_path
|
||||
backup_path = "{}{}{}".format(settings.profile_backup_path, "picons", SEP)
|
||||
backup_path = f"{settings.profile_backup_path}picons{SEP}"
|
||||
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
|
||||
for p_id in picon_ids:
|
||||
picons[p_id] = None
|
||||
@@ -556,8 +560,15 @@ def is_only_one_item_selected(paths, transient):
|
||||
def get_picon_pixbuf(path, size=32):
|
||||
try:
|
||||
return GdkPixbuf.Pixbuf.new_from_file_at_scale(path, width=size, height=size, preserve_aspect_ratio=True)
|
||||
except GLib.GError as e:
|
||||
pass
|
||||
except GLib.GError:
|
||||
pass # NOP
|
||||
|
||||
|
||||
@lru_cache(50)
|
||||
def get_picon_file_name(service_name):
|
||||
""" Returns picon file name by service name. """
|
||||
name = unicodedata.normalize("NFKD", service_name).encode("ASCII", errors="ignore").decode(errors="ignore")
|
||||
return f"{re.sub('[^a-z0-9]', '', name.replace('&', 'and').replace('+', 'plus').replace('*', 'star').lower())}.png"
|
||||
|
||||
|
||||
# ***************** Bouquets ********************* #
|
||||
@@ -615,7 +626,41 @@ def get_bouquets_names(model):
|
||||
return bouquets_names
|
||||
|
||||
|
||||
# ***************** Others *********************#
|
||||
# ***************** Others ********************* #
|
||||
|
||||
def copy_reference(view, app):
|
||||
""" Copying picon id to clipboard. """
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if not is_only_one_item_selected(paths, app.app_window):
|
||||
return
|
||||
|
||||
target = app.get_target_view(view)
|
||||
clipboard = app._clipboard
|
||||
|
||||
if target is ViewTarget.SERVICES:
|
||||
picon_id = model.get_value(model.get_iter(paths), Column.SRV_PICON_ID)
|
||||
if picon_id:
|
||||
clipboard.set_text(picon_id.rstrip(".png"), -1)
|
||||
else:
|
||||
app.show_error_message("No reference is present!")
|
||||
elif target is ViewTarget.FAV:
|
||||
fav_id = model.get_value(model.get_iter(paths), Column.FAV_ID)
|
||||
srv = app.current_services.get(fav_id, None)
|
||||
if srv and srv.picon_id:
|
||||
clipboard.set_text(get_service_reference(srv), -1)
|
||||
else:
|
||||
app.show_error_message("No reference is present!")
|
||||
|
||||
app.emit("clipboard-changed", clipboard.wait_is_text_available())
|
||||
|
||||
|
||||
def get_service_reference(srv):
|
||||
ref = srv.picon_id.rstrip(".png")
|
||||
if srv.service_type == BqServiceType.IPTV.name:
|
||||
p = re.compile(r"\d+")
|
||||
return re.sub(p, re.search(p, srv.fav_id).group(), ref, 1)
|
||||
return ref
|
||||
|
||||
|
||||
def update_entry_data(entry, dialog, settings):
|
||||
""" Updates value in text entry from chooser dialog. """
|
||||
@@ -664,9 +709,9 @@ def append_text_to_tview(char, view):
|
||||
view.scroll_to_mark(insert, 0.0, True, 0.0, 1.0)
|
||||
|
||||
|
||||
def get_iptv_url(row, s_type):
|
||||
def get_iptv_url(row, s_type, column=Column.FAV_ID):
|
||||
""" Returns url from iptv type row """
|
||||
data = row[Column.FAV_ID].split(":" if s_type is SettingsType.ENIGMA_2 else "::")
|
||||
data = row[column].split(":" if s_type is SettingsType.ENIGMA_2 else "::")
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
data = list(filter(lambda x: "http" in x, data))
|
||||
if data:
|
||||
@@ -674,6 +719,15 @@ def get_iptv_url(row, s_type):
|
||||
return unquote(url) if s_type is SettingsType.ENIGMA_2 else url
|
||||
|
||||
|
||||
def get_iptv_data(fav_id):
|
||||
""" Returns the reference and URL as a tuple from the fav_id. """
|
||||
data, sep, desc = fav_id.partition("#DESCRIPTION")
|
||||
data = data.split(":")
|
||||
if len(data) < 11:
|
||||
return None, None, desc
|
||||
return ":".join(data[:10]), unquote(data[10].strip())
|
||||
|
||||
|
||||
def on_popup_menu(menu, event):
|
||||
""" Shows popup menu for the view """
|
||||
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2022 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
|
||||
@@ -251,8 +251,8 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkBox" id="header_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">15</property>
|
||||
<property name="margin_right">15</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
@@ -348,51 +348,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="send_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Transfer to receiver</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_send" swapped="no"/>
|
||||
<signal name="drag-data-received" handler="on_send_button_drag_data_received" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="send_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-go-up</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="download_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Download from the receiver</property>
|
||||
<signal name="clicked" handler="on_download" swapped="no"/>
|
||||
<signal name="drag-data-received" handler="on_download_button_drag_data_received" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="download_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-go-down</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="remove_button">
|
||||
<property name="visible">True</property>
|
||||
@@ -400,7 +355,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Remove all picons from the receiver</property>
|
||||
<signal name="clicked" handler="on_remove" swapped="no"/>
|
||||
<signal name="drag-data-received" handler="on_remove_button_drag_data_received" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="remove_button_image">
|
||||
<property name="visible">True</property>
|
||||
@@ -430,22 +384,14 @@ Author: Dmitriy Yefremov
|
||||
<property name="layout_style">expand</property>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="manager_button">
|
||||
<property name="width_request">50</property>
|
||||
<property name="label" translatable="yes">Explorer</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Explorer</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<property name="group">converter_button</property>
|
||||
<signal name="toggled" handler="on_tool_switched" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">folder-open-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
@@ -456,7 +402,7 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="downloader_button">
|
||||
<property name="width_request">50</property>
|
||||
<property name="label" translatable="yes">Downloader</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
@@ -465,13 +411,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="draw_indicator">False</property>
|
||||
<property name="group">manager_button</property>
|
||||
<signal name="toggled" handler="on_tool_switched" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">insert-image-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
@@ -482,7 +421,7 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="converter_button">
|
||||
<property name="width_request">50</property>
|
||||
<property name="label" translatable="yes">Converter</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
@@ -491,13 +430,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="draw_indicator">False</property>
|
||||
<property name="group">manager_button</property>
|
||||
<signal name="toggled" handler="on_tool_switched" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">object-rotate-right-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
@@ -609,6 +541,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="primary_icon_name">edit-find-replace-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="primary_icon_sensitive">False</property>
|
||||
<property name="placeholder_text" translatable="yes">Filter: N1|N2|N3, etc..</property>
|
||||
<signal name="search-changed" handler="on_picons_filter_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
@@ -618,10 +551,10 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="auto_filer_switch">
|
||||
<object class="GtkSwitch" id="auto_filter_switch">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Automatically set the name selected in the favorites list. </property>
|
||||
<property name="tooltip_text" translatable="yes">Automatically set the name selected in the favorites list.</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_right">5</property>
|
||||
</object>
|
||||
@@ -633,7 +566,7 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="auto_filer_label">
|
||||
<object class="GtkLabel" id="auto_filter_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Auto</property>
|
||||
@@ -675,7 +608,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="src_filter_button">
|
||||
<property name="label" translatable="yes">Filer</property>
|
||||
<property name="label" translatable="yes">Filter</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="receives_default">False</property>
|
||||
@@ -705,8 +638,10 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkTreeView" id="picons_src_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Drag the services to the desired picon or picon to the list of selected services. </property>
|
||||
<property name="tooltip_text" translatable="yes">Drag the services to the desired picon or picon to the list of selected services.</property>
|
||||
<property name="model">picons_src_sort_model</property>
|
||||
<property name="fixed_height_mode">True</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<property name="enable_grid_lines">horizontal</property>
|
||||
<property name="tooltip_column">0</property>
|
||||
<property name="activate_on_single_click">True</property>
|
||||
@@ -718,15 +653,21 @@ Author: Dmitriy Yefremov
|
||||
<signal name="key-press-event" handler="on_tree_view_key_press" swapped="no"/>
|
||||
<signal name="query-tooltip" handler="on_view_query_tooltip" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="picons_src_view_selection"/>
|
||||
<object class="GtkTreeSelection" id="picons_src_view_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="src_picon_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">150</property>
|
||||
<property name="title" translatable="yes">Picon</property>
|
||||
<property name="alignment">0.49000000953674316</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererPixbuf" id="picons_src_renderer"/>
|
||||
<object class="GtkCellRendererPixbuf" id="picons_src_renderer">
|
||||
<property name="height">50</property>
|
||||
<property name="ypad">5</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
@@ -735,6 +676,7 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="src_title_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="title" translatable="yes">Name</property>
|
||||
<property name="alignment">0.49000000953674316</property>
|
||||
<child>
|
||||
@@ -750,6 +692,7 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="src_path_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="title" translatable="yes">column</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="path_src_renderer"/>
|
||||
@@ -833,8 +776,10 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkTreeView" id="picons_dest_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Drag the services to the desired picon or picon to the list of selected services. </property>
|
||||
<property name="tooltip_text" translatable="yes">Drag the services to the desired picon or picon to the list of selected services.</property>
|
||||
<property name="model">picons_dst_sort_model</property>
|
||||
<property name="fixed_height_mode">True</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<property name="enable_grid_lines">horizontal</property>
|
||||
<property name="tooltip_column">0</property>
|
||||
<property name="activate_on_single_click">True</property>
|
||||
@@ -847,15 +792,21 @@ Author: Dmitriy Yefremov
|
||||
<signal name="key-press-event" handler="on_tree_view_key_press" swapped="no"/>
|
||||
<signal name="query-tooltip" handler="on_view_query_tooltip" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="picons_dest_view_selection"/>
|
||||
<object class="GtkTreeSelection" id="picons_dest_view_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="dest_picon_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">150</property>
|
||||
<property name="title" translatable="yes">Picon</property>
|
||||
<property name="alignment">0.49000000953674316</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererPixbuf" id="picons_dest_renderer"/>
|
||||
<object class="GtkCellRendererPixbuf" id="picons_dest_renderer">
|
||||
<property name="height">50</property>
|
||||
<property name="ypad">5</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
@@ -864,6 +815,7 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="dest_title_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="title" translatable="yes">Name</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.49000000953674316</property>
|
||||
@@ -880,6 +832,7 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="dest_path_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="title" translatable="yes">column</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="path_dest_renderer"/>
|
||||
@@ -1715,89 +1668,13 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="info_bar_frame">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="label_xalign">0</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="baseline_position">bottom</property>
|
||||
<property name="message_type">other</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="layout_style">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox" id="info_bar_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="info_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="info_view">
|
||||
<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>
|
||||
<property name="top_margin">2</property>
|
||||
<property name="bottom_margin">2</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label_item">
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="bottom_bar_box">
|
||||
<property name="height_request">24</property>
|
||||
<property name="height_request">26</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">2</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="count_image">
|
||||
|
||||
222
app/ui/picons.py
222
app/ui/picons.py
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -37,14 +37,13 @@ from gi.repository import GLib, GdkPixbuf, Gio
|
||||
|
||||
from app.commons import run_idle, run_task, run_with_delay, log
|
||||
from app.connections import upload_data, DownloadType, download_data, remove_picons
|
||||
from app.settings import SettingsType, Settings, SEP, IS_LINUX, IS_DARWIN
|
||||
from app.settings import SettingsType, Settings, SEP, IS_DARWIN
|
||||
from app.tools.picons import (PiconsParser, parse_providers, Provider, convert_to, download_picon, PiconsCzDownloader,
|
||||
PiconsError)
|
||||
from app.tools.satellites import SatellitesParser, SatelliteSource
|
||||
from .dialogs import show_dialog, DialogType, get_message, get_builder, get_chooser_dialog
|
||||
from .main_helper import (update_entry_data, append_text_to_tview, scroll_to, on_popup_menu, get_base_model, set_picon,
|
||||
get_picon_pixbuf)
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TV_ICON, Column, KeyboardKey, Page
|
||||
from .main_helper import scroll_to, on_popup_menu, get_base_model, set_picon, get_picon_pixbuf, get_picon_dialog
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TV_ICON, Column, KeyboardKey, Page, ViewTarget
|
||||
|
||||
|
||||
class PiconManager(Gtk.Box):
|
||||
@@ -56,9 +55,12 @@ class PiconManager(Gtk.Box):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._app = app
|
||||
self._app.connect("data-receive", self.on_download)
|
||||
self._app.connect("data-send", self.on_send)
|
||||
self._app.connect("page-changed", self.update_picons_dest)
|
||||
self._app.connect("filter-toggled", self.on_app_filter_toggled)
|
||||
self._app.connect("profile-changed", self.on_profile_changed)
|
||||
self._app.connect("picon-assign", self.on_picon_assign)
|
||||
self._app.fav_view.connect("row-activated", self.on_fav_changed)
|
||||
self._picon_ids = picon_ids
|
||||
self._sat_positions = sat_positions
|
||||
@@ -83,10 +85,7 @@ class PiconManager(Gtk.Box):
|
||||
"on_extract": self.on_extract,
|
||||
"on_receive": self.on_receive,
|
||||
"on_cancel": self.on_cancel,
|
||||
"on_send": self.on_send,
|
||||
"on_download": self.on_download,
|
||||
"on_remove": self.on_remove,
|
||||
"on_picons_dir_open": self.on_picons_dir_open,
|
||||
"on_selected_toggled": self.on_selected_toggled,
|
||||
"on_url_changed": self.on_url_changed,
|
||||
"on_picons_filter_changed": self.on_picons_filter_changed,
|
||||
@@ -97,9 +96,6 @@ class PiconManager(Gtk.Box):
|
||||
"on_picons_view_drag_data_received": self.on_picons_view_drag_data_received,
|
||||
"on_picons_view_drag_end": self.on_picons_view_drag_end,
|
||||
"on_picon_info_image_drag_data_received": self.on_picon_info_image_drag_data_received,
|
||||
"on_send_button_drag_data_received": self.on_send_button_drag_data_received,
|
||||
"on_download_button_drag_data_received": self.on_download_button_drag_data_received,
|
||||
"on_remove_button_drag_data_received": self.on_remove_button_drag_data_received,
|
||||
"on_selective_send": self.on_selective_send,
|
||||
"on_selective_download": self.on_selective_download,
|
||||
"on_selective_remove": self.on_selective_remove,
|
||||
@@ -166,20 +162,14 @@ class PiconManager(Gtk.Box):
|
||||
self._info_check_button = builder.get_object("info_check_button")
|
||||
self._picon_info_image = builder.get_object("picon_info_image")
|
||||
self._picon_info_label = builder.get_object("picon_info_label")
|
||||
self._info_view = builder.get_object("info_view")
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._info_bar.bind_property("visible", builder.get_object("info_bar_frame"), "visible")
|
||||
self._info_bar.connect("response", lambda b, r: b.set_visible(False))
|
||||
# Filter.
|
||||
self._filter_bar = builder.get_object("filter_bar")
|
||||
self._auto_filer_switch = builder.get_object("auto_filer_switch")
|
||||
self._auto_filter_switch = builder.get_object("auto_filter_switch")
|
||||
self._filter_button = builder.get_object("filter_button")
|
||||
self._filter_button.bind_property("active", self._filter_bar, "visible")
|
||||
self._filter_button.bind_property("active", self._src_filter_button, "visible")
|
||||
self._filter_button.bind_property("active", self._dst_filter_button, "visible")
|
||||
self._filter_button.bind_property("visible", self._info_check_button, "visible")
|
||||
self._filter_button.bind_property("visible", self._send_button, "visible")
|
||||
self._filter_button.bind_property("visible", self._download_button, "visible")
|
||||
self._filter_button.bind_property("visible", self._remove_button, "visible")
|
||||
self._src_button = builder.get_object("src_button")
|
||||
self._src_button.bind_property("active", builder.get_object("explorer_dst_label"), "visible")
|
||||
@@ -196,6 +186,11 @@ class PiconManager(Gtk.Box):
|
||||
self._manager_button.bind_property("active", builder.get_object("add_menu_button"), "visible")
|
||||
# Init drag-and-drop
|
||||
self.init_drag_and_drop()
|
||||
# Rendering.
|
||||
column = builder.get_object("dest_picon_column")
|
||||
column.set_cell_data_func(builder.get_object("picons_dest_renderer"), self.picon_data_func)
|
||||
column = builder.get_object("src_picon_column")
|
||||
column.set_cell_data_func(builder.get_object("picons_src_renderer"), self.picon_data_func)
|
||||
# Settings
|
||||
self._settings = settings
|
||||
self._s_type = settings.setting_type
|
||||
@@ -250,6 +245,19 @@ class PiconManager(Gtk.Box):
|
||||
self._current_path_label.set_text(self._settings.profile_picons_path)
|
||||
self.update_picons_dest(app, self._app.page)
|
||||
|
||||
def on_picon_assign(self, app, target):
|
||||
if target is ViewTarget.SERVICES:
|
||||
model, paths = app.services_view.get_selection().get_selected_rows()
|
||||
ids = {model[p][Column.SRV_FAV_ID] for p in paths}
|
||||
else:
|
||||
model, paths = app.fav_view.get_selection().get_selected_rows()
|
||||
ids = {model[p][Column.FAV_ID] for p in paths}
|
||||
|
||||
self._filter_button.set_active(True)
|
||||
self._dst_filter_button.set_active(True)
|
||||
self._picons_filter_entry.set_text(
|
||||
"|".join(s.service for f, s in self._app.current_services.items() if f in ids))
|
||||
|
||||
def update_picons_data(self, view, path=None):
|
||||
if view is self._picons_dest_view:
|
||||
self.update_picon_info()
|
||||
@@ -260,7 +268,7 @@ class PiconManager(Gtk.Box):
|
||||
def update_picons(self, path, view):
|
||||
p_model = view.get_model()
|
||||
model = get_base_model(p_model)
|
||||
factor = self._app.DEL_FACTOR
|
||||
factor = self._app.DEL_FACTOR * 2
|
||||
|
||||
for index, itr in enumerate([row.iter for row in model]):
|
||||
model.remove(itr)
|
||||
@@ -268,18 +276,12 @@ class PiconManager(Gtk.Box):
|
||||
yield True
|
||||
|
||||
self._dst_count_label.set_text("0")
|
||||
if not os.path.isdir(path):
|
||||
return
|
||||
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
for index, file in enumerate(os.listdir(path)):
|
||||
if self._terminate:
|
||||
return
|
||||
|
||||
p_path = f"{path}{SEP}{file}"
|
||||
p = self.get_pixbuf_at_scale(p_path, 72, 48, True)
|
||||
if p:
|
||||
model.append((p, file, p_path))
|
||||
|
||||
model.append((None, file, f"{path}{SEP}{file}"))
|
||||
if index % factor == 0:
|
||||
self._dst_count_label.set_text(str(len(model)))
|
||||
yield True
|
||||
@@ -287,6 +289,9 @@ class PiconManager(Gtk.Box):
|
||||
self._dst_count_label.set_text(str(len(model)))
|
||||
yield True
|
||||
|
||||
def picon_data_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("pixbuf", self.get_pixbuf_at_scale(model.get_value(itr, 2), 72, 48, True))
|
||||
|
||||
def update_picons_from_file(self, view, uri):
|
||||
""" Adds picons in the view on dragging from file system. """
|
||||
path = Path(urlparse(unquote(uri)).path.strip())
|
||||
@@ -327,20 +332,14 @@ class PiconManager(Gtk.Box):
|
||||
self._picon_info_image.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
|
||||
self._picon_info_image.drag_dest_add_uri_targets()
|
||||
|
||||
self._send_button.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
|
||||
self._send_button.drag_dest_add_uri_targets()
|
||||
|
||||
self._download_button.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
|
||||
self._download_button.drag_dest_add_uri_targets()
|
||||
|
||||
self._remove_button.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
|
||||
self._remove_button.drag_dest_add_uri_targets()
|
||||
|
||||
def on_picons_view_drag_data_get(self, view, drag_context, data, info, time):
|
||||
model, path = view.get_selection().get_selected_rows()
|
||||
if path:
|
||||
data.set_uris([Path(model[path][-1]).as_uri(),
|
||||
Path(self._settings.profile_picons_path).as_uri()])
|
||||
dest_uri = Path(self._settings.profile_picons_path).as_uri()
|
||||
if IS_DARWIN:
|
||||
data.set_uris([f"{Path(model[path][-1]).as_uri()}{self._app.DRAG_SEP}{dest_uri}"])
|
||||
else:
|
||||
data.set_uris([Path(model[path][-1]).as_uri(), dest_uri])
|
||||
|
||||
def on_picons_view_drag_drop(self, view, drag_context, x, y, time):
|
||||
view.stop_emission_by_name("drag_drop")
|
||||
@@ -357,8 +356,8 @@ class PiconManager(Gtk.Box):
|
||||
self.update_picons_from_file(view, txt)
|
||||
return
|
||||
|
||||
itr_str, sep, src = txt.partition("::::")
|
||||
if src == self._app.BQ_MODEL_NAME:
|
||||
itr_str, sep, src = txt.partition(self._app.DRAG_SEP)
|
||||
if src == self._app.BQ_MODEL:
|
||||
return
|
||||
|
||||
path, pos = view.get_dest_row_at_pos(x, y) or (None, None)
|
||||
@@ -366,7 +365,7 @@ class PiconManager(Gtk.Box):
|
||||
return
|
||||
|
||||
model = view.get_model()
|
||||
if src == self._app.FAV_MODEL_NAME:
|
||||
if src == self._app.FAV_MODEL:
|
||||
target_view = self._app.fav_view
|
||||
c_id = Column.FAV_ID
|
||||
else:
|
||||
@@ -375,7 +374,7 @@ class PiconManager(Gtk.Box):
|
||||
|
||||
t_mod = target_view.get_model()
|
||||
dest_path = self._settings.profile_picons_path
|
||||
self.update_picons_dest_view(self._app.on_assign_picon(target_view, model[path][-1], dest_path))
|
||||
self.update_picons_dest_view(self._app.on_assign_picon_file(target_view, model[path][-1], dest_path))
|
||||
self.show_assign_info([t_mod.get_value(t_mod.get_iter_from_string(itr), c_id) for itr in itr_str.split(",")])
|
||||
|
||||
@run_idle
|
||||
@@ -400,13 +399,12 @@ class PiconManager(Gtk.Box):
|
||||
|
||||
@run_idle
|
||||
def show_assign_info(self, fav_ids):
|
||||
self._info_bar.show()
|
||||
self._info_view.get_buffer().set_text("")
|
||||
self._app.change_action_state("on_logs_show", GLib.Variant.new_boolean(True))
|
||||
for i in fav_ids:
|
||||
srv = self._app.current_services.get(i, None)
|
||||
if srv:
|
||||
info = self._app.get_hint_for_srv_list(srv)
|
||||
self.append_output(f"Picon assignment for the service:\n{info}\n{' * ' * 30}\n")
|
||||
log(f"Picon assignment for the service:\n{info}\n{' * ' * 30}\n")
|
||||
|
||||
def on_picons_view_drag_end(self, view, drag_context):
|
||||
self.update_picons_dest_view(self._app.picons_buffer)
|
||||
@@ -431,21 +429,6 @@ class PiconManager(Gtk.Box):
|
||||
gen = self.update_picon_in_lists(dst, fav_id)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def on_send_button_drag_data_received(self, button, drag_context, x, y, data, info, time):
|
||||
path = self.get_path_from_uris(data)
|
||||
if path:
|
||||
self.on_send(files_filter={path.name}, path=path.parent)
|
||||
|
||||
def on_download_button_drag_data_received(self, button, drag_context, x, y, data, info, time):
|
||||
path = self.get_path_from_uris(data)
|
||||
if path:
|
||||
self.on_download(files_filter={path.name})
|
||||
|
||||
def on_remove_button_drag_data_received(self, button, drag_context, x, y, data, info, time):
|
||||
path = self.get_path_from_uris(data)
|
||||
if path:
|
||||
self.on_remove(files_filter={path.name})
|
||||
|
||||
def get_path_from_uris(self, data):
|
||||
uris = data.get_uris()
|
||||
if len(uris) == 2:
|
||||
@@ -461,42 +444,12 @@ class PiconManager(Gtk.Box):
|
||||
|
||||
def on_add(self, item):
|
||||
""" Adds (copies) picons from an external folder to the profile picons folder. """
|
||||
dialog = self.get_copy_dialog()
|
||||
dialog = get_picon_dialog(self._app_window, get_message("Add picons"), get_message("Add"))
|
||||
if dialog.run() in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
|
||||
self.copy_picons_file(dialog.get_filenames())
|
||||
|
||||
def get_copy_dialog(self):
|
||||
""" Returns a copy dialog with a preview of images [picons -> *.png]. """
|
||||
dialog = Gtk.FileChooserNative.new(get_message("Add picons"), self._app_window,
|
||||
Gtk.FileChooserAction.OPEN, get_message("Add"))
|
||||
dialog.set_select_multiple(True)
|
||||
dialog.set_modal(True)
|
||||
# Filter.
|
||||
file_filter = Gtk.FileFilter()
|
||||
file_filter.set_name("*.png")
|
||||
file_filter.add_pattern("*.png")
|
||||
file_filter.add_mime_type("image/png") if IS_DARWIN else None
|
||||
dialog.add_filter(file_filter)
|
||||
|
||||
if IS_LINUX:
|
||||
preview_image = Gtk.Image(margin_right=10)
|
||||
dialog.set_preview_widget(preview_image)
|
||||
|
||||
def update_preview_widget(dlg):
|
||||
path = dialog.get_preview_filename()
|
||||
if not path:
|
||||
return
|
||||
|
||||
pix = get_picon_pixbuf(path, 220)
|
||||
preview_image.set_from_pixbuf(pix)
|
||||
dlg.set_preview_widget_active(bool(pix))
|
||||
|
||||
dialog.connect("update-preview", update_preview_widget)
|
||||
|
||||
return dialog
|
||||
|
||||
def on_extract(self, item):
|
||||
""" Extracts picons from an archives to the profile picons folder. """
|
||||
file_filter = None
|
||||
@@ -537,12 +490,12 @@ class PiconManager(Gtk.Box):
|
||||
def on_selective_send(self, view):
|
||||
path = self.get_selected_path(view)
|
||||
if path:
|
||||
self.on_send(files_filter={path.name}, path=path.parent)
|
||||
self.on_picons_send(files_filter={path.name}, path=path.parent)
|
||||
|
||||
def on_selective_download(self, view):
|
||||
path = self.get_selected_path(view)
|
||||
if path:
|
||||
self.on_download(files_filter={path.name})
|
||||
self.on_picons_download(files_filter={path.name})
|
||||
|
||||
def on_selective_remove(self, view):
|
||||
path = self.get_selected_path(view)
|
||||
@@ -552,37 +505,55 @@ class PiconManager(Gtk.Box):
|
||||
def on_local_remove(self, view):
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if paths and show_dialog(DialogType.QUESTION, self._app_window) == Gtk.ResponseType.OK:
|
||||
itr = model.get_iter(paths.pop())
|
||||
p_path = Path(model.get_value(itr, 2)).resolve()
|
||||
if p_path.is_file():
|
||||
p_path.unlink()
|
||||
base_model = get_base_model(model)
|
||||
filter_model = model.get_model()
|
||||
itr = filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr))
|
||||
base_model.remove(itr)
|
||||
base_model = get_base_model(model)
|
||||
filter_model = model.get_model()
|
||||
to_del = []
|
||||
|
||||
for p in paths:
|
||||
itr = model.get_iter(p)
|
||||
p_path = Path(model.get_value(itr, 2)).resolve()
|
||||
if p_path.is_file():
|
||||
p_path.unlink()
|
||||
to_del.append(filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr)))
|
||||
|
||||
list(map(base_model.remove, to_del))
|
||||
self._app.update_picons()
|
||||
|
||||
if view is self._picons_dest_view:
|
||||
self._dst_count_label.set_text(str(len(model)))
|
||||
|
||||
def on_send(self, item=None, files_filter=None, path=None):
|
||||
def on_send(self, app, page):
|
||||
if page is Page.PICONS:
|
||||
view = self._picons_src_view if self._picons_src_view.is_focus() else self._picons_dest_view
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if paths:
|
||||
self.on_picons_send(files_filter={Path(model[p][-1]).resolve().name for p in paths})
|
||||
else:
|
||||
self._app.show_error_message("No selected item!")
|
||||
|
||||
def on_picons_send(self, item=None, files_filter=None, path=None):
|
||||
dest_path = path or self._settings.profile_picons_path
|
||||
settings = Settings(self._settings.settings)
|
||||
settings.profile_picons_path = f"{dest_path}{SEP}"
|
||||
settings.current_profile = self._settings.current_profile
|
||||
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
|
||||
self.run_func(lambda: upload_data(settings=settings,
|
||||
download_type=DownloadType.PICONS,
|
||||
callback=self.append_output,
|
||||
done_callback=lambda: self.show_info_message(get_message("Done!"),
|
||||
Gtk.MessageType.INFO),
|
||||
files_filter=files_filter))
|
||||
|
||||
def on_download(self, item=None, files_filter=None, path=None):
|
||||
def on_download(self, app, page):
|
||||
if page is Page.PICONS:
|
||||
self.on_picons_download()
|
||||
|
||||
def on_picons_download(self, item=None, files_filter=None, path=None):
|
||||
path = path or self._settings.profile_picons_path
|
||||
settings = Settings(self._settings.settings)
|
||||
settings.profile_picons_path = path + SEP
|
||||
settings.current_profile = self._settings.current_profile
|
||||
self.run_func(lambda: download_data(settings=settings,
|
||||
download_type=DownloadType.PICONS,
|
||||
callback=self.append_output,
|
||||
files_filter=files_filter), True)
|
||||
|
||||
def on_remove(self, item=None, files_filter=None):
|
||||
@@ -590,7 +561,6 @@ class PiconManager(Gtk.Box):
|
||||
return
|
||||
|
||||
self.run_func(lambda: remove_picons(settings=self._settings,
|
||||
callback=self.append_output,
|
||||
done_callback=lambda: self.show_info_message(get_message("Done!"),
|
||||
Gtk.MessageType.INFO),
|
||||
files_filter=files_filter))
|
||||
@@ -671,7 +641,7 @@ class PiconManager(Gtk.Box):
|
||||
self.show_info_message("Getting satellites list error!", Gtk.MessageType.ERROR)
|
||||
|
||||
self._sat_names = {s[1]: s[0] for s in self._sats} # position -> satellite name
|
||||
self._picon_cz_downloader = PiconsCzDownloader(self._picon_ids, self.append_output)
|
||||
self._picon_cz_downloader = PiconsCzDownloader(self._picon_ids)
|
||||
self.init_satellites(view)
|
||||
|
||||
@run_task
|
||||
@@ -759,7 +729,7 @@ class PiconManager(Gtk.Box):
|
||||
@run_task
|
||||
def start_download(self, providers):
|
||||
self._is_downloading = True
|
||||
GLib.idle_add(self._info_bar.set_visible, True)
|
||||
self._app.change_action_state("on_logs_show", GLib.Variant.new_boolean(True))
|
||||
|
||||
for prv in providers:
|
||||
if self._download_src is self.DownloadSource.LYNG_SAT and not self._POS_PATTERN.match(prv[2]):
|
||||
@@ -805,7 +775,7 @@ class PiconManager(Gtk.Box):
|
||||
if pic:
|
||||
picons.extend(pic)
|
||||
# Getting picon images.
|
||||
futures = {executor.submit(download_picon, *pic, self.append_output): pic for pic in picons}
|
||||
futures = {executor.submit(download_picon, *pic): pic for pic in picons}
|
||||
done, not_done = concurrent.futures.wait(futures, timeout=0)
|
||||
while self._is_downloading and not_done:
|
||||
done, not_done = concurrent.futures.wait(not_done, timeout=5)
|
||||
@@ -824,10 +794,9 @@ class PiconManager(Gtk.Box):
|
||||
|
||||
try:
|
||||
# We download it sequentially.
|
||||
for p in providers:
|
||||
self._picon_cz_downloader.download(p, path, p_ids)
|
||||
[self._picon_cz_downloader.download(p, path, p_ids) for p in providers]
|
||||
except PiconsError as e:
|
||||
self.append_output(f"Error: {str(e)}\n")
|
||||
log(f"Error: {str(e)}\n")
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
else:
|
||||
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
|
||||
@@ -848,13 +817,9 @@ class PiconManager(Gtk.Box):
|
||||
return {services.get(fav_id).picon_id for fav_id in fav_bouquet}
|
||||
|
||||
def process_provider(self, prv, picons_path):
|
||||
self.append_output(f"Getting links to picons for: {prv.name}.\n")
|
||||
log(f"Getting links to picons for: {prv.name}.\n")
|
||||
return PiconsParser.parse(prv, picons_path, self._picon_ids, self.get_picons_format())
|
||||
|
||||
@run_idle
|
||||
def append_output(self, char):
|
||||
append_text_to_tview(char, self._info_view)
|
||||
|
||||
@run_task
|
||||
def resize(self, path):
|
||||
self.show_info_message(get_message("Resizing..."), Gtk.MessageType.INFO)
|
||||
@@ -886,19 +851,10 @@ class PiconManager(Gtk.Box):
|
||||
self._is_downloading = False
|
||||
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._terminate = True
|
||||
self._is_downloading = False
|
||||
self._app.update_picons()
|
||||
GLib.idle_add(self._app_window.destroy)
|
||||
|
||||
@run_task
|
||||
def run_func(self, func, update=False):
|
||||
try:
|
||||
GLib.idle_add(self._info_bar.set_visible, True)
|
||||
self._app.change_action_state("on_logs_show", GLib.Variant.new_boolean(True))
|
||||
GLib.idle_add(self._header_download_box.set_sensitive, False)
|
||||
func()
|
||||
except OSError as e:
|
||||
@@ -911,9 +867,6 @@ class PiconManager(Gtk.Box):
|
||||
def show_info_message(self, text, message_type):
|
||||
self._app.show_info_message(text, message_type)
|
||||
|
||||
def on_picons_dir_open(self, entry, icon, event_button):
|
||||
update_entry_data(entry, self._app_window, settings=self._settings)
|
||||
|
||||
@run_idle
|
||||
def on_selected_toggled(self, toggle, path):
|
||||
model = self._providers_view.get_model()
|
||||
@@ -937,7 +890,7 @@ class PiconManager(Gtk.Box):
|
||||
self._filter_button.set_active(not self._filter_button.get_active())
|
||||
|
||||
def on_fav_changed(self, view, path, column):
|
||||
if self._app.page is Page.PICONS and self._auto_filer_switch.get_active():
|
||||
if self._app.page is Page.PICONS and self._auto_filter_switch.get_active():
|
||||
model = view.get_model()
|
||||
self._picons_filter_entry.set_text(model.get_value(model.get_iter(path), Column.FAV_SERVICE))
|
||||
|
||||
@@ -952,10 +905,10 @@ class PiconManager(Gtk.Box):
|
||||
|
||||
@run_with_delay(0.5)
|
||||
def on_picons_filter_changed(self, entry):
|
||||
txt = entry.get_text().upper()
|
||||
self._filter_cache.clear()
|
||||
txt = entry.get_text().upper().split("|")
|
||||
for s in self._app.current_services.values():
|
||||
self._filter_cache[s.picon_id] = txt in s.service.upper()
|
||||
self._filter_cache[s.picon_id] = any(t in s.service.upper() or t in str(s.picon_id) for t in txt)
|
||||
|
||||
GLib.idle_add(self._picons_src_filter_model.refilter, priority=GLib.PRIORITY_LOW)
|
||||
GLib.idle_add(self._picons_dst_filter_model.refilter, priority=GLib.PRIORITY_LOW)
|
||||
@@ -1050,11 +1003,10 @@ class PiconManager(Gtk.Box):
|
||||
self._app.show_error_message("Select paths!")
|
||||
return
|
||||
|
||||
self._info_bar.set_visible(True)
|
||||
self._app.change_action_state("on_logs_show", GLib.Variant.new_boolean(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
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Audio Track</property>
|
||||
<property name="icon_name">multimedia-volume-control</property>
|
||||
<property name="icon_name">audio-volume-high</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 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
|
||||
@@ -36,9 +36,9 @@ from app.connections import HttpAPI
|
||||
from app.eparser.ecommons import BqServiceType
|
||||
from app.settings import PlayStreamsMode, IS_DARWIN, SettingsType
|
||||
from app.tools.media import Player
|
||||
from app.ui.dialogs import get_builder
|
||||
from app.ui.dialogs import get_builder, get_message
|
||||
from app.ui.main_helper import get_iptv_url
|
||||
from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, Column, IS_GNOME_SESSION
|
||||
from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, Column, IS_GNOME_SESSION, Page
|
||||
|
||||
|
||||
class PlayerBox(Gtk.Box):
|
||||
@@ -57,6 +57,8 @@ class PlayerBox(Gtk.Box):
|
||||
|
||||
self._app = app
|
||||
self._app.connect("fav-clicked", self.on_fav_clicked)
|
||||
self._app.connect("srv-clicked", self.on_srv_clicked)
|
||||
self._app.connect("iptv-clicked", self.on_iptv_clicked)
|
||||
self._app.connect("page-changed", self.on_page_changed)
|
||||
self._app.connect("play-current", self.on_play_current)
|
||||
self._app.connect("play-recording", self.on_play_recording)
|
||||
@@ -112,6 +114,42 @@ class PlayerBox(Gtk.Box):
|
||||
elif mode is FavClickMode.PLAY:
|
||||
self.on_play_service()
|
||||
|
||||
def on_srv_clicked(self, app, mode):
|
||||
if not self._app.http_api:
|
||||
return
|
||||
|
||||
view = self._app.services_view
|
||||
path, column = view.get_cursor()
|
||||
if path:
|
||||
srv = self._app.current_services.get(view.get_model()[path][Column.SRV_FAV_ID], None)
|
||||
if not srv or not srv.picon_id:
|
||||
return
|
||||
|
||||
ref = self._app.get_service_ref_data(srv)
|
||||
s_type = self._app.app_settings.setting_type
|
||||
error_msg = "No connection to the receiver!"
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
def zap(rq):
|
||||
self.on_watch() if rq and rq.get("e2state", False) else self.on_error(None, error_msg)
|
||||
|
||||
self._app.http_api.send(HttpAPI.Request.ZAP, ref, zap)
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
def zap(rq):
|
||||
self.on_watch() if rq and rq.get("data", None) == "ok" else self.on_error(None, error_msg)
|
||||
|
||||
self._app.http_api.send(HttpAPI.Request.N_ZAP, f"?{ref}", zap)
|
||||
|
||||
def on_iptv_clicked(self, app, mode):
|
||||
if not self._app.http_api:
|
||||
return
|
||||
|
||||
view = self._app.iptv_services_view
|
||||
path, column = view.get_cursor()
|
||||
if path:
|
||||
row = view.get_model()[path][:]
|
||||
url = get_iptv_url(row, self._app.app_settings.setting_type, Column.IPTV_FAV_ID)
|
||||
self.play(url, row[Column.IPTV_SERVICE]) if url else self.on_error(None, "No reference is present!")
|
||||
|
||||
def on_play_current(self, app, url):
|
||||
self.on_watch()
|
||||
|
||||
@@ -266,7 +304,7 @@ class PlayerBox(Gtk.Box):
|
||||
if click_mode is FavClickMode.PLAY:
|
||||
self.on_play_service()
|
||||
elif click_mode is FavClickMode.ZAP_PLAY:
|
||||
self.on_zap(self.on_watch)
|
||||
self._app.on_zap(self.on_watch)
|
||||
elif click_mode is FavClickMode.STREAM:
|
||||
self.on_play_stream()
|
||||
|
||||
@@ -295,14 +333,14 @@ class PlayerBox(Gtk.Box):
|
||||
""" Returns a string representation of time from duration in milliseconds """
|
||||
m, s = divmod(duration // 1000, 60)
|
||||
h, m = divmod(m, 60)
|
||||
return "{}{:02d}:{:02d}".format(str(h) + ":" if h else "", m, s)
|
||||
return f"{str(h) + ':' if h else ''}{m:02d}:{s:02d}"
|
||||
|
||||
def set_player_area_size(self, widget):
|
||||
w, h = self._app.app_window.get_size()
|
||||
widget.set_size_request(w * 0.6, -1)
|
||||
|
||||
@run_idle
|
||||
def show_playback_window(self):
|
||||
def show_playback_window(self, title=None):
|
||||
width, height = 480, 240
|
||||
size = self._app.app_settings.get("playback_window_size")
|
||||
if size:
|
||||
@@ -310,8 +348,9 @@ class PlayerBox(Gtk.Box):
|
||||
|
||||
if self._playback_window:
|
||||
self._playback_window.show()
|
||||
self._playback_window.set_title(title or self.get_playback_title())
|
||||
else:
|
||||
self._playback_window = Gtk.Window(title=self.get_playback_title(),
|
||||
self._playback_window = Gtk.Window(title=title or self.get_playback_title(),
|
||||
window_position=Gtk.WindowPosition.CENTER,
|
||||
icon_name="demon-editor")
|
||||
|
||||
@@ -331,10 +370,13 @@ class PlayerBox(Gtk.Box):
|
||||
self._playback_window.show()
|
||||
|
||||
def get_playback_title(self):
|
||||
path, column = self._fav_view.get_cursor()
|
||||
if path:
|
||||
return "DemonEditor [{}]".format(self._app.fav_view.get_model()[path][:][Column.FAV_SERVICE])
|
||||
return "DemonEditor [Playback]"
|
||||
if self._app.page is not Page.RECORDINGS:
|
||||
path, column = self._fav_view.get_cursor()
|
||||
if path:
|
||||
return f"DemonEditor [{self._app.fav_view.get_model()[path][:][Column.FAV_SERVICE]}]"
|
||||
else:
|
||||
return f"DemonEditor [{get_message('Recordings')}]"
|
||||
return f"DemonEditor [{get_message('Playback')}]"
|
||||
|
||||
def on_play_stream(self):
|
||||
path, column = self._fav_view.get_cursor()
|
||||
@@ -377,7 +419,7 @@ class PlayerBox(Gtk.Box):
|
||||
url = self._app.get_url_from_m3u(data)
|
||||
GLib.timeout_add_seconds(1, self.play, url) if url else self.on_error(None, "Can't Playback!")
|
||||
|
||||
def play(self, url):
|
||||
def play(self, url, title=None):
|
||||
if self._play_mode is PlayStreamsMode.M3U:
|
||||
self._app.save_stream_to_m3u(url)
|
||||
return
|
||||
@@ -389,18 +431,20 @@ class PlayerBox(Gtk.Box):
|
||||
if self._play_mode is PlayStreamsMode.BUILT_IN:
|
||||
self.show()
|
||||
elif self._play_mode is PlayStreamsMode.WINDOW:
|
||||
self.show_playback_window()
|
||||
self.show_playback_window(title)
|
||||
|
||||
if self._player:
|
||||
self.emit("play", url)
|
||||
else:
|
||||
self._current_mrl = url
|
||||
|
||||
@run_idle
|
||||
def on_played(self, player, duration):
|
||||
GLib.idle_add(self._fav_view.set_sensitive, True)
|
||||
self._fav_view.set_sensitive(True)
|
||||
if not IS_DARWIN:
|
||||
self.on_duration_changed(duration)
|
||||
|
||||
@run_idle
|
||||
def on_error(self, player, msg):
|
||||
self._app.show_error_message(msg)
|
||||
self._fav_view.set_sensitive(True)
|
||||
|
||||
581
app/ui/recordings.glade
Normal file
581
app/ui/recordings.glade
Normal file
@@ -0,0 +1,581 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2022 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.20"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor. -->
|
||||
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkMenu" id="popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="play_menu_item">
|
||||
<property name="label">gtk-media-play</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_play" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="menu_separator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="remove_menu_item">
|
||||
<property name="label">gtk-remove</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="activate" handler="on_recording_remove" swapped="no"/>
|
||||
<accelerator key="Delete" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkListStore" id="rec_paths_model">
|
||||
<columns>
|
||||
<!-- column-name icon -->
|
||||
<column type="GdkPixbuf"/>
|
||||
<!-- column-name title -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name path -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkListStore" id="recordings_model">
|
||||
<columns>
|
||||
<!-- column-name logo -->
|
||||
<column type="GdkPixbuf"/>
|
||||
<!-- column-name service -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name title -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name time -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name length -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name file -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name desc -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name data -->
|
||||
<column type="PyObject"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkTreeModelFilter" id="recordings_filter_model">
|
||||
<property name="child_model">recordings_model</property>
|
||||
</object>
|
||||
<object class="GtkTreeModelSort" id="recordings_sort_model">
|
||||
<property name="model">recordings_filter_model</property>
|
||||
<signal name="row-deleted" handler="on_recordings_model_changed" swapped="no"/>
|
||||
<signal name="row-inserted" handler="on_recordings_model_changed" swapped="no"/>
|
||||
</object>
|
||||
<object class="GtkBox" id="recordings_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkPaned" id="recordings_paned">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="wide_handle">True</property>
|
||||
<child>
|
||||
<object class="GtkFrame" id="recordings_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0.49000000953674316</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="recordings_main_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">2</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="recordings_header_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="recordings_filter_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Filter</property>
|
||||
<signal name="toggled" handler="on_recordings_filter_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="recordings_filter_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-find-replace-symbolic</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="recordings_remove_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Remove</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_recording_remove" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="remove_recording_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">user-trash-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</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="GtkBox" id="recordings_fs_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="recordings_filter_entry">
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">edit-find-replace-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="primary_icon_sensitive">False</property>
|
||||
<property name="visible" bind-source="recordings_filter_button" bind-property="active"/>
|
||||
<signal name="search-changed" handler="on_recordings_filter_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="recordings_search_box">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="recordings_search_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">edit-find-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="primary_icon_sensitive">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="recordings_search_down_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<child>
|
||||
<object class="GtkArrow" id="recordings_down_arrow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="arrow_type">down</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="recordings_search_up_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<child>
|
||||
<object class="GtkArrow" id="recordings_up_arrow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="arrow_type">up</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="group"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="recordings_view_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="recordings_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">recordings_sort_model</property>
|
||||
<property name="fixed_height_mode">True</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<property name="enable_grid_lines">both</property>
|
||||
<property name="tooltip_column">6</property>
|
||||
<signal name="button-press-event" handler="on_popup_menu" object="popup_menu" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_recordings_key_press" swapped="no"/>
|
||||
<signal name="row-activated" handler="on_recordings_activated" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="recordings_view_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_service_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">150</property>
|
||||
<property name="title" translatable="yes">Service</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">1</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererPixbuf" id="rec_log_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="ypad">2</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_service_renderer">
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_title_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">150</property>
|
||||
<property name="title" translatable="yes">Title</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">2</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_title_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_time_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="fixed_width">180</property>
|
||||
<property name="min_width">100</property>
|
||||
<property name="title" translatable="yes">Time</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">3</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_time_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_len_column">
|
||||
<property name="min_width">100</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="title" translatable="yes">Length</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">4</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_len_renderer">
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">4</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_file_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">100</property>
|
||||
<property name="title" translatable="yes">File</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_file_renderer">
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">5</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_desc_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="title" translatable="yes">Description</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">6</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_desc_renderer">
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">6</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>
|
||||
<child>
|
||||
<object class="GtkBox" id="recordings_status_box">
|
||||
<property name="height_request">26</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="recordings_count_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">document-properties</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="recordings_count_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="recordings_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Recordings</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="recordings_paths_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0.49000000953674316</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="paths_view_scrolled_window">
|
||||
<property name="width_request">250</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_bottom">5</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="min_content_height">100</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="recordings_paths_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">rec_paths_model</property>
|
||||
<property name="headers_visible">False</property>
|
||||
<property name="search_column">1</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<property name="activate_on_single_click">True</property>
|
||||
<signal name="button-press-event" handler="on_path_press" swapped="no"/>
|
||||
<signal name="row-activated" handler="on_path_activated" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="rec_paths_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_paths_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="min_width">100</property>
|
||||
<property name="title" translatable="yes">Paths</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="clickable">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>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="recordings_path_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Paths</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">False</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
337
app/ui/recordings.py
Normal file
337
app/ui/recordings.py
Normal file
@@ -0,0 +1,337 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 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 recordings. """
|
||||
import os
|
||||
from datetime import datetime
|
||||
from ftplib import all_errors
|
||||
from io import BytesIO, TextIOWrapper
|
||||
from urllib.parse import quote
|
||||
|
||||
from app.ui.tasks import BGTaskWidget
|
||||
from .dialogs import get_builder, show_dialog, DialogType
|
||||
from .main_helper import get_base_paths, get_base_model, on_popup_menu
|
||||
from .uicommons import Gtk, Gdk, GLib, UI_RESOURCES_PATH, Column, KeyboardKey, Page
|
||||
from ..commons import run_task, run_idle, log
|
||||
from ..connections import UtfFTP, HttpAPI
|
||||
from ..settings import IS_DARWIN, PlayStreamsMode
|
||||
|
||||
|
||||
class RecordingsTool(Gtk.Box):
|
||||
ROOT = ".."
|
||||
DEFAULT_PATH = "/hdd"
|
||||
|
||||
def __init__(self, app, settings, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._app = app
|
||||
self._app.connect("layout-changed", self.on_layout_changed)
|
||||
self._app.connect("data-receive", self.on_data_receive)
|
||||
self._app.connect("profile-changed", self.init)
|
||||
|
||||
self._settings = settings
|
||||
self._ftp = None
|
||||
self._logos = {}
|
||||
# Icon.
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
icon = "folder-symbolic" if IS_DARWIN else "folder"
|
||||
self._icon = theme.load_icon(icon, 24, 0) if theme.lookup_icon(icon, 24, 0) else None
|
||||
|
||||
handlers = {"on_path_press": self.on_path_press,
|
||||
"on_path_activated": self.on_path_activated,
|
||||
"on_recordings_activated": self.on_recordings_activated,
|
||||
"on_play": self.on_play,
|
||||
"on_recording_remove": self.on_recording_remove,
|
||||
"on_recordings_model_changed": self.on_recordings_model_changed,
|
||||
"on_recordings_filter_changed": self.on_recordings_filter_changed,
|
||||
"on_recordings_filter_toggled": self.on_recordings_filter_toggled,
|
||||
"on_recordings_key_press": self.on_recordings_key_press,
|
||||
"on_popup_menu": on_popup_menu}
|
||||
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}recordings.glade", handlers)
|
||||
|
||||
self._rec_view = builder.get_object("recordings_view")
|
||||
self._paths_view = builder.get_object("recordings_paths_view")
|
||||
self._paned = builder.get_object("recordings_paned")
|
||||
self._model = builder.get_object("recordings_model")
|
||||
self._filter_model = builder.get_object("recordings_filter_model")
|
||||
self._filter_model.set_visible_func(self.recordings_filter_function)
|
||||
self._filter_entry = builder.get_object("recordings_filter_entry")
|
||||
self._recordings_count_label = builder.get_object("recordings_count_label")
|
||||
self.pack_start(builder.get_object("recordings_box"), True, True, 0)
|
||||
self._rec_view.get_model().set_sort_func(3, self.time_sort_func, 3)
|
||||
|
||||
srv_column = builder.get_object("rec_service_column")
|
||||
renderer = builder.get_object("rec_log_renderer")
|
||||
size = self._app.app_settings.list_picon_size
|
||||
renderer.set_fixed_size(size, size * 0.65)
|
||||
srv_column.set_cell_data_func(renderer, self.logo_data_func)
|
||||
|
||||
if settings.alternate_layout:
|
||||
self.on_layout_changed(app, True)
|
||||
|
||||
self.init()
|
||||
self.show()
|
||||
|
||||
def clear_data(self):
|
||||
self._model.clear()
|
||||
self._paths_view.get_model().clear()
|
||||
|
||||
def on_layout_changed(self, app, alt_layout):
|
||||
ch1 = self._paned.get_child1()
|
||||
ch2 = self._paned.get_child2()
|
||||
self._paned.remove(ch1)
|
||||
self._paned.remove(ch2)
|
||||
self._paned.add1(ch2)
|
||||
self._paned.add(ch1)
|
||||
|
||||
def on_data_receive(self, app, page):
|
||||
if page is Page.RECORDINGS:
|
||||
model, paths = self._rec_view.get_selection().get_selected_rows()
|
||||
if not paths:
|
||||
self._app.show_error_message("No selected item!")
|
||||
return
|
||||
|
||||
response = show_dialog(DialogType.CHOOSER, self._app.app_window, settings=self._settings,
|
||||
title="Open folder", create_dir=True)
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
|
||||
files = (model[p][5] for p in paths)
|
||||
bgw = BGTaskWidget(self._app, "Downloading recordings...", self.download_recordings, files, response)
|
||||
self._app.emit("add-background-task", bgw)
|
||||
|
||||
def download_recordings(self, files, dst):
|
||||
for file in files:
|
||||
try:
|
||||
with open(os.path.join(dst, os.path.basename(file)), "wb") as f:
|
||||
log(f"Downloading recording: {file}. Status: {self._ftp.download_binary(file, f)}".rstrip())
|
||||
except OSError as e:
|
||||
log(str(e))
|
||||
|
||||
@run_task
|
||||
def init(self, app=None, arg=None):
|
||||
GLib.idle_add(self.clear_data)
|
||||
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"
|
||||
except all_errors:
|
||||
pass # NOP
|
||||
else:
|
||||
self.init_paths(self.DEFAULT_PATH)
|
||||
|
||||
@run_idle
|
||||
def init_paths(self, path=None):
|
||||
self.clear_data()
|
||||
if not self._ftp:
|
||||
return
|
||||
|
||||
if path:
|
||||
try:
|
||||
self._ftp.cwd(path)
|
||||
except all_errors as e:
|
||||
pass
|
||||
|
||||
files = []
|
||||
try:
|
||||
self._ftp.dir(files.append)
|
||||
except all_errors as e:
|
||||
log(e)
|
||||
else:
|
||||
self.append_paths(files)
|
||||
|
||||
@run_idle
|
||||
def append_paths(self, files):
|
||||
model = self._paths_view.get_model()
|
||||
model.clear()
|
||||
model.append((None, self.ROOT, self._ftp.pwd()))
|
||||
|
||||
for f in files:
|
||||
f_data = self._ftp.get_file_data(f)
|
||||
if len(f_data) < 9:
|
||||
log(f"{__class__.__name__}. Folder data parsing error. [{f}]")
|
||||
continue
|
||||
|
||||
f_type = f_data[0][0]
|
||||
|
||||
if f_type == "d":
|
||||
model.append((self._icon, f_data[8], self._ftp.pwd()))
|
||||
|
||||
def on_path_activated(self, view, path, column):
|
||||
row = view.get_model()[path][:]
|
||||
path = f"{row[-1]}/{row[1]}/"
|
||||
self._app.send_http_request(HttpAPI.Request.RECORDINGS, quote(path), self.update_recordings_data)
|
||||
|
||||
def on_path_press(self, view, event):
|
||||
target = view.get_path_at_pos(event.x, event.y)
|
||||
if not target or event.button != Gdk.BUTTON_PRIMARY:
|
||||
return
|
||||
|
||||
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS:
|
||||
self.init_paths(self._paths_view.get_model()[target[0]][1])
|
||||
|
||||
@run_idle
|
||||
def update_recordings_data(self, recordings):
|
||||
self._model.clear()
|
||||
recs = recordings.get("recordings", [])
|
||||
list(map(self._model.append, (self.get_recordings_row(r) for r in recs)))
|
||||
list(map(self.get_rec_service_logo, recs))
|
||||
|
||||
def get_recordings_row(self, rec):
|
||||
service = rec.get("e2servicename")
|
||||
title = rec.get("e2title", "")
|
||||
r_time = datetime.fromtimestamp(int(rec.get("e2time", "0"))).strftime("%a, %x, %H:%M")
|
||||
length = rec.get("e2length", "0")
|
||||
file = rec.get("e2filename", "")
|
||||
desc = rec.get("e2description", "")
|
||||
|
||||
return None, service, title, r_time, length, file, desc, rec
|
||||
|
||||
def get_rec_service_logo(self, rec_data):
|
||||
if not rec_data.get("e2servicename", None):
|
||||
return
|
||||
|
||||
ref = rec_data.get("e2servicereference", None)
|
||||
logo = self._logos.get(rec_data.get("e2servicereference", None))
|
||||
|
||||
if not logo:
|
||||
file = rec_data.get("e2filename", None)
|
||||
if file:
|
||||
meta = f"RETR {file}.meta"
|
||||
io = BytesIO()
|
||||
try:
|
||||
self._ftp.retrbinary(meta, io.write)
|
||||
except all_errors:
|
||||
pass
|
||||
else:
|
||||
io.seek(0)
|
||||
f_ref, sep, name = TextIOWrapper(io, errors="ignore").readline().partition("::")
|
||||
self._logos[ref] = self._app.picons.get(f"{f_ref.replace(':', '_')}.png")
|
||||
|
||||
def on_recordings_activated(self, view, path, column):
|
||||
rec = view.get_model()[path][-1]
|
||||
self._app.send_http_request(HttpAPI.Request.STREAM_TS, rec.get("e2filename", ""), self.on_play_recording)
|
||||
|
||||
def on_play(self, item):
|
||||
path, column = self._rec_view.get_cursor()
|
||||
if not path:
|
||||
self._app.show_error_message("No selected item!")
|
||||
return
|
||||
|
||||
self.on_recordings_activated(self._rec_view, path, column)
|
||||
|
||||
def on_play_recording(self, m3u):
|
||||
url = self._app.get_url_from_m3u(m3u)
|
||||
if url:
|
||||
self._app.emit("play-recording", url)
|
||||
|
||||
def on_recording_remove(self, action=None, value=None):
|
||||
""" Removes recordings via FTP. """
|
||||
model, paths = self._rec_view.get_selection().get_selected_rows()
|
||||
if not paths:
|
||||
self._app.show_error_message("No selected item!")
|
||||
return
|
||||
|
||||
if show_dialog(DialogType.QUESTION, self._app.app_window) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
paths = get_base_paths(paths, model)
|
||||
model = get_base_model(model)
|
||||
to_delete = []
|
||||
|
||||
if paths and self._ftp:
|
||||
for file, itr in ((model[p][-1].get("e2filename", ""), model.get_iter(p)) for p in paths):
|
||||
resp = self._ftp.delete_file(file)
|
||||
if resp.startswith("2"):
|
||||
to_delete.append((itr, file))
|
||||
else:
|
||||
self._app.show_error_message(resp)
|
||||
break
|
||||
|
||||
[self.remove_meta_files(f) for i, f in to_delete if model.remove(i) or True]
|
||||
|
||||
@run_task
|
||||
def remove_meta_files(self, file):
|
||||
name, ex = os.path.splitext(file)
|
||||
[self._ftp.delete_file(f"{name}{suf}") for suf in (f"{ex}.ap", f"{ex}.cuts", f"{ex}.meta", f"{ex}.sc", ".eit")]
|
||||
|
||||
def on_recordings_model_changed(self, model, path, itr=None):
|
||||
self._recordings_count_label.set_text(str(len(model)))
|
||||
|
||||
def on_recordings_filter_changed(self, entry):
|
||||
self._filter_model.refilter()
|
||||
|
||||
def recordings_filter_function(self, model, itr, data):
|
||||
txt = self._filter_entry.get_text().upper()
|
||||
return next((s for s in model.get(itr, 1, 2, 3, 5, 6) if s and txt in s.upper()), False)
|
||||
|
||||
def on_recordings_filter_toggled(self, button):
|
||||
if not button.get_active():
|
||||
self._filter_entry.set_text("")
|
||||
|
||||
def on_recordings_key_press(self, view, event):
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
return
|
||||
|
||||
key = KeyboardKey(key_code)
|
||||
if key is KeyboardKey.DELETE:
|
||||
self.on_recording_remove()
|
||||
|
||||
def on_playback(self, box, state):
|
||||
""" Updates state of the UI elements for playback mode. """
|
||||
if self._settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
|
||||
self._paned.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
self.update_rec_columns_visibility(False)
|
||||
|
||||
def on_playback_close(self, box, state):
|
||||
""" Restores UI elements state after playback mode. """
|
||||
self._paned.set_orientation(Gtk.Orientation.HORIZONTAL)
|
||||
self.update_rec_columns_visibility(True)
|
||||
|
||||
def update_rec_columns_visibility(self, state):
|
||||
for c in (Column.REC_TIME, Column.REC_LEN, Column.REC_FILE, Column.REC_DESC):
|
||||
self._rec_view.get_column(c).set_visible(state)
|
||||
|
||||
def logo_data_func(self, column, renderer, model, itr, data):
|
||||
rec_data = model.get_value(itr, 7)
|
||||
renderer.set_property("pixbuf", self._logos.get(rec_data.get("e2servicereference", None)))
|
||||
|
||||
def time_sort_func(self, model, iter1, iter2, column):
|
||||
""" Custom sort function for time column. """
|
||||
rec1 = model.get_value(iter1, 7)
|
||||
rec2 = model.get_value(iter2, 7)
|
||||
|
||||
return int(rec1.get("e2time", "0")) - int(rec2.get("e2time", "0"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
File diff suppressed because it is too large
Load Diff
@@ -275,6 +275,38 @@ Author: Dmitriy Yefremov
|
||||
<child type="titlebar">
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_cancel" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="apply_button">
|
||||
<property name="label" translatable="yes">Apply</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Save current service</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_save" swapped="no"/>
|
||||
<accelerator key="Return" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="create_button">
|
||||
<property name="label" translatable="yes">Create</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Create and save as new service</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_create_new" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox" id="dialog_vbox">
|
||||
<property name="can_focus">False</property>
|
||||
@@ -287,53 +319,6 @@ Author: Dmitriy Yefremov
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_cancel" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="apply_button">
|
||||
<property name="label" translatable="yes">Apply</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Save current service</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_save" swapped="no"/>
|
||||
<accelerator key="Return" signal="activate"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="create_button">
|
||||
<property name="label" translatable="yes">Create</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Create and save as new service</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_create_new" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -1644,6 +1629,9 @@ Author: Dmitriy Yefremov
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<action-widgets>
|
||||
<action-widget response="-6">cancel_button</action-widget>
|
||||
</action-widgets>
|
||||
</object>
|
||||
<object class="GtkListStore" id="transponder_services_liststore">
|
||||
<columns>
|
||||
@@ -1676,6 +1664,24 @@ Author: Dmitriy Yefremov
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="tr_services_no_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="tr_services_ok_button">
|
||||
<property name="label" translatable="yes">OK</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox" id="tr_services_dialog_vbox">
|
||||
<property name="can_focus">False</property>
|
||||
@@ -1685,34 +1691,6 @@ Author: Dmitriy Yefremov
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="tr_services_no_button">
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="tr_services_ok_button">
|
||||
<property name="label" translatable="yes">OK</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 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
|
||||
@@ -62,7 +62,7 @@ class ServiceDetailsDialog:
|
||||
|
||||
_DIGIT_ENTRY_NAME = "digit-entry"
|
||||
|
||||
def __init__(self, transient, settings, srv_view, fav_view, services, bouquets, new_color, action=Action.EDIT):
|
||||
def __init__(self, app, new_color, action=Action.EDIT):
|
||||
handlers = {"on_system_changed": self.on_system_changed,
|
||||
"on_save": self.on_save,
|
||||
"on_create_new": self.on_create_new,
|
||||
@@ -76,19 +76,19 @@ class ServiceDetailsDialog:
|
||||
|
||||
builder = get_builder(_UI_PATH, handlers, use_str=True)
|
||||
self._builder = builder
|
||||
settings = app.app_settings
|
||||
|
||||
self._dialog = builder.get_object("service_details_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._dialog.set_transient_for(app.app_window)
|
||||
self._s_type = settings.setting_type
|
||||
self._tr_type = TrType.Satellite
|
||||
self._satellites_xml_path = settings.profile_data_path + "satellites.xml"
|
||||
self._picons_path = settings.profile_picons_path
|
||||
self._services_view = srv_view
|
||||
self._fav_view = fav_view
|
||||
self._services_view = app.services_view
|
||||
self._fav_view = app.fav_view
|
||||
self._action = action
|
||||
self._old_service = None
|
||||
self._services = services
|
||||
self._bouquets = bouquets
|
||||
self._services = app.current_services
|
||||
self._bouquets = app.current_bouquets
|
||||
self._new_color = new_color
|
||||
self._transponder_services_iters = None
|
||||
self._current_model = None
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 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
|
||||
@@ -28,17 +28,14 @@
|
||||
|
||||
import os
|
||||
import re
|
||||
from collections import Counter
|
||||
|
||||
from app.commons import run_task, run_idle, log
|
||||
from app.connections import test_telnet, test_ftp, TestException, test_http, HttpApiException
|
||||
from app.settings import SettingsType, Settings, PlayStreamsMode, IS_LINUX, SEP, IS_WIN
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_message, get_chooser_dialog, get_builder
|
||||
from .main_helper import update_entry_data, scroll_to, get_picon_pixbuf
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON, APP_FONT
|
||||
|
||||
|
||||
def show_settings_dialog(transient, options):
|
||||
return SettingsDialog(transient, options).show()
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON, APP_FONT, IS_GNOME_SESSION
|
||||
|
||||
|
||||
class SettingsDialog:
|
||||
@@ -46,11 +43,10 @@ class SettingsDialog:
|
||||
_DIGIT_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
|
||||
|
||||
def __init__(self, transient, settings: Settings):
|
||||
handlers = {"on_field_icon_press": self.on_field_icon_press,
|
||||
handlers = {"on_field_button_press": self.on_field_button_press,
|
||||
"on_settings_type_changed": self.on_settings_type_changed,
|
||||
"on_reset": self.on_reset,
|
||||
"on_response": self.on_response,
|
||||
"apply_settings": self.apply_settings,
|
||||
"on_connection_test": self.on_connection_test,
|
||||
"on_info_bar_close": self.on_info_bar_close,
|
||||
"on_set_color_switch": self.on_set_color_switch,
|
||||
@@ -67,6 +63,10 @@ class SettingsDialog:
|
||||
"on_profile_edited": self.on_profile_edited,
|
||||
"on_profile_selected": self.on_profile_selected,
|
||||
"on_profile_set_default": self.on_profile_set_default,
|
||||
"on_host_focus_in": self.on_host_focus_in,
|
||||
"on_host_focus_out": self.on_host_focus_out,
|
||||
"on_add_host": self.on_add_host,
|
||||
"on_remove_host": self.on_remove_host,
|
||||
"on_add_picon_path": self.on_add_picon_path,
|
||||
"on_remove_picon_path": self.on_remove_picon_path,
|
||||
"on_lang_changed": self.on_lang_changed,
|
||||
@@ -86,11 +86,12 @@ class SettingsDialog:
|
||||
"on_icon_theme_add": self.on_icon_theme_add,
|
||||
"on_icon_theme_remove": self.on_icon_theme_remove}
|
||||
|
||||
# Settings
|
||||
# Settings.
|
||||
self._ext_settings = settings
|
||||
self._settings = Settings(settings.settings)
|
||||
self._profiles = self._settings.profiles
|
||||
self._s_type = self._settings.setting_type
|
||||
self._updated = False
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "settings_dialog.glade", handlers)
|
||||
|
||||
@@ -100,7 +101,10 @@ class SettingsDialog:
|
||||
self._dialog.set_margin_left(0)
|
||||
self._main_stack = builder.get_object("main_stack")
|
||||
# Network.
|
||||
self._host_iter = None
|
||||
self._host_field = builder.get_object("host_field")
|
||||
self._hosts_box = builder.get_object("hosts_box")
|
||||
self._remove_host_button = builder.get_object("remove_host_button")
|
||||
self._port_field = builder.get_object("port_field")
|
||||
self._login_field = builder.get_object("login_field")
|
||||
self._password_field = builder.get_object("password_field")
|
||||
@@ -116,16 +120,17 @@ class SettingsDialog:
|
||||
self._services_field = builder.get_object("services_field")
|
||||
self._user_bouquet_field = builder.get_object("user_bouquet_field")
|
||||
self._satellites_xml_field = builder.get_object("satellites_xml_field")
|
||||
self._epg_dat_box = builder.get_object("epg_dat_box")
|
||||
self._picons_paths_box = builder.get_object("picons_paths_box")
|
||||
self._remove_picon_path_button = builder.get_object("remove_picon_path_button")
|
||||
# Paths.
|
||||
self._picons_path_field = builder.get_object("picons_path_field")
|
||||
self._data_path_field = builder.get_object("data_path_field")
|
||||
self._backup_path_field = builder.get_object("backup_path_field")
|
||||
self._record_data_path_field = builder.get_object("record_data_path_field")
|
||||
self._recordings_path_field = builder.get_object("recordings_path_field")
|
||||
self._default_data_paths_switch = builder.get_object("default_data_paths_switch")
|
||||
self._default_data_paths_switch.bind_property("active", self._backup_path_field, "sensitive", 4)
|
||||
self._default_data_paths_switch.bind_property("active", self._picons_path_field, "sensitive", 4)
|
||||
self._default_data_paths_switch.bind_property("active", builder.get_object("picons_path_box"), "sensitive", 4)
|
||||
self._default_data_paths_switch.bind_property("active", builder.get_object("backup_path_box"), "sensitive", 4)
|
||||
# Info bar.
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("info_bar_message_label")
|
||||
@@ -133,9 +138,7 @@ class SettingsDialog:
|
||||
# Settings type.
|
||||
self._enigma_radio_button = builder.get_object("enigma_radio_button")
|
||||
self._neutrino_radio_button = builder.get_object("neutrino_radio_button")
|
||||
self._support_ver5_switch = builder.get_object("support_ver5_switch")
|
||||
self._force_bq_name_switch = builder.get_object("force_bq_name_switch")
|
||||
# Streaming
|
||||
# Streaming.
|
||||
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")
|
||||
@@ -149,14 +152,12 @@ class SettingsDialog:
|
||||
self._audio_codec_combo_box = builder.get_object("audio_codec_combo_box")
|
||||
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")
|
||||
self._edit_preset_switch.bind_property("active", builder.get_object("video_options_grid"), "sensitive")
|
||||
self._edit_preset_switch.bind_property("active", builder.get_object("audio_options_grid"), "sensitive")
|
||||
self._play_streams_combo_box = builder.get_object("play_streams_combo_box")
|
||||
self._stream_lib_combo_box = builder.get_object("stream_lib_combo_box")
|
||||
self._double_click_combo_box = builder.get_object("double_click_combo_box")
|
||||
self._allow_main_list_playback_switch = builder.get_object("allow_main_list_playback_switch")
|
||||
# Program.
|
||||
self._before_save_switch = builder.get_object("before_save_switch")
|
||||
self._before_downloading_switch = builder.get_object("before_downloading_switch")
|
||||
@@ -173,17 +174,16 @@ class SettingsDialog:
|
||||
self._new_color_button = builder.get_object("new_color_button")
|
||||
self._extra_color_button = builder.get_object("extra_color_button")
|
||||
# Extra.
|
||||
self._use_http_switch = builder.get_object("use_http_switch")
|
||||
self._remove_unused_bq_switch = builder.get_object("remove_unused_bq_switch")
|
||||
self._keep_power_mode_switch = builder.get_object("keep_power_mode_switch")
|
||||
self._compress_picons_switch = builder.get_object("compress_picons_switch")
|
||||
self._force_bq_name_switch = builder.get_object("force_bq_name_switch")
|
||||
self._support_ver5_switch = builder.get_object("support_ver5_switch")
|
||||
self._support_http_api_switch = builder.get_object("support_http_api_switch")
|
||||
self._enable_yt_dl_switch = builder.get_object("enable_yt_dl_switch")
|
||||
self._enable_update_yt_dl_switch = builder.get_object("enable_update_yt_dl_switch")
|
||||
self._enable_send_to_switch = builder.get_object("enable_send_to_switch")
|
||||
self._click_mode_disabled_button = builder.get_object("click_mode_disabled_button")
|
||||
self._click_mode_stream_button = builder.get_object("click_mode_stream_button")
|
||||
self._click_mode_play_button = builder.get_object("click_mode_play_button")
|
||||
self._click_mode_zap_button = builder.get_object("click_mode_zap_button")
|
||||
self._click_mode_zap_and_play_button = builder.get_object("click_mode_zap_and_play_button")
|
||||
self._click_mode_zap_button.bind_property("sensitive", self._click_mode_play_button, "sensitive")
|
||||
self._click_mode_zap_button.bind_property("sensitive", self._click_mode_zap_and_play_button, "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")
|
||||
@@ -192,22 +192,34 @@ class SettingsDialog:
|
||||
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")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("allow_double_click_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")
|
||||
# Network.
|
||||
# Separated due to a bug with response (presumably in the builder) in ubuntu 18.04 and derivatives.
|
||||
builder.get_object("network_settings_frame").add(builder.get_object("network_grid"))
|
||||
# Style.
|
||||
self._style_provider = Gtk.CssProvider()
|
||||
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_path(f"{UI_RESOURCES_PATH}style.css")
|
||||
screen = Gdk.Screen.get_default()
|
||||
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_element_style(el, screen, style_provider) for el in self._digit_elems]
|
||||
self.init_element_style(self._host_field, screen, style_provider)
|
||||
|
||||
if IS_GNOME_SESSION:
|
||||
switcher = builder.get_object("main_stack_switcher")
|
||||
switcher.set_margin_top(0)
|
||||
switcher.set_margin_bottom(0)
|
||||
builder.get_object("main_box").remove(switcher)
|
||||
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True)
|
||||
header_bar.set_custom_title(switcher)
|
||||
self._dialog.set_titlebar(header_bar)
|
||||
|
||||
self.init_ui_elements()
|
||||
self.init_profiles()
|
||||
|
||||
@@ -233,8 +245,6 @@ class SettingsDialog:
|
||||
self._neutrino_radio_button.set_active(self._s_type is SettingsType.NEUTRINO_MP)
|
||||
self.update_picon_paths()
|
||||
self.update_title()
|
||||
http_active = self._support_http_api_switch.get_active()
|
||||
self._click_mode_zap_button.set_sensitive(is_enigma_profile and http_active)
|
||||
self._lang_combo_box.set_active_id(self._ext_settings.language)
|
||||
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)
|
||||
@@ -250,6 +260,9 @@ class SettingsDialog:
|
||||
self.on_profile_selected(self._profile_view, False)
|
||||
self._profile_remove_button.set_sensitive(len(self._profile_view.get_model()) > 1)
|
||||
|
||||
def init_element_style(self, elem, screen, provider):
|
||||
elem.get_style_context().add_provider_for_screen(screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
def update_title(self):
|
||||
title = "{} [{}]"
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
@@ -267,16 +280,17 @@ class SettingsDialog:
|
||||
self._picons_paths_box.set_active(0)
|
||||
|
||||
def show(self):
|
||||
self._dialog.run()
|
||||
return self._dialog.run()
|
||||
|
||||
def is_updated(self):
|
||||
return self._updated
|
||||
|
||||
def on_response(self, dialog, resp):
|
||||
if resp == Gtk.ResponseType.OK and not self.apply_settings():
|
||||
return
|
||||
if resp == Gtk.ResponseType.ACCEPT:
|
||||
self._updated = self.on_save_settings()
|
||||
dialog.destroy()
|
||||
|
||||
self._dialog.destroy()
|
||||
return resp
|
||||
|
||||
def on_field_icon_press(self, entry, icon, event_button):
|
||||
def on_field_button_press(self, entry):
|
||||
update_entry_data(entry, self._dialog, self._settings)
|
||||
|
||||
def on_settings_type_changed(self, item):
|
||||
@@ -293,7 +307,9 @@ class SettingsDialog:
|
||||
|
||||
def set_settings(self):
|
||||
self._s_type = self._settings.setting_type
|
||||
self._host_field.set_text(self._settings.host)
|
||||
self._hosts_box.remove_all()
|
||||
self._remove_host_button.set_sensitive(len([self._hosts_box.append(h, h) for h in self._settings.hosts]) > 1)
|
||||
self._hosts_box.set_active_id(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)
|
||||
@@ -304,16 +320,18 @@ class SettingsDialog:
|
||||
self._services_field.set_text(self._settings.services_path)
|
||||
self._user_bouquet_field.set_text(self._settings.user_bouquet_path)
|
||||
self._satellites_xml_field.set_text(self._settings.satellites_xml_path)
|
||||
self._epg_dat_box.set_active_id(self._settings.epg_dat_path)
|
||||
self._picons_paths_box.set_active_id(self._settings.picons_path)
|
||||
self._data_path_field.set_text(self._settings.default_data_path)
|
||||
self._picons_path_field.set_text(self._settings.default_picon_path)
|
||||
self._backup_path_field.set_text(self._settings.default_backup_path)
|
||||
self._record_data_path_field.set_text(self._settings.records_path)
|
||||
self._recordings_path_field.set_text(self._settings.recordings_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._play_streams_combo_box.set_active_id(str(self._settings.play_streams_mode.value))
|
||||
self._stream_lib_combo_box.set_active_id(self._settings.stream_lib)
|
||||
self._double_click_combo_box.set_active_id(str(self._settings.fav_click_mode))
|
||||
self._allow_main_list_playback_switch.set_active(self._settings.main_list_playback)
|
||||
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)
|
||||
@@ -324,12 +342,16 @@ class SettingsDialog:
|
||||
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)
|
||||
self._support_http_api_switch.set_active(self._settings.http_api_support)
|
||||
|
||||
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._use_http_switch.set_active(self._settings.use_http)
|
||||
self._remove_unused_bq_switch.set_active(self._settings.remove_unused_bouquets)
|
||||
self._keep_power_mode_switch.set_active(self._settings.keep_power_mode)
|
||||
self._compress_picons_switch.set_active(self._settings.compress_picons)
|
||||
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_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)
|
||||
@@ -354,6 +376,7 @@ class SettingsDialog:
|
||||
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.hosts = [h[1] for h in self._hosts_box.get_model()]
|
||||
self._settings.port = self._port_field.get_text()
|
||||
self._settings.user = self._login_field.get_text()
|
||||
self._settings.password = self._password_field.get_text()
|
||||
@@ -363,20 +386,21 @@ class SettingsDialog:
|
||||
self._settings.telnet_timeout = int(self._telnet_timeout_spin_button.get_value())
|
||||
self._settings.services_path = self._services_field.get_text()
|
||||
self._settings.user_bouquet_path = self._user_bouquet_field.get_text()
|
||||
self._settings.satellites_xml_path = self._satellites_xml_field.get_text()
|
||||
self._settings.epg_dat_path = self._epg_dat_box.get_active_id()
|
||||
self._settings.picons_path = self._picons_paths_box.get_active_id()
|
||||
|
||||
def apply_settings(self, item=None):
|
||||
def on_save_settings(self, item=None):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.OK:
|
||||
return
|
||||
return False
|
||||
|
||||
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.play_streams_mode = PlayStreamsMode(int(self._play_streams_combo_box.get_active_id()))
|
||||
self._ext_settings.stream_lib = self._stream_lib_combo_box.get_active_id()
|
||||
self._ext_settings.fav_click_mode = int(self._double_click_combo_box.get_active_id())
|
||||
self._ext_settings.main_list_playback = self._allow_main_list_playback_switch.get_active()
|
||||
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()
|
||||
@@ -385,12 +409,13 @@ class SettingsDialog:
|
||||
self._ext_settings.default_data_path = self._data_path_field.get_text()
|
||||
self._ext_settings.default_backup_path = self._backup_path_field.get_text()
|
||||
self._ext_settings.default_picon_path = self._picons_path_field.get_text()
|
||||
self._ext_settings.records_path = self._record_data_path_field.get_text()
|
||||
self._ext_settings.recordings_path = self._recordings_path_field.get_text()
|
||||
self._ext_settings.activate_transcoding = self._transcoding_switch.get_active()
|
||||
self._ext_settings.active_preset = self._presets_combo_box.get_active_id()
|
||||
self._ext_settings.list_picon_size = int(self._picons_size_button.get_active_id())
|
||||
self._ext_settings.tooltip_logo_size = int(self._tooltip_logo_size_button.get_active_id())
|
||||
self._ext_settings.list_font = self._list_font_button.get_font()
|
||||
self._ext_settings.http_api_support = self._support_http_api_switch.get_active()
|
||||
|
||||
if not IS_LINUX:
|
||||
self._ext_settings.dark_mode = self._dark_mode_switch.get_active()
|
||||
@@ -405,14 +430,18 @@ class SettingsDialog:
|
||||
self._ext_settings.new_color = self._new_color_button.get_rgba().to_string()
|
||||
self._ext_settings.extra_color = self._extra_color_button.get_rgba().to_string()
|
||||
self._ext_settings.v5_support = self._support_ver5_switch.get_active()
|
||||
self._ext_settings.use_http = self._use_http_switch.get_active()
|
||||
self._ext_settings.remove_unused_bouquets = self._remove_unused_bq_switch.get_active()
|
||||
self._ext_settings.keep_power_mode = self._keep_power_mode_switch.get_active()
|
||||
self._ext_settings.compress_picons = self._compress_picons_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_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]
|
||||
self._ext_settings.save()
|
||||
|
||||
return True
|
||||
|
||||
@run_task
|
||||
@@ -481,11 +510,8 @@ class SettingsDialog:
|
||||
self._colors_grid.set_sensitive(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)
|
||||
if self._main_stack.get_visible_child_name() == "program" and not state:
|
||||
self.show_info_message("May affect some features availability! ", Gtk.MessageType.WARNING)
|
||||
|
||||
def on_experimental_switch(self, switch, state):
|
||||
if not state:
|
||||
@@ -515,7 +541,7 @@ class SettingsDialog:
|
||||
name = "profile"
|
||||
while name in self._profiles:
|
||||
count += 1
|
||||
name = "profile{}".format(count)
|
||||
name = f"profile{count}"
|
||||
|
||||
self._profiles[name] = self._s_type.get_default_settings()
|
||||
model.append((name, None))
|
||||
@@ -578,6 +604,42 @@ class SettingsDialog:
|
||||
def on_profile_inserted(self, model, path, itr):
|
||||
self._profile_remove_button.set_sensitive(len(model) > 1)
|
||||
|
||||
def on_host_focus_in(self, entry, event):
|
||||
self._host_iter = self._hosts_box.get_active_iter()
|
||||
|
||||
def on_host_focus_out(self, entry, event=None):
|
||||
if self._host_iter:
|
||||
model = self._hosts_box.get_model()
|
||||
host = entry.get_text()
|
||||
model.set_value(self._host_iter, 0, host)
|
||||
model.set_value(self._host_iter, 1, host)
|
||||
|
||||
if Counter(r[0] for r in model).get(host, 0) > 1:
|
||||
self._host_field.set_name(self._DIGIT_ENTRY_NAME)
|
||||
self.show_info_message("The host already exists!", Gtk.MessageType.WARNING)
|
||||
else:
|
||||
self._host_field.set_name("GtkEntry")
|
||||
self.on_info_bar_close()
|
||||
|
||||
def on_add_host(self, button):
|
||||
model = self._hosts_box.get_model()
|
||||
count = 1
|
||||
host = "127.0.0.1"
|
||||
hosts = {r[0] for r in model}
|
||||
|
||||
while host in hosts:
|
||||
count += 1
|
||||
host = f"127.0.0.{count}"
|
||||
|
||||
self._hosts_box.append(host, host)
|
||||
self._hosts_box.set_active_id(host)
|
||||
self._remove_host_button.set_sensitive(len(model) > 1)
|
||||
|
||||
def on_remove_host(self, button):
|
||||
self._hosts_box.remove(self._hosts_box.get_active())
|
||||
self._hosts_box.set_active(0)
|
||||
self._remove_host_button.set_sensitive(len(self._hosts_box.get_model()) > 1)
|
||||
|
||||
def on_add_picon_path(self, button):
|
||||
response = show_dialog(DialogType.INPUT, self._dialog, self._settings.picons_path)
|
||||
if response is Gtk.ResponseType.CANCEL:
|
||||
@@ -594,7 +656,7 @@ class SettingsDialog:
|
||||
self._ext_settings.picons_paths = tuple(r[0] for r in model)
|
||||
|
||||
def on_remove_picon_path(self, button):
|
||||
msg = f"{get_message('This may change the settings of other profiles!')}\n\n\t\t{'Are you sure?'}"
|
||||
msg = f"{get_message('This may change the settings of other profiles!')}\n\n\t\t{get_message('Are you sure?')}"
|
||||
if show_dialog(DialogType.QUESTION, self._dialog, msg) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
@@ -624,78 +686,26 @@ class SettingsDialog:
|
||||
self._settings.http_port = port
|
||||
|
||||
def on_click_mode_togged(self, button):
|
||||
if self._main_stack.get_visible_child_name() != "extra":
|
||||
if self._main_stack.get_visible_child_name() != "streaming":
|
||||
return
|
||||
|
||||
mode = self.get_fav_click_mode()
|
||||
mode = FavClickMode(int(self._double_click_combo_box.get_active_id()))
|
||||
if mode is FavClickMode.PLAY:
|
||||
self.show_info_message("Operates in standby mode or current active transponder!", Gtk.MessageType.WARNING)
|
||||
elif mode is FavClickMode.STREAM:
|
||||
self.show_info_message("Playback IPTV streams only!", Gtk.MessageType.WARNING)
|
||||
elif mode is FavClickMode.DISABLED:
|
||||
self._allow_main_list_playback_switch.set_active(False)
|
||||
else:
|
||||
self.on_info_bar_close()
|
||||
|
||||
@run_idle
|
||||
def set_fav_click_mode(self, mode):
|
||||
mode = FavClickMode(mode)
|
||||
self._click_mode_disabled_button.set_active(mode is FavClickMode.DISABLED)
|
||||
self._click_mode_stream_button.set_active(mode is FavClickMode.STREAM)
|
||||
self._click_mode_play_button.set_active(mode is FavClickMode.PLAY)
|
||||
self._click_mode_zap_button.set_active(mode is FavClickMode.ZAP)
|
||||
self._click_mode_zap_and_play_button.set_active(mode is FavClickMode.ZAP_PLAY)
|
||||
|
||||
def get_fav_click_mode(self):
|
||||
if self._click_mode_zap_button.get_active():
|
||||
return FavClickMode.ZAP
|
||||
if self._click_mode_play_button.get_active():
|
||||
return FavClickMode.PLAY
|
||||
if self._click_mode_zap_and_play_button.get_active():
|
||||
return FavClickMode.ZAP_PLAY
|
||||
if self._click_mode_stream_button.get_active():
|
||||
return FavClickMode.STREAM
|
||||
|
||||
return FavClickMode.DISABLED
|
||||
self._allow_main_list_playback_switch.set_sensitive(mode is not FavClickMode.DISABLED)
|
||||
|
||||
def on_play_mode_changed(self, button):
|
||||
if self._main_stack.get_visible_child_name() != "streaming" or not button.get_active():
|
||||
if self._main_stack.get_visible_child_name() != "streaming":
|
||||
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_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"
|
||||
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
|
||||
|
||||
def on_transcoding_preset_changed(self, button):
|
||||
presets = self._settings.transcoding_presets
|
||||
@@ -790,15 +800,16 @@ class SettingsDialog:
|
||||
@run_task
|
||||
def unpack_theme(self, src, dst, button):
|
||||
try:
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
from shutil import unpack_archive
|
||||
|
||||
import subprocess
|
||||
log("Unpacking '{}' started...".format(src))
|
||||
p = subprocess.Popen(["tar", "-xvf", src, "-C", dst],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT)
|
||||
p.communicate()
|
||||
log(f"Unpacking '{src}' started...")
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
unpack_archive(src, dst)
|
||||
log("Unpacking end.")
|
||||
except (ValueError, OSError) as e:
|
||||
msg = f"Unpacking error: {e}"
|
||||
log(msg)
|
||||
self.show_info_message(msg, Gtk.MessageType.ERROR)
|
||||
finally:
|
||||
self.update_theme_button(button, dst)
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
* {
|
||||
-GtkDialog-action-area-border: 6;
|
||||
}
|
||||
|
||||
#digit-entry {
|
||||
border-color: Red;
|
||||
}
|
||||
@@ -10,12 +14,32 @@
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
#task-button {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#stack-switch-button {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
paned > separator {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
paned.horizontal > separator {
|
||||
background-size: 2px 24px;
|
||||
}
|
||||
|
||||
paned.vertical > separator {
|
||||
background-size: 24px 2px;
|
||||
}
|
||||
|
||||
popover .view {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.red-button {
|
||||
background-image: none;
|
||||
background-color: red;
|
||||
|
||||
86
app/ui/tasks.py
Normal file
86
app/ui/tasks.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 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.ui.dialogs import get_message
|
||||
from .uicommons import Gtk, GLib
|
||||
|
||||
|
||||
class BGTaskWidget(Gtk.Box):
|
||||
""" Widget for displaying and running background tasks. """
|
||||
|
||||
TASK_LIMIT = 1
|
||||
|
||||
def __init__(self, app, text, target, *args):
|
||||
super().__init__(spacing=2, orientation=Gtk.Orientation.HORIZONTAL, valign=Gtk.Align.CENTER)
|
||||
self._app = app
|
||||
|
||||
self._label = Gtk.Label(get_message(text))
|
||||
self.pack_start(self._label, False, False, 0)
|
||||
|
||||
self._spinner = Gtk.Spinner(active=True)
|
||||
self.pack_start(self._spinner, False, False, 0)
|
||||
|
||||
close_button = Gtk.Button.new_from_icon_name("window-close", Gtk.IconSize.MENU)
|
||||
close_button.set_relief(Gtk.ReliefStyle.NONE)
|
||||
close_button.set_valign(Gtk.Align.CENTER)
|
||||
close_button.set_tooltip_text(get_message("Cancel"))
|
||||
close_button.set_name("task-button")
|
||||
close_button.connect("clicked", lambda b: self._app.emit("task-cancel", self))
|
||||
self.pack_start(close_button, False, False, 0)
|
||||
|
||||
self.show_all()
|
||||
|
||||
# Just prototype. -> It may not work properly!
|
||||
# TODO: Different options need to be tested. Possibly with normal threads.
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
self._executor = ThreadPoolExecutor(max_workers=self.TASK_LIMIT)
|
||||
future = self._executor.submit(target, *args)
|
||||
future.add_done_callback(lambda f: GLib.idle_add(self._app.emit, "task-done", self))
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return self._label.get_text()
|
||||
|
||||
@text.setter
|
||||
def text(self, value):
|
||||
self._label.set_text(value)
|
||||
|
||||
@property
|
||||
def tooltip(self):
|
||||
return self.get_tooltip_text()
|
||||
|
||||
@tooltip.setter
|
||||
def tooltip(self, value):
|
||||
self.set_tooltip_text(value)
|
||||
|
||||
def cancel(self):
|
||||
self._executor.shutdown(wait=False)
|
||||
self._app.emit("task-canceled", None)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
||||
1765
app/ui/timers.glade
Normal file
1765
app/ui/timers.glade
Normal file
File diff suppressed because it is too large
Load Diff
560
app/ui/timers.py
Normal file
560
app/ui/timers.py
Normal file
@@ -0,0 +1,560 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 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 timers. """
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from urllib.parse import quote
|
||||
|
||||
from app.ui.main_helper import on_popup_menu
|
||||
from .dialogs import get_builder, get_message, show_dialog, DialogType
|
||||
from .uicommons import Gtk, Gdk, GLib, UI_RESOURCES_PATH, Page, Column, KeyboardKey, IS_GNOME_SESSION, MOD_MASK
|
||||
from ..commons import run_idle, log
|
||||
from ..connections import HttpAPI
|
||||
from ..eparser.ecommons import BqServiceType
|
||||
|
||||
|
||||
class TimerTool(Gtk.Box):
|
||||
TIME_STR = "%Y-%m-%d %H:%M"
|
||||
|
||||
ACTION = {"0": "Record", "1": "Zap"}
|
||||
|
||||
AFTER_EVENT = {"0": "Do Nothing",
|
||||
"1": "Standby",
|
||||
"2": "Shut down",
|
||||
"3": "Auto"}
|
||||
|
||||
class TimerAction(Enum):
|
||||
ADD = 0
|
||||
EVENT = 1
|
||||
CHANGE = 2
|
||||
|
||||
class TimerDialog(Gtk.Dialog):
|
||||
def __init__(self, parent, action=None, timer_data=None, *args, **kwargs):
|
||||
super().__init__(use_header_bar=IS_GNOME_SESSION, *args, **kwargs)
|
||||
|
||||
self._action = action or TimerTool.TimerAction.ADD
|
||||
self._timer_data = timer_data or {}
|
||||
self._request = ""
|
||||
|
||||
handlers = {"on_timer_begins_set": self.on_timer_begins_set,
|
||||
"on_timer_ends_set": self.on_timer_ends_set}
|
||||
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}timers.glade", handlers,
|
||||
objects=("timer_dialog_frame", "timer_ends_popover", "end_hour_adjustment",
|
||||
"min_end_adjustment", "timer_begins_popover", "begins_hour_adjustment",
|
||||
"min_begins_adjustment"))
|
||||
|
||||
self.set_title(get_message("Timer"))
|
||||
self.set_modal(True)
|
||||
self.set_skip_pager_hint(True)
|
||||
self.set_skip_taskbar_hint(True)
|
||||
self.set_transient_for(parent)
|
||||
self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
|
||||
self.set_resizable(False)
|
||||
|
||||
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._days_buttons = (builder.get_object("timer_mo_check_button"),
|
||||
builder.get_object("timer_tu_check_button"),
|
||||
builder.get_object("timer_we_check_button"),
|
||||
builder.get_object("timer_th_check_button"),
|
||||
builder.get_object("timer_fr_check_button"),
|
||||
builder.get_object("timer_sa_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()
|
||||
|
||||
self.add_buttons(get_message("Cancel"), Gtk.ResponseType.CANCEL, get_message("Save"), Gtk.ResponseType.OK)
|
||||
self.get_content_area().pack_start(builder.get_object("timer_dialog_frame"), True, True, 5)
|
||||
|
||||
if self._action is TimerTool.TimerAction.ADD:
|
||||
self.set_timer_for_add()
|
||||
elif self._action is TimerTool.TimerAction.CHANGE:
|
||||
self.set_timer_for_edit()
|
||||
elif self._action is TimerTool.TimerAction.EVENT:
|
||||
self.set_timer_from_event_data()
|
||||
else:
|
||||
log(f"{__class__.__name__} error: No action set for timer!")
|
||||
|
||||
@property
|
||||
def request(self):
|
||||
return self._request
|
||||
|
||||
def run(self):
|
||||
resp = super().run()
|
||||
if resp == Gtk.ResponseType.OK:
|
||||
self._request = self.get_request()
|
||||
return resp
|
||||
|
||||
def get_request(self):
|
||||
""" Constructs str representation of add/update request. """
|
||||
args = []
|
||||
t_data = self.get_timer_data()
|
||||
s_ref = quote(t_data.get("sRef", ""))
|
||||
|
||||
if self._action is TimerTool.TimerAction.EVENT:
|
||||
args.append(f"timeraddbyeventid?sRef={s_ref}")
|
||||
args.append(f"eventid={t_data.get('eit', '0')}")
|
||||
args.append(f"justplay={t_data.get('justplay', '')}")
|
||||
args.append(f"tags={''}")
|
||||
else:
|
||||
if self._action is TimerTool.TimerAction.ADD:
|
||||
args.append(f"timeradd?sRef={s_ref}")
|
||||
args.append(f"deleteOldOnSave={0}")
|
||||
elif self._action is TimerTool.TimerAction.CHANGE:
|
||||
args.append(f"timerchange?sRef={s_ref}")
|
||||
args.append(f"channelOld={s_ref}")
|
||||
args.append(f"beginOld={self._timer_data.get('e2timebegin', '0')}")
|
||||
args.append(f"endOld={self._timer_data.get('e2timeend', '0')}")
|
||||
args.append(f"deleteOldOnSave={1}")
|
||||
|
||||
args.append(f"begin={t_data.get('begin', '')}")
|
||||
args.append(f"end={t_data.get('end', '')}")
|
||||
args.append(f"name={quote(t_data.get('name', ''))}")
|
||||
args.append(f"description={quote(t_data.get('description', ''))}")
|
||||
args.append(f"tags={''}")
|
||||
args.append(f"eit={'0'}")
|
||||
args.append(f"disabled={t_data.get('disabled', '1')}")
|
||||
args.append(f"justplay={t_data.get('justplay', '1')}")
|
||||
args.append(f"afterevent={t_data.get('afterevent', '0')}")
|
||||
args.append(f"repeated={TimerTool.get_repetition_flags(self._days_buttons)}")
|
||||
|
||||
if self._timer_location_switch.get_active():
|
||||
args.append(f"dirname={self._timer_location_entry.get_text()}")
|
||||
|
||||
return "&".join(args)
|
||||
|
||||
def on_timer_begins_set(self, action, value=None):
|
||||
b_date = self.get_begins_date()
|
||||
if b_date > self.get_ends_date():
|
||||
self.set_ends_date(b_date + timedelta(hours=1))
|
||||
self.set_begins_date(b_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(f"{date.year}-{date.month:02d}-{date.day:02d} {hour:02d}:{minute:02d}")
|
||||
|
||||
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(f"{date.year}-{date.month:02d}-{date.day:02d} {hour:02d}:{minute:02d}")
|
||||
|
||||
def set_timer_for_add(self):
|
||||
self._timer_service_entry.set_text(self._timer_data.get("e2servicename", ""))
|
||||
self._timer_service_ref_entry.set_text(self._timer_data.get("e2servicereference", ""))
|
||||
date = datetime.now()
|
||||
self.set_begins_date(date)
|
||||
self.set_ends_date(date + timedelta(hours=1))
|
||||
self._timer_event_id_entry.set_text("")
|
||||
self._timer_location_switch.set_active(False)
|
||||
TimerTool.set_repetition_flags(0, self._days_buttons)
|
||||
|
||||
def set_timer_for_edit(self):
|
||||
self._timer_name_entry.set_text(self._timer_data.get("e2name", ""))
|
||||
self._timer_desc_entry.set_text(self._timer_data.get("e2description", "") or "")
|
||||
self._timer_service_entry.set_text(self._timer_data.get("e2servicename", "") or "")
|
||||
self._timer_service_ref_entry.set_text(self._timer_data.get("e2servicereference", ""))
|
||||
self._timer_event_id_entry.set_text(self._timer_data.get("e2eit", ""))
|
||||
self._timer_enabled_switch.set_active((self._timer_data.get("e2disabled", "0") == "0"))
|
||||
self._timer_action_combo_box.set_active_id(self._timer_data.get("e2justplay", "0"))
|
||||
self._timer_after_combo_box.set_active_id(self._timer_data.get("e2afterevent", "0"))
|
||||
self.set_time_data(int(self._timer_data.get("e2timebegin", "0")),
|
||||
int(self._timer_data.get("e2timeend", "0")))
|
||||
location = self._timer_data.get("e2location", "")
|
||||
self._timer_location_entry.set_text("" if location == "None" else location)
|
||||
TimerTool.set_repetition_flags(int(self._timer_data.get("e2repeated", "0")), self._days_buttons)
|
||||
|
||||
def set_timer_from_event_data(self):
|
||||
self._timer_name_entry.set_text(self._timer_data.get("e2eventtitle", ""))
|
||||
self._timer_desc_entry.set_text(self._timer_data.get("e2eventdescription", ""))
|
||||
self._timer_service_entry.set_text(self._timer_data.get("e2eventservicename", ""))
|
||||
self._timer_service_ref_entry.set_text(self._timer_data.get("e2eventservicereference", ""))
|
||||
self._timer_event_id_entry.set_text(self._timer_data.get("e2eventid", ""))
|
||||
self._timer_action_combo_box.set_active_id("1")
|
||||
self._timer_after_combo_box.set_active_id("3")
|
||||
start_time = int(self._timer_data.get("e2eventstart", "0"))
|
||||
self.set_time_data(start_time, start_time + int(self._timer_data.get("e2eventduration", "0")))
|
||||
|
||||
def set_time_data(self, start_time, end_time):
|
||||
""" Sets values for time widgets. """
|
||||
now = datetime.now()
|
||||
ev_time_start = datetime.fromtimestamp(start_time) or now
|
||||
ev_time_end = datetime.fromtimestamp(end_time) or now + timedelta(hours=1)
|
||||
self._timer_begins_entry.set_text(ev_time_start.strftime(TimerTool.TIME_STR))
|
||||
self._timer_ends_entry.set_text(ev_time_end.strftime(TimerTool.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(), TimerTool.TIME_STR).timestamp()),
|
||||
"end": int(datetime.strptime(self._timer_ends_entry.get_text(), TimerTool.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": TimerTool.get_repetition_flags(self._days_buttons)}
|
||||
|
||||
def __init__(self, app, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._app = app
|
||||
self._app.connect("page-changed", self.update_timer_list)
|
||||
# Icon.
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
icon = "alarm-symbolic"
|
||||
self._icon = theme.load_icon(icon, 16, 0) if theme.lookup_icon(icon, 16, 0) else None
|
||||
|
||||
handlers = {"on_timer_add": self.on_timer_add,
|
||||
"on_timer_edit": self.on_timer_edit,
|
||||
"on_timer_remove": self.on_timer_remove,
|
||||
"on_model_changed": self.on_model_changed,
|
||||
"on_timers_press": self.on_timers_press,
|
||||
"on_timers_key_press": self.on_timers_key_press,
|
||||
"on_timer_cursor_changed": self.on_timer_cursor_changed,
|
||||
"on_timers_drag_data_received": self.on_timers_drag_data_received}
|
||||
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}timers.glade", handlers,
|
||||
objects=("timers_frame", "timer_model", "popup_menu", "popup_menu_add_image"))
|
||||
|
||||
self._view = builder.get_object("timer_view")
|
||||
self._remove_button = builder.get_object("timer_remove_button")
|
||||
self._remove_button.bind_property("sensitive", builder.get_object("timer_edit_button"), "sensitive")
|
||||
self._remove_button.bind_property("sensitive", builder.get_object("edit_menu_item"), "sensitive")
|
||||
self._remove_button.bind_property("sensitive", builder.get_object("remove_menu_item"), "sensitive")
|
||||
self._info_button = builder.get_object("timer_info_check_button")
|
||||
self._info_button.bind_property("active", builder.get_object("timer_info_frame"), "visible")
|
||||
self._info_enabled_switch = builder.get_object("timer_info_enabled_switch")
|
||||
self._timers_count_label = builder.get_object("timers_count_label")
|
||||
self._ref_info_label = builder.get_object("timer_ref_value_label")
|
||||
self._event_id_info_label = builder.get_object("timer_event_id_value_label")
|
||||
self._begins_info_label = builder.get_object("timer_begins_value_label")
|
||||
self._ends_info_label = builder.get_object("timer_ends_value_label")
|
||||
self._action_info_label = builder.get_object("timer_action_value_label")
|
||||
self._after_info_label = builder.get_object("timer_after_value_label")
|
||||
self._timer_location_switch = builder.get_object("timer_location_switch")
|
||||
self._info_location_entry = builder.get_object("timer_info_location_entry")
|
||||
self._days_buttons = (builder.get_object("timer_info_mo_check_button"),
|
||||
builder.get_object("timer_info_tu_check_button"),
|
||||
builder.get_object("timer_info_we_check_button"),
|
||||
builder.get_object("timer_info_th_check_button"),
|
||||
builder.get_object("timer_info_fr_check_button"),
|
||||
builder.get_object("timer_info_sa_check_button"),
|
||||
builder.get_object("timer_info_su_check_button"))
|
||||
# Disable button presses.
|
||||
list(map(lambda b: b.connect("button-press-event", lambda bx, e: True), self._days_buttons))
|
||||
self._info_enabled_switch.connect("button-press-event", lambda b, e: True)
|
||||
# DnD initialization for the timer list.
|
||||
self._view.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
|
||||
self._view.drag_dest_add_text_targets()
|
||||
|
||||
self.pack_start(builder.get_object("timers_frame"), True, True, 0)
|
||||
self.show()
|
||||
|
||||
def update_timer_list(self, app, page):
|
||||
if page is Page.TIMERS:
|
||||
self._app.wait_dialog.show()
|
||||
self._app.send_http_request(HttpAPI.Request.TIMER_LIST, "", self.update_timers_data)
|
||||
|
||||
@run_idle
|
||||
def update_timers_data(self, timers):
|
||||
model = self._view.get_model()
|
||||
model.clear()
|
||||
list(map(model.append, (self.get_timer_row(t) for t in timers.get("timer_list", []))))
|
||||
self._remove_button.set_sensitive(len(model))
|
||||
self._app.wait_dialog.hide()
|
||||
|
||||
def get_timer_row(self, timer):
|
||||
disabled = self._icon if timer.get("e2disabled", "0") == "0" else None
|
||||
name = timer.get("e2name", "") or ""
|
||||
description = timer.get("e2description", "") or ""
|
||||
service = timer.get("e2servicename", "") or ""
|
||||
start_time = datetime.fromtimestamp(int(timer.get("e2timebegin", "0")))
|
||||
end_time = datetime.fromtimestamp(int(timer.get("e2timeend", "0")))
|
||||
time = f"{start_time.strftime('%a, %x, %H:%M')} - {end_time.strftime('%H:%M')}"
|
||||
|
||||
return disabled, name, service, time, description, timer
|
||||
|
||||
def on_timer_add(self, timer=None, value=None):
|
||||
model, paths = self._app.fav_view.get_selection().get_selected_rows()
|
||||
p_count = len(paths)
|
||||
|
||||
if p_count == 1:
|
||||
service = self._app.current_services.get(model[paths][Column.FAV_ID], None)
|
||||
if service:
|
||||
self.add_timer({"e2servicename": service.service,
|
||||
"e2servicereference": service.picon_id.rstrip(".png").replace("_", ":")})
|
||||
elif p_count > 1:
|
||||
self._app.show_error_message("Please, select only one item!")
|
||||
else:
|
||||
self._app.show_error_message("No selected item!")
|
||||
|
||||
def add_timer(self, timer_data):
|
||||
dialog = self.TimerDialog(self._app.app_window, self.TimerAction.ADD, timer_data)
|
||||
response = dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
self._app.send_http_request(HttpAPI.Request.TIMER, dialog.request, self.timer_add_edit_callback)
|
||||
dialog.destroy()
|
||||
|
||||
def on_timer_edit(self, action=None, value=None):
|
||||
model, paths = self._view.get_selection().get_selected_rows()
|
||||
if len(paths) > 1:
|
||||
self._app.show_error_message("Please, select only one item!")
|
||||
return
|
||||
|
||||
dialog = self.TimerDialog(self._app.app_window, self.TimerAction.CHANGE, model[paths][-1])
|
||||
response = dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
self._app.send_http_request(HttpAPI.Request.TIMER, dialog.request, self.timer_add_edit_callback)
|
||||
dialog.destroy()
|
||||
|
||||
@run_idle
|
||||
def timer_add_edit_callback(self, resp):
|
||||
if "error_code" in resp:
|
||||
msg = f"Error getting timer status.\n{resp.get('error_code')}"
|
||||
self._app.show_error_message(msg)
|
||||
log(msg)
|
||||
return
|
||||
|
||||
state = resp.get("e2state", None)
|
||||
if state == "False":
|
||||
msg = resp.get("e2statetext", "")
|
||||
self._app.show_error_message(msg)
|
||||
log(msg)
|
||||
if state == "True":
|
||||
msg = resp.get("e2statetext", "")
|
||||
log(msg)
|
||||
self._app.show_info_message(msg, Gtk.MessageType.INFO)
|
||||
self.update_timer_list(self._app, Page.TIMERS)
|
||||
else:
|
||||
log("Error getting timer status. No response!")
|
||||
|
||||
def on_timer_remove(self, action=None, value=None):
|
||||
model, paths = self._view.get_selection().get_selected_rows()
|
||||
if not paths or show_dialog(DialogType.QUESTION, self._app.app_window) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
refs = {}
|
||||
for path in paths:
|
||||
timer = model[path][-1]
|
||||
ref = "timerdelete?sRef={}&begin={}&end={}".format(quote(timer.get("e2servicereference", "")),
|
||||
timer.get("e2timebegin", ""),
|
||||
timer.get("e2timeend", ""))
|
||||
refs[ref] = model.get_iter(path)
|
||||
|
||||
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
|
||||
|
||||
model = self._view.get_model()
|
||||
list(map(model.remove, (refs[ref] for ref in refs if ref in removed)))
|
||||
self._app.wait_dialog.hide()
|
||||
self._remove_button.set_sensitive(len(model))
|
||||
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._app.send_http_request(HttpAPI.Request.TIMER, ref, callback)
|
||||
yield True
|
||||
|
||||
def on_model_changed(self, model, path, itr=None):
|
||||
self._timers_count_label.set_text(str(len(model)))
|
||||
|
||||
def on_timers_press(self, menu, event):
|
||||
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(self._view.get_model()) > 0:
|
||||
self.on_timer_edit()
|
||||
else:
|
||||
on_popup_menu(menu, event)
|
||||
|
||||
def on_timers_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.DELETE:
|
||||
self.on_timer_remove()
|
||||
elif key is KeyboardKey.INSERT:
|
||||
self.on_timer_add()
|
||||
elif ctrl and key is KeyboardKey.E:
|
||||
self.on_timer_edit()
|
||||
|
||||
def on_timer_cursor_changed(self, view):
|
||||
path, column = view.get_cursor()
|
||||
if not path:
|
||||
return
|
||||
|
||||
timer = view.get_model()[path][-1]
|
||||
self._info_enabled_switch.set_active((timer.get("e2disabled", "0") == "0"))
|
||||
self._ref_info_label.set_text(timer.get("e2servicereference", ""))
|
||||
self._event_id_info_label.set_text(timer.get("e2eit", ""))
|
||||
self._action_info_label.set_text(get_message(self.ACTION.get(timer.get("e2justplay", "0"), "0")))
|
||||
self._after_info_label.set_text(get_message(self.AFTER_EVENT.get(timer.get("e2afterevent", "0"), "0")))
|
||||
self._begins_info_label.set_text(str(datetime.fromtimestamp(int(timer.get("e2timebegin", "0")))))
|
||||
self._ends_info_label.set_text(str(datetime.fromtimestamp(int(timer.get("e2timeend", "0")))))
|
||||
self.set_repetition_flags(int(timer.get("e2repeated", "0")), self._days_buttons)
|
||||
location = timer.get("e2location", "")
|
||||
self._info_location_entry.set_text("" if location == "None" else location)
|
||||
|
||||
@staticmethod
|
||||
def get_repetition_flags(boxes):
|
||||
""" Returns flags for repetition.
|
||||
|
||||
@param boxes: Buttons tuple for the days of the week.
|
||||
"""
|
||||
day_flags = 0
|
||||
for i, box in enumerate(boxes):
|
||||
if box.get_active():
|
||||
day_flags = day_flags | (1 << i)
|
||||
|
||||
return day_flags
|
||||
|
||||
@staticmethod
|
||||
def set_repetition_flags(flags, boxes):
|
||||
""" Sets flags for repetition.
|
||||
|
||||
@param flags: Flags value.
|
||||
@param boxes: Buttons tuple for the days of the week.
|
||||
"""
|
||||
for i, box in enumerate(boxes):
|
||||
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_message("Please, select only one item!")
|
||||
return
|
||||
|
||||
fav_id = None
|
||||
if source == self._app.FAV_MODEL:
|
||||
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:
|
||||
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 {get_message('Not implemented yet!')}"
|
||||
show_dialog(DialogType.ERROR, transient=self._app.app_window, text=msg)
|
||||
context.finish(False, False, time)
|
||||
return
|
||||
|
||||
self.add_timer({"e2servicename": service.service,
|
||||
"e2servicereference": service.picon_id.rstrip(".png").replace("_", ":")})
|
||||
|
||||
context.finish(True, False, time)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@@ -1,3 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 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 locale
|
||||
import os
|
||||
from enum import Enum, IntEnum
|
||||
@@ -7,7 +35,7 @@ import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("Gdk", "3.0")
|
||||
from gi.repository import Gtk, Gdk
|
||||
from gi.repository import Gtk, Gdk, GLib
|
||||
|
||||
from app.settings import Settings, SettingsException, IS_DARWIN, GTK_PATH, IS_LINUX
|
||||
|
||||
@@ -58,8 +86,8 @@ if IS_LINUX:
|
||||
try:
|
||||
gi.require_version("Notify", "0.7")
|
||||
from gi.repository import Notify
|
||||
except ImportError:
|
||||
pass
|
||||
except (ImportError, ValueError):
|
||||
pass # NOP
|
||||
else:
|
||||
NOTIFY_IS_INIT = Notify.init("DemonEditor")
|
||||
elif IS_DARWIN:
|
||||
@@ -78,16 +106,22 @@ else:
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
theme.append_search_path(UI_RESOURCES_PATH + "icons")
|
||||
|
||||
_IMAGE_MISSING = theme.load_icon("image-missing", 16, 0) if theme.lookup_icon("image-missing", 16, 0) else None
|
||||
CODED_ICON = theme.load_icon("emblem-readonly", 16, 0) if theme.lookup_icon(
|
||||
"emblem-readonly", 16, 0) else _IMAGE_MISSING
|
||||
LOCKED_ICON = theme.load_icon("changes-prevent-symbolic", 16, 0) if theme.lookup_icon(
|
||||
"system-lock-screen", 16, 0) else _IMAGE_MISSING
|
||||
HIDE_ICON = theme.load_icon("go-jump", 16, 0) if theme.lookup_icon("go-jump", 16, 0) else _IMAGE_MISSING
|
||||
TV_ICON = theme.load_icon("tv-symbolic", 16, 0) if theme.lookup_icon("tv-symbolic", 16, 0) else _IMAGE_MISSING
|
||||
IPTV_ICON = theme.load_icon("emblem-shared", 16, 0) if theme.lookup_icon("emblem-shared", 16, 0) else None
|
||||
EPG_ICON = theme.load_icon("gtk-index", 16, 0) if theme.lookup_icon("gtk-index", 16, 0) else None
|
||||
DEFAULT_ICON = theme.load_icon("emblem-default", 16, 0) if theme.lookup_icon("emblem-default", 16, 0) else None
|
||||
|
||||
def get_icon(name, size, default=None):
|
||||
try:
|
||||
return theme.load_icon(name, size, 0) if theme.lookup_icon(name, size, 0) else default
|
||||
except GLib.Error:
|
||||
return default
|
||||
|
||||
|
||||
_IMAGE_MISSING = get_icon("image-missing", 16)
|
||||
CODED_ICON = get_icon("emblem-readonly", 16, _IMAGE_MISSING)
|
||||
LOCKED_ICON = get_icon("changes-prevent-symbolic", 16, _IMAGE_MISSING)
|
||||
HIDE_ICON = get_icon("go-jump", 16, _IMAGE_MISSING)
|
||||
TV_ICON = get_icon("tv-symbolic", 16, _IMAGE_MISSING)
|
||||
IPTV_ICON = get_icon("emblem-shared", 16, _IMAGE_MISSING)
|
||||
EPG_ICON = get_icon("gtk-index", 16, _IMAGE_MISSING)
|
||||
DEFAULT_ICON = get_icon("emblem-default", 16, _IMAGE_MISSING)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
@@ -155,6 +189,7 @@ class ViewTarget(Enum):
|
||||
BOUQUET = 0
|
||||
FAV = 1
|
||||
SERVICES = 2
|
||||
IPTV = 3
|
||||
|
||||
|
||||
class BqGenType(Enum):
|
||||
@@ -225,6 +260,15 @@ class Column(IntEnum):
|
||||
REC_LEN = 3
|
||||
REC_FILE = 4
|
||||
REC_DESC = 5
|
||||
# IPTV view
|
||||
IPTV_SERVICE = 0
|
||||
IPTV_TYPE = 1
|
||||
IPTV_PICON = 2
|
||||
IPTV_REF = 3
|
||||
IPTV_URL = 4
|
||||
IPTV_FAV_ID = 5
|
||||
IPTV_PICON_ID = 6
|
||||
IPTV_TOOLTIP = 7
|
||||
|
||||
def __index__(self):
|
||||
""" Overridden to get the index in slices directly """
|
||||
@@ -263,10 +307,13 @@ if IS_LINUX:
|
||||
LEFT = 113
|
||||
RIGHT = 114
|
||||
F2 = 68
|
||||
F4 = 70
|
||||
F5 = 71
|
||||
F7 = 73
|
||||
SPACE = 65
|
||||
DELETE = 119
|
||||
BACK_SPACE = 22
|
||||
RETURN = 36
|
||||
CTRL_L = 37
|
||||
CTRL_R = 105
|
||||
# Laptop codes
|
||||
@@ -301,10 +348,13 @@ elif IS_DARWIN:
|
||||
LEFT = 123
|
||||
RIGHT = 123
|
||||
F2 = 120
|
||||
F4 = 118
|
||||
F5 = 96
|
||||
F7 = 98
|
||||
SPACE = 49
|
||||
DELETE = 51
|
||||
BACK_SPACE = 76
|
||||
RETURN = 36
|
||||
CTRL_L = 55
|
||||
CTRL_R = 55
|
||||
# Laptop codes.
|
||||
@@ -337,10 +387,13 @@ else:
|
||||
LEFT = 37
|
||||
RIGHT = 39
|
||||
F2 = 113
|
||||
F4 = 115
|
||||
F5 = 116
|
||||
F7 = 118
|
||||
SPACE = 32
|
||||
DELETE = 46
|
||||
BACK_SPACE = 8
|
||||
RETURN = 13
|
||||
CTRL_L = 17
|
||||
CTRL_R = 163
|
||||
# Laptop codes.
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
* {
|
||||
-GtkDialog-action-area-border: 6;
|
||||
-GtkDialog-action-area-border: 12;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 24px;
|
||||
min-width: 24px;
|
||||
switch {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
spinbutton entry {
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
button > image {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
grid > button {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
0
app/ui/xml/__init__.py
Normal file
0
app/ui/xml/__init__.py
Normal file
984
app/ui/xml/dialogs.glade
Normal file
984
app/ui/xml/dialogs.glade
Normal file
@@ -0,0 +1,984 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2022 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.20"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkGrid" id="cable_tr_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="column_spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="cable_freq_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Freq</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="cable_rate_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Rate</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="cable_rate_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">12</property>
|
||||
<property name="max_width_chars">14</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="placeholder_text">6900000</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="cable_fec_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">FEC</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="cable_mod_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Mod</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="cable_freq_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">12</property>
|
||||
<property name="max_width_chars">14</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_sensitive">False</property>
|
||||
<property name="placeholder_text">120000</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="cable_fec_box">
|
||||
<property name="width_request">75</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="cable_mod_box">
|
||||
<property name="width_request">100</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkListStore" id="fec_store">
|
||||
<columns>
|
||||
<!-- column-name fec -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0">1/2</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">2/3</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">3/4</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">5/6</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">6/7</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">7/8</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">8/9</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">3/5</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">4/5</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">9/10</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Auto</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkListStore" id="mod_store">
|
||||
<columns>
|
||||
<!-- column-name mod -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0">Auto</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">QPSK</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">8PSK</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">16APSK</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">32APSK</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkListStore" id="pls_mode_store">
|
||||
<columns>
|
||||
<!-- column-name pls_mode -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0">Root</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">Gold</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">Combo</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkListStore" id="pol_store">
|
||||
<columns>
|
||||
<!-- column-name pol -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0">H</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">V</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">R</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">L</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkAdjustment" id="pos_adjustment">
|
||||
<property name="upper">180</property>
|
||||
<property name="step_increment">0.10000000000000001</property>
|
||||
<property name="page_increment">10</property>
|
||||
</object>
|
||||
<object class="GtkListStore" id="side_store">
|
||||
<columns>
|
||||
<!-- column-name side -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0">E</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">W</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkGrid" id="sat_dialog_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="column_spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label11">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Name</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label12">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Position</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="sat_name_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_sensitive">False</property>
|
||||
<property name="placeholder_text" translatable="yes">satellite name</property>
|
||||
<property name="input_purpose">name</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="sat_position_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">5</property>
|
||||
<property name="secondary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_sensitive">False</property>
|
||||
<property name="input_purpose">number</property>
|
||||
<property name="adjustment">pos_adjustment</property>
|
||||
<property name="digits">1</property>
|
||||
<property name="numeric">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="side_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">side_store</property>
|
||||
<property name="active">0</property>
|
||||
<property name="id_column">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="side_cellrenderertext"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkListStore" id="system_store">
|
||||
<columns>
|
||||
<!-- column-name system -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0">DVB-S</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">DVB-S2</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkBox" id="sat_tr_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkGrid" id="sat_tr_dialog_grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="column_spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="sat_freq_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Freq</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="sat_rate_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Rate</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="sat_pol_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Pol</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="sat_fec_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">FEC</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="sat_sys_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">System</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">4</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="sat_mod_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Mod</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">5</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="freq_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">12</property>
|
||||
<property name="max_width_chars">14</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_sensitive">False</property>
|
||||
<property name="placeholder_text">11700000</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="rate_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">12</property>
|
||||
<property name="max_width_chars">14</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="placeholder_text">27500000</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="pol_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">pol_store</property>
|
||||
<property name="id_column">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="pol_cellrenderertext"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="fec_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">fec_store</property>
|
||||
<property name="id_column">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="cellrenderertext4"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="sys_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">system_store</property>
|
||||
<property name="id_column">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="cellrenderertext5"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">4</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="mod_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">mod_store</property>
|
||||
<property name="id_column">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="cellrenderertext6"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">5</property>
|
||||
<property name="top_attach">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="GtkExpander" id="expander">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="resize_toplevel">True</property>
|
||||
<child>
|
||||
<object class="GtkGrid" id="tr_dialog_grid2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="column_spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="tr_pls_mode_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Pls mode</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="tr_pls_code_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Pls code</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="id_id_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Is ID</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="pls_mode_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">pls_mode_store</property>
|
||||
<property name="id_column">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="pls_mode_cellrenderertext1"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="pls_code_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">5</property>
|
||||
<property name="max_width_chars">12</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="placeholder_text" translatable="yes">0 - 262142</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="is_id_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">5</property>
|
||||
<property name="max_width_chars">12</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="placeholder_text" translatable="yes">0 - 255</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="tr_t2mi_plp_id_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">T2-MI PLP ID</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="t2mi_plp_id_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">5</property>
|
||||
<property name="max_width_chars">12</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="placeholder_text" translatable="yes">0 - 255</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="expander_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Extra:</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkBox" id="ter_tr_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkGrid" id="ter_tr_grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="column_spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_freq_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Freq</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="ter_freq_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">12</property>
|
||||
<property name="max_width_chars">14</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_sensitive">False</property>
|
||||
<property name="placeholder_text">170000000</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_sys_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">System</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="ter_sys_box">
|
||||
<property name="width_request">100</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="ter_bandwidth_box">
|
||||
<property name="width_request">110</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_bandwidth_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Bandwidth</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_constellation_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Constellation</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_sr_hp_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">SR (HP)</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">4</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_sr_lp_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">SR (LP)</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">5</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="ter_sr_hp_box">
|
||||
<property name="width_request">75</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">4</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="ter_sr_lp_box">
|
||||
<property name="width_request">75</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">5</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="ter_constellation_box">
|
||||
<property name="width_request">100</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">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="GtkGrid" id="ter_tr_ext_grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="column_spacing">5</property>
|
||||
<property name="column_homogeneous">True</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_guard_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Guard</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_transmission_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Transmission</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_hierarchy_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Hierarchy</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_inversion_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Inversion</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="ter_guard_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="ter_transmission_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="ter_hierarchy_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="ter_inversion_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="ter_plp_id_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">8</property>
|
||||
<property name="max_width_chars">8</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="placeholder_text">0-255</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">4</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_plp_id_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">PLP ID</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">4</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -27,357 +27,121 @@
|
||||
|
||||
|
||||
import concurrent.futures
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from math import fabs
|
||||
from pyexpat import ExpatError
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
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.eparser import Satellite, Transponder
|
||||
from app.eparser.ecommons import (PLS_MODE, get_key_by_value, POLARIZATION, FEC, SYSTEM, MODULATION, Terrestrial, Cable,
|
||||
T_SYSTEM, BANDWIDTH, CONSTELLATION, T_FEC, GUARD_INTERVAL, TRANSMISSION_MODE,
|
||||
HIERARCHY, Inversion, C_MODULATION, FEC_DEFAULT, TerTransponder, CableTransponder)
|
||||
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, 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
|
||||
from ..dialogs import show_dialog, DialogType, get_message, get_builder
|
||||
from ..main_helper import append_text_to_tview, get_base_model, on_popup_menu
|
||||
from ..search import SearchProvider
|
||||
from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, IS_GNOME_SESSION
|
||||
|
||||
_UI_PATH = UI_RESOURCES_PATH + "satellites.glade"
|
||||
_DIALOGS_UI_PATH = f"{UI_RESOURCES_PATH}xml{os.sep}dialogs.glade"
|
||||
|
||||
|
||||
class SatellitesTool(Gtk.Box):
|
||||
_aggr = [None for x in range(9)] # aggregate
|
||||
class DVBDialog(Gtk.Dialog):
|
||||
""" Base dialog class for editing DVB (-> *.xml) data. """
|
||||
|
||||
def __init__(self, app, settings, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
def __init__(self, parent, title, data=None, *args, **kwargs):
|
||||
super().__init__(transient_for=parent,
|
||||
title=get_message(title),
|
||||
modal=True,
|
||||
resizable=False,
|
||||
default_width=320,
|
||||
skip_taskbar_hint=True,
|
||||
skip_pager_hint=True,
|
||||
destroy_with_parent=True,
|
||||
use_header_bar=IS_GNOME_SESSION,
|
||||
window_position=Gtk.WindowPosition.CENTER_ON_PARENT,
|
||||
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK),
|
||||
*args, **kwargs)
|
||||
|
||||
self._app = app
|
||||
self._settings = settings
|
||||
self._current_sat_path = None
|
||||
self.frame = Gtk.Frame(margin=5, label_xalign=0.02)
|
||||
self.get_content_area().pack_start(self.frame, True, True, 0)
|
||||
|
||||
handlers = {"on_remove": self.on_remove,
|
||||
"on_update": self.on_update,
|
||||
"on_up": self.on_up,
|
||||
"on_down": self.on_down,
|
||||
"on_button_press": self.on_button_press,
|
||||
"on_satellite_add": self.on_satellite_add,
|
||||
"on_transponder_add": self.on_transponder_add,
|
||||
"on_edit": self.on_edit,
|
||||
"on_key_release": self.on_key_release,
|
||||
"on_satellite_selection": self.on_satellite_selection}
|
||||
self._data = data
|
||||
|
||||
builder = get_builder(_UI_PATH, handlers, use_str=True,
|
||||
objects=("satellite_editor_box", "satellite_view_model", "transponder_view_model",
|
||||
"satellite_popup_menu", "transponder_popup_menu", "left_header_menu",
|
||||
"popup_menu_add_image", "popup_menu_add_image_2"))
|
||||
|
||||
self._satellite_view = builder.get_object("satellite_view")
|
||||
self._transponder_view = builder.get_object("transponder_view")
|
||||
builder.get_object("sat_pos_column").set_cell_data_func(builder.get_object("sat_pos_renderer"),
|
||||
self.sat_pos_func)
|
||||
|
||||
self._stores = {3: builder.get_object("pol_store"),
|
||||
4: builder.get_object("fec_store"),
|
||||
5: builder.get_object("system_store"),
|
||||
6: builder.get_object("mod_store")}
|
||||
|
||||
self.pack_start(builder.get_object("satellite_editor_box"), True, True, 0)
|
||||
self._app.connect("profile-changed", lambda a, m: self.load_satellites_list())
|
||||
self.show()
|
||||
self.load_satellites_list()
|
||||
|
||||
def load_satellites_list(self, path=None):
|
||||
gen = self.on_satellites_list_load(path)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
@run_idle
|
||||
def on_open(self):
|
||||
response = get_chooser_dialog(self._app.app_window, self._settings, "satellites.xml", ("*.xml",))
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
|
||||
if not str(response).endswith("satellites.xml"):
|
||||
self._app.show_error_message("No satellites.xml file is selected!")
|
||||
return
|
||||
|
||||
self.load_satellites_list(response)
|
||||
|
||||
def on_satellite_selection(self, view):
|
||||
model = self._transponder_view.get_model()
|
||||
model.clear()
|
||||
|
||||
self._current_sat_path, column = view.get_cursor()
|
||||
if self._current_sat_path:
|
||||
list(map(model.append, view.get_model()[self._current_sat_path][-1]))
|
||||
|
||||
def on_up(self, item):
|
||||
move_items(KeyboardKey.UP, self._satellite_view)
|
||||
|
||||
def on_down(self, item):
|
||||
move_items(KeyboardKey.DOWN, self._satellite_view)
|
||||
|
||||
def on_button_press(self, menu, event):
|
||||
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS:
|
||||
self.on_edit(self._satellite_view if self._satellite_view.is_focus() else self._transponder_view)
|
||||
else:
|
||||
on_popup_menu(menu, event)
|
||||
|
||||
def on_key_release(self, view, event):
|
||||
""" Handling keystrokes """
|
||||
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.DELETE:
|
||||
self.on_remove(view)
|
||||
elif key is KeyboardKey.INSERT:
|
||||
pass
|
||||
elif ctrl and key is KeyboardKey.E:
|
||||
self.on_edit(view)
|
||||
elif ctrl and key is KeyboardKey.S:
|
||||
self.on_satellite()
|
||||
elif ctrl and key is KeyboardKey.T:
|
||||
self.on_transponder()
|
||||
elif ctrl and key in MOVE_KEYS:
|
||||
move_items(key, self._satellite_view)
|
||||
elif key is KeyboardKey.LEFT or key is KeyboardKey.RIGHT:
|
||||
view.do_unselect_all(view)
|
||||
|
||||
def on_satellites_list_load(self, path=None):
|
||||
""" Load satellites data into model """
|
||||
model = self._satellite_view.get_model()
|
||||
model.clear()
|
||||
|
||||
try:
|
||||
path = path or self._settings.profile_data_path + "satellites.xml"
|
||||
satellites = get_satellites(path)
|
||||
yield True
|
||||
except FileNotFoundError as e:
|
||||
msg = get_message("Please, download files from receiver or setup your path for read data!")
|
||||
self._app.show_error_message(f"{e}\n{msg}")
|
||||
except ExpatError as e:
|
||||
msg = f"The file [{path}] is not formatted correctly or contains invalid characters! Cause: {e}"
|
||||
self._app.show_error_message(msg)
|
||||
else:
|
||||
for sat in satellites:
|
||||
yield model.append(sat)
|
||||
|
||||
def on_add(self, view):
|
||||
""" Common adding """
|
||||
self.on_edit(view, force=True)
|
||||
|
||||
def on_satellite_add(self, item):
|
||||
self.on_satellite(None)
|
||||
|
||||
def on_transponder_add(self, item):
|
||||
self.on_transponder(None)
|
||||
|
||||
def on_edit(self, view, force=False):
|
||||
""" Common edit """
|
||||
paths = self.check_selection(view, "Please, select only one item!")
|
||||
if not paths:
|
||||
return
|
||||
|
||||
model = view.get_model()
|
||||
row = model[paths][:]
|
||||
itr = model.get_iter(paths)
|
||||
|
||||
if view is self._satellite_view:
|
||||
self.on_satellite(None if force else Satellite(*row), itr)
|
||||
elif view is self._transponder_view:
|
||||
self.on_transponder(None if force else Transponder(*row), itr)
|
||||
|
||||
def on_satellite(self, satellite=None, edited_itr=None):
|
||||
""" Create or edit satellite"""
|
||||
sat_dialog = SatelliteDialog(self._app.get_active_window(), satellite)
|
||||
sat = sat_dialog.run()
|
||||
sat_dialog.destroy()
|
||||
|
||||
if sat:
|
||||
model, paths = self._satellite_view.get_selection().get_selected_rows()
|
||||
if satellite and edited_itr:
|
||||
model.set(edited_itr, {i: v for i, v in enumerate(sat)})
|
||||
else:
|
||||
if len(model):
|
||||
index = paths[0].get_indices()[0] + 1
|
||||
model.insert(index, sat)
|
||||
else:
|
||||
model.append(sat)
|
||||
|
||||
def on_transponder(self, transponder=None, edited_itr=None):
|
||||
""" Create or edit transponder """
|
||||
|
||||
paths = self.check_selection(self._satellite_view, "Please, select only one satellite!")
|
||||
if paths is None:
|
||||
return
|
||||
elif len(paths) == 0:
|
||||
self._app.show_error_message("No satellite is selected!")
|
||||
return
|
||||
|
||||
dialog = TransponderDialog(self._app.get_active_window(), transponder)
|
||||
tr = dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
if tr:
|
||||
sat_model = self._satellite_view.get_model()
|
||||
transponders = sat_model[paths][-1]
|
||||
tr_model, tr_paths = self._transponder_view.get_selection().get_selected_rows()
|
||||
|
||||
if transponder and edited_itr:
|
||||
tr_model.set(edited_itr, {i: v for i, v in enumerate(tr)})
|
||||
transponders[tr_model.get_path(edited_itr).get_indices()[0]] = tr
|
||||
else:
|
||||
index = paths[0].get_indices()[0] + 1
|
||||
tr_model.insert(index, tr)
|
||||
transponders.insert(index, tr)
|
||||
|
||||
def check_selection(self, view, message):
|
||||
""" Checks if any row is selected. Shows error dialog if selected more than one.
|
||||
|
||||
Returns selected path or None.
|
||||
"""
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if len(paths) > 1:
|
||||
self._app.show_error_message(message)
|
||||
return
|
||||
|
||||
return paths
|
||||
|
||||
def on_remove(self, view):
|
||||
""" Removes selected satellites and transponders. """
|
||||
selection = view.get_selection()
|
||||
model, paths = selection.get_selected_rows()
|
||||
|
||||
if view is self._satellite_view:
|
||||
list(map(model.remove, [model.get_iter(path) for path in paths]))
|
||||
elif view is self._transponder_view:
|
||||
if self._current_sat_path:
|
||||
trs = self._satellite_view.get_model()[self._current_sat_path][-1]
|
||||
list(map(trs.pop, sorted(map(lambda p: p.get_indices()[0], paths), reverse=True)))
|
||||
list(map(model.remove, [model.get_iter(path) for path in paths]))
|
||||
else:
|
||||
self._app.show_error_message("No satellite is selected!")
|
||||
|
||||
def sat_pos_func(self, column, renderer, model, itr, data):
|
||||
""" Converts and sets the satellite position value to a readable format. """
|
||||
pos = int(model.get_value(itr, 2))
|
||||
renderer.set_property("text", f"{abs(pos / 10):0.1f}{'W' if pos < 0 else 'E'}")
|
||||
|
||||
@run_idle
|
||||
def on_save(self):
|
||||
if show_dialog(DialogType.QUESTION, self._app.app_window) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
write_satellites((Satellite(*r) for r in self._satellite_view.get_model()),
|
||||
self._settings.profile_data_path + "satellites.xml")
|
||||
|
||||
def on_save_as(self):
|
||||
show_dialog(DialogType.ERROR, transient=self._app.app_window, text="Not implemented yet!")
|
||||
|
||||
@run_idle
|
||||
def on_update(self, item):
|
||||
SatellitesUpdateDialog(self._app.get_active_window(), self._settings, self._satellite_view.get_model()).show()
|
||||
@property
|
||||
def data(self):
|
||||
return self._data
|
||||
|
||||
|
||||
# ***************** Transponder dialog *******************#
|
||||
class TransponderDialog(DVBDialog):
|
||||
""" Base transponder dialog class. """
|
||||
|
||||
class TransponderDialog:
|
||||
""" Shows dialog for adding or edit transponder """
|
||||
|
||||
def __init__(self, transient, transponder: Transponder = None):
|
||||
|
||||
handlers = {"on_entry_changed": self.on_entry_changed}
|
||||
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)
|
||||
self._freq_entry = builder.get_object("freq_entry")
|
||||
self._rate_entry = builder.get_object("rate_entry")
|
||||
self._pol_box = builder.get_object("pol_box")
|
||||
self._fec_box = builder.get_object("fec_box")
|
||||
self._sys_box = builder.get_object("sys_box")
|
||||
self._mod_box = builder.get_object("mod_box")
|
||||
self._pls_mode_box = builder.get_object("pls_mode_box")
|
||||
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(r"\D")
|
||||
# style
|
||||
self._style_provider = Gtk.CssProvider()
|
||||
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
self._freq_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
self._rate_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
if transponder:
|
||||
self.init_transponder(transponder)
|
||||
def __init__(self, parent, title, data=None, *args, **kwargs):
|
||||
super().__init__(parent, title, data, *args, **kwargs)
|
||||
self.frame.set_label(get_message("Transponder properties:"))
|
||||
# Pattern for digits entries.
|
||||
self.digit_pattern = re.compile(r"\D")
|
||||
# Style
|
||||
self.style_provider = Gtk.CssProvider()
|
||||
self.style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
|
||||
def run(self):
|
||||
while self._dialog.run() != Gtk.ResponseType.CANCEL:
|
||||
tr = self.to_transponder()
|
||||
if self.is_accept(tr):
|
||||
return tr
|
||||
show_dialog(DialogType.ERROR, self._dialog, "Please check your parameters and try again.")
|
||||
|
||||
def destroy(self):
|
||||
self._dialog.destroy()
|
||||
|
||||
def init_transponder(self, transponder):
|
||||
self._freq_entry.set_text(transponder.frequency)
|
||||
self._rate_entry.set_text(transponder.symbol_rate)
|
||||
self._pol_box.set_active_id(transponder.polarization)
|
||||
self._fec_box.set_active_id(transponder.fec_inner)
|
||||
self._sys_box.set_active_id(transponder.system)
|
||||
self._mod_box.set_active_id(transponder.modulation)
|
||||
self._pls_mode_box.set_active_id(PLS_MODE.get(transponder.pls_mode, None))
|
||||
self._is_id_entry.set_text(transponder.is_id if transponder.is_id else "")
|
||||
self._pls_code_entry.set_text(transponder.pls_code if transponder.pls_code else "")
|
||||
|
||||
def to_transponder(self):
|
||||
return Transponder(frequency=self._freq_entry.get_text(),
|
||||
symbol_rate=self._rate_entry.get_text(),
|
||||
polarization=self._pol_box.get_active_id(),
|
||||
fec_inner=self._fec_box.get_active_id(),
|
||||
system=self._sys_box.get_active_id(),
|
||||
modulation=self._mod_box.get_active_id(),
|
||||
pls_mode=get_key_by_value(PLS_MODE, self._pls_mode_box.get_active_id()),
|
||||
pls_code=self._pls_code_entry.get_text(),
|
||||
is_id=self._is_id_entry.get_text())
|
||||
|
||||
def on_entry_changed(self, entry):
|
||||
entry.set_name("digit-entry" if self._pattern.search(entry.get_text()) else "GtkEntry")
|
||||
|
||||
def is_accept(self, tr):
|
||||
if self._pattern.search(tr.frequency) or not tr.frequency:
|
||||
return False
|
||||
elif self._pattern.search(tr.symbol_rate) or not tr.symbol_rate:
|
||||
return False
|
||||
elif None in (tr.polarization, tr.fec_inner, tr.system, tr.modulation):
|
||||
return False
|
||||
elif self._pattern.search(tr.pls_code) or self._pattern.search(tr.is_id):
|
||||
return False
|
||||
resp = super().run()
|
||||
while resp == Gtk.ResponseType.OK:
|
||||
if self.is_accept():
|
||||
return resp
|
||||
show_dialog(DialogType.ERROR, self, "Please check your parameters and try again.")
|
||||
resp = super().run()
|
||||
return resp
|
||||
|
||||
def is_accept(self):
|
||||
return True
|
||||
|
||||
def init_transponder_data(self, data):
|
||||
self._data = data
|
||||
|
||||
# ***************** Satellite dialog *******************#
|
||||
def to_transponder(self):
|
||||
return self.data
|
||||
|
||||
class SatelliteDialog:
|
||||
""" Shows dialog for adding or edit satellite """
|
||||
def on_entry_changed(self, entry):
|
||||
""" Digit entries handler. """
|
||||
entry.set_name("digit-entry" if self.digit_pattern.search(entry.get_text()) else "GtkEntry")
|
||||
|
||||
def __init__(self, transient, satellite=None):
|
||||
builder = get_builder(_UI_PATH, use_str=True, objects=("satellite_dialog", "side_store", "pos_adjustment"))
|
||||
def set_style_provider(self, widget):
|
||||
context = widget.get_style_context()
|
||||
context.add_provider_for_screen(Gdk.Screen.get_default(), self.style_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
self._dialog = builder.get_object("satellite_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
|
||||
class TCDialog(DVBDialog):
|
||||
def __init__(self, parent, title=None, data=None, *args, **kwargs):
|
||||
super().__init__(parent, title, data, *args, **kwargs)
|
||||
|
||||
self._entry = Gtk.Entry(margin=5)
|
||||
self.frame.add(self._entry)
|
||||
self.frame.set_label(get_message("Name:"))
|
||||
self.show_all()
|
||||
|
||||
if data:
|
||||
self._entry.set_text(data.name)
|
||||
|
||||
|
||||
class SatelliteDialog(DVBDialog):
|
||||
""" Dialog for adding or edit satellite. """
|
||||
|
||||
def __init__(self, transient, title, satellite=None, *args, **kwargs):
|
||||
super().__init__(transient, title, *args, **kwargs)
|
||||
builder = get_builder(_DIALOGS_UI_PATH, use_str=True,
|
||||
objects=("sat_dialog_box", "side_store", "pos_adjustment"))
|
||||
|
||||
self.frame.add(builder.get_object("sat_dialog_box"))
|
||||
self.frame.set_label(get_message("Satellite properties:"))
|
||||
self._sat_name = builder.get_object("sat_name_entry")
|
||||
self._sat_position = builder.get_object("sat_position_button")
|
||||
self._side = builder.get_object("side_box")
|
||||
self._transponders = satellite.transponders if satellite else []
|
||||
self.show_all()
|
||||
|
||||
if satellite:
|
||||
self._sat_name.set_text(satellite.name)
|
||||
@@ -386,15 +150,10 @@ class SatelliteDialog:
|
||||
self._sat_position.set_value(fabs(pos))
|
||||
self._side.set_active(0 if pos >= 0 else 1) # E or W
|
||||
|
||||
def run(self):
|
||||
if self._dialog.run() == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self.to_satellite()
|
||||
|
||||
def destroy(self):
|
||||
self._dialog.destroy()
|
||||
|
||||
def to_satellite(self):
|
||||
name = self._sat_name.get_text()
|
||||
pos = round(self._sat_position.get_value(), 1)
|
||||
@@ -404,10 +163,230 @@ class SatelliteDialog:
|
||||
return Satellite(name=name, flags="0", position=pos, transponders=self._transponders)
|
||||
|
||||
|
||||
class TerrestrialDialog(TCDialog):
|
||||
""" Dialog for adding or edit terrestrial region. """
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
name = self._entry.get_text()
|
||||
return self._data._replace(name=name) if self._data else Terrestrial(name, "5", None, [])
|
||||
|
||||
|
||||
class CableDialog(TCDialog):
|
||||
""" Dialog for adding or edit cable provider. """
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
name = self._entry.get_text()
|
||||
return self._data._replace(name=name) if self._data else Cable(name, "true", "9", None, [])
|
||||
|
||||
|
||||
class SatTransponderDialog(TransponderDialog):
|
||||
""" Dialog for adding or edit satellite transponder. """
|
||||
|
||||
def __init__(self, transient, title, data=None, *args, **kwargs):
|
||||
super().__init__(transient, title, data, *args, **kwargs)
|
||||
|
||||
handlers = {"on_entry_changed": self.on_entry_changed}
|
||||
objects = ("sat_tr_box", "pol_store", "fec_store", "mod_store", "system_store", "pls_mode_store")
|
||||
builder = get_builder(_DIALOGS_UI_PATH, handlers, use_str=True, objects=objects)
|
||||
|
||||
self.frame.add(builder.get_object("sat_tr_box"))
|
||||
self._freq_entry = builder.get_object("freq_entry")
|
||||
self._rate_entry = builder.get_object("rate_entry")
|
||||
self._pol_box = builder.get_object("pol_box")
|
||||
self._fec_box = builder.get_object("fec_box")
|
||||
self._sys_box = builder.get_object("sys_box")
|
||||
self._mod_box = builder.get_object("mod_box")
|
||||
self._pls_mode_box = builder.get_object("pls_mode_box")
|
||||
self._pls_code_entry = builder.get_object("pls_code_entry")
|
||||
self._is_id_entry = builder.get_object("is_id_entry")
|
||||
self._t2mi_plp_id_entry = builder.get_object("t2mi_plp_id_entry")
|
||||
|
||||
self.set_style_provider(self._freq_entry)
|
||||
self.set_style_provider(self._rate_entry)
|
||||
self.show_all()
|
||||
|
||||
self.init_transponder_data(data)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self.to_transponder()
|
||||
|
||||
def init_transponder_data(self, transponder):
|
||||
if transponder:
|
||||
self._freq_entry.set_text(transponder.frequency)
|
||||
self._rate_entry.set_text(transponder.symbol_rate)
|
||||
self._pol_box.set_active_id(POLARIZATION.get(transponder.polarization, None))
|
||||
self._fec_box.set_active_id(FEC.get(transponder.fec_inner, None))
|
||||
self._sys_box.set_active_id(SYSTEM.get(transponder.system, None))
|
||||
self._mod_box.set_active_id(MODULATION.get(transponder.modulation, None))
|
||||
self._pls_mode_box.set_active_id(PLS_MODE.get(transponder.pls_mode, None))
|
||||
self._is_id_entry.set_text(transponder.is_id if transponder.is_id else "")
|
||||
self._pls_code_entry.set_text(transponder.pls_code if transponder.pls_code else "")
|
||||
self._t2mi_plp_id_entry.set_text(transponder.t2mi_plp_id if transponder.t2mi_plp_id else "")
|
||||
|
||||
def to_transponder(self):
|
||||
return Transponder(frequency=self._freq_entry.get_text(),
|
||||
symbol_rate=self._rate_entry.get_text(),
|
||||
polarization=get_key_by_value(POLARIZATION, self._pol_box.get_active_id()),
|
||||
fec_inner=get_key_by_value(FEC, self._fec_box.get_active_id()),
|
||||
system=get_key_by_value(SYSTEM, self._sys_box.get_active_id()),
|
||||
modulation=get_key_by_value(MODULATION, self._mod_box.get_active_id()),
|
||||
pls_mode=get_key_by_value(PLS_MODE, self._pls_mode_box.get_active_id()),
|
||||
pls_code=self._pls_code_entry.get_text(),
|
||||
is_id=self._is_id_entry.get_text(),
|
||||
t2mi_plp_id=self._t2mi_plp_id_entry.get_text())
|
||||
|
||||
def is_accept(self):
|
||||
tr = self.to_transponder()
|
||||
if self.digit_pattern.search(tr.frequency) or not tr.frequency:
|
||||
return False
|
||||
elif self.digit_pattern.search(tr.symbol_rate) or not tr.symbol_rate:
|
||||
return False
|
||||
elif None in (tr.polarization, tr.fec_inner, tr.system, tr.modulation):
|
||||
return False
|
||||
elif self.digit_pattern.search(tr.pls_code) or self.digit_pattern.search(tr.is_id):
|
||||
return False
|
||||
elif self.digit_pattern.search(tr.t2mi_plp_id):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class TerTransponderDialog(TransponderDialog):
|
||||
""" Dialog for adding or edit terrestrial transponder. """
|
||||
|
||||
def __init__(self, transient, title, data=None, *args, **kwargs):
|
||||
super().__init__(transient, title, data, *args, **kwargs)
|
||||
|
||||
handlers = {"on_entry_changed": self.on_entry_changed}
|
||||
builder = get_builder(_DIALOGS_UI_PATH, handlers, use_str=True, objects=("ter_tr_box",))
|
||||
|
||||
self.frame.add(builder.get_object("ter_tr_box"))
|
||||
self._freq_entry = builder.get_object("ter_freq_entry")
|
||||
self._sys_box = builder.get_object("ter_sys_box")
|
||||
self._bandwidth_box = builder.get_object("ter_bandwidth_box")
|
||||
self._constellation_box = builder.get_object("ter_constellation_box")
|
||||
self._sr_hp_box = builder.get_object("ter_sr_hp_box")
|
||||
self._sr_lp_box = builder.get_object("ter_sr_lp_box")
|
||||
self._guard_box = builder.get_object("ter_guard_box")
|
||||
self._transmission_box = builder.get_object("ter_transmission_box")
|
||||
self._hierarchy_box = builder.get_object("ter_hierarchy_box")
|
||||
self._inversion_box = builder.get_object("ter_inversion_box")
|
||||
self._plp_id_entry = builder.get_object("ter_plp_id_entry")
|
||||
|
||||
self.set_style_provider(self._freq_entry)
|
||||
self.set_style_provider(self._plp_id_entry)
|
||||
self.show_all()
|
||||
|
||||
self.init_transponder_data(data)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self.to_transponder()
|
||||
|
||||
def init_transponder_data(self, transponder):
|
||||
[self._sys_box.append(k, v) for k, v in T_SYSTEM.items()]
|
||||
[self._bandwidth_box.append(k, v) for k, v in BANDWIDTH.items()]
|
||||
[self._constellation_box.append(k, v) for k, v in CONSTELLATION.items()]
|
||||
[self._sr_hp_box.append(k, v) for k, v in T_FEC.items()]
|
||||
[self._sr_lp_box.append(k, v) for k, v in T_FEC.items()]
|
||||
[self._guard_box.append(k, v) for k, v in GUARD_INTERVAL.items()]
|
||||
[self._transmission_box.append(k, v) for k, v in TRANSMISSION_MODE.items()]
|
||||
[self._hierarchy_box.append(k, v) for k, v in HIERARCHY.items()]
|
||||
[self._inversion_box.append(k.value, k.name) for k in Inversion]
|
||||
|
||||
if transponder:
|
||||
self._freq_entry.set_text(transponder.centre_frequency)
|
||||
self._sys_box.set_active_id(transponder.system)
|
||||
self._bandwidth_box.set_active_id(transponder.bandwidth)
|
||||
self._constellation_box.set_active_id(transponder.constellation)
|
||||
self._sr_hp_box.set_active_id(transponder.code_rate_hp)
|
||||
self._sr_lp_box.set_active_id(transponder.code_rate_lp)
|
||||
self._guard_box.set_active_id(transponder.guard_interval)
|
||||
self._transmission_box.set_active_id(transponder.transmission_mode)
|
||||
self._hierarchy_box.set_active_id(transponder.hierarchy_information)
|
||||
self._inversion_box.set_active_id(transponder.inversion)
|
||||
self._plp_id_entry.set_text(transponder.plp_id or "")
|
||||
|
||||
def is_accept(self):
|
||||
tr = self.to_transponder()
|
||||
if not tr.centre_frequency or self.digit_pattern.search(tr.centre_frequency):
|
||||
return False
|
||||
elif tr.plp_id and self.digit_pattern.search(tr.plp_id):
|
||||
return False
|
||||
return True
|
||||
|
||||
def to_transponder(self):
|
||||
return TerTransponder(centre_frequency=self._freq_entry.get_text(),
|
||||
system=self._sys_box.get_active_id(),
|
||||
bandwidth=self._bandwidth_box.get_active_id(),
|
||||
constellation=self._constellation_box.get_active_id(),
|
||||
code_rate_hp=self._sr_hp_box.get_active_id(),
|
||||
code_rate_lp=self._sr_lp_box.get_active_id(),
|
||||
guard_interval=self._guard_box.get_active_id(),
|
||||
transmission_mode=self._transmission_box.get_active_id(),
|
||||
hierarchy_information=self._hierarchy_box.get_active_id(),
|
||||
inversion=self._inversion_box.get_active_id(),
|
||||
plp_id=self._plp_id_entry.get_text() or None)
|
||||
|
||||
|
||||
class CableTransponderDialog(TransponderDialog):
|
||||
""" Dialog for adding or edit cable transponder. """
|
||||
|
||||
def __init__(self, transient, title, data=None, *args, **kwargs):
|
||||
super().__init__(transient, title, data, *args, **kwargs)
|
||||
|
||||
handlers = {"on_entry_changed": self.on_entry_changed}
|
||||
builder = get_builder(_DIALOGS_UI_PATH, handlers, use_str=True, objects=("cable_tr_box",))
|
||||
|
||||
self.frame.add(builder.get_object("cable_tr_box"))
|
||||
|
||||
self._freq_entry = builder.get_object("cable_freq_entry")
|
||||
self._rate_entry = builder.get_object("cable_rate_entry")
|
||||
self._fec_box = builder.get_object("cable_fec_box")
|
||||
self._mod_box = builder.get_object("cable_mod_box")
|
||||
|
||||
self.set_style_provider(self._freq_entry)
|
||||
self.set_style_provider(self._rate_entry)
|
||||
self.show_all()
|
||||
|
||||
self.init_transponder_data(data)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self.to_transponder()
|
||||
|
||||
def init_transponder_data(self, transponder):
|
||||
[self._fec_box.append(k, v) for k, v in FEC_DEFAULT.items()]
|
||||
[self._mod_box.append(k, v) for k, v in C_MODULATION.items()]
|
||||
|
||||
if transponder:
|
||||
self._freq_entry.set_text(transponder.frequency)
|
||||
self._rate_entry.set_text(transponder.symbol_rate)
|
||||
self._fec_box.set_active_id(transponder.fec_inner)
|
||||
self._mod_box.set_active_id(transponder.modulation)
|
||||
|
||||
def is_accept(self):
|
||||
tr = self.to_transponder()
|
||||
if not tr.frequency or self.digit_pattern.search(tr.frequency):
|
||||
return False
|
||||
elif not tr.symbol_rate or self.digit_pattern.search(tr.symbol_rate):
|
||||
return False
|
||||
return True
|
||||
|
||||
def to_transponder(self):
|
||||
return CableTransponder(frequency=self._freq_entry.get_text(),
|
||||
symbol_rate=self._rate_entry.get_text(),
|
||||
fec_inner=self._fec_box.get_active_id(),
|
||||
modulation=self._mod_box.get_active_id())
|
||||
|
||||
|
||||
# ********************** Update dialogs ************************ #
|
||||
|
||||
class UpdateDialog:
|
||||
""" Base dialog for update satellites, transponders and services from the web."""
|
||||
""" Base dialog for update satellites, transponders and services from the Web."""
|
||||
|
||||
def __init__(self, transient, settings, title=None):
|
||||
handlers = {"on_update_satellites_list": self.on_update_satellites_list,
|
||||
@@ -430,19 +409,14 @@ class UpdateDialog:
|
||||
self._parser = None
|
||||
self._size_name = f"{'_'.join(re.findall('[A-Z][^A-Z]*', self.__class__.__name__))}_window_size".lower()
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "satellites.glade", handlers,
|
||||
objects=("satellites_update_window", "update_source_store", "update_sat_list_store",
|
||||
"update_sat_list_model_filter", "update_sat_list_model_sort", "side_store",
|
||||
"pos_adjustment", "pos_adjustment2", "satellites_update_popup_menu",
|
||||
"remove_selection_image", "sat_update_cancel_image", "sat_receive_image",
|
||||
"sat_update_image", "update_transponder_store", "update_service_store"))
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}xml{os.sep}update.glade", handlers)
|
||||
|
||||
self._window = builder.get_object("satellites_update_window")
|
||||
self._window.set_transient_for(transient)
|
||||
if title:
|
||||
self._window.set_title(title)
|
||||
|
||||
self._transponder_frame = builder.get_object("sat_update_tr_frame")
|
||||
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")
|
||||
@@ -451,6 +425,9 @@ class UpdateDialog:
|
||||
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._satellites_count_label = builder.get_object("satellites_count_label")
|
||||
self._transponders_count_label = builder.get_object("transponders_count_label")
|
||||
self._services_count_label = builder.get_object("services_count_label")
|
||||
self._receive_button.bind_property("visible", builder.get_object("cancel_data_button"), "visible", 4)
|
||||
update_button = builder.get_object("sat_update_button")
|
||||
self._sat_view.bind_property("sensitive", update_button, "sensitive")
|
||||
@@ -502,9 +479,7 @@ class UpdateDialog:
|
||||
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
|
||||
return
|
||||
|
||||
get_base_model(self._sat_view.get_model()).clear()
|
||||
self._transponder_view.get_model().clear()
|
||||
self._service_view.get_model().clear()
|
||||
self.clear_data()
|
||||
|
||||
self.is_download = True
|
||||
self._sat_view.set_sensitive(False)
|
||||
@@ -514,17 +489,24 @@ class UpdateDialog:
|
||||
|
||||
self.get_sat_list(src, self.append_satellites)
|
||||
|
||||
def clear_data(self):
|
||||
get_base_model(self._sat_view.get_model()).clear()
|
||||
self._transponder_view.get_model().clear()
|
||||
self._service_view.get_model().clear()
|
||||
self._satellites_count_label.set_text("0")
|
||||
self._transponders_count_label.set_text("0")
|
||||
self._services_count_label.set_text("0")
|
||||
|
||||
@run_task
|
||||
def get_sat_list(self, src, callback):
|
||||
sat_src = SatelliteSource.FLYSAT
|
||||
sat_src = SatelliteSource.LYNGSAT
|
||||
if src == 1:
|
||||
sat_src = SatelliteSource.LYNGSAT
|
||||
elif src == 2:
|
||||
sat_src = SatelliteSource.KINGOFSAT
|
||||
elif src == 2:
|
||||
sat_src = SatelliteSource.FLYSAT
|
||||
|
||||
sats = self._parser.get_satellites_list(sat_src)
|
||||
if sats:
|
||||
callback(sats)
|
||||
callback(sats)
|
||||
self.is_download = False
|
||||
|
||||
@run_idle
|
||||
@@ -534,6 +516,7 @@ class UpdateDialog:
|
||||
model.append(sat)
|
||||
|
||||
self._sat_view.set_sensitive(True)
|
||||
self._satellites_count_label.set_text(str(len(model)))
|
||||
|
||||
@run_idle
|
||||
def on_receive_data(self, item):
|
||||
@@ -634,7 +617,7 @@ class UpdateDialog:
|
||||
|
||||
|
||||
class SatellitesUpdateDialog(UpdateDialog):
|
||||
""" Dialog for update satellites from the web. """
|
||||
""" Dialog for update satellites from the Web. """
|
||||
|
||||
def __init__(self, transient, settings, main_model):
|
||||
super().__init__(transient=transient, settings=settings)
|
||||
@@ -678,10 +661,10 @@ class SatellitesUpdateDialog(UpdateDialog):
|
||||
appender.send("-" * 75 + "\n")
|
||||
sat_count = len(sats)
|
||||
|
||||
sats = {s[2]: s for s in sats} # key = position, v = satellite
|
||||
sats = {s[0]: s for s in sats} # key = name, v = satellite
|
||||
|
||||
for row in self._main_model:
|
||||
pos = row[2]
|
||||
pos = row[0]
|
||||
if pos in sats:
|
||||
sat = sats.pop(pos)
|
||||
appender.send(f"Updating satellite: {row[0]}\n")
|
||||
@@ -702,7 +685,7 @@ class SatellitesUpdateDialog(UpdateDialog):
|
||||
|
||||
|
||||
class ServicesUpdateDialog(UpdateDialog):
|
||||
""" Dialog for updating services from the web. """
|
||||
""" Dialog for updating services from the Web. """
|
||||
|
||||
def __init__(self, transient, settings, callback):
|
||||
super().__init__(transient=transient, settings=settings, title="Services update")
|
||||
@@ -729,10 +712,8 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
self._transponder_view.connect("button-press-event", lambda w, e: on_popup_menu(tr_popup_menu, e))
|
||||
self._transponder_view.connect("select_all", lambda w: self.update_transponder_selection(True))
|
||||
|
||||
self._transponder_frame.set_visible(True)
|
||||
self._source_box.remove(0)
|
||||
self._transponder_paned.set_visible(True)
|
||||
self._source_box.connect("changed", self.on_update_satellites_list)
|
||||
self._source_box.set_active(0)
|
||||
|
||||
@run_idle
|
||||
def on_receive_data(self, item):
|
||||
@@ -754,7 +735,7 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
non_cached_sats = []
|
||||
sat_names = {}
|
||||
t_names = {}
|
||||
t_urls = []
|
||||
t_urls = set()
|
||||
services = []
|
||||
|
||||
for r in (r for r in model if r[-1]):
|
||||
@@ -766,7 +747,7 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
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_urls.add(t.url)
|
||||
t_names[t.url] = t.text
|
||||
else:
|
||||
non_cached_sats.append(url)
|
||||
@@ -783,7 +764,7 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
|
||||
appender.send(f"Getting transponders for: {sat_names.get(futures[future])}.\n")
|
||||
for t in future.result():
|
||||
t_urls.append(t.url)
|
||||
t_urls.add(t.url)
|
||||
t_names[t.url] = t.text
|
||||
|
||||
appender.send("-" * 75 + "\n")
|
||||
@@ -804,14 +785,17 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
return
|
||||
|
||||
appender.send(f"Getting services for: {t_names.get(futures[future], '')}.\n")
|
||||
list(map(services.append, future.result()))
|
||||
try:
|
||||
list(map(services.append, future.result()))
|
||||
except ValueError as e:
|
||||
log(f"Getting services error: {e} [{t_names.get(futures[future])}]")
|
||||
|
||||
appender.send("-" * 75 + "\n")
|
||||
appender.send(f"Consumed: {time.time() - start:0.0f}s, {len(services)} services received.")
|
||||
|
||||
try:
|
||||
from app.eparser.enigma.lamedb import LameDbReader
|
||||
# Used for double checking!
|
||||
# Used for double check!
|
||||
reader = LameDbReader(path=None)
|
||||
srvs = reader.get_services_list("".join(reader.get_services_lines(services)))
|
||||
except ValueError as e:
|
||||
@@ -826,11 +810,10 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
sat_src = SatelliteSource.LYNGSAT
|
||||
if src == 1:
|
||||
sat_src = SatelliteSource.KINGOFSAT
|
||||
self._services_parser.source = sat_src
|
||||
|
||||
self._services_parser.source = sat_src
|
||||
sats = self._parser.get_satellites_list(sat_src)
|
||||
if sats:
|
||||
callback(sats)
|
||||
callback(sats)
|
||||
self.is_download = False
|
||||
|
||||
def on_satellite_toggled(self, toggle, path):
|
||||
@@ -896,6 +879,7 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
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)
|
||||
self._transponders_count_label.set_text(str(len(model)))
|
||||
|
||||
@run_task
|
||||
def on_activate_transponder(self, view, path, column):
|
||||
@@ -915,6 +899,7 @@ class ServicesUpdateDialog(UpdateDialog):
|
||||
model.append((None, s.service, s.package, s.service_type, str(s.ssid), None))
|
||||
|
||||
self._transponder_view.set_sensitive(True)
|
||||
self._services_count_label.set_text(str(len(model)))
|
||||
|
||||
def update_transponder_selection(self, select):
|
||||
m = self._transponder_view.get_model()
|
||||
585
app/ui/xml/edit.py
Normal file
585
app/ui/xml/edit.py
Normal file
@@ -0,0 +1,585 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 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 enum import Enum
|
||||
from pyexpat import ExpatError
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import run_idle
|
||||
from app.connections import DownloadType
|
||||
from app.eparser import get_satellites, write_satellites, Satellite, Transponder
|
||||
from app.eparser.ecommons import (POLARIZATION, FEC, SYSTEM, MODULATION, T_SYSTEM, BANDWIDTH, CONSTELLATION, T_FEC,
|
||||
GUARD_INTERVAL, TRANSMISSION_MODE, HIERARCHY, Inversion, FEC_DEFAULT, C_MODULATION,
|
||||
Terrestrial, Cable, CableTransponder, TerTransponder)
|
||||
from app.eparser.satxml import get_terrestrial, get_cable, write_terrestrial, write_cable
|
||||
from .dialogs import SatelliteDialog, SatellitesUpdateDialog, TerrestrialDialog, CableDialog, SatTransponderDialog, \
|
||||
CableTransponderDialog, TerTransponderDialog
|
||||
from ..dialogs import show_dialog, DialogType, get_chooser_dialog, get_message, get_builder
|
||||
from ..main_helper import move_items, on_popup_menu, scroll_to
|
||||
from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, MOVE_KEYS, KeyboardKey, MOD_MASK, Page
|
||||
|
||||
|
||||
class SatellitesTool(Gtk.Box):
|
||||
""" Class to processing *.xml data. """
|
||||
|
||||
class DVB(str, Enum):
|
||||
SAT = "satellites"
|
||||
TERRESTRIAL = "terrestrial"
|
||||
CABLE = "cable"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
def __init__(self, app, settings, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._app = app
|
||||
self._app.connect("data-save", self.on_save)
|
||||
self._app.connect("data-save-as", self.on_save_as)
|
||||
self._app.connect("data-receive", self.on_download)
|
||||
self._app.connect("data-send", self.on_upload)
|
||||
|
||||
self._settings = settings
|
||||
self._current_sat_path = None
|
||||
self._current_ter_path = None
|
||||
self._current_cable_path = None
|
||||
self._dvb_type = self.DVB.SAT
|
||||
|
||||
handlers = {"on_satellite_view_realize": self.on_satellite_view_realize,
|
||||
"on_terrestrial_view_realize": self.on_terrestrial_view_realize,
|
||||
"on_cable_view_realize": self.on_cable_view_realize,
|
||||
"on_update": self.on_update,
|
||||
"on_up": self.on_up,
|
||||
"on_down": self.on_down,
|
||||
"on_button_press": self.on_button_press,
|
||||
"on_tr_button_press": self.on_tr_button_press,
|
||||
"on_add": self.on_add,
|
||||
"on_edit": self.on_edit,
|
||||
"on_remove": self.on_remove,
|
||||
"on_transponder_add": self.on_transponder_add,
|
||||
"on_transponder_edit": self.on_transponder_edit,
|
||||
"on_transponder_remove": self.on_transponder_remove,
|
||||
"on_key_press": self.on_key_press,
|
||||
"on_tr_key_press": self.on_tr_key_press,
|
||||
"on_visible_page": self.on_visible_page,
|
||||
"on_satellite_selection": self.on_satellite_selection,
|
||||
"on_terrestrial_selection": self.on_terrestrial_selection,
|
||||
"on_cable_selection": self.on_cable_selection,
|
||||
"on_sat_model_changed": self.on_sat_model_changed,
|
||||
"on_sat_tr_model_changed": self.on_sat_tr_model_changed,
|
||||
"on_ter_model_changed": self.on_ter_model_changed,
|
||||
"on_ter_tr_model_changed": self.on_ter_tr_model_changed,
|
||||
"on_cable_model_changed": self.on_cable_model_changed,
|
||||
"on_cable_tr_model_changed": self.on_cable_tr_model_changed}
|
||||
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}xml/editor.glade", handlers)
|
||||
|
||||
self._satellite_view = builder.get_object("satellite_view")
|
||||
self._terrestrial_view = builder.get_object("terrestrial_view")
|
||||
self._cable_view = builder.get_object("cable_view")
|
||||
self._sat_tr_view = builder.get_object("sat_tr_view")
|
||||
self._ter_tr_view = builder.get_object("ter_tr_view")
|
||||
self._cable_tr_view = builder.get_object("cable_tr_view")
|
||||
|
||||
self._sat_count_label = builder.get_object("sat_count_label")
|
||||
self._sat_tr_count_label = builder.get_object("sat_tr_count_label")
|
||||
self._ter_count_label = builder.get_object("ter_count_label")
|
||||
self._ter_tr_count_label = builder.get_object("ter_tr_count_label")
|
||||
self._cable_count_label = builder.get_object("cable_count_label")
|
||||
self._cable_tr_count_label = builder.get_object("cable_tr_count_label")
|
||||
|
||||
self._transponders_stack = builder.get_object("transponders_stack")
|
||||
self._add_header_button = builder.get_object("add_header_button")
|
||||
self._update_header_button = builder.get_object("update_header_button")
|
||||
self.pack_start(builder.get_object("main_paned"), True, True, 0)
|
||||
self._app.connect("profile-changed", self.on_profile_changed)
|
||||
# Custom renderers.
|
||||
renderer = builder.get_object("sat_pos_renderer")
|
||||
builder.get_object("sat_pos_column").set_cell_data_func(renderer, self.sat_pos_func)
|
||||
# Satellite.
|
||||
renderer = builder.get_object("sat_pol_renderer")
|
||||
builder.get_object("pol_column").set_cell_data_func(renderer, self.sat_pol_func)
|
||||
renderer = builder.get_object("sat_fec_renderer")
|
||||
builder.get_object("fec_column").set_cell_data_func(renderer, self.sat_fec_func)
|
||||
renderer = builder.get_object("sat_sys_renderer")
|
||||
builder.get_object("sys_column").set_cell_data_func(renderer, self.sat_sys_func)
|
||||
renderer = builder.get_object("sat_mod_renderer")
|
||||
builder.get_object("mod_column").set_cell_data_func(renderer, self.sat_mod_func)
|
||||
# Terrestrial.
|
||||
renderer = builder.get_object("ter_system_renderer")
|
||||
builder.get_object("ter_system_column").set_cell_data_func(renderer, self.ter_sys_func)
|
||||
renderer = builder.get_object("ter_bandwidth_renderer")
|
||||
builder.get_object("ter_bandwidth_column").set_cell_data_func(renderer, self.ter_bandwidth_func)
|
||||
renderer = builder.get_object("ter_constellation_renderer")
|
||||
builder.get_object("ter_constellation_column").set_cell_data_func(renderer, self.ter_constellation_func)
|
||||
renderer = builder.get_object("ter_rate_hp_renderer")
|
||||
builder.get_object("ter_rate_hp_column").set_cell_data_func(renderer, self.ter_fec_hp_func)
|
||||
renderer = builder.get_object("ter_rate_lp_renderer")
|
||||
builder.get_object("ter_rate_lp_column").set_cell_data_func(renderer, self.ter_fec_lp_func)
|
||||
renderer = builder.get_object("ter_guard_renderer")
|
||||
builder.get_object("ter_guard_column").set_cell_data_func(renderer, self.ter_guard_func)
|
||||
renderer = builder.get_object("ter_tr_mode_renderer")
|
||||
builder.get_object("ter_tr_mode_column").set_cell_data_func(renderer, self.ter_transmission_func)
|
||||
renderer = builder.get_object("ter_hierarchy_renderer")
|
||||
builder.get_object("ter_hierarchy_column").set_cell_data_func(renderer, self.ter_hierarchy_func)
|
||||
renderer = builder.get_object("ter_inversion_renderer")
|
||||
builder.get_object("ter_inversion_column").set_cell_data_func(renderer, self.ter_inversion_func)
|
||||
# Cable.
|
||||
renderer = builder.get_object("cable_fec_renderer")
|
||||
builder.get_object("cable_fec_column").set_cell_data_func(renderer, self.cable_fec_func)
|
||||
renderer = builder.get_object("cable_mod_renderer")
|
||||
builder.get_object("cable_mod_column").set_cell_data_func(renderer, self.cable_mod_func)
|
||||
|
||||
self.show()
|
||||
|
||||
# ******************** Custom renderers ******************** #
|
||||
|
||||
def sat_pos_func(self, column, renderer, model, itr, data):
|
||||
""" Converts and sets the satellite position value to a readable format. """
|
||||
pos = int(model.get_value(itr, 2))
|
||||
renderer.set_property("text", f"{abs(pos / 10):0.1f}{'W' if pos < 0 else 'E'}")
|
||||
|
||||
def sat_pol_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", POLARIZATION.get(model.get_value(itr, 2), None))
|
||||
|
||||
def sat_fec_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", FEC.get(model.get_value(itr, 3), None))
|
||||
|
||||
def sat_sys_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", SYSTEM.get(model.get_value(itr, 4), None))
|
||||
|
||||
def sat_mod_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", MODULATION.get(model.get_value(itr, 5), None))
|
||||
|
||||
def ter_sys_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", T_SYSTEM.get(model.get_value(itr, 1), None))
|
||||
|
||||
def ter_bandwidth_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", BANDWIDTH.get(model.get_value(itr, 2), None))
|
||||
|
||||
def ter_constellation_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", CONSTELLATION.get(model.get_value(itr, 3), None))
|
||||
|
||||
def ter_fec_hp_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", T_FEC.get(model.get_value(itr, 4), None))
|
||||
|
||||
def ter_fec_lp_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", T_FEC.get(model.get_value(itr, 5), None))
|
||||
|
||||
def ter_guard_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", GUARD_INTERVAL.get(model.get_value(itr, 6), None))
|
||||
|
||||
def ter_transmission_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", TRANSMISSION_MODE.get(model.get_value(itr, 7), None))
|
||||
|
||||
def ter_hierarchy_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", HIERARCHY.get(model.get_value(itr, 8), None))
|
||||
|
||||
def ter_inversion_func(self, column, renderer, model, itr, data):
|
||||
value = model.get_value(itr, 9)
|
||||
if value:
|
||||
value = Inversion(value).name
|
||||
renderer.set_property("text", value)
|
||||
|
||||
def cable_fec_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", FEC_DEFAULT.get(model.get_value(itr, 2), None))
|
||||
|
||||
def cable_mod_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", C_MODULATION.get(model.get_value(itr, 3), None))
|
||||
|
||||
def on_satellite_view_realize(self, view):
|
||||
self.load_satellites_list()
|
||||
|
||||
def on_terrestrial_view_realize(self, view):
|
||||
self.load_terrestrial_list()
|
||||
|
||||
def on_cable_view_realize(self, view):
|
||||
self.load_cable_list()
|
||||
|
||||
def load_satellites_list(self, path=None):
|
||||
gen = self.on_satellites_list_load(path)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def load_terrestrial_list(self, path=None):
|
||||
gen = self.on_terrestrial_list_load(path)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def load_cable_list(self, path=None):
|
||||
gen = self.on_cable_list_load(path)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def on_visible_page(self, stack, param):
|
||||
self._dvb_type = self.DVB(stack.get_visible_child_name())
|
||||
self._transponders_stack.set_visible_child_name(self._dvb_type)
|
||||
self._update_header_button.set_sensitive(self._dvb_type is self.DVB.SAT)
|
||||
|
||||
if self._dvb_type is self.DVB.SAT:
|
||||
self._app.on_info_bar_close()
|
||||
|
||||
else:
|
||||
self._app.show_info_message("EXPERIMENTAL!", Gtk.MessageType.WARNING)
|
||||
|
||||
def on_satellite_selection(self, view):
|
||||
model = self._sat_tr_view.get_model()
|
||||
model.clear()
|
||||
|
||||
self._current_sat_path, column = view.get_cursor()
|
||||
if self._current_sat_path:
|
||||
sat_model = view.get_model()
|
||||
list(map(model.append, sat_model[self._current_sat_path][-1]))
|
||||
|
||||
def on_terrestrial_selection(self, view):
|
||||
model = self._ter_tr_view.get_model()
|
||||
model.clear()
|
||||
|
||||
self._current_ter_path, column = view.get_cursor()
|
||||
if self._current_ter_path:
|
||||
ter_model = view.get_model()
|
||||
list(map(model.append, ter_model[self._current_ter_path][-1]))
|
||||
|
||||
def on_cable_selection(self, view):
|
||||
model = self._cable_tr_view.get_model()
|
||||
model.clear()
|
||||
|
||||
self._current_cable_path, column = view.get_cursor()
|
||||
if self._current_cable_path:
|
||||
cable_model = view.get_model()
|
||||
list(map(model.append, cable_model[self._current_cable_path][-1]))
|
||||
|
||||
def on_sat_model_changed(self, model, path, itr=None):
|
||||
self._sat_count_label.set_text(str(len(model)))
|
||||
|
||||
def on_sat_tr_model_changed(self, model, path, itr=None):
|
||||
self._sat_tr_count_label.set_text(str(len(model)))
|
||||
|
||||
def on_ter_model_changed(self, model, path, itr=None):
|
||||
self._ter_count_label.set_text(str(len(model)))
|
||||
|
||||
def on_ter_tr_model_changed(self, model, path, itr=None):
|
||||
self._ter_tr_count_label.set_text(str(len(model)))
|
||||
|
||||
def on_cable_model_changed(self, model, path, itr=None):
|
||||
self._cable_count_label.set_text(str(len(model)))
|
||||
|
||||
def on_cable_tr_model_changed(self, model, path, itr=None):
|
||||
self._cable_tr_count_label.set_text(str(len(model)))
|
||||
|
||||
def on_up(self, item):
|
||||
move_items(KeyboardKey.UP, self._satellite_view)
|
||||
|
||||
def on_down(self, item):
|
||||
move_items(KeyboardKey.DOWN, self._satellite_view)
|
||||
|
||||
def on_button_press(self, menu, event):
|
||||
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS:
|
||||
self.on_edit()
|
||||
else:
|
||||
on_popup_menu(menu, event)
|
||||
|
||||
def on_tr_button_press(self, menu, event):
|
||||
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS:
|
||||
self.on_transponder_edit()
|
||||
else:
|
||||
on_popup_menu(menu, event)
|
||||
|
||||
def on_key_press(self, view, event):
|
||||
""" Handling keystrokes. """
|
||||
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.DELETE:
|
||||
self.on_remove(view)
|
||||
elif key is KeyboardKey.INSERT:
|
||||
self.on_edit(force=True)
|
||||
elif ctrl and key is KeyboardKey.E:
|
||||
self.on_edit()
|
||||
elif ctrl and key in MOVE_KEYS:
|
||||
move_items(key, view)
|
||||
elif key is KeyboardKey.LEFT or key is KeyboardKey.RIGHT:
|
||||
view.do_unselect_all(view)
|
||||
|
||||
def on_tr_key_press(self, view, event):
|
||||
""" Handling transponder view keystrokes. """
|
||||
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.DELETE:
|
||||
self.on_transponder_remove()
|
||||
elif key is KeyboardKey.INSERT:
|
||||
self.on_transponder_edit(force=True)
|
||||
elif ctrl and key is KeyboardKey.E:
|
||||
self.on_transponder_edit()
|
||||
elif ctrl and key in MOVE_KEYS:
|
||||
move_items(key, view)
|
||||
elif key is KeyboardKey.LEFT or key is KeyboardKey.RIGHT:
|
||||
view.do_unselect_all(view)
|
||||
|
||||
def on_satellites_list_load(self, path=None):
|
||||
""" Load satellites data into model """
|
||||
path = path or f"{self._settings.profile_data_path}satellites.xml"
|
||||
yield from self.load_data(self._satellite_view, get_satellites, path)
|
||||
|
||||
def on_terrestrial_list_load(self, path=None):
|
||||
path = path or f"{self._settings.profile_data_path}terrestrial.xml"
|
||||
yield from self.load_data(self._terrestrial_view, get_terrestrial, path)
|
||||
|
||||
def on_cable_list_load(self, path=None):
|
||||
path = path or f"{self._settings.profile_data_path}cables.xml"
|
||||
yield from self.load_data(self._cable_view, get_cable, path)
|
||||
|
||||
def load_data(self, view, func, path):
|
||||
model = view.get_model()
|
||||
model.clear()
|
||||
|
||||
try:
|
||||
data = func(path)
|
||||
yield True
|
||||
except FileNotFoundError as e:
|
||||
msg = get_message("Please, download files from receiver or setup your path for read data!")
|
||||
self._app.show_error_message(f"{e}\n{msg}")
|
||||
except ExpatError as e:
|
||||
msg = f"The file [{path}] is not formatted correctly or contains invalid characters! Cause: {e}"
|
||||
self._app.show_error_message(msg)
|
||||
else:
|
||||
for d in data:
|
||||
yield model.append(d)
|
||||
|
||||
def on_add(self, item):
|
||||
""" Common adding. """
|
||||
self.on_edit(item, force=True)
|
||||
|
||||
def on_transponder_add(self, item):
|
||||
self.on_transponder_edit(force=True)
|
||||
|
||||
def on_edit(self, item=None, force=False):
|
||||
self.on_data_edit(self.get_active_dvb_view(), force)
|
||||
|
||||
def on_transponder_edit(self, item=None, force=False):
|
||||
self.on_data_edit(self.get_active_transponder_view(), force)
|
||||
|
||||
def on_data_edit(self, view, force=False):
|
||||
""" Common edit. """
|
||||
if force:
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
else:
|
||||
paths = self.check_selection(view, "Please, select only one item!")
|
||||
if not paths:
|
||||
return
|
||||
|
||||
model = view.get_model()
|
||||
row = model[paths][:] if paths else None
|
||||
itr = model.get_iter(paths) if paths else None
|
||||
|
||||
if view is self._satellite_view:
|
||||
self.on_dvb_data_edit(SatelliteDialog, "Satellite", view, None if force else Satellite(*row), itr)
|
||||
elif view is self._terrestrial_view:
|
||||
self.on_dvb_data_edit(TerrestrialDialog, "Region", view, None if force else Terrestrial(*row), itr)
|
||||
elif view is self._cable_view:
|
||||
self.on_dvb_data_edit(CableDialog, "Provider", view, None if force else Cable(*row), itr)
|
||||
elif view is self._sat_tr_view:
|
||||
data = None if force else Transponder(*row)
|
||||
self.on_transponder_data_edit(SatTransponderDialog, "Transponder", view, self._satellite_view, data, itr)
|
||||
elif view is self._ter_tr_view:
|
||||
data = None if force else TerTransponder(*row)
|
||||
self.on_transponder_data_edit(TerTransponderDialog, "Transponder", view, self._terrestrial_view, data, itr)
|
||||
elif view is self._cable_tr_view:
|
||||
data = None if force else CableTransponder(*row)
|
||||
self.on_transponder_data_edit(CableTransponderDialog, "Transponder", view, self._cable_view, data, itr)
|
||||
else:
|
||||
self._app.show_error_message("Not implemented yet!")
|
||||
|
||||
def on_dvb_data_edit(self, dialog, title, view, data=None, edited_itr=None):
|
||||
""" Creates or edits DVB data. """
|
||||
dialog = dialog(self._app.get_active_window(), title, data)
|
||||
if dialog.run() == Gtk.ResponseType.OK:
|
||||
dvb_data = dialog.data
|
||||
if dvb_data:
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if data and edited_itr:
|
||||
model.set(edited_itr, {i: v for i, v in enumerate(dvb_data)})
|
||||
else:
|
||||
if paths:
|
||||
index = paths[0].get_indices()[0] + 1
|
||||
model.insert(index, dvb_data)
|
||||
else:
|
||||
model.append(dvb_data)
|
||||
scroll_to(len(model) - 1, view)
|
||||
dialog.destroy()
|
||||
|
||||
def on_transponder_data_edit(self, dialog, title, view, src_view, data=None, edited_itr=None):
|
||||
""" Creates or edits transponder data. """
|
||||
paths = self.check_selection(src_view, "Please, select only one item!")
|
||||
if paths is None:
|
||||
return
|
||||
elif len(paths) == 0:
|
||||
self._app.show_error_message("No source selected!")
|
||||
return
|
||||
|
||||
dialog = dialog(self._app.app_window, title, data)
|
||||
if dialog.run() == Gtk.ResponseType.OK:
|
||||
tr = dialog.data
|
||||
if tr:
|
||||
src_model = src_view.get_model()
|
||||
transponders = src_model[paths][-1]
|
||||
tr_model, tr_paths = view.get_selection().get_selected_rows()
|
||||
|
||||
if data and edited_itr:
|
||||
tr_model.set(edited_itr, {i: v for i, v in enumerate(tr)})
|
||||
transponders[tr_model.get_path(edited_itr).get_indices()[0]] = tr
|
||||
else:
|
||||
index = paths[0].get_indices()[0] + 1
|
||||
tr_model.insert(index, tr)
|
||||
transponders.insert(index, tr)
|
||||
dialog.destroy()
|
||||
|
||||
def check_selection(self, view, message):
|
||||
""" Checks if any row is selected. Shows error dialog if selected more than one.
|
||||
|
||||
Returns selected path or None.
|
||||
"""
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if len(paths) > 1:
|
||||
self._app.show_error_message(message)
|
||||
return
|
||||
|
||||
return paths
|
||||
|
||||
def on_remove(self, view=None):
|
||||
""" Removes selected satellites and transponders. """
|
||||
view = self.get_active_dvb_view()
|
||||
selection = view.get_selection()
|
||||
model, paths = selection.get_selected_rows()
|
||||
list(map(model.remove, [model.get_iter(path) for path in paths]))
|
||||
|
||||
def on_transponder_remove(self, item=None):
|
||||
view = self.get_active_transponder_view()
|
||||
trs = None
|
||||
if view is self._sat_tr_view:
|
||||
if self._current_sat_path:
|
||||
trs = self._satellite_view.get_model()[self._current_sat_path][-1]
|
||||
else:
|
||||
self._app.show_error_message("No satellite is selected!")
|
||||
elif view is self._ter_tr_view:
|
||||
if self._current_ter_path:
|
||||
trs = self._terrestrial_view.get_model()[self._current_ter_path][-1]
|
||||
else:
|
||||
self._app.show_error_message("No terrestrial is selected!")
|
||||
elif view is self._cable_tr_view:
|
||||
if self._current_cable_path:
|
||||
trs = self._cable_view.get_model()[self._current_cable_path][-1]
|
||||
else:
|
||||
self._app.show_error_message("No cable is selected!")
|
||||
|
||||
if trs:
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
list(map(trs.pop, sorted(map(lambda p: p.get_indices()[0], paths), reverse=True)))
|
||||
list(map(model.remove, [model.get_iter(path) for path in paths]))
|
||||
|
||||
def get_active_dvb_view(self):
|
||||
if self._dvb_type is self.DVB.SAT:
|
||||
return self._satellite_view
|
||||
elif self._dvb_type is self.DVB.TERRESTRIAL:
|
||||
return self._terrestrial_view
|
||||
return self._cable_view
|
||||
|
||||
def get_active_transponder_view(self):
|
||||
if self._dvb_type is self.DVB.SAT:
|
||||
return self._sat_tr_view
|
||||
elif self._dvb_type is self.DVB.TERRESTRIAL:
|
||||
return self._ter_tr_view
|
||||
return self._cable_tr_view
|
||||
|
||||
@run_idle
|
||||
def on_open(self):
|
||||
xml_file = "satellites.xml"
|
||||
if self._dvb_type is self.DVB.TERRESTRIAL:
|
||||
xml_file = "terrestrial.xml"
|
||||
elif self._dvb_type is self.DVB.CABLE:
|
||||
xml_file = "cables.xml"
|
||||
|
||||
response = get_chooser_dialog(self._app.app_window, self._settings, xml_file, ("*.xml",))
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
|
||||
if not str(response).endswith(xml_file):
|
||||
self._app.show_error_message(f"No {xml_file} file is selected!")
|
||||
return
|
||||
|
||||
if self._dvb_type is self.DVB.SAT:
|
||||
self.load_satellites_list(response)
|
||||
elif self._dvb_type is self.DVB.TERRESTRIAL:
|
||||
self.load_terrestrial_list(response)
|
||||
else:
|
||||
self.load_cable_list(response)
|
||||
|
||||
@run_idle
|
||||
def on_profile_changed(self, app, profile):
|
||||
self.load_satellites_list()
|
||||
self.load_terrestrial_list()
|
||||
self.load_cable_list()
|
||||
|
||||
@run_idle
|
||||
def on_save(self, app, page):
|
||||
if page is Page.SATELLITE and show_dialog(DialogType.QUESTION, self._app.app_window) == Gtk.ResponseType.OK:
|
||||
if self._dvb_type is self.DVB.SAT:
|
||||
write_satellites((Satellite(*r) for r in self._satellite_view.get_model()),
|
||||
f"{self._settings.profile_data_path}satellites.xml")
|
||||
elif self._dvb_type is self.DVB.TERRESTRIAL:
|
||||
write_terrestrial((Terrestrial(*r) for r in self._terrestrial_view.get_model()),
|
||||
f"{self._settings.profile_data_path}terrestrial.xml")
|
||||
else:
|
||||
write_cable((Cable(*r) for r in self._cable_view.get_model()),
|
||||
f"{self._settings.profile_data_path}cables.xml")
|
||||
|
||||
def on_save_as(self, app, page):
|
||||
self._app.show_error_message("Not implemented yet!")
|
||||
|
||||
def on_download(self, app, page):
|
||||
if page is Page.SATELLITE:
|
||||
self._app.on_download_data(DownloadType.SATELLITES)
|
||||
|
||||
def on_upload(self, app, page):
|
||||
if page is Page.SATELLITE:
|
||||
self._app.upload_data(DownloadType.SATELLITES)
|
||||
|
||||
@run_idle
|
||||
def on_update(self, item):
|
||||
SatellitesUpdateDialog(self._app.get_active_window(), self._settings, self._satellite_view.get_model()).show()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
1398
app/ui/xml/editor.glade
Normal file
1398
app/ui/xml/editor.glade
Normal file
File diff suppressed because it is too large
Load Diff
1173
app/ui/xml/update.glade
Normal file
1173
app/ui/xml/update.glade
Normal file
File diff suppressed because it is too large
Load Diff
40
build/BUILD_WIN.md
Normal file
40
build/BUILD_WIN.md
Normal file
@@ -0,0 +1,40 @@
|
||||
## Launch
|
||||
The best way to run this program from source is using of [MSYS2](https://www.msys2.org/) platform.
|
||||
1. Download and install the platform as described [here](https://www.msys2.org/) up to point 4.
|
||||
2. Launch **mingw64** shell.
|
||||

|
||||
3. Run first `pacman -Suy` After that, you may need to restart the terminal and re-run the update command.
|
||||
4. Install minimal required packages:
|
||||
`pacman -S mingw-w64-x86_64-gtk3 mingw-w64-x86_64-python3 mingw-w64-x86_64-python3-gobject mingw-w64-x86_64-python3-pip mingw-w64-x86_64-python3-requests`
|
||||
Optional: `pacman -S mingw-w64-x86_64-python3-pillow`
|
||||
To support streams playback, install the following packages (the list may not be complete):
|
||||
For [MPV](https://mpv.io/) `pacman -S mingw-w64-x86_64-mpv`,
|
||||
For [GStreamer](https://gstreamer.freedesktop.org/) `pacman -S mingw-w64-x86_64-gst-libav mingw-w64-x86_64-gst-plugins-bad mingw-w64-x86_64-gst-plugins-base mingw-w64-x86_64-gst-plugins-good mingw-w64-x86_64-gstreamer`
|
||||
5. Download and unzip the archive with sources from preferred branch (e.g. [master](https://github.com/DYefremov/DemonEditor/archive/refs/heads/master.zip)) in to folder where MSYS2 is installed. E.g: `c:\msys64\home\username\`
|
||||
6. Run mingw64 shell. Go to the folder where the program was unpacked. E.g: `cd DemonEditor/`
|
||||
And run: `./start.py`
|
||||
|
||||
## Building a package
|
||||
To build a standalone package, we can use [PyInstaller](https://pyinstaller.readthedocs.io/en/stable/).
|
||||
1. Launch mingw64 shell.
|
||||
2. Install PyInstaller via pip: `pip3 install pyinstaller`
|
||||
3. Go to the folder where the program was unpacked. E.g: `c:\msys64\home\username\DemonEditor\`
|
||||
4. Сopy and replace the files from the /build/win/ folder to the root .
|
||||
5. Go to the folder with the program in the running terminal: `cd DemonEditor/`
|
||||
6. Give the following command: `pyinstaller.exe DemonEditor.spec`
|
||||
7. Wait until the operation end. In the dist folder you will find a ready-made build.
|
||||
|
||||
### Appearance
|
||||
To change the look we can use third party [Gtk3 themes and Icon sets](https://www.gnome-look.org).
|
||||
To set the default theme:
|
||||
1. Сreate a folder "`\etc\gtk-3.0\`" in the root of the finished build folder.
|
||||
2. Create a _settings.ini_ file in this folder with the following content:
|
||||
```
|
||||
[Settings]
|
||||
gtk-icon-theme-name = Adwaita
|
||||
gtk-theme-name = Windows-10
|
||||
```
|
||||
In this case, we are using the default icon theme "Adwaita" and the [third party theme](https://github.com/B00merang-Project/Windows-10) "Windows-10".
|
||||
Themes and icon sets should be located in the `share\themes` and `share\icons` folders respectively.
|
||||
To fine-tune the default theme you use, you can use the _win_style.css_ file in the `ui` folder.
|
||||
You can find more info about changing the appearance of Gtk applications on the Web yourself.
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
VER="2.0.0_Alpha2"
|
||||
VER="3.1.0_Alpha"
|
||||
B_PATH="dist/DemonEditor"
|
||||
DEB_PATH="$B_PATH/usr/share/demoneditor"
|
||||
|
||||
@@ -8,11 +8,7 @@ cp -TRv deb $B_PATH
|
||||
rsync --exclude=app/ui/lang --exclude=app/ui/icons --exclude=__pycache__ -arv ../../app $DEB_PATH
|
||||
|
||||
cd dist
|
||||
fakeroot dpkg-deb --build DemonEditor
|
||||
fakeroot dpkg-deb -Zxz --build DemonEditor
|
||||
mv DemonEditor.deb DemonEditor_$VER.deb
|
||||
|
||||
rm -R DemonEditor
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Package: demon-editor
|
||||
Version: 2.0.0-Alpha2
|
||||
Version: 3.1.0-Alpha
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Architecture: all
|
||||
@@ -8,8 +8,18 @@ Depends: python3 (>= 3.6),
|
||||
python3-requests,
|
||||
python3-gi,
|
||||
python3-gi-cairo,
|
||||
gir1.2-notify-0.7
|
||||
gir1.2-notify-0.7,
|
||||
p7zip-full
|
||||
Recommends: libmpv1,
|
||||
python3-chardet
|
||||
python3-chardet,
|
||||
libgtksourceview (>= 3.0)
|
||||
Maintainer: Dmitriy Yefremov <dmitry.v.yefremov@gmail.com>
|
||||
Homepage: https://dyefremov.github.io/DemonEditor
|
||||
Description: Enigma2 channel and satellite list editor
|
||||
Editing bouquets, channels, satellites, importing services,
|
||||
downloading picons and updating satellites from the Web,
|
||||
extended support of IPTV, assignment of EPG from DVB or
|
||||
XML for IPTV services, playback of IPTV or other streams
|
||||
directly from the bouquet list, control panel (via HTTP API),
|
||||
ability to view EPG and manage timers (via HTTP API),
|
||||
simple FTP client (experimental).
|
||||
|
||||
@@ -5,7 +5,7 @@ Source: https://github.com/DYefremov/DemonEditor
|
||||
Files: *
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2022 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,10 +1,15 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=DemonEditor
|
||||
GenericName=Enigma2 bouquets editor
|
||||
GenericName[it]=Editor di bouquet per Enigma2
|
||||
Comment=Channel and satellite list editor for Enigma2
|
||||
Comment[ru]=Редактор списка каналов и спутников для Enigma2
|
||||
Comment[be]=Рэдактар спіса каналаў і спадарожнікаў для Enigma2
|
||||
Comment[de]=Programm- und Satellitenlisten-Editor für Enigma2
|
||||
Comment[it]=Editor di liste canali e satelliti per Enigma2
|
||||
Comment[tr]=Enigma2 için kanal ve uydu listesi editörü
|
||||
Comment[es]=Editor de listas de canales y satélites para Enigma2
|
||||
Icon=demon-editor
|
||||
Exec=/usr/bin/demon-editor
|
||||
Terminal=false
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -21,6 +21,8 @@ excludes = ['app.tools.mpv',
|
||||
ui_files = [('app/ui/*.glade', 'ui'),
|
||||
('app/ui/*.css', 'ui'),
|
||||
('app/ui/*.ui', 'ui'),
|
||||
('app/ui/epg/*.glade', 'ui/epg'),
|
||||
('app/ui/xml/*.glade', 'ui/xml'),
|
||||
('app/ui/lang*', 'share/locale'),
|
||||
('app/ui/icons*', 'share/icons')
|
||||
]
|
||||
@@ -32,6 +34,15 @@ a = Analysis([EXE_NAME],
|
||||
hiddenimports=['fileinput', 'uuid'],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
hooksconfig={
|
||||
"gi": {
|
||||
"languages": ["en", "be", "es", "it", "nl",
|
||||
"pl", "pt", "ru", "tr", "zh_CN"],
|
||||
"module-versions": {
|
||||
"Gtk": "3.0"
|
||||
},
|
||||
},
|
||||
},
|
||||
excludes=excludes,
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
@@ -69,7 +80,8 @@ app = BUNDLE(coll,
|
||||
'CFBundleGetInfoString': "Enigma2 channel and satellite editor",
|
||||
'LSApplicationCategoryType': 'public.app-category.utilities',
|
||||
'LSMinimumSystemVersion': '10.13',
|
||||
'CFBundleShortVersionString': f"2.0.0.{BUILD_DATE} Alpha2",
|
||||
'NSHumanReadableCopyright': u"Copyright © 2021, Dmitriy Yefremov",
|
||||
'NSRequiresAquaSystemAppearance': 'false'
|
||||
'CFBundleShortVersionString': f"3.1.0.{BUILD_DATE} Alpha",
|
||||
'NSHumanReadableCopyright': u"Copyright © 2022, Dmitriy Yefremov",
|
||||
'NSRequiresAquaSystemAppearance': 'false',
|
||||
'NSHighResolutionCapable': 'true'
|
||||
})
|
||||
|
||||
@@ -18,6 +18,8 @@ excludes = ['app.tools.mpv',
|
||||
ui_files = [('app\\ui\\*.glade', 'ui'),
|
||||
('app\\ui\\*.css', 'ui'),
|
||||
('app\\ui\\*.ui', 'ui'),
|
||||
('app\\ui\\epg\\*.glade', 'ui\\epg'),
|
||||
('app\\ui\\xml\\*.glade', 'ui\\xml'),
|
||||
('app\\ui\\lang*', 'share\\locale'),
|
||||
('app\\ui\\icons*', 'share\\icons')
|
||||
]
|
||||
@@ -30,6 +32,16 @@ a = Analysis([EXE_NAME],
|
||||
hiddenimports=['fileinput', 'uuid', 'ctypes.wintypes'],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
hooksconfig={
|
||||
"gi": {
|
||||
"languages": ["en", "be", "es", "it", "nl",
|
||||
"pl", "pt", "ru", "tr", "zh_CN"],
|
||||
"module-versions": {
|
||||
"Gtk": "3.0",
|
||||
"GtkSource": "3",
|
||||
},
|
||||
},
|
||||
},
|
||||
excludes=excludes,
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import ssl
|
||||
|
||||
if __name__ == "__main__":
|
||||
from multiprocessing import freeze_support
|
||||
from app.ui.main import start_app
|
||||
|
||||
os.environ["PYTHONUTF8"] = "1"
|
||||
# TODO There needs to be a more "correct" way.
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
|
||||
freeze_support()
|
||||
start_app()
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=DemonEditor
|
||||
GenericName=Enigma2 bouquets editor
|
||||
GenericName[it]=Editor di bouquet per Enigma2
|
||||
Comment=Channel and satellite list editor for Enigma2
|
||||
Comment[ru]=Редактор списка каналов и спутников для Enigma2
|
||||
Comment[be]=Рэдактар спіса каналаў і спадарожнікаў для Enigma2
|
||||
Comment[de]=Programm- und Satellitenlisten-Editor für Enigma2
|
||||
Comment[it]=Editor di liste canali e satelliti per Enigma2
|
||||
Comment[tr]=Enigma2 için kanal ve uydu listesi editörü
|
||||
Comment[es]=Editor de listas de canales y satélites para Enigma2
|
||||
Icon=demon-editor
|
||||
Exec=bash -c 'cd $(dirname %k) && ./start.py'
|
||||
Terminal=false
|
||||
@@ -50,6 +50,9 @@ msgstr "Цякучы IP:"
|
||||
msgid "Assign"
|
||||
msgstr "Прывязаць"
|
||||
|
||||
msgid "Assign file"
|
||||
msgstr "Прывязаць файл"
|
||||
|
||||
msgid "Bouquet details"
|
||||
msgstr "Сэрвісы букета"
|
||||
|
||||
@@ -230,6 +233,9 @@ msgstr "Скід профілю"
|
||||
msgid "Satellites"
|
||||
msgstr "Спадарожнікі"
|
||||
|
||||
msgid "Transponders"
|
||||
msgstr "Транспондары"
|
||||
|
||||
msgid "Satellites.xml file:"
|
||||
msgstr "Файл satellites.xml:"
|
||||
|
||||
@@ -1247,6 +1253,9 @@ msgstr "Выконваецца загрузка дадзеных!"
|
||||
msgid "Recordings"
|
||||
msgstr "Запісы"
|
||||
|
||||
msgid "Recordings:"
|
||||
msgstr "Запісы:"
|
||||
|
||||
msgid "Help"
|
||||
msgstr "Даведка"
|
||||
|
||||
@@ -1258,3 +1267,123 @@ msgstr "Дадаць піконы"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "Логі"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Назва"
|
||||
|
||||
msgid "Time"
|
||||
msgstr "Час"
|
||||
|
||||
msgid "Length"
|
||||
msgstr "Працягласць"
|
||||
|
||||
msgid "Additional source"
|
||||
msgstr "Дадатковая крыніца"
|
||||
|
||||
msgid "Automatically set the name selected in the favorites list."
|
||||
msgstr "Аўтаматычная ўсталёўка імя са спіса абранага."
|
||||
|
||||
msgid "Playback"
|
||||
msgstr "Прайграванне"
|
||||
|
||||
msgid "Playback:"
|
||||
msgstr "Прайграванне:"
|
||||
|
||||
msgid "Audio"
|
||||
msgstr "Аўдыё"
|
||||
|
||||
msgid "Audio Track"
|
||||
msgstr "Аўдыёдарожка"
|
||||
|
||||
msgid "Subtitle"
|
||||
msgstr "Субтытры"
|
||||
|
||||
msgid "Subtitle Track"
|
||||
msgstr "Дарожка субтытраў"
|
||||
|
||||
msgid "Aspect ratio"
|
||||
msgstr "Стасунак бакоў"
|
||||
|
||||
msgid "This may change the settings of other profiles!"
|
||||
msgstr "Гэта можа змяніць налады іншых профіляў!"
|
||||
|
||||
msgid "Drag the services to the desired picon or picon to the list of selected services."
|
||||
msgstr "Перацягніце сэрвісы на патрэбны пікон ці пікон на спіс абраных сэрвісаў."
|
||||
|
||||
msgid "Sets the profile folder as default to store picons, backups, etc."
|
||||
msgstr "Усталёўвае тэчку профілю па змаўчанні для захоўвання піконаў, рэзервовых копій і т. п."
|
||||
|
||||
msgid "New sub-bouquet"
|
||||
msgstr "Стварыць саббукет"
|
||||
|
||||
msgid "Mark not presented in Bouquets"
|
||||
msgstr "Адзначыць адсутныя у букетах"
|
||||
|
||||
msgid "Not in Bouquets"
|
||||
msgstr "Адсутныя у букетах"
|
||||
|
||||
msgid "Do not show services present in Bouquets."
|
||||
msgstr "Не паказваць сэрвісы якія прысутнічаюць у букетах."
|
||||
|
||||
msgid "IPTV services only"
|
||||
msgstr "Толькі IPTV сэрвісы"
|
||||
|
||||
msgid "Display picons"
|
||||
msgstr "Адлюстроўваць пiконы"
|
||||
|
||||
msgid "Alternate layout"
|
||||
msgstr "Альтэрнатыўная кампаноўка"
|
||||
|
||||
msgid "Layout of elements has been changed!"
|
||||
msgstr "Зменена месцаванне элементаў!"
|
||||
|
||||
msgid "Restart the program to apply all changes."
|
||||
msgstr "Перазапусціце праграму, каб ужыць усе змены."
|
||||
|
||||
msgid "New folder"
|
||||
msgstr "Стварыць тэчку"
|
||||
|
||||
msgid "Rename"
|
||||
msgstr "Змяніць назву"
|
||||
|
||||
msgid "Bookmarks"
|
||||
msgstr "Закладкі"
|
||||
|
||||
msgid "Add bookmark"
|
||||
msgstr "Дадаць закладку"
|
||||
|
||||
msgid "All bouquets"
|
||||
msgstr "Усе букеты"
|
||||
|
||||
msgid "Playback from the main list"
|
||||
msgstr "Прайграванне з асноўнага спіса"
|
||||
|
||||
msgid "Enables URL parsing using youtube-dl to get direct links to media."
|
||||
msgstr "Улучае аналіз URL-адрасоў з дапамогай youtube-dl для атрымання прамых спасылак на медыя."
|
||||
|
||||
msgid "Permissions..."
|
||||
msgstr "Дазволы..."
|
||||
|
||||
msgid "Display EPG in bouquet list"
|
||||
msgstr "Адлюстроўваць EPG у спісе букета"
|
||||
|
||||
msgid "EPG *.dat file:"
|
||||
msgstr "Файл EPG *.dat:"
|
||||
|
||||
msgid "Use HTTP to reload data in the receiver"
|
||||
msgstr "Скарыстаць HTTP для перазагрузкі дадзеных у рэсіверы"
|
||||
|
||||
msgid "Enable picons compression"
|
||||
msgstr "Уключыць сціск пiконаў"
|
||||
|
||||
msgid "Update interval (sec):"
|
||||
msgstr "Інтэрвал абнаўлення (сек):"
|
||||
|
||||
msgid "Update:"
|
||||
msgstr "Абнаўляць:"
|
||||
|
||||
msgid "Daily"
|
||||
msgstr "Штодня"
|
||||
|
||||
msgid "Assign reference"
|
||||
msgstr "Прысвоіць спасылку"
|
||||
|
||||
@@ -52,6 +52,9 @@ msgstr "Akt.IP:"
|
||||
msgid "Assign"
|
||||
msgstr "Zuweisen"
|
||||
|
||||
msgid "Assign file"
|
||||
msgstr "Datei zuweisen"
|
||||
|
||||
msgid "Bouquet details"
|
||||
msgstr "Bouquet-Details"
|
||||
|
||||
@@ -232,6 +235,9 @@ msgstr "Profil zurücksetzen"
|
||||
msgid "Satellites"
|
||||
msgstr "Satelliten"
|
||||
|
||||
msgid "Transponders"
|
||||
msgstr "Transpondern"
|
||||
|
||||
msgid "Satellites.xml file:"
|
||||
msgstr "Satellites.xml Datei:"
|
||||
|
||||
@@ -929,7 +935,7 @@ msgid "Built-in player"
|
||||
msgstr "Integrierter Player"
|
||||
|
||||
msgid "In a separate window"
|
||||
msgstr "In einem separaten Fenster"
|
||||
msgstr "In separatem Fenster"
|
||||
|
||||
msgid "Only get m3u file"
|
||||
msgstr "Nur m3u-Datei erhalten"
|
||||
@@ -1261,6 +1267,9 @@ msgstr "Daten werden geladen!"
|
||||
msgid "Recordings"
|
||||
msgstr "Aufnahmen"
|
||||
|
||||
msgid "Recordings:"
|
||||
msgstr "Aufnahmen:"
|
||||
|
||||
msgid "Help"
|
||||
msgstr "Hilfe"
|
||||
|
||||
@@ -1272,3 +1281,123 @@ msgstr "Picons hinzufügen"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "Logs"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Titel"
|
||||
|
||||
msgid "Time"
|
||||
msgstr "Zeit"
|
||||
|
||||
msgid "Length"
|
||||
msgstr "Dauer"
|
||||
|
||||
msgid "Additional source"
|
||||
msgstr "Zusätzliche Quelle"
|
||||
|
||||
msgid "Automatically set the name selected in the favorites list."
|
||||
msgstr "Automatisch den in der Favoritenliste ausgewählten Namen einstellen."
|
||||
|
||||
msgid "Playback"
|
||||
msgstr "Wiedergabe"
|
||||
|
||||
msgid "Playback:"
|
||||
msgstr "Wiedergabe:"
|
||||
|
||||
msgid "Audio"
|
||||
msgstr "Audio"
|
||||
|
||||
msgid "Audio Track"
|
||||
msgstr "Audio"
|
||||
|
||||
msgid "Subtitle"
|
||||
msgstr "Untertitel"
|
||||
|
||||
msgid "Subtitle Track"
|
||||
msgstr "Untertitelspur"
|
||||
|
||||
msgid "Aspect ratio"
|
||||
msgstr "Seitenverhältnis"
|
||||
|
||||
msgid "This may change the settings of other profiles!"
|
||||
msgstr "Dadurch können sich die Einstellungen anderer Profile ändern!"
|
||||
|
||||
msgid "Drag the services to the desired picon or picon to the list of selected services."
|
||||
msgstr "Ziehe die Dienste auf das gewünschte Picon oder Picon in die Liste der ausgewählten Dienste."
|
||||
|
||||
msgid "Sets the profile folder as default to store picons, backups, etc."
|
||||
msgstr "Legt den Profilordner als Standardordner zum Speichern von Piconen, Backups usw. fest."
|
||||
|
||||
msgid "New sub-bouquet"
|
||||
msgstr "Neues Sub-Bouquet"
|
||||
|
||||
msgid "Mark not presented in Bouquets"
|
||||
msgstr "Markieren die nicht in Bouquets vorhanden"
|
||||
|
||||
msgid "Not in Bouquets"
|
||||
msgstr "Nicht in Bouquets"
|
||||
|
||||
msgid "Do not show services present in Bouquets."
|
||||
msgstr "Vorhandene in Bouquets Dienste nicht anzeigen."
|
||||
|
||||
msgid "IPTV services only"
|
||||
msgstr "Nur IPTV-Dienste"
|
||||
|
||||
msgid "Display picons"
|
||||
msgstr "Picons anzeigen"
|
||||
|
||||
msgid "Alternate layout"
|
||||
msgstr "Alternatives Layout"
|
||||
|
||||
msgid "Layout of elements has been changed!"
|
||||
msgstr "Das Layout der Elemente wurde geändert!"
|
||||
|
||||
msgid "Restart the program to apply all changes."
|
||||
msgstr "Starte das Programm neu, um alle Änderungen zu übernehmen."
|
||||
|
||||
msgid "New folder"
|
||||
msgstr "Ordner erstellen"
|
||||
|
||||
msgid "Rename"
|
||||
msgstr "Umbenennen"
|
||||
|
||||
msgid "Bookmarks"
|
||||
msgstr "Lesezeichen"
|
||||
|
||||
msgid "Add bookmark"
|
||||
msgstr "Lesezeichen hinzufügen"
|
||||
|
||||
msgid "All bouquets"
|
||||
msgstr "Alle Bouquets"
|
||||
|
||||
msgid "Playback from the main list"
|
||||
msgstr "Wiedergabe aus der Hauptliste"
|
||||
|
||||
msgid "Enables URL parsing using youtube-dl to get direct links to media."
|
||||
msgstr "Aktiviert URL-Parsing mit youtube-dl, um direkte Links zu Medien zu erhalten."
|
||||
|
||||
msgid "Permissions..."
|
||||
msgstr "Berechtigungen..."
|
||||
|
||||
msgid "Display EPG in bouquet list"
|
||||
msgstr "EPG in Bouquet-Liste anzeigen"
|
||||
|
||||
msgid "EPG *.dat file:"
|
||||
msgstr "EPG *.dat Datei:"
|
||||
|
||||
msgid "Use HTTP to reload data in the receiver"
|
||||
msgstr "HTTP verwenden, um Daten in den Receiver updaten"
|
||||
|
||||
msgid "Enable picons compression"
|
||||
msgstr "Picon-Komprimierung aktivieren"
|
||||
|
||||
msgid "Update interval (sec):"
|
||||
msgstr "Aktualisierungsintervall (sec):"
|
||||
|
||||
msgid "Update:"
|
||||
msgstr "Aktualisieren:"
|
||||
|
||||
msgid "Daily"
|
||||
msgstr "Täglich"
|
||||
|
||||
msgid "Assign reference"
|
||||
msgstr "Referenz zuweisen"
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
# Copyright (C) 2018-2020 Frank Neirynck
|
||||
# Copyright (C) 2018-2022 Víctor Pont
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
# Frank Neirynck <frank@insink.be>, 2018-2020.
|
||||
#
|
||||
# Víctor Pont <victor@pont.cat>, 2021-2022.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Last-Translator: Frank Neirynck\n"
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: Víctor Pont\n"
|
||||
"Language-Team: \n"
|
||||
"Language: es\n"
|
||||
"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"
|
||||
"X-Generator: Poedit 3.0.1\n"
|
||||
|
||||
msgid "translator-credits"
|
||||
msgstr "Frank Neirynck <frank@insink.be>"
|
||||
msgstr ""
|
||||
"Víctor Pont <victor@pont.cat>\n"
|
||||
"Frank Neirynck <frank@insink.be>"
|
||||
|
||||
# Main
|
||||
msgid "Service"
|
||||
@@ -33,19 +35,19 @@ msgid "Picon"
|
||||
msgstr "Picon"
|
||||
|
||||
msgid "Freq"
|
||||
msgstr "Frec."
|
||||
msgstr "Frec"
|
||||
|
||||
msgid "Rate"
|
||||
msgstr "Ratio"
|
||||
|
||||
msgid "Pol"
|
||||
msgstr "Pol."
|
||||
msgstr "Pol"
|
||||
|
||||
msgid "System"
|
||||
msgstr "Sistema"
|
||||
|
||||
msgid "Pos"
|
||||
msgstr "Pos."
|
||||
msgstr "Pos"
|
||||
|
||||
msgid "Num"
|
||||
msgstr "Núm"
|
||||
@@ -56,6 +58,9 @@ msgstr "IP actual:"
|
||||
msgid "Assign"
|
||||
msgstr "Asignar"
|
||||
|
||||
msgid "Assign file"
|
||||
msgstr "Asignar fichero"
|
||||
|
||||
msgid "Bouquet details"
|
||||
msgstr "Detalles bouquet"
|
||||
|
||||
@@ -110,6 +115,9 @@ msgstr "Establecer nombre predeterminado"
|
||||
msgid "Insert marker"
|
||||
msgstr "Insertar marcador"
|
||||
|
||||
msgid "Insert space"
|
||||
msgstr "Insertar espacio"
|
||||
|
||||
msgid "Locate in services"
|
||||
msgstr "Buscar en servicios"
|
||||
|
||||
@@ -162,10 +170,10 @@ msgid "Satellites downloader"
|
||||
msgstr "Descarga de satélites"
|
||||
|
||||
msgid "Remove"
|
||||
msgstr "Quitar"
|
||||
msgstr "Borrar"
|
||||
|
||||
msgid "Remove all unavailable"
|
||||
msgstr "Quitar todo lo indisponible"
|
||||
msgstr "Borrar lo que no esté disponible"
|
||||
|
||||
msgid "Satellites editor"
|
||||
msgstr "Editor de satélites"
|
||||
@@ -225,7 +233,7 @@ msgid "Receiver IP:"
|
||||
msgstr "IP del receptor:"
|
||||
|
||||
msgid "Remove unused bouquets"
|
||||
msgstr "Quitar bouquets sin usar"
|
||||
msgstr "Borrar bouquets sin usar"
|
||||
|
||||
msgid "Reset profile"
|
||||
msgstr "Restablecer perfil"
|
||||
@@ -233,6 +241,9 @@ msgstr "Restablecer perfil"
|
||||
msgid "Satellites"
|
||||
msgstr "Satélites"
|
||||
|
||||
msgid "Transponders"
|
||||
msgstr "Transponders"
|
||||
|
||||
msgid "Satellites.xml file:"
|
||||
msgstr "Fichero satellites.xml:"
|
||||
|
||||
@@ -327,7 +338,7 @@ msgid "Path to Enigma2 picons:"
|
||||
msgstr "Ruta a picons de Enigma2:"
|
||||
|
||||
msgid "Specify the correct position value for the provider!"
|
||||
msgstr "¡Especifique el valor correcto de la posición del proveedor!"
|
||||
msgstr "¡Especifica el valor correcto de la posición del proveedor!"
|
||||
|
||||
msgid "Converter between name formats"
|
||||
msgstr "Conversor entre formatos de nombre"
|
||||
@@ -341,10 +352,9 @@ msgstr "Cargar proveedores de satélite."
|
||||
msgid ""
|
||||
"To automatically set the identifiers for picons,\n"
|
||||
"first load the required services list into the main application window."
|
||||
|
||||
msgstr ""
|
||||
"Para configurar automáticamente los identificadores para picons,\n"
|
||||
"cargue primero la lista de servicios requeridos en la ventana principal."
|
||||
"carga primero la lista de servicios requeridos en la ventana principal."
|
||||
|
||||
# Satellites editor
|
||||
msgid "Satellites edit tool"
|
||||
@@ -376,7 +386,7 @@ msgid "Satellites update"
|
||||
msgstr "Actualizar satélites"
|
||||
|
||||
msgid "Remove selection"
|
||||
msgstr "Quitar selección"
|
||||
msgstr "Borrar selección"
|
||||
|
||||
# Service details dialog
|
||||
msgid "Service data:"
|
||||
@@ -396,7 +406,7 @@ msgid ""
|
||||
"Continue?"
|
||||
msgstr ""
|
||||
"Los cambios se aplicarán a todos los servicios de este transpondedor!\n"
|
||||
"¿Continuar?"
|
||||
"¿Quieres continuar?"
|
||||
|
||||
msgid "Reference"
|
||||
msgstr "Referencia"
|
||||
@@ -408,7 +418,7 @@ msgid "Flags:"
|
||||
msgstr "Flags:"
|
||||
|
||||
msgid "Delays (ms):"
|
||||
msgstr "Retraso (ms)"
|
||||
msgstr "Retraso (ms):"
|
||||
|
||||
msgid "Bitstream"
|
||||
msgstr "Secuencia de bits"
|
||||
@@ -493,7 +503,7 @@ msgid "Error. No bouquet is selected!"
|
||||
msgstr "Error. ¡Ningún bouquet seleccionado!"
|
||||
|
||||
msgid "This item is not allowed to be removed!"
|
||||
msgstr "¡Este elemento no puede ser quitado!"
|
||||
msgstr "¡Este elemento no puede ser borrado!"
|
||||
|
||||
msgid "This item is not allowed to edit!"
|
||||
msgstr "¡Este elemento no puede ser editado!"
|
||||
@@ -502,7 +512,7 @@ msgid "Not allowed in this context!"
|
||||
msgstr "¡No permitido en este contexto!"
|
||||
|
||||
msgid "Please, download files from receiver or setup your path for read data!"
|
||||
msgstr "Por favor, descargue ficheros desde el receptor o configure la ruta para leer los datos!"
|
||||
msgstr "Por favor, descarga ficheros desde el receptor o configura la ruta para leer los datos!"
|
||||
|
||||
msgid "Reading data error!"
|
||||
msgstr "¡Error de lectura de datos!"
|
||||
@@ -511,17 +521,20 @@ msgid "No m3u file is selected!"
|
||||
msgstr "¡No se ha seleccionado ningún fichero m3u!"
|
||||
|
||||
msgid "Not implemented yet!"
|
||||
msgstr "¡No implementado!"
|
||||
msgstr "¡No implementado todavía!"
|
||||
|
||||
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 un único elemento!"
|
||||
msgstr "¡Por favor, selecciona un único elemento!"
|
||||
|
||||
msgid "No png file is selected!"
|
||||
msgstr "¡No se ha seleccionado ningún fichero png!"
|
||||
|
||||
msgid "No profile selected!"
|
||||
msgstr "¡No se ha seleccionado ningún perfil!"
|
||||
|
||||
msgid "No reference is present!"
|
||||
msgstr "¡Ninguna referencia presente!"
|
||||
|
||||
@@ -535,7 +548,7 @@ msgid "Done!"
|
||||
msgstr "¡Hecho!"
|
||||
|
||||
msgid "Please, wait..."
|
||||
msgstr "Por favor, espere..."
|
||||
msgstr "Por favor, espera..."
|
||||
|
||||
msgid "Resizing..."
|
||||
msgstr "Redimensionando..."
|
||||
@@ -550,29 +563,29 @@ msgid "Please, select only one satellite!"
|
||||
msgstr "¡Seleccione sólo un Satélite!"
|
||||
|
||||
msgid "Please check your parameters and try again."
|
||||
msgstr "¡Por favor revise sus parámetros y vuelva a intentarlo!"
|
||||
msgstr "Por favor, revisa tus parámetros y vuelve a intentarlo."
|
||||
|
||||
msgid "No satellites.xml file is selected!"
|
||||
msgstr "¡Ningún satellites.xml seleccionado!"
|
||||
|
||||
msgid "Error. Verify the data!"
|
||||
msgstr "Error. ¡Revise los datos!"
|
||||
msgstr "Error. ¡Revisa los datos!"
|
||||
|
||||
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. ¡Compruebe que está instalado!"
|
||||
msgstr "VLC no encontrado. ¡Comprueba que está instalado!"
|
||||
|
||||
# Search unavailable streams dialog
|
||||
msgid "Please wait, streams testing in progress..."
|
||||
msgstr "Por favor espere, hay una prueba de stream en progreso..."
|
||||
msgstr "Por favor espera, hay una prueba de stream en progreso..."
|
||||
|
||||
msgid "Found"
|
||||
msgstr "Encontrado"
|
||||
msgstr "Encontrado(s)"
|
||||
|
||||
msgid "unavailable streams."
|
||||
msgstr "Streams no presentes."
|
||||
msgstr "streams no presentes."
|
||||
|
||||
msgid "No changes required!"
|
||||
msgstr "¡No se requieren cambios!"
|
||||
@@ -657,10 +670,10 @@ msgid "No bouquet file is selected!"
|
||||
msgstr "¡No se ha seleccionado nigún fichero de bouquet!"
|
||||
|
||||
msgid "Remove all unused"
|
||||
msgstr "Quitar todos sin usar"
|
||||
msgstr "Borrar los que están sin usar"
|
||||
|
||||
msgid "Test"
|
||||
msgstr "Prueba"
|
||||
msgstr "Probar"
|
||||
|
||||
msgid "Test connection"
|
||||
msgstr "Probar conexión"
|
||||
@@ -684,7 +697,7 @@ msgid "Enable HTTP API"
|
||||
msgstr "Habilitar API HTTP"
|
||||
|
||||
msgid "Switch(zap) the channel(Ctrl + Z)"
|
||||
msgstr "Poner el canal (Ctrl + Z)"
|
||||
msgstr "Poner (zapear) el canal (Ctrl + Z)"
|
||||
|
||||
msgid "Switch the channel and watch in the program(Ctrl + W)"
|
||||
msgstr "Poner el canal y ver en el programa (Ctrl + W)"
|
||||
@@ -708,7 +721,7 @@ msgid "Service names source:"
|
||||
msgstr "Origen nombres de servicio:"
|
||||
|
||||
msgid "Main service list"
|
||||
msgstr "Lista principal de servicios:"
|
||||
msgstr "Lista principal de servicios"
|
||||
|
||||
msgid "XML file"
|
||||
msgstr "Fichero XML"
|
||||
@@ -770,8 +783,12 @@ msgstr "Terminar reproducción"
|
||||
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 "¡Se ha encontrado enlace al recurso de YouTube!\n¿Intentar obtener un enlace directo al vídeo?"
|
||||
msgid ""
|
||||
"Found a link to the YouTube resource!\n"
|
||||
"Try to get a direct link to the video?"
|
||||
msgstr ""
|
||||
"¡Se ha encontrado un enlace al recurso de YouTube!\n"
|
||||
"¿Quieres intentar obtener un enlace directo al vídeo?"
|
||||
|
||||
msgid "Playlist import"
|
||||
msgstr "Importar lista de reproducción"
|
||||
@@ -786,7 +803,7 @@ msgid "Apply profile settings"
|
||||
msgstr "Aplicar ajustes de perfil"
|
||||
|
||||
msgid "Settings type:"
|
||||
msgstr "Tipo de ajustes:"
|
||||
msgstr "Tipo:"
|
||||
|
||||
msgid "Set default"
|
||||
msgstr "Por defecto"
|
||||
@@ -813,7 +830,7 @@ 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"
|
||||
msgstr "Borrar los enlaces añadidos a la lista de reproducción"
|
||||
|
||||
msgid "A bouquet with that name exists!"
|
||||
msgstr "¡Ya existe un bouquet con ese nombre!"
|
||||
@@ -867,7 +884,7 @@ 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"
|
||||
msgstr "Usar por defecto el directorio de perfil para los datos adicionales"
|
||||
|
||||
msgid "Default data path:"
|
||||
msgstr "Ruta estándar de datos:"
|
||||
@@ -923,8 +940,8 @@ msgstr "Modo de reproducción de los streams:"
|
||||
msgid "Built-in player"
|
||||
msgstr "Reproductor interno"
|
||||
|
||||
msgid "VLC media player"
|
||||
msgstr "Reproductor VLC"
|
||||
msgid "In a separate window"
|
||||
msgstr "En una ventana aparte"
|
||||
|
||||
msgid "Only get m3u file"
|
||||
msgstr "Sólo bajar fichero m3u"
|
||||
@@ -933,7 +950,7 @@ 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!"
|
||||
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!"
|
||||
@@ -984,7 +1001,7 @@ msgid "Download from the receiver"
|
||||
msgstr "Descargar desde el receptor"
|
||||
|
||||
msgid "Remove all picons from the receiver"
|
||||
msgstr "Eliminar todos los picons del receptor"
|
||||
msgstr "Borrar todos los picons del receptor"
|
||||
|
||||
msgid "Service reference"
|
||||
msgstr "Referencia del servicio"
|
||||
@@ -1010,14 +1027,24 @@ 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 ""
|
||||
"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 ""
|
||||
"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"
|
||||
msgstr "Borrar del receptor"
|
||||
|
||||
msgid "Screenshot"
|
||||
msgstr "Captura de pantalla"
|
||||
@@ -1025,3 +1052,333 @@ msgstr "Captura de pantalla"
|
||||
msgid "Video"
|
||||
msgstr "Vídео"
|
||||
|
||||
msgid "The Neutrino has only experimental support. Not all features are supported!"
|
||||
msgstr "El Neutrino sólo dispone de soporte experimental. ¡No todas las funciones estarán disponibles!"
|
||||
|
||||
msgid "Enable experimental features"
|
||||
msgstr "Activar las funciones experimentales"
|
||||
|
||||
msgid "Can't Playback!"
|
||||
msgstr "¡No se puede reproducir!"
|
||||
|
||||
msgid "Enable Dark Mode"
|
||||
msgstr "Habilitar modo oscuro"
|
||||
|
||||
msgid "Extract..."
|
||||
msgstr "Extraer..."
|
||||
|
||||
msgid "Unsupported format!"
|
||||
msgstr "¡Formato no soportado!"
|
||||
|
||||
msgid "Combine with the current data?"
|
||||
msgstr "¿Combinar con los datos actuales?"
|
||||
|
||||
msgid "Importing data done!"
|
||||
msgstr "¡Importación finalizada!"
|
||||
|
||||
msgid "Current service"
|
||||
msgstr "Servicio actual"
|
||||
|
||||
msgid "Open folder"
|
||||
msgstr "Abrir carpeta"
|
||||
|
||||
msgid "Open archive"
|
||||
msgstr "Abrir fichero"
|
||||
|
||||
msgid "Import from Web"
|
||||
msgstr "Importar de Internet"
|
||||
|
||||
msgid "Control"
|
||||
msgstr "Control"
|
||||
|
||||
msgid "Timers"
|
||||
msgstr "Temporizadores"
|
||||
|
||||
msgid "Timer"
|
||||
msgstr "Temporizador"
|
||||
|
||||
msgid "Add timer"
|
||||
msgstr "Añadir temporizador"
|
||||
|
||||
msgid "Hr."
|
||||
msgstr "Hora"
|
||||
|
||||
msgid "Min."
|
||||
msgstr "Min."
|
||||
|
||||
msgid "Power"
|
||||
msgstr "Power"
|
||||
|
||||
msgid "Standby"
|
||||
msgstr "Standby"
|
||||
|
||||
msgid "Wake Up"
|
||||
msgstr "Despertar"
|
||||
|
||||
msgid "Reboot"
|
||||
msgstr "Reiniciar"
|
||||
|
||||
msgid "Restart GUI"
|
||||
msgstr "Reiniciar GUI"
|
||||
|
||||
msgid "Shutdown"
|
||||
msgstr "Apagar"
|
||||
|
||||
msgid "Shut down"
|
||||
msgstr "Apagar"
|
||||
|
||||
msgid "Do Nothing"
|
||||
msgstr "No hacer nada"
|
||||
|
||||
msgid "Auto"
|
||||
msgstr "Auto"
|
||||
|
||||
msgid "Grab screenshot"
|
||||
msgstr "Captura de pantalla"
|
||||
|
||||
msgid "Enabled:"
|
||||
msgstr "Activo:"
|
||||
|
||||
msgid "Name:"
|
||||
msgstr "Nombre:"
|
||||
|
||||
msgid "Description:"
|
||||
msgstr "Descripción:"
|
||||
|
||||
msgid "Service:"
|
||||
msgstr "Servicio:"
|
||||
|
||||
msgid "Service reference:"
|
||||
msgstr "Referencia del servicio:"
|
||||
|
||||
msgid "Event ID:"
|
||||
msgstr "ID evento:"
|
||||
|
||||
msgid "Begins:"
|
||||
msgstr "Inicio:"
|
||||
|
||||
msgid "Ends:"
|
||||
msgstr "Final:"
|
||||
|
||||
msgid "Repeated:"
|
||||
msgstr "Repetir:"
|
||||
|
||||
msgid "Action:"
|
||||
msgstr "Acción:"
|
||||
|
||||
msgid "After event:"
|
||||
msgstr "Después del evento:"
|
||||
|
||||
msgid "Location:"
|
||||
msgstr "Ubicación:"
|
||||
|
||||
msgid "Mo"
|
||||
msgstr "Lu"
|
||||
|
||||
msgid "Tu"
|
||||
msgstr "Ma"
|
||||
|
||||
msgid "We"
|
||||
msgstr "Mi"
|
||||
|
||||
msgid "Th"
|
||||
msgstr "Ju"
|
||||
|
||||
msgid "Fr"
|
||||
msgstr "Vi"
|
||||
|
||||
msgid "Sa"
|
||||
msgstr "Sa"
|
||||
|
||||
msgid "Su"
|
||||
msgstr "Do"
|
||||
|
||||
msgid "Set"
|
||||
msgstr "Poner"
|
||||
|
||||
msgid "Services update"
|
||||
msgstr "Actualización servicios"
|
||||
|
||||
msgid "Create folder"
|
||||
msgstr "Crear carpeta"
|
||||
|
||||
msgid "FTP client"
|
||||
msgstr "Cliente FTP"
|
||||
|
||||
msgid "The file size is too large!"
|
||||
msgstr "¡El fichero es demasiado grande!"
|
||||
|
||||
msgid "Connect"
|
||||
msgstr "Conectar"
|
||||
|
||||
msgid "Disconnect"
|
||||
msgstr "Desconectar"
|
||||
|
||||
msgid "Size"
|
||||
msgstr "Tamaño"
|
||||
|
||||
msgid "Date"
|
||||
msgstr "Fecha"
|
||||
|
||||
msgid "Attr."
|
||||
msgstr "Atr."
|
||||
|
||||
msgid "Toggle display position"
|
||||
msgstr "Cambiar la posición de la pantalla"
|
||||
|
||||
msgid "Alternatives"
|
||||
msgstr "Alternativas"
|
||||
|
||||
msgid "Add alternatives"
|
||||
msgstr "Añadir alternativas"
|
||||
|
||||
msgid "DreamOS only!"
|
||||
msgstr "¡Sólo DreamOS!"
|
||||
|
||||
msgid "A similar service is already in this list!"
|
||||
msgstr "¡Un servicio similar ya está en esta lista!"
|
||||
|
||||
msgid ""
|
||||
"Play mode has been changed!\n"
|
||||
"Restart the program to apply the settings."
|
||||
msgstr ""
|
||||
"¡El modo de reproducción se ha cambiado!\n"
|
||||
"Reinicia el programa para aplicar los cambios."
|
||||
|
||||
msgid "Set values for TID, NID and Namespace for correct naming of the picons!"
|
||||
msgstr "¡Poner los valores de TID, NID y Namespace para una correcta nomenclatura de los picons!"
|
||||
|
||||
msgid "Streams detected:"
|
||||
msgstr "Streams encontrados:"
|
||||
|
||||
msgid "Download picons"
|
||||
msgstr "Descargar picons"
|
||||
|
||||
msgid "Errors:"
|
||||
msgstr "Errores:"
|
||||
|
||||
msgid "Use to play streams:"
|
||||
msgstr "Reproductor de streams:"
|
||||
|
||||
msgid "Font in the lists:"
|
||||
msgstr "Fuente en las listas:"
|
||||
|
||||
msgid "Picons size in the lists:"
|
||||
msgstr "Tamaño de los picons en las listas:"
|
||||
|
||||
msgid "Logo size in tooltips:"
|
||||
msgstr "Tamaño de los logos en las listas:"
|
||||
|
||||
msgid "Save as"
|
||||
msgstr "Guardar como"
|
||||
|
||||
msgid "Mark duplicates"
|
||||
msgstr "Marcar duplicados"
|
||||
|
||||
msgid "Load only for selected bouquet"
|
||||
msgstr "Cargar sólo para el bouquet seleccionado"
|
||||
|
||||
msgid "The task is canceled!"
|
||||
msgstr "¡Se ha cancelado la tarea!"
|
||||
|
||||
msgid "Data loading in progress!"
|
||||
msgstr "¡Carga de datos en progreso!"
|
||||
|
||||
msgid "Recordings"
|
||||
msgstr "Grabaciones"
|
||||
|
||||
msgid "Help"
|
||||
msgstr "Ayuda"
|
||||
|
||||
msgid "HTTP API is not activated. Check your settings!"
|
||||
msgstr "HTTP API no activa. ¡Comprueba la configuración!"
|
||||
|
||||
msgid "Add picons"
|
||||
msgstr "Añadir picons"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "Logs"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Título"
|
||||
|
||||
msgid "Time"
|
||||
msgstr "Hora"
|
||||
|
||||
msgid "Length"
|
||||
msgstr "Duración"
|
||||
|
||||
msgid "Additional source"
|
||||
msgstr "Fuente adicional"
|
||||
|
||||
msgid "Automatically set the name selected in the favorites list."
|
||||
msgstr "Poner automáticamente el nombre en la lista de favoritos seleccionada."
|
||||
|
||||
msgid "Playback"
|
||||
msgstr "Reproducción"
|
||||
|
||||
msgid "Audio"
|
||||
msgstr "Audio"
|
||||
|
||||
msgid "Audio Track"
|
||||
msgstr "Pista de audio"
|
||||
|
||||
msgid "Subtitle"
|
||||
msgstr "Subtítulos"
|
||||
|
||||
msgid "Subtitle Track"
|
||||
msgstr "Pista de subtítulos"
|
||||
|
||||
msgid "Aspect ratio"
|
||||
msgstr "Relación de aspecto"
|
||||
|
||||
msgid "This may change the settings of other profiles!"
|
||||
msgstr "¡Esto podría cambiar la configuración de otros perfiles!"
|
||||
|
||||
msgid "Drag the services to the desired picon or picon to the list of selected services."
|
||||
msgstr "Arrastrar los servicios al picon deseado o al picon de la lista de los servicios seleccionados."
|
||||
|
||||
msgid "Sets the profile folder as default to store picons, backups, etc."
|
||||
msgstr "Pone la carpeta del perfil como por defecto para almacenar picons, backups, etc."
|
||||
|
||||
msgid "New sub-bouquet"
|
||||
msgstr "Nuevo sub-bouquet"
|
||||
|
||||
msgid "Mark not presented in Bouquets"
|
||||
msgstr "Marcar los no presentes en los bouquets"
|
||||
|
||||
msgid "Not in Bouquets"
|
||||
msgstr "No en los bouquets"
|
||||
|
||||
msgid "Do not show services present in Bouquets."
|
||||
msgstr "No mostrar los servicios presentes en los bouquets."
|
||||
|
||||
msgid "IPTV services only"
|
||||
msgstr "Sólo servicios IPTV"
|
||||
|
||||
msgid "Display picons"
|
||||
msgstr "Mostrar picons"
|
||||
|
||||
msgid "Alternate layout"
|
||||
msgstr "Diseño alternativo"
|
||||
|
||||
msgid "Layout of elements has been changed!"
|
||||
msgstr "¡El diseño de los elementos ha cambiado!"
|
||||
|
||||
msgid "Restart the program to apply all changes."
|
||||
msgstr "Reinicia el programa para aplicar los cambios."
|
||||
|
||||
msgid "New folder"
|
||||
msgstr "Nueva carpeta"
|
||||
|
||||
msgid "Rename"
|
||||
msgstr "Renombrar"
|
||||
|
||||
msgid "Bookmarks"
|
||||
msgstr "Marcadores"
|
||||
|
||||
msgid "Add bookmark"
|
||||
msgstr "Añadir marcador"
|
||||
|
||||
msgid "All bouquets"
|
||||
msgstr "Todos los bouquets"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (C) 2018-2022 Dmitriy Yefremov
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
#
|
||||
@@ -50,6 +50,9 @@ msgstr "Текущий IP:"
|
||||
msgid "Assign"
|
||||
msgstr "Привязать"
|
||||
|
||||
msgid "Assign file"
|
||||
msgstr "Привязать файл"
|
||||
|
||||
msgid "Bouquet details"
|
||||
msgstr "Сервисы букета"
|
||||
|
||||
@@ -230,6 +233,9 @@ msgstr "Сброс профиля"
|
||||
msgid "Satellites"
|
||||
msgstr "Спутники"
|
||||
|
||||
msgid "Transponders"
|
||||
msgstr "Транспондеры"
|
||||
|
||||
msgid "Satellites.xml file:"
|
||||
msgstr "Файл satellites.xml:"
|
||||
|
||||
@@ -1244,6 +1250,9 @@ msgstr "Выполняется загрузка данных!"
|
||||
msgid "Recordings"
|
||||
msgstr "Записи"
|
||||
|
||||
msgid "Recordings:"
|
||||
msgstr "Записи:"
|
||||
|
||||
msgid "Help"
|
||||
msgstr "Справка"
|
||||
|
||||
@@ -1255,3 +1264,123 @@ msgstr "Добавить пиконы"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "Журнал"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Название"
|
||||
|
||||
msgid "Time"
|
||||
msgstr "Время"
|
||||
|
||||
msgid "Length"
|
||||
msgstr "Длительность"
|
||||
|
||||
msgid "Additional source"
|
||||
msgstr "Дополнительный источник"
|
||||
|
||||
msgid "Automatically set the name selected in the favorites list."
|
||||
msgstr "Автоматическая установка имени из списка избранного."
|
||||
|
||||
msgid "Playback"
|
||||
msgstr "Воспроизведение"
|
||||
|
||||
msgid "Playback:"
|
||||
msgstr "Воспроизведение:"
|
||||
|
||||
msgid "Audio"
|
||||
msgstr "Аудио"
|
||||
|
||||
msgid "Audio Track"
|
||||
msgstr "Аудиодорожка"
|
||||
|
||||
msgid "Subtitle"
|
||||
msgstr "Субтитры"
|
||||
|
||||
msgid "Subtitle Track"
|
||||
msgstr "Дорожка субтитров"
|
||||
|
||||
msgid "Aspect ratio"
|
||||
msgstr "Соотношение сторон"
|
||||
|
||||
msgid "This may change the settings of other profiles!"
|
||||
msgstr "Это может изменить настройки других профилей!"
|
||||
|
||||
msgid "Drag the services to the desired picon or picon to the list of selected services."
|
||||
msgstr "Перетащите сервисы на нужный пикон или пикон на список выбранных сервисов."
|
||||
|
||||
msgid "Sets the profile folder as default to store picons, backups, etc."
|
||||
msgstr "Устанавливает папку профиля по умолчанию для хранения пиконов, резервных копий и т. п."
|
||||
|
||||
msgid "New sub-bouquet"
|
||||
msgstr "Создать суббукет"
|
||||
|
||||
msgid "Mark not presented in Bouquets"
|
||||
msgstr "Отметить отсутствующие в букетах"
|
||||
|
||||
msgid "Not in Bouquets"
|
||||
msgstr "Не в букетах"
|
||||
|
||||
msgid "Do not show services present in Bouquets."
|
||||
msgstr "Не показывать сервисы присутствующие в букетах."
|
||||
|
||||
msgid "IPTV services only"
|
||||
msgstr "Только IPTV сервисы"
|
||||
|
||||
msgid "Display picons"
|
||||
msgstr "Отображать пиконы"
|
||||
|
||||
msgid "Alternate layout"
|
||||
msgstr "Альтернативная компоновка"
|
||||
|
||||
msgid "Layout of elements has been changed!"
|
||||
msgstr "Изменено расположение элементов!"
|
||||
|
||||
msgid "Restart the program to apply all changes."
|
||||
msgstr "Перезапустите программу, чтобы применить все изменения."
|
||||
|
||||
msgid "New folder"
|
||||
msgstr "Создать папку"
|
||||
|
||||
msgid "Rename"
|
||||
msgstr "Переименовать"
|
||||
|
||||
msgid "Bookmarks"
|
||||
msgstr "Закладки"
|
||||
|
||||
msgid "Add bookmark"
|
||||
msgstr "Добавить в закладки"
|
||||
|
||||
msgid "All bouquets"
|
||||
msgstr "Все букеты"
|
||||
|
||||
msgid "Playback from the main list"
|
||||
msgstr "Воспроизведение из основного списка"
|
||||
|
||||
msgid "Enables URL parsing using youtube-dl to get direct links to media."
|
||||
msgstr "Включает анализ URL-адресов с помощью youtube-dl для получения прямых ссылок на медиа."
|
||||
|
||||
msgid "Permissions..."
|
||||
msgstr "Разрешения..."
|
||||
|
||||
msgid "Display EPG in bouquet list"
|
||||
msgstr "Отображать EPG в списке букета"
|
||||
|
||||
msgid "EPG *.dat file:"
|
||||
msgstr "Файл EPG *.dat:"
|
||||
|
||||
msgid "Use HTTP to reload data in the receiver"
|
||||
msgstr "Использовать HTTP для перезагрузки данных в ресивере"
|
||||
|
||||
msgid "Enable picons compression"
|
||||
msgstr "Включить сжатие пиконов"
|
||||
|
||||
msgid "Update interval (sec):"
|
||||
msgstr "Интервал обновления (сек):"
|
||||
|
||||
msgid "Update:"
|
||||
msgstr "Обновлять:"
|
||||
|
||||
msgid "Daily"
|
||||
msgstr "Ежедневно"
|
||||
|
||||
msgid "Assign reference"
|
||||
msgstr "Присвоить ссылку"
|
||||
|
||||
@@ -3,15 +3,15 @@ msgstr ""
|
||||
"Project-Id-Version: DemonEditor\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2020-04-16 15:59+0300\n"
|
||||
"PO-Revision-Date: 2021-06-13 14:54+0300\n"
|
||||
"PO-Revision-Date: 2022-08-27 23:17+0300\n"
|
||||
"Last-Translator: audi06_19 <info@dreamosat-forum.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: tr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Last-Translator: audi06_19 <info@dreamosat-forum.com>\n"
|
||||
"Language-Team: \n"
|
||||
"X-Generator: Poedit 2.4.1\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"Language: tr\n"
|
||||
"X-Generator: Poedit 3.0.1\n"
|
||||
|
||||
msgid "translator-credits"
|
||||
msgstr "audi06_19 <info@dreamosat-forum.com>"
|
||||
@@ -53,6 +53,9 @@ msgstr "Geçerli IP:"
|
||||
msgid "Assign"
|
||||
msgstr "Ata"
|
||||
|
||||
msgid "Assign file"
|
||||
msgstr "Dosya ata"
|
||||
|
||||
msgid "Bouquet details"
|
||||
msgstr "Buket detayları"
|
||||
|
||||
@@ -206,8 +209,8 @@ msgstr "Mevcut veri yolu:"
|
||||
msgid "Data:"
|
||||
msgstr "Veri:"
|
||||
|
||||
msgid "Enigma2 channel and satellite list editor for GNU/Linux."
|
||||
msgstr "GNU/Linux için Enigma2 kanalı ve uydu listesi editörü."
|
||||
msgid "Enigma2 channel and satellite list editor."
|
||||
msgstr "Enigma2 kanal ve uydu listesi düzenleyicisi."
|
||||
|
||||
msgid "Host:"
|
||||
msgstr "Ana bilgisayar:"
|
||||
@@ -233,6 +236,9 @@ msgstr "Profili sıfırla"
|
||||
msgid "Satellites"
|
||||
msgstr "Uydular"
|
||||
|
||||
msgid "Transponders"
|
||||
msgstr "Transponders"
|
||||
|
||||
msgid "Satellites.xml file:"
|
||||
msgstr "Satellites.xml dosyası:"
|
||||
|
||||
@@ -299,8 +305,8 @@ msgstr "Piconların adı biçimi:"
|
||||
msgid "Resize:"
|
||||
msgstr "Yeniden boyutlandır:"
|
||||
|
||||
msgid "Current picons path:"
|
||||
msgstr "Geçerli picon yolları:"
|
||||
msgid "Current picons path"
|
||||
msgstr "Mevcut piconların yolu"
|
||||
|
||||
msgid "Receiver picons path:"
|
||||
msgstr "Alıcı picon yolu:"
|
||||
@@ -1269,3 +1275,144 @@ msgstr "Yalnızca seçilen buket için yükle"
|
||||
|
||||
msgid "The task is canceled!"
|
||||
msgstr "Görev iptal edildi!"
|
||||
|
||||
msgid "Data loading in progress!"
|
||||
msgstr "Veri yükleme devam ediyor!"
|
||||
|
||||
msgid "Recordings"
|
||||
msgstr "Kayıtlar"
|
||||
|
||||
msgid "Recordings:"
|
||||
msgstr "Kayıtlar:"
|
||||
|
||||
msgid "Help"
|
||||
msgstr "Yardım"
|
||||
|
||||
msgid "HTTP API is not activated. Check your settings!"
|
||||
msgstr "HTTP API etkinleştirilmedi. Ayarlarınızı kontrol edin!"
|
||||
|
||||
msgid "Add picons"
|
||||
msgstr "Piconlar ekle"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "Loglar"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Başlık"
|
||||
|
||||
msgid "Time"
|
||||
msgstr "Zaman"
|
||||
|
||||
msgid "Length"
|
||||
msgstr "Uzunluk"
|
||||
|
||||
msgid "Additional source"
|
||||
msgstr "Ek kaynak"
|
||||
|
||||
msgid "Automatically set the name selected in the favorites list."
|
||||
msgstr "Favoriler listesinde seçilen adı otomatik olarak ayarlayın."
|
||||
|
||||
msgid "Playback"
|
||||
msgstr "Oynatım"
|
||||
|
||||
msgid "Playback:"
|
||||
msgstr "Oynatım:"
|
||||
|
||||
msgid "Audio"
|
||||
msgstr "Ses"
|
||||
|
||||
msgid "Audio Track"
|
||||
msgstr "Ses parçası"
|
||||
|
||||
msgid "Subtitle"
|
||||
msgstr "Altyazı"
|
||||
|
||||
msgid "Subtitle Track"
|
||||
msgstr "Altyazı Parçası"
|
||||
|
||||
msgid "Aspect ratio"
|
||||
msgstr "En boy oranı"
|
||||
|
||||
msgid "This may change the settings of other profiles!"
|
||||
msgstr "Bu, diğer profillerin ayarlarını değiştirebilir!"
|
||||
|
||||
msgid "Drag the services to the desired picon or picon to the list of selected services."
|
||||
msgstr "Hizmetleri istediğiniz simgeye veya simgeyi seçili hizmetler listesine sürükleyin."
|
||||
|
||||
msgid "Sets the profile folder as default to store picons, backups, etc."
|
||||
msgstr "Picon'ları, yedekleri vb. depolamak için profil klasörünü varsayılan olarak ayarlar."
|
||||
|
||||
msgid "New sub-bouquet"
|
||||
msgstr "Yeni alt buket"
|
||||
|
||||
msgid "Mark not presented in Bouquets"
|
||||
msgstr "Buketlerde olmayanları işaretleyin"
|
||||
|
||||
msgid "Not in Bouquets"
|
||||
msgstr "Buketlerde Değil"
|
||||
|
||||
msgid "Do not show services present in Bouquets."
|
||||
msgstr "Buketlerde bulunan hizmetleri göstermeyin."
|
||||
|
||||
msgid "IPTV services only"
|
||||
msgstr "Yalnızca IPTV hizmetleri"
|
||||
|
||||
msgid "Display picons"
|
||||
msgstr "Display piconlar"
|
||||
|
||||
msgid "Alternate layout"
|
||||
msgstr "Alternatif düzen"
|
||||
|
||||
msgid "Layout of elements has been changed!"
|
||||
msgstr "Öğelerin düzeni değiştirildi!"
|
||||
|
||||
msgid "Restart the program to apply all changes."
|
||||
msgstr "Tüm değişiklikleri uygulamak için programı yeniden başlatın."
|
||||
|
||||
msgid "New folder"
|
||||
msgstr "Yeni dosya"
|
||||
|
||||
msgid "Rename"
|
||||
msgstr "Düzenle"
|
||||
|
||||
msgid "Bookmarks"
|
||||
msgstr "Yer imleri"
|
||||
|
||||
msgid "Add bookmark"
|
||||
msgstr "Yer imleri ekle"
|
||||
|
||||
msgid "All bouquets"
|
||||
msgstr "Tüm buketler"
|
||||
|
||||
msgid "Playback from the main list"
|
||||
msgstr "Ana listeden oynatma"
|
||||
|
||||
msgid "Enables URL parsing using youtube-dl to get direct links to media."
|
||||
msgstr "Medyaya doğrudan bağlantılar almak için youtube-dl kullanarak URL ayrıştırmayı etkinleştirir."
|
||||
|
||||
msgid "Permissions..."
|
||||
msgstr "İzinler..."
|
||||
|
||||
msgid "Display EPG in bouquet list"
|
||||
msgstr "EPG'yi buket listesinde göster"
|
||||
|
||||
msgid "EPG *.dat file:"
|
||||
msgstr "EPG *.dat dosyası:"
|
||||
|
||||
msgid "Use HTTP to reload data in the receiver"
|
||||
msgstr "Alıcıdaki verileri yeniden yüklemek için HTTP kullanın"
|
||||
|
||||
msgid "Enable picons compression"
|
||||
msgstr "Picon sıkıştırmayı etkinleştir"
|
||||
|
||||
msgid "Update interval (sec):"
|
||||
msgstr "Güncelleme aralığı (sn):"
|
||||
|
||||
msgid "Update:"
|
||||
msgstr "Güncelleme:"
|
||||
|
||||
msgid "Daily"
|
||||
msgstr "Günlük"
|
||||
|
||||
msgid "Assign reference"
|
||||
msgstr "Referans ata"
|
||||
|
||||
10
start.py
10
start.py
@@ -4,21 +4,21 @@ import os
|
||||
|
||||
def update_icon():
|
||||
need_update = False
|
||||
icon_name = "DemonEditor.desktop"
|
||||
icon_name = "demon-editor.desktop"
|
||||
|
||||
with open(icon_name, "r") as f:
|
||||
with open(icon_name, "r", encoding="utf-8") as f:
|
||||
lines = f.readlines()
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith("Icon="):
|
||||
icon_path = line.lstrip("Icon=")
|
||||
current_path = "{}/app/ui/icons/hicolor/96x96/apps/demon-editor.png".format(os.getcwd())
|
||||
current_path = f"{os.getcwd()}/app/ui/icons/hicolor/96x96/apps/demon-editor.png"
|
||||
if icon_path != current_path:
|
||||
need_update = True
|
||||
lines[i] = "Icon={}\n".format(current_path)
|
||||
lines[i] = f"Icon={current_path}\n"
|
||||
break
|
||||
|
||||
if need_update:
|
||||
with open(icon_name, "w") as f:
|
||||
with open(icon_name, "w", encoding="utf-8") as f:
|
||||
f.writelines(lines)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user