Compare commits

..

341 Commits

Author SHA1 Message Date
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
DYefremov
d95ec0af17 Russian, Belarusian and German translations update 2021-01-12 11:53:00 +03:00
DYefremov
77c159fc1a added separator to profile toolbar 2021-01-12 11:22:57 +03:00
DYefremov
95508c3015 changed alternatives naming 2021-01-11 17:15:39 +03:00
DYefremov
1f6e6b9812 added 8739 stream type 2021-01-11 12:30:44 +03:00
DYefremov
20697a08f2 naming alternatives fix 2021-01-10 22:42:16 +03:00
DYefremov
95c7213988 added moving alternatives in the list 2021-01-10 14:05:01 +03:00
DYefremov
a988ea6e68 added editing of alternatives 2021-01-08 23:01:16 +03:00
DYefremov
061e42a36e set extra tools visible by default 2021-01-08 13:16:14 +03:00
DYefremov
3a5473ca03 removed bq position option 2021-01-07 10:34:10 +03:00
DYefremov
54a5be9c3d added separate column for picon to fav list 2021-01-06 22:46:12 +03:00
DYefremov
858c64d531 added basic support for alternatives (#42) 2021-01-05 23:07:15 +03:00
DYefremov
f05802841d some streams detection fix 2021-01-02 23:17:22 +03:00
DYefremov
e4af45b0e0 redesigned network settings 2020-12-29 22:41:28 +03:00
DYefremov
0cad69cb00 bump version 2020-12-29 22:41:12 +03:00
DYefremov
071fffb303 _config.yml update 2020-12-27 23:19:29 +03:00
DYefremov
95d51d37e9 README update 2020-12-26 19:55:18 +03:00
DYefremov
f1935e4a56 added display option for bouquet details list 2020-12-24 23:30:29 +03:00
DYefremov
96aeb05dd1 minor compatibility [xenial] fix 2020-12-23 19:39:57 +03:00
DYefremov
af1c3c3ca6 README update 2020-12-23 09:10:03 +03:00
DYefremov
2ba37f1506 Russian, Belarusian and German translations update 2020-12-23 09:09:42 +03:00
DYefremov
8581de5a10 yt fix 2020-12-23 09:09:08 +03:00
DYefremov
155f3b254a ftp client improvements 2020-12-22 14:18:16 +03:00
DYefremov
13e6c858d2 storing app window size on close 2020-12-19 12:36:42 +03:00
DYefremov
7fac48c44d added alternate layout option 2020-12-18 22:13:15 +03:00
DYefremov
9aa2ec191e added ftp client to main window 2020-12-16 23:28:00 +03:00
DYefremov
13e12165eb added ftp client base class 2020-12-16 23:20:51 +03:00
DYefremov
ae37feebaa added keyboard key 2020-12-16 00:32:32 +03:00
DYefremov
573bcba32f ftp refactoring [func extension] 2020-12-13 15:19:38 +03:00
DYefremov
f4e5c56d39 allowed to add dir during path config 2020-12-13 15:19:01 +03:00
DYefremov
4dc0dffc1f README update 2020-12-10 21:28:09 +03:00
DYefremov
ceb329700d added encoding detection for *.m3u import 2020-12-07 09:30:03 +03:00
DYefremov
8cca2da48d data load rework (#37 fix [can't decode byte]) 2020-12-05 14:16:07 +03:00
DYefremov
e9bab4ebb7 bump version 2020-12-04 15:06:12 +03:00
DYefremov
3e7750e4ef added playlist extraction via youtube-dl 2020-12-04 15:03:21 +03:00
DYefremov
184c1cbee5 upd. README 2020-12-02 01:02:25 +03:00
DYefremov
c9e8625ea5 added types to the service parser 2020-11-28 14:19:41 +03:00
DYefremov
fd72615a63 minor changes in control panel gui 2020-11-28 08:24:51 +03:00
DYefremov
f11a40e04d added sorting reset when loading data 2020-11-25 16:27:48 +03:00
DYefremov
9522267dee translations correction for app description 2020-11-25 15:14:24 +03:00
DYefremov
f538d609b9 added timeouts for telnet login 2020-11-25 13:27:53 +03:00
DYefremov
3edc699277 config.yml update 2020-11-25 11:30:16 +03:00
DYefremov
19562c7281 description update 2020-11-25 11:25:50 +03:00
DYefremov
5a104c1778 Russian, Belarusian and German translations update 2020-11-23 13:51:43 +03:00
DYefremov
09afc5b76a fix update of sat positions after web import 2020-11-23 12:58:40 +03:00
DYefremov
6129c5ac23 some changes in the control panel 2020-11-22 15:40:19 +03:00
DYefremov
a552d13933 style correction 2020-11-21 15:09:32 +03:00
DYefremov
c4fa3b1eee Russian, Belarusian and German translations update 2020-11-19 20:27:09 +03:00
DYefremov
795c708a61 added basic support for timers via http api 2020-11-19 18:05:40 +03:00
DYefremov
92721c65e4 added styles 2020-11-19 15:01:07 +03:00
DYefremov
b8470f7dfa added transponder view popup menu 2020-11-16 13:48:05 +03:00
DYefremov
fd02ddedb7 small http api refactoring 2020-11-11 15:34:12 +03:00
DYefremov
f3beec141c added epg display in control panel 2020-11-07 18:38:40 +03:00
DYefremov
8910b6d68b control decoupling 2020-11-03 20:36:21 +03:00
DYefremov
2a4c4576cc minor compatibility fix 2020-11-03 12:17:52 +03:00
DYefremov
4e8fd693a6 added web import for services (#16) 2020-11-02 21:55:34 +03:00
DYefremov
7184642e68 small decoupling for lamedb parsing 2020-11-02 14:33:55 +03:00
DYefremov
10f4a461bd added prototype of simple control panel (#38) 2020-10-25 12:54:08 +03:00
DYefremov
bec7f35b17 added remote control requests 2020-10-24 15:46:59 +03:00
DYefremov
2a6dc3f167 bump version 2020-10-22 15:44:37 +03:00
DYefremov
e9e36c4a9c added comments 2020-10-22 15:43:33 +03:00
DYefremov
0c49096733 upd README 2020-10-13 13:34:47 +03:00
DYefremov
e2aa21060b minor fix for the picon parser 2020-10-10 15:19:00 +03:00
DYefremov
b15691207b minor fix for yt 2020-10-06 11:25:26 +03:00
DYefremov
ab3ca134a7 Russian, Belarusian and German translations update 2020-10-04 14:53:42 +03:00
DYefremov
7f839f3fa0 data loading refactoring (prevent #37) 2020-10-02 13:42:43 +03:00
DYefremov
c3e8eac4b9 changed getting of the drag icon 2020-09-30 20:54:03 +03:00
DYefremov
04f808843d upd README 2020-09-25 18:02:32 +03:00
DYefremov
eea4a68993 Russian, Belarusian and German translations update 2020-09-25 18:01:36 +03:00
DYefremov
bfba5b5237 reworked and improved dnd for lists 2020-09-24 23:17:15 +03:00
DYefremov
e203a38966 added support for loading and importing data via dnd 2020-09-19 12:32:08 +03:00
DYefremov
a3b5609138 version update 2020-09-17 17:18:58 +03:00
DYefremov
b3c131b753 added support for opening archives 2020-09-17 17:16:00 +03:00
DYefremov
a8ea5ad974 minor rework of the chooser dialog 2020-09-17 10:00:02 +03:00
DYefremov
831184af2e displaying sid value in uppercase for tooltips(#34) 2020-09-12 14:11:42 +03:00
DYefremov
1f51766dea Display the sid value in tooltips in hex and dec format(#34). 2020-09-12 11:33:17 +03:00
DYefremov
f8f1536213 copy es *.mo file 2020-09-11 16:36:03 +03:00
Víctor Pont
0accfbd3d1 Spanish translation update (#36) 2020-09-11 16:24:59 +03:00
DYefremov
ee462b24f7 renaming bouquet fix [losing custom names](#33) 2020-09-08 12:21:10 +03:00
DYefremov
17ee189db8 upd README 2020-09-07 20:05:24 +03:00
DYefremov
8389293b4b copy pl *.mo file 2020-09-02 23:02:46 +03:00
Wieslaw Weglowski
99e0c79b6c Polish translation corrections (#31) 2020-09-02 19:16:21 +03:00
DYefremov
02cdbc4e56 Russian, Belarusian and German translations update 2020-09-02 14:16:48 +03:00
DYefremov
d57f0490d2 version update 2020-08-31 22:21:43 +03:00
DYefremov
9680347180 minor fix for picon assignment 2020-08-31 22:20:56 +03:00
DYefremov
7fbdc32f91 added Belarusian translation 2020-08-31 11:46:52 +03:00
DYefremov
a69f54435d fix playback from the start screen 2020-08-30 16:25:10 +03:00
DYefremov
0d47433f80 small rework of picons manager header 2020-08-30 14:26:18 +03:00
DYefremov
d16a8e44f6 resize fix in satellite dialog 2020-08-30 14:16:28 +03:00
DYefremov
053a834d6d telnet password visibility fix 2020-08-24 22:42:05 +03:00
DYefremov
62c1ef852c Polish translation update 2020-08-24 22:17:20 +03:00
DYefremov
b5234c55e8 minor optimization 2020-08-24 22:06:30 +03:00
Wieslaw Weglowski
472ebba8e9 Update demon-editor.po (#30) 2020-08-24 21:17:19 +03:00
DYefremov
d427cf66b0 rework of the picons resizing 2020-08-19 20:41:51 +03:00
DYefremov
9fe3d8077f German and Russian translation update 2020-08-15 16:51:47 +03:00
DYefremov
2f12ef7bdd minor yt fix 2020-08-15 16:50:34 +03:00
DYefremov
b6ad661e39 added dark mode option 2020-08-08 14:43:26 +03:00
DYefremov
6355e0d75a minor fixes 2020-08-07 11:31:40 +03:00
DYefremov
29016056c2 skipping enigma2 stop during picons upload 2020-08-06 21:16:21 +03:00
DYefremov
23fe71e5cc minor correction of translations 2020-08-04 12:45:10 +03:00
DYefremov
80e4edd084 version update 2020-08-03 22:47:44 +03:00
DYefremov
3d0bb6ad3c added option for experimental features 2020-08-03 22:41:14 +03:00
DYefremov
a8937d0698 Spanish, Portuguese and Dutch translation update 2020-07-27 16:30:10 +03:00
DYefremov
b97997b7a0 update ref fix 2020-07-24 11:37:27 +03:00
DYefremov
0003c6c5d5 German and Russian translation update 2020-07-20 11:32:56 +03:00
DYefremov
13b9d64bd0 added keyboard[del] support 2020-07-20 11:11:10 +03:00
DYefremov
c68511b223 loading providers fix 2020-07-19 20:51:18 +03:00
DYefremov
c6ef61222e fix display of cas 2020-07-19 14:59:37 +03:00
DYefremov
8309353784 version update 2020-07-18 21:08:52 +03:00
DYefremov
3f65975ac2 added lock support for iptv 2020-07-18 20:55:15 +03:00
DYefremov
4f6443e6e3 German translation update 2020-07-17 09:52:12 +03:00
DYefremov
d517b3b9d6 Russian translation update 2020-07-17 09:51:45 +03:00
DYefremov
faf228fa6f added audio codec option 2020-07-16 20:45:27 +03:00
DYefremov
eaf5e39458 added notifications 2020-07-15 11:16:09 +03:00
DYefremov
8a2539d57b minor fix 2020-07-12 22:28:49 +03:00
DYefremov
8c827b126f added bouquets import via dnd 2020-07-12 11:11:32 +03:00
DYefremov
db1bfb0fb9 minor gui fix 2020-07-11 13:00:01 +03:00
DYefremov
8ba7751b97 added debug mode option 2020-07-11 12:58:03 +03:00
DYefremov
65b58c9d08 added sorting of bouquet services 2020-07-09 22:29:33 +03:00
DYefremov
72aed5ff6e added download dialog options 2020-07-04 13:38:39 +03:00
DYefremov
ad07469c35 auto save profile settings 2020-07-02 17:24:21 +03:00
DYefremov
6f0de03b22 fix lock/hide in filter mode 2020-06-22 19:45:30 +03:00
DYefremov
1cec96d2b5 skip of marker counting (#27) 2020-06-22 11:07:44 +03:00
DYefremov
a9935dd0a7 changed callback for screenshots 2020-06-15 17:00:28 +03:00
DYefremov
65dfd6c1c4 small yt refactoring 2020-06-13 20:57:37 +03:00
DYefremov
65c24a324a get fav id fix 2020-06-13 01:01:20 +03:00
DYefremov
78b0cc1517 added space item to fav elements 2020-06-12 22:37:29 +03:00
DYefremov
d66d4e6402 added frame for info bar 2020-06-11 10:44:53 +03:00
DYefremov
7b11822664 small http api init fix 2020-06-10 18:35:49 +03:00
DYefremov
8c5f27cc8a added basic youtube-dl support 2020-06-10 11:10:41 +03:00
DYefremov
b51c8a9aed added eserviceuri (8193) stream type 2020-06-09 18:38:18 +03:00
DYefremov
2c4302f57d copy tr *.mo file 2020-06-08 23:50:41 +03:00
audi06_19
bd0ac077f9 Turkish translation correction (#26) 2020-06-08 23:50:17 +03:00
DYefremov
105b907392 small rework of screenshot mode 2020-06-08 19:32:18 +03:00
DYefremov
b4fb684af4 impl local removing for picons 2020-06-08 14:32:49 +03:00
DYefremov
20a1bac22e logging extension on data downloading 2020-06-08 13:33:46 +03:00
DYefremov
9a9229f67c added dnd for selective download/send 2020-06-08 13:16:50 +03:00
DYefremov
a941c96c61 added selective download/send of picons 2020-06-07 18:44:46 +03:00
DYefremov
05a6e36589 added update picons dest view 2020-06-06 09:36:11 +03:00
DYefremov
50a5cf6fc3 added accelerator for input dialog 2020-06-04 11:33:26 +03:00
DYefremov
6ef844157e added check for unsaved changes 2020-06-04 11:32:53 +03:00
DYefremov
72583ba879 rm unavailable iptv fix 2020-06-03 11:25:53 +03:00
DYefremov
e7d96b0cbb copy pl *.mo file 2020-06-02 10:01:32 +03:00
DYefremov
a7458494a3 Polish translation correction 2020-06-02 09:37:45 +03:00
Wieslaw Weglowski
747c1a9722 Polish translation update(#23) 2020-06-02 09:15:54 +03:00
DYefremov
dac2fe17a6 improved functionality of the picons explorer 2020-06-01 21:36:56 +03:00
DYefremov
fb8cf6c882 added youtube-dl options 2020-05-29 16:43:23 +03:00
DYefremov
bcf69231a8 added app settings read exception 2020-05-29 13:24:55 +03:00
DYefremov
5352d87b82 zap mode fix 2020-05-28 11:30:32 +03:00
DYefremov
c978f5abab added services filtering from picons manager 2020-05-25 18:25:36 +03:00
DYefremov
636442bcd3 adding picons to src via dnd 2020-05-25 11:42:41 +03:00
DYefremov
9e74f0f525 added basic screenshots support 2020-05-24 18:47:56 +03:00
DYefremov
b7028ae27d modified cas values 2020-05-23 15:17:45 +03:00
DYefremov
4f3b05ede5 added space [hidden marker] support 2020-05-23 15:16:31 +03:00
DYefremov
e6e6d3510d version update 2020-05-23 15:01:55 +03:00
DYefremov
1d1b4acdca small bq parsing changes (prevent #12) 2020-05-23 14:23:20 +03:00
DYefremov
0e50f1952d drag on icon fix 2020-05-23 00:03:53 +03:00
DYefremov
8686e15446 German translation update 2020-05-19 14:28:36 +03:00
DYefremov
5fee00a150 Russian translation update 2020-05-19 14:27:44 +03:00
DYefremov
0ec9873940 minor log changes 2020-05-19 11:39:12 +03:00
DYefremov
0600737319 Dutch translation update 2020-05-18 16:28:10 +03:00
DYefremov
cf51a5bfd8 small dnd fix 2020-05-18 12:34:28 +03:00
DYefremov
533d8fae25 minor fixes 2020-05-17 13:08:02 +03:00
DYefremov
16167b1b13 scaling picons on loading 2020-05-12 20:44:37 +03:00
DYefremov
38f6c06292 copy tr *.mo file 2020-05-11 21:58:58 +03:00
audi06
4170864a74 Turkish translation update (#22) 2020-05-11 21:48:43 +03:00
DYefremov
3171540bf2 minor optimization and fix 2020-05-10 21:17:24 +03:00
DYefremov
0ed4f6d7f9 German translation update 2020-05-10 18:31:27 +03:00
DYefremov
637becaa59 small fix to prevent (#12) 2020-05-10 18:30:06 +03:00
DYefremov
fd5a0d23a8 added picons filter by service name 2020-05-10 13:22:13 +03:00
DYefremov
22626ea03d Russian translation update 2020-05-09 14:02:19 +03:00
DYefremov
2e34568f74 fix to prevent #12 2020-05-08 17:13:17 +03:00
DYefremov
f42b2f3c75 added skip upload if file not found 2020-05-05 00:02:52 +03:00
DYefremov
f88b36cee7 fix use colors 2020-05-04 22:32:02 +03:00
DYefremov
4421f767e1 fix to prevent (#21) 2020-05-04 19:17:16 +03:00
DYefremov
898b32ca5f minor fixes for yt 2020-05-03 02:04:51 +03:00
DYefremov
7c2840c570 changed data dir creation 2020-05-03 00:22:16 +03:00
DYefremov
1c9c58d48a added accelerators and tooltips 2020-04-28 22:08:30 +03:00
DYefremov
45a1c79808 added download/upload of [terrestrial, cable].xml 2020-04-28 14:49:10 +03:00
DYefremov
19fbc753c5 slight optimization of loading/deleting data 2020-04-28 11:23:22 +03:00
DYefremov
c8c424750b setting text for wait dialog 2020-04-27 17:00:52 +03:00
DYefremov
3d769a5e18 path resolve fix 2020-04-27 12:51:10 +03:00
DYefremov
1b7d2f15b0 reworking of picons dialog 2020-04-26 19:26:36 +03:00
DYefremov
58cf299097 minor fixes for filter and search 2020-04-23 10:32:18 +03:00
DYefremov
af8fe227e0 added group style 2020-04-22 09:53:06 +03:00
DYefremov
3160ec2455 small refactoring of chooser dialog 2020-04-21 14:43:57 +03:00
DYefremov
04fd8b7182 upd README 2020-04-20 20:40:07 +03:00
DYefremov
ee90d11557 added picons assignment by drag on icon 2020-04-19 23:34:42 +03:00
DYefremov
b312816804 added hints support for the main list 2020-04-19 17:20:51 +03:00
DYefremov
39647cb811 minor refactoring 2020-04-19 13:23:18 +03:00
audi06
e64cd66977 added Turkish selection (#20) 2020-04-16 21:02:57 +03:00
DYefremov
754c586fb1 copy tr *.mo file 2020-04-16 18:49:35 +03:00
audi06
d5a2b22819 added Turkish translation (#19)
(cherry picked from commit 8d96f02e2e)
2020-04-16 18:20:03 +03:00
DYefremov
b20bcce5fa added bouquet file naming option 2020-04-16 11:55:48 +03:00
DYefremov
aba82c7120 added appearance settings 2020-04-13 13:46:02 +03:00
DYefremov
e0beeef2a3 top separator revert 2020-04-10 15:09:58 +03:00
DYefremov
c8a9d3f4a0 added basic hints support 2020-04-06 16:50:11 +03:00
DYefremov
9f7c713712 added option for hints 2020-04-05 18:19:12 +03:00
DYefremov
f89196041b epg options fix 2020-04-02 16:50:58 +03:00
DYefremov
efa2d94239 changed some player args 2020-03-29 12:57:33 +03:00
DYefremov
f53f483dce basic implementation of the play mode 2020-03-28 17:56:39 +03:00
DYefremov
ebcf0a90b5 small cleaning 2020-03-28 17:38:40 +03:00
DYefremov
2311b046e7 copy *.mo file 2020-03-24 15:36:08 +03:00
wwns
9544b6028f Polish translation update (#18) 2020-03-24 15:26:30 +03:00
DYefremov
e04144b10f wrap m3u data 2020-03-22 23:26:01 +03:00
DYefremov
93f68a7fe2 added play streams mode options 2020-03-22 14:13:01 +03:00
DYefremov
947ea21ed1 copy .mo file 2020-03-21 16:45:19 +03:00
Víctor Pont
d20d40c19b Spanish translation update and corrections (#17) 2020-03-21 16:31:07 +03:00
DYefremov
bb349a3fe9 changed record button update 2020-03-13 14:36:16 +03:00
DYefremov
bb3ecf975b added transcoding options 2020-03-09 14:53:03 +03:00
DYefremov
bbc693e5e6 added record of current service 2020-03-07 18:33:51 +03:00
DYefremov
cb29cf0155 added new paths settings 2020-03-06 11:55:34 +03:00
DYefremov
0c8592fb0d fix service status info 2020-02-29 21:15:24 +03:00
DYefremov
66f067340d version update 2020-02-28 21:10:13 +03:00
DYefremov
b0ec8e5483 added picons multiple assignment 2020-02-28 20:59:53 +03:00
DYefremov
aa4b31edfc copy .mo file 2020-02-27 23:53:28 +03:00
DYefremov
3d627b57a4 German translation update 2020-02-27 23:50:54 +03:00
DYefremov
6b1bec500c upd README 2020-02-24 12:48:17 +03:00
DYefremov
7444db7e21 small fix 2020-02-24 12:17:10 +03:00
DYefremov
8be92a9c7e copy .mo file 2020-02-20 14:20:09 +03:00
DYefremov
7554f40c6a Russian translation update 2020-02-20 14:07:35 +03:00
DYefremov
17ab321e44 gui changes for send to 2020-02-20 11:38:45 +03:00
DYefremov
ac345d4ef3 fix getting sats 2020-02-19 12:04:02 +03:00
DYefremov
e2a56a316d .mo files update 2020-02-17 09:19:39 +03:00
wwns
9b79bf2b81 Polish translation update (#11)
Polish translation update.
2020-02-17 08:54:17 +03:00
DYefremov
ee2a9bda90 added appindicator support 2020-02-15 12:51:16 +03:00
DYefremov
da0c5fa8a6 updated .mo files 2020-02-14 22:50:35 +03:00
wwns
e202ec6abe Polish translation update (#10) 2020-02-14 22:28:15 +03:00
DYefremov
e87be79f42 minor fix 2020-02-13 20:15:18 +03:00
DYefremov
6372ac474c toolbar changes 2020-02-13 01:09:40 +03:00
DYefremov
c6b0f70c8e update of data path 2020-02-12 13:51:40 +03:00
DYefremov
6a921ad394 added Polish selection 2020-02-12 12:39:12 +03:00
DYefremov
4c8743517f copy .mo file 2020-02-11 22:02:20 +03:00
wwns
74ec0fe956 added a Polish translation (#5)
* added a Polish translation

* added a Polish translation

* name change
2020-02-11 21:52:54 +03:00
DYefremov
041f717a01 hotkey refactoring 2020-02-11 13:18:14 +03:00
DYefremov
67dbdb19d7 fix bq deletion 2020-02-10 19:24:48 +03:00
DYefremov
de49179dd2 changing profile on data download 2020-02-10 17:00:46 +03:00
DYefremov
2723d255fe fix profile edit 2020-02-10 14:45:05 +03:00
DYefremov
4515b2538b moved get yt icon 2020-02-07 16:56:01 +03:00
DYefremov
88e3a22cf0 auto rename bouquets with duplicate names 2020-01-31 15:09:56 +03:00
DYefremov
7ac63b81c0 added checking for bouquet names duplicate 2020-01-29 14:50:02 +03:00
DYefremov
234611b686 added controls to the transmitter 2020-01-28 15:08:57 +03:00
DYefremov
fdb2691430 added player requests 2020-01-28 14:51:23 +03:00
DYefremov
d81700c30c minor gui changes 2020-01-24 00:53:42 +03:00
DYefremov
e91c4c33a5 added reset button hiding 2020-01-24 00:04:43 +03:00
DYefremov
40bf54e94f fix size of picons 2020-01-23 23:42:28 +03:00
DYefremov
4a50c36ab4 added remove and download to picons 2020-01-23 00:47:01 +03:00
95 changed files with 24058 additions and 11037 deletions

13
DemonEditor.desktop Executable file
View File

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

View File

@@ -1,66 +0,0 @@
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
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=['youtube_dl'],
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 satellites editor",
'CFBundleShortVersionString': "1.0.0 Alpha-2 (Build: {})".format(BUILD_DATE),
'NSHumanReadableCopyright': u"Copyright © 2020, Dmitriy Yefremov",
'NSRequiresAquaSystemAppearance': '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

151
README.md
View File

@@ -1,73 +1,94 @@
# <img src="app/ui/icons/hicolor/96x96/apps/demon-editor.png" width="32" /> DemonEditor
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
## Enigma2 channel and satellites list editor for macOS (experimental).
### The functionality and performance of this version may be different from the Linux version!
[![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/118884719-8277e980-b8ff-11eb-8621-c8c4afd6181b.png" width="560"/>](https://user-images.githubusercontent.com/7511379/118884719-8277e980-b8ff-11eb-8621-c8c4afd6181b.png)
### Main features of the program:
* Editing bouquets, channels, satellites.
* Import function.
* Backup function.
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc).
## Main features of the program
* Editing bouquets, channels, satellites.
[<img src="https://user-images.githubusercontent.com/7511379/118884747-8ad02480-b8ff-11eb-9104-8cf8fb6e785d.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118884747-8ad02480-b8ff-11eb-9104-8cf8fb6e785d.png)
* Import function.
[<img src="https://user-images.githubusercontent.com/7511379/118526825-4dc23180-b749-11eb-8197-e9bbccbc3bdf.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118526825-4dc23180-b749-11eb-8197-e9bbccbc3bdf.png)
* Backup function.
[<img src="https://user-images.githubusercontent.com/7511379/118528402-f58c2f00-b74a-11eb-9b84-edf220526e6e.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118528402-f58c2f00-b74a-11eb-9b84-edf220526e6e.png)
* Support of picons.
[<img src="https://user-images.githubusercontent.com/7511379/118526864-5c104d80-b749-11eb-8497-6e8c78542ab1.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118526864-5c104d80-b749-11eb-8497-6e8c78542ab1.png)
* Importing services, downloading picons and updating satellites from the Web.
[<img src="https://user-images.githubusercontent.com/7511379/118530243-1a81a180-b74d-11eb-8e01-aea904d954af.png" width="250"/>](https://user-images.githubusercontent.com/7511379/118530243-1a81a180-b74d-11eb-8e01-aea904d954af.png)
[<img src="https://user-images.githubusercontent.com/7511379/118526706-31be9000-b749-11eb-9956-c4bf2e13f968.png" width="292"/>](https://user-images.githubusercontent.com/7511379/118526706-31be9000-b749-11eb-9956-c4bf2e13f968.png)
* Extended support of IPTV.
* Support of picons.
* Downloading of picons and updating of satellites (transponders) from the web.
* Import to bouquet(Neutrino WEBTV) from m3u.
* Export of bouquets with IPTV services in m3u.
* Assignment of EPGs from DVB or XML for IPTV services (only Enigma2, experimental).
* Preview (playback) of IPTV or other streams directly from the bouquet list (should be installed [VLC](https://www.videolan.org/vlc/)).
#### Keyboard shortcuts:
* **&#8984; + X** - only in bouquet list.
* **&#8984; + C** - only in services list.
Clipboard is **"rubber"**. There is an accumulation before the insertion!
* **&#8984; + E** - edit.
* **&#8984; + R, F2** - rename.
* **&#8984; + S, T** in Satellites edit tool for create satellite or transponder.
* **&#8984; + L** - parental lock.
* **&#8984; + H** - hide/skip.
* **&#8984; + P** - start play IPTV or other stream in the bouquet list.
* **&#8984; + Z** - switch(**zap**) the channel(works when the HTTP API is enabled, Enigma2 only).
* **&#8984; + W** - switch to the channel and watch in the program.
* **&#8984; + Up/Down** - move selected items in the list.
* **&#8984; + O** - (re)load user data from current dir.
* **&#8984; + D** - load data from receiver.
* **&#8984; + U/B** - upload data/bouquets to receiver.
* **&#8984; + F** - show/hide search bar.
* **&#8679; + &#8984; + F** - show/hide filter bar.
* **Left/Right** - remove selection.
* Assignment of EPG from DVB or XML for IPTV services (only Enigma2, experimental).
* Preview (playback) of IPTV or other streams directly from the bouquet list.
[<img src="https://user-images.githubusercontent.com/7511379/118884891-b3f0b500-b8ff-11eb-8717-3588d6e089de.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118884891-b3f0b500-b8ff-11eb-8717-3588d6e089de.png)
* Control panel with the ability to view EPG and manage timers (via HTTP API, experimental).
[<img src="https://user-images.githubusercontent.com/7511379/118886284-66754780-b901-11eb-9068-29b5a607ccaf.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118886284-66754780-b901-11eb-9068-29b5a607ccaf.png)
* Simple FTP client (experimental).
[<img src="https://user-images.githubusercontent.com/7511379/118527372-e8bb0b80-b749-11eb-9653-4ad64c99a05a.png" width="480"/>](https://user-images.githubusercontent.com/7511379/118527372-e8bb0b80-b749-11eb-9653-4ad64c99a05a.png)
#### Keyboard shortcuts
* **Ctrl + X** - only in bouquet list.
* **Ctrl + C** - only in services list.
Clipboard is **"rubber"**. There is an accumulation before the insertion!
* **Ctrl + Insert** - copies the selected channels from the main list to the bouquet
beginning or inserts (creates) a new bouquet.
* **Ctrl + BackSpace** - copies the selected channels from the main list to the bouquet end.
* **Ctrl + E** - edit.
* **Ctrl + R, F2** - rename.
* **Ctrl + S, T** in Satellites edit tool for create satellite or transponder.
* **Ctrl + L** - parental lock.
* **Ctrl + H** - hide/skip.
* **Ctrl + P** - start play IPTV or other stream in the bouquet list.
* **Ctrl + Z** - switch(**zap**) the channel(works when the HTTP API is enabled, Enigma2 only).
* **Ctrl + W** - switch to the channel and watch in the program.
* **Space** - select/deselect.
* **Left/Right** - remove selection.
* **Ctrl + Up, Down, PageUp, PageDown, Home, End**- move selected items in the list.
* **Ctrl + O** - (re)load user data from current dir.
* **Ctrl + D** - load data from receiver.
* **Ctrl + U/B** - upload data/bouquets to receiver.
* **Ctrl + I** - extra info, details.
* **Ctrl + F** - show/hide search bar.
* **Ctrl + Shift + F** - show/hide filter bar.
For **multiple** selection with the mouse, press and hold the **Ctrl** key!
For multiple mouse selection (including Drag and Drop), press and hold the **&#8984;** key!
## Minimum requirements
*Python >= 3.5.2, GTK+ >= 3.22, python3-gi, python3-gi-cairo, python3-requests.*
### Minimum requirements:
Python >= **3.5**, GTK+ >= **3.16**, pygobject3, adwaita-icon-theme, python3-requests.
### Installation:
```brew install python3 gtk+3 pygobject3 adwaita-icon-theme```
```pip3 install requests```
### Optional:
```brew install wget```
```pip3 install pillow, pyobjc```
### Launching:
To start the program, just download the archive, unpack and run it from the terminal with the command: ```./start.py```
### Building standalone application:
Install [PyInstaller](https://www.pyinstaller.org/) with the command from the terminal:
```pip3 install pyinstaller```
and in th root dir run command:
```pyinstaller DemonEditor.spec```
### Standalone package:
You can 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!
**The package may not contain all the latest changes. Not all features can be supported and tested!**
### Note:
THIS SOFTWARE COMES WITH ABSOLUTELY NO WARRANTY.
AUTHOR IS NOT LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY CONNECTION WITH THIS SOFTWARE.
The package 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 package you agree to the terms of this [license](http://www.gnu.org/licenses/gpl-3.0.html) and the possible inconvenience associated with this!
### Important:
Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2!
Main supported *lamedb* format is version **4**. Versions **3** and **5** has only **experimental** support!
For version **3** is only read mode available. When saving, version **4** format is used instead!
***Optional:** python3-pil, python3-chardet.*
## Installation and Launch
* ### Linux
To start the program, in most cases it is enough to download the [archive](https://github.com/DYefremov/DemonEditor/archive/master.zip), unpack
and run it by double clicking on DemonEditor.desktop in the root directory,
or launching from the console with the command:```./start.py```
Extra folders can be deleted, excluding the *app* folder and root files like *DemonEditor.desktop* and *start.py*!
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.
To create a simple **debian package**, you can use the *build-deb.sh.* You can also download a ready-made *.deb package from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
Users of **LTS** versions of [Ubuntu](https://ubuntu.com/) or distributions based on them can use [PPA](https://launchpad.net/~dmitriy-yefremov/+archive/ubuntu/demon-editor) repository.
A ready-made [package](https://aur.archlinux.org/packages/demoneditor-bin) is also available for [Arch Linux](https://archlinux.org/) users in the [AUR](https://aur.archlinux.org/) repository.
* ### macOS
**This program can be run on macOS.** To work in this OS, you must use a [separate branch](https://github.com/DYefremov/DemonEditor/tree/experimental-mac). A ready-made package can be downloaded from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
**The functionality and performance of this version may be different from the Linux version!**
## Important
The program is tested only with [openATV](https://www.opena.tv/) image and **Formuler F1** receiver in [Linux Mint](https://linuxmint.com/) (MATE 64-bit) distribution!
Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2.
Main supported *lamedb* format is version **4**. Versions **3** and **5** has only **experimental** support! For version **3** is only read mode available. When saving, version **4** format is used instead.
When using the multiple import feature, from *lamedb* will be taken data **only for channels that are in the selected bouquets!**
If you need full set of the data, including *[satellites, terrestrial, cables].xml* (current files will be overwritten),
just load your data via *"File/Open"* and press *"Save"*. When importing separate bouquet files, only those services
(excluding IPTV) that are in the **current open lamedb** (main list of services) will be imported.
For streams playback, this app supports [VLC](https://www.videolan.org/vlc/), [MPV](https://mpv.io/) and [GStreamer](https://gstreamer.freedesktop.org/). Depending on your distro, you may need to install additional packages and libraries.
#### Command line arguments:
* **-l** - write logs to file.
* **-d on/off** - turn on/off debug mode. Allows to display more information in the logs.
## License
Licensed under the [MIT](LICENSE) license.

View File

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

View File

@@ -5,7 +5,7 @@ import time
import urllib
import xml.etree.ElementTree as ETree
from enum import Enum
from ftplib import FTP, error_perm
from ftplib import FTP, CRLF, Error, error_perm
from http.client import RemoteDisconnected
from telnetlib import Telnet
from urllib.error import HTTPError, URLError
@@ -35,25 +35,6 @@ class DownloadType(Enum):
EPG = 5
class HttpRequestType(Enum):
ZAP = "zap?sRef="
INFO = "about"
SIGNAL = "signal"
STREAM = "stream.m3u?ref="
STREAM_CURRENT = "streamcurrent.m3u"
CURRENT = "getcurrent"
TEST = None
TOKEN = "session"
PLAY = "mediaplayerplay?file="
PLAYER_LIST = "mediaplayerlist?path=playlist"
PLAYER_PLAY = "mediaplayercmd?command=play"
PLAYER_NEXT = "mediaplayercmd?command=next"
PLAYER_PREV = "mediaplayercmd?command=previous"
PLAYER_STOP = "mediaplayercmd?command=stop"
PLAYER_REMOVE = "mediaplayerremove?file="
GRUB = "grab?format=jpg&"
class TestException(Exception):
pass
@@ -62,8 +43,289 @@ class HttpApiException(Exception):
pass
def download_data(*, settings, download_type=DownloadType.ALL, callback=print, files_filter=None):
with FTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
class UtfFTP(FTP):
""" FTP class wrapper. """
def retrlines(self, cmd, callback=None):
""" Small modification of the original method.
It is used to retrieve data in line mode and skip errors related
to reading file names in encoding other than UTF-8 or Latin-1.
Decode errors are ignored [UnicodeDecodeError, etc].
"""
if callback is None:
callback = log
self.sendcmd("TYPE A")
with self.transfercmd(cmd) as conn, conn.makefile("r", encoding=self.encoding, errors="ignore") as fp:
while 1:
line = fp.readline(self.maxline + 1)
if len(line) > self.maxline:
msg = "UtfFTP [retrlines] error: got more than {} bytes".format(self.maxline)
log(msg)
raise Error(msg)
if self.debugging > 2:
log('UtfFTP [retrlines] *retr* {}'.format(repr(line)))
if not line:
break
if line[-2:] == CRLF:
line = line[:-2]
elif line[-1:] == "\n":
line = line[:-1]
callback(line)
return self.voidresp()
# ***************** Download ******************* #
def download_files(self, save_path, file_list, callback=None):
""" Downloads files from the receiver via FTP. """
for file in filter(lambda s: s.endswith(file_list), self.nlst()):
self.download_file(file, save_path, callback)
def download_file(self, name, save_path, callback=None):
with open(save_path + name, "wb") as f:
msg = "Downloading file: {}. Status: {}\n"
try:
resp = str(self.retrbinary("RETR " + name, f.write))
except error_perm as e:
resp = str(e)
msg = msg.format(name, e)
log(msg.rstrip())
else:
msg = msg.format(name, resp)
callback(msg) if callback else log(msg.rstrip())
return resp
def download_dir(self, path, save_path, callback=None):
""" Downloads directory from FTP with all contents.
Creates a leaf directory and all intermediate ones. This is recursive.
"""
os.makedirs(os.path.join(save_path, path), exist_ok=True)
files = []
self.dir(path, files.append)
for f in files:
f_data = f.split()
f_path = os.path.join(path, " ".join(f_data[8:]))
if f_data[0][0] == "d":
try:
os.makedirs(os.path.join(save_path, f_path), exist_ok=True)
except OSError as e:
msg = "Download dir error: {}".format(e).rstrip()
log(msg)
return "500 " + msg
else:
self.download_dir(f_path, save_path, callback)
else:
try:
self.download_file(f_path, save_path, callback)
except OSError as e:
log("Download dir error: {}".format(e).rstrip())
resp = "226 Transfer complete."
msg = "Copy directory {}. Status: {}".format(path, resp)
log(msg)
if callback:
callback(msg)
return resp
def download_xml(self, data_path, xml_path, xml_files, callback):
""" Used for download *.xml files. """
self.cwd(xml_path)
self.download_files(data_path, xml_files, callback)
def download_picons(self, src, dest, callback, files_filter=None):
try:
self.cwd(src)
except error_perm as e:
callback(str(e))
return
for file in filter(picons_filter_function(files_filter), self.nlst()):
self.download_file(file, dest, callback)
# ***************** Uploading ******************* #
def upload_bouquets(self, data_path, remove_unused, callback):
if remove_unused:
self.remove_unused_bouquets(callback)
self.upload_files(data_path, BQ_FILES_LIST, callback)
def upload_files(self, data_path, file_list, callback):
for file_name in os.listdir(data_path):
if file_name in STC_XML_FILE or file_name in WEB_TV_XML_FILE:
continue
if file_name.endswith(file_list):
self.send_file(file_name, data_path, callback)
def upload_xml(self, data_path, xml_path, xml_files, callback):
""" Used for transfer *.xml files. """
self.cwd(xml_path)
for xml_file in xml_files:
self.send_file(xml_file, data_path, callback)
def upload_picons(self, src, dest, callback, files_filter=None):
try:
self.cwd(dest)
except error_perm as e:
if str(e).startswith("550"):
self.mkd(dest) # if not exist
self.cwd(dest)
for file_name in filter(picons_filter_function(files_filter), os.listdir(src)):
self.send_file(file_name, src, callback)
def remove_unused_bouquets(self, callback):
bq_files = ("userbouquet.", "bouquets.xml", "ubouquets.xml")
for file in filter(lambda f: f.startswith(bq_files), self.nlst()):
self.delete_file(file, callback)
def send_file(self, file_name, path, callback=None):
""" Opens the file in binary mode and transfers into receiver """
file_src = path + file_name
resp = "500"
if not os.path.isfile(file_src):
log("Uploading file: '{}'. File not found. Skipping.".format(file_src))
return resp + " File not found."
with open(file_src, "rb") as f:
msg = "Uploading file: {}. Status: {}\n"
try:
resp = str(self.storbinary("STOR " + file_name, f))
except Error as e:
resp = str(e)
msg = msg.format(file_name, resp)
log(msg)
else:
msg = msg.format(file_name, resp)
if callback:
callback(msg)
return resp
def upload_dir(self, path, callback=None):
""" Uploads directory to FTP with all contents.
Creates a leaf directory and all intermediate ones. This is recursive.
"""
resp = "200"
msg = "Uploading directory: {}. Status: {}"
try:
files = os.listdir(path)
except OSError as e:
log(e)
else:
os.chdir(path)
for f in files:
file = r"{}{}".format(path, f)
if os.path.isfile(file):
self.send_file(f, path, callback)
elif os.path.isdir(file):
try:
self.mkd(f)
except Error:
pass # NOP
try:
self.cwd(f)
except Error as e:
resp = str(e)
log(msg.format(f, resp))
else:
self.upload_dir(file + "/")
self.cwd("..")
os.chdir("..")
if callback:
callback(msg.format(path, resp))
return resp
# ****************** Deletion ******************** #
def delete_picons(self, callback, dest=None, files_filter=None):
if dest:
try:
self.cwd(dest)
except Error as e:
callback(str(e))
return
for file in filter(picons_filter_function(files_filter), self.nlst()):
self.delete_file(file, callback)
def delete_file(self, file, callback=log):
msg = "Deleting file: {}. Status: {}\n"
try:
resp = self.delete(file)
except Error as e:
resp = str(e)
msg = msg.format(file, resp)
log(msg)
else:
msg = msg.format(file, resp)
if callback:
callback(msg)
return resp
def delete_dir(self, path, callback=None):
files = []
self.dir(path, files.append)
for f in files:
f_data = f.split()
name = " ".join(f_data[8:])
f_path = path + "/" + name
if f_data[0][0] == "d":
self.delete_dir(f_path, callback)
else:
self.delete_file(f_path, callback)
msg = "Remove directory {}. Status: {}\n"
try:
resp = self.rmd(path)
except Error as e:
msg = msg.format(path, e)
log(msg)
return "500"
else:
msg = msg.format(path, resp)
log(msg.rstrip())
if callback:
callback(msg)
return resp
def rename_file(self, from_name, to_name, callback=None):
msg = "File rename: {}. Status: {}\n"
try:
resp = self.rename(from_name, to_name)
except Error as e:
resp = str(e)
msg = msg.format(from_name, resp)
log(msg)
else:
msg = msg.format(from_name, resp)
if callback:
callback(msg)
return resp
def download_data(*, settings, download_type=DownloadType.ALL, callback=log, files_filter=None):
with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
ftp.encoding = "utf-8"
callback("FTP OK.\n")
save_path = settings.data_local_path
@@ -72,18 +334,17 @@ def download_data(*, settings, download_type=DownloadType.ALL, callback=print, f
if download_type is DownloadType.ALL or download_type is DownloadType.BOUQUETS:
ftp.cwd(settings.services_path)
file_list = BQ_FILES_LIST + DATA_FILES_LIST if download_type is DownloadType.ALL else BQ_FILES_LIST
for file in filter(lambda f: f.endswith(file_list), ftp.nlst()):
download_file(ftp, file, save_path, callback)
ftp.download_files(save_path, file_list, callback)
# *.xml and webtv
if download_type in (DownloadType.ALL, DownloadType.SATELLITES):
download_xml(ftp, save_path, settings.satellites_xml_path, STC_XML_FILE, callback)
ftp.download_xml(save_path, settings.satellites_xml_path, STC_XML_FILE, callback)
if download_type in (DownloadType.ALL, DownloadType.WEBTV):
download_xml(ftp, save_path, settings.satellites_xml_path, WEB_TV_XML_FILE, callback)
ftp.download_xml(save_path, settings.satellites_xml_path, WEB_TV_XML_FILE, callback)
if download_type is DownloadType.PICONS:
picons_path = settings.picons_local_path
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
download_picons(ftp, settings.picons_path, picons_path, callback, files_filter)
ftp.download_picons(settings.picons_path, picons_path, callback, files_filter)
# epg.dat
if download_type is DownloadType.EPG:
stb_path = settings.services_path
@@ -93,14 +354,13 @@ def download_data(*, settings, download_type=DownloadType.ALL, callback=print, f
save_path = epg_options.get("epg_dat_path", save_path)
ftp.cwd(stb_path)
for file in filter(lambda f: f.endswith("epg.dat"), ftp.nlst()):
download_file(ftp, file, save_path, callback)
ftp.download_files(save_path, "epg.dat", callback)
callback("\nDone.\n")
def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False,
callback=print, done_callback=None, use_http=False, files_filter=None):
callback=log, done_callback=None, use_http=False, files_filter=None):
s_type = settings.setting_type
data_path = settings.data_local_path
host = settings.host
@@ -110,7 +370,7 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
try:
if s_type is SettingsType.ENIGMA_2 and use_http:
ht = http(settings.http_user, settings.http_password, base_url, callback, settings.http_use_ssl)
ht = http(settings.user, settings.password, base_url, callback, settings.http_use_ssl)
next(ht)
message = ""
if download_type is DownloadType.BOUQUETS:
@@ -133,44 +393,47 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
if download_type is not DownloadType.PICONS:
# telnet
tn = telnet(host=host,
user=settings.telnet_user,
password=settings.telnet_password,
user=settings.user,
password=settings.password,
timeout=settings.telnet_timeout)
next(tn)
# terminate enigma or neutrino
callback("Telnet initialization ...\n")
tn.send("init 4")
callback("Stopping GUI...\n")
with FTP(host=host, user=settings.user, passwd=settings.password) as ftp:
with UtfFTP(host=host, user=settings.user, passwd=settings.password) as ftp:
ftp.encoding = "utf-8"
callback("FTP OK.\n")
sat_xml_path = settings.satellites_xml_path
services_path = settings.services_path
if download_type is DownloadType.SATELLITES:
upload_xml(ftp, data_path, sat_xml_path, STC_XML_FILE, callback)
ftp.upload_xml(data_path, sat_xml_path, STC_XML_FILE, callback)
if s_type is SettingsType.NEUTRINO_MP and download_type is DownloadType.WEBTV:
upload_xml(ftp, data_path, sat_xml_path, WEB_TV_XML_FILE, callback)
ftp.upload_xml(data_path, sat_xml_path, WEB_TV_XML_FILE, callback)
if download_type is DownloadType.BOUQUETS:
ftp.cwd(services_path)
upload_bouquets(ftp, data_path, remove_unused, callback)
ftp.upload_bouquets(data_path, remove_unused, callback)
if download_type is DownloadType.ALL:
upload_xml(ftp, data_path, sat_xml_path, STC_XML_FILE, callback)
ftp.upload_xml(data_path, sat_xml_path, STC_XML_FILE, callback)
if s_type is SettingsType.NEUTRINO_MP:
upload_xml(ftp, data_path, sat_xml_path, WEB_TV_XML_FILE, callback)
ftp.upload_xml(data_path, sat_xml_path, WEB_TV_XML_FILE, callback)
ftp.cwd(services_path)
upload_bouquets(ftp, data_path, remove_unused, callback)
upload_files(ftp, data_path, DATA_FILES_LIST, callback)
ftp.upload_bouquets(data_path, remove_unused, callback)
ftp.upload_files(data_path, DATA_FILES_LIST, callback)
if download_type is DownloadType.PICONS:
upload_picons(ftp, settings.picons_local_path, settings.picons_path, callback, files_filter)
ftp.upload_picons(settings.picons_local_path, settings.picons_path, callback, files_filter)
if tn and not use_http:
# resume enigma or restart neutrino
tn.send("init 3" if s_type is SettingsType.ENIGMA_2 else "init 6")
callback("Starting...\n" if s_type is SettingsType.ENIGMA_2 else "Rebooting...\n")
elif ht and use_http:
if download_type is DownloadType.BOUQUETS:
ht.send((url + "servicelistreload?mode=2", "Reloading Userbouquets."))
@@ -187,80 +450,13 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
ht.close()
def upload_bouquets(ftp, data_path, remove_unused, callback):
if remove_unused:
remove_unused_bouquets(ftp, callback)
upload_files(ftp, data_path, BQ_FILES_LIST, callback)
def upload_files(ftp, data_path, file_list, callback):
for file_name in os.listdir(data_path):
if file_name in STC_XML_FILE or file_name in WEB_TV_XML_FILE:
continue
if file_name.endswith(file_list):
send_file(file_name, data_path, ftp, callback)
def remove_unused_bouquets(ftp, callback):
for file in filter(lambda f: f.endswith(("tv", "radio", "bouquets.xml", "ubouquets.xml")), ftp.nlst()):
callback("Deleting file: {}. Status: {}\n".format(file, ftp.delete(file)))
def upload_xml(ftp, data_path, xml_path, xml_files, callback):
""" Used for transfer *.xml files. """
ftp.cwd(xml_path)
for xml_file in xml_files:
send_file(xml_file, data_path, ftp, callback)
def download_xml(ftp, data_path, xml_path, xml_files, callback):
""" Used for download *.xml files. """
ftp.cwd(xml_path)
list(map(lambda f: download_file(ftp, f, data_path, callback), (f for f in ftp.nlst() if f.endswith(xml_files))))
# ***************** Picons *******************#
def upload_picons(ftp, src, dest, callback, files_filter=None):
try:
ftp.cwd(dest)
except error_perm as e:
if str(e).startswith("550"):
ftp.mkd(dest) # if not exist
ftp.cwd(dest)
for file_name in filter(picons_filter_function(files_filter), os.listdir(src)):
send_file(file_name, src, ftp, callback)
def download_picons(ftp, src, dest, callback, files_filter=None):
try:
ftp.cwd(src)
except error_perm as e:
callback(str(e))
return
for file in filter(picons_filter_function(files_filter), ftp.nlst()):
download_file(ftp, file, dest, callback)
def delete_picons(ftp, callback, dest=None, files_filter=None):
if dest:
try:
ftp.cwd(dest)
except error_perm as e:
callback(str(e))
return
for file in filter(picons_filter_function(files_filter), ftp.nlst()):
callback("Delete file: {}. Status: {}\n".format(file, ftp.delete(file)))
def remove_picons(*, settings, callback, done_callback=None, files_filter=None):
with FTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
ftp.encoding = "utf-8"
callback("FTP OK.\n")
delete_picons(ftp, callback, settings.picons_path, files_filter)
ftp.delete_picons(callback, settings.picons_path, files_filter)
if done_callback:
done_callback()
@@ -269,29 +465,13 @@ def picons_filter_function(files_filter=None):
return lambda f: f in files_filter if files_filter else f.endswith(PICONS_SUF)
def download_file(ftp, name, save_path, callback):
with open(save_path + name, "wb") as f:
callback("Downloading file: {}. Status: {}\n".format(name, str(ftp.retrbinary("RETR " + name, f.write))))
def send_file(file_name, path, ftp, callback):
""" Opens the file in binary mode and transfers into receiver """
file_src = path + file_name
if not os.path.isfile(file_src):
log("Uploading file: '{}'. File not found. Skipping.".format(file_src))
return
with open(file_src, "rb") as f:
callback("Uploading file: {}. Status: {}\n".format(file_name, str(ftp.storbinary("STOR " + file_name, f))))
def http(user, password, url, callback, use_ssl=False):
init_auth(user, password, url, use_ssl)
data = get_post_data(url, password, url)
while True:
url, message = yield
resp = get_response(HttpRequestType.TEST, url, data).get("e2statetext", None)
resp = get_response(HttpAPI.Request.TEST, url, data).get("e2statetext", None)
callback("HTTP: {} {}\n".format(message, "Successful." if resp and message else ""))
@@ -304,11 +484,11 @@ def telnet(host, port=23, user="", password="", timeout=5):
time.sleep(1)
command = yield
if user != "":
tn.read_until(b"login: ")
tn.read_until(b"login: ", timeout)
tn.write(user.encode("utf-8") + b"\n")
time.sleep(timeout)
if password != "":
tn.read_until(b"Password: ")
tn.read_until(b"Password: ", timeout)
tn.write(password.encode("utf-8") + b"\n")
time.sleep(timeout)
tn.write("{}\r\n".format(command).encode("utf-8"))
@@ -325,6 +505,74 @@ def telnet(host, port=23, user="", password="", timeout=5):
class HttpAPI:
__MAX_WORKERS = 4
class Request(Enum):
ZAP = "zap?sRef="
INFO = "about"
SIGNAL = "signal"
STREAM = "stream.m3u?ref="
STREAM_TS = "ts.m3u?file="
STREAM_CURRENT = "streamcurrent.m3u"
CURRENT = "getcurrent"
TEST = None
TOKEN = "session"
# Player
PLAY = "mediaplayerplay?file="
PLAYER_LIST = "mediaplayerlist?path=playlist"
PLAYER_PLAY = "mediaplayercmd?command=play"
PLAYER_NEXT = "mediaplayercmd?command=next"
PLAYER_PREV = "mediaplayercmd?command=previous"
PLAYER_STOP = "mediaplayercmd?command=stop"
PLAYER_REMOVE = "mediaplayerremove?file="
# Remote control
POWER = "powerstate?newstate="
REMOTE = "remotecontrol?command="
VOL = "vol?set=set"
# EPG
EPG = "epgservice?sRef="
# Timer
TIMER = ""
TIMER_LIST = "timerlist"
# Recordings
RECORDINGS = "movielist?dirname="
REC_DIRS = "getlocations"
REC_CURRENT = "getcurrlocation"
# Screenshot
GRUB = "grab?format=jpg&"
class Remote(str, Enum):
""" Args for HttpRequestType [REMOTE] class. """
UP = "103"
LEFT = "105"
RIGHT = "106"
DOWN = "108"
MENU = "139"
EXIT = "174"
OK = "352"
RED = "398"
GREEN = "399"
YELLOW = "400"
BLUE = "401"
class Power(str, Enum):
""" Args for HttpRequestType [POWER] class. """
TOGGLE_STANDBY = "0"
DEEP_STANDBY = "1"
REBOOT = "2"
RESTART_GUI = "3"
WAKEUP = "4"
STANDBY = "5"
PARAM_REQUESTS = {Request.REMOTE,
Request.POWER,
Request.VOL,
Request.EPG,
Request.TIMER,
Request.RECORDINGS}
STREAM_REQUESTS = {Request.STREAM,
Request.STREAM_CURRENT,
Request.STREAM_TS}
def __init__(self, settings):
from concurrent.futures import ThreadPoolExecutor as PoolExecutor
self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS)
@@ -345,13 +593,15 @@ class HttpAPI:
url = self._base_url + req_type.value
data = self._data
if req_type is HttpRequestType.ZAP or req_type is HttpRequestType.STREAM:
if req_type is self.Request.ZAP or req_type in self.STREAM_REQUESTS:
url += urllib.parse.quote(ref)
elif req_type is HttpRequestType.PLAY or req_type is HttpRequestType.PLAYER_REMOVE:
elif req_type is self.Request.PLAY or req_type is self.Request.PLAYER_REMOVE:
url += "{}{}".format(ref_prefix, urllib.parse.quote(ref).replace("%3A", "%253A"))
elif req_type is HttpRequestType.GRUB:
elif req_type is self.Request.GRUB:
data = None # Must be disabled for token-based security.
url = "{}/{}{}".format(self._main_url, req_type.value, ref)
elif req_type in self.PARAM_REQUESTS:
url += ref
def done_callback(f):
callback(f.result())
@@ -361,17 +611,17 @@ class HttpAPI:
@run_task
def init(self):
user, password = self._settings.http_user, self._settings.http_password
user, password = self._settings.user, self._settings.password
use_ssl = self._settings.http_use_ssl
self._main_url = "http{}://{}:{}".format("s" if use_ssl else "", self._settings.host, self._settings.http_port)
self._base_url = "{}/web/".format(self._main_url)
init_auth(user, password, self._main_url, use_ssl)
url = "{}/web/{}".format(self._main_url, HttpRequestType.TOKEN.value)
url = "{}/web/{}".format(self._main_url, self.Request.TOKEN.value)
s_id = get_session_id(user, password, url)
if s_id != "0":
self._data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
self.send(HttpRequestType.INFO, None, self.init_callback)
self.send(self.Request.INFO, None, self.init_callback)
def init_callback(self, info):
if info:
@@ -394,24 +644,35 @@ class HttpAPI:
def get_response(req_type, url, data=None):
try:
with urlopen(Request(url, data=data), timeout=10) as f:
if req_type is HttpRequestType.STREAM or req_type is HttpRequestType.STREAM_CURRENT:
if req_type in HttpAPI.STREAM_REQUESTS:
return {"m3u": f.read().decode("utf-8")}
elif req_type is HttpRequestType.GRUB:
elif req_type is HttpAPI.Request.GRUB:
return {"img_data": f.read()}
elif req_type is HttpRequestType.CURRENT:
elif req_type is HttpAPI.Request.CURRENT:
for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2event"):
return {el.tag: el.text for el in el.iter()} # return first[current] event from the list
elif req_type is HttpRequestType.PLAYER_LIST:
elif req_type is HttpAPI.Request.PLAYER_LIST:
return [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2file")]
elif req_type is HttpAPI.Request.EPG:
return {"event_list": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2event")]}
elif req_type is HttpAPI.Request.TIMER_LIST:
return {"timer_list": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2timer")]}
elif req_type is HttpAPI.Request.REC_DIRS:
return {"rec_dirs": [el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2location")]}
elif req_type is HttpAPI.Request.RECORDINGS:
return {"recordings": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2movie")]}
else:
return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()}
except HTTPError as e:
if req_type is HttpRequestType.TEST:
if req_type is HttpAPI.Request.TEST:
raise e
return {"error_code": e.code}
except (URLError, RemoteDisconnected, ConnectionResetError) as e:
if req_type is HttpRequestType.TEST:
if req_type is HttpAPI.Request.TEST:
raise e
except ETree.ParseError as e:
log("Parsing response error: {}".format(e))
@@ -438,11 +699,11 @@ def init_auth(user, password, url, use_ssl=False):
def get_session_id(user, password, url):
data = urllib.parse.urlencode(dict(user=user, password=password)).encode("utf-8")
return get_response(HttpRequestType.TOKEN, url, data=data).get("e2sessionid", "0")
return get_response(HttpAPI.Request.TOKEN, url, data=data).get("e2sessionid", "0")
def get_post_data(base_url, password, user):
s_id = get_session_id(user, password, "{}/web/{}".format(base_url, HttpRequestType.TOKEN.value))
s_id = get_session_id(user, password, "{}/web/{}".format(base_url, HttpAPI.Request.TOKEN.value))
data = None
if s_id != "0":
data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
@@ -468,7 +729,7 @@ def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message
data = get_post_data(base_url, password, user)
try:
return get_response(HttpRequestType.TEST, "{}/web/{}".format(base_url, params), data).get("e2statetext", "")
return get_response(HttpAPI.Request.TEST, "{}/web/{}".format(base_url, params), data).get("e2statetext", "")
except (RemoteDisconnected, URLError, HTTPError) as e:
raise TestException(e)

View File

@@ -1,8 +1,35 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
from app.commons import run_task
from app.settings import SettingsType
from .ecommons import Service, Satellite, Transponder, Bouquet, Bouquets, is_transponder_valid
from .enigma.blacklist import get_blacklist, write_blacklist
from .enigma.bouquets import get_bouquets as get_enigma_bouquets, write_bouquets as write_enigma_bouquets, to_bouquet_id
from .enigma.bouquets import to_bouquet_id, BouquetsWriter, BouquetsReader
from .enigma.lamedb import get_services as get_enigma_services, write_services as write_enigma_services
from .iptv import parse_m3u
from .neutrino.bouquets import get_bouquets as get_neutrino_bouquets, write_bouquets as write_neutrino_bouquets
@@ -27,15 +54,24 @@ def write_services(path, channels, s_type, format_version):
def get_bouquets(path, s_type):
if s_type is SettingsType.ENIGMA_2:
return get_enigma_bouquets(path)
return BouquetsReader(path).get()
elif s_type is SettingsType.NEUTRINO_MP:
return get_neutrino_bouquets(path)
def write_bouquet(path, bq, s_type):
if s_type is SettingsType.ENIGMA_2:
writer = BouquetsWriter(path, None)
writer.write_bouquet(path + "userbouquet.{}.{}".format(bq.name, bq.type), bq.name, bq.services)
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:
write_enigma_bouquets(path, bouquets, force_bq_names)
BouquetsWriter(path, bouquets, force_bq_names).write()
elif s_type is SettingsType.NEUTRINO_MP:
write_neutrino_bouquets(path, bouquets)

View File

@@ -14,9 +14,11 @@ class BqServiceType(Enum):
IPTV = "IPTV"
MARKER = "MARKER" # 64
SPACE = "SPACE" # 832 [hidden marker]
ALT = "ALT" # Service with alternatives
Bouquet = namedtuple("Bouquet", ["name", "type", "services", "locked", "hidden"])
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"])
@@ -33,6 +35,7 @@ class TrType(Enum):
Satellite = "s"
Terrestrial = "t"
Cable = "c"
ATSC = "a"
class BqType(Enum):
@@ -145,6 +148,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"}

View File

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

View File

@@ -1,76 +1,229 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" Module for working with Enigma2 bouquets. """
import re
from collections import Counter
from pathlib import Path
from app.commons import log
from app.eparser.ecommons import BqServiceType, BouquetService, Bouquets, Bouquet, BqType
_TV_ROOT_FILE_NAME = "bouquets.tv"
_RADIO_ROOT_FILE_NAME = "bouquets.radio"
_TV_FILE = "bouquets.tv"
_RADIO_FILE = "bouquets.radio"
_DEFAULT_BOUQUET_NAME = "favourites"
def get_bouquets(path):
return parse_bouquets(path, "bouquets.tv", BqType.TV.value), parse_bouquets(path, "bouquets.radio",
BqType.RADIO.value)
def write_bouquets(path, bouquets, force_bq_names=False):
""" Creating and writing bouquets files.
class BouquetsWriter:
""" Class for creating and writing bouquet files..
If "force_bq_names" then naming the files using the name of the bouquet.
Some images may have problems displaying the favorites list!
"""
srv_line = '#SERVICE 1:7:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
line = []
pattern = re.compile("[^\\w_()]+")
m_index = [0]
s_index = [0]
_SERVICE = '#SERVICE 1:7:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
_MARKER = "#SERVICE 1:64:{:X}:0:0:0:0:0:0:0::{}\n"
_SPACE = "#SERVICE 1:832:D:{}:0:0:0:0:0:0:\n"
_ALT = '#SERVICE 1:134:1:0:0:0:0:0:0:0:FROM BOUQUET "{}" ORDER BY bouquet\n'
_ALT_PAT = r"[<>:\"/\\|?*\-\s]"
for bqs in bouquets:
line.clear()
line.append("#NAME {}\n".format(bqs.name))
def __init__(self, path, bouquets, force_bq_names=False):
self._path = path
self._bouquets = bouquets
self._force_bq_names = force_bq_names
self._marker_index = 1
self._space_index = 0
self._alt_names = set()
for index, bq in enumerate(bqs.bouquets):
bq_name = bq.name
if bq_name == "Favourites (TV)" or bq_name == "Favourites (Radio)":
bq_name = _DEFAULT_BOUQUET_NAME
def write(self):
line = []
pattern = re.compile("[^\\w_()]+")
for bqs in self._bouquets:
line.clear()
line.append("#NAME {}\n".format(bqs.name))
bq_file_names = {b.file for b in bqs.bouquets}
count = 1
for bq in bqs.bouquets:
bq_name = bq.file
if not bq_name:
if self._force_bq_names:
bq_name = re.sub(pattern, "_", bq.name)
else:
bq_name = "de{0:02d}".format(count)
while bq_name in bq_file_names:
count += 1
bq_name = "de{0:02d}".format(count)
bq_file_names.add(bq_name)
line.append(self._SERVICE.format(2 if bq.type == BqType.RADIO.value else 1, bq_name, bq.type))
self.write_bouquet(self._path + "userbouquet.{}.{}".format(bq_name, bq.type), bq.name, bq.services)
with open(self._path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file:
file.writelines(line)
def write_bouquet(self, path, name, services):
""" Writes single bouquet file. """
bouquet = ["#NAME {}\n".format(name)]
for srv in services:
s_type = srv.service_type
if s_type == BqServiceType.IPTV.name:
bouquet.append("#SERVICE {}\n".format(srv.fav_id.strip()))
elif s_type == BqServiceType.MARKER.name:
m_data = srv.fav_id.strip().split(":")
m_data[2] = self._marker_index
self._marker_index += 1
bouquet.append(self._MARKER.format(m_data[2], m_data[-1]))
elif s_type == BqServiceType.SPACE.name:
bouquet.append(self._SPACE.format(self._space_index))
self._space_index += 1
elif s_type == BqServiceType.ALT.name:
services = srv.transponder
if services:
p = Path(path)
alt_name = srv.data_id
f_name = "alternatives.{}{}".format(alt_name, p.suffix)
if self._force_bq_names:
alt_name = re.sub(self._ALT_PAT, "_", srv.service).lower()
f_name = "alternatives.{}{}".format(alt_name, p.suffix)
alt_path = "{}/{}".format(p.parent, f_name)
bouquet.append(self._ALT.format(f_name))
self.write_bouquet(alt_path, srv.service, services)
else:
bq_name = re.sub(pattern, "_", bq.name) if force_bq_names else "de{0:02d}".format(index)
line.append(srv_line.format(2 if bq.type == BqType.RADIO.value else 1, bq_name, bq.type))
write_bouquet(path + "userbouquet.{}.{}".format(bq_name, bq.type), bq.name, bq.services, m_index, s_index)
data = to_bouquet_id(srv)
if srv.service:
bouquet.append("#SERVICE {}:{}\n#DESCRIPTION {}\n".format(data, srv.service, srv.service))
else:
bouquet.append("#SERVICE {}\n".format(data))
with open(path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file:
file.writelines(line)
with open(path, "w", encoding="utf-8") as file:
file.writelines(bouquet)
def write_bouquet(path, name, services, current_marker, current_space):
bouquet = ["#NAME {}\n".format(name)]
marker = "#SERVICE 1:64:{:X}:0:0:0:0:0:0:0::{}\n"
space = "#SERVICE 1:832:D:{}:0:0:0:0:0:0:\n"
class BouquetsReader:
""" Class for reading and parsing bouquets. """
_ALT_PAT = re.compile(".*alternatives\\.+(.*)\\.([tv|radio]+).*")
_BQ_PAT = re.compile(".*userbouquet\\.+(.*)\\.+[tv|radio].*")
_STREAM_TYPES = {"4097", "5001", "5002", "8193", "8739"}
for srv in services:
s_type = srv.service_type
__slots__ = ["_path"]
if s_type == BqServiceType.IPTV.name:
bouquet.append("#SERVICE {}\n".format(srv.fav_id.strip()))
elif s_type == BqServiceType.MARKER.name:
m_data = srv.fav_id.strip().split(":")
m_data[2] = current_marker[0]
current_marker[0] += 1
bouquet.append(marker.format(m_data[2], m_data[-1]))
elif s_type == BqServiceType.SPACE.name:
bouquet.append(space.format(current_space[0]))
current_space[0] += 1
else:
data = to_bouquet_id(srv)
if srv.service:
bouquet.append("#SERVICE {}:{}\n#DESCRIPTION {}\n".format(data, srv.service, srv.service))
else:
bouquet.append("#SERVICE {}\n".format(data))
def __init__(self, path):
self._path = path
with open(path, "w", encoding="utf-8") as file:
file.writelines(bouquet)
def get(self):
""" Returns a tuple of TV and Radio bouquets. """
return self.parse_bouquets(_TV_FILE, BqType.TV.value), self.parse_bouquets(_RADIO_FILE, BqType.RADIO.value)
def parse_bouquets(self, bq_name, bq_type):
with open(self._path + bq_name, encoding="utf-8", errors="replace") as file:
lines = file.readlines()
bouquets = None
nm_sep = "#NAME"
b_names = set()
real_b_names = Counter()
for line in lines:
if nm_sep in line:
_, _, name = line.partition(nm_sep)
bouquets = Bouquets(name.strip(), bq_type, [])
if bouquets and "#SERVICE" in line:
name = re.match(self._BQ_PAT, line)
if name:
b_name = name.group(1)
if b_name in b_names:
log("The list of bouquets contains duplicate [{}] names!".format(b_name))
else:
b_names.add(b_name)
rb_name, services = self.get_bouquet(self._path, b_name, bq_type)
if rb_name in real_b_names:
log("Bouquet file 'userbouquet.{}.{}' has duplicate name: {}".format(b_name, bq_type,
rb_name))
real_b_names[rb_name] += 1
rb_name = "{} {}".format(rb_name, real_b_names[rb_name])
else:
real_b_names[rb_name] = 0
bouquets[2].append(Bouquet(rb_name, bq_type, services, None, None, b_name))
else:
raise ValueError("No bouquet name found for: {}".format(line))
return bouquets
@staticmethod
def get_bouquet(path, bq_name, bq_type, prefix="userbouquet"):
""" Parsing services ids from bouquet file. """
with open(path + "{}.{}.{}".format(prefix, bq_name, bq_type), encoding="utf-8", errors="replace") as file:
chs_list = file.read()
services = []
srvs = list(filter(None, chs_list.split("\n#SERVICE"))) # filtering ['']
# May come across empty[wrong] files!
if not srvs:
log("Bouquet file 'userbouquet.{}.{}' is empty or wrong!".format(bq_name, bq_type))
return "{} [empty]".format(bq_name), services
bq_name = srvs.pop(0)
for num, srv in enumerate(srvs, start=1):
srv_data = srv.strip().split(":")
data_len = len(srv_data)
if data_len < 10:
log("The bouquet [{}] service [{}] has the wrong data format: [{}]".format(bq_name, num, srv))
continue
s_type = srv_data[1]
if s_type == "64":
m_data, sep, desc = srv.partition("#DESCRIPTION")
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.MARKER, srv, num))
elif s_type == "832":
m_data, sep, desc = srv.partition("#DESCRIPTION")
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.SPACE, srv, num))
elif s_type == "134":
alt = re.match(BouquetsReader._ALT_PAT, srv)
if alt:
alt_name, alt_type = alt.group(1), alt.group(2)
alt_bq_name, alt_srvs = BouquetsReader.get_bouquet(path, alt_name, alt_type, "alternatives")
services.append(BouquetService(alt_bq_name, BqServiceType.ALT, alt_name, tuple(alt_srvs)))
elif srv_data[0].strip() in BouquetsReader._STREAM_TYPES or srv_data[10].startswith(("http", "rtsp")):
stream_data, sep, desc = srv.partition("#DESCRIPTION")
desc = desc.lstrip(":").strip() if desc else srv_data[-1].strip()
services.append(BouquetService(desc, BqServiceType.IPTV, srv, num))
else:
fav_id = "{}:{}:{}:{}".format(srv_data[3], srv_data[4], srv_data[5], srv_data[6])
name = None
if data_len == 12:
name, sep, desc = str(srv_data[-1]).partition("\n#DESCRIPTION")
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id.upper(), num))
return bq_name.lstrip("#NAME").strip(), services
def to_bouquet_id(srv):
@@ -82,81 +235,5 @@ def to_bouquet_id(srv):
return "{}:0:{:X}:{}:0:0:0:".format(1, data_type, srv.fav_id)
def get_bouquet(path, bq_name, bq_type):
""" Parsing services ids from bouquet file. """
with open(path + "userbouquet.{}.{}".format(bq_name, bq_type), encoding="utf-8", errors="replace") as file:
chs_list = file.read()
services = []
srvs = list(filter(None, chs_list.split("\n#SERVICE"))) # filtering ['']
# May come across empty[wrong] files!
if not srvs:
log("Bouquet file 'userbouquet.{}.{}' is empty or wrong!".format(bq_name, bq_type))
return "{} [empty]".format(bq_name), services
bq_name = srvs.pop(0)
for num, srv in enumerate(srvs, start=1):
srv_data = srv.strip().split(":")
if srv_data[1] == "64":
m_data, sep, desc = srv.partition("#DESCRIPTION")
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.MARKER, srv, num))
elif srv_data[1] == "832":
m_data, sep, desc = srv.partition("#DESCRIPTION")
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.SPACE, srv, num))
elif "http" in srv or srv_data[0] == "8193":
stream_data, sep, desc = srv.partition("#DESCRIPTION")
desc = desc.lstrip(":").strip() if desc else srv_data[-1].strip()
services.append(BouquetService(desc, BqServiceType.IPTV, srv, num))
else:
fav_id = "{}:{}:{}:{}".format(srv_data[3], srv_data[4], srv_data[5], srv_data[6])
name = None
if len(srv_data) == 12:
name, sep, desc = str(srv_data[-1]).partition("\n#DESCRIPTION")
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id.upper(), num))
return bq_name.lstrip("#NAME").strip(), services
def parse_bouquets(path, bq_name, bq_type):
with open(path + bq_name, encoding="utf-8", errors="replace") as file:
lines = file.readlines()
bouquets = None
nm_sep = "#NAME"
bq_pattern = re.compile(".*userbouquet\\.+(.*)\\.+[tv|radio].*")
b_names = set()
real_b_names = Counter()
for line in lines:
if nm_sep in line:
_, _, name = line.partition(nm_sep)
bouquets = Bouquets(name.strip(), bq_type, [])
if bouquets and "#SERVICE" in line:
name = re.match(bq_pattern, line)
if name:
b_name = name.group(1)
if b_name in b_names:
log("The list of bouquets contains duplicate [{}] names!".format(b_name))
else:
b_names.add(b_name)
rb_name, services = get_bouquet(path, b_name, bq_type)
if rb_name in real_b_names:
log("Bouquet file 'userbouquet.{}.{}' has duplicate name: {}".format(b_name, bq_type, rb_name))
real_b_names[rb_name] += 1
rb_name = "{} {}".format(rb_name, real_b_names[rb_name])
else:
real_b_names[rb_name] = 0
bouquets[2].append(Bouquet(name=rb_name,
type=bq_type,
services=services,
locked=None,
hidden=None))
else:
raise ValueError("No bouquet name found for: {}".format(line))
return bouquets
if __name__ == "__main__":
pass

View File

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

View File

@@ -3,9 +3,10 @@ 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 = "{}::{}::{}::{}::{}::{}::{}::{}::{}::{}"
@@ -19,33 +20,74 @@ class StreamType(Enum):
NONE_REC_1 = "5001"
NONE_REC_2 = "5002"
E_SERVICE_URI = "8193"
E_SERVICE_HLS = "8739"
def parse_m3u(path, s_type):
with open(path) as file:
def parse_m3u(path, s_type, detect_encoding=True, params=None):
with open(path, "rb") as file:
data = file.read()
encoding = "utf-8"
if detect_encoding:
try:
import chardet
except ModuleNotFoundError:
pass
else:
enc = chardet.detect(data)
encoding = enc.get("encoding", "utf-8")
aggr = [None] * 10
s_aggr = aggr[: -3]
services = []
groups = set()
counter = 0
marker_counter = 1
sid_counter = 1
name = None
picon = None
p_id = "1_0_1_0_0_0_0_0_0_0.png"
st = BqServiceType.IPTV.name
params = params or [0, 0, 0, 0]
for line in file.readlines():
for line in str(data, encoding=encoding, errors="ignore").splitlines():
if line.startswith("#EXTINF"):
name = line[1 + line.index(","):].strip()
inf, sep, line = line.partition(" ")
if not line:
line = inf
line, sep, name = line.rpartition(",")
data = re.split('"', line)
size = len(data)
if size < 3:
continue
d = {data[i].lower().strip(" ="): data[i + 1] for i in range(0, len(data) - 1, 2)}
picon = d.get("tvg-logo", None)
grp_name = d.get("group-title", None)
if grp_name not in groups:
groups.add(grp_name)
fav_id = MARKER_FORMAT.format(marker_counter, grp_name, grp_name)
marker_counter += 1
mr = Service(None, None, None, grp_name, *aggr[0:3], BqServiceType.MARKER.name, *aggr, fav_id, None)
services.append(mr)
elif line.startswith("#EXTGRP") and s_type is SettingsType.ENIGMA_2:
grp_name = line.strip("#EXTGRP:").strip()
if grp_name not in groups:
groups.add(grp_name)
fav_id = MARKER_FORMAT.format(counter, grp_name, grp_name)
counter += 1
fav_id = MARKER_FORMAT.format(marker_counter, grp_name, grp_name)
marker_counter += 1
mr = Service(None, None, None, grp_name, *aggr[0:3], BqServiceType.MARKER.name, *aggr, fav_id, None)
services.append(mr)
elif not line.startswith("#"):
url = line.strip()
fav_id = get_fav_id(url, name, s_type)
if name and url:
srv = Service(None, None, IPTV_ICON, name, *aggr[0:3], BqServiceType.IPTV.name, *aggr, fav_id, None)
params[0] = sid_counter
sid_counter += 1
fav_id = get_fav_id(url, name, s_type, params)
if all((name, url, fav_id)):
srv = Service(None, None, IPTV_ICON, name, *aggr[0:3], st, picon, p_id, *s_aggr, url, fav_id, None)
services.append(srv)
else:
log("*.m3u* parse error ['{}']: name[{}], url[{}], fav id[{}]".format(path, name, url, fav_id))
return services
@@ -73,12 +115,13 @@ def export_to_m3u(path, bouquet, s_type):
file.writelines(lines)
def get_fav_id(url, service_name, s_type):
def get_fav_id(url, service_name, settings_type, params=None, stream_type=None, s_type=1):
""" Returns fav id depending on the profile. """
if s_type is SettingsType.ENIGMA_2:
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:
stream_type = stream_type or StreamType.NONE_TS.value
params = params or (0, 0, 0, 0)
return ENIGMA2_FAV_ID_FORMAT.format(stream_type, s_type, *params, quote(url), service_name, service_name, None)
elif settings_type is SettingsType.NEUTRINO_MP:
return NEUTRINO_FAV_ID_FORMAT.format(url, "", 0, None, None, None, None, "", "", 1)

View File

@@ -89,11 +89,8 @@ def parse_webtv(path, name, bq_type):
group = group.value if group else group
fav_id = NEUTRINO_FAV_ID_FORMAT.format(url, description, urlkey, account, usrname, psw, s_type, iconsrc,
iconsrc_b, group)
srv = BouquetService(name=title,
type=BqServiceType.IPTV,
data=fav_id,
num=0)
services.append(srv)
services.append(BouquetService(name=title, type=BqServiceType.IPTV, data=fav_id, num=0))
bouquet = Bouquet(name="default", type=bq_type, services=services, locked=None, hidden=None)
bouquets[2].append(bouquet)
@@ -125,14 +122,15 @@ def write_bouquet(file, bouquet):
root.appendChild(bq_elem)
for srv in bq.services:
f_data = srv.flags_cas.split(":")
tr_id, on, ssid = srv.fav_id.split(":")
srv_elem = doc.createElement("S")
srv_elem.setAttribute("i", ssid)
srv_elem.setAttribute("n", srv.service)
srv_elem.setAttribute("t", tr_id)
srv_elem.setAttribute("on", on)
srv_elem.setAttribute("s", srv.pos.replace(".", ""))
srv_elem.setAttribute("frq", srv.freq[:-3])
srv_elem.setAttribute("s", f_data[1])
srv_elem.setAttribute("frq", srv.freq)
srv_elem.setAttribute("l", "0") # temporary !!!
bq_elem.appendChild(srv_elem)

View File

@@ -1,5 +1,6 @@
from xml.dom.minidom import parse, Document
from app.commons import log
from ..ecommons import Service, POLARIZATION, FEC, SYSTEM, SERVICE_TYPE, PROVIDER
_FILE = "services.xml"
@@ -28,7 +29,7 @@ def write_services(path, services):
tr_atr = sat.split(":")
sat_elem = doc.createElement("sat")
sat_elem.setAttribute("name", tr_atr[0])
sat_elem.setAttribute("position", tr_atr[1].replace(".", ""))
sat_elem.setAttribute("position", tr_atr[1])
sat_elem.setAttribute("diseqc", tr_atr[2])
sat_elem.setAttribute("uncommited", tr_atr[3])
root.appendChild(sat_elem)
@@ -88,7 +89,6 @@ def parse_services(path):
if elem.hasAttributes():
sat_name = elem.attributes["name"].value
sat_pos = elem.attributes["position"].value
sat_pos = "{}.{}".format(sat_pos[:-1], sat_pos[-1:])
diseqc = elem.attributes.get("diseqc")
diseqc = diseqc.value if diseqc else diseqc
uncommited = elem.attributes.get("uncommited")
@@ -117,6 +117,15 @@ def parse_transponder(api, sat, sat_pos, services, tr_elem):
tr = "{}:{}:{}:{}:{}:{}:{}:{}:{}".format(tr_id, on, freq, inv, rate, fec, pol, mod, sys)
tr_id = tr_id.lstrip("0")
pol = POLARIZATION.get(pol)
# Formatting displayed values.
try:
freq = "{}".format(int(freq) // 1000)
rate = "{}".format(int(rate) // 1000)
sat_pos = int(sat_pos)
sat_pos = "{:0.1f}{}".format(abs(sat_pos / 10), "W" if sat_pos < 0 else "E")
except ValueError as e:
log("Neutrino parsing error [parse_transponder]: {}".format(e))
for srv_elem in tr_elem.getElementsByTagName("S"):
if srv_elem.hasAttributes():
@@ -141,27 +150,10 @@ def parse_transponder(api, sat, sat_pos, services, tr_elem):
data_id = "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}".format(api, srv_type, sys, num, f, v, a, p, pmt, tx, vt)
fav_id = "{}:{}:{}".format(tr_id, on.lstrip("0"), ssid.lstrip("0"))
picon_id = "{}{}{}.png".format(tr_id, on, ssid)
prv, st, = PROVIDER.get(int(on, 16)), SERVICE_TYPE.get(str(int(srv_type, 16)), SERVICE_TYPE.get("-2"))
srv = Service(flags_cas=sat,
transponder_type=None,
coded=None,
service=name,
locked=None,
hide=None,
package=PROVIDER.get(int(on, 16)),
service_type=SERVICE_TYPE.get(str(int(srv_type, 16))),
picon=None,
picon_id=picon_id,
ssid=ssid,
freq=freq,
rate=rate,
pol=POLARIZATION.get(pol),
fec=FEC.get(fec),
system=SYSTEM.get(sys),
pos=sat_pos,
data_id=data_id,
fav_id=fav_id,
transponder=tr)
srv = Service(sat, None, None, name, None, None, prv, st, None, picon_id, ssid, freq, rate, pol,
FEC.get(fec), SYSTEM.get(sys), sat_pos, data_id, fav_id, tr)
services.append(srv)

View File

@@ -53,9 +53,9 @@ def write_satellites(satellites, data_path):
transponder_child.setAttribute("frequency", tr.frequency)
transponder_child.setAttribute("symbol_rate", tr.symbol_rate)
transponder_child.setAttribute("polarization", get_key_by_value(POLARIZATION, tr.polarization))
transponder_child.setAttribute("fec_inner", get_key_by_value(FEC, tr.fec_inner))
transponder_child.setAttribute("system", get_key_by_value(SYSTEM, tr.system))
transponder_child.setAttribute("modulation", get_key_by_value(MODULATION, tr.modulation))
transponder_child.setAttribute("fec_inner", get_key_by_value(FEC, tr.fec_inner) or "0")
transponder_child.setAttribute("system", get_key_by_value(SYSTEM, tr.system) or "0")
transponder_child.setAttribute("modulation", get_key_by_value(MODULATION, tr.modulation) or "0")
if tr.pls_mode:
transponder_child.setAttribute("pls_mode", tr.pls_mode)
if tr.pls_code:
@@ -90,7 +90,6 @@ def parse_transponders(elem, sat_name):
atr["is_id"].value if "is_id" in atr else None)
except Exception as e:
message = "Error: can't parse transponder for '{}' satellite! {}".format(sat_name, repr(e))
print(message)
log(message)
else:
transponders.append(tr)

View File

@@ -30,8 +30,11 @@ class Defaults(Enum):
USE_COLORS = True
NEW_COLOR = "rgb(255,230,204)"
EXTRA_COLOR = "rgb(179,230,204)"
TOOLTIP_LOGO_SIZE = 96
LIST_PICON_SIZE = 32
FAV_CLICK_MODE = 0
PLAY_STREAMS_MODE = 1 if IS_DARWIN else 0
STREAM_LIB = "vlc"
PROFILE_FOLDER_DEFAULT = False
RECORDS_PATH = DATA_PATH + "records/"
ACTIVATE_TRANSCODING = False
@@ -99,10 +102,10 @@ class SettingsType(IntEnum):
""" Returns default settings for current type """
if self is self.ENIGMA_2:
return {"setting_type": self.value,
"host": "127.0.0.1", "port": "21", "user": "root", "password": "root", "timeout": 5,
"http_user": "root", "http_password": "", "http_port": "80",
"http_timeout": 5, "http_use_ssl": False,
"telnet_user": "root", "telnet_password": "", "telnet_port": "23", "telnet_timeout": 5,
"host": "127.0.0.1", "port": "21", "timeout": 5,
"user": "root", "password": "root",
"http_port": "80", "http_timeout": 5, "http_use_ssl": False,
"telnet_port": "23", "telnet_timeout": 5,
"services_path": "/etc/enigma2/", "user_bouquet_path": "/etc/enigma2/",
"satellites_xml_path": "/etc/tuxbox/", "data_local_path": DATA_PATH + "enigma2/",
"picons_path": "/usr/share/enigma2/picon/",
@@ -110,9 +113,10 @@ class SettingsType(IntEnum):
"backup_local_path": DATA_PATH + "enigma2/backup/"}
elif self is self.NEUTRINO_MP:
return {"setting_type": self,
"host": "127.0.0.1", "port": "21", "user": "root", "password": "root", "timeout": 5,
"http_user": "", "http_password": "", "http_port": "80", "http_timeout": 2, "http_use_ssl": False,
"telnet_user": "root", "telnet_password": "", "telnet_port": "23", "telnet_timeout": 1,
"host": "127.0.0.1", "port": "21", "timeout": 5,
"user": "root", "password": "root",
"http_port": "80", "http_timeout": 2, "http_use_ssl": False,
"telnet_port": "23", "telnet_timeout": 1,
"services_path": "/var/tuxbox/config/zapit/", "user_bouquet_path": "/var/tuxbox/config/zapit/",
"satellites_xml_path": "/var/tuxbox/config/", "data_local_path": DATA_PATH + "neutrino/",
"picons_path": "/usr/share/tuxbox/neutrino/icons/logo/",
@@ -131,7 +135,7 @@ class SettingsReadException(SettingsException):
class PlayStreamsMode(IntEnum):
""" Behavior mode when opening streams. """
BUILT_IN = 0
VLC = 1
WINDOW = 1
M3U = 2
@@ -279,22 +283,6 @@ class Settings:
def password(self, value):
self._cp_settings["password"] = value
@property
def http_user(self):
return self._cp_settings.get("http_user", self.get_default("http_user"))
@http_user.setter
def http_user(self, value):
self._cp_settings["http_user"] = value
@property
def http_password(self):
return self._cp_settings.get("http_password", self.get_default("http_password"))
@http_password.setter
def http_password(self, value):
self._cp_settings["http_password"] = value
@property
def http_port(self):
return self._cp_settings.get("http_port", self.get_default("http_port"))
@@ -319,22 +307,6 @@ class Settings:
def http_use_ssl(self, value):
self._cp_settings["http_use_ssl"] = value
@property
def telnet_user(self):
return self._cp_settings.get("telnet_user", self.get_default("telnet_user"))
@telnet_user.setter
def telnet_user(self, value):
self._cp_settings["telnet_user"] = value
@property
def telnet_password(self):
return self._cp_settings.get("telnet_password", self.get_default("telnet_password"))
@telnet_password.setter
def telnet_password(self, value):
self._cp_settings["telnet_password"] = value
@property
def telnet_port(self):
return self._cp_settings.get("telnet_port", self.get_default("telnet_port"))
@@ -467,6 +439,14 @@ 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
# *********** EPG ************ #
@property
@@ -544,30 +524,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)
@@ -612,6 +568,54 @@ 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):
return self._settings.get("dark_mode", False)
@@ -620,6 +624,22 @@ class Settings:
def dark_mode(self, value):
self._settings["dark_mode"] = value
@property
def alternate_layout(self):
return self._settings.get("alternate_layout", IS_DARWIN)
@alternate_layout.setter
def alternate_layout(self, value):
self._settings["alternate_layout"] = value
@property
def bq_details_first(self):
return self._settings.get("bq_details_first", False)
@bq_details_first.setter
def bq_details_first(self, value):
self._settings["bq_details_first"] = value
@property
def is_themes_support(self):
return self._settings.get("is_themes_support", False)

View File

@@ -1,59 +1,412 @@
# -*- 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 subprocess
import sys
from abc import ABC, abstractmethod
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
from app.commons import run_task, log, _DATE_FORMAT, run_with_delay
class Player:
class Player(ABC):
""" Base player class. Also used as a factory. """
@abstractmethod
def get_play_mode(self):
pass
@abstractmethod
def play(self, mrl=None):
pass
@abstractmethod
def stop(self):
pass
@abstractmethod
def pause(self):
pass
@abstractmethod
def set_time(self, time):
pass
@abstractmethod
def release(self):
pass
@abstractmethod
def is_playing(self):
pass
@abstractmethod
def get_instance(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
pass
def get_window_handle(self, widget):
""" Returns the identifier [pointer] for the window.
Based on gtkvlc.py[get_window_pointer] example from here:
https://github.com/oaubert/python-vlc/tree/master/examples
"""
if sys.platform == "linux":
return widget.get_window().get_xid()
else:
is_darwin = sys.platform == "darwin"
try:
import ctypes
libgdk = ctypes.CDLL("libgdk-3.0.dylib" if is_darwin else "libgdk-3-0.dll")
except OSError as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
else:
# https://gitlab.gnome.org/GNOME/pygobject/-/issues/112
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object]
gpointer = ctypes.pythonapi.PyCapsule_GetPointer(widget.get_window().__gpointer__, None)
get_pointer = libgdk.gdk_quartz_window_get_nsview if is_darwin else libgdk.gdk_win32_window_get_handle
get_pointer.restype = ctypes.c_void_p
get_pointer.argtypes = [ctypes.c_void_p]
return get_pointer(gpointer)
def get_video_widget(self, widget):
area = Gtk.DrawingArea(visible=True)
area.connect("draw", self.on_drawing_area_draw)
area.connect("motion-notify-event", self.on_mouse_motion)
area.set_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)
widget.add(area)
return area
def on_drawing_area_draw(self, widget, cr):
""" Used for black background drawing in the player drawing area. """
cr.set_source_rgb(0, 0, 0)
cr.paint()
def on_mouse_motion(self, widget, event):
display = widget.get_display()
window = widget.get_window()
cursor = Gdk.Cursor.new_from_name(display, "default")
window.set_cursor(cursor)
self.hide_mouse_cursor(window, display)
@run_with_delay(3)
def hide_mouse_cursor(self, window, display):
cursor = Gdk.Cursor.new_for_display(display, Gdk.CursorType.BLANK_CURSOR)
window.set_cursor(cursor)
@staticmethod
def make(name, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None):
""" Factory method. We will not use a separate factory to return a specific implementation.
@param name: implementation name.
@param mode: current player mode [Built-in or windowed].
@param widget: parent of video widget.
@param buf_cb: buffering callback.
@param position_cb: time (position) callback.
@param error_cb: error callback.
@param playing_cb: playing state callback.
Throws a NameError if there is no implementation for the given name.
"""
if name == "mpv":
return MpvPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
elif name == "gst":
return GstPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
elif name == "vlc":
return VlcPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
else:
raise NameError("There is no such [{}] implementation.".format(name))
class MpvPlayer(Player):
""" Simple wrapper for MPV media player.
Uses python-mvp [https://github.com/jaseg/python-mpv].
"""
__INSTANCE = None
def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
try:
from app.tools import mpv
self._player = mpv.MPV(wid=str(self.get_window_handle(self.get_video_widget(widget), )),
input_default_bindings=False,
input_cursor=False,
cursor_autohide="no")
except OSError as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
raise ImportError("No libmpv is found. Check that it is installed!")
else:
self._mode = mode
self._is_playing = False
@self._player.event_callback(mpv.MpvEventID.FILE_LOADED)
def on_open(event):
log("Starting playback...")
playing_cb()
@self._player.event_callback(mpv.MpvEventID.END_FILE)
def on_end(event):
event = event.get("event", {})
if event.get("reason", mpv.MpvEventEndFile.ERROR) == mpv.MpvEventEndFile.ERROR:
log("Stream playback error: {}".format(event.get("error", mpv.ErrorCode.GENERIC)))
error_cb()
@classmethod
def get_instance(cls, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
if not cls.__INSTANCE:
cls.__INSTANCE = MpvPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
return cls.__INSTANCE
def get_play_mode(self):
return self._mode
@run_task
def play(self, mrl=None):
if not mrl:
return
self._player.play(mrl)
self._is_playing = True
@run_task
def stop(self):
self._player.stop()
self._is_playing = True
def pause(self):
pass
def set_time(self, time):
pass
@run_task
def release(self):
self._player.terminate()
self.__INSTANCE = None
def is_playing(self):
return self._is_playing
class GstPlayer(Player):
""" Simple wrapper for GStreamer playbin. """
__INSTANCE = None
def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
try:
import gi
gi.require_version("Gst", "1.0")
gi.require_version("GstVideo", "1.0")
from gi.repository import Gst, GstVideo
# Initialization of GStreamer.
Gst.init(sys.argv)
gtk_sink = Gst.ElementFactory.make("gtksink")
if not gtk_sink:
msg = "GStreamer error: gtksink plugin not installed!"
log(msg)
raise ImportError(msg)
except (OSError, ValueError) as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
raise ImportError("No GStreamer is found. Check that it is installed!")
else:
self._error_cb = error_cb
self._playing_cb = playing_cb
self.STATE = Gst.State
self.STAT_RETURN = Gst.StateChangeReturn
self._mode = mode
self._is_playing = False
self._player = Gst.ElementFactory.make("playbin", "player")
# Initialization of the playback widget.
self._player.set_property("video-sink", gtk_sink)
vid_widget = gtk_sink.get_property("widget")
vid_widget.connect("motion-notify-event", self.on_mouse_motion)
widget.add(vid_widget)
vid_widget.show()
bus = self._player.get_bus()
bus.add_signal_watch()
bus.connect("message::error", self.on_error)
bus.connect("message::state-changed", self.on_state_changed)
bus.connect("message::eos", self.on_eos)
@classmethod
def get_instance(cls, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None):
if not cls.__INSTANCE:
cls.__INSTANCE = GstPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
return cls.__INSTANCE
def get_play_mode(self):
return self._mode
def play(self, mrl=None):
self._player.set_state(self.STATE.READY)
if not mrl:
return
self._player.set_property("uri", mrl)
log("Setting the URL for playback: {}".format(mrl))
ret = self._player.set_state(self.STATE.PLAYING)
if ret == self.STAT_RETURN.FAILURE:
log("ERROR: Unable to set the 'PLAYING' state for '{}'.".format(mrl))
else:
self._is_playing = True
def stop(self):
log("Stop playback...")
self._player.set_state(self.STATE.READY)
self._is_playing = False
def pause(self):
self._player.set_state(self.STATE.PAUSED)
def set_time(self, time):
pass
@run_task
def release(self):
self._is_playing = False
self._player.set_state(self.STATE.NULL)
self.__INSTANCE = None
def set_mrl(self, mrl):
self._player.set_property("uri", mrl)
def is_playing(self):
return self._is_playing
def on_error(self, bus, msg):
err, dbg = msg.parse_error()
log(err)
self._error_cb()
def on_state_changed(self, bus, msg):
if not msg.src == self._player:
# Not from the player.
return
old_state, new_state, pending = msg.parse_state_changed()
if new_state is self.STATE.PLAYING:
log("Starting playback...")
self._playing_cb()
self.get_stream_info()
def on_eos(self, bus, msg):
""" Called when an end-of-stream message appears. """
self._player.set_state(self.STATE.READY)
self._is_playing = False
def get_stream_info(self):
log("Getting stream info...")
nr_video = self._player.get_property("n-video")
for i in range(nr_video):
# Retrieve the stream's video tags.
tags = self._player.emit("get-video-tags", i)
if tags:
_, cod = tags.get_string("video-codec")
log("Video codec: {}".format(cod or "unknown"))
nr_audio = self._player.get_property("n-audio")
for i in range(nr_audio):
# Retrieve the stream's video tags.
tags = self._player.emit("get-audio-tags", i)
if tags:
_, cod = tags.get_string("audio-codec")
log("Audio codec: {}".format(cod or "unknown"))
class VlcPlayer(Player):
""" Simple wrapper for VLC media player.
Uses python-vlc [https://github.com/oaubert/python-vlc].
"""
__VLC_INSTANCE = None
__PLAY_STREAMS_MODE = PlayStreamsMode.BUILT_IN
def __init__(self, rewind_callback, position_callback, error_callback, playing_callback):
def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
try:
from app.tools import vlc
from app.tools.vlc import EventType
except OSError as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
raise ImportError
else:
self._is_playing = False
args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib")
self._player = vlc.Instance(args).media_player_new()
vlc.libvlc_video_set_key_input(self._player, False)
vlc.libvlc_video_set_mouse_input(self._player, False)
except (OSError, AttributeError, NameError) as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
raise ImportError("No VLC is found. Check that it is installed!")
else:
self._mode = mode
self._is_playing = False
ev_mgr = self._player.event_manager()
if rewind_callback:
if buf_cb:
# TODO look other EventType options
ev_mgr.event_attach(EventType.MediaPlayerBuffering,
lambda et, p: rewind_callback(p.get_media().get_duration()),
lambda et, p: buf_cb(p.get_media().get_duration()),
self._player)
if position_callback:
if position_cb:
ev_mgr.event_attach(EventType.MediaPlayerTimeChanged,
lambda et, p: position_callback(p.get_time()),
lambda et, p: position_cb(p.get_time()),
self._player)
if error_callback:
if error_cb:
ev_mgr.event_attach(EventType.MediaPlayerEncounteredError,
lambda et, p: error_callback(),
lambda et, p: error_cb(),
self._player)
if playing_callback:
if playing_cb:
ev_mgr.event_attach(EventType.MediaPlayerPlaying,
lambda et, p: playing_callback(),
lambda et, p: playing_cb(),
self._player)
self.init_video_widget(widget)
@classmethod
def get_instance(cls, rewind_callback=None, position_callback=None, error_callback=None, playing_callback=None):
def get_instance(cls, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None):
if not cls.__VLC_INSTANCE:
cls.__VLC_INSTANCE = Player(rewind_callback, position_callback, error_callback, playing_callback)
cls.__VLC_INSTANCE = VlcPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
return cls.__VLC_INSTANCE
@staticmethod
def get_play_mode():
return Player.__PLAY_STREAMS_MODE
def get_play_mode(self):
return self._mode
@run_task
def play(self, mrl=None):
@@ -80,29 +433,7 @@ class Player:
self._is_playing = False
self._player.stop()
self._player.release()
def set_xwindow(self, xid):
self._player.set_xwindow(xid)
def set_nso(self, widget):
""" Used on MacOS to set NSObject.
Based on gtkvlc.py[get_window_pointer] example from here:
https://github.com/oaubert/python-vlc/tree/master/examples
"""
try:
import ctypes
g_dll = ctypes.CDLL("libgdk-3.0.dylib")
except OSError as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
else:
get_nsview = g_dll.gdk_quartz_window_get_nsview
get_nsview.restype, get_nsview.argtypes = ctypes.c_void_p, [ctypes.c_void_p]
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object]
# Get the C void* pointer to the window
pointer = ctypes.pythonapi.PyCapsule_GetPointer(widget.get_window().__gpointer__, None)
self._player.set_nsobject(get_nsview(pointer))
self.__VLC_INSTANCE = None
def set_mrl(self, mrl):
self._player.set_mrl(mrl)
@@ -110,94 +441,14 @@ class Player:
def is_playing(self):
return self._is_playing
def set_full_screen(self, full):
self._player.set_fullscreen(full)
class HttpPlayer:
""" Simple wrapper for VLC media player to interact over http. """
__VLC_INSTANCE = None
__PLAY_STREAMS_MODE = PlayStreamsMode.VLC
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 __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):
video_widget = self.get_video_widget(widget)
if sys.platform == "linux":
self._player.set_xwindow(video_widget.get_window().get_xid())
elif sys.platform == "darwin":
self._player.set_nsobject(self.get_window_handle(video_widget))
else:
self._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(video_widget))
class Recorder:

1941
app/tools/mpv.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,230 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
import glob
import os
import re
import shutil
import subprocess
from collections import namedtuple
from html.parser import HTMLParser
from app.commons import run_task
import requests
from app.commons import run_task, log
from app.settings import SettingsType
from .satellites import _HEADERS
_ENIGMA2_PICON_KEY = "{:X}:{:X}:{}"
_NEUTRINO_PICON_KEY = "{:x}{:04x}{:04x}.png"
Provider = namedtuple("Provider", ["logo", "name", "pos", "url", "on_id", "ssid", "single", "selected"])
Picon = namedtuple("Picon", ["ref", "ssid", "v_pid"])
Picon = namedtuple("Picon", ["ref", "ssid"])
class PiconsError(Exception):
pass
class PiconsCzDownloader:
""" The main class for loading picons from the https://picon.cz/ source (by Chocholoušek). """
_PERM_URL = "https://picon.cz/download/7337"
_BASE_URL = "https://picon.cz/download/"
_BASE_LOGO_URL = "https://picon.cz/picon/0/"
_HEADER = {"User-Agent": "DemonEditor/1.0.10", "Referer": ""}
_LINK_PATTERN = re.compile(r"((.*)-\d+x\d+)-(.*)_by_chocholousek.7z$")
_FILE_PATTERN = re.compile(b"\\s+(1_.*\\.png).*")
def __init__(self, picon_ids=set(), appender=log):
self._perm_links = {}
self._providers = {}
self._provider_logos = {}
self._picon_ids = picon_ids
self._appender = appender
def init(self):
""" Initializes dict with values: download_id -> perm link and provider data. """
if self._perm_links:
return
self._HEADER["Referer"] = self._PERM_URL
with requests.get(url=self._PERM_URL, headers=self._HEADER, stream=True) as request:
if request.reason == "OK":
logo_map = self.get_logos_map()
name_map = self.get_name_map()
for line in request.iter_lines():
l_id, perm_link = line.decode(encoding="utf-8", errors="ignore").split(maxsplit=1)
self._perm_links[str(l_id)] = str(perm_link)
data = re.match(self._LINK_PATTERN, perm_link)
if data:
sat_pos = data.group(3)
# Logo url.
logo = logo_map.get(data.group(2), None)
l_name = name_map.get(sat_pos, None) or sat_pos.replace(".", "")
logo_url = "{}{}/{}.png".format(self._BASE_LOGO_URL, logo, l_name) if logo else None
prv = Provider(None, data.group(1), sat_pos, self._BASE_URL + l_id, l_id, logo_url, None, False)
if sat_pos in self._providers:
self._providers[sat_pos].append(prv)
else:
self._providers[sat_pos] = [prv]
else:
log("{} [get permalinks] error: {}".format(self.__class__.__name__, request.reason))
raise PiconsError(request.reason)
@property
def providers(self):
return self._providers
def get_sat_providers(self, url):
return self._providers.get(url, [])
def download(self, provider, picons_path, picon_ids=None):
self._HEADER["Referer"] = provider.url
with requests.get(url=provider.url, headers=self._HEADER, stream=True) as request:
if request.reason == "OK":
dest = "{}{}.7z".format(picons_path, provider.on_id)
self._appender("Downloading: {}\n".format(provider.url))
with open(dest, mode="bw") as f:
for data in request.iter_content(chunk_size=1024):
f.write(data)
self._appender("Extracting: {}\n".format(provider.on_id))
self.extract(dest, picons_path, picon_ids)
else:
log("{} [download] error: {}".format(self.__class__.__name__, request.reason))
def extract(self, src, dest, picon_ids=None):
""" Extracts 7z archives. """
# TODO: think about https://github.com/miurahr/py7zr
exe = "7zr"
if not os.path.isfile("/usr/bin/7zr"):
raise PiconsError("7-zip [7zr] archiver not found!")
cmd = [exe, "l", src]
try:
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
if err:
log("{} [extract] error: {}".format(self.__class__.__name__, err))
raise PiconsError(err)
except OSError as e:
log("{} [extract] error: {}".format(self.__class__.__name__, e))
raise PiconsError(e)
is_filter = bool(picon_ids)
ids = picon_ids or self._picon_ids
to_extract = []
for o in re.finditer(self._FILE_PATTERN, out):
p_id = o.group(1).decode("utf-8", errors="ignore")
if p_id in ids:
to_extract.append(p_id)
if is_filter and not to_extract:
if os.path.isfile(src):
os.remove(src)
raise PiconsError("No matching picons found!")
cmd = [exe, "e", src, "-o{}".format(dest), "-y", "-r"]
cmd.extend(to_extract)
try:
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
if err:
log("{} [extract] error: {}".format(self.__class__.__name__, err))
raise PiconsError(err)
else:
if os.path.isfile(src):
os.remove(src)
except OSError as e:
log(e)
raise PiconsError(e)
def get_logo_data(self, url):
""" Returns the logo data if present. """
return self._provider_logos.get(url, None)
def get_provider_logo(self, url):
""" Retrieves package logo. """
# Getting package logo.
logo = self._provider_logos.get(url, None)
if logo:
return logo
try:
with requests.get(url=url, stream=True) as logo_request:
if logo_request.reason == "OK":
data = logo_request.content
self._provider_logos[url] = data
return data
else:
log("Downloading package logo error: {}".format(logo_request.reason))
except requests.exceptions.ConnectionError as e:
log("{} error [get provider logo]: {}".format(self.__class__.__name__, e))
def get_logos_map(self):
return {"piconblack": "b50",
"picontransparent": "t50",
"piconwhite": "w50",
"piconmirrorglass": "mr100",
"piconnoName": "n100",
"piconsrhd": "srhd100",
"piconfreezeframe": "ff220",
"piconfreezewhite": "fw100",
"piconpoolrainbow": "r100",
"piconsimpleblack": "s220",
"piconjustblack": "jb220",
"picondirtypaper": "dp220",
"picongray": "g400",
"piconmonochrom": "m220",
"picontransparentwhite": "tw100",
"picontransparentdark": "td220",
"piconoled": "o96",
"piconblack80": "b50",
"piconblack3d": "b50"
}
def get_name_map(self):
return {"antiksat": "ANTIK",
"digiczsk": "DIGI",
"DTTitaly": "picon_trs-it",
"dvbtCZSK": "picon_trs",
"PolandDTT": "picon_trs-pl",
"freeSAT": "UPC DIRECT",
"orangesat": "ORANGE TV",
"skylink": "M7 GROUP",
}
class PiconsParser(HTMLParser):
""" Parser for package html page. (https://www.lyngsat.com/packages/*provider-name*.html) """
_BASE_URL = "https://www.lyngsat.com"
def __init__(self, entities=False, separator=' ', single=None):
@@ -33,9 +240,9 @@ class PiconsParser(HTMLParser):
self.picons = []
def handle_starttag(self, tag, attrs):
if tag == 'td':
if tag == "td":
self._is_td = True
if tag == 'th':
if tag == "th":
self._is_th = True
if tag == "img":
self._current_row.append(attrs[0][1])
@@ -46,32 +253,29 @@ class PiconsParser(HTMLParser):
self._current_cell.append(data.strip())
def handle_endtag(self, tag):
if tag == 'td':
if tag == "td":
self._is_td = False
elif tag == 'th':
elif tag == "th":
self._is_th = False
if tag in ('td', 'th'):
if tag in ("td", "th"):
final_cell = self._separator.join(self._current_cell).strip()
self._current_row.append(final_cell)
self._current_cell = []
elif tag == 'tr':
elif tag == "tr":
row = self._current_row
ln = len(row)
if self._single and ln == 4 and row[0].startswith("../../logo/"):
self.picons.append(Picon(row[0].strip("../"), "0", "0"))
if self._single and ln == 4 and row[0].startswith("/logo/"):
self.picons.append(Picon(row[0].strip(), "0"))
else:
if 9 < ln < 13:
if ln > 8:
url = None
if row[0].startswith("../logo/"):
url = row[0]
elif row[1].startswith("../logo/"):
url = row[1]
if row[2].startswith("/logo/"):
url = row[2]
ssid = row[-4]
if url and len(ssid) > 2:
self.picons.append(Picon(url, ssid, row[-3]))
if url and row[0].isdigit():
self.picons.append(Picon(url, row[0]))
self._current_row = []
@@ -79,34 +283,47 @@ class PiconsParser(HTMLParser):
pass
@staticmethod
def parse(open_path, picons_path, tmp_path, provider, picon_ids, s_type=SettingsType.ENIGMA_2):
with open(open_path, encoding="utf-8", errors="replace") as f:
on_id, pos, ssid, single = provider.on_id, provider.pos, provider.ssid, provider.single
neg_pos = pos.endswith("W")
pos = int("".join(c for c in pos if c.isdigit()))
# For negative (West) positions 3600 - numeric position value!!!
if neg_pos:
pos = 3600 - pos
parser = PiconsParser(single=single)
parser.reset()
parser.feed(f.read())
picons = parser.picons
if picons:
os.makedirs(picons_path, exist_ok=True)
for p in picons:
try:
if single:
on_id, freq = on_id.strip().split("::")
namespace = "{:X}{:X}".format(int(pos), int(freq))
else:
namespace = "{:X}0000".format(int(pos))
name = PiconsParser.format(ssid if single else p.ssid, on_id, namespace, picon_ids, s_type)
p_name = picons_path + (name if name else os.path.basename(p.ref))
shutil.copyfile(tmp_path + "www.lyngsat.com/" + p.ref.lstrip("."), p_name)
except (TypeError, ValueError) as e:
msg = "Picons format parse error: {}".format(p) + "\n" + str(e)
# log(msg)
print(msg)
def parse(provider, picons_path, picon_ids, s_type=SettingsType.ENIGMA_2):
""" Returns tuple(url, picon file name) list. """
req = requests.get(provider.url, timeout=5)
if req.status_code == 200:
logo_data = req.text
else:
log("Provider picons downloading error: {} {}".format(provider.url, req.reason))
return
on_id, pos, ssid, single = provider.on_id, provider.pos, provider.ssid, provider.single
neg_pos = pos.endswith("W")
pos = int("".join(c for c in pos if c.isdigit()))
# For negative (West) positions 3600 - numeric position value!!!
if neg_pos:
pos = 3600 - pos
parser = PiconsParser(single=provider.single)
parser.reset()
parser.feed(logo_data)
picons = parser.picons
picons_data = []
if picons:
for p in picons:
try:
if single:
on_id, freq = on_id.strip().split("::")
namespace = "{:X}{:X}".format(int(pos), int(freq))
else:
namespace = "{:X}0000".format(int(pos))
if single and not ssid.isdigit():
ssid = "".join(c for c in ssid if c.isdigit()) or "0"
name = PiconsParser.format(ssid if single else p.ssid, on_id, namespace, picon_ids, s_type)
p_name = picons_path + (name if name else os.path.basename(p.ref))
picons_data.append(("{}{}".format(PiconsParser._BASE_URL, p.ref), p_name))
except (TypeError, ValueError) as e:
msg = "Picons format parse error: {}".format(p) + "\n" + str(e)
log(msg)
return picons_data
@staticmethod
def format(ssid, on_id, namespace, picon_ids, s_type):
@@ -125,7 +342,8 @@ class ProviderParser(HTMLParser):
_POSITION_PATTERN = re.compile("at\s\d+\..*(?:E|W)']")
_ONID_TID_PATTERN = re.compile("^\d+-\d+.*")
_TRANSPONDER_FREQUENCY_PATTERN = re.compile("^\d+ [HVLR]+")
_DOMAINS = {"/tvchannels/", "/radiochannels/", "/packages/"}
_DOMAINS = {"/tvchannels/", "/radiochannels/", "/packages/", "/logo/"}
_BASE_URL = "https://www.lyngsat.com"
def __init__(self, entities=False, separator=' '):
@@ -153,26 +371,17 @@ class ProviderParser(HTMLParser):
if tag == 'tr':
self._is_th = True
if tag == "img":
if attrs[0][1].startswith("logo/"):
if attrs[0][1].startswith("/logo/"):
self._current_row.append(attrs[0][1])
if tag == "a":
url = attrs[0][1]
if 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':
@@ -185,41 +394,49 @@ class ProviderParser(HTMLParser):
self._current_row.append(final_cell)
self._current_cell = []
elif tag == 'tr':
r = self._current_row
row = self._current_row
# Satellite position
if not self._positon:
pos = re.findall(self._POSITION_PATTERN, str(r))
pos = re.findall(self._POSITION_PATTERN, str(row))
if pos:
self._positon = "".join(c for c in str(pos) if c.isdigit() or c in ".EW")
len_row = len(r)
len_row = len(row)
if len_row > 2:
m = self._TRANSPONDER_FREQUENCY_PATTERN.match(r[1])
m = self._TRANSPONDER_FREQUENCY_PATTERN.match(row[0])
if m:
self._freq = m.group().split()[0]
if len_row == 12:
if len_row > 12:
# Providers
name = r[5]
name = row[5]
self._prv_names.add(name)
m = self._ONID_TID_PATTERN.match(str(r[-2]))
m = self._ONID_TID_PATTERN.match(str(row[-5]))
if m:
on_id, tid = m.group().split("-")
if on_id not in self._ids:
r[-2] = on_id
self._on_id = on_id
row[-2] = on_id
self._ids.add(on_id)
r[0] = self._positon
row[0] = self._positon
if name + on_id not in self._prv_names:
self._prv_names.add(name + on_id)
self.rows.append(Provider(logo=r[2], name=name, pos=self._positon, url=r[6], on_id=on_id,
logo_data = None
if row[2].startswith("/logo/"):
req = requests.get(self._BASE_URL + row[2], timeout=5)
if req.status_code == 200:
logo_data = req.content
else:
log("Downloading provider logo error: {}".format(req.reason))
self.rows.append(Provider(logo=logo_data, name=name, pos=self._positon, url=row[6], on_id=on_id,
ssid=None, single=False, selected=True))
elif 6 < len_row < 10:
elif 6 < len_row < 12:
# Single services
name, url, ssid = None, None, None
if r[0].startswith("http"):
name, url, ssid = r[1], r[0], r[4]
elif r[1].startswith("http"):
name, url, ssid = r[2], r[1], r[5]
if row[0].startswith("http"):
name, url, ssid = row[1], row[0], row[0]
elif row[1].startswith("http"):
name, url, ssid = row[2], row[1], row[0]
if name and url:
on_id = "{}::{}".format(self._on_id if self._on_id else "1", self._freq)
@@ -235,14 +452,51 @@ class ProviderParser(HTMLParser):
super().reset()
def parse_providers(open_path):
def parse_providers(url):
""" Returns a list of providers sorted by logo [single channels after providers]. """
parser = ProviderParser()
parser.reset()
with open(open_path, encoding="utf-8", errors="replace") as f:
parser.feed(f.read())
request = requests.get(url=url, headers=_HEADERS)
if request.status_code == 200:
parser.feed(request.text)
else:
log("Parse providers error [{}]: {}".format(url, request.reason))
return parser.rows
def srt(p):
if p.logo is None:
return 1
return 0
providers = parser.rows
providers.sort(key=srt)
return providers
def download_picon(src_url, dest_path, callback):
""" Downloads and saves the picon to file. """
err_msg = "Picon download error: {} [{}]"
timeout = (3, 5) # connect and read timeouts
if callback:
callback("Downloading: {}.\n".format(os.path.basename(dest_path)))
req = requests.get(src_url, timeout=timeout, stream=True)
if req.status_code != 200:
err_msg = err_msg.format(src_url, req.reason)
log(err_msg)
if callback:
callback(err_msg + "\n")
else:
try:
with open(dest_path, "wb") as f:
for chunk in req:
f.write(chunk)
except OSError as e:
err_msg = "Saving picon [{}] error: {}".format(dest_path, e)
log(err_msg)
if callback:
callback(err_msg + "\n")
@run_task

View File

@@ -1,31 +1,83 @@
""" Module for download satellites from internet ("flysat.com")
for replace or update current satellites.xml file.
""" Module for downloading satellites, transponders ans services from the web.
Sources: www.flysat.com, www.lyngsat.com.
Replaces or updates the current satellites.xml file.
"""
import re
import requests
from enum import Enum
from html.parser import HTMLParser
import requests
from app.commons import log
from app.eparser import Satellite, Transponder, is_transponder_valid
from app.eparser.ecommons import PLS_MODE
from app.eparser.ecommons import (PLS_MODE, get_key_by_value, FEC, SYSTEM, POLARIZATION, MODULATION, SERVICE_TYPE,
Service, CAS)
_HEADERS = {"User-Agent": "Mozilla/5.0 (Linux x86_64; rv:85.0) Gecko/20100101 Firefox/85.0"}
class SatelliteSource(Enum):
FLYSAT = ("https://www.flysat.com/satlist.php",)
LYNGSAT = ("https://www.lyngsat.com/asia.html", "https://www.lyngsat.com/europe.html",
"https://www.lyngsat.com/atlantic.html", "https://www.lyngsat.com/america.html")
KINGOFSAT = ("https://en.kingofsat.net/satellites.php",)
@staticmethod
def get_sources(src):
return src.value
class Cell:
""" Cell representation for table parsers. """
__slots__ = ["_text", "_url", "_img"]
def __init__(self, text=None, link=None, img=None):
self._text = text
self._url = link
self._img = img
def __repr__(self):
return "Cell({}, {}, {})".format(self._text, self._url, self._img)
def __str__(self):
return "<Cell(text={}, link={}, img={})>".format(self._text, self._url, self._img)
def __iter__(self):
return (x for x in (self._text, self._url, self._img))
def __len__(self):
return 3
@property
def text(self):
return self._text
@text.setter
def text(self, value):
self._text = value
@property
def url(self):
return self._url
@url.setter
def url(self, value):
self._url = value
@property
def img(self):
return self._img
@img.setter
def img(self, value):
self._img = value
class SatellitesParser(HTMLParser):
""" Parser for satellite html page. """
_HEADERS = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:45.0) Gecko/20100101 Firefox/59.02"}
POS_PAT = re.compile(r".*?(\d+\.\d°?[EW]).*")
def __init__(self, source=SatelliteSource.FLYSAT, entities=False, separator=' '):
@@ -42,12 +94,14 @@ class SatellitesParser(HTMLParser):
self._source = source
def handle_starttag(self, tag, attrs):
if tag == 'td':
if tag == "td":
self._is_td = True
if tag == 'tr':
if tag == "tr":
self._is_th = True
if tag == "a":
self._current_row.append(attrs[0][1])
for atr in attrs:
if atr[0] == "href":
self._current_row.append(atr[1])
def handle_data(self, data):
""" Save content to a cell """
@@ -55,16 +109,16 @@ class SatellitesParser(HTMLParser):
self._current_cell.append(data.strip())
def handle_endtag(self, tag):
if tag == 'td':
if tag == "td":
self._is_td = False
elif tag == 'tr':
elif tag == "tr":
self._is_th = False
if tag in ('td', 'th'):
if tag in ("td", "th"):
final_cell = self._separator.join(self._current_cell).strip()
self._current_row.append(final_cell)
self._current_cell = []
elif tag == 'tr':
elif tag == "tr":
row = self._current_row
self._rows.append(row)
self._current_row = []
@@ -80,7 +134,7 @@ class SatellitesParser(HTMLParser):
for src in SatelliteSource.get_sources(self._source):
try:
request = requests.get(url=src, headers=self._HEADERS)
request = requests.get(url=src, headers=_HEADERS)
except requests.exceptions.ConnectionError as e:
log(repr(e))
return []
@@ -98,34 +152,27 @@ class SatellitesParser(HTMLParser):
return list(map(get_sat, filter(lambda x: all(x) and len(x) == 5, self._rows)))
elif self._source is SatelliteSource.LYNGSAT:
extra_pattern = re.compile("^https://www\.lyngsat\.com/[\w-]+\.html")
base_url = "https://www.lyngsat.com/"
sats = []
current_pos = "0"
for row in filter(lambda x: len(x) in (5, 7, 8), self._rows):
r_len = len(row)
if r_len == 7:
current_pos = self.parse_position(row[2])
name = row[1].rsplit("/")[-1].rstrip(".html").replace("-", " ")
sats.append((name, current_pos, row[5], base_url + row[1], False)) # [all in one] satellites
sats.append((row[4], current_pos, row[5], base_url + row[3], False))
if r_len == 8: # for a very limited number of satellites
data = list(filter(None, row))
urls = set()
sat_type = ""
for d in data:
url = re.match(extra_pattern, d)
if url:
urls.add(url.group(0))
if d in ("C", "Ku", "CKu"):
sat_type = d
current_pos = self.parse_position(data[1])
for url in urls:
name = url.rsplit("/")[-1].rstrip(".html").replace("-", " ")
sats.append((name, current_pos, sat_type, base_url + url, False))
elif r_len == 5:
sats.append((row[2], current_pos, row[3], base_url + row[1], False))
cur_pos = "0"
for row in filter(lambda x: 3 < len(x) < 8, self._rows):
if not row[0]:
row = row[1:]
pos = self.parse_position(row[1])
if not self.POS_PAT.match(pos):
if len(row) == 4 and row[0].endswith(".html"):
sats.append((row[1], cur_pos, row[-2], base_url + row[0], False))
continue
sats.append((row[-3], pos, row[-2], base_url + row[0], False))
cur_pos = pos
return sats
elif source is SatelliteSource.KINGOFSAT:
def get_sat(r):
return r[3], self.parse_position(r[1]), None, r[0], False
return list(map(get_sat, filter(lambda x: len(x) == 17, self._rows)))
def get_satellite(self, sat):
pos = sat[1]
@@ -145,16 +192,29 @@ class SatellitesParser(HTMLParser):
def get_transponders(self, sat_url):
""" Getting transponders(sorted by frequency). """
self._rows.clear()
url = "https://www.flysat.com/" + sat_url if self._source is SatelliteSource.FLYSAT else sat_url
request = requests.get(url=url, headers=self._HEADERS)
reason = request.reason
trs = []
if reason == "OK":
self.feed(request.text)
if self._source is SatelliteSource.FLYSAT:
self.get_transponders_for_fly_sat(trs)
elif self._source is SatelliteSource.LYNGSAT:
self.get_transponders_for_lyng_sat(trs)
url = sat_url
if self._source is SatelliteSource.FLYSAT:
url = "https://www.flysat.com/" + sat_url
elif self._source is SatelliteSource.KINGOFSAT:
url = "https://en.kingofsat.net/" + sat_url
try:
request = requests.get(url=url, headers=_HEADERS)
except requests.exceptions.ConnectionError as e:
log("Getting transponders error: {}".format(e))
else:
if request.status_code == 200:
self.feed(request.text)
if self._source is SatelliteSource.FLYSAT:
self.get_transponders_for_fly_sat(trs)
elif self._source is SatelliteSource.LYNGSAT:
self.get_transponders_for_lyng_sat(trs)
elif self._source is SatelliteSource.KINGOFSAT:
self.get_transponders_for_king_of_sat(trs)
else:
log("SatellitesParser [get transponders] error: {} {}".format(url, request.reason))
return sorted(trs, key=lambda x: int(x.frequency))
@@ -210,42 +270,235 @@ class SatellitesParser(HTMLParser):
trs.extend(n_trs)
def get_transponders_for_lyng_sat(self, trs):
""" Parsing transponders for LyngSat """
""" Parsing transponders for LyngSat. """
frq_pol_pattern = re.compile("(\\d{4,5})\\s+([RLHV]).*")
sr_fec_pattern = re.compile("^(\\d{4,5})-(\\d/\\d)(.+PSK)?(.*)?$")
sys_pattern = re.compile("(DVB-S[2]?) ?(PLS+ (Root|Gold|Combo)+ (\\d+))* ?(multistream stream (\\d+))?",
re.IGNORECASE)
sr_fec_pattern = re.compile(r"(DVB-S[2]?)\s+(.+PSK)?.*?(\d+)\s+(\d/\d)\s*(?:T2-MI\s+PLP\s+(\d+))?.*")
zeros = "000"
pls_modes = {v: k for k, v in PLS_MODE.items()}
pls_mode, pls_code, pls_id = None, None, None
for r in filter(lambda x: len(x) > 8, self._rows):
for frq in r[1], r[2], r[3]:
for row in filter(lambda x: len(x) > 8, self._rows):
for frq in row[1], row[2], row[3]:
freq = re.match(frq_pol_pattern, frq)
if freq:
break
if not freq:
continue
frq, pol = freq.group(1), freq.group(2)
sr_fec = re.match(sr_fec_pattern, r[-3])
srf = " ".join(row[3:5])
sr_fec = re.search(sr_fec_pattern, srf)
if not sr_fec:
continue
sr, fec, mod = sr_fec.group(1), sr_fec.group(2), sr_fec.group(3)
sys, mod, sr, fec = sr_fec.group(1), sr_fec.group(2), sr_fec.group(3), sr_fec.group(4)
mod = mod.strip() if mod else "Auto"
res = re.match(sys_pattern, r[-4])
if not res:
continue
sys = res.group(1)
pls_mode = res.group(3)
pls_mode = pls_modes.get(pls_mode.capitalize(), None) if pls_mode else pls_mode
pls_code = res.group(4)
pls_id = res.group(6)
pls_id = sr_fec.group(5)
tr = Transponder(frq + zeros, sr + zeros, pol, fec, sys, mod, pls_mode, pls_code, pls_id)
if is_transponder_valid(tr):
trs.append(tr)
def get_transponders_for_king_of_sat(self, trs):
""" Getting transponders for KingOfSat source.
Since the *.ini file contains incomplete information, it is not used.
"""
zeros = "000"
pat = re.compile(
r"(\d+).00\s+([RLHV])\s+(DVB-S[2]?)\s+(?:T2-MI, PLP (\d+)\s+)?(.*PSK).*?(?:Stream\s+(\d+))?\s+(\d+)\s+(\d+/\d+)$")
for row in filter(lambda r: len(r) == 16 and self.POS_PAT.match(r[0]), self._rows):
res = pat.search(" ".join((row[0], row[2], row[3], row[8], row[9], row[10])))
if res:
freq, sr, pol, fec, sys = res.group(1), res.group(7), res.group(2), res.group(8), res.group(3)
mod, pls_id, pls_code = res.group(5), res.group(4), res.group(6)
tr = Transponder(freq + zeros, sr + zeros, pol, fec, sys, mod, None, pls_code, pls_id)
if is_transponder_valid(tr):
trs.append(tr)
class ServicesParser(HTMLParser):
""" Services parser for LYNGSAT source. """
def __init__(self, source=SatelliteSource.LYNGSAT, entities=False, separator=' '):
HTMLParser.__init__(self)
self._S_TYPES = {"": "2", "MPEG-2 SD": "1", "MPEG-2/SD": "1", "SD": "1", "MPEG-4 SD": "22", "MPEG-4/SD": "22",
"MPEG-4": "22", "HEVC SD": "22", "MPEG-4/HD": "25", "MPEG-4 HD": "25", "MPEG-4 HD 1080": "25",
"MPEG-4 HD 720": "25", "HEVC HD": "25", "HEVC/HD": "25", "HEVC": "31", "HEVC/UHD": "31",
"HEVC UHD": "31", "HEVC UHD 4K": "31"}
self._TR_PAT = re.compile(
r".*?(\d+)\s+([RLHV]).*(DVB-S[2]?)/?(.*PSK)?\s(T2-MI)?\s?SR-FEC:\s(\d+)-(\d/\d)\s+.*ONID-TID:\s+(\d+)-(\d+).*")
self._POS_PAT = re.compile(r".*?(\d+\.\d°[EW]).*")
self._TR = "s {}000:{}000:{}:{}:{}:{}:{}:{}"
self._S2_TR = "{}:{}:{}:{}"
self._parse_html_entities = entities
self._separator = separator
self._is_td = False
self._is_th = False
self._current_row = []
self._current_cell_text = []
self._current_cell = Cell()
self._rows = []
self._source = source
def handle_starttag(self, tag, attrs):
if tag == "td":
self._is_td = True
elif tag == "tr":
self._is_th = True
elif tag == "a" and not self._current_cell.url:
self._current_cell.url = attrs[0][1]
elif tag == "img":
img_link = attrs[0][1]
if img_link.startswith("/logo/"):
self._current_cell.img = img_link
def handle_data(self, data):
""" Save content to a cell """
if self._is_td or self._is_th:
self._current_cell_text.append(data.strip())
def handle_endtag(self, tag):
if tag == "td":
self._is_td = False
elif tag == "tr":
self._is_th = False
if tag in ("td", "th"):
final_cell = self._separator.join(self._current_cell_text).strip()
self._current_cell.text = final_cell
self._current_row.append(self._current_cell)
self._current_cell_text = []
self._current_cell = Cell()
elif tag == "tr":
row = self._current_row
self._rows.append(row)
self._current_row = []
def error(self, message):
log("ServicesParser error: {}".format(message))
def init_data(self, url):
""" Initializes data for the given URL. """
if self._source is not SatelliteSource.LYNGSAT:
raise ValueError("Unsupported source: {}!".format(self._source.name))
self._rows.clear()
request = requests.get(url=url, headers=_HEADERS)
reason = request.reason
if reason == "OK":
self.feed(request.text)
else:
raise ValueError(reason)
def get_transponders_links(self, sat_url):
""" Returns transponder links. """
try:
self.init_data(sat_url)
except ValueError as e:
log(e)
else:
url = "https://www.lyngsat.com/muxes/"
return [row[0] for row in
filter(lambda x: x and len(x) > 8 and x[0].url and x[0].url.startswith(url), self._rows)]
return []
def get_transponder_services(self, tr_url, sat_position=None, use_pids=False):
""" Returns services for given transponder.
@param tr_url: transponder URL.
@param sat_position: custom satellite position. Sometimes required to adjust the namespace.
@param use_pids: if possible use additional pids [video, audio].
"""
services = []
try:
self.init_data(tr_url)
except ValueError as e:
log(e)
else:
pos, freq, sr, fec, pol, namespace, tid, nid = sat_position or 0, 0, 0, 0, 0, 0, 0, 0
sys = "DVB-S"
pos_found = False
tr = None
# Transponder
for r in filter(lambda x: x and 6 < len(x) < 9, self._rows):
if not pos_found:
pos_tr = re.match(self._POS_PAT, r[0].text)
if not pos_tr:
continue
if not sat_position:
pos = int(SatellitesParser.get_position(
"".join(c for c in pos_tr.group(1) if c.isdigit() or c.isalpha())))
pos_found = True
if pos_found:
text = " ".join(c.text for c in r[1:])
td = re.match(self._TR_PAT, text)
if td:
freq, pol = int(td.group(1)), get_key_by_value(POLARIZATION, td.group(2))
if td.group(5):
log("Detected T2-MI transponder!")
continue
sys, mod, sr, _fec, = td.group(3), td.group(4), td.group(6), td.group(7)
nid, tid = td.group(8), td.group(9)
neg_pos = False # POS = W
# For negative (West) positions: 3600 - numeric position value!!!
namespace = "{:04x}0000".format(3600 - pos if neg_pos else pos)
inv = 2 # Default
fec = get_key_by_value(FEC, _fec)
sys = get_key_by_value(SYSTEM, sys)
tr_flag = 1
mod = get_key_by_value(MODULATION, mod)
roll_off = 0 # 35% DVB-S2/DVB-S (default)
pilot = 2 # Auto
s2_flags = "" if sys == "DVB-S" else self._S2_TR.format(tr_flag, mod or 0, roll_off, pilot)
nid, tid = int(nid), int(tid)
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
if not tr:
msg = "ServicesParser error [get transponder services]: {}"
er = "Transponder [{}] not found or its type [T2-MI, etc] not supported yet.".format(freq)
log(msg.format(er))
return []
# Services
for r in filter(lambda x: x and len(x) == 12 and (x[0].text.isdigit()), self._rows):
sid, name, s_type, v_pid, a_pid, cas, pkg = r[0].text, r[2].text, r[4].text, r[
5].text.strip(), r[6].text.split(), r[9].text, r[10].text.strip()
try:
s_type = self._S_TYPES.get(s_type, "3") # 3 = Data
_s_type = SERVICE_TYPE.get(s_type, SERVICE_TYPE.get("3")) # str repr
sid = int(sid)
data_id = "{:04x}:{}:{:04x}:{:04x}:{}:0:0".format(sid, namespace, tid, nid, s_type)
fav_id = "{}:{}:{}:{}".format(sid, tid, nid, namespace)
picon_id = "1_0_{:X}_{}_{}_{}_{}_0_0_0.png".format(int(s_type), sid, tid, nid, namespace)
# Flags.
flags = "p:{}".format(pkg)
cas = ",".join(get_key_by_value(CAS, c) or "C:0000" for c in cas.split()) if cas else None
if use_pids:
v_pid = "c:00{:04x}".format(int(v_pid)) if v_pid else None
a_pid = ",".join(["c:01{:04x}".format(int(p)) for p in a_pid]) if a_pid else None
flags = ",".join(filter(None, (flags, v_pid, a_pid, cas)))
else:
flags = ",".join(filter(None, (flags, cas)))
services.append(Service(flags, "s", None, name, None, None, pkg, _s_type, r[1].img, picon_id,
sid, freq, sr, pol, fec, sys, pos, data_id, fav_id, tr))
except ValueError as e:
log("ServicesParser error [get transponder services]: {}".format(e))
return services
if __name__ == "__main__":
pass

View File

@@ -129,6 +129,21 @@ class YouTube:
return None, rsn
def get_yt_playlist(self, list_id, url=None):
""" Returns tuple from the playlist header and list of tuples (title, video id). """
if self._settings.enable_yt_dl and url:
try:
self._yt_dl.update_options({"noplaylist": False, "extract_flat": True})
info = self._yt_dl.get_info(url, skip_errors=False)
if "url" in info:
info = self._yt_dl.get_info(info.get("url"), skip_errors=False)
return info.get("title", ""), [(e.get("title", ""), e.get("id", "")) for e in info.get("entries", [])]
finally:
# Restoring default options
self._yt_dl.update_options({"noplaylist": True, "extract_flat": False})
return PlayListParser.get_yt_playlist(list_id)
class PlayListParser(HTMLParser):
""" Very simple parser to handle YouTube playlist pages. """
@@ -139,6 +154,7 @@ class PlayListParser(HTMLParser):
self._header = ""
self._playlist = []
self._is_script = False
self._scr_start = ('var ytInitialData = ', 'window["ytInitialData"] = ')
def handle_starttag(self, tag, attrs):
if tag == "script":
@@ -147,8 +163,11 @@ class PlayListParser(HTMLParser):
def handle_data(self, data):
if self._is_script:
data = data.lstrip()
if data.startswith('window["ytInitialData"] = '):
data = data.split(";")[0].lstrip('window["ytInitialData"] = ')
if data.startswith(self._scr_start):
data = data.split(";")[0]
for s in self._scr_start:
data = data.lstrip(s)
try:
resp = json.loads(data)
except JSONDecodeError as e:
@@ -164,7 +183,7 @@ class PlayListParser(HTMLParser):
ct = resp.get("contents", None)
if ct:
for d in [(d.get("title", {}).get("simpleText", ""),
for d in [(d.get("title", {}).get("runs", [{}])[0].get("text", ""),
d.get("videoId", "")) for d in flat("playlistVideoRenderer", ct)]:
self._playlist.append(d)
self._is_script = False
@@ -205,6 +224,7 @@ class YouTubeDL:
_DownloadError = None
_LATEST_RELEASE_URL = "https://api.github.com/repos/ytdl-org/youtube-dl/releases/latest"
_OPTIONS = {"noplaylist": True, # Single video instead of a playlist [ignoring playlist in URL].
"extract_flat": False, # Do not resolve URLs, return the immediate result.
"quiet": True, # Do not print messages to stdout.
"simulate": True, # Do not download the video files.
"cookiefile": "cookies.txt"} # File name where cookies should be read from and dumped to.
@@ -316,8 +336,17 @@ class YouTubeDL:
self._callback("Update process. Please wait.", False)
return {}, ""
info = self.get_info(url, skip_errors)
fmts = info.get("formats", None)
if fmts:
return {Quality.get(int(fm["format_id"])): fm.get("url", "") for fm in fmts if
fm.get("format_id", "") in self._supported}, info.get("title", "")
return {}, info.get("title", "")
def get_info(self, url, skip_errors=False):
try:
info = self._dl.extract_info(url, download=False)
return self._dl.extract_info(url, download=False)
except URLError as e:
log(str(e))
raise YouTubeException(e)
@@ -325,13 +354,13 @@ class YouTubeDL:
log(str(e))
if not skip_errors:
raise YouTubeException(e)
else:
fmts = info.get("formats", None)
if fmts:
return {Quality.get(int(fm["format_id"])): fm.get("url", "") for fm in fmts if
fm.get("format_id", "") in self._supported}, info.get("title", "")
return {}, info.get("title", "")
def update_options(self, options):
self._dl.params.update(options)
@property
def options(self):
return self._dl.params
def flat(key, d):

View File

@@ -1,143 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="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="menu_bar">
<submenu>
<attribute name="label" translatable="yes">File</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">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">Save</attribute>
<attribute name="action">app.on_data_save</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>
<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>
<section>
<item>
<attribute name="label" translatable="yes">Search</attribute>
<attribute name="action">win.search</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Filter</attribute>
<attribute name="action">win.filter</attribute>
</item>
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">Tools</attribute>
<section>
<item>
<attribute name="label" translatable="yes">Satellites editor</attribute>
<attribute name="action">app.on_satellite_editor_show</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Picons manager</attribute>
<attribute name="action">app.on_picons_manager_show</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Backups</attribute>
<attribute name="action">app.on_backup_tool_show</attribute>
</item>
</section>
<section id="telnet_section">
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">IPTV</attribute>
<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_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>
</submenu>
</menu>
</interface>

View File

@@ -8,9 +8,9 @@ from enum import Enum
from app.commons import run_idle
from app.settings import SettingsType
from app.ui.dialogs import show_dialog, DialogType
from app.ui.dialogs import show_dialog, DialogType, get_builder
from app.ui.main_helper import append_text_to_tview
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey
class RestoreType(Enum):
@@ -30,10 +30,7 @@ class BackupDialog:
"on_resize": self.on_resize,
"on_key_release": self.on_key_release}
builder = Gtk.Builder()
builder.set_translation_domain("demon-editor")
builder.add_from_file(UI_RESOURCES_PATH + "backup_dialog.glade")
builder.connect_signals(handlers)
builder = get_builder(UI_RESOURCES_PATH + "backup_dialog.glade", handlers)
self._settings = settings
self._s_type = settings.setting_type
@@ -176,7 +173,7 @@ class BackupDialog:
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
if key is KeyboardKey.DELETE:
self.on_remove(view)

View File

@@ -33,12 +33,6 @@ 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 -->
@@ -47,89 +41,133 @@ 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>
<placeholder/>
<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">Backups</property>
<property name="spacing">2</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkBox" id="header_box">
<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">
<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"/>
<child>
<object class="GtkImage" id="restore_bouquets_header_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-revert</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="restore_all_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">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>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</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"/>
</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="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="margin_top">1</property>
<property name="margin_bottom">1</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkPaned" id="main_paned">
<property name="visible">True</property>
@@ -213,106 +251,6 @@ Author: Dmitriy Yefremov
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">15</property>
<property name="margin_right">15</property>
<child>
<object class="GtkButtonBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="layout_style">expand</property>
<child>
<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">True</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">True</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">True</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>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="info_check_button">
<property name="label" translatable="yes">Details</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</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>
<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>
</object>
<packing>
<property name="expand">False</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>
@@ -363,4 +301,57 @@ 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>

1952
app/ui/control.glade Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,826 @@
""" Receiver control module via HTTP API. """
import os
from datetime import datetime
from enum import Enum
from urllib.parse import quote
from gi.repository import GLib
from .dialogs import show_dialog, DialogType, get_message, get_builder
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column
from ..commons import run_task, run_with_delay, log, run_idle
from ..connections import HttpAPI, UtfFTP
from ..eparser.ecommons import BqServiceType
class ControlBox(Gtk.HBox):
_TIME_STR = "%Y-%m-%d %H:%M"
class Tool(Enum):
""" The currently displayed tool. """
REMOTE = "control"
EPG = "epg"
TIMERS = "timers"
TIMER = "timer"
RECORDINGS = "recordings"
class EpgRow(Gtk.ListBoxRow):
def __init__(self, event: dict, **properties):
super().__init__(**properties)
self._event_data = event
h_box = Gtk.HBox()
h_box.set_orientation(Gtk.Orientation.VERTICAL)
self._title = event.get("e2eventtitle", "")
title_label = Gtk.Label(self._title)
self._desc = event.get("e2eventdescription", "")
description = Gtk.Label()
description.set_markup("<i>{}</i>".format(self._desc))
description.set_line_wrap(True)
description.set_max_width_chars(25)
start = int(event.get("e2eventstart", "0"))
start_time = datetime.fromtimestamp(start)
end_time = datetime.fromtimestamp(start + int(event.get("e2eventduration", "0")))
time_label = Gtk.Label()
time_label.set_margin_top(5)
self._time_header = "{} - {}".format(start_time.strftime("%A, %H:%M"), end_time.strftime("%H:%M"))
time_label.set_markup("<b>{}</b>".format(self._time_header))
h_box.add(time_label)
h_box.add(title_label)
h_box.add(description)
sep = Gtk.Separator()
sep.set_margin_top(5)
h_box.add(sep)
h_box.set_spacing(5)
self.add(h_box)
self.show_all()
@property
def event_data(self):
return self._event_data or {}
@property
def title(self):
return self._title or ""
@property
def desc(self):
return self._desc or ""
@property
def time_header(self):
return self._time_header or ""
class TimerRow(Gtk.ListBoxRow):
_UI_PATH = UI_RESOURCES_PATH + "timer_row.glade"
def __init__(self, timer, **properties):
super().__init__(**properties)
self._timer = timer
builder = get_builder(self._UI_PATH, None, use_str=True)
row_box = builder.get_object("timer_row_box")
name_label = builder.get_object("timer_name_label")
description_label = builder.get_object("timer_description_label")
service_name_label = builder.get_object("timer_service_name_label")
time_label = builder.get_object("timer_time_label")
name_label.set_text(timer.get("e2name", "") or "")
description_label.set_text(timer.get("e2description", "") or "")
service_name_label.set_text(timer.get("e2servicename", "") or "")
# Time
start_time = datetime.fromtimestamp(int(timer.get("e2timebegin", "0")))
end_time = datetime.fromtimestamp(int(timer.get("e2timeend", "0")))
time_label.set_text("{} - {}".format(start_time.strftime("%A, %H:%M"), end_time.strftime("%H:%M")))
self.add(row_box)
self.show()
@property
def timer(self):
return self._timer
class TimerAction(Enum):
ADD = 0
EVENT = 1
CHANGE = 2
class RecordingsRow(Gtk.ListBoxRow):
def __init__(self, movie: dict, **properties):
super().__init__(**properties)
self._movie = movie
h_box = Gtk.HBox()
h_box.set_orientation(Gtk.Orientation.VERTICAL)
self._service = movie.get("e2servicename")
service_label = Gtk.Label()
service_label.set_markup("<b>{}</b>".format(self._service))
self._title = movie.get("e2title", "")
title_label = Gtk.Label(self._title)
self._desc = movie.get("e2description", "")
description = Gtk.Label()
description.set_markup("<i>{}</i>".format(self._desc))
description.set_line_wrap(True)
description.set_max_width_chars(25)
start_time = datetime.fromtimestamp(int(movie.get("e2time", "0")))
start_time_label = Gtk.Label()
start_time_label.set_margin_top(5)
start_time_label.set_markup("<b>{}</b>".format(start_time.strftime("%A, %H:%M")))
time_label = Gtk.Label()
time_label.set_margin_top(5)
time_label.set_markup("<b>{}</b>".format(movie.get("e2length", "0")))
info_box = Gtk.HBox()
info_box.set_orientation(Gtk.Orientation.HORIZONTAL)
info_box.set_spacing(10)
info_box.pack_start(start_time_label, False, True, 5)
info_box.pack_end(time_label, False, True, 5)
h_box.add(service_label)
h_box.add(title_label)
h_box.add(description)
h_box.add(info_box)
sep = Gtk.Separator()
sep.set_margin_top(5)
h_box.add(sep)
h_box.set_spacing(5)
self.set_tooltip_text(movie.get("e2filename", ""))
self.add(h_box)
self.show_all()
@property
def movie(self):
return self._movie
@property
def service(self):
return self._service or ""
@property
def title(self):
return self._title or ""
@property
def desc(self):
return self._desc or ""
@property
def file(self):
return self._movie.get("e2filename", "")
def __init__(self, app, http_api, settings, *args, **kwargs):
super().__init__(*args, **kwargs)
self._http_api = http_api
self._settings = settings
self._update_epg = False
self._app = app
self._last_tool = self.Tool.REMOTE
self._timer_action = self.TimerAction.ADD
self._current_timer = {}
handlers = {"on_visible_tool": self.on_visible_tool,
"on_volume_changed": self.on_volume_changed,
"on_epg_press": self.on_epg_press,
"on_epg_filter_changed": self.on_epg_filter_changed,
"on_timers_press": self.on_timers_press,
"on_timers_drag_data_received": self.on_timers_drag_data_received,
"on_recordings_press": self.on_recordings_press,
"on_recording_filter_changed": self.on_recording_filter_changed,
"on_recordings_dir_changed": self.on_recordings_dir_changed}
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers)
self.add(builder.get_object("main_box_frame"))
self._stack = builder.get_object("stack")
self._screenshot_image = builder.get_object("screenshot_image")
self._screenshot_button_box = builder.get_object("screenshot_button_box")
self._screenshot_check_button = builder.get_object("screenshot_check_button")
self._screenshot_check_button.bind_property("active", self._screenshot_image, "visible")
self._snr_value_label = builder.get_object("snr_value_label")
self._ber_value_label = builder.get_object("ber_value_label")
self._agc_value_label = builder.get_object("agc_value_label")
self._volume_button = builder.get_object("volume_button")
self._epg_list_box = builder.get_object("epg_list_box")
self._epg_list_box.set_filter_func(self.epg_filter_function)
self._epg_filter_entry = builder.get_object("epg_filter_entry")
self._timers_list_box = builder.get_object("timers_list_box")
self._app._control_revealer.bind_property("visible", self, "visible")
# Timers
self._timer_remove_button = builder.get_object("timer_remove_button")
self._timer_remove_button.bind_property("visible", builder.get_object("timer_edit_button"), "visible")
# Timer
self._timer_name_entry = builder.get_object("timer_name_entry")
self._timer_desc_entry = builder.get_object("timer_desc_entry")
self._timer_service_entry = builder.get_object("timer_service_entry")
self._timer_service_ref_entry = builder.get_object("timer_service_ref_entry")
self._timer_event_id_entry = builder.get_object("timer_event_id_entry")
self._timer_begins_entry = builder.get_object("timer_begins_entry")
self._timer_ends_entry = builder.get_object("timer_ends_entry")
self._timer_begins_calendar = builder.get_object("timer_begins_calendar")
self._timer_begins_hr_button = builder.get_object("timer_begins_hr_button")
self._timer_begins_min_button = builder.get_object("timer_begins_min_button")
self._timer_ends_calendar = builder.get_object("timer_ends_calendar")
self._timer_ends_hr_button = builder.get_object("timer_ends_hr_button")
self._timer_ends_min_button = builder.get_object("timer_ends_min_button")
self._timer_enabled_switch = builder.get_object("timer_enabled_switch")
self._timer_action_combo_box = builder.get_object("timer_action_combo_box")
self._timer_after_combo_box = builder.get_object("timer_after_combo_box")
self._timer_mo_check_button = builder.get_object("timer_mo_check_button")
self._timer_tu_check_button = builder.get_object("timer_tu_check_button")
self._timer_we_check_button = builder.get_object("timer_we_check_button")
self._timer_th_check_button = builder.get_object("timer_th_check_button")
self._timer_fr_check_button = builder.get_object("timer_fr_check_button")
self._timer_sa_check_button = builder.get_object("timer_sa_check_button")
self._timer_su_check_button = builder.get_object("timer_su_check_button")
self._timer_location_switch = builder.get_object("timer_location_switch")
self._timer_location_entry = builder.get_object("timer_location_entry")
self._timer_location_switch.bind_property("active", self._timer_location_entry, "sensitive")
# Disable DnD for timer entries.
self._timer_name_entry.drag_dest_unset()
self._timer_desc_entry.drag_dest_unset()
self._timer_service_entry.drag_dest_unset()
# DnD initialization for the timer list.
self._timers_list_box.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
self._timers_list_box.drag_dest_add_text_targets()
# Recordings.
self._recordings_list_box = builder.get_object("recordings_list_box")
self._recordings_list_box.set_filter_func(self.recording_filter_function)
self._recordings_filter_entry = builder.get_object("recordings_filter_entry")
self._recordings_dir_box = builder.get_object("recordings_dir_box")
self.init_actions(app)
self.connect("hide", self.on_hide)
self.show()
def init_actions(self, app):
# Remote controller actions
app.set_action("on_up", lambda a, v: self.on_remote_action(HttpAPI.Remote.UP))
app.set_action("on_down", lambda a, v: self.on_remote_action(HttpAPI.Remote.DOWN))
app.set_action("on_left", lambda a, v: self.on_remote_action(HttpAPI.Remote.LEFT))
app.set_action("on_right", lambda a, v: self.on_remote_action(HttpAPI.Remote.RIGHT))
app.set_action("on_ok", lambda a, v: self.on_remote_action(HttpAPI.Remote.OK))
app.set_action("on_menu", lambda a, v: self.on_remote_action(HttpAPI.Remote.MENU))
app.set_action("on_exit", lambda a, v: self.on_remote_action(HttpAPI.Remote.EXIT))
app.set_action("on_red", lambda a, v: self.on_remote_action(HttpAPI.Remote.RED))
app.set_action("on_green", lambda a, v: self.on_remote_action(HttpAPI.Remote.GREEN))
app.set_action("on_yellow", lambda a, v: self.on_remote_action(HttpAPI.Remote.YELLOW))
app.set_action("on_blue", lambda a, v: self.on_remote_action(HttpAPI.Remote.BLUE))
# Power
app.set_action("on_standby", lambda a, v: self.on_power_action(HttpAPI.Power.STANDBY))
app.set_action("on_wake_up", lambda a, v: self.on_power_action(HttpAPI.Power.WAKEUP))
app.set_action("on_reboot", lambda a, v: self.on_power_action(HttpAPI.Power.REBOOT))
app.set_action("on_restart_gui", lambda a, v: self.on_power_action(HttpAPI.Power.RESTART_GUI))
app.set_action("on_shutdown", lambda a, v: self.on_power_action(HttpAPI.Power.DEEP_STANDBY))
# Screenshots
app.set_action("on_screenshot_all", self.on_screenshot_all)
app.set_action("on_screenshot_video", self.on_screenshot_video)
app.set_action("on_screenshot_osd", self.on_screenshot_osd)
# Timers
app.set_action("on_timer_add", self.on_timer_add)
app.set_action("on_timer_add_from_event", self.on_timer_add_from_event)
app.set_action("on_timer_remove", self.on_timer_remove)
app.set_action("on_timer_edit", self.on_timer_edit)
app.set_action("on_timer_save", self.on_timer_save)
app.set_action("on_timer_cancel", self.on_timer_cancel)
app.set_action("on_timer_begins_set", self.on_timer_begins_set)
app.set_action("on_timer_ends_set", self.on_timer_ends_set)
# Recordings
app.set_action("on_recording_remove", self.on_recording_remove)
@property
def update_epg(self):
return self._update_epg
def on_visible_tool(self, stack, param):
tool = self.Tool(stack.get_visible_child_name())
self._update_epg = tool is self.Tool.EPG
if tool is self.Tool.TIMERS:
self.update_timer_list()
if tool is self.Tool.RECORDINGS:
self.update_recordings_list()
if tool is not self.Tool.TIMER:
self._last_tool = tool
def on_hide(self, item):
self._update_epg = False
# ***************** Remote controller ********************* #
def on_remote(self, action, state=False):
""" Shows/Hides [R key] remote controller. """
action.set_state(state)
self._remote_revealer.set_visible(state)
self._remote_revealer.set_reveal_child(state)
if state:
self._http_api.send(HttpAPI.Request.VOL, "state", self.update_volume)
def on_remote_action(self, action):
self._http_api.send(HttpAPI.Request.REMOTE, action, self.on_response)
@run_with_delay(0.5)
def on_volume_changed(self, button, value):
self._http_api.send(HttpAPI.Request.VOL, "{:.0f}".format(value), self.on_response)
def update_volume(self, vol):
if "error_code" in vol:
return
GLib.idle_add(self._volume_button.set_value, int(vol.get("e2current", "0")))
def on_response(self, resp):
if "error_code" in resp:
return
if self._screenshot_check_button.get_active():
ref = "mode=all" if self._http_api.is_owif else "d="
self._http_api.send(HttpAPI.Request.GRUB, ref, self.update_screenshot)
@run_task
def update_screenshot(self, data):
if "error_code" in data:
return
data = data.get("img_data", None)
if data:
from gi.repository import GdkPixbuf
loader = GdkPixbuf.PixbufLoader.new_with_type("jpeg")
loader.set_size(280, 165)
try:
loader.write(data)
pix = loader.get_pixbuf()
except GLib.Error:
pass # NOP
else:
GLib.idle_add(self._screenshot_image.set_from_pixbuf, pix)
finally:
loader.close()
def on_screenshot_all(self, action, value=None):
self._http_api.send(HttpAPI.Request.GRUB, "mode=all" if self._http_api.is_owif else "d=",
self.on_screenshot)
def on_screenshot_video(self, action, value=None):
self._http_api.send(HttpAPI.Request.GRUB, "mode=video" if self._http_api.is_owif else "v=",
self.on_screenshot)
def on_screenshot_osd(self, action, value=None):
self._http_api.send(HttpAPI.Request.GRUB, "mode=osd" if self._http_api.is_owif else "o=",
self.on_screenshot)
@run_task
def on_screenshot(self, data):
if "error_code" in data:
return
img = data.get("img_data", None)
if img:
is_darwin = self._settings.is_darwin
GLib.idle_add(self._screenshot_button_box.set_sensitive, is_darwin)
path = os.path.expanduser("~/Desktop") if is_darwin else None
try:
import tempfile
import subprocess
with tempfile.NamedTemporaryFile(mode="wb", suffix=".jpg", dir=path, delete=not is_darwin) as tf:
tf.write(img)
cmd = ["open" if is_darwin else "xdg-open", tf.name]
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
finally:
GLib.idle_add(self._screenshot_button_box.set_sensitive, True)
def on_power_action(self, action):
self._http_api.send(HttpAPI.Request.POWER, action, lambda resp: log("Power status changed..."))
def update_signal(self, sig):
self._snr_value_label.set_text(sig.get("e2snrdb", "0 dB").strip())
self._ber_value_label.set_text(str(sig.get("e2ber", None) or "0").strip())
self._agc_value_label.set_text(sig.get("e2acg", "0 %").strip())
# ************************ EPG **************************** #
def on_service_changed(self, ref):
self._app._wait_dialog.show()
self._http_api.send(HttpAPI.Request.EPG, quote(ref), self.update_epg_data)
@run_idle
def update_epg_data(self, epg):
list(map(self._epg_list_box.remove, (r for r in self._epg_list_box)))
list(map(lambda e: self._epg_list_box.add(self.EpgRow(e)), epg.get("event_list", [])))
self._app._wait_dialog.hide()
def on_epg_press(self, list_box, event):
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(list_box) > 0:
row = list_box.get_selected_row()
if row:
self.set_timer_from_event_data(row.event_data)
def on_epg_filter_changed(self, entry):
self._epg_list_box.invalidate_filter()
def epg_filter_function(self, row):
txt = self._epg_filter_entry.get_text().upper()
return any((not txt, txt in row.time_header.upper(), txt in row.title.upper(), txt in row.desc.upper()))
def on_timer_add_from_event(self, action, value=None):
rows = self._epg_list_box.get_selected_rows()
if not rows:
self._app.show_error_dialog("No selected item!")
return
refs = []
for row in rows:
event = row.event_data
ref = "timeraddbyeventid?sRef={}&eventid={}&justplay=0".format(event.get("e2eventservicereference", ""),
event.get("e2eventid", ""))
refs.append(ref)
gen = self.write_timers_list(refs)
GLib.idle_add(lambda: next(gen, False))
def write_timers_list(self, refs):
self._app._wait_dialog.show()
tasks = list(refs)
for ref in refs:
self._http_api.send(HttpAPI.Request.TIMER, ref, lambda x: tasks.pop())
yield True
while tasks:
yield True
self._stack.set_visible_child_name(self.Tool.TIMERS.value)
# *********************** Timers *************************** #
def on_timers_press(self, list_box, event):
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(list_box) > 0:
self.on_timer_edit()
def update_timer_list(self):
self._app._wait_dialog.show()
self._http_api.send(HttpAPI.Request.TIMER_LIST, "", self.update_timers_data)
@run_idle
def update_timers_data(self, timers):
list(map(self._timers_list_box.remove, (r for r in self._timers_list_box)))
list(map(lambda t: self._timers_list_box.add(self.TimerRow(t)), timers.get("timer_list", [])))
self._timer_remove_button.set_visible(len(self._timers_list_box))
self._app._wait_dialog.hide()
def on_timer_add(self, action=None, value=None):
self._timer_action = self.TimerAction.ADD
date = datetime.now()
self.set_begins_date(date)
self.set_ends_date(date)
self._timer_event_id_entry.set_text("")
self._timer_location_switch.set_active(False)
self.set_repetition_flags(0)
self._stack.set_visible_child_name(self.Tool.TIMER.value)
def on_timer_remove(self, action, value=None):
rows = self._timers_list_box.get_selected_rows()
if not rows or show_dialog(DialogType.QUESTION, self._app._main_window) != Gtk.ResponseType.OK:
return
refs = {}
for row in rows:
timer = row.timer
ref = "timerdelete?sRef={}&begin={}&end={}".format(quote(timer.get("e2servicereference", "")),
timer.get("e2timebegin", ""),
timer.get("e2timeend", ""))
refs[ref] = row
self._app._wait_dialog.show("Deleting data...")
gen = self.remove_timers(refs)
GLib.idle_add(lambda: next(gen, False))
def remove_timers(self, refs):
tasks = list(refs)
removed = set()
for ref in refs:
yield from self.remove_timer(ref, removed, tasks)
while tasks:
yield True
list(map(self._timers_list_box.remove, (refs[ref] for ref in refs if ref in removed)))
self._app._wait_dialog.hide()
self._timer_remove_button.set_visible(len(self._timers_list_box))
yield True
def remove_timer(self, ref, removed, tasks=None):
def callback(resp):
if resp.get("e2state", "") == "True":
log(resp.get("e2statetext", ""))
removed.add(ref)
else:
log(resp.get("e2statetext", None) or "Timer deletion error.")
if tasks:
tasks.pop()
self._http_api.send(HttpAPI.Request.TIMER, ref, callback)
yield True
def on_timer_edit(self, action=None, value=None):
row = self._timers_list_box.get_selected_row()
if row:
self._timer_action = self.TimerAction.CHANGE
timer = row.timer
self._current_timer = timer
self._timer_name_entry.set_text(timer.get("e2name", ""))
self._timer_desc_entry.set_text(timer.get("e2description", "") or "")
self._timer_service_entry.set_text(timer.get("e2servicename", "") or "")
self._timer_service_ref_entry.set_text(timer.get("e2servicereference", ""))
self._timer_event_id_entry.set_text(timer.get("e2eit", ""))
self._timer_enabled_switch.set_active((timer.get("e2disabled", "0") == "0"))
self._timer_action_combo_box.set_active_id(timer.get("e2justplay", "0"))
self._timer_after_combo_box.set_active_id(timer.get("e2afterevent", "0"))
self.set_time_data(int(timer.get("e2timebegin", "0")), int(timer.get("e2timeend", "0")))
location = timer.get("e2location", "")
self._timer_location_entry.set_text("" if location == "None" else location)
# Days
self.set_repetition_flags(int(timer.get("e2repeated", "0")))
self._stack.set_visible_child_name(self.Tool.TIMER.value)
def on_timer_save(self, action, value=None):
args = []
t_data = self.get_timer_data()
s_ref = quote(t_data.get("sRef", ""))
if self._timer_action is self.TimerAction.EVENT:
args.append("timeraddbyeventid?sRef={}".format(s_ref))
args.append("eventid={}".format(t_data.get("eit", "0")))
args.append("justplay={}".format(t_data.get("justplay", "")))
args.append("tags={}".format(""))
else:
if self._timer_action is self.TimerAction.ADD:
args.append("timeradd?sRef={}".format(s_ref))
args.append("deleteOldOnSave={}".format(0))
elif self._timer_action is self.TimerAction.CHANGE:
args.append("timerchange?sRef={}".format(s_ref))
args.append("channelOld={}".format(s_ref))
args.append("beginOld={}".format(self._current_timer.get("e2timebegin", "0")))
args.append("endOld={}".format(self._current_timer.get("e2timeend", "0")))
args.append("deleteOldOnSave={}".format(1))
args.append("begin={}".format(t_data.get("begin", "")))
args.append("end={}".format(t_data.get("end", "")))
args.append("name={}".format(quote(t_data.get("name", ""))))
args.append("description={}".format(quote(t_data.get("description", ""))))
args.append("tags={}".format(""))
args.append("eit={}".format("0"))
args.append("disabled={}".format(t_data.get("disabled", "1")))
args.append("justplay={}".format(t_data.get("justplay", "1")))
args.append("afterevent={}".format(t_data.get("afterevent", "0")))
args.append("repeated={}".format(self.get_repetition_flags()))
if self._timer_location_switch.get_active():
args.append("dirname={}".format(self._timer_location_entry.get_text()))
self._http_api.send(HttpAPI.Request.TIMER, "&".join(args), self.timer_add_edit_callback)
@run_idle
def timer_add_edit_callback(self, resp):
if "error_code" in resp:
msg = "Error getting timer status.\n{}".format(resp.get("error_code"))
self._app.show_error_dialog(msg)
log(msg)
return
state = resp.get("e2state", None)
if state == "False":
msg = resp.get("e2statetext", "")
self._app.show_error_dialog(msg)
log(msg)
if state == "True":
log(resp.get("e2statetext", ""))
self._stack.set_visible_child_name(self._last_tool.value)
else:
log("Error getting timer status. No response!")
def on_timer_cancel(self, action, value=None):
self._stack.set_visible_child_name(self._last_tool.value)
def on_timer_begins_set(self, action, value=None):
self.set_begins_date(self.get_begins_date())
def on_timer_ends_set(self, action, value=None):
self.set_ends_date(self.get_ends_date())
def get_begins_date(self):
date = self._timer_begins_calendar.get_date()
return datetime(year=date.year, month=date.month + 1, day=date.day,
hour=int(self._timer_begins_hr_button.get_value()),
minute=int(self._timer_begins_min_button.get_value()))
def set_begins_date(self, date):
hour = date.hour
minute = date.minute
self._timer_begins_hr_button.set_value(hour)
self._timer_begins_min_button.set_value(minute)
self._timer_begins_calendar.select_day(date.day)
self._timer_begins_calendar.select_month(date.month - 1, date.year)
self._timer_begins_entry.set_text("{}-{}-{} {}:{:02d}".format(date.year, date.month, date.day, hour, minute))
def get_ends_date(self):
date = self._timer_ends_calendar.get_date()
return datetime(year=date.year, month=date.month + 1, day=date.day,
hour=int(self._timer_ends_hr_button.get_value()),
minute=int(self._timer_ends_min_button.get_value()))
def set_ends_date(self, date):
hour = date.hour
minute = date.minute
self._timer_ends_hr_button.set_value(hour)
self._timer_ends_min_button.set_value(minute)
self._timer_ends_calendar.select_day(date.day)
self._timer_ends_calendar.select_month(date.month - 1, date.year)
self._timer_ends_entry.set_text("{}-{}-{} {}:{:02d}".format(date.year, date.month, date.day, hour, minute))
def set_timer_from_event_data(self, timer):
self._stack.set_visible_child_name(self.Tool.TIMER.value)
self._timer_action = self.TimerAction.EVENT
self._timer_name_entry.set_text(timer.get("e2eventtitle", ""))
self._timer_desc_entry.set_text(timer.get("e2eventdescription", ""))
self._timer_service_entry.set_text(timer.get("e2eventservicename", ""))
self._timer_service_ref_entry.set_text(timer.get("e2eventservicereference", ""))
self._timer_event_id_entry.set_text(timer.get("e2eventid", ""))
self._timer_action_combo_box.set_active_id("1")
self._timer_after_combo_box.set_active_id("3")
start_time = int(timer.get("e2eventstart", "0"))
self.set_time_data(start_time, start_time + int(timer.get("e2eventduration", "0")))
def set_time_data(self, start_time, end_time):
""" Sets values for time widgets. """
ev_time_start = datetime.fromtimestamp(start_time) or datetime.now()
ev_time_end = datetime.fromtimestamp(end_time) or datetime.now()
self._timer_begins_entry.set_text(ev_time_start.strftime(self._TIME_STR))
self._timer_ends_entry.set_text(ev_time_end.strftime(self._TIME_STR))
self._timer_begins_calendar.select_day(ev_time_start.day)
self._timer_begins_calendar.select_month(ev_time_start.month - 1, ev_time_start.year)
self._timer_ends_calendar.select_day(ev_time_end.day)
self._timer_ends_calendar.select_month(ev_time_end.month - 1, ev_time_end.year)
self._timer_begins_hr_button.set_value(ev_time_start.hour)
self._timer_begins_min_button.set_value(ev_time_start.minute)
self._timer_ends_hr_button.set_value(ev_time_end.hour)
self._timer_ends_min_button.set_value(ev_time_end.minute)
def get_timer_data(self):
""" Returns timer data as a dict. """
return {"sRef": self._timer_service_ref_entry.get_text(),
"begin": int(datetime.strptime(self._timer_begins_entry.get_text(), self._TIME_STR).timestamp()),
"end": int(datetime.strptime(self._timer_ends_entry.get_text(), self._TIME_STR).timestamp()),
"name": self._timer_name_entry.get_text(),
"description": self._timer_desc_entry.get_text(),
"dirname": "",
"eit": self._timer_event_id_entry.get_text(),
"disabled": int(not self._timer_enabled_switch.get_active()),
"justplay": self._timer_action_combo_box.get_active_id(),
"afterevent": self._timer_after_combo_box.get_active_id(),
"repeated": self.get_repetition_flags()}
def get_repetition_flags(self):
""" Returns flags for repetition. """
day_flags = 0
for i, box in enumerate((self._timer_mo_check_button,
self._timer_tu_check_button,
self._timer_we_check_button,
self._timer_th_check_button,
self._timer_fr_check_button,
self._timer_sa_check_button,
self._timer_su_check_button)):
if box.get_active():
day_flags = day_flags | (1 << i)
return day_flags
def set_repetition_flags(self, flags):
for i, box in enumerate((self._timer_mo_check_button,
self._timer_tu_check_button,
self._timer_we_check_button,
self._timer_th_check_button,
self._timer_fr_check_button,
self._timer_sa_check_button,
self._timer_su_check_button)):
box.set_active(flags & 1 == 1)
flags = flags >> 1
# ***************** Drag-and-drop ********************* #
def on_timers_drag_data_received(self, box, context, x, y, data, info, time):
txt = data.get_text()
if txt:
itr_str, sep, source = txt.partition(self._app.DRAG_SEP)
if not source:
return
itrs = itr_str.split(",")
if len(itrs) > 1:
self._app.show_error_dialog("Please, select only one item!")
return
fav_id = None
if source == self._app.FAV_MODEL_NAME:
model = self._app.fav_view.get_model()
fav_id = model.get_value(model.get_iter_from_string(itrs[0]), Column.FAV_ID)
elif source == self._app.SERVICE_MODEL_NAME:
model = self._app.services_view.get_model()
fav_id = model.get_value(model.get_iter_from_string(itrs[0]), Column.SRV_FAV_ID)
service = self._app.current_services.get(fav_id, None)
if service:
if service.service_type == BqServiceType.ALT.name:
msg = "Alternative service.\n\n {}".format(get_message("Not implemented yet!"))
show_dialog(DialogType.ERROR, transient=self._app._main_window, text=msg)
context.finish(False, False, time)
return
self._timer_name_entry.set_text(service.service)
self._timer_service_entry.set_text(service.service)
self._timer_service_ref_entry.set_text(service.picon_id.rstrip(".png").replace("_", ":"))
self.on_timer_add()
context.finish(True, False, time)
# *********************** Recordings *************************** #
def on_recordings_press(self, list_box, event):
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(list_box) > 0:
row = list_box.get_selected_row()
if row:
self._http_api.send(HttpAPI.Request.STREAM_TS,
row.movie.get("e2filename", ""),
self.on_play_recording)
def on_recording_filter_changed(self, entry):
self._recordings_list_box.invalidate_filter()
def recording_filter_function(self, row):
txt = self._recordings_filter_entry.get_text().upper()
return any((not txt, txt in row.service.upper(), txt in row.title.upper(), txt in row.desc.upper()))
def on_recording_remove(self, action, value=None):
""" Removes recordings via FTP. """
if show_dialog(DialogType.QUESTION, self._app._main_window) != Gtk.ResponseType.OK:
return
rows = self._recordings_list_box.get_selected_rows()
if rows:
settings = self._app._settings
with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
ftp.encoding = "utf-8"
for r in rows:
resp = ftp.delete_file(r.file)
if resp.startswith("2"):
GLib.idle_add(self._recordings_list_box.remove, r)
else:
show_dialog(DialogType.ERROR, transient=self._app._main_window, text=resp)
break
def on_recordings_dir_changed(self, box: Gtk.ComboBoxText):
self._http_api.send(HttpAPI.Request.RECORDINGS, quote(box.get_active_id()), self.update_recordings_data)
def update_recordings_list(self):
if not len(self._recordings_dir_box.get_model()):
self._http_api.send(HttpAPI.Request.REC_CURRENT, "", self.update_current_rec_dir)
def update_current_rec_dir(self, current):
cur = current.get("e2location", None)
if cur:
self._recordings_dir_box.append(cur, cur)
self._http_api.send(HttpAPI.Request.REC_DIRS, "", self.update_rec_dirs)
def update_rec_dirs(self, dirs):
for d in dirs.get("rec_dirs", []):
self._recordings_dir_box.append(d, d)
@run_idle
def update_recordings_data(self, recordings):
list(map(self._recordings_list_box.remove, (r for r in self._recordings_list_box)))
list(map(lambda r: self._recordings_list_box.add(self.RecordingsRow(r)), recordings.get("recordings", [])))
def on_play_recording(self, m3u):
url = self._app.get_url_from_m3u(m3u)
if url:
self._app.play(url)

View File

@@ -1,40 +0,0 @@
* {
-GtkDialog-action-area-border: 5em;
}
entry {
min-height: 2em;
}
button {
min-height: 1.5em;
padding: 0.1em;
}
spinbutton {
min-height: 1.5em;
}
toolbutton {
padding: 0.1em;
}
spinner {
padding-left: 1em;
padding-right: 1em;
}
infobar {
min-height: 2em;
}
switch slider {
min-height: 1.5em;
min-width: 1.5em;
}
paned > separator {
background-repeat: no-repeat;
background-position: center;
background-size: 1px 24px;
}

View File

@@ -3,7 +3,7 @@
The MIT License (MIT)
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
@@ -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 macOS. -->
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkAboutDialog" id="about_dialog">
<property name="can_focus">False</property>
@@ -40,18 +40,17 @@ Author: Dmitriy Yefremov
<property name="icon_name">system-help</property>
<property name="type_hint">normal</property>
<property name="program_name">DemonEditor</property>
<property name="version">1.0.0 Alpha-2</property>
<property name="copyright">2018-2020 Dmitriy Yefremov
<property name="version">1.0.10 Beta</property>
<property name="copyright">2018-2021 Dmitriy Yefremov
</property>
<property name="comments" translatable="yes">Enigma2 channel and satellites list editor for MacOS.
(Experimental)</property>
<property name="website">https://github.com/DYefremov/DemonEditor/tree/experimental-mac</property>
<property name="comments" translatable="yes">Enigma2 channel and satellite list editor for GNU/Linux.</property>
<property name="website">https://dyefremov.github.io/DemonEditor/</property>
<property name="license" translatable="yes">Это приложение распространяется без каких-либо гарантий.
Подробнее в &lt;a href="http://opensource.org/licenses/mit-license.php"&gt;The MIT License (MIT)&lt;/a&gt;.</property>
<property name="authors">Dmitriy Yefremov
</property>
<property name="translator_credits" translatable="yes">translator-credits</property>
<property name="artists">Program logo: &lt;a href="http://ihad.tv"&gt; mfgeg&lt;/a&gt;</property>
<property name="artists">Program logo: &lt;a href="http://ihad.tv"&gt;mfgeg&lt;/a&gt;</property>
<property name="logo_icon_name">demon-editor</property>
<property name="wrap_license">True</property>
<property name="license_type">mit-x11</property>
@@ -66,9 +65,7 @@ Author: Dmitriy Yefremov
<child internal-child="action_area">
<object class="GtkButtonBox" id="aboutdialog_action_area">
<property name="can_focus">False</property>
<property name="margin_left">50</property>
<property name="margin_right">50</property>
<property name="layout_style">expand</property>
<property name="layout_style">end</property>
</object>
<packing>
<property name="expand">False</property>
@@ -93,58 +90,32 @@ Author: Dmitriy Yefremov
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="gravity">center</property>
<child type="titlebar">
<placeholder/>
<child type="action">
<object class="GtkButton" id="input_dialog_cancel_button">
<property name="label">gtk-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>
</object>
</child>
<child type="action">
<object class="GtkButton" id="input_dialog_ok_button">
<property name="label">gtk-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>
<accelerator key="Return" signal="activate"/>
</object>
</child>
<child internal-child="vbox">
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">4</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="input_dialog_cancel_button">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="input_dialog_ok_button">
<property name="label" translatable="yes">OK</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
<accelerator key="Return" signal="activate"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="input_entry">
<property name="visible">True</property>
@@ -153,7 +124,7 @@ Author: Dmitriy Yefremov
<property name="margin_right">2</property>
<property name="margin_top">2</property>
<property name="margin_bottom">2</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="secondary_icon_sensitive">False</property>
@@ -161,7 +132,7 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
<property name="position">1</property>
</packing>
</child>
</object>
@@ -171,7 +142,7 @@ Author: Dmitriy Yefremov
<action-widget response="ok">input_dialog_ok_button</action-widget>
</action-widgets>
</object>
<object class="GtkWindow" id="wait_dialog">
<object class="GtkDialog" id="wait_dialog">
<property name="can_focus">False</property>
<property name="resizable">False</property>
<property name="modal">True</property>
@@ -181,42 +152,63 @@ Author: Dmitriy Yefremov
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="decorated">False</property>
<property name="gravity">center</property>
<child type="titlebar">
<child>
<placeholder/>
</child>
<child>
<object class="GtkBox" id="wait_dialog_box">
<property name="width_request">100</property>
<property name="visible">True</property>
<child internal-child="vbox">
<object class="GtkBox" id="wait_dialog_vbox">
<property name="width_request">120</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>
<object class="GtkSpinner" id="spinner">
<property name="width_request">150</property>
<property name="height_request">45</property>
<property name="visible">True</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="dialog-action_area4">
<property name="can_focus">False</property>
<property name="active">True</property>
<property name="opacity">0</property>
<property name="layout_style">end</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="wait_dialog_label">
<object class="GtkBox" id="box4">
<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>
<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>
</object>
<packing>
<property name="expand">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>

View File

@@ -1,7 +1,8 @@
""" Common module for showing dialogs """
import gettext
import locale
from enum import Enum
from functools import lru_cache
from pathlib import Path
from app.commons import run_idle
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN, IS_GNOME_SESSION
@@ -72,12 +73,13 @@ class WaitDialog:
self._dialog.destroy()
def show_dialog(dialog_type: DialogType, transient, text=None, settings=None, action_type=None, file_filter=None):
""" Shows dialogs by name """
def show_dialog(dialog_type, transient, text=None, settings=None, action_type=None, file_filter=None, buttons=None,
title=None, create_dir=False):
""" Shows dialogs by name. """
if dialog_type in (DialogType.INFO, DialogType.ERROR):
return get_message_dialog(transient, dialog_type, Gtk.ButtonsType.OK, text)
elif dialog_type is DialogType.CHOOSER and settings:
return get_file_chooser_dialog(transient, text, settings, action_type, file_filter)
return get_file_chooser_dialog(transient, text, settings, action_type, file_filter, buttons, title, create_dir)
elif dialog_type is DialogType.INPUT:
return get_input_dialog(transient, text)
elif dialog_type is DialogType.QUESTION:
@@ -87,7 +89,7 @@ def show_dialog(dialog_type: DialogType, transient, text=None, settings=None, ac
return get_about_dialog(transient)
def get_chooser_dialog(transient, settings, name, patterns):
def get_chooser_dialog(transient, settings, name, patterns, title=None):
file_filter = Gtk.FileFilter()
file_filter.set_name(name)
for p in patterns:
@@ -97,30 +99,30 @@ def get_chooser_dialog(transient, settings, name, patterns):
transient=transient,
settings=settings,
action_type=Gtk.FileChooserAction.OPEN,
file_filter=file_filter)
file_filter=file_filter,
title=title)
def get_file_chooser_dialog(transient, text, settings, action_type, file_filter):
dialog = Gtk.FileChooserNative()
dialog.set_title(get_message(text) if text else "")
dialog.set_transient_for(transient)
dialog.set_action(action_type if action_type is not None else Gtk.FileChooserAction.SELECT_FOLDER)
dialog.set_create_folders(False)
dialog.set_modal(True)
def get_file_chooser_dialog(transient, text, settings, action_type, file_filter, buttons=None, title=None, dirs=False):
text = get_message(text) if text else ""
action_type = Gtk.FileChooserAction.SELECT_FOLDER if action_type is None else action_type
buttons = buttons or (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
dialog = Gtk.FileChooserDialog(text, transient, action_type, buttons, use_header_bar=IS_GNOME_SESSION)
dialog.set_title(get_message(title) if title else "")
dialog.set_create_folders(dirs)
if file_filter is not None:
dialog.add_filter(file_filter)
path = settings.data_local_path
dialog.set_current_folder(path)
dialog.set_current_folder(settings.data_local_path)
response = dialog.run()
if response == Gtk.ResponseType.ACCEPT:
if dialog.get_filename():
path = dialog.get_filename()
if action_type is not Gtk.FileChooserAction.OPEN:
path = path + "/"
response = path
if response not in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
path = Path(dialog.get_filename() or dialog.get_current_folder())
if path.is_dir():
response = "{}/".format(path.resolve())
elif path.is_file():
response = str(path.resolve())
dialog.destroy()
return response
@@ -174,7 +176,7 @@ def get_dialog_from_xml(dialog_type, transient, use_header=0, title=""):
def get_message(message):
""" returns translated message """
return gettext.dgettext(TEXT_DOMAIN, message)
return locale.dgettext(TEXT_DOMAIN, message)
@lru_cache(maxsize=5)
@@ -183,5 +185,26 @@ def get_dialogs_string(path):
return "".join(f)
def get_builder(path, handlers=None, use_str=False, objects=None):
""" Creates and returns a Gtk.Builder instance. """
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
if use_str:
if objects:
builder.add_objects_from_string(get_dialogs_string(path).format(use_header=IS_GNOME_SESSION), objects)
else:
builder.add_from_string(get_dialogs_string(path).format(use_header=IS_GNOME_SESSION))
else:
if objects:
builder.add_objects_from_file(path, objects)
else:
builder.add_from_file(path)
builder.connect_signals(handlers or {})
return builder
if __name__ == "__main__":
pass

673
app/ui/download_dialog.glade Executable file → Normal file
View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1
<!-- Generated with glade 3.22.2
The MIT License (MIT)
Copyright (c) 2018-2020 Dmitriy Yefremov
Copyright (c) 2018 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,25 +30,12 @@ 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 macOS. -->
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
<!-- 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">500</property>
<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>
@@ -56,21 +43,227 @@ Author: Dmitriy Yefremov
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="gravity">center</property>
<child>
<placeholder/>
<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>
</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">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_top">10</property>
<property name="margin_bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="label10">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="all_radio_button">
<property name="label" translatable="yes">All</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">satellites_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="bouquets_radio_button">
<property name="label" translatable="yes">Bouquets</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">satellites_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="satellites_radio_button">
<property name="label" translatable="yes">Satellites</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">all_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="webtv_radio_button">
<property name="label" translatable="yes">WebTV</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">all_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">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">10</property>
<property name="margin_right">10</property>
<property name="margin_top">10</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property>
@@ -78,8 +271,8 @@ Author: Dmitriy Yefremov
<object class="GtkBox" id="main_settings_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
@@ -231,425 +424,6 @@ Author: Dmitriy Yefremov
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="settings_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkGrid" id="settings_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="row_spacing">2</property>
<property name="column_spacing">2</property>
<child>
<object class="GtkLabel" id="login_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Login:</property>
<property name="xalign">0.10000000149011612</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="login_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="text">root</property>
<property name="caps_lock_warning">False</property>
<property name="primary_icon_name">avatar-default-symbolic</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="password_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Password:</property>
<property name="xalign">0.10000000149011612</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="password_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="visibility">False</property>
<property name="invisible_char">●</property>
<property name="text">root</property>
<property name="primary_icon_name">emblem-readonly</property>
<property name="input_purpose">password</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="port_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Port:</property>
<property name="xalign">0.10000000149011612</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="port_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="width_chars">8</property>
<property name="max_width_chars">8</property>
<property name="text" translatable="yes">21</property>
<property name="caps_lock_warning">False</property>
<property name="primary_icon_name">network-workgroup-symbolic</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="timeout_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="width_chars">8</property>
<property name="max_width_chars">8</property>
<property name="caps_lock_warning">False</property>
<property name="primary_icon_name">alarm-symbolic</property>
<property name="input_purpose">digits</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="timeout_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Timeout:</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">0</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkBox" id="settings_buttons_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkRadioButton" id="ftp_radio_button">
<property name="label" translatable="yes">FTP</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">False</property>
<property name="group">telnet_radio_button</property>
<signal name="toggled" handler="on_settings_button" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="http_radio_button">
<property name="label" translatable="yes">HTTP</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">False</property>
<property name="group">telnet_radio_button</property>
<signal name="toggled" handler="on_settings_button" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="telnet_radio_button">
<property name="label" translatable="yes">Telnet</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">False</property>
<property name="group">ftp_radio_button</property>
<signal name="toggled" handler="on_settings_button" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<style>
<class name="group"/>
</style>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkFrame">
<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_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="selection_data_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_top">10</property>
<property name="margin_bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="label10">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="all_radio_button">
<property name="label" translatable="yes">All</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">satellites_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="bouquets_radio_button">
<property name="label" translatable="yes">Bouquets</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">satellites_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="satellites_radio_button">
<property name="label" translatable="yes">Satellites</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">all_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="webtv_radio_button">
<property name="label" translatable="yes">WebTV</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">all_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Profile:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="profile_combo_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="focus_on_click">False</property>
<property name="active">0</property>
<property name="has_frame">False</property>
<signal name="changed" handler="on_profile_changed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="options_button">
<property name="label" translatable="yes">Options</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Options</property>
<property name="valign">center</property>
<signal name="clicked" handler="on_settings" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkButtonBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="margin_left">20</property>
<property name="margin_right">20</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="homogeneous">True</property>
<property name="layout_style">expand</property>
<child>
<object class="GtkButton" id="receive_button">
<property name="label" translatable="yes">Receive</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Receive</property>
<property name="valign">center</property>
<property name="image">download_image</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_receive" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="send_button">
<property name="label" translatable="yes">Send</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Send</property>
<property name="valign">center</property>
<property name="image">send_image</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_send" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">6</property>
</packing>
</child>
<child>
<object class="GtkExpander" id="expander">
<property name="visible">True</property>
@@ -686,7 +460,7 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">7</property>
<property name="position">4</property>
</packing>
</child>
<child>
@@ -742,17 +516,10 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">8</property>
<property name="position">5</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkSizeGroup" id="settings_size_group">
<widgets>
<widget name="ftp_radio_button"/>
<widget name="http_radio_button"/>
<widget name="telnet_radio_button"/>
</widgets>
</object>
</interface>

View File

@@ -8,7 +8,7 @@ from app.settings import SettingsType
from app.ui.backup import backup_data, restore_data
from app.ui.main_helper import append_text_to_tview
from app.ui.settings_dialog import show_settings_dialog
from .dialogs import show_dialog, DialogType, get_message
from .dialogs import show_dialog, DialogType, get_message, get_builder
from .uicommons import Gtk, UI_RESOURCES_PATH
@@ -21,18 +21,14 @@ class DownloadDialog:
handlers = {"on_receive": self.on_receive,
"on_send": self.on_send,
"on_settings_button": self.on_settings_button,
"on_settings": self.on_settings,
"on_profile_changed": self.on_profile_changed,
"on_use_http_state_set": self.on_use_http_state_set,
"on_remove_unused_bouquets_toggled": self.on_remove_unused_bouquets_toggled,
"on_info_bar_close": self.on_info_bar_close}
builder = Gtk.Builder()
builder.add_from_file(UI_RESOURCES_PATH + "download_dialog.glade")
builder.connect_signals(handlers)
builder = get_builder(UI_RESOURCES_PATH + "download_dialog.glade", handlers)
self._current_property = "FTP"
self._dialog_window = builder.get_object("download_dialog_window")
self._dialog_window.set_transient_for(transient)
self._info_bar = builder.get_object("info_bar")
@@ -46,12 +42,6 @@ class DownloadDialog:
self._bouquets_radio_button = builder.get_object("bouquets_radio_button")
self._satellites_radio_button = builder.get_object("satellites_radio_button")
self._webtv_radio_button = builder.get_object("webtv_radio_button")
self._login_entry = builder.get_object("login_entry")
self._password_entry = builder.get_object("password_entry")
self._host_entry = builder.get_object("host_entry")
self._port_entry = builder.get_object("port_entry")
self._timeout_entry = builder.get_object("timeout_entry")
self._settings_buttons_box = builder.get_object("settings_buttons_box")
self._use_http_switch = builder.get_object("use_http_switch")
self._http_radio_button = builder.get_object("http_radio_button")
self._use_http_box = builder.get_object("use_http_box")
@@ -71,7 +61,6 @@ class DownloadDialog:
self._data_path_entry.set_text(self._settings.data_local_path)
is_enigma = self._s_type is SettingsType.ENIGMA_2
self._webtv_radio_button.set_visible(not is_enigma)
self._http_radio_button.set_visible(is_enigma)
self._use_http_box.set_visible(is_enigma)
self._use_http_switch.set_active(is_enigma and self._settings.use_http)
self._remove_unused_check_button.set_active(self._settings.remove_unused_bouquets)
@@ -104,26 +93,6 @@ class DownloadDialog:
def destroy(self):
self._dialog_window.destroy()
def on_settings_button(self, button):
if button.get_active():
label = button.get_label()
if label == "Telnet":
self._login_entry.set_text(self._settings.telnet_user)
self._password_entry.set_text(self._settings.telnet_password)
self._port_entry.set_text(self._settings.telnet_port)
self._timeout_entry.set_text(str(self._settings.telnet_timeout))
elif label == "HTTP":
self._login_entry.set_text(self._settings.http_user)
self._password_entry.set_text(self._settings.http_password)
self._port_entry.set_text(self._settings.http_port)
self._timeout_entry.set_text(str(self._settings.http_timeout))
elif label == "FTP":
self._login_entry.set_text(self._settings.user)
self._password_entry.set_text(self._settings.password)
self._port_entry.set_text(self._settings.port)
self._timeout_entry.set_text("")
self._current_property = label
def on_settings(self, item):
response = show_settings_dialog(self._dialog_window, self._settings)
if response != Gtk.ResponseType.CANCEL:
@@ -132,11 +101,6 @@ class DownloadDialog:
gen = self._update_settings_callback()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
for button in self._settings_buttons_box.get_children():
if button.get_active():
self.on_settings_button(button)
break
def on_profile_changed(self, box):
active = box.get_active_text()
if active in self._settings.profiles:

View File

@@ -33,18 +33,6 @@ 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="apply_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-save-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="auto_config_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="GtkListStore" id="bouquet_list_store">
<columns>
<!-- column-name num -->
@@ -87,16 +75,10 @@ Author: Dmitriy Yefremov
<property name="image">copy_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_copy_ref" swapped="no"/>
<accelerator key="c" signal="activate" modifiers="Primary"/>
<accelerator key="c" signal="activate" modifiers="GDK_CONTROL_MASK"/>
</object>
</child>
</object>
<object class="GtkImage" id="filter_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-find-replace-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="insert_link_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
@@ -118,7 +100,7 @@ Author: Dmitriy Yefremov
<property name="image">insert_link_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_assign_ref" swapped="no"/>
<accelerator key="v" signal="activate" modifiers="Primary"/>
<accelerator key="v" signal="activate" modifiers="GDK_CONTROL_MASK"/>
</object>
</child>
<child>
@@ -148,12 +130,6 @@ Author: Dmitriy Yefremov
</object>
</child>
</object>
<object class="GtkImage" id="save_to_xml_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-save-as-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkListStore" id="services_list_store">
<columns>
<!-- column-name service -->
@@ -336,7 +312,7 @@ Author: Dmitriy Yefremov
<object class="GtkEntry" id="url_to_xml_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">network-transmit-receive-symbolic</property>
<property name="primary_icon_stock">gtk-connect</property>
<property name="primary_icon_activatable">False</property>
</object>
<packing>
@@ -436,8 +412,8 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="text" translatable="yes">/data/epg/</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="secondary_icon_name">folder-open-symbolic</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="secondary_icon_stock">gtk-directory</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_tooltip_text" translatable="yes">Select</property>
<signal name="icon-press" handler="on_field_icon_press" swapped="no"/>
@@ -466,7 +442,7 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="text" translatable="yes">/etc/enigma2/</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="primary_icon_activatable">False</property>
</object>
<packing>
@@ -583,104 +559,58 @@ Author: Dmitriy Yefremov
</object>
</child>
</object>
<object class="GtkImage" id="update_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">emblem-synchronizing-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkWindow" id="epg_dialog_window">
<property name="width_request">480</property>
<property name="height_request">320</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">EPG</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="default_height">240</property>
<property name="destroy_with_parent">True</property>
<property name="skip_taskbar_hint">True</property>
<property name="icon_name">gtk-index</property>
<property name="skip_pager_hint">True</property>
<property name="gravity">center</property>
<signal name="check-resize" handler="on_resize" swapped="no"/>
<signal name="delete-event" handler="on_close_dialog" swapped="no"/>
<child>
<placeholder/>
</child>
<child>
<object class="GtkBox" id="main_box">
<child type="titlebar">
<object class="GtkHeaderBar" id="header_bar">
<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="title" translatable="yes">EPG</property>
<property name="subtitle" translatable="yes">List configuration</property>
<property name="spacing">2</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkBox" id="main_actions_box">
<object class="GtkBox" id="left_header_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">15</property>
<property name="margin_right">15</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="spacing">10</property>
<property name="spacing">2</property>
<child>
<object class="GtkButtonBox" id="left_action_box">
<object class="GtkButton" id="apply_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Apply</property>
<signal name="clicked" handler="on_apply" swapped="no"/>
<child>
<object class="GtkImage" id="apply_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-apply</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="GtkSeparator">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="layout_style">expand</property>
<child>
<object class="GtkButton" id="apply_button">
<property name="label" translatable="yes">Apply</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Apply</property>
<property name="image">apply_image</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_apply" 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="update_button">
<property name="label" translatable="yes">Update</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Update</property>
<property name="image">update_image</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_update" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkToggleButton" id="filter_button">
<property name="label" translatable="yes">Filter</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Filter</property>
<property name="image">filter_image</property>
<property name="always_show_image">True</property>
<signal name="toggled" handler="on_filter_toggled" swapped="no"/>
<accelerator key="f" signal="clicked" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/>
</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>
@@ -688,11 +618,20 @@ Author: Dmitriy Yefremov
<property name="position">1</property>
</packing>
</child>
<child type="center">
<object class="GtkLabel" id="list_label">
<child>
<object class="GtkButton" id="update_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">List configuration</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Update</property>
<signal name="clicked" handler="on_update" swapped="no"/>
<child>
<object class="GtkImage" id="update_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-refresh</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -701,106 +640,111 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkButtonBox" id="right_action_box">
<object class="GtkToggleButton" id="filter_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="layout_style">expand</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Filter</property>
<signal name="toggled" handler="on_filter_toggled" swapped="no"/>
<child>
<object class="GtkButton" id="auto_config_button">
<property name="label" translatable="yes">Auto</property>
<object class="GtkImage" id="filter_button_image">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Auto configuration by service names.</property>
<property name="image">auto_config_image</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_auto_configuration" swapped="no"/>
<property name="can_focus">False</property>
<property name="stock">gtk-spell-check</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<accelerator key="f" signal="clicked" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="right_header_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkButton" id="auto_config_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Auto configuration by service names.</property>
<signal name="clicked" handler="on_auto_configuration" swapped="no"/>
<child>
<object class="GtkButton" id="save_to_xml_button">
<property name="label" translatable="yes">Save</property>
<object class="GtkImage" id="auto_config_button_image">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Save list to xml.</property>
<property name="image">save_to_xml_image</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_save_to_xml" swapped="no"/>
<property name="can_focus">False</property>
<property name="stock">gtk-find-and-replace</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkMenuButton" id="options_menu_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="direction">none</property>
<property name="popover">options_popover</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkImage" id="options_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Options</property>
<property name="icon_name">applications-system-symbolic</property>
<property name="icon_size">1</property>
</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>
<property name="label" translatable="yes">Options</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</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="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="save_to_xml_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Save list to xml.</property>
<signal name="clicked" handler="on_save_to_xml" swapped="no"/>
<child>
<object class="GtkImage" id="save_to_xml_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-save-as</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="options_menu_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="direction">none</property>
<property name="popover">options_popover</property>
<child>
<object class="GtkImage" id="options_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Options</property>
<property name="stock">gtk-properties</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
<property name="pack_type">end</property>
<property name="position">1</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="GtkSearchBar" id="filter_bar">
<property name="visible">True</property>
@@ -809,7 +753,7 @@ Author: Dmitriy Yefremov
<object class="GtkSearchEntry" id="filter_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">edit-find-replace-symbolic</property>
<property name="primary_icon_name">tools-check-spelling</property>
<property name="primary_icon_activatable">False</property>
<property name="primary_icon_sensitive">False</property>
<signal name="search-changed" handler="on_filter_changed" swapped="no"/>
@@ -918,14 +862,13 @@ Author: Dmitriy Yefremov
<property name="height_request">24</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_left">2</property>
<property name="spacing">2</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-properties-symbolic</property>
<property name="icon_size">1</property>
<property name="stock">gtk-properties</property>
</object>
<packing>
<property name="expand">False</property>
@@ -947,7 +890,6 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="source_info_box">
<property name="height_request">26</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
@@ -1201,17 +1143,16 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="bouquet_info_bar_box">
<property name="height_request">26</property>
<property name="height_request">24</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_left">2</property>
<property name="spacing">2</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-properties-symbolic</property>
<property name="icon_size">1</property>
<property name="stock">gtk-properties</property>
</object>
<packing>
<property name="expand">False</property>
@@ -1248,9 +1189,7 @@ Author: Dmitriy Yefremov
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">2</property>
<property name="stock">gtk-index</property>
<property name="icon_size">1</property>
</object>
<packing>
<property name="expand">False</property>

View File

@@ -9,13 +9,13 @@ from urllib.error import HTTPError, URLError
from gi.repository import GLib
from app.commons import run_idle
from app.commons import run_idle, run_task
from app.connections import download_data, DownloadType
from app.eparser.ecommons import BouquetService, BqServiceType
from app.tools.epg import EPG, ChannelsParser
from app.ui.dialogs import get_message, show_dialog, DialogType
from app.ui.dialogs import get_message, show_dialog, DialogType, get_builder
from .main_helper import on_popup_menu, update_entry_data
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, Column, EPG_ICON, KeyboardKey, MOD_MASK
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column, EPG_ICON, KeyboardKey
class RefsSource(Enum):
@@ -67,10 +67,7 @@ class EpgDialog:
self._show_tooltips = True
self._download_xml_is_active = False
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_from_file(UI_RESOURCES_PATH + "epg_dialog.glade")
builder.connect_signals(handlers)
builder = get_builder(UI_RESOURCES_PATH + "epg_dialog.glade", handlers)
self._dialog = builder.get_object("epg_dialog_window")
self._dialog.set_transient_for(transient)
@@ -279,7 +276,7 @@ class EpgDialog:
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
if ctrl and key is KeyboardKey.C:
self.on_copy_ref()
@@ -539,6 +536,7 @@ class EpgDialog:
# ***************** Downloads *********************#
@run_task
def download_epg_from_stb(self):
""" Download the epg.dat file via ftp from the receiver. """
download_data(settings=self._settings, download_type=DownloadType.EPG, callback=print)

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

@@ -0,0 +1,657 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2
The MIT License (MIT)
Copyright (c) 2018-2020 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface domain="demon-editor">
<requires lib="gtk+" version="3.16"/>
<!-- interface-css-provider-path style.css -->
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellite list editor for GNU/Linux. -->
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkListStore" id="bookmarks_list_store">
<columns>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name url -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkImage" id="file_create_folder_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">folder-new</property>
</object>
<object class="GtkListStore" id="file_list_store">
<columns>
<!-- column-name icon -->
<column type="GdkPixbuf"/>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name size -->
<column type="gchararray"/>
<!-- column-name date -->
<column type="gchararray"/>
<!-- column-name type -->
<column type="gchararray"/>
<!-- column-name extra -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkImage" id="ftp_create_folder_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">folder-new</property>
</object>
<object class="GtkListStore" id="ftp_list_store">
<columns>
<!-- column-name icon -->
<column type="GdkPixbuf"/>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name size -->
<column type="gchararray"/>
<!-- column-name date -->
<column type="gchararray"/>
<!-- column-name attr -->
<column type="gchararray"/>
<!-- column-name extra -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkFrame" id="main_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="margin_top">1</property>
<property name="margin_bottom">1</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkBox" id="main_ftp_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkBox" id="ftp_button_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="spacing">2</property>
<child>
<object class="GtkButton" id="connect_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Connect</property>
<signal name="clicked" handler="on_connect" swapped="no"/>
<child>
<object class="GtkImage" id="connect_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-connect</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="disconnect_button">
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Disconnect</property>
<signal name="clicked" handler="on_disconnect" swapped="no"/>
<child>
<object class="GtkImage" id="disconnect_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-disconnect</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="bookmark_button">
<property name="can_focus">False</property>
<property name="model">bookmarks_list_store</property>
<property name="id_column">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkPaned" id="paned">
<property name="width_request">320</property>
<property name="height_request">240</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="wide_handle">True</property>
<child>
<object class="GtkBox" id="ftp_bpx">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkBox" id="ftp_info_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="ftp_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">FTP:</property>
<attributes>
<attribute name="weight" value="semibold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ftp_info_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="ellipsize">end</property>
<property name="max_width_chars">75</property>
<property name="yalign">1</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="ftp_view_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="min_content_height">100</property>
<child>
<object class="GtkTreeView" id="ftp_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">ftp_list_store</property>
<property name="search_column">1</property>
<property name="rubber_banding">True</property>
<signal name="button-press-event" handler="on_view_popup_menu" object="ftp_popup_menu" swapped="no"/>
<signal name="button-press-event" handler="on_view_press" swapped="no"/>
<signal name="button-release-event" handler="on_view_release" swapped="no"/>
<signal name="drag-begin" handler="on_view_drag_begin" after="yes" swapped="no"/>
<signal name="drag-data-get" handler="on_ftp_drag_data_get" swapped="no"/>
<signal name="drag-data-received" handler="on_ftp_drag_data_received" swapped="no"/>
<signal name="drag-end" handler="on_view_drag_end" swapped="no"/>
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
<signal name="row-activated" handler="on_ftp_row_activated" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="ftp_selection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="ftp_name_column">
<property name="resizable">True</property>
<property name="min_width">100</property>
<property name="title" translatable="yes">Name</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<property name="sort_column_id">1</property>
<child>
<object class="GtkCellRendererPixbuf" id="ftp_icon_column_renderer">
<property name="xalign">0.019999999552965164</property>
</object>
<attributes>
<attribute name="pixbuf">0</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererText" id="ftp_name_column_renderer">
<property name="xalign">0.019999999552965164</property>
<property name="ellipsize">end</property>
<signal name="edited" handler="on_ftp_edited" swapped="no"/>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="ftp_size_column">
<property name="sizing">fixed</property>
<property name="min_width">75</property>
<property name="title" translatable="yes">Size</property>
<property name="alignment">0.5</property>
<property name="sort_column_id">2</property>
<child>
<object class="GtkCellRendererText" id="ftp_size_column_renderer">
<property name="xalign">0.94999998807907104</property>
</object>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="ftp_date_column">
<property name="min_width">75</property>
<property name="title" translatable="yes">Date</property>
<property name="alignment">0.5</property>
<property name="sort_column_id">3</property>
<child>
<object class="GtkCellRendererText" id="ftp_date_column_renderer"/>
<attributes>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="ftp_attr_column">
<property name="sizing">fixed</property>
<property name="min_width">85</property>
<property name="title" translatable="yes">Attr.</property>
<property name="alignment">0.5</property>
<property name="sort_column_id">4</property>
<child>
<object class="GtkCellRendererText" id="ftp_attr_column_renderer">
<property name="xalign">0.50999999046325684</property>
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">4</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="ftp_extra_column">
<property name="visible">False</property>
<property name="title" translatable="yes">Extra</property>
<child>
<object class="GtkCellRendererText" id="ftp_extra_column_renderer"/>
<attributes>
<attribute name="text">5</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<object class="GtkBox" id="file_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkBox" id="pc_info_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="pc_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="margin_left">10</property>
<property name="label" translatable="yes">PC:</property>
<attributes>
<attribute name="weight" value="semibold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="pc_info_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="ellipsize">end</property>
<property name="max_width_chars">75</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="file_view_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="min_content_height">100</property>
<child>
<object class="GtkTreeView" id="file_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">file_list_store</property>
<property name="search_column">1</property>
<property name="rubber_banding">True</property>
<signal name="button-press-event" handler="on_view_popup_menu" object="file_popup_menu" swapped="no"/>
<signal name="button-press-event" handler="on_view_press" swapped="no"/>
<signal name="button-release-event" handler="on_view_release" swapped="no"/>
<signal name="drag-begin" handler="on_view_drag_begin" after="yes" swapped="no"/>
<signal name="drag-data-get" handler="on_file_drag_data_get" swapped="no"/>
<signal name="drag-data-received" handler="on_file_drag_data_received" swapped="no"/>
<signal name="drag-end" handler="on_view_drag_end" swapped="no"/>
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
<signal name="row-activated" handler="on_file_row_activated" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="file_selection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="file_name_column">
<property name="resizable">True</property>
<property name="min_width">100</property>
<property name="title" translatable="yes">Name</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<property name="sort_column_id">1</property>
<child>
<object class="GtkCellRendererPixbuf" id="file_icon_column_renderer">
<property name="xalign">0.20000000298023224</property>
</object>
<attributes>
<attribute name="pixbuf">0</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererText" id="file_name_column_renderer">
<property name="ellipsize">end</property>
<signal name="edited" handler="on_file_edited" swapped="no"/>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="file_size_column">
<property name="sizing">fixed</property>
<property name="min_width">75</property>
<property name="title" translatable="yes">Size</property>
<property name="alignment">0.5</property>
<property name="sort_column_id">2</property>
<child>
<object class="GtkCellRendererText" id="file_size_column_renderer">
<property name="xalign">0.94999998807907104</property>
</object>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="file_date_column">
<property name="min_width">75</property>
<property name="title" translatable="yes">Date</property>
<property name="alignment">0.5</property>
<property name="sort_column_id">3</property>
<child>
<object class="GtkCellRendererText" id="file_date_column_renderer"/>
<attributes>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="file_type_column">
<property name="visible">False</property>
<property name="sizing">fixed</property>
<property name="min_width">50</property>
<property name="title" translatable="yes">Path</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="file_path_column_renderer"/>
<attributes>
<attribute name="text">4</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="file_extra_column">
<property name="visible">False</property>
<property name="title" translatable="yes">Extra</property>
<child>
<object class="GtkCellRendererText" id="file_extra_column_renderer"/>
<attributes>
<attribute name="text">5</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<object class="GtkImage" id="remove_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">list-remove</property>
</object>
<object class="GtkImage" id="remove_image_2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">list-remove</property>
</object>
<object class="GtkImage" id="rename_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">gtk-edit</property>
</object>
<object class="GtkMenu" id="ftp_popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="ftp_create_folder_menu_item">
<property name="label" translatable="yes">Create folder</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">ftp_create_folder_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_ftp_create_folder" object="ftp_name_column_renderer" swapped="no"/>
<accelerator key="F7" signal="activate"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="ftp_edit_menu_item">
<property name="label" translatable="yes">Edit</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">rename_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_ftp_edit" object="ftp_name_column_renderer" swapped="no"/>
<accelerator key="r" signal="activate" modifiers="Primary"/>
<accelerator key="F2" signal="activate"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="ftp_remove_menu_item">
<property name="label" translatable="yes">Remove</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">remove_image_2</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_ftp_remove" swapped="no"/>
<accelerator key="Delete" signal="activate"/>
</object>
</child>
</object>
<object class="GtkImage" id="rename_image_2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">gtk-edit</property>
</object>
<object class="GtkMenu" id="file_popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="file_create_folder_menu_item">
<property name="label" translatable="yes">Create folder</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">file_create_folder_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_file_create_folder" object="file_name_column_renderer" swapped="no"/>
<accelerator key="F7" signal="activate"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="file_edit_menu_item">
<property name="label" translatable="yes">Edit</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">rename_image_2</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_file_edit" object="file_name_column_renderer" swapped="no"/>
<accelerator key="r" signal="activate" modifiers="GDK_CONTROL_MASK"/>
<accelerator key="F2" signal="activate"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="file_remove_menu_item">
<property name="label" translatable="yes">Remove</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">remove_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_file_remove" swapped="no"/>
<accelerator key="Delete" signal="activate"/>
</object>
</child>
</object>
</interface>

572
app/ui/ftp.py Normal file
View File

@@ -0,0 +1,572 @@
""" Simple FTP client module. """
import subprocess
from collections import namedtuple
from datetime import datetime
from enum import IntEnum
from ftplib import all_errors
from pathlib import Path
from shutil import rmtree
from urllib.parse import urlparse, unquote
from gi.repository import GLib
from app.commons import log, run_task, run_idle
from app.connections import UtfFTP
from app.ui.dialogs import show_dialog, DialogType, get_builder
from app.ui.main_helper import on_popup_menu
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK
File = namedtuple("File", ["icon", "name", "size", "date", "attr", "extra"])
class FtpClientBox(Gtk.HBox):
""" Simple FTP client base class. """
ROOT = ".."
FOLDER = "<Folder>"
LINK = "<Link>"
MAX_SIZE = 10485760 # 10 MB file limit
URI_SEP = "::::"
class Column(IntEnum):
ICON = 0
NAME = 1
SIZE = 2
DATE = 3
ATTR = 4
EXTRA = 5
def __init__(self, app, settings, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_spacing(2)
self.set_orientation(Gtk.Orientation.VERTICAL)
self._app = app
self._settings = settings
self._ftp = None
self._select_enabled = True
handlers = {"on_connect": self.on_connect,
"on_disconnect": self.on_disconnect,
"on_ftp_row_activated": self.on_ftp_row_activated,
"on_file_row_activated": self.on_file_row_activated,
"on_ftp_edit": self.on_ftp_edit,
"on_ftp_edited": self.on_ftp_edited,
"on_file_edit": self.on_file_edit,
"on_file_edited": self.on_file_edited,
"on_file_remove": self.on_file_remove,
"on_ftp_remove": self.on_ftp_file_remove,
"on_file_create_folder": self.on_file_create_folder,
"on_ftp_create_folder": self.on_ftp_create_folder,
"on_view_drag_begin": self.on_view_drag_begin,
"on_ftp_drag_data_get": self.on_ftp_drag_data_get,
"on_ftp_drag_data_received": self.on_ftp_drag_data_received,
"on_file_drag_data_get": self.on_file_drag_data_get,
"on_file_drag_data_received": self.on_file_drag_data_received,
"on_view_drag_end": self.on_view_drag_end,
"on_view_popup_menu": on_popup_menu,
"on_view_key_press": self.on_view_key_press,
"on_view_press": self.on_view_press,
"on_view_release": self.on_view_release}
builder = get_builder(UI_RESOURCES_PATH + "ftp.glade", handlers)
self.add(builder.get_object("main_frame"))
self._ftp_info_label = builder.get_object("ftp_info_label")
self._ftp_view = builder.get_object("ftp_view")
self._ftp_model = builder.get_object("ftp_list_store")
self._ftp_name_renderer = builder.get_object("ftp_name_column_renderer")
self._file_view = builder.get_object("file_view")
self._file_model = builder.get_object("file_list_store")
self._file_name_renderer = builder.get_object("file_name_column_renderer")
# Buttons
self._connect_button = builder.get_object("connect_button")
disconnect_button = builder.get_object("disconnect_button")
disconnect_button.bind_property("visible", builder.get_object("ftp_create_folder_menu_item"), "sensitive")
disconnect_button.bind_property("visible", builder.get_object("ftp_edit_menu_item"), "sensitive")
disconnect_button.bind_property("visible", builder.get_object("ftp_remove_menu_item"), "sensitive")
self._connect_button.bind_property("visible", builder.get_object("disconnect_button"), "visible", 4)
# Force Ctrl
self._ftp_view.connect("key-press-event", self._app.force_ctrl)
self._file_view.connect("key-press-event", self._app.force_ctrl)
# Icons
theme = Gtk.IconTheme.get_default()
folder_icon = "folder-symbolic" if settings.is_darwin else "folder"
self._folder_icon = theme.load_icon(folder_icon, 16, 0) if theme.lookup_icon(folder_icon, 16, 0) else None
self._link_icon = theme.load_icon("emblem-symbolic-link", 16, 0) if theme.lookup_icon("emblem-symbolic-link",
16, 0) else None
# Initialization
self.init_drag_and_drop()
self.init_ftp()
self.init_file_data()
self.show()
@run_task
def init_ftp(self):
GLib.idle_add(self._ftp_model.clear)
try:
if self._ftp:
self._ftp.close()
self._ftp = UtfFTP(host=self._settings.host, user=self._settings.user, passwd=self._settings.password)
self._ftp.encoding = "utf-8"
self.update_ftp_info(self._ftp.getwelcome())
except all_errors as e:
self.update_ftp_info(str(e))
self.on_disconnect()
else:
self.init_ftp_data()
@run_task
def init_ftp_data(self, path=None):
if not self._ftp:
return
if path:
try:
self._ftp.cwd(path)
except all_errors as e:
self.update_ftp_info(str(e))
files = []
try:
self._ftp.dir(files.append)
except all_errors as e:
log(e)
self.update_ftp_info(str(e))
self.on_disconnect()
else:
self.append_ftp_data(files)
GLib.idle_add(self._connect_button.set_visible, False)
@run_task
def init_file_data(self, path=None):
self.append_file_data(Path(path if path else self._settings.data_local_path))
@run_idle
def append_file_data(self, path: Path):
self._file_model.clear()
self._file_model.append(File(None, self.ROOT, None, None, str(path), "0"))
try:
dirs = [p for p in path.iterdir()]
except OSError as e:
log(e)
else:
for p in dirs:
is_dir = p.is_dir()
st = p.stat()
size = str(st.st_size)
date = datetime.fromtimestamp(st.st_mtime).strftime("%d-%m-%y %H:%M")
icon = None
if is_dir:
r_size = self.FOLDER
icon = self._folder_icon
elif p.is_symlink():
r_size = self.LINK
icon = self._link_icon
else:
r_size = self.get_size_from_bytes(size)
self._file_model.append(File(icon, p.name, r_size, date, str(p.resolve()), size))
@run_idle
def append_ftp_data(self, files):
self._ftp_model.clear()
self._ftp_model.append(File(None, self.ROOT, None, None, self._ftp.pwd(), "0"))
for f in files:
f_data = f.split()
f_type = f_data[0][0]
is_dir = f_type == "d"
is_link = f_type == "l"
size = f_data[4]
icon = None
if is_dir:
r_size = self.FOLDER
icon = self._folder_icon
elif is_link:
r_size = self.LINK
icon = self._link_icon
else:
r_size = self.get_size_from_bytes(size)
date = "{}, {} {}".format(f_data[5], f_data[6], f_data[7])
self._ftp_model.append(File(icon, " ".join(f_data[8:]), r_size, date, f_data[0], size))
def on_connect(self, item=None):
self.init_ftp()
def on_disconnect(self, item=None):
if self._ftp:
self._ftp.close()
self._connect_button.set_visible(True)
GLib.idle_add(self._ftp_model.clear)
def on_ftp_row_activated(self, view, path, column):
row = self._ftp_model[path][:]
f_path = row[self.Column.NAME]
size = row[self.Column.SIZE]
if size == self.FOLDER or f_path == self.ROOT:
self.init_ftp_data(f_path)
else:
b_size = row[self.Column.EXTRA]
if b_size.isdigit() and int(b_size) > self.MAX_SIZE:
self._app.show_error_dialog("The file size is too large!")
else:
self.open_ftp_file(f_path)
def on_file_row_activated(self, view, path, column):
row = self._file_model[path][:]
path = Path(row[self.Column.ATTR])
if row[self.Column.SIZE] == self.FOLDER:
self.init_file_data(path)
elif row[self.Column.NAME] == self.ROOT:
self.init_file_data(path.parent)
else:
self.open_file(row[self.Column.ATTR])
@run_task
def open_file(self, path):
GLib.idle_add(self._file_view.set_sensitive, False)
try:
cmd = ["open" if self._settings.is_darwin else "xdg-open", path]
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
finally:
GLib.idle_add(self._file_view.set_sensitive, True)
@run_task
def open_ftp_file(self, f_path):
is_darwin = self._settings.is_darwin
GLib.idle_add(self._ftp_view.set_sensitive, False)
try:
import tempfile
import os
path = os.path.expanduser("~/Desktop") if is_darwin else None
with tempfile.NamedTemporaryFile(mode="wb", dir=path, delete=not is_darwin) as tf:
msg = "Downloading file: {}. Status: {}"
try:
status = self._ftp.retrbinary("RETR " + f_path, tf.write)
self.update_ftp_info(msg.format(f_path, status))
except all_errors as e:
self.update_ftp_info(msg.format(f_path, e))
tf.flush()
cmd = ["open" if is_darwin else "xdg-open", tf.name]
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
finally:
GLib.idle_add(self._ftp_view.set_sensitive, True)
def on_ftp_edit(self, renderer):
model, paths = self._ftp_view.get_selection().get_selected_rows()
if not paths:
return
if len(paths) > 1:
self._app.show_error_dialog("Please, select only one item!")
return
renderer.set_property("editable", True)
self._ftp_view.set_cursor(paths, self._ftp_view.get_column(0), True)
def on_ftp_edited(self, renderer, path, new_value):
renderer.set_property("editable", False)
row = self._ftp_model[path]
old_name = row[self.Column.NAME]
if old_name == new_value:
return
resp = self._ftp.rename_file(old_name, new_value)
self.update_ftp_info("{} Status: {}".format(old_name, resp))
if resp[0] == "2":
row[self.Column.NAME] = new_value
def on_file_edit(self, renderer):
model, paths = self._file_view.get_selection().get_selected_rows()
if len(paths) > 1:
self._app.show_error_dialog("Please, select only one item!")
return
renderer.set_property("editable", True)
self._file_view.set_cursor(paths, self._file_view.get_column(0), True)
def on_file_edited(self, renderer, path, new_value):
renderer.set_property("editable", False)
row = self._file_model[path]
old_name = row[self.Column.NAME]
if old_name == new_value:
return
path = Path(row[self.Column.ATTR])
if path.exists():
try:
new_path = path.rename("{}/{}".format(path.parent, new_value))
except ValueError as e:
log(e)
self._app.show_error_dialog(str(e))
else:
if new_path.name == new_value:
row[self.Column.NAME] = new_value
row[self.Column.ATTR] = str(new_path.resolve())
def on_file_remove(self, item=None):
if show_dialog(DialogType.QUESTION, self._app._main_window) != Gtk.ResponseType.OK:
return
model, paths = self._file_view.get_selection().get_selected_rows()
to_delete = []
for path in filter(lambda p: model[p][self.Column.NAME] != self.ROOT, paths):
f_path = Path(model[path][self.Column.ATTR])
try:
rmtree(f_path, ignore_errors=True) if f_path.is_dir() else f_path.unlink()
except OSError as e:
log(e)
else:
to_delete.append(model.get_iter(path))
list(map(model.remove, to_delete))
def on_ftp_file_remove(self, item=None):
if show_dialog(DialogType.QUESTION, self._app._main_window) != Gtk.ResponseType.OK:
return
model, paths = self._ftp_view.get_selection().get_selected_rows()
to_delete = []
for path in filter(lambda p: model[p][self.Column.NAME] != self.ROOT, paths):
row = model[path][:]
name = row[self.Column.NAME]
if row[self.Column.SIZE] == self.FOLDER:
resp = self._ftp.delete_dir(name, self.update_ftp_info)
else:
resp = self._ftp.delete_file(name, self.update_ftp_info)
if resp[0] == "2":
to_delete.append(model.get_iter(path))
list(map(model.remove, to_delete))
def on_file_create_folder(self, renderer):
itr = self._file_model.get_iter_first()
if not itr:
return
name = self.get_new_folder_name(self._file_model)
cur_path = self._file_model.get_value(itr, self.Column.ATTR)
path = Path("{}/{}".format(cur_path, name))
try:
path.mkdir()
except OSError as e:
log(e)
self._app.show_error_dialog(str(e))
else:
itr = self._file_model.append(File(self._folder_icon, path.name, self.FOLDER, "", str(path.resolve()), "0"))
renderer.set_property("editable", True)
self._file_view.set_cursor(self._file_model.get_path(itr), self._file_view.get_column(0), True)
def on_ftp_create_folder(self, renderer):
itr = self._ftp_model.get_iter_first()
if not itr:
return
cur_path = self._ftp_model.get_value(itr, self.Column.ATTR)
name = self.get_new_folder_name(self._ftp_model)
try:
folder = "{}/{}".format(cur_path, name)
resp = self._ftp.mkd(folder)
except all_errors as e:
self.update_ftp_info(str(e))
log(e)
else:
if resp == "{}/{}".format(cur_path, name):
itr = self._ftp_model.append(File(self._folder_icon, name, self.FOLDER, "", "drwxr-xr-x", "0"))
renderer.set_property("editable", True)
self._ftp_view.set_cursor(self._ftp_model.get_path(itr), self._ftp_view.get_column(0), True)
def get_new_folder_name(self, model):
""" Returns the default name for the newly created folder. """
name = "new folder"
names = {r[self.Column.NAME] for r in model}
count = 0
while name in names:
count += 1
name = "{}{}".format(name, count)
return name
# ***************** Drag-and-drop ********************* #
def init_drag_and_drop(self):
self._ftp_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
self._ftp_view.enable_model_drag_dest([], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
self._ftp_view.drag_source_add_uri_targets()
self._ftp_view.drag_dest_add_uri_targets()
self._file_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
self._file_view.enable_model_drag_dest([], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
self._file_view.drag_source_add_uri_targets()
self._file_view.drag_dest_add_uri_targets()
self._ftp_view.get_selection().set_select_function(lambda *args: self._select_enabled)
self._file_view.get_selection().set_select_function(lambda *args: self._select_enabled)
def on_view_drag_begin(self, view, context):
model, paths = view.get_selection().get_selected_rows()
if len(paths) < 1:
return
pix = self._app.get_drag_icon_pixbuf(model, paths, self.Column.NAME, self.Column.SIZE)
Gtk.drag_set_icon_pixbuf(context, pix, 0, 0)
return True
def on_ftp_drag_data_get(self, view, context, data, info, time):
model, paths = view.get_selection().get_selected_rows()
if len(paths) > 0:
sep = self.URI_SEP if self._settings.is_darwin else "\n"
uris = []
for r in [model[p][:] for p in paths]:
if r[self.Column.SIZE] != self.LINK and r[self.Column.NAME] != self.ROOT:
uris.append(Path("/{}:{}".format(r[self.Column.NAME], r[self.Column.ATTR])).as_uri())
data.set_uris([sep.join(uris)])
@run_task
def on_ftp_drag_data_received(self, view, context, x, y, data: Gtk.SelectionData, info, time):
if not self._ftp:
return
resp = "2"
try:
GLib.idle_add(self._app._wait_dialog.show)
uris = data.get_uris()
if self._settings.is_darwin and len(uris) == 1:
uris = uris[0].split(self.URI_SEP)
for uri in uris:
uri = urlparse(unquote(uri)).path
path = Path(uri)
if path.is_dir():
try:
self._ftp.mkd(path.name)
except all_errors as e:
pass # NOP
self._ftp.cwd(path.name)
resp = self._ftp.upload_dir(str(path.resolve()) + "/", self.update_ftp_info)
else:
resp = self._ftp.send_file(path.name, str(path.parent) + "/", callback=self.update_ftp_info)
finally:
GLib.idle_add(self._app._wait_dialog.hide)
if resp and resp[0] == "2":
itr = self._ftp_model.get_iter_first()
if itr:
self.init_ftp_data(self._ftp_model.get_value(itr, self.Column.ATTR))
Gtk.drag_finish(context, True, False, time)
return True
def on_file_drag_data_get(self, view, context, data: Gtk.SelectionData, info, time):
model, paths = view.get_selection().get_selected_rows()
if len(paths) > 0:
sep = self.URI_SEP if self._settings.is_darwin else "\n"
uris = [sep.join([Path(model[p][self.Column.ATTR]).as_uri() for p in paths])]
data.set_uris(uris)
@run_task
def on_file_drag_data_received(self, view, context, x, y, data, info, time):
cur_path = self._file_model.get_value(self._file_model.get_iter_first(), self.Column.ATTR) + "/"
try:
GLib.idle_add(self._app._wait_dialog.show)
uris = data.get_uris()
if self._settings.is_darwin and len(uris) == 1:
uris = uris[0].split(self.URI_SEP)
for uri in uris:
name, sep, attr = unquote(Path(uri).name).partition(":")
if not attr:
return True
if attr[0] == "d":
self._ftp.download_dir(name, cur_path, self.update_ftp_info)
else:
self._ftp.download_file(name, cur_path, self.update_ftp_info)
except OSError as e:
log(e)
finally:
GLib.idle_add(self._app._wait_dialog.hide)
self.init_file_data(cur_path)
Gtk.drag_finish(context, True, False, time)
return True
def on_view_drag_end(self, view, context):
self._select_enabled = True
view.get_selection().unselect_all()
@run_idle
def update_ftp_info(self, message):
message = message.strip()
self._ftp_info_label.set_text(message)
self._ftp_info_label.set_tooltip_text(message)
def on_view_key_press(self, view, event):
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK
if key is KeyboardKey.F7:
if self._ftp_view.is_focus():
self.on_ftp_create_folder(self._ftp_name_renderer)
elif self._file_view.is_focus():
self.on_file_create_folder(self._file_name_renderer)
elif key is KeyboardKey.F2 or ctrl and KeyboardKey.R:
if self._ftp_view.is_focus():
self.on_ftp_edit(self._ftp_name_renderer)
elif self._file_view.is_focus():
self.on_file_edit(self._file_name_renderer)
elif key is KeyboardKey.DELETE:
if self._ftp_view.is_focus():
self.on_ftp_file_remove()
elif self._file_view.is_focus():
self.on_file_remove()
def on_view_press(self, view, event):
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_PRIMARY:
target = view.get_path_at_pos(event.x, event.y)
mask = not (event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK))
if target and mask and view.get_selection().path_is_selected(target[0]):
self._select_enabled = False
def on_view_release(self, view, event):
""" Handles a mouse click (release) to view. """
# Enable selection.
self._select_enabled = True
def get_size_from_bytes(self, size):
""" Simple convert function from bytes to other units like K, M or G. """
try:
b = float(size)
except ValueError:
return size
else:
kb, mb, gb = 1024.0, 1048576.0, 1073741824.0
if b < kb:
return str(b)
elif kb <= b < mb:
return "{0:.1f} K".format(b / kb)
elif mb <= b < gb:
return "{0:.1f} M".format(b / mb)
elif gb <= b:
return "{0:.1f} G".format(b / gb)
if __name__ == '__main__':
pass

View File

@@ -26,25 +26,13 @@ THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface domain="demon-editor">
<interface>
<requires lib="gtk+" version="3.16"/>
<!-- interface-css-provider-path style.css -->
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor for macOS. -->
<!-- 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>
</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 -->
@@ -94,7 +82,6 @@ Author: Dmitriy Yefremov
</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>
@@ -103,8 +90,53 @@ Author: Dmitriy Yefremov
<property name="type_hint">dialog</property>
<property name="gravity">center</property>
<signal name="check-resize" handler="on_resize" swapped="no"/>
<child>
<placeholder/>
<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">
@@ -129,8 +161,8 @@ Author: Dmitriy Yefremov
<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="margin_top">2</property>
<property name="margin_bottom">2</property>
<property name="label" translatable="yes">Bouquets</property>
</object>
<packing>
@@ -228,8 +260,8 @@ Author: Dmitriy Yefremov
<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="margin_top">2</property>
<property name="margin_bottom">2</property>
<property name="label" translatable="yes">Bouquet details</property>
</object>
<packing>
@@ -305,62 +337,6 @@ Author: Dmitriy Yefremov
<property name="position">0</property>
</packing>
</child>
<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">5</property>
<property name="margin_bottom">5</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>
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
<accelerator key="i" signal="clicked"/>
</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">False</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>
@@ -406,7 +382,7 @@ 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>
</object>

View File

@@ -1,13 +1,12 @@
from contextlib import suppress
from pathlib import Path
from app.commons import run_idle
from app.eparser import get_bouquets, get_services
from app.commons import run_idle, log
from app.eparser import get_bouquets, get_services, BouquetsReader
from app.eparser.ecommons import BqType, BqServiceType, Bouquet
from app.eparser.enigma.bouquets import get_bouquet
from app.eparser.neutrino.bouquets import parse_webtv, parse_bouquets as get_neutrino_bouquets
from app.settings import SettingsType
from app.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, get_message
from app.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, get_message, get_builder
from app.ui.main_helper import on_popup_menu
from .uicommons import Gtk, UI_RESOURCES_PATH, KeyboardKey, Column
@@ -21,7 +20,7 @@ def import_bouquet(transient, model, path, settings, services, appender, file_pa
if profile is SettingsType.ENIGMA_2:
pattern = ".{}".format(bq_type.value)
f_pattern = "*" + pattern if settings.is_darwin else "userbouquet.*{}".format(pattern)
f_pattern = "userbouquet.*{}".format(pattern)
elif profile is SettingsType.NEUTRINO_MP:
pattern = "webtv.xml" if bq_type is BqType.WEBTV else "bouquets.xml"
f_pattern = "bouquets.xml"
@@ -34,15 +33,11 @@ def import_bouquet(transient, model, path, settings, services, appender, file_pa
if file_path == Gtk.ResponseType.CANCEL:
return
if not file_path.endswith(pattern):
if not str(file_path).endswith(pattern):
show_dialog(DialogType.ERROR, transient, text="No bouquet file is selected!")
return
if profile is SettingsType.ENIGMA_2:
if settings.is_darwin and file_path.rfind("userbouquet.") < 0:
show_dialog(DialogType.ERROR, transient, text="No bouquet file is selected!")
return
bq = get_enigma2_bouquet(file_path)
imported = list(filter(lambda x: x.data in services or x.type is BqServiceType.IPTV, bq.services))
@@ -67,7 +62,7 @@ def import_bouquet(transient, model, path, settings, services, appender, file_pa
def get_enigma2_bouquet(path):
path, sep, f_name = path.rpartition("userbouquet.")
name, sep, suf = f_name.rpartition(".")
bq = get_bouquet(path, name, suf)
bq = BouquetsReader.get_bouquet(path, name, suf)
bouquet = Bouquet(name=bq[0], type=BqType(suf).value, services=bq[1], locked=None, hidden=None)
return bouquet
@@ -85,10 +80,7 @@ class ImportDialog:
"on_resize": self.on_resize,
"on_key_press": self.on_key_press}
builder = Gtk.Builder()
builder.set_translation_domain("demon-editor")
builder.add_from_file(UI_RESOURCES_PATH + "import_dialog.glade")
builder.connect_signals(handlers)
builder = get_builder(UI_RESOURCES_PATH + "import_dialog.glade", handlers)
self._bq_services = {}
self._services = {}
@@ -123,16 +115,25 @@ class ImportDialog:
self._services_model.clear()
try:
if not self._bouquets:
log("Import [init data]: getting bouquets...")
self._bouquets = get_bouquets(path, self._profile)
for bqs in self._bouquets:
for bq in bqs.bouquets:
self._main_model.append((bq.name, bq.type, True))
self._bq_services[(bq.name, bq.type)] = bq.services
# Note! Getting default format ver. 4
services = get_services(path, self._profile, 4 if self._profile is SettingsType.ENIGMA_2 else 0)
if self._profile is SettingsType.ENIGMA_2:
services = get_services(path, self._profile, 5 if self._settings.v5_support else 4)
elif self._profile is SettingsType.NEUTRINO_MP:
services = get_services(path, self._profile, 0)
else:
self.show_info_message("Setting format not supported!", Gtk.MessageType.ERROR)
return
for srv in services:
self._services[srv.fav_id] = srv
except FileNotFoundError as e:
log("Import error [init data]: {}".format(e))
self.show_info_message(str(e), Gtk.MessageType.ERROR)
def on_import(self, item):
@@ -143,9 +144,17 @@ class ImportDialog:
if not self._bouquets or show_dialog(DialogType.QUESTION, self._dialog_window) == Gtk.ResponseType.CANCEL:
return
self.import_data()
@run_idle
def import_data(self):
""" Importing data into models. """
if not self._bouquets:
return
log("Importing data...")
services = set()
to_delete = set()
for row in self._main_model:
bq = (row[0], row[1])
if row[-1]:
@@ -155,19 +164,16 @@ class ImportDialog:
services.add(srv)
else:
to_delete.add(bq)
bqs_to_delete = []
for bqs in self._bouquets:
for bq in bqs.bouquets:
if (bq.name, bq.type) in to_delete:
bqs_to_delete.append(bq)
for bqs in self._bouquets:
bq = bqs.bouquets
for b in bqs_to_delete:
with suppress(ValueError):
bq.remove(b)
self._append(self._bouquets, list(filter(lambda s: s.fav_id not in self._service_ids, services)))
self._dialog_window.destroy()

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,51 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
import concurrent.futures
import 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 PlayListParser, 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.tools.yt import YouTubeException, YouTube
from app.ui.dialogs import Action, show_dialog, DialogType, get_message, get_builder
from app.ui.main_helper import get_base_model, get_iptv_url, on_popup_menu, get_picon_pixbuf
from app.ui.uicommons import (Gtk, Gdk, UI_RESOURCES_PATH, IPTV_ICON, Column, KeyboardKey, get_yt_icon)
_DIGIT_ENTRY_NAME = "digit-entry"
_ENIGMA2_REFERENCE = "{}:0:{}:{:X}:{:X}:{:X}:{:X}:0:0:0"
@@ -40,7 +70,9 @@ def get_stream_type(box):
return StreamType.NONE_REC_1.value
elif active == 3:
return StreamType.NONE_REC_2.value
return StreamType.E_SERVICE_URI.value
elif active == 4:
return StreamType.E_SERVICE_URI.value
return StreamType.E_SERVICE_HLS.value
class IptvDialog:
@@ -62,11 +94,8 @@ class IptvDialog:
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)
@@ -161,6 +190,8 @@ class IptvDialog:
self._stream_type_combobox.set_active(3)
elif stream_type is StreamType.E_SERVICE_URI:
self._stream_type_combobox.set_active(4)
elif stream_type is StreamType.E_SERVICE_HLS:
self._stream_type_combobox.set_active(5)
except ValueError:
self.show_info_message("Unknown stream type {}".format(s_type), Gtk.MessageType.ERROR)
@@ -200,7 +231,8 @@ class IptvDialog:
def on_url_changed(self, entry):
url_str = entry.get_text()
url = urlparse(url_str)
cond = all([url.scheme, url.netloc, url.path]) or self.get_type() == StreamType.E_SERVICE_URI.value
e_types = (StreamType.E_SERVICE_URI.value, StreamType.E_SERVICE_HLS.value)
cond = all([url.scheme, url.netloc, url.path]) or self.get_type() in e_types
entry.set_name("GtkEntry" if cond else _DIGIT_ENTRY_NAME)
yt_id = YouTube.get_yt_id(url_str)
@@ -251,7 +283,7 @@ class IptvDialog:
yield True
def on_stream_type_changed(self, item):
if self.get_type() == StreamType.E_SERVICE_URI.value:
if self.get_type() in (StreamType.E_SERVICE_URI.value, StreamType.E_SERVICE_HLS.value):
self.show_info_message("DreamOS only!", Gtk.MessageType.WARNING)
self.update_reference_entry()
@@ -315,10 +347,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)
@@ -394,9 +424,10 @@ class SearchUnavailableDialog:
self._dialog.destroy()
class IptvListConfigurationDialog:
class IptvListDialog:
""" Base class for working with iptv lists. """
def __init__(self, transient, services, iptv_rows, bouquet, fav_model, 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,
@@ -410,20 +441,15 @@ 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")
@@ -438,22 +464,28 @@ class IptvListConfigurationDialog:
self._list_tid_entry = builder.get_object("list_tid_entry")
self._list_nid_entry = builder.get_object("list_nid_entry")
self._list_namespace_entry = builder.get_object("list_namespace_entry")
self._reset_to_default_switch = builder.get_object("reset_to_default_lists_switch")
# style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._apply_button = builder.get_object("list_configuration_apply_button")
self._cancel_button = builder.get_object("cancel_config_list_button")
self._ok_button = builder.get_object("list_configuration_ok_button")
# Style
style_provider = Gtk.CssProvider()
style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._default_elems = (self._stream_type_check_button, self._type_check_button, self._sid_auto_check_button,
self._tid_check_button, self._nid_check_button, self._namespace_check_button)
self._digit_elems = (self._list_srv_type_entry, self._list_sid_entry, self._list_tid_entry,
self._list_nid_entry, self._list_namespace_entry)
for el in self._digit_elems:
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
def show(self):
self._dialog.run()
def on_response(self, dialog, response):
if response == Gtk.ResponseType.CANCEL:
self._dialog.destroy()
if response == Gtk.ResponseType.APPLY:
return True
self._dialog.destroy()
def on_stream_type_changed(self, box):
self.update_reference()
@@ -489,19 +521,51 @@ class IptvListConfigurationDialog:
self._list_namespace_entry.set_sensitive(not button.get_active())
@run_idle
def on_reset_to_default(self, item, active):
item.set_sensitive(not active)
def on_reset_to_default(self, item):
self._stream_type_combobox.set_active(1)
self._list_srv_type_entry.set_text("1")
for el in (self._list_sid_entry, self._list_nid_entry, self._list_tid_entry, self._list_namespace_entry):
for el in self._digit_elems[1:]:
el.set_text("0")
for el in (self._stream_type_check_button, self._type_check_button, self._sid_auto_check_button,
self._tid_check_button, self._nid_check_button, self._namespace_check_button):
for el in self._default_elems:
el.set_active(True)
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
def on_apply(self, item):
pass
@run_idle
def update_reference(self):
if is_data_correct(self._digit_elems):
stream_type = get_stream_type(self._stream_type_combobox)
self._reference_label.set_text(
_ENIGMA2_REFERENCE.format(stream_type, *[int(elem.get_text()) for elem in self._digit_elems]))
def on_entry_changed(self, entry):
if _PATTERN.search(entry.get_text()):
entry.set_name(_DIGIT_ENTRY_NAME)
else:
entry.set_name("GtkEntry")
self.update_reference()
def is_default_values(self):
return any(el.get_text() == "0" for el in self._digit_elems[2:])
def is_all_data_default(self):
return all(el.get_active() for el in self._default_elems)
class IptvListConfigurationDialog(IptvListDialog):
def __init__(self, transient, services, iptv_rows, bouquet, fav_model, s_type):
super().__init__(transient, s_type)
self._rows = iptv_rows
self._bouquet = bouquet
self._fav_model = fav_model
self._services = services
@run_idle
def on_apply(self, item):
if not is_data_correct(self._digit_elems):
@@ -509,14 +573,13 @@ class IptvListConfigurationDialog:
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)
stream_type = get_stream_type(self._stream_type_combobox)
srv_type = "1" if type_default else self._list_srv_type_entry.get_text()
tid = "0" if tid_default else "{:X}".format(int(self._list_tid_entry.get_text()))
nid = "0" if nid_default else "{:X}".format(int(self._list_nid_entry.get_text()))
@@ -527,7 +590,7 @@ class IptvListConfigurationDialog:
data, sep, desc = fav_id.partition("http")
data = data.split(":")
if reset:
if self.is_all_data_default():
data[2], data[3], data[4], data[5], data[6] = "10000"
else:
data[0], data[2], data[4], data[5], data[6] = stream_type, srv_type, tid, nid, namespace
@@ -546,19 +609,213 @@ class IptvListConfigurationDialog:
self._info_bar.set_visible(True)
@run_idle
def update_reference(self):
if is_data_correct(self._digit_elems):
stream_type = get_stream_type(self._stream_type_combobox)
self._reference_label.set_text(
_ENIGMA2_REFERENCE.format(stream_type, *[int(elem.get_text()) for elem in self._digit_elems]))
def on_entry_changed(self, entry):
if _PATTERN.search(entry.get_text()):
entry.set_name(_DIGIT_ENTRY_NAME)
class M3uImportDialog(IptvListDialog):
""" Import dialog for *.m3u* playlists. """
def __init__(self, transient, s_type, m3_path, app):
super().__init__(transient, s_type)
self._app = app
self._picons = app._picons
self._pic_path = app._settings.picons_local_path
self._services = None
self._url_count = 0
self._errors_count = 0
self._max_count = 0
self._is_download = False
self._cancellable = Gio.Cancellable()
self._dialog.set_title(get_message("Playlist import"))
self._dialog.connect("delete-event", self.on_close)
self._apply_button.set_label(get_message("Import"))
self._ok_button.bind_property("visible", self._apply_button, "visible", 4)
self._ok_button.bind_property("visible", self._cancel_button, "visible", 4)
# Progress
self._progress_bar = Gtk.ProgressBar(visible=False, valign="center")
self._spinner = Gtk.Spinner(active=False)
self._info_label = Gtk.Label(visible=True, ellipsize="end", max_width_chars=30)
load_label = Gtk.Label(label=get_message("Loading data..."))
self._spinner.bind_property("active", self._spinner, "visible")
self._spinner.bind_property("visible", load_label, "visible")
self._spinner.bind_property("active", self._start_values_grid, "sensitive", 4)
progress_box = Gtk.HBox(visible=True, spacing=2)
progress_box.add(self._progress_bar)
progress_box.pack_end(self._spinner, False, False, 0)
progress_box.pack_start(load_label, False, False, 0)
# Picons
self._picons_switch = Gtk.Switch(visible=True)
self._picon_box = Gtk.HBox(visible=True, sensitive=False, spacing=2)
self._picon_box.pack_end(self._picons_switch, False, False, 0)
self._picon_box.pack_end(Gtk.Label(visible=True, label=get_message("Download picons")), False, False, 0)
# Extra box
extra_box = Gtk.HBox(visible=True, spacing=2, margin_bottom=5, margin_top=5)
extra_box.set_center_widget(progress_box)
extra_box.pack_start(self._info_label, False, False, 5)
extra_box.pack_end(self._picon_box, True, True, 5)
frame = Gtk.Frame(visible=True)
frame.add(extra_box)
self._data_box.add(frame)
self.get_m3u(m3_path, s_type)
@run_task
def get_m3u(self, path, s_type):
try:
GLib.idle_add(self._spinner.set_property, "active", True)
self._services = parse_m3u(path, s_type)
for s in self._services:
if s.picon:
GLib.idle_add(self._picon_box.set_sensitive, True)
break
finally:
msg = "{} {}.".format(get_message("Streams detected:"), len(self._services) if self._services else 0)
GLib.idle_add(self._info_label.set_text, msg)
GLib.idle_add(self._spinner.set_property, "active", False)
def on_apply(self, item):
if not is_data_correct(self._digit_elems):
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
picons = {}
services = self._services
if not self.is_all_data_default():
services = []
params = [int(el.get_text()) for el in self._digit_elems]
s_type = params[0]
params = params[1:]
stream_type = get_stream_type(self._stream_type_combobox)
for i, s in enumerate(self._services, start=params[0]):
# Skipping markers.
if not s.data_id:
services.append(s)
continue
params[0] = i
picon_id = "{}_0_{:X}_{:X}_{:X}_{:X}_{:X}_0_0_0.png".format(stream_type, s_type, *params)
fav_id = get_fav_id(s.data_id, s.service, self._s_type, params, stream_type, s_type)
if s.picon:
picons[s.picon] = picon_id
services.append(s._replace(picon=None, picon_id=picon_id, data_id=None, fav_id=fav_id))
if self._picons_switch.get_active():
if self.is_default_values():
show_dialog(DialogType.ERROR, self._dialog,
"Set values for TID, NID and Namespace for correct naming of the picons!")
return
self.download_picons(picons)
else:
entry.set_name("GtkEntry")
self.update_reference()
GLib.idle_add(self._ok_button.set_visible, True)
GLib.idle_add(self._info_bar.set_visible, True, priority=GLib.PRIORITY_LOW)
self._app.append_imported_services(services)
@run_task
def download_picons(self, picons):
self._is_download = True
os.makedirs(os.path.dirname(self._pic_path), exist_ok=True)
GLib.idle_add(self._apply_button.set_sensitive, False)
GLib.idle_add(self._progress_bar.set_visible, True)
self._errors_count = 0
self._url_count = len(picons)
self._max_count = self._url_count
self._cancellable.reset()
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
futures = {executor.submit(self.download_picon, p, picons.get(p, None)): p for p in filter(None, picons)}
done, not_done = concurrent.futures.wait(futures, timeout=0)
while self._is_download and not_done:
done, not_done = concurrent.futures.wait(not_done, timeout=5)
for future in not_done:
future.cancel()
concurrent.futures.wait(not_done)
self.update_progress(self._url_count)
self.on_done()
def download_picon(self, url, pic_data):
err_msg = "Picon download error: {} [{}]"
timeout = (3, 5) # connect and read timeouts
req = requests.get(url, timeout=timeout)
if req.status_code != 200:
log(err_msg.format(url, req.reason))
self.update_progress(1)
else:
self.on_picon_load_done(req.content, pic_data)
@run_idle
def on_picon_load_done(self, data, user_data):
try:
self._info_label.set_text("Processing: {}".format(user_data))
f = Gio.MemoryInputStream.new_from_data(data)
pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale(f, 220, 132, False, self._cancellable)
path = "{}{}".format(self._pic_path, user_data)
pixbuf.savev(path, "png", [], [])
self._picons[user_data] = get_picon_pixbuf(path)
except GLib.GError as e:
self.update_progress(1)
if e.code != Gio.IOErrorEnum.CANCELLED:
log("Loading picon [{}] data error: {}".format(user_data, e))
else:
self.update_progress()
@run_idle
def update_progress(self, error=0):
self._errors_count += error
self._url_count -= 1
frac = 1 - self._url_count / self._max_count
self._progress_bar.set_fraction(frac)
@run_idle
def on_done(self):
self._progress_bar.set_visible(False)
self._progress_bar.set_fraction(0.0)
self._apply_button.set_sensitive(True)
self._info_label.set_text("Errors: {}.".format(self._errors_count))
self._is_download = False
gen = self.update_fav_model()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def update_fav_model(self):
services = self._app._services
picons = self._app._picons
model = self._app.fav_view.get_model()
for r in model:
s = services.get(r[Column.FAV_ID], None)
if s:
model.set_value(r.iter, Column.FAV_PICON, picons.get(s.picon_id, None))
yield True
self._info_bar.set_visible(True)
self._ok_button.set_visible(True)
yield True
def on_response(self, dialog, response):
if response == Gtk.ResponseType.APPLY:
return True
if response == Gtk.ResponseType.CANCEL and not self._is_download or not self.on_close():
self._dialog.destroy()
def on_close(self, window=None, event=None):
if self._is_download:
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.OK:
self._is_download = False
self._cancellable.cancel()
return False
return True
return False
class YtListImportDialog:
@@ -582,13 +839,9 @@ class YtListImportDialog:
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", "yt_receive_image",
"yt_import_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"))
self._dialog = builder.get_object("yt_import_dialog_window")
self._dialog.set_transient_for(transient)
@@ -666,7 +919,9 @@ class YtListImportDialog:
def update_refs_list(self):
if self._yt_list_id:
try:
self._yt_list_title, links = PlayListParser.get_yt_playlist(self._yt_list_id)
if not self._yt:
self._yt = YouTube.get_instance(self._settings)
self._yt_list_title, links = self._yt.get_yt_playlist(self._yt_list_id, self._url_entry.get_text())
except Exception as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
return

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -46,54 +46,57 @@ def move_items(key, view: Gtk.TreeView):
""" Move items in the tree view """
selection = view.get_selection()
model, paths = selection.get_selected_rows()
if not paths:
return
if paths:
mod_length = len(model)
if mod_length == len(paths):
return
cursor_path = view.get_cursor()[0]
max_path = Gtk.TreePath.new_from_indices((mod_length,))
min_path = Gtk.TreePath.new_from_indices((0,))
is_tree_store = type(model) is Gtk.TreeStore
mod_length = len(model)
if not is_tree_store and mod_length == len(paths):
return
cursor_path = view.get_cursor()[0]
max_path = Gtk.TreePath.new_from_indices((mod_length,))
min_path = Gtk.TreePath.new_from_indices((0,))
if is_tree_store:
is_tree_store = False
parent_paths = list(filter(lambda p: p.get_depth() == 1, paths))
if parent_paths:
paths = parent_paths
min_path = model.get_path(model.get_iter_first())
view.collapse_all()
if mod_length == len(paths):
return
else:
if not is_some_level(paths):
return
parent_itr = model.iter_parent(model.get_iter(paths[0]))
parent_index = model.get_path(parent_itr)
children_num = model.iter_n_children(parent_itr)
if key in (KeyboardKey.PAGE_DOWN, KeyboardKey.END, KeyboardKey.END_KP, KeyboardKey.PAGE_DOWN_KP):
children_num -= 1
min_path = Gtk.TreePath.new_from_string("{}:{}".format(parent_index, 0))
max_path = Gtk.TreePath.new_from_string("{}:{}".format(parent_index, children_num))
is_tree_store = True
if type(model) is Gtk.TreeStore:
parent_paths = list(filter(lambda p: p.get_depth() == 1, paths))
if parent_paths:
paths = parent_paths
min_path = model.get_path(model.get_iter_first())
view.collapse_all()
if mod_length == len(paths):
return
else:
if not is_some_level(paths):
return
parent_itr = model.iter_parent(model.get_iter(paths[0]))
parent_index = model.get_path(parent_itr)
children_num = model.iter_n_children(parent_itr)
if key in (KeyboardKey.PAGE_DOWN, KeyboardKey.END, KeyboardKey.END_KP, KeyboardKey.PAGE_DOWN_KP):
children_num -= 1
min_path = Gtk.TreePath.new_from_string("{}:{}".format(parent_index, 0))
max_path = Gtk.TreePath.new_from_string("{}:{}".format(parent_index, children_num))
is_tree_store = True
if key is KeyboardKey.UP:
top_path = Gtk.TreePath(paths[0])
set_cursor(top_path, paths, selection, view)
top_path.prev()
move_up(top_path, model, paths)
elif key is KeyboardKey.DOWN:
down_path = Gtk.TreePath(paths[-1])
set_cursor(down_path, paths, selection, view)
down_path.next()
if down_path < max_path:
move_down(down_path, model, paths)
else:
max_path.prev()
move_down(max_path, model, paths)
elif key in (KeyboardKey.PAGE_UP, KeyboardKey.HOME, KeyboardKey.PAGE_UP_KP, KeyboardKey.HOME_KP):
move_up(min_path if is_tree_store else cursor_path, model, paths)
elif key in (KeyboardKey.PAGE_DOWN, KeyboardKey.END, KeyboardKey.END_KP, KeyboardKey.PAGE_DOWN_KP):
move_down(max_path if is_tree_store else cursor_path, model, paths)
if key is KeyboardKey.UP:
top_path = Gtk.TreePath(paths[0])
set_cursor(top_path, paths, selection, view)
top_path.prev()
move_up(top_path, model, paths)
elif key is KeyboardKey.DOWN:
down_path = Gtk.TreePath(paths[-1])
set_cursor(down_path, paths, selection, view)
down_path.next()
if down_path < max_path:
move_down(down_path, model, paths)
else:
max_path.prev()
move_down(max_path, model, paths)
elif key in (KeyboardKey.PAGE_UP, KeyboardKey.HOME, KeyboardKey.PAGE_UP_KP, KeyboardKey.HOME_KP):
move_up(min_path if is_tree_store else cursor_path, model, paths)
elif key in (KeyboardKey.PAGE_DOWN, KeyboardKey.END, KeyboardKey.END_KP, KeyboardKey.PAGE_DOWN_KP):
move_down(max_path if is_tree_store else cursor_path, model, paths)
def move_up(top_path, model, paths):
@@ -161,7 +164,8 @@ def rename(view, parent_window, target, fav_view=None, service_view=None, servic
return
srv_name = response
model.set_value(itr, Column.FAV_SERVICE, response)
if not model.get_value(itr, Column.FAV_BACKGROUND):
model.set_value(itr, Column.FAV_SERVICE, response)
if service_view is not None:
for row in get_base_model(service_view.get_model()):
@@ -349,12 +353,12 @@ def scroll_to(index, view, paths=None):
# ***************** Picons *********************#
def update_picons_data(path, picons):
def update_picons_data(path, picons, size=32):
if not os.path.exists(path):
return
for file in os.listdir(path):
pf = get_picon_pixbuf(path + file)
pf = get_picon_pixbuf(path + file, size)
if pf:
picons[file] = pf
@@ -401,15 +405,19 @@ def assign_picons(target, srv_view, fav_view, transient, picons, settings, servi
picons_path = dst_path or settings.picons_local_path
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
picon_file = picons_path + picon_id
shutil.copy(src_path, picon_file)
picons_files.append(picon_file)
picon = get_picon_pixbuf(picon_file)
picons[picon_id] = picon
model.set_value(itr, p_pos, picon)
if target is ViewTarget.SERVICES:
set_picon(fav_id, fav_view.get_model(), picon, Column.FAV_ID, p_pos)
try:
shutil.copy(src_path, picon_file)
except shutil.SameFileError:
pass # NOP
else:
set_picon(fav_id, get_base_model(srv_view.get_model()), picon, Column.SRV_FAV_ID, p_pos)
picons_files.append(picon_file)
picon = get_picon_pixbuf(picon_file)
picons[picon_id] = picon
model.set_value(itr, p_pos, picon)
if target is ViewTarget.SERVICES:
set_picon(fav_id, fav_view.get_model(), picon, Column.FAV_ID, p_pos)
else:
set_picon(fav_id, get_base_model(srv_view.get_model()), picon, Column.SRV_FAV_ID, p_pos)
return picons_files
@@ -522,7 +530,7 @@ def get_picon_pixbuf(path, size=32):
# ***************** Bouquets *********************#
def gen_bouquets(view, bq_view, transient, gen_type, tv_types, s_type, callback):
def gen_bouquets(view, bq_view, transient, gen_type, s_type, callback):
""" Auto-generate and append list of bouquets """
fav_id_index = Column.SRV_FAV_ID
index = Column.SRV_TYPE
@@ -537,8 +545,6 @@ def gen_bouquets(view, bq_view, transient, gen_type, tv_types, s_type, callback)
if not is_only_one_item_selected(paths, transient):
return
service = Service(*model[paths][:Column.SRV_TOOLTIP])
if service.service_type not in tv_types:
bq_type = BqType.RADIO.value
append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model,
[service.package if gen_type is BqGenType.PACKAGE else
service.pos if gen_type is BqGenType.SAT else service.service_type], s_type)
@@ -583,8 +589,8 @@ def get_bouquets_names(model):
# ***************** Others *********************#
def update_entry_data(entry, dialog, settings):
""" Updates value in text entry from chooser dialog """
response = show_dialog(dialog_type=DialogType.CHOOSER, transient=dialog, settings=settings)
""" Updates value in text entry from chooser dialog. """
response = show_dialog(dialog_type=DialogType.CHOOSER, transient=dialog, settings=settings, create_dir=True)
if response not in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
entry.set_text(response)
return response
@@ -617,7 +623,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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,78 @@
# -*- 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 shutil
import subprocess
import tempfile
from enum import Enum
from pathlib import Path
from urllib.parse import urlparse, unquote
from gi.repository import GLib, GdkPixbuf
from gi.repository import GLib, GdkPixbuf, Gio
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.picons import (PiconsParser, parse_providers, Provider, convert_to, download_picon, PiconsCzDownloader,
PiconsError)
from app.tools.satellites import SatellitesParser, SatelliteSource
from .dialogs import show_dialog, DialogType, get_message
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, GTK_PATH, KeyboardKey
from .dialogs import show_dialog, DialogType, get_message, get_builder
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:
class DownloadSource(Enum):
LYNG_SAT = "lyngsat"
PICON_CZ = "piconcz"
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._is_downloading = False
self._filter_binding = None
self._services = None
self._current_picon_info = None
self._filter_cache = {}
# Downloader
self._sats = None
self._sat_names = None
self._download_src = self.DownloadSource.PICON_CZ
self._picon_cz_downloader = 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,
@@ -64,7 +101,10 @@ class PiconsDialog:
"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_download_source_changed": self.on_download_source_changed,
"on_satellites_view_realize": self.on_satellites_view_realize,
"on_satellite_filter_toggled": self.on_satellite_filter_toggled,
"on_providers_view_query_tooltip": self.on_providers_view_query_tooltip,
"on_satellite_selection": self.on_satellite_selection,
"on_select_all": self.on_select_all,
"on_unselect_all": self.on_unselect_all,
@@ -72,12 +112,11 @@ class PiconsDialog:
"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_view_query_tooltip": self.on_view_query_tooltip,
"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)
builder = get_builder(UI_RESOURCES_PATH + "picons_manager.glade", handlers)
self._dialog = builder.get_object("picons_dialog")
self._dialog.set_transient_for(transient)
@@ -99,15 +138,14 @@ class PiconsDialog:
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_toggle_button = builder.get_object("info_toggle_button")
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._download_source_button = builder.get_object("download_source_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")
@@ -122,35 +160,35 @@ class PiconsDialog:
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._explorer_action_box = builder.get_object("explorer_action_box")
self._provider_header_label = builder.get_object("provider_header_label")
self._satellite_filter_switch = builder.get_object("satellite_filter_switch")
self._bouquet_filter_switch = builder.get_object("bouquet_filter_switch")
self._bouquet_filter_grid = builder.get_object("bouquet_filter_grid")
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", builder.get_object("receive_button"), "visible", 4)
self._cancel_button.bind_property("visible", self._load_providers_button, "visible", 4)
self._convert_button.bind_property("visible", self._explorer_action_box, "visible", 4)
downloader_action_box = builder.get_object("downloader_action_box")
self._explorer_action_box.bind_property("visible", downloader_action_box, "visible", 4)
self._convert_button.bind_property("visible", downloader_action_box, "visible", 4)
self._satellite_label.bind_property("visible", self._download_source_button, "sensitive")
self._satellite_label.bind_property("visible", self._satellites_view, "sensitive")
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._download_source_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_toggle_button, "visible")
self._filter_button.bind_property("visible", self._info_check_button, "visible")
self._filter_button.bind_property("visible", self._send_button, "visible")
self._filter_button.bind_property("visible", self._download_button, "visible")
self._filter_button.bind_property("visible", self._remove_button, "visible")
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_toggle_button.bind_property("active", explorer_info_bar, "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)
# Settings
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")
@@ -263,9 +301,8 @@ class PiconsDialog:
def on_picons_view_drag_data_get(self, view, drag_context, data, info, time):
model, path = view.get_selection().get_selected_rows()
if path:
p_uri = Path(model[path][-1]).as_uri()
dest_uri = Path(self._explorer_dest_path_button.get_filename()).as_uri()
data.set_uris(["{}::::{}".format(p_uri, dest_uri)])
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")
@@ -340,11 +377,10 @@ class PiconsDialog:
return
uris = data.get_uris()
if uris:
if len(uris) == 2:
name, fav_id = self._current_picon_info
src, sep, dst = uris[0].partition("::::")
src = urlparse(unquote(src)).path
dst = "{}/{}".format(urlparse(unquote(dst)).path, name)
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()):
@@ -357,7 +393,7 @@ class PiconsDialog:
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 and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.OK:
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):
@@ -372,9 +408,8 @@ class PiconsDialog:
def get_path_from_uris(self, data):
uris = data.get_uris()
if uris:
src, sep, dst = uris[0].partition("::::")
return Path(urlparse(unquote(src)).path).resolve()
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)
@@ -386,7 +421,7 @@ class PiconsDialog:
def on_selective_send(self, view):
path = self.get_selected_path(view)
if path and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.OK:
if path:
self.on_send(files_filter={path.name}, path=path.parent)
def on_selective_download(self, view):
@@ -439,7 +474,7 @@ class PiconsDialog:
files_filter=files_filter), True)
def on_remove(self, item=None, files_filter=None):
if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.OK:
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
self.run_func(lambda: remove_picons(settings=self._settings,
@@ -466,133 +501,254 @@ class PiconsDialog:
# ******************** Downloader ************************* #
def on_download_source_changed(self, button):
self._download_src = self.DownloadSource(button.get_active_id())
self.set_providers_header()
self._bouquet_filter_grid.set_sensitive(self._download_src is self.DownloadSource.PICON_CZ)
GLib.idle_add(self._providers_view.get_model().clear)
self.init_satellites(self._satellites_view)
def on_satellites_view_realize(self, view):
self.set_providers_header()
self.get_satellites(view)
def on_satellite_filter_toggled(self, button, state):
self.init_satellites(self._satellites_view)
def on_providers_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
if self._download_src is self.DownloadSource.LYNG_SAT:
return False
dest = view.get_dest_row_at_pos(x, y)
if not dest:
return False
path, pos = dest
model = view.get_model()
itr = model.get_iter(path)
logo_url = model.get_value(itr, 5)
if logo_url:
pix_data = self._picon_cz_downloader.get_logo_data(logo_url)
if pix_data:
pix = self.get_pixbuf(pix_data)
model.set_value(itr, 0, pix if pix else TV_ICON)
size = self._settings.tooltip_logo_size
tooltip.set_icon(self.get_pixbuf(pix_data, size, size))
else:
self.update_logo_data(itr, model, logo_url)
tooltip.set_text(model.get_value(itr, 1))
view.set_tooltip_row(tooltip, path)
return True
@run_task
def update_logo_data(self, itr, model, url):
pix_data = self._picon_cz_downloader.get_provider_logo(url)
if pix_data:
pix = self.get_pixbuf(pix_data)
GLib.idle_add(model.set_value, itr, 0, pix if pix else TV_ICON)
@run_idle
def set_providers_header(self):
msg = "{} [{}]"
tooltip = ""
if self._download_src is self.DownloadSource.PICON_CZ:
tooltip = "https://picon.cz (by Chocholoušek)"
msg = msg.format(get_message("Package"), tooltip)
elif self._download_src is self.DownloadSource.LYNG_SAT:
tooltip = "https://www.lyngsat.com"
msg = msg.format(get_message("Providers"), tooltip)
else:
msg = ""
self._provider_header_label.set_text(msg)
self._provider_header_label.set_tooltip_text(tooltip)
@run_task
def get_satellites(self, view):
sats = SatellitesParser().get_satellites_list(SatelliteSource.LYNGSAT)
if not sats:
self._sats = SatellitesParser().get_satellites_list(SatelliteSource.LYNGSAT)
if not self._sats:
self.show_info_message("Getting satellites list error!", Gtk.MessageType.ERROR)
self._sat_names = {s[1]: s[0] for s in self._sats} # position -> satellite name
self._picon_cz_downloader = PiconsCzDownloader(self._picon_ids, self.append_output)
self.init_satellites(view)
@run_task
def init_satellites(self, view):
sats = self._sats
if self._download_src is self.DownloadSource.PICON_CZ:
if not self._picon_cz_downloader:
return
try:
self._picon_cz_downloader.init()
except PiconsError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
providers = self._picon_cz_downloader.providers
sats = ((self._sat_names.get(p, p), p, None, p, False) for p in providers)
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])
is_filter = self._satellite_filter_switch.get_active()
if model:
model.clear()
if not self._terminate and model:
if pos in self._sat_positions:
yield model.append((name, sat[3], pos))
try:
for sat in sorted(sats):
pos = sat[1]
name = "{} ({})".format(sat[0], pos)
if is_filter and pos not in self._sat_positions:
continue
if not model:
return
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])
self.on_info_bar_close()
model = self._providers_view.get_model()
model.clear()
self._satellite_label.set_visible(False)
self.get_providers(view.get_model()[path][1], model)
@run_task
def get_providers(self, url, model):
if self._download_src is self.DownloadSource.LYNG_SAT:
providers = parse_providers(url)
elif self._download_src is self.DownloadSource.PICON_CZ:
providers = self._picon_cz_downloader.get_sat_providers(url)
else:
return
self.append_providers(providers or [], model)
@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()
def append_providers(self, providers, model):
if self._download_src is self.DownloadSource.LYNG_SAT:
for p in providers:
model.append(p._replace(logo=self.get_pixbuf(p.logo) if p.logo else TV_ICON))
elif self._download_src is self.DownloadSource.PICON_CZ:
for p in providers:
logo_data = self._picon_cz_downloader.get_logo_data(p.ssid)
model.append(p._replace(logo=self.get_pixbuf(logo_data) if logo_data else TV_ICON))
try:
exe = "{}wget".format("./" if GTK_PATH else "")
self._current_process = subprocess.Popen([exe, "-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)
self.update_receive_button_state()
GLib.idle_add(self._satellite_label.set_visible, True)
@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 get_pixbuf(self, img_data, w=48, h=32):
if img_data:
f = Gio.MemoryInputStream.new_from_data(img_data)
return GdkPixbuf.Pixbuf.new_from_stream_at_scale(f, w, h, True, None)
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:
if self._is_downloading:
self.show_dialog("The task is already running!", DialogType.ERROR)
return
self._terminate = False
self._expander.set_expanded(True)
providers = self.get_selected_providers()
if self._download_src is self.DownloadSource.PICON_CZ and len(providers) > 1:
self.show_dialog("Please, select only one item!", DialogType.ERROR)
return
self._cancel_button.show()
self.start_download(providers)
@run_task
def start_download(self, providers):
self._is_downloading = True
GLib.idle_add(self._expander.set_expanded, True)
for prv in providers:
if not self._POS_PATTERN.match(prv[2]):
if self._download_src is self.DownloadSource.LYNG_SAT and 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))
picons_path = self._picons_dir_entry.get_text()
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
providers = (Provider(*p) for p in providers)
if self._download_src is self.DownloadSource.LYNG_SAT:
self.get_picons_for_lyngsat(picons_path, providers)
elif self._download_src is self.DownloadSource.PICON_CZ:
self.get_picons_for_picon_cz(picons_path, providers)
if not self._is_downloading:
return
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)
self.resize(picons_path)
finally:
GLib.idle_add(self._cancel_button.hide)
self._terminate = False
self._is_downloading = False
def process_provider(self, prv):
url = prv.url
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
exe = "{}wget".format("./" if GTK_PATH else "")
self._current_process = subprocess.Popen([exe, "-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 get_picons_for_lyngsat(self, path, providers):
import concurrent.futures
def write_to_buffer(self, fd, condition):
if condition == GLib.IO_IN:
char = fd.read(1)
self.append_output(char)
return char
return False
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
picons = []
# Getting links to picons.
futures = {executor.submit(self.process_provider, p, path): p for p in providers}
for future in concurrent.futures.as_completed(futures):
if not self._is_downloading:
executor.shutdown()
return
pic = future.result()
if pic:
picons.extend(pic)
# Getting picon images.
futures = {executor.submit(download_picon, *pic, self.append_output): pic for pic in picons}
done, not_done = concurrent.futures.wait(futures, timeout=0)
while self._is_downloading and not_done:
done, not_done = concurrent.futures.wait(not_done, timeout=5)
for future in not_done:
future.cancel()
concurrent.futures.wait(not_done)
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
def get_picons_for_picon_cz(self, path, providers):
p_ids = None
if self._bouquet_filter_switch.get_active():
p_ids = self.get_bouquet_picon_ids()
if not p_ids:
return
try:
# We download it sequentially.
for p in providers:
self._picon_cz_downloader.download(p, path, p_ids)
except PiconsError as e:
self.append_output("Error: {}\n".format(str(e)))
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
def get_bouquet_picon_ids(self):
""" Returns picon ids for selected bouquet or None. """
bq_selected = self._app.check_bouquet_selection()
if not bq_selected:
return
model, paths = self._app.bouquets_view.get_selection().get_selected_rows()
if len(paths) > 1:
self.show_dialog("Please, select only one bouquet!", DialogType.ERROR)
return
fav_bouquet = self._app.current_bouquets[bq_selected]
services = self._app.current_services
return {services.get(fav_id).picon_id for fav_id in fav_bouquet}
def process_provider(self, prv, picons_path):
self.append_output("Getting links to picons for: {}.\n".format(prv.name))
return PiconsParser.parse(prv, picons_path, self._picon_ids, self.get_picons_format())
@run_idle
def append_output(self, char):
@@ -618,7 +774,7 @@ class PiconsDialog:
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:
if self._is_downloading and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return True
self.terminate_task()
@@ -626,18 +782,16 @@ class PiconsDialog:
@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)
self._is_downloading = False
self.show_info_message(get_message("The task is canceled!"), Gtk.MessageType.WARNING)
def on_close(self, window, event):
if self.on_cancel():
return True
self._terminate = True
self._is_downloading = False
self.save_window_size(window)
self.clean_data()
self._app.update_picons()
GLib.idle_add(self._dialog.destroy)
@@ -646,22 +800,16 @@ class PiconsDialog:
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._explorer_action_box.set_sensitive, False)
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._explorer_action_box.set_sensitive, True)
GLib.idle_add(self._header_download_box.set_sensitive, True)
if update:
self.on_picons_dest_changed(self._explorer_dest_path_button)
@@ -670,9 +818,10 @@ class PiconsDialog:
@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._info_bar.set_visible(False)
self._message_label.set_text(get_message(text))
self._info_bar.set_message_type(message_type)
self._info_bar.set_visible(True)
def on_picons_dir_open(self, entry, icon, event_button):
update_entry_data(entry, self._dialog, settings=self._settings)
@@ -715,8 +864,13 @@ class PiconsDialog:
self._filter_binding.unbind()
self._app.filter_entry.set_text("")
@run_with_delay(1)
@run_with_delay(0.5)
def on_picons_filter_changed(self, entry):
txt = entry.get_text().upper()
self._filter_cache.clear()
for s in self._app.current_services.values():
self._filter_cache[s.picon_id] = txt in s.service.upper()
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)
@@ -736,11 +890,10 @@ class PiconsDialog:
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())))
return txt in t.upper() or self._filter_cache.get(t, False)
def on_picon_activated(self, view):
if self._info_toggle_button.get_active():
if self._info_check_button.get_active():
model, path = view.get_selection().get_selected_rows()
if not path:
return
@@ -768,6 +921,20 @@ class PiconsDialog:
get_message("System"), srv.system, get_message("Freq"), srv.freq,
ref)
def on_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
dest = view.get_dest_row_at_pos(x, y)
if not dest:
return False
path, pos = dest
model = view.get_model()
row = model[path][:]
tooltip.set_icon(get_picon_pixbuf(row[-1], size=self._settings.tooltip_logo_size))
tooltip.set_text(row[1])
view.set_tooltip_row(tooltip, path)
return True
def on_tree_view_key_press(self, view, event):
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
@@ -780,7 +947,7 @@ class PiconsDialog:
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)
self._download_source_button.set_sensitive(suit if suit else False)
def on_position_edited(self, render, path, value):
model = self._providers_view.get_model()
@@ -790,8 +957,9 @@ class PiconsDialog:
def on_visible_page(self, stack: Gtk.Stack, param):
name = stack.get_visible_child_name()
self._convert_button.set_visible(name == "converter")
self._download_source_button.set_visible(name == "downloader")
is_explorer = name == "explorer"
self._explorer_action_box.set_visible(is_explorer)
self._filter_button.set_visible(is_explorer)
if is_explorer:
self.on_picons_dest_changed(self._explorer_dest_path_button)
@@ -836,9 +1004,6 @@ class PiconsDialog:
return picon_format
def is_task_running(self):
return self._current_process and self._current_process.poll() is None
if __name__ == "__main__":
pass

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,18 @@
import concurrent.futures
import re
import time
import concurrent.futures
from math import fabs
from gi.repository import GLib
from app.commons import run_idle, run_task
from app.commons import run_idle, run_task, log
from app.eparser import get_satellites, write_satellites, Satellite, Transponder
from app.eparser.ecommons import PLS_MODE, get_key_by_value
from app.tools.satellites import SatellitesParser, SatelliteSource
from .dialogs import show_dialog, DialogType, get_dialogs_string, get_chooser_dialog
from app.tools.satellites import SatellitesParser, SatelliteSource, ServicesParser
from .dialogs import show_dialog, DialogType, get_chooser_dialog, get_message, get_builder
from .main_helper import move_items, scroll_to, append_text_to_tview, get_base_model, on_popup_menu
from .search import SearchProvider
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, 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"
@@ -44,13 +44,9 @@ class SatellitesDialog:
"on_resize": self.on_resize,
"on_quit": self.on_quit}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH),
("satellites_editor_window", "satellites_tree_store", "popup_menu",
"left_header_menu", "popup_menu_add_image", "popup_menu_add_image_2",
"sat_editor_save_image", "sat_editor_update_image"))
builder.connect_signals(handlers)
builder = get_builder(_UI_PATH, handlers, use_str=True,
objects=("satellites_editor_window", "satellites_tree_store", "popup_menu",
"left_header_menu", "popup_menu_add_image", "popup_menu_add_image_2"))
self._window = builder.get_object("satellites_editor_window")
self._window.set_transient_for(transient)
@@ -254,13 +250,22 @@ class SatellitesDialog:
return paths
@staticmethod
def on_remove(view):
@run_idle
def on_remove(self, view):
""" Removal of selected satellites and transponders.
The satellites are removed first! Then transponders.
"""
selection = view.get_selection()
model, paths = selection.get_selected_rows()
for itr in [model.get_iter(path) for path in paths]:
model.remove(itr)
itrs = [model.get_iter(path) for path in paths]
satellites = list(filter(model.iter_has_child, itrs))
if len(satellites):
# Removing selected satellites.
list(map(model.remove, satellites))
else:
# Removing selected transponders.
list(map(model.remove, itrs))
@run_idle
def on_save(self, view):
@@ -280,7 +285,7 @@ class SatellitesDialog:
@run_idle
def on_update(self, item):
SatellitesUpdateDialog(self._window, self._sat_view.get_model()).show()
SatellitesUpdateDialog(self._window, self._settings, self._sat_view.get_model()).show()
@staticmethod
def parse_data(model, path, itr, sats):
@@ -307,13 +312,8 @@ class TransponderDialog:
def __init__(self, transient, transponder: Transponder = None):
handlers = {"on_entry_changed": self.on_entry_changed}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("transponder_dialog", "pol_store", "fec_store", "mod_store", "system_store",
"pls_mode_store"))
builder.connect_signals(handlers)
objects = ("transponder_dialog", "pol_store", "fec_store", "mod_store", "system_store", "pls_mode_store")
builder = get_builder(_UI_PATH, handlers, use_str=True, objects=objects)
self._dialog = builder.get_object("transponder_dialog")
self._dialog.set_transient_for(transient)
@@ -327,7 +327,7 @@ class TransponderDialog:
self._pls_code_entry = builder.get_object("pls_code_entry")
self._is_id_entry = builder.get_object("is_id_entry")
# pattern for frequency and rate entries (only digits)
self._pattern = re.compile("\D")
self._pattern = re.compile(r"\D")
# style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
@@ -392,10 +392,7 @@ class SatelliteDialog:
""" Shows dialog for adding or edit satellite """
def __init__(self, transient, satellite: Satellite = None):
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("satellite_dialog", "side_store", "pos_adjustment"))
builder = get_builder(_UI_PATH, use_str=True, objects=("satellite_dialog", "side_store", "pos_adjustment"))
self._dialog = builder.get_object("satellite_dialog")
self._dialog.set_transient_for(transient)
@@ -429,16 +426,17 @@ class SatelliteDialog:
return Satellite(name=name, flags="0", position=pos, transponders=None)
# ***************** Satellite update dialog *******************#
# ********************** Update dialogs ************************ #
class SatellitesUpdateDialog:
""" Dialog for update satellites over internet """
class UpdateDialog:
""" Base dialog for update satellites, transponders and services from the web."""
def __init__(self, transient, main_model):
def __init__(self, transient, settings, title=None):
handlers = {"on_update_satellites_list": self.on_update_satellites_list,
"on_receive_satellites_list": self.on_receive_satellites_list,
"on_receive_data": self.on_receive_data,
"on_cancel_receive": self.on_cancel_receive,
"on_selected_toggled": self.on_selected_toggled,
"on_satellite_toggled": self.on_satellite_toggled,
"on_transponder_toggled": self.on_transponder_toggled,
"on_info_bar_close": self.on_info_bar_close,
"on_filter_toggled": self.on_filter_toggled,
"on_find_toggled": self.on_find_toggled,
@@ -451,27 +449,33 @@ class SatellitesUpdateDialog:
"on_search_up": self.on_search_up,
"on_quit": self.on_quit}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
("satellites_update_window", "update_source_store", "update_sat_list_store",
self._settings = settings
self._download_task = False
self._parser = None
self._size_name = "{}_window_size".format("_".join(re.findall("[A-Z][^A-Z]*", self.__class__.__name__))).lower()
builder = get_builder(UI_RESOURCES_PATH + "satellites_dialog.glade", handlers,
objects=("satellites_update_window", "update_source_store", "update_sat_list_store",
"update_sat_list_model_filter", "update_sat_list_model_sort", "side_store",
"pos_adjustment", "pos_adjustment2", "satellites_update_popup_menu",
"remove_selection_image", "sat_update_cancel_image", "sat_receive_image",
"sat_update_filter_image", "sat_update_search_image", "sat_update_image"))
builder.connect_signals(handlers)
"remove_selection_image", "update_transponder_store", "update_service_store"))
self._window = builder.get_object("satellites_update_window")
self._window.set_transient_for(transient)
self._main_model = main_model
# self._dialog.get_content_area().set_border_width(0)
if title:
self._window.set_title(title)
self._transponder_paned = builder.get_object("sat_update_tr_paned")
self._sat_view = builder.get_object("sat_update_tree_view")
self._transponder_view = builder.get_object("sat_update_tr_view")
self._service_view = builder.get_object("sat_update_srv_view")
self._source_box = builder.get_object("source_combo_box")
self._sat_update_expander = builder.get_object("sat_update_expander")
self._text_view = builder.get_object("text_view")
self._receive_button = builder.get_object("receive_sat_list_tool_button")
self._receive_button = builder.get_object("receive_data_button")
self._sat_update_info_bar = builder.get_object("sat_update_info_bar")
self._info_bar_message_label = builder.get_object("info_bar_message_label")
self._receive_button.bind_property("visible", builder.get_object("cancel_data_button"), "visible", 4)
# Filter
self._filter_bar = builder.get_object("sat_update_filter_bar")
self._from_pos_button = builder.get_object("from_pos_button")
@@ -487,21 +491,31 @@ class SatellitesUpdateDialog:
builder.get_object("sat_update_search_down_button"),
builder.get_object("sat_update_search_up_button"))
self._download_task = False
self._parser = None
window_size = self._settings.get(self._size_name)
if window_size:
self._window.resize(*window_size)
def show(self):
self._window.show()
@property
def is_download(self):
return self._download_task
@is_download.setter
def is_download(self, value):
self._download_task = value
self._receive_button.set_visible(not value)
@run_idle
def on_update_satellites_list(self, item):
if self._download_task:
if self.is_download:
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return
model = get_base_model(self._sat_view.get_model())
model.clear()
self._download_task = True
self.is_download = True
src = self._source_box.get_active()
if not self._parser:
self._parser = SatellitesParser()
@@ -510,10 +524,16 @@ class SatellitesUpdateDialog:
@run_task
def get_sat_list(self, src, callback):
sats = self._parser.get_satellites_list(SatelliteSource.FLYSAT if src == 0 else SatelliteSource.LYNGSAT)
sat_src = SatelliteSource.FLYSAT
if src == 1:
sat_src = SatelliteSource.LYNGSAT
elif src == 2:
sat_src = SatelliteSource.KINGOFSAT
sats = self._parser.get_satellites_list(sat_src)
if sats:
callback(sats)
self._download_task = False
self.is_download = False
@run_idle
def append_satellites(self, sats):
@@ -522,70 +542,16 @@ class SatellitesUpdateDialog:
model.append(sat)
@run_idle
def on_receive_satellites_list(self, item):
if self._download_task:
def on_receive_data(self, item):
if self.is_download:
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return
self.receive_satellites()
@run_task
def receive_satellites(self):
self._download_task = True
self.update_expander()
model = self._sat_view.get_model()
start = time.time()
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
text = "Processing: {}\n"
sats = []
appender = self.append_output()
next(appender)
futures = {executor.submit(self._parser.get_satellite, sat[:-1]): sat for sat in [r for r in model if r[4]]}
for future in concurrent.futures.as_completed(futures):
if not self._download_task:
self._download_task = True
executor.shutdown()
appender.send("\nCanceled\n")
appender.close()
self._download_task = False
return
data = future.result()
appender.send(text.format(data[0]))
sats.append(data)
appender.send("-" * 75 + "\n")
appender.send("Consumed : {:0.0f}s, {} satellites received.".format(start - time.time(), len(sats)))
appender.close()
sats = {s[2]: s for s in sats} # key = position, v = satellite
for row in self._main_model:
pos = row[-1]
if pos in sats:
sat = sats.pop(pos)
itr = row.iter
self.update_satellite(itr, row, sat)
for sat in sats.values():
append_satellite(self._main_model, sat)
self._download_task = False
@run_idle
def update_expander(self):
self._sat_update_expander.set_expanded(True)
self._text_view.get_buffer().set_text("", 0)
@run_idle
def update_satellite(self, itr, row, sat):
if self._main_model.iter_has_child(itr):
children = row.iterchildren()
for ch in children:
self._main_model.remove(ch.iter)
for tr in sat[3]:
self._main_model.append(itr, ["Transponder:", *tr, None, None])
def append_output(self):
@run_idle
def append(t):
@@ -598,11 +564,15 @@ class SatellitesUpdateDialog:
def on_cancel_receive(self, item=None):
self._download_task = False
def on_selected_toggled(self, toggle, path):
def on_satellite_toggled(self, toggle, path):
model = self._sat_view.get_model()
self.update_state(model, path, not toggle.get_active())
self.update_receive_button_state(self._filter_model)
def on_transponder_toggled(self, toggle, path):
model = self._transponder_view.get_model()
model.set_value(model.get_iter(path), 2, not toggle.get_active())
@run_idle
def update_receive_button_state(self, model):
self._receive_button.set_sensitive((any(r[4] for r in model)))
@@ -627,7 +597,7 @@ class SatellitesUpdateDialog:
self._filter_positions = self.get_positions()
self._filter_model.refilter()
def filter_function(self, model, iter, data):
def filter_function(self, model, itr, data):
if self._filter_model is None or self._filter_model == "None":
return True
@@ -638,7 +608,7 @@ class SatellitesUpdateDialog:
if from_pos > to_pos:
from_pos, to_pos = to_pos, from_pos
return from_pos <= float(self._parser.get_position(model.get(iter, 1)[0])) <= to_pos
return from_pos <= float(self._parser.get_position(model.get(itr, 1)[0])) <= to_pos
def get_positions(self):
from_pos = round(self._from_pos_button.get_value(), 1) * (-1 if self._filter_from_combo_box.get_active() else 1)
@@ -671,10 +641,299 @@ class SatellitesUpdateDialog:
self._filter_model.get_model().set_value(itr, 4, select)
def on_quit(self, window, event):
self._download_task = False
self._settings.add(self._size_name, window.get_size())
self.is_download = False
# ***************** Commons *******************#
class SatellitesUpdateDialog(UpdateDialog):
""" Dialog for update satellites from the web. """
def __init__(self, transient, settings, main_model):
super().__init__(transient=transient, settings=settings)
self._main_model = main_model
@run_idle
def on_receive_data(self, item):
if self.is_download:
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return
self.receive_satellites()
@run_task
def receive_satellites(self):
self.is_download = True
self.update_expander()
model = self._sat_view.get_model()
start = time.time()
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
text = "Processing: {}\n"
sats = []
appender = self.append_output()
next(appender)
futures = {executor.submit(self._parser.get_satellite, sat[:-1]): sat for sat in [r for r in model if r[4]]}
for future in concurrent.futures.as_completed(futures):
if not self.is_download:
self.is_download = True
executor.shutdown()
appender.send("\nCanceled\n")
appender.close()
self.is_download = False
return
data = future.result()
appender.send(text.format(data[0]))
sats.append(data)
appender.send("-" * 75 + "\n")
appender.send("Consumed: {:0.0f}s, {} satellites received.".format(time.time() - start, len(sats)))
appender.close()
sats = {s[2]: s for s in sats} # key = position, v = satellite
for row in self._main_model:
pos = row[-1]
if pos in sats:
sat = sats.pop(pos)
itr = row.iter
self.update_satellite(itr, row, sat)
for sat in sats.values():
append_satellite(self._main_model, sat)
self.is_download = False
@run_idle
def update_satellite(self, itr, row, sat):
if self._main_model.iter_has_child(itr):
children = row.iterchildren()
for ch in children:
self._main_model.remove(ch.iter)
for tr in sat[3]:
self._main_model.append(itr, ["Transponder:", *tr, None, None])
class ServicesUpdateDialog(UpdateDialog):
""" Dialog for updating services from the web. """
def __init__(self, transient, settings, callback):
super().__init__(transient=transient, settings=settings, title="Services update")
self._callback = callback
self._satellite_paths = {}
self._transponders = {}
self._services = {}
self._selected_transponders = set()
self._services_parser = ServicesParser(source=SatelliteSource.LYNGSAT)
self._transponder_paned.set_visible(True)
self._source_box.remove(0)
self._source_box.remove(1)
self._source_box.set_active(0)
# Transponder view popup menu
tr_popup_menu = Gtk.Menu()
select_all_item = Gtk.ImageMenuItem.new_from_stock("gtk-select-all")
select_all_item.connect("activate", lambda w: self.update_transponder_selection(True))
tr_popup_menu.append(select_all_item)
remove_selection_item = Gtk.ImageMenuItem.new_from_stock("gtk-undo")
remove_selection_item.set_label(get_message("Remove selection"))
remove_selection_item.connect("activate", lambda w: self.update_transponder_selection(False))
tr_popup_menu.append(remove_selection_item)
tr_popup_menu.show_all()
self._sat_view.connect("row-activated", self.on_activate_satellite)
self._transponder_view.connect("row-activated", self.on_activate_transponder)
self._transponder_view.connect("button-press-event", lambda w, e: on_popup_menu(tr_popup_menu, e))
self._transponder_view.connect("select_all", lambda w: self.update_transponder_selection(True))
@run_idle
def on_receive_data(self, item):
if self.is_download:
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return
self.receive_services()
@run_task
def receive_services(self):
self.is_download = True
self.update_expander()
model = self._sat_view.get_model()
appender = self.append_output()
next(appender)
start = time.time()
non_cached_sats = []
sat_names = {}
t_names = {}
t_urls = []
services = []
for r in (r for r in model if r[-1]):
if not self.is_download:
appender.send("\nCanceled\n")
return
sat, url = r[0], r[3]
trs = self._transponders.get(url, None)
if trs:
for t in filter(lambda tp: tp.url in self._selected_transponders, trs):
t_urls.append(t.url)
t_names[t.url] = t.text
else:
non_cached_sats.append(url)
sat_names[url] = sat
if non_cached_sats:
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
futures = {executor.submit(self._services_parser.get_transponders_links, u): u for u in non_cached_sats}
for future in concurrent.futures.as_completed(futures):
if not self.is_download:
appender.send("\nCanceled.\n")
self.is_download = False
return
appender.send("Getting transponders for: {}.\n".format(sat_names.get(futures[future])))
for t in future.result():
t_urls.append(t.url)
t_names[t.url] = t.text
appender.send("-" * 75 + "\n")
appender.send("{} transponders received.\n\n".format(len(t_urls)))
non_cached_ts = []
for tr in t_urls:
srvs = self._services.get(tr)
services.extend(srvs) if srvs else non_cached_ts.append(tr)
if non_cached_ts:
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
futures = {executor.submit(self._services_parser.get_transponder_services, u): u for u in non_cached_ts}
for future in concurrent.futures.as_completed(futures):
if not self.is_download:
appender.send("\nCanceled.\n")
self.is_download = False
return
appender.send("Getting services for: {}.\n".format(t_names.get(futures[future], "")))
list(map(services.append, future.result()))
appender.send("-" * 75 + "\n")
appender.send("Consumed: {:0.0f}s, {} services received.".format(time.time() - start, len(services)))
try:
from app.eparser.enigma.lamedb import LameDbReader
# Used for double checking!
reader = LameDbReader(path=None)
srvs = reader.get_services_list("".join(reader.get_services_lines(services)))
except ValueError as e:
log("ServicesUpdateDialog [on receive data] error: {}".format(e))
else:
self._callback(srvs)
self.is_download = False
@run_task
def get_sat_list(self, src, callback):
sats = self._parser.get_satellites_list(SatelliteSource.LYNGSAT)
if sats:
callback(sats)
self.is_download = False
def on_satellite_toggled(self, toggle, path):
model = self._sat_view.get_model()
self.update_state(model, path, not toggle.get_active())
self.update_receive_button_state(self._filter_model)
url = model.get_value(model.get_iter(path), 3)
selected = toggle.get_active()
transponders = self._transponders.get(url, None)
if transponders:
for t in transponders:
self._selected_transponders.add(t.url) if selected else self._selected_transponders.discard(t.url)
def on_transponder_toggled(self, toggle, path):
model = self._transponder_view.get_model()
itr = model.get_iter(path)
active = not toggle.get_active()
url = self.update_transponder_state(itr, model, active)
s_path = self._satellite_paths.get(url, None)
if s_path:
self.update_sat_state(model, s_path, active)
def update_sat_state(self, model, path, active):
sat_model = self._sat_view.get_model()
if active:
self.update_state(sat_model, path, active)
else:
self.update_state(sat_model, path, any((r[-1] for r in model)))
self.update_receive_button_state(self._filter_model)
def update_transponder_state(self, itr, model, active):
model.set_value(itr, 2, active)
url = model.get_value(itr, 1)
self._selected_transponders.add(url) if active else self._selected_transponders.discard(url)
return url
@run_task
def on_activate_satellite(self, view, path, column):
model = view.get_model()
itr = model.get_iter(path)
url, selected = model.get_value(itr, 3), model.get_value(itr, 4)
transponders = self._transponders.get(url, None)
if transponders is None:
GLib.idle_add(view.set_sensitive, False)
transponders = self._services_parser.get_transponders_links(url)
self._transponders[url] = transponders
for t in transponders:
t_url = t.url
self._satellite_paths[t_url] = path
self._selected_transponders.add(t_url) if selected else self._selected_transponders.discard(t_url)
self.append_transponders(self._transponder_view.get_model(), transponders)
@run_idle
def append_transponders(self, model, trs_list):
model.clear()
list(map(model.append, [(t.text, t.url, t.url in self._selected_transponders) for t in trs_list]))
self._sat_view.set_sensitive(True)
@run_task
def on_activate_transponder(self, view, path, column):
url = view.get_model()[path][1]
services = self._services.get(url, None)
if services is None:
GLib.idle_add(view.set_sensitive, False)
services = self._services_parser.get_transponder_services(url)
self._services[url] = services
self.append_services(self._service_view.get_model(), services)
@run_idle
def append_services(self, model, srv_list):
model.clear()
for s in srv_list:
model.append((None, s.service, s.package, s.service_type, str(s.ssid), None))
self._transponder_view.set_sensitive(True)
def update_transponder_selection(self, select):
m = self._transponder_view.get_model()
if not len(m):
return
s_path = self._satellite_paths.get({self.update_transponder_state(r.iter, m, select) for r in m}.pop(), None)
if s_path:
self.update_sat_state(m, s_path, select)
# ************************* Commons ************************* #
@run_idle
def append_satellite(model, sat):

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,37 +40,37 @@
</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>
@@ -63,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>
@@ -80,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>
@@ -103,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>
@@ -120,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>
@@ -137,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>
@@ -157,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>
@@ -223,10 +253,10 @@
</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>
@@ -246,6 +276,41 @@
<child type="titlebar">
<placeholder/>
</child>
<child type="action">
<object class="GtkButton" id="cancel_button">
<property name="label">gtk-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>
<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="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>
<signal name="clicked" handler="on_save" swapped="no"/>
</object>
</child>
<child type="action">
<object class="GtkButton" id="create_button">
<property name="label">gtk-new</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>
<signal name="clicked" handler="on_create_new" swapped="no"/>
</object>
</child>
<child internal-child="vbox">
<object class="GtkBox" id="dialog_vbox">
<property name="can_focus">False</property>
@@ -256,54 +321,6 @@
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="cancel_button">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="apply_button">
<property name="label" translatable="yes">Apply</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Save current service</property>
<property name="valign">center</property>
<signal name="clicked" handler="on_save" swapped="no"/>
<accelerator key="Return" signal="activate"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="create_button">
<property name="label" translatable="yes">Create</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Create and save as new service</property>
<property name="valign">center</property>
<signal name="clicked" handler="on_create_new" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -328,7 +345,7 @@
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<property name="spacing">5</property>
<child>
<object class="GtkGrid" id="srv_grid">
<property name="visible">True</property>
@@ -349,7 +366,7 @@
<object class="GtkEntry" id="name_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_stock">gtk-edit</property>
</object>
<packing>
<property name="left_attach">0</property>
@@ -371,7 +388,7 @@
<object class="GtkEntry" id="package_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_stock">gtk-edit</property>
</object>
<packing>
<property name="left_attach">1</property>
@@ -395,7 +412,7 @@
<property name="can_focus">True</property>
<property name="width_chars">10</property>
<property name="max_width_chars">10</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_stock">gtk-edit</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
<signal name="key-release-event" handler="update_reference" swapped="no"/>
</object>
@@ -440,7 +457,7 @@
<property name="can_focus">True</property>
<property name="width_chars">7</property>
<property name="max_width_chars">7</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_stock">gtk-edit</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
<signal name="changed" handler="update_reference" swapped="no"/>
</object>
@@ -790,10 +807,14 @@
<object class="GtkBox" id="flags_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">10</property>
<child>
<object class="GtkGrid" id="flags_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="margin_left">5</property>
<property name="margin_right">10</property>
<property name="column_spacing">2</property>
<child>
<object class="GtkLabel" id="flags_label">
@@ -866,6 +887,47 @@
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="extra_flags_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel" id="extra_pids_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Extra:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="extra_pids_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="width_chars">20</property>
<property name="max_width_chars">20</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="placeholder_text">c:000000,etc.</property>
<signal name="changed" handler="on_extra_pids_entry_changed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="caids_grid">
<property name="visible">True</property>
@@ -876,10 +938,10 @@
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="tooltip_text">C:0000,C:a1b2,etc.</property>
<property name="width_chars">15</property>
<property name="max_width_chars">26</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="placeholder_text" translatable="yes">C:0000,C:a1b2,etc.</property>
<property name="width_chars">20</property>
<property name="max_width_chars">20</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="placeholder_text">C:0000,C:a1b2,etc.</property>
<signal name="changed" handler="on_cas_entry_changed" swapped="no"/>
</object>
<packing>
@@ -903,7 +965,7 @@
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
<property name="position">2</property>
</packing>
</child>
</object>
@@ -981,7 +1043,7 @@
<property name="can_focus">True</property>
<property name="width_chars">12</property>
<property name="max_width_chars">12</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_stock">gtk-edit</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
</object>
<packing>
@@ -1007,7 +1069,7 @@
<property name="can_focus">True</property>
<property name="width_chars">12</property>
<property name="max_width_chars">12</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_stock">gtk-edit</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
</object>
<packing>
@@ -1086,7 +1148,7 @@
<property name="can_focus">True</property>
<property name="width_chars">12</property>
<property name="max_width_chars">12</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_stock">gtk-edit</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
<signal name="key-release-event" handler="update_reference" swapped="no"/>
</object>
@@ -1106,21 +1168,6 @@
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="sat_pos_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="input_purpose">number</property>
<property name="adjustment">sat_pos_adjustment</property>
<property name="digits">1</property>
<property name="numeric">True</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="tid_label">
<property name="visible">True</property>
@@ -1139,7 +1186,7 @@
<property name="can_focus">True</property>
<property name="width_chars">8</property>
<property name="max_width_chars">10</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_stock">gtk-edit</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
<signal name="key-release-event" handler="update_reference" swapped="no"/>
</object>
@@ -1166,7 +1213,7 @@
<property name="can_focus">True</property>
<property name="width_chars">8</property>
<property name="max_width_chars">10</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_stock">gtk-edit</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
<signal name="key-release-event" handler="update_reference" swapped="no"/>
</object>
@@ -1175,6 +1222,50 @@
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="sat_pos_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">1</property>
<child>
<object class="GtkSpinButton" id="sat_pos_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="input_purpose">number</property>
<property name="adjustment">sat_pos_adjustment</property>
<property name="digits">1</property>
<property name="numeric">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="pos_side_box">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="active">0</property>
<items>
<item id="E">E</item>
<item id="W">W</item>
</items>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -1331,7 +1422,7 @@
<property name="can_focus">True</property>
<property name="width_chars">8</property>
<property name="max_width_chars">10</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_stock">gtk-edit</property>
<signal name="changed" handler="on_digit_entry_changed" swapped="no"/>
</object>
<packing>
@@ -1357,7 +1448,7 @@
<property name="can_focus">True</property>
<property name="width_chars">8</property>
<property name="max_width_chars">10</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_stock">gtk-edit</property>
<signal name="changed" handler="on_digit_entry_changed" swapped="no"/>
</object>
<packing>
@@ -1383,7 +1474,7 @@
<property name="can_focus">True</property>
<property name="width_chars">8</property>
<property name="max_width_chars">10</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_stock">gtk-edit</property>
<signal name="changed" handler="on_digit_entry_changed" swapped="no"/>
</object>
<packing>
@@ -1512,7 +1603,7 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="icon_name">document-edit-symbolic</property>
<property name="stock">gtk-edit</property>
</object>
<packing>
<property name="expand">False</property>
@@ -1576,6 +1667,26 @@
<child>
<placeholder/>
</child>
<child type="action">
<object class="GtkButton" id="tr_services_no_button">
<property name="label">gtk-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>
</object>
</child>
<child type="action">
<object class="GtkButton" id="tr_services_ok_button">
<property name="label">gtk-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>
</object>
</child>
<child internal-child="vbox">
<object class="GtkBox" id="tr_services_dialog_vbox">
<property name="can_focus">False</property>
@@ -1584,35 +1695,6 @@
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="valign">center</property>
<child>
<object class="GtkButton" id="tr_services_no_button">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="tr_services_ok_button">
<property name="label" translatable="yes">OK</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -1639,7 +1721,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,15 +1,44 @@
import re
import os
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
from app.commons import run_idle
import os
import re
from app.commons import run_idle, log
from app.eparser import Service
from app.eparser.ecommons import MODULATION, Inversion, ROLL_OFF, Pilot, Flag, Pids, POLARIZATION, \
get_key_by_value, get_value_by_name, FEC_DEFAULT, PLS_MODE, SERVICE_TYPE, T_MODULATION, C_MODULATION, TrType, \
SystemCable, T_SYSTEM, BANDWIDTH, TRANSMISSION_MODE, GUARD_INTERVAL, HIERARCHY, T_FEC
from app.eparser.ecommons import (MODULATION, Inversion, ROLL_OFF, Pilot, Flag, Pids, POLARIZATION, get_key_by_value,
get_value_by_name, FEC_DEFAULT, PLS_MODE, SERVICE_TYPE, T_MODULATION, C_MODULATION,
TrType, SystemCable, T_SYSTEM, BANDWIDTH, TRANSMISSION_MODE, GUARD_INTERVAL, T_FEC,
HIERARCHY, A_MODULATION)
from app.settings import SettingsType
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, TEXT_DOMAIN, CODED_ICON, Column, IS_GNOME_SESSION
from .dialogs import show_dialog, DialogType, Action, get_dialogs_string
from .dialogs import show_dialog, DialogType, Action, get_builder
from .main_helper import get_base_model
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, CODED_ICON, Column
_UI_PATH = UI_RESOURCES_PATH + "service_details_dialog.glade"
@@ -41,19 +70,18 @@ class ServiceDetailsDialog:
"on_tr_edit_toggled": self.on_tr_edit_toggled,
"update_reference": self.update_reference,
"on_cas_entry_changed": self.on_cas_entry_changed,
"on_extra_pids_entry_changed": self.on_extra_pids_entry_changed,
"on_digit_entry_changed": self.on_digit_entry_changed,
"on_non_empty_entry_changed": self.on_non_empty_entry_changed}
"on_non_empty_entry_changed": self.on_non_empty_entry_changed,
"on_cancel": lambda item: self._dialog.destroy()}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION))
builder.connect_signals(handlers)
builder = get_builder(_UI_PATH, handlers, use_str=True)
self._builder = builder
self._dialog = builder.get_object("service_details_dialog")
self._dialog.set_transient_for(transient)
self._s_type = settings.setting_type
self._tr_type = None
self._tr_type = TrType.Satellite
self._satellites_xml_path = settings.data_local_path + "satellites.xml"
self._picons_dir_path = settings.picons_local_path
self._services_view = srv_view
@@ -70,6 +98,7 @@ class ServiceDetailsDialog:
self._DIGIT_PATTERN = re.compile("\\D")
self._NON_EMPTY_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
self._CAID_PATTERN = re.compile("(?:^[\\s]*$)|(C:[0-9a-fA-F]{1,4})(,C:[0-9a-fA-F]{1,4})*")
self._PIDS_PATTERN = re.compile("(?:^[\\s]*$)|(c:[0-9]{2}[0-9a-fA-F]{4})(,c:[0-9]{2}[0-9a-fA-F]{4})*")
# Buttons
self._apply_button = builder.get_object("apply_button")
self._create_button = builder.get_object("create_button")
@@ -105,6 +134,7 @@ class ServiceDetailsDialog:
self._stream_id_entry = self._digit_elements.get("stream_id_entry")
self._tr_flag_entry = self._digit_elements.get("tr_flag_entry")
self._namespace_entry = self._non_empty_elements.get("namespace_entry")
self._extra_pids_entry = builder.get_object("extra_pids_entry")
# Service elements
self._name_entry = builder.get_object("name_entry")
self._package_entry = builder.get_object("package_entry")
@@ -119,6 +149,7 @@ class ServiceDetailsDialog:
self._pids_grid = builder.get_object("pids_grid")
# Transponder elements
self._sat_pos_button = builder.get_object("sat_pos_button")
self._pos_side_box = builder.get_object("pos_side_box")
self._pol_combo_box = builder.get_object("pol_combo_box")
self._fec_combo_box = builder.get_object("fec_combo_box")
self._rate_lp_combo_box = builder.get_object("rate_lp_combo_box")
@@ -136,7 +167,7 @@ class ServiceDetailsDialog:
self._TRANSPONDER_ELEMENTS = (self._sat_pos_button, self._pol_combo_box, self._invertion_combo_box,
self._sys_combo_box, self._freq_entry, self._transponder_id_entry,
self._network_id_entry, self._namespace_entry, self._fec_combo_box,
self._rate_entry, self._rate_lp_combo_box)
self._rate_entry, self._rate_lp_combo_box, self._pos_side_box)
if self._action is Action.EDIT:
self.update_data_elements()
@@ -144,12 +175,7 @@ class ServiceDetailsDialog:
self.init_default_data_elements()
def show(self):
response = self._dialog.run()
if response == Gtk.ResponseType.OK:
pass
self._dialog.destroy()
return response
self._dialog.show()
@run_idle
def init_default_data_elements(self):
@@ -208,6 +234,8 @@ class ServiceDetailsDialog:
self.update_ui_for_terrestrial()
elif self._tr_type is TrType.Cable:
self.update_ui_for_cable()
elif self._tr_type is TrType.ATSC:
self.update_ui_for_atsc()
else:
self.set_sat_positions(srv.pos)
@@ -247,6 +275,7 @@ class ServiceDetailsDialog:
def init_enigma2_pids(self, flags):
pids = list(filter(lambda x: x.startswith("c:"), flags))
if pids:
extra_pids = []
for pid in pids:
if pid.startswith(Pids.VIDEO.value):
self._video_pid_entry.set_text(str(int(pid[4:], 16)))
@@ -259,15 +288,19 @@ class ServiceDetailsDialog:
elif pid.startswith(Pids.AC3.value):
self._ac3_pid_entry.set_text(str(int(pid[4:], 16)))
elif pid.startswith(Pids.VIDEO_TYPE.value):
pass
extra_pids.append(pid)
elif pid.startswith(Pids.AUDIO_CHANNEL.value):
pass
extra_pids.append(pid)
elif pid.startswith(Pids.BIT_STREAM_DELAY.value):
self._bitstream_entry.set_text(str(int(pid[4:], 16)))
elif pid.startswith(Pids.PCM_DELAY.value):
self._pcm_entry.set_text(str(int(pid[4:], 16)))
elif pid.startswith(Pids.SUBTITLE.value):
pass
extra_pids.append(pid)
else:
extra_pids.append(pid)
self._extra_pids_entry.set_text(",".join(extra_pids))
def init_enigma2_transponder_data(self, srv):
""" Transponder data initialisation """
@@ -309,6 +342,11 @@ class ServiceDetailsDialog:
self.select_active_text(self._pls_mode_combo_box, HIERARCHY.get(tr_data[7]))
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[8]).name)
self.select_active_text(self._sys_combo_box, T_SYSTEM.get(tr_data[9]))
elif tr_type is TrType.ATSC:
self._sys_combo_box.set_active(0)
self.select_active_text(self._mod_combo_box, A_MODULATION.get(tr_data[2]))
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[1]).name)
# Should be called last to properly initialize the reference
self._srv_type_entry.set_text(data[4])
@@ -335,7 +373,8 @@ class ServiceDetailsDialog:
def set_sat_positions(self, sat_pos):
""" Sat positions initialisation """
self._sat_pos_button.set_value(float(sat_pos))
self._sat_pos_button.set_value(float(sat_pos[:-1]))
self._pos_side_box.set_active_id(sat_pos[-1:])
def on_system_changed(self, box):
if not self._tr_edit_switch.get_active():
@@ -375,18 +414,19 @@ class ServiceDetailsDialog:
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
self.on_edit() if self._action is Action.EDIT else self.on_new()
self._dialog.destroy()
if self.on_edit() if self._action is Action.EDIT else self.on_new():
self._dialog.destroy()
def on_new(self):
""" Create new service. """
service = self.get_service(*self.get_srv_data(), self.get_satellite_transponder_data())
show_dialog(DialogType.ERROR, transient=self._dialog, text="Not implemented yet!")
return True
def on_edit(self):
""" Edit current service. """
fav_id, data_id = self.get_srv_data()
# transponder
# Transponder
transponder = self._old_service.transponder
if self._tr_edit_switch.get_active():
try:
@@ -396,17 +436,24 @@ class ServiceDetailsDialog:
transponder = self.get_terrestrial_transponder_data()
elif self._tr_type is TrType.Cable:
transponder = self.get_cable_transponder_data()
elif self._tr_type is TrType.ATSC:
transponder = self.get_atsc_transponder_data()
except Exception as e:
print(e)
log("Edit service error: {}".format(e))
show_dialog(DialogType.ERROR, transient=self._dialog, text="Error getting transponder parameters!")
else:
if self._transponder_services_iters:
self.update_transponder_services(transponder)
# service
self.update_transponder_services(transponder, self.get_sat_position())
# Service
service = self.get_service(fav_id, data_id, transponder)
old_fav_id = self._old_service.fav_id
if old_fav_id != fav_id:
if fav_id in self._services:
msg = "{}\n\n\t{}".format("A similar service is already in this list!", "Are you sure?")
if show_dialog(DialogType.QUESTION, transient=self._dialog, text=msg) != Gtk.ResponseType.OK:
return False
self.update_bouquets(fav_id, old_fav_id)
self._services[fav_id] = service
if self._old_service.picon_id != service.picon_id:
@@ -414,7 +461,7 @@ class ServiceDetailsDialog:
flags = service.flags_cas
extra_data = {Column.SRV_TOOLTIP: None, Column.SRV_BACKGROUND: None}
if flags:
if self._s_type is SettingsType.ENIGMA_2 and flags:
f_flags = list(filter(lambda x: x.startswith("f:"), flags.split(",")))
if f_flags and Flag.is_new(int(f_flags[0][2:])):
extra_data[Column.SRV_BACKGROUND] = self._new_color
@@ -423,6 +470,7 @@ class ServiceDetailsDialog:
self._current_model.set(self._current_itr, {i: v for i, v in enumerate(service)})
self.update_fav_view(self._old_service, service)
self._old_service = service
return True
def update_bouquets(self, fav_id, old_fav_id):
self._services.pop(old_fav_id, None)
@@ -438,14 +486,17 @@ class ServiceDetailsDialog:
def update_fav_view(self, old_service, new_service):
model = self._fav_view.get_model()
for row in filter(lambda r: old_service.fav_id == r[7], model):
model.set(row.iter, {1: new_service.coded,
2: new_service.service,
3: new_service.locked,
4: new_service.hide,
5: new_service.service_type,
6: new_service.pos,
7: new_service.fav_id,
8: new_service.picon})
itr = row.iter
if not model.get_value(itr, Column.FAV_BACKGROUND):
model.set_value(itr, Column.FAV_SERVICE, new_service.service)
model.set(itr, {Column.FAV_CODED: new_service.coded,
Column.FAV_LOCKED: new_service.locked,
Column.FAV_HIDE: new_service.hide,
Column.FAV_TYPE: new_service.service_type,
Column.FAV_POS: new_service.pos,
Column.FAV_ID: new_service.fav_id,
Column.FAV_PICON: new_service.picon})
def update_picon_name(self, old_name, new_name):
if not os.path.isdir(self._picons_dir_path):
@@ -487,7 +538,9 @@ class ServiceDetailsDialog:
if self._s_type is SettingsType.ENIGMA_2:
return self.get_enigma2_flags()
elif self._s_type is SettingsType.NEUTRINO_MP:
return self._old_service.flags_cas
flags = self._old_service.flags_cas.split(":")
flags[1] = self.get_sat_position()
return ":".join(flags)
def get_enigma2_flags(self):
flags = ["p:{}".format(self._package_entry.get_text())]
@@ -517,6 +570,9 @@ class ServiceDetailsDialog:
pcm_pid = self._pcm_entry.get_text()
if pcm_pid:
flags.append("{}{:04x}".format(Pids.PCM_DELAY.value, int(pcm_pid)))
extra_pids = self._extra_pids_entry.get_text()
if extra_pids:
flags.append(extra_pids)
# flags
f_flags = Flag.KEEP.value if self._keep_check_button.get_active() else 0
f_flags = f_flags + Flag.HIDE.value if self._hide_check_button.get_active() else f_flags
@@ -539,7 +595,9 @@ class ServiceDetailsDialog:
return fav_id, data_id
elif self._s_type is SettingsType.NEUTRINO_MP:
fav_id = self._NEUTRINO_FAV_ID.format(tr_id, net_id, ssid)
return fav_id, self._old_service.data_id
data_id = self._old_service.data_id.split(":")
data_id[1] = "{:x}".format(int(service_type))
return fav_id, ":".join(data_id)
# ***************** Transponder ********************* #
@@ -547,27 +605,27 @@ class ServiceDetailsDialog:
freq = self._freq_entry.get_text()
fec = self._fec_combo_box.get_active_id()
system = self._sys_combo_box.get_active_id()
o_srv = self._old_service
if self._tr_type is TrType.Satellite or self._s_type is SettingsType.NEUTRINO_MP:
freq = self._freq_entry.get_text()
rate = self._rate_entry.get_text()
pol = self._pol_combo_box.get_active_id()
pos = str(round(self._sat_pos_button.get_value(), 1))
pos = "{}{}".format(round(self._sat_pos_button.get_value(), 1), self._pos_side_box.get_active_id())
return freq, rate, pol, fec, system, pos
elif self._tr_type is TrType.Terrestrial:
o_srv = self._old_service
elif self._tr_type in (TrType.Terrestrial, TrType.ATSC):
return freq, o_srv.rate, o_srv.pol, fec, system, o_srv.pos
elif self._tr_type is TrType.Cable:
o_srv = self._old_service
return freq, self._rate_entry.get_text(), o_srv.pol, fec, o_srv.system, o_srv.pos
def get_satellite_transponder_data(self):
sys = self._sys_combo_box.get_active_id()
freq = self._freq_entry.get_text()
rate = self._rate_entry.get_text()
freq = "{}000".format(self._freq_entry.get_text())
rate = "{}000".format(self._rate_entry.get_text())
pol = self.get_value_from_combobox_id(self._pol_combo_box, POLARIZATION)
fec = self.get_value_from_combobox_id(self._fec_combo_box, FEC_DEFAULT)
sat_pos = str(round(self._sat_pos_button.get_value(), 1)).replace(".", "")
sat_pos = self.get_sat_position()
inv = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
srv_sys = "0" # !!!
@@ -591,12 +649,17 @@ class ServiceDetailsDialog:
srv_sys = None
return self._NEUTRINO_TRANSPONDER_DATA.format(tr_id, on_id, freq, inv, rate, fec, pol, mod, srv_sys)
def get_sat_position(self):
sat_pos = self._sat_pos_button.get_value() * (-1 if self._pos_side_box.get_active_id() == "W" else 1)
sat_pos = str(round(sat_pos, 1)).replace(".", "")
return sat_pos
def get_terrestrial_transponder_data(self):
tr_data = re.split("\s|:", self._old_service.transponder)
# frequency, bandwidth, code rate HP, code rate LP, modulation, transmission mode, guard interval, hierarchy,
# inversion, system, plp_id
# Bandwidth -> Pol, Rate HP -> FEC, TransmissionMode -> Roll off, GuardInterval -> Pilot, Hierarchy -> Pls Mode
tr_data[1] = self._freq_entry.get_text()
tr_data[1] = "{}000".format(self._freq_entry.get_text())
tr_data[2] = self.get_value_from_combobox_id(self._pol_combo_box, BANDWIDTH)
tr_data[3] = self.get_value_from_combobox_id(self._fec_combo_box, T_FEC)
tr_data[4] = self.get_value_from_combobox_id(self._rate_lp_combo_box, T_FEC)
@@ -606,28 +669,50 @@ class ServiceDetailsDialog:
tr_data[8] = self.get_value_from_combobox_id(self._pls_mode_combo_box, HIERARCHY)
tr_data[9] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
tr_data[10] = self.get_value_from_combobox_id(self._sys_combo_box, T_SYSTEM)
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
def get_cable_transponder_data(self):
tr_data = re.split("\s|:", self._old_service.transponder)
# frequency, symbol_rate, modulation, inversion, fec_inner, system;
tr_data[1] = self._freq_entry.get_text()
tr_data[2] = self._rate_entry.get_text()
tr_data[1] = "{}000".format(self._freq_entry.get_text())
tr_data[2] = "{}000".format(self._rate_entry.get_text())
tr_data[3] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
tr_data[4] = self.get_value_from_combobox_id(self._mod_combo_box, C_MODULATION)
tr_data[5] = self.get_value_from_combobox_id(self._fec_combo_box, FEC_DEFAULT)
tr_data[6] = get_value_by_name(SystemCable, self._sys_combo_box.get_active_id())
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
def update_transponder_services(self, transponder):
def get_atsc_transponder_data(self):
tr_data = re.split("\s|:", self._old_service.transponder)
# frequency, inversion, modulation, system
tr_data[1] = "{}000".format(self._freq_entry.get_text())
tr_data[2] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
tr_data[3] = self.get_value_from_combobox_id(self._mod_combo_box, A_MODULATION)
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
def update_transponder_services(self, transponder, sat_pos):
for itr in self._transponder_services_iters:
srv = self._current_model[itr][:Column.SRV_TOOLTIP]
srv = self._current_model[itr][:]
srv[Column.SRV_FREQ], srv[Column.SRV_RATE], srv[Column.SRV_POL], srv[Column.SRV_FEC], srv[
Column.SRV_SYSTEM], srv[Column.SRV_POS] = self.get_transponder_values()
srv[Column.SRV_TRANSPONDER] = transponder
srv = Service(*srv)
self._services[srv.fav_id] = self._services.pop(srv.fav_id)._replace(transponder=transponder)
self._current_model.set(itr, {i: v for i, v in enumerate(srv)})
fav_id = srv[Column.SRV_FAV_ID]
old_srv = self._services.pop(fav_id, None)
if not old_srv:
log("Update transponder services error: No service found for ID {}".format(srv[Column.SRV_FAV_ID]))
continue
if self._s_type is SettingsType.NEUTRINO_MP:
flags = srv[Column.SRV_CAS_FLAGS].split(":")
flags[1] = sat_pos
srv[Column.SRV_CAS_FLAGS] = ":".join(flags)
self._services[fav_id] = Service(*srv[:Column.SRV_TOOLTIP])
self._current_model.set_row(itr, srv)
# ***************** Others *********************#
@@ -647,6 +732,9 @@ class ServiceDetailsDialog:
def on_cas_entry_changed(self, entry):
entry.set_name("GtkEntry" if self._CAID_PATTERN.fullmatch(entry.get_text()) else self._DIGIT_ENTRY_NAME)
def on_extra_pids_entry_changed(self, entry):
entry.set_name("GtkEntry" if self._PIDS_PATTERN.fullmatch(entry.get_text()) else self._DIGIT_ENTRY_NAME)
def get_value_from_combobox_id(self, box: Gtk.ComboBox, dc: dict):
cb_id = box.get_active_id()
return get_key_by_value(dc, cb_id)
@@ -656,16 +744,16 @@ class ServiceDetailsDialog:
if active and self._action is Action.EDIT:
self._transponder_services_iters = []
response = TransponderServicesDialog(self._dialog,
self._current_model,
self._services_view,
self._old_service.transponder,
self._transponder_services_iters).show()
if response == Gtk.ResponseType.CANCEL or response == -4:
if response == Gtk.ResponseType.CANCEL or response == Gtk.ResponseType.DELETE_EVENT:
switch.set_active(False)
self._transponder_services_iters = None
return
self.update_dvb_s2_elements(active and (self._sys_combo_box.get_active_id() == "DVB-S2"
or self._old_service.transponder_type in "tc"))
or self._old_service.transponder_type in "tca"))
for elem in self._TRANSPONDER_ELEMENTS:
elem.set_sensitive(active)
@@ -679,6 +767,8 @@ class ServiceDetailsDialog:
return False
if self._cas_entry.get_name() == self._DIGIT_ENTRY_NAME:
return False
if self._extra_pids_entry.get_name() == self._DIGIT_ENTRY_NAME:
return False
return True
def update_reference(self, entry, event=None):
@@ -796,6 +886,18 @@ class ServiceDetailsDialog:
self._transponder_id_entry.set_max_width_chars(8)
self._network_id_entry.set_max_width_chars(8)
def update_ui_for_atsc(self):
self.update_ui_for_cable()
tr_grid = self._builder.get_object("tr_grid")
tr_grid.remove_column(1)
tr_grid.remove_column(1)
# Init models
fec_model, modulation_model, system_model = self.get_models_for_non_satellite()
system_model.append((TrType.ATSC.name,))
[modulation_model.append((v,)) for k, v in A_MODULATION.items()]
# Extra
self._namespace_entry.set_max_width_chars(25)
def get_transponder_grid_for_non_satellite(self):
self._pids_grid.set_visible(False)
tr_grid = self._builder.get_object("tr_grid")
@@ -813,23 +915,23 @@ class ServiceDetailsDialog:
class TransponderServicesDialog:
def __init__(self, transient, model, transponder, tr_iters):
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("tr_services_dialog", "transponder_services_liststore"))
def __init__(self, transient, services_view, transponder, tr_iters):
builder = get_builder(_UI_PATH, use_str=True, objects=("tr_services_dialog", "transponder_services_liststore"))
self._dialog = builder.get_object("tr_services_dialog")
self._dialog.set_transient_for(transient)
self._srv_model = builder.get_object("transponder_services_liststore")
self.append_services(model, transponder, tr_iters)
self.append_services(services_view, transponder, tr_iters)
builder.get_object("srv_list_dialog_info_bar").connect("response", lambda bar, resp: bar.hide())
def append_services(self, model, transponder, tr_iters):
def append_services(self, view, transponder, tr_iters):
model = view.get_model()
filter_model = model.get_model()
for row in model:
if row[Column.SRV_TRANSPONDER] == transponder:
self._srv_model.append((row[Column.SRV_SERVICE], row[Column.SRV_PACKAGE], row[Column.SRV_TYPE],
row[Column.SRV_SSID], row[Column.SRV_FREQ], row[Column.SRV_POS]))
tr_iters.append(model.get_iter(row.path))
itr = model.get_iter(row.path)
tr_iters.append(filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr)))
def show(self):
response = self._dialog.run()

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,18 @@
import os
import re
from enum import Enum
from app.commons import run_task, run_idle, log
from app.connections import test_telnet, test_ftp, TestException, test_http, HttpApiException
from app.settings import SettingsType, Settings, PlayStreamsMode
from app.ui.dialogs import show_dialog, DialogType, get_message, get_chooser_dialog
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
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON, APP_FONT
def show_settings_dialog(transient, options):
return SettingsDialog(transient, options).show()
class Property(Enum):
FTP = "ftp"
HTTP = "http"
TELNET = "telnet"
class SettingsDialog:
_DIGIT_ENTRY_NAME = "digit-entry"
_DIGIT_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
@@ -50,7 +43,6 @@ class SettingsDialog:
"on_profile_set_default": self.on_profile_set_default,
"on_lang_changed": self.on_lang_changed,
"on_main_settings_visible": self.on_main_settings_visible,
"on_network_settings_visible": self.on_network_settings_visible,
"on_http_use_ssl_toggled": self.on_http_use_ssl_toggled,
"on_click_mode_togged": self.on_click_mode_togged,
"on_play_mode_changed": self.on_play_mode_changed,
@@ -58,10 +50,11 @@ 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,
"on_icon_theme_changed": self.on_icon_theme_changed,
"on_appearance_changed": self.on_appearance_changed,
"on_icon_theme_add": self.on_icon_theme_add,
"on_icon_theme_remove": self.on_icon_theme_remove}
@@ -71,9 +64,7 @@ class SettingsDialog:
self._profiles = self._settings.profiles
self._s_type = self._settings.setting_type
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)
@@ -84,15 +75,13 @@ class SettingsDialog:
self._port_field = builder.get_object("port_field")
self._login_field = builder.get_object("login_field")
self._password_field = builder.get_object("password_field")
self._http_login_field = builder.get_object("http_login_field")
self._http_password_field = builder.get_object("http_password_field")
self._http_port_field = builder.get_object("http_port_field")
self._http_use_ssl_check_button = builder.get_object("http_use_ssl_check_button")
self._telnet_login_field = builder.get_object("telnet_login_field")
self._telnet_password_field = builder.get_object("telnet_password_field")
self._telnet_port_field = builder.get_object("telnet_port_field")
self._telnet_timeout_spin_button = builder.get_object("telnet_timeout_spin_button")
self._settings_stack = builder.get_object("settings_stack")
# Test
self._ftp_radio_button = builder.get_object("ftp_radio_button")
self._http_radio_button = builder.get_object("http_radio_button")
# Paths
self._services_field = builder.get_object("services_field")
self._user_bouquet_field = builder.get_object("user_bouquet_field")
@@ -114,6 +103,7 @@ class SettingsDialog:
self._support_ver5_switch = builder.get_object("support_ver5_switch")
self._force_bq_name_switch = builder.get_object("force_bq_name_switch")
# Streaming
header_separator = builder.get_object("header_separator")
self._apply_presets_button = builder.get_object("apply_presets_button")
self._transcoding_switch = builder.get_object("transcoding_switch")
self._edit_preset_switch = builder.get_object("edit_preset_switch")
@@ -125,25 +115,32 @@ 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._play_in_window_radio_button = builder.get_object("play_in_window_radio_button")
self._get_m3u_radio_button = builder.get_object("get_m3u_radio_button")
self._gst_lib_button = builder.get_object("gst_lib_button")
self._vlc_lib_button = builder.get_object("vlc_lib_button")
self._mpv_lib_button = builder.get_object("mpv_lib_button")
# Program
self._before_save_switch = builder.get_object("before_save_switch")
self._before_downloading_switch = builder.get_object("before_downloading_switch")
self._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")
# 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")
@@ -173,7 +170,7 @@ class SettingsDialog:
self._profile_add_button = builder.get_object("profile_add_button")
self._profile_remove_button = builder.get_object("profile_remove_button")
self._apply_profile_button = builder.get_object("apply_profile_button")
self._apply_profile_button.bind_property("visible", builder.get_object("reset_button"), "visible")
self._apply_profile_button.bind_property("visible", header_separator, "visible")
# Style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
@@ -186,24 +183,25 @@ class SettingsDialog:
self.init_profiles()
if self._settings.is_darwin:
# Appearance
self._appearance_box = builder.get_object("appearance_box")
self._appearance_box.set_visible(True)
# Themes
self._layout_switch = builder.get_object("layout_switch")
self._layout_switch.bind_property("active", builder.get_object("bouquet_box"), "sensitive")
self._layout_switch.set_active(self._ext_settings.alternate_layout)
self._theme_frame = builder.get_object("theme_frame")
self._theme_frame.set_visible(True)
self._theme_thumbnail_image = builder.get_object("theme_thumbnail_image")
self._theme_combo_box = builder.get_object("theme_combo_box")
self._icon_theme_combo_box = builder.get_object("icon_theme_combo_box")
self._dark_mode_switch = builder.get_object("dark_mode_switch")
self._themes_support_switch = builder.get_object("themes_support_switch")
self._themes_support_switch.bind_property("active", 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_title()
self._settings_stack.get_child_by_name(Property.HTTP.value).set_visible(is_enigma_profile)
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)
self._lang_combo_box.set_active_id(self._ext_settings.language)
@@ -221,12 +219,12 @@ 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_title(self):
title = "{} [{}]"
def update_header_bar(self):
label, sep, st = self._header_bar.get_subtitle().partition(":")
if self._s_type is SettingsType.ENIGMA_2:
self._dialog.set_title(title.format(get_message("Options"), self._enigma_radio_button.get_label()))
self._header_bar.set_subtitle("{}: {}".format(label, self._enigma_radio_button.get_label()))
elif self._s_type is SettingsType.NEUTRINO_MP:
self._dialog.set_title(title.format(get_message("Options"), self._neutrino_radio_button.get_label()))
self._header_bar.set_subtitle("{}: {}".format(label, self._neutrino_radio_button.get_label()))
def show(self):
self._dialog.run()
@@ -259,12 +257,8 @@ class SettingsDialog:
self._port_field.set_text(self._settings.port)
self._login_field.set_text(self._settings.user)
self._password_field.set_text(self._settings.password)
self._http_login_field.set_text(self._settings.http_user)
self._http_password_field.set_text(self._settings.http_password)
self._http_port_field.set_text(self._settings.http_port)
self._http_use_ssl_check_button.set_active(self._settings.http_use_ssl)
self._telnet_login_field.set_text(self._settings.telnet_user)
self._telnet_password_field.set_text(self._settings.telnet_password)
self._telnet_port_field.set_text(self._settings.telnet_port)
self._telnet_timeout_spin_button.set_value(self._settings.telnet_timeout)
self._services_field.set_text(self._settings.services_path)
@@ -280,6 +274,7 @@ class SettingsDialog:
self._before_downloading_switch.set_active(self._settings.backup_before_downloading)
self.set_fav_click_mode(self._settings.fav_click_mode)
self.set_play_stream_mode(self._settings.play_streams_mode)
self.set_stream_lib(self._settings.stream_lib)
self._load_on_startup_switch.set_active(self._settings.load_last_config)
self._bouquet_hints_switch.set_active(self._settings.show_bq_hints)
self._services_hints_switch.set_active(self._settings.show_srv_hints)
@@ -287,6 +282,9 @@ 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)
if self._s_type is SettingsType.ENIGMA_2:
self._enable_exp_switch.set_active(self._settings.is_enable_experimental)
@@ -320,12 +318,8 @@ class SettingsDialog:
self._settings.port = self._port_field.get_text()
self._settings.user = self._login_field.get_text()
self._settings.password = self._password_field.get_text()
self._settings.http_user = self._http_login_field.get_text()
self._settings.http_password = self._http_password_field.get_text()
self._settings.http_port = self._http_port_field.get_text()
self._settings.http_use_ssl = self._http_use_ssl_check_button.get_active()
self._settings.telnet_user = self._telnet_login_field.get_text()
self._settings.telnet_password = self._telnet_password_field.get_text()
self._settings.telnet_port = self._telnet_port_field.get_text()
self._settings.telnet_timeout = int(self._telnet_timeout_spin_button.get_value())
self._settings.services_path = self._services_field.get_text()
@@ -346,6 +340,7 @@ class SettingsDialog:
self._ext_settings.backup_before_downloading = self._before_downloading_switch.get_active()
self._ext_settings.fav_click_mode = self.get_fav_click_mode()
self._ext_settings.play_streams_mode = self.get_play_stream_mode()
self._ext_settings.stream_lib = self.get_stream_lib()
self._ext_settings.language = self._lang_combo_box.get_active_id()
self._ext_settings.load_last_config = self._load_on_startup_switch.get_active()
self._ext_settings.show_bq_hints = self._bouquet_hints_switch.get_active()
@@ -355,9 +350,13 @@ class SettingsDialog:
self._ext_settings.records_path = self._record_data_dir_field.get_text()
self._ext_settings.activate_transcoding = self._transcoding_switch.get_active()
self._ext_settings.active_preset = self._presets_combo_box.get_active_id()
self._ext_settings.list_picon_size = int(self._picons_size_button.get_active_id())
self._ext_settings.tooltip_logo_size = int(self._tooltip_logo_size_button.get_active_id())
self._ext_settings.list_font = self._list_font_button.get_font()
if self._ext_settings.is_darwin:
self._ext_settings.dark_mode = self._dark_mode_switch.get_active()
self._ext_settings.alternate_layout = self._layout_switch.get_active()
self._ext_settings.is_themes_support = self._themes_support_switch.get_active()
self._ext_settings.theme = self._theme_combo_box.get_active_id()
self._ext_settings.icon_theme = self._icon_theme_combo_box.get_active_id()
@@ -383,16 +382,15 @@ class SettingsDialog:
if self._test_spinner.get_state() is Gtk.StateType.ACTIVE:
return
self.show_spinner(True)
current_property = Property(self._settings_stack.get_visible_child_name())
if current_property is Property.HTTP:
self.test_http()
elif current_property is Property.TELNET:
self.test_telnet()
elif current_property is Property.FTP:
if self._ftp_radio_button.get_active():
self.test_ftp()
elif self._http_radio_button.get_active():
self.test_http()
else:
self.test_telnet()
def test_http(self):
user, password = self._http_login_field.get_text(), self._http_password_field.get_text()
user, password = self._login_field.get_text(), self._password_field.get_text()
host, port = self._host_field.get_text(), self._http_port_field.get_text()
use_ssl = self._http_use_ssl_check_button.get_active()
try:
@@ -407,7 +405,7 @@ class SettingsDialog:
def test_telnet(self):
timeout = int(self._telnet_timeout_spin_button.get_value())
host, port = self._host_field.get_text(), self._telnet_port_field.get_text()
user, password = self._telnet_login_field.get_text(), self._telnet_password_field.get_text()
user, password = self._login_field.get_text(), self._password_field.get_text()
try:
self.show_info_message(test_telnet(host, port, user, password, timeout), Gtk.MessageType.INFO)
self.show_spinner(False)
@@ -573,9 +571,6 @@ class SettingsDialog:
self._apply_profile_button.set_visible(name == "profiles")
self._apply_presets_button.set_visible(name == "streaming")
def on_network_settings_visible(self, stack, param):
self._http_use_ssl_check_button.set_visible(Property(stack.get_visible_child_name()) is Property.HTTP)
def on_http_use_ssl_toggled(self, button):
active = button.get_active()
self._settings.http_use_ssl = active
@@ -615,9 +610,14 @@ class SettingsDialog:
return FavClickMode.DISABLED
def on_play_mode_changed(self, button):
if self._main_stack.get_visible_child_name() != "streaming":
if self._main_stack.get_visible_child_name() != "streaming" or not button.get_active():
return
if self._settings.is_darwin:
is_gst = self._gst_lib_button.get_active()
self._play_in_built_radio_button.set_sensitive(is_gst)
self._play_in_window_radio_button.set_active(not is_gst and self._play_in_built_radio_button.get_active())
if button.get_active():
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
@@ -625,19 +625,34 @@ class SettingsDialog:
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._play_in_window_radio_button.set_active(mode is PlayStreamsMode.WINDOW)
self._get_m3u_radio_button.set_active(mode is PlayStreamsMode.M3U)
if self._settings.is_darwin and self._settings.stream_lib != "gst":
self._play_in_built_radio_button.set_sensitive(False)
def get_play_stream_mode(self):
if self._play_in_built_radio_button.get_active():
return PlayStreamsMode.BUILT_IN
if self._play_in_vlc_radio_button.get_active():
return PlayStreamsMode.VLC
if self._play_in_window_radio_button.get_active():
return PlayStreamsMode.WINDOW
if self._get_m3u_radio_button.get_active():
return PlayStreamsMode.M3U
return self._settings.play_streams_mode
def set_stream_lib(self, mode):
self._vlc_lib_button.set_active(mode == "vlc")
self._gst_lib_button.set_active(mode == "gst")
self._mpv_lib_button.set_active(mode == "mpv")
def get_stream_lib(self):
if self._gst_lib_button.get_active():
return "gst"
elif self._vlc_lib_button.get_active():
return "vlc"
return "mpv"
def on_transcoding_preset_changed(self, button):
presets = self._settings.transcoding_presets
prs = presets.get(button.get_active_id())
@@ -682,6 +697,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
@@ -702,7 +722,7 @@ class SettingsDialog:
Gtk.Settings().get_default().set_property("gtk-theme-name", "")
self.remove_theme(self._theme_combo_box, self._ext_settings.themes_path)
def on_icon_theme_changed(self, button, state=False):
def on_appearance_changed(self, button, state=False):
if self._main_stack.get_visible_child_name() != "appearance":
return
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
@@ -720,7 +740,7 @@ 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
@@ -737,7 +757,6 @@ class SettingsDialog:
log("Unpacking end.")
finally:
self.update_theme_button(button, dst)
self._appearance_box.set_sensitive(True)
@run_idle
def update_theme_button(self, button, dst):
@@ -750,6 +769,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):
@@ -773,7 +793,7 @@ class SettingsDialog:
button.set_active(0)
@run_idle
def init_appearance(self):
def init_themes(self):
self._dark_mode_switch.set_active(self._ext_settings.dark_mode)
t_support = self._ext_settings.is_themes_support
self._themes_support_switch.set_active(t_support)

View File

@@ -1,21 +1,49 @@
#digit-entry {
border-color: Red;
border-width: 0.15em;
}
#status-bar-button {
margin: 0.1em;
padding-top: 1px;
padding-bottom: 1px;
padding-left: 3px;
padding-right: 3px;
margin: 1px;
}
#textview-large {
font-size: 14px;
paned > separator {
background-repeat: no-repeat;
background-position: center;
background-size: 2px 24px;
}
.red-button {
background-image: none;
background-color: red;
}
.green-button {
background-image: none;
background-color: green;
}
.yellow-button {
background-image: none;
background-color: yellow;
}
.blue-button {
background-image: none;
background-color: blue;
}
.time-entry {
padding: 0px;
margin: 0px;
}
.group {}
.group :first-child {
padding-left: 0.5em;
padding-right: 0.5em;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
@@ -23,10 +51,22 @@
.group :last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left-width: 0;
}
.group :not(:first-child):not(:last-child) {
border-radius: 0;
border-left-width: 0;
border-right-width: 0;
}
border-right-width: 1px;
}
.stack-switcher > button > label {
padding-left: 2px;
padding-right: 2px;
min-width: 75px;
}
.stack-switcher > button.text-button {
padding-left: 2px;
padding-right: 2px;
}

View File

@@ -1,250 +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 domain="demon-editor">
<requires lib="gtk+" version="3.16"/>
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-copyright 2018-2020 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="GtkWindow" id="dialog_window">
<property name="can_focus">False</property>
<property name="title" translatable="yes">DemonEditor [Telnet client]</property>
<property name="destroy_with_parent">True</property>
<property name="icon_name">terminal</property>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<signal name="delete-event" handler="on_close" swapped="no"/>
<child>
<placeholder/>
</child>
<child>
<object class="GtkBox" id="main_box">
<property name="width_request">560</property>
<property name="height_request">320</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<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">0</property>
</packing>
</child>
<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">2</property>
<child>
<object class="GtkComboBoxText" id="profile_combo_box">
<property name="visible">True</property>
<property name="can_focus">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">0</property>
</packing>
</child>
<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">1</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">4</property>
</packing>
</child>
<child type="center">
<placeholder/>
</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="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="GtkInfoBar" id="info_bar">
<property name="app_paintable">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</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="layout_style">end</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="info_bar_message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">Info</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="wrap_mode">word-char</property>
<property name="lines">2</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View File

@@ -1,256 +0,0 @@
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.settings import Settings
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 TelnetDialog:
""" Dialog of 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, transient, settings):
self._handlers = {"on_profile_changed": self.on_profile_changed,
"on_clear": self.on_clear,
"on_text_view_realize": self.on_text_view_realize,
"on_view_key_press": self.on_view_key_press,
"on_info_bar_close": self.on_info_bar_close,
"on_connect": self.on_connect,
"on_disconnect": self.on_disconnect,
"on_close": self.on_close}
builder = Gtk.Builder()
builder.add_from_file(UI_RESOURCES_PATH + "telnet.glade")
builder.connect_signals(self._handlers)
self._dialog_window = builder.get_object("dialog_window")
self._dialog_window.set_transient_for(transient)
self._profile_combo_box = builder.get_object("profile_combo_box")
self._info_bar = builder.get_object("info_bar")
self._info_message_label = builder.get_object("info_bar_message_label")
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)
provider = Gtk.CssProvider()
provider.load_from_path(UI_RESOURCES_PATH + "style.css")
builder.get_object("main_box").get_style_context().add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
window_size = settings.get("telnet_dialog_window_size")
if window_size:
self._dialog_window.resize(*window_size)
self._ext_settings = settings
self._settings = Settings(settings.settings)
self._tn = None
self._app_mode = False
self._commands = deque(maxlen=10)
def show(self):
self._dialog_window.show()
def on_close(self, window, event):
""" Performs shutdown tasks """
self._ext_settings.add("telnet_dialog_window_size", window.get_size())
self.on_disconnect()
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._info_message_label.set_text(text)
def on_text_view_realize(self, view):
self.init_profiles()
self.on_connect()
@run_idle
def init_profiles(self):
for p in self._settings.profiles:
self._profile_combo_box.append(p, p)
self._profile_combo_box.set_active_id(self._settings.current_profile)
@run_task
def on_connect(self, item=None):
try:
GLib.idle_add(self._connect_button.set_visible, False)
GLib.idle_add(self.on_info_bar_close)
user, password = self._settings.telnet_user, self._settings.telnet_password
timeout = self._settings.telnet_timeout
self._tn = ExtTelnet(self.append_output,
host=self._settings.host,
port=self._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("{}: {}".format(self.__class__.__name__, e))
self.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_profile_changed(self, button):
self._settings.current_profile = button.get_active_id()
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.show_info_message("'{}' is not supported by this client.".format(cmd), 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

141
app/ui/timer_row.glade Normal file
View File

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

View File

@@ -5,15 +5,15 @@ import gi
from gi.repository import GLib
from app.commons import log
from app.settings import IS_DARWIN
from app.connections import HttpRequestType
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
class LinksTransmitter:
""" The main media bar class for the "send to" function..
""" The main class for the "send to" function.
It used for direct playback of media links by the enigma2 media player.
"""
@@ -35,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")
@@ -48,19 +46,16 @@ class LinksTransmitter:
self._status_passive = None
self._yt = YouTube.get_instance(settings)
if IS_DARWIN:
try:
gi.require_version("AppIndicator3", "0.1")
from gi.repository import AppIndicator3
except (ImportError, ValueError) as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
self._tray = builder.get_object("status_icon")
else:
try:
gi.require_version("AppIndicator3", "0.1")
from gi.repository import AppIndicator3
except (ImportError, ValueError) as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
self._tray = builder.get_object("status_icon")
else:
self._is_status_icon = False
self._status_active = AppIndicator3.IndicatorStatus.ACTIVE
self._status_passive = AppIndicator3.IndicatorStatus.PASSIVE
self._is_status_icon = False
self._status_active = AppIndicator3.IndicatorStatus.ACTIVE
self._status_passive = AppIndicator3.IndicatorStatus.PASSIVE
category = AppIndicator3.IndicatorCategory.APPLICATION_STATUS
path = Path(UI_RESOURCES_PATH + "/icons/hicolor/scalable/apps/demon-editor.svg")
@@ -128,7 +123,7 @@ class LinksTransmitter:
else:
self._url_entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None)
self._http_api.send(HttpRequestType.PLAY, url, self.on_done, self.__STREAM_PREFIX)
self._http_api.send(HttpAPI.Request.PLAY, url, self.on_done, self.__STREAM_PREFIX)
yield True
def on_done(self, res):
@@ -138,21 +133,21 @@ class LinksTransmitter:
GLib.idle_add(self._tool_bar.set_sensitive, True)
def on_previous(self, item):
self._http_api.send(HttpRequestType.PLAYER_PREV, None, self.on_done)
self._http_api.send(HttpAPI.Request.PLAYER_PREV, None, self.on_done)
def on_next(self, item):
self._http_api.send(HttpRequestType.PLAYER_NEXT, None, self.on_done)
self._http_api.send(HttpAPI.Request.PLAYER_NEXT, None, self.on_done)
def on_play(self, item):
self._http_api.send(HttpRequestType.PLAYER_PLAY, None, self.on_done)
self._http_api.send(HttpAPI.Request.PLAYER_PLAY, None, self.on_done)
def on_stop(self, item):
self._http_api.send(HttpRequestType.PLAYER_STOP, None, self.on_done)
self._http_api.send(HttpAPI.Request.PLAYER_STOP, None, self.on_done)
def on_clear(self, item):
""" Remove added links in the playlist. """
GLib.idle_add(self._tool_bar.set_sensitive, False)
self._http_api.send(HttpRequestType.PLAYER_LIST, None, self.clear_playlist)
self._http_api.send(HttpAPI.Request.PLAYER_LIST, None, self.clear_playlist)
def clear_playlist(self, res):
GLib.idle_add(self._tool_bar.set_sensitive, not res)
@@ -163,7 +158,7 @@ class LinksTransmitter:
for ref in res:
GLib.idle_add(self._tool_bar.set_sensitive, False)
self._http_api.send(HttpRequestType.PLAYER_REMOVE,
self._http_api.send(HttpAPI.Request.PLAYER_REMOVE,
ref.get("e2servicereference", ""),
self.on_done,
self.__STREAM_PREFIX)

View File

@@ -1,24 +1,27 @@
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")
from gi.repository import Gtk, Gdk, GLib
gi.require_version("Notify", "0.7")
from gi.repository import Gtk, Gdk, Notify
# Setting mod mask for keyboard depending on platform
from app.settings import Settings, SettingsException, IS_DARWIN
# Init notify
Notify.init("DemonEditor")
# Setting mod mask for the keyboard depending on the platform.
MOD_MASK = Gdk.ModifierType.MOD2_MASK if IS_DARWIN else Gdk.ModifierType.CONTROL_MASK
# Path to *.glade files
UI_RESOURCES_PATH = "app/ui/" if os.path.exists("app/ui/") else "ui/"
LANG_PATH = UI_RESOURCES_PATH + "lang"
GTK_PATH = os.environ.get("GTK_PATH", None)
NOTIFY_IS_INIT = False
# Path to *.glade files.
UI_RESOURCES_PATH = "app/ui/" if os.path.exists("app/ui/") else "/usr/share/demoneditor/app/ui/"
IS_GNOME_SESSION = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
# Translation.
TEXT_DOMAIN = "demon-editor"
APP_FONT = None
try:
settings = Settings.get_instance()
@@ -26,60 +29,31 @@ 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()
st.set_property("gtk-application-prefer-dark-theme", settings.dark_mode)
APP_FONT = st.get_property("gtk-font-name")
if not settings.list_font:
settings.list_font = APP_FONT
if settings.is_themes_support:
st.set_property("gtk-theme-name", settings.theme)
st.set_property("gtk-icon-theme-name", settings.icon_theme)
else:
style_provider = Gtk.CssProvider()
s_path = "{}default_style.css".format(GTK_PATH + "/" + UI_RESOURCES_PATH if GTK_PATH else UI_RESOURCES_PATH)
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_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:
import locale
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")
theme = Gtk.IconTheme.get_default()
theme.append_search_path(GTK_PATH + "/share/icons" if GTK_PATH else UI_RESOURCES_PATH + "icons")
theme.append_search_path(UI_RESOURCES_PATH + "icons")
def get_theme_icon(icon_theme, name, size):
try:
return icon_theme.load_icon(name, size, 0)
except GLib.Error:
pass
_IMAGE_MISSING = get_theme_icon(theme, "image-missing", 16)
CODED_ICON = get_theme_icon(theme, "emblem-readonly", 16) or _IMAGE_MISSING
LOCKED_ICON = get_theme_icon(theme, "changes-prevent-symbolic", 16) or _IMAGE_MISSING
HIDE_ICON = get_theme_icon(theme, "go-jump", 16) or _IMAGE_MISSING
TV_ICON = get_theme_icon(theme, "tv-symbolic", 16) or _IMAGE_MISSING
IPTV_ICON = get_theme_icon(theme, "emblem-shared", 16)
EPG_ICON = get_theme_icon(theme, "gtk-index", 16)
DEFAULT_ICON = get_theme_icon(theme, "emblem-default", 16)
_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
@lru_cache(maxsize=1)
@@ -96,12 +70,11 @@ def get_yt_icon(icon_name, size=24):
import glob
for theme_name in map(os.path.basename, filter(os.path.isdir, glob.glob("/usr/share/icons/*"))):
theme.set_custom_theme(theme_name)
n_theme.set_custom_theme(theme_name)
if n_theme.has_icon(icon_name):
return n_theme.load_icon(icon_name, size, 0)
if default_theme.lookup_icon(Gtk.STOCK_APPLY, size, 0):
return default_theme.load_icon(Gtk.STOCK_APPLY, size, 0)
return default_theme.load_icon("info", size, 0)
def show_notification(message, timeout=10000, urgency=1):
@@ -111,46 +84,41 @@ def show_notification(message, timeout=10000, urgency=1):
@param timeout: milliseconds
@param urgency: 0 - low, 1 - normal, 2 - critical
"""
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()
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. """
F = 3 if IS_DARWIN else 41
E = 14 if IS_DARWIN else 26
R = 15 if IS_DARWIN else 27
T = 17 if IS_DARWIN else 28
P = 35 if IS_DARWIN else 33
S = 1 if IS_DARWIN else 39
H = 4 if IS_DARWIN else 43
L = 37 if IS_DARWIN else 46
X = 7 if IS_DARWIN else 53
C = 8 if IS_DARWIN else 54
V = 9 if IS_DARWIN else 55
W = 13 if IS_DARWIN else 25
Z = 6 if IS_DARWIN else 52
INSERT = -1 if IS_DARWIN else 118
HOME = -1 if IS_DARWIN else 110
END = -1 if IS_DARWIN else 115
UP = 126 if IS_DARWIN else 111
DOWN = 125 if IS_DARWIN else 116
PAGE_UP = -1 if IS_DARWIN else 112
PAGE_DOWN = -1 if IS_DARWIN else 117
LEFT = 123 if IS_DARWIN else 113
RIGHT = 123 if IS_DARWIN else 114
F2 = 120 if IS_DARWIN else 68
SPACE = 49 if IS_DARWIN else 65
DELETE = 51 if IS_DARWIN else 119
BACK_SPACE = 76 if IS_DARWIN else 22
CTRL_L = 55 if IS_DARWIN else 37
CTRL_R = 55 if IS_DARWIN else 105
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
@@ -195,7 +163,7 @@ class BqGenType(Enum):
class Column(IntEnum):
""" Column nums in the views """
# main view
# Main view
SRV_CAS_FLAGS = 0
SRV_STANDARD = 1
SRV_CODED = 2
@@ -218,7 +186,7 @@ class Column(IntEnum):
SRV_TRANSPONDER = 19
SRV_TOOLTIP = 20
SRV_BACKGROUND = 21
# fav view
# FAV view
FAV_NUM = 0
FAV_CODED = 1
FAV_SERVICE = 2
@@ -230,11 +198,20 @@ class Column(IntEnum):
FAV_PICON = 8
FAV_TOOLTIP = 9
FAV_BACKGROUND = 10
# bouquets view
# Bouquets view
BQ_NAME = 0
BQ_LOCKED = 1
BQ_HIDDEN = 2
BQ_TYPE = 3
# Alternatives view
ALT_NUM = 0
ALT_PICON = 1
ALT_SERVICE = 2
ALT_TYPE = 3
ALT_POS = 4
ALT_FAV_ID = 5
ALT_ID = 6
ALT_ITER = 7
def __index__(self):
""" Overridden to get the index in slices directly """

18
build-deb.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
VER="1.0.10_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
cd dist
fakeroot dpkg-deb --build DemonEditor
mv DemonEditor.deb DemonEditor_$VER.deb
rm -R DemonEditor

62
deb/DEBIAN/README.source Normal file
View File

@@ -0,0 +1,62 @@
demon-editor for Debian
----------------------
DemonEditor
Enigma2 channel and satellites list editor for GNU/Linux.
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc).
Main features of the program:
Editing bouquets, channels, satellites.
Import function.
Backup function.
Support of picons.
Importing services, downloading picons and updating satellites from the Web.
Extended support of IPTV.
Import to bouquet(Neutrino WEBTV) from m3u.
Export of bouquets with IPTV services in m3u.
Assignment of EPGs from DVB or XML for IPTV services (only Enigma2, experimental).
Playback of IPTV or other streams directly from the bouquet list.
Control panel with the ability to view EPG and manage timers (via HTTP API, experimental).
Simple FTP client (experimental).
Keyboard shortcuts:
Ctrl + Insert - copies the selected channels from the main list to the the bouquet beginning or inserts (creates) a new bouquet.
Ctrl + BackSpace - copies the selected channels from the main list to the bouquet end.
Ctrl + X - only in bouquet list. Ctrl + C - only in services list.
Clipboard is "rubber". There is an accumulation before the insertion!
Ctrl + E - edit.
Ctrl + R, F2 - rename.
Ctrl + S, T in Satellites edit tool for create satellite or transponder.
Ctrl + L - parental lock.
Ctrl + H - hide/skip.
Ctrl + P - start play IPTV or other stream in the bouquet list.
Ctrl + Z - switch (zap) the channel (works when the HTTP API is enabled, Enigma2 only).
Ctrl + W - switch to the channel and watch in the program.
Space - select/deselect.
Left/Right - remove selection.
Ctrl + Up, Down, PageUp, PageDown, Home, End - move selected items in the list.
Ctrl + O - (re)load user data from current dir.
Ctrl + D - load data from receiver.
Ctrl + U/B upload data/bouquets to receiver.
Ctrl + F - show/hide search bar.
Ctrl + Shift + F - show/hide filter bar.
For multiple selection with the mouse, press and hold the Ctrl key!
Minimum requirements:
Python >= 3.5.2, GTK+ >= 3.22, python3-gi, python3-gi-cairo, python3-requests.
Important:
Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2!
Main supported *lamedb* format is version **4**. Versions **3** and **5** has only **experimental** support!
For version **3** is only read mode available. When saving, version **4** format is used instead!
When using the multiple import feature, from *lamedb* will be taken data **only for channels that are in the
selected bouquets!** If you need full set of the data, including *[satellites, terrestrial, cables].xml* (current files will be overwritten),
just load your data via *"File/Open"* and press *"Save"*. When importing separate bouquet files, only those services
(excluding IPTV) that are in the **current open lamedb** (main list of services) will be imported.
For streams playback, this app supports VLC, MPV and GStreamer.
Depending on your distro, you may need to install additional packages and libraries.

14
deb/DEBIAN/control Normal file
View File

@@ -0,0 +1,14 @@
Package: demon-editor
Version: 1.0.10-Beta
Section: utils
Priority: optional
Architecture: all
Essential: no
Depends: python3 (>= 3.5),
python3-requests,
python3-gi,
python3-gi-cairo
Recommends: libmpv1,
python3-chardet
Maintainer: Dmitriy Yefremov <dmitry.v.yefremov@gmail.com>
Description: Enigma2 channel and satellite list editor

26
deb/DEBIAN/copyright Normal file
View File

@@ -0,0 +1,26 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Contact: Dmitriy Yefremov <dmitry.v.yefremov@gmail.com>
Source: https://github.com/DYefremov/DemonEditor
Files: *
MIT License
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.

View File

@@ -0,0 +1 @@
README.source

2
deb/usr/bin/demon-editor Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
python3 /usr/share/demoneditor/start.py $1

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,634 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg1541"
version="1.1"
viewBox="0 0 16.743339 16.72816"
height="63.224556"
width="63.281971"
sodipodi:docname="demon-editor.svg"
inkscape:version="0.92.4 (unknown)">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="716"
id="namedview127"
showgrid="true"
fit-margin-left="0"
inkscape:zoom="6.1714295"
inkscape:cx="40.088627"
inkscape:cy="31.742631"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1541"
fit-margin-top="0"
fit-margin-right="0"
fit-margin-bottom="0">
<inkscape:grid
type="xygrid"
id="grid128"
originx="-10.604603"
originy="-1.1727724" />
</sodipodi:namedview>
<title
id="title1088">DeamonEditor Icons</title>
<defs
id="defs1535">
<linearGradient
id="linearGradient2198">
<stop
id="stop2194"
style="stop-color:#ffb320;stop-opacity:1"
offset="0" />
<stop
id="stop2196"
style="stop-color:#e7ff00;stop-opacity:1"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient2192">
<stop
offset="0"
style="stop-color:#ffb320;stop-opacity:1"
id="stop2188" />
<stop
offset="1"
style="stop-color:#b3c54c;stop-opacity:1"
id="stop2190" />
</linearGradient>
<linearGradient
id="linearGradient3700-8">
<stop
offset="0"
style="stop-color:#2e4f84;stop-opacity:1"
id="stop2183" />
<stop
offset="1"
style="stop-color:#4c77c5;stop-opacity:1"
id="stop2185" />
</linearGradient>
<linearGradient
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
spreadMethod="pad"
id="linearGradient1844">
<stop
style="stop-opacity:1;stop-color:#29282b"
offset="0"
id="stop1826" />
<stop
id="stop1828"
offset="0.13293758"
style="stop-color:#b5bdcf;stop-opacity:1" />
<stop
style="stop-opacity:1;stop-color:#92979f"
offset="0.21261224"
id="stop1832" />
<stop
style="stop-opacity:1;stop-color:#737881"
offset="0.29780689"
id="stop1834" />
<stop
style="stop-opacity:1;stop-color:#70757e"
offset="0.29780689"
id="stop1836" />
<stop
style="stop-opacity:1;stop-color:#e4e6e8"
offset="0.45395693"
id="stop1838" />
<stop
style="stop-opacity:1;stop-color:#696c76"
offset="0.71871042"
id="stop1840" />
<stop
style="stop-opacity:1;stop-color:#29282b"
offset="1"
id="stop1842" />
</linearGradient>
<linearGradient
id="linearGradient1754"
spreadMethod="pad"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
gradientUnits="userSpaceOnUse"
y2="0"
x2="1"
y1="0"
x1="0">
<stop
id="stop1736"
offset="0"
style="stop-opacity:1;stop-color:#29282b" />
<stop
style="stop-color:#b5bdcf;stop-opacity:1"
offset="0.02582242"
id="stop1738" />
<stop
id="stop1740"
offset="0.14669065"
style="stop-opacity:1;stop-color:#868c95" />
<stop
id="stop1742"
offset="0.21261224"
style="stop-opacity:1;stop-color:#92979f" />
<stop
id="stop1744"
offset="0.29780689"
style="stop-opacity:1;stop-color:#737881" />
<stop
id="stop1746"
offset="0.29780689"
style="stop-opacity:1;stop-color:#70757e" />
<stop
id="stop1748"
offset="0.45395693"
style="stop-opacity:1;stop-color:#e4e6e8" />
<stop
id="stop1750"
offset="0.71871042"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1752"
offset="1"
style="stop-opacity:1;stop-color:#1d191a" />
</linearGradient>
<linearGradient
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
spreadMethod="pad"
id="linearGradient1606">
<stop
style="stop-opacity:1;stop-color:#29282b"
offset="0"
id="stop1590" />
<stop
id="stop1608"
offset="0.03065561"
style="stop-color:#b5bdcf;stop-opacity:1" />
<stop
style="stop-opacity:1;stop-color:#868c95"
offset="0.1125849"
id="stop1592" />
<stop
style="stop-opacity:1;stop-color:#92979f"
offset="0.13955873"
id="stop1594" />
<stop
style="stop-opacity:1;stop-color:#737881"
offset="0.1915853"
id="stop1596" />
<stop
style="stop-opacity:1;stop-color:#70757e"
offset="0.25400829"
id="stop1598" />
<stop
style="stop-opacity:1;stop-color:#e4e6e8"
offset="0.45395693"
id="stop1600" />
<stop
style="stop-opacity:1;stop-color:#ffffff"
offset="0.71871042"
id="stop1602" />
<stop
style="stop-opacity:1;stop-color:#1d191a"
offset="1"
id="stop1604" />
</linearGradient>
<linearGradient
id="linearGradient886-0">
<stop
style="stop-color:#535353;stop-opacity:1"
offset="0"
id="stop888" />
<stop
style="stop-color:#f0f0f0;stop-opacity:1"
offset="1"
id="stop890" />
</linearGradient>
<linearGradient
id="linearGradient1473">
<stop
id="stop1469"
offset="0"
style="stop-color:#ffffff;stop-opacity:0" />
<stop
id="stop1471"
offset="1"
style="stop-color:#ffffff;stop-opacity:0.96088022" />
</linearGradient>
<linearGradient
id="linearGradient2447-35">
<stop
id="stop2449"
offset="0"
style="stop-color:#00c62e;stop-opacity:1" />
<stop
id="stop2451"
offset="1"
style="stop-color:#136100;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient3795-1">
<stop
id="stop3797-1"
offset="0"
style="stop-color:#803400;stop-opacity:1" />
<stop
id="stop3799-3"
offset="1"
style="stop-color:#c87137;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient2447-3">
<stop
style="stop-color:#c60300;stop-opacity:1"
offset="0"
id="stop2443" />
<stop
style="stop-color:#c40e00;stop-opacity:1"
offset="1"
id="stop2445" />
</linearGradient>
<linearGradient
id="linearGradient2458">
<stop
id="stop2454"
offset="0"
style="stop-color:#c60300;stop-opacity:1" />
<stop
id="stop2456"
offset="1"
style="stop-color:#ee6000;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient3519">
<stop
id="stop3521"
offset="0"
style="stop-color:#1d2120;stop-opacity:1" />
<stop
id="stop3523"
offset="1"
style="stop-color:#545d5d;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient1446">
<stop
id="stop1442"
offset="0"
style="stop-color:#ffffff;stop-opacity:1;" />
<stop
id="stop1444"
offset="1"
style="stop-color:#ffffff;stop-opacity:0;" />
</linearGradient>
<linearGradient
id="linearGradient1547"
spreadMethod="pad"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
gradientUnits="userSpaceOnUse"
y2="0"
x2="1"
y1="0"
x1="0">
<stop
id="stop1529"
offset="0"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1531"
offset="0.0263736"
style="stop-opacity:1;stop-color:#29282b" />
<stop
id="stop1533"
offset="0.263736"
style="stop-opacity:1;stop-color:#868c95" />
<stop
id="stop1535"
offset="0.395604"
style="stop-opacity:1;stop-color:#92979f" />
<stop
id="stop1537"
offset="0.39560401"
style="stop-opacity:1;stop-color:#737881" />
<stop
id="stop1539"
offset="0.42333773"
style="stop-opacity:1;stop-color:#70757e" />
<stop
id="stop1541"
offset="0.56268591"
style="stop-opacity:1;stop-color:#e4e6e8" />
<stop
id="stop1543"
offset="0.62400264"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1545"
offset="1"
style="stop-opacity:1;stop-color:#1d191a" />
</linearGradient>
<linearGradient
id="linearGradient1527"
spreadMethod="pad"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
gradientUnits="userSpaceOnUse"
y2="0"
x2="1"
y1="0"
x1="0">
<stop
id="stop1509"
offset="0"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1511"
offset="0.0527472"
style="stop-opacity:1;stop-color:#29282b" />
<stop
id="stop1513"
offset="0.142147"
style="stop-opacity:1;stop-color:#868c95" />
<stop
id="stop1515"
offset="0.19700864"
style="stop-opacity:1;stop-color:#92979f" />
<stop
id="stop1517"
offset="0.25031137"
style="stop-opacity:1;stop-color:#737881" />
<stop
id="stop1519"
offset="0.3710371"
style="stop-opacity:1;stop-color:#70757e" />
<stop
id="stop1521"
offset="0.53961843"
style="stop-opacity:1;stop-color:#e4e6e8" />
<stop
id="stop1523"
offset="0.76283824"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1525"
offset="1"
style="stop-opacity:1;stop-color:#1d191a" />
</linearGradient>
<linearGradient
gradientTransform="matrix(0.26458333,0,0,0.26458333,-5.8254634,-78.732754)"
y2="1179.7145"
x2="66.791626"
y1="1188.7661"
x1="99.044022"
gradientUnits="userSpaceOnUse"
id="linearGradient1485"
xlink:href="#linearGradient1473" />
<linearGradient
gradientTransform="matrix(0.26458333,0,0,0.26458333,-2.2733744,-70.526334)"
gradientUnits="userSpaceOnUse"
y2="1155.8046"
x2="67.311417"
y1="1161.6112"
x1="77.19442"
id="linearGradient1448"
xlink:href="#linearGradient1446" />
<linearGradient
gradientTransform="matrix(0.26458333,0,0,0.26458333,-18.198747,-79.859424)"
gradientUnits="userSpaceOnUse"
y2="1186.8096"
x2="146.16808"
y1="1186.8096"
x1="131.86871"
id="linearGradient2180"
xlink:href="#linearGradient886-0" />
<linearGradient
gradientTransform="matrix(0.20863982,0,0,0.20863982,-0.63935937,-13.031239)"
gradientUnits="userSpaceOnUse"
y2="1184.73"
x2="101.19952"
y1="1184.73"
x1="83.066002"
id="linearGradient2160"
xlink:href="#linearGradient886-0" />
<linearGradient
gradientTransform="matrix(0.26458333,0,0,0.26458333,-6.2370714,-102.12414)"
gradientUnits="userSpaceOnUse"
y2="1267.1335"
x2="117.99127"
y1="1267.1335"
x1="75.853806"
id="linearGradient2131"
xlink:href="#linearGradient886-0" />
<linearGradient
id="linearGradient3700-8-3">
<stop
id="stop3702-1"
style="stop-color:#2e4f84;stop-opacity:1"
offset="0" />
<stop
id="stop3704-8"
style="stop-color:#4c77c5;stop-opacity:1"
offset="1" />
</linearGradient>
<defs
id="defs4922">
<filter
style="color-interpolation-filters:sRGB"
id="Adobe_OpacityMaskFilter"
filterUnits="userSpaceOnUse"
x="3.7850001"
y="4.6750002"
width="5.8829999"
height="73.013">
<feColorMatrix
type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"
id="feColorMatrix4925" />
</filter>
</defs>
<mask
maskUnits="userSpaceOnUse"
x="3.785"
y="4.675"
width="5.883"
height="73.013"
id="SVGID_2_">
<g
style="filter:url(#Adobe_OpacityMaskFilter)"
id="g4928">
<linearGradient
id="SVGID_3_"
gradientUnits="userSpaceOnUse"
x1="3.7852001"
y1="41.181198"
x2="9.6680002"
y2="41.181198">
<stop
offset="0"
style="stop-color:#FFFFFF"
id="stop4931" />
<stop
offset="0.0029"
style="stop-color:#FAFBFB"
id="stop4933" />
<stop
offset="0.0756"
style="stop-color:#BBBDBF"
id="stop4935" />
<stop
offset="0.1438"
style="stop-color:#898B8E"
id="stop4937" />
<stop
offset="0.2053"
style="stop-color:#646567"
id="stop4939" />
<stop
offset="0.259"
style="stop-color:#444446"
id="stop4941" />
<stop
offset="0.3028"
style="stop-color:#1D1C1D"
id="stop4943" />
<stop
offset="0.3313"
style="stop-color:#000000"
id="stop4945" />
</linearGradient>
<rect
style="fill:url(#SVGID_3_)"
x="3.7850001"
y="4.6750002"
width="5.8829999"
height="73.013"
id="rect4947" />
</g>
</mask>
<filter
style="color-interpolation-filters:sRGB"
id="filter1268"
filterUnits="userSpaceOnUse"
x="3.7850001"
y="4.6750002"
width="5.8829999"
height="73.013">
<feColorMatrix
type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"
id="feColorMatrix1266" />
</filter>
<linearGradient
gradientUnits="userSpaceOnUse"
y2="203.22046"
x2="23.551136"
y1="203.22046"
x1="13.487289"
id="linearGradient1160"
xlink:href="#linearGradient3519" />
<clipPath
id="clipPath1890"
clipPathUnits="userSpaceOnUse">
<circle
style="opacity:1;fill:#2d2d2d;fill-opacity:1;stroke:#434242;stroke-width:0.0575568;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle1892"
cx="78.548424"
cy="-31.019459"
r="6.4721422"
transform="scale(1,-1)" />
</clipPath>
<linearGradient
gradientTransform="translate(-0.24389927,14.877856)"
y2="27.314217"
x2="84.864914"
y1="27.314217"
x1="72.164909"
gradientUnits="userSpaceOnUse"
id="linearGradient2134"
xlink:href="#linearGradient3700-8-3" />
</defs>
<metadata
id="metadata1538">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>DeamonEditor Icons</dc:title>
<dc:publisher>
<cc:Agent>
<dc:title>mfgeg</dc:title>
</cc:Agent>
</dc:publisher>
<dc:date>7.1.2020</dc:date>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="matrix(1.1690805,0,0,1.1690805,-14.929261,-167.45253)"
id="layer1">
<g
transform="translate(0.86526724,-82.691658)"
id="g1351">
<path
d="m 13.715358,227.65981 h 2.97616 v 12.57332 h -2.97616 z m 5.79826,-1.73376 -0.327076,0.42227 c -0.179879,0.23226 -0.586303,0.67932 -0.903223,0.99412 -0.480677,0.47726 -0.640897,0.57235 -0.967777,0.57235 -0.223557,0 -0.380895,-0.0584 -0.624024,-0.25067 v 3.18307 c 0.172884,-0.0214 0.359647,-0.0333 0.575071,-0.0352 0.68403,-0.006 0.91815,0.0416 1.436333,0.29581 1.14586,0.56274 1.762492,1.5589 1.768251,2.8566 0.0094,2.10836 -1.870896,3.55161 -3.779655,3.16529 v 3.10345 c 4.345352,0.0758 4.093104,-2.37537 6.573781,-2.25837 1.486139,0.0748 1.333421,0.16555 1.796225,-1.064 l 0.259834,-0.6913 -0.803704,-0.66493 c -0.960855,-0.79478 -1.303983,-1.29079 -1.205013,-1.74132 0.06631,-0.30189 1.20818,-1.50093 1.779011,-1.86778 0.240181,-0.1543 0.244255,-0.17878 0.09575,-0.59661 -0.08522,-0.23979 -0.259522,-0.65746 -0.386789,-0.92744 l -0.23132,-0.49115 h -1.412666 c -1.868582,0 -1.802386,0.068 -1.805368,-1.82472 l -0.0021,-1.43582 -0.917745,-0.37171 z"
style="opacity:1;fill:#000000;fill-opacity:0.5372549;stroke:none;stroke-width:0.18460207;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.5012225"
id="path2133"
inkscape:connector-curvature="0" />
<path
id="path2106"
style="opacity:1;fill:url(#linearGradient2131);fill-opacity:1;stroke:none;stroke-width:0.17733108;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.5012225"
d="m 13.832581,227.93117 h 2.858936 v 12.07807 h -2.858936 z m 5.569881,-1.6655 -0.314194,0.40566 c -0.172793,0.22309 -0.563209,0.65257 -0.867647,0.95496 -0.46174,0.45847 -0.615654,0.5498 -0.929658,0.5498 -0.214752,0 -0.365893,-0.056 -0.599446,-0.24079 v 3.05768 c 0.166077,-0.0206 0.345483,-0.0319 0.55242,-0.0338 0.657089,-0.006 0.881987,0.0399 1.379764,0.28416 1.100724,0.54057 1.693073,1.49747 1.698606,2.74408 0.009,2.02535 -1.797211,3.41174 -3.63079,3.04061 v 2.98122 c 4.174205,0.0728 3.931888,-2.28179 6.314859,-2.1694 1.427604,0.0718 1.2809,0.15902 1.725477,-1.02213 l 0.249597,-0.66408 -0.772046,-0.63871 c -0.923009,-0.76345 -1.252622,-1.23996 -1.157552,-1.67277 0.0637,-0.29001 1.160592,-1.44177 1.708939,-1.79419 0.230721,-0.14825 0.234635,-0.17174 0.09198,-0.57312 -0.08187,-0.23032 -0.249296,-0.63156 -0.371555,-0.89088 l -0.222207,-0.4718 h -1.357022 c -1.794983,0 -1.731396,0.0653 -1.734262,-1.75284 l -0.0021,-1.3793 -0.881603,-0.35705 z"
inkscape:connector-curvature="0" />
<path
id="path2151"
d="m 17.266983,230.95755 c -0.215637,0.003 -0.402408,0.014 -0.575465,0.0354 v 6.28854 c 1.910653,0.38671 3.792716,-1.05815 3.783336,-3.16865 -0.0058,-1.29897 -0.623064,-2.29597 -1.770059,-2.85927 -0.518699,-0.25451 -0.753103,-0.30234 -1.437812,-0.29607 z m 0.482341,1.17002 c 0.433747,-0.004 0.582202,0.0265 0.910784,0.18761 0.726595,0.35682 1.117604,0.98848 1.121256,1.81134 0.0059,1.33694 -1.186343,2.25211 -2.396695,2.00715 v -3.98359 c 0.109628,-0.0135 0.228054,-0.0214 0.364655,-0.0225 z"
style="opacity:1;fill:url(#linearGradient2160);fill-opacity:1;stroke:none;stroke-width:0.18478528;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.5012225"
inkscape:connector-curvature="0" />
<path
style="opacity:1;fill:none;fill-opacity:1;stroke:url(#linearGradient2180);stroke-width:0.18478528;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.5012225"
d="m 17.266981,230.95753 c -0.215636,0.003 -0.402408,0.014 -0.575464,0.0354 v 6.28853 c 1.910652,0.38672 3.792715,-1.05815 3.783336,-3.16865 -0.0057,-1.29897 -0.623065,-2.29597 -1.77006,-2.85927 -0.518697,-0.2545 -0.753103,-0.30234 -1.437812,-0.29607 z m 0.482343,1.17001 c 0.433747,-0.004 0.5822,0.0265 0.910783,0.18762 0.726594,0.35681 1.117605,0.98848 1.121257,1.81133 0.006,1.33694 -1.186344,2.25211 -2.396697,2.00716 v -3.98359 c 0.109628,-0.0135 0.228055,-0.0214 0.364657,-0.0225 z"
id="path2170"
inkscape:connector-curvature="0" />
<path
id="path1422"
d="m 17.266983,230.95755 c -0.215637,0.003 -0.402408,0.014 -0.575464,0.0354 v 6.28854 c 1.910652,0.38671 3.792715,-1.05815 3.783335,-3.16865 -0.0057,-1.29897 -0.623064,-2.29597 -1.770059,-2.85927 -0.518699,-0.25451 -0.753103,-0.30234 -1.437812,-0.29607 z m 0.482342,1.17002 c 0.433748,-0.004 0.582201,0.0265 0.910784,0.18761 0.726594,0.35682 1.117605,0.98848 1.121257,1.81134 0.006,1.33694 -1.186344,2.25211 -2.396697,2.00715 v -3.98359 c 0.109628,-0.0135 0.228054,-0.0214 0.364656,-0.0225 z"
style="opacity:1;fill:none;fill-opacity:1;stroke:url(#linearGradient1448);stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:connector-curvature="0" />
<path
id="path1454"
d="m 19.402462,226.26149 -0.314194,0.40566 c -0.172793,0.22309 -0.563209,0.65259 -0.867644,0.95499 -0.461741,0.45847 -0.615657,0.54983 -0.929661,0.54983 -0.214752,0 -0.365893,-0.056 -0.599446,-0.2408 v -0.004 h -2.858741 v 12.0783 h 2.858741 c 4.174205,0.0728 3.931888,-2.28177 6.314861,-2.16937 1.427604,0.0718 1.280898,0.15899 1.725475,-1.02217 l 0.249597,-0.66402 -0.772046,-0.63873 c -0.923009,-0.76346 -1.252622,-1.23997 -1.157552,-1.67278 0.0637,-0.29001 1.160595,-1.44176 1.708939,-1.79419 0.230721,-0.14825 0.234635,-0.17171 0.09198,-0.57309 -0.08187,-0.23032 -0.249296,-0.63158 -0.371555,-0.8909 l -0.222207,-0.47181 h -1.357025 c -1.794983,0 -1.731393,0.0653 -1.734259,-1.75286 l -0.0021,-1.37925 -0.8816,-0.35708 z"
style="opacity:1;fill:none;fill-opacity:1;stroke:url(#linearGradient1485);stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:connector-curvature="0" />
</g>
<g
transform="translate(63.49728,-55.136805)"
id="g1174" />
<g
transform="translate(50.48327,11.624037)"
id="g1812" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
icon.icns

Binary file not shown.

1251
po/be/demon-editor.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,9 @@
# Copyright (C) 2018-2020 Dmitriy Yefremov
# Copyright (C) 2018-2021 Dmitriy Yefremov
# This file is distributed under the MIT license.
#
# Charly, 2019.
# Dmitriy Yefremov, 2020.
# Dmitriy Yefremov, 2020-2021.
# Thomas Schmidt, 2021
msgid ""
msgstr ""
"Last-Translator: Dmitriy Yefremov\n"
@@ -12,7 +13,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
msgid "translator-credits"
msgstr "Charly\nDmitriy Yefremov"
msgstr "Charly\nDmitriy Yefremov\nThomas Schmidt"
# Main
msgid "Service"
@@ -22,7 +23,7 @@ msgid "Package"
msgstr "Paket"
msgid "Type"
msgstr "Model"
msgstr "Typ"
msgid "Picon"
msgstr "Picon"
@@ -31,7 +32,7 @@ msgid "Freq"
msgstr "Freq"
msgid "Rate"
msgstr "Bewertung"
msgstr "SR"
msgid "Pol"
msgstr "Pol."
@@ -105,6 +106,9 @@ msgstr "Standardbezeichnung setzen"
msgid "Insert marker"
msgstr "Marker einfügen"
msgid "Insert space"
msgstr "Leerzeichen einfügen"
msgid "Locate in services"
msgstr "In den Diensten suchen"
@@ -160,7 +164,7 @@ msgid "Remove"
msgstr "Entfernen"
msgid "Remove all unavailable"
msgstr "Entfernt alle nicht verfügbaren"
msgstr "Entfernen alle nicht verfügbaren"
msgid "Satellites editor"
msgstr "Satelliten-Editor"
@@ -201,8 +205,8 @@ msgstr "Aktueller Datenpfad:"
msgid "Data:"
msgstr "Daten:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux"
msgstr "Enigma2 Kanal- und Satellitenlisteneditor für GNU/Linux"
msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "Enigma2 Kanal- und Satellitenlisteneditor für GNU/Linux."
msgid "Host:"
msgstr "Host:"
@@ -516,6 +520,9 @@ msgstr "Bitte wählen Sie nur einen Eintrag aus!"
msgid "No png file is selected!"
msgstr "Es ist keine png-Datei ausgewählt!"
msgid "No profile selected!"
msgstr "Kein Profil ausgewählt!"
msgid "No reference is present!"
msgstr "Es liegt keine Referenz vor!"
@@ -660,16 +667,16 @@ msgid "Test connection"
msgstr "Test Verbindung"
msgid "Double click on the service in the bouquet list:"
msgstr "Doppelklicke auf das Service in der Bouquetliste:"
msgstr "Doppelklick auf das Service in der Bouquet-Liste:"
msgid "Zap"
msgstr "Zap"
msgid "Play stream"
msgstr "Play Stream"
msgstr "Stream abspielen"
msgid "Disabled"
msgstr "Ausgeschaltet"
msgstr "Deaktiviert"
msgid "Enable lamedb ver. 5 support"
msgstr "Lamedb ver. 5 Unterstützung aktivieren"
@@ -687,7 +694,7 @@ msgid "Play IPTV or other stream in the program(Ctrl + P)"
msgstr "Wiedergabe von IPTV oder anderen Streams im Programm(Strg + P)"
msgid "Export to m3u"
msgstr "Export nach m3u"
msgstr "Exportieren nach m3u"
msgid "EPG configuration"
msgstr "EPG Konfiguration"
@@ -714,13 +721,13 @@ msgid "Url to *.xml.gz file:"
msgstr "Url zur *.xml.gz Datei:"
msgid "Enable filtering"
msgstr "Filterung einschalten"
msgstr "Filter aktivieren"
msgid "Filter by presence in the epg.dat file."
msgstr "Filtern nach dem Vorhandensein in der epg.dat Datei."
msgid "Paths to the epg.dat file:"
msgstr "Pfade zur epg.dat Datei:"
msgstr "Pfad zur epg.dat Datei:"
msgid "Local path:"
msgstr "Local path:"
@@ -744,7 +751,7 @@ msgid "Unsupported file type:"
msgstr "Nicht unterstützter Dateityp:"
msgid "Unpacking data error."
msgstr "Fehler beim Entpacken von Daten."
msgstr "Fehler beim Entpacken der Daten."
msgid "XML parsing error:"
msgstr "XML Parsing-Fehler:"
@@ -759,7 +766,7 @@ msgid "Use HTTP"
msgstr "HTTP verwenden"
msgid "Close playback"
msgstr "Wiedergabe schliessen"
msgstr "Wiedergabe schließen"
msgid "Import YouTube playlist"
msgstr "YouTube-Wiedergabeliste importieren"
@@ -768,8 +775,8 @@ msgid ""
"Found a link to the YouTube resource!\n"
"Try to get a direct link to the video?"
msgstr ""
"Ich habe einen Link zur YouTube-Ressource gefunden!\n"
"Versuchen einen direkten Link zum Video zu bekommen?"
"Link zur YouTube-Ressource gefunden!\n"
"Soll versucht werden, einen Direkt-Link zum Video zu erzeugen?"
msgid "Playlist import"
msgstr "Playlist-Import"
@@ -778,7 +785,7 @@ msgid "Getting link error:"
msgstr "Link-Fehler erhalten:"
msgid "Extra"
msgstr "Extra"
msgstr "Extras"
msgid "Apply profile settings"
msgstr "Profileinstellungen anwenden"
@@ -787,7 +794,7 @@ msgid "Settings type:"
msgstr "Art der Einstellungen:"
msgid "Set default"
msgstr "Standard setzen"
msgstr "Standard wiederherstellen"
msgid "Language:"
msgstr "Sprache:"
@@ -799,16 +806,16 @@ msgid "Enable direct playback bar"
msgstr "Aktivieren der direkten Wiedergabeleiste"
msgid "Enables direct sending and playback of media links on the receiver"
msgstr "Ermöglicht das direkte Senden und Abspielen von Medienlinks auf dem Box"
msgstr "Ermöglicht das direkte Senden und Abspielen von Medienlinks auf dem Receiver"
msgid "Watch the channel in the program"
msgstr "Gucken den Kanal im Programm an"
msgstr "Kanal im Programm ansehen"
msgid "Zap and Play"
msgstr "Zap und Abspielen"
msgid "Drag or paste the link here"
msgstr "Ziehe den Link hierher oder füge ihn ein"
msgstr "Link hineinziehen oder einfügen"
msgid "Remove added links in the playlist"
msgstr "Hinzugefügte Links in der Wiedergabeliste entfernen"
@@ -826,13 +833,13 @@ msgid "Reset"
msgstr "Reset"
msgid "File"
msgstr "Ablage"
msgstr "Datei"
msgid "Picons manager"
msgstr "Picons-Manager"
msgid "Explorer"
msgstr ""
msgstr "Explorer"
msgid "Satellite url:"
msgstr "Satellit URL:"
@@ -910,40 +917,40 @@ msgid "Height (px):"
msgstr "Höhe (px):"
msgid "Channels:"
msgstr "Kanälen:"
msgstr "Kanäle:"
msgid "Sample rate (Hz):"
msgstr "Samplerate (Hz):"
msgstr "Sample-Rate (Hz):"
msgid "Play streams mode:"
msgstr "Streams Abspielen-Modus:"
msgstr "Stream-Abspielmodus:"
msgid "Built-in player"
msgstr "Integrierter Player"
msgid "VLC media player"
msgstr "VLC Media Player"
msgid "In a separate window"
msgstr "In einem separaten Fenster"
msgid "Only get m3u file"
msgstr "Nur m3u-Datei erhalten"
msgid "Save and restart the program to apply the settings."
msgstr "Speicher und starte das Programm neu, um die Einstellungen zu übernehmen."
msgstr "Änderungen speichern und anschließend das Programm neustarten, um die Einstellungen zu übernehmen."
msgid "Some images may have problems displaying the favorites list!"
msgstr "Einige Images können Probleme mit der Anzeige der Favoritenliste haben!"
msgstr "Einige Bilder können Probleme in der Anzeige der Favoritenliste haben!"
msgid "Operates in standby mode or current active transponder!"
msgstr "Arbeitet im Standby-Modus oder auf dem aktuell aktiven Transponder!"
msgstr "Arbeitet im Standby-Modus oder der Transponder ist bereits in Verwendung!"
msgid "No connection to the receiver!"
msgstr "Keine Verbindung zum Box!"
msgstr "Keine Verbindung zum Receiver!"
msgid "Signal level"
msgstr "Signalpegel"
msgid "Receiver info"
msgstr "Box-Info"
msgstr "Receiver-Info"
msgid "A profile with that name exists!"
msgstr "Ein Profil mit diesem Namen existiert!"
@@ -952,10 +959,10 @@ msgid "Show short info as hints in the main services list"
msgstr "Kurzinfos als Tooltips in der Hauptliste anzeigen"
msgid "Show detailed info as hints in the bouquet list"
msgstr "Detaillierteinfos als Tooltips in der Bouquetliste anzeigen"
msgstr "Detaillierte Informationen als Tooltips in der Bouquetliste anzeigen"
msgid "Enable alternate bouquet file naming"
msgstr "Aktivieren der Alternativerbenennung für Bouquet-Dateien"
msgstr "Aktivieren der alternativen Benennung für Bouquet-Dateien"
msgid "Allows you to name bouquet files using their names."
msgstr "Ermöglicht Bouquet-Dateien mit ihren Namen zu benennen."
@@ -964,7 +971,7 @@ msgid "Appearance"
msgstr "Aussehen"
msgid "Enable Themes support"
msgstr "Unterstützung von Themen aktivieren"
msgstr "Theme Unterstützung aktivieren"
msgid "Gtk3 Theme:"
msgstr "Gtk3-Theme:"
@@ -973,7 +980,7 @@ msgid "Icon Theme:"
msgstr "Icon-Theme:"
msgid "Gtk3 Themes and Icons:"
msgstr "Gtk3 Themes and Icons:"
msgstr "Gtk3 Themes und Icons:"
msgid "Deleting data..."
msgstr "Daten löschen..."
@@ -982,7 +989,7 @@ msgid "Download from the receiver"
msgstr "Downloaden vom Receiver"
msgid "Remove all picons from the receiver"
msgstr "Alle Picons aus dem Receiver entfernen"
msgstr "Alle Picons vom dem Receiver entfernen"
msgid "Service reference"
msgstr "Kanalreferenz"
@@ -1015,7 +1022,7 @@ msgid "Are you sure you want to change the order\n\t of services in this bouquet
msgstr "Bist du sicher, dass du die Reihenfolge der Dienstleistungen\n\t in diesem Bouquet ändern willst?"
msgid "Remove from the receiver"
msgstr "Aus dem Receiver entfernen"
msgstr "Vom Receiver entfernen"
msgid "Screenshot"
msgstr "Screenshot"
@@ -1033,8 +1040,226 @@ msgid "Can't Playback!"
msgstr "Kann nicht abgespielt werden!"
msgid "Enable Dark Mode"
msgstr "Dunkelmodus aktivieren"
msgstr "Dunkler Modus aktivieren"
msgid "Extract..."
msgstr "Entpacken..."
msgid "Unsupported format!"
msgstr "Nicht unterstütztes Format!"
msgid "Combine with the current data?"
msgstr "Mit den aktuellen Daten kombinieren?"
msgid "Importing data done!"
msgstr "Daten-Import abgeschlossen!"
msgid "Current service"
msgstr "Aktueller Service"
msgid "Open folder"
msgstr "Ordner öffnen"
msgid "Open archive"
msgstr "Archiv öffnen"
msgid "Import from Web"
msgstr "Import aus dem Internet"
msgid "Control"
msgstr "Steuerung"
msgid "Timers"
msgstr "Timer"
msgid "Timer"
msgstr "Timer"
msgid "Add timer"
msgstr "Timer hinzufügen"
msgid "Hr."
msgstr "Std."
msgid "Min."
msgstr "Min."
msgid "Power"
msgstr "Power"
msgid "Standby"
msgstr "Standby"
msgid "Wake Up"
msgstr "Aufwachen"
msgid "Reboot"
msgstr "Neustarten"
msgid "Restart GUI"
msgstr "GUI neustarten"
msgid "Shutdown"
msgstr "Ausschalten"
msgid "Shut down"
msgstr "Ausschalten"
msgid "Do Nothing"
msgstr "Nichts tun"
msgid "Auto"
msgstr "Auto"
msgid "Grab screenshot"
msgstr "Screenshot aufnehmen"
msgid "Enabled:"
msgstr "Aktiviert:"
msgid "Name:"
msgstr "Name:"
msgid "Description:"
msgstr "Beschreibung:"
msgid "Service:"
msgstr "Service"
msgid "Service reference:"
msgstr "Kanalreferenz"
msgid "Event ID:"
msgstr "Ereignis-ID:"
msgid "Begins:"
msgstr "Beginnt:"
msgid "Ends:"
msgstr "Endet:"
msgid "Repeated:"
msgstr "Wiederholt:"
msgid "Action:"
msgstr "Aktion:"
msgid "After event:"
msgstr "Nach dem Ereignis:"
msgid "Location:"
msgstr "Zielverzeichnis:"
msgid "Mo"
msgstr "Mo"
msgid "Tu"
msgstr "Di"
msgid "We"
msgstr "Mi"
msgid "Th"
msgstr "Do"
msgid "Fr"
msgstr "Fr"
msgid "Sa"
msgstr "Sa"
msgid "Su"
msgstr "So"
msgid "Set"
msgstr "Einstellen"
msgid "Services update"
msgstr "Dienste-Update"
msgid "Create folder"
msgstr "Ordner erstellen"
msgid "FTP client"
msgstr "FTP-Client"
msgid "The file size is too large!"
msgstr "Die Datei ist zu groß!"
msgid "Connect"
msgstr "Verbinden"
msgid "Disconnect"
msgstr "Verbindung trennen"
msgid "Size"
msgstr "Größe"
msgid "Date"
msgstr "Datum"
msgid "Attr."
msgstr "Attr."
msgid "Toggle display position"
msgstr "Anzeigeposition umschalten"
msgid "Alternatives"
msgstr "Alternativen"
msgid "Add alternatives"
msgstr "Alternativen hinzufügen"
msgid "DreamOS only!"
msgstr "Nur DreamOS!"
msgid "A similar service is already in this list!"
msgstr "Ein ähnlicher Dienst ist bereits in dieser Liste enthalten!"
msgid "Play mode has been changed!\nRestart the program to apply the settings."
msgstr "Abspiel-Modus wurde geändert!\nStarte das Programm neu, um die Einstellungen zu übernehmen."
msgid "Set values for TID, NID and Namespace for correct naming of the picons!"
msgstr "Stelle die Werte für TID, NID und Namespace für die korrekte Benennung der Picons ein!"
msgid "Streams detected:"
msgstr "Streams gefunden:"
msgid "Download picons"
msgstr "Download picons"
msgid "Errors:"
msgstr "Fehler:"
msgid "Use to play streams:"
msgstr "Zum Abspielen von Streams verwenden:"
msgid "Font in the lists:"
msgstr "Schrift in den Listen:"
msgid "Picons size in the lists:"
msgstr "Picons Größe in den Listen:"
msgid "Logo size in tooltips:"
msgstr "Logo-Größe in Tooltips:"
msgid "Save as"
msgstr "Speichern als"
msgid "Mark duplicates"
msgstr "Duplikate markieren"
msgid "Load only for selected bouquet"
msgstr "Nur für ausgewähltes Bouquet laden"
msgid "The task is canceled!"
msgstr "Der Task wird abgebrochen!"
msgid "Data loading in progress!"
msgstr "Daten werden geladen!"
msgid "Recordings"
msgstr "Aufnahmen"
msgid "Help"
msgstr "Hilfe"

View File

@@ -1,7 +1,7 @@
# Copyright (C) 2018-2020 Frank Neirynck
# This file is distributed under the MIT license.
#
# Frank Neirynck <frank@insink.be>, 2018-2019.
# Frank Neirynck <frank@insink.be>, 2018-2020.
#
msgid ""
msgstr ""
@@ -54,7 +54,7 @@ msgid "Current IP:"
msgstr "IP actual:"
msgid "Assign"
msgstr "Assignar"
msgstr "Asignar"
msgid "Bouquet details"
msgstr "Detalles bouquet"
@@ -66,7 +66,7 @@ msgid "Copy"
msgstr "Copiar"
msgid "Copy reference"
msgstr "Copia de referencia"
msgstr "Copiar referencia"
msgid "Download"
msgstr "Descargar"
@@ -123,7 +123,7 @@ msgid "New"
msgstr "Nuevo"
msgid "New bouquet"
msgstr "Bouquet nuevo"
msgstr "Nuevo bouquet"
msgid "Create bouquet"
msgstr "Crear bouquet"
@@ -156,10 +156,10 @@ msgid "Picons"
msgstr "Picons"
msgid "Picons downloader"
msgstr "Descargar picons"
msgstr "Descarga de picons"
msgid "Satellites downloader"
msgstr "Descargar satélites"
msgstr "Descarga de satélites"
msgid "Remove"
msgstr "Quitar"
@@ -206,11 +206,11 @@ msgstr "Ruta de datos actual:"
msgid "Data:"
msgstr "Datos:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux"
msgstr "Editor de canales y satélites Enigma2 para GNU/Linux"
msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "Editor de canales y satélites Enigma2 para GNU/Linux."
msgid "Host:"
msgstr "Anfitrión:"
msgstr "Host:"
msgid "Loading data..."
msgstr "Cargando datos..."
@@ -256,7 +256,7 @@ msgstr "Extra:"
# Filter bar
msgid "Only free"
msgstr "Solamente gratis"
msgstr "Solamente libres"
msgid "All positions"
msgstr "Todas las posiciones"
@@ -272,10 +272,10 @@ msgid "Stop playback"
msgstr "Detener la reproducción"
msgid "Previous stream in the list"
msgstr "Anterior flujo en la lista"
msgstr "Anterior stream en la lista"
msgid "Next stream in the list"
msgstr "Siguiente flujo en la lista"
msgstr "Siguiente stream en la lista"
msgid "Toggle in fullscreen"
msgstr "Cambiar a pantalla completa"
@@ -294,7 +294,7 @@ msgid "Receive picons"
msgstr "Recibir picons"
msgid "Picons name format:"
msgstr "Picons formato nombres:"
msgstr "Formato del nombre de los picons:"
msgid "Resize:"
msgstr "Redimensionar:"
@@ -433,7 +433,7 @@ msgstr "Buscar"
# IPTV dialog
msgid "Stream data"
msgstr "Transmitir flujo"
msgstr "Transmitir stream"
# IPTV list configuration dialog
msgid "Starting values"
@@ -443,7 +443,7 @@ msgid "Reset to default"
msgstr "Restablecer a predeterminado"
msgid "IPTV streams list configuration"
msgstr "Configurar lista de flujos IPTV"
msgstr "Configurar lista de streams IPTV"
# Settings dialog
msgid "Preferences"
@@ -511,13 +511,13 @@ msgid "No m3u file is selected!"
msgstr "¡No se ha seleccionado ningún fichero m3u!"
msgid "Not implemented yet!"
msgstr "¡Aún sin implementar!"
msgstr "¡No implementado!"
msgid "The text of marker is empty, please try again!"
msgstr "¡El texto del marcador está vacío, inténtalo de nuevo!"
msgid "Please, select only one item!"
msgstr "¡Por favor, seleccione sólo un elemento!"
msgstr "¡Por favor, seleccione un único elemento!"
msgid "No png file is selected!"
msgstr "¡No se ha seleccionado ningún fichero png!"
@@ -526,7 +526,7 @@ msgid "No reference is present!"
msgstr "¡Ninguna referencia presente!"
msgid "No selected item!"
msgstr "¡Ningún elemento seleccionado!"
msgstr "¡No se ha seleccionado ningún elemento!"
msgid "The task is already running!"
msgstr "¡La tarea ya se está ejecutando!"
@@ -562,23 +562,23 @@ msgid "Operation not allowed in this context!"
msgstr "¡Operación no permitida en este contexto!"
msgid "No VLC is found. Check that it is installed!"
msgstr "VLC no encontrado. ¡Verifique que está instalado!"
msgstr "VLC no encontrado. ¡Compruebe que está instalado!"
# Search unavailable streams dialog
msgid "Please wait, streams testing in progress..."
msgstr "Por favor espere, hay una prueba de flujo en progreso..."
msgstr "Por favor espere, hay una prueba de stream en progreso..."
msgid "Found"
msgstr "Encontrado"
msgid "unavailable streams."
msgstr "Flujos no presentes."
msgstr "Streams no presentes."
msgid "No changes required!"
msgstr "¡Ningún cambio requerido!"
msgstr "¡No se requieren cambios!"
msgid "This list does not contains IPTV streams!"
msgstr "¡La lista no contiene flujos IPTV!"
msgstr "¡La lista no contiene streams IPTV!"
msgid "New empty configuration"
msgstr "Nueva configuración vacía"
@@ -672,7 +672,7 @@ msgid "Zap"
msgstr "Zapear"
msgid "Play stream"
msgstr "Reproducir flujo"
msgstr "Reproducir stream"
msgid "Disabled"
msgstr "Desactivado"
@@ -690,7 +690,7 @@ msgid "Switch the channel and watch in the program(Ctrl + W)"
msgstr "Poner el canal y ver en el programa (Ctrl + W)"
msgid "Play IPTV or other stream in the program(Ctrl + P)"
msgstr "Reproducir IPTV u otro flujo en el programa (Ctrl + P)"
msgstr "Reproducir IPTV u otro stream en el programa (Ctrl + P)"
msgid "Export to m3u"
msgstr "Exportar a m3u"
@@ -795,7 +795,7 @@ msgid "Language:"
msgstr "Idioma:"
msgid "Load the last open configuration at program startup"
msgstr "Cargar la última configuración abierta al iniciar el programa"
msgstr "Cargar la última configuración usada al iniciar el programa"
msgid "Enable direct playback bar"
msgstr "Habilitar la barra de reproducción directa"
@@ -828,16 +828,16 @@ msgid "Reset"
msgstr "Reset"
msgid "File"
msgstr "Archivo"
msgstr "Fichero"
msgid "Picons manager"
msgstr "Picons manager"
msgstr "Gestor de picons"
msgid "Explorer"
msgstr "Explorador"
msgid "Satellite url:"
msgstr "Url Satelite:"
msgstr "Url satélite:"
msgid "Cut"
msgstr "Cortar"
@@ -867,43 +867,43 @@ msgid "IPTV tools"
msgstr "Intrumentos IPTV"
msgid "Make profile folder as default for the additional data"
msgstr "Has folder de perfil estandar para datos adicionales"
msgstr "Usar por defecto el directorio de perfil para los datos adionales"
msgid "Default data path:"
msgstr "Ruta estandar de datos:"
msgstr "Ruta estándar de datos:"
msgid "Streams record path:"
msgstr "Ruta de gravacion de stream:"
msgstr "Ruta de grabación del stream:"
msgid "Record"
msgstr "Gravar"
msgstr "Grabación"
msgid "Record:"
msgstr "Gravar:"
msgstr "Grabación:"
msgid "Record to disk:"
msgstr "Gravar en disco:"
msgstr "Grabación en disco:"
msgid "Streaming"
msgstr "Streameando"
msgstr "Streaming"
msgid "Activate transcoding"
msgstr "Activer transcodificacion"
msgstr "Activar transcodificación"
msgid "Presets:"
msgstr "Presets:"
msgstr "Preajustes:"
msgid "Video options:"
msgstr "Opciones Video:"
msgstr "Opciones deo:"
msgid "Audio options:"
msgstr "Opciones Audio:"
msgstr "Opciones audio:"
msgid "Bitrate (kb/s):"
msgstr "Bitrate (kb/s):"
msgid "Codec:"
msgstr "Codec:"
msgstr "Códec:"
msgid "Width (px):"
msgstr "Ancho (px):"
@@ -915,10 +915,10 @@ msgid "Channels:"
msgstr "Canales:"
msgid "Sample rate (Hz):"
msgstr "Sample rate (Гц):"
msgstr "Frecuencia de muestreo (Hz):"
msgid "Play streams mode:"
msgstr "Tocar en modo streams:"
msgstr "Modo de reproducción de los streams:"
msgid "Built-in player"
msgstr "Reproductor interno"
@@ -927,40 +927,40 @@ msgid "VLC media player"
msgstr "Reproductor VLC"
msgid "Only get m3u file"
msgstr "Solo bajar archivo *.m3u"
msgstr "Sólo bajar fichero m3u"
msgid "Save and restart the program to apply the settings."
msgstr "Guarde y reinicie el programa para aplicar la configuración."
msgstr "Guardar y reiniciar el programa para aplicar la configuración."
msgid "Some images may have problems displaying the favorites list!"
msgstr "Algunas imágenes pueden tener problemas para mostrar la lista de favoritos!"
msgstr "Algunas imágenes pueden tener problemas al mostrar la lista de favoritos!"
msgid "Operates in standby mode or current active transponder!"
msgstr "Funciona en modo de espera o transpondedor activo actual!"
msgstr "¡Funciona en modo de espera o transpondedor activo actual!"
msgid "No connection to the receiver!"
msgstr "Sin conexión al receptor!"
msgstr "¡Desconectado del receptor!"
msgid "Signal level"
msgstr "Nivel de señal"
msgid "Receiver info"
msgstr "Informacion sobre receptor"
msgstr "Información sobre el receptor"
msgid "A profile with that name exists!"
msgstr "Existe un perfil con ese nombre!"
msgstr "¡Ya existe un perfil con ese nombre!"
msgid "Show short info as hints in the main services list"
msgstr "Mostrar información breve como sugerencias en la lista de servicios principal"
msgid "Show detailed info as hints in the bouquet list"
msgstr "Mostrar información detallada como pistas en la lista de bouquet"
msgstr "Mostrar información detallada como consejo en la lista de bouquets"
msgid "Enable alternate bouquet file naming"
msgstr "Habilitar nombres alternativos de archivos de bouquet"
msgstr "Habilitar nombres alternativos para los ficheros de bouquets"
msgid "Allows you to name bouquet files using their names."
msgstr "Le permite nombrar archivos de bouquet usando sus nombres."
msgstr "Permite nombrar ficheros de bouquets usando sus propios nombres."
msgid "Appearance"
msgstr "Apariencia"
@@ -969,16 +969,16 @@ msgid "Enable Themes support"
msgstr "Habilitar compatibilidad con temas"
msgid "Gtk3 Theme:"
msgstr "Тема Gtk3:"
msgstr "Теma Gtk3:"
msgid "Icon Theme:"
msgstr "Тема Icono:"
msgstr "Icono del tema:"
msgid "Gtk3 Themes and Icons:"
msgstr "Tema Gtk3 e Iconos:"
msgstr "Tema Gtk3 e iconos:"
msgid "Deleting data..."
msgstr "Borrando datos ..."
msgstr "Borrando datos..."
msgid "Download from the receiver"
msgstr "Descargar desde el receptor"
@@ -987,13 +987,13 @@ msgid "Remove all picons from the receiver"
msgstr "Eliminar todos los picons del receptor"
msgid "Service reference"
msgstr "Referencia de servicio"
msgstr "Referencia del servicio"
msgid "Enable support for"
msgstr "Habilitar soporte para"
msgid "Auto-check for updates"
msgstr "Verificación automática de actualizaciones"
msgstr "Comprobación automática de actualizaciones"
msgid "Filter services"
msgstr "Filtrar servicios"
@@ -1005,23 +1005,23 @@ msgid "Destination:"
msgstr "Destino:"
msgid "EXPERIMENTAL!"
msgstr "EXPERIMENTAL!"
msgstr "¡EXPERIMENTAL!"
msgid "Sorting data..."
msgstr "Ordenando datos..."
msgid "There are unsaved changes.\n\n\t Save them now?"
msgstr "Hay cambios sin guardar.\n\n\t ¿Guardarlos ahora?"
msgstr "Hay cambios sin guardar.\n\n\t ¿Desea guardarlos ahora?"
msgid "Are you sure you want to change the order\n\t of services in this bouquet?"
msgstr "¿Está seguro de que desea cambiar el orden\n\t de servicios en este bouquet?"
msgstr "¿Está seguro de querer cambiar el orden\n\t de servicios en este bouquet?"
msgid "Remove from the receiver"
msgstr "Retirar del receptor"
msgstr "Eliminar del receptor"
msgid "Screenshot"
msgstr "Captura de pantalla"
msgid "Video"
msgstr "Vidео"
msgstr "Vídео"

View File

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

View File

@@ -1,6 +1,7 @@
# Copyright (C) 2018-2019 Dmitriy Yefremov
# Copyright (C) 2018-2020 Dmitriy Yefremov
# This file is distributed under the MIT license.
#
#
msgid ""
msgstr ""
"Last-Translator: wwns\n"
@@ -205,8 +206,8 @@ msgstr "Aktualna ścieżka danych:"
msgid "Data:"
msgstr "Dane:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux"
msgstr "Edytor kanałów Enigma2 i listy satelitów dla GNU/Linux"
msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "Edytor kanałów Enigma2 i listy satelitów dla GNU/Linux."
msgid "Host:"
msgstr "Host:"
@@ -265,6 +266,9 @@ msgstr "Ukryj/Pomiń"
msgid "Parent lock"
msgstr "Blokada rodzicielska"
msgid "There are unsaved changes.\n\n\t Save them now?"
msgstr "Istnieją niezapisane zmiany.\n\n\t Zapisać je teraz?"
# Filter bar
msgid "Only free"
msgstr "Tylko FTA"
@@ -386,6 +390,12 @@ msgid "Remove selection"
msgstr "Usuń wybrane"
# Service details dialog
msgid "To the top"
msgstr "Przenieś na góre bukietu"
msgid "To the end"
msgstr "Przenieś na koniec bukietu"
msgid "Service data:"
msgstr "Dane usług:"
@@ -596,9 +606,6 @@ msgstr "Brak danych do zapisania!"
msgid "Network"
msgstr "Sieć"
msgid "Program"
msgstr "Program"
msgid "Backup"
msgstr "Kopia"
@@ -626,6 +633,15 @@ msgstr "Wyjście"
msgid "Tools"
msgstr "Narzędzia"
msgid "Cut"
msgstr "Wytnij"
msgid "Paste"
msgstr "Wklej"
msgid "Insert space"
msgstr "Wstaw spację"
# Import
msgid "Import"
msgstr "Importuj"
@@ -648,12 +664,12 @@ msgstr "Usuń wszystkie nieużywane"
msgid "Test"
msgstr "Test"
msgid "Details"
msgstr "Właściwości"
msgid "Test connection"
msgstr "Testuj połączenie"
msgid "Double click on the service in the bouquet list:"
msgstr "Kliknij dwukrotnie usługę na liście bukietów:"
msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Przełącz(zap) kanał(Ctrl + Z)"
@@ -667,7 +683,7 @@ msgid "Export to m3u"
msgstr "Eksportuj do m3u"
msgid "EPG configuration"
msgstr "Koniguruj EPG"
msgstr "Konfiguruj EPG"
msgid "Apply"
msgstr "Zatwierdź"
@@ -754,17 +770,11 @@ msgstr "Import listy odtwarzania"
msgid "Getting link error:"
msgstr "Błąd pobierania łącza:"
msgid "Apply profile settings"
msgstr "Zastosuj ustawienia profilu"
msgid "Settings type:"
msgstr "Ustawienia dla:"
msgid "Set default"
msgstr "Uataw domyślnie"
msgid "Enable direct playback bar"
msgstr "Włącz pasek bezpośredniego odtwarzania"
msgstr "Ustaw domyślnie"
msgid "Enables direct sending and playback of media links on the receiver"
msgstr "Umożliwia bezpośrednie wysyłanie i odtwarzanie łączy multimedialnych w odbiorniku"
@@ -785,7 +795,10 @@ msgid "Remove picons from the receiver"
msgstr "Usuń pikony z odbiornika"
msgid "Use http to reload data in the receiver."
msgstr "Użyj http aby ponownie załadować dane do odbiornika."
msgstr "Użyj http aby przeładować dane w odbiorniku."
msgid "Apply profile settings"
msgstr "Zastosuj ustawienia profilu"
msgid "Drag or paste the link here"
msgstr "Przeciągnij lub wklej tutaj link"
@@ -808,10 +821,16 @@ msgstr "Pobierz z odbiornika"
msgid "The Neutrino has only experimental support. Not all features are supported!"
msgstr "Neutrino ma jedynie wsparcie eksperymentalne. Nie wszystkie funkcje są obsługiwane!"
msgid "Some images may have problems displaying the favorites list!"
msgstr "Niektóre obrazy mogą mieć problemy z wyświetlaniem listy ulubionych!"
# Appearance
msgid "Appearance"
msgstr "Wygląd"
msgid "Enable Dark Mode"
msgstr "Włącz tryb ciemny"
msgid "Enable Themes support"
msgstr "Włącz obsługę motywów"
@@ -834,23 +853,17 @@ msgstr "Zapisz i uruchom ponownie program, aby zastosować ustawienia."
msgid "Extra"
msgstr "Ekstra"
msgid "Enable lamedb ver. 5 support"
msgstr "Włącz lamedb wer. 5 wsparcie"
msgid "Enable alternate bouquet file naming"
msgstr "Włącz alternatywne nazewnictwo plików bukietów"
msgid "Some images may have problems displaying the favorites list!"
msgstr "Niektóre obrazy mogą mieć problemy z wyświetlaniem listy ulubionych!"
msgid "Allows you to name bouquet files using their names."
msgstr "Pozwala nazwać pliki bukietów przy użyciu ich nazw."
msgid "Enable HTTP API"
msgstr "Włącz API HTTP"
msgid "Enable send to receiver"
msgstr "Włącz wysyłanie do odbiornika"
msgid "Double click on the service in the bouquet list:"
msgstr "Kliknij dwukrotnie usługę na liście bukietów:"
msgid "Zap"
msgstr "Przełącz"
@@ -867,6 +880,28 @@ msgstr "Odtwórz strumień"
msgid "Disabled"
msgstr "Wyłączone"
msgid "Enable experimental features"
msgstr "Włącz funkcje eksperymentalne"
msgid "Enable lamedb ver. 5 support"
msgstr "Włącz wsparcie dla lamedb w wer.5"
msgid "Enable support for"
msgstr "Włącz obsługę"
msgid "Enables parsing links using youtube-dl to get direct links to media"
msgstr "Umożliwia analizowanie linków za pomocą youtube-dl, aby uzyskać bezpośrednie linki do multimediów"
msgid "Auto-check for updates"
msgstr "Automatyczne sprawdzanie aktualizacji"
msgid "Enable direct playback bar"
msgstr "Włącz pasek bezpośredniego odtwarzania"
#Program
msgid "Program"
msgstr "Program"
msgid "Language:"
msgstr "Język:"
@@ -876,7 +911,7 @@ msgstr "Załaduj ostatnią otwartą konfigurację podczas uruchamiania programu"
msgid "Show short info as hints in the main services list"
msgstr "Pokaż krótkie informacje jako wskazówki na głównej liście usług"
msgid "Show detailed info as hints in the bouquet list"
msgid "Show detailed info as hints in the bouquet list"
msgstr "Pokaż szczegółowe informacje jako wskazówki na liście bukietów"
msgid "Set background color for the services"
@@ -962,8 +997,8 @@ msgstr "Ustawia folder profilu jako domyślny do przechowywania pikonów, kopii
msgid "Default data path:"
msgstr "Domyślna ścieżka danych:"
msgid "Record"
msgstr "Nagrania"
msgid "Record:"
msgstr "Nagrania:"
msgid "Streams record path:"
msgstr "Ścieżka zapisu nagrań:"

View File

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

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2018-2020 Dmitriy Yefremov
# Copyright (C) 2018-2021 Dmitriy Yefremov
# This file is distributed under the MIT license.
#
#
@@ -104,6 +104,9 @@ msgstr "Имя по умолчанию"
msgid "Insert marker"
msgstr "Вставить маркер"
msgid "Insert space"
msgstr "Вставить пробел"
msgid "Locate in services"
msgstr "Найти в списке сервисов"
@@ -200,8 +203,8 @@ msgstr "Текущий путь к данным:"
msgid "Data:"
msgstr "Данные:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux"
msgstr "Редактор списка каналов и спутников Enigma2\n для GNU/Linux"
msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "Редактор списка каналов и спутников Enigma2\n для GNU/Linux."
msgid "Host:"
msgstr "Адрес ресивера:"
@@ -507,6 +510,9 @@ msgstr "Пожалуйста, выберите только один элеме
msgid "No png file is selected!"
msgstr "Не выбран png файл!"
msgid "No profile selected!"
msgstr "Не выбран профиль!"
msgid "No reference is present!"
msgstr "Ссылка не найдена!"
@@ -672,7 +678,7 @@ msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Переключить канал(Ctrl + Z)"
msgid "Switch the channel and watch in the program(Ctrl + W)"
msgstr "Переклють канал и просмотр в программе(Ctrl + W)."
msgstr "Переключить канал и просмотр в программе(Ctrl + W)."
msgid "Play IPTV or other stream in the program(Ctrl + P)"
msgstr "Воспроизведение IPTV или другого потока в программе(Ctrl + P)"
@@ -908,8 +914,8 @@ msgstr "Режим воспроизведения потоков:"
msgid "Built-in player"
msgstr "Встроенный плеер"
msgid "VLC media player"
msgstr "VLC медиаплеер"
msgid "In a separate window"
msgstr "В отдельном окне"
msgid "Only get m3u file"
msgstr "Получить файл *.m3u"
@@ -1022,3 +1028,221 @@ msgstr "Не удается воспроизвести!"
msgid "Enable Dark Mode"
msgstr "Включить темный режим"
msgid "Extract..."
msgstr "Извлечь..."
msgid "Unsupported format!"
msgstr "Неподдерживаемый формат!"
msgid "Combine with the current data?"
msgstr "Объединить с текущими данными?"
msgid "Importing data done!"
msgstr "Импорт данных завершен!"
msgid "Current service"
msgstr "Текущий сервис"
msgid "Open folder"
msgstr "Открыть папку"
msgid "Open archive"
msgstr "Открыть архив"
msgid "Import from Web"
msgstr "Импорт из сети"
msgid "Control"
msgstr "Управление"
msgid "Timers"
msgstr "Таймеры"
msgid "Timer"
msgstr "Таймер"
msgid "Add timer"
msgstr "Добавить таймер"
msgid "Hr."
msgstr "ч."
msgid "Min."
msgstr "мин."
msgid "Power"
msgstr "Питание"
msgid "Standby"
msgstr "Режим ожидания"
msgid "Wake Up"
msgstr "Пробуждение"
msgid "Reboot"
msgstr "Перезагрузка"
msgid "Restart GUI"
msgstr "Перезагрузить графический интерфейс"
msgid "Shutdown"
msgstr "Выключение"
msgid "Shut down"
msgstr "Выключить"
msgid "Do Nothing"
msgstr "Ничего не делать"
msgid "Auto"
msgstr "Авто"
msgid "Grab screenshot"
msgstr "Сделать скриншот"
msgid "Enabled:"
msgstr "Включен:"
msgid "Name:"
msgstr "Имя:"
msgid "Description:"
msgstr "Описание:"
msgid "Service:"
msgstr "Сервис:"
msgid "Service reference:"
msgstr "Сервисная ссылка:"
msgid "Event ID:"
msgstr "ID события:"
msgid "Begins:"
msgstr "Начало:"
msgid "Ends:"
msgstr "Окончание:"
msgid "Repeated:"
msgstr "Повтор:"
msgid "Action:"
msgstr "Действие:"
msgid "After event:"
msgstr "После события:"
msgid "Location:"
msgstr "Расположение:"
msgid "Mo"
msgstr "Пн"
msgid "Tu"
msgstr "Вт"
msgid "We"
msgstr "Ср"
msgid "Th"
msgstr "Чт"
msgid "Fr"
msgstr "Пт"
msgid "Sa"
msgstr "Сб"
msgid "Su"
msgstr "Вс"
msgid "Set"
msgstr "Установить"
msgid "Services update"
msgstr "Обновление сервисов"
msgid "Create folder"
msgstr "Создать папку"
msgid "FTP client"
msgstr "FTP-клиент"
msgid "The file size is too large!"
msgstr "Размер файла слишком велик!"
msgid "Connect"
msgstr "Соединение"
msgid "Disconnect"
msgstr "Разъединить"
msgid "Size"
msgstr "Размер"
msgid "Date"
msgstr "Дата"
msgid "Toggle display position"
msgstr "Переключить позицию отображения"
msgid "Alternatives"
msgstr "Альтернативы"
msgid "Add alternatives"
msgstr "Добавить альтернативы"
msgid "DreamOS only!"
msgstr "Только DreamOS!"
msgid "A similar service is already in this list!"
msgstr "Подобный сервис уже есть в этом списке!"
msgid "Play mode has been changed!\nRestart the program to apply the settings."
msgstr "Изменен режим воспроизведения!\nПерезапустите программу для применения настроек."
msgid "Set values for TID, NID and Namespace for correct naming of the picons!"
msgstr "Установите значения TID, NID и пр. имен для правильного именования пиконов!"
msgid "Streams detected:"
msgstr "Обнаружено потоков:"
msgid "Download picons"
msgstr "Загрузить пиконы"
msgid "Errors:"
msgstr "Ошибок:"
msgid "Use to play streams:"
msgstr "Использовать для воспроизведения потоков:"
msgid "Font in the lists:"
msgstr "Шрифт в списках:"
msgid "Picons size in the lists:"
msgstr "Размер пиконов в списках:"
msgid "Logo size in tooltips:"
msgstr "Размер логотипа во всплывающих подсказках:"
msgid "Save as"
msgstr "Сохранить как"
msgid "Mark duplicates"
msgstr "Отметить дубликаты"
msgid "Load only for selected bouquet"
msgstr "Загрузить только для выбранного букета"
msgid "The task is canceled!"
msgstr "Задание отменено!"
msgid "Data loading in progress!"
msgstr "Выполняется загрузка данных!"
msgid "Recordings"
msgstr "Записи"
msgid "Help"
msgstr "Справка"

View File

@@ -3,13 +3,13 @@ msgstr ""
"Project-Id-Version: DemonEditor\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-16 15:59+0300\n"
"PO-Revision-Date: 2020-06-08 21:53+0300\n"
"PO-Revision-Date: 2021-06-13 14:54+0300\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Last-Translator: audi06_19 <info@dreamosat-forum.com>\n"
"Language-Team: \n"
"X-Generator: Poedit 2.2.1\n"
"X-Generator: Poedit 2.4.1\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Language: tr\n"
@@ -107,6 +107,9 @@ msgstr "Varsayılan adı ayarla"
msgid "Insert marker"
msgstr "İşaretçi ekle"
msgid "Insert space"
msgstr "Boşluk ekle"
msgid "Locate in services"
msgstr "Hizmetlerde bulun"
@@ -203,8 +206,8 @@ msgstr "Mevcut veri yolu:"
msgid "Data:"
msgstr "Veri:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux"
msgstr "GNU/Linux için Enigma2 kanalı ve uydu listesi editörü"
msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "GNU/Linux için Enigma2 kanalı ve uydu listesi editörü."
msgid "Host:"
msgstr "Ana bilgisayar:"
@@ -518,6 +521,9 @@ msgstr "Lütfen sadece bir ürün seçiniz!"
msgid "No png file is selected!"
msgstr "Hiçbir png dosyası seçilmedi!"
msgid "No profile selected!"
msgstr "Profil seçilmedi!"
msgid "No reference is present!"
msgstr "Referans yok!"
@@ -923,8 +929,8 @@ msgstr "Akışları oynatma modu:"
msgid "Built-in player"
msgstr "Dahili oynatıcı"
msgid "VLC media player"
msgstr "VLC media player"
msgid "In a separate window"
msgstr "Ayrı bir pencerede"
msgid "Only get m3u file"
msgstr "Sadece m3u dosyası al"
@@ -988,3 +994,278 @@ msgstr "Alıcıdaki tüm piconları kaldırın"
msgid "Service reference"
msgstr "Servis referansı"
msgid "Enable support for"
msgstr "Temalar için desteği etkinleştir"
msgid "Auto-check for updates"
msgstr "Güncellemeleri otomatik kontrol et"
msgid "Filter services"
msgstr "Services filtrele"
msgid "Filter services in the main list."
msgstr "Ana listedeki services filtreleyin."
msgid "Destination:"
msgstr "Hedef:"
msgid "EXPERIMENTAL!"
msgstr "EXPERIMENTAL!"
msgid "Sorting data..."
msgstr "Verilerin sıralanması..."
msgid ""
"There are unsaved changes.\n"
"\n"
"\t Save them now?"
msgstr ""
"Kaydedilmemiş değişiklikler var.\n"
"\n"
"\t Şimdi kaydedilsin mi?"
msgid ""
"Are you sure you want to change the order\n"
"\t of services in this bouquet?"
msgstr ""
"Sırayı değiştirmek istediğinizden emin misiniz\n"
"\t Bu buketdeki hizmetlerin sayısı?"
msgid "Remove from the receiver"
msgstr "Alıcıdaki tüm piconları kaldırın"
msgid "Screenshot"
msgstr "Ekran görüntüsü"
msgid "Video"
msgstr "Video"
msgid "The Neutrino has only experimental support. Not all features are supported!"
msgstr "Neutrino'nun yalnızca experimental desteği var. Tüm özellikler desteklenmiyor!"
msgid "Enable experimental features"
msgstr "Experimental özellikleri etkinleştirin"
msgid "Can't Playback!"
msgstr "Oynatılamıyor!"
msgid "Enable Dark Mode"
msgstr "Koyu Modu Etkinleştir"
msgid "Extract..."
msgstr "Çıkart..."
msgid "Unsupported format!"
msgstr "Desteklenmeyen format!"
msgid "Combine with the current data?"
msgstr "Mevcut verilerle birleştirilsin mi?"
msgid "Importing data done!"
msgstr "Verilerin içe aktarılması tamamlandı!"
msgid "Current service"
msgstr "Mevcut service"
msgid "Open folder"
msgstr "Dosya aç"
msgid "Open archive"
msgstr "Arşiv aç"
msgid "Import from Web"
msgstr "Web'den içe aktar"
msgid "Control"
msgstr "Control"
msgid "Timers"
msgstr "Zamanlayıcılar"
msgid "Timer"
msgstr "Zamanlayıcı"
msgid "Add timer"
msgstr "Zamanlayıcı ekle"
msgid "Hr."
msgstr "Hr."
msgid "Min."
msgstr "Min."
msgid "Power"
msgstr "Power"
msgid "Standby"
msgstr "Standby"
msgid "Wake Up"
msgstr "Wake Up"
msgid "Reboot"
msgstr "Reboot"
msgid "Restart GUI"
msgstr "Restart GUI"
msgid "Shutdown"
msgstr "Shutdown"
msgid "Shut down"
msgstr "Kapat"
msgid "Do Nothing"
msgstr "Hiçbir şey yapma"
msgid "Auto"
msgstr "Auto"
msgid "Grab screenshot"
msgstr "Ekran görüntüsü al"
msgid "Enabled:"
msgstr "Etkin:"
msgid "Name:"
msgstr "Ad:"
msgid "Description:"
msgstr "Açıklama:"
msgid "Service:"
msgstr "Service:"
msgid "Service reference:"
msgstr "Servis referansı:"
msgid "Event ID:"
msgstr "Olay Kimliği:"
msgid "Begins:"
msgstr "Başlıyor:"
msgid "Ends:"
msgstr "Bitiş:"
msgid "Repeated:"
msgstr "Tekrar:"
msgid "Action:"
msgstr "Aksiyon:"
msgid "After event:"
msgstr "Olaydan sonra:"
msgid "Location:"
msgstr "Konum:"
msgid "Mo"
msgstr "Pzt"
msgid "Tu"
msgstr "Sal"
msgid "We"
msgstr "Çar"
msgid "Th"
msgstr "Per"
msgid "Fr"
msgstr "Cum"
msgid "Sa"
msgstr "Cmt"
msgid "Su"
msgstr "Paz"
msgid "Set"
msgstr "Yüklemek"
msgid "Services update"
msgstr "Servisleri güncelle"
msgid "Create folder"
msgstr "Klasör oluştur"
msgid "FTP client"
msgstr "FTP istemcisi"
msgid "The file size is too large!"
msgstr "Dosya boyutu çok büyük!"
msgid "Connect"
msgstr "Bağlan"
msgid "Disconnect"
msgstr "Bağlantıyı kes"
msgid "Size"
msgstr "Boyut"
msgid "Date"
msgstr "Saat"
msgid "Attr."
msgstr "Özellik."
msgid "Toggle display position"
msgstr "Görüntü konumunu değiştir"
msgid "Alternatives"
msgstr "Alternatifler"
msgid "Add alternatives"
msgstr "Alternatif ekleyin"
msgid "DreamOS only!"
msgstr "Sadece DreamOS!"
msgid "A similar service is already in this list!"
msgstr "Bu listede zaten benzer bir hizmet var!"
msgid ""
"Play mode has been changed!\n"
"Restart the program to apply the settings."
msgstr ""
"Oynatma modu değiştirildi!\n"
"Ayarları uygulamak için programı yeniden başlatın."
msgid "Set values for TID, NID and Namespace for correct naming of the picons!"
msgstr "Piconların doğru isimlendirilmesi için TID, NID ve Namespace değerlerini ayarlayın!"
msgid "Streams detected:"
msgstr "Akışlar algılandı:"
msgid "Download picons"
msgstr "Picon'lar indirin"
msgid "Errors:"
msgstr "Hatalar:"
msgid "Use to play streams:"
msgstr "Akışları oynatmak için kullanın:"
msgid "Font in the lists:"
msgstr "Listelerdeki yazı tipi:"
msgid "Picons size in the lists:"
msgstr "Listelerdeki Piconların boyutu:"
msgid "Logo size in tooltips:"
msgstr "Araç ipuçlarındaki logo boyutu:"
msgid "Save as"
msgstr "Farklı kaydet"
msgid "Mark duplicates"
msgstr "Yinelenenleri işaretle"
msgid "Load only for selected bouquet"
msgstr "Yalnızca seçilen buket için yükle"
msgid "The task is canceled!"
msgstr "Görev iptal edildi!"

1246
po/zh_CN/demon-editor.po Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,19 +1,29 @@
#!/usr/bin/env python3
import os
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)
try:
from Cocoa import NSBundle
except ImportError as e:
print(e)
else:
ns_bundle = NSBundle.mainBundle()
if ns_bundle:
ns_bundle = ns_bundle.localizedInfoDictionary() or ns_bundle.infoDictionary()
if ns_bundle:
ns_bundle["CFBundleName"] = "DemonEditor"
if __name__ == "__main__":
from multiprocessing import set_start_method
from app.ui.main_app_window import start_app
set_start_method("fork") # For compatibility [Python > 3.7]
update_icon()
start_app()