Compare commits

..

317 Commits

Author SHA1 Message Date
DYefremov
39a592fd4d bump version 2022-03-30 17:42:09 +03:00
DYefremov
e40e0f2458 added force external themes option 2022-03-30 14:46:57 +03:00
DYefremov
d5889cd96c minor mac style changes 2022-03-30 02:17:10 +03:00
DYefremov
77a8bfe2c6 fixed name parsing for KingOfSat source 2022-03-27 11:33:09 +03:00
DYefremov
470d2d843b fixed translation missing (#85)
* Fixed translation missing for dropdown lists on Windows.
2022-03-26 12:12:22 +03:00
DYefremov
a908845b4e filtering support for recordings tab 2022-03-25 21:25:30 +03:00
DYefremov
8fee5033a4 updated it *.mo file 2022-03-24 21:20:56 +03:00
mapi68
a9b1f8b26c Italian translation сorrection (#84) 2022-03-24 21:13:23 +03:00
DYefremov
8fa306a9d1 updated it *.mo file 2022-03-23 11:02:15 +03:00
mapi68
383ea2b9b3 Italian translation update (#82) 2022-03-23 10:55:20 +03:00
DYefremov
a40ba2ff68 minor correction for settings dialog 2022-03-21 12:19:00 +03:00
DYefremov
421d9b1c96 control file correction 2022-03-20 11:57:26 +03:00
DYefremov
7357939241 bump version 2022-03-20 11:36:58 +03:00
DYefremov
08ef7bc451 ftp bookmark activation via single click 2022-03-20 11:31:21 +03:00
DYefremov
8b255ec824 Russian, Belarusian and German translations update 2022-03-20 11:26:12 +03:00
DYefremov
c81084015d correction of iptv streams check (#69) 2022-03-20 01:02:19 +03:00
mapi68
66c8e9e916 Update control (#80)
Added homepage and extended info.
2022-03-20 00:57:51 +03:00
DYefremov
4b93ae6950 updated it *.mo file 2022-03-19 21:48:42 +03:00
mapi68
e2cafef113 Italian translation correction (#79)
Fixed column labels
2022-03-19 21:44:16 +03:00
DYefremov
c35be2aa24 added newline parameter 2022-03-19 21:07:49 +03:00
mapi68
852404bae6 Update control (#78)
Added p7zip-full to Depends (to extract Picons from picon,cz)
2022-03-19 13:44:22 +03:00
mapi68
f6d2765137 Italian translation correction (#77) 2022-03-19 13:43:55 +03:00
DYefremov
182c7a9cc7 updated it *.mo file 2022-03-14 20:52:48 +03:00
mapi68
271ea97040 Italian translation update (#73)
Fixed and updated italian language.
2022-03-14 20:34:36 +03:00
DYefremov
364fb68743 minor correction 2022-03-14 14:18:05 +03:00
DYefremov
85a9d5e67e added comment in italian to *.desktop file 2022-03-14 10:10:47 +03:00
mapi68
50c0a0cf37 Update DemonEditor.desktop (#72) 2022-03-14 10:06:29 +03:00
DYefremov
25fd6df967 playback support from the main list 2022-03-13 21:08:33 +03:00
DYefremov
6106e86d18 minor settings dialog improvements 2022-03-13 17:57:06 +03:00
DYefremov
1c5f7fab11 tooltips support for main iptv list 2022-03-13 00:07:59 +03:00
DYefremov
024f90d23f t2mi plp id support for satellite editing tool (#70) 2022-03-11 11:40:23 +03:00
DYefremov
ee3041174c streams playback from the main list 2022-03-10 23:06:06 +03:00
DYefremov
6f28aae40c style correction 2022-03-06 12:22:39 +03:00
DYefremov
1b2de795a2 minor rework of settings dialog 2022-03-06 12:19:54 +03:00
DYefremov
8d485a9993 minor style corrections 2022-02-27 09:03:39 +03:00
Víctor Pont
1721567731 Spanish translation fixes and new strings (#68) 2022-02-24 13:46:21 +03:00
DYefremov
fd4325961c minor style changes 2022-02-23 14:22:29 +03:00
DYefremov
294d32c705 Russian, Belarusian and German translations update 2022-02-22 19:55:39 +03:00
DYefremov
aa961030ce changing path by link for FTP client 2022-02-22 14:44:55 +03:00
DYefremov
bc4c6746c9 minor corrections 2022-02-22 14:18:40 +03:00
DYefremov
61282b0cc8 copied tr *.mo file 2022-02-22 10:21:10 +03:00
audi06_19
07e855f99d Turkish translation update (#66) 2022-02-22 10:15:12 +03:00
DYefremov
0d0f19122b updated comments in *.desktop file 2022-02-21 20:33:24 +03:00
DYefremov
4789688efd updated es *.mo file 2022-02-21 20:09:40 +03:00
Víctor Pont
b13c2c3be0 Spanish translation update (#65) 2022-02-21 20:03:21 +03:00
DYefremov
d72189abc4 bump version 2022-02-21 15:03:17 +03:00
DYefremov
35d194100b added keyboard shortcut for renaming 2022-02-21 14:28:47 +03:00
DYefremov
aa0b97b9ae added extra tab for IPTV 2022-02-21 12:22:44 +03:00
DYefremov
5f54452ee2 fixed getting satellites list from FlySat 2022-02-21 00:23:07 +03:00
DYefremov
580e8ca82c basic bookmarks support for the FTP client 2022-02-13 00:29:44 +03:00
DYefremov
4ba2fb1a04 skip importing groups from m3u for Neutrino 2022-02-12 14:22:48 +03:00
DYefremov
b4612c26cb changed file names reading for FTP client 2022-02-09 21:56:45 +03:00
DYefremov
9a8b1e871d bump version 2022-02-05 15:20:28 +03:00
DYefremov
3a53a95f86 fixed current channel recording on Windows 2022-02-05 15:11:29 +03:00
DYefremov
24a94cfe9a improved MPV support 2022-02-05 13:50:59 +03:00
DYefremov
0ca08e3a1d fixed bouquets DND for macOS (#64) 2022-02-04 02:12:48 +03:00
DYefremov
db4e9d2696 fixed single bouquets import on macOS (#63) 2022-02-04 01:16:37 +03:00
DYefremov
3be9b374c8 bump version 2022-01-29 18:43:36 +03:00
DYefremov
0b9fd37ee9 minor revision and improvement of the FTP client 2022-01-28 16:31:12 +03:00
DYefremov
be90d9694a fixed removal of unused picons (#62) 2022-01-28 00:37:02 +03:00
DYefremov
a5144e8e34 fixed multiple selection with the Shift key (#61) 2022-01-27 22:46:34 +03:00
DYefremov
04e0a25956 minor fixes for picons 2022-01-26 23:07:41 +03:00
DYefremov
dc24c899af version update 2022-01-25 18:16:00 +03:00
DYefremov
a55495fd7c reworked built-in function for getting yt links 2022-01-25 18:07:34 +03:00
DYefremov
ed3aea42f5 minor corrections for EPG dialog 2022-01-24 19:17:11 +03:00
DYefremov
a6904360f9 improvement of EPG refs mapping (#59) 2022-01-24 16:39:59 +03:00
DYefremov
1e42d693cc added info panel auto closing for playback (#58) 2022-01-23 14:52:34 +03:00
DYefremov
5e64605be6 minor changes in saving the EPG link mapping list (#59) 2022-01-23 14:24:50 +03:00
DYefremov
63c55ea2ed Russian, Belarusian and German translations update 2022-01-19 22:32:47 +03:00
DYefremov
c7f85b027d bump version 2022-01-05 15:10:10 +03:00
DYefremov
b24910a9a5 fixed command to open file for FTP 2022-01-05 14:57:39 +03:00
DYefremov
1bdb4f123f minor data cleaning optimization 2022-01-05 12:16:16 +03:00
DYefremov
8f8d7633b8 added display picons option 2022-01-05 00:41:06 +03:00
DYefremov
c997724300 refactoring of picon assignment 2022-01-04 20:39:59 +03:00
DYefremov
727a3fa8a2 data rendering optimization 2022-01-03 00:08:31 +03:00
DYefremov
4009f5c2a2 multi-stream support for services web import 2022-01-01 23:09:31 +03:00
DYefremov
ca5d648032 multi-stream transponders support for LyngSat 2021-12-30 15:54:09 +03:00
DYefremov
938fe297c5 corrected satellite update from KingOfSat 2021-12-30 00:02:32 +03:00
DYefremov
db29b78fd7 basic editing support for FTP client 2021-12-29 00:21:11 +03:00
DYefremov
e599ea04c7 improved bouquet export to *.m3u (#56) 2021-12-28 15:17:39 +03:00
DYefremov
50a2f66fc3 bump version 2021-12-24 11:42:33 +03:00
DYefremov
34056b1006 minor rework of recordings tab 2021-12-24 00:15:17 +03:00
DYefremov
6188fecda9 reduced waiting time for web import 2021-12-23 22:07:06 +03:00
DYefremov
eb41b9629e minor revision of satellite update dialog 2021-12-22 14:19:10 +03:00
DYefremov
7694754919 fixed satellites web update from FlySat (#55) 2021-12-21 22:08:54 +03:00
DYefremov
11f240a81f minor rework of ftp client 2021-12-19 20:37:23 +03:00
DYefremov
1b75034317 basic alternate layout support 2021-12-19 13:15:58 +03:00
DYefremov
5b6900cae7 tooltips support for the bouquets list 2021-12-11 16:48:13 +03:00
DYefremov
eeeca881e8 added default id value for iptv service 2021-12-10 12:09:27 +03:00
DYefremov
ed16fb0195 minor correction for settings dialog 2021-12-07 15:05:58 +03:00
DYefremov
833b386356 win style correction 2021-12-07 14:54:12 +03:00
DYefremov
83424124d3 fix saving sub-bouquets 2021-12-07 14:39:31 +03:00
DYefremov
d77aa68a39 Russian, Belarusian and German translations update 2021-12-04 14:53:47 +03:00
DYefremov
94266e13b8 bump version 2021-12-03 21:11:00 +03:00
DYefremov
8db6fb1b0b improved picons filtering and assignment (#49) 2021-12-03 20:05:46 +03:00
DYefremov
e07c5d4bf7 minor code cleanup 2021-12-02 17:28:25 +03:00
DYefremov
6f3090a7e1 sub-bouquets creation support 2021-12-02 15:39:33 +03:00
DYefremov
b73c1d1118 sub bouquets saving support 2021-12-01 11:37:23 +03:00
DYefremov
df127c05f3 services marking not presented in bouquets 2021-11-28 23:41:16 +03:00
DYefremov
9b579af528 added filter option "not in bouquets" 2021-11-28 18:55:37 +03:00
DYefremov
8290e723c9 state saving for the main paned widgets 2021-11-27 10:20:10 +03:00
DYefremov
b12c29be84 minor correction for tid and nid values 2021-11-24 23:54:16 +03:00
DYefremov
1780fbadbd fixed signal update for control tab 2021-11-22 19:41:30 +03:00
DYefremov
4fe4e92442 fixed some folders display for the recording tab 2021-11-22 16:33:24 +03:00
DYefremov
bfad5cf9ac bump version 2021-11-22 14:19:52 +03:00
DYefremov
5d285f61c0 fixed a typo in the Italian translation 2021-11-18 19:52:40 +03:00
DYefremov
f61d9a1f61 fix tid and nid order for KingOfSat web source 2021-11-18 11:31:01 +03:00
DYefremov
8d115677d1 minor fix for picon downloader 2021-11-17 22:42:09 +03:00
DYefremov
f7c6cd6908 preventing gen bouquets for an empty config 2021-11-17 13:05:15 +03:00
DYefremov
2d2a90542c main icons initialization refactoring 2021-11-16 13:21:52 +03:00
DYefremov
535c9c9102 fix themes unpacking on Windows 2021-11-16 12:09:29 +03:00
DYefremov
866e18762d enabled http api setting for Neutrino 2021-11-14 23:37:56 +03:00
DYefremov
aef5027d23 bump version 2021-11-14 17:28:38 +03:00
DYefremov
3a142eca4a README update 2021-11-14 17:16:40 +03:00
DYefremov
606bad7716 fix getting youtube-dl for Windows 2021-11-13 13:13:52 +03:00
DYefremov
fa07f8bf85 fix picon path on profile change 2021-11-12 19:11:19 +03:00
DYefremov
92280162c6 added logging for transponder validation 2021-11-12 16:23:20 +03:00
DYefremov
5d285e88d8 copied tr *.mo file 2021-11-12 11:10:56 +03:00
audi06_19
0355714e92 Turkish translation update (#53) 2021-11-12 11:05:37 +03:00
DYefremov
b06e877a0c update of pl *.mo file 2021-11-11 22:37:58 +03:00
Wieslaw Weglowski
9b479b051d Polish translation update (#52) 2021-11-11 22:28:23 +03:00
DYefremov
b953ee8762 minor fix of playback window title 2021-11-08 11:06:31 +03:00
DYefremov
0e7d6bec69 fixed some configs loading with dvb-t 2021-11-07 23:34:47 +03:00
DYefremov
cb9824d404 changed audio menu icon 2021-11-07 21:47:11 +03:00
DYefremov
c87adb256f fix sid config for IPTV lists 2021-11-07 21:20:06 +03:00
DYefremov
899d05a186 added "Return" key support for FTP client 2021-11-07 00:10:15 +03:00
DYefremov
bd4f86e91e Russian, Belarusian and German translations update 2021-11-06 15:45:58 +03:00
DYefremov
42fb365b45 fix picon assignment by drag for mac 2021-11-06 14:42:13 +03:00
DYefremov
67d6ea861e minor playback fixes 2021-11-06 12:11:05 +03:00
DYefremov
d887a61636 fix settings saving for Ubuntu [18.04] 2021-11-05 23:01:07 +03:00
DYefremov
9d9efb7577 Russian, Belarusian and German translations update 2021-11-03 18:14:41 +03:00
DYefremov
b6d331a311 redesigned info output for download dialog 2021-11-03 18:09:46 +03:00
DYefremov
1060e169a1 minor refactoring 2021-11-03 12:30:23 +03:00
DYefremov
562c1a5955 modifiers correction 2021-11-01 11:09:45 +03:00
DYefremov
3c4dec323f minor mac style correction 2021-11-01 00:44:12 +03:00
DYefremov
3bafe08030 adapting dialogs for Gnome 2021-10-31 20:26:19 +03:00
DYefremov
722f8df813 header bar for Gnome in epg dialog 2021-10-31 16:09:07 +03:00
DYefremov
8f6984dbaf added src and pos display for epg (#51) 2021-10-31 13:34:48 +03:00
DYefremov
bf6e9617ec header bar changes in the backup dialog 2021-10-30 18:25:20 +03:00
DYefremov
ec27c32d35 bump version 2021-10-29 17:26:34 +03:00
DYefremov
bfa3b1aa66 added dialog call before reset of settings 2021-10-29 17:21:30 +03:00
DYefremov
a1e32abd07 minor settings dialog changes for Gnome 2021-10-29 15:57:28 +03:00
DYefremov
f93370293b Russian, Belarusian and German translations update 2021-10-29 11:31:59 +03:00
DYefremov
79a2a034eb fix app menu translation for Windows 2021-10-29 10:45:13 +03:00
DYefremov
791c073d1a redesigned info output for sat update dialog 2021-10-26 12:24:46 +03:00
DYefremov
de5ec53a18 Russian, Belarusian and German translations update 2021-10-26 11:47:26 +03:00
DYefremov
156ac7d364 bump version 2021-10-25 20:29:14 +03:00
DYefremov
5680423f14 added picons extraction from archives 2021-10-25 20:06:33 +03:00
DYefremov
fa256c5a0b added picon copying menu from ext path 2021-10-25 17:06:23 +03:00
DYefremov
d9a5d9a972 added tool menu for Gnome session 2021-10-23 19:25:19 +03:00
DYefremov
4e59bdf38e redesigned info displaying for the picons tab 2021-10-23 11:48:20 +03:00
DYefremov
7edb03836a added support for setting id for iptv 2021-10-23 00:18:51 +03:00
DYefremov
0ea0c889d4 fixed language selection for *.deb 2021-10-22 10:51:15 +03:00
DYefremov
e494a34bc4 fix for display dvb-t2 system (#50) 2021-10-21 22:50:35 +03:00
DYefremov
4a57234293 added logs display in the gui 2021-10-21 18:53:57 +03:00
DYefremov
aca4875ee6 fixed data loading with hex flag values (#50) 2021-10-20 11:46:22 +03:00
DYefremov
a289b9fb53 starting edit sats by double click 2021-10-19 09:00:14 +03:00
DYefremov
a735b3db13 minor picons loading optimization 2021-10-18 19:36:19 +03:00
DYefremov
e57e8f9dd7 added simple telnet client 2021-10-17 23:05:29 +03:00
DYefremov
300e0a38c3 added build scripts 2021-10-17 11:29:23 +03:00
DYefremov
5eb6f8d63f minor rework of yt import dialog 2021-10-16 14:37:21 +03:00
DYefremov
3ec4817665 minor dnd refactoring for the picon explorer 2021-10-14 21:32:56 +03:00
DYefremov
55bb7b2f45 picons auto filtering on channel selection (#49) 2021-10-14 17:37:53 +03:00
DYefremov
a3cf5edd5e changed 7-zip extract cmd 2021-10-14 11:56:30 +03:00
DYefremov
4f8f6cfcc9 small refactoring of management via http api 2021-10-13 01:41:24 +03:00
DYefremov
ef97cd365c support of adding custom path to picons 2021-10-11 00:31:29 +03:00
DYefremov
4b27e81f5e added win style 2021-10-10 14:22:07 +03:00
DYefremov
408753f124 deb start fix 2021-10-09 20:50:41 +03:00
DYefremov
c3bd3cd2bb mac style correction 2021-10-09 18:20:01 +03:00
DYefremov
a15666204c minor ui rework of the picon downloader 2021-10-09 16:55:06 +03:00
DYefremov
1e5c47c3cc minor rework of the picon manager 2021-10-07 13:08:50 +03:00
DYefremov
bc94bb3505 added player and signal elems for control tab 2021-10-05 12:39:34 +03:00
DYefremov
6cfb72b1d7 corrected screenshots creation 2021-10-01 23:47:14 +03:00
DYefremov
3c5144134c minor http api correction for neutrino 2021-10-01 11:44:38 +03:00
DYefremov
4fd2939d2e basic kingofsat support for channels web import 2021-09-30 21:41:48 +03:00
DYefremov
68b5fcfd39 minor adjustments of bouquets parsing 2021-09-28 00:07:30 +03:00
DYefremov
9fcc1e5f90 minor fix for extract dialog 2021-09-27 19:58:34 +03:00
DYefremov
92b028dc16 stream playback support for neutrino 2021-09-27 18:09:17 +03:00
DYefremov
665de22549 support data reloading via http for neutrino 2021-09-26 13:53:55 +03:00
DYefremov
5036ecee37 added extra playback menus 2021-09-25 14:43:43 +03:00
DYefremov
e755e9eb6e epg filtering support 2021-09-23 21:34:38 +03:00
DYefremov
305323726f enabled dark mode support 2021-09-23 18:21:55 +03:00
DYefremov
7b3ae01253 crud support for timers 2021-09-23 17:40:03 +03:00
DYefremov
08fb55ea54 preventing duplicate data loading for last config 2021-09-20 15:50:03 +03:00
DYefremov
f1ee406818 minor ui correction 2021-09-19 20:38:19 +03:00
DYefremov
8aa9d0797f search rework 2021-09-19 00:23:23 +03:00
DYefremov
8d61663d3e auto resizing screenshot 2021-09-18 09:07:27 +03:00
DYefremov
3a9ca26619 added Italian translation 2021-09-17 16:43:44 +03:00
DYefremov
228466d523 import dialog rework 2021-09-15 23:43:02 +03:00
DYefremov
e9b3b3f374 epg dialog rework 2021-09-15 13:12:25 +03:00
DYefremov
05b1619d1e basic markers support in the bouquets list 2021-09-14 16:30:27 +03:00
DYefremov
9a2a2b49f6 streams playback rework 2021-09-13 16:52:19 +03:00
DYefremov
a034d0476d picons page rework 2021-09-05 13:58:26 +03:00
DYefremov
0cf2f070b0 rework of keyboard keys support 2021-09-04 21:45:41 +03:00
DYefremov
dd70dffa1c hiding some items for neutrino mode 2021-09-04 14:46:11 +03:00
DYefremov
5902b207e1 moved iptv menu 2021-09-03 18:09:26 +03:00
DYefremov
b23319ba52 decoupling for neutrino xml handling 2021-09-02 18:29:59 +03:00
DYefremov
cb8ec264cc added recordings page 2021-09-02 12:20:29 +03:00
DYefremov
9fc07308ab modifiers fix 2021-09-01 22:46:24 +03:00
DYefremov
5f0f1e4e64 added timers page 2021-09-01 14:15:39 +03:00
DYefremov
3fe78e0292 added epg page 2021-09-01 00:05:23 +03:00
DYefremov
0ae9123d98 satellite editor rework 2021-08-31 14:16:14 +03:00
DYefremov
7da3c0fd94 paths settings rework 2021-08-30 15:04:15 +03:00
DYefremov
b584126bff network settings rework 2021-08-27 00:19:16 +03:00
DYefremov
fb173449c6 minor rework of download dialog 2021-08-25 14:05:59 +03:00
DYefremov
64de141807 minor refactoring for neutrino 2021-08-25 14:05:27 +03:00
DYefremov
cab20f744f optimization of bouquets generation 2021-08-24 16:19:39 +03:00
DYefremov
9d44b8002c edit dialogs rework 2021-08-23 23:52:51 +03:00
DYefremov
d4389e8b0a base dialogs rework 2021-08-23 18:02:48 +03:00
DYefremov
57696a2460 base dialogs rework 2021-08-23 16:19:46 +03:00
DYefremov
8383214e15 basic dvb-t and cable support for neutrino 2021-08-23 14:34:56 +03:00
DYefremov
56f1c75a41 rework of view menu 2021-08-21 14:50:05 +03:00
DYefremov
f7be210b85 added services.xml for neutrino 2021-08-20 21:24:27 +03:00
DYefremov
7e0b694a4c title bar depending on the platform 2021-08-20 17:24:48 +03:00
DYefremov
9a24cae626 added displaying sub bouquets support 2021-08-18 19:46:47 +03:00
DYefremov
6e459f80bd some corrections 2021-08-18 00:24:51 +03:00
DYefremov
c55645e5db added mac app menu 2021-08-17 16:19:42 +03:00
DYefremov
d6796bc5a5 locale setting refactoring 2021-08-17 11:00:13 +03:00
DYefremov
4ac04e5401 rework of view headers 2021-08-15 17:24:30 +03:00
DYefremov
3c72f0cc3c added picons page 2021-08-15 15:42:27 +03:00
DYefremov
4b940b7135 gui rework 2021-08-15 14:37:21 +03:00
DYefremov
0aa9eaa401 bump version 2021-08-15 09:05:08 +03:00
DYefremov
de8445d55a Russian, Belarusian and German translations update 2021-08-12 13:55:01 +03:00
DYefremov
8153c5e6d6 fixed switching to full screen mode 2021-08-12 13:48:14 +03:00
DYefremov
51fd013e0f minor style changes of the control panel 2021-08-08 11:51:43 +03:00
DYefremov
872f3f0f81 added recordings tab to the control panel 2021-08-06 13:23:06 +03:00
DYefremov
95aa8aaed6 minor filtering optimization 2021-08-04 08:37:52 +03:00
DYefremov
b18207b376 Chinese translation correction 2021-08-02 19:09:26 +03:00
DYefremov
8f840ed3f5 added Chinese translation 2021-08-01 16:16:25 +03:00
DYefremov
6841090cc0 added "Save as" feature 2021-07-26 11:11:42 +03:00
DYefremov
4e700ca1e8 minor optimization of fav list loading 2021-07-26 09:45:05 +03:00
DYefremov
3312576416 data deletion optimization 2021-07-25 22:33:00 +03:00
DYefremov
b047153591 bump version 2021-07-25 21:47:57 +03:00
DYefremov
b15cae8d79 loading services list in the background 2021-07-11 23:29:19 +03:00
DYefremov
c5e8e19941 copy tr *.mo file 2021-06-13 17:23:42 +03:00
audi06_19
50d8e3365e Turkish translations update (#48)
* Turkish translations update

* Update: Turkish translations update

Co-authored-by: audi06_19 <info@dreamosat-forum.com>
2021-06-13 17:15:28 +03:00
DYefremov
42fd490a33 added bouquet service data validation 2021-05-27 12:56:11 +03:00
DYefremov
ef84a45ebb bump version 2021-05-26 19:58:39 +03:00
DYefremov
d924643436 minor fix for vlc init 2021-05-25 12:22:47 +03:00
DYefremov
a7bde2e25f README update 2021-05-20 00:37:36 +03:00
DYefremov
f64d9d31ab Russian, Belarusian and German translations update 2021-05-19 17:52:24 +03:00
DYefremov
ec4ebfa24a added support for editing extra pid's for services 2021-05-19 17:51:48 +03:00
DYefremov
03ba1af356 changed style of status bar buttons 2021-05-19 13:47:06 +03:00
DYefremov
d231edd0bb added check for 7-zip archiver 2021-05-19 13:20:30 +03:00
DYefremov
97fef050b7 small refactoring for iptv list dialog 2021-05-18 16:03:52 +03:00
DYefremov
61fe46da8a prevent header buttons visibility when ftp client is active 2021-05-17 14:26:38 +03:00
DYefremov
dc7dc9a087 minor fixes 2021-05-17 08:59:29 +03:00
DYefremov
e27c34df4e changes for m3u import dialog buttons 2021-05-17 00:07:44 +03:00
DYefremov
a3b6d37fd3 added error catching for logo loading 2021-05-16 12:50:02 +03:00
DYefremov
d01af855f9 minor changes for picon downloader 2021-05-15 16:29:31 +03:00
DYefremov
5353af94ac added support for saving single bouquets 2021-05-12 14:59:55 +03:00
DYefremov
2656d0b3a9 added picons loading only for the selected bouquet 2021-05-11 12:46:26 +03:00
DYefremov
a99c1e00b9 added basic support for downloading picons from picon.cz 2021-05-11 00:18:27 +03:00
DYefremov
73a1f2ccef bump version 2021-05-04 11:24:28 +03:00
DYefremov
a4892efe61 refactoring of picons downloading for m3u 2021-05-02 00:08:46 +03:00
DYefremov
5ded562e12 builder creation refactoring 2021-04-28 14:12:59 +03:00
DYefremov
d2508218f7 changed layout for ftp client 2021-04-22 11:39:25 +03:00
DYefremov
1e10df6f03 fixed getting sats for lyngsat 2021-04-19 17:08:04 +03:00
DYefremov
06887d9440 added mark for duplicates in fav list 2021-04-19 13:15:36 +03:00
DYefremov
3e9776e8e0 preventing size save of the maximized window 2021-04-12 17:08:43 +03:00
DYefremov
6b2c4dc79f fix picons downloading from the web 2021-04-11 18:00:40 +03:00
DYefremov
726abc4965 enabled lamedb5 support for import feature 2021-04-11 16:14:29 +03:00
DYefremov
6450cf1b17 fixed loading of services from the web 2021-04-10 22:40:29 +03:00
DYefremov
2aaa196acf combining of the search and filtering panels 2021-04-09 20:00:37 +03:00
DYefremov
4d35364a30 added auto-hide mouse cursor in playback mode 2021-04-09 19:09:11 +03:00
DYefremov
f1c5a57abd fix build *deb 2021-04-04 11:59:45 +03:00
DYefremov
dada57141f copy de *.mo file 2021-04-04 10:17:51 +03:00
Thomas Schmidt
d45d1d42c2 Update german translation (#47)
* Update german translation

* Update demon-editor.po

Co-authored-by: Dmitriy Yefremov <dmitry.v.yefremov@gmail.com>
2021-04-04 09:58:50 +03:00
DYefremov
9ee757aa19 minor fixes 2021-04-03 20:51:14 +03:00
DYefremov
17de98991d sid value fix for some picons 2021-04-03 15:21:53 +03:00
DYefremov
2862f22243 fixed width for provider name 2021-04-03 13:18:58 +03:00
DYefremov
9c2ab5c3e6 fixed picons download from the web 2021-04-01 23:00:46 +03:00
DYefremov
4d6fbfce6c added support for ATSC services 2021-04-01 09:01:08 +03:00
DYefremov
5c0d1d27f5 bump version 2021-03-31 15:27:39 +03:00
DYefremov
1c26887e8f README update 2021-03-26 21:19:50 +03:00
DYefremov
2d67fbfe44 Russian, Belarusian and German translations update 2021-03-26 15:11:28 +03:00
DYefremov
f502bce2aa added multiple choice of pos and type to filter feature 2021-03-26 11:46:43 +03:00
DYefremov
8c442280ec added base support for mpv 2021-03-23 22:13:33 +03:00
DYefremov
3b79677a9b some corrections for playback mode 2021-03-17 09:51:02 +03:00
DYefremov
33529ca010 minor fix for epg 2021-03-14 21:33:46 +03:00
DYefremov
eae14a472f added logo for the tooltip in the picon explorer 2021-03-14 13:17:48 +03:00
DYefremov
f85bbceceb added recommends to *.deb 2021-03-13 10:17:54 +03:00
DYefremov
f7cfc5a9a0 fixed picons download for providers 2021-03-12 13:37:06 +03:00
DYefremov
6b57c8f617 added display bouquet list in playback mode 2021-03-12 10:30:12 +03:00
DYefremov
1006251490 added new appearance options [list font, picons size] 2021-03-12 10:24:09 +03:00
DYefremov
e871492006 added dependencies to *.deb 2021-03-09 18:17:10 +03:00
DYefremov
353fb9cc53 reworking of built-in player [GStreamer support] 2021-03-09 18:14:15 +03:00
DYefremov
497964822e bump version 2021-02-23 10:32:54 +03:00
DYefremov
a067821d46 copy tr *.mo file 2021-02-23 10:27:06 +03:00
audi06_19
ab52964a20 Turkish translations update (#45) 2021-02-23 10:14:58 +03:00
DYefremov
344ac43996 improved satellites import [added KingOfSat support] 2021-02-20 15:29:49 +03:00
DYefremov
0e07b8d492 added info message after m3u import finished 2021-02-13 23:52:03 +03:00
DYefremov
7580a448c8 added custom sort function for position column 2021-02-13 15:04:54 +03:00
DYefremov
4886d2dfcf revert of action widget 2021-02-12 18:57:10 +03:00
DYefremov
2dfc571a64 fixed import of satellites from the web 2021-02-12 10:14:16 +03:00
DYefremov
7922f368b5 refactoring of picons downloading 2021-02-10 23:21:30 +03:00
DYefremov
d87c878d4d alt service timer delete/edit fix 2021-02-09 10:30:36 +03:00
DYefremov
564adf1c61 Russian, Belarusian and German translations update 2021-02-08 15:31:09 +03:00
DYefremov
9057dc1f81 minor changes in the player toolbar 2021-02-08 15:00:27 +03:00
DYefremov
108b3a72b9 minor fix 2021-02-08 14:59:56 +03:00
DYefremov
0090cc6262 streams play mode refactoring 2021-02-08 13:08:28 +03:00
DYefremov
acc946eb30 improved built-in player [added windowed mode] 2021-02-07 19:27:38 +03:00
DYefremov
b1dffe622d added picons downloading to * .m3u import 2021-02-05 01:12:44 +03:00
DYefremov
ab008aeb9c services web import fix 2021-02-02 00:09:33 +03:00
DYefremov
c4b6e009d5 improved *.m3u import 2021-01-31 16:27:35 +03:00
DYefremov
7164c6debd README update 2021-01-29 17:40:48 +03:00
DYefremov
fb06e4364f added saving of bouquet file names 2021-01-27 16:46:37 +03:00
DYefremov
7ff8212fa6 added epg display from the alternatives list 2021-01-21 16:51:25 +03:00
DYefremov
9822129307 improved service details editing [neutrino] 2021-01-21 15:03:01 +03:00
DYefremov
876aaeb4fe adaptation to the new format 2021-01-18 22:37:11 +03:00
DYefremov
b8140c4df0 changed format for freq, sr and pol columns 2021-01-18 18:52:58 +03:00
DYefremov
a364ff22bf lamedb parsing refactoring 2021-01-18 14:24:48 +03:00
DYefremov
bde2abf2e4 changed names for new config 2021-01-18 14:24:27 +03:00
DYefremov
42644e37c6 fix first bouquet name 2021-01-17 23:44:40 +03:00
DYefremov
9016143bb9 fix drag icon in filter mode 2021-01-14 23:05:31 +03:00
DYefremov
45cc506dd0 copyright update 2021-01-14 23:01:59 +03:00
DYefremov
c6a841de76 bump version 2021-01-14 23:00:36 +03:00
121 changed files with 34612 additions and 17802 deletions

View File

@@ -5,6 +5,9 @@ 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

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018-2020 Dmitriy Yefremov
Copyright (c) 2018-2021 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,33 +1,32 @@
# <img src="app/ui/icons/hicolor/96x96/apps/demon-editor.png" width="32" /> DemonEditor
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) ![platform](https://img.shields.io/badge/platform-linux%20|%20macos-lightgrey)
### Enigma2 channel and satellite list editor for GNU/Linux.
[<img src="https://user-images.githubusercontent.com/7511379/103152885-4a7bd880-479d-11eb-96e8-4ad0f5dc3e2e.png" width="560"/>](https://user-images.githubusercontent.com/7511379/103152885-4a7bd880-479d-11eb-96e8-4ad0f5dc3e2e.png)
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/100156250-a3fc9900-2eb9-11eb-8729-7bcb6ddcdd4a.png" width="480"/>](https://user-images.githubusercontent.com/7511379/100156250-a3fc9900-2eb9-11eb-8729-7bcb6ddcdd4a.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/103150878-8a38c500-4789-11eb-9e03-0c8ee832ff99.png" width="480"/>](https://user-images.githubusercontent.com/7511379/103150878-8a38c500-4789-11eb-9e03-0c8ee832ff99.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/103150886-a0df1c00-4789-11eb-88fa-91996b72d1f9.png" width="480"/>](https://user-images.githubusercontent.com/7511379/103150886-a0df1c00-4789-11eb-88fa-91996b72d1f9.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/103150891-accade00-4789-11eb-8804-e1807df89c99.png" width="480"/>](https://user-images.githubusercontent.com/7511379/103150891-accade00-4789-11eb-8804-e1807df89c99.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/97909451-4c0ac080-1d59-11eb-9626-85bed91f7ccc.png" width="250"/>](https://user-images.githubusercontent.com/7511379/97909451-4c0ac080-1d59-11eb-9626-85bed91f7ccc.png)
[<img src="https://user-images.githubusercontent.com/7511379/103150872-77be8b80-4789-11eb-8a74-7a49fb3edd98.png" width="292"/>](https://user-images.githubusercontent.com/7511379/103150872-77be8b80-4789-11eb-8a74-7a49fb3edd98.png)
[<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 (should be installed [VLC](https://www.videolan.org/vlc/)).
[<img src="https://user-images.githubusercontent.com/7511379/103151911-89a52c00-4793-11eb-9941-8430f4e87eef.png" width="480"/>](https://user-images.githubusercontent.com/7511379/103151911-89a52c00-4793-11eb-9941-8430f4e87eef.png)
* Control panel with the ability to view EPG and manage timers (via HTTP API, experimental).
[<img src="https://user-images.githubusercontent.com/7511379/103150898-c79d5280-4789-11eb-9d16-e7f89225738b.png" width="480"/>](https://user-images.githubusercontent.com/7511379/103150898-c79d5280-4789-11eb-9d16-e7f89225738b.png)
* 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/103152009-7e9ecb80-4794-11eb-85f1-c97e189a3195.png" width="480"/>](https://user-images.githubusercontent.com/7511379/103152009-7e9ecb80-4794-11eb-85f1-c97e189a3195.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.
@@ -51,15 +50,17 @@ 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!
## Minimum requirements
*Python >= 3.5.2, GTK+ >= 3.16 with PyGObject bindings, python3-requests.*
***Optional:** python3-gi-cairo, python3-pil, python3-chardet.*
*Python >= 3.6, GTK+ >= 3.22, python3-gi, python3-gi-cairo, python3-requests.*
***Optional:** python3-pil, python3-chardet.*
## Installation and Launch
* ### Linux
To start the program, in most cases it is enough to download the [archive](https://github.com/DYefremov/DemonEditor/archive/master.zip), unpack
@@ -68,23 +69,45 @@ or launching from the console with the command:```./start.py```
Extra folders can be deleted, excluding the *app* folder and root files like *DemonEditor.desktop* and *start.py*!
To create a simple **debian package**, you can use the *build-deb.sh.* You can also download a ready-made *.deb package from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
Users of **LTS** versions of [Ubuntu](https://ubuntu.com/) or distributions based on them can use [PPA](https://launchpad.net/~dmitriy-yefremov/+archive/ubuntu/demon-editor) repository.
* ### macOS (experimental)
**This program can also be run on macOS.**
To work in this OS, you must use a [separate branch](https://github.com/DYefremov/DemonEditor/tree/experimental-mac).
**The functionality and performance of this version may be different from the Linux version!**
## Important
The program is tested only with [openATV](https://www.opena.tv/) image and **Formuler F1** receiver in [Linux Mint](https://linuxmint.com/) (MATE 64-bit) distribution!
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 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```
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.
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.
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!
Support for DVB-T/T2 and DVB-C channels for Neutrino is not fully implemented and has an experimental status.
Main supported *lamedb* format is version **4**. Versions **3** and **5** has only **experimental** support! For version **3** is only read mode available. When saving, version **4** format is used instead.
When using the multiple import feature, from *lamedb* will be taken data **only for channels that are in the selected bouquets!**
If you need full set of the data, including *[satellites, terrestrial, cables].xml* (current files will be overwritten),
just load your data via *"File/Open"* and press *"Save"*. When importing separate bouquet files, only those services
(excluding IPTV) that are in the **current open lamedb** (main list of services) will be imported.
**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.
* **-d on/off** - turn on/off debug mode. Allows to display more information in the logs.

View File

@@ -1,4 +1,5 @@
import logging
from collections import defaultdict
from functools import wraps
from threading import Thread, Timer
@@ -6,13 +7,12 @@ from gi.repository import GLib
_LOG_FILE = "demon-editor.log"
_DATE_FORMAT = "%d-%m-%y %H:%M:%S"
_LOGGER_NAME = None
LOGGER_NAME = "main_logger"
def init_logger():
global _LOGGER_NAME
_LOGGER_NAME = "main_logger"
logging.Logger(_LOGGER_NAME)
logging.Logger(LOGGER_NAME)
logging.basicConfig(level=logging.INFO,
format="%(asctime)s %(message)s",
datefmt=_DATE_FORMAT,
@@ -22,7 +22,7 @@ def init_logger():
def log(message, level=logging.ERROR, debug=False, fmt_message="{}"):
""" The main logging function. """
logger = logging.getLogger(_LOGGER_NAME)
logger = logging.getLogger(LOGGER_NAME)
if debug:
from traceback import format_exc
logger.log(level, fmt_message.format(format_exc()))
@@ -77,5 +77,18 @@ def run_with_delay(timeout=5):
return run_with
class DefaultDict(defaultdict):
""" Extended to support functions with params as default factory. """
def __missing__(self, key):
if self.default_factory:
value = self[key] = self.default_factory(key)
return value
return super().__missing__(key)
def get(self, key, default=None):
return self[key]
if __name__ == "__main__":
pass

View File

@@ -1,3 +1,31 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
import os
import re
import socket
@@ -9,7 +37,7 @@ from ftplib import FTP, CRLF, Error, error_perm
from http.client import RemoteDisconnected
from telnetlib import Telnet
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.parse import urlencode, quote
from urllib.request import (urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener,
install_opener, Request)
@@ -17,7 +45,7 @@ from app.commons import log, run_task
from app.settings import SettingsType
BQ_FILES_LIST = ("tv", "radio", # enigma 2
"myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino
"services.xml", "myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino
DATA_FILES_LIST = ("lamedb", "lamedb5", "blacklist", "whitelist",)
@@ -107,8 +135,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:
@@ -181,7 +209,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)
@@ -282,9 +310,8 @@ 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)
@@ -323,12 +350,21 @@ 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")
save_path = settings.data_local_path
save_path = settings.profile_data_path
os.makedirs(os.path.dirname(save_path), exist_ok=True)
# bouquets
if download_type is DownloadType.ALL or download_type is DownloadType.BOUQUETS:
@@ -342,7 +378,7 @@ def download_data(*, settings, download_type=DownloadType.ALL, callback=log, fil
ftp.download_xml(save_path, settings.satellites_xml_path, WEB_TV_XML_FILE, callback)
if download_type is DownloadType.PICONS:
picons_path = settings.picons_local_path
picons_path = settings.profile_picons_path
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
ftp.download_picons(settings.picons_path, picons_path, callback, files_filter)
# epg.dat
@@ -362,15 +398,16 @@ def download_data(*, settings, download_type=DownloadType.ALL, callback=log, fil
def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False,
callback=log, done_callback=None, use_http=False, files_filter=None):
s_type = settings.setting_type
data_path = settings.data_local_path
host = settings.host
base_url = "http{}://{}:{}".format("s" if settings.http_use_ssl else "", host, settings.http_port)
url = "{}/web/".format(base_url)
data_path = settings.profile_data_path
host, port, use_ssl = settings.host, settings.http_port, settings.http_use_ssl
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
try:
if s_type is SettingsType.ENIGMA_2 and use_http:
ht = http(settings.user, settings.password, base_url, callback, settings.http_use_ssl)
if use_http:
ht = http(settings.user, settings.password, base_url, callback, use_ssl, s_type)
next(ht)
message = ""
if download_type is DownloadType.BOUQUETS:
@@ -382,22 +419,26 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
elif download_type is DownloadType.PICONS:
message = "Picons will be updated!"
params = urlencode({"text": message, "type": 2, "timeout": 5})
ht.send((url + "message?{}".format(params), "Sending info message... "))
if s_type is SettingsType.ENIGMA_2:
params = urlencode({"text": message, "type": 2, "timeout": 5})
else:
params = urlencode({"nmsg": message, "timeout": 5}, quote_via=quote)
if download_type is DownloadType.ALL:
ht.send((f"{url}message?{params}", "Sending info message... "))
if s_type is SettingsType.ENIGMA_2 and download_type is DownloadType.ALL:
time.sleep(5)
ht.send((url + "powerstate?newstate=0", "Toggle Standby "))
ht.send((f"{url}powerstate?newstate=0", "Toggle Standby "))
time.sleep(2)
else:
if download_type is not DownloadType.PICONS:
# telnet
# Telnet
tn = telnet(host=host,
user=settings.user,
password=settings.password,
timeout=settings.telnet_timeout)
next(tn)
# terminate enigma or neutrino
# Terminate Enigma2 or Neutrino.
callback("Telnet initialization ...\n")
tn.send("init 4")
callback("Stopping GUI...\n")
@@ -428,18 +469,21 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
ftp.upload_files(data_path, DATA_FILES_LIST, callback)
if download_type is DownloadType.PICONS:
ftp.upload_picons(settings.picons_local_path, settings.picons_path, callback, files_filter)
ftp.upload_picons(settings.profile_picons_path, settings.picons_path, callback, files_filter)
if tn and not use_http:
# resume enigma or restart neutrino
# 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")
elif ht and use_http:
if download_type is DownloadType.BOUQUETS:
ht.send((url + "servicelistreload?mode=2", "Reloading Userbouquets."))
elif download_type is DownloadType.ALL:
ht.send((url + "servicelistreload?mode=0", "Reloading lamedb and Userbouquets."))
ht.send((url + "powerstate?newstate=4", "Wakeup from Standby."))
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."))
else:
ht.send((f"{url}reloadchannels", "Reloading channels..."))
if done_callback is not None:
done_callback()
@@ -465,14 +509,17 @@ def picons_filter_function(files_filter=None):
return lambda f: f in files_filter if files_filter else f.endswith(PICONS_SUF)
def http(user, password, url, callback, use_ssl=False):
init_auth(user, password, url, use_ssl)
data = get_post_data(url, password, url)
def http(user, password, url, callback, use_ssl=False, s_type=SettingsType.ENIGMA_2):
HttpAPI.init_auth(user, password, url, use_ssl)
data = HttpAPI.get_post_data(url, password, url) if s_type is SettingsType.ENIGMA_2 else None
while True:
url, message = yield
resp = get_response(HttpAPI.Request.TEST, url, data).get("e2statetext", None)
callback("HTTP: {} {}\n".format(message, "Successful." if resp and message else ""))
resp = HttpAPI.get_response(HttpAPI.Request.TEST, url, data, s_type)
if s_type is SettingsType.ENIGMA_2:
resp = resp.get("e2statetext", None)
callback(f"HTTP: {message} {'Successful.' if resp and message else ''}\n")
def telnet(host, port=23, user="", password="", timeout=5):
@@ -505,11 +552,12 @@ def telnet(host, port=23, user="", password="", timeout=5):
class HttpAPI:
__MAX_WORKERS = 4
class Request(Enum):
class Request(str, Enum):
ZAP = "zap?sRef="
INFO = "about"
SIGNAL = "signal"
STREAM = "stream.m3u?ref="
STREAM_TS = "ts.m3u?file="
STREAM_CURRENT = "streamcurrent.m3u"
CURRENT = "getcurrent"
TEST = None
@@ -531,8 +579,16 @@ class HttpAPI:
# Timer
TIMER = ""
TIMER_LIST = "timerlist"
# Recordings
RECORDINGS = "movielist?dirname="
REC_DIRS = "getlocations"
REC_CURRENT = "getcurrlocation"
# Screenshot
GRUB = "grab?format=jpg&"
# Neutrino requests.
N_INFO = "info"
N_ZAP = "zapto"
N_STREAM = "build_playlist?id="
class Remote(str, Enum):
""" Args for HttpRequestType [REMOTE] class. """
@@ -543,10 +599,12 @@ class HttpAPI:
MENU = "139"
EXIT = "174"
OK = "352"
INFO = "358"
RED = "398"
GREEN = "399"
YELLOW = "400"
BLUE = "401"
BACK = "412"
class Power(str, Enum):
""" Args for HttpRequestType [POWER] class. """
@@ -557,6 +615,19 @@ class HttpAPI:
WAKEUP = "4"
STANDBY = "5"
PARAM_REQUESTS = {Request.REMOTE,
Request.POWER,
Request.VOL,
Request.EPG,
Request.TIMER,
Request.RECORDINGS,
Request.N_ZAP}
STREAM_REQUESTS = {Request.STREAM,
Request.STREAM_CURRENT,
Request.STREAM_TS,
Request.N_STREAM}
def __init__(self, settings):
from concurrent.futures import ThreadPoolExecutor as PoolExecutor
self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS)
@@ -568,46 +639,45 @@ class HttpAPI:
self._base_url = None
self._data = None
self._is_owif = True
self._s_type = SettingsType.ENIGMA_2
self.init()
def send(self, req_type, ref, callback=print, ref_prefix=""):
if self._shutdown:
return
url = self._base_url + req_type.value
url = self._base_url + req_type
data = self._data
if req_type is self.Request.ZAP or req_type is self.Request.STREAM:
url += urllib.parse.quote(ref)
if req_type is self.Request.ZAP or req_type in self.STREAM_REQUESTS:
url += quote(ref)
elif req_type is self.Request.PLAY or req_type is self.Request.PLAYER_REMOVE:
url += "{}{}".format(ref_prefix, urllib.parse.quote(ref).replace("%3A", "%253A"))
url = f"{url}{ref_prefix}{quote(ref).replace('%3A', '%253A')}"
elif req_type is self.Request.GRUB:
data = None # Must be disabled for token-based security.
url = "{}/{}{}".format(self._main_url, req_type.value, ref)
elif req_type in (self.Request.REMOTE,
self.Request.POWER,
self.Request.VOL,
self.Request.EPG,
self.Request.TIMER):
url = f"{self._main_url}/{req_type}{ref}"
elif req_type in self.PARAM_REQUESTS:
url += ref
def done_callback(f):
callback(f.result())
future = self._executor.submit(get_response, req_type, url, data)
future = self._executor.submit(self.get_response, req_type, url, data, self._s_type)
future.add_done_callback(done_callback)
@run_task
def init(self):
user, password = self._settings.user, self._settings.password
use_ssl = self._settings.http_use_ssl
self._main_url = "http{}://{}:{}".format("s" if use_ssl else "", self._settings.host, self._settings.http_port)
self._base_url = "{}/web/".format(self._main_url)
init_auth(user, password, self._main_url, use_ssl)
url = "{}/web/{}".format(self._main_url, self.Request.TOKEN.value)
s_id = get_session_id(user, password, url)
if s_id != "0":
self._data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
self._s_type = self._settings.setting_type
user, password, use_ssl = self._settings.user, self._settings.password, self._settings.http_use_ssl
self._main_url = f"http{'s' if use_ssl else ''}://{self._settings.host}:{self._settings.http_port}"
self._base_url = f"{self._main_url}/{'web' if self._s_type is SettingsType.ENIGMA_2 else 'control'}/"
self.init_auth(user, password, self._main_url, use_ssl)
self._data = None
if self._s_type is SettingsType.ENIGMA_2:
s_id = self.get_session_id(user, password, f"{self._main_url}/web/{self.Request.TOKEN}")
if s_id != "0":
self._data = urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
self.send(self.Request.INFO, None, self.init_callback)
@@ -628,69 +698,91 @@ class HttpAPI:
self._shutdown = True
self._executor.shutdown()
@staticmethod
def get_response(req_type, url, data=None, s_type=SettingsType.ENIGMA_2):
try:
with urlopen(Request(url, data=data), timeout=10) 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:
return HttpAPI.get_neutrino_response_data(req_type, f)
else:
return f.read().decode("utf-8")
except HTTPError as e:
if req_type is HttpAPI.Request.TEST:
raise e
return {"error_code": e.code}
except (URLError, RemoteDisconnected, ConnectionResetError) as e:
if req_type is HttpAPI.Request.TEST:
raise e
except ETree.ParseError as e:
log("Parsing response error: {}".format(e))
def get_response(req_type, url, data=None):
try:
with urlopen(Request(url, data=data), timeout=10) as f:
if req_type is HttpAPI.Request.STREAM or req_type is HttpAPI.Request.STREAM_CURRENT:
return {"m3u": f.read().decode("utf-8")}
elif req_type is HttpAPI.Request.GRUB:
return {"img_data": f.read()}
elif req_type is HttpAPI.Request.CURRENT:
for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2event"):
return {el.tag: el.text for el in el.iter()} # return first[current] event from the list
elif req_type is HttpAPI.Request.PLAYER_LIST:
return [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2file")]
elif req_type is HttpAPI.Request.EPG:
return {"event_list": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2event")]}
elif req_type is HttpAPI.Request.TIMER_LIST:
return {"timer_list": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2timer")]}
else:
return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()}
except HTTPError as e:
if req_type is HttpAPI.Request.TEST:
raise e
return {"error_code": e.code}
except (URLError, RemoteDisconnected, ConnectionResetError) as e:
if req_type is HttpAPI.Request.TEST:
raise e
except ETree.ParseError as e:
log("Parsing response error: {}".format(e))
return {"error_code": -1}
return {"error_code": -1}
@staticmethod
def get_e2_response_data(req_type, f):
if req_type in HttpAPI.STREAM_REQUESTS:
return {"m3u": f.read().decode("utf-8")}
elif req_type is HttpAPI.Request.GRUB:
return {"img_data": f.read()}
elif req_type is HttpAPI.Request.CURRENT:
for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2event"):
return {el.tag: el.text for el in el.iter()} # return first[current] event from the list
elif req_type is HttpAPI.Request.PLAYER_LIST:
return [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2file")]
elif req_type is HttpAPI.Request.EPG:
return {"event_list": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2event")]}
elif req_type is HttpAPI.Request.TIMER_LIST:
return {"timer_list": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2timer")]}
elif req_type is HttpAPI.Request.REC_DIRS:
return {"rec_dirs": [el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2location")]}
elif req_type is HttpAPI.Request.RECORDINGS:
return {"recordings": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2movie")]}
else:
return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()}
@staticmethod
def get_neutrino_response_data(req_type, f):
if req_type is HttpAPI.Request.N_INFO:
return {"info": f.read().decode("utf-8").strip()}
elif req_type is HttpAPI.Request.N_STREAM:
return {"m3u": f.read().decode("utf-8")}
return {"data": f.read().decode("utf-8")}
def init_auth(user, password, url, use_ssl=False):
""" Init authentication """
pass_mgr = HTTPPasswordMgrWithDefaultRealm()
pass_mgr.add_password(None, url, user, password)
auth_handler = HTTPBasicAuthHandler(pass_mgr)
@staticmethod
def init_auth(user, password, url, use_ssl=False):
""" Init authentication """
pass_mgr = HTTPPasswordMgrWithDefaultRealm()
pass_mgr.add_password(None, url, user, password)
auth_handler = HTTPBasicAuthHandler(pass_mgr)
if use_ssl:
import ssl
from urllib.request import HTTPSHandler
if use_ssl:
import ssl
from urllib.request import HTTPSHandler
opener = build_opener(auth_handler, HTTPSHandler(context=ssl._create_unverified_context()))
else:
opener = build_opener(auth_handler)
opener = build_opener(auth_handler, HTTPSHandler(context=ssl._create_unverified_context()))
else:
opener = build_opener(auth_handler)
install_opener(opener)
install_opener(opener)
@staticmethod
def get_session_id(user, password, url):
data = urllib.parse.urlencode(dict(user=user, password=password)).encode("utf-8")
return HttpAPI.get_response(HttpAPI.Request.TOKEN, url, data=data).get("e2sessionid", "0")
def get_session_id(user, password, url):
data = urllib.parse.urlencode(dict(user=user, password=password)).encode("utf-8")
return get_response(HttpAPI.Request.TOKEN, url, data=data).get("e2sessionid", "0")
def get_post_data(base_url, password, user):
s_id = get_session_id(user, password, "{}/web/{}".format(base_url, HttpAPI.Request.TOKEN.value))
data = None
if s_id != "0":
data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
return data
@staticmethod
def get_post_data(base_url, password, user):
s_id = HttpAPI.get_session_id(user, password, "{}/web/{}".format(base_url, HttpAPI.Request.TOKEN))
data = None
if s_id != "0":
data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
return data
# ***************** Connections testing *******************#
@@ -703,16 +795,29 @@ def test_ftp(host, port, user, password, timeout=5):
raise TestException(e)
def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message=False):
params = urlencode({"text": "Connection test", "type": 2, "timeout": timeout})
params = "statusinfo" if skip_message else "message?{}".format(params)
base_url = "http{}://{}:{}".format("s" if use_ssl else "", host, port)
# authentication
init_auth(user, password, base_url, use_ssl)
data = get_post_data(base_url, password, user)
def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message=False, s_type=SettingsType.ENIGMA_2):
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}"
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}"
else:
raise TestException("This type of settings is not supported!")
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}/{params}"
# Authentication
HttpAPI.init_auth(user, password, base_url, use_ssl)
data = HttpAPI.get_post_data(base_url, password, user) if s_type is SettingsType.ENIGMA_2 else None
try:
return get_response(HttpAPI.Request.TEST, "{}/web/{}".format(base_url, params), data).get("e2statetext", "")
resp = HttpAPI.get_response(HttpAPI.Request.TEST, url, data, s_type)
if s_type is SettingsType.ENIGMA_2:
return resp.get("e2statetext", "")
return resp
except (RemoteDisconnected, URLError, HTTPError) as e:
raise TestException(e)

View File

@@ -1,3 +1,30 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
from app.commons import run_task
from app.settings import SettingsType
from .ecommons import Service, Satellite, Transponder, Bouquet, Bouquets, is_transponder_valid
@@ -32,6 +59,15 @@ def get_bouquets(path, s_type):
return get_neutrino_bouquets(path)
def write_bouquet(path, bq, s_type):
if s_type is SettingsType.ENIGMA_2:
writer = BouquetsWriter(path, None)
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):
if s_type is SettingsType.ENIGMA_2:

View File

@@ -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"])
@@ -15,9 +45,15 @@ class BqServiceType(Enum):
MARKER = "MARKER" # 64
SPACE = "SPACE" # 832 [hidden marker]
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"])
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"])
@@ -25,8 +61,8 @@ BouquetService = namedtuple("BouquetService", ["name", "type", "data", "num"])
Satellite = namedtuple("Satellite", ["name", "flags", "position", "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"])
class TrType(Enum):
@@ -34,14 +70,20 @@ class TrType(Enum):
Satellite = "s"
Terrestrial = "t"
Cable = "c"
ATSC = "a"
class BqType(Enum):
""" Bouquet type"""
""" Bouquet type. """
BOUQUET = "bouquet"
TV = "tv"
RADIO = "radio"
WEBTV = "webtv"
MARKER = "marker"
@classmethod
def _missing_(cls, value):
return cls.TV
class Flag(Enum):
@@ -74,6 +116,21 @@ class Flag(Enum):
def is_new(value: int):
return value & 1 << 5
@staticmethod
def parse(value: str) -> int:
""" Returns an int representation of the flag value.
The flag value is usually represented by the number [int],
but can also be appear in hex format.
"""
if len(value) < 3:
return 0
value = value[2:]
if value.isdigit():
return int(value)
return int(value, 16)
class Pids(Enum):
VIDEO = "c:00"
@@ -93,12 +150,20 @@ class Inversion(Enum):
On = "1"
Auto = "2"
@classmethod
def _missing_(cls, value):
return cls.Auto
class Pilot(Enum):
Off = "0"
On = "1"
Auto = "2"
@classmethod
def _missing_(cls, value):
return cls.Auto
class SystemCable(Enum):
""" System of cable service """
@@ -146,6 +211,10 @@ T_SYSTEM = {"0": "DVB-T", "1": "DVB-T2", "-1": "DVB-T/T2"}
# Cable
C_MODULATION = {"0": "Auto", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM128", "5": "QAM256"}
# ATSC
A_MODULATION = {"0": "Auto", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM128", "5": "QAM256", "6": "8VSB",
"7": "16VSB"}
# CAS
CAS = {"C:26": "BISS", "C:0B": "Conax", "C:06": "Irdeto", "C:18": "Nagravision", "C:05": "Viaccess", "C:01": "SECA",
"C:0E": "PowerVu", "C:4A": "DRE-Crypt", "C:7B": "DRE-Crypt", "C:56": "Verimatrix", "C:09": "VideoGuard"}
@@ -171,14 +240,16 @@ 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():

View File

@@ -1,3 +1,31 @@
# -*- 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
#
""" This module used for parsing blacklist file
Parent Lock/Unlock
@@ -9,14 +37,14 @@ __FILE_NAME = "blacklist"
def get_blacklist(path):
with suppress(FileNotFoundError):
with open(path + __FILE_NAME, "r") as file:
with open(path + __FILE_NAME, "r", encoding="utf-8") as file:
# filter empty values and "\n"
return {*list(filter(None, (x.strip() for x in file.readlines())))}
return {}
def write_blacklist(path, channels):
with open(path + __FILE_NAME, "w") as file:
with open(path + __FILE_NAME, "w", encoding="utf-8") as file:
if channels:
file.writelines("\n".join(channels))

View File

@@ -1,6 +1,35 @@
# -*- 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 Enigma2 bouquets. """
import re
from collections import Counter
from enum import Enum
from pathlib import Path
from app.commons import log
@@ -9,10 +38,11 @@ from app.eparser.ecommons import BqServiceType, BouquetService, Bouquets, Bouque
_TV_FILE = "bouquets.tv"
_RADIO_FILE = "bouquets.radio"
_DEFAULT_BOUQUET_NAME = "favourites"
_MARKER_PREFIX = "[MARKER!] "
class BouquetsWriter:
""" Class for creating and writing bouquet files..
""" Class for creating and writing bouquet files.
If "force_bq_names" then naming the files using the name of the bouquet.
Some images may have problems displaying the favorites list!
@@ -27,37 +57,57 @@ class BouquetsWriter:
self._path = path
self._bouquets = bouquets
self._force_bq_names = force_bq_names
self._marker_index = 0
self._marker_index = 1
self._space_index = 0
self._alt_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
for index, bq in enumerate(bqs.bouquets):
bq_name = bq.name
if bq_name == "Favourites (TV)" or bq_name == "Favourites (Radio)":
bq_name = _DEFAULT_BOUQUET_NAME
for bq in bqs.bouquets:
bq_name = bq.file
if not bq_name:
if self._force_bq_names:
bq_name = re.sub(self._NAME_PATTERN, "_", bq.name)
else:
bq_name = f"de{count:02d}"
while bq_name in bq_file_names:
count += 1
bq_name = f"de{count:02d}"
bq_file_names.add(bq_name)
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:
bq_name = re.sub(pattern, "_", bq.name) if self._force_bq_names else "de{0:02d}".format(index)
line.append(self._SERVICE.format(2 if bq.type == BqType.RADIO.value else 1, bq_name, bq.type))
self.write_bouquet(self._path + "userbouquet.{}.{}".format(bq_name, bq.type), bq.name, bq.services)
if bq_type is BqType.BOUQUET:
bq_name = re.sub(self._NAME_PATTERN, "_", bq.name)
self.write_sub_bouquet(self._path, bq_name, bq, bqs.type)
else:
self.write_bouquet(f"{self._path}userbouquet.{bq_name}.{bqs.type}", bq.name, bq.services)
line.append(self._SERVICE.format(2 if bqs.type == BqType.RADIO.value else 1, bq_name, bqs.type))
with open(self._path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file:
with open(f"{self._path}bouquets.{bqs.type}", "w", encoding="utf-8", newline="\n") as file:
file.writelines(line)
def write_bouquet(self, path, name, services):
""" Writes single bouquet file. """
bouquet = ["#NAME {}\n".format(name)]
bouquet = [f"#NAME {name}\n"]
for srv in services:
s_type = srv.service_type
if s_type == BqServiceType.IPTV.name:
bouquet.append("#SERVICE {}\n".format(srv.fav_id.strip()))
bouquet.append(f"#SERVICE {srv.fav_id.strip()}\n")
elif s_type == BqServiceType.MARKER.name:
m_data = srv.fav_id.strip().split(":")
m_data[2] = self._marker_index
@@ -70,30 +120,57 @@ class BouquetsWriter:
services = srv.transponder
if services:
p = Path(path)
alt_name = srv.data_id
f_name = f"alternatives.{alt_name}{p.suffix}"
if self._force_bq_names:
alt_name = re.sub(self._ALT_PAT, "_", srv.service).lower()
f_name = "alternatives.{}{}".format(alt_name, p.suffix)
else:
f_name = "alternatives.de{:02d}{}".format(self._alt_index, p.suffix)
self._alt_index += 1
alt_path = "{}/{}".format(p.parent, f_name)
f_name = f"alternatives.{alt_name}{p.suffix}"
bouquet.append(self._ALT.format(f_name))
self.write_bouquet(alt_path, srv.service, services)
self.write_bouquet(f"{p.parent}/{f_name}", srv.service, services)
else:
data = to_bouquet_id(srv)
if srv.service:
bouquet.append("#SERVICE {}:{}\n#DESCRIPTION {}\n".format(data, srv.service, srv.service))
bouquet.append(f"#SERVICE {data}:{srv.service}\n#DESCRIPTION {srv.service}\n")
else:
bouquet.append("#SERVICE {}\n".format(data))
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)
class ServiceType(Enum):
SERVICE = "0"
BOUQUET = "7" # Sub bouquet.
MARKER = "64"
SPACE = "832" # Hidden marker.
ALT = "134" # Alternatives.
UDP = "256"
@classmethod
def _missing_(cls, value):
log("Error. No matching service type [{} {}] was found.".format(cls.__name__, value))
return cls.SERVICE
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]+).*")
_STREAM_TYPES = {"4097", "5001", "5002", "8193", "8739"}
__slots__ = ["_path"]
@@ -107,77 +184,96 @@ class BouquetsReader:
def parse_bouquets(self, bq_name, bq_type):
with open(self._path + bq_name, encoding="utf-8", errors="replace") as file:
lines = file.readlines()
bouquets = None
nm_sep = "#NAME"
line = file.readline()
_, _, bqs_name = line.partition("#NAME")
if not bqs_name:
log(f"No bouquets name found in '{bq_name}'")
bqs_name = "Bouquets (TV)" if bq_type == BqType.TV.value else "Bouquets (Radio)"
bouquets = Bouquets(bqs_name.strip(), bq_type, [])
b_names = set()
real_b_names = Counter()
for line in lines:
if nm_sep in line:
_, _, name = line.partition(nm_sep)
bouquets = Bouquets(name.strip(), bq_type, [])
if bouquets and "#SERVICE" in line:
for line in file.readlines():
if "#SERVICE" in line:
name = re.match(self._BQ_PAT, line)
if name:
b_name = name.group(1)
if b_name in b_names:
log("The list of bouquets contains duplicate [{}] names!".format(b_name))
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)
if rb_name in real_b_names:
log("Bouquet file 'userbouquet.{}.{}' has duplicate name: {}".format(b_name, bq_type,
rb_name))
log(f"Bouquet file 'userbouquet.{b_name}.{bq_type}' has duplicate name: {rb_name}")
real_b_names[rb_name] += 1
rb_name = "{} {}".format(rb_name, real_b_names[rb_name])
rb_name = f"{rb_name} {real_b_names[rb_name]}"
else:
real_b_names[rb_name] = 0
bouquets[2].append(Bouquet(rb_name, bq_type, services, None, None))
bouquets[2].append(Bouquet(rb_name, bq_type, services, None, None, b_name))
else:
raise ValueError("No bouquet name found for: {}".format(line))
s_data = line.split(":")
if len(s_data) == 12 and s_data[1] == ServiceType.MARKER.value:
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}].")
else:
log(f"Unsupported or invalid line format: [{line}].")
return bouquets
@staticmethod
def get_bouquet(path, bq_name, bq_type, prefix="userbouquet"):
""" Parsing services ids from bouquet file. """
with open(path + "{}.{}.{}".format(prefix, bq_name, bq_type), encoding="utf-8", errors="replace") as file:
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)
for num, srv in enumerate(srvs, start=1):
srv_data = srv.strip().split(":")
s_type = srv_data[1]
if s_type == "64":
data_len = len(srv_data)
if data_len < 10:
log(f"The bouquet [{bq_name}] service [{num}] has the wrong data format: [{srv}]")
continue
s_type = ServiceType(srv_data[1])
if s_type is ServiceType.MARKER:
m_data, sep, desc = srv.partition("#DESCRIPTION")
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.MARKER, srv, num))
elif s_type == "832":
elif s_type is ServiceType.SPACE:
m_data, sep, desc = srv.partition("#DESCRIPTION")
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.SPACE, srv, num))
elif s_type == "134":
elif s_type is ServiceType.ALT:
alt = re.match(BouquetsReader._ALT_PAT, srv)
if alt:
alt_name, alt_type = alt.group(1), alt.group(2)
alt_bq_name, alt_srvs = BouquetsReader.get_bouquet(path, alt_name, alt_type, "alternatives")
services.append(BouquetService(alt_bq_name, BqServiceType.ALT, srv.lstrip(), tuple(alt_srvs)))
services.append(BouquetService(alt_bq_name, BqServiceType.ALT, alt_name, tuple(alt_srvs)))
elif s_type is ServiceType.BOUQUET:
sub = re.match(BouquetsReader._SUB_BQ_PAT, srv)
if sub:
sub_name, sub_type = sub.group(1), sub.group(2)
sub_bq_name, sub_srvs = BouquetsReader.get_bouquet(path, sub_name, sub_type, "subbouquet")
bq = Bouquet(sub_bq_name, sub_type, tuple(sub_srvs), None, None, sub_name)
services.append(BouquetService(sub_bq_name, BqServiceType.BOUQUET, bq, num))
elif srv_data[0].strip() in BouquetsReader._STREAM_TYPES or srv_data[10].startswith(("http", "rtsp")):
stream_data, sep, desc = srv.partition("#DESCRIPTION")
desc = desc.lstrip(":").strip() if desc else srv_data[-1].strip()
services.append(BouquetService(desc, BqServiceType.IPTV, srv, num))
else:
fav_id = "{}:{}:{}:{}".format(srv_data[3], srv_data[4], srv_data[5], srv_data[6])
fav_id = f"{srv_data[3]}:{srv_data[4]}:{srv_data[5]}:{srv_data[6]}"
name = None
if len(srv_data) == 12:
if data_len == 12:
name, sep, desc = str(srv_data[-1]).partition("\n#DESCRIPTION")
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id.upper(), num))

View File

@@ -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
#
""" This module used for parsing and write lamedb file """
import re
@@ -13,282 +41,302 @@ _END_LINE = "# File was created in DemonEditor.\n# ....Enjoy watching!....\n"
def get_services(path, format_version):
return parse(path, format_version)
return LameDbReader(path, format_version).parse()
def write_services(path, services, format_version=4):
if format_version == 4:
write_to_lamedb(path, services)
elif format_version == 5:
write_to_lamedb5(path, services)
LameDbWriter(path, services, format_version).write()
def write_to_lamedb(path, services):
""" Writing lamedb file ver.4 """
with open(path + _FILE_NAME, "w") as file:
file.writelines(get_services_lines(services))
class LameDbReader:
""" Lamedb parser class.
Reads and parses the Enigma2 lamedb[5] file.
Supports versions 3, 4 and 5.
"""
__slots__ = ["_path", "_fmt"]
def __init__(self, path, fmt=4):
self._path = path
self._fmt = fmt
def parse(self):
""" Parsing lamedb. """
if self._fmt == 4:
return self.parse_v4()
elif self._fmt == 5:
return self.parse_v5()
raise SyntaxError("Unsupported version of the format.")
def parse_v3(self, services, transponders):
""" Parsing version 3. """
for t in transponders:
tr = transponders[t].lower()
tr_type = tr[0:1]
if tr_type == "c":
tr += ":0:0:0"
elif tr_type == "t" or tr_type == "a":
tr += ":0:0"
else:
tr_data = tr.split(_SEP)
len_data = len(tr_data)
if len_data == 6:
tr_data.append("0")
elif len_data == 9:
tr_data.insert(6, "0")
tr_data.append("0")
tr_data.append("2")
tr = _SEP.join(tr_data)
transponders[t] = tr
return self.parse_services(services, transponders)
def parse_v4(self):
""" Parsing version 4. """
with open(self._path + _FILE_NAME, "r", encoding="utf-8", errors="replace") as file:
try:
data = str(file.read())
except UnicodeDecodeError as e:
log(f"lamedb parse error: {e}")
else:
return self.get_services_list(data)
def parse_v5(self):
""" Parsing version 5. """
with open(self._path + "lamedb5", "r", encoding="utf-8", errors="replace") as file:
lns = file.readlines()
if lns and not lns[0].endswith("/5/\n"):
raise SyntaxError("lamedb ver.5 parsing error: unsupported format.")
trs, srvs = {}, [""]
for line in lns:
if line.startswith("s:"):
srv_data = line.strip("s:").split(",", 2)
srv_data[1] = srv_data[1].strip("\"\n")
data_len = len(srv_data)
if data_len == 3:
srv_data[2] = srv_data[2].strip()
elif data_len == 2:
srv_data.append("p:")
srvs.extend(srv_data)
elif line.startswith("t:"):
data = line.split(",")
len_data = len(data)
if len_data > 1:
tr, srv = data[0].strip("t:"), data[1].strip().replace(":", " ", 1)
trs[tr] = srv
else:
log(f"Error while parsing transponder data [ver. 5] for line: {line}")
return self.parse_services(srvs, trs)
def parse_services(self, services, transponders):
""" Parsing services. """
services_list = []
blacklist = get_blacklist(self._path) if self._path else {}
srvs = self.split(services, 3)
if srvs[0][0] == "": # Remove first empty element.
srvs.remove(srvs[0])
for srv in srvs:
data_id = str(srv[0]).lower() # Lower is for lamedb ver.3.
data = data_id.split(_SEP)
sp = "0"
tid = data[2]
nid = data[3]
# For lamedb ver.3
is_v3 = False
if len(tid) < 4:
is_v3 = True
tid = f"{tid:0>4}"
data[2] = tid
if len(nid) < 4:
is_v3 = True
nid = f"{nid:0>4}"
data[3] = nid
if is_v3:
data[0] = f"{data[0]:0>4}"
data_id = _SEP.join(data)
srv_type = int(data[4])
transponder_id = f"{data[1]}:{tid}:{nid}"
transponder = transponders.get(transponder_id, None)
# 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 = 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
flags = list(filter(lambda x: x.startswith("f:"), all_flags))
hide = HIDE_ICON if flags and Flag.is_hide(Flag.parse(flags[0])) else None
locked = LOCKED_ICON if s_id in blacklist else None
package = list(filter(lambda x: x.startswith("p:"), all_flags))
package = package[0][2:] if package else ""
if transponder is not None:
tr_type, sp, tr = str(transponder).partition(" ")
tr_type = TrType(tr_type)
tr = tr.split(_SEP)
service_type = SERVICE_TYPE.get(data[4], SERVICE_TYPE["-2"])
# Removing all non-printable symbols!
srv_name = "".join(c for c in srv[1] if c.isprintable())
freq = tr[0]
rate = tr[1]
pol = None
fec = None
system = None
pos = None
if tr_type is TrType.Satellite:
pol = POLARIZATION.get(tr[2], None)
fec = FEC.get(tr[3], None)
system = "DVB-S2" if len(tr) > 7 else "DVB-S"
pos = tr[4]
if tr_type is TrType.Terrestrial:
system = T_SYSTEM.get(tr[10] if len(tr) > 10 else "0", None)
pos = "T"
fec = T_FEC.get(tr[3], None)
elif tr_type is TrType.Cable:
system = "DVB-C"
pos = "C"
fec = FEC_DEFAULT.get(tr[4])
elif tr_type is TrType.ATSC:
system = "ATSC"
pos = "T"
fec = FEC_DEFAULT.get("0")
# Formatting displayed values.
try:
freq = f"{int(freq) // 1000}"
rate = f"{int(rate) // 1000}"
if tr_type is TrType.Satellite:
pos = int(pos)
pos = f"{abs(pos / 10):0.1f}{'W' if pos < 0 else 'E'}"
except ValueError as 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)
services_list.append(s)
return services_list
def get_services_list(self, data):
""" Returns a list of services from a string data representation. """
transponders, sep, services = data.partition("transponders") # 1 step
pattern = re.compile("/[34]/$")
match = re.search(pattern, transponders)
if not match:
msg = "lamedb parsing error: unsupported format."
log(msg)
raise SyntaxError(msg)
transponders, sep, services = services.partition("services") # 2 step
services, sep, _ = services.partition("\nend") # 3 step
if match.group() == "/3/":
return self.parse_v3(services.split("\n"), self.parse_transponders(transponders.split("/")))
return self.parse_services(services.split("\n"), self.parse_transponders(transponders.split("/")))
@staticmethod
def get_services_lines(services):
""" Returns a list of strings from services for lamedb [v.4]. """
lines = [_HEADER.format(4), "\ntransponders\n"]
tr_lines = []
services_lines = ["end\nservices\n"]
tr_set = set()
for srv in services:
data_id = str(srv.data_id).split(_SEP)
tr_id = f"{data_id[1]}:{data_id[2]}:{data_id[3]}"
if tr_id not in tr_set:
tr_lines.append(f"{tr_id}\n\t{srv.transponder}\n/\n")
tr_set.add(tr_id)
# Services
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(f"end\n{_END_LINE}")
return lines
def parse_transponders(self, arg):
""" Parsing transponders. """
transponders = {}
for ar in arg:
tr = ar.replace("\n", "").split("\t")
if len(tr) == 2:
transponders[tr[0]] = tr[1]
return transponders
def split(self, itr, size):
""" Divide the iterable. """
srv = []
tmp = []
for i, line in enumerate(itr):
tmp.append(line)
if i % size == 0:
srv.append(tuple(tmp))
tmp.clear()
return srv
def get_services_lines(services):
""" Returns a list of strings from services for lamedb [v.4]. """
lines = [_HEADER.format(4), "\ntransponders\n"]
tr_lines = []
services_lines = ["end\nservices\n"]
tr_set = set()
for srv in services:
data_id = str(srv.data_id).split(_SEP)
tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3])
if tr_id not in tr_set:
transponder = "{}\n\t{}\n/\n".format(tr_id, srv.transponder)
tr_lines.append(transponder)
tr_set.add(tr_id)
# Services
services_lines.append("{}\n{}\n{}\n".format(srv.data_id, srv.service, srv.flags_cas))
class LameDbWriter:
""" Writes the Enigma2 lamedb[5] file.
tr_lines.sort()
lines.extend(tr_lines)
lines.extend(services_lines)
lines.append("end\n" + _END_LINE)
Version 4 will be used instead of version 3!
"""
__slots__ = ["_path", "_fmt", "_services"]
return lines
def __init__(self, path, services, fmt=4):
self._path = path
self._fmt = fmt
self._services = services
def write(self):
if self._fmt == 4:
# Writing lamedb file ver.4
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()
def write_to_lamedb5(path, services):
""" Writing lamedb5 file. """
lines = [_HEADER.format(5) + "\n"]
services_lines = []
tr_set = set()
def write_to_lamedb5(self):
""" Writing lamedb5 file. """
lines = [_HEADER.format(5) + "\n"]
services_lines = []
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_set.add("t:{},{}\n".format(tr_id, srv.transponder.replace(" ", ":", 1)))
# Removing empty packages
flags = list(filter(lambda x: x != "p:", srv.flags_cas.split(",")))
flags = ",".join(flags)
flags = "," + flags if flags else ""
services_lines.append("s:{},\"{}\"{}\n".format(srv.data_id, srv.service, flags))
for srv in self._services:
data_id = str(srv.data_id).split(_SEP)
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(f"s:{srv.data_id},\"{srv.service}\"{flags}\n")
lines.extend(sorted(tr_set))
lines.extend(services_lines)
lines.append(_END_LINE)
lines.extend(sorted(tr_set))
lines.extend(services_lines)
lines.append(_END_LINE)
with open(path + "lamedb5", "w") as file:
file.writelines(lines)
def parse(path, version=4):
""" Parsing lamedb. """
if version == 4:
return parse_v4(path)
elif version == 5:
return parse_v5(path)
raise SyntaxError("Unsupported version of the format.")
def parse_v3(services, transponders, path):
""" Parsing version 3. """
for t in transponders:
tr = transponders[t].lower()
tr_type = tr[0:1]
if tr_type == "c":
tr += ":0:0:0"
elif tr_type == "t":
tr += ":0:0"
else:
tr_data = tr.split(_SEP)
len_data = len(tr_data)
if len_data == 6:
tr_data.append("0")
elif len_data == 9:
tr_data.insert(6, "0")
tr_data.append("0")
tr_data.append("2")
tr = _SEP.join(tr_data)
transponders[t] = tr
return parse_services(services, transponders, path)
def parse_v4(path):
""" Parsing version 4. """
with open(path + _FILE_NAME, "r", encoding="utf-8", errors="replace") as file:
try:
data = str(file.read())
except UnicodeDecodeError as e:
log("lamedb parse error: " + str(e))
else:
return get_services_list(data, path)
def get_services_list(data, path=None):
""" Returns a list of services from a string data representation. """
transponders, sep, services = data.partition("transponders") # 1 step
pattern = re.compile("/[34]/$")
match = re.search(pattern, transponders)
if not match:
msg = "lamedb parsing error: unsupported format."
log(msg)
raise SyntaxError(msg)
transponders, sep, services = services.partition("services") # 2 step
services, sep, _ = services.partition("\nend") # 3 step
if match.group() == "/3/":
return parse_v3(services.split("\n"), parse_transponders(transponders.split("/")), path)
return parse_services(services.split("\n"), parse_transponders(transponders.split("/")), path)
def parse_v5(path):
""" Parsing version 5. """
with open(path + "lamedb5", "r", encoding="utf-8", errors="replace") as file:
lns = file.readlines()
if lns and not lns[0].endswith("/5/\n"):
raise SyntaxError("lamedb v.5 parsing error: unsupported format.")
trs, srvs = {}, [""]
for line in lns:
if line.startswith("s:"):
srv_data = line.strip("s:").split(",", 2)
srv_data[1] = srv_data[1].strip("\"")
data_len = len(srv_data)
if data_len == 3:
srv_data[2] = srv_data[2].strip()
elif data_len == 2:
srv_data.append("p:")
srvs.extend(srv_data)
elif line.startswith("t:"):
tr, srv = line.split(",")
trs[tr.strip("t:")] = srv.strip().replace(":", " ", 1)
return parse_services(srvs, trs, path)
def parse_transponders(arg):
""" Parsing transponders. """
transponders = {}
for ar in arg:
tr = ar.replace("\n", "").split("\t")
if len(tr) == 2:
transponders[tr[0]] = tr[1]
return transponders
def parse_services(services, transponders, path):
""" Parsing services. """
services_list = []
blacklist = get_blacklist(path) if path else {}
srvs = split(services, 3)
if srvs[0][0] == "": # remove first empty element
srvs.remove(srvs[0])
for srv in srvs:
data_id = str(srv[0]).lower() # lower is for lamedb ver.3
data = data_id.split(_SEP)
sp = "0"
tid = data[2]
nid = data[3]
# For lamedb ver.3
is_v3 = False
if len(tid) < 4:
is_v3 = True
tid = "{:0>4}".format(tid)
data[2] = tid
if len(nid) < 4:
is_v3 = True
nid = "{:0>4}".format(nid)
data[3] = nid
if is_v3:
data[0] = "{:0>4}".format(data[0])
data_id = _SEP.join(data)
srv_type = int(data[4])
transponder_id = "{}:{}:{}".format(data[1], tid, nid)
transponder = transponders.get(transponder_id, None)
tid = tid.lstrip(sp).upper()
nid = nid.lstrip(sp).upper()
ssid = str(data[0]).lstrip(sp).upper()
onid = str(data[1]).lstrip(sp).upper()
# For comparison in bouquets. Needed in upper case!!!
fav_id = "{}:{}:{}:{}".format(ssid, tid, nid, onid)
picon_id = "1_0_{:X}_{}_{}_{}_{}_0_0_0.png".format(srv_type, ssid, tid, nid, onid)
s_id = "1:0:{:X}:{}:{}:{}:{}:0:0:0:".format(srv_type, ssid, tid, nid, onid)
all_flags = srv[2].split(",")
coded = CODED_ICON if list(filter(lambda x: x.startswith("C:"), all_flags)) else None
flags = list(filter(lambda x: x.startswith("f:"), all_flags))
hide = HIDE_ICON if flags and Flag.is_hide(int(flags[0][2:])) else None
locked = LOCKED_ICON if s_id in blacklist else None
package = list(filter(lambda x: x.startswith("p:"), all_flags))
package = package[0][2:] if package else ""
if transponder is not None:
tr_type, sp, tr = str(transponder).partition(" ")
tr_type = TrType(tr_type)
tr = tr.split(_SEP)
service_type = SERVICE_TYPE.get(data[4], SERVICE_TYPE["-2"])
# removing all non printable symbols!
srv_name = "".join(c for c in srv[1] if c.isprintable())
pol = None
fec = None
system = None
pos = None
if tr_type is TrType.Satellite:
pol = POLARIZATION.get(tr[2], None)
fec = FEC.get(tr[3], None)
system = "DVB-S2" if len(tr) > 7 else "DVB-S"
pos = "{}.{}".format(tr[4][:-1], tr[4][-1:])
if tr_type is TrType.Terrestrial:
system = T_SYSTEM.get(tr[9], None)
pos = "T"
fec = T_FEC.get(tr[3], None)
elif tr_type is TrType.Cable:
system = "DVB-C"
pos = "C"
fec = FEC_DEFAULT.get(tr[4])
services_list.append(Service(flags_cas=srv[2],
transponder_type=tr_type.value,
coded=coded,
service=srv_name,
locked=locked,
hide=hide,
package=package,
service_type=service_type,
picon=None,
picon_id=picon_id,
ssid=data[0],
freq=tr[0],
rate=tr[1],
pol=pol,
fec=fec,
system=system,
pos=pos,
data_id=data_id,
fav_id=fav_id,
transponder=transponder))
return services_list
def split(itr, size):
""" Divide the iterable. """
srv = []
tmp = []
for i, line in enumerate(itr):
tmp.append(line)
if i % size == 0:
srv.append(tuple(tmp))
tmp.clear()
return srv
with open(self._path + "lamedb5", "w", encoding="utf-8", newline="\n") as file:
file.writelines(lines)
if __name__ == "__main__":

View File

@@ -1,16 +1,46 @@
# -*- 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 IPTV and streams support """
import re
from enum import Enum
from urllib.parse import unquote, quote
from app.commons import log
from app.eparser.ecommons import BqServiceType, Service
from app.settings import SettingsType
from app.ui.uicommons import IPTV_ICON
from .ecommons import BqServiceType, Service
# url, description, urlkey, account, usrname, psw, s_type, iconsrc, iconsrc_b, group
NEUTRINO_FAV_ID_FORMAT = "{}::{}::{}::{}::{}::{}::{}::{}::{}::{}"
ENIGMA2_FAV_ID_FORMAT = " {}:0:{}:{: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):
@@ -20,9 +50,14 @@ 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):
def parse_m3u(path, s_type, detect_encoding=True, params=None):
with open(path, "rb") as file:
data = file.read()
encoding = "utf-8"
@@ -37,33 +72,63 @@ def parse_m3u(path, s_type, detect_encoding=True):
encoding = enc.get("encoding", "utf-8")
aggr = [None] * 10
s_aggr = aggr[: -3]
services = []
groups = set()
counter = 0
marker_counter = 1
sid_counter = 1
name = None
picon = None
p_id = "1_0_1_0_0_0_0_0_0_0.png"
st = BqServiceType.IPTV.name
params = params or [0, 0, 0, 0]
m_name = BqServiceType.MARKER.name
for line in str(data, encoding=encoding, errors="ignore").splitlines():
if line.startswith("#EXTINF"):
name = line[1 + line.index(","):].strip()
line, sep, name = line.rpartition(",")
data = re.split('"', line)
size = len(data)
if size < 3:
continue
d = {data[i].lower().strip(" ="): data[i + 1] for i in range(0, len(data) - 1, 2)}
picon = d.get("tvg-logo", None)
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(counter, grp_name, grp_name)
counter += 1
mr = Service(None, None, None, grp_name, *aggr[0:3], BqServiceType.MARKER.name, *aggr, fav_id, None)
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 not line.startswith("#"):
url = line.strip()
fav_id = get_fav_id(url, name, s_type)
if name and url:
srv = Service(None, None, IPTV_ICON, name, *aggr[0:3], BqServiceType.IPTV.name, *aggr, fav_id, None)
params[0] = sid_counter
sid_counter += 1
fav_id = get_fav_id(url, name, s_type, params)
if 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(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
@@ -74,26 +139,35 @@ 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)
def get_fav_id(url, service_name, s_type):
def get_fav_id(url, name, settings_type, params=None, st_type=None, s_id=0, srv_type=1):
""" Returns fav id depending on the profile. """
if s_type is SettingsType.ENIGMA_2:
stream_type = StreamType.NONE_TS.value
return ENIGMA2_FAV_ID_FORMAT.format(stream_type, 1, 0, 0, 0, 0, quote(url), service_name, service_name, None)
elif s_type is SettingsType.NEUTRINO_MP:
if settings_type is SettingsType.ENIGMA_2:
st_type = st_type or StreamType.NONE_TS.value
params = params or (0, 0, 0, 0)
return ENIGMA2_FAV_ID_FORMAT.format(st_type, s_id, srv_type, *params, quote(url), name, name, None)
elif settings_type is SettingsType.NEUTRINO_MP:
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

View File

@@ -0,0 +1,39 @@
# -*- 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
#
SP = "_:::_"
KSP = "_::_"
API_VER = "4"
def get_attributes(data):
return {el[0]: el[1] for el in (e.split(KSP) for e in data.split(SP))}
def get_xml_attributes(attr):
attrs = attr.attributes
return {t: attrs[t].value for t in attrs.keys()}

View File

@@ -1,7 +1,36 @@
# -*- 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 xml.dom.minidom import parse, Document
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
@@ -23,29 +52,29 @@ def parse_bouquets(file, name, bq_type):
if not os.path.exists(file):
return bouquets
dom = parse(file)
dom = XmlHandler.parse(file)
for elem in dom.getElementsByTagName("Bouquet"):
if elem.hasAttributes():
bq_name = elem.attributes["name"].value
hidden = elem.attributes.get("hidden")
hidden = hidden.value if hidden else hidden
locked = elem.attributes.get("locked")
locked = locked.value if locked else locked
# epg = elem.attributes["epg"].value
bq_attrs = get_xml_attributes(elem)
bq_name = bq_attrs.get("name", "")
hidden = bq_attrs.get("hidden", "0")
locked = bq_attrs.get("locked", "0")
services = []
for srv_elem in elem.getElementsByTagName("S"):
if srv_elem.hasAttributes():
ssid = srv_elem.attributes["i"].value
on = srv_elem.attributes["on"].value
tr_id = srv_elem.attributes["t"].value
s_attrs = get_xml_attributes(srv_elem)
ssid = s_attrs.get("i", "0")
on = s_attrs.get("on", "0")
tr_id = s_attrs.get("t", "0")
fav_id = "{}:{}:{}".format(tr_id, on, ssid)
services.append(BouquetService(None, BqServiceType.DEFAULT, fav_id, 0))
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))
hidden=HIDE_ICON if hidden == "1" else None,
file=SP.join("{}{}{}".format(k, KSP, v) for k, v in bq_attrs.items())))
if BqType(bq_type) is BqType.BOUQUET:
for bq in bouquets.bouquets:
@@ -63,38 +92,27 @@ def parse_webtv(path, name, bq_type):
if not os.path.exists(path):
return bouquets
dom = parse(path)
dom = XmlHandler.parse(path)
services = []
for elem in dom.getElementsByTagName("webtv"):
if elem.hasAttributes():
title = elem.attributes["title"].value
url = elem.attributes["url"].value
description = elem.attributes.get("description")
description = description.value if description else description
urlkey = elem.attributes.get("urlkey", None)
urlkey = urlkey.value if urlkey else urlkey
account = elem.attributes.get("account", None)
account = account.value if account else account
usrname = elem.attributes.get("usrname", None)
usrname = usrname.value if usrname else usrname
psw = elem.attributes.get("psw", None)
psw = psw.value if psw else psw
s_type = elem.attributes.get("type", None)
s_type = s_type.value if s_type else s_type
iconsrc = elem.attributes.get("iconsrc", None)
iconsrc = iconsrc.value if iconsrc else iconsrc
iconsrc_b = elem.attributes.get("iconsrc_b", None)
iconsrc_b = iconsrc_b.value if iconsrc_b else iconsrc_b
group = elem.attributes.get("group", None)
group = group.value if group else group
web_attrs = get_xml_attributes(elem)
title = web_attrs.get("title", "")
url = web_attrs.get("url", "")
description = web_attrs.get("description", "")
urlkey = web_attrs.get("urlkey", None)
account = web_attrs.get("account", None)
usrname = web_attrs.get("usrname", None)
psw = web_attrs.get("psw", None)
s_type = web_attrs.get("type", None)
iconsrc = web_attrs.get("iconsrc", None)
iconsrc_b = web_attrs.get("iconsrc_b", None)
group = web_attrs.get("group", None)
fav_id = NEUTRINO_FAV_ID_FORMAT.format(url, description, urlkey, account, usrname, psw, s_type, iconsrc,
iconsrc_b, group)
srv = BouquetService(name=title,
type=BqServiceType.IPTV,
data=fav_id,
num=0)
services.append(srv)
bouquet = Bouquet(name="default", type=bq_type, services=services, locked=None, hidden=None)
services.append(BouquetService(name=title, type=BqServiceType.IPTV, data=fav_id, num=0))
bouquet = Bouquet(name="default", type=bq_type, services=services, locked=None, hidden=None, file=None)
bouquets[2].append(bouquet)
return bouquets
@@ -110,18 +128,29 @@ def write_bouquets(path, bouquets):
def write_bouquet(file, bouquet):
doc = Document()
doc = NeutrinoDocument()
root = doc.createElement("zapit")
root.setAttribute("api", API_VER)
doc.appendChild(root)
comment = doc.createComment(_COMMENT)
doc.appendChild(comment)
for bq in bouquet.bouquets:
attrs = get_attributes(bq.file) if bq.file else {}
attrs["name"] = bq.name
if bq.hidden:
attrs["hidden"] = "1"
else:
attrs.pop("hidden", None)
if bq.locked:
attrs["locked"] = "1"
else:
attrs.pop("locked", None)
bq_elem = doc.createElement("Bouquet")
bq_elem.setAttribute("name", bq.name)
bq_elem.setAttribute("hidden", "1" if bq.hidden else "0")
bq_elem.setAttribute("locked", "1" if bq.locked else "0")
bq_elem.setAttribute("epg", "0")
for k, v in attrs.items():
bq_elem.setAttribute(k, v)
root.appendChild(bq_elem)
for srv in bq.services:
@@ -131,16 +160,15 @@ def write_bouquet(file, bouquet):
srv_elem.setAttribute("n", srv.service)
srv_elem.setAttribute("t", tr_id)
srv_elem.setAttribute("on", on)
srv_elem.setAttribute("s", srv.pos.replace(".", ""))
srv_elem.setAttribute("frq", srv.freq[:-3])
srv_elem.setAttribute("l", "0") # temporary !!!
srv_elem.setAttribute("frq", srv.freq)
srv_elem.setAttribute("s", get_attributes(srv.flags_cas).get("position", "0"))
bq_elem.appendChild(srv_elem)
doc.writexml(open(file, "w"), addindent=" ", newl="\n", encoding="UTF-8")
doc.write_xml(file)
def write_webtv(file, bouquet):
doc = Document()
doc = NeutrinoDocument()
root = doc.createElement("webtvs")
doc.appendChild(root)
comment = doc.createComment(_COMMENT)
@@ -174,7 +202,7 @@ def write_webtv(file, bouquet):
root.appendChild(srv_elem)
doc.writexml(open(file, "w"), addindent=" ", newl="\n", encoding="UTF-8")
doc.write_xml(file)
if __name__ == "__main__":

View File

@@ -0,0 +1,114 @@
# -*- 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
#
""" Additional module for working with Neutrino xml files. """
import re
from xml.dom.minidom import parseString, Document, Element, Node
from xml.parsers.expat import ExpatError
from app.commons import log
class XmlHandler:
""" Utility class for handling Neutrino xml files. """
__slots__ = ()
ERROR_MESSAGE = "The file [{}] is not formatted correctly or contains invalid characters! Cause: {}"
@staticmethod
def parse(path):
""" Parses a file into the DOM by filename. """
try:
return parseString(open(path, "r", encoding="utf-8", errors="ignore").read())
except ExpatError as e:
# Some neutrino configuration files may contain text data with invalid character ['&'].
# https://www.w3.org/TR/xml/#syntax
# Apparently there is an error in Neutrino itself and the document is not initially formed correctly.
log(XmlHandler.ERROR_MESSAGE.format(path, e))
return XmlHandler.preprocess(path)
@staticmethod
def preprocess(path):
""" Pre-processing xml [for '&' symbol] for correct parsing. """
with open(path, "r", encoding="utf-8", errors="ignore") as f:
pat = re.compile("&([^;\\W]*([^;\\w]|$))")
log("Processing the file '{}'...".format(path))
try:
dom = parseString(re.sub(pat, "&amp;", f.read()))
except ExpatError as e:
msg = XmlHandler.ERROR_MESSAGE.format(path, e)
log(msg)
raise ValueError(e)
else:
log("Done!")
return dom
class NeutrinoDocument(Document):
def createElement(self, tag_name):
e = NElement(tag_name)
e.ownerDocument = self
return e
def write_xml(self, path):
self.writexml(open(path, "w", encoding="utf-8"), addindent=" ", newl="\n", encoding="UTF-8")
class NElement(Element):
def writexml(self, writer, indent="", add_indent="", new_line=""):
""" Overridden specifically for neutrino for more correct [&apos; -> optional] xml attrs generation. """
writer.write(indent + "<" + self.tagName)
attrs = self._get_attributes()
for a_name in attrs.keys():
writer.write(" %s=\"" % a_name)
self.write_data(writer, attrs[a_name].value)
writer.write("\"")
if self.childNodes:
writer.write(">")
if len(self.childNodes) == 1 and self.childNodes[0].nodeType in (Node.TEXT_NODE, Node.CDATA_SECTION_NODE):
self.childNodes[0].writexml(writer, '', '', '')
else:
writer.write(new_line)
for node in self.childNodes:
node.writexml(writer, indent + add_indent, add_indent, new_line)
writer.write(indent)
writer.write("</%s>%s" % (self.tagName, new_line))
else:
writer.write("/>%s" % new_line)
@staticmethod
def write_data(writer, data):
""" Writes data chars to writer."""
if data:
data = data.replace("&", "&amp;").replace("<", "&lt;").replace("\"", "&quot;").replace(">", "&gt;")
data = data.replace("'", "&apos;")
writer.write(data)

View File

@@ -1,168 +1,227 @@
from xml.dom.minidom import parse, Document
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
from ..ecommons import Service, POLARIZATION, FEC, SYSTEM, SERVICE_TYPE, PROVIDER
from collections import defaultdict
from app.commons import log
from app.eparser.ecommons import (Service, POLARIZATION, FEC, SYSTEM, SERVICE_TYPE, PROVIDER, T_SYSTEM, TrType,
SystemCable)
from app.eparser.neutrino import get_xml_attributes, SP, KSP, get_attributes, API_VER
from app.eparser.neutrino.nxml import XmlHandler, NeutrinoDocument
_FILE = "services.xml"
_TR_ATTR_NAMES = ("id", "on", "frq", "inv", "sr", "fec", "pol", "mod", "sys") # transponder attributes
_SRV_ATTR_NAMES = ("t", "s", "num", "f", "v", "a", "p", "pmt", "tx", "vt") # service attributes
def write_services(path, services):
doc = Document()
root = doc.createElement("zapit")
root.setAttribute("api", "4")
doc.appendChild(root)
comment = doc.createComment(" File was created in DemonEditor. Enjoy watching! ")
doc.appendChild(comment)
sats = {}
for srv in services:
flag = srv[0]
if flag in sats:
sats.get(flag).append(srv)
else:
srv_list = [srv]
sats[flag] = srv_list
for sat in sats:
tr_atr = sat.split(":")
sat_elem = doc.createElement("sat")
sat_elem.setAttribute("name", tr_atr[0])
sat_elem.setAttribute("position", tr_atr[1].replace(".", ""))
sat_elem.setAttribute("diseqc", tr_atr[2])
sat_elem.setAttribute("uncommited", tr_atr[3])
root.appendChild(sat_elem)
transponers = {}
for srv in sats.get(sat):
flag = srv[-1]
if flag in transponers:
transponers.get(flag).append(srv)
else:
srv_list = [srv]
transponers[flag] = srv_list
for tr in transponers:
tr_elem = doc.createElement("TS")
tr_atr = tr.split(":")
for i, value in enumerate(tr_atr):
if value == "None":
continue
tr_elem.setAttribute(_TR_ATTR_NAMES[i], value)
sat_elem.appendChild(tr_elem)
for srv in transponers.get(tr):
srv_elem = doc.createElement("S")
srv_elem.setAttribute("i", srv.ssid)
srv_elem.setAttribute("n", srv.service)
srv_attrs = srv.data_id.split(":")
api = srv_attrs.pop(0)
if api == "3":
root.setAttribute("api", "3") # !!!
for i, value in enumerate(srv_attrs):
if value == "None":
continue
srv_elem.setAttribute(_SRV_ATTR_NAMES[i], value)
tr_elem.appendChild(srv_elem)
doc.writexml(open(path + _FILE, "w"), addindent=" ", newl="\n", encoding="UTF-8")
doc.unlink()
NeutrinoServiceWriter(path, services).write()
def get_services(path):
return parse_services(path)
return NeutrinoServicesReader(path).get_services()
def parse_services(path):
""" Parsing services from xml"""
dom = parse(path + _FILE)
services = []
class NeutrinoServiceWriter:
for root in dom.getElementsByTagName("zapit"):
api = root.attributes["api"].value
def __init__(self, path, services):
self._path = path + _FILE
self._services = services
for elem in root.getElementsByTagName("sat"):
if elem.hasAttributes():
sat_name = elem.attributes["name"].value
sat_pos = elem.attributes["position"].value
sat_pos = "{}.{}".format(sat_pos[:-1], sat_pos[-1:])
diseqc = elem.attributes.get("diseqc")
diseqc = diseqc.value if diseqc else diseqc
uncommited = elem.attributes.get("uncommited")
uncommited = uncommited.value if uncommited else uncommited
sat = "{}:{}:{}:{}".format(sat_name, sat_pos, diseqc, uncommited)
self._api = API_VER
self._doc = NeutrinoDocument()
self._root = self._doc.createElement("zapit")
self._root.setAttribute("api", self._api)
self._doc.appendChild(self._root)
self._doc.appendChild(self._doc.createComment(" File was created in DemonEditor. Enjoy watching! "))
for tr_elem in elem.getElementsByTagName("TS"):
if tr_elem.hasAttributes():
parse_transponder(api, sat, sat_pos, services, tr_elem)
def write(self):
srvs = defaultdict(list)
for s in self._services:
srvs[s.transponder_type].append(s)
self.append_services(srvs.get(TrType.Satellite.value), "sat")
self.append_services(srvs.get(TrType.Terrestrial.value), "terrestrial")
self.append_services(srvs.get(TrType.Cable.value), "cable")
return services
self._doc.write_xml(self._path)
self._doc.unlink()
def append_services(self, services, s_type):
if not services:
return
sats = defaultdict(list)
for srv in services:
sats[srv[0]].append(srv)
for sat in sats:
sat_elem = self._doc.createElement(s_type)
attrs = get_attributes(sat)
for k, v in attrs.items():
sat_elem.setAttribute(k, v)
self._root.appendChild(sat_elem)
transponders = defaultdict(list)
for srv in sats.get(sat):
transponders[srv[-1]].append(srv)
for tr in transponders:
tr_elem = self._doc.createElement("TS")
for k, v in get_attributes(tr).items():
tr_elem.setAttribute(k, v)
sat_elem.appendChild(tr_elem)
for srv in transponders.get(tr):
srv_elem = self._doc.createElement("S")
s_attrs = get_attributes(srv.data_id)
api = s_attrs.pop("api", self._api)
if api != self._api:
self._root.setAttribute("api", api)
for k, v in s_attrs.items():
srv_elem.setAttribute(k, v)
tr_elem.appendChild(srv_elem)
def parse_transponder(api, sat, sat_pos, services, tr_elem):
tr_id = tr_elem.attributes["id"].value
on = tr_elem.attributes["on"].value
freq = tr_elem.attributes["frq"].value
rate = tr_elem.attributes["sr"].value
inv = tr_elem.attributes["inv"].value
fec = tr_elem.attributes["fec"].value
pol = tr_elem.attributes["pol"].value
mod = tr_elem.attributes.get("mod")
mod = mod.value if mod else mod
sys = tr_elem.attributes.get("sys")
sys = sys.value if sys else sys
class NeutrinoServicesReader:
tr = "{}:{}:{}:{}:{}:{}:{}:{}:{}".format(tr_id, on, freq, inv, rate, fec, pol, mod, sys)
tr_id = tr_id.lstrip("0")
def __init__(self, path):
self._path = path + _FILE
self._attrs = None
self._tr = None
self._api = "4"
self._services = []
for srv_elem in tr_elem.getElementsByTagName("S"):
if srv_elem.hasAttributes():
ssid = srv_elem.attributes["i"].value
name = srv_elem.attributes["n"].value
srv_type = srv_elem.attributes["t"].value
sys = srv_elem.attributes["s"].value
num = srv_elem.attributes.get("num")
num = num.value if num else num
f = srv_elem.attributes.get("f")
f = f.value if f else f
v, a, p, pmt, tx, vt = [None] * 6
# For v3 is possible so: '<S i="0001" n="name" t="1" s="0" num="770" f="4"/>' (equals v4 api)
if api == "3" and len(srv_elem.attributes) > 6:
v = srv_elem.attributes["v"].value
a = srv_elem.attributes["a"].value
p = srv_elem.attributes["p"].value
pmt = srv_elem.attributes["pmt"].value
tx = srv_elem.attributes["tx"].value
vt = srv_elem.attributes["vt"].value
def get_services(self):
dom = XmlHandler.parse(self._path)
data_id = "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}".format(api, srv_type, sys, num, f, v, a, p, pmt, tx, vt)
fav_id = "{}:{}:{}".format(tr_id, on.lstrip("0"), ssid.lstrip("0"))
picon_id = "{}{}{}.png".format(tr_id, on, ssid)
for root in dom.getElementsByTagName("zapit"):
if root.hasAttributes():
api = root.attributes["api"]
self._api = api.value if api else self._api
srv = Service(flags_cas=sat,
transponder_type=None,
coded=None,
service=name,
locked=None,
hide=None,
package=PROVIDER.get(int(on, 16)),
service_type=SERVICE_TYPE.get(str(int(srv_type, 16))),
picon=None,
picon_id=picon_id,
ssid=ssid,
freq=freq,
rate=rate,
pol=POLARIZATION.get(pol),
fec=FEC.get(fec),
system=SYSTEM.get(sys),
pos=sat_pos,
data_id=data_id,
fav_id=fav_id,
transponder=tr)
services.append(srv)
for elem in root.getElementsByTagName("sat"):
if elem.hasAttributes():
sat_attrs = get_xml_attributes(elem)
sat_pos = 0
try:
sat_pos = int(sat_attrs.get("position", "0"))
sat_pos = "{:0.1f}{}".format(abs(sat_pos / 10), "W" if sat_pos < 0 else "E")
except ValueError as e:
log("Neutrino parsing error [parse sat position]: {}".format(e))
sat = SP.join("{}{}{}".format(k, KSP, v) for k, v in sat_attrs.items())
for tr_elem in elem.getElementsByTagName("TS"):
if tr_elem.hasAttributes():
self.parse_sat_transponder(sat, sat_pos, tr_elem)
# Terrestrial DVB-T[2].
for elem in root.getElementsByTagName("terrestrial"):
if elem.hasAttributes():
terr_attrs = get_xml_attributes(elem)
terr = SP.join("{}{}{}".format(k, KSP, v) for k, v in terr_attrs.items())
for tr_elem in elem.getElementsByTagName("TS"):
if tr_elem.hasAttributes():
self.parse_ct_transponder(terr, tr_elem, TrType.Terrestrial)
# Cable.
for elem in root.getElementsByTagName("cable"):
if elem.hasAttributes():
cable_attrs = get_xml_attributes(elem)
cable = SP.join("{}{}{}".format(k, KSP, v) for k, v in cable_attrs.items())
for tr_elem in elem.getElementsByTagName("TS"):
if tr_elem.hasAttributes():
self.parse_ct_transponder(cable, tr_elem, TrType.Cable)
return self._services
def parse_sat_transponder(self, sat, sat_pos, tr_elem):
tr_attr = get_xml_attributes(tr_elem)
tr = SP.join("{}{}{}".format(k, KSP, v) for k, v in tr_attr.items())
tr_id = tr_attr.get("id", "0").lstrip("0")
on = tr_attr.get("on", "0")
freq = tr_attr.get("frq", "0")
rate = tr_attr.get("sr", "0")
fec = tr_attr.get("fec", "0")
pol = POLARIZATION.get(tr_attr.get("pol", "0"))
# Formatting displayed values.
try:
freq = "{}".format(int(freq) // 1000)
rate = "{}".format(int(rate) // 1000)
except ValueError as e:
log("Neutrino parsing error [parse_transponder]: {}".format(e))
for srv_elem in tr_elem.getElementsByTagName("S"):
if srv_elem.hasAttributes():
at = get_xml_attributes(srv_elem)
at["api"] = self._api
ssid, name, s_type, sys = at.get("i", "0"), at.get("n", ""), at.get("t", "3"), at.get("s", "0")
data_id = SP.join("{}{}{}".format(k, KSP, v) for k, v in at.items())
fav_id = "{}:{}:{}".format(tr_id, on.lstrip("0"), ssid.lstrip("0"))
picon_id = "{}{}{}.png".format(tr_id, on, ssid)
prv = PROVIDER.get(int(on, 16), "")
st = SERVICE_TYPE.get(str(int(s_type, 16)), SERVICE_TYPE.get("-2"))
srv = Service(sat, TrType.Satellite.value, None, name, None, None, prv, st, None, picon_id, ssid, freq,
rate, pol, FEC.get(fec), SYSTEM.get(sys), sat_pos, data_id, fav_id, tr)
self._services.append(srv)
def parse_ct_transponder(self, terr, tr_elem, tr_type):
attrs = get_xml_attributes(tr_elem)
tr = SP.join("{}{}{}".format(k, KSP, v) for k, v in attrs.items())
tr_id, on, freq = attrs.get("id", "0").lstrip("0"), attrs.get("on", "0"), attrs.get("frq", "0")
for srv_elem in tr_elem.getElementsByTagName("S"):
if srv_elem.hasAttributes():
s_at = get_xml_attributes(srv_elem)
s_at["api"] = self._api
ssid, name, s_type, sys = s_at.get("i", "0"), s_at.get("n", ""), s_at.get("t", "3"), s_at.get("s", "0")
data_id = SP.join("{}{}{}".format(k, KSP, v) for k, v in s_at.items())
fav_id = "{}:{}:{}".format(tr_id, on.lstrip("0"), ssid.lstrip("0"))
picon_id = "{}{}{}.png".format(tr_id, on, ssid)
prv = PROVIDER.get(int(on, 16), "")
st = SERVICE_TYPE.get(str(int(s_type, 16)), SERVICE_TYPE.get("-2"))
if tr_type is TrType.Terrestrial:
sys = T_SYSTEM.get(sys)
pos = "T"
elif tr_type is TrType.Cable:
sys = SystemCable(sys).name
pos = "C"
else:
log("Parse transponder error: Not supported type [{}]".format(tr_type))
break
srv = Service(terr, tr_type.value, None, name, None, None, prv, st, None, picon_id, ssid,
freq, "0", None, None, sys, pos, data_id, fav_id, tr)
self._services.append(srv)
if __name__ == "__main__":

View File

@@ -1,4 +1,32 @@
""" 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
#
""" Module for parsing satellites.xml file.
For more info see __COMMENT
"""
@@ -53,15 +81,17 @@ def write_satellites(satellites, data_path):
transponder_child.setAttribute("frequency", tr.frequency)
transponder_child.setAttribute("symbol_rate", tr.symbol_rate)
transponder_child.setAttribute("polarization", get_key_by_value(POLARIZATION, tr.polarization))
transponder_child.setAttribute("fec_inner", get_key_by_value(FEC, tr.fec_inner))
transponder_child.setAttribute("system", get_key_by_value(SYSTEM, tr.system))
transponder_child.setAttribute("modulation", get_key_by_value(MODULATION, tr.modulation))
transponder_child.setAttribute("fec_inner", get_key_by_value(FEC, tr.fec_inner) or "0")
transponder_child.setAttribute("system", get_key_by_value(SYSTEM, tr.system) or "0")
transponder_child.setAttribute("modulation", get_key_by_value(MODULATION, tr.modulation) or "0")
if tr.pls_mode:
transponder_child.setAttribute("pls_mode", tr.pls_mode)
if tr.pls_code:
transponder_child.setAttribute("pls_code", tr.pls_code)
if tr.is_id:
transponder_child.setAttribute("is_id", tr.is_id)
if tr.t2mi_plp_id:
transponder_child.setAttribute("t2mi_plp_id", tr.t2mi_plp_id)
sat_child.appendChild(transponder_child)
root.appendChild(sat_child)
doc.writexml(open(data_path, "w"),
@@ -87,10 +117,10 @@ def parse_transponders(elem, sat_name):
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)
atr["is_id"].value if "is_id" in atr else None,
atr["t2mi_plp_id"].value if "t2mi_plp_id" in atr else None)
except Exception as e:
message = "Error: can't parse transponder for '{}' satellite! {}".format(sat_name, repr(e))
print(message)
message = f"Error: can't parse transponder for '{sat_name}' satellite! {repr(e)}"
log(message)
else:
transponders.append(tr)
@@ -98,7 +128,7 @@ def parse_transponders(elem, sat_name):
def parse_sat(elem):
""" Parsing satellite """
""" Parsing satellite. """
sat_name = elem.attributes["name"].value
return Satellite(sat_name,
elem.attributes["flags"].value,
@@ -107,7 +137,7 @@ def parse_sat(elem):
def parse_satellites(path):
""" Parsing satellites from xml"""
""" Parsing satellites from xml. """
dom = parse(path)
satellites = []

View File

@@ -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 copy
import json
import locale
@@ -9,85 +37,66 @@ from pathlib import Path
from pprint import pformat
from textwrap import dedent
SEP = os.sep
HOME_PATH = str(Path.home())
CONFIG_PATH = HOME_PATH + "/.config/demon-editor/"
CONFIG_PATH = HOME_PATH + "{}.config{}demon-editor{}".format(SEP, SEP, SEP)
CONFIG_FILE = CONFIG_PATH + "config.json"
DATA_PATH = HOME_PATH + "/DemonEditor/data/"
DATA_PATH = HOME_PATH + "{}DemonEditor{}".format(SEP, SEP)
GTK_PATH = os.environ.get("GTK_PATH", None)
IS_DARWIN = sys.platform == "darwin"
IS_WIN = sys.platform == "win32"
IS_LINUX = sys.platform == "linux"
class Defaults(Enum):
""" Default program settings """
USER = "root"
PASSWORD = ""
HOST = "127.0.0.1"
FTP_PORT = "21"
HTTP_PORT = "80"
TELNET_PORT = "23"
HTTP_USE_SSL = False
# Enigma2.
BOX_SERVICES_PATH = "/etc/enigma2/"
BOX_SATELLITE_PATH = "/etc/tuxbox/"
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/")
# 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)
DEFAULT_PROFILE = "default"
BACKUP_BEFORE_DOWNLOADING = True
BACKUP_BEFORE_SAVE = True
V5_SUPPORT = False
FORCE_BQ_NAMES = False
HTTP_API_SUPPORT = False
HTTP_API_SUPPORT = True
ENABLE_YT_DL = False
ENABLE_SEND_TO = False
USE_COLORS = True
NEW_COLOR = "rgb(255,230,204)"
EXTRA_COLOR = "rgb(179,230,204)"
TOOLTIP_LOGO_SIZE = 96
LIST_PICON_SIZE = 32
FAV_CLICK_MODE = 0
PLAY_STREAMS_MODE = 1 if IS_DARWIN else 0
STREAM_LIB = "mpv" if IS_WIN else "vlc"
MAIN_LIST_PLAYBACK = False
PROFILE_FOLDER_DEFAULT = False
RECORDS_PATH = DATA_PATH + "records/"
RECORDS_PATH = DATA_PATH + "records{}".format(SEP)
ACTIVATE_TRANSCODING = False
ACTIVE_TRANSCODING_PRESET = "720p TV/device"
def get_settings():
if not os.path.isfile(CONFIG_FILE) or os.stat(CONFIG_FILE).st_size == 0:
write_settings(get_default_settings())
with open(CONFIG_FILE, "r") as config_file:
return json.load(config_file)
def get_default_settings(profile_name="default"):
def_settings = SettingsType.ENIGMA_2.get_default_settings()
set_local_paths(def_settings, profile_name)
return {
"version": 1,
"default_profile": Defaults.DEFAULT_PROFILE.value,
"profiles": {profile_name: def_settings},
"v5_support": Defaults.V5_SUPPORT.value,
"http_api_support": Defaults.HTTP_API_SUPPORT.value,
"enable_yt_dl": Defaults.ENABLE_YT_DL.value,
"enable_send_to": Defaults.ENABLE_SEND_TO.value,
"use_colors": Defaults.USE_COLORS.value,
"new_color": Defaults.NEW_COLOR.value,
"extra_color": Defaults.EXTRA_COLOR.value,
"fav_click_mode": Defaults.FAV_CLICK_MODE.value,
"profile_folder_is_default": Defaults.PROFILE_FOLDER_DEFAULT.value,
"records_path": Defaults.RECORDS_PATH.value
}
def get_default_transcoding_presets():
return {"720p TV/device": {"vcodec": "h264", "vb": "1500", "width": "1280", "height": "720", "acodec": "mp3",
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"},
"1080p TV/device": {"vcodec": "h264", "vb": "3500", "width": "1920", "height": "1080", "acodec": "mp3",
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"}}
def write_settings(config):
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
with open(CONFIG_FILE, "w") as config_file:
json.dump(config, config_file, indent=" ")
def set_local_paths(settings, profile_name, data_path=DATA_PATH, use_profile_folder=False):
settings["data_local_path"] = "{}{}/".format(data_path, profile_name)
if use_profile_folder:
settings["picons_local_path"] = "{}{}/{}/".format(data_path, profile_name, "picons")
settings["backup_local_path"] = "{}{}/{}/".format(data_path, profile_name, "backup")
else:
settings["picons_local_path"] = "{}{}/{}/".format(data_path, "picons", profile_name)
settings["backup_local_path"] = "{}{}/{}/".format(data_path, "backup", profile_name)
ACTIVE_TRANSCODING_PRESET = "720p TV{}device".format(SEP)
class SettingsType(IntEnum):
@@ -96,29 +105,35 @@ class SettingsType(IntEnum):
NEUTRINO_MP = 1
def get_default_settings(self):
""" Returns default settings for current type """
""" Returns default settings for current type. """
if self is self.ENIGMA_2:
return {"setting_type": self.value,
"host": "127.0.0.1", "port": "21", "timeout": 5,
"user": "root", "password": "root",
"http_port": "80", "http_timeout": 5, "http_use_ssl": False,
"telnet_port": "23", "telnet_timeout": 5,
"services_path": "/etc/enigma2/", "user_bouquet_path": "/etc/enigma2/",
"satellites_xml_path": "/etc/tuxbox/", "data_local_path": DATA_PATH + "enigma2/",
"picons_path": "/usr/share/enigma2/picon/",
"picons_local_path": DATA_PATH + "enigma2/picons/",
"backup_local_path": DATA_PATH + "enigma2/backup/"}
elif self is self.NEUTRINO_MP:
return {"setting_type": self,
"host": "127.0.0.1", "port": "21", "timeout": 5,
"user": "root", "password": "root",
"http_port": "80", "http_timeout": 2, "http_use_ssl": False,
"telnet_port": "23", "telnet_timeout": 1,
"services_path": "/var/tuxbox/config/zapit/", "user_bouquet_path": "/var/tuxbox/config/zapit/",
"satellites_xml_path": "/var/tuxbox/config/", "data_local_path": DATA_PATH + "neutrino/",
"picons_path": "/usr/share/tuxbox/neutrino/icons/logo/",
"picons_local_path": DATA_PATH + "neutrino/picons/",
"backup_local_path": DATA_PATH + "neutrino/backup/"}
srv_path = Defaults.BOX_SERVICES_PATH.value
sat_path = Defaults.BOX_SATELLITE_PATH.value
picons_path = Defaults.BOX_PICON_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
http_timeout = 2
telnet_timeout = 1
return {"setting_type": self.value,
"host": Defaults.HOST.value,
"port": Defaults.FTP_PORT.value,
"timeout": 5,
"user": Defaults.USER.value,
"password": Defaults.PASSWORD.value,
"http_port": Defaults.HTTP_PORT.value,
"http_timeout": http_timeout,
"http_use_ssl": Defaults.HTTP_USE_SSL.value,
"telnet_port": Defaults.TELNET_PORT.value,
"telnet_timeout": telnet_timeout,
"services_path": srv_path,
"user_bouquet_path": srv_path,
"satellites_xml_path": sat_path,
"picons_path": picons_path}
class SettingsException(Exception):
@@ -132,17 +147,17 @@ class SettingsReadException(SettingsException):
class PlayStreamsMode(IntEnum):
""" Behavior mode when opening streams. """
BUILT_IN = 0
VLC = 1
WINDOW = 1
M3U = 2
class Settings:
__INSTANCE = None
__VERSION = 1
__VERSION = 2
def __init__(self, ext_settings=None):
try:
settings = ext_settings or get_settings()
settings = ext_settings or self.get_settings()
except PermissionError as e:
raise SettingsReadException(e)
@@ -173,22 +188,18 @@ class Settings:
return cls.__INSTANCE
def save(self):
write_settings(self._settings)
self.write_settings(self._settings)
def reset(self, force_write=False):
for k, v in self.setting_type.get_default_settings().items():
self._cp_settings[k] = v
def_path = self.default_data_path
def_path += "enigma2/" if self.setting_type is SettingsType.ENIGMA_2 else "neutrino/"
set_local_paths(self._cp_settings, self._current_profile, def_path, self.profile_folder_is_default)
if force_write:
self.save()
@staticmethod
def reset_to_default():
write_settings(get_default_settings())
Settings.write_settings(Settings.get_default_settings())
def get_default(self, p_name):
""" Returns default value for current settings type """
@@ -198,9 +209,9 @@ class Settings:
""" Adds extra options """
self._settings[name] = value
def get(self, name):
def get(self, name, default=None):
""" Returns extra options or None """
return self._settings.get(name, None)
return self._settings.get(name, default)
@property
def settings(self):
@@ -229,6 +240,10 @@ class Settings:
def default_profile(self, value):
self._settings["default_profile"] = value
@property
def current_profile_settings(self):
return self._cp_settings
@property
def profiles(self):
return self._profiles
@@ -352,6 +367,20 @@ class Settings:
def picons_path(self, value):
self._cp_settings["picons_path"] = value
@property
def picons_paths(self):
if self.setting_type is SettingsType.NEUTRINO_MP:
return self._settings.get("neutrino_picon_paths", Defaults.NEUTRINO_BOX_PICON_PATHS.value)
else:
return self._settings.get("picon_paths", Defaults.BOX_PICON_PATHS.value)
@picons_paths.setter
def picons_paths(self, value):
if self.setting_type is SettingsType.NEUTRINO_MP:
self._settings["neutrino_picon_paths"] = value
else:
self._settings["picon_paths"] = value
# ***** Local paths ***** #
@property
@@ -371,28 +400,48 @@ class Settings:
self._settings["default_data_path"] = value
@property
def data_local_path(self):
return self._cp_settings.get("data_local_path", self.get_default("data_local_path"))
def default_backup_path(self):
return self._settings.get("default_backup_path", Defaults.BACKUP_PATH.value)
@data_local_path.setter
def data_local_path(self, value):
self._cp_settings["data_local_path"] = value
@default_backup_path.setter
def default_backup_path(self, value):
self._settings["default_backup_path"] = value
@property
def picons_local_path(self):
return self._cp_settings.get("picons_local_path", self.get_default("picons_local_path"))
def default_picon_path(self):
return self._settings.get("default_picon_path", Defaults.PICON_PATH.value)
@picons_local_path.setter
def picons_local_path(self, value):
self._cp_settings["picons_local_path"] = value
@default_picon_path.setter
def default_picon_path(self, value):
self._settings["default_picon_path"] = value
@property
def backup_local_path(self):
return self._cp_settings.get("backup_local_path", self.get_default("backup_local_path"))
def profile_data_path(self):
return f"{self.default_data_path}data{SEP}{self._current_profile}{SEP}"
@backup_local_path.setter
def backup_local_path(self, value):
self._cp_settings["backup_local_path"] = value
@profile_data_path.setter
def profile_data_path(self, value):
self._cp_settings["profile_data_path"] = value
@property
def profile_picons_path(self):
if self.profile_folder_is_default:
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):
self._cp_settings["profile_picons_path"] = value
@property
def profile_backup_path(self):
if self.profile_folder_is_default:
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):
@@ -422,7 +471,7 @@ class Settings:
@property
def transcoding_presets(self):
return self._settings.get("transcoding_presets", get_default_transcoding_presets())
return self._settings.get("transcoding_presets", self.get_default_transcoding_presets())
@transcoding_presets.setter
def transcoding_presets(self, value):
@@ -436,6 +485,30 @@ class Settings:
def play_streams_mode(self, value):
self._settings["play_streams_mode"] = value
@property
def stream_lib(self):
return self._settings.get("stream_lib", Defaults.STREAM_LIB.value)
@stream_lib.setter
def stream_lib(self, value):
self._settings["stream_lib"] = value
@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
@@ -447,6 +520,16 @@ class Settings:
def epg_options(self, value):
self._cp_settings["epg_options"] = 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
@@ -513,38 +596,6 @@ class Settings:
def enable_send_to(self, value):
self._settings["enable_send_to"] = value
@property
def use_colors(self):
return self._settings.get("use_colors", Defaults.USE_COLORS.value)
@use_colors.setter
def use_colors(self, value):
self._settings["use_colors"] = value
@property
def new_color(self):
return self._settings.get("new_color", Defaults.NEW_COLOR.value)
@new_color.setter
def new_color(self, value):
self._settings["new_color"] = value
@property
def extra_color(self):
return self._settings.get("extra_color", Defaults.EXTRA_COLOR.value)
@extra_color.setter
def extra_color(self, value):
self._settings["extra_color"] = 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")
@@ -581,14 +632,77 @@ class Settings:
# *********** Appearance *********** #
@property
def list_font(self):
return self._settings.get("list_font", "")
@list_font.setter
def list_font(self, value):
self._settings["list_font"] = value
@property
def list_picon_size(self):
return self._settings.get("list_picon_size", Defaults.LIST_PICON_SIZE.value)
@list_picon_size.setter
def list_picon_size(self, value):
self._settings["list_picon_size"] = value
@property
def tooltip_logo_size(self):
return self._settings.get("tooltip_logo_size", Defaults.TOOLTIP_LOGO_SIZE.value)
@tooltip_logo_size.setter
def tooltip_logo_size(self, value):
self._settings["tooltip_logo_size"] = value
@property
def use_colors(self):
return self._settings.get("use_colors", Defaults.USE_COLORS.value)
@use_colors.setter
def use_colors(self, value):
self._settings["use_colors"] = value
@property
def new_color(self):
return self._settings.get("new_color", Defaults.NEW_COLOR.value)
@new_color.setter
def new_color(self, value):
self._settings["new_color"] = value
@property
def extra_color(self):
return self._settings.get("extra_color", Defaults.EXTRA_COLOR.value)
@extra_color.setter
def extra_color(self, value):
self._settings["extra_color"] = value
@property
def dark_mode(self):
if IS_DARWIN:
import subprocess
cmd = ["defaults", "read", "-g", "AppleInterfaceStyle"]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
return "Dark" in str(p[0])
return self._settings.get("dark_mode", False)
@dark_mode.setter
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 alternate_layout(self):
return self._settings.get("alternate_layout", IS_DARWIN)
@@ -624,7 +738,7 @@ class Settings:
@property
@lru_cache(1)
def themes_path(self):
return "{}/.themes/".format(HOME_PATH)
return f"{HOME_PATH}{SEP}.themes{SEP}"
@property
def icon_theme(self):
@@ -637,12 +751,20 @@ class Settings:
@property
@lru_cache(1)
def icon_themes_path(self):
return "{}/.icons/".format(HOME_PATH)
return f"{HOME_PATH}{SEP}.icons{SEP}"
@property
def is_darwin(self):
return IS_DARWIN
@property
def force_external_themes(self):
return self._settings.get("force_external_themes", False)
@force_external_themes.setter
def force_external_themes(self, value):
self._settings["force_external_themes"] = value
# *********** Download dialog *********** #
@property
@@ -682,6 +804,52 @@ class Settings:
def is_enable_experimental(self, value):
self._settings["enable_experimental"] = value
# **************** Get-Set settings **************** #
@staticmethod
def get_settings():
if not os.path.isfile(CONFIG_FILE) or os.stat(CONFIG_FILE).st_size == 0:
Settings.write_settings(Settings.get_default_settings())
with open(CONFIG_FILE, "r", encoding="utf-8") as config_file:
try:
return json.load(config_file)
except ValueError as e:
raise SettingsReadException(e)
@staticmethod
def get_default_settings(profile_name="default"):
def_settings = SettingsType.ENIGMA_2.get_default_settings()
return {
"version": Settings.__VERSION,
"default_profile": Defaults.DEFAULT_PROFILE.value,
"profiles": {profile_name: def_settings},
"v5_support": Defaults.V5_SUPPORT.value,
"http_api_support": Defaults.HTTP_API_SUPPORT.value,
"enable_yt_dl": Defaults.ENABLE_YT_DL.value,
"enable_send_to": Defaults.ENABLE_SEND_TO.value,
"use_colors": Defaults.USE_COLORS.value,
"new_color": Defaults.NEW_COLOR.value,
"extra_color": Defaults.EXTRA_COLOR.value,
"fav_click_mode": Defaults.FAV_CLICK_MODE.value,
"profile_folder_is_default": Defaults.PROFILE_FOLDER_DEFAULT.value,
"records_path": Defaults.RECORDS_PATH.value
}
@staticmethod
def get_default_transcoding_presets():
return {"720p TV/device": {"vcodec": "h264", "vb": "1500", "width": "1280", "height": "720", "acodec": "mp3",
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"},
"1080p TV/device": {"vcodec": "h264", "vb": "3500", "width": "1920", "height": "1080", "acodec": "mp3",
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"}}
@staticmethod
def write_settings(config):
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
with open(CONFIG_FILE, "w", encoding="utf-8") as config_file:
json.dump(config, config_file, indent=" ")
if __name__ == "__main__":
pass

View File

@@ -1,4 +1,32 @@
""" Module for working with epg.dat file """
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" Module for working with epg.dat file. """
import struct
from datetime import datetime
from xml.dom.minidom import parse, Node, Document
@@ -11,7 +39,7 @@ class EPG:
@staticmethod
def get_epg_refs(path):
""" 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
https://github.com/OpenPLi/enigma2/blob/develop/lib/dvb/epgcache.cpp#L955
"""
refs = set()
@@ -21,17 +49,23 @@ class EPG:
raise ValueError("Epg file has incorrect byte order!")
header = f.read(13).decode()
if header != "ENIGMA_EPG_V7":
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 = "{:X}:{:X}:{:X}".format(sid, tsid, nid)
service_id = f"{sid:X}:{tsid:X}:{nid:X}"
for j in range(events_size):
_type, _len = struct.unpack("<BB", f.read(2))
_type, _len = struct.unpack(_type_read_str, f.read(_len_read_size))
f.read(10)
n_crc = (_len - 10) // 4
if n_crc > 0:
@@ -90,14 +124,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()

View File

@@ -1,68 +1,443 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
import os
import subprocess
import sys
from datetime import datetime
from enum import Enum
from urllib.request import urlopen
from app.commons import run_task, log, _DATE_FORMAT
from app.settings import PlayStreamsMode
from gi.repository import Gdk, Gtk, GObject
from app.commons import run_task, log, _DATE_FORMAT, run_with_delay
from app.settings import IS_DARWIN, IS_LINUX, IS_WIN
class Player:
__VLC_INSTANCE = None
__PLAY_STREAMS_MODE = PlayStreamsMode.BUILT_IN
class Player(Gtk.DrawingArea):
""" Base player class. Also used as a factory. """
def __init__(self, rewind_callback, position_callback, error_callback, playing_callback):
try:
from app.tools import vlc
from app.tools.vlc import EventType
except OSError as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
raise ImportError
def __init__(self, mode, widget, **kwargs):
super().__init__(**kwargs)
GObject.signal_new("error", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("message", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("position", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("played", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("audio-track", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("subtitle-track", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
self.connect("draw", self.on_draw)
self.connect("motion-notify-event", self.on_mouse_motion)
self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)
widget.add(self)
parent = widget.get_parent()
parent.connect("play", self.on_play)
parent.connect("stop", self.on_stop)
self.show()
def get_play_mode(self):
pass
def play(self, mrl=None):
pass
def stop(self):
pass
def pause(self):
pass
def set_time(self, time):
pass
def release(self):
pass
def is_playing(self):
pass
def set_audio_track(self, track):
pass
def get_audio_track(self):
pass
def set_subtitle_track(self, track):
pass
def set_aspect_ratio(self, ratio):
pass
def get_instance(self, mode, widget):
pass
def on_play(self, widget, url):
self.play(url)
def on_stop(self, widget, state):
self.stop()
def on_release(self, widget, state):
self.release()
def get_window_handle(self):
""" Returns the identifier [pointer] for the window.
Based on gtkvlc.py[get_window_pointer] example from here:
https://github.com/oaubert/python-vlc/tree/master/examples
"""
if IS_LINUX:
return self.get_window().get_xid()
else:
self._is_playing = False
args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib")
self._player = vlc.Instance(args).media_player_new()
ev_mgr = self._player.event_manager()
try:
import ctypes
if rewind_callback:
# TODO look other EventType options
ev_mgr.event_attach(EventType.MediaPlayerBuffering,
lambda et, p: rewind_callback(p.get_media().get_duration()),
self._player)
if position_callback:
ev_mgr.event_attach(EventType.MediaPlayerTimeChanged,
lambda et, p: position_callback(p.get_time()),
self._player)
libgdk = ctypes.CDLL("libgdk-3.0.dylib" if IS_DARWIN else "libgdk-3-0.dll")
except OSError as 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.restype = ctypes.c_void_p
get_pointer.argtypes = [ctypes.c_void_p]
if error_callback:
ev_mgr.event_attach(EventType.MediaPlayerEncounteredError,
lambda et, p: error_callback(),
self._player)
if playing_callback:
ev_mgr.event_attach(EventType.MediaPlayerPlaying,
lambda et, p: playing_callback(),
self._player)
return get_pointer(gpointer)
@classmethod
def get_instance(cls, rewind_callback=None, position_callback=None, error_callback=None, playing_callback=None):
if not cls.__VLC_INSTANCE:
cls.__VLC_INSTANCE = Player(rewind_callback, position_callback, error_callback, playing_callback)
return cls.__VLC_INSTANCE
def on_draw(self, widget, cr):
""" Used for black background drawing in the player drawing area. """
cr.set_source_rgb(0, 0, 0)
cr.paint()
def on_mouse_motion(self, widget, event):
display = widget.get_display()
window = widget.get_window()
cursor = Gdk.Cursor.new_from_name(display, "default")
window.set_cursor(cursor)
self.hide_mouse_cursor(window, display)
@run_with_delay(3)
def hide_mouse_cursor(self, window, display):
cursor = Gdk.Cursor.new_for_display(display, Gdk.CursorType.BLANK_CURSOR)
window.set_cursor(cursor)
@staticmethod
def get_play_mode():
return Player.__PLAY_STREAMS_MODE
def make(name, mode, widget):
""" Factory method. We will not use a separate factory to return a specific implementation.
@param name: implementation name.
@param mode: current player mode [Built-in or windowed].
@param widget: parent of video widget.
Throws a NameError if there is no implementation for the given name.
"""
if name == "mpv":
return MpvPlayer.get_instance(mode, widget)
elif name == "gst":
return GstPlayer.get_instance(mode, widget)
elif name == "vlc":
return VlcPlayer.get_instance(mode, widget)
else:
raise NameError(f"There is no such [{name}] implementation.")
class MpvPlayer(Player):
""" Simple wrapper for MPV media player.
Uses python-mvp [https://github.com/jaseg/python-mpv].
"""
__INSTANCE = None
def __init__(self, mode, widget):
super().__init__(mode, widget)
try:
from app.tools import mpv
self._player = mpv.MPV(wid=str(self.get_window_handle()),
input_default_bindings=False,
input_cursor=False,
cursor_autohide="no")
except OSError as e:
log(f"{__class__.__name__}: Load library error: {e}")
raise ImportError("No libmpv is found. Check that it is installed!")
else:
self._mode = mode
self._is_playing = False
@self._player.event_callback(mpv.MpvEventID.FILE_LOADED)
def on_open(event):
log("Starting playback...")
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(f"Stream playback error: {event.get('error', mpv.ErrorCode.GENERIC)}")
self.emit("error", "Can't Playback!")
@classmethod
def get_instance(cls, mode, widget):
if not cls.__INSTANCE:
cls.__INSTANCE = MpvPlayer(mode, widget)
return cls.__INSTANCE
def get_play_mode(self):
return self._mode
def play(self, mrl=None):
if not mrl:
return
self._player.play(mrl)
self._is_playing = True
def stop(self):
self._player.stop()
self._is_playing = True
def pause(self):
pass
def set_time(self, time):
pass
@run_task
def release(self):
self._player.terminate()
self.__INSTANCE = None
def is_playing(self):
return self._is_playing
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. """
__INSTANCE = None
def __init__(self, mode, widget):
super().__init__(mode, widget)
try:
import gi
gi.require_version("Gst", "1.0")
gi.require_version("GstVideo", "1.0")
from gi.repository import Gst, GstVideo
# Initialization of GStreamer.
Gst.init(sys.argv)
except (OSError, ValueError) as 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
self.STAT_RETURN = Gst.StateChangeReturn
self._mode = mode
self._is_playing = False
self._player = Gst.ElementFactory.make("playbin", "player")
self._player.set_window_handle(self.get_window_handle())
bus = self._player.get_bus()
bus.add_signal_watch()
bus.connect("message::error", self.on_error)
bus.connect("message::state-changed", self.on_state_changed)
bus.connect("message::eos", self.on_eos)
@classmethod
def get_instance(cls, mode, widget):
if not cls.__INSTANCE:
cls.__INSTANCE = GstPlayer(mode, widget)
return cls.__INSTANCE
def get_play_mode(self):
return self._mode
def play(self, mrl=None):
self._player.set_state(self.STATE.READY)
if not mrl:
return
self._player.set_property("uri", mrl)
log(f"Setting the URL for playback: {mrl}")
ret = self._player.set_state(self.STATE.PLAYING)
if ret == self.STAT_RETURN.FAILURE:
msg = f"ERROR: Unable to set the 'PLAYING' state for '{mrl}'."
log(msg)
self.emit("error", msg)
else:
self.emit("played", 0)
self._is_playing = True
def stop(self):
log("Stop playback...")
self._player.set_state(self.STATE.READY)
self._is_playing = False
def pause(self):
self._player.set_state(self.STATE.PAUSED)
def set_time(self, time):
pass
@run_task
def release(self):
self._is_playing = False
self._player.set_state(self.STATE.NULL)
self.__INSTANCE = None
def set_mrl(self, mrl):
self._player.set_property("uri", mrl)
def is_playing(self):
return self._is_playing
def on_error(self, bus, msg):
err, dbg = msg.parse_error()
log(err)
self.emit("error", "Can't Playback!")
def on_state_changed(self, bus, msg):
if not msg.src == self._player:
# Not from the player.
return
old_state, new_state, pending = msg.parse_state_changed()
if new_state is self.STATE.PLAYING:
log("Starting playback...")
self.emit("played", 0)
self.get_stream_info()
def on_eos(self, bus, msg):
""" Called when an end-of-stream message appears. """
self._player.set_state(self.STATE.READY)
self._is_playing = False
def get_stream_info(self):
log("Getting stream info...")
nr_video = self._player.get_property("n-video")
for i in range(nr_video):
# Retrieve the stream's video tags.
tags = self._player.emit("get-video-tags", i)
if tags:
_, cod = tags.get_string("video-codec")
log(f"Video codec: {cod or 'unknown'}")
nr_audio = self._player.get_property("n-audio")
for i in range(nr_audio):
# Retrieve the stream's video tags.
tags = self._player.emit("get-audio-tags", i)
if tags:
_, cod = tags.get_string("audio-codec")
log(f"Audio codec: {cod or 'unknown'}")
class VlcPlayer(Player):
""" Simple wrapper for VLC media player.
Uses python-vlc [https://github.com/oaubert/python-vlc].
"""
__VLC_INSTANCE = None
def __init__(self, mode, widget):
super().__init__(mode, widget)
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
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(f"{__class__.__name__}: Load library error: {e}")
raise ImportError("No VLC is found. Check that it is installed!")
else:
self._mode = mode
self._is_playing = False
ev_mgr = self._player.event_manager()
ev_mgr.event_attach(EventType.MediaPlayerVout, self.on_playback_start)
ev_mgr.event_attach(EventType.MediaPlayerTimeChanged,
lambda et: self.emit("position", self._player.get_time()))
ev_mgr.event_attach(EventType.MediaPlayerEncounteredError, lambda et: self.emit("error", "Can't Playback!"))
self.init_video_widget(widget)
@classmethod
def get_instance(cls, mode, widget):
if not cls.__VLC_INSTANCE:
cls.__VLC_INSTANCE = VlcPlayer(mode, widget)
return cls.__VLC_INSTANCE
def get_play_mode(self):
return self._mode
def play(self, mrl=None):
if mrl:
self._player.set_mrl(mrl)
self._player.play()
self._is_playing = True
@run_task
def stop(self):
if self._is_playing:
self._player.stop()
@@ -80,29 +455,7 @@ class Player:
self._is_playing = False
self._player.stop()
self._player.release()
def set_xwindow(self, xid):
self._player.set_xwindow(xid)
def set_nso(self, widget):
""" Used on MacOS to set NSObject.
Based on gtkvlc.py[get_window_pointer] example from here:
https://github.com/oaubert/python-vlc/tree/master/examples
"""
try:
import ctypes
g_dll = ctypes.CDLL("libgdk-3.0.dylib")
except OSError as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
else:
get_nsview = g_dll.gdk_quartz_window_get_nsview
get_nsview.restype, get_nsview.argtypes = ctypes.c_void_p, [ctypes.c_void_p]
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object]
# Get the C void* pointer to the window
pointer = ctypes.pythonapi.PyCapsule_GetPointer(widget.get_window().__gpointer__, None)
self._player.set_nsobject(get_nsview(pointer))
self.__VLC_INSTANCE = None
def set_mrl(self, mrl):
self._player.set_mrl(mrl)
@@ -110,94 +463,34 @@ class Player:
def is_playing(self):
return self._is_playing
def set_full_screen(self, full):
self._player.set_fullscreen(full)
def set_audio_track(self, track):
self._player.audio_set_track(track)
def get_audio_track(self):
return self._player.audio_get_track()
class HttpPlayer:
""" Simple wrapper for VLC media player to interact over http. """
def set_subtitle_track(self, track):
self._player.video_set_spu(track)
__VLC_INSTANCE = None
__PLAY_STREAMS_MODE = PlayStreamsMode.VLC
def set_aspect_ratio(self, ratio):
self._player.video_set_aspect_ratio(ratio)
class Commands(Enum):
STATUS = "http://127.0.0.1:{}/requests/status.xml"
PLAY = "http://127.0.0.1:{}/requests/status.xml?command=in_play&input={}"
STOP = "http://127.0.0.1:{}/requests/status.xml?command=pl_stop"
CLEAR = "http://127.0.0.1:{}/requests/status.xml?command=pl_empty"
def on_playback_start(self, event):
self.emit("played", self._player.get_media().get_duration())
# 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.
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__(self, exe, port, is_darwin):
from concurrent.futures import ThreadPoolExecutor as PoolExecutor
self._executor = PoolExecutor(max_workers=1)
self._cmd = [exe, "--no-stats", "--verbose=-1", "--extraintf", "http", "--http-port", port, "--quiet"]
if not is_darwin:
self._cmd.append("--one-instance")
self._p = None
self._state = None
self._port = port
@classmethod
def get_instance(cls, settings):
if not cls.__VLC_INSTANCE:
import shutil
is_darwin = settings.is_darwin
# TODO Add options[vlc_exe and port] to the settings!
exe = "/Applications/VLC.app/Contents/MacOS/VLC" if is_darwin else "/usr/bin/vlc"
if shutil.which(exe) is None:
raise ImportError
cls.__VLC_INSTANCE = HttpPlayer(exe=exe, port=str(9090), is_darwin=is_darwin)
return cls.__VLC_INSTANCE
@staticmethod
def get_play_mode():
return HttpPlayer.__PLAY_STREAMS_MODE
@run_task
def play(self, mrl=None):
if not self._p or self._p and self._p.poll() is not None:
self._p = subprocess.Popen(self._cmd + [mrl], preexec_fn=os.setsid)
self._p.communicate()
def init_video_widget(self, widget):
if IS_LINUX:
self._player.set_xwindow(self.get_window_handle())
elif IS_DARWIN:
self._player.set_nsobject(self.get_window_handle())
else:
self._executor.submit(self.open_command, self.Commands.CLEAR)
self._executor.submit(self.open_command, self.Commands.PLAY, mrl)
def open_command(self, command, url=None):
if command is self.Commands.PLAY:
url = self.Commands.PLAY.value.format(self._port, url)
else:
url = command.value.format(self._port)
try:
with urlopen(url, timeout=5) as f:
self._state = command
except Exception as e:
log("{}[open_command, {}] error: {}".format(__class__.__name__, command, e))
def stop(self):
if self._state is self.Commands.PLAY:
self._executor.submit(self.open_command, self.Commands.STOP)
def pause(self):
pass
def set_time(self, time):
pass
@run_task
def release(self):
if self._p and self._p.poll() is None:
import signal
# Good explanation here: https://stackoverflow.com/a/4791612
os.killpg(os.getpgid(self._p.pid), signal.SIGTERM)
def is_playing(self):
return self._state is self.Commands.PLAY
def set_full_screen(self, full):
pass
self._player.set_hwnd(self.get_window_handle())
class Recorder:
@@ -208,15 +501,18 @@ class Recorder:
def __init__(self, settings):
try:
if IS_WIN:
os.add_dll_directory(r"C:\Program Files\VideoLAN\VLC")
from app.tools import vlc
from app.tools.vlc import EventType
except OSError as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
log(f"{__class__.__name__}: Load library error: {e}")
raise ImportError
else:
self._settings = settings
self._is_record = False
args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib")
args = f"--quiet {'' if IS_DARWIN else '--no-xlib'}"
self._recorder = vlc.Instance(args).media_player_new()
@classmethod
@@ -233,7 +529,8 @@ class Recorder:
path = self._settings.records_path
os.makedirs(os.path.dirname(path), exist_ok=True)
d_now = datetime.now().strftime(_DATE_FORMAT)
path = "{}{}_{}".format(path, name.replace(" ", "_"), d_now.replace(" ", "_"))
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()
@@ -241,7 +538,7 @@ class Recorder:
self._recorder.set_media(media)
self._is_record = True
self._recorder.play()
log("Record started {}".format(d_now))
log(f"Record started {d_now}")
@run_task
def stop(self):
@@ -263,7 +560,7 @@ class Recorder:
def get_transcoding_cmd(self, path):
presets = self._settings.transcoding_presets
prs = presets.get(self._settings.active_preset)
return self._TR_CMD.format(",".join("{}={}".format(k, v) for k, v in prs.items()), path)
return self._TR_CMD.format(",".join(f"{k}={v}" for k, v in prs.items()), path)
if __name__ == "__main__":

1941
app/tools/mpv.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,243 @@
# -*- 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 glob
import os
import re
import shutil
import subprocess
from collections import namedtuple
from html.parser import HTMLParser
import requests
from app.commons import run_task, log
from app.settings import SettingsType
from app.settings import SettingsType, IS_LINUX, IS_WIN, IS_DARWIN, GTK_PATH
from .satellites import _HEADERS
_ENIGMA2_PICON_KEY = "{:X}:{:X}:{}"
_NEUTRINO_PICON_KEY = "{:x}{:04x}{:04x}.png"
Provider = namedtuple("Provider", ["logo", "name", "pos", "url", "on_id", "ssid", "single", "selected"])
Picon = namedtuple("Picon", ["ref", "ssid", "v_pid"])
Picon = namedtuple("Picon", ["ref", "ssid"])
class PiconsError(Exception):
pass
class PiconsCzDownloader:
""" The main class for loading picons from the https://picon.cz/ source (by Chocholoušek). """
_PERM_URL = "https://picon.cz/download/7337"
_BASE_URL = "https://picon.cz/download/"
_BASE_LOGO_URL = "https://picon.cz/picon/0/"
_HEADER = {"User-Agent": "DemonEditor/2.2.2", "Referer": ""}
_LINK_PATTERN = re.compile(r"((.*)-\d+x\d+)-(.*)_by_chocholousek.7z$")
_FILE_PATTERN = re.compile(b"\\s+(1_.*\\.png).*")
def __init__(self, picon_ids=set(), appender=log):
self._perm_links = {}
self._providers = {}
self._provider_logos = {}
self._picon_ids = picon_ids
self._appender = appender
def init(self):
""" Initializes dict with values: download_id -> perm link and provider data. """
if self._perm_links:
return
self._HEADER["Referer"] = self._PERM_URL
with requests.get(url=self._PERM_URL, headers=self._HEADER, stream=True) as request:
if request.reason == "OK":
logo_map = self.get_logos_map()
name_map = self.get_name_map()
for line in request.iter_lines():
data = line.decode(encoding="utf-8", errors="ignore").split(maxsplit=1)
if len(data) != 2:
continue
l_id, perm_link = data
self._perm_links[str(l_id)] = str(perm_link)
data = re.match(self._LINK_PATTERN, perm_link)
if data:
sat_pos = data.group(3)
# Logo url.
logo = logo_map.get(data.group(2), None)
l_name = name_map.get(sat_pos, None) or sat_pos.replace(".", "")
logo_url = f"{self._BASE_LOGO_URL}{logo}/{l_name}.png" if logo else None
prv = Provider(None, data.group(1), sat_pos, self._BASE_URL + l_id, l_id, logo_url, None, False)
if sat_pos in self._providers:
self._providers[sat_pos].append(prv)
else:
self._providers[sat_pos] = [prv]
else:
log(f"{self.__class__.__name__} [get permalinks] error: {request.reason}")
raise PiconsError(request.reason)
@property
def providers(self):
return self._providers
def get_sat_providers(self, url):
return self._providers.get(url, [])
def download(self, provider, picons_path, picon_ids=None):
self._HEADER["Referer"] = provider.url
with requests.get(url=provider.url, headers=self._HEADER, stream=True) as request:
if request.reason == "OK":
dest = f"{picons_path}{provider.on_id}.7z"
self._appender(f"Downloading: {provider.url}\n")
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.extract(dest, picons_path, picon_ids)
else:
log(f"{self.__class__.__name__} [download] error: {request.reason}")
def extract(self, src, dest, picon_ids=None):
""" Extracts 7z archives. """
# TODO: think about https://github.com/miurahr/py7zr
exe = "7z"
if IS_DARWIN and GTK_PATH:
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"
if not os.path.isfile(exe):
raise PiconsError("7-Zip executable not found!")
cmd = [exe, "l", src]
try:
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
if err:
log(f"{self.__class__.__name__} [extract] error: {err}")
raise PiconsError(err)
except OSError as e:
log(f"{self.__class__.__name__} [extract] error: {e}")
raise PiconsError(e)
is_filter = bool(picon_ids)
ids = picon_ids or self._picon_ids
to_extract = []
for o in re.finditer(self._FILE_PATTERN, out):
p_id = o.group(1).decode("utf-8", errors="ignore")
if p_id in ids:
to_extract.append(p_id)
if is_filter and not to_extract:
if os.path.isfile(src):
os.remove(src)
raise PiconsError("No matching picons found!")
cmd = [exe, "e", src, "-o{}".format(dest), "-y", "-r"]
cmd.extend(to_extract)
try:
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
if err:
log(f"{self.__class__.__name__} [extract] error: {err}")
raise PiconsError(err)
else:
if os.path.isfile(src):
os.remove(src)
except OSError as e:
log(e)
raise PiconsError(e)
def get_logo_data(self, url):
""" Returns the logo data if present. """
return self._provider_logos.get(url, None)
def get_provider_logo(self, url):
""" Retrieves package logo. """
# Getting package logo.
logo = self._provider_logos.get(url, None)
if logo:
return logo
try:
with requests.get(url=url, stream=True) as logo_request:
if logo_request.reason == "OK":
data = logo_request.content
self._provider_logos[url] = data
return data
else:
log(f"Downloading package logo error: {logo_request.reason}")
except requests.exceptions.ConnectionError as e:
log(f"{self.__class__.__name__} error [get provider logo]: {e}")
def get_logos_map(self):
return {"piconblack": "b50",
"picontransparent": "t50",
"piconwhite": "w50",
"piconmirrorglass": "mr100",
"piconnoName": "n100",
"piconsrhd": "srhd100",
"piconfreezeframe": "ff220",
"piconfreezewhite": "fw100",
"piconpoolrainbow": "r100",
"piconsimpleblack": "s220",
"piconjustblack": "jb220",
"picondirtypaper": "dp220",
"picongray": "g400",
"piconmonochrom": "m220",
"picontransparentwhite": "tw100",
"picontransparentdark": "td220",
"piconoled": "o96",
"piconblack80": "b50",
"piconblack3d": "b50",
"piconwin11": "win11220"
}
def get_name_map(self):
return {"antiksat": "ANTIK",
"digiczsk": "DIGI",
"DTTitaly": "picon_trs-it",
"dvbtCZSK": "picon_trs",
"PolandDTT": "picon_trs-pl",
"freeSAT": "UPC DIRECT",
"orangesat": "ORANGE TV",
"skylink": "M7 GROUP",
}
class PiconsParser(HTMLParser):
""" Parser for package html page. (https://www.lyngsat.com/packages/*provider-name*.html) """
_BASE_URL = "https://www.lyngsat.com"
def __init__(self, entities=False, separator=' ', single=None):
@@ -58,19 +279,16 @@ class PiconsParser(HTMLParser):
row = self._current_row
ln = len(row)
if self._single and ln == 4 and row[0].startswith("../../logo/"):
self.picons.append(Picon(row[0].strip("../"), "0", "0"))
if self._single and ln == 4 and row[0].startswith("/logo/"):
self.picons.append(Picon(row[0].strip(), "0"))
else:
if 9 < ln < 13:
if ln > 8:
url = None
if row[0].startswith("../logo/"):
url = row[0]
elif row[1].startswith("../logo/"):
url = row[1]
if row[2].startswith("/logo/"):
url = row[2]
ssid = row[-4]
if url and len(ssid) > 2:
self.picons.append(Picon(url, ssid, row[-3]))
if url and row[0].isdigit():
self.picons.append(Picon(url, row[0]))
self._current_row = []
@@ -78,37 +296,47 @@ class PiconsParser(HTMLParser):
pass
@staticmethod
def parse(open_path, picons_path, tmp_path, provider, picon_ids, s_type=SettingsType.ENIGMA_2):
if not os.path.isfile(open_path):
log("PiconsParser error [parse]. No such file or directory: {}".format(open_path))
def parse(provider, picons_path, picon_ids, s_type=SettingsType.ENIGMA_2):
""" Returns tuple(url, picon file name) list. """
req = requests.get(provider.url, timeout=5)
if req.status_code == 200:
logo_data = req.text
else:
log("Provider picons downloading error: {} {}".format(provider.url, req.reason))
return
with open(open_path, encoding="utf-8", errors="replace") as f:
on_id, pos, ssid, single = provider.on_id, provider.pos, provider.ssid, provider.single
neg_pos = pos.endswith("W")
pos = int("".join(c for c in pos if c.isdigit()))
# For negative (West) positions 3600 - numeric position value!!!
if neg_pos:
pos = 3600 - pos
parser = PiconsParser(single=single)
parser.reset()
parser.feed(f.read())
picons = parser.picons
if picons:
os.makedirs(picons_path, exist_ok=True)
for p in picons:
try:
if single:
on_id, freq = on_id.strip().split("::")
namespace = "{:X}{:X}".format(int(pos), int(freq))
else:
namespace = "{:X}0000".format(int(pos))
name = PiconsParser.format(ssid if single else p.ssid, on_id, namespace, picon_ids, s_type)
p_name = picons_path + (name if name else os.path.basename(p.ref))
shutil.copyfile(tmp_path + "www.lyngsat.com/" + p.ref.lstrip("."), p_name)
except (TypeError, ValueError) as e:
msg = "Picons format parse error: {}".format(p) + "\n" + str(e)
log(msg)
on_id, pos, ssid, single = provider.on_id, provider.pos, provider.ssid, provider.single
neg_pos = pos.endswith("W")
pos = int("".join(c for c in pos if c.isdigit()))
# For negative (West) positions 3600 - numeric position value!!!
if neg_pos:
pos = 3600 - pos
parser = PiconsParser(single=provider.single)
parser.reset()
parser.feed(logo_data)
picons = parser.picons
picons_data = []
if picons:
for p in picons:
try:
if single:
on_id, freq = on_id.strip().split("::")
namespace = "{:X}{:X}".format(int(pos), int(freq))
else:
namespace = "{:X}0000".format(int(pos))
if single and not ssid.isdigit():
ssid = "".join(c for c in ssid if c.isdigit()) or "0"
name = PiconsParser.format(ssid if single else p.ssid, on_id, namespace, picon_ids, s_type)
p_name = picons_path + (name if name else os.path.basename(p.ref))
picons_data.append(("{}{}".format(PiconsParser._BASE_URL, p.ref), p_name))
except (TypeError, ValueError) as e:
msg = "Picons format parse error: {}".format(p) + "\n" + str(e)
log(msg)
return picons_data
@staticmethod
def format(ssid, on_id, namespace, picon_ids, s_type):
@@ -127,7 +355,8 @@ class ProviderParser(HTMLParser):
_POSITION_PATTERN = re.compile("at\s\d+\..*(?:E|W)']")
_ONID_TID_PATTERN = re.compile("^\d+-\d+.*")
_TRANSPONDER_FREQUENCY_PATTERN = re.compile("^\d+ [HVLR]+")
_DOMAINS = {"/tvchannels/", "/radiochannels/", "/packages/"}
_DOMAINS = {"/tvchannels/", "/radiochannels/", "/packages/", "/logo/"}
_BASE_URL = "https://www.lyngsat.com"
def __init__(self, entities=False, separator=' '):
@@ -155,26 +384,17 @@ class ProviderParser(HTMLParser):
if tag == 'tr':
self._is_th = True
if tag == "img":
if attrs[0][1].startswith("logo/"):
if attrs[0][1].startswith("/logo/"):
self._current_row.append(attrs[0][1])
if tag == "a":
url = attrs[0][1]
if any(d in url for d in self._DOMAINS):
self._current_row.append(url)
if tag == "font" and len(attrs) == 1:
atr = attrs[0]
if len(atr) == 2 and atr[1] == "darkgreen":
self._is_onid_tid = True
def handle_data(self, data):
""" Save content to a cell """
if self._is_td or self._is_th:
self._current_cell.append(data.strip())
if self._is_onid_tid:
m = self._ONID_TID_PATTERN.match(data)
if m:
self._on_id, tid = m.group().split("-")
self._is_onid_tid = False
def handle_endtag(self, tag):
if tag == 'td':
@@ -187,41 +407,49 @@ class ProviderParser(HTMLParser):
self._current_row.append(final_cell)
self._current_cell = []
elif tag == 'tr':
r = self._current_row
row = self._current_row
# Satellite position
if not self._positon:
pos = re.findall(self._POSITION_PATTERN, str(r))
pos = re.findall(self._POSITION_PATTERN, str(row))
if pos:
self._positon = "".join(c for c in str(pos) if c.isdigit() or c in ".EW")
len_row = len(r)
len_row = len(row)
if len_row > 2:
m = self._TRANSPONDER_FREQUENCY_PATTERN.match(r[1])
m = self._TRANSPONDER_FREQUENCY_PATTERN.match(row[0])
if m:
self._freq = m.group().split()[0]
if len_row == 12:
if len_row > 12:
# Providers
name = r[5]
name = row[5]
self._prv_names.add(name)
m = self._ONID_TID_PATTERN.match(str(r[-2]))
m = self._ONID_TID_PATTERN.match(str(row[-5]))
if m:
on_id, tid = m.group().split("-")
if on_id not in self._ids:
r[-2] = on_id
self._on_id = on_id
row[-2] = on_id
self._ids.add(on_id)
r[0] = self._positon
row[0] = self._positon
if name + on_id not in self._prv_names:
self._prv_names.add(name + on_id)
self.rows.append(Provider(logo=r[2], name=name, pos=self._positon, url=r[6], on_id=on_id,
logo_data = None
if row[2].startswith("/logo/"):
req = requests.get(self._BASE_URL + row[2], timeout=5)
if req.status_code == 200:
logo_data = req.content
else:
log("Downloading provider logo error: {}".format(req.reason))
self.rows.append(Provider(logo=logo_data, name=name, pos=self._positon, url=row[6], on_id=on_id,
ssid=None, single=False, selected=True))
elif 6 < len_row < 10:
elif 6 < len_row < 12:
# Single services
name, url, ssid = None, None, None
if r[0].startswith("http"):
name, url, ssid = r[1], r[0], r[4]
elif r[1].startswith("http"):
name, url, ssid = r[2], r[1], r[5]
if row[0].startswith("http"):
name, url, ssid = row[1], row[0], row[0]
elif row[1].startswith("http"):
name, url, ssid = row[2], row[1], row[0]
if name and url:
on_id = "{}::{}".format(self._on_id if self._on_id else "1", self._freq)
@@ -237,14 +465,51 @@ class ProviderParser(HTMLParser):
super().reset()
def parse_providers(open_path):
def parse_providers(url):
""" Returns a list of providers sorted by logo [single channels after providers]. """
parser = ProviderParser()
parser.reset()
with open(open_path, encoding="utf-8", errors="replace") as f:
parser.feed(f.read())
request = requests.get(url=url, headers=_HEADERS)
if request.status_code == 200:
parser.feed(request.text)
else:
log("Parse providers error [{}]: {}".format(url, request.reason))
return parser.rows
def srt(p):
if p.logo is None:
return 1
return 0
providers = parser.rows
providers.sort(key=srt)
return providers
def download_picon(src_url, dest_path, callback):
""" Downloads and saves the picon to file. """
err_msg = "Picon download error: {} [{}]"
timeout = (3, 5) # connect and read timeouts
if callback:
callback("Downloading: {}.\n".format(os.path.basename(dest_path)))
req = requests.get(src_url, timeout=timeout, stream=True)
if req.status_code != 200:
err_msg = err_msg.format(src_url, req.reason)
log(err_msg)
if callback:
callback(err_msg + "\n")
else:
try:
with open(dest_path, "wb") as f:
for chunk in req:
f.write(chunk)
except OSError as e:
err_msg = "Saving picon [{}] error: {}".format(dest_path, e)
log(err_msg)
if callback:
callback(err_msg + "\n")
@run_task

View File

@@ -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
@@ -14,13 +42,15 @@ from app.eparser import Satellite, Transponder, is_transponder_valid
from app.eparser.ecommons import (PLS_MODE, get_key_by_value, FEC, SYSTEM, POLARIZATION, MODULATION, SERVICE_TYPE,
Service, CAS)
_HEADERS = {"User-Agent": "Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/69.0"}
_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",)
@staticmethod
def get_sources(src):
@@ -37,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))
@@ -76,6 +106,8 @@ class Cell:
class SatellitesParser(HTMLParser):
""" Parser for satellite html page. """
POS_PAT = re.compile(r".*?(\d+\.\d°?[EW]).*")
def __init__(self, source=SatelliteSource.FLYSAT, entities=False, separator=' '):
HTMLParser.__init__(self)
@@ -89,6 +121,7 @@ class SatellitesParser(HTMLParser):
self._current_cell = []
self._rows = []
self._source = source
self.pls_modes = {v: k for k, v in PLS_MODE.items()}
def handle_starttag(self, tag, attrs):
if tag == "td":
@@ -96,7 +129,9 @@ class SatellitesParser(HTMLParser):
if tag == "tr":
self._is_th = True
if tag == "a":
self._current_row.append(attrs[0][1])
for atr in attrs:
if atr[0] == "href":
self._current_row.append(atr[1])
def handle_data(self, data):
""" Save content to a cell """
@@ -129,9 +164,9 @@ 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))
request = requests.get(url=src, headers=_HEADERS, timeout=_TIMEOUT)
except requests.exceptions.RequestException as e:
log(f"Getting satellite list error: {repr(e)}")
return []
else:
reason = request.reason
@@ -142,163 +177,230 @@ class SatellitesParser(HTMLParser):
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:
extra_pattern = re.compile(r"^https://www\.lyngsat\.com/[\w-]+\.html")
base_url = "https://www.lyngsat.com/"
sats = []
names = set()
current_pos = "0"
for row in filter(lambda x: len(x) in (5, 7, 8), self._rows):
r_len = len(row)
if r_len == 7:
current_pos = self.parse_position(row[2])
name = row[1].rsplit("/")[-1].rstrip(".html").replace("-", " ")
if name not in names:
# [all in one] satellites
sats.append((name, current_pos, row[5], base_url + row[1], False))
names.add(name)
name = row[4]
if name not in names:
sats.append((name, current_pos, row[5], base_url + row[3], False))
names.add(name)
if r_len == 8: # for a very limited number of satellites
data = list(filter(None, row))
urls = set()
sat_type = ""
for d in data:
url = re.match(extra_pattern, d)
if url:
urls.add(url.group(0))
if d in ("C", "Ku", "CKu"):
sat_type = d
current_pos = self.parse_position(data[1])
for url in urls:
name = url.rsplit("/")[-1].rstrip(".html").replace("-", " ")
sats.append((name, current_pos, sat_type, base_url + url, False))
elif r_len == 5:
sats.append((row[2], current_pos, row[3], base_url + row[1], False))
return sats
return self.get_satellites_for_lyng_sat()
elif source is SatelliteSource.KINGOFSAT:
return self.get_satellites_for_king_of_sat()
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()
url = "https://www.flysat.com/" + sat_url if self._source is SatelliteSource.FLYSAT else sat_url
request = requests.get(url=url, headers=_HEADERS)
reason = request.reason
trs = []
if reason == "OK":
self.feed(request.text)
if self._source is SatelliteSource.FLYSAT:
self.get_transponders_for_fly_sat(trs)
elif self._source is SatelliteSource.LYNGSAT:
self.get_transponders_for_lyng_sat(trs)
if self._source is SatelliteSource.KINGOFSAT:
sat_url = "https://en.kingofsat.net/" + sat_url
try:
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)
if self._source is SatelliteSource.FLYSAT:
self.get_transponders_for_fly_sat(trs)
elif self._source is SatelliteSource.LYNGSAT:
self.get_transponders_for_lyng_sat(trs)
elif self._source is SatelliteSource.KINGOFSAT:
self.get_transponders_for_king_of_sat(trs)
else:
log(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", pol, fec, sys, mod, 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("^(\\d{4,5})-(\\d/\\d)(.+PSK)?(.*)?$")
sys_pattern = re.compile("(DVB-S[2]?) ?(PLS+ (Root|Gold|Combo)+ (\\d+))* ?(multistream stream (\\d+))?",
re.IGNORECASE)
zeros = "000"
pls_modes = {v: k for k, v in PLS_MODE.items()}
""" Parsing transponders for LyngSat. """
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 r in filter(lambda x: len(x) > 8, self._rows):
for frq in r[1], r[2], r[3]:
freq = re.match(frq_pol_pattern, frq)
if freq:
for row in filter(lambda x: len(x) > 8, self._rows):
for freq in row[1], row[2], row[3]:
res = re.match(frq_pol_pattern, freq)
if res:
break
if not freq:
continue
frq, pol = freq.group(1), freq.group(2)
sr_fec = re.match(sr_fec_pattern, r[-3])
if not sr_fec:
continue
sr, fec, mod = sr_fec.group(1), sr_fec.group(2), sr_fec.group(3)
mod = mod.strip() if mod else "Auto"
res = re.match(sys_pattern, r[-4])
if not res:
continue
sys = res.group(1)
pls_mode = res.group(3)
pls_mode = pls_modes.get(pls_mode.capitalize(), None) if pls_mode else pls_mode
pls_code = res.group(4)
pls_id = res.group(6)
freq, pol = res.group(1), res.group(2)
res = re.search(sr_fec_pattern, row[3])
if not res:
continue
tr = Transponder(frq + zeros, sr + zeros, pol, fec, sys, mod, pls_mode, pls_code, pls_id)
sys, mod, sr, fec = res.group(1), res.group(2), res.group(3), res.group(4)
mod = mod.strip() if mod else "Auto"
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)
if plp is not None:
log(f"Detected T2-MI transponder! [{freq} {sr} {pol}] ")
tr = Transponder(f"{freq}000", f"{sr}000", pol, fec, sys, mod, pls_mode, pls_code, is_id, None)
if is_transponder_valid(tr):
trs.append(tr)
def get_transponders_for_king_of_sat(self, trs):
""" Getting transponders for KingOfSat source.
Since the *.ini file contains incomplete information, it is not used.
"""
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):
freq, pol = row[2].replace(".", "0"), row[3]
if not freq.isdigit() or pol not in "VHLR":
continue
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", pol, fec, sys, mod, pls_id, pls_code, is_id, None)
if is_transponder_valid(tr):
trs.append(tr)
@@ -310,23 +412,56 @@ class ServicesParser(HTMLParser):
HTMLParser.__init__(self)
self._S_TYPES = {"": "2", "MPEG-2 SD": "1", "SD": "1", "MPEG-4 SD": "22", "HEVC SD": "22", "MPEG-4 HD": "25",
"MPEG-4 HD 1080": "25", "MPEG-4 HD 720": "25", "HEVC HD": "25", "HEVC UHD": "31",
"HEVC UHD 4K": "31"}
self._TR_PAT = re.compile(r"(DVB-S[2]?)/?(.*PSK)?\s+SR\s+(\d+)\s+FEC\s+(\d/\d).*ONID/TID:\s+(\d+)/(\d+)\s+.*")
self._PTR_PAT = re.compile(r".*?(\d+\.\d°[EW]):\s+(\d+)\s+([RLHV]).*")
self._S_TYPES = {"": "2", "MPEG-2 SD": "1", "MPEG-2/SD": "1", "SD": "1", "MPEG-4 SD": "22", "MPEG-4/SD": "22",
"MPEG-4": "22", "HEVC SD": "22", "MPEG-4/HD": "25", "MPEG-4 HD": "25", "MPEG-4 HD 1080": "25",
"MPEG-4 HD 720": "25", "HEVC HD": "25", "HEVC/HD": "25", "HEVC": "31", "HEVC/UHD": "31",
"HEVC UHD": "31", "HEVC UHD 4K": "31", "3": "Data"}
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()
self._rows = []
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):
return self._source
@source.setter
def source(self, value):
self._source = value
self.reset()
@property
def use_short_names(self):
return self._use_short_names
@use_short_names.setter
def use_short_names(self, value):
self._use_short_names = value
def handle_starttag(self, tag, attrs):
if tag == "td":
@@ -334,17 +469,39 @@ class ServicesParser(HTMLParser):
elif tag == "tr":
self._is_th = True
elif tag == "a" and not self._current_cell.url:
self._current_cell.url = attrs[0][1]
if attrs:
for a in attrs:
if a[0] == "href":
self._current_cell.url = a[1]
if self._source is SatelliteSource.KINGOFSAT and self._use_short_names:
if a[0] != "title":
continue
txt = a[1]
sep = "Id: "
if txt and txt.startswith(sep):
# Saving the 'short' name.
_, sep, name = txt.partition(sep)
self._current_cell.text = name
elif tag == "img":
img_link = attrs[0][1]
if img_link.startswith("/logo/"):
if self._source is SatelliteSource.LYNGSAT:
if img_link.startswith("/logo/"):
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
@@ -352,8 +509,9 @@ class ServicesParser(HTMLParser):
self._is_th = False
if tag in ("td", "th"):
final_cell = self._separator.join(self._current_cell_text).strip()
self._current_cell.text = final_cell
if not self._current_cell.text:
txt = self._separator.join(self._current_cell_text).strip()
self._current_cell.text = txt
self._current_row.append(self._current_cell)
self._current_cell_text = []
self._current_cell = Cell()
@@ -363,32 +521,48 @@ 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 is not SatelliteSource.LYNGSAT:
raise ValueError("Unsupported source: {}!".format(self._source.name))
if self._source not in (SatelliteSource.LYNGSAT, SatelliteSource.KINGOFSAT):
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. """
try:
if self._source is SatelliteSource.KINGOFSAT:
sat_url = "https://en.kingofsat.net/" + sat_url
self.init_data(sat_url)
except ValueError as e:
log(e)
else:
url = "https://www.lyngsat.com/muxes/"
return [row[1] for row in
filter(lambda x: x and len(x) > 8 and x[1].url and x[1].url.startswith(url), self._rows)]
if self._source is SatelliteSource.LYNGSAT:
url = "https://www.lyngsat.com/muxes/"
return [row[0] for row in
filter(lambda x: x and len(x) > 8 and x[0].url and x[0].url.startswith(url), self._rows)]
elif self._source is SatelliteSource.KINGOFSAT:
trs = []
for r in self._rows:
if len(r) == 13 and SatellitesParser.POS_PAT.match(r[0].text):
t_cell = r[4]
if t_cell.url and t_cell.url.startswith("tp.php?tp="):
t_cell.url = f"https://en.kingofsat.net/{t_cell.url}"
t_cell.text = f"{r[2].text} {r[3].text} {r[6].text} {r[8].text}"
trs.append(t_cell)
return trs
return []
def get_transponder_services(self, tr_url, sat_position=None, use_pids=False):
@@ -398,103 +572,189 @@ class ServicesParser(HTMLParser):
@param sat_position: custom satellite position. Sometimes required to adjust the namespace.
@param use_pids: if possible use additional pids [video, audio].
"""
services = []
try:
self._t_url = tr_url
self.init_data(tr_url)
except ValueError as e:
log(e)
return []
else:
pos, freq, sr, fec, pol, namespace, tid, nid = sat_position or 0, 0, 0, 0, 0, 0, 0, 0
sys = "DVB-S"
tr_found = False
pos_found = False
tr = None
# Transponder
for r in filter(lambda x: x and len(x) == 2, self._rows):
if not pos_found:
pos_tr = re.match(self._PTR_PAT, r[1].text)
if pos_tr:
if not sat_position:
pos = int(SatellitesParser.get_position(
"".join(c for c in pos_tr.group(1) if c.isdigit() or c.isalpha())))
freq = int(pos_tr.group(2))
pol = get_key_by_value(POLARIZATION, pos_tr.group(3))
pos_found = True
if 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)
return []
if pos_found and not tr_found:
td = re.match(self._TR_PAT, r[1].text) or re.match(self._TR_PAT, r[0].text)
if td:
sys, mod, sr, _fec, nid, tid = td.group(1), td.group(2), td.group(3), td.group(4), td.group(
5), td.group(6)
neg_pos = False # POS = W
# For negative (West) positions: 3600 - numeric position value!!!
namespace = "{:04x}0000".format(3600 - pos if neg_pos else pos)
inv = 2 # Default
fec = get_key_by_value(FEC, _fec)
sys = get_key_by_value(SYSTEM, sys)
tr_flag = 1
mod = get_key_by_value(MODULATION, mod)
roll_off = 0 # 35% DVB-S2/DVB-S (default)
pilot = 2 # Auto
s2_flags = "" if sys == "DVB-S" else self._S2_TR.format(tr_flag, mod or 0, roll_off, pilot)
nid, tid = int(nid), int(tid)
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
tr_found = True
def get_lyngsat_services(self, sat_position=None, use_pids=False):
services = []
pos, freq, sr, fec, pol, nsp, tid, nid = sat_position or 0, 0, 0, 0, 0, 0, 0, 0
sys = "DVB-S"
pos_found = False
tr = None
# 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)
if not pos_tr:
continue
if not tr:
msg = "ServicesParser error [get transponder services]: {}"
er = "Transponder [{}] not found or its type [T2-MI, etc] not supported yet.".format(freq)
log(msg.format(er))
return []
if not sat_position:
pos = self.get_position(pos_tr.group(1))
pos_found = True
# Services
for r in filter(lambda x: x and len(x) == 12 and (x[0].text.isdigit()), self._rows):
sid, name, cas, pkg, s_type, v_pid, a_pid = r[0].text, r[2].text, r[4].text, r[5].text, r[
6].text.strip(), r[7].text, r[8].text.split()
if pos_found:
text = " ".join(c.text for c in r[1:])
td = re.match(self._TR_PAT, text)
if td:
freq, pol = int(td.group(1)), get_key_by_value(POLARIZATION, td.group(2))
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 [{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(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
sid = int(sid)
data_id = "{:04x}:{}:{:04x}:{:04x}:{}:0:0".format(sid, namespace, tid, nid, s_type)
fav_id = "{}:{}:{}:{}".format(sid, tid, nid, namespace)
picon_id = "1_0_{:X}_{}_{}_{}_{}_0_0_0.png".format(int(s_type), sid, tid, nid, namespace)
# Flags.
flags = "p:{}".format(pkg)
cas = ",".join(get_key_by_value(CAS, c) or "C:0000" for c in cas.split()) if cas else None
if use_pids:
v_pid = "c:00{:04x}".format(int(v_pid)) if v_pid else None
a_pid = ",".join(["c:01{:04x}".format(int(p)) for p in a_pid]) if a_pid else None
flags = ",".join(filter(None, (flags, v_pid, a_pid, cas)))
else:
flags = ",".join(filter(None, (flags, cas)))
srv = Service(flags_cas=flags,
transponder_type="s",
coded=None,
service=name,
locked=None,
hide=None,
package=pkg,
service_type=_s_type,
picon=r[1].img,
picon_id=picon_id,
ssid=sid,
freq=freq,
rate=sr,
pol=pol,
fec=fec,
system=sys,
pos=pos,
data_id=data_id,
fav_id=fav_id,
transponder=tr)
services.append(srv)
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("ServicesParser error [get transponder services]: {}".format(e))
log(f"ServicesParser error [get transponder services]: {e}")
return services
def get_kingofsat_services(self, sat_position=None, use_pids=False):
services = []
# Transponder
tr = list(filter(lambda r: len(r) == 13 and r[4].url and r[4].url.startswith("tp.php?tp="), self._rows))
if not tr:
log(f"ServicesParser error [get transponder services]: Transponder [{self._t_url}] not found!")
return services
tr, multi_tr, tid, nid, nsp = None, None, None, None, None
freq, sr, pol, fec, sys, pos = None, None, None, None, None, None
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
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
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))
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)
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
def get_transponder_data(self, pos, fec, sys, mod):
""" Returns converted transponder data. """
sys = get_key_by_value(SYSTEM, sys)
mod = get_key_by_value(MODULATION, mod)
fec = get_key_by_value(FEC, fec)
# For negative (West) positions: 3600 - numeric position value!!!
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
s2_flags = "" if sys == "DVB-S" else self._S2_TR.format(tr_flag, mod or 0, roll_off, pilot)
inv = 2 # Default
return sys, mod, fec, namespace, s2_flags, roll_off, pilot, inv
@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 = 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 = 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 = 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)))
return flags, sid, fav_id, picon_id, data_id
@staticmethod
def get_position(pos):
return int(SatellitesParser.get_position("".join(c for c in pos if c.isdigit() or c.isalpha())))
if __name__ == "__main__":
pass

View File

@@ -1,4 +1,32 @@
""" Module for working with YouTube service """
# -*- 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 YouTube service. """
import gzip
import json
import os
@@ -7,19 +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"}
@@ -80,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)
@@ -90,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. """
@@ -171,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:
@@ -189,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):
@@ -205,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)
@@ -230,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 = settings.default_data_path + "tools/"
self._path = f"{settings.default_data_path}tools{SEP}"
self._update = settings.enable_yt_dl_update
self._supported = {"22", "18"}
self._dl = None
@@ -247,7 +335,7 @@ class YouTubeDL:
return cls._DL_INSTANCE
def init(self):
if not os.path.isfile(self._path + "youtube_dl/version.py"):
if not os.path.isfile(f"{self._path}youtube_dl{SEP}version.py"):
self.get_latest_release()
if self._path not in sys.path:
@@ -259,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)
@@ -289,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:
@@ -300,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)
@@ -307,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
@@ -348,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)

481
app/ui/app_menu.ui Normal file
View File

@@ -0,0 +1,481 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="menu_bar">
<submenu>
<attribute name="label" translatable="yes">Playback</attribute>
<attribute name="action">app.hide_media_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<section>
<item>
<attribute name="label" translatable="yes">Close</attribute>
<attribute name="action">app.on_playback_close</attribute>
</item>
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">File</attribute>
<attribute name="action">app.hide_menu_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<section>
<submenu>
<attribute name="label" translatable="yes">Import</attribute>
<section>
<item>
<attribute name="label" translatable="yes">Bouquet</attribute>
<attribute name="action">app.on_import_bouquet</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Bouquets and services</attribute>
<attribute name="action">app.on_import_bouquets</attribute>
</item>
</section>
</submenu>
<item>
<attribute name="label" translatable="yes">Import from Web</attribute>
<attribute name="action">app.on_import_from_web</attribute>
</item>
<item>
<attribute name="label" translatable="yes">New empty configuration</attribute>
<attribute name="action">app.on_new_configuration</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Open</attribute>
<attribute name="action">app.on_data_open</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Extract...</attribute>
<attribute name="action">app.on_archive_open</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Save</attribute>
<attribute name="action">app.on_data_save</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Save as</attribute>
<attribute name="action">app.on_data_save_as</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">FTP-transfer</attribute>
<attribute name="action">app.on_download</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Settings</attribute>
<attribute name="action">app.on_settings</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Exit</attribute>
<attribute name="action">app.on_close_app</attribute>
</item>
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">Edit</attribute>
<attribute name="action">app.hide_menu_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<section>
<item>
<attribute name="label" translatable="yes">Lock</attribute>
<attribute name="action">app.on_locked</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Hide</attribute>
<attribute name="action">app.on_hide</attribute>
</item>
</section>
</submenu>
<submenu id="view_menu">
<attribute name="label" translatable="yes">View</attribute>
<attribute name="action">app.hide_menu_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<section>
<item>
<attribute name="label" translatable="yes">Bouquets</attribute>
<attribute name="action">app.show_bouquets</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Satellites</attribute>
<attribute name="action">app.show_satellites</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Picons</attribute>
<attribute name="action">app.show_picons</attribute>
</item>
<item>
<attribute name="label" translatable="yes">EPG</attribute>
<attribute name="action">app.show_epg</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Timers</attribute>
<attribute name="action">app.show_timers</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Recordings</attribute>
<attribute name="action">app.show_recordings</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
<item>
<attribute name="label" translatable="yes">FTP</attribute>
<attribute name="action">app.show_ftp</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Control</attribute>
<attribute name="action">app.show_control</attribute>
<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">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>
<attribute name="action">app.hide_menu_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<section>
<item>
<attribute name="label" translatable="yes">Backups</attribute>
<attribute name="action">app.on_backup_tool_show</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Telnet</attribute>
<attribute name="action">app.on_telnet_show</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Logs</attribute>
<attribute name="action">app.on_logs_show</attribute>
</item>
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">FTP client</attribute>
<attribute name="action">app.show_ftp_menu</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<item>
<attribute name="label" translatable="yes">Close</attribute>
<attribute name="action">app.on_ftp_client_close</attribute>
</item>
</submenu>
<submenu>
<attribute name="label" translatable="yes">Help</attribute>
<section>
<item>
<attribute name="label" translatable="yes">About</attribute>
<attribute name="action">app.on_about_app</attribute>
</item>
</section>
</submenu>
</menu>
<menu id="mac_app_menu">
<section>
<item>
<attribute name="label" translatable="yes">About</attribute>
<attribute name="action">app.on_about_app</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Settings</attribute>
<attribute name="action">app.on_settings</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Exit</attribute>
<attribute name="action">app.on_close_app</attribute>
</item>
</section>
</menu>
<menu id="mac_menu_bar">
<submenu>
<attribute name="label" translatable="yes">Playback</attribute>
<attribute name="action">app.hide_media_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<section>
<item>
<attribute name="label" translatable="yes">Play</attribute>
<attribute name="action">app.on_play</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Stop</attribute>
<attribute name="action">app.on_stop</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Close</attribute>
<attribute name="action">app.on_playback_close</attribute>
</item>
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">File</attribute>
<attribute name="action">app.hide_menu_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<section>
<submenu>
<attribute name="label" translatable="yes">Import</attribute>
<section>
<item>
<attribute name="label" translatable="yes">Bouquet</attribute>
<attribute name="action">app.on_import_bouquet</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Bouquets and services</attribute>
<attribute name="action">app.on_import_bouquets</attribute>
</item>
</section>
</submenu>
<item>
<attribute name="label" translatable="yes">Import from Web</attribute>
<attribute name="action">app.on_import_from_web</attribute>
</item>
<item>
<attribute name="label" translatable="yes">New empty configuration</attribute>
<attribute name="action">app.on_new_configuration</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Open</attribute>
<attribute name="action">app.on_data_open</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Extract...</attribute>
<attribute name="action">app.on_archive_open</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Save</attribute>
<attribute name="action">app.on_data_save</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Save as</attribute>
<attribute name="action">app.on_data_save_as</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">FTP-transfer</attribute>
<attribute name="action">app.on_download</attribute>
</item>
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">Edit</attribute>
<attribute name="action">app.hide_menu_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<section>
<item>
<attribute name="label" translatable="yes">Lock</attribute>
<attribute name="action">app.on_locked</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Hide</attribute>
<attribute name="action">app.on_hide</attribute>
</item>
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">View</attribute>
<attribute name="action">app.hide_menu_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<section>
<item>
<attribute name="label" translatable="yes">Bouquets</attribute>
<attribute name="action">app.show_bouquets</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Satellites</attribute>
<attribute name="action">app.show_satellites</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Picons</attribute>
<attribute name="action">app.show_picons</attribute>
</item>
<item>
<attribute name="label" translatable="yes">EPG</attribute>
<attribute name="action">app.show_epg</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Timers</attribute>
<attribute name="action">app.show_timers</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Recordings</attribute>
<attribute name="action">app.show_recordings</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
<item>
<attribute name="label" translatable="yes">FTP</attribute>
<attribute name="action">app.show_ftp</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Control</attribute>
<attribute name="action">app.show_control</attribute>
<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">Alternate layout</attribute>
<attribute name="action">app.set_alternate_layout</attribute>
</item>
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">Tools</attribute>
<attribute name="action">app.hide_menu_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<section>
<item>
<attribute name="label" translatable="yes">Backups</attribute>
<attribute name="action">app.on_backup_tool_show</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Telnet</attribute>
<attribute name="action">app.on_telnet_show</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Logs</attribute>
<attribute name="action">app.on_logs_show</attribute>
</item>
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">FTP client</attribute>
<attribute name="action">app.show_ftp_menu</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<item>
<attribute name="label" translatable="yes">Close</attribute>
<attribute name="action">app.on_ftp_client_close</attribute>
</item>
</submenu>
</menu>
<menu id="iptv_menu">
<item>
<attribute name="label" translatable="yes">Add IPTV or stream service</attribute>
<attribute name="action">app.on_iptv</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Import YouTube playlist</attribute>
<attribute name="action">app.on_import_yt_list</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Import m3u</attribute>
<attribute name="action">app.on_import_m3u</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Export to m3u</attribute>
<attribute name="action">app.on_export_iptv_to_m3u</attribute>
</item>
<section>
<item>
<attribute name="label" translatable="yes">EPG configuration</attribute>
<attribute name="action">app.on_epg_list_configuration</attribute>
</item>
<item>
<attribute name="label" translatable="yes">List configuration</attribute>
<attribute name="action">app.on_iptv_list_configuration</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Remove all unavailable</attribute>
<attribute name="action">app.on_remove_all_unavailable</attribute>
</item>
</section>
</menu>
<menu id="audio_menu">
<submenu>
<attribute name="label" translatable="yes">Audio</attribute>
<attribute name="action">app.hide_media_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<section>
<submenu id="audio_track_menu">
<attribute name="label" translatable="yes">Audio Track</attribute>
</submenu>
</section>
</submenu>
</menu>
<menu id="video_menu">
<submenu>
<attribute name="label" translatable="yes">Video</attribute>
<attribute name="action">app.hide_media_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<section>
<submenu id="aspect_ratio_menu">
<attribute name="label" translatable="yes">Aspect ratio</attribute>
<item>
<attribute name="label" translatable="yes">Default</attribute>
<attribute name="action">app.on_set_aspect_ratio</attribute>
<attribute name="target"/>
</item>
<item>
<attribute name="label" translatable="yes">16:9</attribute>
<attribute name="action">app.on_set_aspect_ratio</attribute>
<attribute name="target">16:9</attribute>
</item>
<item>
<attribute name="label" translatable="yes">4:3</attribute>
<attribute name="action">app.on_set_aspect_ratio</attribute>
<attribute name="target">4:3</attribute>
</item>
<item>
<attribute name="label" translatable="yes">1:1</attribute>
<attribute name="action">app.on_set_aspect_ratio</attribute>
<attribute name="target">1:1</attribute>
</item>
<item>
<attribute name="label" translatable="yes">16:10</attribute>
<attribute name="action">app.on_set_aspect_ratio</attribute>
<attribute name="target">16:10</attribute>
</item>
<item>
<attribute name="label" translatable="yes">5:4</attribute>
<attribute name="action">app.on_set_aspect_ratio</attribute>
<attribute name="target">5:4</attribute>
</item>
</submenu>
</section>
</submenu>
</menu>
<menu id="subtitle_menu">
<submenu>
<attribute name="label" translatable="yes">Subtitle</attribute>
<attribute name="action">app.hide_media_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<section>
<submenu id="subtitle_track_menu">
<attribute name="label" translatable="yes">Subtitle Track</attribute>
<item>
<attribute name="label" translatable="yes">Default</attribute>
<attribute name="action">app.on_set_subtitle_track</attribute>
<attribute name="target"/>
</item>
</submenu>
</section>
</submenu>
</menu>
</interface>

View File

@@ -1,3 +1,31 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
import os
import shutil
import tempfile
@@ -7,10 +35,10 @@ from datetime import datetime
from enum import Enum
from app.commons import run_idle
from app.settings import SettingsType
from app.ui.dialogs import show_dialog, DialogType
from app.settings import SettingsType, SEP
from app.ui.dialogs import show_dialog, DialogType, get_builder
from app.ui.main_helper import append_text_to_tview
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, IS_GNOME_SESSION
class RestoreType(Enum):
@@ -30,15 +58,12 @@ class BackupDialog:
"on_resize": self.on_resize,
"on_key_release": self.on_key_release}
builder = Gtk.Builder()
builder.set_translation_domain("demon-editor")
builder.add_from_file(UI_RESOURCES_PATH + "backup_dialog.glade")
builder.connect_signals(handlers)
builder = get_builder(UI_RESOURCES_PATH + "backup_dialog.glade", handlers)
self._settings = settings
self._s_type = settings.setting_type
self._data_path = self._settings.data_local_path
self._backup_path = self._settings.backup_local_path or self._data_path + "backup/"
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._open_data_callback = callback
self._dialog_window = builder.get_object("dialog_window")
self._dialog_window.set_transient_for(transient)
@@ -49,6 +74,24 @@ 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")
if IS_GNOME_SESSION:
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True)
self._dialog_window.set_titlebar(header_bar)
button_box = builder.get_object("main_button_box")
button_box.set_margin_top(0)
button_box.set_margin_bottom(0)
button_box.set_margin_left(0)
button_box.reparent(header_bar)
ch_button = builder.get_object("info_check_button")
ch_button.set_margin_right(0)
h_bar = builder.get_object("header_bar")
h_bar.remove(ch_button)
h_bar.set_visible(False)
header_bar.pack_end(ch_button)
# Setting the last size of the dialog window if it was saved
window_size = self._settings.get("backup_tool_window_size")
if window_size:
@@ -152,7 +195,7 @@ class BackupDialog:
clear_data_path(self._data_path)
shutil.unpack_archive(full_file_name, self._data_path)
elif restore_type is RestoreType.BOUQUETS:
tmp_dir = tempfile.gettempdir() + "/" + file_name
tmp_dir = tempfile.gettempdir() + SEP + file_name
cond = (".tv", ".radio") if self._s_type is SettingsType.ENIGMA_2 else "bouquets.xml"
shutil.unpack_archive(full_file_name, tmp_dir)
for file in filter(lambda f: f.endswith(cond), os.listdir(self._data_path)):
@@ -176,7 +219,7 @@ class BackupDialog:
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
ctrl = event.state & MOD_MASK
if key is KeyboardKey.DELETE:
self.on_remove(view)
@@ -191,7 +234,7 @@ 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"))
backup_path = "{}{}{}".format(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)

View File

@@ -33,6 +33,12 @@ Author: Dmitriy Yefremov
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
<!-- interface-copyright 2018-2020 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>
<property name="icon_size">1</property>
</object>
<object class="GtkListStore" id="main_list_store">
<columns>
<!-- column-name date -->
@@ -41,41 +47,147 @@ Author: Dmitriy Yefremov
<column type="gboolean"/>
</columns>
</object>
<object class="GtkMenu" id="popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="restore_bouquets_popup_menu_item">
<property name="label" translatable="yes">Restore bouquets</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_restore_bouquets" swapped="no"/>
<accelerator key="r" signal="activate" modifiers="Primary"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="restore_all_popup_menu_item">
<property name="label" translatable="yes">Restore all</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_restore_all" swapped="no"/>
<accelerator key="e" signal="activate" modifiers="Primary"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="popup_menu_separator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="remove_popup_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_remove" swapped="no"/>
<accelerator key="Delete" signal="activate"/>
</object>
</child>
</object>
<object class="GtkImage" id="remove_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">user-trash-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="restore_all_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-select-all-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="restore_bouquets_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-revert-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkWindow" id="dialog_window">
<property name="width_request">560</property>
<property name="height_request">320</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Backups</property>
<property name="modal">True</property>
<property name="window_position">center-on-parent</property>
<property name="destroy_with_parent">True</property>
<property name="icon_name">document-revert</property>
<property name="gravity">center</property>
<signal name="check-resize" handler="on_resize" swapped="no"/>
<child type="titlebar">
<object class="GtkHeaderBar" id="header_bar">
<child>
<placeholder/>
</child>
<child>
<object class="GtkBox" id="main_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Backups</property>
<property name="spacing">2</property>
<property name="show_close_button">True</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="header_box">
<object class="GtkBox" id="header_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkButton" id="restore_bouquets_header_button">
<object class="GtkButtonBox" id="main_button_box">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Restore bouquets</property>
<signal name="clicked" handler="on_restore_bouquets" swapped="no"/>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="margin_left">15</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="layout_style">expand</property>
<child>
<object class="GtkImage" id="restore_bouquets_header_button_image">
<object class="GtkButton" id="restore_bouquets_header_button">
<property name="label" translatable="yes">Restore bouquets</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-revert</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
<property name="image">restore_bouquets_image</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_restore_bouquets" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="restore_all_header_button">
<property name="label" translatable="yes">Restore all</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
<property name="image">restore_all_image</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_restore_all" 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="remove_header_button">
<property name="label" translatable="yes">Remove</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
<property name="image">remove_image</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_remove" swapped="no"/>
<accelerator key="Delete" signal="clicked"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
@@ -85,93 +197,44 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkButton" id="restore_all_header_button">
<object class="GtkCheckButton" id="info_check_button">
<property name="label" translatable="yes">Details</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Restore all</property>
<signal name="clicked" handler="on_restore_all" swapped="no"/>
<child>
<object class="GtkImage" id="restore_all_header_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-select-all</property>
</object>
</child>
<property name="can_focus">False</property>
<property name="receives_default">False</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"/>
<accelerator key="i" signal="clicked" modifiers="Primary"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="remove_header_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Remove</property>
<signal name="clicked" handler="on_remove" swapped="no"/>
<child>
<object class="GtkImage" id="remove_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">user-trash</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkCheckButton" id="info_check_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Details</property>
<property name="draw_indicator">False</property>
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
<child>
<object class="GtkImage" id="info_check_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-dialog-info</property>
</object>
</child>
<accelerator key="i" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
<style>
<class name="primary-toolbar"/>
</style>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="main_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkPaned" id="main_paned">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_top">5</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>
<child>
<object class="GtkScrolledWindow" id="main_view_scrolled_window">
@@ -248,7 +311,7 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
<property name="position">1</property>
</packing>
</child>
<child>
@@ -276,6 +339,7 @@ Author: Dmitriy Yefremov
<object class="GtkLabel" id="message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="label" translatable="yes">message</property>
</object>
<packing>
@@ -301,57 +365,4 @@ Author: Dmitriy Yefremov
</object>
</child>
</object>
<object class="GtkImage" id="restore_popup_menu_item_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-revert-to-saved</property>
</object>
<object class="GtkImage" id="restore_popup_menu_item_image2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-select-all</property>
</object>
<object class="GtkMenu" id="popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="restore_bouquets_popup_menu_item">
<property name="label" translatable="yes">Restore bouquets</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">restore_popup_menu_item_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_restore_bouquets" swapped="no"/>
<accelerator key="r" signal="activate" modifiers="GDK_CONTROL_MASK"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="restore_all_popup_menu_item">
<property name="label" translatable="yes">Restore all</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">restore_popup_menu_item_image2</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_restore_all" swapped="no"/>
<accelerator key="e" signal="activate" modifiers="GDK_CONTROL_MASK"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="popup_menu_separator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="remove_popup_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_remove" swapped="no"/>
<accelerator key="Delete" signal="activate"/>
</object>
</child>
</object>
</interface>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
The MIT License (MIT)
Copyright (c) 2018-2020 Dmitriy Yefremov
Copyright (c) 2018-2022 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -30,8 +30,8 @@ Author: Dmitriy Yefremov
<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-2020 Dmitriy Yefremov -->
<!-- interface-description Enigma2 channel and satellites list editor. -->
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkAboutDialog" id="about_dialog">
<property name="can_focus">False</property>
@@ -40,10 +40,10 @@ Author: Dmitriy Yefremov
<property name="icon_name">system-help</property>
<property name="type_hint">normal</property>
<property name="program_name">DemonEditor</property>
<property name="version">1.0.4 Beta</property>
<property name="copyright">2018-2020 Dmitriy Yefremov
<property name="version">2.2.2 Beta</property>
<property name="copyright">2018-2022 Dmitriy Yefremov
</property>
<property name="comments" translatable="yes">Enigma2 channel and satellite list editor for GNU/Linux.</property>
<property name="comments" translatable="yes">Enigma2 channel and satellite list editor.</property>
<property name="website">https://dyefremov.github.io/DemonEditor/</property>
<property name="license" translatable="yes">Это приложение распространяется без каких-либо гарантий.
Подробнее в &lt;a href="http://opensource.org/licenses/mit-license.php"&gt;The MIT License (MIT)&lt;/a&gt;.</property>
@@ -65,7 +65,9 @@ Author: Dmitriy Yefremov
<child internal-child="action_area">
<object class="GtkButtonBox" id="aboutdialog_action_area">
<property name="can_focus">False</property>
<property name="layout_style">end</property>
<property name="margin_left">50</property>
<property name="margin_right">50</property>
<property name="layout_style">expand</property>
</object>
<packing>
<property name="expand">False</property>
@@ -86,45 +88,55 @@ Author: Dmitriy Yefremov
<property name="window_position">center</property>
<property name="default_width">320</property>
<property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property>
<property name="type_hint">utility</property>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="gravity">center</property>
<child type="action">
<object class="GtkButton" id="input_dialog_cancel_button">
<property name="label">gtk-cancel</property>
<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="use_stock">True</property>
<property name="always_show_image">True</property>
<property name="valign">center</property>
</object>
</child>
<child type="action">
<object class="GtkButton" id="input_dialog_ok_button">
<property name="label">gtk-ok</property>
<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="use_stock">True</property>
<property name="always_show_image">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="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>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<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="primary_icon_stock">gtk-edit</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>
<property name="secondary_icon_sensitive">False</property>
@@ -132,7 +144,7 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
<property name="position">0</property>
</packing>
</child>
</object>
@@ -142,7 +154,7 @@ Author: Dmitriy Yefremov
<action-widget response="ok">input_dialog_ok_button</action-widget>
</action-widgets>
</object>
<object class="GtkDialog" id="wait_dialog">
<object class="GtkWindow" id="wait_dialog">
<property name="can_focus">False</property>
<property name="resizable">False</property>
<property name="modal">True</property>
@@ -153,68 +165,43 @@ Author: Dmitriy Yefremov
<property name="skip_pager_hint">True</property>
<property name="decorated">False</property>
<child>
<placeholder/>
</child>
<child internal-child="vbox">
<object class="GtkBox" id="wait_dialog_vbox">
<property name="width_request">120</property>
<object class="GtkBox" id="wait_dialog_box">
<property name="width_request">100</property>
<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="orientation">vertical</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="dialog-action_area4">
<child>
<object class="GtkSpinner" id="spinner">
<property name="width_request">150</property>
<property name="height_request">45</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="opacity">0</property>
<property name="layout_style">end</property>
<property name="active">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="box4">
<object class="GtkLabel" id="wait_dialog_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">2</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkSpinner" id="spinner">
<property name="width_request">150</property>
<property name="height_request">45</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="active">True</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="wait_dialog_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="label" translatable="yes">Loading data...</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="label" translatable="yes">Loading data...</property>
</object>
<packing>
<property name="expand">True</property>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</child> <!-- NOP -->
<style>
<class name="app-notification"/>
</style>

View File

@@ -1,10 +1,40 @@
# -*- 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 module for showing dialogs """
import locale
import gettext
import xml.etree.ElementTree as ET
from enum import Enum
from functools import lru_cache
from pathlib import Path
from app.commons import run_idle
from app.settings import SEP, IS_WIN
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN, IS_GNOME_SESSION
@@ -17,12 +47,11 @@ class Dialog(Enum):
<property name="use-header-bar">{use_header}</property>
<property name="can_focus">False</property>
<property name="modal">True</property>
<property name="default_width">320</property>
<property name="width_request">250</property>
<property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="gravity">center</property>
<property name="message_type">{message_type}</property>
<property name="buttons">{buttons_type}</property>
</object>
@@ -89,11 +118,12 @@ def show_dialog(dialog_type, transient, text=None, settings=None, action_type=No
return get_about_dialog(transient)
def get_chooser_dialog(transient, settings, name, patterns, title=None):
file_filter = Gtk.FileFilter()
file_filter.set_name(name)
for p in patterns:
file_filter.add_pattern(p)
def get_chooser_dialog(transient, settings, name, patterns, title=None, file_filter=None):
if not file_filter:
file_filter = Gtk.FileFilter()
file_filter.set_name(name)
for p in patterns:
file_filter.add_pattern(p)
return show_dialog(dialog_type=DialogType.CHOOSER,
transient=transient,
@@ -104,23 +134,21 @@ def get_chooser_dialog(transient, settings, name, patterns, title=None):
def get_file_chooser_dialog(transient, text, settings, action_type, file_filter, buttons=None, title=None, dirs=False):
text = get_message(text) if text else ""
action_type = Gtk.FileChooserAction.SELECT_FOLDER if action_type is None else action_type
buttons = buttons or (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
dialog = Gtk.FileChooserDialog(text, transient, action_type, buttons, use_header_bar=IS_GNOME_SESSION)
dialog.set_title(get_message(title) if title else "")
dialog = Gtk.FileChooserNative.new(get_message(title) if title else "", transient, action_type)
dialog.set_create_folders(dirs)
dialog.set_modal(True)
if file_filter is not None:
dialog.add_filter(file_filter)
dialog.set_current_folder(settings.data_local_path)
dialog.set_current_folder(settings.profile_data_path)
response = dialog.run()
if response not in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
if response == Gtk.ResponseType.ACCEPT:
path = Path(dialog.get_filename() or dialog.get_current_folder())
if path.is_dir():
response = "{}/".format(path.resolve())
response = "{}{}".format(path.resolve(), SEP)
elif path.is_file():
response = str(path.resolve())
dialog.destroy()
@@ -176,13 +204,53 @@ def get_dialog_from_xml(dialog_type, transient, use_header=0, title=""):
def get_message(message):
""" returns translated message """
return locale.dgettext(TEXT_DOMAIN, message)
return gettext.dgettext(TEXT_DOMAIN, message)
@lru_cache(maxsize=5)
def get_dialogs_string(path):
with open(path, "r") as f:
return "".join(f)
def get_dialogs_string(path, tag="property"):
if IS_WIN:
return translate_xml(path, tag)
else:
with open(path, "r", encoding="utf-8") as f:
return "".join(f)
def get_builder(path, handlers=None, use_str=False, objects=None, tag="property"):
""" Creates and returns a Gtk.Builder instance. """
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
if use_str:
if objects:
builder.add_objects_from_string(get_dialogs_string(path, tag).format(use_header=IS_GNOME_SESSION), objects)
else:
builder.add_from_string(get_dialogs_string(path, tag).format(use_header=IS_GNOME_SESSION))
else:
if objects:
builder.add_objects_from_string(get_dialogs_string(path, tag), objects)
else:
builder.add_from_string(get_dialogs_string(path, tag))
builder.connect_signals(handlers or {})
return builder
def translate_xml(path, tag="property"):
""" 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():
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")
if __name__ == "__main__":

View File

@@ -3,7 +3,7 @@
The MIT License (MIT)
Copyright (c) 2018 Dmitriy Yefremov
Copyright (c) 2018-2021 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -30,119 +30,94 @@ Author: Dmitriy Yefremov
<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-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</property>
<property name="icon_name">mail-send-receive-symbolic</property>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="gravity">center</property>
<child type="titlebar">
<object class="GtkHeaderBar" id="header_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">FTP-transfer</property>
<property name="spacing">5</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkBox" id="header_left_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkButton" id="receive_button">
<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>
<signal name="clicked" handler="on_receive" swapped="no"/>
<child>
<object class="GtkImage" id="receive_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-goto-bottom</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="send_button">
<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>
<signal name="clicked" handler="on_send" 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-goto-top</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="options_button">
<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"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-properties</property>
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<placeholder/>
</child>
<child>
<object class="GtkBox" id="main_dialog_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="margin_bottom">1</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkBox" id="selection_data_box">
<object class="GtkBox" id="header_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>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="label10">
<object class="GtkBox" id="profile_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</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>
@@ -151,13 +126,84 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkRadioButton" id="all_radio_button">
<property name="label" translatable="yes">All</property>
<object class="GtkBox" id="selection_data_box">
<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>
<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>
@@ -165,50 +211,9 @@ Author: Dmitriy Yefremov
<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>
<style>
<class name="primary-toolbar"/>
</style>
</object>
<packing>
<property name="expand">False</property>
@@ -216,72 +221,31 @@ Author: Dmitriy Yefremov
<property name="position">0</property>
</packing>
</child>
<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>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</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">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</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">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</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>
<property name="spacing">10</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="row_spacing">5</property>
<property name="column_spacing">10</property>
<property name="column_homogeneous">True</property>
<child>
<object class="GtkLabel" id="ip_label">
@@ -347,9 +311,8 @@ Author: Dmitriy Yefremov
<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>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<child>
<object class="GtkCheckButton" id="remove_unused_check_button">
<property name="label" translatable="yes">Remove unused bouquets</property>
@@ -421,54 +384,139 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkExpander" id="expander">
<object class="GtkButtonBox" id="button_box">
<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>
<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="GtkScrolledWindow" id="scrolled_window">
<property name="height_request">120</property>
<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="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>
<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 type="label">
<object class="GtkLabel" id="expander_label">
<child>
<object class="GtkButton" id="send_button">
<property name="label" translatable="yes">Send</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Extra:</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">2</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="log_bar_frame">
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkInfoBar" id="log_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" id="log_bar_button_box">
<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="log_bar_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="height_request">100</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>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</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">4</property>
<property name="position">3</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">
@@ -516,7 +564,7 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">5</property>
<property name="position">8</property>
</packing>
</child>
</object>

View File

@@ -1,3 +1,31 @@
# -*- 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
@@ -7,8 +35,8 @@ from app.connections import download_data, DownloadType, upload_data
from app.settings import SettingsType
from app.ui.backup import backup_data, restore_data
from app.ui.main_helper import append_text_to_tview
from app.ui.settings_dialog import show_settings_dialog
from .dialogs import show_dialog, DialogType, get_message
from app.ui.settings_dialog import SettingsDialog
from .dialogs import show_dialog, DialogType, get_message, get_builder
from .uicommons import Gtk, UI_RESOURCES_PATH
@@ -27,16 +55,10 @@ class DownloadDialog:
"on_remove_unused_bouquets_toggled": self.on_remove_unused_bouquets_toggled,
"on_info_bar_close": self.on_info_bar_close}
builder = Gtk.Builder()
builder.add_from_file(UI_RESOURCES_PATH + "download_dialog.glade")
builder.connect_signals(handlers)
builder = get_builder(UI_RESOURCES_PATH + "download_dialog.glade", handlers)
self._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")
@@ -46,8 +68,14 @@ class DownloadDialog:
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._use_http_box = builder.get_object("use_http_box")
self._profile_combo_box = builder.get_object("profile_combo_box")
# Info.
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._log_bar = builder.get_object("log_bar")
self._log_bar.bind_property("visible", builder.get_object("log_bar_frame"), "visible")
self._log_bar.connect("response", lambda b, r: b.set_visible(False))
self.init_settings()
@@ -60,11 +88,10 @@ class DownloadDialog:
def init_ui_settings(self):
self._host_entry.set_text(self._settings.host)
self._data_path_entry.set_text(self._settings.data_local_path)
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_box.set_visible(is_enigma)
self._use_http_switch.set_active(is_enigma and self._settings.use_http)
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):
@@ -96,8 +123,9 @@ class DownloadDialog:
self._dialog_window.destroy()
def on_settings(self, item):
response = show_settings_dialog(self._dialog_window, self._settings)
if response != Gtk.ResponseType.CANCEL:
dialog = SettingsDialog(self._dialog_window, self._settings)
dialog.show()
if dialog.is_updated():
self._s_type = self._settings.setting_type
self.update_profiles()
gen = self._update_settings_callback()
@@ -123,16 +151,16 @@ class DownloadDialog:
@run_task
def download(self, download, d_type):
""" Download/upload data from/to receiver """
GLib.idle_add(self._expander.set_expanded, True)
GLib.idle_add(self._log_bar.set_visible, 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.data_local_path or self._data_path_entry.get_text()
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.backup_local_path or data_path + "backup/"
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)

File diff suppressed because it is too large Load Diff

View File

@@ -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 gzip
import locale
import os
@@ -9,13 +37,14 @@ from urllib.error import HTTPError, URLError
from gi.repository import GLib
from app.commons import run_idle
from app.commons import run_idle, run_task, run_with_delay
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
from app.ui.dialogs import get_message, show_dialog, DialogType, get_builder
from .main_helper import on_popup_menu, update_entry_data
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, Column, EPG_ICON, KeyboardKey
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column, EPG_ICON, KeyboardKey, IS_GNOME_SESSION
class RefsSource(Enum):
@@ -25,7 +54,7 @@ class RefsSource(Enum):
class EpgDialog:
def __init__(self, transient, settings, services, bouquet, fav_model, bouquet_name):
def __init__(self, app, bouquet, bouquet_name):
handlers = {"on_close_dialog": self.on_close_dialog,
"on_apply": self.on_apply,
@@ -51,12 +80,14 @@ class EpgDialog:
"on_enable_filtering_switch": self.on_enable_filtering_switch,
"on_update_on_start_switch": self.on_update_on_start_switch,
"on_field_icon_press": self.on_field_icon_press,
"on_key_release": self.on_key_release}
"on_key_press": self.on_key_press,
"on_bq_cursor_changed": self.on_bq_cursor_changed}
self._app = app
self._services = {}
self._ex_services = services
self._ex_fav_model = fav_model
self._settings = settings
self._ex_services = self._app.current_services
self._ex_fav_model = self._app.fav_view.get_model()
self._settings = self._app.app_settings
self._bouquet = bouquet
self._bouquet_name = bouquet_name
self._current_ref = []
@@ -64,16 +95,12 @@ class EpgDialog:
self._use_web_source = False
self._update_epg_data_on_start = False
self._refs_source = RefsSource.SERVICES
self._show_tooltips = True
self._download_xml_is_active = False
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_from_file(UI_RESOURCES_PATH + "epg_dialog.glade")
builder.connect_signals(handlers)
builder = get_builder(UI_RESOURCES_PATH + "epg.glade", handlers)
self._dialog = builder.get_object("epg_dialog_window")
self._dialog.set_transient_for(transient)
self._dialog.set_transient_for(self._app.app_window)
self._source_view = builder.get_object("source_view")
self._bouquet_view = builder.get_object("bouquet_view")
self._bouquet_model = builder.get_object("bouquet_list_store")
@@ -86,6 +113,7 @@ class EpgDialog:
# Filter
self._filter_bar = builder.get_object("filter_bar")
self._filter_entry = builder.get_object("filter_entry")
self._filter_auto_switch = builder.get_object("filter_auto_switch")
self._services_filter_model = builder.get_object("services_filter_model")
self._services_filter_model.set_visible_func(self.services_filter_function)
# Info
@@ -105,6 +133,19 @@ class EpgDialog:
self._epg_dat_stb_path_entry = builder.get_object("epg_dat_stb_path_entry")
self._update_on_start_switch = builder.get_object("update_on_start_switch")
self._epg_dat_source_box = builder.get_object("epg_dat_source_box")
if IS_GNOME_SESSION:
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True, title="EPG",
subtitle=get_message("List configuration"))
self._dialog.set_titlebar(header_bar)
builder.get_object("left_action_box").reparent(header_bar)
right_box = builder.get_object("right_action_box")
builder.get_object("main_actions_box").remove(right_box)
header_bar.pack_end(right_box)
builder.get_object("toolbar_box").set_visible(False)
self._app.connect("epg-dat-downloaded", self.on_epg_dat_downloaded)
# Setting the last size of the dialog window
window_size = self._settings.get("epg_tool_window_size")
if window_size:
@@ -140,8 +181,11 @@ class EpgDialog:
def on_update(self, item=None):
self.clear_data()
self.init_options()
gen = self.init_data()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
if self._update_epg_data_on_start:
self.download_epg_from_stb()
else:
gen = self.init_data()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def clear_data(self):
self._services_model.clear()
@@ -157,23 +201,15 @@ class EpgDialog:
refs = None
if self._enable_dat_filter:
if self._update_epg_data_on_start:
try:
self.download_epg_from_stb()
except OSError as e:
self.show_info_message("Download epg.dat file error: {}".format(e), Gtk.MessageType.ERROR)
return
yield True
try:
refs = EPG.get_epg_refs(self._epg_dat_path_entry.get_text() + "epg.dat")
except FileNotFoundError as e:
self.show_info_message("Read data error: {}".format(e), Gtk.MessageType.ERROR)
except (OSError, ValueError) as e:
self.show_info_message(f"Read data error: {e}", Gtk.MessageType.ERROR)
return
yield True
if self._refs_source is RefsSource.SERVICES:
self.init_lamedb_source(refs)
yield from self.init_lamedb_source(refs)
elif self._refs_source is RefsSource.XML:
xml_gen = self.init_xml_source(refs)
try:
@@ -198,8 +234,15 @@ class EpgDialog:
s_types = (BqServiceType.MARKER.value, BqServiceType.IPTV.value)
filtered = filter(None, [srvs.get(ref) for ref in refs]) if refs else filter(
lambda s: s.service_type not in s_types, self._ex_services.values())
list(map(self._services_model.append, map(lambda s: (s.service, s.fav_id), filtered)))
factor = self._app.DEL_FACTOR / 4
for index, srv in enumerate(filtered):
self._services_model.append((srv.service, srv.pos, srv.fav_id))
if index % factor == 0:
yield True
self.update_source_count_info()
yield True
def init_xml_source(self, refs):
path = self._epg_dat_path_entry.get_text() if self._use_web_source else self._xml_chooser_button.get_filename()
@@ -247,7 +290,7 @@ class EpgDialog:
path = tfp.name.rstrip(".gz")
except (HTTPError, URLError) as e:
raise ValueError("{} {}".format(get_message("Download XML file error."), e))
raise ValueError(f"{get_message('Download XML file error.')} {e}")
else:
try:
with open(path, "wb") as f_out:
@@ -255,7 +298,7 @@ class EpgDialog:
shutil.copyfileobj(f, f_out)
os.remove(tfp.name)
except Exception as e:
raise ValueError("{} {}".format(get_message("Unpacking data error."), e))
raise ValueError(f"{get_message('Unpacking data error.')} {e}")
finally:
self._download_xml_is_active = False
self.update_active_header_elements(True)
@@ -264,16 +307,22 @@ class EpgDialog:
s_refs, info = ChannelsParser.get_refs_from_xml(path)
yield True
except Exception as e:
raise ValueError("{} {}".format(get_message("XML parsing error:"), e))
raise ValueError(f"{get_message('XML parsing error:')} {e}")
else:
if refs:
s_refs = filter(lambda x: x.num in refs, s_refs)
list(map(lambda s: self._services_model.append((s.name, s.data)), s_refs))
factor = self._app.DEL_FACTOR / 4
for index, srv in enumerate(s_refs):
self._services_model.append((srv.name, " ", srv.data))
if index % factor == 0:
yield True
self.update_source_info(info)
self.update_source_count_info()
yield True
def on_key_release(self, view, event):
def on_key_press(self, view, event):
""" Handling keystrokes """
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
@@ -286,6 +335,13 @@ class EpgDialog:
elif ctrl and key is KeyboardKey.V:
self.on_assign_ref()
def on_bq_cursor_changed(self, view):
if self._filter_bar.get_visible() and self._filter_auto_switch.get_active():
path, column = view.get_cursor()
model = view.get_model()
if path:
self._filter_entry.set_text(model[path][Column.FAV_SERVICE] or "")
@run_idle
def on_save_to_xml(self, item):
response = show_dialog(DialogType.CHOOSER, self._dialog, settings=self._settings)
@@ -321,7 +377,7 @@ class EpgDialog:
for row in self._services_model:
name = re.sub("\\W+", "", str(row[0])).upper()
name = name.translate(tr) if use_cyrillic else name
source[name] = row[1]
source[name] = row
success_count = 0
not_founded = {}
@@ -351,7 +407,7 @@ class EpgDialog:
get_message("Count of successfully configured services:"),
success_count), Gtk.MessageType.INFO)
def assign_data(self, row, ref, show_error=False):
def assign_data(self, row, data, show_error=False):
if row[Column.FAV_TYPE] != BqServiceType.IPTV.value:
if not show_error:
self.show_info_message(get_message("Not allowed in this context!"), Gtk.MessageType.ERROR)
@@ -359,18 +415,23 @@ class EpgDialog:
fav_id = row[Column.FAV_ID]
fav_id_data = fav_id.split(":")
fav_id_data[3:7] = ref.split(":")
fav_id_data[3:7] = data[-1].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
pos = f"({data[1] if self._refs_source is RefsSource.SERVICES else 'XML'})"
src = f"{get_message('EPG source')}: {(GLib.markup_escape_text(data[0] or ''))} {pos}"
row[Column.FAV_TOOLTIP] = f"{get_message('Service reference')}: {':'.join(fav_id_data[:10])}\n{src}"
def on_filter_toggled(self, button: Gtk.ToggleButton):
self._filter_bar.set_search_mode(button.get_active())
def on_filter_toggled(self, button):
self._filter_bar.set_visible(button.get_active())
if not button.get_active():
self._filter_entry.set_text("")
@run_with_delay(1)
def on_filter_changed(self, entry):
self._services_filter_model.refilter()
@@ -385,7 +446,7 @@ class EpgDialog:
model, paths = self._source_view.get_selection().get_selected_rows()
self._current_ref.clear()
if paths:
self._current_ref.append(model[paths][1])
self._current_ref.append(model[paths][:])
def on_assign_ref(self, item=None):
if self._current_ref:
@@ -454,7 +515,7 @@ class EpgDialog:
# ***************** Drag-and-drop *********************#
def init_drag_and_drop(self):
""" Enable drag-and-drop """
""" Enable drag-and-drop. """
target = []
self._source_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, target, Gdk.DragAction.COPY)
self._source_view.drag_source_add_text_targets()
@@ -467,23 +528,28 @@ class EpgDialog:
if selection.count_selected_rows() > 1:
view.do_toggle_cursor_row(view)
def on_drag_data_get(self, view: Gtk.TreeView, drag_context, data, info, time):
def on_drag_data_get(self, view, drag_context, data, info, time):
model, paths = view.get_selection().get_selected_rows()
if paths:
val = model.get_value(model.get_iter(paths), 1)
data.set_text(val, -1)
s_data = model[paths][:]
if all(s_data):
data.set_text("::::".join(s_data), -1)
else:
self.show_info_message(get_message("Source error!"), Gtk.MessageType.ERROR)
def on_drag_data_received(self, view: Gtk.TreeView, drag_context, x, y, data, info, time):
def on_drag_data_received(self, view, drag_context, x, y, data, info, time):
path, pos = view.get_dest_row_at_pos(x, y)
model = view.get_model()
self.assign_data(model[path], data.get_text())
self.update_epg_count()
data = data.get_text()
if data:
self.assign_data(model[path], data.split("::::"))
self.update_epg_count()
return False
# ***************** Options *********************#
def init_options(self):
epg_dat_path = self._settings.data_local_path + "epg/"
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
@@ -539,9 +605,19 @@ class EpgDialog:
# ***************** Downloads *********************#
def on_epg_dat_downloaded(self, app, value):
gen = self.init_data()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
@run_task
def download_epg_from_stb(self):
""" Download the epg.dat file via ftp from the receiver. """
download_data(settings=self._settings, download_type=DownloadType.EPG, callback=print)
try:
download_data(settings=self._settings, download_type=DownloadType.EPG, callback=print)
except Exception as e:
GLib.idle_add(self.show_info_message, f"Download epg.dat file error: {e}", Gtk.MessageType.ERROR)
else:
GLib.idle_add(self._app.emit, "epg-dat-downloaded", None)
if __name__ == "__main__":

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,38 @@
# -*- 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
#
""" Simple FTP client module. """
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
@@ -12,9 +41,10 @@ from gi.repository import GLib
from app.commons import log, run_task, run_idle
from app.connections import UtfFTP
from app.ui.dialogs import show_dialog, DialogType
from app.settings import IS_LINUX, IS_DARWIN, IS_WIN, SEP
from app.ui.dialogs import show_dialog, DialogType, get_builder
from app.ui.main_helper import on_popup_menu
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, IS_GNOME_SESSION
File = namedtuple("File", ["icon", "name", "size", "date", "attr", "extra"])
@@ -35,6 +65,86 @@ class FtpClientBox(Gtk.HBox):
ATTR = 4
EXTRA = 5
class TextEditDialog(Gtk.Dialog):
""" 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)
self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK, )
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)
def __init__(self, app, settings, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_spacing(2)
@@ -49,12 +159,17 @@ 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_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,
@@ -63,16 +178,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 = Gtk.Builder()
builder.add_from_file(UI_RESOURCES_PATH + "ftp.glade")
builder.connect_signals(handlers)
builder = get_builder(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")
@@ -80,13 +195,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)
@@ -104,6 +226,7 @@ class FtpClientBox(Gtk.HBox):
@run_task
def init_ftp(self):
self.init_bookmarks()
GLib.idle_add(self._ftp_model.clear)
try:
if self._ftp:
@@ -142,7 +265,7 @@ class FtpClientBox(Gtk.HBox):
@run_task
def init_file_data(self, path=None):
self.append_file_data(Path(path if path else self._settings.data_local_path))
self.append_file_data(Path(path if path else self._settings.profile_data_path))
@run_idle
def append_file_data(self, path: Path):
@@ -177,7 +300,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"
@@ -193,8 +316,8 @@ class FtpClientBox(Gtk.HBox):
else:
r_size = self.get_size_from_bytes(size)
date = "{}, {} {}".format(f_data[5], f_data[6], f_data[7])
self._ftp_model.append(File(icon, " ".join(f_data[8:]), r_size, date, f_data[0], size))
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()
@@ -212,10 +335,14 @@ 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:
self._app.show_error_dialog("The file size is too large!")
self._app.show_error_message("The file size is too large!")
else:
self.open_ftp_file(f_path)
@@ -233,23 +360,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))
@@ -257,24 +383,84 @@ 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 = self.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
if len(paths) > 1:
self._app.show_error_dialog("Please, select only one item!")
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]
@@ -282,20 +468,20 @@ 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_file_rename(self, renderer):
model, paths = self._file_view.get_selection().get_selected_rows()
if len(paths) > 1:
self._app.show_error_dialog("Please, select only one item!")
self._app.show_error_message("Please, select only one item!")
return
renderer.set_property("editable", True)
self._file_view.set_cursor(paths, self._file_view.get_column(0), True)
def on_file_edited(self, renderer, path, new_value):
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]
@@ -305,17 +491,25 @@ 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_dialog(str(e))
self._app.show_error_message(str(e))
else:
if new_path.name == new_value:
row[self.Column.NAME] = new_value
row[self.Column.ATTR] = str(new_path.resolve())
def on_file_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()
@@ -333,7 +527,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()
@@ -359,13 +553,13 @@ 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()
except OSError as e:
log(e)
self._app.show_error_dialog(str(e))
self._app.show_error_message(str(e))
else:
itr = self._file_model.append(File(self._folder_icon, path.name, self.FOLDER, "", str(path.resolve()), "0"))
renderer.set_property("editable", True)
@@ -380,13 +574,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)
@@ -398,7 +592,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 ********************* #
@@ -427,30 +621,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:
@@ -458,34 +665,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(":")
@@ -499,12 +712,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()
@@ -515,6 +725,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):
@@ -530,14 +761,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:
@@ -551,7 +796,13 @@ class FtpClientBox(Gtk.HBox):
# Enable selection.
self._select_enabled = True
def get_size_from_bytes(self, size):
@staticmethod
def on_paned_size_allocate(paned, allocation):
""" Sets default homogeneous sizes. """
paned.set_position(0.5 * allocation.width)
@staticmethod
def get_size_from_bytes(size):
""" Simple convert function from bytes to other units like K, M or G. """
try:
b = float(size)
@@ -563,11 +814,11 @@ class FtpClientBox(Gtk.HBox):
if b < kb:
return str(b)
elif kb <= b < mb:
return "{0:.1f} K".format(b / kb)
return f"{b / kb:.1f} K"
elif mb <= b < gb:
return "{0:.1f} M".format(b / mb)
return f"{b / mb:.1f} M"
elif gb <= b:
return "{0:.1f} G".format(b / gb)
return f"{b / gb:.1f} G"
if __name__ == '__main__':

View File

@@ -1,391 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1
The MIT License (MIT)
Copyright (c) 2018-2020 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface>
<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-2020 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkListStore" id="main_list_store">
<columns>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name type -->
<column type="gchararray"/>
<!-- column-name selected -->
<column type="gboolean"/>
</columns>
</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>
</object>
<object class="GtkMenu" id="popup_menu">
<property name="visible">True</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"/>
</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="image">remove_selection_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_unselect_all" object="main_view" swapped="no"/>
</object>
</child>
</object>
<object class="GtkListStore" id="services_list_store">
<columns>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name type -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkWindow" id="dialog_window">
<property name="can_focus">False</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="gravity">center</property>
<signal name="check-resize" handler="on_resize" swapped="no"/>
<child type="titlebar">
<object class="GtkHeaderBar" id="header_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Import</property>
<property name="subtitle" translatable="yes">Bouquets and services</property>
<property name="spacing">2</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkButton" id="import_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Import</property>
<signal name="clicked" handler="on_import" swapped="no"/>
<child>
<object class="GtkImage" id="import_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-revert-to-saved</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkCheckButton" id="info_check_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Details</property>
<property name="draw_indicator">False</property>
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
<child>
<object class="GtkImage" id="info_check_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-dialog-info</property>
</object>
</child>
<accelerator key="i" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="main_box">
<property name="width_request">480</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="margin_bottom">1</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkPaned" id="main_paned">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="wide_handle">True</property>
<child>
<object class="GtkBox" id="bouquets_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">2</property>
<property name="margin_bottom">2</property>
<property name="label" translatable="yes">Bouquets</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="bouquets_screlled_window">
<property name="width_request">200</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="main_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">main_list_store</property>
<property name="headers_clickable">False</property>
<property name="search_column">0</property>
<signal name="button-press-event" handler="on_popup_menu" object="popup_menu" swapped="no"/>
<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>
</object>
</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>
</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.50999999046325684</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">
<property name="xalign">0.50999999046325684</property>
<signal name="toggled" handler="on_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">2</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<object class="GtkBox" id="services_box">
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">2</property>
<property name="margin_bottom">2</property>
<property name="label" translatable="yes">Bouquet details</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<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>
<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>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<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.019999999552965164</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>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</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="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="message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">message</property>
<property name="wrap">True</property>
</object>
<packing>
<property name="expand">False</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="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

441
app/ui/imports.glade Normal file
View File

@@ -0,0 +1,441 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2
The MIT License (MIT)
Copyright (c) 2018-2020 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface domain="demon-editor">
<requires lib="gtk+" version="3.16"/>
<!-- interface-css-provider-path style.css -->
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellite list editor. -->
<!-- interface-copyright 2018-2021 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">
<columns>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name type -->
<column type="gchararray"/>
<!-- column-name selected -->
<column type="gboolean"/>
</columns>
</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>
</object>
<object class="GtkMenu" id="popup_menu">
<property name="visible">True</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"/>
</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="image">remove_selection_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_unselect_all" object="main_view" swapped="no"/>
</object>
</child>
</object>
<object class="GtkListStore" id="services_list_store">
<columns>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name type -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkWindow" id="dialog_window">
<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>
<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="visible">True</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>
<child>
<object class="GtkButtonBox" id="actions_box">
<property name="visible">True</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>
<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="image">import_image</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="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>
<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"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
<property name="secondary">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</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="GtkPaned" id="main_paned">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="wide_handle">True</property>
<child>
<object class="GtkFrame" id="bouquets_box_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0.49000000953674316</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkBox" id="bouquets_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="bouquets_screlled_window">
<property name="width_request">200</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="main_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">main_list_store</property>
<property name="headers_clickable">False</property>
<property name="search_column">0</property>
<signal name="button-press-event" handler="on_popup_menu" object="popup_menu" swapped="no"/>
<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>
</object>
</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>
</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.50999999046325684</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">
<property name="xalign">0.50999999046325684</property>
<signal name="toggled" handler="on_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>
</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>
<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>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<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>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<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">Bouquet details</property>
</object>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<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="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="message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">message</property>
<property name="wrap">True</property>
</object>
<packing>
<property name="expand">False</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="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View File

@@ -1,3 +1,31 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
from contextlib import suppress
from pathlib import Path
@@ -5,8 +33,8 @@ 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.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, get_message
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
@@ -19,8 +47,8 @@ def import_bouquet(transient, model, path, settings, services, appender, file_pa
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 +66,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,7 +87,7 @@ 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)
file_path = f"{Path(file_path).parent}{SEP}"
ImportDialog(transient, file_path, settings, services.keys(), lambda b, s: appender(b), (bqs,)).show()
@@ -71,7 +103,6 @@ class ImportDialog:
def __init__(self, transient, path, settings, service_ids, appender, bouquets=None):
handlers = {"on_import": self.on_import,
"on_cursor_changed": self.on_cursor_changed,
"on_info_button_toggled": self.on_info_button_toggled,
"on_selected_toggled": self.on_selected_toggled,
"on_info_bar_close": self.on_info_bar_close,
"on_select_all": self.on_select_all,
@@ -80,10 +111,7 @@ class ImportDialog:
"on_resize": self.on_resize,
"on_key_press": self.on_key_press}
builder = Gtk.Builder()
builder.set_translation_domain("demon-editor")
builder.add_from_file(UI_RESOURCES_PATH + "import_dialog.glade")
builder.connect_signals(handlers)
builder = get_builder(UI_RESOURCES_PATH + "imports.glade", handlers)
self._bq_services = {}
self._services = {}
@@ -99,8 +127,8 @@ class ImportDialog:
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._services_box = builder.get_object("services_box")
self._info_check_button = builder.get_object("info_check_button")
self._info_check_button.bind_property("active", builder.get_object("services_box_frame"), "visible")
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("message_label")
window_size = self._settings.get("import_dialog_window_size")
@@ -124,8 +152,15 @@ class ImportDialog:
for bq in bqs.bouquets:
self._main_model.append((bq.name, bq.type, True))
self._bq_services[(bq.name, bq.type)] = bq.services
# Note! Getting default format ver. 4
services = get_services(path, self._profile, 4 if self._profile is SettingsType.ENIGMA_2 else 0)
if self._profile is SettingsType.ENIGMA_2:
services = get_services(path, self._profile, 5 if self._settings.v5_support else 4)
elif self._profile is SettingsType.NEUTRINO_MP:
services = get_services(path, self._profile, 0)
else:
self.show_info_message("Setting format not supported!", Gtk.MessageType.ERROR)
return
for srv in services:
self._services[srv.fav_id] = srv
except FileNotFoundError as e:
@@ -192,10 +227,6 @@ class ImportDialog:
else:
self._services_model.append((bq_srv.name, bq_srv.type.value))
def on_info_button_toggled(self, button):
active = button.get_active()
self._services_box.set_visible(active)
def on_selected_toggled(self, toggle, path):
self._main_model.set_value(self._main_model.get_iter(path), 2, not toggle.get_active())

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,54 @@
# -*- 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 concurrent.futures
import os
import re
import urllib
from urllib.error import HTTPError
from urllib.parse import urlparse, unquote, quote
from urllib.request import Request, urlopen
from gi.repository import GLib
import requests
from gi.repository import GLib, Gio, GdkPixbuf
from app.commons import run_idle, run_task
from app.commons import run_idle, run_task, log
from app.eparser.ecommons import BqServiceType, Service
from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT, get_fav_id, MARKER_FORMAT
from app.eparser.iptv import (NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT, get_fav_id, MARKER_FORMAT,
parse_m3u)
from app.settings import SettingsType
from app.tools.yt import YouTubeException, YouTube
from .dialogs import Action, show_dialog, DialogType, get_dialogs_string, get_message
from .main_helper import get_base_model, get_iptv_url, on_popup_menu
from .uicommons import (Gtk, Gdk, TEXT_DOMAIN, UI_RESOURCES_PATH, IPTV_ICON, Column, IS_GNOME_SESSION, KeyboardKey,
get_yt_icon)
from app.ui.dialogs import Action, show_dialog, DialogType, get_message, get_builder
from app.ui.main_helper import get_iptv_url, on_popup_menu, get_picon_pixbuf
from app.ui.uicommons import (Gtk, Gdk, UI_RESOURCES_PATH, IPTV_ICON, Column, KeyboardKey, get_yt_icon)
_DIGIT_ENTRY_NAME = "digit-entry"
_ENIGMA2_REFERENCE = "{}:0:{}:{:X}:{:X}:{:X}:{:X}:0:0:0"
_ENIGMA2_REFERENCE = "{}:{}:{}:{:X}:{:X}:{:X}:{:X}:0:0:0"
_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
_UI_PATH = UI_RESOURCES_PATH + "iptv.glade"
@@ -47,7 +77,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,
@@ -56,27 +86,25 @@ 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
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("iptv_dialog", "stream_type_liststore", "yt_quality_liststore"))
builder.connect_signals(handlers)
builder = get_builder(_UI_PATH, handlers, use_str=True,
objects=("iptv_dialog", "stream_type_liststore", "yt_quality_liststore"))
self._dialog = builder.get_object("iptv_dialog")
self._dialog.set_transient_for(transient)
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")
self._reference_entry = builder.get_object("reference_entry")
self._srv_type_entry = builder.get_object("srv_type_entry")
self._srv_id_entry = builder.get_object("srv_id_entry")
self._sid_entry = builder.get_object("sid_entry")
self._tr_id_entry = builder.get_object("tr_id_entry")
self._net_id_entry = builder.get_object("net_id_entry")
@@ -92,8 +120,8 @@ class IptvDialog:
# style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._digit_elems = (self._srv_type_entry, self._sid_entry, self._tr_id_entry, self._net_id_entry,
self._namespace_entry)
self._digit_elems = (self._srv_id_entry, self._srv_type_entry, self._sid_entry, self._tr_id_entry,
self._net_id_entry, self._namespace_entry)
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)
@@ -114,7 +142,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):
@@ -139,8 +167,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):
@@ -168,6 +196,7 @@ class IptvDialog:
except ValueError:
self.show_info_message("Unknown stream type {}".format(s_type), Gtk.MessageType.ERROR)
self._srv_id_entry.set_text(data[1])
self._srv_type_entry.set_text(data[2])
self._sid_entry.set_text(str(int(data[3], 16)))
self._tr_id_entry.set_text(str(int(data[4], 16)))
@@ -185,6 +214,7 @@ class IptvDialog:
if self._s_type is SettingsType.ENIGMA_2 and is_data_correct(self._digit_elems):
self.on_url_changed(self._url_entry)
self._reference_entry.set_text(_ENIGMA2_REFERENCE.format(self.get_type(),
self._srv_id_entry.get_text(),
self._srv_type_entry.get_text(),
int(self._sid_entry.get_text()),
int(self._tr_id_entry.get_text()),
@@ -269,6 +299,7 @@ class IptvDialog:
def save_enigma2_data(self):
name = self._name_entry.get_text().strip()
fav_id = ENIGMA2_FAV_ID_FORMAT.format(self.get_type(),
self._srv_id_entry.get_text(),
self._srv_type_entry.get_text(),
int(self._sid_entry.get_text()),
int(self._tr_id_entry.get_text()),
@@ -276,11 +307,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()
@@ -289,20 +321,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", (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):
@@ -320,10 +357,8 @@ class SearchUnavailableDialog:
def __init__(self, transient, model, fav_bouquet, iptv_rows, s_type):
handlers = {"on_response": self.on_response}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "iptv.glade", ("search_unavailable_streams_dialog",))
builder.connect_signals(handlers)
builder = get_builder(UI_RESOURCES_PATH + "iptv.glade", handlers,
objects=("search_unavailable_streams_dialog",))
self._dialog = builder.get_object("search_unavailable_streams_dialog")
self._dialog.set_transient_for(transient)
@@ -344,9 +379,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()
@@ -355,13 +394,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)
@@ -399,13 +438,15 @@ class SearchUnavailableDialog:
self._dialog.destroy()
class IptvListConfigurationDialog:
class IptvListDialog:
""" Base class for working with iptv lists. """
def __init__(self, transient, services, iptv_rows, bouquet, fav_model, s_type):
def __init__(self, transient, s_type):
handlers = {"on_apply": self.on_apply,
"on_response": self.on_response,
"on_stream_type_default_togged": self.on_stream_type_default_togged,
"on_stream_type_changed": self.on_stream_type_changed,
"on_default_id_toggled": self.on_default_id_toggled,
"on_default_type_toggled": self.on_default_type_toggled,
"on_auto_sid_toggled": self.on_auto_sid_toggled,
"on_default_tid_toggled": self.on_default_tid_toggled,
@@ -415,50 +456,56 @@ class IptvListConfigurationDialog:
"on_entry_changed": self.on_entry_changed,
"on_info_bar_close": self.on_info_bar_close}
self._rows = iptv_rows
self._services = services
self._bouquet = bouquet
self._fav_model = fav_model
self._s_type = s_type
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("iptv_list_configuration_dialog", "stream_type_liststore"))
builder.connect_signals(handlers)
builder = get_builder(_UI_PATH, handlers, use_str=True,
objects=("iptv_list_configuration_dialog", "stream_type_liststore"))
self._dialog = builder.get_object("iptv_list_configuration_dialog")
self._dialog.set_transient_for(transient)
self._data_box = builder.get_object("iptv_list_data_box")
self._start_values_grid = builder.get_object("start_values_grid")
self._info_bar = builder.get_object("list_configuration_info_bar")
self._reference_label = builder.get_object("reference_label")
self._stream_type_check_button = builder.get_object("stream_type_default_check_button")
self._id_default_check_button = builder.get_object("id_default_check_button")
self._type_check_button = builder.get_object("type_default_check_button")
self._sid_auto_check_button = builder.get_object("sid_auto_check_button")
self._tid_check_button = builder.get_object("tid_default_check_button")
self._nid_check_button = builder.get_object("nid_default_check_button")
self._namespace_check_button = builder.get_object("namespace_default_check_button")
self._stream_type_combobox = builder.get_object("stream_type_list_combobox")
self._list_srv_id_entry = builder.get_object("list_srv_id_entry")
self._list_srv_type_entry = builder.get_object("list_srv_type_entry")
self._list_sid_entry = builder.get_object("list_sid_entry")
self._list_tid_entry = builder.get_object("list_tid_entry")
self._list_nid_entry = builder.get_object("list_nid_entry")
self._list_namespace_entry = builder.get_object("list_namespace_entry")
self._reset_to_default_switch = builder.get_object("reset_to_default_lists_switch")
# style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._digit_elems = (self._list_srv_type_entry, self._list_sid_entry, self._list_tid_entry,
self._list_nid_entry, self._list_namespace_entry)
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")
self._default_elems = (self._stream_type_check_button, self._id_default_check_button, self._type_check_button,
self._sid_auto_check_button, self._tid_check_button, self._nid_check_button,
self._namespace_check_button)
self._digit_elems = (self._list_srv_id_entry, self._list_srv_type_entry, self._list_sid_entry,
self._list_tid_entry, self._list_nid_entry, self._list_namespace_entry)
for el in self._digit_elems:
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
def show(self):
self._dialog.run()
def on_response(self, dialog, response):
if response == Gtk.ResponseType.CANCEL:
self._dialog.destroy()
if response == Gtk.ResponseType.APPLY:
return True
self._dialog.destroy()
def on_stream_type_changed(self, box):
self.update_reference()
@@ -468,88 +515,43 @@ class IptvListConfigurationDialog:
self._stream_type_combobox.set_active(1)
self._stream_type_combobox.set_sensitive(not button.get_active())
def on_default_id_toggled(self, button):
self.set_default(button, self._list_srv_id_entry, "0")
def on_default_type_toggled(self, button):
if button.get_active():
self._list_srv_type_entry.set_text("1")
self._list_srv_type_entry.set_sensitive(not button.get_active())
self.set_default(button, self._list_srv_type_entry, "1")
def on_auto_sid_toggled(self, button):
if button.get_active():
self._list_sid_entry.set_text("0")
self._list_sid_entry.set_sensitive(not button.get_active())
self.set_default(button, self._list_sid_entry, "0")
def on_default_tid_toggled(self, button):
if button.get_active():
self._list_tid_entry.set_text("0")
self._list_tid_entry.set_sensitive(not button.get_active())
self.set_default(button, self._list_tid_entry, "0")
def on_default_nid_toggled(self, button):
if button.get_active():
self._list_nid_entry.set_text("0")
self._list_nid_entry.set_sensitive(not button.get_active())
self.set_default(button, self._list_nid_entry, "0")
def on_default_namespace_toggled(self, button):
self.set_default(button, self._list_namespace_entry, "0")
def set_default(self, button, entry, value):
if button.get_active():
self._list_namespace_entry.set_text("0")
self._list_namespace_entry.set_sensitive(not button.get_active())
entry.set_text(value)
entry.set_sensitive(not button.get_active())
@run_idle
def on_reset_to_default(self, item, active):
item.set_sensitive(not active)
def on_reset_to_default(self, item):
self._stream_type_combobox.set_active(1)
self._list_srv_type_entry.set_text("1")
for el in (self._list_sid_entry, self._list_nid_entry, self._list_tid_entry, self._list_namespace_entry):
for el in self._digit_elems[1:]:
el.set_text("0")
for el in (self._stream_type_check_button, self._type_check_button, self._sid_auto_check_button,
self._tid_check_button, self._nid_check_button, self._namespace_check_button):
for el in self._default_elems:
el.set_active(True)
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@run_idle
def on_apply(self, item):
if not is_data_correct(self._digit_elems):
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
if self._s_type is SettingsType.ENIGMA_2:
reset = self._reset_to_default_switch.get_active()
type_default = self._type_check_button.get_active()
tid_default = self._tid_check_button.get_active()
sid_auto = self._sid_auto_check_button.get_active()
nid_default = self._nid_check_button.get_active()
namespace_default = self._namespace_check_button.get_active()
stream_type = StreamType.NONE_TS.value if reset else get_stream_type(self._stream_type_combobox)
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()))
for index, row in enumerate(self._rows):
fav_id = row[Column.FAV_ID]
data, sep, desc = fav_id.partition("http")
data = data.split(":")
if reset:
data[2], data[3], data[4], data[5], data[6] = "10000"
else:
data[0], data[2], data[4], data[5], data[6] = stream_type, srv_type, tid, nid, namespace
data[3] = "{:X}".format(index) if sid_auto else "0"
data = ":".join(data)
new_fav_id = "{}{}{}".format(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._bouquet.clear()
list(map(lambda r: self._bouquet.append(r[Column.FAV_ID]), self._fav_model))
self._info_bar.set_visible(True)
pass
@run_idle
def update_reference(self):
@@ -565,9 +567,283 @@ class IptvListConfigurationDialog:
entry.set_name("GtkEntry")
self.update_reference()
def is_default_values(self):
return any(el.get_text() == "0" for el in self._digit_elems[3:])
def is_all_data_default(self):
return all(el.get_active() for el in self._default_elems)
class IptvListConfigurationDialog(IptvListDialog):
def __init__(self, transient, services, iptv_rows, bouquet, fav_model, s_type):
super().__init__(transient, s_type)
self._rows = iptv_rows
self._bouquet = bouquet
self._fav_model = fav_model
self._services = services
@run_idle
def on_apply(self, item):
if not is_data_correct(self._digit_elems):
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
if self._s_type is SettingsType.ENIGMA_2:
id_default = self._id_default_check_button.get_active()
type_default = self._type_check_button.get_active()
tid_default = self._tid_check_button.get_active()
sid_auto = self._sid_auto_check_button.get_active()
nid_default = self._nid_check_button.get_active()
namespace_default = self._namespace_check_button.get_active()
all_default = self.is_all_data_default()
st_type = get_stream_type(self._stream_type_combobox)
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()
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}"
for index, row in enumerate(self._rows):
fav_id = row[Column.FAV_ID]
data, sep, desc = fav_id.partition("http")
data = data.split(":")
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] = f"{index:X}" if sid_auto else sid
data = ":".join(data)
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._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):
""" Import dialog for *.m3u* playlists. """
def __init__(self, transient, s_type, m3_path, app):
super().__init__(transient, s_type)
self._app = app
self._picons = app.picons
self._pic_path = app._settings.profile_picons_path
self._services = None
self._url_count = 0
self._errors_count = 0
self._max_count = 0
self._is_download = False
self._cancellable = Gio.Cancellable()
self._dialog.set_title(get_message("Playlist import"))
self._dialog.connect("delete-event", self.on_close)
self._apply_button.set_label(get_message("Import"))
# Progress
self._progress_bar = Gtk.ProgressBar(visible=False, valign="center")
self._spinner = Gtk.Spinner(active=False)
self._info_label = Gtk.Label(visible=True, ellipsize="end", max_width_chars=30)
load_label = Gtk.Label(label=get_message("Loading data..."))
self._spinner.bind_property("active", self._spinner, "visible")
self._spinner.bind_property("visible", load_label, "visible")
self._spinner.bind_property("active", self._start_values_grid, "sensitive", 4)
progress_box = Gtk.HBox(visible=True, spacing=2)
progress_box.add(self._progress_bar)
progress_box.pack_end(self._spinner, False, False, 0)
progress_box.pack_start(load_label, False, False, 0)
# Picons
self._picons_switch = Gtk.Switch(visible=True)
self._picon_box = Gtk.HBox(visible=True, sensitive=False, spacing=2)
self._picon_box.pack_end(self._picons_switch, False, False, 0)
self._picon_box.pack_end(Gtk.Label(visible=True, label=get_message("Download picons")), False, False, 0)
# Extra box
extra_box = Gtk.HBox(visible=True, spacing=2, margin_bottom=5, margin_top=5)
extra_box.set_center_widget(progress_box)
extra_box.pack_start(self._info_label, False, False, 5)
extra_box.pack_end(self._picon_box, True, True, 5)
frame = Gtk.Frame(visible=True, margin_bottom=5)
frame.add(extra_box)
self._data_box.add(frame)
self.get_m3u(m3_path, s_type)
@run_task
def get_m3u(self, path, s_type):
try:
GLib.idle_add(self._spinner.set_property, "active", True)
self._services = parse_m3u(path, s_type)
for s in self._services:
if s.picon:
GLib.idle_add(self._picon_box.set_sensitive, True)
break
finally:
msg = 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)
def on_apply(self, item):
if not is_data_correct(self._digit_elems):
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
picons = {}
services = self._services
if not self.is_all_data_default():
services = []
params = [int(el.get_text()) for el in self._digit_elems]
s_id = params[0]
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.
if not s.data_id:
services.append(s)
continue
params[0] = i if sid_auto else sid
picon_id = "{}_{}_{:X}_{:X}_{:X}_{:X}_{:X}_0_0_0.png".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
services.append(s._replace(picon=None, picon_id=picon_id, data_id=None, fav_id=fav_id))
if self._picons_switch.get_active():
if self.is_default_values():
show_dialog(DialogType.ERROR, self._dialog,
"Set values for TID, NID and Namespace for correct naming of the picons!")
return
self.download_picons(picons)
else:
GLib.idle_add(self._ok_button.set_visible, True)
GLib.idle_add(self._info_bar.set_visible, True, priority=GLib.PRIORITY_LOW)
self._app.append_imported_services(services)
@run_task
def download_picons(self, picons):
self._is_download = True
os.makedirs(os.path.dirname(self._pic_path), exist_ok=True)
GLib.idle_add(self._apply_button.set_sensitive, False)
GLib.idle_add(self._progress_bar.set_visible, True)
self._errors_count = 0
self._url_count = len(picons)
self._max_count = self._url_count
self._cancellable.reset()
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
futures = {executor.submit(self.download_picon, p, picons.get(p, None)): p for p in filter(None, picons)}
done, not_done = concurrent.futures.wait(futures, timeout=0)
while self._is_download and not_done:
done, not_done = concurrent.futures.wait(not_done, timeout=5)
for future in not_done:
future.cancel()
concurrent.futures.wait(not_done)
self.update_progress(self._url_count)
self.on_done()
def download_picon(self, url, pic_data):
err_msg = "Picon download error: {} [{}]"
timeout = (3, 5) # connect and read timeouts
req = requests.get(url, timeout=timeout)
if req.status_code != 200:
log(err_msg.format(url, req.reason))
self.update_progress(1)
else:
self.on_picon_load_done(req.content, pic_data)
@run_idle
def on_picon_load_done(self, data, user_data):
try:
self._info_label.set_text(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 = 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(f"Loading picon [{user_data}] data error: {e}")
else:
self.update_progress()
@run_idle
def update_progress(self, error=0):
self._errors_count += error
self._url_count -= 1
frac = 1 - self._url_count / self._max_count
self._progress_bar.set_fraction(frac)
@run_idle
def on_done(self):
self._progress_bar.set_visible(False)
self._progress_bar.set_fraction(0.0)
self._apply_button.set_sensitive(True)
self._info_label.set_text(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.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)
if s:
model.set_value(r.iter, Column.FAV_PICON, picons.get(s.picon_id, None))
yield True
self._info_bar.set_visible(True)
self._ok_button.set_visible(True)
yield True
def on_response(self, dialog, response):
if response == Gtk.ResponseType.APPLY:
return True
if response == Gtk.ResponseType.CANCEL and not self._is_download or not self.on_close():
self._dialog.destroy()
def on_close(self, window=None, event=None):
if self._is_download:
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.OK:
self._is_download = False
self._cancellable.cancel()
return False
return True
return False
class YtListImportDialog:
def __init__(self, transient, 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,
@@ -579,23 +855,22 @@ class YtListImportDialog:
"on_key_press": self.on_key_press,
"on_close": self.on_close}
self.appender = appender
self._s_type = settings.setting_type
# self._main_window, self._settings, self.append_imported_services
self.appender = app.append_imported_services
self._settings = app.app_settings
self._s_type = self._settings.setting_type
self._download_task = False
self._yt_list_id = None
self._yt_list_title = None
self._settings = settings
self._yt = None
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("yt_import_dialog_window", "yt_liststore", "yt_quality_liststore",
"yt_popup_menu", "remove_selection_image"))
builder.connect_signals(handlers)
builder = get_builder(_UI_PATH, handlers, use_str=True,
objects=("yt_import_dialog_window", "yt_liststore", "yt_quality_liststore",
"yt_popup_menu", "remove_selection_image", "yt_receive_image",
"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")
@@ -611,10 +886,14 @@ class YtListImportDialog:
self._import_button.bind_property("visible", self._quality_box, "visible")
self._import_button.bind_property("sensitive", self._quality_box, "sensitive")
self._receive_button.bind_property("sensitive", self._import_button, "sensitive")
# style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
window_size = self._settings.get("yt_import_dialog_size")
if window_size:
self._dialog.resize(*window_size)
# Style
style_provider = Gtk.CssProvider()
style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
def show(self):
@@ -659,7 +938,6 @@ class YtListImportDialog:
self.update_active_elements(True)
def on_receive(self, item):
self.show_invisible_elements()
self.update_active_elements(False)
self._model.clear()
self._yt_count_label.set_text("0")
@@ -718,11 +996,6 @@ class YtListImportDialog:
self._url_entry.set_sensitive(sensitive)
self._receive_button.set_sensitive(sensitive)
def show_invisible_elements(self):
self._list_view_scrolled_window.set_visible(True)
self._info_bar_box.set_visible(True)
self._dialog.set_resizable(True)
def on_url_entry_changed(self, entry):
url_str = entry.get_text()
yt_id = YouTube.get_yt_list_id(url_str)
@@ -778,7 +1051,9 @@ class YtListImportDialog:
def on_close(self, window, event):
if self._download_task and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return True
self._download_task = False
self._settings.add("yt_import_dialog_size", self._dialog.get_size())
if __name__ == "__main__":

Binary file not shown.

Binary file not shown.

125
app/ui/logs.glade Normal file
View File

@@ -0,0 +1,125 @@
<?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.18"/>
<!-- 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-authors Dmitriy Yefremov -->
<object class="GtkFrame" id="log_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="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_bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="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_bottom">2</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="clear_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Clear</property>
<property name="halign">center</property>
<property name="valign">center</property>
<signal name="clicked" handler="on_clear" swapped="no"/>
<child>
<object class="GtkImage" id="clear_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-clear</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="log_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">5</property>
<property name="bottom_margin">5</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="log_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Logs</property>
</object>
</child>
</object>
</interface>

66
app/ui/logs.py Normal file
View File

@@ -0,0 +1,66 @@
# -*- 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 logging
from gi.repository import GLib
from app.commons import LOGGER_NAME
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
class LogsClient(Gtk.Box):
""" Logger GUI client. """
class LogHandler(logging.Handler):
def __init__(self, view):
logging.Handler.__init__(self)
self._view = view
def handle(self, rec):
GLib.idle_add(append_text_to_tview, f"{rec.msg}\n", self._view)
def __init__(self, app, *args, **kwargs):
super().__init__(*args, **kwargs)
self._app = app
handlers = {"on_clear": self.on_clear}
builder = get_builder(UI_RESOURCES_PATH + "logs.glade", handlers)
self._log_view = builder.get_object("log_view")
self.pack_start(builder.get_object("log_frame"), True, True, 0)
logger = logging.getLogger(LOGGER_NAME)
logger.addHandler(LogsClient.LogHandler(self._log_view))
self.show()
def on_clear(self, button):
GLib.idle_add(self._log_view.get_buffer().set_text, "")

65
app/ui/mac_style.css Normal file
View File

@@ -0,0 +1,65 @@
* {
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;
}
button {
min-height: 1.5em;
min-width: 1.5em;
padding-top: 0.1em;
padding-bottom: 0.1em;
}
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.1em;
}
spinbutton {
min-height: 1.5em;
}
toolbutton {
padding: 0.1em;
}
spinner {
padding-left: 1em;
padding-right: 1em;
}
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;
}

4892
app/ui/main.glade Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,52 @@
""" Helper module for the ui. """
# -*- 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
#
""" Helper module for the GUI. """
__all__ = ("insert_marker", "move_items", "rename", "ViewTarget", "set_flags", "locate_in_services",
"scroll_to", "get_base_model", "copy_picon_reference", "assign_picons", "remove_picon",
"is_only_one_item_selected", "gen_bouquets", "BqGenType", "get_selection",
"get_model_data", "remove_all_unused_picons", "get_picon_pixbuf", "get_base_itrs", "get_iptv_url",
"update_entry_data", "append_text_to_tview", "on_popup_menu")
import os
import shutil
from collections import defaultdict
from pathlib import Path
from urllib.parse import unquote
from gi.repository import GdkPixbuf, GLib
from app.commons import run_task
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
from .dialogs import show_dialog, DialogType, get_chooser_dialog, WaitDialog
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
@@ -29,7 +65,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
@@ -75,8 +111,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:
@@ -246,7 +282,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)
@@ -324,7 +360,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()
@@ -336,7 +372,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
@@ -351,26 +387,36 @@ def scroll_to(index, view, paths=None):
selection.select_path(index)
# ***************** Picons *********************#
# ***************** Picons ********************* #
def update_picons_data(path, picons):
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)
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):
@@ -380,10 +426,16 @@ 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
if not str(src_path).endswith(".png") or not os.path.isfile(src_path):
show_dialog(DialogType.ERROR, transient, text="No png file is selected!")
return picons_files
@@ -402,7 +454,7 @@ def assign_picons(target, srv_view, fav_view, transient, picons, settings, servi
picon_id = services.get(fav_id)[Column.SRV_PICON_ID]
if picon_id:
picons_path = dst_path or settings.picons_local_path
picons_path = dst_path or settings.profile_picons_path
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
picon_file = picons_path + picon_id
try:
@@ -411,7 +463,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:
@@ -492,15 +544,17 @@ def copy_picon_reference(target, view, services, clipboard, transient):
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.picons_local_path
backup_path = settings.backup_local_path + "picons/"
pions_path = settings.profile_picons_path
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
@@ -528,10 +582,16 @@ def get_picon_pixbuf(path, size=32):
pass
# ***************** Bouquets *********************#
# ***************** Bouquets ********************* #
def gen_bouquets(view, bq_view, transient, gen_type, s_type, callback):
""" Auto-generate and append list of bouquets. """
model, paths = view.get_selection().get_selected_rows()
single_types = (BqGenType.SAT, BqGenType.PACKAGE, BqGenType.TYPE)
if gen_type in single_types:
if not is_only_one_item_selected(paths, transient):
return
def gen_bouquets(view, bq_view, transient, gen_type, tv_types, s_type, callback):
""" Auto-generate and append list of bouquets """
fav_id_index = Column.SRV_FAV_ID
index = Column.SRV_TYPE
if gen_type in (BqGenType.PACKAGE, BqGenType.EACH_PACKAGE):
@@ -539,52 +599,41 @@ def gen_bouquets(view, bq_view, transient, gen_type, tv_types, s_type, callback)
elif gen_type in (BqGenType.SAT, BqGenType.EACH_SAT):
index = Column.SRV_POS
model, paths = view.get_selection().get_selected_rows()
# Splitting services [caching] by column value.
s_data = defaultdict(list)
for row in model:
s_data[row[index]].append(BouquetService(None, BqServiceType.DEFAULT, row[fav_id_index], 0))
bq_type = BqType.BOUQUET.value if s_type is SettingsType.NEUTRINO_MP else BqType.TV.value
if gen_type in (BqGenType.SAT, BqGenType.PACKAGE, BqGenType.TYPE):
if not is_only_one_item_selected(paths, transient):
return
service = Service(*model[paths][:Column.SRV_TOOLTIP])
if service.service_type not in tv_types:
bq_type = BqType.RADIO.value
append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model,
[service.package if gen_type is BqGenType.PACKAGE else
service.pos if gen_type is BqGenType.SAT else service.service_type], s_type)
else:
wait_dialog = WaitDialog(transient)
wait_dialog.show()
append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model,
{row[index] for row in model}, s_type, wait_dialog)
@run_task
def append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model, names, s_type, wait_dialog=None):
bq_index = 0 if s_type is SettingsType.ENIGMA_2 else 1
bq_root_iter = bq_view.get_model().get_iter(bq_index)
srv = Service(*model[paths][:Column.SRV_TOOLTIP])
cond = srv.package if gen_type is BqGenType.PACKAGE else srv.pos if gen_type is BqGenType.SAT else srv.service_type
bq_view.expand_row(Gtk.TreePath(bq_index), 0)
bqs_model = bq_view.get_model()
bouquets_names = get_bouquets_names(bqs_model)
for pos, name in enumerate(sorted(names)):
if name not in bouquets_names:
services = [BouquetService(None, BqServiceType.DEFAULT, row[fav_id_index], 0)
for row in model if row[index] == name]
callback(Bouquet(name=name, type=bq_type, services=services, locked=None, hidden=None),
bqs_model.get_iter(bq_index))
bq_names = get_bouquets_names(bq_view.get_model())
if wait_dialog is not None:
wait_dialog.destroy()
if gen_type in single_types:
if cond in bq_names:
show_dialog(DialogType.ERROR, transient, "A bouquet with that name exists!")
else:
callback(Bouquet(cond, bq_type, s_data.get(cond)), bq_root_iter)
else:
# We add a bouquet only if the given name is missing [keys - names]!
for name in sorted(s_data.keys() - bq_names):
callback(Bouquet(name, BqType.TV.value, s_data.get(name)), bq_root_iter)
def get_bouquets_names(model):
""" Returns all current bouquets names """
bouquets_names = []
bouquets_names = set()
for row in model:
itr = row.iter
if model.iter_has_child(itr):
num_of_children = model.iter_n_children(itr)
for num in range(num_of_children):
child_itr = model.iter_nth_child(itr, num)
bouquets_names.append(model[child_itr][0])
bouquets_names.add(model[child_itr][0])
return bouquets_names
@@ -625,7 +674,7 @@ def get_base_paths(paths, model):
def get_model_data(view):
""" Returns model name and base model from the given view """
model = get_base_model(view.get_model())
model_name = model.get_name()
model_name = model.get_name() if model else ""
return model_name, model
@@ -637,9 +686,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:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1068
app/ui/picons.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,842 +0,0 @@
import os
import re
import shutil
import subprocess
import tempfile
from pathlib import Path
from urllib.parse import urlparse, unquote
from gi.repository import GLib, GdkPixbuf
from app.commons import run_idle, run_task, run_with_delay
from app.connections import upload_data, DownloadType, download_data, remove_picons
from app.settings import SettingsType, Settings
from app.tools.picons import PiconsParser, parse_providers, Provider, convert_to
from app.tools.satellites import SatellitesParser, SatelliteSource
from .dialogs import show_dialog, DialogType, get_message
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
class PiconsDialog:
def __init__(self, transient, settings, picon_ids, sat_positions, app):
self._picon_ids = picon_ids
self._sat_positions = sat_positions
self._app = app
self._TMP_DIR = tempfile.gettempdir() + "/"
self._BASE_URL = "www.lyngsat.com/packages/"
self._PATTERN = re.compile(r"^https://www\.lyngsat\.com/[\w-]+\.html$")
self._POS_PATTERN = re.compile(r"^\d+\.\d+[EW]?$")
self._current_process = None
self._terminate = False
self._filter_binding = None
self._services = None
self._current_picon_info = None
handlers = {"on_receive": self.on_receive,
"on_load_providers": self.on_load_providers,
"on_cancel": self.on_cancel,
"on_close": self.on_close,
"on_send": self.on_send,
"on_download": self.on_download,
"on_remove": self.on_remove,
"on_info_bar_close": self.on_info_bar_close,
"on_picons_dir_open": self.on_picons_dir_open,
"on_selected_toggled": self.on_selected_toggled,
"on_url_changed": self.on_url_changed,
"on_picons_filter_changed": self.on_picons_filter_changed,
"on_position_edited": self.on_position_edited,
"on_visible_page": self.on_visible_page,
"on_convert": self.on_convert,
"on_picons_src_changed": self.on_picons_src_changed,
"on_picons_dest_changed": self.on_picons_dest_changed,
"on_picons_view_drag_data_get": self.on_picons_view_drag_data_get,
"on_picons_src_view_drag_drop": self.on_picons_src_view_drag_drop,
"on_picons_src_view_drag_data_received": self.on_picons_src_view_drag_data_received,
"on_picons_src_view_drag_end": self.on_picons_src_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,
"on_local_remove": self.on_local_remove,
"on_picons_dest_view_realize": self.on_picons_dest_view_realize,
"on_satellites_view_realize": self.on_satellites_view_realize,
"on_satellite_selection": self.on_satellite_selection,
"on_select_all": self.on_select_all,
"on_unselect_all": self.on_unselect_all,
"on_filter_toggled": self.on_filter_toggled,
"on_fiter_srcs_toggled": self.on_fiter_srcs_toggled,
"on_filter_services_switch": self.on_filter_services_switch,
"on_picon_activated": self.on_picon_activated,
"on_tree_view_key_press": self.on_tree_view_key_press,
"on_popup_menu": on_popup_menu}
builder = Gtk.Builder()
builder.add_from_file(UI_RESOURCES_PATH + "picons_manager.glade")
builder.connect_signals(handlers)
self._dialog = builder.get_object("picons_dialog")
self._dialog.set_transient_for(transient)
self._picons_src_view = builder.get_object("picons_src_view")
self._picons_dest_view = builder.get_object("picons_dest_view")
self._providers_view = builder.get_object("providers_view")
self._satellites_view = builder.get_object("satellites_view")
self._picons_src_filter_model = builder.get_object("picons_src_filter_model")
self._picons_src_filter_model.set_visible_func(self.picons_src_filter_function)
self._picons_dst_filter_model = builder.get_object("picons_dst_filter_model")
self._picons_dst_filter_model.set_visible_func(self.picons_dst_filter_function)
self._explorer_src_path_button = builder.get_object("explorer_src_path_button")
self._explorer_dest_path_button = builder.get_object("explorer_dest_path_button")
self._expander = builder.get_object("expander")
self._text_view = builder.get_object("text_view")
self._info_bar = builder.get_object("info_bar")
self._filter_bar = builder.get_object("filter_bar")
self._filter_button = builder.get_object("filter_button")
self._src_filter_button = builder.get_object("src_filter_button")
self._dst_filter_button = builder.get_object("dst_filter_button")
self._picons_filter_entry = builder.get_object("picons_filter_entry")
self._ip_entry = builder.get_object("ip_entry")
self._picons_entry = builder.get_object("picons_entry")
self._url_entry = builder.get_object("url_entry")
self._picons_dir_entry = builder.get_object("picons_dir_entry")
self._info_bar = builder.get_object("info_bar")
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label")
self._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._load_providers_button = builder.get_object("load_providers_button")
self._receive_button = builder.get_object("receive_button")
self._convert_button = builder.get_object("convert_button")
self._enigma2_path_button = builder.get_object("enigma2_path_button")
self._save_to_button = builder.get_object("save_to_button")
self._send_button = builder.get_object("send_button")
self._download_button = builder.get_object("download_button")
self._remove_button = builder.get_object("remove_button")
self._cancel_button = builder.get_object("cancel_button")
self._enigma2_radio_button = builder.get_object("enigma2_radio_button")
self._neutrino_mp_radio_button = builder.get_object("neutrino_mp_radio_button")
self._resize_no_radio_button = builder.get_object("resize_no_radio_button")
self._resize_220_132_radio_button = builder.get_object("resize_220_132_radio_button")
self._resize_100_60_radio_button = builder.get_object("resize_100_60_radio_button")
self._satellite_label = builder.get_object("satellite_label")
self._header_download_box = builder.get_object("header_download_box")
self._satellite_label.bind_property("visible", builder.get_object("loading_data_label"), "visible", 4)
self._satellite_label.bind_property("visible", builder.get_object("loading_data_spinner"), "visible", 4)
self._cancel_button.bind_property("visible", self._header_download_box, "visible", 4)
self._convert_button.bind_property("visible", self._header_download_box, "visible", 4)
self._load_providers_button.bind_property("visible", self._receive_button, "visible")
self._filter_bar.bind_property("search-mode-enabled", self._filter_bar, "visible")
self._explorer_src_path_button.bind_property("sensitive", builder.get_object("picons_view_sw"), "sensitive")
self._filter_button.bind_property("active", builder.get_object("filter_service_box"), "visible")
self._filter_button.bind_property("active", builder.get_object("src_title_grid"), "visible")
self._filter_button.bind_property("active", builder.get_object("dst_title_grid"), "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")
explorer_info_bar = builder.get_object("explorer_info_bar")
explorer_info_bar.bind_property("visible", builder.get_object("explorer_info_bar_frame"), "visible")
self._info_check_button.bind_property("active", explorer_info_bar, "visible")
# Init drag-and-drop
self.init_drag_and_drop()
# Style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
self._settings = settings
self._s_type = settings.setting_type
self._ip_entry.set_text(self._settings.host)
self._picons_entry.set_text(self._settings.picons_path)
self._picons_dir_entry.set_text(self._settings.picons_local_path)
window_size = self._settings.get("picons_downloader_window_size")
if window_size:
self._dialog.resize(*window_size)
if not len(self._picon_ids) and self._s_type is SettingsType.ENIGMA_2:
message = get_message("To automatically set the identifiers for picons,\n"
"first load the required services list into the main application window.")
self.show_info_message(message, Gtk.MessageType.WARNING)
self._satellite_label.show()
def show(self):
self._dialog.show()
def on_picons_dest_view_realize(self, view):
self._services = {s.picon_id: s for s in self._app.current_services.values() if s.picon_id}
self._explorer_dest_path_button.select_filename(self._settings.picons_local_path)
def on_picons_src_changed(self, button):
self.update_picons_data(self._picons_src_view, button)
def on_picons_dest_changed(self, button):
self.update_picon_info()
self.update_picons_data(self._picons_dest_view, button)
def update_picons_data(self, view, button):
path = button.get_filename()
if not path or not os.path.exists(path):
return
GLib.idle_add(button.set_sensitive, False)
gen = self.update_picons(path, view, button)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def update_picons(self, path, view, button):
p_model = view.get_model()
if not p_model:
button.set_sensitive(True)
return
model = get_base_model(p_model)
view.set_model(None)
factor = self._app.DEL_FACTOR
for index, itr in enumerate([row.iter for row in model]):
model.remove(itr)
if index % factor == 0:
yield True
for file in os.listdir(path):
if self._terminate:
return
p_path = "{}/{}".format(path, file)
p = self.get_pixbuf_at_scale(p_path, 72, 48, True)
if p:
yield model.append((p, file, p_path))
view.set_model(p_model)
button.set_sensitive(True)
yield 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())
f_path = str(path.resolve())
if not f_path:
return
model = get_base_model(view.get_model())
if path.is_file():
p = self.get_pixbuf_at_scale(f_path, 72, 48, True)
if p:
model.append((p, path.name, f_path))
elif path.is_dir():
self._explorer_src_path_button.select_filename(f_path)
def get_pixbuf_at_scale(self, path, width, height, p_ratio):
try:
return GdkPixbuf.Pixbuf.new_from_file_at_scale(path, width, height, p_ratio)
except GLib.GError:
pass
# ***************** Drag-and-drop ********************* #
def init_drag_and_drop(self):
self._picons_src_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
self._picons_src_view.drag_source_add_uri_targets()
self._picons_dest_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
self._picons_dest_view.drag_source_add_uri_targets()
self._picons_src_view.enable_model_drag_dest([], Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE)
self._picons_src_view.drag_dest_add_text_targets()
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._explorer_dest_path_button.get_filename()).as_uri()])
def on_picons_src_view_drag_drop(self, view, drag_context, x, y, time):
view.stop_emission_by_name("drag_drop")
targets = drag_context.list_targets()
view.drag_get_data(drag_context, targets[-1] if targets else Gdk.atom_intern("text/plain", False), time)
def on_picons_src_view_drag_data_received(self, view, drag_context, x, y, data, info, time):
view.stop_emission_by_name("drag_data_received")
txt = data.get_text()
if not txt:
return
if txt.startswith("file://"):
self.update_picons_from_file(view, txt)
return
itr_str, sep, src = txt.partition("::::")
if src == self._app.BQ_MODEL_NAME:
return
path, pos = view.get_dest_row_at_pos(x, y) or (None, None)
if not path:
return
model = view.get_model()
if src == self._app.FAV_MODEL_NAME:
target_view = self._app.fav_view
c_id = Column.FAV_ID
else:
target_view = self._app.services_view
c_id = Column.SRV_FAV_ID
t_mod = target_view.get_model()
dest_path = self._explorer_dest_path_button.get_filename() + "/"
self.update_picons_dest_view(self._app.on_assign_picon(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
def update_picons_dest_view(self, picons):
""" Update destination view on adding/changing picons. """
if picons:
dest_model = get_base_model(self._picons_dest_view.get_model())
paths = {r[1]: r.iter for r in dest_model}
for p_path in picons:
p = self.get_pixbuf_at_scale(p_path, 72, 48, True)
if p:
p_name = Path(p_path).name
itr = paths.get(p_name, None)
if itr:
dest_model.set_value(itr, 0, p)
else:
itr = dest_model.append((p, p_name, p_path))
scroll_to(dest_model.get_path(itr), self._picons_dest_view)
@run_idle
def show_assign_info(self, fav_ids):
self._expander.set_expanded(True)
self._text_view.get_buffer().set_text("")
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("Picon assignment for the service:\n{}\n{}\n".format(info, " * " * 30))
def on_picons_src_view_drag_end(self, view, drag_context):
self.update_picons_dest_view(self._app.picons_buffer)
def on_picon_info_image_drag_data_received(self, img, drag_context, x, y, data, info, time):
if not self._current_picon_info:
self.show_info_message("No selected item!", Gtk.MessageType.ERROR)
return
uris = data.get_uris()
if len(uris) == 2:
name, fav_id = self._current_picon_info
src = urlparse(unquote(uris[0])).path
dst = "{}/{}".format(urlparse(unquote(uris[1])).path, name)
if src != dst:
shutil.copy(src, dst)
for row in get_base_model(self._picons_dest_view.get_model()):
if name == row[1]:
row[0] = self.get_pixbuf_at_scale(row[-1], 72, 48, True)
img.set_from_pixbuf(self.get_pixbuf_at_scale(row[-1], 100, 60, True))
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:
return Path(urlparse(unquote(uris[0])).path).resolve()
def update_picon_in_lists(self, dst, fav_id):
picon = get_picon_pixbuf(dst)
p_pos = Column.SRV_PICON
yield set_picon(fav_id, get_base_model(self._app.services_view.get_model()), picon, Column.SRV_FAV_ID, p_pos)
yield set_picon(fav_id, get_base_model(self._app.fav_view.get_model()), picon, Column.FAV_ID, p_pos)
# ******************** Download/Upload/Remove ************************* #
def on_selective_send(self, view):
path = self.get_selected_path(view)
if path:
self.on_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})
def on_selective_remove(self, view):
path = self.get_selected_path(view)
if path:
self.on_remove(files_filter={path.name})
def on_local_remove(self, view):
model, paths = view.get_selection().get_selected_rows()
if paths and show_dialog(DialogType.QUESTION, self._dialog) == 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)
def on_send(self, item=None, files_filter=None, path=None):
dest_path = path or self.check_dest_path()
if not dest_path:
return
settings = Settings(self._settings.settings)
settings.picons_local_path = "{}/".format(dest_path)
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):
path = path or self.check_dest_path()
if not path:
return
settings = Settings(self._settings.settings)
settings.picons_local_path = path + "/"
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):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
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))
def get_selected_path(self, view):
model, paths = view.get_selection().get_selected_rows()
if paths:
return Path(model[paths.pop()][-1]).resolve()
def check_dest_path(self):
""" Checks the destination path and returns if present. """
if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.OK:
return
path = self._explorer_dest_path_button.get_filename()
if not path:
show_dialog(DialogType.ERROR, transient=self._dialog, text="Select paths!")
return
return path
# ******************** Downloader ************************* #
def on_satellites_view_realize(self, view):
self.get_satellites(view)
@run_task
def get_satellites(self, view):
sats = SatellitesParser().get_satellites_list(SatelliteSource.LYNGSAT)
if not sats:
self.show_info_message("Getting satellites list error!", Gtk.MessageType.ERROR)
gen = self.append_satellites(view.get_model(), sats)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def append_satellites(self, model, sats):
try:
for sat in sats:
pos = sat[1]
name, pos = "{} ({})".format(sat[0], pos), "{}{}".format("-" if pos[-1] == "W" else "", pos[:-1])
if not self._terminate and model:
if pos in self._sat_positions:
yield model.append((name, sat[3], pos))
finally:
self._satellite_label.show()
def on_satellite_selection(self, view, path, column):
model = view.get_model()
self._url_entry.set_text(model.get(model.get_iter(path), 1)[0])
@run_idle
def on_load_providers(self, item):
self._expander.set_expanded(True)
self.on_info_bar_close()
self._cancel_button.show()
url = self._url_entry.get_text()
try:
self._current_process = subprocess.Popen(["wget", "-pkP", self._TMP_DIR, url],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
except FileNotFoundError as e:
self._cancel_button.hide()
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
GLib.io_add_watch(self._current_process.stderr, GLib.IO_IN, self.write_to_buffer)
model = self._providers_view.get_model()
model.clear()
self.append_providers(url, model)
@run_task
def append_providers(self, url, model):
self._current_process.wait()
try:
self._terminate = False
providers = parse_providers(self._TMP_DIR + url[url.find("w"):])
except FileNotFoundError:
pass # NOP
else:
if providers:
for p in providers:
if self._terminate:
return
model.append((self.get_pixbuf(p[0]) if p[0] else TV_ICON, *p[1:]))
self.update_receive_button_state()
finally:
GLib.idle_add(self._cancel_button.hide)
self._terminate = False
def get_pixbuf(self, img_url):
return GdkPixbuf.Pixbuf.new_from_file_at_scale(filename=self._TMP_DIR + "www.lyngsat.com/" + img_url,
width=48, height=48, preserve_aspect_ratio=True)
def on_receive(self, item):
self._cancel_button.show()
self.start_download()
@run_task
def start_download(self):
if self._current_process.poll() is None:
self.show_dialog("The task is already running!", DialogType.ERROR)
return
self._terminate = False
self._expander.set_expanded(True)
providers = self.get_selected_providers()
for prv in providers:
if not self._POS_PATTERN.match(prv[2]):
self.show_info_message(
get_message("Specify the correct position value for the provider!"), Gtk.MessageType.ERROR)
scroll_to(prv.path, self._providers_view)
return
try:
for prv in providers:
if self._terminate:
return
self.process_provider(Provider(*prv))
if not self._resize_no_radio_button.get_active():
self.resize(self._picons_dir_entry.get_text())
else:
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
finally:
GLib.idle_add(self._cancel_button.hide)
self._terminate = False
def process_provider(self, prv):
url = prv.url
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
self._current_process = subprocess.Popen(["wget", "-pkP", self._TMP_DIR, url],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
GLib.io_add_watch(self._current_process.stderr, GLib.IO_IN, self.write_to_buffer)
self._current_process.wait()
path = self._TMP_DIR + (url[url.find("//") + 2:] if prv.single else self._BASE_URL + url[url.rfind("/") + 1:])
PiconsParser.parse(path, self._picons_dir_entry.get_text(),
self._TMP_DIR, prv, self._picon_ids, self.get_picons_format())
def write_to_buffer(self, fd, condition):
if condition == GLib.IO_IN:
char = fd.read(1)
self.append_output(char)
return True
return False
@run_idle
def append_output(self, char):
append_text_to_tview(char, self._text_view)
@run_task
def resize(self, path):
self.show_info_message(get_message("Resizing..."), Gtk.MessageType.INFO)
try:
from pathlib import Path
from PIL import Image
except ImportError as e:
self.show_info_message("{} {}".format(get_message("Conversion error."), e), Gtk.MessageType.ERROR)
else:
res = (220, 132) if self._resize_220_132_radio_button.get_active() else (100, 60)
for img_file in Path(path).glob("*.png"):
img = Image.open(img_file)
img = img.resize(res, Image.ANTIALIAS)
img.save(img_file, "PNG", optimize=True)
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
def on_cancel(self, item=None):
if self.is_task_running() and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return True
self.terminate_task()
@run_task
def terminate_task(self):
self._terminate = True
if self._current_process:
self._current_process.terminate()
self.show_info_message(get_message("The task is canceled!"), Gtk.MessageType.WARNING)
def on_close(self, window, event):
if self.on_cancel():
return True
self._terminate = True
self.save_window_size(window)
self.clean_data()
self._app.update_picons()
GLib.idle_add(self._dialog.destroy)
def save_window_size(self, window):
size = window.get_size()
height = size.height - self._text_view.get_allocated_height() - self._info_bar.get_allocated_height()
self._settings.add("picons_downloader_window_size", (size.width, height))
@run_task
def clean_data(self):
path = self._TMP_DIR + "www.lyngsat.com"
if os.path.exists(path):
shutil.rmtree(path)
@run_task
def run_func(self, func, update=False):
try:
GLib.idle_add(self._expander.set_expanded, True)
GLib.idle_add(self._header_download_box.set_sensitive, False)
func()
except OSError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
finally:
GLib.idle_add(self._header_download_box.set_sensitive, True)
if update:
self.on_picons_dest_changed(self._explorer_dest_path_button)
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(get_message(text))
def on_picons_dir_open(self, entry, icon, event_button):
update_entry_data(entry, self._dialog, settings=self._settings)
@run_idle
def on_selected_toggled(self, toggle, path):
model = self._providers_view.get_model()
model.set_value(model.get_iter(path), 7, not toggle.get_active())
self.update_receive_button_state()
def on_select_all(self, view):
self.update_selection(view, True)
def on_unselect_all(self, view):
self.update_selection(view, False)
def update_selection(self, view, select):
view.get_model().foreach(lambda mod, path, itr: mod.set_value(itr, 7, select))
self.update_receive_button_state()
# *********************** Filter **************************** #
def on_filter_toggled(self, button):
active = button.get_active()
self._filter_bar.set_search_mode(active)
if not active:
self._picons_filter_entry.set_text("")
def on_fiter_srcs_toggled(self, filter_model):
""" Activates re-filtering for model when filter check-button has toggled. """
GLib.idle_add(filter_model.refilter, priority=GLib.PRIORITY_LOW)
def on_filter_services_switch(self, button, state):
""" Activates or deactivates filtering in the main list of services. """
if state:
self._filter_binding = self._picons_filter_entry.bind_property("text", self._app.filter_entry, "text")
self._app.filter_entry.set_text(self._picons_filter_entry.get_text())
else:
if self._filter_binding:
self._filter_binding.unbind()
self._app.filter_entry.set_text("")
@run_with_delay(1)
def on_picons_filter_changed(self, entry):
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)
def picons_src_filter_function(self, model, itr, data):
return self.filter_function(itr, model, self._src_filter_button.get_active())
def picons_dst_filter_function(self, model, itr, data):
return self.filter_function(itr, model, self._dst_filter_button.get_active())
def filter_function(self, itr, model, active):
""" Main filtering function. """
if any((not active, model is None, model == "None")):
return True
t = model.get_value(itr, 1)
if not t:
return True
txt = self._picons_filter_entry.get_text().upper()
return txt in t.upper() or t in (
map(lambda s: s.picon_id, filter(lambda s: txt in s.service.upper(), self._app.current_services.values())))
def on_picon_activated(self, view):
if self._info_check_button.get_active():
model, path = view.get_selection().get_selected_rows()
if not path:
return
row = model[path][:]
name, path = row[1], row[-1]
srv = self._services.get(row[1], None)
self.update_picon_info(name, path, srv)
def update_picon_info(self, name=None, path=None, srv=None):
self._picon_info_image.set_from_pixbuf(self.get_pixbuf_at_scale(path, 100, 60, True) if path else None)
self._picon_info_label.set_text(self.get_service_info(srv))
self._current_picon_info = (name, srv.fav_id) if srv else None
def get_service_info(self, srv):
""" Returns short info about the service. """
if not srv:
return ""
if srv.service_type == "IPTV":
return self._app.get_hint_for_srv_list(srv)
header, ref = self._app.get_hint_header_info(srv)
return "{} {}: {}\n{}: {} {}: {}\n{}".format(header.rstrip(), get_message("Package"), srv.package,
get_message("System"), srv.system, get_message("Freq"), srv.freq,
ref)
def on_tree_view_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_local_remove(view)
def on_url_changed(self, entry):
suit = self._PATTERN.search(entry.get_text())
entry.set_name("GtkEntry" if suit else "digit-entry")
self._load_providers_button.set_sensitive(suit if suit else False)
def on_position_edited(self, render, path, value):
model = self._providers_view.get_model()
model.set_value(model.get_iter(path), 2, value)
@run_idle
def on_visible_page(self, stack: Gtk.Stack, param):
name = stack.get_visible_child_name()
self._convert_button.set_visible(name == "converter")
self._load_providers_button.set_visible(name == "downloader")
is_explorer = name == "explorer"
self._filter_button.set_visible(is_explorer)
if is_explorer:
self.on_picons_dest_changed(self._explorer_dest_path_button)
@run_idle
def on_convert(self, item):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
picons_path = self._enigma2_path_button.get_filename()
save_path = self._save_to_button.get_filename()
if not picons_path or not save_path:
show_dialog(DialogType.ERROR, transient=self._dialog, text="Select paths!")
return
self._expander.set_expanded(True)
convert_to(src_path=picons_path,
dest_path=save_path,
s_type=SettingsType.ENIGMA_2,
callback=self.append_output,
done_callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO))
@run_idle
def update_receive_button_state(self):
try:
self._receive_button.set_sensitive(len(self.get_selected_providers()) > 0)
except TypeError:
pass # NOP
def get_selected_providers(self):
""" returns selected providers """
return [r for r in self._providers_view.get_model() if r[7]]
@run_idle
def show_dialog(self, message, dialog_type):
show_dialog(dialog_type, self._dialog, message)
def get_picons_format(self):
picon_format = SettingsType.ENIGMA_2
if self._neutrino_mp_radio_button.get_active():
picon_format = SettingsType.NEUTRINO_MP
return picon_format
def is_task_running(self):
return self._current_process and self._current_process.poll() is None
if __name__ == "__main__":
pass

242
app/ui/playback.glade Normal file
View File

@@ -0,0 +1,242 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkEventBox" id="event_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<signal name="button-press-event" handler="on_press" swapped="no"/>
<signal name="realize" handler="on_realize" swapped="no"/>
<child>
<placeholder/>
</child>
</object>
<object class="GtkToolbar" id="tool_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkToolButton" id="prev_button">
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Previous stream in the list</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-media-previous</property>
<signal name="clicked" handler="on_previous" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="play_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Play</property>
<property name="action_name">app.on_play</property>
<property name="stock_id">gtk-media-play</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="stop_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Stop playback</property>
<property name="action_name">app.on_stop</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-media-stop</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="next_button">
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Next stream in the list</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-media-next</property>
<signal name="clicked" handler="on_next" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkToolItem" id="player_item">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkBox" id="rewind_box">
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel" id="current_time_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScale" id="scale">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="restrict_to_fill_level">False</property>
<property name="fill_level">0</property>
<property name="draw_value">False</property>
<property name="has_origin">False</property>
<signal name="change-value" handler="on_rewind" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="full_time_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkToolItem" id="extras_item">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkBox" id="extras_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="spacing">2</property>
<child>
<object class="GtkMenuButton" id="audio_menu_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="relief">none</property>
<child>
<object class="GtkImage" id="audio_menu_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Audio Track</property>
<property name="icon_name">audio-volume-high</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="GtkMenuButton" id="video_menu_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="relief">none</property>
<child>
<object class="GtkImage" id="video_menu_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Aspect ratio</property>
<property name="icon_name">view-restore</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="GtkMenuButton" id="subtitle_menu_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="relief">none</property>
<child>
<object class="GtkImage" id="subtitle_menu_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Subtitle Track</property>
<property name="icon_name">format-text-underline</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="full_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Toggle in fullscreen</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-fullscreen</property>
<signal name="clicked" handler="on_full_screen" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="close_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Close playback</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-close</property>
<signal name="clicked" handler="on_close" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
</object>
</interface>

454
app/ui/playback.py Normal file
View File

@@ -0,0 +1,454 @@
# -*- 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
#
""" Additional module for playback. """
from functools import lru_cache
from gi.repository import GLib, GObject, Gio
from app.commons import run_idle, run_with_delay
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, 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, Page
class PlayerBox(Gtk.Box):
def __init__(self, app, *args, **kwargs):
super().__init__(*args, **kwargs)
# Signals.
GObject.signal_new("playback-full-screen", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("playback-close", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("play", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("stop", 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)
self._fav_view = app.fav_view
self._player = None
self._current_mrl = None
self._full_screen = False
self._playback_window = None
self._audio_track_menu = None
self._subtitle_track_menu = None
self._play_mode = self._app.app_settings.play_streams_mode
handlers = {"on_realize": self.on_realize,
"on_press": self.on_press,
"on_next": self.on_next,
"on_previous": self.on_previous,
"on_rewind": self.on_rewind,
"on_full_screen": self.on_full_screen,
"on_close": self.on_close}
builder = get_builder(UI_RESOURCES_PATH + "playback.glade", handlers)
self.set_spacing(5)
self.set_orientation(Gtk.Orientation.VERTICAL)
self._event_box = builder.get_object("event_box")
self.pack_start(self._event_box, True, True, 0)
if not IS_DARWIN:
self.pack_end(builder.get_object("tool_bar"), False, True, 0)
self._scale = builder.get_object("scale")
self._full_time_label = builder.get_object("full_time_label")
self._current_time_label = builder.get_object("current_time_label")
self._rewind_box = builder.get_object("rewind_box")
self._tool_bar = builder.get_object("tool_bar")
self._prev_button = builder.get_object("prev_button")
self._next_button = builder.get_object("next_button")
self._audio_menu_button = builder.get_object("audio_menu_button")
self._video_menu_button = builder.get_object("video_menu_button")
self._subtitle_menu_button = builder.get_object("subtitle_menu_button")
self._fav_view.bind_property("sensitive", self._prev_button, "sensitive")
self._fav_view.bind_property("sensitive", self._next_button, "sensitive")
self.connect("delete-event", self.on_delete)
self.connect("show", self.set_player_area_size)
def on_fav_clicked(self, app, mode):
if mode is not FavClickMode.STREAM and not self._app.http_api:
return
self._fav_view.set_sensitive(False)
if mode is FavClickMode.STREAM:
self.on_play_stream()
elif mode is FavClickMode.ZAP_PLAY:
self._app.on_zap(self.on_watch)
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()
def on_play_recording(self, app, url):
self.play(url)
def on_page_changed(self, app, page):
self.on_close()
self.set_visible(False)
def on_realize(self, box):
if not self._player:
settings = self._app.app_settings
try:
self._player = Player.make(settings.stream_lib, settings.play_streams_mode, self._event_box)
except (ImportError, NameError) as e:
self._app.show_error_message(str(e))
return True
else:
self.init_playback_elements()
self.emit("play", self._current_mrl)
finally:
if settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
self.set_player_area_size(box)
def init_playback_elements(self):
self._player.connect("error", self.on_error)
self._player.connect("played", self.on_played)
self._player.connect("audio-track", self.on_audio_track_changed)
self._player.connect("subtitle-track", self.on_subtitle_track_changed)
self._app.app_window.connect("key-press-event", self.on_key_press)
builder = get_builder(UI_RESOURCES_PATH + "app_menu.ui")
self._audio_track_menu = builder.get_object("audio_track_menu")
self._subtitle_track_menu = builder.get_object("subtitle_track_menu")
audio_menu = builder.get_object("audio_menu")
video_menu = builder.get_object("video_menu")
subtitle_menu = builder.get_object("subtitle_menu")
if not IS_GNOME_SESSION:
menu_bar = self._app.get_menubar()
menu_bar.insert_section(1, None, audio_menu)
menu_bar.insert_section(2, None, video_menu)
menu_bar.insert_section(3, None, subtitle_menu)
if not IS_DARWIN:
self._player.connect("position", self.on_time_changed)
self._audio_menu_button.set_menu_model(self._audio_track_menu)
self._video_menu_button.set_menu_model(builder.get_object("aspect_ratio_menu"))
self._subtitle_menu_button.set_menu_model(self._subtitle_track_menu)
# Actions.
self._app.set_action("on_play", self.on_play)
self._app.set_action("on_stop", self.on_stop)
audio_track_action = Gio.SimpleAction.new_stateful("on_set_audio_track", GLib.VariantType.new("i"),
GLib.Variant("i", 0))
audio_track_action.connect("activate", self.on_set_audio_track)
self._app.add_action(audio_track_action)
aspect_action = Gio.SimpleAction.new_stateful("on_set_aspect_ratio", GLib.VariantType.new("s"),
GLib.Variant("s", ""))
aspect_action.connect("activate", self.on_set_aspect_ratio)
self._app.add_action(aspect_action)
subtitle_track_action = Gio.SimpleAction.new_stateful("on_set_subtitle_track", GLib.VariantType.new("i"),
GLib.Variant("i", -1))
subtitle_track_action.connect("activate", self.on_set_subtitle_track)
self._app.add_action(subtitle_track_action)
def on_play(self, action=None, value=None):
self.emit("play", None)
def on_stop(self, action=None, value=None):
self.emit("stop", None)
def on_next(self, button):
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, 1):
self.set_player_action()
def on_previous(self, button):
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, -1):
self.set_player_action()
def on_rewind(self, scale, scroll_type, value):
self._player.set_time(int(value))
def on_full_screen(self, item=None):
self._full_screen = not self._full_screen
if self._play_mode is PlayStreamsMode.BUILT_IN:
self._tool_bar.set_visible(not self._full_screen)
self.emit("playback-full-screen", not self._full_screen)
elif self._playback_window:
if not IS_DARWIN:
self._tool_bar.set_visible(not self._full_screen)
self._playback_window.fullscreen() if self._full_screen else self._playback_window.unfullscreen()
def on_close(self, action=None, value=None):
if self._playback_window:
self._app.app_settings.add("playback_window_size", self._playback_window.get_size())
self._playback_window.hide()
self.on_stop()
self.hide()
self.emit("playback-close", None)
return True
@run_with_delay(1)
def on_audio_track_changed(self, player, tracks):
self._audio_track_menu.remove_all()
for t in tracks:
item = Gio.MenuItem.new(t[1], None)
item.set_action_and_target_value("app.on_set_audio_track", GLib.Variant("i", t[0]))
self._audio_track_menu.append_item(item)
@run_with_delay(1)
def on_subtitle_track_changed(self, player, tracks):
self._subtitle_track_menu.remove_all()
for t in tracks:
item = Gio.MenuItem.new(t[1], None)
item.set_action_and_target_value("app.on_set_subtitle_track", GLib.Variant("i", t[0]))
self._subtitle_track_menu.append_item(item)
def on_set_audio_track(self, action, value):
action.set_state(value)
self._player.set_audio_track(value.get_int32())
def on_set_aspect_ratio(self, action, value):
action.set_state(value)
self._player.set_aspect_ratio(value.get_string())
def on_set_subtitle_track(self, action, value):
action.set_state(value)
self._player.set_subtitle_track(value.get_int32())
def on_press(self, area, event):
if event.button == Gdk.BUTTON_PRIMARY:
if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
self.on_full_screen()
def on_key_press(self, widget, event):
if self._player and self.get_visible():
key = event.keyval
if any((key == Gdk.KEY_F11, key == Gdk.KEY_f, self._full_screen and key == Gdk.KEY_Escape)):
self.on_full_screen()
def on_delete(self, box):
if self._player:
self._player.release()
@run_with_delay(1)
def set_player_action(self):
click_mode = self._app.app_settings.fav_click_mode
self._fav_view.set_sensitive(False)
if click_mode is FavClickMode.PLAY:
self.on_play_service()
elif click_mode is FavClickMode.ZAP_PLAY:
self._app.on_zap(self.on_watch)
elif click_mode is FavClickMode.STREAM:
self.on_play_stream()
def update_buttons(self):
if self._player:
path, column = self._fav_view.get_cursor()
current_index = path[0]
self._player_prev_button.set_sensitive(current_index != 0)
self._player_next_button.set_sensitive(len(self._fav_model) != current_index + 1)
@lru_cache(maxsize=1)
def on_duration_changed(self, duration):
self._scale.set_value(0)
self._scale.get_adjustment().set_upper(duration)
GLib.idle_add(self._rewind_box.set_visible, duration > 0, priority=GLib.PRIORITY_LOW)
GLib.idle_add(self._current_time_label.set_text, "0", priority=GLib.PRIORITY_LOW)
GLib.idle_add(self._full_time_label.set_text, self.get_time_str(duration),
priority=GLib.PRIORITY_LOW)
def on_time_changed(self, widget, t):
if not self._full_screen and self._rewind_box.get_visible():
GLib.idle_add(self._current_time_label.set_text, self.get_time_str(t),
priority=GLib.PRIORITY_LOW)
def get_time_str(self, duration):
""" Returns a string representation of time from duration in milliseconds """
m, s = divmod(duration // 1000, 60)
h, m = divmod(m, 60)
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, title=None):
width, height = 480, 240
size = self._app.app_settings.get("playback_window_size")
if size:
width, height = size
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=title or self.get_playback_title(),
window_position=Gtk.WindowPosition.CENTER,
icon_name="demon-editor")
self._playback_window.connect("delete-event", self.on_close)
self._playback_window.connect("key-press-event", self.on_key_press)
self._playback_window.bind_property("visible", self._event_box, "visible")
if not IS_DARWIN:
self._prev_button.set_visible(False)
self._next_button.set_visible(False)
self.reparent(self._playback_window)
self._playback_window.set_application(self._app)
self.show()
self._playback_window.resize(width, height)
self._playback_window.show()
def get_playback_title(self):
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()
if path:
row = self._fav_view.get_model()[path][:]
if row[Column.FAV_TYPE] != BqServiceType.IPTV.name:
self.on_error(None, "Not allowed in this context!")
return
url = get_iptv_url(row, self._app.app_settings.setting_type)
self.play(url) if url else self.on_error(None, "No reference is present!")
def on_play_service(self, item=None):
path, column = self._fav_view.get_cursor()
if not path or not self._app.http_api:
return
ref = self._app.get_service_ref(path)
if not ref:
return
if self._player and self._player.is_playing():
self.emit("stop", None)
s_type = self._app.app_settings.setting_type
req = HttpAPI.Request.STREAM if s_type is SettingsType.ENIGMA_2 else HttpAPI.Request.N_STREAM
self._app.http_api.send(req, ref, self.watch)
def on_watch(self, item=None):
""" Switch to the channel and watch in the player. """
s_type = self._app.app_settings.setting_type
if s_type is SettingsType.ENIGMA_2:
self._app.http_api.send(HttpAPI.Request.STREAM_CURRENT, "", self.watch)
elif s_type is SettingsType.NEUTRINO_MP:
self._app.http_api.send(HttpAPI.Request.N_ZAP, "",
lambda rf: self._app.http_api.send(HttpAPI.Request.N_STREAM, rf.get("data", ""),
self.watch))
def watch(self, data):
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, title=None):
if self._play_mode is PlayStreamsMode.M3U:
self._app.save_stream_to_m3u(url)
return
if self._play_mode is not self._app.app_settings.play_streams_mode:
self.on_error(None, "Play mode has been changed!\nRestart the program to apply the settings.")
return
if self._play_mode is PlayStreamsMode.BUILT_IN:
self.show()
elif self._play_mode is PlayStreamsMode.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):
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)
if __name__ == "__main__":
pass

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,36 @@
# -*- 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 concurrent.futures
import re
import time
from math import fabs
from pyexpat import ExpatError
from gi.repository import GLib
@@ -9,104 +38,90 @@ from app.commons import run_idle, run_task, log
from app.eparser import get_satellites, write_satellites, Satellite, Transponder
from app.eparser.ecommons import PLS_MODE, get_key_by_value
from app.tools.satellites import SatellitesParser, SatelliteSource, ServicesParser
from .dialogs import show_dialog, DialogType, get_dialogs_string, get_chooser_dialog, get_message
from .main_helper import move_items, scroll_to, append_text_to_tview, get_base_model, on_popup_menu
from .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, TEXT_DOMAIN, MOVE_KEYS, KeyboardKey, IS_GNOME_SESSION, MOD_MASK
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, MOVE_KEYS, KeyboardKey, MOD_MASK
_UI_PATH = UI_RESOURCES_PATH + "satellites_dialog.glade"
_UI_PATH = UI_RESOURCES_PATH + "satellites.glade"
def show_satellites_dialog(transient, options):
SatellitesDialog(transient, options).show()
class SatellitesDialog:
class SatellitesTool(Gtk.Box):
_aggr = [None for x in range(9)] # aggregate
def __init__(self, transient, settings):
self._data_path = settings.data_local_path + "satellites.xml"
self._settings = settings
def __init__(self, app, settings, *args, **kwargs):
super().__init__(*args, **kwargs)
handlers = {"on_open": self.on_open,
"on_remove": self.on_remove,
"on_save": self.on_save,
"on_save_as": self.on_save_as,
self._app = app
self._settings = settings
self._current_sat_path = None
handlers = {"on_remove": self.on_remove,
"on_update": self.on_update,
"on_up": self.on_up,
"on_down": self.on_down,
"on_popup_menu": on_popup_menu,
"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_row_activated": self.on_row_activated,
"on_resize": self.on_resize,
"on_quit": self.on_quit}
"on_satellite_selection": self.on_satellite_selection}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH),
("satellites_editor_window", "satellites_tree_store", "popup_menu",
"left_header_menu", "popup_menu_add_image", "popup_menu_add_image_2"))
builder.connect_signals(handlers)
builder = get_builder(_UI_PATH, handlers, use_str=True,
objects=("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._window = builder.get_object("satellites_editor_window")
self._window.set_transient_for(transient)
self._sat_view = builder.get_object("satellites_editor_tree_view")
# Setting the last size of the dialog window if it was saved
window_size = self._settings.get("sat_editor_window_size")
if window_size:
self._window.resize(*window_size)
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.load_satellites_list(self._sat_view.get_model())
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, model):
gen = self.on_satellites_list_load(model)
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 show(self):
self._window.show()
def on_resize(self, window):
""" Stores new size properties for dialog window after resize """
if self._settings:
self._settings.add("sat_editor_window_size", window.get_size())
@run_idle
def on_quit(self, *args):
self._window.destroy()
@run_idle
def on_open(self, model):
response = get_chooser_dialog(self._window, self._settings, "satellites.xml", ("*.xml",))
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"):
show_dialog(DialogType.ERROR, self._window, text="No satellites.xml file is selected!")
self._app.show_error_message("No satellites.xml file is selected!")
return
self._data_path = response
self.load_satellites_list(model)
self.load_satellites_list(response)
@staticmethod
def on_row_activated(view, path, column):
if view.row_expanded(path):
view.collapse_row(path)
else:
view.expand_row(path, column)
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._sat_view)
move_items(KeyboardKey.UP, self._satellite_view)
def on_down(self, item):
move_items(KeyboardKey.DOWN, self._sat_view)
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 """
@@ -127,34 +142,38 @@ class SatellitesDialog:
elif ctrl and key is KeyboardKey.T:
self.on_transponder()
elif ctrl and key in MOVE_KEYS:
move_items(key, self._sat_view)
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, model):
def on_satellites_list_load(self, path=None):
""" Load satellites data into model """
model = self._satellite_view.get_model()
model.clear()
try:
satellites = get_satellites(self._data_path)
path = path or self._settings.profile_data_path + "satellites.xml"
satellites = get_satellites(path)
yield True
except FileNotFoundError as e:
show_dialog(DialogType.ERROR, self._window, getattr(e, "message", str(e)) +
"\n\nPlease, download files from receiver or setup your path for read data!")
return
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:
model.clear()
for sat in satellites:
append_satellite(model, sat)
yield True
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)
self.on_satellite()
def on_transponder_add(self, item):
self.on_transponder(None)
self.on_transponder()
def on_edit(self, view, force=False):
""" Common edit """
@@ -163,139 +182,104 @@ class SatellitesDialog:
return
model = view.get_model()
itr = model.get_iter(paths[0])
row = model.get(itr, *[x for x in range(view.get_n_columns())])
row = model[paths][:]
itr = model.get_iter(paths)
if row[-1]: # satellite
self.on_satellite(None if force else Satellite(row[0], None, row[-1], None), itr)
else:
self.on_transponder(None if force else Transponder(*row[1:-2]), itr)
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._window, satellite)
sat_dialog = SatelliteDialog(self._app.get_active_window(), satellite)
sat = sat_dialog.run()
sat_dialog.destroy()
if sat:
view = self._sat_view
model = view.get_model()
model, paths = self._satellite_view.get_selection().get_selected_rows()
if satellite and edited_itr:
model.set(edited_itr, {0: sat.name, 10: sat.flags, 11: sat.position})
model.set(edited_itr, {i: v for i, v in enumerate(sat)})
else:
index = self.get_sat_position_index(sat.position, model)
model.insert(None, index, [sat.name, *self._aggr, sat.flags, sat.position])
scroll_to(index, view)
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._sat_view, "Please, select only one satellite!")
paths = self.check_selection(self._satellite_view, "Please, select only one satellite!")
if paths is None:
return
elif len(paths) == 0:
show_dialog(DialogType.ERROR, self._window, "No satellite is selected!")
self._app.show_error_message("No satellite is selected!")
return
dialog = TransponderDialog(self._window, transponder)
dialog = TransponderDialog(self._app.get_active_window(), transponder)
tr = dialog.run()
dialog.destroy()
if tr:
view = self._sat_view
model = view.get_model()
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:
model.set(edited_itr, {1: tr.frequency, 2: tr.symbol_rate, 3: tr.polarization,
4: tr.fec_inner, 5: tr.system, 6: tr.modulation,
7: tr.pls_mode, 8: tr.pls_code, 9: tr.is_id})
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:
row = ["Transponder:", *tr, None, None]
model, paths = view.get_selection().get_selected_rows()
itr = model.get_iter(paths[0])
view.expand_row(paths[0], 0)
# Get parent iter if selected transponder
parent_itr = model.iter_parent(itr)
if parent_itr:
itr = parent_itr
freq = int(tr.frequency if tr.frequency else 0)
tr_itr = model.iter_children(itr)
# Inserting according to frequency value.
while tr_itr:
cur_freq = int(model.get_value(tr_itr, 1))
if freq <= cur_freq:
path = model.get_path(tr_itr)
index = path.get_indices()[1]
model.insert(model.iter_parent(tr_itr), index, row)
scroll_to(path, view)
break
else:
tr_itr = model.iter_next(tr_itr)
else:
itr = model.append(itr, row)
scroll_to(model.get_path(itr), view)
def get_sat_position_index(self, pos, model):
""" Search and returns index after given position """
pos = int(pos)
row = next(filter(lambda r: int(r[-1]) >= pos, model), None)
return row.path[0] if row else len(model)
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
Returns selected path or None.
"""
model, paths = view.get_selection().get_selected_rows()
if len(paths) > 1:
show_dialog(DialogType.ERROR, self._window, message)
self._app.show_error_message(message)
return
return paths
@staticmethod
def on_remove(view):
def on_remove(self, view):
""" Removes selected satellites and transponders. """
selection = view.get_selection()
model, paths = selection.get_selected_rows()
for itr in [model.get_iter(path) for path in paths]:
model.remove(itr)
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, view):
if show_dialog(DialogType.QUESTION, self._window) == Gtk.ResponseType.CANCEL:
def on_save(self):
if show_dialog(DialogType.QUESTION, self._app.app_window) == Gtk.ResponseType.CANCEL:
return
model = view.get_model()
satellites = []
model.foreach(self.parse_data, satellites)
write_satellites(satellites, self._data_path)
write_satellites((Satellite(*r) for r in self._satellite_view.get_model()),
self._settings.profile_data_path + "satellites.xml")
def on_save_as(self, item):
response = self.get_file_dialog_response(Gtk.FileChooserAction.SAVE)
if response == Gtk.ResponseType.CANCEL:
return
show_dialog(DialogType.ERROR, transient=self._window, text="Not implemented yet!")
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._window, self._settings, self._sat_view.get_model()).show()
@staticmethod
def parse_data(model, path, itr, sats):
if model.iter_has_child(itr):
num_of_children = model.iter_n_children(itr)
transponders = []
num_columns = model.get_n_columns()
for num in range(num_of_children):
transponder_itr = model.iter_nth_child(itr, num)
transponder = model.get(transponder_itr, *[item for item in range(num_columns)])
transponders.append(Transponder(*transponder[1:-2]))
sat = model.get(itr, *[item for item in range(num_columns)])
satellite = Satellite(sat[0], sat[-2], sat[-1], transponders)
sats.append(satellite)
SatellitesUpdateDialog(self._app.get_active_window(), self._settings, self._satellite_view.get_model()).show()
# ***************** Transponder dialog *******************#
@@ -306,13 +290,8 @@ class TransponderDialog:
def __init__(self, transient, transponder: Transponder = None):
handlers = {"on_entry_changed": self.on_entry_changed}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("transponder_dialog", "pol_store", "fec_store", "mod_store", "system_store",
"pls_mode_store"))
builder.connect_signals(handlers)
objects = ("transponder_dialog", "pol_store", "fec_store", "mod_store", "system_store", "pls_mode_store")
builder = get_builder(_UI_PATH, handlers, use_str=True, objects=objects)
self._dialog = builder.get_object("transponder_dialog")
self._dialog.set_transient_for(transient)
@@ -325,6 +304,7 @@ class TransponderDialog:
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")
# pattern for frequency and rate entries (only digits)
self._pattern = re.compile(r"\D")
# style
@@ -357,6 +337,7 @@ class TransponderDialog:
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(),
@@ -367,7 +348,8 @@ class TransponderDialog:
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())
is_id=self._is_id_entry.get_text(),
t2mi_plp_id=self._t2mi_plp_id_entry.get_text())
def on_entry_changed(self, entry):
entry.set_name("digit-entry" if self._pattern.search(entry.get_text()) else "GtkEntry")
@@ -381,6 +363,8 @@ class TransponderDialog:
return False
elif self._pattern.search(tr.pls_code) or self._pattern.search(tr.is_id):
return False
elif self._pattern.search(tr.t2mi_plp_id):
return False
return True
@@ -390,22 +374,20 @@ class TransponderDialog:
class SatelliteDialog:
""" Shows dialog for adding or edit satellite """
def __init__(self, transient, satellite: Satellite = None):
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("satellite_dialog", "side_store", "pos_adjustment"))
def __init__(self, transient, satellite=None):
builder = get_builder(_UI_PATH, use_str=True, objects=("satellite_dialog", "side_store", "pos_adjustment"))
self._dialog = builder.get_object("satellite_dialog")
self._dialog.set_transient_for(transient)
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 []
if satellite:
self._sat_name.set_text(satellite.name[0:satellite.name.find("(")].strip())
self._sat_name.set_text(satellite.name)
pos = satellite.position
pos = float("{}.{}".format(pos[:-1], pos[-1:]))
pos = float(f"{pos[:-1]}.{pos[-1:]}")
self._sat_position.set_value(fabs(pos))
self._side.set_active(0 if pos >= 0 else 1) # E or W
@@ -422,10 +404,9 @@ class SatelliteDialog:
name = self._sat_name.get_text()
pos = round(self._sat_position.get_value(), 1)
side = self._side.get_active()
name = "{} ({}{})".format(name, pos, self._side.get_active_id())
pos = "{}{}{}".format("-" if side == 1 else "", *str(pos).split("."))
return Satellite(name=name, flags="0", position=pos, transponders=None)
return Satellite(name=name, flags="0", position=pos, transponders=self._transponders)
# ********************** Update dialogs ************************ #
@@ -438,6 +419,7 @@ class UpdateDialog:
"on_receive_data": self.on_receive_data,
"on_cancel_receive": self.on_cancel_receive,
"on_satellite_toggled": self.on_satellite_toggled,
"on_satellite_changed": self.on_satellite_changed,
"on_transponder_toggled": self.on_transponder_toggled,
"on_info_bar_close": self.on_info_bar_close,
"on_filter_toggled": self.on_filter_toggled,
@@ -446,24 +428,19 @@ class UpdateDialog:
"on_select_all": self.on_select_all,
"on_unselect_all": self.on_unselect_all,
"on_filter": self.on_filter,
"on_search": self.on_search,
"on_search_down": self.on_search_down,
"on_search_up": self.on_search_up,
"on_quit": self.on_quit}
self._settings = settings
self._download_task = False
self._parser = None
self._size_name = "{}_window_size".format("_".join(re.findall("[A-Z][^A-Z]*", self.__class__.__name__))).lower()
self._size_name = f"{'_'.join(re.findall('[A-Z][^A-Z]*', self.__class__.__name__))}_window_size".lower()
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
("satellites_update_window", "update_source_store", "update_sat_list_store",
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", "update_transponder_store", "update_service_store"))
builder.connect_signals(handlers)
"remove_selection_image", "sat_update_cancel_image", "sat_receive_image",
"sat_update_image", "update_transponder_store", "update_service_store"))
self._window = builder.get_object("satellites_update_window")
self._window.set_transient_for(transient)
@@ -475,12 +452,20 @@ class UpdateDialog:
self._transponder_view = builder.get_object("sat_update_tr_view")
self._service_view = builder.get_object("sat_update_srv_view")
self._source_box = builder.get_object("source_combo_box")
self._sat_update_expander = builder.get_object("sat_update_expander")
self._text_view = builder.get_object("text_view")
self._receive_button = builder.get_object("receive_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
self._filter_bar = builder.get_object("sat_update_filter_bar")
self._from_pos_button = builder.get_object("from_pos_button")
@@ -490,11 +475,18 @@ class UpdateDialog:
self._filter_model = builder.get_object("update_sat_list_model_filter")
self._filter_model.set_visible_func(self.filter_function)
self._filter_positions = (0, 0)
# Search
self._filter_bar.bind_property("search-mode-enabled", self._filter_bar, "visible")
# Log.
self._log_frame = builder.get_object("log_frame")
builder.get_object("log_info_bar").connect("response", lambda b, r: self._log_frame.set_visible(False))
# Search.
self._search_bar = builder.get_object("sat_update_search_bar")
self._search_provider = SearchProvider((self._sat_view,),
builder.get_object("sat_update_search_down_button"),
builder.get_object("sat_update_search_up_button"))
self._search_bar.bind_property("search-mode-enabled", self._search_bar, "visible")
search_provider = SearchProvider(self._sat_view,
builder.get_object("sat_update_search_entry"),
builder.get_object("sat_update_search_down_button"),
builder.get_object("sat_update_search_up_button"))
builder.get_object("sat_update_find_button").connect("toggled", search_provider.on_search_toggled)
window_size = self._settings.get(self._size_name)
if window_size:
@@ -513,25 +505,39 @@ class UpdateDialog:
self._receive_button.set_visible(not value)
@run_idle
def on_update_satellites_list(self, item):
def on_update_satellites_list(self, item=None):
if self.is_download:
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return
model = get_base_model(self._sat_view.get_model())
model.clear()
self.clear_data()
self.is_download = True
self._sat_view.set_sensitive(False)
src = self._source_box.get_active()
if not self._parser:
self._parser = SatellitesParser()
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):
sats = self._parser.get_satellites_list(SatelliteSource.FLYSAT if src == 0 else SatelliteSource.LYNGSAT)
if sats:
callback(sats)
sat_src = SatelliteSource.FLYSAT
if src == 1:
sat_src = SatelliteSource.LYNGSAT
elif src == 2:
sat_src = SatelliteSource.KINGOFSAT
sats = self._parser.get_satellites_list(sat_src)
callback(sats)
self.is_download = False
@run_idle
@@ -540,6 +546,9 @@ class UpdateDialog:
for sat in sats:
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):
if self.is_download:
@@ -547,8 +556,8 @@ class UpdateDialog:
return
@run_idle
def update_expander(self):
self._sat_update_expander.set_expanded(True)
def update_log_visibility(self):
self._log_frame.set_visible(True)
self._text_view.get_buffer().set_text("", 0)
def append_output(self):
@@ -563,6 +572,9 @@ class UpdateDialog:
def on_cancel_receive(self, item=None):
self._download_task = False
def on_satellite_changed(self, box):
self.on_update_satellites_list()
def on_satellite_toggled(self, toggle, path):
model = self._sat_view.get_model()
self.update_state(model, path, not toggle.get_active())
@@ -614,15 +626,6 @@ class UpdateDialog:
to_pos = round(self._to_pos_button.get_value(), 1) * (-1 if self._filter_to_combo_box.get_active() else 1)
return from_pos, to_pos
def on_search(self, entry):
self._search_provider.search(entry.get_text())
def on_search_down(self, item):
self._search_provider.on_search_down()
def on_search_up(self, item):
self._search_provider.on_search_up()
def on_select_all(self, view):
self.update_selection(view, True)
@@ -651,6 +654,7 @@ class SatellitesUpdateDialog(UpdateDialog):
super().__init__(transient=transient, settings=settings)
self._main_model = main_model
self._source_box.connect("changed", self.on_update_satellites_list)
@run_idle
def on_receive_data(self, item):
@@ -663,7 +667,7 @@ class SatellitesUpdateDialog(UpdateDialog):
@run_task
def receive_satellites(self):
self.is_download = True
self.update_expander()
self.update_log_visibility()
model = self._sat_view.get_model()
start = time.time()
@@ -686,32 +690,29 @@ class SatellitesUpdateDialog(UpdateDialog):
sats.append(data)
appender.send("-" * 75 + "\n")
appender.send("Consumed: {:0.0f}s, {} satellites received.".format(time.time() - start, len(sats)))
appender.close()
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[-1]
pos = row[0]
if pos in sats:
sat = sats.pop(pos)
itr = row.iter
self.update_satellite(itr, row, sat)
appender.send(f"Updating satellite: {row[0]}\n")
GLib.idle_add(self._main_model.set, row.iter, {i: v for i, v in enumerate(sat)})
for sat in sats.values():
append_satellite(self._main_model, sat)
for p, s in sats.items():
appender.send(f"Adding satellite: {s.name}\n")
self.append_satellite(s)
appender.send("-" * 75 + "\n")
appender.send(f"Consumed: {time.time() - start:0.0f}s, {sat_count} satellites received.\n")
appender.close()
self.is_download = False
@run_idle
def update_satellite(self, itr, row, sat):
if self._main_model.iter_has_child(itr):
children = row.iterchildren()
for ch in children:
self._main_model.remove(ch.iter)
for tr in sat[3]:
self._main_model.append(itr, ["Transponder:", *tr, None, None])
def append_satellite(self, sat):
self._main_model.append(sat)
class ServicesUpdateDialog(UpdateDialog):
@@ -726,11 +727,6 @@ class ServicesUpdateDialog(UpdateDialog):
self._services = {}
self._selected_transponders = set()
self._services_parser = ServicesParser(source=SatelliteSource.LYNGSAT)
self._transponder_paned.set_visible(True)
s_model = self._source_box.get_model()
s_model.remove(s_model.get_iter_first())
self._source_box.set_active(0)
# Transponder view popup menu
tr_popup_menu = Gtk.Menu()
select_all_item = Gtk.ImageMenuItem.new_from_stock("gtk-select-all")
@@ -747,6 +743,11 @@ 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_paned.set_visible(True)
self._source_box.remove(0)
self._source_box.connect("changed", self.on_update_satellites_list)
self._source_box.set_active(0)
@run_idle
def on_receive_data(self, item):
if self.is_download:
@@ -758,7 +759,7 @@ class ServicesUpdateDialog(UpdateDialog):
@run_task
def receive_services(self):
self.is_download = True
self.update_expander()
self.update_log_visibility()
model = self._sat_view.get_model()
appender = self.append_output()
next(appender)
@@ -767,7 +768,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]):
@@ -779,7 +780,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)
@@ -794,13 +795,13 @@ class ServicesUpdateDialog(UpdateDialog):
self.is_download = False
return
appender.send("Getting transponders for: {}.\n".format(sat_names.get(futures[future])))
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")
appender.send("{} transponders received.\n\n".format(len(t_urls)))
appender.send(f"{len(t_urls)} transponders received.\n\n")
non_cached_ts = []
for tr in t_urls:
@@ -816,18 +817,22 @@ class ServicesUpdateDialog(UpdateDialog):
self.is_download = False
return
appender.send("Getting services for: {}.\n".format(t_names.get(futures[future], "")))
list(map(services.append, future.result()))
appender.send(f"Getting services for: {t_names.get(futures[future], '')}.\n")
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("Consumed: {:0.0f}s, {} services received.".format(time.time() - start, len(services)))
appender.send(f"Consumed: {time.time() - start:0.0f}s, {len(services)} services received.")
try:
from app.eparser.enigma.lamedb import get_services_lines, get_services_list
# Used for double checking!
srvs = get_services_list("".join(get_services_lines(services)))
from app.eparser.enigma.lamedb import LameDbReader
# Used for double check!
reader = LameDbReader(path=None)
srvs = reader.get_services_list("".join(reader.get_services_lines(services)))
except ValueError as e:
log("ServicesUpdateDialog [on receive data] error: {}".format(e))
log(f"ServicesUpdateDialog [on receive data] error: {e}")
else:
self._callback(srvs)
@@ -835,9 +840,13 @@ class ServicesUpdateDialog(UpdateDialog):
@run_task
def get_sat_list(self, src, callback):
sats = self._parser.get_satellites_list(SatelliteSource.LYNGSAT)
if sats:
callback(sats)
sat_src = SatelliteSource.LYNGSAT
if src == 1:
sat_src = SatelliteSource.KINGOFSAT
self._services_parser.source = sat_src
sats = self._parser.get_satellites_list(sat_src)
callback(sats)
self.is_download = False
def on_satellite_toggled(self, toggle, path):
@@ -879,6 +888,9 @@ class ServicesUpdateDialog(UpdateDialog):
@run_task
def on_activate_satellite(self, view, path, column):
GLib.idle_add(self._transponder_view.get_model().clear)
GLib.idle_add(self._service_view.get_model().clear)
model = view.get_model()
itr = model.get_iter(path)
url, selected = model.get_value(itr, 3), model.get_value(itr, 4)
@@ -900,6 +912,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):
@@ -919,6 +932,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()
@@ -930,17 +944,5 @@ class ServicesUpdateDialog(UpdateDialog):
self.update_sat_state(m, s_path, select)
# ************************* Commons ************************* #
@run_idle
def append_satellite(model, sat):
""" Common function for append satellite to the model """
name, flags, pos, transponders = sat
parent = model.append(None, [name, *(None,) * 9, flags, pos])
for transponder in transponders:
model.append(parent, ["Transponder:", *transponder, None, None])
if __name__ == "__main__":
pass

View File

@@ -1,31 +1,41 @@
""" This is helper module for search features """
from app.commons import run_with_delay
class SearchProvider:
def __init__(self, views, down_button, up_button):
def __init__(self, view, entry, down_button, up_button, columns=None):
self._paths = []
self._current_index = -1
self._max_indexes = 0
self._views = views
self._view = view
self._entry = entry
self._up_button = up_button
self._down_button = down_button
self._columns = columns
entry.connect("changed", self.on_search)
self._down_button.connect("clicked", self.on_search_down)
self._up_button.connect("clicked", self.on_search_up)
def search(self, text):
self._current_index = -1
self._paths.clear()
for view in self._views:
model = view.get_model()
selection = view.get_selection()
selection.unselect_all()
if not text:
continue
model = self._view.get_model()
selection = self._view.get_selection()
if not selection:
return
text = text.upper()
for r in model:
if text in str(r[:]).upper():
path = r.path
selection.select_path(r.path)
self._paths.append((view, path))
selection.unselect_all()
if not text:
return
text = text.upper()
for r in model:
data = [r[i] for i in self._columns] if self._columns else r[:]
if next((s for s in data if text in str(s).upper()), False):
path = r.path
selection.select_path(r.path)
self._paths.append(path)
self._max_indexes = len(self._paths) - 1
if self._max_indexes > 0:
@@ -34,16 +44,15 @@ class SearchProvider:
self.update_navigation_buttons()
def scroll_to(self, index):
view, path = self._paths[index]
view.scroll_to_cell(path, None)
self._view.scroll_to_cell(self._paths[index], None)
self.update_navigation_buttons()
def on_search_down(self):
def on_search_down(self, button=None):
if self._current_index < self._max_indexes:
self._current_index += 1
self.scroll_to(self._current_index)
def on_search_up(self):
def on_search_up(self, button=None):
if self._current_index > -1:
self._current_index -= 1
self.scroll_to(self._current_index)
@@ -52,6 +61,13 @@ class SearchProvider:
self._up_button.set_sensitive(self._current_index > 0)
self._down_button.set_sensitive(self._current_index < self._max_indexes)
@run_with_delay(1)
def on_search(self, entry):
self.search(entry.get_text())
def on_search_toggled(self, action, value=None):
self._entry.grab_focus() if action.get_active() else self._entry.set_text("")
if __name__ == "__main__":
pass

View File

@@ -1,7 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<!-- Generated with glade 3.22.2
The MIT License (MIT)
Copyright (c) 2018-2021 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface>
<requires lib="gtk+" version="3.16"/>
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellite list editor for GNU/Linux. -->
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkListStore" id="fec_list_store">
<columns>
<!-- column-name fec -->
@@ -9,40 +40,53 @@
</columns>
<data>
<row>
<col id="0" translatable="yes">Auto</col>
<col id="0">Auto</col>
</row>
<row>
<col id="0" translatable="yes">1/2</col>
<col id="0">1/2</col>
</row>
<row>
<col id="0" translatable="yes">2/3</col>
</row>
<row>
<col id="0" translatable="yes">3/4</col>
<col id="0">3/4</col>
</row>
<row>
<col id="0" translatable="yes">5/6</col>
<col id="0">5/6</col>
</row>
<row>
<col id="0" translatable="yes">7/8</col>
<col id="0">7/8</col>
</row>
<row>
<col id="0" translatable="yes">8/9</col>
<col id="0">8/9</col>
</row>
<row>
<col id="0" translatable="yes">3/5</col>
<col id="0">3/5</col>
</row>
<row>
<col id="0" translatable="yes">4/5</col>
<col id="0">4/5</col>
</row>
<row>
<col id="0" translatable="yes">6/7</col>
<col id="0">6/7</col>
</row>
<row>
<col id="0" translatable="yes">9/10</col>
<col id="0">9/10</col>
</row>
</data>
</object>
<object class="GtkComboBox" id="rate_lp_combo_box">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="model">fec_list_store</property>
<property name="id_column">0</property>
<child>
<object class="GtkCellRendererText" id="rate_lp_cellrenderertext"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<object class="GtkListStore" id="invertion_list_store">
<columns>
<!-- column-name invertion -->
@@ -50,13 +94,13 @@
</columns>
<data>
<row>
<col id="0" translatable="yes">Off</col>
<col id="0">Off</col>
</row>
<row>
<col id="0" translatable="yes">On</col>
<col id="0">On</col>
</row>
<row>
<col id="0" translatable="yes">Auto</col>
<col id="0">Auto</col>
</row>
</data>
</object>
@@ -67,19 +111,19 @@
</columns>
<data>
<row>
<col id="0" translatable="yes">Auto</col>
<col id="0">Auto</col>
</row>
<row>
<col id="0" translatable="yes">QPSK</col>
<col id="0">QPSK</col>
</row>
<row>
<col id="0" translatable="yes">8PSK</col>
<col id="0">8PSK</col>
</row>
<row>
<col id="0" translatable="yes">16APSK</col>
<col id="0">16APSK</col>
</row>
<row>
<col id="0" translatable="yes">32APSK</col>
<col id="0">32APSK</col>
</row>
</data>
</object>
@@ -90,13 +134,13 @@
</columns>
<data>
<row>
<col id="0" translatable="yes">Off</col>
<col id="0">Off</col>
</row>
<row>
<col id="0" translatable="yes">On</col>
<col id="0">On</col>
</row>
<row>
<col id="0" translatable="yes">Auto</col>
<col id="0">Auto</col>
</row>
</data>
</object>
@@ -107,13 +151,13 @@
</columns>
<data>
<row>
<col id="0" translatable="yes">Root</col>
<col id="0">Root</col>
</row>
<row>
<col id="0" translatable="yes">Gold</col>
<col id="0">Gold</col>
</row>
<row>
<col id="0" translatable="yes">Combo</col>
<col id="0">Combo</col>
</row>
</data>
</object>
@@ -124,16 +168,16 @@
</columns>
<data>
<row>
<col id="0" translatable="yes">H</col>
<col id="0">H</col>
</row>
<row>
<col id="0" translatable="yes">V</col>
<col id="0">V</col>
</row>
<row>
<col id="0" translatable="yes">R</col>
<col id="0">R</col>
</row>
<row>
<col id="0" translatable="yes">L</col>
<col id="0">L</col>
</row>
</data>
</object>
@@ -144,21 +188,20 @@
</columns>
<data>
<row>
<col id="0" translatable="yes">35%</col>
<col id="0">35%</col>
</row>
<row>
<col id="0" translatable="yes">25%</col>
<col id="0">25%</col>
</row>
<row>
<col id="0" translatable="yes">20%</col>
<col id="0">20%</col>
</row>
<row>
<col id="0" translatable="yes">Auto</col>
<col id="0">Auto</col>
</row>
</data>
</object>
<object class="GtkAdjustment" id="sat_pos_adjustment">
<property name="lower">-180</property>
<property name="upper">180</property>
<property name="step_increment">0.10000000000000001</property>
<property name="page_increment">10</property>
@@ -210,26 +253,13 @@
</columns>
<data>
<row>
<col id="0" translatable="yes">DVB-S</col>
<col id="0">DVB-S</col>
</row>
<row>
<col id="0" translatable="yes">DVB-S2</col>
<col id="0">DVB-S2</col>
</row>
</data>
</object>
<object class="GtkComboBox" id="rate_lp_combo_box">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="model">fec_list_store</property>
<property name="id_column">0</property>
<child>
<object class="GtkCellRendererText" id="rate_lp_cellrenderertext"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<object class="GtkDialog" id="service_details_dialog">
<property name="use-header-bar">{use_header}</property>
<property name="can_focus">False</property>
@@ -242,41 +272,38 @@
<property name="type_hint">dialog</property>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="gravity">center</property>
<child type="titlebar">
<placeholder/>
</child>
<child type="action">
<object class="GtkButton" id="cancel_button">
<property name="label">gtk-cancel</property>
<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="tooltip_text" translatable="yes">Cancel</property>
<property name="use_stock">True</property>
<property name="always_show_image">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">gtk-save</property>
<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="use_stock">True</property>
<property name="always_show_image">True</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">gtk-new</property>
<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="use_stock">True</property>
<property name="always_show_image">True</property>
<property name="valign">center</property>
<signal name="clicked" handler="on_create_new" swapped="no"/>
</object>
</child>
@@ -290,6 +317,8 @@
<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>
</object>
<packing>
<property name="expand">False</property>
@@ -314,7 +343,7 @@
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<property name="spacing">5</property>
<child>
<object class="GtkGrid" id="srv_grid">
<property name="visible">True</property>
@@ -335,7 +364,7 @@
<object class="GtkEntry" id="name_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="primary_icon_name">document-edit-symbolic</property>
</object>
<packing>
<property name="left_attach">0</property>
@@ -357,7 +386,7 @@
<object class="GtkEntry" id="package_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="primary_icon_name">document-edit-symbolic</property>
</object>
<packing>
<property name="left_attach">1</property>
@@ -381,7 +410,7 @@
<property name="can_focus">True</property>
<property name="width_chars">10</property>
<property name="max_width_chars">10</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
<signal name="key-release-event" handler="update_reference" swapped="no"/>
</object>
@@ -426,7 +455,7 @@
<property name="can_focus">True</property>
<property name="width_chars">7</property>
<property name="max_width_chars">7</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
<signal name="changed" handler="update_reference" swapped="no"/>
</object>
@@ -776,10 +805,14 @@
<object class="GtkBox" id="flags_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">10</property>
<child>
<object class="GtkGrid" id="flags_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="margin_left">5</property>
<property name="margin_right">10</property>
<property name="column_spacing">2</property>
<child>
<object class="GtkLabel" id="flags_label">
@@ -852,6 +885,47 @@
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="extra_flags_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel" id="extra_pids_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Extra:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="extra_pids_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="width_chars">20</property>
<property name="max_width_chars">20</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="placeholder_text">c:000000,etc.</property>
<signal name="changed" handler="on_extra_pids_entry_changed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="caids_grid">
<property name="visible">True</property>
@@ -862,9 +936,9 @@
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="tooltip_text">C:0000,C:a1b2,etc.</property>
<property name="width_chars">15</property>
<property name="max_width_chars">26</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="width_chars">20</property>
<property name="max_width_chars">20</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="placeholder_text" translatable="yes">C:0000,C:a1b2,etc.</property>
<signal name="changed" handler="on_cas_entry_changed" swapped="no"/>
</object>
@@ -889,7 +963,7 @@
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
<property name="position">2</property>
</packing>
</child>
</object>
@@ -967,7 +1041,7 @@
<property name="can_focus">True</property>
<property name="width_chars">12</property>
<property name="max_width_chars">12</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
</object>
<packing>
@@ -993,7 +1067,7 @@
<property name="can_focus">True</property>
<property name="width_chars">12</property>
<property name="max_width_chars">12</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
</object>
<packing>
@@ -1072,7 +1146,7 @@
<property name="can_focus">True</property>
<property name="width_chars">12</property>
<property name="max_width_chars">12</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
<signal name="key-release-event" handler="update_reference" swapped="no"/>
</object>
@@ -1092,21 +1166,6 @@
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="sat_pos_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="input_purpose">number</property>
<property name="adjustment">sat_pos_adjustment</property>
<property name="digits">1</property>
<property name="numeric">True</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="tid_label">
<property name="visible">True</property>
@@ -1125,7 +1184,7 @@
<property name="can_focus">True</property>
<property name="width_chars">8</property>
<property name="max_width_chars">10</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
<signal name="key-release-event" handler="update_reference" swapped="no"/>
</object>
@@ -1152,7 +1211,7 @@
<property name="can_focus">True</property>
<property name="width_chars">8</property>
<property name="max_width_chars">10</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
<signal name="key-release-event" handler="update_reference" swapped="no"/>
</object>
@@ -1161,6 +1220,50 @@
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="sat_pos_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">1</property>
<child>
<object class="GtkSpinButton" id="sat_pos_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="input_purpose">number</property>
<property name="adjustment">sat_pos_adjustment</property>
<property name="digits">1</property>
<property name="numeric">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="pos_side_box">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="active">0</property>
<items>
<item id="E">E</item>
<item id="W">W</item>
</items>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -1317,7 +1420,7 @@
<property name="can_focus">True</property>
<property name="width_chars">8</property>
<property name="max_width_chars">10</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_digit_entry_changed" swapped="no"/>
</object>
<packing>
@@ -1343,7 +1446,7 @@
<property name="can_focus">True</property>
<property name="width_chars">8</property>
<property name="max_width_chars">10</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_digit_entry_changed" swapped="no"/>
</object>
<packing>
@@ -1369,7 +1472,7 @@
<property name="can_focus">True</property>
<property name="width_chars">8</property>
<property name="max_width_chars">10</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_digit_entry_changed" swapped="no"/>
</object>
<packing>
@@ -1498,7 +1601,7 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="stock">gtk-edit</property>
<property name="icon_name">document-edit-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
@@ -1558,28 +1661,25 @@
<property name="type_hint">dialog</property>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="gravity">center</property>
<child>
<placeholder/>
</child>
<child type="action">
<object class="GtkButton" id="tr_services_no_button">
<property name="label">gtk-cancel</property>
<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="use_stock">True</property>
<property name="always_show_image">True</property>
<property name="valign">center</property>
</object>
</child>
<child type="action">
<object class="GtkButton" id="tr_services_ok_button">
<property name="label">gtk-ok</property>
<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="use_stock">True</property>
<property name="always_show_image">True</property>
<property name="valign">center</property>
</object>
</child>
<child internal-child="vbox">
@@ -1590,6 +1690,7 @@
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="valign">center</property>
</object>
<packing>
<property name="expand">False</property>
@@ -1616,7 +1717,7 @@
<property name="margin_right">20</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="label" translatable="yes">Changes will be applied to all services of this transponder!
<property name="label" translatable="yes">Changes will be applied to all services of this transponder!
Continue?</property>
<property name="justify">center</property>
<property name="lines">2</property>

View File

@@ -1,16 +1,45 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
import os
import re
from app.commons import run_idle
from app.commons import run_idle, log
from app.eparser import Service
from app.eparser.ecommons import (MODULATION, Inversion, ROLL_OFF, Pilot, Flag, Pids, POLARIZATION, get_key_by_value,
get_value_by_name, FEC_DEFAULT, PLS_MODE, SERVICE_TYPE, T_MODULATION, C_MODULATION,
TrType, SystemCable, T_SYSTEM, BANDWIDTH, TRANSMISSION_MODE, GUARD_INTERVAL, T_FEC,
HIERARCHY)
HIERARCHY, A_MODULATION)
from app.eparser.neutrino import get_attributes, SP, KSP
from app.settings import SettingsType
from .dialogs import show_dialog, DialogType, Action, get_dialogs_string
from .dialogs import show_dialog, DialogType, Action, get_builder
from .main_helper import get_base_model
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, TEXT_DOMAIN, CODED_ICON, Column, IS_GNOME_SESSION
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, CODED_ICON, Column
_UI_PATH = UI_RESOURCES_PATH + "service_details_dialog.glade"
@@ -24,8 +53,6 @@ class ServiceDetailsDialog:
_NEUTRINO_FAV_ID = "{:x}:{:x}:{:x}"
_NEUTRINO_TRANSPONDER_DATA = "{:04x}:{:04x}:{}:{}:{}:{}:{}:{}:{}"
_DIGIT_ENTRY_ELEMENTS = ("bitstream_entry", "pcm_entry", "video_pid_entry", "pcr_pid_entry", "srv_type_entry",
"ac3_pid_entry", "ac3plus_pid_entry", "acc_pid_entry", "he_acc_pid_entry",
"teletext_pid_entry", "pls_code_entry", "stream_id_entry", "tr_flag_entry",
@@ -35,34 +62,33 @@ 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,
"on_tr_edit_toggled": self.on_tr_edit_toggled,
"update_reference": self.update_reference,
"on_cas_entry_changed": self.on_cas_entry_changed,
"on_extra_pids_entry_changed": self.on_extra_pids_entry_changed,
"on_digit_entry_changed": self.on_digit_entry_changed,
"on_non_empty_entry_changed": self.on_non_empty_entry_changed}
"on_non_empty_entry_changed": self.on_non_empty_entry_changed,
"on_cancel": lambda item: self._dialog.destroy()}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION))
builder.connect_signals(handlers)
builder = get_builder(_UI_PATH, handlers, use_str=True)
self._builder = builder
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 = None
self._satellites_xml_path = settings.data_local_path + "satellites.xml"
self._picons_dir_path = settings.picons_local_path
self._services_view = srv_view
self._fav_view = fav_view
self._tr_type = TrType.Satellite
self._picons_path = settings.profile_picons_path
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
@@ -71,6 +97,7 @@ class ServiceDetailsDialog:
self._DIGIT_PATTERN = re.compile("\\D")
self._NON_EMPTY_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
self._CAID_PATTERN = re.compile("(?:^[\\s]*$)|(C:[0-9a-fA-F]{1,4})(,C:[0-9a-fA-F]{1,4})*")
self._PIDS_PATTERN = re.compile("(?:^[\\s]*$)|(c:[0-9]{2}[0-9a-fA-F]{4})(,c:[0-9]{2}[0-9a-fA-F]{4})*")
# Buttons
self._apply_button = builder.get_object("apply_button")
self._create_button = builder.get_object("create_button")
@@ -106,6 +133,7 @@ class ServiceDetailsDialog:
self._stream_id_entry = self._digit_elements.get("stream_id_entry")
self._tr_flag_entry = self._digit_elements.get("tr_flag_entry")
self._namespace_entry = self._non_empty_elements.get("namespace_entry")
self._extra_pids_entry = builder.get_object("extra_pids_entry")
# Service elements
self._name_entry = builder.get_object("name_entry")
self._package_entry = builder.get_object("package_entry")
@@ -120,6 +148,7 @@ class ServiceDetailsDialog:
self._pids_grid = builder.get_object("pids_grid")
# Transponder elements
self._sat_pos_button = builder.get_object("sat_pos_button")
self._pos_side_box = builder.get_object("pos_side_box")
self._pol_combo_box = builder.get_object("pol_combo_box")
self._fec_combo_box = builder.get_object("fec_combo_box")
self._rate_lp_combo_box = builder.get_object("rate_lp_combo_box")
@@ -137,7 +166,7 @@ class ServiceDetailsDialog:
self._TRANSPONDER_ELEMENTS = (self._sat_pos_button, self._pol_combo_box, self._invertion_combo_box,
self._sys_combo_box, self._freq_entry, self._transponder_id_entry,
self._network_id_entry, self._namespace_entry, self._fec_combo_box,
self._rate_entry, self._rate_lp_combo_box)
self._rate_entry, self._rate_lp_combo_box, self._pos_side_box)
if self._action is Action.EDIT:
self.update_data_elements()
@@ -145,12 +174,7 @@ class ServiceDetailsDialog:
self.init_default_data_elements()
def show(self):
response = self._dialog.run()
if response == Gtk.ResponseType.OK:
pass
self._dialog.destroy()
return response
self._dialog.show()
@run_idle
def init_default_data_elements(self):
@@ -198,8 +222,7 @@ class ServiceDetailsDialog:
self._package_entry.set_text(srv.package)
self._sid_entry.set_text(str(int(srv.ssid, 16)))
# Transponder
if self._s_type is SettingsType.ENIGMA_2:
self._tr_type = TrType(srv.transponder_type)
self._tr_type = TrType(srv.transponder_type)
self._freq_entry.set_text(srv.freq)
self._rate_entry.set_text(srv.rate)
self.select_active_text(self._pol_combo_box, srv.pol)
@@ -209,6 +232,8 @@ class ServiceDetailsDialog:
self.update_ui_for_terrestrial()
elif self._tr_type is TrType.Cable:
self.update_ui_for_cable()
elif self._tr_type is TrType.ATSC:
self.update_ui_for_atsc()
else:
self.set_sat_positions(srv.pos)
@@ -234,7 +259,7 @@ class ServiceDetailsDialog:
def init_enigma2_flags(self, flags):
f_flags = list(filter(lambda x: x.startswith("f:"), flags))
if f_flags:
value = int(f_flags[0][2:])
value = Flag.parse(f_flags[0])
self._keep_check_button.set_active(Flag.is_keep(value))
self._hide_check_button.set_active(Flag.is_hide(value))
self._use_pids_check_button.set_active(Flag.is_pids(value))
@@ -248,6 +273,7 @@ class ServiceDetailsDialog:
def init_enigma2_pids(self, flags):
pids = list(filter(lambda x: x.startswith("c:"), flags))
if pids:
extra_pids = []
for pid in pids:
if pid.startswith(Pids.VIDEO.value):
self._video_pid_entry.set_text(str(int(pid[4:], 16)))
@@ -260,15 +286,19 @@ class ServiceDetailsDialog:
elif pid.startswith(Pids.AC3.value):
self._ac3_pid_entry.set_text(str(int(pid[4:], 16)))
elif pid.startswith(Pids.VIDEO_TYPE.value):
pass
extra_pids.append(pid)
elif pid.startswith(Pids.AUDIO_CHANNEL.value):
pass
extra_pids.append(pid)
elif pid.startswith(Pids.BIT_STREAM_DELAY.value):
self._bitstream_entry.set_text(str(int(pid[4:], 16)))
elif pid.startswith(Pids.PCM_DELAY.value):
self._pcm_entry.set_text(str(int(pid[4:], 16)))
elif pid.startswith(Pids.SUBTITLE.value):
pass
extra_pids.append(pid)
else:
extra_pids.append(pid)
self._extra_pids_entry.set_text(",".join(extra_pids))
def init_enigma2_transponder_data(self, srv):
""" Transponder data initialisation """
@@ -310,16 +340,23 @@ class ServiceDetailsDialog:
self.select_active_text(self._pls_mode_combo_box, HIERARCHY.get(tr_data[7]))
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[8]).name)
self.select_active_text(self._sys_combo_box, T_SYSTEM.get(tr_data[9]))
elif tr_type is TrType.ATSC:
self._sys_combo_box.set_active(0)
self.select_active_text(self._mod_combo_box, A_MODULATION.get(tr_data[2]))
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[1]).name)
# Should be called last to properly initialize the reference
self._srv_type_entry.set_text(data[4])
# ***************** Init Neutrino data *********************#
def init_neutrino_data(self, srv):
tr_data = srv.transponder.split(":")
self._transponder_id_entry.set_text(str(int(tr_data[0], 16)))
self._network_id_entry.set_text(str(int(tr_data[1], 16)))
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[3]).name)
if self._tr_type is not TrType.Satellite:
return
tr_data = get_attributes(srv.transponder)
self._transponder_id_entry.set_text(str(int(tr_data.get("id", "0"), 16)))
self._network_id_entry.set_text(str(int(tr_data.get("on", "0"), 16)))
self.select_active_text(self._invertion_combo_box, Inversion(tr_data.get("inv", "2")).name)
self.select_active_text(self._service_type_combo_box, srv.service_type)
self.update_reference_entry()
@@ -331,12 +368,14 @@ class ServiceDetailsDialog:
tr_grid.set_margin_bottom(5)
self._builder.get_object("tr_extra_expander").set_visible(False)
self._builder.get_object("srv_separator").set_visible(False)
self._package_entry.set_sensitive(False)
# ***************** Init Sat positions *********************#
def set_sat_positions(self, sat_pos):
""" Sat positions initialisation """
self._sat_pos_button.set_value(float(sat_pos))
self._sat_pos_button.set_value(float(sat_pos[:-1]))
self._pos_side_box.set_active_id(sat_pos[-1:])
def on_system_changed(self, box):
if not self._tr_edit_switch.get_active():
@@ -369,6 +408,10 @@ class ServiceDetailsDialog:
self.save_data()
def save_data(self):
if self._s_type is SettingsType.NEUTRINO_MP and self._tr_type is not TrType.Satellite:
show_dialog(DialogType.ERROR, transient=self._dialog, text="Not implemented yet!")
return
if not self.is_data_correct():
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
@@ -376,18 +419,19 @@ class ServiceDetailsDialog:
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
self.on_edit() if self._action is Action.EDIT else self.on_new()
self._dialog.destroy()
if self.on_edit() if self._action is Action.EDIT else self.on_new():
self._dialog.destroy()
def on_new(self):
""" Create new service. """
service = self.get_service(*self.get_srv_data(), self.get_satellite_transponder_data())
show_dialog(DialogType.ERROR, transient=self._dialog, text="Not implemented yet!")
return True
def on_edit(self):
""" Edit current service. """
fav_id, data_id = self.get_srv_data()
# transponder
# Transponder
transponder = self._old_service.transponder
if self._tr_edit_switch.get_active():
try:
@@ -397,17 +441,24 @@ class ServiceDetailsDialog:
transponder = self.get_terrestrial_transponder_data()
elif self._tr_type is TrType.Cable:
transponder = self.get_cable_transponder_data()
elif self._tr_type is TrType.ATSC:
transponder = self.get_atsc_transponder_data()
except Exception as e:
print(e)
log("Edit service error: {}".format(e))
show_dialog(DialogType.ERROR, transient=self._dialog, text="Error getting transponder parameters!")
else:
if self._transponder_services_iters:
self.update_transponder_services(transponder)
# service
self.update_transponder_services(transponder, self.get_sat_position())
# Service
service = self.get_service(fav_id, data_id, transponder)
old_fav_id = self._old_service.fav_id
if old_fav_id != fav_id:
if fav_id in self._services:
msg = "{}\n\n\t{}".format("A similar service is already in this list!", "Are you sure?")
if show_dialog(DialogType.QUESTION, transient=self._dialog, text=msg) != Gtk.ResponseType.OK:
return False
self.update_bouquets(fav_id, old_fav_id)
self._services[fav_id] = service
if self._old_service.picon_id != service.picon_id:
@@ -415,15 +466,16 @@ class ServiceDetailsDialog:
flags = service.flags_cas
extra_data = {Column.SRV_TOOLTIP: None, Column.SRV_BACKGROUND: None}
if flags:
if self._s_type is SettingsType.ENIGMA_2 and flags:
f_flags = list(filter(lambda x: x.startswith("f:"), flags.split(",")))
if f_flags and Flag.is_new(int(f_flags[0][2:])):
if f_flags and Flag.is_new(Flag.parse(f_flags[0])):
extra_data[Column.SRV_BACKGROUND] = self._new_color
self._current_model.set(self._current_itr, extra_data)
self._current_model.set(self._current_itr, {i: v for i, v in enumerate(service)})
self.update_fav_view(self._old_service, service)
self._old_service = service
return True
def update_bouquets(self, fav_id, old_fav_id):
self._services.pop(old_fav_id, None)
@@ -452,13 +504,13 @@ class ServiceDetailsDialog:
Column.FAV_PICON: new_service.picon})
def update_picon_name(self, old_name, new_name):
if not os.path.isdir(self._picons_dir_path):
if not os.path.isdir(self._picons_path):
return
for file_name in os.listdir(self._picons_dir_path):
for file_name in os.listdir(self._picons_path):
if file_name == old_name:
old_file = os.path.join(self._picons_dir_path, old_name)
new_file = os.path.join(self._picons_dir_path, new_name)
old_file = os.path.join(self._picons_path, old_name)
new_file = os.path.join(self._picons_path, new_name)
os.rename(old_file, new_file)
break
@@ -491,7 +543,9 @@ class ServiceDetailsDialog:
if self._s_type is SettingsType.ENIGMA_2:
return self.get_enigma2_flags()
elif self._s_type is SettingsType.NEUTRINO_MP:
return self._old_service.flags_cas
flags = get_attributes(self._old_service.flags_cas)
flags["position"] = self.get_sat_position()
return SP.join("{}{}{}".format(k, KSP, v) for k, v in flags.items())
def get_enigma2_flags(self):
flags = ["p:{}".format(self._package_entry.get_text())]
@@ -521,6 +575,9 @@ class ServiceDetailsDialog:
pcm_pid = self._pcm_entry.get_text()
if pcm_pid:
flags.append("{}{:04x}".format(Pids.PCM_DELAY.value, int(pcm_pid)))
extra_pids = self._extra_pids_entry.get_text()
if extra_pids:
flags.append(extra_pids)
# flags
f_flags = Flag.KEEP.value if self._keep_check_button.get_active() else 0
f_flags = f_flags + Flag.HIDE.value if self._hide_check_button.get_active() else f_flags
@@ -542,8 +599,12 @@ class ServiceDetailsDialog:
fav_id = self._ENIGMA2_FAV_ID.format(ssid, tr_id, net_id, namespace)
return fav_id, data_id
elif self._s_type is SettingsType.NEUTRINO_MP:
data = get_attributes(self._old_service.data_id)
data["n"] = self._name_entry.get_text()
data["t"] = "{:x}".format(int(service_type))
data["i"] = "{:04x}".format(ssid)
fav_id = self._NEUTRINO_FAV_ID.format(tr_id, net_id, ssid)
return fav_id, self._old_service.data_id
return fav_id, SP.join("{}{}{}".format(k, KSP, v) for k, v in data.items())
# ***************** Transponder ********************* #
@@ -551,27 +612,27 @@ class ServiceDetailsDialog:
freq = self._freq_entry.get_text()
fec = self._fec_combo_box.get_active_id()
system = self._sys_combo_box.get_active_id()
o_srv = self._old_service
if self._tr_type is TrType.Satellite or self._s_type is SettingsType.NEUTRINO_MP:
freq = self._freq_entry.get_text()
rate = self._rate_entry.get_text()
pol = self._pol_combo_box.get_active_id()
pos = str(round(self._sat_pos_button.get_value(), 1))
pos = "{}{}".format(round(self._sat_pos_button.get_value(), 1), self._pos_side_box.get_active_id())
return freq, rate, pol, fec, system, pos
elif self._tr_type is TrType.Terrestrial:
o_srv = self._old_service
elif self._tr_type in (TrType.Terrestrial, TrType.ATSC):
return freq, o_srv.rate, o_srv.pol, fec, system, o_srv.pos
elif self._tr_type is TrType.Cable:
o_srv = self._old_service
return freq, self._rate_entry.get_text(), o_srv.pol, fec, o_srv.system, o_srv.pos
def get_satellite_transponder_data(self):
sys = self._sys_combo_box.get_active_id()
freq = self._freq_entry.get_text()
rate = self._rate_entry.get_text()
freq = "{}000".format(self._freq_entry.get_text())
rate = "{}000".format(self._rate_entry.get_text())
pol = self.get_value_from_combobox_id(self._pol_combo_box, POLARIZATION)
fec = self.get_value_from_combobox_id(self._fec_combo_box, FEC_DEFAULT)
sat_pos = str(round(self._sat_pos_button.get_value(), 1)).replace(".", "")
sat_pos = self.get_sat_position()
inv = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
srv_sys = "0" # !!!
@@ -588,19 +649,31 @@ class ServiceDetailsDialog:
pls_code = self._pls_code_entry.get_text()
st_id = self._stream_id_entry.get_text()
pls = ":{}:{}:{}".format(st_id, pls_code, pls_mode) if pls_mode and pls_code and st_id else ""
return "{}:{}:{}:{}:{}{}".format(dvb_s_tr, flag, mod, roll_off, pilot, pls)
elif self._s_type is SettingsType.NEUTRINO_MP:
on_id, tr_id = int(self._network_id_entry.get_text()), int(self._transponder_id_entry.get_text())
mod = self.get_value_from_combobox_id(self._mod_combo_box, MODULATION) if sys == "DVB-S2" else None
srv_sys = None
return self._NEUTRINO_TRANSPONDER_DATA.format(tr_id, on_id, freq, inv, rate, fec, pol, mod, srv_sys)
tr_data = get_attributes(self._old_service.transponder)
tr_data["frq"] = freq
tr_data["sr"] = rate
tr_data["pol"] = pol
tr_data["fec"] = fec
tr_data["on"] = "{:04x}".format(int(self._network_id_entry.get_text()))
tr_data["id"] = "{:04x}".format(int(self._transponder_id_entry.get_text()))
tr_data["inv"] = inv
return SP.join("{}{}{}".format(k, KSP, v) for k, v in tr_data.items())
def get_sat_position(self):
sat_pos = self._sat_pos_button.get_value() * (-1 if self._pos_side_box.get_active_id() == "W" else 1)
sat_pos = str(round(sat_pos, 1)).replace(".", "")
return sat_pos
def get_terrestrial_transponder_data(self):
tr_data = re.split("\s|:", self._old_service.transponder)
# frequency, bandwidth, code rate HP, code rate LP, modulation, transmission mode, guard interval, hierarchy,
# inversion, system, plp_id
# Bandwidth -> Pol, Rate HP -> FEC, TransmissionMode -> Roll off, GuardInterval -> Pilot, Hierarchy -> Pls Mode
tr_data[1] = self._freq_entry.get_text()
tr_data[1] = "{}000".format(self._freq_entry.get_text())
tr_data[2] = self.get_value_from_combobox_id(self._pol_combo_box, BANDWIDTH)
tr_data[3] = self.get_value_from_combobox_id(self._fec_combo_box, T_FEC)
tr_data[4] = self.get_value_from_combobox_id(self._rate_lp_combo_box, T_FEC)
@@ -610,28 +683,50 @@ class ServiceDetailsDialog:
tr_data[8] = self.get_value_from_combobox_id(self._pls_mode_combo_box, HIERARCHY)
tr_data[9] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
tr_data[10] = self.get_value_from_combobox_id(self._sys_combo_box, T_SYSTEM)
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
def get_cable_transponder_data(self):
tr_data = re.split("\s|:", self._old_service.transponder)
# frequency, symbol_rate, modulation, inversion, fec_inner, system;
tr_data[1] = self._freq_entry.get_text()
tr_data[2] = self._rate_entry.get_text()
tr_data[1] = "{}000".format(self._freq_entry.get_text())
tr_data[2] = "{}000".format(self._rate_entry.get_text())
tr_data[3] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
tr_data[4] = self.get_value_from_combobox_id(self._mod_combo_box, C_MODULATION)
tr_data[5] = self.get_value_from_combobox_id(self._fec_combo_box, FEC_DEFAULT)
tr_data[6] = get_value_by_name(SystemCable, self._sys_combo_box.get_active_id())
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
def update_transponder_services(self, transponder):
def get_atsc_transponder_data(self):
tr_data = re.split("\s|:", self._old_service.transponder)
# frequency, inversion, modulation, system
tr_data[1] = "{}000".format(self._freq_entry.get_text())
tr_data[2] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
tr_data[3] = self.get_value_from_combobox_id(self._mod_combo_box, A_MODULATION)
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
def update_transponder_services(self, transponder, sat_pos):
for itr in self._transponder_services_iters:
srv = self._current_model[itr][:Column.SRV_TOOLTIP]
srv = self._current_model[itr][:]
srv[Column.SRV_FREQ], srv[Column.SRV_RATE], srv[Column.SRV_POL], srv[Column.SRV_FEC], srv[
Column.SRV_SYSTEM], srv[Column.SRV_POS] = self.get_transponder_values()
srv[Column.SRV_TRANSPONDER] = transponder
srv = Service(*srv)
self._services[srv.fav_id] = self._services.pop(srv.fav_id)._replace(transponder=transponder)
self._current_model.set(itr, {i: v for i, v in enumerate(srv)})
fav_id = srv[Column.SRV_FAV_ID]
old_srv = self._services.pop(fav_id, None)
if not old_srv:
log("Update transponder services error: No service found for ID {}".format(srv[Column.SRV_FAV_ID]))
continue
if self._s_type is SettingsType.NEUTRINO_MP:
flags = get_attributes(srv[Column.SRV_CAS_FLAGS])
flags["position"] = sat_pos
srv[Column.SRV_CAS_FLAGS] = SP.join("{}{}{}".format(k, KSP, v) for k, v in flags.items())
self._services[fav_id] = Service(*srv[:Column.SRV_TOOLTIP])
self._current_model.set_row(itr, srv)
# ***************** Others *********************#
@@ -651,6 +746,9 @@ class ServiceDetailsDialog:
def on_cas_entry_changed(self, entry):
entry.set_name("GtkEntry" if self._CAID_PATTERN.fullmatch(entry.get_text()) else self._DIGIT_ENTRY_NAME)
def on_extra_pids_entry_changed(self, entry):
entry.set_name("GtkEntry" if self._PIDS_PATTERN.fullmatch(entry.get_text()) else self._DIGIT_ENTRY_NAME)
def get_value_from_combobox_id(self, box: Gtk.ComboBox, dc: dict):
cb_id = box.get_active_id()
return get_key_by_value(dc, cb_id)
@@ -660,16 +758,16 @@ class ServiceDetailsDialog:
if active and self._action is Action.EDIT:
self._transponder_services_iters = []
response = TransponderServicesDialog(self._dialog,
self._current_model,
self._services_view,
self._old_service.transponder,
self._transponder_services_iters).show()
if response == Gtk.ResponseType.CANCEL or response == -4:
if response == Gtk.ResponseType.CANCEL or response == Gtk.ResponseType.DELETE_EVENT:
switch.set_active(False)
self._transponder_services_iters = None
return
self.update_dvb_s2_elements(active and (self._sys_combo_box.get_active_id() == "DVB-S2"
or self._old_service.transponder_type in "tc"))
or self._old_service.transponder_type in "tca"))
for elem in self._TRANSPONDER_ELEMENTS:
elem.set_sensitive(active)
@@ -683,6 +781,8 @@ class ServiceDetailsDialog:
return False
if self._cas_entry.get_name() == self._DIGIT_ENTRY_NAME:
return False
if self._extra_pids_entry.get_name() == self._DIGIT_ENTRY_NAME:
return False
return True
def update_reference(self, entry, event=None):
@@ -800,6 +900,18 @@ class ServiceDetailsDialog:
self._transponder_id_entry.set_max_width_chars(8)
self._network_id_entry.set_max_width_chars(8)
def update_ui_for_atsc(self):
self.update_ui_for_cable()
tr_grid = self._builder.get_object("tr_grid")
tr_grid.remove_column(1)
tr_grid.remove_column(1)
# Init models
fec_model, modulation_model, system_model = self.get_models_for_non_satellite()
system_model.append((TrType.ATSC.name,))
[modulation_model.append((v,)) for k, v in A_MODULATION.items()]
# Extra
self._namespace_entry.set_max_width_chars(25)
def get_transponder_grid_for_non_satellite(self):
self._pids_grid.set_visible(False)
tr_grid = self._builder.get_object("tr_grid")
@@ -817,23 +929,23 @@ class ServiceDetailsDialog:
class TransponderServicesDialog:
def __init__(self, transient, model, transponder, tr_iters):
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("tr_services_dialog", "transponder_services_liststore"))
def __init__(self, transient, services_view, transponder, tr_iters):
builder = get_builder(_UI_PATH, use_str=True, objects=("tr_services_dialog", "transponder_services_liststore"))
self._dialog = builder.get_object("tr_services_dialog")
self._dialog.set_transient_for(transient)
self._srv_model = builder.get_object("transponder_services_liststore")
self.append_services(model, transponder, tr_iters)
self.append_services(services_view, transponder, tr_iters)
builder.get_object("srv_list_dialog_info_bar").connect("response", lambda bar, resp: bar.hide())
def append_services(self, model, transponder, tr_iters):
def append_services(self, view, transponder, tr_iters):
model = view.get_model()
filter_model = model.get_model()
for row in model:
if row[Column.SRV_TRANSPONDER] == transponder:
self._srv_model.append((row[Column.SRV_SERVICE], row[Column.SRV_PACKAGE], row[Column.SRV_TYPE],
row[Column.SRV_SSID], row[Column.SRV_FREQ], row[Column.SRV_POS]))
tr_iters.append(model.get_iter(row.path))
itr = model.get_iter(row.path)
tr_iters.append(filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr)))
def show(self):
response = self._dialog.run()

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,40 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
import os
import re
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
from app.ui.dialogs import show_dialog, DialogType, get_message, get_chooser_dialog
from app.settings import SettingsType, Settings, PlayStreamsMode, IS_LINUX, SEP, IS_WIN
from app.ui.dialogs import show_dialog, DialogType, get_message, get_chooser_dialog, get_builder
from .main_helper import update_entry_data, scroll_to, get_picon_pixbuf
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON
def show_settings_dialog(transient, options):
return SettingsDialog(transient, options).show()
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON, APP_FONT, IS_GNOME_SESSION
class SettingsDialog:
@@ -22,8 +46,6 @@ class SettingsDialog:
"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_apply_profile_settings": self.on_apply_profile_settings,
"on_connection_test": self.on_connection_test,
"on_info_bar_close": self.on_info_bar_close,
"on_set_color_switch": self.on_set_color_switch,
@@ -32,7 +54,6 @@ class SettingsDialog:
"on_experimental_switch": self.on_experimental_switch,
"on_yt_dl_switch": self.on_yt_dl_switch,
"on_default_path_mode_switch": self.on_default_path_mode_switch,
"on_default_data_path_changed": self.on_default_data_path_changed,
"on_profile_add": self.on_profile_add,
"on_profile_edit": self.on_profile_edit,
"on_profile_remove": self.on_profile_remove,
@@ -41,6 +62,8 @@ 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_add_picon_path": self.on_add_picon_path,
"on_remove_picon_path": self.on_remove_picon_path,
"on_lang_changed": self.on_lang_changed,
"on_main_settings_visible": self.on_main_settings_visible,
"on_http_use_ssl_toggled": self.on_http_use_ssl_toggled,
@@ -50,6 +73,7 @@ class SettingsDialog:
"on_apply_presets": self.on_apply_presets,
"on_digit_entry_changed": self.on_digit_entry_changed,
"on_view_popup_menu": self.on_view_popup_menu,
"on_list_font_reset": self.on_list_font_reset,
"on_theme_changed": self.on_theme_changed,
"on_theme_add": self.on_theme_add,
"on_theme_remove": self.on_theme_remove,
@@ -57,21 +81,21 @@ 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 = Gtk.Builder()
builder.add_from_file(UI_RESOURCES_PATH + "settings_dialog.glade")
builder.connect_signals(handlers)
builder = get_builder(UI_RESOURCES_PATH + "settings_dialog.glade", handlers)
self._dialog = builder.get_object("settings_dialog")
self._dialog.set_transient_for(transient)
self._header_bar = builder.get_object("header_bar")
self._dialog.set_border_width(0)
self._dialog.set_margin_left(0)
self._main_stack = builder.get_object("main_stack")
# Network
# Network.
self._host_field = builder.get_object("host_field")
self._port_field = builder.get_object("port_field")
self._login_field = builder.get_object("login_field")
@@ -80,31 +104,34 @@ class SettingsDialog:
self._http_use_ssl_check_button = builder.get_object("http_use_ssl_check_button")
self._telnet_port_field = builder.get_object("telnet_port_field")
self._telnet_timeout_spin_button = builder.get_object("telnet_timeout_spin_button")
# Test
self._reset_button = builder.get_object("reset_button")
# Test.
self._ftp_radio_button = builder.get_object("ftp_radio_button")
self._http_radio_button = builder.get_object("http_radio_button")
# Paths
# Network paths.
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._data_dir_field = builder.get_object("data_dir_field")
self._picons_field = builder.get_object("picons_field")
self._picons_dir_field = builder.get_object("picons_dir_field")
self._backup_dir_field = builder.get_object("backup_dir_field")
self._default_data_dir_field = builder.get_object("default_data_dir_field")
self._record_data_dir_field = builder.get_object("record_data_dir_field")
self._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._default_data_paths_switch = builder.get_object("default_data_paths_switch")
# Info bar
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)
# Info bar.
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label")
self._test_spinner = builder.get_object("test_spinner")
# Settings type
# 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
header_separator = builder.get_object("header_separator")
# 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")
@@ -116,57 +143,53 @@ class SettingsDialog:
self._audio_channels_combo_box = builder.get_object("audio_channels_combo_box")
self._audio_sample_rate_combo_box = builder.get_object("audio_sample_rate_combo_box")
self._audio_codec_combo_box = builder.get_object("audio_codec_combo_box")
self._apply_presets_button.bind_property("visible", header_separator, "visible")
self._transcoding_switch.bind_property("active", builder.get_object("record_box"), "sensitive")
self._edit_preset_switch.bind_property("active", self._apply_presets_button, "sensitive")
self._edit_preset_switch.bind_property("active", builder.get_object("video_options_frame"), "sensitive")
self._edit_preset_switch.bind_property("active", builder.get_object("audio_options_frame"), "sensitive")
self._play_in_built_radio_button = builder.get_object("play_in_built_radio_button")
self._play_in_vlc_radio_button = builder.get_object("play_in_vlc_radio_button")
self._get_m3u_radio_button = builder.get_object("get_m3u_radio_button")
# Program
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")
self._enable_experimental_box = builder.get_object("enable_experimental_box")
self._colors_grid = builder.get_object("colors_grid")
self._set_color_switch = builder.get_object("set_color_switch")
self._new_color_button = builder.get_object("new_color_button")
self._extra_color_button = builder.get_object("extra_color_button")
self._load_on_startup_switch = builder.get_object("load_on_startup_switch")
self._bouquet_hints_switch = builder.get_object("bouquet_hints_switch")
self._services_hints_switch = builder.get_object("services_hints_switch")
self._lang_combo_box = builder.get_object("lang_combo_box")
# Extra
# Appearance.
self._list_font_button = builder.get_object("list_font_button")
self._picons_size_button = builder.get_object("picons_size_button")
self._tooltip_logo_size_button = builder.get_object("tooltip_logo_size_button")
self._colors_grid = builder.get_object("colors_grid")
self._set_color_switch = builder.get_object("set_color_switch")
self._new_color_button = builder.get_object("new_color_button")
self._extra_color_button = builder.get_object("extra_color_button")
# Extra.
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
# 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
# Enigma2 only.
self._enigma_radio_button.bind_property("active", builder.get_object("bq_naming_grid"), "sensitive")
self._enigma_radio_button.bind_property("active", builder.get_object("enable_http_box"), "sensitive")
self._enigma_radio_button.bind_property("active", builder.get_object("enable_experimental_box"), "sensitive")
self._enigma_radio_button.bind_property("active", builder.get_object("program_frame"), "sensitive")
self._enigma_radio_button.bind_property("active", builder.get_object("experimental_box"), "sensitive")
# Profiles
self._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")
self._apply_profile_button = builder.get_object("apply_profile_button")
self._apply_profile_button.bind_property("visible", header_separator, "visible")
# Style
# 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_box"))
# Style.
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._digit_elems = (self._port_field, self._http_port_field, self._telnet_port_field, self._video_width_field,
@@ -174,31 +197,43 @@ class SettingsDialog:
for el in self._digit_elems:
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
self.init_ui_elements(self._s_type)
if IS_GNOME_SESSION:
switcher = builder.get_object("main_stack_switcher")
switcher.set_margin_top(0)
switcher.set_margin_bottom(0)
builder.get_object("main_box").remove(switcher)
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True)
header_bar.set_custom_title(switcher)
self._dialog.set_titlebar(header_bar)
self.init_ui_elements()
self.init_profiles()
if self._settings.is_darwin:
# Appearance
self._appearance_box = builder.get_object("appearance_box")
self._appearance_box.set_visible(True)
if not IS_LINUX:
# Themes.
builder.get_object("style_frame").set_visible(IS_WIN)
builder.get_object("themes_support_frame").set_visible(True)
self._layout_switch = builder.get_object("layout_switch")
self._layout_switch.set_active(self._ext_settings.alternate_layout)
self._force_ext_themes_switch = builder.get_object("force_ext_themes_switch")
self._force_ext_themes_switch.set_active(self._settings.force_external_themes)
self._theme_frame = builder.get_object("theme_frame")
self._theme_frame.set_visible(True)
self._theme_thumbnail_image = builder.get_object("theme_thumbnail_image")
self._theme_combo_box = builder.get_object("theme_combo_box")
self._icon_theme_combo_box = builder.get_object("icon_theme_combo_box")
self._dark_mode_switch = builder.get_object("dark_mode_switch")
self._layout_switch = builder.get_object("layout_switch")
self._layout_switch.bind_property("active", builder.get_object("bouquet_box"), "sensitive")
self._dark_mode_switch.set_active(self._ext_settings.dark_mode)
self._themes_support_switch = builder.get_object("themes_support_switch")
self._themes_support_switch.bind_property("active", builder.get_object("gtk_theme_frame"), "sensitive")
self._themes_support_switch.bind_property("active", builder.get_object("icon_theme_frame"), "sensitive")
self.init_appearance()
self._themes_support_switch.bind_property("active", self._theme_frame, "sensitive")
self.init_themes()
@run_idle
def init_ui_elements(self, s_type):
is_enigma_profile = s_type is SettingsType.ENIGMA_2
self._neutrino_radio_button.set_active(s_type is SettingsType.NEUTRINO_MP)
self.update_header_bar()
http_active = self._support_http_api_switch.get_active()
self._click_mode_zap_button.set_sensitive(is_enigma_profile and http_active)
def init_ui_elements(self):
is_enigma_profile = self._s_type is SettingsType.ENIGMA_2
self._neutrino_radio_button.set_active(self._s_type is SettingsType.NEUTRINO_MP)
self.update_picon_paths()
self.update_title()
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)
@@ -214,22 +249,32 @@ class SettingsDialog:
self.on_profile_selected(self._profile_view, False)
self._profile_remove_button.set_sensitive(len(self._profile_view.get_model()) > 1)
def update_header_bar(self):
label, sep, st = self._header_bar.get_subtitle().partition(":")
def update_title(self):
title = "{} [{}]"
if self._s_type is SettingsType.ENIGMA_2:
self._header_bar.set_subtitle("{}: {}".format(label, self._enigma_radio_button.get_label()))
self._dialog.set_title(title.format(get_message("Options"), self._enigma_radio_button.get_label()))
elif self._s_type is SettingsType.NEUTRINO_MP:
self._header_bar.set_subtitle("{}: {}".format(label, self._neutrino_radio_button.get_label()))
self._dialog.set_title(title.format(get_message("Options"), self._neutrino_radio_button.get_label()))
def update_picon_paths(self):
model = self._picons_paths_box.get_model()
model.clear()
list(map(lambda p: model.append((p, p)), self._settings.picons_paths))
if self._settings.picons_path in self._settings.picons_paths:
self._picons_paths_box.set_active_id(self._settings.picons_path)
else:
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
self._dialog.destroy()
return resp
if resp == Gtk.ResponseType.ACCEPT:
self._updated = self.on_save_settings()
dialog.destroy()
def on_field_icon_press(self, entry, icon, event_button):
update_entry_data(entry, self._dialog, self._settings)
@@ -240,7 +285,7 @@ class SettingsDialog:
self._settings.setting_type = s_type
self._s_type = s_type
self.on_reset()
self.init_ui_elements(s_type)
self.init_ui_elements()
def on_reset(self, item=None):
self._settings.reset()
@@ -259,16 +304,17 @@ 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._picons_field.set_text(self._settings.picons_path)
self._data_dir_field.set_text(self._settings.data_local_path)
self._picons_dir_field.set_text(self._settings.picons_local_path)
self._backup_dir_field.set_text(self._settings.backup_local_path)
self._default_data_dir_field.set_text(self._settings.default_data_path)
self._record_data_dir_field.set_text(self._settings.records_path)
self._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._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._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)
@@ -276,12 +322,15 @@ class SettingsDialog:
self._transcoding_switch.set_active(self._settings.activate_transcoding)
self._presets_combo_box.set_active_id(self._settings.active_preset)
self.on_transcoding_preset_changed(self._presets_combo_box)
self._picons_size_button.set_active_id(str(self._settings.list_picon_size))
self._tooltip_logo_size_button.set_active_id(str(self._settings.tooltip_logo_size))
self._list_font_button.set_font(self._settings.list_font)
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._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)
@@ -316,37 +365,43 @@ class SettingsDialog:
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.picons_path = self._picons_field.get_text()
self._settings.data_local_path = self._data_dir_field.get_text()
self._settings.picons_local_path = self._picons_dir_field.get_text()
self._settings.backup_local_path = self._backup_dir_field.get_text()
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.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()
self._ext_settings.show_srv_hints = self._services_hints_switch.get_active()
self._ext_settings.profile_folder_is_default = self._default_data_paths_switch.get_active()
self._ext_settings.default_data_path = self._default_data_dir_field.get_text()
self._ext_settings.records_path = self._record_data_dir_field.get_text()
self._ext_settings.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.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 self._ext_settings.is_darwin:
if not IS_LINUX:
self._ext_settings.dark_mode = self._dark_mode_switch.get_active()
self._ext_settings.alternate_layout = self._layout_switch.get_active()
self._ext_settings.is_themes_support = self._themes_support_switch.get_active()
self._ext_settings.theme = self._theme_combo_box.get_active_id()
self._ext_settings.icon_theme = self._icon_theme_combo_box.get_active_id()
self._ext_settings.force_external_themes = self._force_ext_themes_switch.get_active()
if self._s_type is SettingsType.ENIGMA_2:
self._ext_settings.is_enable_experimental = self._enable_exp_switch.get_active()
@@ -355,13 +410,13 @@ class SettingsDialog:
self._ext_settings.extra_color = self._extra_color_button.get_rgba().to_string()
self._ext_settings.v5_support = self._support_ver5_switch.get_active()
self._ext_settings.force_bq_names = self._force_bq_name_switch.get_active()
self._ext_settings.http_api_support = self._support_http_api_switch.get_active()
self._ext_settings.enable_yt_dl = self._enable_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
@@ -381,7 +436,8 @@ class SettingsDialog:
host, port = self._host_field.get_text(), self._http_port_field.get_text()
use_ssl = self._http_use_ssl_check_button.get_active()
try:
self.show_info_message(test_http(host, port, user, password, use_ssl=use_ssl), Gtk.MessageType.INFO)
self.show_info_message(test_http(host, port, user, password, use_ssl=use_ssl, s_type=self._s_type),
Gtk.MessageType.INFO)
except TestException as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
except HttpApiException as e:
@@ -404,7 +460,7 @@ class SettingsDialog:
host, port = self._host_field.get_text(), self._port_field.get_text()
user, password = self._login_field.get_text(), self._password_field.get_text()
try:
self.show_info_message("OK. {}".format(test_ftp(host, port, user, password)), Gtk.MessageType.INFO)
self.show_info_message(f"OK. {test_ftp(host, port, user, password)}", Gtk.MessageType.INFO)
except TestException as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
finally:
@@ -412,9 +468,10 @@ class SettingsDialog:
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(True)
self._info_bar.set_visible(False)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(get_message(text))
self._info_bar.set_visible(True)
@run_idle
def show_spinner(self, show):
@@ -428,11 +485,8 @@ class SettingsDialog:
self._colors_grid.set_sensitive(state)
def on_http_mode_switch(self, switch, state):
self._click_mode_zap_button.set_sensitive(state)
if any((self._click_mode_play_button.get_active(),
self._click_mode_zap_button.get_active(),
self._click_mode_zap_and_play_button.get_active())):
self._click_mode_disabled_button.set_active(True)
if self._main_stack.get_visible_child_name() == "program" and not state:
self.show_info_message("May affect some features availability! ", Gtk.MessageType.WARNING)
def on_experimental_switch(self, switch, state):
if not state:
@@ -456,16 +510,13 @@ class SettingsDialog:
def on_default_path_mode_switch(self, switch, state):
self._settings.profile_folder_is_default = state
def on_default_data_path_changed(self, entry):
self._settings.default_data_path = entry.get_text()
def on_profile_add(self, item):
model = self._profile_view.get_model()
count = 0
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))
@@ -505,29 +556,8 @@ class SettingsDialog:
if p_settings:
row[0] = new_value
self._profiles[new_value] = p_settings
self.update_local_paths(new_value, old_name)
self.on_profile_selected(self._profile_view, False)
def update_local_paths(self, p_name, old_name, force_rename=False):
data_path = self._settings.data_local_path
picons_path = self._settings.picons_local_path
backup_path = self._settings.backup_local_path
self._settings.data_local_path = p_name.join(data_path.rsplit(old_name, 1))
self._settings.picons_local_path = p_name.join(picons_path.rsplit(old_name, 1))
self._settings.backup_local_path = p_name.join(backup_path.rsplit(old_name, 1))
if force_rename:
try:
if os.path.isdir(picons_path):
os.rename(picons_path, self._settings.picons_local_path)
if os.path.isdir(data_path):
os.rename(data_path, self._settings.data_local_path)
if os.path.isdir(backup_path):
os.rename(backup_path, self._settings.backup_local_path)
except OSError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
def on_profile_selected(self, view, force=True):
if force:
self.on_apply_profile_settings()
@@ -549,14 +579,43 @@ class SettingsDialog:
def on_profile_inserted(self, model, path, itr):
self._profile_remove_button.set_sensitive(len(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:
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{get_message('Are you sure?')}"
if show_dialog(DialogType.QUESTION, self._dialog, msg) != Gtk.ResponseType.OK:
return
model = self._picons_paths_box.get_model()
active = self._picons_paths_box.get_active_iter()
if active:
model.remove(active)
self._picons_paths_box.set_active(0)
self._remove_picon_path_button.set_sensitive(len(model) > 1)
self._ext_settings.picons_paths = tuple(r[0] for r in model)
def on_lang_changed(self, box):
if box.get_active_id() != self._settings.language:
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
def on_main_settings_visible(self, stack, param):
name = stack.get_visible_child_name()
self._apply_profile_button.set_visible(name == "profiles")
self._apply_presets_button.set_visible(name == "streaming")
self._reset_button.set_visible(name == "profiles")
def on_http_use_ssl_toggled(self, button):
active = button.get_active()
@@ -566,59 +625,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":
return
if button.get_active():
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
@run_idle
def set_play_stream_mode(self, mode):
self._play_in_built_radio_button.set_sensitive(not self._settings.is_darwin)
self._play_in_built_radio_button.set_active(mode is PlayStreamsMode.BUILT_IN)
self._play_in_vlc_radio_button.set_active(mode is PlayStreamsMode.VLC)
self._get_m3u_radio_button.set_active(mode is PlayStreamsMode.M3U)
def get_play_stream_mode(self):
if self._play_in_built_radio_button.get_active():
return PlayStreamsMode.BUILT_IN
if self._play_in_vlc_radio_button.get_active():
return PlayStreamsMode.VLC
if self._get_m3u_radio_button.get_active():
return PlayStreamsMode.M3U
return self._settings.play_streams_mode
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
@@ -664,6 +690,11 @@ class SettingsDialog:
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
menu.popup(None, None, None, None, event.button, event.time)
def on_list_font_reset(self, button):
self._list_font_button.set_font(APP_FONT)
# ******************* Themes *********************** #
def on_theme_changed(self, button):
if self._main_stack.get_visible_child_name() != "appearance":
return
@@ -673,7 +704,7 @@ class SettingsDialog:
@run_idle
def set_theme_thumbnail_image(self, theme_name):
img_path = "{}{}/gtk-3.0/thumbnail.png".format(self._ext_settings.themes_path, theme_name)
img_path = "{}{}{}gtk-3.0{}thumbnail.png".format(self._ext_settings.themes_path, theme_name, SEP, SEP)
self._theme_thumbnail_image.set_from_pixbuf(get_picon_pixbuf(img_path, 96))
def on_theme_add(self, button):
@@ -702,24 +733,24 @@ class SettingsDialog:
response = get_chooser_dialog(self._dialog, self._settings, "Themes Archive [*.xz, *.zip]", ("*.xz", "*.zip"))
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
self._appearance_box.set_sensitive(False)
self._theme_frame.set_sensitive(False)
self.unpack_theme(response, path, button)
@run_task
def unpack_theme(self, src, dst, button):
try:
os.makedirs(os.path.dirname(dst), exist_ok=True)
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)
self._appearance_box.set_sensitive(True)
@run_idle
def update_theme_button(self, button, dst):
@@ -732,6 +763,7 @@ class SettingsDialog:
button.append(theme, theme)
button.set_active_id(theme)
self.show_info_message("Done!", Gtk.MessageType.INFO)
self._theme_frame.set_sensitive(True)
@run_idle
def remove_theme(self, button, path):
@@ -755,9 +787,7 @@ class SettingsDialog:
button.set_active(0)
@run_idle
def init_appearance(self):
self._dark_mode_switch.set_active(self._ext_settings.dark_mode)
self._layout_switch.set_active(self._ext_settings.alternate_layout)
def init_themes(self):
t_support = self._ext_settings.is_themes_support
self._themes_support_switch.set_active(t_support)
if t_support:

View File

@@ -1,18 +1,37 @@
* {
-GtkDialog-action-area-border: 6;
}
#digit-entry {
border-color: Red;
}
#status-bar-button {
padding: 1px;
padding-top: 1px;
padding-bottom: 1px;
padding-left: 3px;
padding-right: 3px;
margin: 1px;
}
#stack-switch-button {
padding-top: 0;
padding-bottom: 0;
}
paned > separator {
background-repeat: no-repeat;
background-position: center;
}
paned.horizontal > separator {
background-size: 2px 24px;
}
paned.vertical > separator {
background-size: 24px 2px;
}
.red-button {
background-image: none;
background-color: red;
@@ -55,4 +74,16 @@ paned > separator {
border-radius: 0;
border-left-width: 0;
border-right-width: 1px;
}
}
.stack-switcher > button > label {
padding-left: 5px;
padding-right: 5px;
min-width: 50px;
}
.stack-switcher > button.text-button {
padding-left: 5px;
padding-right: 5px;
min-width: 50px;
}

177
app/ui/telnet.glade Normal file
View File

@@ -0,0 +1,177 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2
The MIT License (MIT)
Copyright (c) 2018-2020 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface domain="demon-editor">
<requires lib="gtk+" version="3.18"/>
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkTextTagTable" id="tag_table">
<child type="tag">
<object class="GtkTextTag" id="end_tag">
<property name="font">Normal</property>
<property name="editable">False</property>
</object>
</child>
</object>
<object class="GtkTextBuffer" id="text_buffer">
<property name="tag_table">tag_table</property>
</object>
<object class="GtkFrame" id="telnet_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="telnet_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_bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="commands_entry">
<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">2</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="connect_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Connect</property>
<signal name="clicked" handler="on_connect" swapped="no"/>
<child>
<object class="GtkImage" id="connect_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-connect</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="disconnect_button">
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Disconnect</property>
<signal name="clicked" handler="on_disconnect" swapped="no"/>
<child>
<object class="GtkImage" id="disconnect_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-disconnect</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="clear_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Clear</property>
<property name="halign">center</property>
<property name="valign">center</property>
<signal name="clicked" handler="on_clear" swapped="no"/>
<child>
<object class="GtkImage" id="clear_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-clear</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="telnet_scrolled_window">
<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="name">textview-large</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="wrap_mode">char</property>
<property name="left_margin">5</property>
<property name="right_margin">5</property>
<property name="buffer">text_buffer</property>
<property name="overwrite">True</property>
<property name="input_hints">GTK_INPUT_HINT_WORD_COMPLETION | GTK_INPUT_HINT_NONE</property>
<property name="monospace">True</property>
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
<signal name="realize" handler="on_text_view_realize" swapped="no"/>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Telnet</property>
</object>
</child>
</object>
</interface>

252
app/ui/telnet.py Normal file
View File

@@ -0,0 +1,252 @@
# -*- 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 re
import selectors
import socket
from collections import deque
from telnetlib import Telnet
from gi.repository import GLib
from app.commons import run_task, run_idle, log
from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK
class ExtTelnet(Telnet):
def __init__(self, output_callback, **kwargs):
super().__init__(**kwargs)
self._output_callback = output_callback
def interact(self):
""" Interaction function, emulates a very dumb telnet client. """
with selectors.DefaultSelector() as selector:
selector.register(self, selectors.EVENT_READ)
while True:
for key, events in selector.select():
if key.fileobj is self:
try:
text = self.read_very_eager()
except EOFError as e:
msg = "\n*** Connection closed by remote host ***\n"
self._output_callback(msg)
log(msg)
raise e
else:
if text:
self._output_callback(text)
class TelnetClient(Gtk.Box):
""" Very simple telnet client. """
_COLOR_PATTERN = re.compile("\x1b.*?m") # Color info
_ERASING_PATTERN = re.compile("\x1b.*?K") # Erase to right
_APP_MODE_PATTERN = re.compile("\x1b.*?(1h)|(1l)") # h - on, l - off
_ALL_PATTERN = re.compile(r'(\x1b\[|\x9b)[0-?]*[@-~]')
_NOT_SUPPORTED = {"mc", "mcedit", "vi", "nano"}
def __init__(self, app, *args, **kwargs):
super().__init__(*args, **kwargs)
self._app = app
self._app.connect("profile-changed", self.on_profile_changed)
self._tn = None
self._app_mode = False
self._commands = deque(maxlen=10)
self._handlers = {"on_clear": self.on_clear,
"on_text_view_realize": self.on_text_view_realize,
"on_view_key_press": self.on_view_key_press,
"on_connect": self.on_connect,
"on_disconnect": self.on_disconnect}
builder = Gtk.Builder()
builder.add_from_file(UI_RESOURCES_PATH + "telnet.glade")
builder.connect_signals(self._handlers)
self._text_view = builder.get_object("text_view")
self._buf = builder.get_object("text_buffer")
self._end_tag = builder.get_object("end_tag")
self._connect_button = builder.get_object("connect_button")
self._connect_button.bind_property("visible", builder.get_object("disconnect_button"), "visible", 4)
main_frame = builder.get_object("telnet_frame")
provider = Gtk.CssProvider()
provider.load_from_path(UI_RESOURCES_PATH + "style.css")
main_frame.get_style_context().add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
self.pack_start(main_frame, True, True, 0)
self.show()
def on_profile_changed(self, app, data):
self.on_clear()
self.on_disconnect()
self.on_connect()
def on_text_view_realize(self, view):
self.on_connect()
@run_task
def on_connect(self, item=None):
try:
GLib.idle_add(self._connect_button.set_visible, False)
settings = self._app.app_settings
user, password, timeout = settings.user, settings.password, settings.telnet_timeout
self._tn = ExtTelnet(self.append_output, host=settings.host, port=settings.telnet_port, timeout=timeout)
if user != "":
self._tn.read_until(b"login: ")
self._tn.write(user.encode("utf-8") + b"\n")
if password != "":
self._tn.read_until(b"Password: ")
self._tn.write(password.encode("utf-8") + b"\n")
self._tn.interact()
except (OSError, EOFError, socket.timeout, ConnectionRefusedError) as e:
log(f"{self.__class__.__name__}: {e}")
self._app.show_info_message(str(e), Gtk.MessageType.ERROR)
finally:
GLib.idle_add(self._connect_button.set_visible, True)
@run_task
def on_disconnect(self, item=None):
if self._tn:
GLib.idle_add(self._connect_button.set_visible, True)
self._tn.close()
def on_command_done(self, entry):
command = entry.get_text()
entry.set_text("")
if command and self._tn:
self._tn.write(command.encode("ascii") + b"\r")
def on_clear(self, item=None):
self._buf.delete(self._buf.get_start_iter(), self._buf.get_end_iter())
def on_view_key_press(self, view, event):
""" Handling keystrokes on press. """
if event.keyval == Gdk.KEY_Return:
self.do_command()
return True
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK
if ctrl and key is KeyboardKey.C:
if self._tn and self._tn.sock:
self._tn.write(b"\x03") # interrupt
# Last commands navigation.
if key is KeyboardKey.UP:
self.delete_last_command()
if self._commands:
cmd = self._commands.pop()
self._commands.appendleft(cmd)
self._buf.insert_at_cursor(cmd, -1)
return True
elif key is KeyboardKey.DOWN:
self.delete_last_command()
if self._commands:
cmd = self._commands.popleft()
self._commands.append(cmd)
self._buf.insert_at_cursor(cmd, -1)
return True
def delete_last_command(self):
end = self._buf.get_end_iter()
if end.ends_tag(self._end_tag):
return
if end.backward_to_tag_toggle(self._end_tag):
self._buf.delete(self._buf.get_end_iter(), end)
def do_command(self):
count = self._buf.get_line_count()
begin = self._buf.get_iter_at_line(count)
end = self._buf.get_end_iter()
command = []
while end.backward_to_tag_toggle(self._end_tag):
command.append(self._buf.get_text(end, begin, False))
break
else: # if buf is empty
command.append(self._buf.get_text(begin, end, False))
# To preventing duplication of the command in the buf.
self._buf.delete(end, begin)
if command and self._tn.sock:
cmd = command[0]
if cmd in self._NOT_SUPPORTED:
self._app.show_info_message(f"'{cmd}' is not supported by this client.", Gtk.MessageType.ERROR)
else:
self._tn.write(cmd.encode("ascii") + b"\r")
self._commands.append(cmd)
@run_idle
def append_output(self, txt):
t = txt.decode("ascii", errors="ignore")
ap = re.search(self._APP_MODE_PATTERN, t)
if ap:
on, of = ap.group(1), ap.group(2)
if on:
self._app_mode = True
elif of:
self._app_mode = False
self.on_clear()
t = re.sub(self._ALL_PATTERN, "", t) # Removing [replacing] ascii escape sequences.
if self._app_mode:
start, end = self._buf.get_start_iter(), self._buf.get_end_iter()
count = self._buf.get_line_count()
new_lines = t.split("\r\n")
ext_lines = self._buf.get_text(start, end, True).split("\r\n")
if count < len(new_lines):
self._buf.set_text(re.sub(self._ERASING_PATTERN, "", t))
else:
for i, line in enumerate(new_lines):
if line:
ext_lines[i] = re.sub(self._ERASING_PATTERN, "", line)
self._buf.set_text("\r\n".join(ext_lines))
else:
self._buf.insert_at_cursor(t, -1)
insert = self._buf.get_insert()
self._text_view.scroll_to_mark(insert, 0.0, True, 0.0, 1.0)
self._buf.apply_tag(self._end_tag, self._buf.get_start_iter(), self._buf.get_end_iter())
if __name__ == "__main__":
pass

View File

@@ -1,141 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2 -->
<interface domain="demon-editor">
<requires lib="gtk+" version="3.16"/>
<object class="GtkBox" id="timer_row_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">2</property>
<property name="margin_right">2</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="timer_name_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="timer_name_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="ellipsize">end</property>
<attributes>
<attribute name="weight" value="semibold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="timer_description_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="timer_description_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="ellipsize">end</property>
<attributes>
<attribute name="style" value="italic"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="timer_service_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="timer_service_name_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="ellipsize">end</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="timer_time_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<attributes>
<attribute name="size" value="8000"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkSeparator" id="timer_row_separator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
</interface>

View File

@@ -7,6 +7,7 @@ from gi.repository import GLib
from app.commons import log
from app.connections import HttpAPI
from app.tools.yt import YouTube
from app.ui.dialogs import get_builder
from app.ui.iptv import get_yt_icon
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH
@@ -34,9 +35,7 @@ class LinksTransmitter:
self._app_window = app_window
self._is_status_icon = True
builder = Gtk.Builder()
builder.add_from_file(UI_RESOURCES_PATH + "transmitter.glade")
builder.connect_signals(handlers)
builder = get_builder(UI_RESOURCES_PATH + "transmitter.glade", handlers)
self._main_window = builder.get_object("main_window")
self._url_entry = builder.get_object("url_entry")

View File

@@ -1,53 +1,127 @@
# -*- 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 locale
import os
from enum import Enum, IntEnum
from functools import lru_cache
from app.settings import Settings, SettingsException, IS_DARWIN
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Gdk", "3.0")
gi.require_version("Notify", "0.7")
from gi.repository import Gtk, Gdk, Notify
from gi.repository import Gtk, Gdk, GLib
# Init notify
Notify.init("DemonEditor")
# Setting mod mask for the keyboard depending on the platform.
from app.settings import Settings, SettingsException, IS_DARWIN, GTK_PATH, IS_LINUX
# Setting mod mask for keyboard depending on platform
MOD_MASK = Gdk.ModifierType.MOD2_MASK if IS_DARWIN else Gdk.ModifierType.CONTROL_MASK
# Paths.
BASE_PATH = "app/ui/"
EX_PATH = "/usr/share/demoneditor/app/ui/" if IS_LINUX else "ui/"
# Path to *.glade files.
UI_RESOURCES_PATH = "app/ui/" if os.path.exists("app/ui/") else "/usr/share/demoneditor/app/ui/"
IS_GNOME_SESSION = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
UI_RESOURCES_PATH = BASE_PATH if os.path.exists(BASE_PATH) else EX_PATH
# Translation.
LANG_PATH = UI_RESOURCES_PATH + "lang"
TEXT_DOMAIN = "demon-editor"
NOTIFY_IS_INIT = False
APP_FONT = None
IS_GNOME_SESSION = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
try:
settings = Settings.get_instance()
except SettingsException:
pass
else:
os.environ["LANGUAGE"] = settings.language
if UI_RESOURCES_PATH == "app/ui/":
locale.bindtextdomain(TEXT_DOMAIN, UI_RESOURCES_PATH + "lang")
st = Gtk.Settings().get_default()
APP_FONT = st.get_property("gtk-font-name")
st.set_property("gtk-application-prefer-dark-theme", settings.dark_mode)
if settings.is_themes_support:
st = Gtk.Settings().get_default()
st.set_property("gtk-theme-name", settings.theme)
st.set_property("gtk-icon-theme-name", settings.icon_theme)
else:
if not IS_LINUX:
if IS_DARWIN:
s_path = f"{GTK_PATH + '/' + UI_RESOURCES_PATH if GTK_PATH else UI_RESOURCES_PATH}mac_style.css"
else:
s_path = f"{UI_RESOURCES_PATH}win_style.css"
style_provider = Gtk.CssProvider()
style_provider.load_from_path(s_path)
Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
if IS_LINUX:
if UI_RESOURCES_PATH == BASE_PATH:
locale.bindtextdomain(TEXT_DOMAIN, LANG_PATH)
# Init notify
try:
gi.require_version("Notify", "0.7")
from gi.repository import Notify
except ImportError:
pass
else:
NOTIFY_IS_INIT = Notify.init("DemonEditor")
elif IS_DARWIN:
import gettext
if GTK_PATH:
LANG_PATH = GTK_PATH + "/share/locale"
gettext.bindtextdomain(TEXT_DOMAIN, LANG_PATH)
# For launching from the bundle.
if os.getcwd() == "/" and GTK_PATH:
os.chdir(GTK_PATH)
else:
locale.setlocale(locale.LC_NUMERIC, "C")
# Icons.
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)
@@ -68,7 +142,7 @@ def get_yt_icon(icon_name, size=24):
if n_theme.has_icon(icon_name):
return n_theme.load_icon(icon_name, size, 0)
return default_theme.load_icon("info", size, 0)
return default_theme.load_icon("emblem-important-symbolic", size, 0)
def show_notification(message, timeout=10000, urgency=1):
@@ -78,55 +152,27 @@ def show_notification(message, timeout=10000, urgency=1):
@param timeout: milliseconds
@param urgency: 0 - low, 1 - normal, 2 - critical
"""
notify = Notify.Notification.new("DemonEditor", message, "demon-editor")
notify.set_urgency(urgency)
notify.set_timeout(timeout)
notify.show()
if IS_DARWIN:
# Since NSUserNotification has been deprecated, osascript will be used.
os.system("""osascript -e 'display notification "{}" with title "DemonEditor"'""".format(message))
elif NOTIFY_IS_INIT:
notify = Notify.Notification.new("DemonEditor", message, "demon-editor")
notify.set_urgency(urgency)
notify.set_timeout(timeout)
notify.show()
class KeyboardKey(Enum):
""" The raw(hardware) codes of the keyboard keys. """
E = 26
R = 27
T = 28
P = 33
S = 39
F = 41
X = 53
C = 54
V = 55
W = 25
Z = 52
INSERT = 118
HOME = 110
END = 115
UP = 111
DOWN = 116
PAGE_UP = 112
PAGE_DOWN = 117
LEFT = 113
RIGHT = 114
F2 = 68
F7 = 73
SPACE = 65
DELETE = 119
BACK_SPACE = 22
CTRL_L = 37
CTRL_R = 105
# Laptop codes
HOME_KP = 79
END_KP = 87
PAGE_UP_KP = 81
PAGE_DOWN_KP = 89
@classmethod
def value_exist(cls, value):
return value in (val.value for val in cls.__members__.values())
# Keys for move in lists. KEY_KP_(NAME) for laptop!!!
MOVE_KEYS = (KeyboardKey.UP, KeyboardKey.PAGE_UP, KeyboardKey.DOWN, KeyboardKey.PAGE_DOWN, KeyboardKey.HOME,
KeyboardKey.END, KeyboardKey.HOME_KP, KeyboardKey.END_KP, KeyboardKey.PAGE_UP_KP, KeyboardKey.PAGE_DOWN_KP)
class Page(Enum):
""" Main stack widget page. """
INFO = "info"
SERVICES = "services"
SATELLITE = "satellite"
PICONS = "picons"
EPG = "epg"
TIMERS = "timers"
RECORDINGS = "recordings"
FTP = "ftp"
CONTROL = "control"
class FavClickMode(IntEnum):
@@ -143,6 +189,7 @@ class ViewTarget(Enum):
BOUQUET = 0
FAV = 1
SERVICES = 2
IPTV = 3
class BqGenType(Enum):
@@ -206,11 +253,159 @@ class Column(IntEnum):
ALT_FAV_ID = 5
ALT_ID = 6
ALT_ITER = 7
# Recordings view
REC_SERVICE = 0
REC_TITLE = 1
REC_TIME = 2
REC_LEN = 3
REC_FILE = 4
REC_DESC = 5
# IPTV view
IPTV_SERVICE = 0
IPTV_TYPE = 1
IPTV_PICON = 2
IPTV_REF = 3
IPTV_FAV_ID = 4
IPTV_PICON_ID = 5
def __index__(self):
""" Overridden to get the index in slices directly """
return self.value
# *************** Keyboard keys *************** #
class BaseKeyboardKey(Enum):
@classmethod
def value_exist(cls, value):
return value in (val.value for val in cls.__members__.values())
if IS_LINUX:
class KeyboardKey(BaseKeyboardKey):
""" The raw(hardware) codes [Linux] of the keyboard keys. """
E = 26
R = 27
T = 28
P = 33
S = 39
F = 41
X = 53
C = 54
V = 55
W = 25
Z = 52
INSERT = 118
HOME = 110
END = 115
UP = 111
DOWN = 116
PAGE_UP = 112
PAGE_DOWN = 117
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
HOME_KP = 79
END_KP = 87
PAGE_UP_KP = 81
PAGE_DOWN_KP = 89
elif IS_DARWIN:
class KeyboardKey(BaseKeyboardKey):
""" The raw(hardware) codes [macOS] of the keyboard keys. """
F = 3
E = 14
R = 15
T = 17
P = 35
S = 1
H = 4
L = 37
X = 7
C = 8
V = 9
W = 13
Z = 6
INSERT = -1
HOME = -1
END = -1
UP = 126
DOWN = 125
PAGE_UP = -1
PAGE_DOWN = -1
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.
HOME_KP = -1
END_KP = -1
PAGE_UP_KP = -1
PAGE_DOWN_KP = -1
else:
class KeyboardKey(BaseKeyboardKey):
""" The raw(hardware) codes [Windows] of the keyboard keys. """
E = 69
R = 82
T = 84
P = 80
S = 83
F = 70
X = 88
C = 67
V = 86
W = 87
Z = 90
INSERT = 45
HOME = 36
END = 35
UP = 38
DOWN = 40
PAGE_UP = 33
PAGE_DOWN = 34
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.
HOME_KP = -1
END_KP = -1
PAGE_UP_KP = -1
PAGE_DOWN_KP = -1
# Keys for move in lists. KEY_KP_(NAME) for laptop!
MOVE_KEYS = {KeyboardKey.UP, KeyboardKey.PAGE_UP,
KeyboardKey.DOWN, KeyboardKey.PAGE_DOWN,
KeyboardKey.HOME, KeyboardKey.END,
KeyboardKey.HOME_KP, KeyboardKey.END_KP,
KeyboardKey.PAGE_UP_KP, KeyboardKey.PAGE_DOWN_KP}
if __name__ == "__main__":
pass

11
app/ui/win_style.css Normal file
View File

@@ -0,0 +1,11 @@
* {
-GtkDialog-action-area-border: 12;
}
switch {
margin-right: 2px;
}
spinbutton entry {
min-height: 16px;
}

7
build/BUILDING.md Normal file
View File

@@ -0,0 +1,7 @@
## Building DemonEditor
This directory contains build scripts and additional files for various platforms and distributions.
### Supported platforms
* GNU/Linux
* macOS
* MS Windows

View File

@@ -0,0 +1,49 @@
diff -Nru demon-editor-2.0-development-orig/DemonEditor.desktop demon-editor-2.0-development/DemonEditor.desktop
--- demon-editor-2.0-development-orig/DemonEditor.desktop 2021-10-14 21:32:56.000000000 +0300
+++ demon-editor-2.0-development/DemonEditor.desktop 2021-09-29 13:19:24.000000000 +0300
@@ -6,8 +6,8 @@
Comment[be]=Рэдактар спіса каналаў і спадарожнікаў для Enigma2
Comment[de]=Programm- und Satellitenlisten-Editor für Enigma2
Icon=demon-editor
-Exec=bash -c 'cd $(dirname %k) && ./start.py'
+Exec=demon-editor
Terminal=false
Type=Application
-Categories=Utility;Application;
+Categories=Utility;
StartupNotify=false
diff -Nru demon-editor-2.0-development-orig/start.py demon-editor-2.0-development/start.py
--- demon-editor-2.0-development-orig/start.py 2021-10-14 21:32:56.000000000 +0300
+++ demon-editor-2.0-development/start.py 2021-09-29 13:19:24.000000000 +0300
@@ -1,29 +1,4 @@
#!/usr/bin/env python3
-import os
+from app.ui.main import start_app
-
-def update_icon():
- need_update = False
- icon_name = "DemonEditor.desktop"
-
- with open(icon_name, "r") as f:
- lines = f.readlines()
- for i, line in enumerate(lines):
- if line.startswith("Icon="):
- icon_path = line.lstrip("Icon=")
- current_path = "{}/app/ui/icons/hicolor/96x96/apps/demon-editor.png".format(os.getcwd())
- if icon_path != current_path:
- need_update = True
- lines[i] = "Icon={}\n".format(current_path)
- break
-
- if need_update:
- with open(icon_name, "w") as f:
- f.writelines(lines)
-
-
-if __name__ == "__main__":
- from app.ui.main import start_app
-
- update_icon()
- start_app()
+start_app()

View File

@@ -0,0 +1,86 @@
Name: demon-editor
Version: 2.0
Release: slava0
BuildArch: noarch
Summary: Enigma2 channel and satellite list editor
Url: https://github.com/DYefremov/DemonEditor
License: MIT
Group: Other
Source: %name-%version-development.tar.gz
Patch0: %name-%version-development-startfix.patch
AutoReq: no
Requires: python3 python3-module-requests python3-module-pygobject3 python3-module-chardet libmpv1
BuildRequires: python3-dev python3-module-mpl_toolkits
%description
Enigma2 channel and satellites list editor for GNU/Linux.
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc).
Main features of the program:
Editing bouquets, channels, satellites.
Import function.
Backup function.
Support of picons.
Importing services, downloading picons and updating satellites from the Web.
Extended support of IPTV.
Import to bouquet(Neutrino WEBTV) from m3u.
Export of bouquets with IPTV services in m3u.
Assignment of EPGs from DVB or XML for IPTV services (only Enigma2, experimental).
Playback of IPTV or other streams directly from the bouquet list.
Control panel with the ability to view EPG and manage timers (via HTTP API, experimental).
Simple FTP client (experimental).
%prep
%setup -n %name-%version-development
%patch0 -p1
%install
%__install -d %buildroot%_datadir/demoneditor/app
%__install -m644 app/*py %buildroot%_datadir/demoneditor/app
%__install -d %buildroot%_datadir/demoneditor/app/eparser
%__install -m644 app/eparser/*py %buildroot%_datadir/demoneditor/app/eparser
%__install -d %buildroot%_datadir/demoneditor/app/eparser/enigma
%__install -m644 app/eparser/enigma/*py %buildroot%_datadir/demoneditor/app/eparser/enigma
%__install -d %buildroot%_datadir/demoneditor/app/eparser/neutrino
%__install -m644 app/eparser/neutrino/*py %buildroot%_datadir/demoneditor/app/eparser/neutrino
%__install -d %buildroot%_datadir/demoneditor/app/tools
%__install -m644 app/tools/*py %buildroot%_datadir/demoneditor/app/tools
%__install -d %buildroot%_datadir/demoneditor/app/ui
%__install -m644 app/ui/*py %buildroot%_datadir/demoneditor/app/ui
%__install -m644 app/ui/*glade %buildroot%_datadir/demoneditor/app/ui
%__install -m644 app/ui/*css %buildroot%_datadir/demoneditor/app/ui
%__install -m644 app/ui/*ui %buildroot%_datadir/demoneditor/app/ui
%__install -m755 start.py %buildroot%_datadir/demoneditor
%__install -d %buildroot%_iconsdir/hicolor/96x96/apps
%__install -d %buildroot%_iconsdir/hicolor/scalable/apps
%__install -m644 app/ui/icons/hicolor/96x96/apps/%name.* %buildroot%_iconsdir/hicolor/96x96/apps
%__install -m644 app/ui/icons/hicolor/scalable/apps%name.* -d %buildroot%_iconsdir/hicolor/scalable/apps
%__install -d %buildroot%_datadir/locale
cp -r app/ui/lang/* %buildroot%_datadir/locale
%__install -d %buildroot%_bindir
echo "#!/bin/bash
python3 %_datadir/demoneditor/start.py $1" > %buildroot%_bindir/%name
chmod 755 %buildroot%_bindir/%name
%__install -d %buildroot%_desktopdir
%__install -m644 DemonEditor.desktop %buildroot%_desktopdir/DemonEditor.desktop
%find_lang %name
%files -f %name.lang
%doc deb/DEBIAN/README.source
%_bindir/%name
%_datadir/demoneditor
%_iconsdir/*/*/*/%name.*
%_desktopdir/DemonEditor.desktop
%changelog
* Wed Sep 29 2021 Viacheslav Dikonov <sdiconov@mail.ru> 1.0.10-slava0
- ALTLinux package

View File

@@ -1,11 +1,11 @@
#!/bin/bash
VER="1.0.4_Beta"
VER="2.2.2_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 -arv app $DEB_PATH
rsync --exclude=app/ui/lang --exclude=app/ui/icons --exclude=__pycache__ -arv ../../app $DEB_PATH
cd dist
fakeroot dpkg-deb --build DemonEditor

View File

@@ -1,7 +1,7 @@
demon-editor for Debian
----------------------
DemonEditor
Enigma2 channel and satellites list editor for GNU/Linux.
Enigma2 channel and satellite list editor for GNU/Linux.
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc).
@@ -10,13 +10,15 @@ Main features of the program:
Editing bouquets, channels, satellites.
Import function.
Backup function.
Extended support of IPTV.
Support of picons.
Downloading of picons and updating of satellites (transponders) from the web.
Importing services, downloading picons and updating satellites from the Web.
Extended support of IPTV.
Import to bouquet(Neutrino WEBTV) from m3u.
Export of bouquets with IPTV services in m3u.
Assignment of EPGs from DVB or XML for IPTV services (only Enigma2, experimental).
Preview (playback) of IPTV or other streams directly from the bouquet list (should be installed VLC).
Playback of IPTV or other streams directly from the bouquet list.
Control panel with the ability to view EPG and manage timers (via HTTP API, experimental).
Simple FTP client (experimental).
Keyboard shortcuts:
Ctrl + Insert - copies the selected channels from the main list to the the bouquet beginning or inserts (creates) a new bouquet.
@@ -43,7 +45,7 @@ Keyboard shortcuts:
For multiple selection with the mouse, press and hold the Ctrl key!
Minimum requirements:
Python >= 3.5.2 and GTK+ >= 3.16 with PyGObject bindings, python3-requests.
Python >= 3.6, GTK+ >= 3.22, python3-gi, python3-gi-cairo, python3-requests.
Important:
Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2!
@@ -55,4 +57,6 @@ Important:
just load your data via *"File/Open"* and press *"Save"*. When importing separate bouquet files, only those services
(excluding IPTV) that are in the **current open lamedb** (main list of services) will be imported.
For streams playback, this app supports VLC, MPV and GStreamer.
Depending on your distro, you may need to install additional packages and libraries.

View File

@@ -0,0 +1,25 @@
Package: demon-editor
Version: 2.2.2-Beta
Section: utils
Priority: optional
Architecture: all
Essential: no
Depends: python3 (>= 3.6),
python3-requests,
python3-gi,
python3-gi-cairo,
gir1.2-notify-0.7,
p7zip-full
Recommends: libmpv1,
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).

View File

@@ -5,7 +5,7 @@ Source: https://github.com/DYefremov/DemonEditor
Files: *
MIT License
Copyright (c) 2018-2020 Dmitriy Yefremov
Copyright (c) 2018-2022 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -5,6 +5,9 @@ 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

View File

@@ -0,0 +1,4 @@
#!/usr/bin/env python3
from app.ui.main import start_app
start_app()

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,75 @@
import os
import datetime
import distutils.util
EXE_NAME = 'start.py'
DIR_PATH = os.getcwd()
COMPILING_PLATFORM = distutils.util.get_platform()
PATH_EXE = [os.path.join(DIR_PATH, EXE_NAME)]
STRIP = True
BUILD_DATE = datetime.datetime.now().strftime("%y%m%d")
block_cipher = None
excludes = ['app.tools.mpv',
'gi.repository.Gst',
'gi.repository.GstBase',
'gi.repository.GstVideo',
'youtube_dl',
'tkinter']
ui_files = [('app/ui/*.glade', 'ui'),
('app/ui/*.css', 'ui'),
('app/ui/*.ui', 'ui'),
('app/ui/lang*', 'share/locale'),
('app/ui/icons*', 'share/icons')
]
a = Analysis([EXE_NAME],
pathex=PATH_EXE,
binaries=None,
datas=ui_files,
hiddenimports=['fileinput', 'uuid'],
hookspath=[],
runtime_hooks=[],
excludes=excludes,
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher)
pyz = PYZ(a.pure,
a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
exclude_binaries=True,
name='DemonEditor',
debug=False,
strip=STRIP,
upx=True,
console=False)
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=STRIP,
upx=True,
name='DemonEditor')
app = BUNDLE(coll,
name='DemonEditor.app',
icon='icon.icns',
bundle_identifier=None,
info_plist={
'NSPrincipalClass': 'NSApplication',
'CFBundleName': 'DemonEditor',
'CFBundleDisplayName': 'DemonEditor',
'CFBundleGetInfoString': "Enigma2 channel and satellite editor",
'LSApplicationCategoryType': 'public.app-category.utilities',
'LSMinimumSystemVersion': '10.13',
'CFBundleShortVersionString': f"2.2.2.{BUILD_DATE} Beta",
'NSHumanReadableCopyright': u"Copyright © 2022, Dmitriy Yefremov",
'NSRequiresAquaSystemAppearance': 'false'
})

BIN
build/mac/icon.icns Normal file

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More