mirror of
https://github.com/DYefremov/DemonEditor.git
synced 2026-05-08 08:06:40 +02:00
Compare commits
376 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
907415a2c9 | ||
|
|
f9afffbdb3 | ||
|
|
326856b1e3 | ||
|
|
865a326fe9 | ||
|
|
dd2661c6c9 | ||
|
|
623e5a17f5 | ||
|
|
18c1fa736b | ||
|
|
0e6142c751 | ||
|
|
c274c265c6 | ||
|
|
777c09c9b8 | ||
|
|
12a68a4dbb | ||
|
|
eab869d4d5 | ||
|
|
aec76eec45 | ||
|
|
bdab316ba7 | ||
|
|
771ecb696f | ||
|
|
8993fbed5d | ||
|
|
f3cad81a7d | ||
|
|
c1cf343f69 | ||
|
|
f998f66a35 | ||
|
|
9eb4cdc574 | ||
|
|
20120e0db4 | ||
|
|
3446bb225c | ||
|
|
191975bd14 | ||
|
|
f4be52a202 | ||
|
|
ba2272cf13 | ||
|
|
7bd3fcd9a6 | ||
|
|
e54719ca2c | ||
|
|
0c1c44c866 | ||
|
|
06b82251ef | ||
|
|
fb929ec723 | ||
|
|
fd1c1bfd6e | ||
|
|
c48a08b239 | ||
|
|
b647b0a338 | ||
|
|
9b608eeb74 | ||
|
|
07e55b3f1e | ||
|
|
7e639f5637 | ||
|
|
5570d47cae | ||
|
|
5c49c0d123 | ||
|
|
ee6dd511b5 | ||
|
|
1f847233b3 | ||
|
|
835e1af8e4 | ||
|
|
50e0d8b66a | ||
|
|
fb0789664a | ||
|
|
5dd39492f2 | ||
|
|
33be9f21a2 | ||
|
|
7acc9ae74f | ||
|
|
d492022232 | ||
|
|
b034995130 | ||
|
|
f037b3554d | ||
|
|
7f1f27da57 | ||
|
|
d7f3afecb0 | ||
|
|
f309005c52 | ||
|
|
25661816e7 | ||
|
|
a2652cef4b | ||
|
|
adbc9ad322 | ||
|
|
2dc8611294 | ||
|
|
72bfd21056 | ||
|
|
65ef018f81 | ||
|
|
1236c5ebc9 | ||
|
|
f0011ebcf2 | ||
|
|
392e94e7ba | ||
|
|
c6de18271d | ||
|
|
71a65242c1 | ||
|
|
4efc956870 | ||
|
|
a605fdd545 | ||
|
|
285c1cae69 | ||
|
|
2e937a42a3 | ||
|
|
e208cf4656 | ||
|
|
3db82e3e18 | ||
|
|
38e9a85694 | ||
|
|
438e9c10d4 | ||
|
|
920fa01159 | ||
|
|
d2787364cd | ||
|
|
839c0fae23 | ||
|
|
f87548e12e | ||
|
|
463702c371 | ||
|
|
0b84a81439 | ||
|
|
6b68740961 | ||
|
|
92aa2400f6 | ||
|
|
2cf4e5b756 | ||
|
|
6cfa68e219 | ||
|
|
138aa54b44 | ||
|
|
3e1a3d1595 | ||
|
|
b833458c45 | ||
|
|
d1e88be1cc | ||
|
|
72e128aeb9 | ||
|
|
457b4e4645 | ||
|
|
076243b0ac | ||
|
|
e5ff185791 | ||
|
|
2b6b0dd827 | ||
|
|
a11fdd683e | ||
|
|
636bc5c52f | ||
|
|
90d64d46c7 | ||
|
|
115f77108c | ||
|
|
2082f8e973 | ||
|
|
7e586bf0a6 | ||
|
|
c5c4823534 | ||
|
|
6235519cf9 | ||
|
|
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 |
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2023 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
|
||||
|
||||
73
README.md
73
README.md
@@ -1,43 +1,42 @@
|
||||
# <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.
|
||||
* **Ctrl + C** - only in services list.
|
||||
Clipboard is **"rubber"**. There is an accumulation before the insertion!
|
||||
* **Ctrl + C** - only in services list.
|
||||
* **Ctrl + Insert** - copies the selected channels from the main list to the bouquet
|
||||
beginning or inserts (creates) a new bouquet.
|
||||
* **Ctrl + BackSpace** - copies the selected channels from the main list to the bouquet end.
|
||||
* **Ctrl + E** - edit.
|
||||
* **Ctrl + R, F2** - rename.
|
||||
* **Ctrl + 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 +50,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 +72,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 +106,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-2023 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,8 +33,9 @@ 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 pathlib import Path
|
||||
from telnetlib import Telnet
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlencode, quote
|
||||
@@ -16,14 +45,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):
|
||||
@@ -60,11 +90,11 @@ class UtfFTP(FTP):
|
||||
while 1:
|
||||
line = fp.readline(self.maxline + 1)
|
||||
if len(line) > self.maxline:
|
||||
msg = "UtfFTP [retrlines] error: got more than {} bytes".format(self.maxline)
|
||||
msg = f"UtfFTP [retrlines] error: got more than {self.maxline} bytes"
|
||||
log(msg)
|
||||
raise Error(msg)
|
||||
if self.debugging > 2:
|
||||
log('UtfFTP [retrlines] *retr* {}'.format(repr(line)))
|
||||
log(f"UtfFTP [retrlines] *retr* {repr(line)}")
|
||||
if not line:
|
||||
break
|
||||
if line[-2:] == CRLF:
|
||||
@@ -83,20 +113,21 @@ 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)
|
||||
|
||||
resp = self.download_binary(name, f)
|
||||
msg = f"Downloading file: {name}. Status: {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 = Path(p_dst).parent.as_posix()
|
||||
|
||||
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"
|
||||
@@ -556,6 +650,16 @@ class HttpAPI:
|
||||
|
||||
class Remote(str, Enum):
|
||||
""" Args for HttpRequestType [REMOTE] class. """
|
||||
ONE = "2"
|
||||
TWO = "3"
|
||||
THREE = "4"
|
||||
FOUR = "5"
|
||||
FIVE = "6"
|
||||
SIX = "7"
|
||||
SEVEN = "8"
|
||||
EIGHT = "9"
|
||||
NINE = "10"
|
||||
ZERO = "11"
|
||||
UP = "103"
|
||||
LEFT = "105"
|
||||
RIGHT = "106"
|
||||
@@ -564,10 +668,18 @@ class HttpAPI:
|
||||
EXIT = "174"
|
||||
OK = "352"
|
||||
INFO = "358"
|
||||
EPG = "365"
|
||||
TV = "377"
|
||||
RADIO = "385"
|
||||
AUDIO = "392"
|
||||
FAV = "393"
|
||||
RED = "398"
|
||||
GREEN = "399"
|
||||
YELLOW = "400"
|
||||
BLUE = "401"
|
||||
CH_UP = "402"
|
||||
CH_DOWN = "403"
|
||||
NEXT = "407"
|
||||
BACK = "412"
|
||||
|
||||
class Power(str, Enum):
|
||||
@@ -583,6 +695,8 @@ class HttpAPI:
|
||||
Request.POWER,
|
||||
Request.VOL,
|
||||
Request.EPG,
|
||||
Request.EPG_NOW,
|
||||
Request.EPG_MULTI,
|
||||
Request.TIMER,
|
||||
Request.RECORDINGS,
|
||||
Request.N_ZAP}
|
||||
@@ -594,7 +708,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 +720,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 +740,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 +777,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 +790,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 +810,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 +869,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 +877,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 +892,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)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2023 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,16 +62,15 @@ 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)
|
||||
|
||||
|
||||
@run_task
|
||||
def write_bouquets(path, bouquets, s_type, force_bq_names=False):
|
||||
def write_bouquets(path, bouquets, s_type, force_bq_names=False, blacklist=None):
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
BouquetsWriter(path, bouquets, force_bq_names).write()
|
||||
BouquetsWriter(path, bouquets, force_bq_names, blacklist).write()
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
write_neutrino_bouquets(path, bouquets)
|
||||
|
||||
|
||||
@@ -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-2023 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
|
||||
@@ -47,27 +47,30 @@ class BouquetsWriter:
|
||||
If "force_bq_names" then naming the files using the name of the bouquet.
|
||||
Some images may have problems displaying the favorites list!
|
||||
"""
|
||||
_SERVICE = '#SERVICE 1:7:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
|
||||
_SERVICE = '#SERVICE 1:{}:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
|
||||
_MARKER = "#SERVICE 1:64:{:X}:0:0:0:0:0:0:0::{}\n"
|
||||
_SPACE = "#SERVICE 1:832:D:{}:0:0:0:0:0:0:\n"
|
||||
_LOCKED = '1:{}:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet'
|
||||
_ALT = '#SERVICE 1:134:1:0:0:0:0:0:0:0:FROM BOUQUET "{}" ORDER BY bouquet\n'
|
||||
_ALT_PAT = r"[<>:\"/\\|?*\-\s]"
|
||||
|
||||
def __init__(self, path, bouquets, force_bq_names=False):
|
||||
def __init__(self, path, bouquets, force_bq_names=False, blacklist=None):
|
||||
self._path = path
|
||||
self._bouquets = bouquets
|
||||
self._force_bq_names = force_bq_names
|
||||
self._black_list = set() if blacklist is None else blacklist
|
||||
|
||||
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 +79,35 @@ 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)
|
||||
bq_type = 2 if bqs.type == BqType.RADIO.value else 1
|
||||
# Parental lock.
|
||||
locked = self._LOCKED.format(ServiceType.SERVICE, bq_type, bq_name, bqs.type)
|
||||
self._black_list.add(locked) if bq.locked else self._black_list.discard(locked)
|
||||
# Hiding.
|
||||
s_type = ServiceType.HIDDEN if bq.hidden else ServiceType.BOUQUET
|
||||
line.append(self._SERVICE.format(s_type, bq_type, 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 +145,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)
|
||||
|
||||
|
||||
@@ -139,21 +165,25 @@ class ServiceType(Enum):
|
||||
SERVICE = "0"
|
||||
BOUQUET = "7" # Sub bouquet.
|
||||
MARKER = "64"
|
||||
SPACE = "832" # Hidden marker.
|
||||
SPACE = "832"
|
||||
ALT = "134" # Alternatives.
|
||||
UDP = "256"
|
||||
HIDDEN = "519" # Skip, hide.
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
log("Error. No matching service type [{} {}] was found.".format(cls.__name__, value))
|
||||
return cls.SERVICE
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
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"]
|
||||
@@ -180,26 +210,31 @@ class BouquetsReader:
|
||||
for line in file.readlines():
|
||||
if "#SERVICE" in line:
|
||||
name = re.match(self._BQ_PAT, line)
|
||||
s_data = line.split(":")
|
||||
s_type = ServiceType(s_data[1])
|
||||
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:
|
||||
real_b_names[rb_name] = 0
|
||||
# Locked, hidden.
|
||||
s_data[:2] = "10"
|
||||
locked = ":".join(s_data).rstrip()
|
||||
hidden = s_type is ServiceType.HIDDEN
|
||||
|
||||
bouquets[2].append(Bouquet(rb_name, bq_type, services, None, None, b_name))
|
||||
bouquets[2].append(Bouquet(rb_name, bq_type, services, locked, hidden, b_name))
|
||||
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())
|
||||
if len(s_data) == 12 and s_type is ServiceType.MARKER:
|
||||
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 +246,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 +261,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 +289,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
|
||||
@@ -38,8 +38,9 @@ from app.ui.uicommons import IPTV_ICON
|
||||
|
||||
# url, description, urlkey, account, usrname, psw, s_type, iconsrc, iconsrc_b, group
|
||||
NEUTRINO_FAV_ID_FORMAT = "{}::{}::{}::{}::{}::{}::{}::{}::{}::{}"
|
||||
ENIGMA2_FAV_ID_FORMAT = " {}:{}:{}:{:X}:{:X}:{:X}:{:X}:0:0:0:{}:{}\n#DESCRIPTION: {}\n"
|
||||
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 = "{}_{}_{: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,11 @@ 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, st_type=None, s_id=0, srv_type=1):
|
||||
st_type = st_type or StreamType.NONE_TS.value
|
||||
params = params or (0, 0, 0, 0)
|
||||
return PICON_FORMAT.format(st_type, s_id, srv_type, *params)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2023 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,6 @@ import os
|
||||
from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT
|
||||
from app.eparser.neutrino import KSP, SP, get_xml_attributes, get_attributes, API_VER
|
||||
from app.eparser.neutrino.nxml import XmlHandler, NeutrinoDocument
|
||||
from app.ui.uicommons import LOCKED_ICON, HIDE_ICON
|
||||
from ..ecommons import Bouquets, Bouquet, BouquetService, BqServiceType, PROVIDER, BqType
|
||||
|
||||
_FILE = "bouquets.xml"
|
||||
@@ -72,8 +71,8 @@ def parse_bouquets(file, name, bq_type):
|
||||
bouquets[2].append(Bouquet(name=bq_name,
|
||||
type=bq_type,
|
||||
services=services,
|
||||
locked=LOCKED_ICON if locked == "1" else None,
|
||||
hidden=HIDE_ICON if hidden == "1" else None,
|
||||
locked=locked == "1",
|
||||
hidden=hidden == "1",
|
||||
file=SP.join("{}{}{}".format(k, KSP, v) for k, v in bq_attrs.items())))
|
||||
|
||||
if BqType(bq_type) is BqType.BOUQUET:
|
||||
|
||||
@@ -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__":
|
||||
|
||||
186
app/settings.py
186
app/settings.py
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2023 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,25 +61,30 @@ 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/",
|
||||
"/media/usb/picon/",
|
||||
"/media/mmc/picon/",
|
||||
"/media/cf/picon/")
|
||||
"/media/cf/picon/",
|
||||
"/hdd/picon/",
|
||||
"/usb/picon/")
|
||||
# Neutrino.
|
||||
NEUTRINO_BOX_SERVICES_PATH = "/var/tuxbox/config/zapit/"
|
||||
NEUTRINO_BOX_SATELLITE_PATH = "/var/tuxbox/config/"
|
||||
NEUTRINO_BOX_PICON_PATH = "/usr/share/tuxbox/neutrino/icons/logo/"
|
||||
NEUTRINO_BOX_PICON_PATHS = ("/usr/share/tuxbox/neutrino/icons/logo/",)
|
||||
# Paths.
|
||||
BACKUP_PATH = "{}backup{}".format(DATA_PATH, SEP)
|
||||
PICON_PATH = "{}picons{}".format(DATA_PATH, SEP)
|
||||
BACKUP_PATH = f"{DATA_PATH}backup{SEP}"
|
||||
PICON_PATH = f"{DATA_PATH}picons{SEP}"
|
||||
|
||||
DEFAULT_PROFILE = "default"
|
||||
BACKUP_BEFORE_DOWNLOADING = True
|
||||
BACKUP_BEFORE_SAVE = True
|
||||
V5_SUPPORT = False
|
||||
UNLIMITED_COPY_BUFFER = False
|
||||
EXTENSIONS_SUPPORT = False
|
||||
FORCE_BQ_NAMES = False
|
||||
HTTP_API_SUPPORT = True
|
||||
ENABLE_YT_DL = False
|
||||
@@ -92,10 +97,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 +115,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 +140,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 +159,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 +285,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 +381,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 +447,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 +456,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 +466,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 +523,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 +550,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
|
||||
@@ -529,6 +610,22 @@ class Settings:
|
||||
def v5_support(self, value):
|
||||
self._settings["v5_support"] = value
|
||||
|
||||
@property
|
||||
def unlimited_copy_buffer(self):
|
||||
return self._settings.get("unlimited_copy_buffer", Defaults.UNLIMITED_COPY_BUFFER.value)
|
||||
|
||||
@unlimited_copy_buffer.setter
|
||||
def unlimited_copy_buffer(self, value):
|
||||
self._settings["unlimited_copy_buffer"] = value
|
||||
|
||||
@property
|
||||
def extensions_support(self):
|
||||
return self._settings.get("extensions_support", Defaults.EXTENSIONS_SUPPORT.value)
|
||||
|
||||
@extensions_support.setter
|
||||
def extensions_support(self, value):
|
||||
self._settings["extensions_support"] = value
|
||||
|
||||
@property
|
||||
def force_bq_names(self):
|
||||
return self._settings.get("force_bq_names", Defaults.FORCE_BQ_NAMES.value)
|
||||
@@ -569,14 +666,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 +765,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 +816,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 +829,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 +853,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 +898,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 +920,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
|
||||
|
||||
401
app/tools/epg.py
401
app/tools/epg.py
@@ -1,45 +1,369 @@
|
||||
""" Module for working with epg.dat file """
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 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 re
|
||||
import shutil
|
||||
import struct
|
||||
from datetime import datetime
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
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 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", "start", "end", "length", "desc", "event_data"])
|
||||
EpgEvent.__new__.__defaults__ = ("N/A", "N/A", 0, 0, 0, "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 = max(filter(lambda s: s.start < utc, srv.events), key=lambda x: x.start, default=None)
|
||||
if ev:
|
||||
start = datetime.fromtimestamp(ev.start) + offset
|
||||
end_time = datetime.fromtimestamp(ev.duration) + offset
|
||||
start = start.timestamp()
|
||||
end_time = end_time.timestamp()
|
||||
|
||||
for n in srv.names:
|
||||
events[n] = EpgEvent(n, ev.title, start, end_time, int(ev.duration), 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:
|
||||
@@ -51,32 +375,39 @@ class ChannelsParser:
|
||||
refs = []
|
||||
dom = parse(path)
|
||||
description = "".join(n.data + "\n" for n in dom.childNodes if n.nodeType == Node.COMMENT_NODE)
|
||||
pos_pat = re.compile(r"^\d+\.\d+[EW]$")
|
||||
|
||||
for elem in dom.getElementsByTagName("channels"):
|
||||
c_count = 0
|
||||
comment_count = 0
|
||||
current_data = ""
|
||||
data = ""
|
||||
ch_id = None
|
||||
pos = None
|
||||
ch_type = BqServiceType.DEFAULT
|
||||
|
||||
if elem.hasChildNodes():
|
||||
for n in elem.childNodes:
|
||||
if n.nodeType == Node.ELEMENT_NODE:
|
||||
ch_id = n.getAttribute("id")
|
||||
|
||||
if n.nodeType == Node.COMMENT_NODE:
|
||||
c_count += 1
|
||||
comment_count += 1
|
||||
txt = n.data.strip()
|
||||
|
||||
if re.match(pos_pat, txt):
|
||||
pos = txt
|
||||
|
||||
if comment_count:
|
||||
comment_count -= 1
|
||||
else:
|
||||
ref_data = current_data.split(":")
|
||||
refs.append(BouquetService(name=txt,
|
||||
type=BqServiceType.DEFAULT,
|
||||
data="{}:{}:{}:{}".format(*ref_data[3:7]).upper(),
|
||||
num="{}:{}:{}".format(*ref_data[3:6]).upper()))
|
||||
refs.append(BouquetService(name=txt, type=ch_type, data=data.upper(), num=(pos, ch_id)))
|
||||
|
||||
if n.hasChildNodes():
|
||||
for s_node in n.childNodes:
|
||||
if s_node.nodeType == Node.TEXT_NODE:
|
||||
comment_count -= 1
|
||||
current_data = s_node.data
|
||||
data = s_node.data
|
||||
return refs, description
|
||||
|
||||
@staticmethod
|
||||
@@ -90,14 +421,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-2023 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):
|
||||
@@ -62,6 +63,7 @@ class Player(Gtk.DrawingArea):
|
||||
parent = widget.get_parent()
|
||||
parent.connect("play", self.on_play)
|
||||
parent.connect("stop", self.on_stop)
|
||||
parent.connect("pause", self.on_pause)
|
||||
self.show()
|
||||
|
||||
def get_play_mode(self):
|
||||
@@ -106,6 +108,9 @@ class Player(Gtk.DrawingArea):
|
||||
def on_stop(self, widget, state):
|
||||
self.stop()
|
||||
|
||||
def on_pause(self, widget, state):
|
||||
self.pause()
|
||||
|
||||
def on_release(self, widget, state):
|
||||
self.release()
|
||||
|
||||
@@ -115,22 +120,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 +175,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 +195,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 +206,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
|
||||
@@ -230,7 +245,7 @@ class MpvPlayer(Player):
|
||||
self._is_playing = True
|
||||
|
||||
def pause(self):
|
||||
pass
|
||||
self._player.pause = not self._player.pause
|
||||
|
||||
def set_time(self, time):
|
||||
pass
|
||||
@@ -243,6 +258,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 +284,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 +317,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:
|
||||
@@ -310,7 +334,11 @@ class GstPlayer(Player):
|
||||
self._is_playing = False
|
||||
|
||||
def pause(self):
|
||||
self._player.set_state(self.STATE.PAUSED)
|
||||
state = self._player.get_state(self.STATE.NULL).state
|
||||
if state == self.STATE.PLAYING:
|
||||
self._player.set_state(self.STATE.PAUSED)
|
||||
elif state == self.STATE.PAUSED:
|
||||
self._player.set_state(self.STATE.PLAYING)
|
||||
|
||||
def set_time(self, time):
|
||||
pass
|
||||
@@ -356,7 +384,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 +392,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 +406,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 +485,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 +509,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 +534,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 +546,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 +568,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__":
|
||||
|
||||
@@ -16,17 +16,17 @@
|
||||
# <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from ctypes import *
|
||||
import ctypes.util
|
||||
import threading
|
||||
import os
|
||||
import sys
|
||||
from warnings import warn
|
||||
from functools import partial, wraps
|
||||
from contextlib import contextmanager
|
||||
import collections
|
||||
import ctypes.util
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
from contextlib import contextmanager
|
||||
from ctypes import *
|
||||
from functools import partial, wraps
|
||||
from warnings import warn
|
||||
|
||||
if os.name == 'nt':
|
||||
dll = ctypes.util.find_library('mpv-1.dll')
|
||||
@@ -569,10 +569,13 @@ _mpv_free_node_contents = backend.mpv_free_node_contents
|
||||
backend.mpv_create.restype = MpvHandle
|
||||
_mpv_create = backend.mpv_create
|
||||
|
||||
_API_VER = _mpv_client_api_version()[0]
|
||||
|
||||
_handle_func('mpv_destroy' if _API_VER > 1 else 'mpv_detach_destroy', [], None, errcheck=None)
|
||||
_handle_func('mpv_create_client', [c_char_p], MpvHandle, notnull_errcheck)
|
||||
_handle_func('mpv_client_name', [], c_char_p, errcheck=None)
|
||||
_handle_func('mpv_initialize', [], c_int, ec_errcheck)
|
||||
_handle_func('mpv_detach_destroy', [], None, errcheck=None)
|
||||
|
||||
_handle_func('mpv_terminate_destroy', [], None, errcheck=None)
|
||||
_handle_func('mpv_load_config_file', [c_char_p], c_int, ec_errcheck)
|
||||
_handle_func('mpv_get_time_us', [], c_ulonglong, errcheck=None)
|
||||
@@ -608,28 +611,6 @@ _handle_func('mpv_get_wakeup_pipe', [], c_int, errcheck=None)
|
||||
|
||||
_handle_func('mpv_stream_cb_add_ro', [c_char_p, c_void_p, StreamOpenFn], c_int, ec_errcheck)
|
||||
|
||||
# Disabled for compatibility with the old version of mpv!!!
|
||||
# _handle_func('mpv_render_context_create', [MpvRenderCtxHandle, MpvHandle, POINTER(MpvRenderParam)], c_int, ec_errcheck, ctx=None)
|
||||
# _handle_func('mpv_render_context_set_parameter', [MpvRenderParam], c_int, ec_errcheck, ctx=MpvRenderCtxHandle)
|
||||
# _handle_func('mpv_render_context_get_info', [MpvRenderParam], c_int, ec_errcheck, ctx=MpvRenderCtxHandle)
|
||||
# _handle_func('mpv_render_context_set_update_callback', [RenderUpdateFn, c_void_p], None, errcheck=None, ctx=MpvRenderCtxHandle)
|
||||
# _handle_func('mpv_render_context_update', [], c_int64, errcheck=None, ctx=MpvRenderCtxHandle)
|
||||
# _handle_func('mpv_render_context_render', [POINTER(MpvRenderParam)], c_int, ec_errcheck, ctx=MpvRenderCtxHandle)
|
||||
# _handle_func('mpv_render_context_report_swap', [], None, errcheck=None, ctx=MpvRenderCtxHandle)
|
||||
# _handle_func('mpv_render_context_free', [], None, errcheck=None, ctx=MpvRenderCtxHandle)
|
||||
|
||||
|
||||
# Deprecated in v0.29.0 and may disappear eventually
|
||||
if hasattr(backend, 'mpv_get_sub_api'):
|
||||
_handle_func('mpv_get_sub_api', [MpvSubApi], c_void_p, notnull_errcheck, deprecated=True)
|
||||
|
||||
_handle_gl_func('mpv_opengl_cb_set_update_callback', [OpenGlCbUpdateFn, c_void_p], deprecated=True)
|
||||
_handle_gl_func('mpv_opengl_cb_init_gl', [c_char_p, OpenGlCbGetProcAddrFn, c_void_p], c_int, deprecated=True)
|
||||
_handle_gl_func('mpv_opengl_cb_draw', [c_int, c_int, c_int], c_int, deprecated=True)
|
||||
_handle_gl_func('mpv_opengl_cb_render', [c_int, c_int], c_int, deprecated=True)
|
||||
_handle_gl_func('mpv_opengl_cb_report_flip', [c_ulonglong], c_int, deprecated=True)
|
||||
_handle_gl_func('mpv_opengl_cb_uninit_gl', [], c_int, deprecated=True)
|
||||
|
||||
|
||||
def _mpv_coax_proptype(value, proptype=str):
|
||||
"""Intelligently coax the given python value into something that can be understood as a proptype property."""
|
||||
@@ -934,7 +915,7 @@ class MPV(object):
|
||||
self._message_handlers[target](*args)
|
||||
|
||||
if eid == MpvEventID.SHUTDOWN:
|
||||
_mpv_detach_destroy(self._event_handle)
|
||||
_mpv_destroy(self._event_handle) if _API_VER > 1 else _mpv_detach_destroy(self._event_handle)
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -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,9 +57,9 @@ 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).*")
|
||||
_FILE_PATTERN = re.compile(b"\\s+(\\w+\\.png).*")
|
||||
|
||||
def __init__(self, picon_ids=set(), appender=log):
|
||||
self._perm_links = {}
|
||||
@@ -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,9 @@ class PiconsCzDownloader:
|
||||
"picontransparentdark": "td220",
|
||||
"piconoled": "o96",
|
||||
"piconblack80": "b50",
|
||||
"piconblack3d": "b50"
|
||||
"piconblack3d": "b50",
|
||||
"piconwin11": "win11220",
|
||||
"piconSNPtransparent": "t50"
|
||||
}
|
||||
|
||||
def get_name_map(self):
|
||||
@@ -485,20 +487,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 +505,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 +519,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>
|
||||
@@ -152,6 +176,8 @@
|
||||
<attribute name="action">app.on_logs_show</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section id="extension_section">
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">FTP client</attribute>
|
||||
@@ -260,10 +286,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 +358,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>
|
||||
@@ -342,6 +392,8 @@
|
||||
<attribute name="action">app.on_logs_show</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section id="mac_extension_section">
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">FTP client</attribute>
|
||||
@@ -368,7 +420,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-2023 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.settings import SettingsType, SEP
|
||||
from app.commons import run_idle, get_size_from_bytes
|
||||
from app.settings import SettingsType, SEP, IS_DARWIN
|
||||
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, HeaderBar
|
||||
|
||||
|
||||
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 or IS_DARWIN:
|
||||
header_bar = HeaderBar()
|
||||
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)
|
||||
for file in filter(lambda f: f != "satellites.xml" and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
|
||||
# Backup files in data dir(skipping dirs and *.xml).
|
||||
for file in filter(lambda f: not f.endswith(".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
|
||||
@@ -237,8 +263,8 @@ def restore_data(src, dst):
|
||||
|
||||
|
||||
def clear_data_path(path):
|
||||
""" Clearing data at the specified path excluding satellites.xml file """
|
||||
for file in filter(lambda f: f != "satellites.xml" and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
|
||||
""" Clearing data at the specified path excluding *.xml file. """
|
||||
for file in filter(lambda f: not f.endswith(".xml") and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
|
||||
os.remove(os.path.join(path, 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>
|
||||
|
||||
3704
app/ui/control.glade
3704
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-2023 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,24 +69,52 @@ 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")
|
||||
self._screenshot_button_box = builder.get_object("screenshot_button_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):
|
||||
# Remote controller actions.
|
||||
app.set_action("on_one", lambda a, v: self.on_remote_action(HttpAPI.Remote.ONE))
|
||||
app.set_action("on_two", lambda a, v: self.on_remote_action(HttpAPI.Remote.TWO))
|
||||
app.set_action("on_three", lambda a, v: self.on_remote_action(HttpAPI.Remote.THREE))
|
||||
app.set_action("on_four", lambda a, v: self.on_remote_action(HttpAPI.Remote.FOUR))
|
||||
app.set_action("on_five", lambda a, v: self.on_remote_action(HttpAPI.Remote.FIVE))
|
||||
app.set_action("on_six", lambda a, v: self.on_remote_action(HttpAPI.Remote.SIX))
|
||||
app.set_action("on_seven", lambda a, v: self.on_remote_action(HttpAPI.Remote.SEVEN))
|
||||
app.set_action("on_eight", lambda a, v: self.on_remote_action(HttpAPI.Remote.EIGHT))
|
||||
app.set_action("on_nine", lambda a, v: self.on_remote_action(HttpAPI.Remote.NINE))
|
||||
app.set_action("on_zero", lambda a, v: self.on_remote_action(HttpAPI.Remote.ZERO))
|
||||
app.set_action("on_up", lambda a, v: self.on_remote_action(HttpAPI.Remote.UP))
|
||||
app.set_action("on_down", lambda a, v: self.on_remote_action(HttpAPI.Remote.DOWN))
|
||||
app.set_action("on_left", lambda a, v: self.on_remote_action(HttpAPI.Remote.LEFT))
|
||||
app.set_action("on_right", lambda a, v: self.on_remote_action(HttpAPI.Remote.RIGHT))
|
||||
app.set_action("on_next", lambda a, v: self.on_remote_action(HttpAPI.Remote.NEXT))
|
||||
app.set_action("on_back", lambda a, v: self.on_remote_action(HttpAPI.Remote.BACK))
|
||||
app.set_action("on_info", lambda a, v: self.on_remote_action(HttpAPI.Remote.INFO))
|
||||
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_epg", lambda a, v: self.on_remote_action(HttpAPI.Remote.EPG))
|
||||
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 +131,15 @@ 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)
|
||||
pack_type = Gtk.PackType.START if alt_layout else Gtk.PackType.END
|
||||
self._header_box.set_child_packing(self._screenshot_button_box, False, False, 0, pack_type)
|
||||
|
||||
# ***************** Remote controller ********************* #
|
||||
|
||||
def on_remote(self, action, state=False):
|
||||
@@ -871,7 +159,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:
|
||||
@@ -970,13 +258,86 @@ class ControlTool(Gtk.Box):
|
||||
|
||||
def update_signal(self, sig):
|
||||
snr = sig.get("e2snr", "0 %").strip() if sig else "0 %"
|
||||
snr_db = sig.get("e2snrdb", "0 dB").strip() if sig else "0 dB"
|
||||
acg = sig.get("e2acg", "0 %").strip() if sig else "0 %"
|
||||
ber = (sig.get("e2ber", None) or "").strip() if sig else ""
|
||||
# Labels.
|
||||
self._snr_value_label.set_text(snr)
|
||||
self._snr_value_label.set_text(f"{snr_db} ({snr})")
|
||||
self._agc_value_label.set_text(acg)
|
||||
self._ber_value_label.set_text(ber)
|
||||
# Level bars.
|
||||
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-2023 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-2023 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.4.0 Beta</property>
|
||||
<property name="copyright">2018-2023 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
|
||||
576
app/ui/epg.py
576
app/ui/epg.py
@@ -1,576 +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 gzip
|
||||
import locale
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import urllib.request
|
||||
from enum import Enum
|
||||
from urllib.error import HTTPError, URLError
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import run_idle, run_task
|
||||
from app.connections import download_data, DownloadType
|
||||
from app.eparser.ecommons import BouquetService, BqServiceType
|
||||
from app.settings import SEP
|
||||
from app.tools.epg import EPG, ChannelsParser
|
||||
from app.ui.dialogs import get_message, show_dialog, DialogType, get_builder
|
||||
from .main_helper import on_popup_menu, update_entry_data
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column, EPG_ICON, KeyboardKey
|
||||
|
||||
|
||||
class RefsSource(Enum):
|
||||
SERVICES = 0
|
||||
XML = 1
|
||||
|
||||
|
||||
class EpgDialog:
|
||||
|
||||
def __init__(self, transient, settings, services, bouquet, fav_model, bouquet_name):
|
||||
|
||||
handlers = {"on_close_dialog": self.on_close_dialog,
|
||||
"on_apply": self.on_apply,
|
||||
"on_update": self.on_update,
|
||||
"on_save_to_xml": self.on_save_to_xml,
|
||||
"on_auto_configuration": self.on_auto_configuration,
|
||||
"on_filter_toggled": self.on_filter_toggled,
|
||||
"on_filter_changed": self.on_filter_changed,
|
||||
"on_info_bar_close": self.on_info_bar_close,
|
||||
"on_popup_menu": on_popup_menu,
|
||||
"on_bouquet_popup_menu": self.on_bouquet_popup_menu,
|
||||
"on_copy_ref": self.on_copy_ref,
|
||||
"on_assign_ref": self.on_assign_ref,
|
||||
"on_reset": self.on_reset,
|
||||
"on_list_reset": self.on_list_reset,
|
||||
"on_drag_begin": self.on_drag_begin,
|
||||
"on_drag_data_get": self.on_drag_data_get,
|
||||
"on_drag_data_received": self.on_drag_data_received,
|
||||
"on_resize": self.on_resize,
|
||||
"on_names_source_changed": self.on_names_source_changed,
|
||||
"on_options_save": self.on_options_save,
|
||||
"on_use_web_source_switch": self.on_use_web_source_switch,
|
||||
"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}
|
||||
|
||||
self._services = {}
|
||||
self._ex_services = services
|
||||
self._ex_fav_model = fav_model
|
||||
self._settings = settings
|
||||
self._bouquet = bouquet
|
||||
self._bouquet_name = bouquet_name
|
||||
self._current_ref = []
|
||||
self._enable_dat_filter = False
|
||||
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)
|
||||
|
||||
self._dialog = builder.get_object("epg_dialog_window")
|
||||
self._dialog.set_transient_for(transient)
|
||||
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")
|
||||
self._services_model = builder.get_object("services_list_store")
|
||||
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._xml_download_progress_bar = builder.get_object("xml_download_progress_bar")
|
||||
# Filter
|
||||
self._filter_bar = builder.get_object("filter_bar")
|
||||
self._filter_bar.bind_property("search-mode-enabled", self._filter_bar, "visible")
|
||||
self._filter_entry = builder.get_object("filter_entry")
|
||||
self._services_filter_model = builder.get_object("services_filter_model")
|
||||
self._services_filter_model.set_visible_func(self.services_filter_function)
|
||||
# Info
|
||||
self._source_count_label = builder.get_object("source_count_label")
|
||||
self._source_info_label = builder.get_object("source_info_label")
|
||||
self._bouquet_count_label = builder.get_object("bouquet_count_label")
|
||||
self._bouquet_epg_count_label = builder.get_object("bouquet_epg_count_label")
|
||||
# Options
|
||||
self._xml_radiobutton = builder.get_object("xml_radiobutton")
|
||||
self._xml_chooser_button = builder.get_object("xml_chooser_button")
|
||||
self._names_source_box = builder.get_object("names_source_box")
|
||||
self._web_source_box = builder.get_object("web_source_box")
|
||||
self._use_web_source_switch = builder.get_object("use_web_source_switch")
|
||||
self._url_to_xml_entry = builder.get_object("url_to_xml_entry")
|
||||
self._enable_filtering_switch = builder.get_object("enable_filtering_switch")
|
||||
self._epg_dat_path_entry = builder.get_object("epg_dat_path_entry")
|
||||
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")
|
||||
# Setting the last size of the dialog window
|
||||
window_size = self._settings.get("epg_tool_window_size")
|
||||
if window_size:
|
||||
self._dialog.resize(*window_size)
|
||||
|
||||
self.init_drag_and_drop()
|
||||
self.on_update()
|
||||
|
||||
def show(self):
|
||||
self._dialog.show()
|
||||
|
||||
def on_close_dialog(self, window, event):
|
||||
self._download_xml_is_active = False
|
||||
|
||||
@run_idle
|
||||
def on_apply(self, item):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
self._bouquet.clear()
|
||||
list(map(self._bouquet.append, [r[Column.FAV_ID] 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
|
||||
if row[Column.FAV_TYPE] == BqServiceType.IPTV.name:
|
||||
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)
|
||||
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)
|
||||
|
||||
def clear_data(self):
|
||||
self._services_model.clear()
|
||||
self._bouquet_model.clear()
|
||||
self._services.clear()
|
||||
self._source_info_label.set_text("")
|
||||
self._bouquet_epg_count_label.set_text("")
|
||||
self.on_info_bar_close()
|
||||
|
||||
def init_data(self):
|
||||
gen = self.init_bouquet_data()
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
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)
|
||||
return
|
||||
yield True
|
||||
|
||||
if self._refs_source is RefsSource.SERVICES:
|
||||
self.init_lamedb_source(refs)
|
||||
elif self._refs_source is RefsSource.XML:
|
||||
xml_gen = self.init_xml_source(refs)
|
||||
try:
|
||||
yield from xml_gen
|
||||
except ValueError as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
else:
|
||||
self.show_info_message("Unknown names source!", Gtk.MessageType.ERROR)
|
||||
yield True
|
||||
|
||||
def init_bouquet_data(self):
|
||||
for r in self._ex_fav_model:
|
||||
row = [*r[:]]
|
||||
fav_id = r[Column.FAV_ID]
|
||||
self._services[fav_id] = self._ex_services[fav_id].fav_id
|
||||
yield self._bouquet_model.append(row)
|
||||
self._bouquet_count_label.set_text(str(len(self._bouquet_model)))
|
||||
yield True
|
||||
|
||||
def init_lamedb_source(self, refs):
|
||||
srvs = {k[:k.rfind(":")]: v for k, v in self._ex_services.items()}
|
||||
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)))
|
||||
self.update_source_count_info()
|
||||
|
||||
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()
|
||||
if not path:
|
||||
self.show_info_message("The path to the xml file is not set!", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
if self._use_web_source:
|
||||
# Downloading gzipped xml file that contains services names with references from the web.
|
||||
self._download_xml_is_active = True
|
||||
self.update_active_header_elements(False)
|
||||
url = self._url_to_xml_entry.get_text()
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=2) as fp:
|
||||
headers = fp.info()
|
||||
content_type = headers.get("Content-Type", "")
|
||||
|
||||
if content_type != "application/gzip":
|
||||
self._download_xml_is_active = False
|
||||
raise ValueError("{} {} {}".format(get_message("Download XML file error."),
|
||||
get_message("Unsupported file type:"),
|
||||
content_type))
|
||||
|
||||
file_name = os.path.basename(url)
|
||||
data_path = self._epg_dat_path_entry.get_text()
|
||||
|
||||
with open(data_path + file_name, "wb") as tfp:
|
||||
bs = 1024 * 8
|
||||
size = -1
|
||||
read = 0
|
||||
b_num = 0
|
||||
if "content-length" in headers:
|
||||
size = int(headers["Content-Length"])
|
||||
|
||||
while self._download_xml_is_active:
|
||||
block = fp.read(bs)
|
||||
if not block:
|
||||
break
|
||||
read += len(block)
|
||||
tfp.write(block)
|
||||
b_num += 1
|
||||
self.update_download_progress(b_num * bs / size)
|
||||
yield True
|
||||
|
||||
path = tfp.name.rstrip(".gz")
|
||||
except (HTTPError, URLError) as e:
|
||||
raise ValueError("{} {}".format(get_message("Download XML file error."), e))
|
||||
else:
|
||||
try:
|
||||
with open(path, "wb") as f_out:
|
||||
with gzip.open(tfp.name, "rb") as f:
|
||||
shutil.copyfileobj(f, f_out)
|
||||
os.remove(tfp.name)
|
||||
except Exception as e:
|
||||
raise ValueError("{} {}".format(get_message("Unpacking data error."), e))
|
||||
finally:
|
||||
self._download_xml_is_active = False
|
||||
self.update_active_header_elements(True)
|
||||
|
||||
try:
|
||||
s_refs, info = ChannelsParser.get_refs_from_xml(path)
|
||||
yield True
|
||||
except Exception as e:
|
||||
raise ValueError("{} {}".format(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))
|
||||
self.update_source_info(info)
|
||||
self.update_source_count_info()
|
||||
yield True
|
||||
|
||||
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 & Gdk.ModifierType.CONTROL_MASK
|
||||
|
||||
if ctrl and key is KeyboardKey.C:
|
||||
self.on_copy_ref()
|
||||
elif ctrl and key is KeyboardKey.V:
|
||||
self.on_assign_ref()
|
||||
|
||||
@run_idle
|
||||
def on_save_to_xml(self, item):
|
||||
response = show_dialog(DialogType.CHOOSER, self._dialog, settings=self._settings)
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
|
||||
services = []
|
||||
iptv_types = (BqServiceType.IPTV.value, BqServiceType.MARKER.value)
|
||||
for r in self._bouquet_model:
|
||||
srv_type = r[Column.FAV_TYPE]
|
||||
if srv_type in iptv_types:
|
||||
srv = BouquetService(name=r[Column.FAV_SERVICE],
|
||||
type=BqServiceType(srv_type),
|
||||
data=r[Column.FAV_ID],
|
||||
num=r[Column.FAV_NUM])
|
||||
services.append(srv)
|
||||
|
||||
ChannelsParser.write_refs_to_xml("{}{}.xml".format(response, self._bouquet_name), services)
|
||||
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
|
||||
|
||||
@run_idle
|
||||
def on_auto_configuration(self, item):
|
||||
""" Simple mapping of services by name. """
|
||||
use_cyrillic = locale.getdefaultlocale()[0] in ("ru_RU", "be_BY", "uk_UA", "sr_RS")
|
||||
tr = None
|
||||
if use_cyrillic:
|
||||
# may be not entirely correct
|
||||
symbols = (u"АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯІÏҐЎЈЂЉЊЋЏTB",
|
||||
u"ABVGDEEJZIJKLMNOPRSTUFHZCSS_Y_EUAIEGUEDLNCJTV")
|
||||
tr = {ord(k): ord(v) for k, v in zip(*symbols)}
|
||||
|
||||
source = {}
|
||||
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]
|
||||
|
||||
success_count = 0
|
||||
not_founded = {}
|
||||
|
||||
for r in self._bouquet_model:
|
||||
if r[Column.FAV_TYPE] != BqServiceType.IPTV.value:
|
||||
continue
|
||||
name = re.sub("\\W+", "", str(r[Column.FAV_SERVICE])).upper()
|
||||
if use_cyrillic:
|
||||
name = name.translate(tr)
|
||||
ref = source.get(name, None) # Not [pop], because the list may contain duplicates or similar names!
|
||||
if ref:
|
||||
self.assign_data(r, ref, True)
|
||||
success_count += 1
|
||||
else:
|
||||
not_founded[name] = r
|
||||
# Additional attempt to search in the remaining elements
|
||||
for n in not_founded:
|
||||
for k in source:
|
||||
if k.startswith(n):
|
||||
self.assign_data(not_founded[n], source[k], True)
|
||||
success_count += 1
|
||||
break
|
||||
|
||||
self.update_epg_count()
|
||||
self.show_info_message("{} {} {}".format(get_message("Done!"),
|
||||
get_message("Count of successfully configured services:"),
|
||||
success_count), Gtk.MessageType.INFO)
|
||||
|
||||
def assign_data(self, row, ref, 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)
|
||||
return
|
||||
|
||||
fav_id = row[Column.FAV_ID]
|
||||
fav_id_data = fav_id.split(":")
|
||||
fav_id_data[3:7] = ref.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
|
||||
|
||||
def on_filter_toggled(self, button: Gtk.ToggleButton):
|
||||
self._filter_bar.set_search_mode(button.get_active())
|
||||
|
||||
def on_filter_changed(self, entry):
|
||||
self._services_filter_model.refilter()
|
||||
|
||||
def services_filter_function(self, model, itr, data):
|
||||
txt = self._filter_entry.get_text().upper()
|
||||
return model is None or model == "None" or txt in model.get_value(itr, 0).upper()
|
||||
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
self._info_bar.set_visible(False)
|
||||
|
||||
def on_copy_ref(self, item=None):
|
||||
model, paths = self._source_view.get_selection().get_selected_rows()
|
||||
self._current_ref.clear()
|
||||
if paths:
|
||||
self._current_ref.append(model[paths][1])
|
||||
|
||||
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()
|
||||
|
||||
@run_idle
|
||||
def on_reset(self, item):
|
||||
model, paths = self._bouquet_view.get_selection().get_selected_rows()
|
||||
if paths:
|
||||
row = self._bouquet_model[paths]
|
||||
self.reset_row_data(row)
|
||||
self.update_epg_count()
|
||||
|
||||
@run_idle
|
||||
def on_list_reset(self, item):
|
||||
list(map(self.reset_row_data, self._bouquet_model))
|
||||
self.update_epg_count()
|
||||
|
||||
def reset_row_data(self, row):
|
||||
default_fav_id = self._services.pop(row[Column.FAV_ID], None)
|
||||
if default_fav_id:
|
||||
self._services[default_fav_id] = default_fav_id
|
||||
row[Column.FAV_ID], row[Column.FAV_LOCKED], row[Column.FAV_TOOLTIP] = default_fav_id, None, None
|
||||
|
||||
@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 update_source_info(self, info):
|
||||
lines = info.split("\n")
|
||||
self._source_info_label.set_text(lines[0] if lines else "")
|
||||
self._source_view.set_tooltip_text(info)
|
||||
|
||||
@run_idle
|
||||
def update_source_count_info(self):
|
||||
source_count = len(self._services_model)
|
||||
self._source_count_label.set_text(str(source_count))
|
||||
if self._enable_dat_filter and source_count == 0:
|
||||
msg = get_message("Current epg.dat file does not contains references for the services of this bouquet!")
|
||||
self.show_info_message(msg, Gtk.MessageType.WARNING)
|
||||
|
||||
@run_idle
|
||||
def update_epg_count(self):
|
||||
count = len(list((filter(None, [r[Column.FAV_LOCKED] for r in self._bouquet_model]))))
|
||||
self._bouquet_epg_count_label.set_text(str(count))
|
||||
|
||||
@run_idle
|
||||
def update_active_header_elements(self, state):
|
||||
self._left_header_box.set_sensitive(state)
|
||||
self._xml_download_progress_bar.set_visible(not state)
|
||||
self._source_info_label.set_text("" if state else "Downloading XML:")
|
||||
|
||||
@run_idle
|
||||
def update_download_progress(self, value):
|
||||
self._xml_download_progress_bar.set_fraction(value)
|
||||
|
||||
def on_bouquet_popup_menu(self, menu, event):
|
||||
self._assign_ref_popup_item.set_sensitive(self._current_ref)
|
||||
on_popup_menu(menu, event)
|
||||
|
||||
# ***************** Drag-and-drop *********************#
|
||||
|
||||
def init_drag_and_drop(self):
|
||||
""" 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()
|
||||
self._bouquet_view.enable_model_drag_dest(target, Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE)
|
||||
self._bouquet_view.drag_dest_add_text_targets()
|
||||
|
||||
def on_drag_begin(self, view, context):
|
||||
""" Selects a row under the cursor in the view at the dragging beginning. """
|
||||
selection = view.get_selection()
|
||||
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):
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if paths:
|
||||
val = model.get_value(model.get_iter(paths), 1)
|
||||
data.set_text(val, -1)
|
||||
|
||||
def on_drag_data_received(self, view: Gtk.TreeView, 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()
|
||||
return False
|
||||
|
||||
# ***************** Options *********************#
|
||||
|
||||
def init_options(self):
|
||||
epg_dat_path = "{}epg{}".format(self._settings.profile_data_path, SEP)
|
||||
self._epg_dat_path_entry.set_text(epg_dat_path)
|
||||
default_epg_data_stb_path = "/etc/enigma2"
|
||||
epg_options = self._settings.epg_options
|
||||
if epg_options:
|
||||
self._refs_source = RefsSource.XML if epg_options.get("xml_source", False) else RefsSource.SERVICES
|
||||
self._xml_radiobutton.set_active(self._refs_source is RefsSource.XML)
|
||||
self._use_web_source = epg_options.get("use_web_source", False)
|
||||
self._use_web_source_switch.set_active(self._use_web_source)
|
||||
self._url_to_xml_entry.set_text(epg_options.get("url_to_xml", ""))
|
||||
self._enable_dat_filter = epg_options.get("enable_filtering", False)
|
||||
self._enable_filtering_switch.set_active(self._enable_dat_filter)
|
||||
epg_dat_path = epg_options.get("epg_dat_path", epg_dat_path)
|
||||
self._epg_dat_path_entry.set_text(epg_dat_path)
|
||||
self._epg_dat_stb_path_entry.set_text(epg_options.get("epg_dat_stb_path", default_epg_data_stb_path))
|
||||
self._update_epg_data_on_start = epg_options.get("epg_data_update_on_start", False)
|
||||
self._update_on_start_switch.set_active(self._update_epg_data_on_start)
|
||||
local_xml_path = epg_options.get("local_path_to_xml", None)
|
||||
if local_xml_path:
|
||||
self._xml_chooser_button.set_filename(local_xml_path)
|
||||
os.makedirs(os.path.dirname(self._epg_dat_path_entry.get_text()), exist_ok=True)
|
||||
|
||||
def on_options_save(self, item=None):
|
||||
self._settings.epg_options = {"xml_source": self._xml_radiobutton.get_active(),
|
||||
"use_web_source": self._use_web_source_switch.get_active(),
|
||||
"local_path_to_xml": self._xml_chooser_button.get_filename(),
|
||||
"url_to_xml": self._url_to_xml_entry.get_text(),
|
||||
"enable_filtering": self._enable_filtering_switch.get_active(),
|
||||
"epg_dat_path": self._epg_dat_path_entry.get_text(),
|
||||
"epg_dat_stb_path": self._epg_dat_stb_path_entry.get_text(),
|
||||
"epg_data_update_on_start": self._update_on_start_switch.get_active()}
|
||||
|
||||
def on_resize(self, window):
|
||||
if self._settings:
|
||||
self._settings.add("epg_tool_window_size", window.get_size())
|
||||
|
||||
def on_names_source_changed(self, button):
|
||||
self._refs_source = RefsSource.XML if button.get_active() else RefsSource.SERVICES
|
||||
self._names_source_box.set_sensitive(button.get_active())
|
||||
|
||||
def on_enable_filtering_switch(self, switch, state):
|
||||
self._epg_dat_source_box.set_sensitive(state)
|
||||
self._update_on_start_switch.set_active(False if not state else self._update_epg_data_on_start)
|
||||
|
||||
def on_update_on_start_switch(self, switch, state):
|
||||
pass
|
||||
|
||||
def on_use_web_source_switch(self, switch, state):
|
||||
self._web_source_box.set_sensitive(state)
|
||||
self._xml_chooser_button.set_sensitive(not state)
|
||||
|
||||
def on_field_icon_press(self, entry, icon, event_button):
|
||||
update_entry_data(entry, self._dialog, self._settings)
|
||||
|
||||
# ***************** Downloads *********************#
|
||||
|
||||
@run_task
|
||||
def download_epg_from_stb(self):
|
||||
""" Download the epg.dat file via ftp from the receiver. """
|
||||
download_data(settings=self._settings, download_type=DownloadType.EPG, callback=print)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
0
app/ui/epg/__init__.py
Normal file
0
app/ui/epg/__init__.py
Normal file
File diff suppressed because it is too large
Load Diff
1022
app/ui/epg/epg.py
Normal file
1022
app/ui/epg/epg.py
Normal file
File diff suppressed because it is too large
Load Diff
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>
|
||||
468
app/ui/epg/tab.glade
Normal file
468
app/ui/epg/tab.glade
Normal file
@@ -0,0 +1,468 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2023 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.22"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2023 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 start -->
|
||||
<column type="gint"/>
|
||||
<!-- column-name end -->
|
||||
<column type="gint"/>
|
||||
<!-- column-name length -->
|
||||
<column type="gint"/>
|
||||
<!-- 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.49</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-start">5</property>
|
||||
<property name="margin-end">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="border-width">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child type="center">
|
||||
<object class="GtkToggleButton" id="multi_epg_button">
|
||||
<property name="label" translatable="yes">Multi EPG</property>
|
||||
<property name="name">header-button</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>
|
||||
<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="name">header-entry</property>
|
||||
<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>
|
||||
</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-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="epg_filter_entry">
|
||||
<property name="visible" bind-source="epg_filter_button" bind-property="active">False</property>
|
||||
<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>
|
||||
<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="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_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" bind-source="multi_epg_button" bind-property="active">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.49</property>
|
||||
<property name="sort-column-id">0</property>
|
||||
<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="min-width">150</property>
|
||||
<property name="title" translatable="yes">Title</property>
|
||||
<property name="alignment">0.49</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_start_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">50</property>
|
||||
<property name="title" translatable="yes">Start time</property>
|
||||
<property name="alignment">0.49</property>
|
||||
<property name="sort-column-id">2</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_start_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="xalign">0.49</property>
|
||||
<property name="width-chars">27</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_end_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">50</property>
|
||||
<property name="title" translatable="yes">End time</property>
|
||||
<property name="alignment">0.49</property>
|
||||
<property name="sort-column-id">3</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_end_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="xalign">0.49</property>
|
||||
<property name="width-chars">27</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_length_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="fixed-width">100</property>
|
||||
<property name="min-width">50</property>
|
||||
<property name="title" translatable="yes">Length</property>
|
||||
<property name="alignment">0.49</property>
|
||||
<property name="sort-column-id">4</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_length_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="xalign">0.49</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">4</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_desc_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">100</property>
|
||||
<property name="title" translatable="yes">Description</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.49</property>
|
||||
<property name="sort-column-id">5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_desc_renderer">
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">5</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<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-start">5</property>
|
||||
<property name="margin-end">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-2023 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,14 @@ Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<requires lib="gtk+" version="3.22"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellite list editor. -->
|
||||
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2023 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,113 +44,243 @@ 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_selection_image1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-undo</property>
|
||||
</object>
|
||||
<object class="GtkMenu" id="sat_popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="sat_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="sat_view" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="sat_unselect_all_popup_item">
|
||||
<property name="label" translatable="yes">Remove selection</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="image">remove_selection_image1</property>
|
||||
<property name="use-stock">False</property>
|
||||
<signal name="activate" handler="on_unselect_all" object="sat_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>
|
||||
<object class="GtkListStore" id="sat_list_store">
|
||||
<columns>
|
||||
<!-- column-name position -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name type -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name selected -->
|
||||
<column type="gboolean"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkListStore" id="services_list_store">
|
||||
<columns>
|
||||
<!-- column-name name -->
|
||||
<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">
|
||||
<object class="GtkBox" 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-start">15</property>
|
||||
<property name="margin-end">15</property>
|
||||
<property name="margin-top">10</property>
|
||||
<property name="margin-bottom">10</property>
|
||||
<property name="spacing">5</property>
|
||||
<child type="center">
|
||||
<object class="GtkStackSwitcher" id="stack_switcher">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">Group by</property>
|
||||
<property name="stack">stack</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="import_button">
|
||||
<property name="label" translatable="yes">Import</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Bouquets and services</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Bouquets and services</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>
|
||||
<property name="expand">True</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="info_check_button">
|
||||
<property name="label" translatable="yes">Details</property>
|
||||
<object class="GtkBox" id="extra_header_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Details</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="image">details_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<accelerator key="i" signal="clicked" modifiers="Primary"/>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="replace_existing_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">Replace existing</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="replace_existing_switch">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="details_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">Details</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="image">details_image</property>
|
||||
<property name="always-show-image">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
<property name="secondary">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
@@ -182,88 +301,386 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkPaned" id="main_paned">
|
||||
<object class="GtkPaned" id="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>
|
||||
<signal name="realize" handler="on_main_paned_realize" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkFrame" id="bouquets_box_frame">
|
||||
<object class="GtkStack" id="stack">
|
||||
<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>
|
||||
<signal name="notify::visible-child-name" handler="on_visible_page" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkBox" id="bouquets_box">
|
||||
<object class="GtkFrame" id="bouquets_box_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="orientation">vertical</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="bouquets_screlled_window">
|
||||
<property name="width_request">200</property>
|
||||
<object class="GtkBox" id="bouquets_box">
|
||||
<property name="width-request">100</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</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="GtkTreeView" id="main_view">
|
||||
<object class="GtkScrolledWindow" id="bouquets_screlled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">main_list_store</property>
|
||||
<property name="headers_clickable">False</property>
|
||||
<property name="search_column">0</property>
|
||||
<signal name="button-press-event" handler="on_popup_menu" object="popup_menu" swapped="no"/>
|
||||
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
|
||||
<signal name="select-all" handler="on_select_all" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection">
|
||||
<property name="mode">multiple</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="bq_view">
|
||||
<property name="visible">True</property>
|
||||
<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"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="bouquet_name_column">
|
||||
<property name="title" translatable="yes">Name</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="bq_name_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="bouquet_type_column">
|
||||
<property name="title" translatable="yes">Type</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="bq_type_renderer">
|
||||
<property name="xalign">0.5099999904632568</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="bouquet_selected_column">
|
||||
<property name="title" translatable="yes">Selected</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererToggle" id="bq_selected_renderer">
|
||||
<signal name="toggled" handler="on_bq_selected_toggled" swapped="no"/>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="active">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<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="GtkTreeViewColumn" id="bouquet_name_column">
|
||||
<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="label" translatable="yes">Bouquets</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">services</property>
|
||||
<property name="title" translatable="yes">Bouquets</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="sat_box_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label-xalign">0.49000000953674316</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="satellites_box">
|
||||
<property name="width-request">100</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="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="satellites_screlled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="sat_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="model">sat_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="sat_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="realize" handler="on_sat_view_realize" swapped="no"/>
|
||||
<signal name="select-all" handler="on_select_all" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="sat_position_column">
|
||||
<property name="title" translatable="yes">Position</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="sat_position_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="sat_selected_column">
|
||||
<property name="title" translatable="yes">Selected</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererToggle" id="sat_selected_renderer">
|
||||
<signal name="toggled" handler="on_sat_selected_toggled" swapped="no"/>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="active">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="sat_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="sat_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="sat_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="label" translatable="yes">Satellites</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">satellite</property>
|
||||
<property name="title" translatable="yes">Satellites</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">False</property>
|
||||
<property name="shrink">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="bq_services_box_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label-xalign">0.49000000953674316</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="services_box">
|
||||
<property name="width-request">100</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="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="services_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="services_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="model">services_list_store</property>
|
||||
<property name="headers-clickable">False</property>
|
||||
<property name="search-column">0</property>
|
||||
<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>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="service_name_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">50</property>
|
||||
<property name="title" translatable="yes">Name</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="bq_name_renderer">
|
||||
<object class="GtkCellRendererText" id="info_name_renderer">
|
||||
<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>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="bouquet_type_column">
|
||||
<object class="GtkTreeViewColumn" id="service_type_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">75</property>
|
||||
<property name="title" translatable="yes">Type</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="bq_type_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
<object class="GtkCellRendererText" id="info_type_renderer">
|
||||
<property name="xpad">5</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="bouquet_selected_column">
|
||||
<object class="GtkTreeViewColumn" id="service_selected_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">75</property>
|
||||
<property name="title" translatable="yes">Selected</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererToggle" id="bq_selected_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
<signal name="toggled" handler="on_selected_toggled" swapped="no"/>
|
||||
<object class="GtkCellRendererToggle" id="service_selected_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<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>
|
||||
@@ -278,89 +695,120 @@ Author: Dmitriy Yefremov
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="label" translatable="yes">Bouquets</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="services_box_frame">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0.49000000953674316</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="services_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="services_view_scrolled_window">
|
||||
<property name="width_request">150</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<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="GtkTreeView" id="services_view">
|
||||
<object class="GtkBox" id="service_info_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">services_list_store</property>
|
||||
<property name="headers_clickable">False</property>
|
||||
<property name="search_column">0</property>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection"/>
|
||||
</child>
|
||||
<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="GtkTreeViewColumn" id="service_name_column">
|
||||
<property name="title" translatable="yes">Service</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="info_name_renderer">
|
||||
<property name="xalign">0.02</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="service_type_column">
|
||||
<property name="title" translatable="yes">Type</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="info_type_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<object 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">True</property>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</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>
|
||||
@@ -368,10 +816,10 @@ Author: Dmitriy Yefremov
|
||||
<child type="label">
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="label" translatable="yes">Bouquet details</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="label" translatable="yes">Services</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
@@ -389,14 +837,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 +854,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-2023 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, IS_GNOME_SESSION, Page, HeaderBar
|
||||
|
||||
|
||||
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,77 @@ 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_sat_selected_toggled": self.on_sat_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_sat_view_realize": self.on_sat_view_realize,
|
||||
"on_services_view_realize": self.on_services_view_realize,
|
||||
"on_popup_menu": on_popup_menu,
|
||||
"on_resize": self.on_resize,
|
||||
"on_main_paned_realize": self.on_main_paned_realize,
|
||||
"on_visible_page": self.on_visible_page,
|
||||
"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._bq_services = {}
|
||||
self._app = app
|
||||
self._services = {}
|
||||
self._service_ids = service_ids
|
||||
self._bq_services = {}
|
||||
self._sat_services = defaultdict(list)
|
||||
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._current_sat = None
|
||||
self._existing_srv_background = None
|
||||
self._page = Page.SERVICES
|
||||
|
||||
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._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._dialog_window.set_transient_for(app.app_window)
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("message_label")
|
||||
self._replace_existing_switch = builder.get_object("replace_existing_switch")
|
||||
# Bouquets page.
|
||||
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._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")
|
||||
# Satellites page.
|
||||
self._sat_view = builder.get_object("sat_view")
|
||||
self._sat_model = builder.get_object("sat_list_store")
|
||||
self._sat_count_label = builder.get_object("sat_count_label")
|
||||
|
||||
if IS_GNOME_SESSION or IS_DARWIN:
|
||||
actions_box = builder.get_object("actions_box")
|
||||
builder.get_object("toolbar_box").set_visible(False)
|
||||
header_bar = HeaderBar()
|
||||
stack_switcher = builder.get_object("stack_switcher")
|
||||
actions_box.remove(stack_switcher)
|
||||
header_bar.set_custom_title(stack_switcher)
|
||||
button = builder.get_object("import_button")
|
||||
actions_box.remove(button)
|
||||
header_bar.pack_start(button)
|
||||
extra_box = builder.get_object("extra_header_box")
|
||||
actions_box.remove(extra_box)
|
||||
header_bar.pack_end(extra_box)
|
||||
|
||||
self._dialog_window.set_titlebar(header_bar)
|
||||
|
||||
window_size = self._settings.get("import_dialog_window_size")
|
||||
if window_size:
|
||||
self._dialog_window.resize(*window_size)
|
||||
@@ -110,7 +186,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 +194,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,21 +209,24 @@ 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):
|
||||
self.show_info_message(get_message("No selected item!"), Gtk.MessageType.ERROR)
|
||||
return
|
||||
if self._page is Page.SERVICES:
|
||||
if not any(r[-1] for r in self._bq_model):
|
||||
self.show_info_message(get_message("No selected item!"), Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
if not self._bouquets or show_dialog(DialogType.QUESTION, self._dialog_window) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
if not self._bouquets or show_dialog(DialogType.QUESTION, self._dialog_window) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
self.import_data()
|
||||
self.import_bouquets_data()
|
||||
else:
|
||||
self.import_satellites_data()
|
||||
|
||||
@run_idle
|
||||
def import_data(self):
|
||||
def import_bouquets_data(self):
|
||||
""" Importing data into models. """
|
||||
if not self._bouquets:
|
||||
return
|
||||
@@ -154,12 +234,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 +249,119 @@ 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)))
|
||||
|
||||
self._append(self._bouquets, list(filter(lambda s: s.fav_id not in self._ids, services)))
|
||||
|
||||
if self._replace_existing_switch.get_active():
|
||||
self._app.emit("services_update", {s.fav_id: s for s in filter(lambda s: s.fav_id in self._ids, services)})
|
||||
|
||||
self._dialog_window.destroy()
|
||||
|
||||
def import_satellites_data(self):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog_window) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
replace_existing = self._replace_existing_switch.get_active()
|
||||
services = []
|
||||
current_services = self._app.current_services
|
||||
to_replace = {}
|
||||
|
||||
for row in self._sat_model:
|
||||
if row[-1]:
|
||||
sat = (row[0], row[1])
|
||||
skip = self._skip_import[sat]
|
||||
for s in filter(lambda srv: srv.fav_id not in skip, self._sat_services.get(sat[0], ())):
|
||||
if replace_existing and s.fav_id in self._ids:
|
||||
current_services[s.fav_id] = s
|
||||
to_replace[s.fav_id] = s
|
||||
elif s.fav_id not in self._ids:
|
||||
services.append(s)
|
||||
|
||||
self._append((), services)
|
||||
if to_replace:
|
||||
self._app.emit("services_update", to_replace)
|
||||
|
||||
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))
|
||||
if self._page is Page.SERVICES:
|
||||
self._current_bq = model.get(model.get_iter(paths[0]), 0, 1)
|
||||
self.update_bq_services()
|
||||
else:
|
||||
self._current_sat = model.get(model.get_iter(paths[0]), 0, 1)
|
||||
self.update_sat_services()
|
||||
|
||||
self._services_count_label.set_text(str(len(self._services_model)))
|
||||
|
||||
def update_bq_services(self):
|
||||
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
|
||||
self._services_model.append((srv.service, srv.service_type, srv.fav_id not in skip, bg, srv.fav_id))
|
||||
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
|
||||
self._services_model.append((bq_srv.name, bq_srv.type.value, bq_srv.data not in skip, bg, bq_srv.data))
|
||||
|
||||
def on_selected_toggled(self, toggle, path):
|
||||
self._main_model.set_value(self._main_model.get_iter(path), 2, not toggle.get_active())
|
||||
def update_sat_services(self):
|
||||
sat_services = self._sat_services.get(self._current_sat[0])
|
||||
skip = self._skip_import[self._current_sat]
|
||||
for srv in sat_services:
|
||||
bg = self._existing_srv_background if srv.fav_id in self._ids else None
|
||||
self._services_model.append((srv.service, srv.service_type, srv.fav_id not in skip, bg, srv.fav_id))
|
||||
|
||||
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_sat_selected_toggled(self, toggle, path):
|
||||
self._sat_model.set_value(self._sat_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 self._page is Page.SERVICES else self._current_sat]
|
||||
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,10 +382,33 @@ class ImportDialog:
|
||||
def update_selection(self, view, select):
|
||||
view.get_model().foreach(lambda mod, path, itr: mod.set_value(itr, 2, select))
|
||||
|
||||
def on_sat_view_realize(self, view):
|
||||
if not self._services:
|
||||
return True
|
||||
|
||||
for srv in self._services.values():
|
||||
self._sat_services[srv.pos].append(srv)
|
||||
|
||||
list(map(lambda s: self._sat_model.append((s, None, True)), self._sat_services))
|
||||
self._sat_count_label.set_text(str(len(self._sat_model)))
|
||||
|
||||
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())
|
||||
|
||||
def on_main_paned_realize(self, paned):
|
||||
width = paned.get_allocated_width()
|
||||
paned.set_position(width * 0.35)
|
||||
|
||||
def on_visible_page(self, stack, param):
|
||||
self._page = Page(stack.get_visible_child_name())
|
||||
|
||||
def on_key_press(self, view, event):
|
||||
""" Handling keystrokes """
|
||||
key_code = event.hardware_keycode
|
||||
@@ -229,10 +417,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-2023 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,11 +27,11 @@ Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<requires lib="gtk+" version="3.22"/>
|
||||
<!-- 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-2023 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>
|
||||
@@ -1453,7 +1435,7 @@ Author: Dmitriy Yefremov
|
||||
<property name="image">yt_receive_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_receive" swapped="no"/>
|
||||
<accelerator key="d" signal="clicked"/>
|
||||
<accelerator key="d" signal="clicked" modifiers="Primary"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
@@ -1474,7 +1456,7 @@ Author: Dmitriy Yefremov
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="yt_impotr_box">
|
||||
<object class="GtkButtonBox" id="yt_import_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="layout_style">expand</property>
|
||||
|
||||
133
app/ui/iptv.py
133
app/ui/iptv.py
@@ -2,7 +2,7 @@
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (c) 2018-2023 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,12 +40,13 @@ from gi.repository import GLib, Gio, GdkPixbuf
|
||||
from app.commons import run_idle, run_task, log
|
||||
from app.eparser.ecommons import BqServiceType, Service
|
||||
from app.eparser.iptv import (NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT, get_fav_id, MARKER_FORMAT,
|
||||
parse_m3u)
|
||||
from app.settings import SettingsType
|
||||
parse_m3u, PICON_FORMAT)
|
||||
from app.settings import SettingsType, IS_DARWIN
|
||||
from app.tools.yt import YouTubeException, YouTube
|
||||
from app.ui.dialogs import Action, show_dialog, DialogType, get_message, get_builder
|
||||
from app.ui.main_helper import get_base_model, get_iptv_url, on_popup_menu, get_picon_pixbuf
|
||||
from app.ui.uicommons import (Gtk, Gdk, UI_RESOURCES_PATH, IPTV_ICON, Column, KeyboardKey, get_yt_icon)
|
||||
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,
|
||||
IS_GNOME_SESSION, HeaderBar)
|
||||
|
||||
_DIGIT_ENTRY_NAME = "digit-entry"
|
||||
_ENIGMA2_REFERENCE = "{}:{}:{}:{:X}:{:X}:{:X}:{:X}:0:0:0"
|
||||
@@ -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"{self._reference_entry.get_text().replace(':', '_')}.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", {self._current_srv.fav_id: (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(st_type, 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(st_type, 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,12 @@ class YtListImportDialog:
|
||||
"on_key_press": self.on_key_press,
|
||||
"on_close": self.on_close}
|
||||
|
||||
self.appender = appender
|
||||
self._s_type = settings.setting_type
|
||||
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 +874,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")
|
||||
@@ -871,6 +891,17 @@ class YtListImportDialog:
|
||||
self._import_button.bind_property("sensitive", self._quality_box, "sensitive")
|
||||
self._receive_button.bind_property("sensitive", self._import_button, "sensitive")
|
||||
|
||||
if IS_GNOME_SESSION or IS_DARWIN:
|
||||
header_bar = HeaderBar(title="YouTube", subtitle=get_message("Playlist import"))
|
||||
self._dialog.set_titlebar(header_bar)
|
||||
actions_box = builder.get_object("yt_actions_box")
|
||||
import_box = builder.get_object("yt_import_box")
|
||||
actions_box.remove(import_box)
|
||||
header_bar.pack_end(import_box)
|
||||
actions_box.remove(self._receive_button)
|
||||
header_bar.pack_start(self._receive_button)
|
||||
actions_box.set_visible(False)
|
||||
|
||||
window_size = self._settings.get("yt_import_dialog_size")
|
||||
if window_size:
|
||||
self._dialog.resize(*window_size)
|
||||
|
||||
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.
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;
|
||||
}
|
||||
|
||||
2880
app/ui/main.glade
2880
app/ui/main.glade
File diff suppressed because it is too large
Load Diff
1923
app/ui/main.py
1923
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-2023 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,22 @@
|
||||
#
|
||||
|
||||
|
||||
""" 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", "get_base_paths", "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",
|
||||
"update_toggle_model", "update_filter_sat_positions", "get_pos_num")
|
||||
|
||||
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 +49,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 +69,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 +115,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 +286,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 +364,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 +376,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 +391,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 +430,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 +467,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 +527,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 +561,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 +627,37 @@ 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):
|
||||
return srv.picon_id.rstrip(".png")
|
||||
|
||||
|
||||
def update_entry_data(entry, dialog, settings):
|
||||
""" Updates value in text entry from chooser dialog. """
|
||||
@@ -656,6 +698,40 @@ def get_model_data(view):
|
||||
return model_name, model
|
||||
|
||||
|
||||
def update_toggle_model(model, path, toggle):
|
||||
""" Updates the toggle state for the model. """
|
||||
active = not toggle.get_active()
|
||||
if path == "0":
|
||||
model.foreach(lambda m, p, i: m.set_value(i, 1, active))
|
||||
else:
|
||||
model.set_value(model.get_iter(path), 1, active)
|
||||
if active:
|
||||
model.set_value(model.get_iter_first(), 1, len({r[0] for r in model if r[1]}) == len(model) - 1)
|
||||
else:
|
||||
model.set_value(model.get_iter_first(), 1, False)
|
||||
|
||||
|
||||
def update_filter_sat_positions(model, sat_positions):
|
||||
""" Updates the values for the satellite positions button model. """
|
||||
first = model[model.get_iter_first()][:]
|
||||
model.clear()
|
||||
model.append((first[0], True))
|
||||
sat_positions.discard(first[0])
|
||||
list(map(lambda pos: model.append((pos, True)), sorted(sat_positions, key=get_pos_num, reverse=True)))
|
||||
|
||||
|
||||
def get_pos_num(pos):
|
||||
""" Returns num [float] representation of satellite position. """
|
||||
if not pos:
|
||||
return -183.0
|
||||
|
||||
if len(pos) > 1:
|
||||
m = -1 if pos[-1] == "W" else 1
|
||||
return float(pos[:-1]) * m
|
||||
|
||||
return -181.0 if pos == "T" else -182.0
|
||||
|
||||
|
||||
def append_text_to_tview(char, view):
|
||||
""" Appending text and scrolling to a given line in the text view. """
|
||||
buf = view.get_buffer()
|
||||
@@ -664,9 +740,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 +750,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-2023 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-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
|
||||
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkMenu" id="add_menu">
|
||||
<property name="visible">True</property>
|
||||
@@ -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>
|
||||
@@ -425,27 +379,20 @@ Author: Dmitriy Yefremov
|
||||
<child type="center">
|
||||
<object class="GtkButtonBox" id="header_button_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="name">header-stack-switcher</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<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 +403,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 +412,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 +422,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 +431,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 +542,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 +552,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 +567,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 +609,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 +639,9 @@ 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="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,20 @@ 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="ypad">5</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
@@ -735,6 +675,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 +691,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 +775,9 @@ 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="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 +790,20 @@ 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="ypad">5</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
@@ -864,6 +812,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 +829,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 +1665,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">
|
||||
|
||||
230
app/ui/picons.py
230
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,14 @@ 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,
|
||||
get_picon_file_name)
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TV_ICON, Column, KeyboardKey, Page, ViewTarget
|
||||
|
||||
|
||||
class PiconManager(Gtk.Box):
|
||||
@@ -56,9 +56,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 +86,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 +97,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 +163,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 +187,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 +246,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 +269,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 +277,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 +290,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 +333,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 +357,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 +366,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 +375,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 +400,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 +430,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 +445,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 +491,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 +506,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 +562,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 +642,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 +730,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 +776,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 +795,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)
|
||||
@@ -845,16 +815,17 @@ class PiconManager(Gtk.Box):
|
||||
|
||||
fav_bouquet = self._app.current_bouquets[bq_selected]
|
||||
services = self._app.current_services
|
||||
return {services.get(fav_id).picon_id for fav_id in fav_bouquet}
|
||||
|
||||
ids = set()
|
||||
for s in (services.get(fav_id) for fav_id in fav_bouquet):
|
||||
ids.add(s.picon_id)
|
||||
ids.add(get_picon_file_name(s.service))
|
||||
return ids
|
||||
|
||||
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 +857,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 +873,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 +896,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 +911,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 +1009,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-2023 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):
|
||||
@@ -54,9 +54,13 @@ class PlayerBox(Gtk.Box):
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("stop", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("pause", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
|
||||
self._app = app
|
||||
self._app.connect("fav-clicked", self.on_fav_clicked)
|
||||
self._app.connect("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 +116,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()
|
||||
|
||||
@@ -246,7 +286,9 @@ class PlayerBox(Gtk.Box):
|
||||
|
||||
def on_press(self, area, event):
|
||||
if event.button == Gdk.BUTTON_PRIMARY:
|
||||
if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
|
||||
if event.type == Gdk.EventType.BUTTON_PRESS:
|
||||
self.emit("pause", None)
|
||||
elif event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
|
||||
self.on_full_screen()
|
||||
|
||||
def on_key_press(self, widget, event):
|
||||
@@ -266,7 +308,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 +337,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 +352,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 +374,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 +423,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 +435,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-2023 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.settings import SettingsType, Settings, PlayStreamsMode, IS_LINUX, SEP, IS_WIN, IS_DARWIN
|
||||
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, HeaderBar
|
||||
|
||||
|
||||
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,41 +174,50 @@ 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._unlimited_buffer_switch = builder.get_object("unlimited_buffer_switch")
|
||||
self._enable_extensions_switch = builder.get_object("enable_extensions_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")
|
||||
self._enable_yt_dl_switch.bind_property("active", builder.get_object("yt_dl_update_box"), "sensitive")
|
||||
self._enable_exp_switch.bind_property("active", builder.get_object("v5_support_box"), "sensitive")
|
||||
self._enable_exp_switch.bind_property("active", builder.get_object("enable_direct_playback_box"), "sensitive")
|
||||
# Enigma2 only.
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("bq_naming_grid"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("enable_http_box"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("enable_experimental_box"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("program_frame"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("experimental_box"), "sensitive")
|
||||
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 or IS_DARWIN:
|
||||
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 = HeaderBar()
|
||||
header_bar.set_custom_title(switcher)
|
||||
|
||||
self._dialog.set_titlebar(header_bar)
|
||||
|
||||
self.init_ui_elements()
|
||||
self.init_profiles()
|
||||
|
||||
@@ -233,8 +243,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 +258,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 +278,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 +305,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 +318,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 +340,18 @@ 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._unlimited_buffer_switch.set_active(self._settings.unlimited_copy_buffer)
|
||||
self._enable_extensions_switch.set_active(self._settings.extensions_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,20 @@ 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.unlimited_copy_buffer = self._unlimited_buffer_switch.get_active()
|
||||
self._ext_settings.extensions_support = self._enable_extensions_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,15 +512,14 @@ 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:
|
||||
self._support_ver5_switch.set_active(state)
|
||||
self._unlimited_buffer_switch.set_active(state)
|
||||
self._enable_extensions_switch.set_active(state)
|
||||
self._enable_send_to_switch.set_active(state)
|
||||
self._enable_yt_dl_switch.set_active(state)
|
||||
|
||||
@@ -515,7 +545,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,23 +608,61 @@ 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:
|
||||
return
|
||||
|
||||
if response in self._settings.picons_paths:
|
||||
sep = "/"
|
||||
path = response if response.endswith(sep) else response + sep
|
||||
|
||||
if path in self._settings.picons_paths:
|
||||
self.show_info_message("This path already exists!", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
path = response if response.endswith(SEP) else response + SEP
|
||||
model = self._picons_paths_box.get_model()
|
||||
model.append((path, path))
|
||||
self._picons_paths_box.set_active_id(path)
|
||||
self._ext_settings.picons_paths = tuple(r[0] for r in model)
|
||||
|
||||
def on_remove_picon_path(self, button):
|
||||
msg = f"{get_message('This may change the settings of other profiles!')}\n\n\t\t{'Are you sure?'}"
|
||||
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 +692,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 +806,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,41 @@
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
#task-button {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#header-button {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#header-entry {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
#header-stack-switcher > button {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
buttonbox {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
paned > separator {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
paned.horizontal > separator {
|
||||
background-size: 2px 24px;
|
||||
}
|
||||
|
||||
paned.vertical > separator {
|
||||
background-size: 24px 2px;
|
||||
}
|
||||
|
||||
.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)
|
||||
@@ -128,6 +162,17 @@ def show_notification(message, timeout=10000, urgency=1):
|
||||
notify.show()
|
||||
|
||||
|
||||
class HeaderBar(Gtk.HeaderBar):
|
||||
""" Custom header bar widget. """
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.set_visible(True)
|
||||
self.set_show_close_button(True)
|
||||
|
||||
if IS_DARWIN:
|
||||
self.set_decoration_layout("close,minimize,maximize")
|
||||
|
||||
|
||||
class Page(Enum):
|
||||
""" Main stack widget page. """
|
||||
INFO = "info"
|
||||
@@ -155,6 +200,7 @@ class ViewTarget(Enum):
|
||||
BOUQUET = 0
|
||||
FAV = 1
|
||||
SERVICES = 2
|
||||
IPTV = 3
|
||||
|
||||
|
||||
class BqGenType(Enum):
|
||||
@@ -225,6 +271,23 @@ 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
|
||||
# EPG view
|
||||
EPG_SERVICE = 0
|
||||
EPG_TITLE = 1
|
||||
EPG_START = 2
|
||||
EPG_END = 3
|
||||
EPG_LENGTH = 4
|
||||
EPG_DESC = 5
|
||||
EPG_DATA = 6
|
||||
|
||||
def __index__(self):
|
||||
""" Overridden to get the index in slices directly """
|
||||
@@ -263,10 +326,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 +367,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 +406,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,24 @@
|
||||
* {
|
||||
-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;
|
||||
}
|
||||
|
||||
popover .view {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
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-2023 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,122 @@
|
||||
|
||||
|
||||
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.settings import IS_DARWIN
|
||||
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, HeaderBar
|
||||
|
||||
_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 +151,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 +164,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 +410,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,11 +426,13 @@ 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")
|
||||
self._sat_view.bind_property("sensitive", self._source_box, "sensitive")
|
||||
self._sat_view.bind_property("sensitive", self._source_box, "sensitive")
|
||||
self._sat_view.bind_property("sensitive", self._receive_button, "sensitive")
|
||||
self._receive_button.bind_property("visible", update_button, "visible")
|
||||
# Filter
|
||||
@@ -480,6 +457,20 @@ class UpdateDialog:
|
||||
builder.get_object("sat_update_search_up_button"))
|
||||
builder.get_object("sat_update_find_button").connect("toggled", search_provider.on_search_toggled)
|
||||
|
||||
if IS_GNOME_SESSION or IS_DARWIN:
|
||||
header_bar = HeaderBar()
|
||||
builder.get_object("sat_update_header").set_visible(False)
|
||||
header_box = builder.get_object("satellites_update_header_box")
|
||||
header_box.remove(self._source_box)
|
||||
header_bar.pack_start(self._source_box)
|
||||
action_box = builder.get_object("sat_update_left_action_box")
|
||||
header_box.remove(action_box)
|
||||
header_bar.pack_start(action_box)
|
||||
action_box = builder.get_object("sat_update_right_action_box")
|
||||
header_box.remove(action_box)
|
||||
header_bar.pack_end(action_box)
|
||||
self._window.set_titlebar(header_bar)
|
||||
|
||||
window_size = self._settings.get(self._size_name)
|
||||
if window_size:
|
||||
self._window.resize(*window_size)
|
||||
@@ -502,9 +493,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 +503,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 +530,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 +631,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 +675,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 +699,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 +726,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 +749,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 +761,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 +778,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 +799,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 +824,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 +893,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 +913,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
|
||||
1399
app/ui/xml/editor.glade
Normal file
1399
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,18 +1,15 @@
|
||||
#!/bin/bash
|
||||
VER="2.0.0_Alpha2"
|
||||
VER="3.4.0_Beta"
|
||||
B_PATH="dist/DemonEditor"
|
||||
DEB_PATH="$B_PATH/usr/share/demoneditor"
|
||||
|
||||
mkdir -p $B_PATH
|
||||
cp -TRv deb $B_PATH
|
||||
rsync --exclude=app/ui/lang --exclude=app/ui/icons --exclude=__pycache__ -arv ../../app $DEB_PATH
|
||||
rsync --exclude=__pycache__ -arv ../../extensions $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.4.0-Beta
|
||||
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.
Binary file not shown.
@@ -21,8 +21,11 @@ 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')
|
||||
('app/ui/icons*', 'share/icons'),
|
||||
('extensions/*', 'extensions')
|
||||
]
|
||||
|
||||
a = Analysis([EXE_NAME],
|
||||
@@ -32,6 +35,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 +81,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.4.0.{BUILD_DATE} Beta",
|
||||
'NSHumanReadableCopyright': u"Copyright © 2023, Dmitriy Yefremov",
|
||||
'NSRequiresAquaSystemAppearance': 'false',
|
||||
'NSHighResolutionCapable': 'true'
|
||||
})
|
||||
|
||||
@@ -18,8 +18,11 @@ 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')
|
||||
('app\\ui\\icons*', 'share\\icons'),
|
||||
('extensions\\*', 'extensions')
|
||||
]
|
||||
|
||||
|
||||
@@ -30,6 +33,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
|
||||
10
extensions/README.md
Normal file
10
extensions/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
Extension packages must be located in the following paths:
|
||||
``` app/ui/extensions```, ``` your data path/tools/extensions ```.
|
||||
|
||||
For builds:
|
||||
``` Program Root\extensions ```
|
||||
|
||||
Extensions and examples can be found [here](https://github.com/DYefremov/demoneditor-extensions).
|
||||
The possibilities of extending the API, as well as the creation and publication of the necessary extensions, can be discussed there.
|
||||
|
||||
### Pull requests for extensions are not accepted here!
|
||||
96
extensions/__init__.py
Normal file
96
extensions/__init__.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2023 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 json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
CONFIG_PATH = f"{Path.home()}{os.sep}.config{os.sep}demon-editor{os.sep}extensions{os.sep}"
|
||||
|
||||
|
||||
class Singleton(type):
|
||||
_INSTANCE = None
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if not cls._INSTANCE:
|
||||
cls._INSTANCE = type.__call__(cls, *args, **kwargs)
|
||||
return cls._INSTANCE
|
||||
|
||||
|
||||
class BaseExtension(metaclass=Singleton):
|
||||
""" Base extension (plugin) class. """
|
||||
# The label that will be displayed in the "Tools" menu.
|
||||
LABEL = "Base extension"
|
||||
|
||||
_LOGGER_NAME = "main_logger"
|
||||
|
||||
def __init__(self, app):
|
||||
# Current application instance.
|
||||
# It can be used all public methods, properties or signals.
|
||||
self.app = app
|
||||
self._config_path = f"{CONFIG_PATH}{self.__class__.__name__}{os.sep}config"
|
||||
|
||||
self.log(f"Extension initialized...")
|
||||
|
||||
def exec(self):
|
||||
""" Triggers an action for the given extension.
|
||||
|
||||
E.g. shows a dialog or runs an external script.
|
||||
"""
|
||||
self.app.show_info_message(f"Hello from {self.__class__.__name__} class!")
|
||||
|
||||
def log(self, message, level=logging.ERROR):
|
||||
""" Shows log messages. """
|
||||
logging.getLogger(self._LOGGER_NAME).log(level, f"[{self.__class__.__name__}] {message}")
|
||||
|
||||
def reset_config(self):
|
||||
path = Path(self._config_path)
|
||||
if path.is_file():
|
||||
path.unlink()
|
||||
|
||||
@property
|
||||
def config(self) -> dict:
|
||||
if not Path(self._config_path).is_file():
|
||||
return {}
|
||||
|
||||
with open(self._config_path, "r", encoding="utf-8") as config_file:
|
||||
try:
|
||||
return json.load(config_file)
|
||||
except ValueError as e:
|
||||
self.log(f"Configuration load error: {e}")
|
||||
return {}
|
||||
|
||||
@config.setter
|
||||
def config(self, value: dict):
|
||||
Path(self._config_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(self._config_path, "w", encoding="utf-8") as config_file:
|
||||
json.dump(value, config_file, indent=" ")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018-2021 Dmitriy Yefremov
|
||||
# Copyright (C) 2018-2023 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:"
|
||||
|
||||
@@ -1247,6 +1253,9 @@ msgstr "Выконваецца загрузка дадзеных!"
|
||||
msgid "Recordings"
|
||||
msgstr "Запісы"
|
||||
|
||||
msgid "Recordings:"
|
||||
msgstr "Запісы:"
|
||||
|
||||
msgid "Help"
|
||||
msgstr "Даведка"
|
||||
|
||||
@@ -1258,3 +1267,176 @@ 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 "Прысвоіць спасылку"
|
||||
|
||||
msgid "Specify hostname or IP address"
|
||||
msgstr "Задайце імя хоста або IP-адрас"
|
||||
|
||||
msgid "Default selection"
|
||||
msgstr "Выбар па змаўчанні"
|
||||
|
||||
msgid "Don't change power state"
|
||||
msgstr "Не змяняць стан сілкавання"
|
||||
|
||||
msgid "Don't toggle standby mode when updating bouquets and services."
|
||||
msgstr "Не перамыкаць у рэжым чакання пры абнаўленні букетаў і сэрвісаў."
|
||||
|
||||
msgid "Region"
|
||||
msgstr "Рэгіён"
|
||||
|
||||
msgid "Provider"
|
||||
msgstr "Правайдар"
|
||||
|
||||
msgid "Enables upload as an archive if a large number of picon (> 1000) is selected.\n"
|
||||
" Recommended only if you have external storage."
|
||||
msgstr "Улучае загрузку ў выглядзе архіва, калі абрана вялікая колькасць пiконаў (> 1000).\n"
|
||||
" Рэкамендуецца, толькі калі ў вас ёсць вонкавае сховішча."
|
||||
|
||||
msgid "Clear \"New\" flag"
|
||||
msgstr "Здаліць сцяжок \"New\""
|
||||
|
||||
msgid "Group by"
|
||||
msgstr "Групаваць па"
|
||||
|
||||
msgid "Replace existing"
|
||||
msgstr "Замяніць існуючыя"
|
||||
|
||||
msgid "Already exists"
|
||||
msgstr "Ужо існуе"
|
||||
|
||||
msgid "Enable unlimited copy buffer"
|
||||
msgstr "Уключыць неабмежаваны буфер капіявання"
|
||||
|
||||
msgid "Enables unlimited copy buffer for the bouquets tab."
|
||||
msgstr "Улучае неабмежаваны буфер капіявання для ўкладкі букетаў."
|
||||
|
||||
msgid "Start time"
|
||||
msgstr "Пачатак"
|
||||
|
||||
msgid "End time"
|
||||
msgstr "Сканчэнне"
|
||||
|
||||
msgid "Enable extensions support"
|
||||
msgstr "Уключыць падтрымку пашырэнняў"
|
||||
|
||||
msgid "After uploading the changes you may need to completely reboot the receiver!"
|
||||
msgstr "Пасля загрузкі змен можа запатрабавацца перазапуск рэсівера!"
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
# Charly, 2019.
|
||||
# Dmitriy Yefremov, 2020-2021.
|
||||
# Thomas Schmidt, 2021
|
||||
# Dmitriy Yefremov, 2020-2023.
|
||||
# Thomas Schmidt, 2021.
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Last-Translator: Dmitriy Yefremov\n"
|
||||
@@ -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,176 @@ 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"
|
||||
|
||||
msgid "Specify hostname or IP address"
|
||||
msgstr "Hostname oder IP-Adresse angeben"
|
||||
|
||||
msgid "Default selection"
|
||||
msgstr "Standardauswahl"
|
||||
|
||||
msgid "Don't change power state"
|
||||
msgstr "Energiezustand nicht ändern"
|
||||
|
||||
msgid "Don't toggle standby mode when updating bouquets and services."
|
||||
msgstr "Schalten beim Aktualisieren von Bouquets und Services nicht in den Standby-Modus um."
|
||||
|
||||
msgid "Region"
|
||||
msgstr "Region"
|
||||
|
||||
msgid "Provider"
|
||||
msgstr "Provider"
|
||||
|
||||
msgid "Enables upload as an archive if a large number of picon (> 1000) is selected.\n"
|
||||
" Recommended only if you have external storage."
|
||||
msgstr "Ermöglicht das Hochladen als Archiv, wenn eine große Anzahl von Picons (> 1000)"
|
||||
" ausgewählt ist. Empfohlen nur, wenn einen externen Speicher verfügt wird."
|
||||
|
||||
msgid "Clear \"New\" flag"
|
||||
msgstr "Das \"Neu\"-Flag entfernen"
|
||||
|
||||
msgid "Group by"
|
||||
msgstr "Gruppieren nach"
|
||||
|
||||
msgid "Replace existing"
|
||||
msgstr "Vorhandene ersetzen"
|
||||
|
||||
msgid "Already exists"
|
||||
msgstr "Bereits vorhanden"
|
||||
|
||||
msgid "Enable unlimited copy buffer"
|
||||
msgstr "Unbegrenzte Zwischenablage aktivieren"
|
||||
|
||||
msgid "Enables unlimited copy buffer for the bouquets tab."
|
||||
msgstr "Aktiviert unbegrenzte Zwischenablage für die Bouquets-Tab."
|
||||
|
||||
msgid "Start time"
|
||||
msgstr "Anfangszeit"
|
||||
|
||||
msgid "End time"
|
||||
msgstr "Endzeit"
|
||||
|
||||
msgid "Enable extensions support"
|
||||
msgstr "Erweiterungen Unterstützung aktivieren"
|
||||
|
||||
msgid "After uploading the changes you may need to completely reboot the receiver!"
|
||||
msgstr "Nach dem Hochladen der Änderungen muss den Receiver eventuell komplett neu starten!"
|
||||
|
||||
@@ -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 ""
|
||||
"Project-Id-Version: \n"
|
||||
"POT-Creation-Date: \n"
|
||||
"PO-Revision-Date: \n"
|
||||
"Last-Translator: Frank Neirynck\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,392 @@ 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"
|
||||
|
||||
msgid "Playback from the main list"
|
||||
msgstr "Reproducción desde la lista principal"
|
||||
|
||||
msgid "Enables URL parsing using youtube-dl to get direct links to media."
|
||||
msgstr "Habilita el análisis de URL usando youtube-dl para obtener enlaces directos a los medios."
|
||||
|
||||
msgid "Permissions..."
|
||||
msgstr "Permisos..."
|
||||
|
||||
msgid "Display EPG in bouquet list"
|
||||
msgstr "Mostrar EPG en la lista de bouquet"
|
||||
|
||||
msgid "EPG *.dat file:"
|
||||
msgstr "Archivo EPG *.dat:"
|
||||
|
||||
msgid "Use HTTP to reload data in the receiver"
|
||||
msgstr "Use HTTP para recargar datos en el receptor"
|
||||
|
||||
msgid "Enable picons compression"
|
||||
msgstr "Habilitar la compresión de picons"
|
||||
|
||||
msgid "Update interval (sec):"
|
||||
msgstr "Intervalo de actualización (sec):"
|
||||
|
||||
msgid "Update:"
|
||||
msgstr "Actualización:"
|
||||
|
||||
msgid "Daily"
|
||||
msgstr "Diariamente"
|
||||
|
||||
msgid "Assign reference"
|
||||
msgstr "Asignar referencia"
|
||||
|
||||
msgid "Specify hostname or IP address"
|
||||
msgstr "Especifique el nombre de host o la dirección IP"
|
||||
|
||||
msgid "Default selection"
|
||||
msgstr "Selección predeterminada"
|
||||
|
||||
msgid "Don't change power state"
|
||||
msgstr "No apagues el dispositivo"
|
||||
|
||||
msgid "Don't toggle standby mode when updating bouquets and services."
|
||||
msgstr "No cambie el modo de espera al actualizar bouquets y servicios."
|
||||
|
||||
msgid "Region"
|
||||
msgstr "Región"
|
||||
|
||||
msgid "Provider"
|
||||
msgstr "Proveedor"
|
||||
|
||||
msgid "Enables upload as an archive if a large number of picon (> 1000) is selected.\n"
|
||||
" Recommended only if you have external storage."
|
||||
msgstr "Habilita la carga como un archivo si se selecciona una gran cantidad de picon (> 1000).\n"
|
||||
" Recomendado solo si tienes almacenamiento externo."
|
||||
|
||||
msgid "Clear \"New\" flag"
|
||||
msgstr "Limpiar \"Nuevo\" flag"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
# Copyright (C) 2018-2020 Frank Neirynck
|
||||
# This file is distributed under the MIT license.
|
||||
#
|
||||
# Frank Neirynck <frank@insink.be>, 2018-2020.
|
||||
# Frank Neirynck <frank@insink.be>, 2018-2022.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
@@ -25,7 +25,7 @@ msgid "Type"
|
||||
msgstr "Type"
|
||||
|
||||
msgid "Picon"
|
||||
msgstr "Picon"
|
||||
msgstr "Pict."
|
||||
|
||||
msgid "Freq"
|
||||
msgstr "Freq."
|
||||
@@ -148,10 +148,10 @@ msgid "Parent lock On/Off Ctrl + L"
|
||||
msgstr "Ouderlijk slot aan/uit Ctrl + L"
|
||||
|
||||
msgid "Picons"
|
||||
msgstr "Picons"
|
||||
msgstr "Pictogrammen"
|
||||
|
||||
msgid "Picons downloader"
|
||||
msgstr "Picons downloader"
|
||||
msgstr "Pictogrammen downloader"
|
||||
|
||||
msgid "Satellites downloader"
|
||||
msgstr "Satellieten downloader"
|
||||
@@ -286,22 +286,22 @@ msgid "Providers"
|
||||
msgstr "Leveranciers"
|
||||
|
||||
msgid "Receive picons"
|
||||
msgstr "Ontvang picons"
|
||||
msgstr "Ontvang pictogrammen"
|
||||
|
||||
msgid "Picons name format:"
|
||||
msgstr "Picons naam formaat:"
|
||||
msgstr "Pictogrammen naam formaat:"
|
||||
|
||||
msgid "Resize:"
|
||||
msgstr "Verklein/vergroot:"
|
||||
|
||||
msgid "Current picons path:"
|
||||
msgstr "Huidig pad picons:"
|
||||
msgstr "Huidig pad Pictogrammen:"
|
||||
|
||||
msgid "Receiver picons path:"
|
||||
msgstr "Ontvanger picons pad:"
|
||||
msgstr "Ontvanger Pictogrammen pad:"
|
||||
|
||||
msgid "Picons download tool"
|
||||
msgstr "Picons download gereedschap"
|
||||
msgstr "Pictogrammen download gereedschap"
|
||||
|
||||
msgid "Transfer to receiver"
|
||||
msgstr "Transfereren naar ontvanger"
|
||||
@@ -319,7 +319,7 @@ msgid "Path to save:"
|
||||
msgstr "Pad om op te slaan:"
|
||||
|
||||
msgid "Path to Enigma2 picons:"
|
||||
msgstr "Pad naar Enigma2 picons:"
|
||||
msgstr "Pad naar Enigma2 Pictogrammen:"
|
||||
|
||||
msgid "Specify the correct position value for the provider!"
|
||||
msgstr "Geeft de juiste positie waarde voor de provider!"
|
||||
@@ -328,16 +328,16 @@ msgid "Converter between name formats"
|
||||
msgstr "Omvormer tussen naam formaten"
|
||||
|
||||
msgid "Receive picons for providers"
|
||||
msgstr "Ontvang picons voor leveranciers"
|
||||
msgstr "Ontvang Pictogrammen voor providers"
|
||||
|
||||
msgid "Load satellite providers."
|
||||
msgstr "Laad satelliet leveranciers."
|
||||
msgstr "Laad satelliet providers."
|
||||
|
||||
msgid ""
|
||||
"To automatically set the identifiers for picons,\n"
|
||||
"first load the required services list into the main application window."
|
||||
msgstr ""
|
||||
"Om automatisch de ID in te stellen voor picons,\n"
|
||||
"Om automatisch de ID in te stellen voor pictogrammen,\n"
|
||||
"laad eerst de vereiste serviceslijst in via het hoofdvenster van het programma."
|
||||
|
||||
# Satellites editor
|
||||
@@ -462,7 +462,7 @@ msgid "Password:"
|
||||
msgstr "Paswoord:"
|
||||
|
||||
msgid "Picons:"
|
||||
msgstr "Picons:"
|
||||
msgstr "Pictogrammen:"
|
||||
|
||||
msgid "Port:"
|
||||
msgstr "Poort:"
|
||||
@@ -471,7 +471,7 @@ msgid "Data path:"
|
||||
msgstr "Gegevenspad:"
|
||||
|
||||
msgid "Picons path:"
|
||||
msgstr "Picons pad:"
|
||||
msgstr "Pictogrammen pad:"
|
||||
|
||||
msgid "Network settings:"
|
||||
msgstr "Netwerk instellingen:"
|
||||
@@ -825,7 +825,7 @@ msgid "File"
|
||||
msgstr "File"
|
||||
|
||||
msgid "Picons manager"
|
||||
msgstr "Picons manager"
|
||||
msgstr "Pictogrammen manager"
|
||||
|
||||
msgid "Explorer"
|
||||
msgstr "Explorer"
|
||||
@@ -1013,3 +1013,391 @@ msgstr "Schermafbeelding"
|
||||
msgid "Video"
|
||||
msgstr "Vidео"
|
||||
|
||||
msgid "The Neutrino has only experimental support. Not all features are supported!"
|
||||
msgstr "Neutrino heeft alleen experimentele ondersteuning. Niet alle functies worden ondersteund!"
|
||||
|
||||
msgid "Enable experimental features"
|
||||
msgstr "Schakel experimentele functies in"
|
||||
|
||||
msgid "Can't Playback!"
|
||||
msgstr "Kan niet afspelen!"
|
||||
|
||||
msgid "Enable Dark Mode"
|
||||
msgstr "Schakel de donkere modus in"
|
||||
|
||||
msgid "Extract..."
|
||||
msgstr "Extract..."
|
||||
|
||||
msgid "Unsupported format!"
|
||||
msgstr "Niet ondersteund formaat!"
|
||||
|
||||
msgid "Combine with the current data?"
|
||||
msgstr "Combineren met de huidige gegevens?"
|
||||
|
||||
msgid "Importing data done!"
|
||||
msgstr "Gegevens importeren klaar!"
|
||||
|
||||
msgid "Current service"
|
||||
msgstr "Huidige dienst"
|
||||
|
||||
msgid "Open folder"
|
||||
msgstr "Open Map"
|
||||
|
||||
msgid "Open archive"
|
||||
msgstr "Open Archief"
|
||||
|
||||
msgid "Import from Web"
|
||||
msgstr "Importeren van internet"
|
||||
|
||||
msgid "Control"
|
||||
msgstr "Controle"
|
||||
|
||||
msgid "Timers"
|
||||
msgstr "Timers"
|
||||
|
||||
msgid "Timer"
|
||||
msgstr "Timer"
|
||||
|
||||
msgid "Add timer"
|
||||
msgstr "Timer toevoegen"
|
||||
|
||||
msgid "Hr."
|
||||
msgstr "U."
|
||||
|
||||
msgid "Min."
|
||||
msgstr "мin."
|
||||
|
||||
msgid "Power"
|
||||
msgstr "Power"
|
||||
|
||||
msgid "Standby"
|
||||
msgstr "Standby"
|
||||
|
||||
msgid "Wake Up"
|
||||
msgstr "Wake Up"
|
||||
|
||||
msgid "Reboot"
|
||||
msgstr "Herstart"
|
||||
|
||||
msgid "Restart GUI"
|
||||
msgstr "Herstart GUI"
|
||||
|
||||
msgid "Shutdown"
|
||||
msgstr "Uitschakelen"
|
||||
|
||||
msgid "Shut down"
|
||||
msgstr "Uitschakelen"
|
||||
|
||||
msgid "Do Nothing"
|
||||
msgstr "Doe niets"
|
||||
|
||||
msgid "Auto"
|
||||
msgstr "Auto"
|
||||
|
||||
msgid "Grab screenshot"
|
||||
msgstr "Maak schermafbeelding"
|
||||
|
||||
msgid "Enabled:"
|
||||
msgstr "Ingeschakeld:"
|
||||
|
||||
msgid "Name:"
|
||||
msgstr "Naam:"
|
||||
|
||||
msgid "Description:"
|
||||
msgstr "Omschrijving:"
|
||||
|
||||
msgid "Service:"
|
||||
msgstr "Dienst:"
|
||||
|
||||
msgid "Service reference:"
|
||||
msgstr "Service referentie:"
|
||||
|
||||
msgid "Event ID:"
|
||||
msgstr "Event ID:"
|
||||
|
||||
msgid "Begins:"
|
||||
msgstr "Begint:"
|
||||
|
||||
msgid "Ends:"
|
||||
msgstr "Stopt:"
|
||||
|
||||
msgid "Repeated:"
|
||||
msgstr "Herhaald:"
|
||||
|
||||
msgid "Action:"
|
||||
msgstr "Actie:"
|
||||
|
||||
msgid "After event:"
|
||||
msgstr "Na Event:"
|
||||
|
||||
msgid "Location:"
|
||||
msgstr "Locatiе:"
|
||||
|
||||
msgid "Mo"
|
||||
msgstr "Ma"
|
||||
|
||||
msgid "Tu"
|
||||
msgstr "Di"
|
||||
|
||||
msgid "We"
|
||||
msgstr "Wo"
|
||||
|
||||
msgid "Th"
|
||||
msgstr "Do"
|
||||
|
||||
msgid "Fr"
|
||||
msgstr "Vr"
|
||||
|
||||
msgid "Sa"
|
||||
msgstr "Za"
|
||||
|
||||
msgid "Su"
|
||||
msgstr "Zo"
|
||||
|
||||
msgid "Set"
|
||||
msgstr "Set"
|
||||
|
||||
msgid "Services update"
|
||||
msgstr "Services-update"
|
||||
|
||||
msgid "Create folder"
|
||||
msgstr "Map aanmaken"
|
||||
|
||||
msgid "FTP client"
|
||||
msgstr "FTP client"
|
||||
|
||||
msgid "The file size is too large!"
|
||||
msgstr "De bestandsgrootte is te groot!"
|
||||
|
||||
msgid "Connect"
|
||||
msgstr "Verbind"
|
||||
|
||||
msgid "Disconnect"
|
||||
msgstr "Verbreek Verbinding"
|
||||
|
||||
msgid "Size"
|
||||
msgstr "Grootte"
|
||||
|
||||
msgid "Date"
|
||||
msgstr "Datum"
|
||||
|
||||
msgid "Toggle display position"
|
||||
msgstr "Weergavepositie wisselen"
|
||||
|
||||
msgid "Alternatives"
|
||||
msgstr "Alternatieven"
|
||||
|
||||
msgid "Add alternatives"
|
||||
msgstr "Alternatieven toevoegen"
|
||||
|
||||
msgid "DreamOS only!"
|
||||
msgstr "Enkel DreamOS!"
|
||||
|
||||
msgid "A similar service is already in this list!"
|
||||
msgstr "Een vergelijkbare service staat al in deze lijst!"
|
||||
|
||||
msgid "Play mode has been changed!\nRestart the program to apply the settings."
|
||||
msgstr "De afspeelmodus is gewijzigd!\nStart het programma opnieuw om de instellingen toe te passen."
|
||||
|
||||
msgid "Set values for TID, NID and Namespace for correct naming of the picons!"
|
||||
msgstr "Stel waarden in voor TID, NID en Namespace voor correcte naamgeving van de pictogrammen!"
|
||||
|
||||
msgid "Streams detected:"
|
||||
msgstr "Streams gedetecteerd:"
|
||||
|
||||
msgid "Download picons"
|
||||
msgstr "Pictogrammen downloaden"
|
||||
|
||||
msgid "Errors:"
|
||||
msgstr "Fouten:"
|
||||
|
||||
msgid "Use to play streams:"
|
||||
msgstr "Gebruik om streams af te spelen:"
|
||||
|
||||
msgid "Font in the lists:"
|
||||
msgstr "Lettertype in de lijsten:"
|
||||
|
||||
msgid "Picons size in the lists:"
|
||||
msgstr "Pictogramgrootte in de lijsten:"
|
||||
|
||||
msgid "Logo size in tooltips:"
|
||||
msgstr "Logogrootte in knopinfo:"
|
||||
|
||||
msgid "Save as"
|
||||
msgstr "Opslaan als"
|
||||
|
||||
msgid "Mark duplicates"
|
||||
msgstr "Markeer duplicaten"
|
||||
|
||||
msgid "Load only for selected bouquet"
|
||||
msgstr "Laad alleen voor geselecteerd boeket"
|
||||
|
||||
msgid "The task is canceled!"
|
||||
msgstr "De taak is geannuleerd!"
|
||||
|
||||
msgid "Data loading in progress!"
|
||||
msgstr "Gegevens worden geladen!"
|
||||
|
||||
msgid "Recordings"
|
||||
msgstr "Opnames"
|
||||
|
||||
msgid "Recordings:"
|
||||
msgstr "Opnames:"
|
||||
|
||||
msgid "Help"
|
||||
msgstr "Help"
|
||||
|
||||
msgid "HTTP API is not activated. Check your settings!"
|
||||
msgstr "HTTP-API is niet geactiveerd. Controleer uw instellingen!"
|
||||
|
||||
msgid "Add picons"
|
||||
msgstr "Pictogrammen toevoegen"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "Logs"
|
||||
|
||||
msgid "Title"
|
||||
msgstr "Titel"
|
||||
|
||||
msgid "Time"
|
||||
msgstr "Tijd"
|
||||
|
||||
msgid "Length"
|
||||
msgstr "Lengte"
|
||||
|
||||
msgid "Additional source"
|
||||
msgstr "Extra bron"
|
||||
|
||||
msgid "Automatically set the name selected in the favorites list."
|
||||
msgstr "Stel automatisch de geselecteerde naam in de favorietenlijst in."
|
||||
|
||||
msgid "Playback"
|
||||
msgstr "Weergave"
|
||||
|
||||
msgid "Playback:"
|
||||
msgstr "Weergave:"
|
||||
|
||||
msgid "Audio"
|
||||
msgstr "Audio"
|
||||
|
||||
msgid "Audio Track"
|
||||
msgstr "Audio Spoor"
|
||||
|
||||
msgid "Subtitle"
|
||||
msgstr "Ondertitel"
|
||||
|
||||
msgid "Subtitle Track"
|
||||
msgstr "Ondertitel Spoor"
|
||||
|
||||
msgid "Aspect ratio"
|
||||
msgstr "Beeldverhouding"
|
||||
|
||||
msgid "This may change the settings of other profiles!"
|
||||
msgstr "Hierdoor kunnen de instellingen van andere profielen veranderen!"
|
||||
|
||||
msgid "Drag the services to the desired picon or picon to the list of selected services."
|
||||
msgstr "Sleep de services naar het gewenste pictogram of het pictogram naar de lijst met geselecteerde services."
|
||||
|
||||
msgid "Sets the profile folder as default to store picons, backups, etc."
|
||||
msgstr "Stelt de profielmap in als standaard om pictogrammen, back-ups, etc. op te slaan."
|
||||
|
||||
msgid "New sub-bouquet"
|
||||
msgstr "Nieuw sub-boeket"
|
||||
|
||||
msgid "Mark not presented in Bouquets"
|
||||
msgstr "Stel in als niet in Boeketten"
|
||||
|
||||
msgid "Not in Bouquets"
|
||||
msgstr "Niet in Boeketten"
|
||||
|
||||
msgid "Do not show services present in Bouquets."
|
||||
msgstr "Laat geen services zien die aanwezig zijn in Boeketten."
|
||||
|
||||
msgid "IPTV services only"
|
||||
msgstr "Enkel IPTV diensten"
|
||||
|
||||
msgid "Display picons"
|
||||
msgstr "Toon pictogrammen"
|
||||
|
||||
msgid "Alternate layout"
|
||||
msgstr "Alternatieve indeling"
|
||||
|
||||
msgid "Layout of elements has been changed!"
|
||||
msgstr "Lay-out van elementen is gewijzigd!"
|
||||
|
||||
msgid "Restart the program to apply all changes."
|
||||
msgstr "Start het programma opnieuw om alle wijzigingen toe te passen."
|
||||
|
||||
msgid "New folder"
|
||||
msgstr "Nieuwe map"
|
||||
|
||||
msgid "Rename"
|
||||
msgstr "Hernoem"
|
||||
|
||||
msgid "Bookmarks"
|
||||
msgstr "Bladwijzers"
|
||||
|
||||
msgid "Add bookmark"
|
||||
msgstr "Bladwijzer toevoegen"
|
||||
|
||||
msgid "All bouquets"
|
||||
msgstr "Alle boeketten"
|
||||
|
||||
msgid "Playback from the main list"
|
||||
msgstr "Afspelen vanuit de hoofdlijst"
|
||||
|
||||
msgid "Enables URL parsing using youtube-dl to get direct links to media."
|
||||
msgstr "Schakelt URL-parsing met behulp van youtube-dl in, om directe links naar media te krijgen."
|
||||
|
||||
msgid "Permissions..."
|
||||
msgstr "Rechten..."
|
||||
|
||||
msgid "Display EPG in bouquet list"
|
||||
msgstr "Toon EPG in boeketlijst"
|
||||
|
||||
msgid "EPG *.dat file:"
|
||||
msgstr "EPG *.dat file:"
|
||||
|
||||
msgid "Use HTTP to reload data in the receiver"
|
||||
msgstr "Gebruik HTTP om gegevens opnieuw in de ontvanger te laden"
|
||||
|
||||
msgid "Enable picons compression"
|
||||
msgstr "Pictogrammen-compressie inschakelen"
|
||||
|
||||
msgid "Update interval (sec):"
|
||||
msgstr "Update-interval (sec):"
|
||||
|
||||
msgid "Update:"
|
||||
msgstr "Update:"
|
||||
|
||||
msgid "Daily"
|
||||
msgstr "Dagelijks"
|
||||
|
||||
msgid "Assign reference"
|
||||
msgstr "Referentie toewijzen"
|
||||
|
||||
msgid "Specify hostname or IP address"
|
||||
msgstr "Geef de hostnaam of het IP-adres op"
|
||||
|
||||
msgid "Default selection"
|
||||
msgstr "Standaard selectie"
|
||||
|
||||
msgid "Don't change power state"
|
||||
msgstr "Schakel het toestel niet uit"
|
||||
|
||||
msgid "Don't toggle standby mode when updating bouquets and services."
|
||||
msgstr "Schakel de stand-bymodus niet in bij het updaten van boeketten en services."
|
||||
|
||||
msgid "Region"
|
||||
msgstr "Regio"
|
||||
|
||||
msgid "Provider"
|
||||
msgstr "Provider"
|
||||
|
||||
msgid "Enables upload as an archive if a large number of picon (> 1000) is selected.\n"
|
||||
" Recommended only if you have external storage."
|
||||
msgstr "Maakt uploaden als archief mogelijk als een groot aantal pictogrammen (> 1000) is geselecteerd.\n"
|
||||
" Alleen aanbevolen als u externe opslag heeft."
|
||||
|
||||
msgid "Clear \"New\" flag"
|
||||
msgstr "Opruimen \"Niew\" flag"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user