Compare commits

..

897 Commits

Author SHA1 Message Date
DYefremov
7db02f2a9e added types to the service parser 2020-11-28 14:25:08 +03:00
DYefremov
fd40fd8d72 changes for popup menu actions 2020-11-27 21:27:23 +03:00
DYefremov
0b4313e4cf minor changes in control panel gui 2020-11-27 21:25:00 +03:00
DYefremov
a74628ed5c added sorting reset when loading data 2020-11-26 13:54:00 +03:00
DYefremov
443f6bf252 translations correction for app description 2020-11-26 13:53:49 +03:00
DYefremov
bb243ce281 added timeouts for telnet login 2020-11-26 13:53:20 +03:00
DYefremov
44049c380e config.yml update 2020-11-26 13:53:03 +03:00
DYefremov
281fe2a8f4 Russian, Belarusian and German translations update 2020-11-26 13:52:21 +03:00
DYefremov
39cc0ad8b3 some changes in the control panel 2020-11-26 13:51:46 +03:00
DYefremov
a625dc9f8b style correction 2020-11-26 13:51:29 +03:00
DYefremov
53f69b8f67 Russian, Belarusian and German translations update 2020-11-26 13:51:13 +03:00
DYefremov
94dfda0fa2 added basic support for timers via http api 2020-11-26 13:40:36 +03:00
DYefremov
cfe3f4c707 added styles 2020-11-26 13:39:36 +03:00
DYefremov
d18734910d small http api refactoring 2020-11-26 13:36:45 +03:00
DYefremov
d843633043 added epg display in control panel 2020-11-26 13:28:16 +03:00
DYefremov
b513d7a9b0 added prototype of simple control panel (#38) 2020-11-26 13:09:55 +03:00
DYefremov
92b2f840f8 fix update of sat positions after web import 2020-11-25 22:18:22 +03:00
DYefremov
9e4c8f388c added transponder view popup menu 2020-11-25 22:17:04 +03:00
DYefremov
664c52cfe1 minor compatibility fix 2020-11-25 22:15:27 +03:00
DYefremov
cc96cdd8fd added web import for services (#16) 2020-11-25 22:09:13 +03:00
DYefremov
15cca3f5f7 small decoupling for lamedb parsing 2020-11-25 21:27:22 +03:00
DYefremov
0ec2570043 added remote control requests 2020-11-25 21:26:51 +03:00
DYefremov
97cb26cd60 bump version 2020-11-25 21:26:36 +03:00
DYefremov
1da3eacc8c minor fix for the picon parser 2020-10-10 15:21:47 +03:00
DYefremov
cb6f185032 minor fix for yt 2020-10-06 11:27:09 +03:00
DYefremov
a8918bcf1f Russian, Belarusian and German translations update 2020-10-05 22:17:20 +03:00
DYefremov
c358197080 data loading refactoring 2020-10-05 22:16:54 +03:00
DYefremov
474fc3ec58 adaptation of dnd 2020-10-05 22:15:01 +03:00
DYefremov
c17bad215f changed getting of the drag icon 2020-09-30 20:58:30 +03:00
DYefremov
7891aca6e2 upd README 2020-09-28 13:48:04 +03:00
DYefremov
608de65897 Russian, Belarusian and German translations update 2020-09-28 13:46:08 +03:00
DYefremov
cbed3f7cca reworked and improved dnd for lists 2020-09-28 13:45:01 +03:00
DYefremov
08c1dca06d added support for loading and importing data via dnd 2020-09-28 13:30:12 +03:00
DYefremov
1edbd7d771 version update 2020-09-28 13:25:10 +03:00
DYefremov
0c3f6870dd added support for opening archives 2020-09-28 13:22:19 +03:00
DYefremov
f877872059 minor rework of the chooser dialog 2020-09-28 12:59:38 +03:00
DYefremov
335dfc005a displaying sid value in uppercase for tooltips(#34) 2020-09-12 22:52:18 +03:00
DYefremov
46450cf9b6 Display the sid value in tooltips in hex and dec format(#34). 2020-09-12 22:52:07 +03:00
Víctor Pont
9ed82ea129 Spanish translation update (#36) 2020-09-11 16:38:01 +03:00
DYefremov
555699c2a1 renaming bouquet fix [losing custom names](#33) 2020-09-08 12:45:32 +03:00
DYefremov
83b810286a upd README 2020-09-06 13:30:08 +03:00
DYefremov
61a56f1989 copy pl *.mo file 2020-09-02 23:04:08 +03:00
Wieslaw Weglowski
50ce4a688a Polish translation corrections (#31) 2020-09-02 22:53:47 +03:00
DYefremov
871b428b19 Russian, Belarusian and German translations update 2020-09-02 22:53:22 +03:00
DYefremov
3cd864cd84 version update 2020-08-31 22:27:45 +03:00
DYefremov
78c6a3c9fa minor fix for picon assignment 2020-08-31 22:27:20 +03:00
DYefremov
4c0904cf6c added Belarusian translation 2020-08-31 12:24:33 +03:00
DYefremov
7aa688df15 fix playback from the start screen 2020-08-31 12:20:37 +03:00
DYefremov
c91d58e0cf minor changes in sat dialogs 2020-08-27 23:21:19 +03:00
DYefremov
d071bb5d85 telnet password visibility fix 2020-08-24 22:44:30 +03:00
DYefremov
8cee77357c Polish translation update 2020-08-24 22:22:25 +03:00
DYefremov
20f53dee33 minor optimization 2020-08-24 22:22:09 +03:00
Wieslaw Weglowski
c454a33b3c Update demon-editor.po (#30) 2020-08-24 22:21:56 +03:00
DYefremov
5642b8871c upd README 2020-08-19 21:24:34 +03:00
DYefremov
e7480ec622 rework of the picons resizing 2020-08-19 21:20:07 +03:00
DYefremov
ecce001ce4 German and Russian translation update 2020-08-15 16:55:55 +03:00
DYefremov
7bae895458 minor yt fix 2020-08-15 16:55:40 +03:00
DYefremov
5b3bd48746 output fix for picons downloader 2020-08-09 00:14:58 +03:00
DYefremov
4769a814bd update *.spec 2020-08-09 00:05:46 +03:00
DYefremov
b08d4ed7d7 added dark mode support 2020-08-09 00:05:04 +03:00
DYefremov
233eb6bc53 added dark mode option 2020-08-08 14:47:57 +03:00
DYefremov
8b3d24c006 small changes of settings dialog appearance 2020-08-07 15:11:32 +03:00
DYefremov
48184c1fd9 minor fixes 2020-08-07 11:37:59 +03:00
DYefremov
46d91b93bc skipping enigma2 stop during picons upload 2020-08-06 21:19:47 +03:00
DYefremov
69989e784d minor correction of translations 2020-08-04 12:52:01 +03:00
DYefremov
ce1c978222 version update 2020-08-03 22:50:22 +03:00
DYefremov
5bcac35deb added option for experimental features 2020-08-03 22:23:44 +03:00
DYefremov
3ec5d264a0 Spanish, Portuguese and Dutch translation update 2020-07-27 21:29:07 +03:00
DYefremov
a2882b6589 upd README 2020-07-25 23:34:22 +03:00
DYefremov
31780bbf56 rm dist 2020-07-25 23:13:17 +03:00
DYefremov
286f1ffc3f version update 2020-07-25 13:43:46 +03:00
DYefremov
3bf97e5e0d minor change in dialogs appearance 2020-07-25 13:34:57 +03:00
DYefremov
c7c411c72b update ref fix 2020-07-24 11:00:20 +03:00
DYefremov
6b8a83511a slight appearance change of dialogs 2020-07-24 03:39:14 +03:00
DYefremov
f8e259293a German and Russian translation update 2020-07-22 17:33:36 +03:00
DYefremov
7adbf6b8a9 added keyboard[del] support 2020-07-22 17:32:57 +03:00
DYefremov
b68535e88a loading providers fix 2020-07-22 17:31:10 +03:00
DYefremov
b98ca359df fix display of cas 2020-07-22 17:30:36 +03:00
DYefremov
cbfd1486e1 added lock support for iptv 2020-07-22 17:30:03 +03:00
DYefremov
ad185f1efa German translation update 2020-07-22 17:29:33 +03:00
DYefremov
cea4ed1a66 Russian translation update 2020-07-22 17:28:38 +03:00
DYefremov
853d054a68 added audio codec option 2020-07-22 17:25:01 +03:00
DYefremov
8a1cead2f7 added notifications 2020-07-21 14:30:59 +03:00
DYefremov
37e0a8fdac dist update 2020-07-13 21:44:25 +03:00
DYefremov
29c66142ee minor fix 2020-07-13 21:00:26 +03:00
DYefremov
4fd2a2a600 bouquet import fix 2020-07-12 19:18:11 +03:00
DYefremov
6b360d48c4 added bouquets import via dnd 2020-07-12 16:44:40 +03:00
DYefremov
3a307b277c minor gui fix 2020-07-12 16:33:13 +03:00
DYefremov
9e685058a2 added debug mode option 2020-07-12 16:32:56 +03:00
DYefremov
3f07b09bb5 added sorting of bouquet services 2020-07-12 16:28:18 +03:00
DYefremov
1dca45f18f dist update 2020-07-04 14:02:52 +03:00
DYefremov
8b5ebc132d added download dialog options 2020-07-04 13:41:46 +03:00
DYefremov
b076db23bb auto save profile settings 2020-07-02 17:32:51 +03:00
DYefremov
41d479e18f dist update 2020-06-30 09:07:30 +03:00
DYefremov
cf540e5c9a picons explorer gui changes 2020-06-29 15:58:43 +03:00
DYefremov
3c7c8ebd83 fix lock/hide in filter mode 2020-06-22 19:46:36 +03:00
DYefremov
9b0c173eb8 skip of marker counting 2020-06-22 11:11:21 +03:00
DYefremov
208ce53c48 update *.spec 2020-06-21 10:27:15 +03:00
DYefremov
bb6679eddf fix getting path from uri 2020-06-21 00:52:50 +03:00
DYefremov
57f5e40439 changed filter entry icon 2020-06-15 21:39:13 +03:00
DYefremov
dad02e8e5c changed dnd for picons 2020-06-15 21:32:47 +03:00
DYefremov
844dab10a0 changed callback for screenshots 2020-06-15 16:57:39 +03:00
DYefremov
c1f5fd8006 small yt refactoring 2020-06-13 21:01:14 +03:00
DYefremov
86b974b632 get fav id fix 2020-06-13 19:14:31 +03:00
DYefremov
cf7e3a1b1b added space item to fav elements 2020-06-13 19:12:23 +03:00
DYefremov
bb07eb0a8a small dnd fix 2020-06-12 00:39:46 +03:00
DYefremov
f6de7d0fce version update 2020-06-11 11:48:45 +03:00
DYefremov
647b528899 added frame for info bar 2020-06-11 10:47:27 +03:00
DYefremov
7ed64c76ba added paned style 2020-06-11 10:27:23 +03:00
DYefremov
bcfdb09169 small http api init fix 2020-06-10 18:34:42 +03:00
DYefremov
c0c2ddef34 added basic youtube-dl support 2020-06-10 18:02:47 +03:00
DYefremov
b02eb37f1c corrections after merge 2020-06-10 18:01:23 +03:00
DYefremov
9b9f1d5492 added eserviceuri (8193) stream type 2020-06-10 11:56:01 +03:00
DYefremov
caba789e02 small rework of screenshot mode 2020-06-10 11:55:49 +03:00
DYefremov
5b1bffc078 impl local removing for picons 2020-06-10 11:55:41 +03:00
DYefremov
7e35a081a0 logging extension on data downloading 2020-06-10 11:55:32 +03:00
DYefremov
ccbc7a4315 added dnd for selective download/send 2020-06-10 11:55:21 +03:00
DYefremov
7f3f900725 added selective download/send of picons 2020-06-10 11:53:08 +03:00
DYefremov
921b936db0 added update picons dest view 2020-06-10 11:52:57 +03:00
DYefremov
bc6d372ade improved functionality of the picons explorer 2020-06-10 11:51:51 +03:00
DYefremov
3c28d12579 added youtube-dl options 2020-06-10 11:45:10 +03:00
DYefremov
e9544cc77f added app settings read exception 2020-06-10 11:42:26 +03:00
DYefremov
bd047e5f72 added services filtering from picons manager 2020-06-10 11:42:05 +03:00
DYefremov
adf7262ed6 adding picons to src via dnd 2020-06-10 11:41:23 +03:00
DYefremov
74a1ffea3a added basic screenshots support 2020-06-10 11:40:54 +03:00
DYefremov
6e78a539c3 modified cas values 2020-06-10 11:36:46 +03:00
DYefremov
c3ce3fc82e added space [hidden marker] support 2020-06-10 11:36:32 +03:00
DYefremov
8c61720423 version update 2020-06-10 11:32:54 +03:00
DYefremov
25e0e6939a copy tr *.mo file 2020-06-08 23:55:39 +03:00
audi06_19
e3232e48cf Turkish translation correction (#26) 2020-06-08 23:55:26 +03:00
DYefremov
aaa610852b dist update 2020-06-04 12:47:19 +03:00
DYefremov
04e9179025 added accelerator for input dialog 2020-06-04 11:46:21 +03:00
DYefremov
bce5636eaa added check for unsaved changes 2020-06-04 11:41:38 +03:00
DYefremov
0e10631931 rm unavailable iptv fix 2020-06-03 11:30:26 +03:00
DYefremov
77a3edead2 zap mode fix 2020-06-03 00:07:19 +03:00
DYefremov
8a8b249e14 small bq parsing changes (prevent #12) 2020-06-02 23:58:39 +03:00
DYefremov
4025f0933d copy pl *.mo file 2020-06-02 10:03:31 +03:00
DYefremov
bba4054bff Polish translation correction 2020-06-02 09:36:37 +03:00
Wieslaw Weglowski
e322d36023 Polish translation update(#23) 2020-06-02 09:07:31 +03:00
DYefremov
bb5afb0206 dist update 2020-05-23 01:36:35 +03:00
DYefremov
115f3960a7 drag on icon fix
(cherry picked from commit 0e50f1952d)
2020-05-23 01:22:09 +03:00
DYefremov
6d37da072e German translation update 2020-05-19 14:32:31 +03:00
DYefremov
99c3b1d194 Russian translation update 2020-05-19 14:32:17 +03:00
DYefremov
43afaf77b8 minor log changes 2020-05-19 12:01:11 +03:00
DYefremov
38aabb1b94 Dutch translation update 2020-05-18 16:31:24 +03:00
DYefremov
ef501f1557 small dnd fix 2020-05-18 12:40:21 +03:00
DYefremov
4679f9379c changed dnd for bouquets list 2020-05-17 15:28:44 +03:00
DYefremov
b2ea39f8a6 minor fixes 2020-05-17 14:55:18 +03:00
DYefremov
638be67425 dist update 2020-05-12 21:15:31 +03:00
DYefremov
9ca5a597d5 scaling picons on loading 2020-05-12 21:14:00 +03:00
DYefremov
d95ba7336f copy tr *.mo file 2020-05-12 17:26:12 +03:00
audi06
c78b18ddb7 Turkish translation update (#22) 2020-05-12 17:24:45 +03:00
DYefremov
92984c5fa6 start update 2020-05-12 15:19:47 +03:00
DYefremov
ca65f64a4f changed some dialogs elements 2020-05-12 14:13:01 +03:00
DYefremov
78dcccbd51 minor optimization and fix 2020-05-10 21:39:48 +03:00
DYefremov
f984d10c82 German translation update 2020-05-10 18:50:22 +03:00
DYefremov
c4ea451f52 small fix to prevent (#12) 2020-05-10 18:50:11 +03:00
DYefremov
36ec6d5079 added picons filter by service name 2020-05-10 18:49:59 +03:00
DYefremov
91706c722f Russian translation update 2020-05-10 15:13:06 +03:00
DYefremov
4ef8c4d186 fix to prevent #12 2020-05-08 17:15:34 +03:00
DYefremov
f9e92b28d0 dist update 2020-05-07 15:10:48 +03:00
DYefremov
832bab91a4 reworked settings dialog 2020-05-07 14:19:46 +03:00
DYefremov
951c99338f added skip upload if file not found 2020-05-05 00:04:05 +03:00
DYefremov
ee91eb9413 fix use colors 2020-05-04 22:33:05 +03:00
DYefremov
912c38825b redesigned appearance for most dialogs 2020-05-04 19:36:52 +03:00
DYefremov
de4d012784 fix to prevent (#21) 2020-05-04 19:20:51 +03:00
DYefremov
351ce81e94 minor fixes for yt 2020-05-03 02:05:36 +03:00
DYefremov
3a0f096a6c changed data dir creation 2020-05-03 01:42:28 +03:00
DYefremov
29088ec19e dist update 2020-04-30 14:12:58 +03:00
DYefremov
4c144951f0 reworking of download dialog 2020-04-30 14:09:01 +03:00
DYefremov
dae6ad765a added accelerators and tooltips 2020-04-30 13:55:40 +03:00
DYefremov
b934407d7e added download/upload of [terrestrial, cable].xml 2020-04-30 13:53:15 +03:00
DYefremov
3fb5b82cc6 slight optimization of loading/deleting data 2020-04-30 13:47:24 +03:00
DYefremov
ba3ad9a9ef setting text for wait dialog 2020-04-30 13:45:09 +03:00
DYefremov
7a4620a374 path resolve fix 2020-04-30 13:42:02 +03:00
DYefremov
174634ecbc reworking of picons dialog 2020-04-30 13:36:01 +03:00
DYefremov
73ae57d07b extracting themes with tar 2020-04-23 18:43:22 +03:00
DYefremov
055a700586 small refactoring of base icons init 2020-04-23 15:43:48 +03:00
DYefremov
04203240a7 minor fixes for filter and search 2020-04-23 10:33:56 +03:00
DYefremov
a433e01b65 added group style 2020-04-22 10:02:47 +03:00
DYefremov
8f591a8b9a small refactoring of chooser dialog 2020-04-21 14:45:34 +03:00
DYefremov
dcc217b0de upd README 2020-04-20 20:50:10 +03:00
DYefremov
d06334b0af added picons assignment by drag on icon 2020-04-20 13:55:38 +03:00
DYefremov
6957a960ca added hints support for the main list 2020-04-20 13:51:05 +03:00
DYefremov
9fe328b54e minor refactoring 2020-04-20 13:46:42 +03:00
DYefremov
b3dc9b72c9 version update 2020-04-16 21:42:07 +03:00
DYefremov
b6a4d46227 upd README 2020-04-16 21:41:05 +03:00
audi06
53776bdf62 added Turkish selection (#20) 2020-04-16 21:10:28 +03:00
DYefremov
ba9ba4129f copy tr *.mo file 2020-04-16 21:10:13 +03:00
audi06
a2411ba86e added Turkish translation (#19)
(cherry picked from commit 8d96f02e2e)
2020-04-16 21:10:01 +03:00
DYefremov
a6d8573999 added bouquet file naming option 2020-04-16 17:29:04 +03:00
DYefremov
7510d42fb9 minor fixes 2020-04-13 20:08:22 +03:00
DYefremov
036e666c9b styles decoupling 2020-04-13 20:07:57 +03:00
DYefremov
c9c962e129 added appearance settings 2020-04-13 13:54:54 +03:00
DYefremov
ea71af9462 dist update 2020-04-10 23:22:57 +03:00
DYefremov
0a5b51de6e style changes for some ui elements 2020-04-10 23:09:17 +03:00
DYefremov
8cb413ec92 added basic hints support 2020-04-08 18:40:27 +03:00
DYefremov
5dfb702484 added option for hints 2020-04-08 18:37:10 +03:00
DYefremov
0cab4e1238 epg options fix 2020-04-02 16:53:03 +03:00
DYefremov
85f5c37f28 changed some player args 2020-03-28 20:20:49 +03:00
DYefremov
3df6d7bba0 translations update for dist 2020-03-28 19:56:24 +03:00
DYefremov
e45c56f4cc changed toolbar elements position 2020-03-28 19:31:13 +03:00
DYefremov
7d03631924 basic implementation of the play mode 2020-03-28 18:45:05 +03:00
DYefremov
7b9ec6a4b1 small cleaning 2020-03-28 18:39:13 +03:00
DYefremov
d640210ab0 copy *.mo file 2020-03-24 15:38:35 +03:00
wwns
f7e8283355 Polish translation update (#18) 2020-03-24 15:38:10 +03:00
DYefremov
f93c81de19 wrap m3u data 2020-03-24 15:38:06 +03:00
DYefremov
e1804755d2 added play streams mode options 2020-03-24 15:38:01 +03:00
DYefremov
1cf56639c1 copy .mo file 2020-03-21 16:47:50 +03:00
Víctor Pont
943b4c540f Spanish translation update and corrections (#17) 2020-03-21 16:47:43 +03:00
DYefremov
4602c51c01 added simple telnet client 2020-03-17 14:10:49 +03:00
DYefremov
a84cc7727f minor fix 2020-03-14 09:09:11 +03:00
DYefremov
250e03af5d changed record button update 2020-03-13 14:37:21 +03:00
DYefremov
2c5f8eb0ed toolbar elements changes 2020-03-11 16:47:10 +03:00
DYefremov
6f4ff4c97d added transcoding options 2020-03-11 16:05:13 +03:00
DYefremov
ee29659739 added record of current service 2020-03-11 16:04:41 +03:00
DYefremov
8a1496a84c added new paths settings 2020-03-11 15:57:41 +03:00
DYefremov
23c3035162 fix service status info 2020-03-11 15:56:57 +03:00
DYefremov
a506356547 version update 2020-03-11 15:55:59 +03:00
DYefremov
0c284fb0d9 added picons multiple assignment 2020-03-11 15:50:23 +03:00
DYefremov
b437385325 German translation update
(cherry picked from commit 3d627b57a4)
2020-02-28 00:53:33 +03:00
DYefremov
c60bba5535 upd README 2020-02-24 12:54:29 +03:00
DYefremov
1c2d0ab9ea small fix
(cherry picked from commit 7444db7e21)
2020-02-24 12:51:56 +03:00
DYefremov
f35f7fbc8a update dist 2020-02-21 00:14:10 +03:00
DYefremov
42aaad291f Russian translation update
(cherry picked from commit 7554f40c6a)
2020-02-20 14:16:09 +03:00
DYefremov
9c8c617393 gui changes for send to 2020-02-20 12:11:41 +03:00
DYefremov
98fc963fa1 fix getting sats 2020-02-19 12:03:27 +03:00
DYefremov
fbb5cd0352 added appindicator support 2020-02-19 10:15:01 +03:00
DYefremov
5abe3de3b6 toolbar changes 2020-02-18 00:35:17 +03:00
DYefremov
0b3f26ab84 .mo file update 2020-02-17 09:21:40 +03:00
wwns
2666146b5e Polish translation update (#11)
Polish translation update.
2020-02-17 08:50:32 +03:00
DYefremov
be90b518c9 update of .mo file 2020-02-14 22:55:36 +03:00
wwns
adeae58488 Polish translation update (#10) 2020-02-14 22:20:07 +03:00
DYefremov
b204f042ee update dist 2020-02-12 22:29:50 +03:00
DYefremov
79d0e9d256 added icons path to .spec file 2020-02-12 21:45:14 +03:00
DYefremov
4dcfde8b53 revert of get yt icon 2020-02-12 21:16:47 +03:00
DYefremov
42f687020b moved get yt icon 2020-02-12 20:19:09 +03:00
DYefremov
14bf79dbf9 toolbar changes 2020-02-12 17:35:44 +03:00
DYefremov
f660beef16 update of data path 2020-02-12 13:53:18 +03:00
DYefremov
99d17b36c3 added Polish selection 2020-02-12 12:40:27 +03:00
wwns
3113fadcca added a Polish translation (#5)
* added a Polish translation

* added a Polish translation

* name change
2020-02-11 21:43:53 +03:00
DYefremov
67a394359d fix bq deletion 2020-02-10 19:28:59 +03:00
DYefremov
1acb7fdd81 changing profile on data download 2020-02-10 17:01:48 +03:00
DYefremov
dced81581c fix profile edit 2020-02-10 14:52:30 +03:00
DYefremov
6a52988f1a update dist 2020-02-01 18:55:31 +03:00
DYefremov
4a1d714604 added basic support for keyboard shortcuts 2020-02-01 17:45:36 +03:00
DYefremov
2e12e1ec87 auto rename bouquets with duplicate names 2020-02-01 09:34:45 +03:00
DYefremov
4c6336e75f added checking for bouquet names duplicate 2020-01-29 14:58:32 +03:00
DYefremov
b7d0ba7f4b added controls to the transmitter 2020-01-28 16:37:53 +03:00
DYefremov
405e07bbc4 added player requests 2020-01-28 16:36:08 +03:00
DYefremov
6ded67147b update dist 2020-01-24 20:47:24 +03:00
DYefremov
4a0e2acd9c minor gui changes 2020-01-24 01:06:08 +03:00
DYefremov
5876f70884 upd. README 2020-01-23 19:18:49 +03:00
DYefremov
3cb4f1095d fix picons downloading from bundle 2020-01-23 19:17:54 +03:00
DYefremov
af46c2fb1d minor fixes for player elems 2020-01-23 16:13:09 +03:00
DYefremov
cbcdf19be6 added remove and download to picons 2020-01-23 00:48:57 +03:00
DYefremov
f326a9c723 some corrections 2020-01-19 20:07:59 +03:00
DYefremov
53888a45dc Merge branch 'development' into experimental-mac
# Conflicts:
#	DemonEditor.desktop
#	README.md
#	app/connections.py
#	app/ui/main_app_window.py
#	app/ui/main_window.glade
#	app/ui/uicommons.py
#	build-deb.sh
#	deb/DEBIAN/control
#	deb/DEBIAN/copyright
#	deb/usr/bin/demon-editor
#	deb/usr/share/applications/DemonEditor.desktop
#	start.py
2020-01-18 23:08:45 +03:00
DYefremov
ea305dadf1 upd. README 2020-01-18 21:49:21 +03:00
DYefremov
136fd118cb minor fixes 2020-01-18 15:28:46 +03:00
DYefremov
7df7e0b630 changed status update 2020-01-17 00:34:18 +03:00
DYefremov
0b4e923037 added callbacks to the player 2020-01-16 14:08:34 +03:00
DYefremov
c2d2361de9 scripts update 2020-01-15 07:26:59 +03:00
DYefremov
cc15594338 added app icon 2020-01-15 07:24:16 +03:00
DYefremov
0f64234312 added stream mode 2020-01-14 18:26:05 +03:00
DYefremov
a1098750da fix data loading if pixbuf error 2020-01-12 00:33:33 +03:00
DYefremov
47c80b2b29 minor fix of status update 2020-01-11 20:40:16 +03:00
DYefremov
dfbf019c64 added token security support 2020-01-11 17:58:50 +03:00
DYefremov
9082d5c96e added last configuration load feature 2020-01-09 13:14:49 +03:00
DYefremov
2e3ec1c99d http api refactoring 2020-01-08 21:33:24 +03:00
DYefremov
024d48b464 set settings type to the profile 2020-01-08 14:50:04 +03:00
DYefremov
0571a29b66 added elems to profiles edit 2020-01-07 12:36:29 +03:00
DYefremov
1d62e2660b added http api compatibility test 2020-01-06 13:17:56 +03:00
DYefremov
bcbe2b3f46 added https support to the settings 2020-01-03 23:26:55 +03:00
DYefremov
d0638f7158 added language selection 2020-01-02 15:47:48 +03:00
DYefremov
c2d3cb7673 some changes for translation 2019-12-28 23:21:19 +03:00
DYefremov
614c87cbf3 base implementation of profiles support 2019-12-27 23:05:37 +03:00
DYefremov
5aec42548e settings refactoring 2019-12-22 20:42:29 +03:00
DYefremov
9d4b507559 Merge remote-tracking branch 'origin/experimental-mac' into experimental-mac 2019-12-21 13:57:37 -08:00
DYefremov
2993fcd7f7 update dist 2019-12-18 03:50:17 -08:00
DYefremov
5bea9887db minor clean 2019-12-18 08:56:41 +03:00
DYefremov
dd0edfc811 minor clean 2019-12-17 12:56:07 +03:00
DYefremov
5bf6500809 added exception 2019-12-17 12:03:15 +03:00
DYefremov
3859c84c0e added exception 2019-12-17 11:59:57 +03:00
DYefremov
d05da3f44c minor player fix 2019-12-16 15:49:24 +03:00
DYefremov
77bb4a7fef minor player fix 2019-12-16 15:45:41 +03:00
DYefremov
b251ce8b69 fixes after merge 2019-12-16 10:47:18 +03:00
DYefremov
df36860239 Merge branch 'development' into experimental-mac
# Conflicts:
#	app/properties.py
#	app/tools/media.py
#	app/ui/dialogs.py
#	app/ui/main_app_window.py
#	app/ui/transmitter.py
#	deb/usr/share/locale/es/LC_MESSAGES/demon-editor.mo
2019-12-16 10:36:19 +03:00
DYefremov
a8b4047239 player refactoring 2019-12-16 09:57:27 +03:00
DYefremov
a4800ace14 small clean after refactoring of the settings 2019-12-15 19:01:27 +03:00
Víctor Pont
f26f806147 Spanish translation corrections (#3)
* Spanish translation corrections
2019-12-14 14:30:24 +03:00
DYefremov
6f27040164 upd README 2019-12-14 13:22:22 +03:00
DYefremov
98d3c04a08 settings refactoring 2019-12-13 13:31:07 +03:00
DYefremov
4d488dd224 update dist 2019-12-08 02:38:28 -08:00
DYefremov
c728a59e92 update dist 2019-12-08 02:32:40 -08:00
DYefremov
98341064d3 fix set nso 2019-12-08 01:09:44 -08:00
DYefremov
fc00b25fd2 revert media player view 2019-12-08 01:20:06 +03:00
DYefremov
638f33ac5a update vlc 2019-12-07 21:07:51 +03:00
DYefremov
24311827bf update vlc 2019-12-07 21:06:18 +03:00
DYefremov
88167912b3 fix status image show 2019-12-06 15:20:14 +03:00
DYefremov
450d7f4c72 added dist 2019-12-06 08:50:26 +03:00
DYefremov
24729c064c base impl of send to 2019-12-05 10:52:04 -08:00
DYefremov
df3c2a3938 added build support 2019-12-05 10:47:49 -08:00
DYefremov
17a3ec4fef base impl of send to 2019-12-04 23:06:38 +03:00
DYefremov
44bf8b96ff fix set icon after marge 2019-11-26 18:46:57 +03:00
DYefremov
6b86db4aa4 Merge branch 'development' into experimental-mac
# Conflicts:
#	app/connections.py
#	app/ui/main_app_window.py
#	app/ui/main_window.glade
#	deb/DEBIAN/control
2019-11-26 18:41:07 +03:00
DYefremov
30927b2546 prototype of send to for yt links 2019-11-24 21:58:32 +03:00
DYefremov
84a4cef5b5 minor changes for the settings dialog 2019-11-22 09:34:17 +03:00
DYefremov
5fe2559789 added connection status icon 2019-11-21 23:13:06 +03:00
DYefremov
1e35a69539 http api refactoring 2019-11-21 16:59:43 +03:00
DYefremov
16c58907f4 prototype of send to 2019-11-05 23:04:21 +03:00
DYefremov
783e78dc14 added send to and yt_dl settings support 2019-11-04 20:18:24 +03:00
DYefremov
c7e1f05955 added play to the request types 2019-11-03 18:11:49 +03:00
DYefremov
bf9ad139e5 added style 2019-11-01 00:24:13 +03:00
DYefremov
e11f68e3bd added style 2019-10-29 13:39:11 +03:00
DYefremov
20fc199d02 moved toolbar to the top 2019-10-28 11:59:58 +03:00
DYefremov
e0a22f72fc changed setting service info 2019-10-28 11:44:30 +03:00
DYefremov
8cadc47da5 added current service status info 2019-10-28 11:44:12 +03:00
DYefremov
27f60bcea2 changed setting service info 2019-10-28 11:03:09 +03:00
DYefremov
0a16265aa2 added current service status info 2019-10-28 00:45:47 +03:00
DYefremov
499ca31992 added http-api prototype for vlc 2019-10-27 00:39:32 +03:00
DYefremov
e198f0b1e6 removed separators from toolbar 2019-10-26 15:19:39 +03:00
DYefremov
a43ac0de02 added bottom toolbar 2019-10-26 15:01:23 +03:00
DYefremov
b14e6fac16 added edit and view to the app bar menu 2019-10-25 19:17:31 +03:00
DYefremov
2dcc9a85b5 updated app bar menu 2019-10-23 12:38:23 +03:00
DYefremov
7ce9ba0db2 added prototype of playing current service 2019-10-23 12:24:57 +03:00
DYefremov
fc56b047a1 added prototype of playing current service 2019-10-23 12:20:20 +03:00
DYefremov
78dc62d46e added german translation 2019-10-23 12:19:31 +03:00
DYefremov
49d8c0ef92 added prototype of playing current service 2019-10-23 11:51:28 +03:00
DYefremov
998a6eb118 added german translation 2019-10-21 10:47:27 +03:00
DYefremov
20e7ee3478 removing deb 2019-10-20 23:58:10 +03:00
DYefremov
ffa144367f slight refactoring of stream play 2019-10-20 23:48:35 +03:00
DYefremov
237d09a711 updated version 2019-10-20 23:48:28 +03:00
DYefremov
f7d75c2404 slight refactoring of stream play 2019-10-20 23:46:25 +03:00
DYefremov
9396ec197d updated version 2019-10-20 23:34:56 +03:00
DYefremov
9ad9de4821 minor gui changes 2019-10-20 23:33:09 +03:00
DYefremov
272cbdeb2f changed yt links icon to default 2019-10-19 09:31:51 +03:00
DYefremov
f9191a7465 minor gui changes 2019-10-18 21:07:48 +03:00
DYefremov
f22abe1d87 test nso 2019-10-17 08:11:04 -07:00
DYefremov
b94c08284a new gui test prototype for the player 2019-10-17 15:02:31 +03:00
DYefremov
e303f25f99 updated app bar menu 2019-10-14 22:09:14 +03:00
DYefremov
408a4cfa32 fix of services counter reset 2019-10-14 00:17:06 +03:00
DYefremov
b80dcb7d74 fix save 2019-10-13 18:53:33 +03:00
DYefremov
63c5df0ef6 Improved functionality of the app menu bar 2019-10-13 14:07:46 +03:00
DYefremov
c69888a72d Merge remote-tracking branch 'origin/experimental-mac' into experimental-mac 2019-10-12 23:32:16 +03:00
DYefremov
07606077e5 added app menu bar prototype 2019-10-12 23:32:01 +03:00
DYefremov
e1f63bfed7 updating of dutch, spanish and portuguese 2019-10-12 23:23:48 +03:00
DYefremov
c0865beb3c Merge branch 'development' into experimental-mac 2019-10-11 15:54:53 +03:00
DYefremov
fe3fb1fefe updating of dutch, spanish and portuguese 2019-10-11 15:51:20 +03:00
DYefremov
05eff28b75 upd. README 2019-10-11 14:34:08 +03:00
DYefremov
6d2150b731 changes to start on macos 2019-10-10 14:19:10 -07:00
DYefremov
95b5c7aa74 fix hide info box on new config creation 2019-10-10 12:55:58 +03:00
DYefremov
d2e80c6d44 translation update 2019-10-06 09:50:16 +03:00
DYefremov
57f157ef9b added app info box 2019-10-04 21:31:41 +03:00
DYefremov
f62f104c8d added rewind support to the player 2019-10-02 14:44:58 +03:00
DYefremov
8d6b1303dc slight refactoring of http api 2019-09-28 21:57:41 +03:00
DYefremov
de770169fa changed requests to compressed 2019-09-28 17:44:33 +03:00
DYefremov
7b2e467111 updated yt playlist parser 2019-09-22 16:54:20 +03:00
DYefremov
1c1dff2497 fix sensitivity for iptv elems in neutrino 2019-09-16 17:44:33 +03:00
DYefremov
309f960e1e minor clean 2019-09-10 00:37:48 +03:00
DYefremov
dadb73280c changed callbacks for loading picons 2019-09-10 00:28:38 +03:00
DYefremov
81260211a4 default callback set 2019-09-10 00:24:51 +03:00
DYefremov
d4d1dd397d slight refactoring of picons downloader 2019-09-09 23:48:36 +03:00
DYefremov
fe3e1ef30a fix apply default data for some iptv services 2019-09-08 18:45:12 +03:00
DYefremov
1f46eae6be changed counting of marker number 2019-09-04 10:39:46 +03:00
DYefremov
a398b0c6e9 revert default formats for youtube 2019-08-18 17:02:32 +03:00
DYefremov
272b6af3ad quality selection for a single yt link 2019-08-18 00:06:44 +03:00
DYefremov
d976e02cf6 added quality selection for yt playlist 2019-08-17 22:53:05 +03:00
DYefremov
fb6951896f changed getting yt link 2019-08-13 19:22:08 +03:00
DYefremov
9c62a49968 added min width 2019-08-11 21:24:51 +03:00
DYefremov
99bb508912 minor refactoring of data appending 2019-08-08 21:15:43 +03:00
DYefremov
e382a51f81 optimisation of data load/save 2019-08-01 01:05:30 +03:00
DYefremov
fdd30b2ac9 hiding header items during playback 2019-07-23 10:47:01 +03:00
DYefremov
f2b99f9eea fixed the order of links in the yt dialog 2019-07-19 16:02:00 +03:00
DYefremov
dea5723bb7 reworking of yt dialog 2019-06-30 22:13:26 +03:00
DYefremov
236a7a15d0 added keyboard shortcuts for the yt dialog 2019-06-28 23:25:38 +03:00
DYefremov
cfe281116a added logging for get yt link 2019-06-28 08:58:33 +03:00
DYefremov
984e8ca088 added popup menu for yt dialog 2019-06-28 00:02:34 +03:00
DYefremov
62eae7f029 minor changes in the yt dialog 2019-06-27 22:10:44 +03:00
DYefremov
204962b531 append yt list data 2019-06-26 15:57:22 +03:00
DYefremov
767c06a0ff merged yt dialog 2019-06-26 15:56:23 +03:00
DYefremov
d4ec28e9cd added getting max num of markers 2019-06-26 15:38:34 +03:00
DYefremov
8fee65cabb yt list import dialog skeleton 2019-06-24 00:36:54 +03:00
DYefremov
95069bbf24 refactoring of getting fav id 2019-06-23 23:25:03 +03:00
DYefremov
60f106bc2a added simple parser to handle yt playlists 2019-06-21 14:54:09 +03:00
DYefremov
4becdf1d6e added tooltip text for yt icon 2019-06-19 23:23:32 +03:00
DYefremov
f2f027c6f4 moved youtube logic to the extra module 2019-06-19 22:34:22 +03:00
DYefremov
edeab12b50 skeleton of basic support for youtube links 2019-06-18 18:46:27 +03:00
DYefremov
896aa7f66e added info bar for iptv dialog 2019-06-18 18:26:42 +03:00
DYefremov
6b6220a3ac youtube links detection skeleton 2019-06-15 23:50:42 +03:00
DYefremov
3fa46c6c6c version update 2019-06-15 22:06:41 +03:00
DYefremov
a0b322b188 set text for the question dialog 2019-06-15 21:43:24 +03:00
DYefremov
3550f58603 update spanish, dutch and portuguese 2019-06-11 21:24:06 +03:00
DYefremov
729c85be77 fix type for radio bouquets 2019-06-11 21:03:51 +03:00
DYefremov
f8aee1b807 update russian 2019-06-08 16:04:47 +03:00
DYefremov
75d93f6a19 improved xml download for the epg dialog 2019-06-08 15:45:41 +03:00
DYefremov
4581cc7d4f upd README 2019-06-05 11:53:52 +03:00
DYefremov
2f8ea069e1 bouquet name for xml file when saving from the epg dialog 2019-06-04 13:31:54 +03:00
DYefremov
8afcec6b7e changed date format in the header 2019-06-04 13:06:02 +03:00
DYefremov
b5a9321c5c small refactoring of getting refs from xml 2019-06-04 01:22:26 +03:00
DYefremov
7ee781c39b lazy init of epg data 2019-06-03 15:47:04 +03:00
DYefremov
48f3c1a4d6 added export to m3u for neutrino 2019-05-30 15:56:04 +03:00
DYefremov
717bac6446 fix on open 2019-05-30 12:57:31 +03:00
DYefremov
fe1323f8cf deleted extra dialog 2019-05-30 11:12:22 +03:00
DYefremov
0f30d74edc changing header bar elements 2019-05-29 14:31:44 +03:00
DYefremov
0686c91a5d fix provider name for neutrino 2019-05-29 12:57:03 +03:00
DYefremov
a84090cda7 added support of coupled satellites 2019-05-27 22:17:29 +03:00
DYefremov
291b3aa289 minor appearance changes 2019-05-27 11:02:41 +03:00
DYefremov
dd92ffc9b1 fix getting some transponders 2019-05-27 00:17:38 +03:00
DYefremov
97a8f793c3 lazy loading of satellites list 2019-05-26 22:11:52 +03:00
DYefremov
3ad2e3d6b6 disable cache 2019-05-26 21:50:02 +03:00
DYefremov
7d6763ffb5 fix show iptv services in the info box of import dialog 2019-05-20 20:01:28 +03:00
DYefremov
6582be7a0d added arg for the start script 2019-05-19 14:22:43 +03:00
DYefremov
1e45621bd8 added data recovery if download error 2019-05-19 00:37:07 +03:00
DYefremov
61bcb85bbc global update settings from the download dialog 2019-05-14 22:12:36 +03:00
DYefremov
9eee9ac424 small refactoring of the init of dynamic elems 2019-05-13 14:42:23 +03:00
DYefremov
3f720afedc added command line params support 2019-05-12 16:26:58 +03:00
DYefremov
cd19c5fd9c optional logging 2019-05-12 16:26:19 +03:00
DYefremov
61ca2f3e8b minor changes for the input dialog 2019-05-11 13:27:46 +03:00
DYefremov
75fc7adc88 input dialog refactoring 2019-05-11 00:09:20 +03:00
DYefremov
3678a9d29d satellite dialogs refactoring 2019-05-10 14:42:32 +03:00
DYefremov
a5927dd2b6 service details refactoring 2019-05-10 14:41:33 +03:00
DYefremov
34e0ed4748 setting selected bouquet after rename 2019-05-09 23:51:47 +03:00
DYefremov
d7a214b445 iptv dialogs refactoring 2019-05-09 14:48:29 +03:00
DYefremov
e194827af7 get about dialog 2019-05-09 12:53:11 +03:00
DYefremov
e9e53da5cc dialogs refactoring 2019-05-09 11:11:54 +03:00
DYefremov
822497317d minor changes 2019-05-09 00:01:49 +03:00
DYefremov
c2047bd7b5 question dialog refactoring 2019-05-08 23:35:42 +03:00
DYefremov
3636da60d6 input dialog refactoring 2019-05-08 23:05:32 +03:00
DYefremov
2eebd55b77 updating counter on reset 2019-05-07 22:08:04 +03:00
DYefremov
12f76f8e28 added reset for the epg dialog 2019-05-07 17:22:18 +03:00
DYefremov
28e6cca919 update spanish, dutch and portuguese 2019-05-07 13:25:17 +03:00
DYefremov
9b53538da6 new impl of data mapping for the epg dialog 2019-05-07 00:04:53 +03:00
DYefremov
994541bad5 update russian 2019-05-05 11:49:24 +03:00
DYefremov
3cbb16febe minor gui changes 2019-05-05 11:26:11 +03:00
DYefremov
2b61fa07b9 update russian 2019-05-05 11:08:16 +03:00
DYefremov
406f4bd0f0 little mapping improvements for services with cyrillic names 2019-05-04 23:54:58 +03:00
DYefremov
1ec6b817e9 support of epg.dat download from the receiver 2019-05-04 20:13:57 +03:00
DYefremov
7c55692c99 small decoupling of dialogs 2019-05-04 11:21:20 +03:00
DYefremov
3aa29a788d added groups support by export to m3u 2019-05-01 17:21:51 +03:00
DYefremov
55b0dccc80 added info dialog 2019-05-01 17:19:31 +03:00
DYefremov
edb97cbf8c added keyboard shortcuts for the epg dialog 2019-05-01 13:11:19 +03:00
DYefremov
7620f03e2b added info bars for the epg dialog 2019-04-30 14:17:45 +03:00
DYefremov
cced856297 added base support of xml sources for epg dialog 2019-04-27 19:05:37 +03:00
DYefremov
3bcfd66971 added elements in the epg options widget 2019-04-26 22:07:21 +03:00
DYefremov
e7e7c667e9 added options widget for the epg dialog 2019-04-25 00:18:49 +03:00
DYefremov
6de0bc4201 added popup menus for epg dialog 2019-04-24 21:53:01 +03:00
DYefremov
878520b7f9 epg config dialog skeleton 2019-04-24 20:27:47 +03:00
DYefremov
63ac413982 saving list to xml 2019-04-22 20:25:19 +03:00
DYefremov
171c58c546 epg assignment by drag 2019-04-22 00:12:04 +03:00
DYefremov
6758ae3d16 assign epg data 2019-04-21 21:48:47 +03:00
DYefremov
329513d2a7 epg config dialog skeleton 2019-04-21 01:18:54 +03:00
DYefremov
be195e9001 added epg icon 2019-04-20 20:44:56 +03:00
DYefremov
635a3fb966 added epg skeleton 2019-04-18 23:05:19 +03:00
DYefremov
281f7a28f3 added export to m3u 2019-04-18 21:43:35 +03:00
DYefremov
507f5817c2 update version 2019-04-18 19:12:52 +03:00
DYefremov
d3822474ba small internal refactoring of iptv list config dialog 2019-04-14 20:24:57 +03:00
DYefremov
e1ce9f3006 added non-rec stream types for iptv 2019-04-14 00:03:52 +03:00
DYefremov
c2b0768857 minor cleaning 2019-04-13 15:23:24 +03:00
DYefremov
283d85ef8e fix reading of bouquet names for some configs 2019-04-12 23:29:04 +03:00
DYefremov
f5656d8d5f update spanish, dutch and portuguese 2019-04-08 11:28:49 +03:00
DYefremov
5dd5a09bfc show error message if no item is selected by import 2019-04-05 13:46:37 +03:00
DYefremov
1b5f3372b4 update russian 2019-04-04 21:01:58 +03:00
DYefremov
974e964f42 minor gui changes 2019-04-04 20:38:30 +03:00
DYefremov
8cb6ed02d2 update russian 2019-04-01 10:11:57 +03:00
DYefremov
8bb3b780d1 minor tooltips changes 2019-04-01 10:09:55 +03:00
DYefremov
3000c8830c minor gui changes 2019-03-31 21:51:53 +03:00
DYefremov
ac550e016d update russian 2019-03-31 21:40:55 +03:00
DYefremov
7420751806 Merge remote-tracking branch 'origin/development' into development 2019-03-31 21:10:41 +03:00
DYefremov
f35889e8e4 minor changes in the gui of the settings dialog 2019-03-31 21:10:27 +03:00
DYefremov
857b252f4c upd README 2019-03-28 10:34:13 +03:00
DYefremov
572584a14f fix transponders duplication 2019-03-23 11:16:43 +03:00
DYefremov
7e4ac3e69c fix pls mode 2019-03-22 00:54:44 +03:00
DYefremov
0d73ffa79d show error dialog refactoring 2019-03-19 21:44:05 +03:00
DYefremov
5e2f1ddb84 added extra method for error dialog showing 2019-03-19 00:12:33 +03:00
DYefremov
6c4040901f upd README 2019-03-18 23:37:37 +03:00
DYefremov
103e09b900 added download/upload data using Ctrl + D/U/B shortcuts 2019-03-18 23:04:05 +03:00
DYefremov
8ddc517ab7 added skip message for http test 2019-03-18 23:03:42 +03:00
DYefremov
b26d982db4 added skip message for http test 2019-03-18 22:54:59 +03:00
DYefremov
3733bc395b fix insert stream 2019-03-14 13:43:13 +03:00
DYefremov
26bfbafc0e little cleaning 2019-03-14 12:40:32 +03:00
DYefremov
84d1a18111 added single import for neutrino 2019-03-14 12:37:48 +03:00
DYefremov
1cdacd5276 added support of multistream transponders by update from web 2019-03-12 13:39:30 +03:00
DYefremov
354715558c fix set pls mode for transponder dialog 2019-03-12 10:39:55 +03:00
DYefremov
bca1613bff Added Ctrl + O/Q shortcuts 2019-03-10 18:11:38 +03:00
DYefremov
36b533b890 added double click mode option for bouquet list 2019-03-10 15:33:28 +03:00
DYefremov
2eabccc1a9 fix single import for empty bouquets list 2019-03-06 08:40:38 +03:00
DYefremov
75cd78277e added remove all unused picons 2019-03-03 12:50:40 +03:00
DYefremov
5181b732ed added confirmation dialog before import 2019-02-27 20:12:41 +03:00
DYefremov
513c0e8d3d update dutch, spanish and portuguese 2019-02-26 20:52:01 +03:00
DYefremov
f932feb305 update russian 2019-02-25 23:37:05 +03:00
DYefremov
6a2fda5ec0 changes for single bouquet import 2019-02-25 23:35:50 +03:00
DYefremov
86c30dd2c1 changes for single bouquet import 2019-02-25 23:35:20 +03:00
DYefremov
0ed41c473d minor gui changes 2019-02-24 15:47:19 +03:00
DYefremov
5078a854d2 import single bouquet skeleton 2019-02-23 13:54:00 +03:00
DYefremov
353bf04924 added import elements 2019-02-23 13:53:16 +03:00
DYefremov
474ff8e303 changed data opening from import dialog 2019-02-15 13:04:52 +03:00
DYefremov
5834bd4a0b added selection with space key 2019-02-09 15:25:44 +03:00
DYefremov
81f31e5d8d added space key 2019-02-09 15:16:03 +03:00
DYefremov
267f645c16 little refactoring of working with models 2019-02-09 12:46:06 +03:00
DYefremov
8c10d7d6a5 added columns for bouquets model 2019-02-09 12:43:27 +03:00
DYefremov
bbdb47ee7a enabled import for neutrino 2019-02-09 10:25:49 +03:00
DYefremov
7f6856e6aa import for empty config 2019-02-09 09:55:35 +03:00
DYefremov
3439a3ad0a base implementation of import 2019-02-08 19:11:30 +03:00
DYefremov
ee2e2ac49d added bouquet details for import dialog 2019-02-06 00:19:04 +03:00
DYefremov
a745167fb7 update version 2019-02-05 17:02:23 +03:00
DYefremov
8551bc2459 added import bouquets dialog 2019-02-05 16:58:54 +03:00
DYefremov
38ceb6f65d fix switching(zap) channel 2019-02-03 23:44:53 +03:00
DYefremov
33fe78911a changed download icon 2019-01-31 16:57:42 +03:00
DYefremov
3552415054 changed moving up and down in lists 2019-01-30 14:39:58 +03:00
DYefremov
a2ec6f1e1f small changes of moving from keyboard 2019-01-30 09:10:43 +03:00
DYefremov
5f90d07853 fix markers reading in some configs 2019-01-28 23:47:05 +03:00
DYefremov
d88548eece fix opening some ext configs 2019-01-28 23:10:35 +03:00
DYefremov
9ead5b3918 minor fix 2019-01-28 01:04:11 +03:00
DYefremov
1a376e6922 minor fix 2019-01-28 00:28:46 +03:00
DYefremov
eb29f2e1b2 fix parsing for some m3u 2019-01-27 23:59:06 +03:00
DYefremov
bfaab7b2fb fix get iptv url 2019-01-27 23:28:53 +03:00
DYefremov
3e6a7f8a42 changed parsing for iptv services 2019-01-27 23:20:07 +03:00
DYefremov
aff34d7627 minor gui changes 2019-01-27 09:10:56 +03:00
DYefremov
8ef0a451ff upd README 2019-01-24 10:30:47 +03:00
DYefremov
8678f02fd6 added rate lp setup for terrestrial transponder 2019-01-24 10:04:27 +03:00
DYefremov
a0a64606cd enabled transponder edit for terrestrial services 2019-01-23 16:36:50 +03:00
DYefremov
6574296278 little refactoring 2019-01-23 16:35:19 +03:00
DYefremov
d8414f56ee update data for terrestrial services 2019-01-23 13:14:06 +03:00
DYefremov
bb9499392b enabled transponder edit for cable services 2019-01-23 00:43:02 +03:00
DYefremov
069ea9348f ui changes for terrestrial transponder editing 2019-01-21 18:04:46 +03:00
DYefremov
8c932c8913 ui changes for cable transponder edit 2019-01-20 00:25:02 +03:00
DYefremov
562501dda8 added system for terrestrial services 2019-01-19 23:31:52 +03:00
DYefremov
79583869e4 added portuguese 2019-01-19 11:34:19 +03:00
DYefremov
5c9abcee21 updated spanish and dutch 2019-01-19 11:33:09 +03:00
DYefremov
e8377ed174 skeleton of transponder edit for cable and terrestrial services 2019-01-18 18:35:22 +03:00
DYefremov
16f8df0238 added elems for cable and terrestrial services 2019-01-18 18:26:48 +03:00
DYefremov
dab772e25c changed header menu 2019-01-14 10:37:18 +03:00
DYefremov
37791a5537 keyboard support for backup tool 2019-01-12 20:08:17 +03:00
DYefremov
eb55cc76be completion of the path to backups 2019-01-12 19:17:20 +03:00
DYefremov
f728a01963 update version 2019-01-12 18:12:06 +03:00
DYefremov
8aae503e35 support of setting backup path 2019-01-12 18:10:04 +03:00
DYefremov
fe499d6c94 added extra info to the backup tool 2019-01-12 13:57:40 +03:00
DYefremov
cd2c820324 fix iptv dialog show 2019-01-08 18:06:32 +03:00
DYefremov
b4c5af4c04 minor icons changes 2019-01-06 14:43:07 +03:00
DYefremov
f239957ca2 header bar icons changes 2019-01-06 14:05:03 +03:00
DYefremov
3b835e6f34 updating russian translation 2019-01-05 23:31:16 +03:00
DYefremov
4ab3d2d0e1 updated russian translation 2019-01-05 23:17:06 +03:00
DYefremov
3781686213 revert 2019-01-05 22:58:34 +03:00
DYefremov
0247aeed52 fix user bouquets restore for neutrino 2019-01-05 22:53:51 +03:00
DYefremov
3d34539c16 added backup options 2019-01-03 23:32:28 +03:00
DYefremov
0a06b36f60 setting background color for the services 2018-12-23 16:15:48 +03:00
DYefremov
ea1d536c6c added elems for color changing 2018-12-22 23:37:35 +03:00
DYefremov
5087266693 Merge branch 'master' into experimental 2018-12-22 15:35:32 +03:00
DYefremov
3e617e329c Merge branch 'testing' into experimental 2018-12-22 15:34:49 +03:00
DYefremov
1ab0b3d3f0 Merge branch 'testing' 2018-12-22 15:33:56 +03:00
DYefremov
cb3f2d71d1 fix getting the max marker number 2018-12-22 15:33:32 +03:00
DYefremov
62bcb64640 basic implementation of bouquets restore 2018-12-22 15:26:47 +03:00
DYefremov
300ea3b6d9 base implementation of backup restoring 2018-12-21 00:48:45 +03:00
DYefremov
3ae1901757 simple implementation of backups removing 2018-12-20 22:59:01 +03:00
DYefremov
2b63a59c91 added prototype of backup tool gui 2018-12-20 18:14:19 +03:00
DYefremov
ec69bae2a6 compressing backups to zip 2018-12-19 19:22:07 +03:00
DYefremov
c07e0b606b new setting dialog prototype 2018-12-19 18:21:20 +03:00
DYefremov
61745df2a7 little refactoring 2018-12-19 14:43:43 +03:00
DYefremov
146c59e0db сhanged the style of services highlighting 2018-12-19 14:42:29 +03:00
DYefremov
c7c0a4055b fix getting the maximum marker number, little refactoring 2018-12-18 19:23:44 +03:00
DYefremov
6c7d39889d Column refactoring 2018-12-17 18:31:57 +03:00
DYefremov
9d9626d065 added background for extra names in fav list 2018-12-16 22:44:45 +03:00
DYefremov
9bf7a10bf1 Merge branch 'master' into experimental 2018-12-16 17:50:39 +03:00
DYefremov
9659514feb changed version 2018-12-16 17:49:11 +03:00
DYefremov
f39ddd4315 updating service background after edit 2018-12-16 17:47:55 +03:00
DYefremov
90bd9e0211 added background for new services 2018-12-16 17:28:07 +03:00
DYefremov
48f419fec0 Merge remote-tracking branch 'origin/testing' into testing 2018-12-11 20:46:20 +03:00
DYefremov
b993eba2df Merge remote-tracking branch 'origin/experimental' into experimental 2018-12-11 20:45:55 +03:00
DYefremov
dbab024778 bouquet selection refactoring 2018-12-11 19:09:55 +03:00
DYefremov
7999fc6893 Merge branch 'testing' into experimental 2018-12-11 15:26:46 +03:00
DYefremov
b2eecf6cd9 Merge branch 'testing' 2018-12-11 15:26:23 +03:00
DYefremov
919e1c66ba Added spanish translation 2018-12-11 15:25:47 +03:00
DYefremov
3fc5a3fd68 Added services column data function prototypes 2018-12-11 14:10:44 +03:00
Dmitriy Yefremov
345ebc0983 Update _config.yml 2018-12-10 13:38:45 +03:00
Dmitriy Yefremov
8e14e78847 Update _config.yml 2018-12-10 13:37:32 +03:00
DYefremov
856b09bb4b Merge remote-tracking branch 'origin/master' 2018-12-10 13:25:17 +03:00
DYefremov
c303cd3683 upd README 2018-12-10 13:24:26 +03:00
Dmitriy Yefremov
097bb82af2 Set theme jekyll-theme-cayman 2018-12-10 13:16:31 +03:00
Dmitriy Yefremov
e780448be5 Set theme jekyll-theme-midnight 2018-12-10 13:14:08 +03:00
DYefremov
732076e60e minor changes to the player 2018-12-09 21:09:51 +03:00
DYefremov
6bcbb48993 Added dutch translation 2018-12-07 07:19:02 +03:00
DYefremov
2b80704063 added README to deb 2018-12-06 12:08:16 +03:00
DYefremov
215010e2a4 upd README 2018-12-02 19:57:44 +03:00
DYefremov
d9a035d399 upd README 2018-12-02 13:16:47 +03:00
DYefremov
f9008c62d0 upd README 2018-12-02 01:02:20 +03:00
DYefremov
6c28ce29a9 added the channel watching in the program 2018-12-02 00:45:55 +03:00
DYefremov
6ac88317cc minor gui changes for the satellites editor 2018-12-01 16:16:39 +03:00
DYefremov
332b2342cd delete repo files 2018-12-01 14:27:27 +03:00
DYefremov
3703ab4427 little refactoring 2018-12-01 13:34:26 +03:00
DYefremov
a1586944b7 minor changes for menus 2018-12-01 00:29:43 +03:00
DYefremov
22f93c0b89 added creation of empty configuration 2018-12-01 00:17:21 +03:00
DYefremov
803b26ea02 added creation of empty configuration 2018-12-01 00:13:19 +03:00
DYefremov
d86a566668 upd README 2018-11-29 08:59:11 +03:00
DYefremov
a1011c7516 added basic support of lamedb ver. 3 2018-11-28 22:47:57 +03:00
DYefremov
40df14ace8 changed version 2018-11-28 22:47:14 +03:00
DYefremov
96f9e4cca8 minor fix for the cable channel save 2018-11-25 22:55:15 +03:00
DYefremov
cd1a3c5df2 added basic support of cable services 2018-11-25 19:34:28 +03:00
DYefremov
9f9db9257e upd README 2018-11-25 10:26:53 +03:00
DYefremov
f748fae549 minor gui changes for dialogs 2018-11-23 21:05:20 +03:00
DYefremov
c303162c09 little refactoring for ZAP function 2018-11-23 15:06:36 +03:00
DYefremov
6d4434c652 added Z key 2018-11-23 15:03:10 +03:00
Dmitriy
a92b7526eb some compatibility fix 2018-11-20 22:21:26 +03:00
DYefremov
c33f95e0b5 init http api only if enabled in settings 2018-11-19 17:53:33 +03:00
DYefremov
7bfeef9d73 added http api support option in the settings 2018-11-19 13:37:10 +03:00
DYefremov
506207f2e5 added basic http api skeleton 2018-11-17 23:19:17 +03:00
DYefremov
f9ef03b827 fix reference 2018-11-17 13:48:34 +03:00
DYefremov
b5dabf65c7 added groups import support for m3u 2018-11-16 23:08:40 +03:00
DYefremov
3fb3d04161 skip options for the lists 2018-11-16 22:35:47 +03:00
DYefremov
d76e379542 Added accelerators for popup menus 2018-11-16 21:46:27 +03:00
DYefremov
d046bd7c28 minor gui changes for the download dialog 2018-11-13 21:23:37 +03:00
DYefremov
bc2ec7fff1 refactoring of data uploading 2018-11-13 14:17:59 +03:00
DYefremov
e3c3f20da7 little refactoring 2018-11-12 13:43:05 +03:00
DYefremov
2254164f40 F2 code fix 2018-11-12 11:26:11 +03:00
DYefremov
2ae4ac6383 new implementation skeleton of data uploading 2018-11-11 18:35:45 +03:00
DYefremov
80cd4c89b0 little style changes for the iptv dialogs 2018-11-11 15:33:45 +03:00
DYefremov
3df6fd7d0e some style changes for the service details dialog 2018-11-10 23:31:35 +03:00
DYefremov
f3243c9e40 width changes for some dialogs elements 2018-11-09 16:57:47 +03:00
DYefremov
528df9602b checking if value exist for the keyboard key 2018-11-09 14:14:24 +03:00
DYefremov
cd83539855 authentication for the http test 2018-11-09 12:41:36 +03:00
DYefremov
ecc17fa4b2 added default bouquet name 2018-11-08 17:48:51 +03:00
DYefremov
4c555bb7f4 moving the tests functions 2018-11-08 13:07:24 +03:00
DYefremov
e2592bb87a changing settings from the download dialog 2018-11-06 23:43:49 +03:00
DYefremov
631564b22c writing transfer state to the callback 2018-11-06 23:43:13 +03:00
DYefremov
a8bc81fb13 simple output during data transferring 2018-11-06 21:21:47 +03:00
DYefremov
fecb77090d settings dialog minor changes 2018-11-05 23:24:13 +03:00
DYefremov
692495283c scrolling to the first item when copying to the fav list beginning 2018-11-05 14:23:33 +03:00
DYefremov
5ee6615d6f added keyboard keys for laptops 2018-11-05 11:09:15 +03:00
DYefremov
4e34057d16 new variant of working with keyboard keys 2018-11-05 00:31:44 +03:00
DYefremov
f16b47ebf1 added http default settings 2018-11-04 00:37:19 +03:00
DYefremov
3ac16cb7af http properties dialog prototype 2018-11-04 00:36:07 +03:00
DYefremov
2b3c357657 new download dialog 2018-11-04 00:33:50 +03:00
DYefremov
5658a3cfc3 cyrillic keyboard keys map 2018-11-02 23:13:51 +03:00
DYefremov
06c3cb6c42 little refactoring 2018-11-02 23:13:31 +03:00
DYefremov
b60bb6b3b1 added home, end move keys for laptops 2018-11-02 21:58:10 +03:00
DYefremov
b3648a2dae updating elements for download dialog 2018-10-29 21:49:31 +03:00
DYefremov
04efdab63a set default port for telnet 2018-10-29 21:23:31 +03:00
DYefremov
966dd13c23 new download dialog skeleton 2018-10-29 18:46:46 +03:00
DYefremov
2f6450f2b4 translation for context error message 2018-10-26 20:00:11 +03:00
DYefremov
0db2080e2b little improvements for rename 2018-10-26 19:28:40 +03:00
DYefremov
4210c44ee9 fixed cropping of some bouquets names 2018-10-25 16:39:54 +03:00
DYefremov
9d7fef36c2 renaming for the IPTV and MARKER types 2018-10-25 16:00:25 +03:00
DYefremov
303d4d6107 set elems for edit transponder data for terrestrial services as hidden 2018-10-22 17:41:57 +03:00
DYefremov
9ac847ce19 set picons resizing after all providers downloading 2018-10-21 19:20:27 +03:00
DYefremov
d2c7c297b1 base implementation of terrestrial channels editing 2018-10-21 18:34:14 +03:00
DYefremov
995def4be8 minor changes in bouquets generation 2018-10-21 17:24:58 +03:00
DYefremov
0f08ba67e7 catching connection errors when getting a satellites list 2018-10-21 16:09:57 +03:00
DYefremov
499c1aa104 satellites selective loading in the picons downloader 2018-10-21 11:12:57 +03:00
DYefremov
348b0743b4 refactoring for on popup menu function 2018-10-21 11:10:45 +03:00
DYefremov
769445fafe namespace for single channel 2018-10-21 00:17:22 +03:00
DYefremov
61af6e50ce onid for the single channels parsing 2018-10-20 07:27:12 +03:00
DYefremov
4fd7295762 deleting files when closing the picons downloader 2018-10-19 11:18:55 +03:00
DYefremov
b0ad01da6c added picons downloading for the single channels 2018-10-18 19:19:40 +03:00
DYefremov
4320adc46d show single channels in the picons downloader 2018-10-17 21:36:02 +03:00
DYefremov
e2ec359327 added satellites loading in the picons dialog 2018-10-17 00:12:31 +03:00
DYefremov
dc773339ab little fix for fav end copy 2018-10-16 14:12:07 +03:00
DYefremov
e5f8ebbf37 Merge branch 'master' into experimental 2018-10-15 14:09:14 +03:00
DYefremov
1489b3ba4f skip iptv stream deletion for 403 error code 2018-10-15 14:08:33 +03:00
DYefremov
e871e88f46 fix names saving for the neutrino webtv 2018-10-15 13:37:40 +03:00
DYefremov
ea9ce5dfaf Merge branch 'master' into experimental 2018-10-15 12:36:26 +03:00
DYefremov
a1dada9a55 fix showing iptv dialog for neutrino 2018-10-15 12:36:03 +03:00
DYefremov
b137a790a0 added terrestrial services reading support 2018-10-15 11:39:33 +03:00
DYefremov
9ef776d8ab moving download dialog in the separate file 2018-10-14 20:27:06 +03:00
DYefremov
e5b726cbe6 little refactoring for the download dialog 2018-10-14 16:01:05 +03:00
DYefremov
fcfac6c20b skip receiver reboot during uploading bouquets if reachable webif 2018-10-14 12:45:12 +03:00
DYefremov
e7a4b06945 little refactoring for dynamic elements 2018-10-14 11:37:53 +03:00
DYefremov
3fe84f82f9 upd README 2018-10-13 22:28:21 +03:00
DYefremov
3eee824160 added copying from the main list to the bouquet end 2018-10-13 22:27:32 +03:00
DYefremov
bfcab961d5 minor gui changes for picons dialog 2018-10-13 12:57:37 +03:00
Dmitriy
c3f4390d11 little optimisation 2018-10-13 10:48:39 +03:00
DYefremov
dce3f0104d fix saving in fav list after paste 2018-10-10 22:46:36 +03:00
DYefremov
0c4c0da17f upd README.md 2018-10-10 21:56:38 +03:00
DYefremov
1dec2c8fc1 added url checking before iptv stream save 2018-10-10 21:54:56 +03:00
DYefremov
6eb112eb38 little refactoring for delete and copy functions 2018-10-09 13:16:49 +03:00
DYefremov
8307f153cd added logger for the satellites parser 2018-10-08 23:51:02 +03:00
DYefremov
4796a558bb modulation value fix 2018-10-08 21:41:02 +03:00
DYefremov
61d257f801 fix for compatibility with xenial for some functions 2018-10-08 00:30:09 +03:00
DYefremov
af74e7c32c updating player navigation buttons state 2018-10-06 18:28:59 +03:00
DYefremov
bf474ee8d0 fix opening lamedb5 2018-10-06 16:29:41 +03:00
DYefremov
26e6c40a1b fix rename by Ctrl + R, F2 2018-10-06 15:14:29 +03:00
DYefremov
5723f29b60 translation for the streams player elements 2018-10-05 15:28:15 +03:00
DYefremov
bc65e1b446 added prev and next buttons for the stream player 2018-10-05 15:12:13 +03:00
DYefremov
1842bec2aa minor translation fix 2018-10-04 16:31:05 +03:00
DYefremov
9cc33b9a33 fixed remove all for iptv 2018-10-04 16:24:53 +03:00
DYefremov
c8fefae571 Merge remote-tracking branch 'origin/experimental' into experimental 2018-10-04 09:14:36 +03:00
DYefremov
6d6616425c added repo files 2018-10-04 09:14:14 +03:00
DYefremov
cb4ed7ebd1 update readme 2018-10-02 09:13:49 +03:00
DYefremov
a51929068a stream player minor changes 2018-10-01 20:16:05 +03:00
DYefremov
dff2071fa3 redesign ui elements for the player 2018-09-30 23:16:30 +03:00
DYefremov
fdd2d61a28 new player prototype 2018-09-30 00:12:15 +03:00
DYefremov
1532b213e4 little refactoring and optimization of hide, lock functionality 2018-09-29 21:57:17 +03:00
DYefremov
d2b76b08e1 fix editing from the bouquet list if main list in the filter mode 2018-09-28 10:09:36 +03:00
DYefremov
af40443730 translation for the filter bar elements 2018-09-27 22:12:27 +03:00
DYefremov
62d9e21433 positions update optimisation for the filter 2018-09-27 22:02:35 +03:00
DYefremov
ddfd3db7fa little translation correction 2018-09-26 15:07:26 +03:00
DYefremov
a50a7c426e little optimisation on data loading 2018-09-25 21:26:03 +03:00
DYefremov
e93760b2ac improved functionality of the config dialog for the IPTV streams list 2018-09-23 19:19:34 +03:00
DYefremov
4a80da7515 moving iptv dialogs in the separate file 2018-09-23 00:17:58 +03:00
DYefremov
962db5f736 added selecting row under the cursor for all views 2018-09-22 21:14:56 +03:00
DYefremov
1c0ca0dbeb selecting row under the cursor at the dragging begin 2018-09-22 21:08:28 +03:00
DYefremov
40faa4029d changes of button press handling in the view 2018-09-22 19:14:47 +03:00
DYefremov
86351c61ae Skipping terrestrial and cable channels during parsing 2018-09-22 19:06:23 +03:00
DYefremov
1b56899636 minor changes 2018-09-21 11:15:20 +03:00
DYefremov
b92a7f1bdb bouquets deletion fix 2018-09-21 11:09:40 +03:00
Dmitriy Yefremov
7b1db69867 Merge branch 'master' into experimental 2018-09-21 10:31:02 +03:00
DYefremov
8ae34fa6e6 minor changes 2018-09-21 10:16:30 +03:00
DYefremov
a507f9d401 Updatating bouquets type in the model and dict 2018-09-20 18:37:47 +03:00
DYefremov
ac724fc36f cut-copy-paste impl for the bouquets list 2018-09-20 16:36:03 +03:00
DYefremov
90564859b2 DnD implementation for the bouquets list 2018-09-20 11:06:33 +03:00
DYefremov
562a5e5c6d providers parsing optimisation for picons 2018-09-19 23:02:26 +03:00
DYefremov
49605b87f9 DnD skeleton for bouquets list 2018-09-19 11:46:41 +03:00
DYefremov
27bdac7b4f Changes in handling keystrokes. 2018-09-18 14:40:24 +03:00
DYefremov
b92c02fd63 added copy possibility for fav list 2018-09-18 10:35:10 +03:00
DYefremov
aec3874e0b new player prototype 2018-09-18 07:13:32 +03:00
DYefremov
08a31e9aa0 added verification on service type before rename for the bouquet 2018-09-16 23:40:02 +03:00
DYefremov
8850ff910c added verification on service type before rename for the bouquet 2018-09-16 23:39:31 +03:00
DYefremov
5f79f5b523 little changes for fav popup menu 2018-09-16 16:59:34 +03:00
DYefremov
62bf6eadac fixed removing picon for the iptv service 2018-09-16 15:41:06 +03:00
DYefremov
c7575c4646 translation for some elements 2018-09-15 17:26:01 +03:00
DYefremov
57ae0c8d53 parsing fix of some satellites during update from the web 2018-09-15 16:04:08 +03:00
DYefremov
c37838d2c0 little gui changes for satellite and transponder dialogs 2018-09-15 10:50:40 +03:00
DYefremov
fa9949d562 revert buttons 2018-09-14 14:39:27 +03:00
DYefremov
bf54187f28 settings dialog moved to a separate file 2018-09-14 14:23:25 +03:00
DYefremov
696bce8201 little changes for the filter bar 2018-09-12 17:49:28 +03:00
DYefremov
5d01bd5479 little changes for the filter bar 2018-09-12 17:46:45 +03:00
DYefremov
403426ba75 added free services filter in the main list 2018-09-12 17:26:22 +03:00
DYefremov
43a884159b simple implementation of filtering support by type and position in main list 2018-09-12 14:05:28 +03:00
DYefremov
f925aa5642 setting default name for the service in bouquet 2018-09-11 16:25:12 +03:00
DYefremov
8bdbe45a57 support for extra names in the bouquet list 2018-09-11 15:21:05 +03:00
DYefremov
b2a24974a3 redesign of the satellites update dialog 2018-09-10 22:37:42 +03:00
DYefremov
9dd6633c6a little style changes of service details dialog 2018-09-10 08:46:01 +03:00
DYefremov
1dc5461f90 new elements for the fav popup menu 2018-09-10 00:19:45 +03:00
DYefremov
15418d234d support of opening bouquets with different names of services in bouquet and main list 2018-09-09 23:38:00 +03:00
DYefremov
0ac439cc84 GUI redesign of the some dialogs 2018-09-08 09:58:54 +03:00
DYefremov
b4eda74c6d GUI redesign of the service details dialog 2018-09-08 00:19:20 +03:00
DYefremov
35d598f1f4 redesign of GUI of the IPTV dialog 2018-09-07 23:42:59 +03:00
DYefremov
d2272e5715 redesign of GUI of the picons dialog 2018-09-07 23:10:48 +03:00
DYefremov
252c2245f7 translation some elements 2018-09-02 23:16:32 +03:00
DYefremov
93fd3cd2c3 stream play fix for enigma2 2018-09-01 23:17:03 +03:00
DYefremov
0e3d5df4bf picons support for iptv (enigma2) 2018-09-01 00:49:11 +03:00
DYefremov
e476c26bb5 changed popdown to the hide for compatibility 2018-08-31 17:45:52 +03:00
DYefremov
fb0996f94e full screen mode prototype for the stream player 2018-08-31 17:26:36 +03:00
DYefremov
1da72666d7 fix play 2018-08-25 23:53:53 +03:00
DYefremov
f790f7d0b5 new prototype of the streams player 2018-08-25 15:30:12 +03:00
DYefremov
76a0f43485 added delay for the search and filter functions 2018-08-24 12:03:51 +03:00
DYefremov
0dcbf98d1f added TID for iptv list config dialog 2018-08-22 00:09:52 +03:00
DYefremov
bc0eb37775 info bar close implementations 2018-08-21 17:28:29 +03:00
DYefremov
d494702257 base implementation of iptv list config dialog for enigma2 2018-08-21 17:19:44 +03:00
DYefremov
db60217474 iptv list configuration dialog skeleton 2018-08-19 23:27:13 +03:00
DYefremov
a399660a15 fixed get url for iptv 2018-08-19 10:55:41 +03:00
DYefremov
2eeb53537a little changes for gui elements 2018-08-18 17:36:57 +03:00
DYefremov
ab5f98a2b6 skeleton for IPTV lists configuration dialog 2018-08-18 17:35:30 +03:00
DYefremov
6d92ed667f append picon while adding a service 2018-08-18 12:31:12 +03:00
DYefremov
ff123b579f data loading refactoring 2018-08-18 11:35:44 +03:00
DYefremov
f9a66f8f75 data loading optimisation 2018-08-18 10:21:40 +03:00
DYefremov
32f33815c2 little gui changes 2018-08-15 10:51:19 +03:00
DYefremov
ea9ea98e1a replaced the dialog with a window 2018-08-05 00:54:30 +03:00
DYefremov
9c32d24a20 webtv download fix 2018-08-04 11:38:38 +03:00
DYefremov
25f483d760 clean 2018-08-01 22:34:30 +03:00
DYefremov
d9e471eaec download fix 2018-08-01 22:10:00 +03:00
DYefremov
e4f8a075f4 removed preview mode elements for IPTV 2018-08-01 11:05:29 +03:00
DYefremov
0a3dc8f79d translation for some elements 2018-07-30 22:48:48 +03:00
DYefremov
ad69df0b63 new popup menu for satellites editor 2018-07-13 17:12:02 +03:00
DYefremov
2a3a9e124b file name format fix. for bouquets 2018-07-13 12:28:13 +03:00
DYefremov
8afd1e8a80 select all, including the filter 2018-07-12 19:44:27 +03:00
DYefremov
ee8cc5b139 added (un)select all in the satellites update dialog 2018-07-12 19:11:32 +03:00
DYefremov
bed490f491 minor gui changes 2018-07-12 11:57:02 +03:00
DYefremov
620ff4bd60 added label of the current bouquet name 2018-07-09 19:00:05 +03:00
DYefremov
99ecb0f22e updating dynamic elements before popup menu 2018-07-09 11:38:36 +03:00
DYefremov
31603bfd41 little ui elements changes 2018-07-08 22:05:26 +03:00
DYefremov
abd803a58c lock/hide fix 2018-07-08 14:58:41 +03:00
DYefremov
f84e77cbce new gui for main window 2018-07-08 00:09:26 +03:00
DYefremov
170c8ffc55 new gui for satellites update dialog 2018-07-06 22:26:48 +03:00
DYefremov
bf3ba96fb9 new gui for satellites update dialog 2018-07-06 22:26:27 +03:00
DYefremov
b3e057a5a3 new gui for satellites editor 2018-07-05 17:59:17 +03:00
DYefremov
78c07f3934 added extra dialog for searching unavailable iptv streams 2018-07-03 19:30:45 +03:00
DYefremov
249a49aff5 test implementation of remove all unavailable iptv streams 2018-06-29 22:43:04 +03:00
DYefremov
03c291a61e added lamedb5 to ftp 2018-06-05 20:45:47 +03:00
DYefremov
97d9ce8b68 lamedb5 reading fix 2018-06-01 18:41:59 +03:00
DYefremov
1a55df6674 lamedb5 writing 2018-06-01 17:45:26 +03:00
DYefremov
6ac10c1380 opening lamedb5 2018-06-01 11:16:30 +03:00
DYefremov
13270b6152 v5 support skeleton 2018-05-28 18:45:31 +03:00
DYefremov
b2c0359017 applying filter after values change 2018-05-22 21:59:17 +03:00
DYefremov
8a6dd1da93 added full screen mode for the player 2018-05-19 16:24:20 +03:00
DYefremov
2e1410ca36 satellites list update in the separate thread 2018-05-12 14:36:38 +03:00
DYefremov
3d96181450 translation for some elements 2018-05-12 12:42:42 +03:00
DYefremov
fe749ca594 added checking that vlc is installed 2018-05-12 12:21:34 +03:00
DYefremov
1b6cd58112 added transponder validity checking 2018-05-12 11:47:19 +03:00
DYefremov
8d405d223a new src for the sat update dialog 2018-05-10 23:28:51 +03:00
DYefremov
81ad19043a new source for sat update dialog 2018-05-10 00:44:42 +03:00
DYefremov
34db58f8e0 selection fix in satellites update dialog 2018-05-07 23:55:22 +03:00
DYefremov
890163af4a added search feature for satellites update dialog 2018-05-07 18:19:00 +03:00
DYefremov
c4e8a6646d added simple filter for satellites update dialog 2018-05-07 15:19:05 +03:00
DYefremov
639c8511bf added ui elements for find and filter for update dialog 2018-05-07 00:44:46 +03:00
DYefremov
5e082fc5d7 new header variant for sat update dialog 2018-05-06 00:08:56 +03:00
DYefremov
7f393ff9ba getting of satellites in separate processes 2018-05-05 22:01:50 +03:00
DYefremov
b37aac0cd9 satellite data update in the main model 2018-05-02 21:23:09 +03:00
DYefremov
d857c4b786 append output for sat update dialog 2018-05-01 21:05:18 +03:00
DYefremov
6ffd1d7926 receive_satellites skeleton 2018-04-30 18:37:02 +03:00
DYefremov
415ad79c80 new satellites update dialog skeleton 2018-04-30 11:11:55 +03:00
DYefremov
da4fef7f6b test implementation of IPTV preview 2018-04-29 15:36:35 +03:00
DYefremov
76c034435d iptv preview mode skeleton 2018-04-29 01:44:28 +03:00
Dmitriy Yefremov
f9239f0642 new variant of satellites download skeleton 2018-04-25 17:26:29 +03:00
DYefremov
7f096df998 satellites dialog skeleton 2018-04-23 23:14:07 +03:00
Dmitriy Yefremov
3f0738d874 skeleton of satellites downloader 2018-04-23 14:42:41 +03:00
Dmitriy Yefremov
b310b640b4 little fix for move in tree model 2018-04-16 19:42:48 +03:00
Dmitriy Yefremov
18caa58336 Update README.md 2018-04-16 18:53:49 +03:00
Dmitriy Yefremov
03e5909c23 support of "Home" and "End" keys, new variant of move in lists 2018-04-16 18:50:48 +03:00
Dmitriy Yefremov
d9071632d2 grouping the scattered rows on move 2018-04-14 00:05:08 +03:00
DYefremov
694269113a iptv reference fix 2018-04-10 16:03:36 +03:00
DYefremov
78f347a505 bouquet creation place depending on the profile 2018-04-10 13:32:43 +03:00
DYefremov
eb9be7b190 fav view update after service edit 2018-04-10 13:04:21 +03:00
DYefremov
952aeb4d22 little refactoring for bq selection 2018-04-10 11:15:50 +03:00
Dmitriy Yefremov
8a865513b3 translation for services popup menu 2018-04-09 21:28:19 +03:00
DYefremov
30e38dde3f bouquet auto generation by type 2018-04-07 23:49:36 +03:00
DYefremov
5f68eb0f1a service deletion refactoring 2018-04-06 17:57:04 +03:00
DYefremov
ce92134a00 refactoring for multiple selection in the bouquets 2018-04-06 16:02:16 +03:00
DYefremov
2c80d13170 basic implementation of bouquets generation 2018-04-04 16:52:58 +03:00
Dmitriy Yefremov
a49d6490c5 bouquets gen skeleton 2018-04-03 22:47:29 +03:00
DYefremov
dc76a7801e Skeleton of the bouquets auto-generation feature 2018-04-02 23:55:41 +03:00
DYefremov
0b0d3ded8c IPTV elements translation 2018-04-01 09:54:50 +03:00
DYefremov
c05dd026fb Merge remote-tracking branch 'origin/experimental' into experimental 2018-04-01 09:39:29 +03:00
DYefremov
dca94271b0 checking data in IPTV dialog 2018-04-01 09:39:14 +03:00
Dmitriy Yefremov
8d7aa8736e Update README.md 2018-03-30 09:41:05 +03:00
Dmitriy Yefremov
1d693670f4 Merge branch 'experimental' into testing 2018-03-30 09:07:41 +03:00
Dmitriy Yefremov
52f50cdaf5 upd readme 2018-03-30 09:07:17 +03:00
DYefremov
d2ac5d5ac4 impl of checking the CAID field 2018-03-29 13:49:01 +03:00
Dmitriy Yefremov
fe579358f6 Merge branch 'experimental' into testing 2018-03-28 17:30:00 +03:00
Dmitriy Yefremov
db942ee10b set defaults for dvb-s2 after system change 2018-03-28 17:29:07 +03:00
DYefremov
b4648a6efd Merge remote-tracking branch 'origin/testing' into testing 2018-03-27 23:05:39 +03:00
DYefremov
647f468feb Merge branch 'experimental' into testing 2018-03-27 23:05:21 +03:00
DYefremov
ef608df76b picon name update after service edit 2018-03-27 23:03:56 +03:00
DYefremov
8e32373a99 picon name update after service edit 2018-03-27 23:03:01 +03:00
DYefremov
7f817944fa reference update in the service dialog 2018-03-26 17:33:20 +03:00
Dmitriy Yefremov
7752da92b1 Merge branch 'experimental' into testing 2018-03-25 10:47:33 +03:00
Dmitriy Yefremov
24023d438d ui changes for the service dialog 2018-03-25 10:46:30 +03:00
DYefremov
9e0d8840f3 init neutrino ui elms for service dialog 2018-03-23 21:42:54 +03:00
Dmitriy Yefremov
d762f097d0 state update for navigation buttons 2018-03-20 23:42:06 +03:00
DYefremov
336aa47177 added default init in service dialog 2018-03-20 00:33:00 +03:00
DYefremov
1eeccd654a added validation for non empty entries in service dialog 2018-03-18 18:11:16 +03:00
DYefremov
e37abef359 decoupling for [on save] in service dialog 2018-03-18 17:13:21 +03:00
Dmitriy Yefremov
c120f42ee1 skeleton of getting transponder data for neutrino 2018-03-17 22:18:10 +03:00
DYefremov
1531548e51 add impl for iptv 2018-03-16 00:10:33 +03:00
DYefremov
72ebdceb6e edit impl for iptv 2018-03-15 23:10:22 +03:00
Dmitriy Yefremov
791fa2b5f6 init neutrino data skeleton for iptv dialog 2018-03-13 23:27:55 +03:00
Dmitriy Yefremov
47df44c202 little optimisation 2018-03-13 10:42:56 +03:00
Dmitriy Yefremov
d7635370ba iptv dialog init data 2018-03-12 22:47:43 +03:00
DYefremov
1d577750c0 Iptv dialog skeleton 2018-03-11 21:52:10 +03:00
DYefremov
b56685edb1 Ui skeleton for IPTV dialog 2018-03-10 19:12:56 +03:00
DYefremov
f7f230f40e Separation of service creation and editing 2018-03-10 17:49:53 +03:00
Dmitriy Yefremov
88e19e2fd1 added warning message to the picons dialog 2018-03-09 17:40:26 +03:00
DYefremov
ae6f0e1ae2 pos fix for picons dialog 2018-03-07 23:56:21 +03:00
DYefremov
c3e880890e little picons dialog changes 2018-03-07 22:43:42 +03:00
Dmitriy Yefremov
a7edb6d0f6 Merge branch 'experimental' into testing 2018-03-06 20:46:25 +03:00
DYefremov
03e18401cc global search navigation skeleton 2018-03-06 19:06:16 +03:00
DYefremov
1d6b8c2558 Dialogs translation 2018-03-06 11:34:06 +03:00
Dmitriy Yefremov
ccd111cd94 added ui elements for search navigation 2018-03-05 22:45:21 +03:00
Dmitriy Yefremov
e8f3b5df8a convert to impl 2018-03-04 19:37:41 +03:00
DYefremov
c4c9c73809 neutrino data init skeleton for service dialog 2018-03-03 23:24:43 +03:00
DYefremov
a4514ebb2b fix tv counter 2018-03-03 20:55:08 +03:00
Dmitriy Yefremov
7b44df9afd added converter tab for picons dialog 2018-03-03 13:38:33 +03:00
DYefremov
3a018e9654 added local translation 2018-03-02 17:08:40 +03:00
DYefremov
9d4e571d89 added local translation 2018-03-02 17:06:53 +03:00
DYefremov
f62184c96c type entry for service dialog 2018-03-02 15:12:21 +03:00
Dmitriy Yefremov
320183554c status bar changes 2018-03-01 20:39:00 +03:00
Dmitriy Yefremov
a525816eca russian translation 2018-03-01 00:18:05 +03:00
DYefremov
0e11a223ad russian translate 2018-02-27 22:05:09 +03:00
Dmitriy Yefremov
6e4b992a79 russian translation skeleton 2018-02-27 14:55:03 +03:00
DYefremov
1164d38e5c e2 impl for set transponder 2018-02-26 16:26:53 +03:00
Dmitriy Yefremov
4288d62a53 set transponder for services 2018-02-25 22:59:36 +03:00
Dmitriy Yefremov
b17bd13fb5 revert transponder services dialog 2018-02-25 17:52:21 +03:00
Dmitriy Yefremov
7124fd6a92 new ui variant for services details dialog 2018-02-24 16:12:05 +03:00
DYefremov
81f354207d ServicesListDialog skeleton 2018-02-23 23:56:19 +03:00
Dmitriy Yefremov
15cb611764 fix flags 2018-02-23 17:36:49 +03:00
Dmitriy Yefremov
6115433aba skeleton of implementation of service editing for Enigma 2018-02-23 17:14:08 +03:00
Dmitriy Yefremov
0c5b9165ef intermediate impl of save in service dialog 2018-02-20 23:34:07 +03:00
DYefremov
c432646f30 on save skeleton for service details dialog 2018-02-20 00:20:32 +03:00
DYefremov
f70913832c showing flags in the service details dialog 2018-02-19 23:18:01 +03:00
DYefremov
25ee7f3538 added simple wait dialog 2018-02-18 17:14:02 +03:00
DYefremov
5a76601ae6 little refactoring in UI for edit/rename 2018-02-18 11:23:31 +03:00
Dmitriy Yefremov
4367fe6ead skeleton implementation of service details show 2018-02-17 17:07:32 +03:00
Dmitriy Yefremov
242642a7ed Update README.md 2018-02-17 16:40:40 +03:00
Dmitriy Yefremov
074fc960e5 skeleton implementation of service details show 2018-02-17 16:23:41 +03:00
DYefremov
e26d08ca8e filling pids in service dialog 2018-02-16 07:41:19 +03:00
DYefremov
8c433680a9 little refactoring ) 2018-02-16 01:58:43 +03:00
DYefremov
547046bddb service details dialog data filling 2018-02-16 01:56:28 +03:00
Dmitriy Yefremov
12983bb1a6 new variant of service details dialog 2018-02-15 18:05:13 +03:00
Dmitriy Yefremov
5dc20232ef Update README.md 2018-02-15 15:22:09 +03:00
Dmitriy Yefremov
0040ecee32 Update README.md 2018-02-15 15:21:08 +03:00
Dmitriy Yefremov
e8f30b667d service dialog elements update 2018-02-15 15:16:34 +03:00
DYefremov
99a9f081fa service details dialog ui changes 2018-02-14 19:06:08 +03:00
Dmitriy Yefremov
51605ae680 service details dialog skeleton 2018-02-14 00:00:52 +03:00
Dmitriy Yefremov
ca06400071 little ui changes 2018-02-13 17:21:25 +03:00
Dmitriy Yefremov
588df32b2f added IPTV icon 2018-02-12 14:27:21 +03:00
Dmitriy Yefremov
ab7f560b4f webtv download/upload 2018-02-12 13:34:00 +03:00
DYefremov
ffce103eae m3u gui elements for neutrino 2018-02-11 23:14:22 +03:00
Dmitriy Yefremov
90418f0e28 webtv parse for neutrino 2018-02-11 16:47:20 +03:00
92 changed files with 50252 additions and 6664 deletions

View File

@@ -1,11 +0,0 @@
[Desktop Entry]
Version=1.0
Name=DemonEditor
Comment=Channels and satellites list editor for Enigma2
Comment[ru]=Редактор списка каналов и спутников для Enigma2
Icon=accessories-text-editor
Exec=bash -c 'cd $(dirname %k) && ./start.py'
Terminal=false
Type=Application
Categories=Utility;Application;
StartupNotify=true

67
DemonEditor.spec Normal file
View File

@@ -0,0 +1,67 @@
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', 'tkinter'],
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",
'LSApplicationCategoryType': 'public.app-category.utilities',
'CFBundleShortVersionString': "1.0.0 Beta (Build: {})".format(BUILD_DATE),
'NSHumanReadableCopyright': u"Copyright © 2020, Dmitriy Yefremov",
'NSRequiresAquaSystemAppearance': 'false'
})

View File

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

107
README.md
View File

@@ -1,32 +1,93 @@
# DemonEditor
# <img src="app/ui/icons/hicolor/96x96/apps/demon-editor.png" width="32" /> DemonEditor
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) ![platform](https://img.shields.io/badge/platform-macos-lightgrey)
## Enigma2 channel and satellites list editor for macOS (experimental).
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).
**The functionality and performance of this version may be different from the [Linux version](https://github.com/DYefremov/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 app window in macOS Big Sur.](https://user-images.githubusercontent.com/7511379/92320982-9b20c780-f02e-11ea-8a43-fc0c70503573.png)
Keyboard shortcuts:
Ctrl + X, C, V, Up, Down, PageUp, PageDown, S, T, E, L, H, Space; Insert, Delete, F2.
Insert - copies the selected channels from the main list to the bouquet or inserts (creates) a new bouquet.
Ctrl + X - only in bouquet list. Ctrl + C - only in services list.
Clipboard is "rubber". There is an accumulation before the insertion!
Ctrl + E, F2 - edit/rename.
Ctrl + S, T, E in Satellites edit tool for create and edit satellite or transponder.
Ctrl + L - parental lock.
Ctrl + H - hide/skip.
Left/Right - remove selection.
## Main features of the program
* Editing bouquets, channels, satellites.
* Import function.
* Backup function.
* Extended support of IPTV.
* Support of picons.
* Downloading of picons and updating of satellites (transponders) from the web.
* 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.
Multiple selections in lists only with Space key (as in file managers)!
For **multiple** selection with the mouse, press and hold the **&#8984;** key!
Extra:
Ability to import IPTV into bouquet from m3u files(Enigma2 only)!
Tool for downloading picons from lyngsat.com.
## Minimum requirements
*Python >= 3.5.2, GTK+ >= 3.16 with PyGObject bindings, python3-requests.*
## Installation and Launch
To run the program on macOS, you need to install [brew](https://brew.sh/).
Then install the required components via terminal:
```brew install python3 gtk+3 pygobject3 adwaita-icon-theme```
```pip3 install requests```
#### Optional:
```brew install wget```
```pip3 install pillow, pyobjc```
Tests only in image based on OpenPLi or last BPanther(neutrino) images with GM 990 Spark Reloaded receiver
in my preferred linux distro (Last Linux Mint 18.* - MATE 64-bit)!
To start the program, just download the [archive](https://github.com/DYefremov/DemonEditor/archive/experimental-mac.zip), unpack and run it from the terminal
with the command: ```./start.py```
## Standalone package
You can also download the ready-made package as a ***.dmg** file from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
Recommended copy the package to the **Application** directory.
Perhaps in the security settings it will be necessary to allow the launch of this application!
**The package may not contain all the latest changes. Not all features can be supported and tested!**
Minimum requirements: Python >= 3.5.2 and GTK+ 3 with PyGObject bindings.
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!
Terrestrial and cable channels at the moment are not supported!
#### Building your own package
Install [PyInstaller](https://www.pyinstaller.org/) with the command from the terminal:
```pip3 install pyinstaller```
and in the root dir run command:
```pyinstaller DemonEditor.spec```
## Important
**This version is not fully tested and has experimental status!**
Note. To create a simple debian package, you can use the build-deb.sh
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.
#### Command line arguments:
* **-l** - write logs to file.
* **-d on/off** - turn on/off debug mode. Allows to display more information in the logs.
* **-t on/off** - show/hide simple built-in **telnet** client (experimental). **ANSI escape sequences are not supported!**
## License
Licensed under the [MIT](LICENSE) license.

2
_config.yml Normal file
View File

@@ -0,0 +1,2 @@
theme: jekyll-theme-slate
show_downloads: true

View File

@@ -1,25 +1,33 @@
import logging
from functools import wraps
from threading import Thread
from threading import Thread, Timer
from gi.repository import GLib
_LOG_FILE = "demon-editor.log"
_DATE_FORMAT = "%d-%m-%y %H:%M:%S"
_LOGGER_NAME = "main_logger"
logging.Logger(_LOGGER_NAME)
logging.basicConfig(level=logging.INFO,
filename=_LOG_FILE,
format="%(asctime)s %(message)s",
datefmt=_DATE_FORMAT)
_LOGGER_NAME = None
def get_logger():
return logging.getLogger(_LOGGER_NAME)
def init_logger():
global _LOGGER_NAME
_LOGGER_NAME = "main_logger"
logging.Logger(_LOGGER_NAME)
logging.basicConfig(level=logging.INFO,
format="%(asctime)s %(message)s",
datefmt=_DATE_FORMAT,
handlers=[logging.FileHandler(_LOG_FILE), logging.StreamHandler()])
log("Logging is enabled.", level=logging.INFO)
def log(message, level=logging.ERROR):
get_logger().log(level, message)
def log(message, level=logging.ERROR, debug=False, fmt_message="{}"):
""" The main logging function. """
logger = logging.getLogger(_LOGGER_NAME)
if debug:
from traceback import format_exc
logger.log(level, fmt_message.format(format_exc()))
else:
logger.log(level, message)
def run_idle(func):
@@ -43,5 +51,31 @@ def run_task(func):
return wrapper
def run_with_delay(timeout=5):
""" Starts the function with a delay.
If the previous timer still works, it will canceled!
"""
def run_with(func):
timer = None
@wraps(func)
def wrapper(*args, **kwargs):
nonlocal timer
if timer and timer.is_alive():
timer.cancel()
def run():
GLib.idle_add(func, *args, **kwargs, priority=GLib.PRIORITY_LOW)
timer = Timer(interval=timeout, function=run)
timer.start()
return wrapper
return run_with
if __name__ == "__main__":
pass

570
app/connections.py Normal file
View File

@@ -0,0 +1,570 @@
import os
import re
import socket
import time
import urllib
import xml.etree.ElementTree as ETree
from enum import Enum
from ftplib import FTP, error_perm
from http.client import RemoteDisconnected
from telnetlib import Telnet
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import (urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener,
install_opener, Request)
from app.commons import log, run_task
from app.settings import SettingsType
BQ_FILES_LIST = ("tv", "radio", # enigma 2
"myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino
DATA_FILES_LIST = ("lamedb", "lamedb5", "blacklist", "whitelist",)
STC_XML_FILE = ("satellites.xml", "terrestrial.xml", "cables.xml")
WEB_TV_XML_FILE = ("webtv.xml",)
PICONS_SUF = (".jpg", ".png")
class DownloadType(Enum):
ALL = 0
BOUQUETS = 1
SATELLITES = 2
PICONS = 3
WEBTV = 4
EPG = 5
class TestException(Exception):
pass
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:
ftp.encoding = "utf-8"
callback("FTP OK.\n")
save_path = settings.data_local_path
os.makedirs(os.path.dirname(save_path), exist_ok=True)
# bouquets
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
download_files(ftp, 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)
if download_type in (DownloadType.ALL, DownloadType.WEBTV):
download_xml(ftp, 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)
# epg.dat
if download_type is DownloadType.EPG:
stb_path = settings.services_path
epg_options = settings.epg_options
if epg_options:
stb_path = epg_options.get("epg_dat_stb_path", stb_path)
save_path = epg_options.get("epg_dat_path", save_path)
ftp.cwd(stb_path)
download_files(ftp, 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):
s_type = settings.setting_type
data_path = settings.data_local_path
host = settings.host
base_url = "http{}://{}:{}".format("s" if settings.http_use_ssl else "", host, settings.http_port)
url = "{}/web/".format(base_url)
tn, ht = None, None # telnet, http
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)
next(ht)
message = ""
if download_type is DownloadType.BOUQUETS:
message = "User bouquets will be updated!"
elif download_type is DownloadType.ALL:
message = "All user data will be reloaded!"
elif download_type is DownloadType.SATELLITES:
message = "Satellites.xml file will be updated!"
elif download_type is DownloadType.PICONS:
message = "Picons will be updated!"
params = urlencode({"text": message, "type": 2, "timeout": 5})
ht.send((url + "message?{}".format(params), "Sending info message... "))
if download_type is DownloadType.ALL:
time.sleep(5)
ht.send((url + "powerstate?newstate=0", "Toggle Standby "))
time.sleep(2)
else:
if download_type is not DownloadType.PICONS:
# telnet
tn = telnet(host=host,
user=settings.telnet_user,
password=settings.telnet_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:
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)
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)
if download_type is DownloadType.BOUQUETS:
ftp.cwd(services_path)
upload_bouquets(ftp, data_path, remove_unused, callback)
if download_type is DownloadType.ALL:
upload_xml(ftp, 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.cwd(services_path)
upload_bouquets(ftp, data_path, remove_unused, callback)
upload_files(ftp, 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)
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."))
elif download_type is DownloadType.ALL:
ht.send((url + "servicelistreload?mode=0", "Reloading lamedb and Userbouquets."))
ht.send((url + "powerstate?newstate=4", "Wakeup from Standby."))
if done_callback is not None:
done_callback()
finally:
if tn:
tn.close()
if ht:
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):
files = []
ftp.dir(files.append)
bq_files = ("tv", "radio", "bouquets.xml", "ubouquets.xml")
for file in filter(lambda f: f.endswith(bq_files), map(lambda f: f.split()[-1], map(str.rstrip, files))):
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)
download_files(ftp, data_path, xml_files, callback)
# ***************** 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
files = []
ftp.dir(files.append)
for file in filter(picons_filter_function(files_filter), map(lambda f: f.split()[-1], map(str.rstrip, files))):
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
files = []
ftp.dir(files.append)
for file in filter(picons_filter_function(files_filter), map(lambda f: f.split()[-1], map(str.rstrip, files))):
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:
ftp.encoding = "utf-8"
callback("FTP OK.\n")
delete_picons(ftp, callback, settings.picons_path, files_filter)
if done_callback:
done_callback()
def picons_filter_function(files_filter=None):
return lambda f: f in files_filter if files_filter else f.endswith(PICONS_SUF)
def download_files(ftp, save_path, file_list, callback):
""" Downloads files from the receiver via FTP. """
files = []
ftp.dir(files.append)
for file in map(lambda f: f.split()[-1], filter(lambda s: s.endswith(file_list), map(str.rstrip, files))):
download_file(ftp, file, save_path, callback)
def download_file(ftp, name, save_path, callback):
with open(save_path + name, "wb") as f:
callback("Downloading file: {}. Status: {}\n".format(name, str(ftp.retrbinary("RETR " + name, f.write))))
def send_file(file_name, path, ftp, callback):
""" Opens the file in binary mode and transfers into receiver """
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(HttpAPI.Request.TEST, url, data).get("e2statetext", None)
callback("HTTP: {} {}\n".format(message, "Successful." if resp and message else ""))
def telnet(host, port=23, user="", password="", timeout=5):
try:
tn = Telnet(host=host, port=port, timeout=timeout)
except socket.timeout:
log("telnet error: socket timeout")
else:
time.sleep(1)
command = yield
if user != "":
tn.read_until(b"login: ", timeout)
tn.write(user.encode("utf-8") + b"\n")
time.sleep(timeout)
if 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"))
time.sleep(timeout)
command = yield
time.sleep(timeout)
tn.write("{}\r\n".format(command).encode("utf-8"))
time.sleep(timeout)
yield
# ***************** HTTP API *******************#
class HttpAPI:
__MAX_WORKERS = 4
class Request(Enum):
ZAP = "zap?sRef="
INFO = "about"
SIGNAL = "signal"
STREAM = "stream.m3u?ref="
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"
# 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"
def __init__(self, settings):
from concurrent.futures import ThreadPoolExecutor as PoolExecutor
self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS)
self._settings = settings
self._shutdown = False
self._session_id = 0
self._main_url = None
self._base_url = None
self._data = None
self._is_owif = True
self.init()
def send(self, req_type, ref, callback=print, ref_prefix=""):
if self._shutdown:
return
url = self._base_url + req_type.value
data = self._data
if req_type is self.Request.ZAP or req_type is self.Request.STREAM:
url += urllib.parse.quote(ref)
elif req_type is self.Request.PLAY or req_type is self.Request.PLAYER_REMOVE:
url += "{}{}".format(ref_prefix, urllib.parse.quote(ref).replace("%3A", "%253A"))
elif req_type is self.Request.GRUB:
data = None # Must be disabled for token-based security.
url = "{}/{}{}".format(self._main_url, req_type.value, ref)
elif req_type in (self.Request.REMOTE,
self.Request.POWER,
self.Request.VOL,
self.Request.EPG,
self.Request.TIMER):
url += ref
def done_callback(f):
callback(f.result())
future = self._executor.submit(get_response, req_type, url, data)
future.add_done_callback(done_callback)
@run_task
def init(self):
user, password = self._settings.http_user, self._settings.http_password
use_ssl = self._settings.http_use_ssl
self._main_url = "http{}://{}:{}".format("s" if use_ssl else "", self._settings.host, self._settings.http_port)
self._base_url = "{}/web/".format(self._main_url)
init_auth(user, password, self._main_url, use_ssl)
url = "{}/web/{}".format(self._main_url, self.Request.TOKEN.value)
s_id = get_session_id(user, password, url)
if s_id != "0":
self._data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
self.send(self.Request.INFO, None, self.init_callback)
def init_callback(self, info):
if info:
version = info.get("e2webifversion", "").upper()
self._is_owif = "OWIF" in version
version_info = "Web Interface version: {}".format(version) if version else ""
log("HTTP API initialized... {}".format(version_info))
@property
def is_owif(self):
""" Returns true if the web interface is OpenWebif. """
return self._is_owif
@run_task
def close(self):
self._shutdown = True
self._executor.shutdown()
def get_response(req_type, url, data=None):
try:
with urlopen(Request(url, data=data), timeout=10) as f:
if req_type is HttpAPI.Request.STREAM or req_type is HttpAPI.Request.STREAM_CURRENT:
return {"m3u": f.read().decode("utf-8")}
elif req_type is HttpAPI.Request.GRUB:
return {"img_data": f.read()}
elif req_type is HttpAPI.Request.CURRENT:
for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2event"):
return {el.tag: el.text for el in el.iter()} # return first[current] event from the list
elif req_type is HttpAPI.Request.PLAYER_LIST:
return [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2file")]
elif req_type is HttpAPI.Request.EPG:
return {"event_list": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2event")]}
elif req_type is HttpAPI.Request.TIMER_LIST:
return {"timer_list": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2timer")]}
else:
return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()}
except HTTPError as e:
if req_type is HttpAPI.Request.TEST:
raise e
return {"error_code": e.code}
except (URLError, RemoteDisconnected, ConnectionResetError) as e:
if req_type is HttpAPI.Request.TEST:
raise e
except ETree.ParseError as e:
log("Parsing response error: {}".format(e))
return {"error_code": -1}
def init_auth(user, password, url, use_ssl=False):
""" Init authentication """
pass_mgr = HTTPPasswordMgrWithDefaultRealm()
pass_mgr.add_password(None, url, user, password)
auth_handler = HTTPBasicAuthHandler(pass_mgr)
if use_ssl:
import ssl
from urllib.request import HTTPSHandler
opener = build_opener(auth_handler, HTTPSHandler(context=ssl._create_unverified_context()))
else:
opener = build_opener(auth_handler)
install_opener(opener)
def get_session_id(user, password, url):
data = urllib.parse.urlencode(dict(user=user, password=password)).encode("utf-8")
return get_response(HttpAPI.Request.TOKEN, url, data=data).get("e2sessionid", "0")
def get_post_data(base_url, password, user):
s_id = get_session_id(user, password, "{}/web/{}".format(base_url, HttpAPI.Request.TOKEN.value))
data = None
if s_id != "0":
data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
return data
# ***************** Connections testing *******************#
def test_ftp(host, port, user, password, timeout=5):
try:
with FTP(host=host, user=user, passwd=password, timeout=timeout) as ftp:
return ftp.getwelcome()
except (error_perm, ConnectionRefusedError, OSError) as e:
raise TestException(e)
def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message=False):
params = urlencode({"text": "Connection test", "type": 2, "timeout": timeout})
params = "statusinfo" if skip_message else "message?{}".format(params)
base_url = "http{}://{}:{}".format("s" if use_ssl else "", host, port)
# authentication
init_auth(user, password, base_url, use_ssl)
data = get_post_data(base_url, password, user)
try:
return get_response(HttpAPI.Request.TEST, "{}/web/{}".format(base_url, params), data).get("e2statetext", "")
except (RemoteDisconnected, URLError, HTTPError) as e:
raise TestException(e)
def test_telnet(host, port, user, password, timeout=5):
try:
gen = telnet_test(host, port, user, password, timeout)
res = next(gen)
msg = str(res, encoding="utf8").strip()
log(msg)
next(gen)
if re.search("password", msg, re.IGNORECASE):
raise TestException(msg)
return msg
except (socket.timeout, OSError) as e:
raise TestException(e)
def telnet_test(host, port, user, password, timeout):
tn = Telnet(host=host, port=port, timeout=timeout)
time.sleep(1)
tn.read_until(b"login: ", timeout=2)
tn.write(user.encode("utf-8") + b"\r")
time.sleep(timeout)
tn.read_until(b"Password: ", timeout=2)
tn.write(password.encode("utf-8") + b"\r")
time.sleep(timeout)
yield tn.read_very_eager()
tn.close()
yield
if __name__ == "__main__":
pass

View File

@@ -1,6 +1,6 @@
from app.commons import run_task
from app.properties import Profile
from .ecommons import Service, Satellite, Transponder, Bouquet, Bouquets
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.lamedb import get_services as get_enigma_services, write_services as write_enigma_services
@@ -10,33 +10,33 @@ from .neutrino.services import get_services as get_neutrino_services, write_serv
from .satxml import get_satellites, write_satellites
def get_services(data_path, profile):
if profile is Profile.ENIGMA_2:
return get_enigma_services(data_path)
elif profile is Profile.NEUTRINO_MP:
def get_services(data_path, s_type, format_version):
if s_type is SettingsType.ENIGMA_2:
return get_enigma_services(data_path, format_version)
elif s_type is SettingsType.NEUTRINO_MP:
return get_neutrino_services(data_path)
@run_task
def write_services(path, channels, profile):
if profile is Profile.ENIGMA_2:
write_enigma_services(path, channels)
elif profile is Profile.NEUTRINO_MP:
def write_services(path, channels, s_type, format_version):
if s_type is SettingsType.ENIGMA_2:
write_enigma_services(path, channels, format_version)
elif s_type is SettingsType.NEUTRINO_MP:
write_neutrino_services(path, channels)
def get_bouquets(path, profile):
if profile is Profile.ENIGMA_2:
def get_bouquets(path, s_type):
if s_type is SettingsType.ENIGMA_2:
return get_enigma_bouquets(path)
elif profile is Profile.NEUTRINO_MP:
elif s_type is SettingsType.NEUTRINO_MP:
return get_neutrino_bouquets(path)
@run_task
def write_bouquets(path, bouquets, profile):
if profile is Profile.ENIGMA_2:
write_enigma_bouquets(path, bouquets)
elif profile is Profile.NEUTRINO_MP:
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)
elif s_type is SettingsType.NEUTRINO_MP:
write_neutrino_bouquets(path, bouquets)

View File

@@ -13,6 +13,7 @@ class BqServiceType(Enum):
DEFAULT = "DEFAULT"
IPTV = "IPTV"
MARKER = "MARKER" # 64
SPACE = "SPACE" # 832 [hidden marker]
Bouquet = namedtuple("Bouquet", ["name", "type", "services", "locked", "hidden"])
@@ -27,15 +28,29 @@ Transponder = namedtuple("Transponder", ["frequency", "symbol_rate", "polarizati
"system", "modulation", "pls_mode", "pls_code", "is_id"])
class Type(Enum):
""" Types of DVB transponders """
class TrType(Enum):
""" Transponders type """
Satellite = "s"
Terestrial = "t"
Terrestrial = "t"
Cable = "c"
class FLAG(Enum):
""" Service flags """
class BqType(Enum):
""" Bouquet type"""
BOUQUET = "bouquet"
TV = "tv"
RADIO = "radio"
WEBTV = "webtv"
class Flag(Enum):
""" Service flags
K - last bit (1)
H - second from end (10)
P - third (100)
N - sixth (100000)
"""
KEEP = 1 # Do not automatically update the services parameters.
HIDE = 2
PIDS = 4 # Always use the cached instead of current pids.
@@ -43,9 +58,54 @@ class FLAG(Enum):
NEW = 40 # Marked as new at the last scan
@staticmethod
def hide_values():
return 2, 3, 6, 7, 10, 42, 43, 46, 47
def is_hide(value: int):
return value & 1 << 1
@staticmethod
def is_keep(value: int):
return value & 1 << 0
@staticmethod
def is_pids(value: int):
return value & 1 << 2
@staticmethod
def is_new(value: int):
return value & 1 << 5
class Pids(Enum):
VIDEO = "c:00"
AUDIO = "c:01"
TELETEXT = "c:02"
PCR = "c:03"
AC3 = "c:04"
VIDEO_TYPE = "c:05"
AUDIO_CHANNEL = "c:06"
BIT_STREAM_DELAY = "c:07" # in ms
PCM_DELAY = "c:08" # in ms
SUBTITLE = "c:09"
class Inversion(Enum):
Off = "0"
On = "1"
Auto = "2"
class Pilot(Enum):
Off = "0"
On = "1"
Auto = "2"
class SystemCable(Enum):
""" System of cable service """
ANNEX_A = "0"
ANNEX_C = "1"
ROLL_OFF = {"0": "35%", "1": "25%", "2": "20%", "3": "Auto"}
POLARIZATION = {"0": "H", "1": "V", "2": "L", "3": "R"}
@@ -56,18 +116,77 @@ FEC = {"0": "Auto", "1": "1/2", "2": "2/3", "3": "3/4", "4": "5/6", "5": "7/8",
"17": "4/5", "18": "9/10", "19": "1/2", "20": "2/3", "21": "3/4", "22": "5/6", "23": "7/8", "24": "8/9",
"25": "3/5", "26": "4/5", "27": "9/10", "28": "Auto"}
FEC_DEFAULT = {"0": "Auto", "1": "1/2", "2": "2/3", "3": "3/4", "4": "5/6", "5": "7/8", "6": "8/9", "7": "3/5",
"8": "4/5", "9": "9/10", "10": "6/7", "15": "None"}
SYSTEM = {"0": "DVB-S", "1": "DVB-S2"}
MODULATION = {"0": "Auto", "1": "QPSK", "2": "8PSK", "3": "16APSK", "5": "32APSK"}
MODULATION = {"0": "Auto", "1": "QPSK", "2": "8PSK", "4": "16APSK", "5": "32APSK"}
SERVICE_TYPE = {"-2": "Unknown", "1": "TV", "2": "Radio", "3": "Data",
"10": "Radio", "12": "Data", "22": "TV", "25": "TV (HD)", "31": "TV (UHD)",
"136": "Data", "139": "Data"}
SERVICE_TYPE = {"-2": "Data", "1": "TV", "2": "Radio", "3": "Data", "10": "Radio", "22": "TV (H264)",
"25": "TV (HD)", "31": "TV (UHD)"}
CAS = {"C:2600": "BISS", "C:0b00": "Conax", "C:0b01": "Conax", "C:0b02": "Conax", "C:0baa": "Conax", "C:0602": "Irdeto",
"C:0604": "Irdeto", "C:0606": "Irdeto", "C:0608": "Irdeto", "C:0622": "Irdeto", "C:0626": "Irdeto",
"C:0664": "Irdeto", "C:0614": "Irdeto", "C:0692": "Irdeto", "C:1801": "Nagravision", "C:0500": "Viaccess",
"C:0E00": "PowerVu", "C:4ae0": "DRE-Crypt", "C:4ae1": "DRE-Crypt", "C:7be1": "DRE-Crypt"}
# Terrestrial
BANDWIDTH = {"0": "8MHz", "1": "7MHz", "2": "6MHz", "3": "Auto", "4": "5MHz", "5": "1/712MHz", "6": "10MHz"}
T_MODULATION = {"0": "QPSK", "1": "QAM16", "2": "QAM64", "3": "Auto", "4": "QAM256"}
TRANSMISSION_MODE = {"0": "2k", "1": "8k", "2": "Auto", "3": "4k", "4": "1k", "5": "16k", "6": "32k"}
GUARD_INTERVAL = {"0": "1/32", "1": "1/16", "2": "1/8", "3": "1/4", "4": "Auto", "5": "1/128", "6": "19/128",
"7": "19/256"}
HIERARCHY = {"0": "None", "1": "1", "2": "2", "3": "4", "4": "Auto"}
T_FEC = {"0": "1/2", "1": "2/3", "2": "3/4", "3": "5/6", "4": "7/8", "5": "Auto", "6": "6/7", "7": "8/9"}
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"}
# 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"}
# 'on' attribute 0070(hex) = 112(int) = ONID(ONID-TID on www.lyngsat.com)
PROVIDER = {112: "HTB+", 253: "Tricolor TV"}
# ************* subsidiary functions ****************
def get_key_by_value(dc: dict, value):
""" Returns key from dict by value """
for k, v in dc.items():
if v == value:
return k
def get_value_by_name(en, name):
""" Returns value by name from enums """
for n in en:
if n.name == name:
return n.value
def is_transponder_valid(tr: Transponder):
""" Checks transponder validity """
try:
int(tr.frequency)
int(tr.symbol_rate)
tr.pls_mode is None or int(tr.pls_mode)
tr.pls_code is None or int(tr.pls_code)
tr.is_id is None or int(tr.is_id)
except TypeError:
return False
if tr.polarization not in POLARIZATION.values():
return False
if tr.fec_inner not in FEC.values():
return False
if tr.system not in SYSTEM.values():
return False
if tr.modulation not in MODULATION.values():
return False
return True

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,91 +1,159 @@
""" Module for parsing bouquets """
from app.eparser.ecommons import BqServiceType, BouquetService, Bouquets, Bouquet
""" Module for working with Enigma2 bouquets. """
import re
from collections import Counter
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"
_DEFAULT_BOUQUET_NAME = "favourites"
def get_bouquets(path):
return parse_bouquets(path, "bouquets.tv", "tv"), parse_bouquets(path, "bouquets.radio", "radio")
return parse_bouquets(path, "bouquets.tv", BqType.TV.value), parse_bouquets(path, "bouquets.radio",
BqType.RADIO.value)
def write_bouquets(path, bouquets):
srv_line = '#SERVICE 1:7:1:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
def write_bouquets(path, bouquets, force_bq_names=False):
""" Creating and writing bouquets 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]
for bqs in bouquets:
line.clear()
line.append("#NAME {}\n".format(bqs.name))
for bq in bqs.bouquets:
line.append(srv_line.format(bq.name.replace(" ", "_"), bq.type))
write_bouquet(path, bq.name, bq.type, bq.services)
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
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)
with open(path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file:
file.writelines(line)
def write_bouquet(path, name, bq_type, channels):
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"
for ch in channels:
if not ch: # if was duplicate
continue
for srv in services:
s_type = srv.service_type
if ch.service_type == BqServiceType.IPTV.name or ch.service_type == BqServiceType.MARKER.name:
bouquet.append("#SERVICE {}\n".format(ch.fav_id.strip()))
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:
bouquet.append("#SERVICE {}\n".format(to_bouquet_id(ch)))
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 + "userbouquet.{}.{}".format(name.replace(" ", "_"), bq_type), "w", encoding="utf-8") as file:
with open(path, "w", encoding="utf-8") as file:
file.writelines(bouquet)
def to_bouquet_id(ch):
""" Creates bouquet channel id """
data_type = ch.data_id
def to_bouquet_id(srv):
""" Creates bouquet channel id. """
data_type = srv.data_id
if data_type and len(data_type) > 4:
data_type = int(ch.data_id.split(":")[4])
data_type = int(srv.data_id.split(":")[4])
return "{}:0:{:X}:{}:0:0:0:".format(1, data_type, ch.fav_id)
return "{}:0:{:X}:{}:0:0:0:".format(1, data_type, srv.fav_id)
def get_bouquet(path, name, bq_type):
""" Parsing services ids from bouquet file """
with open(path + "userbouquet.{}.{}".format(name, bq_type), encoding="utf-8") as file:
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 ['']
for ch in srvs[1:]:
ch_data = ch.strip().split(":")
if ch_data[1] == "64":
services.append(BouquetService(ch_data[-1].split("\n")[0], BqServiceType.MARKER, ch, ch_data[2]))
elif "http" in ch:
services.append(BouquetService(ch_data[-1].split("\n")[0], BqServiceType.IPTV, ch, 0))
else:
fav_id = "{}:{}:{}:{}".format(ch_data[3], ch_data[4], ch_data[5], ch_data[6])
services.append(BouquetService(None, BqServiceType.DEFAULT, fav_id, 0))
# 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
return srvs[0].strip("#NAME").strip(), 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) as file:
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:
b_name, services = get_bouquet(path, line.split(".")[1], bq_type)
bouquets[2].append(Bouquet(name=b_name,
type=bq_type,
services=services,
locked=None,
hidden=None))
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

View File

@@ -1,28 +1,40 @@
""" This module used for parsing lamedb file
""" This module used for parsing and write lamedb file """
import re
Currently implemented only for satellite channels!!!
Description of format taken from here: http://www.satsupreme.com/showthread.php/194074-Lamedb-format-explained
"""
from app.commons import log
from app.ui import CODED_ICON, LOCKED_ICON, HIDE_ICON
from app.ui.uicommons import CODED_ICON, LOCKED_ICON, HIDE_ICON
from .blacklist import get_blacklist
from ..ecommons import Service, POLARIZATION, SYSTEM, FEC, SERVICE_TYPE, FLAG
from ..ecommons import Service, POLARIZATION, FEC, SERVICE_TYPE, Flag, T_FEC, TrType, FEC_DEFAULT, T_SYSTEM
_HEADER = "eDVB services /4/"
_HEADER = "eDVB services /{}/"
_SEP = ":" # separator
_FILE_NAME = "lamedb"
_END_LINE = "# File was created in DemonEditor.\n# ....Enjoy watching!....\n"
def get_services(path):
return parse(path)
def get_services(path, format_version):
return parse(path, format_version)
def write_services(path, services):
lines = [_HEADER, "\ntransponders\n"]
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)
def write_to_lamedb(path, services):
""" Writing lamedb file ver.4 """
with open(path + _FILE_NAME, "w") as file:
file.writelines(get_services_lines(services))
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])
@@ -36,33 +48,128 @@ def write_services(path, services):
tr_lines.sort()
lines.extend(tr_lines)
lines.extend(services_lines)
lines.append("end\nFile was created in DemonEditor.\n....Enjoy watching!....\n")
lines.append("end\n" + _END_LINE)
with open(path + _FILE_NAME, "w") as file:
return lines
def write_to_lamedb5(path, services):
""" Writing lamedb5 file. """
lines = [_HEADER.format(5) + "\n"]
services_lines = []
tr_set = set()
for srv in services:
data_id = str(srv.data_id).split(_SEP)
tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3])
tr_set.add("t:{},{}\n".format(tr_id, srv.transponder.replace(" ", ":", 1)))
# Removing empty packages
flags = list(filter(lambda x: x != "p:", srv.flags_cas.split(",")))
flags = ",".join(flags)
flags = "," + flags if flags else ""
services_lines.append("s:{},\"{}\"{}\n".format(srv.data_id, srv.service, flags))
lines.extend(sorted(tr_set))
lines.extend(services_lines)
lines.append(_END_LINE)
with open(path + "lamedb5", "w") as file:
file.writelines(lines)
def parse(path):
""" Parsing lamedb """
def parse(path, version=4):
""" Parsing lamedb. """
if version == 4:
return parse_v4(path)
elif version == 5:
return parse_v5(path)
raise SyntaxError("Unsupported version of the format.")
def parse_v3(services, transponders, path):
""" Parsing version 3. """
for t in transponders:
tr = transponders[t].lower()
tr_type = tr[0:1]
if tr_type == "c":
tr += ":0:0:0"
elif tr_type == "t":
tr += ":0:0"
else:
tr_data = tr.split(_SEP)
len_data = len(tr_data)
if len_data == 6:
tr_data.append("0")
elif len_data == 9:
tr_data.insert(6, "0")
tr_data.append("0")
tr_data.append("2")
tr = _SEP.join(tr_data)
transponders[t] = tr
return parse_services(services, transponders, path)
def parse_v4(path):
""" Parsing version 4. """
with open(path + _FILE_NAME, "r", encoding="utf-8", errors="replace") as file:
try:
data = str(file.read())
except UnicodeDecodeError as e:
log("lamedb parse error: " + str(e))
else:
transponders, sep, services = data.partition("transponders") # 1 step
if not transponders.endswith("/4/\n"):
msg = "lamedb parsing error: unsupported format.\n Only version 4 is supported!"
log(msg)
raise SyntaxError(msg)
transponders, sep, services = services.partition("services") # 2 step
services, sep, _ = services.partition("\nend") # 3 step
return get_services_list(data, path)
return parse_services(services.split("\n"), transponders.split("/"), path)
def get_services_list(data, path=None):
""" Returns a list of services from a string data representation. """
transponders, sep, services = data.partition("transponders") # 1 step
pattern = re.compile("/[34]/$")
match = re.search(pattern, transponders)
if not match:
msg = "lamedb parsing error: unsupported format."
log(msg)
raise SyntaxError(msg)
transponders, sep, services = services.partition("services") # 2 step
services, sep, _ = services.partition("\nend") # 3 step
if match.group() == "/3/":
return parse_v3(services.split("\n"), parse_transponders(transponders.split("/")), path)
return parse_services(services.split("\n"), parse_transponders(transponders.split("/")), path)
def parse_v5(path):
""" Parsing version 5. """
with open(path + "lamedb5", "r", encoding="utf-8", errors="replace") as file:
lns = file.readlines()
if lns and not lns[0].endswith("/5/\n"):
raise SyntaxError("lamedb v.5 parsing error: unsupported format.")
trs, srvs = {}, [""]
for line in lns:
if line.startswith("s:"):
srv_data = line.strip("s:").split(",", 2)
srv_data[1] = srv_data[1].strip("\"")
data_len = len(srv_data)
if data_len == 3:
srv_data[2] = srv_data[2].strip()
elif data_len == 2:
srv_data.append("p:")
srvs.extend(srv_data)
elif line.startswith("t:"):
tr, srv = line.split(",")
trs[tr.strip("t:")] = srv.strip().replace(":", " ", 1)
return parse_services(srvs, trs, path)
def parse_transponders(arg):
""" Parsing transponders """
""" Parsing transponders. """
transponders = {}
for ar in arg:
tr = ar.replace("\n", "").split("\t")
@@ -73,20 +180,34 @@ def parse_transponders(arg):
def parse_services(services, transponders, path):
""" Parsing channels """
channels = []
transponders = parse_transponders(transponders)
blacklist = str(get_blacklist(path))
""" Parsing services. """
services_list = []
blacklist = get_blacklist(path) if path else {}
srvs = split(services, 3)
if srvs[0][0] == "": # remove first empty element
srvs.remove(srvs[0])
srv = split(services, 3)
if srv[0][0] == "": # remove first empty element
srv.remove(srv[0])
for ch in srv:
data = str(ch[0]).split(_SEP)
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)
@@ -96,44 +217,65 @@ def parse_services(services, transponders, path):
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_{}_{}_{}_{}_{}_0_0_0.png".format(1, 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 = ch[2].split(",")
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 int(flags[0][2:]) in FLAG.hide_values() else None
locked = LOCKED_ICON if fav_id in blacklist else None
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 None
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 ch[1] if c.isprintable())
channels.append(Service(flags_cas=ch[2],
transponder_type=tr_type,
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=POLARIZATION[tr[2]],
fec=FEC[tr[3]],
system=SYSTEM[tr[6]],
pos="{}.{}".format(tr[4][:-1], tr[4][-1:]),
data_id=ch[0],
fav_id=fav_id,
transponder=transponder))
return channels
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):

View File

@@ -1,24 +1,85 @@
""" Module for IPTV and streams support """
import re
from enum import Enum
from urllib.parse import unquote, quote
from app.settings import SettingsType
from app.ui.uicommons import IPTV_ICON
from .ecommons import BqServiceType, Service
# url, description, urlkey, account, usrname, psw, s_type, iconsrc, iconsrc_b, group
NEUTRINO_FAV_ID_FORMAT = "{}::{}::{}::{}::{}::{}::{}::{}::{}::{}"
ENIGMA2_FAV_ID_FORMAT = " {}:0:{}:{:X}:{:X}:{:X}:{:X}:0:0:0:{}:{}\n#DESCRIPTION: {}\n"
MARKER_FORMAT = " 1:64:{}:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n"
def parse_m3u(path):
class StreamType(Enum):
DVB_TS = "1"
NONE_TS = "4097"
NONE_REC_1 = "5001"
NONE_REC_2 = "5002"
E_SERVICE_URI = "8193"
def parse_m3u(path, s_type):
with open(path) as file:
aggr = [None] * 10
channels = []
count = 0
services = []
groups = set()
counter = 0
name = None
for line in file.readlines():
if line.startswith("#EXTINF"):
name = line[1 + line.index(","):].strip()
count += 1
elif count == 1:
count = 0
fav_id = " 1:0:1:0:0:0:0:0:0:0:{}:{}\n#DESCRIPTION: {}\n".format(
line.strip().replace(":", "%3a"), name, name, None)
srv = Service(*aggr[0:3], name, *aggr[0:3], BqServiceType.IPTV.name, *aggr, fav_id, None)
channels.append(srv)
elif line.startswith("#EXTGRP") and s_type is SettingsType.ENIGMA_2:
grp_name = line.strip("#EXTGRP:").strip()
if grp_name not in groups:
groups.add(grp_name)
fav_id = MARKER_FORMAT.format(counter, grp_name, grp_name)
counter += 1
mr = Service(None, None, None, grp_name, *aggr[0:3], BqServiceType.MARKER.name, *aggr, fav_id, None)
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)
services.append(srv)
return channels
return services
def export_to_m3u(path, bouquet, s_type):
pattern = re.compile(".*:(http.*):.*") if s_type is SettingsType.ENIGMA_2 else re.compile("(http.*?)::::.*")
lines = ["#EXTM3U\n"]
current_grp = None
for s in bouquet.services:
s_type = s.type
if s_type is BqServiceType.IPTV:
res = re.match(pattern, s.data)
if not res:
continue
data = res.group(1)
lines.append("#EXTINF:-1,{}\n".format(s.name))
if current_grp:
lines.append(current_grp)
lines.append("{}\n".format(unquote(data.strip())))
elif s_type is BqServiceType.MARKER:
current_grp = "#EXTGRP:{}\n".format(s.name)
with open(path + "{}.m3u".format(bouquet.name), "w", encoding="utf-8") as file:
file.writelines(lines)
def get_fav_id(url, service_name, s_type):
""" 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:
return NEUTRINO_FAV_ID_FORMAT.format(url, "", 0, None, None, None, None, "", "", 1)
if __name__ == "__main__":

View File

@@ -1,23 +1,21 @@
import os
from contextlib import suppress
from enum import Enum
from xml.dom.minidom import parse, Document
from app.ui import LOCKED_ICON, HIDE_ICON
from ..ecommons import Bouquets, Bouquet, BouquetService, BqServiceType, PROVIDER
from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT
from app.ui.uicommons import LOCKED_ICON, HIDE_ICON
from ..ecommons import Bouquets, Bouquet, BouquetService, BqServiceType, PROVIDER, BqType
_FILE = "bouquets.xml"
_U_FILE = "ubouquets.xml"
_W_FILE = "webtv.xml"
class BqType(Enum):
BOUQUET = "bouquet"
TV = "tv"
_COMMENT = " File was created in DemonEditor. Enjoy watching! "
def get_bouquets(path):
return (parse_bouquets(path + _FILE, "Providers", BqType.BOUQUET.value),
parse_bouquets(path + _U_FILE, "FAV", BqType.TV.value))
parse_bouquets(path + _U_FILE, "FAV", BqType.TV.value),
parse_webtv(path + _W_FILE, "WEBTV", BqType.WEBTV.value))
def parse_bouquets(file, name, bq_type):
@@ -52,31 +50,70 @@ def parse_bouquets(file, name, bq_type):
if BqType(bq_type) is BqType.BOUQUET:
for bq in bouquets.bouquets:
if bq.services:
name = bq.name
name = name[name.index("]") + 1:]
key = int(bq.services[0].data.split(":")[1], 16)
if key not in PROVIDER:
pos, sep, name = bq.name.partition("]")
PROVIDER[key] = name
return bouquets
def write_bouquets(path, bouquets):
if len(bouquets) < 2:
for f in path + _FILE, path + _U_FILE:
with suppress(FileNotFoundError):
os.remove(f)
def parse_webtv(path, name, bq_type):
bouquets = Bouquets(name=name, type=bq_type, bouquets=[])
if not os.path.exists(path):
return bouquets
dom = parse(path)
services = []
for elem in dom.getElementsByTagName("webtv"):
if elem.hasAttributes():
title = elem.attributes["title"].value
url = elem.attributes["url"].value
description = elem.attributes.get("description")
description = description.value if description else description
urlkey = elem.attributes.get("urlkey", None)
urlkey = urlkey.value if urlkey else urlkey
account = elem.attributes.get("account", None)
account = account.value if account else account
usrname = elem.attributes.get("usrname", None)
usrname = usrname.value if usrname else usrname
psw = elem.attributes.get("psw", None)
psw = psw.value if psw else psw
s_type = elem.attributes.get("type", None)
s_type = s_type.value if s_type else s_type
iconsrc = elem.attributes.get("iconsrc", None)
iconsrc = iconsrc.value if iconsrc else iconsrc
iconsrc_b = elem.attributes.get("iconsrc_b", None)
iconsrc_b = iconsrc_b.value if iconsrc_b else iconsrc_b
group = elem.attributes.get("group", None)
group = group.value if group else group
fav_id = NEUTRINO_FAV_ID_FORMAT.format(url, description, urlkey, account, usrname, psw, s_type, iconsrc,
iconsrc_b, group)
srv = BouquetService(name=title,
type=BqServiceType.IPTV,
data=fav_id,
num=0)
services.append(srv)
bouquet = Bouquet(name="default", type=bq_type, services=services, locked=None, hidden=None)
bouquets[2].append(bouquet)
return bouquets
def write_bouquets(path, bouquets):
for bq in bouquets:
bq_type = BqType(bq.type)
write_bouquet(path + (_FILE if bq_type is BqType.BOUQUET else _U_FILE), bq)
if bq_type is BqType.WEBTV:
write_webtv(path + _W_FILE, bq)
else:
write_bouquet(path + (_FILE if bq_type is BqType.BOUQUET else _U_FILE), bq)
def write_bouquet(file, bouquet):
doc = Document()
root = doc.createElement("zapit")
doc.appendChild(root)
comment = doc.createComment(" File was created in DemonEditor. Enjoy watching! ")
comment = doc.createComment(_COMMENT)
doc.appendChild(comment)
for bq in bouquet.bouquets:
@@ -102,5 +139,43 @@ def write_bouquet(file, bouquet):
doc.writexml(open(file, "w"), addindent=" ", newl="\n", encoding="UTF-8")
def write_webtv(file, bouquet):
doc = Document()
root = doc.createElement("webtvs")
doc.appendChild(root)
comment = doc.createComment(_COMMENT)
doc.appendChild(comment)
for bq in bouquet.bouquets:
for srv in bq.services:
url, description, urlkey, account, usrname, psw, s_type, iconsrc, iconsrc_b, group = srv.fav_id.split("::")
srv_elem = doc.createElement("webtv")
srv_elem.setAttribute("title", srv.service)
srv_elem.setAttribute("url", url)
if description != "None":
srv_elem.setAttribute("description", description)
if urlkey != "None":
srv_elem.setAttribute("urlkey", urlkey)
if account != "None":
srv_elem.setAttribute("account", account)
if usrname != "None":
srv_elem.setAttribute("usrname", usrname)
if psw != "None":
srv_elem.setAttribute("psw", psw)
if s_type != "None":
srv_elem.setAttribute("type", s_type)
if iconsrc != "None":
srv_elem.setAttribute("iconsrc", iconsrc)
if iconsrc_b != "None":
srv_elem.setAttribute("iconsrc_b", iconsrc_b)
if group != "None":
srv_elem.setAttribute("group", group)
root.appendChild(srv_elem)
doc.writexml(open(file, "w"), addindent=" ", newl="\n", encoding="UTF-8")
if __name__ == "__main__":
pass

View File

@@ -4,7 +4,8 @@
"""
from xml.dom.minidom import parse, Document
from .ecommons import POLARIZATION, FEC, SYSTEM, MODULATION, PLS_MODE, Transponder, Satellite
from app.commons import log
from .ecommons import POLARIZATION, FEC, SYSTEM, MODULATION, Transponder, Satellite, get_key_by_value
__COMMENT = (" File was created in DemonEditor\n\n"
"usable flags are\n"
@@ -18,7 +19,7 @@ __COMMENT = (" File was created in DemonEditor\n\n"
"polarization: 0 - Horizontal, 1 - Vertical, 2 - Left Circular, 3 - Right Circular\n"
"fec_inner: 0 - Auto, 1 - 1/2, 2 - 2/3, 3 - 3/4, 4 - 5/6, 5 - 7/8, 6 - 8/9, 7 - 3/5,\n"
"8 - 4/5, 9 - 9/10, 15 - None\n"
"modulation: 0 - Auto, 1 - QPSK, 2 - 8PSK, 3 - 16APSK, 5 - 32APSK\n"
"modulation: 0 - Auto, 1 - QPSK, 2 - 8PSK, 4 - 16APSK, 5 - 32APSK\n"
"rolloff: 0 - 0.35, 1 - 0.25, 2 - 0.20, 3 - Auto\n"
"pilot: 0 - Off, 1 - On, 2 - Auto\n"
"inversion: 0 = Off, 1 = On, 2 = Auto (default)\n"
@@ -56,7 +57,7 @@ def write_satellites(satellites, data_path):
transponder_child.setAttribute("system", get_key_by_value(SYSTEM, tr.system))
transponder_child.setAttribute("modulation", get_key_by_value(MODULATION, tr.modulation))
if tr.pls_mode:
transponder_child.setAttribute("pls_mode", get_key_by_value(PLS_MODE, tr.pls_mode))
transponder_child.setAttribute("pls_mode", tr.pls_mode)
if tr.pls_code:
transponder_child.setAttribute("pls_code", tr.pls_code)
if tr.is_id:
@@ -71,31 +72,38 @@ def write_satellites(satellites, data_path):
doc.unlink()
def parse_transponders(elem):
def parse_transponders(elem, sat_name):
""" Parsing satellite transponders """
transponders = []
for el in elem.getElementsByTagName("transponder"):
if el.hasAttributes():
atr = el.attributes
tr = Transponder(atr["frequency"].value,
atr["symbol_rate"].value,
POLARIZATION[atr["polarization"].value],
FEC[atr["fec_inner"].value],
SYSTEM[atr["system"].value],
MODULATION[atr["modulation"].value],
PLS_MODE[atr["pls_mode"].value] if "pls_mode" in atr else None,
atr["pls_code"].value if "pls_code" in atr else None,
atr["is_id"].value if "is_id" in atr else None)
transponders.append(tr)
try:
tr = Transponder(atr["frequency"].value,
atr["symbol_rate"].value,
POLARIZATION[atr["polarization"].value],
FEC[atr["fec_inner"].value],
SYSTEM[atr["system"].value],
MODULATION[atr["modulation"].value],
atr["pls_mode"].value if "pls_mode" in atr else None,
atr["pls_code"].value if "pls_code" in atr else None,
atr["is_id"].value if "is_id" in atr else None)
except Exception as e:
message = "Error: can't parse transponder for '{}' satellite! {}".format(sat_name, repr(e))
print(message)
log(message)
else:
transponders.append(tr)
return transponders
def parse_sat(elem):
""" Parsing satellite """
return Satellite(elem.attributes["name"].value,
sat_name = elem.attributes["name"].value
return Satellite(sat_name,
elem.attributes["flags"].value,
elem.attributes["position"].value,
parse_transponders(elem))
parse_transponders(elem, sat_name))
def parse_satellites(path):
@@ -110,11 +118,5 @@ def parse_satellites(path):
return satellites
def get_key_by_value(dictionary, value):
for k, v in dictionary.items():
if v == value:
return k
if __name__ == "__main__":
pass

View File

@@ -1,159 +0,0 @@
import os
import socket
import time
from enum import Enum
from ftplib import FTP, error_perm
from telnetlib import Telnet
from app.commons import log
from app.properties import Profile
__DATA_FILES_LIST = ("tv", "radio", "lamedb", "blacklist", "whitelist", # enigma 2
"services.xml", "myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino
class DownloadDataType(Enum):
ALL = 0
BOUQUETS = 1
SATELLITES = 2
PICONS = 3
def download_data(*, properties, download_type=DownloadDataType.ALL, callback=None):
with FTP(host=properties["host"]) as ftp:
ftp.login(user=properties["user"], passwd=properties["password"])
ftp.encoding = "utf-8"
save_path = properties["data_dir_path"]
os.makedirs(os.path.dirname(save_path), exist_ok=True)
files = []
# bouquets section
if download_type is DownloadDataType.ALL or download_type is DownloadDataType.BOUQUETS:
ftp.cwd(properties["services_path"])
ftp.dir(files.append)
for file in files:
name = str(file).strip()
if name.endswith(__DATA_FILES_LIST):
name = name.split()[-1]
with open(save_path + name, "wb") as f:
ftp.retrbinary("RETR " + name, f.write)
# satellites.xml section
if download_type is DownloadDataType.ALL or download_type is DownloadDataType.SATELLITES:
ftp.cwd(properties["satellites_xml_path"])
files.clear()
ftp.dir(files.append)
for file in files:
name = str(file).strip()
xml_file = "satellites.xml"
if name.endswith(xml_file):
with open(save_path + xml_file, 'wb') as f:
ftp.retrbinary("RETR " + xml_file, f.write)
if callback is not None:
callback()
def upload_data(*, properties, download_type=DownloadDataType.ALL, remove_unused=False, profile=Profile.ENIGMA_2,
callback=None):
data_path = properties["data_dir_path"]
host = properties["host"]
# telnet
tn = telnet(host=host, user=properties.get("telnet_user", "root"), password=properties.get("telnet_password", ""),
timeout=properties.get("telnet_timeout", 5))
next(tn)
# terminate enigma or enigma
tn.send("init 4")
with FTP(host=host) as ftp:
ftp.login(user=properties["user"], passwd=properties["password"])
ftp.encoding = "utf-8"
if download_type is DownloadDataType.ALL or download_type is DownloadDataType.SATELLITES:
ftp.cwd(properties["satellites_xml_path"])
file_name = "satellites.xml"
send = send_file(file_name, data_path, ftp)
if download_type == DownloadDataType.SATELLITES:
return send
if download_type is DownloadDataType.ALL or download_type is DownloadDataType.BOUQUETS:
ftp.cwd(properties["services_path"])
if remove_unused:
files = []
ftp.dir(files.append)
for file in files:
name = str(file).strip()
if name.endswith(__DATA_FILES_LIST):
name = name.split()[-1]
ftp.delete(name)
for file_name in os.listdir(data_path):
if file_name == "satellites.xml":
continue
if file_name.endswith(__DATA_FILES_LIST):
send_file(file_name, data_path, ftp)
if download_type is DownloadDataType.PICONS:
picons_dir_path = properties.get("picons_dir_path")
picons_path = properties.get("picons_path")
try:
ftp.cwd(picons_path)
except error_perm as e:
if str(e).startswith("550"):
ftp.mkd(picons_path) # if not exist
ftp.cwd(picons_path)
files = []
ftp.dir(files.append)
picons_suf = (".jpg", ".png")
for file in files:
name = str(file).strip()
if name.endswith(picons_suf):
name = name.split()[-1]
ftp.delete(name)
for file_name in os.listdir(picons_dir_path):
if file_name.endswith(picons_suf):
send_file(file_name, picons_dir_path, ftp)
# resume enigma or restart neutrino
tn.send("init 3" if profile is Profile.ENIGMA_2 else "init 6")
if callback is not None:
callback()
def send_file(file_name, path, ftp):
""" Opens the file in binary mode and transfers into receiver """
with open(path + file_name, "rb") as f:
return ftp.storbinary("STOR " + file_name, f)
def telnet(host, port=23, user="", password="", timeout=5):
try:
tn = Telnet(host=host, port=port, timeout=timeout)
except socket.timeout:
log("telnet error: socket timeout")
else:
time.sleep(1)
command = yield
if user != "":
tn.read_until(b"login: ")
tn.write(user.encode("utf-8") + b"\n")
time.sleep(timeout)
if password != "":
tn.read_until(b"Password: ")
tn.write(password.encode("utf-8") + b"\n")
time.sleep(timeout)
tn.write("{}\r\n".format(command).encode("utf-8"))
time.sleep(timeout)
command = yield
time.sleep(timeout)
tn.write("{}\r\n".format(command).encode("utf-8"))
time.sleep(timeout)
tn.close()
yield
if __name__ == "__main__":
pass

View File

@@ -1,188 +0,0 @@
import os
import shutil
from collections import namedtuple
from html.parser import HTMLParser
from app.commons import log
from app.properties import Profile
_ENIGMA2_PICON_KEY = "{:X}:{:X}:{:X}0000"
_NEUTRINO_PICON_KEY = "{:x}{:04x}{:04x}.png"
Provider = namedtuple("Provider", ["logo", "name", "pos", "url", "on_id", "selected"])
Picon = namedtuple("Picon", ["ref", "ssid", "v_pid"])
class PiconsParser(HTMLParser):
""" Parser for package html page. (https://www.lyngsat.com/packages/*provider-name*.html) """
def __init__(self, entities=False, separator=' '):
HTMLParser.__init__(self)
self._parse_html_entities = entities
self._separator = separator
self._is_td = False
self._is_th = False
self._current_row = []
self._current_cell = []
self.picons = []
def handle_starttag(self, tag, attrs):
if tag == 'td':
self._is_td = True
if tag == 'th':
self._is_th = True
if tag == "img":
self._current_row.append(attrs[0][1])
def handle_data(self, data):
""" Save content to a cell """
if self._is_td or self._is_th:
self._current_cell.append(data.strip())
def handle_endtag(self, tag):
if tag == 'td':
self._is_td = False
elif tag == 'th':
self._is_th = False
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':
row = self._current_row
ln = len(row)
if 9 < ln < 13:
url = None
if row[0].startswith("../logo/"):
url = row[0]
elif row[1].startswith("../logo/"):
url = row[1]
ssid = row[-4]
if url and len(ssid) > 2:
self.picons.append(Picon(url, ssid, row[-3]))
self._current_row = []
def error(self, message):
pass
@staticmethod
def parse(open_path, picons_path, tmp_path, on_id, pos, picon_ids, profile=Profile.ENIGMA_2):
with open(open_path, encoding="utf-8", errors="replace") as f:
parser = PiconsParser()
parser.reset()
parser.feed(f.read())
picons = parser.picons
if picons:
os.makedirs(picons_path, exist_ok=True)
for p in picons:
try:
name = PiconsParser.format(p.ssid, on_id, p.v_pid, pos, picon_ids, profile)
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:
log("Picons format parse error: {}".format(p) + "\n" + str(e))
print(e)
@staticmethod
def format(ssid, on_id, v_pid, pos, picon_ids, profile: Profile):
tr_id = int(ssid[:-2] if len(ssid) < 4 else ssid[:2])
if profile is Profile.ENIGMA_2:
return picon_ids.get(_ENIGMA2_PICON_KEY.format(int(ssid), int(on_id), int(pos)), None)
elif profile is Profile.NEUTRINO_MP:
return _NEUTRINO_PICON_KEY.format(tr_id, int(on_id), int(ssid))
else:
return "{}.png".format(ssid)
class ProviderParser(HTMLParser):
""" Parser for satellite html page. (https://www.lyngsat.com/*sat-name*.html) """
def __init__(self, entities=False, separator=' '):
HTMLParser.__init__(self)
self._ON_ID_BLACK_LIST = ("65535", "?", "0", "1")
self._parse_html_entities = entities
self._separator = separator
self._is_td = False
self._is_th = False
self._is_provider = False
self._current_row = []
self._current_cell = []
self.rows = []
self._ids = set()
self._counter = 0
self._positon = None
def handle_starttag(self, tag, attrs):
if tag == 'td':
self._is_td = True
if tag == 'tr':
self._is_th = True
if tag == "img":
if attrs[0][1].startswith("logo/"):
self._current_row.append(attrs[0][1])
if tag == "a":
if "https://www.lyngsat.com/packages/" in attrs[0][1]:
self._current_row.append(attrs[0][1])
def handle_data(self, data):
""" Save content to a cell """
if self._is_td or self._is_th:
self._current_cell.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).strip()
self._current_row.append(final_cell)
self._current_cell = []
elif tag == 'tr':
row = self._current_row
# Satellite position
self._counter = self._counter + 1
if self._counter == 12:
pos = str(row)
pos = pos[pos.rfind("at") + 2:]
self._positon = "".join(c for c in pos if c.isalnum() or c == ".")
if len(row) == 12:
on_id, sep, tid = str(row[-2]).partition("-")
if tid and on_id not in self._ON_ID_BLACK_LIST and on_id not in self._ids:
row[-2] = on_id
self.rows.append(row)
self._ids.add(on_id)
row[0] = self._positon
self._current_row = []
def error(self, message):
pass
def reset(self):
super().reset()
self._counter = 0
def parse_providers(open_path):
parser = ProviderParser()
parser.reset()
with open(open_path, encoding="utf-8", errors="replace") as f:
parser.feed(f.read())
rows = parser.rows
if rows:
return [Provider(logo=r[2], name=r[5], pos=r[0], url=r[6], on_id=r[-2], selected=True) for r in rows]
if __name__ == "__main__":
pass

View File

@@ -1,67 +0,0 @@
import json
import os
from enum import Enum
from pathlib import Path
CONFIG_PATH = str(Path.home()) + "/.config/demon-editor/"
CONFIG_FILE = CONFIG_PATH + "config.json"
DATA_PATH = "data/"
class Profile(Enum):
""" Profiles for settings """
ENIGMA_2 = "0"
NEUTRINO_MP = "1"
def get_config():
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True) # create dir if not exist
os.makedirs(os.path.dirname(DATA_PATH), exist_ok=True)
if not os.path.isfile(CONFIG_FILE) or os.stat(CONFIG_FILE).st_size == 0:
reset_config()
with open(CONFIG_FILE, "r") as config_file:
return json.load(config_file)
def reset_config():
with open(CONFIG_FILE, "w") as default_config_file:
json.dump(get_default_settings(), default_config_file)
def write_config(config):
assert isinstance(config, dict)
with open(CONFIG_FILE, "w") as config_file:
json.dump(config, config_file)
def get_default_settings():
return {
Profile.ENIGMA_2.value: {
"host": "127.0.0.1", "port": "21",
"user": "root", "password": "root",
"telnet_user": "", "telnet_password": "",
"telnet_port": "21", "telnet_timeout": 5,
"services_path": "/etc/enigma2/",
"user_bouquet_path": "/etc/enigma2/",
"satellites_xml_path": "/etc/tuxbox/",
"picons_path": "/usr/share/enigma2/picon",
"data_dir_path": DATA_PATH + "enigma2/",
"picons_dir_path": DATA_PATH + "enigma2/picons/"},
Profile.NEUTRINO_MP.value: {
"host": "127.0.0.1", "port": "21",
"user": "root", "password": "root",
"telnet_user": "root", "telnet_password": "",
"telnet_port": "21", "telnet_timeout": 1,
"services_path": "/var/tuxbox/config/zapit/",
"user_bouquet_path": "/var/tuxbox/config/zapit/",
"satellites_xml_path": "/var/tuxbox/config/",
"picons_path": "/usr/share/tuxbox/neutrino/icons/logo/",
"data_dir_path": DATA_PATH + "neutrino/",
"picons_dir_path": DATA_PATH + "neutrino/picons/"},
"profile": Profile.ENIGMA_2.value}
if __name__ == "__main__":
pass

702
app/settings.py Normal file
View File

@@ -0,0 +1,702 @@
import copy
import json
import locale
import os
import sys
from enum import Enum, IntEnum
from functools import lru_cache
from pathlib import Path
from pprint import pformat
from textwrap import dedent
HOME_PATH = str(Path.home())
CONFIG_PATH = HOME_PATH + "/.config/demon-editor/"
CONFIG_FILE = CONFIG_PATH + "config.json"
DATA_PATH = HOME_PATH + "/DemonEditor/data/"
IS_DARWIN = sys.platform == "darwin"
class Defaults(Enum):
""" Default program settings """
DEFAULT_PROFILE = "default"
BACKUP_BEFORE_DOWNLOADING = True
BACKUP_BEFORE_SAVE = True
V5_SUPPORT = False
FORCE_BQ_NAMES = False
HTTP_API_SUPPORT = False
ENABLE_YT_DL = False
ENABLE_SEND_TO = False
USE_COLORS = True
NEW_COLOR = "rgb(255,230,204)"
EXTRA_COLOR = "rgb(179,230,204)"
FAV_CLICK_MODE = 0
PLAY_STREAMS_MODE = 1 if IS_DARWIN else 0
PROFILE_FOLDER_DEFAULT = False
RECORDS_PATH = DATA_PATH + "records/"
ACTIVATE_TRANSCODING = False
ACTIVE_TRANSCODING_PRESET = "720p TV/device"
def get_settings():
if not os.path.isfile(CONFIG_FILE) or os.stat(CONFIG_FILE).st_size == 0:
write_settings(get_default_settings())
with open(CONFIG_FILE, "r") as config_file:
return json.load(config_file)
def get_default_settings(profile_name="default"):
def_settings = SettingsType.ENIGMA_2.get_default_settings()
set_local_paths(def_settings, profile_name)
return {
"version": 1,
"default_profile": Defaults.DEFAULT_PROFILE.value,
"profiles": {profile_name: def_settings},
"v5_support": Defaults.V5_SUPPORT.value,
"http_api_support": Defaults.HTTP_API_SUPPORT.value,
"enable_yt_dl": Defaults.ENABLE_YT_DL.value,
"enable_send_to": Defaults.ENABLE_SEND_TO.value,
"use_colors": Defaults.USE_COLORS.value,
"new_color": Defaults.NEW_COLOR.value,
"extra_color": Defaults.EXTRA_COLOR.value,
"fav_click_mode": Defaults.FAV_CLICK_MODE.value,
"profile_folder_is_default": Defaults.PROFILE_FOLDER_DEFAULT.value,
"records_path": Defaults.RECORDS_PATH.value
}
def get_default_transcoding_presets():
return {"720p TV/device": {"vcodec": "h264", "vb": "1500", "width": "1280", "height": "720", "acodec": "mp3",
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"},
"1080p TV/device": {"vcodec": "h264", "vb": "3500", "width": "1920", "height": "1080", "acodec": "mp3",
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"}}
def write_settings(config):
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
with open(CONFIG_FILE, "w") as config_file:
json.dump(config, config_file, indent=" ")
def set_local_paths(settings, profile_name, data_path=DATA_PATH, use_profile_folder=False):
settings["data_local_path"] = "{}{}/".format(data_path, profile_name)
if use_profile_folder:
settings["picons_local_path"] = "{}{}/{}/".format(data_path, profile_name, "picons")
settings["backup_local_path"] = "{}{}/{}/".format(data_path, profile_name, "backup")
else:
settings["picons_local_path"] = "{}{}/{}/".format(data_path, "picons", profile_name)
settings["backup_local_path"] = "{}{}/{}/".format(data_path, "backup", profile_name)
class SettingsType(IntEnum):
""" Profiles for settings """
ENIGMA_2 = 0
NEUTRINO_MP = 1
def get_default_settings(self):
""" 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,
"services_path": "/etc/enigma2/", "user_bouquet_path": "/etc/enigma2/",
"satellites_xml_path": "/etc/tuxbox/", "data_local_path": DATA_PATH + "enigma2/",
"picons_path": "/usr/share/enigma2/picon/",
"picons_local_path": DATA_PATH + "enigma2/picons/",
"backup_local_path": DATA_PATH + "enigma2/backup/"}
elif self is self.NEUTRINO_MP:
return {"setting_type": self,
"host": "127.0.0.1", "port": "21", "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,
"services_path": "/var/tuxbox/config/zapit/", "user_bouquet_path": "/var/tuxbox/config/zapit/",
"satellites_xml_path": "/var/tuxbox/config/", "data_local_path": DATA_PATH + "neutrino/",
"picons_path": "/usr/share/tuxbox/neutrino/icons/logo/",
"picons_local_path": DATA_PATH + "neutrino/picons/",
"backup_local_path": DATA_PATH + "neutrino/backup/"}
class SettingsException(Exception):
pass
class SettingsReadException(SettingsException):
pass
class PlayStreamsMode(IntEnum):
""" Behavior mode when opening streams. """
BUILT_IN = 0
VLC = 1
M3U = 2
class Settings:
__INSTANCE = None
__VERSION = 1
def __init__(self, ext_settings=None):
try:
settings = ext_settings or get_settings()
except PermissionError as e:
raise SettingsReadException(e)
if self.__VERSION > settings.get("version", 0):
raise SettingsException("Outdated version of the settings format!")
self._settings = settings
self._current_profile = self._settings.get("default_profile", "default")
self._profiles = self._settings.get("profiles", {"default": SettingsType.ENIGMA_2.get_default_settings()})
self._cp_settings = self._profiles.get(self._current_profile, None) # Current profile settings
if not self._cp_settings:
raise SettingsException("Error reading settings [current profile].")
def __str__(self):
return dedent(""" Current profile: {}
Current profile options:
{}
Full config:
{}
""").format(self._current_profile,
pformat(self._cp_settings),
pformat(self._settings))
@classmethod
def get_instance(cls):
if not cls.__INSTANCE:
cls.__INSTANCE = Settings()
return cls.__INSTANCE
def save(self):
write_settings(self._settings)
def reset(self, force_write=False):
for k, v in self.setting_type.get_default_settings().items():
self._cp_settings[k] = v
def_path = self.default_data_path
def_path += "enigma2/" if self.setting_type is SettingsType.ENIGMA_2 else "neutrino/"
set_local_paths(self._cp_settings, self._current_profile, def_path, self.profile_folder_is_default)
if force_write:
self.save()
@staticmethod
def reset_to_default():
write_settings(get_default_settings())
def get_default(self, p_name):
""" Returns default value for current settings type """
return self.setting_type.get_default_settings().get(p_name)
def add(self, name, value):
""" Adds extra options """
self._settings[name] = value
def get(self, name):
""" Returns extra options or None """
return self._settings.get(name, None)
@property
def settings(self):
""" Returns copy of the current settings! """
return copy.deepcopy(self._settings)
@settings.setter
def settings(self, value):
""" Sets copy of the settings! """
self._settings = copy.deepcopy(value)
@property
def current_profile(self):
return self._current_profile
@current_profile.setter
def current_profile(self, value):
self._current_profile = value
self._cp_settings = self._profiles.get(self._current_profile)
@property
def default_profile(self):
return self._settings.get("default_profile", "default")
@default_profile.setter
def default_profile(self, value):
self._settings["default_profile"] = value
@property
def profiles(self):
return self._profiles
@profiles.setter
def profiles(self, ps):
self._profiles = ps
self._settings["profiles"] = self._profiles
@property
def setting_type(self):
return SettingsType(self._cp_settings.get("setting_type", SettingsType.ENIGMA_2.value))
@setting_type.setter
def setting_type(self, s_type):
self._cp_settings["setting_type"] = s_type.value
# ******* Network ******** #
@property
def host(self):
return self._cp_settings.get("host", self.get_default("host"))
@host.setter
def host(self, value):
self._cp_settings["host"] = value
@property
def port(self):
return self._cp_settings.get("port", self.get_default("port"))
@port.setter
def port(self, value):
self._cp_settings["port"] = value
@property
def user(self):
return self._cp_settings.get("user", self.get_default("user"))
@user.setter
def user(self, value):
self._cp_settings["user"] = value
@property
def password(self):
return self._cp_settings.get("password", self.get_default("password"))
@password.setter
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"))
@http_port.setter
def http_port(self, value):
self._cp_settings["http_port"] = value
@property
def http_timeout(self):
return self._cp_settings.get("http_timeout", self.get_default("http_timeout"))
@http_timeout.setter
def http_timeout(self, value):
self._cp_settings["http_timeout"] = value
@property
def http_use_ssl(self):
return self._cp_settings.get("http_use_ssl", self.get_default("http_use_ssl"))
@http_use_ssl.setter
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"))
@telnet_port.setter
def telnet_port(self, value):
self._cp_settings["telnet_port"] = value
@property
def telnet_timeout(self):
return self._cp_settings.get("telnet_timeout", self.get_default("telnet_timeout"))
@telnet_timeout.setter
def telnet_timeout(self, value):
self._cp_settings["telnet_timeout"] = value
@property
def services_path(self):
return self._cp_settings.get("services_path", self.get_default("services_path"))
@services_path.setter
def services_path(self, value):
self._cp_settings["services_path"] = value
@property
def user_bouquet_path(self):
return self._cp_settings.get("user_bouquet_path", self.get_default("user_bouquet_path"))
@user_bouquet_path.setter
def user_bouquet_path(self, value):
self._cp_settings["user_bouquet_path"] = value
@property
def satellites_xml_path(self):
return self._cp_settings.get("satellites_xml_path", self.get_default("satellites_xml_path"))
@satellites_xml_path.setter
def satellites_xml_path(self, value):
self._cp_settings["satellites_xml_path"] = value
@property
def picons_path(self):
return self._cp_settings.get("picons_path", self.get_default("picons_path"))
@picons_path.setter
def picons_path(self, value):
self._cp_settings["picons_path"] = value
# ***** Local paths ***** #
@property
def profile_folder_is_default(self):
return self._settings.get("profile_folder_is_default", Defaults.PROFILE_FOLDER_DEFAULT.value)
@profile_folder_is_default.setter
def profile_folder_is_default(self, value):
self._settings["profile_folder_is_default"] = value
@property
def default_data_path(self):
return self._settings.get("default_data_path", DATA_PATH)
@default_data_path.setter
def default_data_path(self, value):
self._settings["default_data_path"] = value
@property
def data_local_path(self):
return self._cp_settings.get("data_local_path", self.get_default("data_local_path"))
@data_local_path.setter
def data_local_path(self, value):
self._cp_settings["data_local_path"] = value
@property
def picons_local_path(self):
return self._cp_settings.get("picons_local_path", self.get_default("picons_local_path"))
@picons_local_path.setter
def picons_local_path(self, value):
self._cp_settings["picons_local_path"] = value
@property
def backup_local_path(self):
return self._cp_settings.get("backup_local_path", self.get_default("backup_local_path"))
@backup_local_path.setter
def backup_local_path(self, value):
self._cp_settings["backup_local_path"] = value
@property
def records_path(self):
return self._settings.get("records_path", Defaults.RECORDS_PATH.value)
@records_path.setter
def records_path(self, value):
self._settings["records_path"] = value
# ******** Streaming ********* #
@property
def activate_transcoding(self):
return self._settings.get("activate_transcoding", Defaults.ACTIVATE_TRANSCODING.value)
@activate_transcoding.setter
def activate_transcoding(self, value):
self._settings["activate_transcoding"] = value
@property
def active_preset(self):
return self._settings.get("active_preset", Defaults.ACTIVE_TRANSCODING_PRESET.value)
@active_preset.setter
def active_preset(self, value):
self._settings["active_preset"] = value
@property
def transcoding_presets(self):
return self._settings.get("transcoding_presets", get_default_transcoding_presets())
@transcoding_presets.setter
def transcoding_presets(self, value):
self._settings["transcoding_presets"] = value
@property
def play_streams_mode(self):
return PlayStreamsMode(self._settings.get("play_streams_mode", Defaults.PLAY_STREAMS_MODE.value))
@play_streams_mode.setter
def play_streams_mode(self, value):
self._settings["play_streams_mode"] = value
# *********** EPG ************ #
@property
def epg_options(self):
""" Options used by the EPG dialog. """
return self._cp_settings.get("epg_options", None)
@epg_options.setter
def epg_options(self, value):
self._cp_settings["epg_options"] = value
# ***** Program settings ***** #
@property
def backup_before_save(self):
return self._settings.get("backup_before_save", Defaults.BACKUP_BEFORE_SAVE.value)
@backup_before_save.setter
def backup_before_save(self, value):
self._settings["backup_before_save"] = value
@property
def backup_before_downloading(self):
return self._settings.get("backup_before_downloading", Defaults.BACKUP_BEFORE_DOWNLOADING.value)
@backup_before_downloading.setter
def backup_before_downloading(self, value):
self._settings["backup_before_downloading"] = value
@property
def v5_support(self):
return self._settings.get("v5_support", Defaults.V5_SUPPORT.value)
@v5_support.setter
def v5_support(self, value):
self._settings["v5_support"] = value
@property
def force_bq_names(self):
return self._settings.get("force_bq_names", Defaults.FORCE_BQ_NAMES.value)
@force_bq_names.setter
def force_bq_names(self, value):
self._settings["force_bq_names"] = value
@property
def http_api_support(self):
return self._settings.get("http_api_support", Defaults.HTTP_API_SUPPORT.value)
@http_api_support.setter
def http_api_support(self, value):
self._settings["http_api_support"] = value
@property
def enable_yt_dl(self):
return self._settings.get("enable_yt_dl", Defaults.ENABLE_YT_DL.value)
@enable_yt_dl.setter
def enable_yt_dl(self, value):
self._settings["enable_yt_dl"] = value
@property
def enable_yt_dl_update(self):
return self._settings.get("enable_yt_dl_update", Defaults.ENABLE_YT_DL.value)
@enable_yt_dl_update.setter
def enable_yt_dl_update(self, value):
self._settings["enable_yt_dl_update"] = value
@property
def enable_send_to(self):
return self._settings.get("enable_send_to", Defaults.ENABLE_SEND_TO.value)
@enable_send_to.setter
def enable_send_to(self, value):
self._settings["enable_send_to"] = value
@property
def use_colors(self):
return self._settings.get("use_colors", Defaults.USE_COLORS.value)
@use_colors.setter
def use_colors(self, value):
self._settings["use_colors"] = value
@property
def new_color(self):
return self._settings.get("new_color", Defaults.NEW_COLOR.value)
@new_color.setter
def new_color(self, value):
self._settings["new_color"] = value
@property
def extra_color(self):
return self._settings.get("extra_color", Defaults.EXTRA_COLOR.value)
@extra_color.setter
def extra_color(self, value):
self._settings["extra_color"] = value
@property
def fav_click_mode(self):
return self._settings.get("fav_click_mode", Defaults.FAV_CLICK_MODE.value)
@fav_click_mode.setter
def fav_click_mode(self, value):
self._settings["fav_click_mode"] = value
@property
def language(self):
return self._settings.get("language", locale.getlocale()[0] or "en_US")
@language.setter
def language(self, value):
self._settings["language"] = value
@property
def load_last_config(self):
return self._settings.get("load_last_config", False)
@load_last_config.setter
def load_last_config(self, value):
self._settings["load_last_config"] = value
@property
def show_srv_hints(self):
""" Show short info as hints in the main services list. """
return self._settings.get("show_srv_hints", True)
@show_srv_hints.setter
def show_srv_hints(self, value):
self._settings["show_srv_hints"] = value
@property
def show_bq_hints(self):
""" Show detailed info as hints in the bouquet list. """
return self._settings.get("show_bq_hints", True)
@show_bq_hints.setter
def show_bq_hints(self, value):
self._settings["show_bq_hints"] = value
# *********** Appearance *********** #
@property
def dark_mode(self):
return self._settings.get("dark_mode", False)
@dark_mode.setter
def dark_mode(self, value):
self._settings["dark_mode"] = value
@property
def is_themes_support(self):
return self._settings.get("is_themes_support", False)
@is_themes_support.setter
def is_themes_support(self, value):
self._settings["is_themes_support"] = value
@property
def theme(self):
return self._settings.get("theme", "Default")
@theme.setter
def theme(self, value):
self._settings["theme"] = value
@property
@lru_cache(1)
def themes_path(self):
return "{}/.themes/".format(HOME_PATH)
@property
def icon_theme(self):
return self._settings.get("icon_theme", "Adwaita")
@icon_theme.setter
def icon_theme(self, value):
self._settings["icon_theme"] = value
@property
@lru_cache(1)
def icon_themes_path(self):
return "{}/.icons/".format(HOME_PATH)
@property
def is_darwin(self):
return IS_DARWIN
# *********** Download dialog *********** #
@property
def use_http(self):
return self._settings.get("use_http", True)
@use_http.setter
def use_http(self, value):
self._settings["use_http"] = value
@property
def remove_unused_bouquets(self):
return self._settings.get("remove_unused_bouquets", True)
@remove_unused_bouquets.setter
def remove_unused_bouquets(self, value):
self._settings["remove_unused_bouquets"] = value
# **************** Debug **************** #
@property
def debug_mode(self):
return self._settings.get("debug_mode", False)
@debug_mode.setter
def debug_mode(self, value):
self._settings["debug_mode"] = value
# **************** Experimental **************** #
@property
def is_enable_experimental(self):
""" Allows experimental functionality. """
return self._settings.get("enable_experimental", False)
@is_enable_experimental.setter
def is_enable_experimental(self, value):
self._settings["enable_experimental"] = value
if __name__ == "__main__":
pass

110
app/tools/epg.py Normal file
View File

@@ -0,0 +1,110 @@
""" Module for working with epg.dat file """
import struct
from datetime import datetime
from xml.dom.minidom import parse, Node, Document
from app.eparser.ecommons import BqServiceType, BouquetService
class EPG:
@staticmethod
def get_epg_refs(path):
""" The read algorithm was taken from the eEPGCache::load() function from this source:
https://github.com/OpenPLi/enigma2/blob/44d9b92f5260c7de1b3b3a1b9a9cbe0f70ca4bf0/lib/dvb/epgcache.cpp#L1300
"""
refs = set()
with open(path, mode="rb") as f:
crc = struct.unpack("<I", f.read(4))[0]
if crc != int(0x98765432):
raise ValueError("Epg file has incorrect byte order!")
header = f.read(13).decode()
if header != "ENIGMA_EPG_V7":
raise ValueError("Unsupported format of epd.dat file!")
channels_count = struct.unpack("<I", f.read(4))[0]
for i in range(channels_count):
sid, nid, tsid, events_size = struct.unpack("<IIII", f.read(16))
service_id = "{:X}:{:X}:{:X}".format(sid, tsid, nid)
for j in range(events_size):
_type, _len = struct.unpack("<BB", f.read(2))
f.read(10)
n_crc = (_len - 10) // 4
if n_crc > 0:
[f.read(4) for n in range(n_crc)]
refs.add(service_id)
return refs
class ChannelsParser:
_COMMENT = "File was created in DemonEditor"
@staticmethod
def get_refs_from_xml(path):
""" Returns tuple from references and description. """
refs = []
dom = parse(path)
description = "".join(n.data + "\n" for n in dom.childNodes if n.nodeType == Node.COMMENT_NODE)
for elem in dom.getElementsByTagName("channels"):
c_count = 0
comment_count = 0
current_data = ""
if elem.hasChildNodes():
for n in elem.childNodes:
if n.nodeType == Node.COMMENT_NODE:
c_count += 1
comment_count += 1
txt = n.data.strip()
if comment_count:
comment_count -= 1
else:
ref_data = current_data.split(":")
refs.append(BouquetService(name=txt,
type=BqServiceType.DEFAULT,
data="{}:{}:{}:{}".format(*ref_data[3:7]).upper(),
num="{}:{}:{}".format(*ref_data[3:6]).upper()))
if n.hasChildNodes():
for s_node in n.childNodes:
if s_node.nodeType == Node.TEXT_NODE:
comment_count -= 1
current_data = s_node.data
return refs, description
@staticmethod
def write_refs_to_xml(path, services):
header = '<?xml version="1.0" encoding="utf-8"?>\n<!-- {} -->\n<!-- {} -->\n<channels>\n'.format(
"Created in DemonEditor.", datetime.now().strftime("%d.%m.%Y %H:%M:%S"))
doc = Document()
lines = [header]
for srv in services:
srv_type = srv.type
if srv_type is BqServiceType.IPTV:
channel_child = doc.createElement("channel")
channel_child.setAttribute("id", str(srv.num))
data = srv.data.strip().split(":")
channel_child.appendChild(doc.createTextNode(":".join(data[:10])))
comment = doc.createComment(srv.name)
lines.append("{} {}\n".format(str(channel_child.toxml()), str(comment.toxml())))
elif srv_type is BqServiceType.MARKER:
comment = doc.createComment(srv.name)
lines.append("{}\n".format(str(comment.toxml())))
lines.append("</channels>")
doc.unlink()
with open(path, "w", encoding="utf-8") as f:
f.writelines(lines)
if __name__ == "__main__":
pass

270
app/tools/media.py Normal file
View File

@@ -0,0 +1,270 @@
import os
import subprocess
import sys
from datetime import datetime
from enum import Enum
from urllib.request import urlopen
from app.commons import run_task, log, _DATE_FORMAT
from app.settings import PlayStreamsMode
class Player:
__VLC_INSTANCE = None
__PLAY_STREAMS_MODE = PlayStreamsMode.BUILT_IN
def __init__(self, rewind_callback, position_callback, error_callback, playing_callback):
try:
from app.tools import vlc
from app.tools.vlc import EventType
except OSError as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
raise ImportError
else:
self._is_playing = False
args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib")
self._player = vlc.Instance(args).media_player_new()
ev_mgr = self._player.event_manager()
if rewind_callback:
# TODO look other EventType options
ev_mgr.event_attach(EventType.MediaPlayerBuffering,
lambda et, p: rewind_callback(p.get_media().get_duration()),
self._player)
if position_callback:
ev_mgr.event_attach(EventType.MediaPlayerTimeChanged,
lambda et, p: position_callback(p.get_time()),
self._player)
if error_callback:
ev_mgr.event_attach(EventType.MediaPlayerEncounteredError,
lambda et, p: error_callback(),
self._player)
if playing_callback:
ev_mgr.event_attach(EventType.MediaPlayerPlaying,
lambda et, p: playing_callback(),
self._player)
@classmethod
def get_instance(cls, rewind_callback=None, position_callback=None, error_callback=None, playing_callback=None):
if not cls.__VLC_INSTANCE:
cls.__VLC_INSTANCE = Player(rewind_callback, position_callback, error_callback, playing_callback)
return cls.__VLC_INSTANCE
@staticmethod
def get_play_mode():
return Player.__PLAY_STREAMS_MODE
@run_task
def play(self, mrl=None):
if mrl:
self._player.set_mrl(mrl)
self._player.play()
self._is_playing = True
@run_task
def stop(self):
if self._is_playing:
self._player.stop()
self._is_playing = False
def pause(self):
self._player.pause()
def set_time(self, time):
self._player.set_time(time)
@run_task
def release(self):
if self._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))
def set_mrl(self, mrl):
self._player.set_mrl(mrl)
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()
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
class Recorder:
__VLC_REC_INSTANCE = None
_CMD = "sout=#std{{access=file,mux=ts,dst={}.ts}}"
_TR_CMD = "sout=#transcode{{{}}}:file{{mux=mp4,dst={}.mp4}}"
def __init__(self, settings):
try:
from app.tools import vlc
from app.tools.vlc import EventType
except OSError as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
raise ImportError
else:
self._settings = settings
self._is_record = False
args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib")
self._recorder = vlc.Instance(args).media_player_new()
@classmethod
def get_instance(cls, settings):
if not cls.__VLC_REC_INSTANCE:
cls.__VLC_REC_INSTANCE = Recorder(settings)
return cls.__VLC_REC_INSTANCE
@run_task
def record(self, url, name):
if self._recorder:
self._recorder.stop()
path = self._settings.records_path
os.makedirs(os.path.dirname(path), exist_ok=True)
d_now = datetime.now().strftime(_DATE_FORMAT)
path = "{}{}_{}".format(path, name.replace(" ", "_"), d_now.replace(" ", "_"))
cmd = self.get_transcoding_cmd(path) if self._settings.activate_transcoding else self._CMD.format(path)
media = self._recorder.get_instance().media_new(url, cmd)
media.get_mrl()
self._recorder.set_media(media)
self._is_record = True
self._recorder.play()
log("Record started {}".format(d_now))
@run_task
def stop(self):
self._recorder.stop()
self._is_record = False
log("Recording stopped.")
def is_record(self):
return self._is_record
@run_task
def release(self):
if self._recorder:
self._recorder.stop()
self._recorder.release()
self._is_record = False
log("Recording stopped. Releasing...")
def get_transcoding_cmd(self, path):
presets = self._settings.transcoding_presets
prs = presets.get(self._settings.active_preset)
return self._TR_CMD.format(",".join("{}={}".format(k, v) for k, v in prs.items()), path)
if __name__ == "__main__":
pass

269
app/tools/picons.py Normal file
View File

@@ -0,0 +1,269 @@
import glob
import os
import re
import shutil
from collections import namedtuple
from html.parser import HTMLParser
from app.commons import run_task, log
from app.settings import SettingsType
_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"])
class PiconsParser(HTMLParser):
""" Parser for package html page. (https://www.lyngsat.com/packages/*provider-name*.html) """
def __init__(self, entities=False, separator=' ', single=None):
HTMLParser.__init__(self)
self._parse_html_entities = entities
self._separator = separator
self._single = single
self._is_td = False
self._is_th = False
self._current_row = []
self._current_cell = []
self.picons = []
def handle_starttag(self, tag, attrs):
if tag == "td":
self._is_td = True
if tag == "th":
self._is_th = True
if tag == "img":
self._current_row.append(attrs[0][1])
def handle_data(self, data):
""" Save content to a cell """
if self._is_td or self._is_th:
self._current_cell.append(data.strip())
def handle_endtag(self, tag):
if tag == "td":
self._is_td = False
elif tag == "th":
self._is_th = False
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":
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"))
else:
if 9 < ln < 13:
url = None
if row[0].startswith("../logo/"):
url = row[0]
elif row[1].startswith("../logo/"):
url = row[1]
ssid = row[-4]
if url and len(ssid) > 2:
self.picons.append(Picon(url, ssid, row[-3]))
self._current_row = []
def error(self, message):
pass
@staticmethod
def parse(open_path, picons_path, tmp_path, provider, picon_ids, s_type=SettingsType.ENIGMA_2):
if not os.path.isfile(open_path):
log("PiconsParser error [parse]. No such file or directory: {}".format(open_path))
return
with open(open_path, encoding="utf-8", errors="replace") as f:
on_id, pos, ssid, single = provider.on_id, provider.pos, provider.ssid, provider.single
neg_pos = pos.endswith("W")
pos = int("".join(c for c in pos if c.isdigit()))
# For negative (West) positions 3600 - numeric position value!!!
if neg_pos:
pos = 3600 - pos
parser = PiconsParser(single=single)
parser.reset()
parser.feed(f.read())
picons = parser.picons
if picons:
os.makedirs(picons_path, exist_ok=True)
for p in picons:
try:
if single:
on_id, freq = on_id.strip().split("::")
namespace = "{:X}{:X}".format(int(pos), int(freq))
else:
namespace = "{:X}0000".format(int(pos))
name = PiconsParser.format(ssid if single else p.ssid, on_id, namespace, picon_ids, s_type)
p_name = picons_path + (name if name else os.path.basename(p.ref))
shutil.copyfile(tmp_path + "www.lyngsat.com/" + p.ref.lstrip("."), p_name)
except (TypeError, ValueError) as e:
msg = "Picons format parse error: {}".format(p) + "\n" + str(e)
log(msg)
@staticmethod
def format(ssid, on_id, namespace, picon_ids, s_type):
if s_type is SettingsType.ENIGMA_2:
return picon_ids.get(_ENIGMA2_PICON_KEY.format(int(ssid), int(on_id), namespace), None)
elif s_type is SettingsType.NEUTRINO_MP:
tr_id = int(ssid[:-2] if len(ssid) < 4 else ssid[:2])
return _NEUTRINO_PICON_KEY.format(tr_id, int(on_id), int(ssid))
else:
return "{}.png".format(ssid)
class ProviderParser(HTMLParser):
""" Parser for satellite html page. (https://www.lyngsat.com/*sat-name*.html) """
_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/"}
def __init__(self, entities=False, separator=' '):
HTMLParser.__init__(self)
self.convert_charrefs = False
self._parse_html_entities = entities
self._separator = separator
self._is_td = False
self._is_th = False
self._is_onid_tid = False
self._is_provider = False
self._current_row = []
self._current_cell = []
self.rows = []
self._ids = set()
self._prv_names = set()
self._positon = None
self._on_id = None
self._freq = None
def handle_starttag(self, tag, attrs):
if tag == 'td':
self._is_td = True
if tag == 'tr':
self._is_th = True
if tag == "img":
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':
self._is_td = False
elif tag == 'tr':
self._is_th = False
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':
r = self._current_row
# Satellite position
if not self._positon:
pos = re.findall(self._POSITION_PATTERN, str(r))
if pos:
self._positon = "".join(c for c in str(pos) if c.isdigit() or c in ".EW")
len_row = len(r)
if len_row > 2:
m = self._TRANSPONDER_FREQUENCY_PATTERN.match(r[1])
if m:
self._freq = m.group().split()[0]
if len_row == 12:
# Providers
name = r[5]
self._prv_names.add(name)
m = self._ONID_TID_PATTERN.match(str(r[-2]))
if m:
on_id, tid = m.group().split("-")
if on_id not in self._ids:
r[-2] = on_id
self._ids.add(on_id)
r[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,
ssid=None, single=False, selected=True))
elif 6 < len_row < 10:
# 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 name and url:
on_id = "{}::{}".format(self._on_id if self._on_id else "1", self._freq)
self.rows.append(Provider(logo=None, name=name, pos=self._positon, url=url, on_id=on_id,
ssid=ssid, single=True, selected=False))
self._current_row = []
def error(self, message):
pass
def reset(self):
super().reset()
def parse_providers(open_path):
parser = ProviderParser()
parser.reset()
with open(open_path, encoding="utf-8", errors="replace") as f:
parser.feed(f.read())
return parser.rows
@run_task
def convert_to(src_path, dest_path, s_type, callback, done_callback):
""" Converts names format of picons.
Copies resulting files from src to dest and writes state to callback.
"""
pattern = "/*_0_0_0.png" if s_type is SettingsType.ENIGMA_2 else "/*.png"
for file in glob.glob(src_path + pattern):
base_name = os.path.basename(file)
pic_data = base_name.rstrip(".png").split("_")
dest_file = _NEUTRINO_PICON_KEY.format(int(pic_data[4], 16), int(pic_data[5], 16), int(pic_data[3], 16))
dest = "{}/{}".format(dest_path, dest_file)
callback('Converting "{}" to "{}"\n'.format(base_name, dest_file))
shutil.copyfile(file, dest)
done_callback()
if __name__ == "__main__":
pass

500
app/tools/satellites.py Normal file
View File

@@ -0,0 +1,500 @@
""" 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
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, get_key_by_value, FEC, SYSTEM, POLARIZATION, MODULATION, SERVICE_TYPE,
Service, CAS)
_HEADERS = {"User-Agent": "Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/69.0"}
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")
@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. """
def __init__(self, source=SatelliteSource.FLYSAT, entities=False, separator=' '):
HTMLParser.__init__(self)
self._parse_html_entities = entities
self._separator = separator
self._is_td = False
self._is_th = False
self._is_provider = False
self._current_row = []
self._current_cell = []
self._rows = []
self._source = source
def handle_starttag(self, tag, attrs):
if tag == "td":
self._is_td = True
if tag == "tr":
self._is_th = True
if tag == "a":
self._current_row.append(attrs[0][1])
def handle_data(self, data):
""" Save content to a cell """
if self._is_td or self._is_th:
self._current_cell.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).strip()
self._current_row.append(final_cell)
self._current_cell = []
elif tag == "tr":
row = self._current_row
self._rows.append(row)
self._current_row = []
def error(self, message):
pass
def get_satellites_list(self, source):
""" Getting complete list of satellites. """
self.reset()
self._rows.clear()
self._source = source
for src in SatelliteSource.get_sources(self._source):
try:
request = requests.get(url=src, headers=_HEADERS)
except requests.exceptions.ConnectionError as e:
log(repr(e))
return []
else:
reason = request.reason
if reason == "OK":
self.feed(request.text)
else:
log(reason)
if self._rows:
if self._source is SatelliteSource.FLYSAT:
def get_sat(r):
return r[1], self.parse_position(r[2]), r[3], r[0], False
return list(map(get_sat, filter(lambda x: all(x) and len(x) == 5, self._rows)))
elif self._source is SatelliteSource.LYNGSAT:
extra_pattern = re.compile(r"^https://www\.lyngsat\.com/[\w-]+\.html")
base_url = "https://www.lyngsat.com/"
sats = []
names = set()
current_pos = "0"
for row in filter(lambda x: len(x) in (5, 7, 8), self._rows):
r_len = len(row)
if r_len == 7:
current_pos = self.parse_position(row[2])
name = row[1].rsplit("/")[-1].rstrip(".html").replace("-", " ")
if name not in names:
# [all in one] satellites
sats.append((name, current_pos, row[5], base_url + row[1], False))
names.add(name)
name = row[4]
if name not in names:
sats.append((name, current_pos, row[5], base_url + row[3], False))
names.add(name)
if r_len == 8: # for a very limited number of satellites
data = list(filter(None, row))
urls = set()
sat_type = ""
for d in data:
url = re.match(extra_pattern, d)
if url:
urls.add(url.group(0))
if d in ("C", "Ku", "CKu"):
sat_type = d
current_pos = self.parse_position(data[1])
for url in urls:
name = url.rsplit("/")[-1].rstrip(".html").replace("-", " ")
sats.append((name, current_pos, sat_type, base_url + url, False))
elif r_len == 5:
sats.append((row[2], current_pos, row[3], base_url + row[1], False))
return sats
def get_satellite(self, sat):
pos = sat[1]
return Satellite(name="{} {}".format(pos, sat[0]),
flags="0",
position=self.get_position(pos.replace(".", "")),
transponders=self.get_transponders(sat[3]))
@staticmethod
def parse_position(pos_str):
return "".join(c for c in pos_str if c.isdigit() or c.isalpha() or c == ".")
@staticmethod
def get_position(pos):
return "{}{}".format("-" if pos[-1] == "W" else "", pos[:-1])
def get_transponders(self, sat_url):
""" Getting transponders(sorted by frequency). """
self._rows.clear()
url = "https://www.flysat.com/" + sat_url if self._source is SatelliteSource.FLYSAT else sat_url
request = requests.get(url=url, headers=_HEADERS)
reason = request.reason
trs = []
if reason == "OK":
self.feed(request.text)
if self._source is SatelliteSource.FLYSAT:
self.get_transponders_for_fly_sat(trs)
elif self._source is SatelliteSource.LYNGSAT:
self.get_transponders_for_lyng_sat(trs)
return sorted(trs, key=lambda x: int(x.frequency))
def get_transponders_for_fly_sat(self, trs):
""" Parsing transponders for FlySat """
pls_pattern = re.compile("(PLS:)+ (Root|Gold|Combo)+ (\\d+)?")
is_id_pattern = re.compile("(Stream) (\\d+)")
pls_modes = {v: k for k, v in PLS_MODE.items()}
n_trs = []
if self._rows:
zeros = "000"
is_ids = []
for r in self._rows:
if len(r) == 1:
is_ids.extend(re.findall(is_id_pattern, r[0]))
continue
if len(r) < 3:
continue
data = r[2].split(" ")
if len(data) != 2:
continue
sr, fec = data
data = r[1].split(" ")
if len(data) < 3:
continue
freq, pol, sys = data[0], data[1], data[2]
sys = sys.split("/")
if len(sys) != 2:
continue
sys, mod = sys
mod = "QPSK" if sys == "DVB-S" else mod
pls = re.findall(pls_pattern, r[1])
pls_code = None
pls_mode = None
if pls:
pls_code = pls[0][2]
pls_mode = pls_modes.get(pls[0][1], None)
if is_ids:
tr = trs.pop()
for index, is_id in enumerate(is_ids):
tr = tr._replace(is_id=is_id[1])
if is_transponder_valid(tr):
n_trs.append(tr)
else:
tr = Transponder(freq + zeros, sr + zeros, pol, fec, sys, mod, pls_mode, pls_code, None)
if is_transponder_valid(tr):
trs.append(tr)
is_ids.clear()
trs.extend(n_trs)
def get_transponders_for_lyng_sat(self, trs):
""" Parsing transponders for LyngSat """
frq_pol_pattern = re.compile("(\\d{4,5})\\s+([RLHV]).*")
sr_fec_pattern = re.compile("^(\\d{4,5})-(\\d/\\d)(.+PSK)?(.*)?$")
sys_pattern = re.compile("(DVB-S[2]?) ?(PLS+ (Root|Gold|Combo)+ (\\d+))* ?(multistream stream (\\d+))?",
re.IGNORECASE)
zeros = "000"
pls_modes = {v: k for k, v in PLS_MODE.items()}
for r in filter(lambda x: len(x) > 8, self._rows):
for frq in r[1], r[2], r[3]:
freq = re.match(frq_pol_pattern, frq)
if freq:
break
if not freq:
continue
frq, pol = freq.group(1), freq.group(2)
sr_fec = re.match(sr_fec_pattern, r[-3])
if not sr_fec:
continue
sr, fec, mod = sr_fec.group(1), sr_fec.group(2), sr_fec.group(3)
mod = mod.strip() if mod else "Auto"
res = re.match(sys_pattern, r[-4])
if not res:
continue
sys = res.group(1)
pls_mode = res.group(3)
pls_mode = pls_modes.get(pls_mode.capitalize(), None) if pls_mode else pls_mode
pls_code = res.group(4)
pls_id = res.group(6)
tr = Transponder(frq + zeros, sr + zeros, pol, fec, sys, mod, pls_mode, 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", "SD": "1", "MPEG-4 SD": "22", "HEVC SD": "22", "MPEG-4 HD": "25",
"MPEG-4 HD 1080": "25", "MPEG-4 HD 720": "25", "HEVC HD": "25", "HEVC UHD": "31",
"HEVC UHD 4K": "31"}
self._TR_PAT = re.compile(r"(DVB-S[2]?)/?(.*PSK)?\s+SR\s+(\d+)\s+FEC\s+(\d/\d).*ONID/TID:\s+(\d+)/(\d+)\s+.*")
self._PTR_PAT = re.compile(r".*?(\d+\.\d°[EW]):\s+(\d+)\s+([RLHV]).*")
self._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[1] for row in
filter(lambda x: x and len(x) > 8 and x[1].url and x[1].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"
tr_found = False
pos_found = False
tr = None
# Transponder
for r in filter(lambda x: x and len(x) == 2, self._rows):
if not pos_found:
pos_tr = re.match(self._PTR_PAT, r[1].text)
if pos_tr:
if not sat_position:
pos = int(SatellitesParser.get_position(
"".join(c for c in pos_tr.group(1) if c.isdigit() or c.isalpha())))
freq = int(pos_tr.group(2))
pol = get_key_by_value(POLARIZATION, pos_tr.group(3))
pos_found = True
if pos_found and not tr_found:
td = re.match(self._TR_PAT, r[1].text) or re.match(self._TR_PAT, r[0].text)
if td:
sys, mod, sr, _fec, nid, tid = td.group(1), td.group(2), td.group(3), td.group(4), td.group(
5), td.group(6)
neg_pos = False # POS = W
# For negative (West) positions: 3600 - numeric position value!!!
namespace = "{:04x}0000".format(3600 - pos if neg_pos else pos)
inv = 2 # Default
fec = get_key_by_value(FEC, _fec)
sys = get_key_by_value(SYSTEM, sys)
tr_flag = 1
mod = get_key_by_value(MODULATION, mod)
roll_off = 0 # 35% DVB-S2/DVB-S (default)
pilot = 2 # Auto
s2_flags = "" if sys == "DVB-S" else self._S2_TR.format(tr_flag, mod or 0, roll_off, pilot)
nid, tid = int(nid), int(tid)
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
tr_found = True
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, cas, pkg, s_type, v_pid, a_pid = r[0].text, r[2].text, r[4].text, r[5].text, r[
6].text.strip(), r[7].text, r[8].text.split()
try:
s_type = self._S_TYPES.get(s_type, "3") # 3 = Data
_s_type = SERVICE_TYPE.get(s_type, SERVICE_TYPE.get("3")) # str repr
sid = int(sid)
data_id = "{:04x}:{}:{:04x}:{:04x}:{}:0:0".format(sid, namespace, tid, nid, s_type)
fav_id = "{}:{}:{}:{}".format(sid, tid, nid, namespace)
picon_id = "1_0_{:X}_{}_{}_{}_{}_0_0_0.png".format(int(s_type), sid, tid, nid, namespace)
# Flags.
flags = "p:{}".format(pkg)
cas = ",".join(get_key_by_value(CAS, c) or "C:0000" for c in cas.split()) if cas else None
if use_pids:
v_pid = "c:00{:04x}".format(int(v_pid)) if v_pid else None
a_pid = ",".join(["c:01{:04x}".format(int(p)) for p in a_pid]) if a_pid else None
flags = ",".join(filter(None, (flags, v_pid, a_pid, cas)))
else:
flags = ",".join(filter(None, (flags, cas)))
srv = Service(flags_cas=flags,
transponder_type="s",
coded=None,
service=name,
locked=None,
hide=None,
package=pkg,
service_type=_s_type,
picon=r[1].img,
picon_id=picon_id,
ssid=sid,
freq=freq,
rate=sr,
pol=pol,
fec=fec,
system=sys,
pos=pos,
data_id=data_id,
fav_id=fav_id,
transponder=tr)
services.append(srv)
except ValueError as e:
log("ServicesParser error [get transponder services]: {}".format(e))
return services
if __name__ == "__main__":
pass

8729
app/tools/vlc.py Normal file

File diff suppressed because it is too large Load Diff

350
app/tools/yt.py Normal file
View File

@@ -0,0 +1,350 @@
""" Module for working with YouTube service """
import gzip
import json
import os
import re
import shutil
import sys
from html.parser import HTMLParser
from json import JSONDecodeError
from urllib.error import URLError
from urllib.parse import unquote
from urllib.request import Request, urlopen, urlretrieve
from app.commons import log
from app.ui.uicommons import show_notification
_YT_PATTERN = re.compile(r"https://www.youtube.com/.+(?:v=)([\w-]{11}).*")
_YT_LIST_PATTERN = re.compile(r"https://www.youtube.com/.+?(?:list=)([\w-]{18,})?.*")
_YT_VIDEO_PATTERN = re.compile(r"https://r\d+---sn-[\w]{10}-[\w]{3,5}.googlevideo.com/videoplayback?.*")
_HEADERS = {"User-Agent": "Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/69.0",
"DNT": "1",
"Accept-Encoding": "gzip, deflate"}
Quality = {137: "1080p", 136: "720p", 135: "480p", 134: "360p",
133: "240p", 160: "144p", 0: "0p", 18: "360p", 22: "720p"}
class YouTubeException(Exception):
pass
class YouTube:
""" Helper class for working with YouTube service. """
_YT_INSTANCE = None
_VIDEO_INFO_LINK = "https://youtube.com/get_video_info?video_id={}&hl=en"
VIDEO_LINK = "https://www.youtube.com/watch?v={}"
def __init__(self, settings, callback):
self._settings = settings
self._yt_dl = None
self._callback = callback
if self._settings.enable_yt_dl:
try:
self._yt_dl = YouTubeDL.get_instance(self._settings, callback=self._callback)
except YouTubeException:
pass # NOP
@classmethod
def get_instance(cls, settings, callback=log):
if not cls._YT_INSTANCE:
cls._YT_INSTANCE = YouTube(settings, callback)
return cls._YT_INSTANCE
@staticmethod
def is_yt_video_link(url):
return re.match(_YT_VIDEO_PATTERN, url)
@staticmethod
def get_yt_id(url):
""" Returns video id or None """
yt = re.search(_YT_PATTERN, url)
if yt:
return yt.group(1)
@staticmethod
def get_yt_list_id(url):
""" Returns playlist id or None """
yt = re.search(_YT_LIST_PATTERN, url)
if yt:
return yt.group(1)
def get_yt_link(self, video_id, url=None, skip_errors=False):
""" Getting link to YouTube video by id or URL.
Returns tuple from the video links dict and title.
"""
if self._settings.enable_yt_dl and url:
if not self._yt_dl:
self._yt_dl = YouTubeDL.get_instance(self._settings, self._callback)
return self._yt_dl.get_yt_link(url, skip_errors)
return self.get_yt_link_by_id(video_id)
@staticmethod
def get_yt_link_by_id(video_id):
""" Getting link to YouTube video by id.
Returns tuple from the video links dict and title.
"""
req = Request(YouTube._VIDEO_INFO_LINK.format(video_id), headers=_HEADERS)
with urlopen(req, timeout=2) as resp:
data = unquote(gzip.decompress(resp.read()).decode("utf-8")).split("&")
out = {k: v for k, sep, v in (str(d).partition("=") for d in map(unquote, data))}
player_resp = out.get("player_response", None)
if player_resp:
try:
resp = json.loads(player_resp)
except JSONDecodeError as e:
log("{}: Parsing player response error: {}".format(__class__.__name__, e))
else:
det = resp.get("videoDetails", None)
title = det.get("title", None) if det else None
streaming_data = resp.get("streamingData", None)
fmts = streaming_data.get("formats", None) if streaming_data else None
if fmts:
urls = {Quality[i["itag"]]: i["url"] for i in
filter(lambda i: i.get("itag", -1) in Quality, fmts) if "url" in i}
if urls and title:
return urls, title.replace("+", " ")
stream_map = out.get("url_encoded_fmt_stream_map", None)
if stream_map:
s_map = {k: v for k, sep, v in (str(d).partition("=") for d in stream_map.split("&"))}
url, title = s_map.get("url", None), out.get("title", None)
url, title = unquote(url) if url else "", title.replace("+", " ") if title else ""
if url and title:
return {Quality[0]: url}, title.replace("+", " ")
rsn = out.get("reason", None)
rsn = rsn.replace("+", " ") if rsn else ""
log("{}: Getting link to video with id {} filed! Cause: {}".format(__class__.__name__, video_id, rsn))
return None, rsn
class PlayListParser(HTMLParser):
""" Very simple parser to handle YouTube playlist pages. """
def __init__(self):
super().__init__()
self._is_header = False
self._header = ""
self._playlist = []
self._is_script = False
def handle_starttag(self, tag, attrs):
if tag == "script":
self._is_script = True
def handle_data(self, data):
if self._is_script:
data = data.lstrip()
if data.startswith('window["ytInitialData"] = '):
data = data.split(";")[0].lstrip('window["ytInitialData"] = ')
try:
resp = json.loads(data)
except JSONDecodeError as e:
log("{}: Parsing data error: {}".format(__class__.__name__, e))
else:
sb = resp.get("sidebar", None)
if sb:
for t in [t["runs"][0] for t in flat("title", sb) if "runs" in t]:
txt = t.get("text", None)
if txt:
self._header = txt
break
ct = resp.get("contents", None)
if ct:
for d in [(d.get("title", {}).get("runs", [{}])[0].get("text", ""),
d.get("videoId", "")) for d in flat("playlistVideoRenderer", ct)]:
self._playlist.append(d)
self._is_script = False
def error(self, message):
log("{} Parsing error: {}".format(__class__.__name__, message))
@property
def header(self):
return self._header
@property
def playlist(self):
return self._playlist
@staticmethod
def get_yt_playlist(play_list_id):
""" Getting YouTube playlist by id.
returns tuple from the playlist header and list of tuples (title, video id)
"""
request = Request("https://www.youtube.com/playlist?list={}&hl=en".format(play_list_id), headers=_HEADERS)
with urlopen(request, timeout=2) as resp:
data = gzip.decompress(resp.read()).decode("utf-8")
parser = PlayListParser()
parser.feed(data)
return parser.header, parser.playlist
class YouTubeDL:
""" Utility class [experimental] for working with youtube-dl.
[https://github.com/ytdl-org/youtube-dl]
"""
_DL_INSTANCE = None
_DownloadError = None
_LATEST_RELEASE_URL = "https://api.github.com/repos/ytdl-org/youtube-dl/releases/latest"
_OPTIONS = {"noplaylist": True, # Single video instead of a playlist [ignoring playlist in URL].
"quiet": True, # Do not print messages to stdout.
"simulate": True, # Do not download the video files.
"cookiefile": "cookies.txt"} # File name where cookies should be read from and dumped to.
def __init__(self, settings, callback):
self._path = settings.default_data_path + "tools/"
self._update = settings.enable_yt_dl_update
self._supported = {"22", "18"}
self._dl = None
self._callback = callback
self._download_exception = None
self._is_update_process = False
self.init()
@classmethod
def get_instance(cls, settings, callback=print):
if not cls._DL_INSTANCE:
cls._DL_INSTANCE = YouTubeDL(settings, callback)
return cls._DL_INSTANCE
def init(self):
if not os.path.isfile(self._path + "youtube_dl/version.py"):
self.get_latest_release()
if self._path not in sys.path:
sys.path.append(self._path)
self.init_dl()
def init_dl(self):
try:
import youtube_dl
except ModuleNotFoundError as e:
log("YouTubeDLHelper error: {}".format(str(e)))
raise YouTubeException(e)
except ImportError as e:
log("YouTubeDLHelper error: {}".format(str(e)))
else:
if self._update:
if hasattr(youtube_dl.version, "__version__"):
l_ver = self.get_last_release_id()
cur_ver = youtube_dl.version.__version__
if l_ver and youtube_dl.version.__version__ < l_ver:
msg = "youtube-dl has new release!\nCurrent: {}. Last: {}.".format(cur_ver, l_ver)
show_notification(msg)
log(msg)
self._callback(msg, False)
self.get_latest_release()
self._DownloadError = youtube_dl.utils.DownloadError
self._dl = youtube_dl.YoutubeDL(self._OPTIONS)
msg = "youtube-dl initialized..."
show_notification(msg)
log(msg)
@staticmethod
def get_last_release_id():
""" Getting last release id. """
url = "https://api.github.com/repos/ytdl-org/youtube-dl/releases/latest"
try:
with urlopen(url, timeout=10) as resp:
return json.loads(resp.read().decode("utf-8")).get("tag_name", "0")
except URLError as e:
log("YouTubeDLHelper error [get last release id]: {}".format(e))
def get_latest_release(self):
try:
self._is_update_process = True
log("Getting the last youtube-dl release...")
with urlopen(YouTubeDL._LATEST_RELEASE_URL, timeout=10) as resp:
r = json.loads(resp.read().decode("utf-8"))
zip_url = r.get("zipball_url", None)
if zip_url:
zip_file = self._path + "yt.zip"
os.makedirs(os.path.dirname(self._path), exist_ok=True)
f_name, headers = urlretrieve(zip_url, filename=zip_file)
import zipfile
with zipfile.ZipFile(f_name) as arch:
if os.path.isdir(self._path):
shutil.rmtree(self._path)
else:
os.makedirs(os.path.dirname(self._path), exist_ok=True)
for info in arch.infolist():
pref, sep, f = info.filename.partition("/youtube_dl/")
if sep:
arch.extract(info.filename)
shutil.move(info.filename, "{}{}{}".format(self._path, sep, f))
shutil.rmtree(pref)
msg = "Getting the last youtube-dl release is done!"
show_notification(msg)
log(msg)
self._callback(msg, False)
return True
except URLError as e:
log("YouTubeDLHelper error: {}".format(e))
raise YouTubeException(e)
finally:
self._is_update_process = False
def get_yt_link(self, url, skip_errors=False):
""" Returns tuple from the video links [dict] and title. """
if self._is_update_process:
self._callback("Update process. Please wait.", False)
return {}, ""
try:
info = self._dl.extract_info(url, download=False)
except URLError as e:
log(str(e))
raise YouTubeException(e)
except self._DownloadError as e:
log(str(e))
if not skip_errors:
raise YouTubeException(e)
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 flat(key, d):
for k, v in d.items():
if k == key:
yield v
elif isinstance(v, dict):
yield from flat(key, v)
elif isinstance(v, list):
for el in v:
if isinstance(el, dict):
yield from flat(key, el)
if __name__ == "__main__":
pass

View File

@@ -1,20 +1 @@
import gi
import os
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
# path to *.glade files
UI_RESOURCES_PATH = "app/ui/" if os.path.exists("app/ui/") else "/usr/share/demoneditor/app/ui/"
theme = Gtk.IconTheme.get_default()
_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("system-lock-screen", 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
if __name__ == "__main__":
pass

151
app/ui/app_menu_bar.ui Normal file
View File

@@ -0,0 +1,151 @@
<?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">Import from Web</attribute>
<attribute name="action">app.on_import_from_web</attribute>
</item>
<item>
<attribute name="label" translatable="yes">New empty configuration</attribute>
<attribute name="action">app.on_new_configuration</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Open</attribute>
<attribute name="action">app.on_data_open</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Extract...</attribute>
<attribute name="action">app.on_archive_open</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Save</attribute>
<attribute name="action">app.on_data_save</attribute>
</item>
</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>

221
app/ui/backup.py Normal file
View File

@@ -0,0 +1,221 @@
import os
import shutil
import tempfile
import time
import zipfile
from datetime import datetime
from enum import Enum
from app.commons import run_idle
from app.settings import SettingsType
from app.ui.dialogs import show_dialog, DialogType
from app.ui.main_helper import append_text_to_tview
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK
class RestoreType(Enum):
BOUQUETS = 0
ALL = 1
class BackupDialog:
def __init__(self, transient, settings, callback):
handlers = {"on_restore_bouquets": self.on_restore_bouquets,
"on_restore_all": self.on_restore_all,
"on_remove": self.on_remove,
"on_view_popup_menu": self.on_view_popup_menu,
"on_info_button_toggled": self.on_info_button_toggled,
"on_info_bar_close": self.on_info_bar_close,
"on_cursor_changed": self.on_cursor_changed,
"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)
self._settings = settings
self._s_type = settings.setting_type
self._data_path = self._settings.data_local_path
self._backup_path = self._settings.backup_local_path or self._data_path + "backup/"
self._open_data_callback = callback
self._dialog_window = builder.get_object("dialog_window")
self._dialog_window.set_transient_for(transient)
self._model = builder.get_object("main_list_store")
self._main_view = builder.get_object("main_view")
self._text_view = builder.get_object("text_view")
self._text_view_scrolled_window = builder.get_object("text_view_scrolled_window")
self._info_check_button = builder.get_object("info_check_button")
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("message_label")
# Setting the last size of the dialog window if it was saved
window_size = self._settings.get("backup_tool_window_size")
if window_size:
self._dialog_window.resize(*window_size)
self.init_data()
def show(self):
self._dialog_window.show()
@run_idle
def init_data(self):
if os.path.isdir(self._backup_path):
for file in filter(lambda x: x.endswith(".zip"), os.listdir(self._backup_path)):
self._model.append((file.rstrip(".zip"), False))
else:
os.makedirs(os.path.dirname(self._backup_path), exist_ok=True)
def on_restore_bouquets(self, item):
self.restore(RestoreType.BOUQUETS)
def on_restore_all(self, item):
self.restore(RestoreType.ALL)
def on_remove(self, item):
model, paths = self._main_view.get_selection().get_selected_rows()
if not paths:
show_dialog(DialogType.ERROR, self._dialog_window, "No selected item!")
return
if show_dialog(DialogType.QUESTION, self._dialog_window) == Gtk.ResponseType.CANCEL:
return
itrs_to_delete = []
try:
for itr in map(model.get_iter, paths):
file_name = model.get_value(itr, 0)
os.remove("{}{}{}".format(self._backup_path, file_name, ".zip"))
itrs_to_delete.append(itr)
except FileNotFoundError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
list(map(model.remove, itrs_to_delete))
def on_view_popup_menu(self, menu, event):
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
menu.popup(None, None, None, None, event.button, event.time)
def on_info_button_toggled(self, button):
active = button.get_active()
self._text_view_scrolled_window.set_visible(active)
if active:
self.on_cursor_changed(self._main_view)
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(text)
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
def on_cursor_changed(self, view):
if not self._info_check_button.get_active():
return
model, paths = view.get_selection().get_selected_rows()
if paths:
try:
file_name = self._backup_path + model.get_value(model.get_iter(paths[0]), 0) + ".zip"
created = time.ctime(os.path.getctime(file_name))
self._text_view.get_buffer().set_text(
"Created: {}\n********** Files: **********\n".format(created))
with zipfile.ZipFile(file_name) as zip_file:
for name in zip_file.namelist():
append_text_to_tview(name + "\n", self._text_view)
except FileNotFoundError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
self._text_view.get_buffer().set_text("")
def restore(self, restore_type):
model, paths = self._main_view.get_selection().get_selected_rows()
if not paths:
show_dialog(DialogType.ERROR, self._dialog_window, "No selected item!")
return
if len(paths) > 1:
show_dialog(DialogType.ERROR, self._dialog_window, "Please, select only one item!")
return
if show_dialog(DialogType.QUESTION, self._dialog_window) == Gtk.ResponseType.CANCEL:
return
file_name = model.get_value(model.get_iter(paths[0]), 0)
full_file_name = self._backup_path + file_name + ".zip"
try:
if restore_type is RestoreType.ALL:
clear_data_path(self._data_path)
shutil.unpack_archive(full_file_name, self._data_path)
elif restore_type is RestoreType.BOUQUETS:
tmp_dir = tempfile.gettempdir() + "/" + file_name
cond = (".tv", ".radio") if self._s_type is SettingsType.ENIGMA_2 else "bouquets.xml"
shutil.unpack_archive(full_file_name, tmp_dir)
for file in filter(lambda f: f.endswith(cond), os.listdir(self._data_path)):
os.remove(os.path.join(self._data_path, file))
for file in filter(lambda f: f.endswith(cond), os.listdir(tmp_dir)):
shutil.move(os.path.join(tmp_dir, file), self._data_path + file)
shutil.rmtree(tmp_dir)
except FileNotFoundError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
self.show_info_message("Done!", Gtk.MessageType.INFO)
self._open_data_callback(self._data_path)
def on_resize(self, window):
if self._settings:
self._settings.add("backup_tool_window_size", window.get_size())
def on_key_release(self, view, event):
""" Handling keystrokes """
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK
if key is KeyboardKey.DELETE:
self.on_remove(view)
elif ctrl and key is KeyboardKey.E:
self.restore(RestoreType.ALL)
elif ctrl and key is KeyboardKey.R:
self.restore(RestoreType.BOUQUETS)
def backup_data(path, backup_path, move=True):
""" Creating data backup from a folder at the specified path
Returns full path to the compressed file.
"""
backup_path = "{}{}/".format(backup_path, datetime.now().strftime("%Y-%m-%d_%H-%M-%S"))
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
os.makedirs(os.path.dirname(path), exist_ok=True)
# backup files in data dir(skipping dirs and satellites.xml)
for file in filter(lambda f: f != "satellites.xml" and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
src, dst = os.path.join(path, file), backup_path + file
shutil.move(src, dst) if move else shutil.copy(src, dst)
# compressing to zip and delete remaining files
zip_file = shutil.make_archive(backup_path, "zip", backup_path)
shutil.rmtree(backup_path)
return zip_file
def restore_data(src, dst):
""" Unpacks backup data. """
clear_data_path(dst)
shutil.unpack_archive(src, dst)
def clear_data_path(path):
""" Clearing data at the specified path excluding satellites.xml file """
for file in filter(lambda f: f != "satellites.xml" and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
os.remove(os.path.join(path, file))
if __name__ == "__main__":
pass

366
app/ui/backup_dialog.glade Normal file
View File

@@ -0,0 +1,366 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1
The MIT License (MIT)
Copyright (c) 2018-2020 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface>
<requires lib="gtk+" version="3.16"/>
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="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 -->
<column type="gchararray"/>
<!-- column-name selected -->
<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>
<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>
<property name="can_focus">True</property>
<property name="wide_handle">True</property>
<child>
<object class="GtkScrolledWindow" id="main_view_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="main_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="model">main_list_store</property>
<property name="headers_visible">False</property>
<property name="search_column">0</property>
<property name="rubber_banding">True</property>
<property name="activate_on_single_click">True</property>
<signal name="button-press-event" handler="on_view_popup_menu" object="popup_menu" swapped="no"/>
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
<signal name="key-release-event" handler="on_key_release" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="main_view_selection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="backup_date_column">
<property name="title" translatable="yes">Backup</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<property name="reorderable">True</property>
<property name="sort_column_id">0</property>
<child>
<object class="GtkCellRendererText" id="date_render">
<property name="xpad">10</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="text_view_scrolled_window">
<property name="can_focus">False</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="text_view">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="pixels_above_lines">5</property>
<property name="editable">False</property>
<property name="left_margin">10</property>
<property name="right_margin">10</property>
<property name="indent">10</property>
<property name="cursor_visible">False</property>
<property name="accepts_tab">False</property>
</object>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</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="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>
<property name="show_close_button">True</property>
<signal name="response" handler="on_info_bar_close" swapped="no"/>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="spacing">6</property>
<property name="layout_style">end</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="spacing">16</property>
<child>
<object class="GtkLabel" id="message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">message</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

1849
app/ui/control.glade Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,680 @@
""" 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 get_dialogs_string, show_dialog, DialogType
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column
from ..commons import run_task, run_with_delay, log, run_idle
from ..connections import HttpAPI
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"
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
@property
def title(self):
return self._title
@property
def desc(self):
return self._desc
@property
def time_header(self):
return self._time_header
class TimerRow(Gtk.ListBoxRow):
_UI_PATH = UI_RESOURCES_PATH + "timer_row.glade"
def __init__(self, timer, **properties):
super().__init__(**properties)
self._timer = timer
builder = Gtk.Builder()
builder.add_from_string(get_dialogs_string(self._UI_PATH))
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
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}
builder = Gtk.Builder()
builder.add_from_file(UI_RESOURCES_PATH + "control.glade")
builder.connect_signals(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()
builder.get_object("stack_switcher").set_visible(settings.is_enable_experimental)
builder.get_object("epg_box").set_visible(settings.is_enable_experimental)
builder.get_object("timers_box").set_visible(settings.is_enable_experimental)
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)
@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 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, 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: EpgRow):
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(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 = 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:
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)

40
app/ui/default_style.css Normal file
View File

@@ -0,0 +1,40 @@
* {
-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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,72 +1,186 @@
""" Common module for showing dialogs """
import gettext
from enum import Enum
from functools import lru_cache
from pathlib import Path
from . import Gtk, UI_RESOURCES_PATH
from app.commons import run_idle
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN, IS_GNOME_SESSION
class Dialog(Enum):
MESSAGE = """
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.16"/>
<object class="GtkMessageDialog" id="message_dialog">
<property name="use-header-bar">{use_header}</property>
<property name="can_focus">False</property>
<property name="modal">True</property>
<property name="default_width">320</property>
<property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="gravity">center</property>
<property name="message_type">{message_type}</property>
<property name="buttons">{buttons_type}</property>
</object>
</interface>
"""
class Action(Enum):
EDIT = 0
ADD = 1
class DialogType(Enum):
INPUT = "input_dialog"
MESSAGE = ""
CHOOSER = "path_chooser_dialog"
ERROR = "error_dialog"
QUESTION = "question_dialog"
ABOUT = "about_dialog"
INPUT = "input"
CHOOSER = "chooser"
ERROR = "error"
QUESTION = "question"
INFO = "info"
ABOUT = "about"
WAIT = "wait"
def __str__(self):
return self.value
def show_dialog(dialog_type: DialogType, transient, text=None, options=None, action_type=None, file_filter=None):
""" Shows dialogs by name """
class WaitDialog:
def __init__(self, transient, text=None):
builder, dialog = get_dialog_from_xml(DialogType.WAIT, transient)
self._dialog = dialog
self._dialog.set_transient_for(transient)
self._label = builder.get_object("wait_dialog_label")
self._default_text = text or self._label.get_text()
def show(self, text=None):
self.set_text(text)
self._dialog.show()
@run_idle
def set_text(self, text):
self._label.set_text(get_message(text or self._default_text))
@run_idle
def hide(self):
self._dialog.hide()
@run_idle
def destroy(self):
self._dialog.destroy()
def show_dialog(dialog_type, transient, text=None, settings=None, action_type=None, file_filter=None, buttons=None,
title=None):
""" 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, buttons, title)
elif dialog_type is DialogType.INPUT:
return get_input_dialog(transient, text)
elif dialog_type is DialogType.QUESTION:
action = action_type if action_type else Gtk.ButtonsType.OK_CANCEL
return get_message_dialog(transient, DialogType.QUESTION, action, text or "Are you sure?")
elif dialog_type is DialogType.ABOUT:
return get_about_dialog(transient)
def get_chooser_dialog(transient, settings, name, patterns, title=None):
file_filter = Gtk.FileFilter()
file_filter.set_name(name)
for p in patterns:
file_filter.add_pattern(p)
return show_dialog(dialog_type=DialogType.CHOOSER,
transient=transient,
settings=settings,
action_type=Gtk.FileChooserAction.OPEN,
file_filter=file_filter,
title=title)
def get_file_chooser_dialog(transient, text, settings, action_type, file_filter, buttons=None, title=None):
action_type = Gtk.FileChooserAction.SELECT_FOLDER if action_type is None else action_type
dialog = Gtk.FileChooserNative.new(get_message(title) if title else "", transient, action_type)
dialog.set_create_folders(False)
dialog.set_modal(True)
if file_filter is not None:
dialog.add_filter(file_filter)
dialog.set_current_folder(settings.data_local_path)
response = dialog.run()
if response == Gtk.ResponseType.ACCEPT:
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
def get_input_dialog(transient, text):
builder, dialog = get_dialog_from_xml(DialogType.INPUT, transient, use_header=IS_GNOME_SESSION)
entry = builder.get_object("input_entry")
entry.set_text(text if text else "")
response = dialog.run()
txt = entry.get_text()
dialog.destroy()
return txt if response == Gtk.ResponseType.OK else Gtk.ResponseType.CANCEL
def get_message_dialog(transient, message_type, buttons_type, text):
builder = Gtk.Builder()
builder.add_from_file(UI_RESOURCES_PATH + "dialogs.glade")
dialog = builder.get_object(dialog_type.value)
builder.set_translation_domain(TEXT_DOMAIN)
dialog_str = Dialog.MESSAGE.value.format(use_header=0, message_type=message_type, buttons_type=int(buttons_type))
builder.add_from_string(dialog_str)
dialog = builder.get_object("message_dialog")
dialog.set_transient_for(transient)
if dialog_type is DialogType.CHOOSER and options:
if action_type is not None:
dialog.set_action(action_type)
if file_filter is not None:
dialog.add_filter(file_filter)
path = options.get("data_dir_path")
dialog.set_current_folder(path)
response = dialog.run()
if response == -12: # -12 for fix assertion 'gtk_widget_get_can_default (widget)' failed
if dialog.get_filename():
path = dialog.get_filename()
if action_type is not Gtk.FileChooserAction.OPEN:
path = path + "/"
response = path
dialog.destroy()
return response
if dialog_type is DialogType.INPUT:
entry = builder.get_object("input_entry")
entry.set_text(text if text else "")
response = dialog.run()
txt = entry.get_text()
dialog.destroy()
return txt if response == Gtk.ResponseType.OK else Gtk.ResponseType.CANCEL
if text:
dialog.set_markup(text)
dialog.set_markup(get_message(text))
response = dialog.run()
dialog.destroy()
return response
def get_chooser_dialog(transient, options, pattern, name):
file_filter = Gtk.FileFilter()
file_filter.add_pattern(pattern)
file_filter.set_name(name)
return show_dialog(dialog_type=DialogType.CHOOSER,
transient=transient,
options=options,
action_type=Gtk.FileChooserAction.OPEN,
file_filter=file_filter)
def get_about_dialog(transient):
builder, dialog = get_dialog_from_xml(DialogType.ABOUT, transient)
dialog.set_transient_for(transient)
response = dialog.run()
dialog.destroy()
return response
def get_dialog_from_xml(dialog_type, transient, use_header=0, title=""):
dialog_name = dialog_type.value + "_dialog"
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
dialog_str = get_dialogs_string(UI_RESOURCES_PATH + "dialogs.glade").format(use_header=use_header, title=title)
builder.add_objects_from_string(dialog_str, (dialog_name,))
dialog = builder.get_object(dialog_name)
dialog.set_transient_for(transient)
return builder, dialog
def get_message(message):
""" returns translated message """
return gettext.dgettext(TEXT_DOMAIN, message)
@lru_cache(maxsize=5)
def get_dialogs_string(path):
with open(path, "r") as f:
return "".join(f)
if __name__ == "__main__":

758
app/ui/download_dialog.glade Executable file
View File

@@ -0,0 +1,758 @@
<?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-description Enigma2 channel and satellites list editor for macOS. -->
<!-- interface-copyright 2018-2020 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="can_focus">False</property>
<property name="title" translatable="yes">FTP-transfer</property>
<property name="resizable">False</property>
<property name="modal">True</property>
<property name="window_position">center-on-parent</property>
<property name="icon_name">mail-send-receive</property>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="gravity">center</property>
<child>
<placeholder/>
</child>
<child>
<object class="GtkBox" id="main_dialog_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="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_bottom">5</property>
<property name="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkBox" id="main_settings_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkGrid" id="main_settings_bo">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">2</property>
<property name="column_spacing">2</property>
<property name="column_homogeneous">True</property>
<child>
<object class="GtkLabel" id="ip_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Receiver IP:</property>
<property name="xalign">0.10000000149011612</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="host_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="max_width_chars">10</property>
<property name="text">127.0.0.1</property>
<property name="caps_lock_warning">False</property>
<property name="primary_icon_name">network-transmit-receive-symbolic</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="data_path_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Current data path:</property>
<property name="xalign">0.10000000149011612</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="data_path_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="text">data/</property>
<property name="caps_lock_warning">False</property>
<property name="primary_icon_name">folder-open-symbolic</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="extra_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<child>
<object class="GtkCheckButton" id="remove_unused_check_button">
<property name="label" translatable="yes">Remove unused bouquets</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_remove_unused_bouquets_toggled" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="use_http_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel" id="use_http_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Use HTTP</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="use_http_switch">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="tooltip_text" translatable="yes">Use http to reload data in the receiver.</property>
<property name="active">True</property>
<signal name="state-set" handler="on_use_http_state_set" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="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>
<property name="can_focus">True</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="margin_bottom">1</property>
<property name="resize_toplevel">True</property>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="height_request">120</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="text_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="left_margin">5</property>
<property name="right_margin">5</property>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="expander_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Extra:</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">7</property>
</packing>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="can_focus">False</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="margin_bottom">1</property>
<property name="show_close_button">True</property>
<signal name="response" handler="on_info_bar_close" swapped="no"/>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="homogeneous">True</property>
<property name="layout_style">expand</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="spacing">16</property>
<child>
<object class="GtkLabel" id="info_bar_message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Info</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">8</property>
</packing>
</child>
</object>
</child>
</object>
<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

@@ -1,88 +1,192 @@
from app.commons import run_idle, run_task
from app.ftp import download_data, DownloadDataType, upload_data
from app.properties import Profile
from . import Gtk, UI_RESOURCES_PATH
from .dialogs import show_dialog, DialogType
import os
from gi.repository import GLib
def show_download_dialog(transient, options, open_data, profile=Profile.ENIGMA_2):
dialog = DownloadDialog(transient, options, open_data, profile)
dialog.run()
dialog.destroy()
from app.commons import run_idle, run_task, log
from app.connections import download_data, DownloadType, upload_data
from app.settings import SettingsType
from app.ui.backup import backup_data, restore_data
from app.ui.main_helper import append_text_to_tview
from app.ui.settings_dialog import show_settings_dialog
from .dialogs import show_dialog, DialogType, get_message
from .uicommons import Gtk, UI_RESOURCES_PATH
class DownloadDialog:
def __init__(self, transient, properties, open_data, profile):
self._properties = properties
self._open_data = open_data
self._profile = profile
def __init__(self, transient, settings, open_data_callback, update_settings_callback):
self._s_type = settings.setting_type
self._settings = settings
self._open_data_callback = open_data_callback
self._update_settings_callback = update_settings_callback
handlers = {"on_receive": self.on_receive,
"on_send": self.on_send,
"on_settings_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_objects_from_file(UI_RESOURCES_PATH + "dialogs.glade", ("download_dialog",))
builder.add_from_file(UI_RESOURCES_PATH + "download_dialog.glade")
builder.connect_signals(handlers)
self._dialog = builder.get_object("download_dialog")
self._dialog.set_transient_for(transient)
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")
self._message_label = builder.get_object("info_bar_message_label")
self._host_entry = builder.get_object("host_entry").set_text(properties["host"])
self._data_path_entry = builder.get_object("data_path_entry").set_text(properties["data_dir_path"])
self._text_view = builder.get_object("text_view")
self._expander = builder.get_object("expander")
self._host_entry = builder.get_object("host_entry")
self._data_path_entry = builder.get_object("data_path_entry")
self._remove_unused_check_button = builder.get_object("remove_unused_check_button")
self._all_radio_button = builder.get_object("all_radio_button")
self._bouquets_radio_button = builder.get_object("bouquets_radio_button")
self._satellites_radio_button = builder.get_object("satellites_radio_button")
# self._dialog.get_content_area().set_border_width(0)
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")
self._profile_combo_box = builder.get_object("profile_combo_box")
self.init_settings()
def show(self):
self._dialog_window.show()
def init_settings(self):
self.update_profiles()
self.init_ui_settings()
def init_ui_settings(self):
self._host_entry.set_text(self._settings.host)
self._data_path_entry.set_text(self._settings.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)
def update_profiles(self):
self._profile_combo_box.remove_all()
for p in self._settings.profiles:
self._profile_combo_box.append(p, p)
self._profile_combo_box.set_active_id(self._settings.current_profile)
@run_idle
def on_receive(self, item):
self.download(True, d_type=self.get_download_type())
self.download(True, self.get_download_type())
@run_idle
def on_send(self, item):
if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.CANCEL:
self.download(d_type=self.get_download_type())
if show_dialog(DialogType.QUESTION, self._dialog_window) != Gtk.ResponseType.CANCEL:
self.download(False, self.get_download_type())
def get_download_type(self):
download_type = DownloadDataType.ALL
download_type = DownloadType.ALL
if self._bouquets_radio_button.get_active():
download_type = DownloadDataType.BOUQUETS
download_type = DownloadType.BOUQUETS
elif self._satellites_radio_button.get_active():
download_type = DownloadDataType.SATELLITES
download_type = DownloadType.SATELLITES
elif self._webtv_radio_button.get_active():
download_type = DownloadType.WEBTV
return download_type
def run(self):
return self._dialog.run()
def destroy(self):
self._dialog.destroy()
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:
self._s_type = self._settings.setting_type
self.update_profiles()
gen = self._update_settings_callback()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
for button in self._settings_buttons_box.get_children():
if button.get_active():
self.on_settings_button(button)
break
def on_profile_changed(self, box):
active = box.get_active_text()
if active in self._settings.profiles:
self._settings.current_profile = active
self._profile_combo_box.set_active_id(active)
self._s_type = self._settings.setting_type
self.init_ui_settings()
def on_use_http_state_set(self, button, state):
self._settings.use_http = state
def on_remove_unused_bouquets_toggled(self, button):
self._settings.remove_unused_bouquets = button.get_active()
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@run_idle
@run_task
def download(self, download=False, d_type=DownloadDataType.ALL):
def download(self, download, d_type):
""" Download/upload data from/to receiver """
GLib.idle_add(self._expander.set_expanded, True)
self.clear_output()
backup, backup_src, data_path = self._settings.backup_before_downloading, None, None
try:
if download:
download_data(properties=self._properties, download_type=d_type)
if backup and d_type is not DownloadType.SATELLITES:
data_path = self._settings.data_local_path or self._data_path_entry.get_text()
os.makedirs(os.path.dirname(data_path), exist_ok=True)
backup_path = self._settings.backup_local_path or data_path + "backup/"
backup_src = backup_data(data_path, backup_path, d_type is DownloadType.ALL)
download_data(settings=self._settings, download_type=d_type, callback=self.append_output)
else:
self.show_info_message("Please, wait...", Gtk.MessageType.INFO)
upload_data(properties=self._properties,
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
upload_data(settings=self._settings,
download_type=d_type,
remove_unused=self._remove_unused_check_button.get_active(),
profile=self._profile,
callback=lambda: self.show_info_message("Done!", Gtk.MessageType.INFO))
callback=self.append_output,
done_callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO),
use_http=self._use_http_switch.get_active())
except Exception as e:
message = str(getattr(e, "message", str(e)))
self.show_info_message(message, Gtk.MessageType.ERROR)
msg = "Downloading data error: {}"
log(msg.format(e), debug=self._settings.debug_mode, fmt_message=msg)
self.show_info_message(str(e), Gtk.MessageType.ERROR)
if all((download, backup, data_path)):
restore_data(backup_src, data_path)
else:
if download and d_type is not DownloadDataType.SATELLITES:
self._open_data()
if download and d_type is not DownloadType.SATELLITES:
GLib.idle_add(self._open_data_callback)
@run_idle
def show_info_message(self, text, message_type):
@@ -90,6 +194,14 @@ class DownloadDialog:
self._info_bar.set_message_type(message_type)
self._message_label.set_text(text)
@run_idle
def append_output(self, text):
append_text_to_tview(text, self._text_view)
@run_idle
def clear_output(self):
self._text_view.get_buffer().set_text("")
if __name__ == "__main__":
pass

1345
app/ui/epg_dialog.glade Normal file

File diff suppressed because it is too large Load Diff

548
app/ui/epg_dialog.py Normal file
View File

@@ -0,0 +1,548 @@
import gzip
import locale
import os
import re
import shutil
import urllib.request
from enum import Enum
from urllib.error import HTTPError, URLError
from gi.repository import GLib
from app.commons import run_idle
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 .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
class RefsSource(Enum):
SERVICES = 0
XML = 1
class EpgDialog:
def __init__(self, transient, settings, services, bouquet, fav_model, bouquet_name):
handlers = {"on_close_dialog": self.on_close_dialog,
"on_apply": self.on_apply,
"on_update": self.on_update,
"on_save_to_xml": self.on_save_to_xml,
"on_auto_configuration": self.on_auto_configuration,
"on_filter_toggled": self.on_filter_toggled,
"on_filter_changed": self.on_filter_changed,
"on_info_bar_close": self.on_info_bar_close,
"on_popup_menu": on_popup_menu,
"on_bouquet_popup_menu": self.on_bouquet_popup_menu,
"on_copy_ref": self.on_copy_ref,
"on_assign_ref": self.on_assign_ref,
"on_reset": self.on_reset,
"on_list_reset": self.on_list_reset,
"on_drag_begin": self.on_drag_begin,
"on_drag_data_get": self.on_drag_data_get,
"on_drag_data_received": self.on_drag_data_received,
"on_resize": self.on_resize,
"on_names_source_changed": self.on_names_source_changed,
"on_options_save": self.on_options_save,
"on_use_web_source_switch": self.on_use_web_source_switch,
"on_enable_filtering_switch": self.on_enable_filtering_switch,
"on_update_on_start_switch": self.on_update_on_start_switch,
"on_field_icon_press": self.on_field_icon_press,
"on_key_release": self.on_key_release}
self._services = {}
self._ex_services = services
self._ex_fav_model = fav_model
self._settings = settings
self._bouquet = bouquet
self._bouquet_name = bouquet_name
self._current_ref = []
self._enable_dat_filter = False
self._use_web_source = False
self._update_epg_data_on_start = False
self._refs_source = RefsSource.SERVICES
self._show_tooltips = True
self._download_xml_is_active = False
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_from_file(UI_RESOURCES_PATH + "epg_dialog.glade")
builder.connect_signals(handlers)
self._dialog = builder.get_object("epg_dialog_window")
self._dialog.set_transient_for(transient)
self._source_view = builder.get_object("source_view")
self._bouquet_view = builder.get_object("bouquet_view")
self._bouquet_model = builder.get_object("bouquet_list_store")
self._services_model = builder.get_object("services_list_store")
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label")
self._assign_ref_popup_item = builder.get_object("bouquet_assign_ref_popup_item")
self._left_header_box = builder.get_object("left_header_box")
self._xml_download_progress_bar = builder.get_object("xml_download_progress_bar")
# Filter
self._filter_bar = builder.get_object("filter_bar")
self._filter_entry = builder.get_object("filter_entry")
self._services_filter_model = builder.get_object("services_filter_model")
self._services_filter_model.set_visible_func(self.services_filter_function)
# Info
self._source_count_label = builder.get_object("source_count_label")
self._source_info_label = builder.get_object("source_info_label")
self._bouquet_count_label = builder.get_object("bouquet_count_label")
self._bouquet_epg_count_label = builder.get_object("bouquet_epg_count_label")
# Options
self._xml_radiobutton = builder.get_object("xml_radiobutton")
self._xml_chooser_button = builder.get_object("xml_chooser_button")
self._names_source_box = builder.get_object("names_source_box")
self._web_source_box = builder.get_object("web_source_box")
self._use_web_source_switch = builder.get_object("use_web_source_switch")
self._url_to_xml_entry = builder.get_object("url_to_xml_entry")
self._enable_filtering_switch = builder.get_object("enable_filtering_switch")
self._epg_dat_path_entry = builder.get_object("epg_dat_path_entry")
self._epg_dat_stb_path_entry = builder.get_object("epg_dat_stb_path_entry")
self._update_on_start_switch = builder.get_object("update_on_start_switch")
self._epg_dat_source_box = builder.get_object("epg_dat_source_box")
# Setting the last size of the dialog window
window_size = self._settings.get("epg_tool_window_size")
if window_size:
self._dialog.resize(*window_size)
self.init_drag_and_drop()
self.on_update()
def show(self):
self._dialog.show()
def on_close_dialog(self, window, event):
self._download_xml_is_active = False
@run_idle
def on_apply(self, item):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
self._bouquet.clear()
list(map(self._bouquet.append, [r[Column.FAV_ID] for r in self._bouquet_model]))
for index, row in enumerate(self._ex_fav_model):
fav_id = self._bouquet[index]
row[Column.FAV_ID] = fav_id
if row[Column.FAV_TYPE] == BqServiceType.IPTV.name:
old_fav_id = self._services[fav_id]
srv = self._ex_services.pop(old_fav_id, None)
if srv:
self._ex_services[fav_id] = srv._replace(fav_id=fav_id)
self._dialog.destroy()
@run_idle
def on_update(self, item=None):
self.clear_data()
self.init_options()
gen = self.init_data()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def clear_data(self):
self._services_model.clear()
self._bouquet_model.clear()
self._services.clear()
self._source_info_label.set_text("")
self._bouquet_epg_count_label.set_text("")
self.on_info_bar_close()
def init_data(self):
gen = self.init_bouquet_data()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
refs = None
if self._enable_dat_filter:
if self._update_epg_data_on_start:
try:
self.download_epg_from_stb()
except OSError as e:
self.show_info_message("Download epg.dat file error: {}".format(e), Gtk.MessageType.ERROR)
return
yield True
try:
refs = EPG.get_epg_refs(self._epg_dat_path_entry.get_text() + "epg.dat")
except FileNotFoundError as e:
self.show_info_message("Read data error: {}".format(e), Gtk.MessageType.ERROR)
return
yield True
if self._refs_source is RefsSource.SERVICES:
self.init_lamedb_source(refs)
elif self._refs_source is RefsSource.XML:
xml_gen = self.init_xml_source(refs)
try:
yield from xml_gen
except ValueError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
self.show_info_message("Unknown names source!", Gtk.MessageType.ERROR)
yield True
def init_bouquet_data(self):
for r in self._ex_fav_model:
row = [*r[:]]
fav_id = r[Column.FAV_ID]
self._services[fav_id] = self._ex_services[fav_id].fav_id
yield self._bouquet_model.append(row)
self._bouquet_count_label.set_text(str(len(self._bouquet_model)))
yield True
def init_lamedb_source(self, refs):
srvs = {k[:k.rfind(":")]: v for k, v in self._ex_services.items()}
s_types = (BqServiceType.MARKER.value, BqServiceType.IPTV.value)
filtered = filter(None, [srvs.get(ref) for ref in refs]) if refs else filter(
lambda s: s.service_type not in s_types, self._ex_services.values())
list(map(self._services_model.append, map(lambda s: (s.service, s.fav_id), filtered)))
self.update_source_count_info()
def init_xml_source(self, refs):
path = self._epg_dat_path_entry.get_text() if self._use_web_source else self._xml_chooser_button.get_filename()
if not path:
self.show_info_message("The path to the xml file is not set!", Gtk.MessageType.ERROR)
return
if self._use_web_source:
# Downloading gzipped xml file that contains services names with references from the web.
self._download_xml_is_active = True
self.update_active_header_elements(False)
url = self._url_to_xml_entry.get_text()
try:
with urllib.request.urlopen(url, timeout=2) as fp:
headers = fp.info()
content_type = headers.get("Content-Type", "")
if content_type != "application/gzip":
self._download_xml_is_active = False
raise ValueError("{} {} {}".format(get_message("Download XML file error."),
get_message("Unsupported file type:"),
content_type))
file_name = os.path.basename(url)
data_path = self._epg_dat_path_entry.get_text()
with open(data_path + file_name, "wb") as tfp:
bs = 1024 * 8
size = -1
read = 0
b_num = 0
if "content-length" in headers:
size = int(headers["Content-Length"])
while self._download_xml_is_active:
block = fp.read(bs)
if not block:
break
read += len(block)
tfp.write(block)
b_num += 1
self.update_download_progress(b_num * bs / size)
yield True
path = tfp.name.rstrip(".gz")
except (HTTPError, URLError) as e:
raise ValueError("{} {}".format(get_message("Download XML file error."), e))
else:
try:
with open(path, "wb") as f_out:
with gzip.open(tfp.name, "rb") as f:
shutil.copyfileobj(f, f_out)
os.remove(tfp.name)
except Exception as e:
raise ValueError("{} {}".format(get_message("Unpacking data error."), e))
finally:
self._download_xml_is_active = False
self.update_active_header_elements(True)
try:
s_refs, info = ChannelsParser.get_refs_from_xml(path)
yield True
except Exception as e:
raise ValueError("{} {}".format(get_message("XML parsing error:"), e))
else:
if refs:
s_refs = filter(lambda x: x.num in refs, s_refs)
list(map(lambda s: self._services_model.append((s.name, s.data)), s_refs))
self.update_source_info(info)
self.update_source_count_info()
yield True
def on_key_release(self, view, event):
""" Handling keystrokes """
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK
if ctrl and key is KeyboardKey.C:
self.on_copy_ref()
elif ctrl and key is KeyboardKey.V:
self.on_assign_ref()
@run_idle
def on_save_to_xml(self, item):
response = show_dialog(DialogType.CHOOSER, self._dialog, settings=self._settings)
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
services = []
iptv_types = (BqServiceType.IPTV.value, BqServiceType.MARKER.value)
for r in self._bouquet_model:
srv_type = r[Column.FAV_TYPE]
if srv_type in iptv_types:
srv = BouquetService(name=r[Column.FAV_SERVICE],
type=BqServiceType(srv_type),
data=r[Column.FAV_ID],
num=r[Column.FAV_NUM])
services.append(srv)
ChannelsParser.write_refs_to_xml("{}{}.xml".format(response, self._bouquet_name), services)
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
@run_idle
def on_auto_configuration(self, item):
""" Simple mapping of services by name. """
use_cyrillic = locale.getdefaultlocale()[0] in ("ru_RU", "be_BY", "uk_UA", "sr_RS")
tr = None
if use_cyrillic:
# may be not entirely correct
symbols = (u"АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯІÏҐЎЈЂЉЊЋЏTB",
u"ABVGDEEJZIJKLMNOPRSTUFHZCSS_Y_EUAIEGUEDLNCJTV")
tr = {ord(k): ord(v) for k, v in zip(*symbols)}
source = {}
for row in self._services_model:
name = re.sub("\\W+", "", str(row[0])).upper()
name = name.translate(tr) if use_cyrillic else name
source[name] = row[1]
success_count = 0
not_founded = {}
for r in self._bouquet_model:
if r[Column.FAV_TYPE] != BqServiceType.IPTV.value:
continue
name = re.sub("\\W+", "", str(r[Column.FAV_SERVICE])).upper()
if use_cyrillic:
name = name.translate(tr)
ref = source.get(name, None) # Not [pop], because the list may contain duplicates or similar names!
if ref:
self.assign_data(r, ref, True)
success_count += 1
else:
not_founded[name] = r
# Additional attempt to search in the remaining elements
for n in not_founded:
for k in source:
if k.startswith(n):
self.assign_data(not_founded[n], source[k], True)
success_count += 1
break
self.update_epg_count()
self.show_info_message("{} {} {}".format(get_message("Done!"),
get_message("Count of successfully configured services:"),
success_count), Gtk.MessageType.INFO)
def assign_data(self, row, ref, show_error=False):
if row[Column.FAV_TYPE] != BqServiceType.IPTV.value:
if not show_error:
self.show_info_message(get_message("Not allowed in this context!"), Gtk.MessageType.ERROR)
return
fav_id = row[Column.FAV_ID]
fav_id_data = fav_id.split(":")
fav_id_data[3:7] = ref.split(":")
new_fav_id = ":".join(fav_id_data)
service = self._services.pop(fav_id, None)
if service:
self._services[new_fav_id] = service
row[Column.FAV_ID] = new_fav_id
row[Column.FAV_LOCKED] = EPG_ICON
row[Column.FAV_TOOLTIP] = ":".join(fav_id_data[:10]) if self._show_tooltips else None
def on_filter_toggled(self, button: Gtk.ToggleButton):
self._filter_bar.set_search_mode(button.get_active())
def on_filter_changed(self, entry):
self._services_filter_model.refilter()
def services_filter_function(self, model, itr, data):
txt = self._filter_entry.get_text().upper()
return model is None or model == "None" or txt in model.get_value(itr, 0).upper()
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
def on_copy_ref(self, item=None):
model, paths = self._source_view.get_selection().get_selected_rows()
self._current_ref.clear()
if paths:
self._current_ref.append(model[paths][1])
def on_assign_ref(self, item=None):
if self._current_ref:
model, paths = self._bouquet_view.get_selection().get_selected_rows()
self.assign_data(model[paths], self._current_ref.pop())
self.update_epg_count()
@run_idle
def on_reset(self, item):
model, paths = self._bouquet_view.get_selection().get_selected_rows()
if paths:
row = self._bouquet_model[paths]
self.reset_row_data(row)
self.update_epg_count()
@run_idle
def on_list_reset(self, item):
list(map(self.reset_row_data, self._bouquet_model))
self.update_epg_count()
def reset_row_data(self, row):
default_fav_id = self._services.pop(row[Column.FAV_ID], None)
if default_fav_id:
self._services[default_fav_id] = default_fav_id
row[Column.FAV_ID], row[Column.FAV_LOCKED], row[Column.FAV_TOOLTIP] = default_fav_id, None, None
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(text)
@run_idle
def update_source_info(self, info):
lines = info.split("\n")
self._source_info_label.set_text(lines[0] if lines else "")
self._source_view.set_tooltip_text(info)
@run_idle
def update_source_count_info(self):
source_count = len(self._services_model)
self._source_count_label.set_text(str(source_count))
if self._enable_dat_filter and source_count == 0:
msg = get_message("Current epg.dat file does not contains references for the services of this bouquet!")
self.show_info_message(msg, Gtk.MessageType.WARNING)
@run_idle
def update_epg_count(self):
count = len(list((filter(None, [r[Column.FAV_LOCKED] for r in self._bouquet_model]))))
self._bouquet_epg_count_label.set_text(str(count))
@run_idle
def update_active_header_elements(self, state):
self._left_header_box.set_sensitive(state)
self._xml_download_progress_bar.set_visible(not state)
self._source_info_label.set_text("" if state else "Downloading XML:")
@run_idle
def update_download_progress(self, value):
self._xml_download_progress_bar.set_fraction(value)
def on_bouquet_popup_menu(self, menu, event):
self._assign_ref_popup_item.set_sensitive(self._current_ref)
on_popup_menu(menu, event)
# ***************** Drag-and-drop *********************#
def init_drag_and_drop(self):
""" Enable drag-and-drop """
target = []
self._source_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, target, Gdk.DragAction.COPY)
self._source_view.drag_source_add_text_targets()
self._bouquet_view.enable_model_drag_dest(target, Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE)
self._bouquet_view.drag_dest_add_text_targets()
def on_drag_begin(self, view, context):
""" Selects a row under the cursor in the view at the dragging beginning. """
selection = view.get_selection()
if selection.count_selected_rows() > 1:
view.do_toggle_cursor_row(view)
def on_drag_data_get(self, view: Gtk.TreeView, drag_context, data, info, time):
model, paths = view.get_selection().get_selected_rows()
if paths:
val = model.get_value(model.get_iter(paths), 1)
data.set_text(val, -1)
def on_drag_data_received(self, view: Gtk.TreeView, drag_context, x, y, data, info, time):
path, pos = view.get_dest_row_at_pos(x, y)
model = view.get_model()
self.assign_data(model[path], data.get_text())
self.update_epg_count()
return False
# ***************** Options *********************#
def init_options(self):
epg_dat_path = self._settings.data_local_path + "epg/"
self._epg_dat_path_entry.set_text(epg_dat_path)
default_epg_data_stb_path = "/etc/enigma2"
epg_options = self._settings.epg_options
if epg_options:
self._refs_source = RefsSource.XML if epg_options.get("xml_source", False) else RefsSource.SERVICES
self._xml_radiobutton.set_active(self._refs_source is RefsSource.XML)
self._use_web_source = epg_options.get("use_web_source", False)
self._use_web_source_switch.set_active(self._use_web_source)
self._url_to_xml_entry.set_text(epg_options.get("url_to_xml", ""))
self._enable_dat_filter = epg_options.get("enable_filtering", False)
self._enable_filtering_switch.set_active(self._enable_dat_filter)
epg_dat_path = epg_options.get("epg_dat_path", epg_dat_path)
self._epg_dat_path_entry.set_text(epg_dat_path)
self._epg_dat_stb_path_entry.set_text(epg_options.get("epg_dat_stb_path", default_epg_data_stb_path))
self._update_epg_data_on_start = epg_options.get("epg_data_update_on_start", False)
self._update_on_start_switch.set_active(self._update_epg_data_on_start)
local_xml_path = epg_options.get("local_path_to_xml", None)
if local_xml_path:
self._xml_chooser_button.set_filename(local_xml_path)
os.makedirs(os.path.dirname(self._epg_dat_path_entry.get_text()), exist_ok=True)
def on_options_save(self, item=None):
self._settings.epg_options = {"xml_source": self._xml_radiobutton.get_active(),
"use_web_source": self._use_web_source_switch.get_active(),
"local_path_to_xml": self._xml_chooser_button.get_filename(),
"url_to_xml": self._url_to_xml_entry.get_text(),
"enable_filtering": self._enable_filtering_switch.get_active(),
"epg_dat_path": self._epg_dat_path_entry.get_text(),
"epg_dat_stb_path": self._epg_dat_stb_path_entry.get_text(),
"epg_data_update_on_start": self._update_on_start_switch.get_active()}
def on_resize(self, window):
if self._settings:
self._settings.add("epg_tool_window_size", window.get_size())
def on_names_source_changed(self, button):
self._refs_source = RefsSource.XML if button.get_active() else RefsSource.SERVICES
self._names_source_box.set_sensitive(button.get_active())
def on_enable_filtering_switch(self, switch, state):
self._epg_dat_source_box.set_sensitive(state)
self._update_on_start_switch.set_active(False if not state else self._update_epg_data_on_start)
def on_update_on_start_switch(self, switch, state):
pass
def on_use_web_source_switch(self, switch, state):
self._web_source_box.set_sensitive(state)
self._xml_chooser_button.set_sensitive(not state)
def on_field_icon_press(self, entry, icon, event_button):
update_entry_data(entry, self._dialog, self._settings)
# ***************** Downloads *********************#
def download_epg_from_stb(self):
""" Download the epg.dat file via ftp from the receiver. """
download_data(settings=self._settings, download_type=DownloadType.EPG, callback=print)
if __name__ == "__main__":
pass

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

415
app/ui/import_dialog.glade Normal file
View File

@@ -0,0 +1,415 @@
<?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-css-provider-path style.css -->
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor for macOS. -->
<!-- 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 -->
<column type="gchararray"/>
<!-- column-name type -->
<column type="gchararray"/>
<!-- column-name selected -->
<column type="gboolean"/>
</columns>
</object>
<object class="GtkImage" id="remove_selection_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-undo</property>
</object>
<object class="GtkMenu" id="popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="select_all_popup_item">
<property name="label">gtk-select-all</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<signal name="activate" handler="on_select_all" object="main_view" swapped="no"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="unselect_all_popup_item">
<property name="label" translatable="yes">Remove selection</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">remove_selection_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_unselect_all" object="main_view" swapped="no"/>
</object>
</child>
</object>
<object class="GtkListStore" id="services_list_store">
<columns>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name type -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkWindow" id="dialog_window">
<property name="can_focus">False</property>
<property name="title" translatable="yes">Import</property>
<property name="modal">True</property>
<property name="window_position">center-on-parent</property>
<property name="default_width">480</property>
<property name="default_height">320</property>
<property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property>
<property name="gravity">center</property>
<signal name="check-resize" handler="on_resize" swapped="no"/>
<child>
<placeholder/>
</child>
<child>
<object class="GtkBox" id="main_box">
<property name="width_request">480</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="margin_bottom">1</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkPaned" id="main_paned">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="wide_handle">True</property>
<child>
<object class="GtkBox" id="bouquets_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="label" translatable="yes">Bouquets</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="bouquets_screlled_window">
<property name="width_request">200</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="main_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">main_list_store</property>
<property name="headers_clickable">False</property>
<property name="search_column">0</property>
<signal name="button-press-event" handler="on_popup_menu" object="popup_menu" swapped="no"/>
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
<signal name="select-all" handler="on_select_all" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="bouquet_name_column">
<property name="title" translatable="yes">Name</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="bq_name_renderer">
<property name="xalign">0.019999999552965164</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="bouquet_type_column">
<property name="title" translatable="yes">Type</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="bq_type_renderer">
<property name="xalign">0.50999999046325684</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="bouquet_selected_column">
<property name="title" translatable="yes">Selected</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererToggle" id="bq_selected_renderer">
<property name="xalign">0.50999999046325684</property>
<signal name="toggled" handler="on_selected_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">2</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<object class="GtkBox" id="services_box">
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="label" translatable="yes">Bouquet details</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="services_view_scrolled_window">
<property name="width_request">150</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="services_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">services_list_store</property>
<property name="headers_clickable">False</property>
<property name="search_column">0</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="service_name_column">
<property name="title" translatable="yes">Service</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="info_name_renderer">
<property name="xalign">0.019999999552965164</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="service_type_column">
<property name="title" translatable="yes">Type</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="info_type_renderer">
<property name="xalign">0.50999999046325684</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="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>
<property name="show_close_button">True</property>
<signal name="response" handler="on_info_bar_close" swapped="no"/>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="spacing">6</property>
<property name="layout_style">end</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="spacing">16</property>
<child>
<object class="GtkLabel" id="message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">message</property>
<property name="wrap">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

245
app/ui/imports.py Normal file
View File

@@ -0,0 +1,245 @@
from contextlib import suppress
from pathlib import Path
from app.commons import run_idle, log
from app.eparser import get_bouquets, get_services
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.main_helper import on_popup_menu
from .uicommons import Gtk, UI_RESOURCES_PATH, KeyboardKey, Column
def import_bouquet(transient, model, path, settings, services, appender, file_path=None):
""" Import of single bouquet """
itr = model.get_iter(path)
bq_type = BqType(model.get(itr, Column.BQ_TYPE)[0])
pattern, f_pattern = None, None
profile = settings.setting_type
if profile is SettingsType.ENIGMA_2:
pattern = ".{}".format(bq_type.value)
f_pattern = "*" + pattern if settings.is_darwin else "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"
if bq_type is BqType.TV:
f_pattern = "ubouquets.xml"
elif bq_type is BqType.WEBTV:
f_pattern = "webtv.xml"
file_path = file_path or get_chooser_dialog(transient, settings, "bouquet files", (f_pattern,))
if file_path == Gtk.ResponseType.CANCEL:
return
if not 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))
if len(imported) == 0:
show_dialog(DialogType.ERROR, transient, text="The main list does not contain services for this bouquet!")
return
if model.iter_n_children(itr):
appender(bq, itr)
else:
p_itr = model.iter_parent(itr)
appender(bq, p_itr) if p_itr else appender(bq, itr)
elif profile is SettingsType.NEUTRINO_MP:
if bq_type is BqType.WEBTV:
bqs = parse_webtv(file_path, "WEBTV", bq_type.value)
else:
bqs = get_neutrino_bouquets(file_path, "", bq_type.value)
file_path = "{}/".format(Path(file_path).parent)
ImportDialog(transient, file_path, settings, services.keys(), lambda b, s: appender(b), (bqs,)).show()
def get_enigma2_bouquet(path):
path, sep, f_name = path.rpartition("userbouquet.")
name, sep, suf = f_name.rpartition(".")
bq = get_bouquet(path, name, suf)
bouquet = Bouquet(name=bq[0], type=BqType(suf).value, services=bq[1], locked=None, hidden=None)
return bouquet
class ImportDialog:
def __init__(self, transient, path, settings, service_ids, appender, bouquets=None):
handlers = {"on_import": self.on_import,
"on_cursor_changed": self.on_cursor_changed,
"on_info_button_toggled": self.on_info_button_toggled,
"on_selected_toggled": self.on_selected_toggled,
"on_info_bar_close": self.on_info_bar_close,
"on_select_all": self.on_select_all,
"on_unselect_all": self.on_unselect_all,
"on_popup_menu": on_popup_menu,
"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)
self._bq_services = {}
self._services = {}
self._service_ids = service_ids
self._append = appender
self._profile = settings.setting_type
self._settings = settings
self._bouquets = bouquets
self._dialog_window = builder.get_object("dialog_window")
self._dialog_window.set_transient_for(transient)
self._main_model = builder.get_object("main_list_store")
self._main_view = builder.get_object("main_view")
self._services_view = builder.get_object("services_view")
self._services_model = builder.get_object("services_list_store")
self._services_box = builder.get_object("services_box")
self._info_check_button = builder.get_object("info_check_button")
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("message_label")
window_size = self._settings.get("import_dialog_window_size")
if window_size:
self._dialog_window.resize(*window_size)
self.init_data(path)
def show(self):
self._dialog_window.show()
@run_idle
def init_data(self, path):
self._main_model.clear()
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)
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):
if not any(r[-1] for r in self._main_model):
self.show_info_message(get_message("No selected item!"), Gtk.MessageType.ERROR)
return
if 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]:
for bq_srv in self._bq_services.get(bq, []):
srv = self._services.get(bq_srv.data, None)
if srv:
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()
@run_idle
def on_cursor_changed(self, view):
if not self._info_check_button.get_active():
return
self._services_model.clear()
model, paths = view.get_selection().get_selected_rows()
if not paths:
return
bq_services = self._bq_services.get(model.get(model.get_iter(paths[0]), 0, 1))
for bq_srv in bq_services:
if bq_srv.type is BqServiceType.DEFAULT:
srv = self._services.get(bq_srv.data, None)
if srv:
self._services_model.append((srv.service, srv.service_type))
else:
self._services_model.append((bq_srv.name, bq_srv.type.value))
def on_info_button_toggled(self, button):
active = button.get_active()
self._services_box.set_visible(active)
def on_selected_toggled(self, toggle, path):
self._main_model.set_value(self._main_model.get_iter(path), 2, not toggle.get_active())
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(text)
@run_idle
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
def on_select_all(self, view):
self.update_selection(view, True)
def on_unselect_all(self, view):
self.update_selection(view, False)
def update_selection(self, view, select):
view.get_model().foreach(lambda mod, path, itr: mod.set_value(itr, 2, select))
def on_resize(self, window):
if self._settings:
self._settings.add("import_dialog_window_size", window.get_size())
def on_key_press(self, view, event):
""" Handling keystrokes """
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
if key is KeyboardKey.SPACE:
path, column = view.get_cursor()
itr = self._main_model.get_iter(path)
selected = self._main_model.get_value(itr, 2)
self._main_model.set_value(itr, 2, not selected)
if __name__ == "__main__":
pass

1688
app/ui/iptv.glade Normal file

File diff suppressed because it is too large Load Diff

779
app/ui/iptv.py Normal file
View File

@@ -0,0 +1,779 @@
import concurrent.futures
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
from app.commons import run_idle, run_task
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.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)
_DIGIT_ENTRY_NAME = "digit-entry"
_ENIGMA2_REFERENCE = "{}:0:{}:{:X}:{:X}:{:X}:{:X}:0:0:0"
_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
_UI_PATH = UI_RESOURCES_PATH + "iptv.glade"
def is_data_correct(elems):
for elem in elems:
if elem.get_name() == _DIGIT_ENTRY_NAME:
return False
return True
def get_stream_type(box):
active = box.get_active()
if active == 0:
return StreamType.DVB_TS.value
elif active == 1:
return StreamType.NONE_TS.value
elif active == 2:
return StreamType.NONE_REC_1.value
elif active == 3:
return StreamType.NONE_REC_2.value
return StreamType.E_SERVICE_URI.value
class IptvDialog:
def __init__(self, transient, view, services, bouquet, settings, action=Action.ADD):
handlers = {"on_response": self.on_response,
"on_entry_changed": self.on_entry_changed,
"on_url_changed": self.on_url_changed,
"on_save": self.on_save,
"on_stream_type_changed": self.on_stream_type_changed,
"on_yt_quality_changed": self.on_yt_quality_changed,
"on_info_bar_close": self.on_info_bar_close}
self._action = action
self._s_type = settings.setting_type
self._settings = settings
self._bouquet = bouquet
self._services = services
self._yt_links = None
self._yt_dl = None
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("iptv_dialog", "stream_type_liststore", "yt_quality_liststore"))
builder.connect_signals(handlers)
self._dialog = builder.get_object("iptv_dialog")
self._dialog.set_transient_for(transient)
self._name_entry = builder.get_object("name_entry")
self._description_entry = builder.get_object("description_entry")
self._url_entry = builder.get_object("url_entry")
self._reference_entry = builder.get_object("reference_entry")
self._srv_type_entry = builder.get_object("srv_type_entry")
self._sid_entry = builder.get_object("sid_entry")
self._tr_id_entry = builder.get_object("tr_id_entry")
self._net_id_entry = builder.get_object("net_id_entry")
self._namespace_entry = builder.get_object("namespace_entry")
self._stream_type_combobox = builder.get_object("stream_type_combobox")
self._add_button = builder.get_object("iptv_dialog_add_button")
self._save_button = builder.get_object("iptv_dialog_save_button")
self._stream_type_combobox = builder.get_object("stream_type_combobox")
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label")
self._yt_quality_box = builder.get_object("yt_iptv_quality_combobox")
self._model, self._paths = view.get_selection().get_selected_rows()
# style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._digit_elems = (self._srv_type_entry, self._sid_entry, self._tr_id_entry, self._net_id_entry,
self._namespace_entry)
for el in self._digit_elems:
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
if self._s_type is SettingsType.NEUTRINO_MP:
builder.get_object("iptv_dialog_ts_data_frame").set_visible(False)
builder.get_object("iptv_type_label").set_visible(False)
builder.get_object("reference_entry").set_visible(False)
builder.get_object("iptv_reference_label").set_visible(False)
self._stream_type_combobox.set_visible(False)
else:
self._description_entry.set_visible(False)
builder.get_object("iptv_description_label").set_visible(False)
if self._action is Action.ADD:
self._save_button.set_visible(False)
self._add_button.set_visible(True)
if self._s_type is SettingsType.ENIGMA_2:
self.update_reference_entry()
self._stream_type_combobox.set_active(1)
elif self._action is Action.EDIT:
self._current_srv = get_base_model(self._model)[self._paths][:]
self.init_data(self._current_srv)
def show(self):
self._dialog.run()
def on_response(self, dialog, response):
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
self._dialog.destroy()
def on_save(self, item):
if self._action is Action.ADD:
self.on_url_changed(self._url_entry)
if not is_data_correct(self._digit_elems) or self._url_entry.get_name() == _DIGIT_ENTRY_NAME:
self.show_info_message(get_message("Error. Verify the data!"), Gtk.MessageType.ERROR)
return
if show_dialog(DialogType.QUESTION, self._dialog) in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
self.save_enigma2_data() if self._s_type is SettingsType.ENIGMA_2 else self.save_neutrino_data()
self._dialog.destroy()
def init_data(self, srv):
name, fav_id = srv[2], srv[7]
self._name_entry.set_text(name)
self.init_enigma2_data(fav_id) if self._s_type is SettingsType.ENIGMA_2 else self.init_neutrino_data(fav_id)
def init_enigma2_data(self, fav_id):
data, sep, desc = fav_id.partition("#DESCRIPTION")
self._description_entry.set_text(desc.strip())
data = data.split(":")
if len(data) < 11:
return
s_type = data[0].strip()
try:
stream_type = StreamType(s_type)
if stream_type is StreamType.DVB_TS:
self._stream_type_combobox.set_active(0)
elif stream_type is StreamType.NONE_TS:
self._stream_type_combobox.set_active(1)
elif stream_type is StreamType.NONE_REC_1:
self._stream_type_combobox.set_active(2)
elif stream_type is StreamType.NONE_REC_2:
self._stream_type_combobox.set_active(3)
elif stream_type is StreamType.E_SERVICE_URI:
self._stream_type_combobox.set_active(4)
except ValueError:
self.show_info_message("Unknown stream type {}".format(s_type), Gtk.MessageType.ERROR)
self._srv_type_entry.set_text(data[2])
self._sid_entry.set_text(str(int(data[3], 16)))
self._tr_id_entry.set_text(str(int(data[4], 16)))
self._net_id_entry.set_text(str(int(data[5], 16)))
self._namespace_entry.set_text(str(int(data[6], 16)))
self._url_entry.set_text(unquote(data[10].strip()))
self.update_reference_entry()
def init_neutrino_data(self, fav_id):
data = fav_id.split("::")
self._url_entry.set_text(data[0])
self._description_entry.set_text(data[1])
def update_reference_entry(self):
if self._s_type is SettingsType.ENIGMA_2 and is_data_correct(self._digit_elems):
self.on_url_changed(self._url_entry)
self._reference_entry.set_text(_ENIGMA2_REFERENCE.format(self.get_type(),
self._srv_type_entry.get_text(),
int(self._sid_entry.get_text()),
int(self._tr_id_entry.get_text()),
int(self._net_id_entry.get_text()),
int(self._namespace_entry.get_text())))
def get_type(self):
return get_stream_type(self._stream_type_combobox)
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_entry()
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
entry.set_name("GtkEntry" if cond else _DIGIT_ENTRY_NAME)
yt_id = YouTube.get_yt_id(url_str)
if yt_id:
entry.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, get_yt_icon("youtube", 32))
text = "Found a link to the YouTube resource!\nTry to get a direct link to the video?"
if show_dialog(DialogType.QUESTION, self._dialog, text=text) == Gtk.ResponseType.OK:
entry.set_sensitive(False)
gen = self.set_yt_url(entry, yt_id)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
elif YouTube.is_yt_video_link(url_str):
entry.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, get_yt_icon("youtube", 32))
else:
entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None)
self._yt_quality_box.set_visible(False)
def set_yt_url(self, entry, video_id):
try:
if not self._yt_dl:
def callback(message, error=True):
msg_type = Gtk.MessageType.ERROR if error else Gtk.MessageType.INFO
self.show_info_message(message, msg_type)
self._yt_dl = YouTube.get_instance(self._settings, callback=callback)
yield True
links, title = self._yt_dl.get_yt_link(video_id, entry.get_text())
yield True
except urllib.error.URLError as e:
self.show_info_message(get_message("Getting link error:") + (str(e)), Gtk.MessageType.ERROR)
return
except YouTubeException as e:
self.show_info_message((str(e)), Gtk.MessageType.ERROR)
return
else:
if self._action is Action.ADD:
self._name_entry.set_text(title)
if links:
if len(links) > 1:
self._yt_quality_box.set_visible(True)
entry.set_text(links[sorted(links, key=lambda x: int(x.rstrip("p")), reverse=True)[0]])
self._yt_links = links
else:
msg = get_message("Getting link error:") + " No link received for id: {}".format(video_id)
self.show_info_message(msg, Gtk.MessageType.ERROR)
finally:
entry.set_sensitive(True)
yield True
def on_stream_type_changed(self, item):
if self.get_type() == StreamType.E_SERVICE_URI.value:
self.show_info_message("DreamOS only!", Gtk.MessageType.WARNING)
self.update_reference_entry()
def on_yt_quality_changed(self, box):
model = box.get_model()
active = model.get_value(box.get_active_iter(), 0)
if self._yt_links and active in self._yt_links:
self._url_entry.set_text(self._yt_links[active])
def save_enigma2_data(self):
name = self._name_entry.get_text().strip()
fav_id = ENIGMA2_FAV_ID_FORMAT.format(self.get_type(),
self._srv_type_entry.get_text(),
int(self._sid_entry.get_text()),
int(self._tr_id_entry.get_text()),
int(self._net_id_entry.get_text()),
int(self._namespace_entry.get_text()),
quote(self._url_entry.get_text()),
name, name)
self.update_bouquet_data(name, fav_id)
def save_neutrino_data(self):
if self._action is Action.EDIT:
id_data = self._current_srv[7].split("::")
else:
id_data = ["", "", "0", None, None, None, None, "", "", "1"]
id_data[0] = self._url_entry.get_text()
id_data[1] = self._description_entry.get_text()
self.update_bouquet_data(self._name_entry.get_text(), NEUTRINO_FAV_ID_FORMAT.format(*id_data))
self._dialog.destroy()
def update_bouquet_data(self, name, fav_id):
if self._action is Action.EDIT:
old_srv = self._services.pop(self._current_srv[7])
self._services[fav_id] = old_srv._replace(service=name, fav_id=fav_id)
self._bouquet[self._paths[0][0]] = fav_id
self._model.set(self._model.get_iter(self._paths), {Column.FAV_SERVICE: name, Column.FAV_ID: fav_id})
else:
aggr = [None] * 10
s_type = BqServiceType.IPTV.name
srv = (None, None, name, None, None, s_type, None, fav_id, *aggr[0:3])
itr = self._model.insert_after(self._model.get_iter(self._paths[0]),
srv) if self._paths else self._model.insert(0, srv)
self._model.set_value(itr, 1, IPTV_ICON)
self._bouquet.insert(self._model.get_path(itr)[0], fav_id)
self._services[fav_id] = Service(None, None, IPTV_ICON, name, *aggr[0:3], s_type, *aggr, fav_id, None)
@run_idle
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(text)
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)
self._dialog = builder.get_object("search_unavailable_streams_dialog")
self._dialog.set_transient_for(transient)
self._model = model
self._counter_label = builder.get_object("streams_rows_counter_label")
self._level_bar = builder.get_object("unavailable_streams_level_bar")
self._bouquet = fav_bouquet
self._s_type = s_type
self._iptv_rows = iptv_rows
self._counter = -1
self._max_rows = len(self._iptv_rows)
self._level_bar.set_max_value(self._max_rows)
self._download_task = True
self._to_delete = []
self.update_counter()
self.do_search()
@run_task
def do_search(self):
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
futures = {executor.submit(self.get_unavailable, row): row for row in self._iptv_rows}
for future in concurrent.futures.as_completed(futures):
if not self._download_task:
executor.shutdown()
return
future.result()
self._download_task = False
self.on_close()
def get_unavailable(self, row):
if not self._download_task:
return
try:
req = Request(get_iptv_url(row, self._s_type))
self.update_bar()
urlopen(req, timeout=2)
except HTTPError as e:
if e.code != 403:
self.append_data(row)
except Exception:
self.append_data(row)
def append_data(self, row):
self._to_delete.append(self._model.get_iter(row.path))
self.update_counter()
@run_idle
def update_bar(self):
self._max_rows -= 1
self._level_bar.set_value(self._max_rows)
@run_idle
def update_counter(self):
self._counter += 1
self._counter_label.set_text(str(self._counter))
def show(self):
response = self._dialog.run()
return self._to_delete if response not in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT) else False
def on_response(self, dialog, response):
if response == Gtk.ResponseType.CANCEL:
self.on_close()
@run_idle
def on_close(self):
if self._download_task and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
self._download_task = False
self._dialog.destroy()
class IptvListConfigurationDialog:
def __init__(self, transient, services, iptv_rows, bouquet, fav_model, s_type):
handlers = {"on_apply": self.on_apply,
"on_response": self.on_response,
"on_stream_type_default_togged": self.on_stream_type_default_togged,
"on_stream_type_changed": self.on_stream_type_changed,
"on_default_type_toggled": self.on_default_type_toggled,
"on_auto_sid_toggled": self.on_auto_sid_toggled,
"on_default_tid_toggled": self.on_default_tid_toggled,
"on_default_nid_toggled": self.on_default_nid_toggled,
"on_default_namespace_toggled": self.on_default_namespace_toggled,
"on_reset_to_default": self.on_reset_to_default,
"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)
self._dialog = builder.get_object("iptv_list_configuration_dialog")
self._dialog.set_transient_for(transient)
self._info_bar = builder.get_object("list_configuration_info_bar")
self._reference_label = builder.get_object("reference_label")
self._stream_type_check_button = builder.get_object("stream_type_default_check_button")
self._type_check_button = builder.get_object("type_default_check_button")
self._sid_auto_check_button = builder.get_object("sid_auto_check_button")
self._tid_check_button = builder.get_object("tid_default_check_button")
self._nid_check_button = builder.get_object("nid_default_check_button")
self._namespace_check_button = builder.get_object("namespace_default_check_button")
self._stream_type_combobox = builder.get_object("stream_type_list_combobox")
self._list_srv_type_entry = builder.get_object("list_srv_type_entry")
self._list_sid_entry = builder.get_object("list_sid_entry")
self._list_tid_entry = builder.get_object("list_tid_entry")
self._list_nid_entry = builder.get_object("list_nid_entry")
self._list_namespace_entry = builder.get_object("list_namespace_entry")
self._reset_to_default_switch = builder.get_object("reset_to_default_lists_switch")
# style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._digit_elems = (self._list_srv_type_entry, self._list_sid_entry, self._list_tid_entry,
self._list_nid_entry, self._list_namespace_entry)
for el in self._digit_elems:
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
def show(self):
self._dialog.run()
def on_response(self, dialog, response):
if response == Gtk.ResponseType.CANCEL:
self._dialog.destroy()
def on_stream_type_changed(self, box):
self.update_reference()
def on_stream_type_default_togged(self, button):
if button.get_active():
self._stream_type_combobox.set_active(1)
self._stream_type_combobox.set_sensitive(not button.get_active())
def on_default_type_toggled(self, button):
if button.get_active():
self._list_srv_type_entry.set_text("1")
self._list_srv_type_entry.set_sensitive(not button.get_active())
def on_auto_sid_toggled(self, button):
if button.get_active():
self._list_sid_entry.set_text("0")
self._list_sid_entry.set_sensitive(not button.get_active())
def on_default_tid_toggled(self, button):
if button.get_active():
self._list_tid_entry.set_text("0")
self._list_tid_entry.set_sensitive(not button.get_active())
def on_default_nid_toggled(self, button):
if button.get_active():
self._list_nid_entry.set_text("0")
self._list_nid_entry.set_sensitive(not button.get_active())
def on_default_namespace_toggled(self, button):
if button.get_active():
self._list_namespace_entry.set_text("0")
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)
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):
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):
el.set_active(True)
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@run_idle
def on_apply(self, item):
if not is_data_correct(self._digit_elems):
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
if self._s_type is SettingsType.ENIGMA_2:
reset = self._reset_to_default_switch.get_active()
type_default = self._type_check_button.get_active()
tid_default = self._tid_check_button.get_active()
sid_auto = self._sid_auto_check_button.get_active()
nid_default = self._nid_check_button.get_active()
namespace_default = self._namespace_check_button.get_active()
stream_type = StreamType.NONE_TS.value if reset else get_stream_type(self._stream_type_combobox)
srv_type = "1" if type_default else self._list_srv_type_entry.get_text()
tid = "0" if tid_default else "{:X}".format(int(self._list_tid_entry.get_text()))
nid = "0" if nid_default else "{:X}".format(int(self._list_nid_entry.get_text()))
namespace = "0" if namespace_default else "{:X}".format(int(self._list_namespace_entry.get_text()))
for index, row in enumerate(self._rows):
fav_id = row[Column.FAV_ID]
data, sep, desc = fav_id.partition("http")
data = data.split(":")
if reset:
data[2], data[3], data[4], data[5], data[6] = "10000"
else:
data[0], data[2], data[4], data[5], data[6] = stream_type, srv_type, tid, nid, namespace
data[3] = "{:X}".format(index) if sid_auto else "0"
data = ":".join(data)
new_fav_id = "{}{}{}".format(data, sep, desc)
row[Column.FAV_ID] = new_fav_id
srv = self._services.pop(fav_id, None)
if srv:
self._services[new_fav_id] = srv._replace(fav_id=new_fav_id)
self._bouquet.clear()
list(map(lambda r: self._bouquet.append(r[Column.FAV_ID]), self._fav_model))
self._info_bar.set_visible(True)
@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()
class YtListImportDialog:
def __init__(self, transient, settings, appender):
handlers = {"on_import": self.on_import,
"on_receive": self.on_receive,
"on_yt_url_entry_changed": self.on_url_entry_changed,
"on_yt_info_bar_close": self.on_info_bar_close,
"on_popup_menu": on_popup_menu,
"on_selected_toggled": self.on_selected_toggled,
"on_select_all": self.on_select_all,
"on_unselect_all": self.on_unselect_all,
"on_key_press": self.on_key_press,
"on_close": self.on_close}
self.appender = appender
self._s_type = settings.setting_type
self._download_task = False
self._yt_list_id = None
self._yt_list_title = None
self._settings = settings
self._yt = None
builder = 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)
self._dialog = builder.get_object("yt_import_dialog_window")
self._dialog.set_transient_for(transient)
self._list_view_scrolled_window = builder.get_object("yt_list_view_scrolled_window")
self._model = builder.get_object("yt_liststore")
self._progress_bar = builder.get_object("yt_progress_bar")
self._info_bar_box = builder.get_object("yt_info_bar_box")
self._message_label = builder.get_object("yt_info_bar_message_label")
self._info_bar = builder.get_object("yt_info_bar")
self._yt_count_label = builder.get_object("yt_count_label")
self._url_entry = builder.get_object("yt_url_entry")
self._receive_button = builder.get_object("yt_receive_button")
self._import_button = builder.get_object("yt_import_button")
self._quality_box = builder.get_object("yt_quality_combobox")
self._quality_model = builder.get_object("yt_quality_liststore")
self._import_button.bind_property("visible", self._quality_box, "visible")
self._import_button.bind_property("sensitive", self._quality_box, "sensitive")
self._receive_button.bind_property("sensitive", self._import_button, "sensitive")
# style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
def show(self):
self._dialog.show()
@run_task
def on_import(self, item):
self.on_info_bar_close()
self.update_active_elements(False)
self._download_task = True
try:
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
done_links = {}
rows = list(filter(lambda r: r[2], self._model))
if not self._yt:
self._yt = YouTube.get_instance(self._settings)
futures = {executor.submit(self._yt.get_yt_link, r[1], YouTube.VIDEO_LINK.format(r[1]),
True): r for r in rows}
size = len(futures)
counter = 0
for future in concurrent.futures.as_completed(futures):
if not self._download_task:
executor.shutdown()
return
done_links[futures[future]] = future.result()
counter += 1
self.update_progress_bar(counter / size)
except YouTubeException as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
except Exception as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
if self._download_task:
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
self.append_services([done_links[r] for r in rows])
finally:
self._download_task = False
self.update_active_elements(True)
def on_receive(self, item):
self.show_invisible_elements()
self.update_active_elements(False)
self._model.clear()
self._yt_count_label.set_text("0")
self.on_info_bar_close()
self.update_refs_list()
@run_task
def update_refs_list(self):
if self._yt_list_id:
try:
self._yt_list_title, links = PlayListParser.get_yt_playlist(self._yt_list_id)
except Exception as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
return
else:
gen = self.update_links(links)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
finally:
self.update_active_elements(True)
def update_links(self, links):
for link in links:
yield self._model.append((link[0], link[1], True, None))
size = len(self._model)
self._yt_count_label.set_text(str(size))
self._import_button.set_visible(size)
yield True
@run_idle
def append_services(self, links):
aggr = [None] * 9
srvs = []
if self._yt_list_title:
title = self._yt_list_title
fav_id = MARKER_FORMAT.format(0, title, title)
mk = Service(None, None, None, title, *aggr[0:3], BqServiceType.MARKER.name, *aggr, 0, fav_id, None)
srvs.append(mk)
act = self._quality_model.get_value(self._quality_box.get_active_iter(), 0)
for link in links:
lnk, title = link or (None, None)
if not lnk:
continue
ln = lnk.get(act) if act in lnk else lnk[sorted(lnk, key=lambda x: int(x.rstrip("p")), reverse=True)[0]]
fav_id = get_fav_id(ln, title, self._s_type)
srv = Service(None, None, IPTV_ICON, title, *aggr[0:3], BqServiceType.IPTV.name, *aggr, None, fav_id, None)
srvs.append(srv)
self.appender(srvs)
@run_idle
def update_active_elements(self, sensitive):
self._url_entry.set_sensitive(sensitive)
self._receive_button.set_sensitive(sensitive)
def show_invisible_elements(self):
self._list_view_scrolled_window.set_visible(True)
self._info_bar_box.set_visible(True)
self._dialog.set_resizable(True)
def on_url_entry_changed(self, entry):
url_str = entry.get_text()
yt_id = YouTube.get_yt_list_id(url_str)
entry.set_name("GtkEntry" if yt_id else _DIGIT_ENTRY_NAME)
self._receive_button.set_sensitive(bool(yt_id))
self._import_button.set_sensitive(bool(yt_id))
self._yt_list_id = yt_id
if yt_id:
entry.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, get_yt_icon("youtube", 32))
else:
entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None)
@run_idle
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@run_idle
def update_progress_bar(self, value):
self._progress_bar.set_visible(value < 1)
self._progress_bar.set_fraction(value)
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(text)
def on_selected_toggled(self, toggle, path):
self._model.set_value(self._model.get_iter(path), 2, not toggle.get_active())
def on_select_all(self, view):
self.update_selection(view, True)
def on_unselect_all(self, view):
self.update_selection(view, False)
def update_selection(self, view, select):
view.get_model().foreach(lambda mod, path, itr: mod.set_value(itr, 2, select))
def on_key_press(self, view, event):
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
if key is KeyboardKey.SPACE:
path, column = view.get_cursor()
itr = self._model.get_iter(path)
selected = self._model.get_value(itr, 2)
self._model.set_value(itr, 2, not selected)
def on_close(self, window, event):
if self._download_task and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return True
self._download_task = False
if __name__ == "__main__":
pass

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.

File diff suppressed because it is too large Load Diff

View File

@@ -1,154 +1,207 @@
""" This is helper module for ui """
from enum import Enum
""" Helper module for the ui. """
import os
import shutil
from gi.repository import GdkPixbuf
from urllib.parse import unquote
from gi.repository import GdkPixbuf, GLib
from app.commons import run_task
from app.eparser import Service
from app.eparser.ecommons import FLAG
from app.eparser.ecommons import Flag, BouquetService, Bouquet, BqType
from app.eparser.enigma.bouquets import BqServiceType, to_bouquet_id
from . import Gtk, Gdk, HIDE_ICON, LOCKED_ICON
from .dialogs import show_dialog, DialogType, get_chooser_dialog
class ViewTarget(Enum):
""" Used for set target view """
BOUQUET = 0
FAV = 1
SERVICES = 2
from app.settings import SettingsType
from .dialogs import show_dialog, DialogType, get_chooser_dialog, WaitDialog
from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON, KeyboardKey, Column
# ***************** Markers *******************#
def insert_marker(view, bouquets, selected_bouquet, channels, parent_window):
def insert_marker(view, bouquets, selected_bouquet, services, parent_window, m_type=BqServiceType.MARKER):
"""" Inserts marker into bouquet services list. """
response = show_dialog(DialogType.INPUT, parent_window)
if response == Gtk.ResponseType.CANCEL:
return
fav_id, text = "1:832:D:0:0:0:0:0:0:0:\n", None
if not response.strip():
show_dialog(DialogType.ERROR, parent_window, "The text of marker is empty, please try again!")
return
if m_type is BqServiceType.MARKER:
response = show_dialog(DialogType.INPUT, parent_window)
if response == Gtk.ResponseType.CANCEL:
return
# Searching for max num value in all marker services (if empty default = 0)
max_num = max(map(lambda num: int(num.data_id, 18),
filter(lambda ch: ch.service_type == BqServiceType.MARKER.name, channels.values())), default=0)
max_num = '{:x}'.format(max_num + 1)
fav_id = "1:64:{}:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n".format(max_num, response, response)
s_type = BqServiceType.MARKER.name
if not response.strip():
show_dialog(DialogType.ERROR, parent_window, "The text of marker is empty, please try again!")
return
fav_id = "1:64:0:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n".format(response, response)
text = response
s_type = m_type.name
model, paths = view.get_selection().get_selected_rows()
marker = (None, None, response, None, None, s_type, None, fav_id, None)
marker = (None, None, text, None, None, s_type, None, fav_id, None, None, None)
itr = model.insert_before(model.get_iter(paths[0]), marker) if paths else model.insert(0, marker)
bouquets[selected_bouquet].insert(model.get_path(itr)[0], fav_id)
channels[fav_id] = Service(None, None, None, response, None, None, None, s_type, *[None] * 9, max_num, fav_id, None)
def edit_marker(view, bouquets, selected_bouquet, channels, parent_window):
""" Edits marker text """
model, paths = view.get_selection().get_selected_rows()
itr = model.get_iter(paths[0])
name, fav_id = model.get(itr, 2, 7)
response = show_dialog(DialogType.INPUT, parent_window, text=name)
if response == Gtk.ResponseType.CANCEL:
return
bq_services = bouquets[selected_bouquet]
index = bq_services.index(fav_id)
old_ch = channels.pop(fav_id, None)
new_fav_id = "{}::{}\n#DESCRIPTION {}\n".format(fav_id.split("::")[0], response, response)
model.set(itr, {2: response, 7: new_fav_id})
channels[new_fav_id] = Service(*old_ch[0:3], response, *old_ch[4:17], old_ch.data_id, new_fav_id, None)
bq_services.pop(index)
bq_services.insert(index, new_fav_id)
services[fav_id] = Service(None, None, None, text, None, None, None, s_type, *[None] * 9, 0, fav_id, None)
# ***************** Movement *******************#
def move_items(key, view):
""" Move items in tree view """
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:
# for correct down move!
if key in (Gdk.KEY_Down, Gdk.KEY_Page_Down, Gdk.KEY_KP_Page_Down):
paths = reversed(paths)
is_tree_store = type(model) is Gtk.TreeStore
mod_length = len(model)
if not is_tree_store and mod_length == len(paths):
return
for path in paths:
itr = model.get_iter(path)
if key == Gdk.KEY_Down:
next_itr = model.iter_next(itr)
if next_itr:
model.move_after(itr, next_itr)
elif key == Gdk.KEY_Up:
prev_itr = model.iter_previous(itr)
if prev_itr:
model.move_before(itr, prev_itr)
elif key == Gdk.KEY_Page_Up or key == Gdk.KEY_KP_Page_Up:
up_itr = model.get_iter(view.get_cursor()[0])
if up_itr:
model.move_before(itr, up_itr)
elif key == Gdk.KEY_Page_Down or key == Gdk.KEY_KP_Page_Down:
down_itr = model.get_iter(view.get_cursor()[0])
if down_itr:
model.move_after(itr, down_itr)
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 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)
# ***************** Edit *******************#
def move_up(top_path, model, paths):
top_iter = model.get_iter(top_path)
for path in paths:
itr = model.get_iter(path)
model.move_before(itr, top_iter)
top_path.next()
top_iter = model.get_iter(top_path)
def edit(view, parent_window, target, fav_view=None, service_view=None, channels=None):
def move_down(down_path, model, paths):
top_iter = model.get_iter(down_path)
for path in reversed(paths):
itr = model.get_iter(path)
model.move_after(itr, top_iter)
down_path.prev()
top_iter = model.get_iter(down_path)
def is_some_level(paths):
for i in range(1, len(paths)):
prev = paths[i - 1]
current = paths[i]
if len(prev) != len(current) or (len(prev) == 2 and len(current) == 2 and prev[0] != current[0]):
return
return True
def set_cursor(dest_path, paths, selection, view):
view.set_cursor(dest_path, view.get_column(0), False)
for p in paths:
selection.select_path(p)
# ***************** Rename *******************#
def rename(view, parent_window, target, fav_view=None, service_view=None, services=None):
selection = get_selection(view, parent_window)
if not selection:
return
model, paths = selection
itr = model.get_iter(paths)
f_id, srv_name, srv_type = None, None, None
if target is ViewTarget.SERVICES:
name, fav_id = model.get(itr, Column.SRV_SERVICE, Column.SRV_FAV_ID)
f_id = fav_id
response = show_dialog(DialogType.INPUT, parent_window, name)
if response == Gtk.ResponseType.CANCEL:
return
srv_name = response
model.set_value(itr, Column.SRV_SERVICE, response)
if fav_view is not None:
for row in fav_view.get_model():
if row[Column.FAV_ID] == fav_id:
row[Column.FAV_SERVICE] = response
break
elif target is ViewTarget.FAV:
name, srv_type, fav_id = model.get(itr, Column.FAV_SERVICE, Column.FAV_TYPE, Column.FAV_ID)
f_id = fav_id
response = show_dialog(DialogType.INPUT, parent_window, name)
if response == Gtk.ResponseType.CANCEL:
return
srv_name = 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()):
if row[Column.SRV_FAV_ID] == fav_id:
row[Column.SRV_SERVICE] = response
break
old_srv = services.get(f_id, None)
if old_srv:
if srv_type == BqServiceType.IPTV.name or srv_type == BqServiceType.MARKER.name:
l, sep, r = f_id.partition("#DESCRIPTION")
old_name = old_srv.service.strip()
new_name = srv_name.strip()
new_fav_id = "".join((new_name.join(l.rsplit(old_name, 1)), sep, new_name.join(r.rsplit(old_name, 1))))
services[f_id] = old_srv._replace(service=srv_name, fav_id=new_fav_id)
else:
services[f_id] = old_srv._replace(service=srv_name)
def get_selection(view, parent):
""" Returns (model, paths) if possible """
model, paths = view.get_selection().get_selected_rows()
model = get_base_model(model)
if not paths:
return
elif len(paths) > 1:
show_dialog(DialogType.ERROR, parent_window, "Please, select only one item!")
show_dialog(DialogType.ERROR, parent, "Please, select only one item!")
return
itr = model.get_iter(paths)
f_id = None
channel_name = None
if target is ViewTarget.SERVICES:
name, fav_id = model.get(itr, 3, 18)
f_id = fav_id
response = show_dialog(DialogType.INPUT, parent_window, name)
if response == Gtk.ResponseType.CANCEL:
return
channel_name = response
model.set_value(itr, 3, response)
if fav_view is not None:
for row in fav_view.get_model():
if row[7] == fav_id:
row[2] = response
break
elif target is ViewTarget.FAV:
name, fav_id = model.get(itr, 2, 7)
f_id = fav_id
response = show_dialog(DialogType.INPUT, parent_window, name)
if response == Gtk.ResponseType.CANCEL:
return
channel_name = response
model.set_value(itr, 2, response)
if service_view is not None:
for row in get_base_model(service_view.get_model()):
if row[18] == fav_id:
row[3] = response
break
old_ch = channels.get(f_id, None)
if old_ch:
channels[f_id] = Service(*old_ch[0:3], channel_name, *old_ch[4:])
return model, paths
# ***************** Flags *******************#
def set_flags(flag, services_view, fav_view, channels, blacklist):
def set_flags(flag, services_view, fav_view, services, blacklist):
""" Updates flags for services. Returns True if any was changed. """
target = ViewTarget.SERVICES if services_view.is_focus() else ViewTarget.FAV if fav_view.is_focus() else None
if not target:
@@ -164,49 +217,64 @@ def set_flags(flag, services_view, fav_view, channels, blacklist):
if not paths:
return
paths = get_base_paths(paths, model)
model = get_base_model(model)
if flag is FLAG.HIDE:
if flag is Flag.HIDE:
if target is ViewTarget.SERVICES:
set_hide(channels, model, paths)
set_hide(services, model, paths)
else:
fav_ids = [model.get_value(model.get_iter(path), 7) for path in paths]
fav_ids = [model.get_value(model.get_iter(path), Column.FAV_ID) for path in paths]
srv_model = get_base_model(services_view.get_model())
srv_paths = [row.path for row in srv_model if row[18] in fav_ids]
set_hide(channels, srv_model, srv_paths)
elif flag is FLAG.LOCK:
set_lock(blacklist, channels, model, paths, target, services_model=get_base_model(services_view.get_model()))
srv_paths = [row.path for row in srv_model if row[Column.SRV_FAV_ID] in fav_ids]
set_hide(services, srv_model, srv_paths)
elif flag is Flag.LOCK:
set_lock(blacklist, services, model, paths, target, services_model=get_base_model(services_view.get_model()))
return True
update_fav_model(fav_view, services)
def set_lock(blacklist, channels, model, paths, target, services_model):
col_num = 4 if target is ViewTarget.SERVICES else 3
def update_fav_model(fav_view, services):
for row in get_base_model(fav_view.get_model()):
srv = services.get(row[Column.FAV_ID], None)
if srv:
row[Column.FAV_LOCKED], row[Column.FAV_HIDE] = srv.locked, srv.hide
def set_lock(blacklist, services, model, paths, target, services_model):
col_num = Column.SRV_LOCKED if target is ViewTarget.SERVICES else Column.FAV_LOCKED
locked = has_locked_hide(model, paths, col_num)
ids = []
skip_type = {BqServiceType.MARKER.name, BqServiceType.SPACE.name}
for path in paths:
itr = model.get_iter(path)
fav_id = model.get_value(itr, 18 if target is ViewTarget.SERVICES else 7)
channel = channels.get(fav_id, None)
if channel:
bq_id = to_bouquet_id(channel)
fav_id = model.get_value(itr, Column.SRV_FAV_ID if target is ViewTarget.SERVICES else Column.FAV_ID)
srv = services.get(fav_id, None)
if srv and srv.service_type not in skip_type:
bq_id = srv.data_id if srv.service_type == BqServiceType.IPTV.name else to_bouquet_id(srv)
if not bq_id:
continue
blacklist.discard(bq_id) if locked else blacklist.add(bq_id)
model.set_value(itr, col_num, None if locked else LOCKED_ICON)
channels[fav_id] = Service(*channel[:4], None if locked else LOCKED_ICON, *channel[5:])
services[fav_id] = srv._replace(locked=None if locked else LOCKED_ICON)
ids.append(fav_id)
if target is ViewTarget.FAV and ids:
for ch in services_model:
if ch[18] in ids:
ch[4] = None if locked else LOCKED_ICON
gen = update_services_model(ids, locked, services_model)
GLib.idle_add(lambda: next(gen, False))
def set_hide(channels, model, paths):
col_num = 5
def update_services_model(ids, locked, services_model):
for srv in services_model:
if srv[Column.SRV_FAV_ID] in ids:
srv[Column.SRV_LOCKED] = None if locked else LOCKED_ICON
yield True
def set_hide(services, model, paths):
col_num = Column.SRV_HIDE
hide = has_locked_hide(model, paths, col_num)
for path in paths:
@@ -223,28 +291,28 @@ def set_hide(channels, model, paths):
value = int(flag[2:]) if flag else 0
if not hide:
if value in FLAG.hide_values():
if Flag.is_hide(value):
continue # skip if already hidden
value += FLAG.HIDE.value
value += Flag.HIDE.value
else:
if value not in FLAG.hide_values():
if not Flag.is_hide(value):
continue # skip if already allowed to show
value -= FLAG.HIDE.value
value -= Flag.HIDE.value
if value == 0 and index is not None:
del flags[index]
else:
value = "f:{}".format(value) if value > 10 else "f:0{}".format(value)
value = "f:{:02d}".format(value)
if index is not None:
flags[index] = value
else:
flags.append(value)
model.set_value(itr, 0, (",".join(reversed(sorted(flags)))))
fav_id = model.get_value(itr, 18)
channel = channels.get(fav_id, None)
if channel:
channels[fav_id] = Service(*channel[:5], None if hide else HIDE_ICON, *channel[6:])
fav_id = model.get_value(itr, Column.SRV_FAV_ID)
srv = services.get(fav_id, None)
if srv:
services[fav_id] = srv._replace(hide=None if hide else HIDE_ICON)
def has_locked_hide(model, paths, col_num):
@@ -266,9 +334,9 @@ def locate_in_services(fav_view, services_view, parent_window):
show_dialog(DialogType.ERROR, parent_window, "Please, select only one item!")
return
fav_id = model.get_value(model.get_iter(paths[0]), 7)
fav_id = model.get_value(model.get_iter(paths[0]), Column.FAV_ID)
for index, row in enumerate(services_view.get_model()):
if row[18] == fav_id:
if row[Column.SRV_FAV_ID] == fav_id:
scroll_to(index, services_view)
break
@@ -285,93 +353,122 @@ def scroll_to(index, view, paths=None):
# ***************** Picons *********************#
def update_picons(path, picons, model):
def update_picons_data(path, picons):
if not os.path.exists(path):
return
for file in os.listdir(path):
picons[file] = get_picon_pixbuf(path + file)
for r in model:
model.set_value(model.get_iter(r.path), 8, picons.get(r[9], None))
pf = get_picon_pixbuf(path + file)
if pf:
picons[file] = pf
def assign_picon(target, srv_view, fav_view, transient, picons, options, services):
def append_picons(picons, model):
def append_picons_data(pcs, mod):
for r in mod:
mod.set_value(mod.get_iter(r.path), Column.SRV_PICON, pcs.get(r[Column.SRV_PICON_ID], None))
yield True
app = append_picons_data(picons, model)
GLib.idle_add(lambda: next(app, False), priority=GLib.PRIORITY_LOW)
def assign_picons(target, srv_view, fav_view, transient, picons, settings, services, src_path=None, dst_path=None):
""" Assigning picons and returns picons files list. """
view = srv_view if target is ViewTarget.SERVICES else fav_view
model, paths = view.get_selection().get_selected_rows()
if not is_only_one_item_selected(paths, transient):
return
picons_files = []
response = get_chooser_dialog(transient, options, "*.png", "png files")
if response == Gtk.ResponseType.CANCEL:
return
if not src_path:
src_path = get_chooser_dialog(transient, settings, "*.png files", ("*.png",))
if src_path == Gtk.ResponseType.CANCEL:
return picons_files
if not str(response).endswith(".png"):
if not str(src_path).endswith(".png") or not os.path.isfile(src_path):
show_dialog(DialogType.ERROR, transient, text="No png file is selected!")
return
return picons_files
picon_pos = 8
model = get_base_model(model)
itr = model.get_iter(paths)
fav_id = model.get_value(itr, 18 if target is ViewTarget.SERVICES else 7)
picon_id = services.get(fav_id)[9]
p_pos = Column.SRV_PICON
col_num = Column.SRV_FAV_ID if target is ViewTarget.SERVICES else Column.FAV_ID
itrs = [model.get_iter(p) for p in paths]
if picon_id:
picon_file = options.get("picons_dir_path") + picon_id
if os.path.isfile(response):
shutil.copy(response, picon_file)
picon = get_picon_pixbuf(picon_file)
picons[picon_id] = picon
model.set_value(itr, picon_pos, picon)
if target is ViewTarget.SERVICES:
set_picon(fav_id, fav_view.get_model(), picon, 7, picon_pos)
if target is ViewTarget.SERVICES:
f_model = model.get_model()
itrs = [f_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr)) for itr in itrs]
model = get_base_model(model)
for itr in itrs:
fav_id = model.get_value(itr, col_num)
picon_id = services.get(fav_id)[Column.SRV_PICON_ID]
if picon_id:
picons_path = dst_path or settings.picons_local_path
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
picon_file = picons_path + picon_id
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, 18, picon_pos)
picons_files.append(picon_file)
picon = get_picon_pixbuf(picon_file)
picons[picon_id] = picon
model.set_value(itr, p_pos, picon)
if target is ViewTarget.SERVICES:
set_picon(fav_id, fav_view.get_model(), picon, Column.FAV_ID, p_pos)
else:
set_picon(fav_id, get_base_model(srv_view.get_model()), picon, Column.SRV_FAV_ID, p_pos)
return picons_files
def set_picon(fav_id, model, picon, fav_id_pos, picon_pos):
for row in model:
if row[fav_id_pos] == fav_id:
row[picon_pos] = picon
break
return True
return True
def remove_picon(target, srv_view, fav_view, picons, options):
def remove_picon(target, srv_view, fav_view, picons, settings):
view = srv_view if target is ViewTarget.SERVICES else fav_view
model, paths = view.get_selection().get_selected_rows()
model = get_base_model(model)
fav_ids = []
picon_ids = []
picon_pos = 8 # picon position is equal for services and fav
picon_pos = Column.SRV_PICON # picon position is equal for services and fav
for path in paths:
itr = model.get_iter(path)
itrs = [model.get_iter(p) for p in paths]
if target is ViewTarget.SERVICES:
f_model = model.get_model()
itrs = [f_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr)) for itr in itrs]
model = get_base_model(model)
for itr in itrs:
model.set_value(itr, picon_pos, None)
if target is ViewTarget.SERVICES:
fav_ids.append(model.get_value(itr, 18))
picon_ids.append(model.get_value(itr, 9))
fav_ids.append(model.get_value(itr, Column.SRV_FAV_ID))
picon_ids.append(model.get_value(itr, Column.SRV_PICON_ID))
else:
fav_ids.append(model.get_value(itr, 7))
srv_type, fav_id = model.get(itr, Column.FAV_TYPE, Column.FAV_ID)
if srv_type == BqServiceType.IPTV.name:
picon_ids.append("{}_{}_{}_{}_{}_{}_{}_{}_{}_{}.png".format(*fav_id.split(":")[0:10]).strip())
else:
fav_ids.append(fav_id)
def remove(md, path, itr):
if md.get_value(itr, 7 if target is ViewTarget.SERVICES else 18) in fav_ids:
md.set_value(itr, picon_pos, None)
fav_id_column = Column.FAV_ID if target is ViewTarget.SERVICES else Column.SRV_FAV_ID
def remove(md, path, it):
if md.get_value(it, fav_id_column) in fav_ids:
md.set_value(it, picon_pos, None)
if target is ViewTarget.FAV:
picon_ids.append(md.get_value(itr, 9))
picon_ids.append(md.get_value(it, Column.SRV_PICON_ID))
fav_view.get_model().foreach(remove) if target is ViewTarget.SERVICES else get_base_model(
srv_view.get_model()).foreach(remove)
pions_path = options.get("picons_dir_path")
backup_path = options.get("data_dir_path") + "backup/picons/"
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
for p_id in picon_ids:
picons[p_id] = None
src = pions_path + p_id
if os.path.isfile(src):
shutil.move(src, backup_path + p_id)
remove_picons(settings, picon_ids, picons)
def copy_picon_reference(target, view, services, clipboard, transient):
@@ -381,13 +478,13 @@ def copy_picon_reference(target, view, services, clipboard, transient):
return
if target is ViewTarget.SERVICES:
picon_id = model.get_value(model.get_iter(paths), 9)
picon_id = model.get_value(model.get_iter(paths), Column.SRV_PICON_ID)
if picon_id:
clipboard.set_text(picon_id.rstrip(".png"), -1)
else:
show_dialog(DialogType.ERROR, transient, "No reference is present!")
elif target is ViewTarget.FAV:
fav_id = model.get_value(model.get_iter(paths), 7)
fav_id = model.get_value(model.get_iter(paths), Column.FAV_ID)
srv = services.get(fav_id, None)
if srv and srv.picon_id:
clipboard.set_text(srv.picon_id.rstrip(".png"), -1)
@@ -395,6 +492,23 @@ def copy_picon_reference(target, view, services, clipboard, transient):
show_dialog(DialogType.ERROR, transient, "No reference is present!")
def remove_all_unused_picons(settings, picons, services):
ids = {s.picon_id for s in services}
pcs = list(filter(lambda x: x not in ids, picons))
remove_picons(settings, pcs, picons)
def remove_picons(settings, picon_ids, picons):
pions_path = settings.picons_local_path
backup_path = settings.backup_local_path + "picons/"
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
for p_id in picon_ids:
picons[p_id] = None
src = pions_path + p_id
if os.path.isfile(src):
shutil.move(src, backup_path + p_id)
def is_only_one_item_selected(paths, transient):
if len(paths) > 1:
show_dialog(DialogType.ERROR, transient, "Please, select only one item!")
@@ -407,37 +521,78 @@ def is_only_one_item_selected(paths, transient):
return True
def get_picon_pixbuf(path):
return GdkPixbuf.Pixbuf.new_from_file_at_scale(filename=path, width=32, height=32, preserve_aspect_ratio=True)
def get_picon_pixbuf(path, size=32):
try:
return GdkPixbuf.Pixbuf.new_from_file_at_scale(path, width=size, height=size, preserve_aspect_ratio=True)
except GLib.GError as e:
pass
# ***************** Search *********************#
# ***************** Bouquets *********************#
def search(text, srv_view, fav_view, bqs_view, services, bouquets):
for view in srv_view, fav_view:
model = get_base_model(view.get_model())
selection = view.get_selection()
selection.unselect_all()
if not text:
continue
paths = []
text = text.upper()
for r in model:
if text in str(r[:]).upper():
path = r.path
selection.select_path(r.path)
paths.append(path)
def gen_bouquets(view, bq_view, transient, gen_type, tv_types, s_type, callback):
""" Auto-generate and append list of bouquets """
fav_id_index = Column.SRV_FAV_ID
index = Column.SRV_TYPE
if gen_type in (BqGenType.PACKAGE, BqGenType.EACH_PACKAGE):
index = Column.SRV_PACKAGE
elif gen_type in (BqGenType.SAT, BqGenType.EACH_SAT):
index = Column.SRV_POS
if paths:
view.scroll_to_cell(paths[0], None)
model, paths = view.get_selection().get_selected_rows()
bq_type = BqType.BOUQUET.value if s_type is SettingsType.NEUTRINO_MP else BqType.TV.value
if gen_type in (BqGenType.SAT, BqGenType.PACKAGE, BqGenType.TYPE):
if not is_only_one_item_selected(paths, transient):
return
service = Service(*model[paths][:Column.SRV_TOOLTIP])
if service.service_type not in tv_types:
bq_type = BqType.RADIO.value
append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model,
[service.package if gen_type is BqGenType.PACKAGE else
service.pos if gen_type is BqGenType.SAT else service.service_type], s_type)
else:
wait_dialog = WaitDialog(transient)
wait_dialog.show()
append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model,
{row[index] for row in model}, s_type, wait_dialog)
@run_task
def append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model, names, s_type, wait_dialog=None):
bq_index = 0 if s_type is SettingsType.ENIGMA_2 else 1
bq_view.expand_row(Gtk.TreePath(bq_index), 0)
bqs_model = bq_view.get_model()
bouquets_names = get_bouquets_names(bqs_model)
for pos, name in enumerate(sorted(names)):
if name not in bouquets_names:
services = [BouquetService(None, BqServiceType.DEFAULT, row[fav_id_index], 0)
for row in model if row[index] == name]
callback(Bouquet(name=name, type=bq_type, services=services, locked=None, hidden=None),
bqs_model.get_iter(bq_index))
if wait_dialog is not None:
wait_dialog.destroy()
def get_bouquets_names(model):
""" Returns all current bouquets names """
bouquets_names = []
for row in model:
itr = row.iter
if model.iter_has_child(itr):
num_of_children = model.iter_n_children(itr)
for num in range(num_of_children):
child_itr = model.iter_nth_child(itr, num)
bouquets_names.append(model[child_itr][0])
return bouquets_names
# ***************** Others *********************#
def update_entry_data(entry, dialog, options):
def update_entry_data(entry, dialog, settings):
""" Updates value in text entry from chooser dialog """
response = show_dialog(dialog_type=DialogType.CHOOSER, transient=dialog, options=options)
response = show_dialog(dialog_type=DialogType.CHOOSER, transient=dialog, settings=settings)
if response not in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
entry.set_text(response)
return response
@@ -445,11 +600,58 @@ def update_entry_data(entry, dialog, options):
def get_base_model(model):
""" Returns base tree model if has wrappers ("TreeModelSort" and "TreeModelFilter") """
""" Returns base tree model if has wrappers [TreeModelSort, TreeModelFilter]. """
if type(model) is Gtk.TreeModelSort:
return model.get_model().get_model()
return model
def get_base_itrs(itrs, model):
""" Returns base iters from wrapper models. """
if type(model) is Gtk.TreeModelSort:
filter_model = model.get_model()
return [filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr)) for itr in itrs]
return itrs
def get_base_paths(paths, model):
""" Returns base paths from wrapper models. """
if type(model) is Gtk.TreeModelSort:
filter_model = model.get_model()
return [filter_model.convert_path_to_child_path(model.convert_path_to_child_path(p)) for p in paths]
return paths
def get_model_data(view):
""" Returns model name and base model from the given view """
model = get_base_model(view.get_model())
model_name = model.get_name()
return model_name, model
def append_text_to_tview(char, view):
""" Appending text and scrolling to a given line in the text view. """
buf = view.get_buffer()
buf.insert_at_cursor(char)
insert = buf.get_insert()
view.scroll_to_mark(insert, 0.0, True, 0.0, 1.0)
def get_iptv_url(row, s_type):
""" Returns url from iptv type row """
data = row[Column.FAV_ID].split(":" if s_type is SettingsType.ENIGMA_2 else "::")
if s_type is SettingsType.ENIGMA_2:
data = list(filter(lambda x: "http" in x, data))
if data:
url = data[0]
return unquote(url) if s_type is SettingsType.ENIGMA_2 else url
def on_popup_menu(menu, event):
""" Shows popup menu for the view """
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
menu.popup(None, None, None, None, event.button, event.time)
if __name__ == "__main__":
pass

File diff suppressed because it is too large Load Diff

View File

@@ -1,719 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.18.3 -->
<interface>
<requires lib="gtk+" version="3.12"/>
<!-- interface-css-provider-path style.css -->
<object class="GtkListStore" id="providers_list_store">
<columns>
<!-- column-name logo -->
<column type="GdkPixbuf"/>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name pos -->
<column type="gchararray"/>
<!-- column-name url -->
<column type="gchararray"/>
<!-- column-name on_id -->
<column type="gchararray"/>
<!-- column-name selected -->
<column type="gboolean"/>
</columns>
</object>
<object class="GtkDialog" id="picons_dialog">
<property name="width_request">480</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Picons download tool</property>
<property name="resizable">False</property>
<property name="destroy_with_parent">True</property>
<property name="icon_name">emblem-photos</property>
<property name="type_hint">dialog</property>
<child internal-child="vbox">
<object class="GtkBox" id="picons_dialog_vbox">
<property name="width_request">320</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">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="dialog_action_area">
<property name="can_focus">False</property>
<property name="margin_left">2</property>
<property name="margin_right">2</property>
<property name="homogeneous">True</property>
<property name="layout_style">spread</property>
<child>
<object class="GtkButton" id="cancel_button">
<property name="label">gtk-close</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_close" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="main_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="GtkGrid" id="grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="column_spacing">2</property>
<property name="column_homogeneous">True</property>
<child>
<object class="GtkEntry" id="ip_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">network-transmit-receive-symbolic</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="picons_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ip_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Receiver IP:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="res_picons_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Receiver picons path:</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</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="GtkLabel" id="picons_dir_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Current picons path:</property>
<property name="xalign">0.019999999552965164</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="picons_dir_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="secondary_icon_name">folder-open-symbolic</property>
<property name="primary_icon_activatable">False</property>
<signal name="icon-press" handler="on_picons_dir_open" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="format_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="column_spacing">2</property>
<child>
<object class="GtkBox" id="name_format_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Picons name format:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="box2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_right">5</property>
<child>
<object class="GtkRadioButton" id="enigma2_radio_button">
<property name="label" translatable="yes">Enigma2 (default)</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="xalign">0</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<property name="group">neutrino_mp_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="neutrino_mp_radio_button">
<property name="label" translatable="yes">Neutrino-MP</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="xalign">0</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<property name="group">enigma2_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="resize_format_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Resize: </property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="resize_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkRadioButton" id="resize_no_radio_button">
<property name="label" translatable="yes">No(default)</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="xalign">0</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<property name="group">resize_100_60_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="resize_220_132_radio_button">
<property name="label" translatable="yes">220x132</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="xalign">0</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<property name="group">resize_100_60_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="resize_100_60_radio_button">
<property name="label" translatable="yes">100x60</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="xalign">0</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<property name="group">resize_no_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkSeparator" id="separator3">
<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="orientation">vertical</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<style>
<class name="primary-toolbar"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkSeparator" id="separator1">
<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">4</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="url_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Satellite url (www.lyngsat.com):</property>
<property name="xalign">0.019999999552965164</property>
<style>
<class name="primary-toolbar"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="url_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">network-workgroup-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="placeholder_text" translatable="yes">https://www.lyngsat.com/*satellite*.html</property>
<property name="input_purpose">url</property>
<signal name="changed" handler="on_url_changed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">6</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="providers_scrolled_window">
<property name="height_request">150</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">out</property>
<child>
<object class="GtkTreeView" id="providers_tree_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="has_focus">True</property>
<property name="model">providers_list_store</property>
<child internal-child="selection">
<object class="GtkTreeSelection" id="treeview_selection"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="provider_column">
<property name="spacing">15</property>
<property name="title" translatable="yes">Providers</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererPixbuf" id="logo_cellrendererpixbuf"/>
<attributes>
<attribute name="pixbuf">0</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererText" id="name_cellrenderertext"/>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="pos_column">
<property name="sizing">autosize</property>
<property name="title" translatable="yes">Position</property>
<child>
<object class="GtkCellRendererText" id="pos_cellrenderertext1">
<property name="xalign">0.50999999046325684</property>
<property name="editable">True</property>
<signal name="edited" handler="on_position_edited" swapped="no"/>
</object>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="url_column">
<property name="visible">False</property>
<property name="title" translatable="yes">Url</property>
<child>
<object class="GtkCellRendererText" id="url_cellrenderertext"/>
<attributes>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="on_id_column">
<property name="visible">False</property>
<property name="title" translatable="yes">ONID</property>
<child>
<object class="GtkCellRendererText" id="onid_cellrenderertext"/>
<attributes>
<attribute name="text">4</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="selected_column">
<property name="title" translatable="yes">Selected</property>
<child>
<object class="GtkCellRendererToggle" id="cellrenderer_toggle">
<signal name="toggled" handler="on_selected_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">5</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">10</property>
</packing>
</child>
<child>
<object class="GtkSeparator" id="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="padding">2</property>
<property name="position">11</property>
</packing>
</child>
<child>
<object class="GtkToolbar" id="toolbar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="show_arrow">False</property>
<child>
<object class="GtkToolButton" id="cancel_tool_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Cancel</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-cancel</property>
<signal name="clicked" handler="on_cancel" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkSeparatorToolItem" id="separatortoolitem2">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="receive_tool_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Receive picons for providers</property>
<property name="is_important">True</property>
<property name="label" translatable="yes">Receive picons</property>
<property name="use_underline">True</property>
<property name="icon_name">go-bottom</property>
<signal name="clicked" handler="on_receive" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="load_providers_tool_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Load satellite providers.</property>
<property name="is_important">True</property>
<property name="label" translatable="yes">Load providers</property>
<property name="use_underline">True</property>
<property name="icon_name">network-server-symbolic</property>
<signal name="clicked" handler="on_load_providers" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkSeparatorToolItem" id="separatortoolitem1">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="send_tool_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Transfer to receiver</property>
<property name="label" translatable="yes">Send</property>
<property name="use_underline">True</property>
<property name="icon_name">go-top</property>
<signal name="clicked" handler="on_send" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="homogeneous">True</property>
</packing>
</child>
<style>
<class name="primary-toolbar"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">12</property>
</packing>
</child>
<child>
<object class="GtkExpander" id="expander">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="resize_toplevel">True</property>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="height_request">150</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="min_content_width">240</property>
<child>
<object class="GtkTextView" id="text_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="wrap_mode">word-char</property>
<property name="overwrite">True</property>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="expander_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Info</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">13</property>
</packing>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="app_paintable">True</property>
<property name="can_focus">False</property>
<property name="show_close_button">True</property>
<signal name="response" handler="on_info_bar_close" swapped="no"/>
<child internal-child="action_area">
<object class="GtkButtonBox" id="infobar-action_area1">
<property name="can_focus">False</property>
<property name="spacing">6</property>
<property name="layout_style">end</property>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<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" id="infobar-content_area1">
<property name="can_focus">False</property>
<property name="spacing">16</property>
<child>
<placeholder/>
</child>
<child>
<object class="GtkLabel" id="info_bar_message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Info</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</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">14</property>
</packing>
</child>
<child>
<object class="GtkSeparator" id="separator2">
<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">15</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkListStore" id="sat_position_list_store">
<columns>
<!-- column-name pos -->
<column type="gchararray"/>
</columns>
</object>
</interface>

View File

@@ -1,246 +0,0 @@
import re
import subprocess
import tempfile
import time
from gi.repository import GLib, GdkPixbuf
from app.commons import run_idle, run_task
from app.ftp import upload_data, DownloadDataType
from app.picons.picons import PiconsParser, parse_providers, Provider
from app.properties import Profile
from . import Gtk, Gdk, UI_RESOURCES_PATH
from .dialogs import show_dialog, DialogType
from .main_helper import update_entry_data
class PiconsDialog:
def __init__(self, transient, options, picon_ids, profile=Profile.ENIGMA_2):
self._picon_ids = picon_ids
self._TMP_DIR = tempfile.gettempdir() + "/"
self._BASE_URL = "www.lyngsat.com/packages/"
self._PATTERN = re.compile("^https://www\.lyngsat\.com/[\w-]+\.html$")
self._current_process = None
self._picons_path = options.get("picons_dir_path", "")
self._terminate = False
handlers = {"on_receive": self.on_receive,
"on_load_providers": self.on_load_providers,
"on_cancel": self.on_cancel,
"on_close": self.on_close,
"on_send": self.on_send,
"on_info_bar_close": self.on_info_bar_close,
"on_picons_dir_open": self.on_picons_dir_open,
"on_selected_toggled": self.on_selected_toggled,
"on_url_changed": self.on_url_changed,
"on_position_edited": self.on_position_edited}
builder = Gtk.Builder()
builder.add_objects_from_file(UI_RESOURCES_PATH + "picons_dialog.glade",
("picons_dialog", "receive_image", "providers_list_store"))
builder.connect_signals(handlers)
self._dialog = builder.get_object("picons_dialog")
self._dialog.set_transient_for(transient)
self._providers_tree_view = builder.get_object("providers_tree_view")
self._expander = builder.get_object("expander")
self._text_view = builder.get_object("text_view")
self._info_bar = builder.get_object("info_bar")
self._ip_entry = builder.get_object("ip_entry")
self._picons_entry = builder.get_object("picons_entry")
self._url_entry = builder.get_object("url_entry")
self._picons_dir_entry = builder.get_object("picons_dir_entry")
self._info_bar = builder.get_object("info_bar")
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label")
self._load_providers_tool_button = builder.get_object("load_providers_tool_button")
self._receive_tool_button = builder.get_object("receive_tool_button")
self._enigma2_radio_button = builder.get_object("enigma2_radio_button")
self._neutrino_mp_radio_button = builder.get_object("neutrino_mp_radio_button")
self._resize_no_radio_button = builder.get_object("resize_no_radio_button")
self._resize_220_132_radio_button = builder.get_object("resize_220_132_radio_button")
self._resize_100_60_radio_button = builder.get_object("resize_100_60_radio_button")
# style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
self._properties = options
self._profile = profile
self._ip_entry.set_text(options.get("host", ""))
self._picons_entry.set_text(options.get("picons_path", ""))
self._picons_path = options.get("picons_dir_path", "")
self._picons_dir_entry.set_text(self._picons_path)
def show(self):
self._dialog.run()
self._dialog.destroy()
@run_idle
def on_load_providers(self, item):
self._expander.set_expanded(True)
url = self._url_entry.get_text()
self._current_process = subprocess.Popen(["wget", "-pkP", self._TMP_DIR, url],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
GLib.io_add_watch(self._current_process.stderr, GLib.IO_IN, self.write_to_buffer)
self.append_providers(url)
@run_task
def append_providers(self, url):
model = self._providers_tree_view.get_model()
model.clear()
self._current_process.wait()
providers = parse_providers(self._TMP_DIR + url[url.find("w"):])
if providers:
for p in providers:
model.append((self.get_pixbuf(p[0]), p.name, p.pos, p.url, p.on_id, p.selected))
self.update_receive_button_state()
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)
@run_idle
def on_receive(self, item):
self.start_download()
@run_task
def start_download(self):
if self._current_process.poll() is None:
self.show_dialog("The task is already running!", DialogType.ERROR)
return
self._terminate = False
self._expander.set_expanded(True)
for prv in self.get_selected_providers():
if self._terminate:
break
self.process_provider(Provider(*prv))
def process_provider(self, prv):
url = prv.url
self.show_info_message("Please, wait...", Gtk.MessageType.INFO)
self._current_process = subprocess.Popen(["wget", "-pkP", self._TMP_DIR, url],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
GLib.io_add_watch(self._current_process.stderr, GLib.IO_IN, self.write_to_buffer)
self._current_process.wait()
path = self._TMP_DIR + self._BASE_URL + url[url.rfind("/") + 1:]
pos = "".join(c for c in prv.pos if c.isdigit())
PiconsParser.parse(path, self._picons_path, self._TMP_DIR, prv.on_id, pos,
self._picon_ids, self.get_picons_format())
self.resize(self._picons_path)
self.show_info_message("Done", Gtk.MessageType.INFO)
def write_to_buffer(self, fd, condition):
if condition == GLib.IO_IN:
char = fd.read(1)
self.append_output(char)
return True
else:
return False
@run_idle
def append_output(self, char):
buf = self._text_view.get_buffer()
buf.insert_at_cursor(char)
self.scroll_to_end(buf)
def scroll_to_end(self, buf):
insert = buf.get_insert()
self._text_view.scroll_to_mark(insert, 0.0, True, 0.0, 1.0)
def resize(self, path):
if self._resize_no_radio_button.get_active():
return
self.show_info_message("Resizing...", Gtk.MessageType.INFO)
command = "mogrify -resize {}! *.png".format(
"320x240" if self._resize_220_132_radio_button.get_active() else "100x60").split()
self._current_process = subprocess.Popen(command, universal_newlines=True, cwd=path)
self._current_process.wait()
@run_task
def on_cancel(self, item):
if self._current_process:
self._terminate = True
self._current_process.terminate()
time.sleep(1)
@run_idle
def on_close(self, item):
self.on_cancel(item)
self._dialog.destroy()
def on_send(self, item):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
self.show_info_message("Please, wait...", Gtk.MessageType.INFO)
self.upload_picons()
@run_task
def upload_picons(self):
if self._current_process is not None and self._current_process.poll() is None:
self.show_dialog("The task is already running!", DialogType.ERROR)
return
upload_data(properties=self._properties,
download_type=DownloadDataType.PICONS,
profile=self._profile,
callback=lambda: self.show_info_message("Done!", Gtk.MessageType.INFO))
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(text)
def on_picons_dir_open(self, entry, icon, event_button):
update_entry_data(entry, self._dialog, options={"data_dir_path": self._picons_path})
@run_idle
def on_selected_toggled(self, toggle, path):
model = self._providers_tree_view.get_model()
model.set_value(model.get_iter(path), 5, not toggle.get_active())
self.update_receive_button_state()
def on_url_changed(self, entry):
suit = self._PATTERN.search(entry.get_text())
entry.set_name("GtkEntry" if suit else "digit-entry")
self._load_providers_tool_button.set_sensitive(suit if suit else False)
def on_position_edited(self, render, path, value):
model = self._providers_tree_view.get_model()
model.set_value(model.get_iter(path), 2, value)
@run_idle
def update_receive_button_state(self):
self._receive_tool_button.set_sensitive(len(self.get_selected_providers()) > 0)
def get_selected_providers(self):
""" returns selected providers """
return [r for r in self._providers_tree_view.get_model() if r[5]]
@run_idle
def show_dialog(self, message, dialog_type):
show_dialog(dialog_type, self._dialog, message)
def get_picons_format(self):
picon_format = Profile.ENIGMA_2
if self._neutrino_mp_radio_button.get_active():
picon_format = Profile.NEUTRINO_MP
return picon_format
if __name__ == "__main__":
pass

1835
app/ui/picons_manager.glade Normal file

File diff suppressed because it is too large Load Diff

844
app/ui/picons_manager.py Normal file
View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +1,41 @@
import concurrent.futures
import re
import time
from math import fabs
from app.commons import run_idle
from gi.repository import GLib
from app.commons import run_idle, run_task, log
from app.eparser import get_satellites, write_satellites, Satellite, Transponder
from . import Gtk, Gdk, UI_RESOURCES_PATH
from .dialogs import show_dialog, DialogType
from .main_helper import move_items, scroll_to
from app.eparser.ecommons import PLS_MODE, get_key_by_value
from app.tools.satellites import SatellitesParser, SatelliteSource, ServicesParser
from .dialogs import show_dialog, DialogType, get_dialogs_string, get_chooser_dialog, get_message
from .main_helper import move_items, scroll_to, append_text_to_tview, get_base_model, on_popup_menu
from .search import SearchProvider
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, MOVE_KEYS, KeyboardKey, IS_GNOME_SESSION, MOD_MASK
_UI_PATH = UI_RESOURCES_PATH + "satellites_dialog.glade"
def show_satellites_dialog(transient, options):
dialog = SatellitesDialog(transient, options)
dialog.run()
dialog.destroy()
SatellitesDialog(transient, options).show()
class SatellitesDialog:
__slots__ = ["_dialog", "_data_path", "_stores", "_options", "_sat_view"]
_aggr = [None for x in range(9)] # aggregate
def __init__(self, transient, options):
self._data_path = options.get("data_dir_path") + "satellites.xml"
self._options = options
def __init__(self, transient, settings):
self._data_path = settings.data_local_path + "satellites.xml"
self._settings = settings
handlers = {"on_open": self.on_open,
"on_remove": self.on_remove,
"on_save": self.on_save,
"on_save_as": self.on_save_as,
"on_update": self.on_update,
"on_up": self.on_up,
"on_down": self.on_down,
"on_popup_menu": self.on_popup_menu,
"on_popup_menu": on_popup_menu,
"on_satellite_add": self.on_satellite_add,
"on_transponder_add": self.on_transponder_add,
"on_edit": self.on_edit,
@@ -38,60 +45,56 @@ class SatellitesDialog:
"on_quit": self.on_quit}
builder = Gtk.Builder()
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
("satellites_editor_dialog", "satellites_tree_store",
"popup_menu", "add_popup_menu", "add_menu_icon"))
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)
# Adding custom image for add_menu_tool_button
add_menu_tool_button = builder.get_object("add_menu_tool_button")
add_menu_tool_button.set_image(builder.get_object("add_menu_icon"))
self._dialog = builder.get_object("satellites_editor_dialog")
self._dialog.set_transient_for(transient)
self._dialog.get_content_area().set_border_width(0) # The width of the border around the app dialog area!
self._window = builder.get_object("satellites_editor_window")
self._window.set_transient_for(transient)
self._sat_view = builder.get_object("satellites_editor_tree_view")
# Setting the last size of the dialog window if it was saved
window_size = self._options.get("sat_editor_window_size", None)
window_size = self._settings.get("sat_editor_window_size")
if window_size:
self._dialog.resize(*window_size)
self._window.resize(*window_size)
self._stores = {3: builder.get_object("pol_store"),
4: builder.get_object("fec_store"),
5: builder.get_object("system_store"),
6: builder.get_object("mod_store")}
self.on_satellites_list_load(self._sat_view.get_model())
def run(self):
self._dialog.run()
self.load_satellites_list(self._sat_view.get_model())
def destroy(self):
self._dialog.destroy()
def load_satellites_list(self, model):
gen = self.on_satellites_list_load(model)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def show(self):
self._window.show()
def on_resize(self, window):
""" Stores new size properties for dialog window after resize """
if self._options:
self._options["sat_editor_window_size"] = window.get_size()
if self._settings:
self._settings.add("sat_editor_window_size", window.get_size())
def on_quit(self, item):
self.destroy()
@run_idle
def on_quit(self, *args):
self._window.destroy()
@run_idle
def on_open(self, model):
file_filter = Gtk.FileFilter()
file_filter.add_pattern("satellites.xml")
file_filter.set_name("satellites.xml")
response = show_dialog(dialog_type=DialogType.CHOOSER,
transient=self._dialog,
options=self._options,
action_type=Gtk.FileChooserAction.OPEN,
file_filter=file_filter)
if response == Gtk.ResponseType.CANCEL:
response = get_chooser_dialog(self._window, self._settings, "satellites.xml", ("*.xml",))
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
if not str(response).endswith("satellites.xml"):
show_dialog(DialogType.ERROR, self._dialog, text="No satellites.xml file is selected!")
show_dialog(DialogType.ERROR, self._window, text="No satellites.xml file is selected!")
return
self._data_path = response
self.on_satellites_list_load(model)
self.load_satellites_list(model)
@staticmethod
def on_row_activated(view, path, column):
@@ -101,51 +104,48 @@ class SatellitesDialog:
view.expand_row(path, column)
def on_up(self, item):
move_items(Gdk.KEY_Up, self._sat_view)
move_items(KeyboardKey.UP, self._sat_view)
def on_down(self, item):
move_items(Gdk.KEY_Down, self._sat_view)
move_items(KeyboardKey.DOWN, self._sat_view)
def on_key_release(self, view, event):
""" Handling keystrokes """
key = event.keyval
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK
if key == Gdk.KEY_Delete:
if key is KeyboardKey.DELETE:
self.on_remove(view)
elif key == Gdk.KEY_Insert:
elif key is KeyboardKey.INSERT:
pass
elif ctrl and key == Gdk.KEY_E or key == Gdk.KEY_e:
elif ctrl and key is KeyboardKey.E:
self.on_edit(view)
elif ctrl and key == Gdk.KEY_s or key == Gdk.KEY_S:
elif ctrl and key is KeyboardKey.S:
self.on_satellite()
elif ctrl and key == Gdk.KEY_t or key == Gdk.KEY_T:
elif ctrl and key is KeyboardKey.T:
self.on_transponder()
elif key == Gdk.KEY_space:
pass
elif ctrl and key in (Gdk.KEY_Up, Gdk.KEY_Page_Up, Gdk.KEY_KP_Page_Up): # KEY_KP_Page_Up for laptop!
move_items(key, self._sat_view)
elif ctrl and key in (Gdk.KEY_Down, Gdk.KEY_Page_Down, Gdk.KEY_KP_Page_Down):
elif ctrl and key in MOVE_KEYS:
move_items(key, self._sat_view)
elif key is KeyboardKey.LEFT or key is KeyboardKey.RIGHT:
view.do_unselect_all(view)
@run_idle
def on_satellites_list_load(self, model):
""" Load satellites data into model """
try:
satellites = get_satellites(self._data_path)
yield True
except FileNotFoundError as e:
show_dialog(DialogType.ERROR, self._dialog, getattr(e, "message", str(e)) +
show_dialog(DialogType.ERROR, self._window, getattr(e, "message", str(e)) +
"\n\nPlease, download files from receiver or setup your path for read data!")
return
else:
model.clear()
self.append_data(model, satellites)
@run_idle
def append_data(self, model, satellites):
for name, flags, pos, transponders in satellites:
parent = model.append(None, [name, *self._aggr, flags, pos])
for transponder in transponders:
model.append(parent, ["Transponder:", *transponder, None, None])
for sat in satellites:
append_satellite(model, sat)
yield True
def on_add(self, view):
""" Common adding """
@@ -174,7 +174,7 @@ class SatellitesDialog:
def on_satellite(self, satellite=None, edited_itr=None):
""" Create or edit satellite"""
sat_dialog = SatelliteDialog(self._dialog, satellite)
sat_dialog = SatelliteDialog(self._window, satellite)
sat = sat_dialog.run()
sat_dialog.destroy()
@@ -195,10 +195,10 @@ class SatellitesDialog:
if paths is None:
return
elif len(paths) == 0:
show_dialog(DialogType.ERROR, self._dialog, "No satellite is selected!")
show_dialog(DialogType.ERROR, self._window, "No satellite is selected!")
return
dialog = TransponderDialog(self._dialog, transponder)
dialog = TransponderDialog(self._window, transponder)
tr = dialog.run()
dialog.destroy()
@@ -248,10 +248,8 @@ class SatellitesDialog:
returns selected path or None
"""
model, paths = view.get_selection().get_selected_rows()
paths_count = len(paths)
if paths_count > 1:
show_dialog(DialogType.ERROR, self._dialog, message)
if len(paths) > 1:
show_dialog(DialogType.ERROR, self._window, message)
return
return paths
@@ -260,13 +258,13 @@ class SatellitesDialog:
def on_remove(view):
selection = view.get_selection()
model, paths = selection.get_selected_rows()
itrs = [model.get_iter(path) for path in paths]
for itr in itrs:
for itr in [model.get_iter(path) for path in paths]:
model.remove(itr)
@run_idle
def on_save(self, view):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
if show_dialog(DialogType.QUESTION, self._window) == Gtk.ResponseType.CANCEL:
return
model = view.get_model()
@@ -274,6 +272,16 @@ class SatellitesDialog:
model.foreach(self.parse_data, satellites)
write_satellites(satellites, self._data_path)
def on_save_as(self, item):
response = self.get_file_dialog_response(Gtk.FileChooserAction.SAVE)
if response == Gtk.ResponseType.CANCEL:
return
show_dialog(DialogType.ERROR, transient=self._window, text="Not implemented yet!")
@run_idle
def on_update(self, item):
SatellitesUpdateDialog(self._window, self._settings, self._sat_view.get_model()).show()
@staticmethod
def parse_data(model, path, itr, sats):
if model.iter_has_child(itr):
@@ -290,11 +298,8 @@ class SatellitesDialog:
satellite = Satellite(sat[0], sat[-2], sat[-1], transponders)
sats.append(satellite)
@staticmethod
def on_popup_menu(menu, event):
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
menu.popup(None, None, None, None, event.button, event.time)
# ***************** Transponder dialog *******************#
class TransponderDialog:
""" Shows dialog for adding or edit transponder """
@@ -304,11 +309,10 @@ class TransponderDialog:
handlers = {"on_entry_changed": self.on_entry_changed}
builder = Gtk.Builder()
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
("transponder_dialog",
"pol_store", "fec_store",
"mod_store", "system_store",
"pls_mode_store"))
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)
self._dialog = builder.get_object("transponder_dialog")
@@ -323,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")
@@ -351,7 +355,7 @@ class TransponderDialog:
self._fec_box.set_active_id(transponder.fec_inner)
self._sys_box.set_active_id(transponder.system)
self._mod_box.set_active_id(transponder.modulation)
self._pls_mode_box.set_active_id(transponder.pls_mode)
self._pls_mode_box.set_active_id(PLS_MODE.get(transponder.pls_mode, None))
self._is_id_entry.set_text(transponder.is_id if transponder.is_id else "")
self._pls_code_entry.set_text(transponder.pls_code if transponder.pls_code else "")
@@ -362,7 +366,7 @@ class TransponderDialog:
fec_inner=self._fec_box.get_active_id(),
system=self._sys_box.get_active_id(),
modulation=self._mod_box.get_active_id(),
pls_mode=self._pls_mode_box.get_active_id(),
pls_mode=get_key_by_value(PLS_MODE, self._pls_mode_box.get_active_id()),
pls_code=self._pls_code_entry.get_text(),
is_id=self._is_id_entry.get_text())
@@ -382,13 +386,16 @@ class TransponderDialog:
return True
# ***************** Satellite dialog *******************#
class SatelliteDialog:
""" Shows dialog for adding or edit satellite """
def __init__(self, transient, satellite: Satellite = None):
builder = Gtk.Builder()
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
("satellite_dialog", "side_store", "pos_adjustment"))
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"))
self._dialog = builder.get_object("satellite_dialog")
self._dialog.set_transient_for(transient)
@@ -422,5 +429,521 @@ class SatelliteDialog:
return Satellite(name=name, flags="0", position=pos, transponders=None)
# ********************** Update dialogs ************************ #
class UpdateDialog:
""" Base dialog for update satellites, transponders and services from the web."""
def __init__(self, transient, settings, title=None):
handlers = {"on_update_satellites_list": self.on_update_satellites_list,
"on_receive_data": self.on_receive_data,
"on_cancel_receive": self.on_cancel_receive,
"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,
"on_popup_menu": on_popup_menu,
"on_select_all": self.on_select_all,
"on_unselect_all": self.on_unselect_all,
"on_filter": self.on_filter,
"on_search": self.on_search,
"on_search_down": self.on_search_down,
"on_search_up": self.on_search_up,
"on_quit": self.on_quit}
self._settings = settings
self._download_task = False
self._parser = None
self._size_name = "{}_window_size".format("_".join(re.findall("[A-Z][^A-Z]*", self.__class__.__name__))).lower()
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",
"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",
"update_transponder_store", "update_service_store"))
builder.connect_signals(handlers)
self._window = builder.get_object("satellites_update_window")
self._window.set_transient_for(transient)
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_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")
self._to_pos_button = builder.get_object("to_pos_button")
self._filter_from_combo_box = builder.get_object("filter_from_combo_box")
self._filter_to_combo_box = builder.get_object("filter_to_combo_box")
self._filter_model = builder.get_object("update_sat_list_model_filter")
self._filter_model.set_visible_func(self.filter_function)
self._filter_positions = (0, 0)
# Search
self._search_bar = builder.get_object("sat_update_search_bar")
self._search_provider = SearchProvider((self._sat_view,),
builder.get_object("sat_update_search_down_button"),
builder.get_object("sat_update_search_up_button"))
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.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.is_download = True
src = self._source_box.get_active()
if not self._parser:
self._parser = SatellitesParser()
self.get_sat_list(src, self.append_satellites)
@run_task
def get_sat_list(self, src, callback):
sats = self._parser.get_satellites_list(SatelliteSource.FLYSAT if src == 0 else SatelliteSource.LYNGSAT)
if sats:
callback(sats)
self.is_download = False
@run_idle
def append_satellites(self, sats):
model = get_base_model(self._sat_view.get_model())
for sat in sats:
model.append(sat)
@run_idle
def on_receive_data(self, item):
if self.is_download:
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return
@run_idle
def update_expander(self):
self._sat_update_expander.set_expanded(True)
self._text_view.get_buffer().set_text("", 0)
def append_output(self):
@run_idle
def append(t):
append_text_to_tview(t, self._text_view)
while True:
text = yield
append(text)
def on_cancel_receive(self, item=None):
self._download_task = 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)
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)))
@run_idle
def show_info_message(self, text, message_type):
self._sat_update_info_bar.set_visible(True)
self._sat_update_info_bar.set_message_type(message_type)
self._info_bar_message_label.set_text(text)
def on_info_bar_close(self, bar=None, resp=None):
self._sat_update_info_bar.set_visible(False)
def on_find_toggled(self, button: Gtk.ToggleToolButton):
self._search_bar.set_search_mode(button.get_active())
def on_filter_toggled(self, button: Gtk.ToggleToolButton):
self._filter_bar.set_search_mode(button.get_active())
@run_idle
def on_filter(self, item):
self._filter_positions = self.get_positions()
self._filter_model.refilter()
def filter_function(self, model, itr, data):
if self._filter_model is None or self._filter_model == "None":
return True
from_pos, to_pos = self._filter_positions
if from_pos == 0 and to_pos == 0:
return True
if from_pos > to_pos:
from_pos, to_pos = to_pos, from_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)
to_pos = round(self._to_pos_button.get_value(), 1) * (-1 if self._filter_to_combo_box.get_active() else 1)
return from_pos, to_pos
def on_search(self, entry):
self._search_provider.search(entry.get_text())
def on_search_down(self, item):
self._search_provider.on_search_down()
def on_search_up(self, item):
self._search_provider.on_search_up()
def on_select_all(self, view):
self.update_selection(view, True)
def on_unselect_all(self, view):
self.update_selection(view, False)
def update_selection(self, view, select):
model = view.get_model()
view.get_model().foreach(lambda mod, path, itr: self.update_state(model, path, select))
self.update_receive_button_state(self._filter_model)
def update_state(self, model, path, select):
""" Updates checkbox state by given path in the list """
itr = self._filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(model.get_iter(path)))
self._filter_model.get_model().set_value(itr, 4, select)
def on_quit(self, window, event):
self._settings.add(self._size_name, window.get_size())
self.is_download = False
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)
s_model = self._source_box.get_model()
s_model.remove(s_model.get_iter_first())
self._source_box.set_active(0)
# Transponder view popup menu
tr_popup_menu = Gtk.Menu()
select_all_item = Gtk.ImageMenuItem.new_from_stock("gtk-select-all")
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 get_services_lines, get_services_list
# Used for double checking!
srvs = get_services_list("".join(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):
""" Common function for append satellite to the model """
name, flags, pos, transponders = sat
parent = model.append(None, [name, *(None,) * 9, flags, pos])
for transponder in transponders:
model.append(parent, ["Transponder:", *transponder, None, None])
if __name__ == "__main__":
pass

57
app/ui/search.py Normal file
View File

@@ -0,0 +1,57 @@
""" This is helper module for search features """
class SearchProvider:
def __init__(self, views, down_button, up_button):
self._paths = []
self._current_index = -1
self._max_indexes = 0
self._views = views
self._up_button = up_button
self._down_button = down_button
def search(self, text):
self._current_index = -1
self._paths.clear()
for view in self._views:
model = view.get_model()
selection = view.get_selection()
selection.unselect_all()
if not text:
continue
text = text.upper()
for r in model:
if text in str(r[:]).upper():
path = r.path
selection.select_path(r.path)
self._paths.append((view, path))
self._max_indexes = len(self._paths) - 1
if self._max_indexes > 0:
self.on_search_down()
self.update_navigation_buttons()
def scroll_to(self, index):
view, path = self._paths[index]
view.scroll_to_cell(path, None)
self.update_navigation_buttons()
def on_search_down(self):
if self._current_index < self._max_indexes:
self._current_index += 1
self.scroll_to(self._current_index)
def on_search_up(self):
if self._current_index > -1:
self._current_index -= 1
self.scroll_to(self._current_index)
def update_navigation_buttons(self):
self._up_button.set_sensitive(self._current_index > 0)
self._down_button.set_sensitive(self._current_index < self._max_indexes)
if __name__ == "__main__":
pass

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,845 @@
import os
import re
from app.commons import run_idle
from app.eparser import Service
from app.eparser.ecommons import (MODULATION, Inversion, ROLL_OFF, Pilot, Flag, Pids, POLARIZATION, get_key_by_value,
get_value_by_name, FEC_DEFAULT, PLS_MODE, SERVICE_TYPE, T_MODULATION, C_MODULATION,
TrType, SystemCable, T_SYSTEM, BANDWIDTH, TRANSMISSION_MODE, GUARD_INTERVAL, T_FEC,
HIERARCHY)
from app.settings import SettingsType
from .dialogs import show_dialog, DialogType, Action, get_dialogs_string
from .main_helper import get_base_model
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, TEXT_DOMAIN, CODED_ICON, Column, IS_GNOME_SESSION
_UI_PATH = UI_RESOURCES_PATH + "service_details_dialog.glade"
class ServiceDetailsDialog:
_ENIGMA2_DATA_ID = "{:04x}:{:08x}:{:04x}:{:04x}:{}:{}"
_ENIGMA2_FAV_ID = "{:X}:{:X}:{:X}:{:X}"
_ENIGMA2_TRANSPONDER_DATA = "{} {}:{}:{}:{}:{}:{}:{}"
_NEUTRINO_FAV_ID = "{:x}:{:x}:{:x}"
_NEUTRINO_TRANSPONDER_DATA = "{:04x}:{:04x}:{}:{}:{}:{}:{}:{}:{}"
_DIGIT_ENTRY_ELEMENTS = ("bitstream_entry", "pcm_entry", "video_pid_entry", "pcr_pid_entry", "srv_type_entry",
"ac3_pid_entry", "ac3plus_pid_entry", "acc_pid_entry", "he_acc_pid_entry",
"teletext_pid_entry", "pls_code_entry", "stream_id_entry", "tr_flag_entry",
"audio_pid_entry")
_NOT_EMPTY_DIGIT_ELEMENTS = ("sid_entry", "freq_entry", "rate_entry", "transponder_id_entry", "network_id_entry",
"namespace_entry", "srv_type_entry")
_DIGIT_ENTRY_NAME = "digit-entry"
def __init__(self, transient, settings, srv_view, fav_view, services, bouquets, new_color, action=Action.EDIT):
handlers = {"on_system_changed": self.on_system_changed,
"on_save": self.on_save,
"on_create_new": self.on_create_new,
"on_tr_edit_toggled": self.on_tr_edit_toggled,
"update_reference": self.update_reference,
"on_cas_entry_changed": self.on_cas_entry_changed,
"on_digit_entry_changed": self.on_digit_entry_changed,
"on_non_empty_entry_changed": self.on_non_empty_entry_changed}
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)
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._satellites_xml_path = settings.data_local_path + "satellites.xml"
self._picons_dir_path = settings.picons_local_path
self._services_view = srv_view
self._fav_view = fav_view
self._action = action
self._old_service = None
self._services = services
self._bouquets = bouquets
self._new_color = new_color
self._transponder_services_iters = None
self._current_model = None
self._current_itr = None
# Patterns
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})*")
# Buttons
self._apply_button = builder.get_object("apply_button")
self._create_button = builder.get_object("create_button")
# style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
# initialization only digit elements
self._digit_elements = {k: builder.get_object(k) for k in self._DIGIT_ENTRY_ELEMENTS}
for elem in self._digit_elements.values():
elem.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
# initialization of non empty elements
self._non_empty_elements = {k: builder.get_object(k) for k in self._NOT_EMPTY_DIGIT_ELEMENTS}
for elem in self._non_empty_elements.values():
elem.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
self._sid_entry = self._non_empty_elements.get("sid_entry")
self._bitstream_entry = self._digit_elements.get("bitstream_entry")
self._pcm_entry = self._digit_elements.get("pcm_entry")
self._video_pid_entry = self._digit_elements.get("video_pid_entry")
self._pcr_pid_entry = self._digit_elements.get("pcr_pid_entry")
self._audio_pid_entry = self._digit_elements.get("audio_pid_entry")
self._ac3_pid_entry = self._digit_elements.get("ac3_pid_entry")
self._ac3plus_pid_entry = self._digit_elements.get("ac3plus_pid_entry")
self._acc_pid_entry = self._digit_elements.get("acc_pid_entry")
self._he_acc_pid_entry = self._digit_elements.get("he_acc_pid_entry")
self._teletext_pid_entry = self._digit_elements.get("teletext_pid_entry")
self._transponder_id_entry = self._non_empty_elements.get("transponder_id_entry")
self._network_id_entry = self._non_empty_elements.get("network_id_entry")
self._freq_entry = self._non_empty_elements.get("freq_entry")
self._rate_entry = self._non_empty_elements.get("rate_entry")
self._pls_code_entry = self._digit_elements.get("pls_code_entry")
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")
# Service elements
self._name_entry = builder.get_object("name_entry")
self._package_entry = builder.get_object("package_entry")
self._srv_type_entry = self._non_empty_elements.get("srv_type_entry")
self._service_type_combo_box = builder.get_object("service_type_combo_box")
self._cas_entry = builder.get_object("cas_entry")
self._reference_entry = builder.get_object("reference_entry")
self._keep_check_button = builder.get_object("keep_check_button")
self._hide_check_button = builder.get_object("hide_check_button")
self._use_pids_check_button = builder.get_object("use_pids_check_button")
self._new_check_button = builder.get_object("new_check_button")
self._pids_grid = builder.get_object("pids_grid")
# Transponder elements
self._sat_pos_button = builder.get_object("sat_pos_button")
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")
self._sys_combo_box = builder.get_object("sys_combo_box")
self._mod_combo_box = builder.get_object("mod_combo_box")
self._invertion_combo_box = builder.get_object("invertion_combo_box")
self._rolloff_combo_box = builder.get_object("rolloff_combo_box")
self._pilot_combo_box = builder.get_object("pilot_combo_box")
self._pls_mode_combo_box = builder.get_object("pls_mode_combo_box")
self._tr_edit_switch = builder.get_object("tr_edit_switch")
self._tr_extra_expander = builder.get_object("tr_extra_expander")
self._DVB_S2_ELEMENTS = (self._mod_combo_box, self._rolloff_combo_box, self._pilot_combo_box,
self._pls_mode_combo_box, self._pls_code_entry, self._stream_id_entry)
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)
if self._action is Action.EDIT:
self.update_data_elements()
elif self._action is Action.ADD:
self.init_default_data_elements()
def show(self):
response = self._dialog.run()
if response == Gtk.ResponseType.OK:
pass
self._dialog.destroy()
return response
@run_idle
def init_default_data_elements(self):
self._apply_button.set_visible(False)
self._create_button.set_visible(True)
self._tr_edit_switch.set_sensitive(False)
self.on_tr_edit_toggled(self._tr_edit_switch.set_active(True), True)
for elem in self._non_empty_elements.values():
elem.set_text(" ")
elem.set_text("")
self._new_check_button.set_active(True)
self._tr_extra_expander.activate()
self._service_type_combo_box.set_active(0)
self._pol_combo_box.set_active(0)
self._fec_combo_box.set_active(0)
self._sys_combo_box.set_active(0)
self._invertion_combo_box.set_active(2)
def update_data_elements(self):
model, paths = self._services_view.get_selection().get_selected_rows()
# Unpacking to search for an iterator for the base model
filter_model = model.get_model()
self._current_model = get_base_model(model)
itr = None
if not paths:
# If editing from bouquet list and services list in the filter mode
fav_model, paths = self._fav_view.get_selection().get_selected_rows()
fav_id = fav_model[paths][7]
for row in self._current_model:
if row[-2] == fav_id:
itr = row.iter
break
else:
itr = model.get_iter(paths)
itr = filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr))
if not itr:
return
srv = Service(*self._current_model[itr][: Column.SRV_TOOLTIP])
self._old_service = srv
self._current_itr = itr
# Service
self._name_entry.set_text(srv.service)
self._package_entry.set_text(srv.package)
self._sid_entry.set_text(str(int(srv.ssid, 16)))
# Transponder
if self._s_type is SettingsType.ENIGMA_2:
self._tr_type = TrType(srv.transponder_type)
self._freq_entry.set_text(srv.freq)
self._rate_entry.set_text(srv.rate)
self.select_active_text(self._pol_combo_box, srv.pol)
self.select_active_text(self._fec_combo_box, srv.fec)
self.select_active_text(self._sys_combo_box, srv.system)
if self._tr_type is TrType.Terrestrial:
self.update_ui_for_terrestrial()
elif self._tr_type is TrType.Cable:
self.update_ui_for_cable()
else:
self.set_sat_positions(srv.pos)
if self._s_type is SettingsType.ENIGMA_2:
self.init_enigma2_service_data(srv)
self.init_enigma2_transponder_data(srv)
elif self._s_type is SettingsType.NEUTRINO_MP:
self.init_neutrino_data(srv)
self.init_neutrino_ui_elements()
# ***************** Init Enigma2 data *********************#
@run_idle
def init_enigma2_service_data(self, srv):
""" Service data initialisation """
flags = srv.flags_cas
if flags:
flags = flags.split(",")
self.init_enigma2_flags(flags)
self.init_enigma2_pids(flags)
self.init_enigma2_cas(flags)
def init_enigma2_flags(self, flags):
f_flags = list(filter(lambda x: x.startswith("f:"), flags))
if f_flags:
value = int(f_flags[0][2:])
self._keep_check_button.set_active(Flag.is_keep(value))
self._hide_check_button.set_active(Flag.is_hide(value))
self._use_pids_check_button.set_active(Flag.is_pids(value))
self._new_check_button.set_active(Flag.is_new(value))
def init_enigma2_cas(self, flags):
cas = list(filter(lambda x: x.startswith("C:"), flags))
if cas:
self._cas_entry.set_text(",".join(cas))
def init_enigma2_pids(self, flags):
pids = list(filter(lambda x: x.startswith("c:"), flags))
if pids:
for pid in pids:
if pid.startswith(Pids.VIDEO.value):
self._video_pid_entry.set_text(str(int(pid[4:], 16)))
elif pid.startswith(Pids.AUDIO.value):
self._audio_pid_entry.set_text(str(int(pid[4:], 16)))
elif pid.startswith(Pids.TELETEXT.value):
self._teletext_pid_entry.set_text(str(int(pid[4:], 16)))
elif pid.startswith(Pids.PCR.value):
self._pcr_pid_entry.set_text(str(int(pid[4:], 16)))
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
elif pid.startswith(Pids.AUDIO_CHANNEL.value):
pass
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
def init_enigma2_transponder_data(self, srv):
""" Transponder data initialisation """
data = srv.data_id.split(":")
tr_data = srv.transponder.split(":")
tr_type = TrType(srv.transponder_type)
self._namespace_entry.set_text(str(int(data[1], 16)))
self._transponder_id_entry.set_text(str(int(data[2], 16)))
self._network_id_entry.set_text(str(int(data[3], 16)))
if tr_type is TrType.Satellite:
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[5]).name)
if srv.system == "DVB-S2":
self.select_active_text(self._mod_combo_box, MODULATION.get(tr_data[8]))
self.select_active_text(self._rolloff_combo_box, ROLL_OFF.get(tr_data[9]))
self.select_active_text(self._pilot_combo_box, Pilot(tr_data[10]).name)
self._tr_flag_entry.set_text(tr_data[7])
if len(tr_data) > 12:
self._stream_id_entry.set_text(tr_data[11])
self._pls_code_entry.set_text(tr_data[12])
self.select_active_text(self._pls_mode_combo_box, PLS_MODE.get(tr_data[13]))
elif tr_type is TrType.Cable:
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[2]).name)
self.select_active_text(self._mod_combo_box, C_MODULATION.get(tr_data[3]))
self.select_active_text(self._fec_combo_box, FEC_DEFAULT.get(tr_data[4]))
self.select_active_text(self._sys_combo_box, SystemCable(tr_data[5]).name)
elif tr_type is TrType.Terrestrial:
self.select_active_text(self._fec_combo_box, T_FEC.get(tr_data[2]))
self.select_active_text(self._rate_lp_combo_box, T_FEC.get(tr_data[3]))
# Pol -> Bandwidth
self.select_active_text(self._pol_combo_box, BANDWIDTH.get(tr_data[1]))
self.select_active_text(self._mod_combo_box, T_MODULATION.get(tr_data[4]))
# Transmission Mode -> Roll off
self.select_active_text(self._rolloff_combo_box, TRANSMISSION_MODE.get(tr_data[5]))
# GuardInterval -> Pilot
self.select_active_text(self._pilot_combo_box, GUARD_INTERVAL.get(tr_data[6]))
# Hierarchy -> Pls Mode
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]))
# Should be called last to properly initialize the reference
self._srv_type_entry.set_text(data[4])
# ***************** Init Neutrino data *********************#
def init_neutrino_data(self, srv):
tr_data = srv.transponder.split(":")
self._transponder_id_entry.set_text(str(int(tr_data[0], 16)))
self._network_id_entry.set_text(str(int(tr_data[1], 16)))
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[3]).name)
self.select_active_text(self._service_type_combo_box, srv.service_type)
self.update_reference_entry()
def init_neutrino_ui_elements(self):
self._builder.get_object("flags_box").set_visible(False)
self._builder.get_object("pids_grid").set_visible(False)
tr_grid = self._builder.get_object("tr_grid")
tr_grid.remove_column(7)
tr_grid.set_margin_bottom(5)
self._builder.get_object("tr_extra_expander").set_visible(False)
self._builder.get_object("srv_separator").set_visible(False)
# ***************** Init Sat positions *********************#
def set_sat_positions(self, sat_pos):
""" Sat positions initialisation """
self._sat_pos_button.set_value(float(sat_pos))
def on_system_changed(self, box):
if not self._tr_edit_switch.get_active():
return
active = box.get_active()
self.update_dvb_s2_elements(active)
def update_dvb_s2_elements(self, active):
for elem in self._DVB_S2_ELEMENTS:
elem.set_sensitive(active)
self._pls_code_entry.set_name("GtkEntry")
self._stream_id_entry.set_name("GtkEntry")
if active:
if not self._mod_combo_box.get_active_id():
self._mod_combo_box.set_active_id(MODULATION["2"])
if not self._rolloff_combo_box.get_active_id():
self._rolloff_combo_box.set_active_id(ROLL_OFF["0"])
if not self._pilot_combo_box.get_active_id():
self._pilot_combo_box.set_active_id(Pilot.Auto.name)
if not self._pls_mode_combo_box.get_active_id():
self._pls_mode_combo_box.set_active_id(PLS_MODE["0"])
# ***************** Save data *********************#
def on_save(self, item):
self.save_data()
def on_create_new(self, item):
self.save_data()
def save_data(self):
if not self.is_data_correct():
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
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()
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!")
def on_edit(self):
""" Edit current service. """
fav_id, data_id = self.get_srv_data()
# transponder
transponder = self._old_service.transponder
if self._tr_edit_switch.get_active():
try:
if self._tr_type is TrType.Satellite:
transponder = self.get_satellite_transponder_data()
elif self._tr_type is TrType.Terrestrial:
transponder = self.get_terrestrial_transponder_data()
elif self._tr_type is TrType.Cable:
transponder = self.get_cable_transponder_data()
except Exception as e:
print(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
service = self.get_service(fav_id, data_id, transponder)
old_fav_id = self._old_service.fav_id
if old_fav_id != fav_id:
self.update_bouquets(fav_id, old_fav_id)
self._services[fav_id] = service
if self._old_service.picon_id != service.picon_id:
self.update_picon_name(self._old_service.picon_id, service.picon_id)
flags = service.flags_cas
extra_data = {Column.SRV_TOOLTIP: None, Column.SRV_BACKGROUND: None}
if 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
self._current_model.set(self._current_itr, extra_data)
self._current_model.set(self._current_itr, {i: v for i, v in enumerate(service)})
self.update_fav_view(self._old_service, service)
self._old_service = service
def update_bouquets(self, fav_id, old_fav_id):
self._services.pop(old_fav_id, None)
for bq in self._bouquets.values():
indexes = []
for i, f_id in enumerate(bq):
if old_fav_id == f_id:
indexes.append(i)
for i in indexes:
bq[i] = fav_id
@run_idle
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):
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):
return
for file_name in os.listdir(self._picons_dir_path):
if file_name == old_name:
old_file = os.path.join(self._picons_dir_path, old_name)
new_file = os.path.join(self._picons_dir_path, new_name)
os.rename(old_file, new_file)
break
# ***************** Service ********************* #
def get_service(self, fav_id, data_id, transponder):
freq, rate, pol, fec, system, pos = self.get_transponder_values()
return Service(flags_cas=self.get_flags(),
transponder_type=self._old_service.transponder_type,
coded=CODED_ICON if self._cas_entry.get_text() else None,
service=self._name_entry.get_text(),
locked=self._old_service.locked,
hide=HIDE_ICON if self._hide_check_button.get_active() else None,
package=self._package_entry.get_text(),
service_type=SERVICE_TYPE.get(self._srv_type_entry.get_text(), SERVICE_TYPE["3"]),
picon=self._old_service.picon,
picon_id=self._reference_entry.get_text().replace(":", "_") + ".png",
ssid="{:04x}".format(int(self._sid_entry.get_text())),
freq=freq,
rate=rate,
pol=pol,
fec=fec,
system=system,
pos=pos,
data_id=data_id,
fav_id=fav_id,
transponder=transponder)
def get_flags(self):
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
def get_enigma2_flags(self):
flags = ["p:{}".format(self._package_entry.get_text())]
# cas
cas = self._cas_entry.get_text()
if cas:
flags.append(cas)
# pids
video_pid = self._video_pid_entry.get_text()
if video_pid:
flags.append("{}{:04x}".format(Pids.VIDEO.value, int(video_pid)))
audio_pid = self._audio_pid_entry.get_text()
if audio_pid:
flags.append("{}{:04x}".format(Pids.AUDIO.value, int(audio_pid)))
teletext_pid = self._teletext_pid_entry.get_text()
if teletext_pid:
flags.append("{}{:04x}".format(Pids.TELETEXT.value, int(teletext_pid)))
pcr_pid = self._pcr_pid_entry.get_text()
if pcr_pid:
flags.append("{}{:04x}".format(Pids.PCR.value, int(pcr_pid)))
ac3_pid = self._ac3_pid_entry.get_text()
if ac3_pid:
flags.append("{}{:04x}".format(Pids.AC3.value, int(ac3_pid)))
bitstream_pid = self._bitstream_entry.get_text()
if bitstream_pid:
flags.append("{}{:04x}".format(Pids.BIT_STREAM_DELAY.value, int(bitstream_pid)))
pcm_pid = self._pcm_entry.get_text()
if pcm_pid:
flags.append("{}{:04x}".format(Pids.PCM_DELAY.value, int(pcm_pid)))
# 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
f_flags = f_flags + Flag.PIDS.value if self._use_pids_check_button.get_active() else f_flags
f_flags = f_flags + Flag.NEW.value if self._new_check_button.get_active() else f_flags
if f_flags:
flags.append("f:{:02d}".format(f_flags))
return ",".join(flags)
def get_srv_data(self):
ssid = int(self._sid_entry.get_text())
net_id, tr_id = int(self._network_id_entry.get_text()), int(self._transponder_id_entry.get_text())
service_type = self._srv_type_entry.get_text()
if self._s_type is SettingsType.ENIGMA_2:
namespace = int(self._namespace_entry.get_text())
data_id = self._ENIGMA2_DATA_ID.format(ssid, namespace, tr_id, net_id, service_type, 0)
fav_id = self._ENIGMA2_FAV_ID.format(ssid, tr_id, net_id, namespace)
return fav_id, data_id
elif self._s_type is SettingsType.NEUTRINO_MP:
fav_id = self._NEUTRINO_FAV_ID.format(tr_id, net_id, ssid)
return fav_id, self._old_service.data_id
# ***************** Transponder ********************* #
def get_transponder_values(self):
freq = self._freq_entry.get_text()
fec = self._fec_combo_box.get_active_id()
system = self._sys_combo_box.get_active_id()
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))
return freq, rate, pol, fec, system, pos
elif self._tr_type is TrType.Terrestrial:
o_srv = self._old_service
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()
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(".", "")
inv = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
srv_sys = "0" # !!!
if self._s_type is SettingsType.ENIGMA_2:
dvb_s_tr = self._ENIGMA2_TRANSPONDER_DATA.format("s", freq, rate, pol, fec, sat_pos, inv, srv_sys)
if sys == "DVB-S":
return dvb_s_tr
if sys == "DVB-S2":
flag = self._tr_flag_entry.get_text()
mod = self.get_value_from_combobox_id(self._mod_combo_box, MODULATION)
roll_off = self.get_value_from_combobox_id(self._rolloff_combo_box, ROLL_OFF)
pilot = get_value_by_name(Pilot, self._pilot_combo_box.get_active_id())
pls_mode = self.get_value_from_combobox_id(self._pls_mode_combo_box, PLS_MODE)
pls_code = self._pls_code_entry.get_text()
st_id = self._stream_id_entry.get_text()
pls = ":{}:{}:{}".format(st_id, pls_code, pls_mode) if pls_mode and pls_code and st_id else ""
return "{}:{}:{}:{}:{}{}".format(dvb_s_tr, flag, mod, roll_off, pilot, pls)
elif self._s_type is SettingsType.NEUTRINO_MP:
on_id, tr_id = int(self._network_id_entry.get_text()), int(self._transponder_id_entry.get_text())
mod = self.get_value_from_combobox_id(self._mod_combo_box, MODULATION) if sys == "DVB-S2" else None
srv_sys = None
return self._NEUTRINO_TRANSPONDER_DATA.format(tr_id, on_id, freq, inv, rate, fec, pol, mod, srv_sys)
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[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)
tr_data[5] = self.get_value_from_combobox_id(self._mod_combo_box, T_MODULATION)
tr_data[6] = self.get_value_from_combobox_id(self._rolloff_combo_box, TRANSMISSION_MODE)
tr_data[7] = self.get_value_from_combobox_id(self._pilot_combo_box, GUARD_INTERVAL)
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[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):
for itr in self._transponder_services_iters:
srv = self._current_model[itr][:Column.SRV_TOOLTIP]
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)})
# ***************** Others *********************#
def select_active_text(self, box, text):
model = box.get_model()
for index, row in enumerate(model):
if row[0] == text:
box.set_active(index)
break
def on_digit_entry_changed(self, entry):
entry.set_name(self._DIGIT_ENTRY_NAME if self._DIGIT_PATTERN.search(entry.get_text()) else "GtkEntry")
def on_non_empty_entry_changed(self, entry):
entry.set_name(self._DIGIT_ENTRY_NAME if self._NON_EMPTY_PATTERN.search(entry.get_text()) else "GtkEntry")
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 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)
@run_idle
def on_tr_edit_toggled(self, switch, active):
if active and self._action is Action.EDIT:
self._transponder_services_iters = []
response = TransponderServicesDialog(self._dialog,
self._current_model,
self._old_service.transponder,
self._transponder_services_iters).show()
if response == Gtk.ResponseType.CANCEL or response == -4:
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"))
for elem in self._TRANSPONDER_ELEMENTS:
elem.set_sensitive(active)
def is_data_correct(self):
for elem in self._digit_elements.values():
if elem.get_name() == self._DIGIT_ENTRY_NAME:
return False
for elem in self._non_empty_elements.values():
if elem.get_name() == self._DIGIT_ENTRY_NAME:
return False
if self._cas_entry.get_name() == self._DIGIT_ENTRY_NAME:
return False
return True
def update_reference(self, entry, event=None):
if not self.is_data_correct() or (event is None and self._s_type is SettingsType.NEUTRINO_MP):
return
self.update_reference_entry()
def update_reference_entry(self):
srv_type = int(self._srv_type_entry.get_text())
ssid = int(self._sid_entry.get_text())
tid = int(self._transponder_id_entry.get_text())
nid = int(self._network_id_entry.get_text())
if self._s_type is SettingsType.ENIGMA_2:
on_id = int(self._namespace_entry.get_text())
ref = "1:0:{:X}:{:X}:{:X}:{:X}:{:X}:0:0:0".format(srv_type, ssid, tid, nid, on_id)
self._reference_entry.set_text(ref)
else:
self._reference_entry.set_text("{:x}{:04x}{:04x}".format(tid, nid, ssid))
def update_ui_for_terrestrial(self):
tr_grid = self.get_transponder_grid_for_non_satellite()
tr_grid.remove_column(1)
tr_grid.insert_column(1)
extra_tr_grid = self._builder.get_object("extra_transponder_grid")
for i in range(4):
extra_tr_grid.remove_column(6)
# Bandwidth -> Pol
pol_label = self._builder.get_object("pol_label")
pol_label.set_text("Bandwidth")
tr_grid.attach(pol_label, 1, 0, 1, 1)
tr_grid.attach(self._pol_combo_box, 1, 1, 1, 1)
# Rate HP -> FEC
self._builder.get_object("fec_label").set_text("Rate HP")
# Rate LP
tr_grid.insert_column(3)
rate_lp_label = self._builder.get_object("pls_code_label")
rate_lp_label.set_text("Rate LP")
tr_grid.attach(rate_lp_label, 3, 0, 1, 1)
tr_grid.attach(self._rate_lp_combo_box, 3, 1, 1, 1)
# Modulation
tr_grid.insert_column(4)
extra_tr_grid.remove_column(1)
tr_grid.attach(self._builder.get_object("mod_label"), 4, 0, 1, 1)
tr_grid.attach(self._mod_combo_box, 4, 1, 1, 1)
# TransmissionMode -> Roll off
rolloff_label = self._builder.get_object("rolloff_label")
rolloff_label.set_text("T mode")
# GuardInterval -> Pilot
pilot_label = self._builder.get_object("pilot_label")
pilot_label.set_text("Guard Interval")
# Hierarchy -> Pls Mode
pls_mode_label = self._builder.get_object("pls_mode_label")
pls_mode_label.set_text("Hierarchy")
# Models
fec_model, modulation_model, sys_model = self.get_models_for_non_satellite()
pol_model = self._pol_combo_box.get_model()
roll_off_model = self._rolloff_combo_box.get_model()
pilot_model = self._pilot_combo_box.get_model()
pls_model = self._pls_mode_combo_box.get_model()
# Models clearing
for m in pol_model, roll_off_model, pilot_model, pls_model:
m.clear()
self.init_terrestrial_models((pol_model, modulation_model, roll_off_model, pilot_model, pls_model, sys_model),
(BANDWIDTH, T_MODULATION, TRANSMISSION_MODE, GUARD_INTERVAL, HIERARCHY, T_SYSTEM))
# Removing the latest FEC elements from the model
for itr in [fec_model.get_iter(Gtk.TreePath.new_from_string(str(i))) for i in range(7, 11)]:
fec_model.remove(itr)
# Extra
self._namespace_entry.set_max_width_chars(15)
self._sys_combo_box.set_hexpand(False)
def init_terrestrial_models(self, models, properties):
for index, model in enumerate(models):
for v in properties[index].values():
model.append((v,))
def update_ui_for_cable(self):
tr_grid = self.get_transponder_grid_for_non_satellite()
tr_box = self._builder.get_object("tr_box")
# Models
fec_model, modulation_model, system_model = self.get_models_for_non_satellite()
extra_tr_grid = self._builder.get_object("extra_transponder_grid")
for child in extra_tr_grid.get_children():
extra_tr_grid.remove(child)
tr_grid.remove(extra_tr_grid)
tr_grid.insert_column(3)
tr_grid.insert_column(4)
tr_grid.insert_column(5)
# Modulation
tr_grid.attach(self._builder.get_object("mod_label"), 3, 0, 1, 1)
tr_grid.attach(self._mod_combo_box, 3, 1, 1, 1)
for v in C_MODULATION.values():
modulation_model.append((v,))
# Inversion
tr_grid.attach(self._builder.get_object("inversion_label"), 4, 0, 1, 1)
tr_grid.attach(self._invertion_combo_box, 4, 1, 1, 1)
# System
tr_grid.attach(self._builder.get_object("system_label"), 5, 0, 1, 1)
tr_grid.attach(self._sys_combo_box, 5, 1, 1, 1)
system_model.append((SystemCable.ANNEX_A.name,))
system_model.append((SystemCable.ANNEX_C.name,))
# FEC
fec_model.append(("None",))
# Extra
tr_box.remove(self._tr_extra_expander)
tr_grid.set_margin_bottom(5)
self._freq_entry.set_width_chars(10)
self._freq_entry.set_max_width_chars(10)
self._rate_entry.set_width_chars(10)
self._rate_entry.set_max_width_chars(10)
self._transponder_id_entry.set_max_width_chars(8)
self._network_id_entry.set_max_width_chars(8)
def get_transponder_grid_for_non_satellite(self):
self._pids_grid.set_visible(False)
tr_grid = self._builder.get_object("tr_grid")
tr_grid.remove_column(0)
tr_grid.remove_column(2)
return tr_grid
def get_models_for_non_satellite(self):
fec_model = self._fec_combo_box.get_model()
modulation_model = self._mod_combo_box.get_model()
modulation_model.clear()
system_model = self._sys_combo_box.get_model()
system_model.clear()
return fec_model, modulation_model, system_model
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"))
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)
builder.get_object("srv_list_dialog_info_bar").connect("response", lambda bar, resp: bar.hide())
def append_services(self, model, transponder, tr_iters):
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))
def show(self):
response = self._dialog.run()
self._dialog.destroy()
return response
if __name__ == "__main__":
pass

3526
app/ui/settings_dialog.glade Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,118 +1,802 @@
from app.properties import write_config, Profile, get_default_settings
from . import Gtk, UI_RESOURCES_PATH
from .main_helper import update_entry_data
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 .main_helper import update_entry_data, scroll_to, get_picon_pixbuf
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON
def show_settings_dialog(transient, options):
return SettingsDialog(transient, options).show()
class Property(Enum):
FTP = "ftp"
HTTP = "http"
TELNET = "telnet"
class SettingsDialog:
def __init__(self, transient, options):
handlers = {"on_data_dir_field_icon_press": self.on_data_dir_field_icon_press,
"on_picons_dir_field_icon_press": self.on_picons_dir_field_icon_press,
"on_profile_changed": self.on_profile_changed,
_DIGIT_ENTRY_NAME = "digit-entry"
_DIGIT_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
def __init__(self, transient, settings: Settings):
handlers = {"on_field_icon_press": self.on_field_icon_press,
"on_settings_type_changed": self.on_settings_type_changed,
"on_reset": self.on_reset,
"apply_settings": self.apply_settings}
"on_response": self.on_response,
"apply_settings": self.apply_settings,
"on_apply_profile_settings": self.on_apply_profile_settings,
"on_connection_test": self.on_connection_test,
"on_info_bar_close": self.on_info_bar_close,
"on_set_color_switch": self.on_set_color_switch,
"on_force_bq_name": self.on_force_bq_name,
"on_http_mode_switch": self.on_http_mode_switch,
"on_experimental_switch": self.on_experimental_switch,
"on_yt_dl_switch": self.on_yt_dl_switch,
"on_default_path_mode_switch": self.on_default_path_mode_switch,
"on_default_data_path_changed": self.on_default_data_path_changed,
"on_profile_add": self.on_profile_add,
"on_profile_edit": self.on_profile_edit,
"on_profile_remove": self.on_profile_remove,
"on_profile_deleted": self.on_profile_deleted,
"on_profile_inserted": self.on_profile_inserted,
"on_profile_edited": self.on_profile_edited,
"on_profile_selected": self.on_profile_selected,
"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,
"on_transcoding_preset_changed": self.on_transcoding_preset_changed,
"on_apply_presets": self.on_apply_presets,
"on_digit_entry_changed": self.on_digit_entry_changed,
"on_view_popup_menu": self.on_view_popup_menu,
"on_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_icon_theme_add": self.on_icon_theme_add,
"on_icon_theme_remove": self.on_icon_theme_remove}
# Settings
self._ext_settings = settings
self._settings = Settings(settings.settings)
self._profiles = self._settings.profiles
self._s_type = self._settings.setting_type
builder = Gtk.Builder()
builder.add_objects_from_file(UI_RESOURCES_PATH + "dialogs.glade",
("settings_dialog", "telnet_timeout_adjustment"))
builder.add_from_file(UI_RESOURCES_PATH + "settings_dialog.glade")
builder.connect_signals(handlers)
self._dialog = builder.get_object("settings_dialog")
self._dialog.set_transient_for(transient)
self._header_bar = builder.get_object("header_bar")
self._main_stack = builder.get_object("main_stack")
# Network
self._host_field = builder.get_object("host_field")
self._port_field = builder.get_object("port_field")
self._login_field = builder.get_object("login_field")
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")
# Paths
self._services_field = builder.get_object("services_field")
self._user_bouquet_field = builder.get_object("user_bouquet_field")
self._satellites_xml_field = builder.get_object("satellites_xml_field")
self._data_dir_field = builder.get_object("data_dir_field")
self._picons_field = builder.get_object("picons_field")
self._picons_dir_field = builder.get_object("picons_dir_field")
self._backup_dir_field = builder.get_object("backup_dir_field")
self._default_data_dir_field = builder.get_object("default_data_dir_field")
self._record_data_dir_field = builder.get_object("record_data_dir_field")
self._default_data_paths_switch = builder.get_object("default_data_paths_switch")
# Info bar
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label")
self._test_spinner = builder.get_object("test_spinner")
# Settings type
self._enigma_radio_button = builder.get_object("enigma_radio_button")
self._neutrino_radio_button = builder.get_object("neutrino_radio_button")
self._support_ver5_switch = builder.get_object("support_ver5_switch")
self._force_bq_name_switch = builder.get_object("force_bq_name_switch")
# Streaming
self._apply_presets_button = builder.get_object("apply_presets_button")
self._transcoding_switch = builder.get_object("transcoding_switch")
self._edit_preset_switch = builder.get_object("edit_preset_switch")
self._presets_combo_box = builder.get_object("presets_combo_box")
self._video_bitrate_field = builder.get_object("video_bitrate_field")
self._video_width_field = builder.get_object("video_width_field")
self._video_height_field = builder.get_object("video_height_field")
self._audio_bitrate_field = builder.get_object("audio_bitrate_field")
self._audio_channels_combo_box = builder.get_object("audio_channels_combo_box")
self._audio_sample_rate_combo_box = builder.get_object("audio_sample_rate_combo_box")
self._audio_codec_combo_box = builder.get_object("audio_codec_combo_box")
self._transcoding_switch.bind_property("active", builder.get_object("record_box"), "sensitive")
self._edit_preset_switch.bind_property("active", self._apply_presets_button, "sensitive")
self._edit_preset_switch.bind_property("active", builder.get_object("video_options_frame"), "sensitive")
self._edit_preset_switch.bind_property("active", builder.get_object("audio_options_frame"), "sensitive")
self._play_in_built_radio_button = builder.get_object("play_in_built_radio_button")
self._play_in_vlc_radio_button = builder.get_object("play_in_vlc_radio_button")
self._get_m3u_radio_button = builder.get_object("get_m3u_radio_button")
# Program
self._before_save_switch = builder.get_object("before_save_switch")
self._before_downloading_switch = builder.get_object("before_downloading_switch")
self._enable_experimental_box = builder.get_object("enable_experimental_box")
self._colors_grid = builder.get_object("colors_grid")
self._set_color_switch = builder.get_object("set_color_switch")
self._new_color_button = builder.get_object("new_color_button")
self._extra_color_button = builder.get_object("extra_color_button")
self._load_on_startup_switch = builder.get_object("load_on_startup_switch")
self._bouquet_hints_switch = builder.get_object("bouquet_hints_switch")
self._services_hints_switch = builder.get_object("services_hints_switch")
self._lang_combo_box = builder.get_object("lang_combo_box")
# Extra
self._support_http_api_switch = builder.get_object("support_http_api_switch")
self._enable_yt_dl_switch = builder.get_object("enable_yt_dl_switch")
self._enable_update_yt_dl_switch = builder.get_object("enable_update_yt_dl_switch")
self._enable_send_to_switch = builder.get_object("enable_send_to_switch")
self._click_mode_disabled_button = builder.get_object("click_mode_disabled_button")
self._click_mode_stream_button = builder.get_object("click_mode_stream_button")
self._click_mode_play_button = builder.get_object("click_mode_play_button")
self._click_mode_zap_button = builder.get_object("click_mode_zap_button")
self._click_mode_zap_and_play_button = builder.get_object("click_mode_zap_and_play_button")
self._click_mode_zap_button.bind_property("sensitive", self._click_mode_play_button, "sensitive")
self._click_mode_zap_button.bind_property("sensitive", self._click_mode_zap_and_play_button, "sensitive")
# EXPERIMENTAL
self._enable_exp_switch = builder.get_object("enable_experimental_switch")
self._enable_exp_switch.bind_property("active", builder.get_object("yt_dl_box"), "sensitive")
self._enable_yt_dl_switch.bind_property("active", builder.get_object("yt_dl_update_box"), "sensitive")
self._enable_exp_switch.bind_property("active", builder.get_object("v5_support_box"), "sensitive")
self._enable_exp_switch.bind_property("active", builder.get_object("enable_direct_playback_box"), "sensitive")
# Enigma2 only
self._enigma_radio_button.bind_property("active", builder.get_object("bq_naming_grid"), "sensitive")
self._enigma_radio_button.bind_property("active", builder.get_object("enable_http_box"), "sensitive")
self._enigma_radio_button.bind_property("active", builder.get_object("enable_experimental_box"), "sensitive")
self._enigma_radio_button.bind_property("active", builder.get_object("program_frame"), "sensitive")
self._enigma_radio_button.bind_property("active", builder.get_object("experimental_box"), "sensitive")
# Profiles
self._profile_view = builder.get_object("profile_tree_view")
self._profile_add_button = builder.get_object("profile_add_button")
self._profile_remove_button = builder.get_object("profile_remove_button")
self._apply_profile_button = builder.get_object("apply_profile_button")
self._apply_profile_button.bind_property("visible", builder.get_object("reset_button"), "visible")
# Style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._digit_elems = (self._port_field, self._http_port_field, self._telnet_port_field, self._video_width_field,
self._video_bitrate_field, self._video_height_field, self._audio_bitrate_field)
for el in self._digit_elems:
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
self.init_ui_elements(self._s_type)
self.init_profiles()
self._options = options
self._active_profile = options.get("profile")
self.set_settings()
self._neutrino_radio_button.set_active(Profile(self._active_profile) is Profile.NEUTRINO_MP)
if self._settings.is_darwin:
# Appearance
self._appearance_box = builder.get_object("appearance_box")
self._appearance_box.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()
@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)
http_active = self._support_http_api_switch.get_active()
self._click_mode_zap_button.set_sensitive(is_enigma_profile and http_active)
self._lang_combo_box.set_active_id(self._ext_settings.language)
self.on_info_bar_close() if is_enigma_profile else self.show_info_message(
"The Neutrino has only experimental support. Not all features are supported!", Gtk.MessageType.WARNING)
def init_profiles(self):
p_def = self._settings.default_profile
model = self._profile_view.get_model()
for ind, p in enumerate(self._profiles):
icon = DEFAULT_ICON if p == p_def else None
model.append((p, icon))
if icon:
scroll_to(ind, self._profile_view)
self.on_profile_selected(self._profile_view, False)
self._profile_remove_button.set_sensitive(len(self._profile_view.get_model()) > 1)
def update_title(self):
title = "{} [{}]"
if self._s_type is SettingsType.ENIGMA_2:
self._dialog.set_title(title.format(get_message("Options"), 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()))
def show(self):
response = self._dialog.run()
if response == Gtk.ResponseType.OK:
self.apply_settings()
write_config(self._options)
self._dialog.run()
def on_response(self, dialog, resp):
if resp == Gtk.ResponseType.OK and not self.apply_settings():
return
self._dialog.destroy()
return resp
return response
def on_field_icon_press(self, entry, icon, event_button):
update_entry_data(entry, self._dialog, self._settings)
def on_data_dir_field_icon_press(self, entry, icon, event_button):
update_entry_data(entry, self._dialog, self._options.get(self._options.get("profile")))
def on_settings_type_changed(self, item):
s_type = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP
if s_type is not self._s_type:
self._settings.setting_type = s_type
self._s_type = s_type
self.on_reset()
self.init_ui_elements(s_type)
def on_picons_dir_field_icon_press(self, entry, icon, event_button):
update_entry_data(entry, self._dialog, self._options.get(self._options.get("profile")))
def on_profile_changed(self, item):
self.set_profile(Profile.ENIGMA_2 if self._enigma_radio_button.get_active() else Profile.NEUTRINO_MP)
def set_profile(self, profile):
self._active_profile = profile.value
self.set_settings()
def on_reset(self, item):
def_settings = get_default_settings()
for key in def_settings:
current = self._options.get(key)
if type(current) is str:
continue
default = def_settings.get(key)
for k in default:
current[k] = default.get(k)
def on_reset(self, item=None):
self._settings.reset()
self.set_settings()
def set_settings(self):
options = self._options.get(self._active_profile)
self._host_field.set_text(options.get("host", ""))
self._port_field.set_text(options.get("port", ""))
self._login_field.set_text(options.get("user", ""))
self._password_field.set_text(options.get("password", ""))
self._telnet_login_field.set_text(options.get("telnet_user", ""))
self._telnet_password_field.set_text(options.get("telnet_password", ""))
self._telnet_port_field.set_text(options.get("telnet_port", ""))
self._telnet_timeout_spin_button.set_value(options.get("telnet_timeout", 5))
self._services_field.set_text(options.get("services_path", ""))
self._user_bouquet_field.set_text(options.get("user_bouquet_path", ""))
self._satellites_xml_field.set_text(options.get("satellites_xml_path", ""))
self._picons_field.set_text(options.get("picons_path", ""))
self._data_dir_field.set_text(options.get("data_dir_path", ""))
self._picons_dir_field.set_text(options.get("picons_dir_path", ""))
self._s_type = self._settings.setting_type
self._host_field.set_text(self._settings.host)
self._port_field.set_text(self._settings.port)
self._login_field.set_text(self._settings.user)
self._password_field.set_text(self._settings.password)
self._http_login_field.set_text(self._settings.http_user)
self._http_password_field.set_text(self._settings.http_password)
self._http_port_field.set_text(self._settings.http_port)
self._http_use_ssl_check_button.set_active(self._settings.http_use_ssl)
self._telnet_login_field.set_text(self._settings.telnet_user)
self._telnet_password_field.set_text(self._settings.telnet_password)
self._telnet_port_field.set_text(self._settings.telnet_port)
self._telnet_timeout_spin_button.set_value(self._settings.telnet_timeout)
self._services_field.set_text(self._settings.services_path)
self._user_bouquet_field.set_text(self._settings.user_bouquet_path)
self._satellites_xml_field.set_text(self._settings.satellites_xml_path)
self._picons_field.set_text(self._settings.picons_path)
self._data_dir_field.set_text(self._settings.data_local_path)
self._picons_dir_field.set_text(self._settings.picons_local_path)
self._backup_dir_field.set_text(self._settings.backup_local_path)
self._default_data_dir_field.set_text(self._settings.default_data_path)
self._record_data_dir_field.set_text(self._settings.records_path)
self._before_save_switch.set_active(self._settings.backup_before_save)
self._before_downloading_switch.set_active(self._settings.backup_before_downloading)
self.set_fav_click_mode(self._settings.fav_click_mode)
self.set_play_stream_mode(self._settings.play_streams_mode)
self._load_on_startup_switch.set_active(self._settings.load_last_config)
self._bouquet_hints_switch.set_active(self._settings.show_bq_hints)
self._services_hints_switch.set_active(self._settings.show_srv_hints)
self._default_data_paths_switch.set_active(self._settings.profile_folder_is_default)
self._transcoding_switch.set_active(self._settings.activate_transcoding)
self._presets_combo_box.set_active_id(self._settings.active_preset)
self.on_transcoding_preset_changed(self._presets_combo_box)
if self._s_type is SettingsType.ENIGMA_2:
self._enable_exp_switch.set_active(self._settings.is_enable_experimental)
self._support_ver5_switch.set_active(self._settings.v5_support)
self._force_bq_name_switch.set_active(self._settings.force_bq_names)
self._support_http_api_switch.set_active(self._settings.http_api_support)
self._enable_yt_dl_switch.set_active(self._settings.enable_yt_dl)
self._enable_update_yt_dl_switch.set_active(self._settings.enable_yt_dl_update)
self._enable_send_to_switch.set_active(self._settings.enable_send_to)
self._set_color_switch.set_active(self._settings.use_colors)
new_rgb = Gdk.RGBA()
new_rgb.parse(self._settings.new_color)
extra_rgb = Gdk.RGBA()
extra_rgb.parse(self._settings.extra_color)
self._new_color_button.set_rgba(new_rgb)
self._extra_color_button.set_rgba(extra_rgb)
if self._s_type is SettingsType.ENIGMA_2:
self._enigma_radio_button.activate()
else:
self._neutrino_radio_button.activate()
def on_apply_profile_settings(self, item=None):
if not self.is_data_correct(self._digit_elems):
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
self._s_type = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP
self._settings.setting_type = self._s_type
self._settings.host = self._host_field.get_text()
self._settings.port = self._port_field.get_text()
self._settings.user = self._login_field.get_text()
self._settings.password = self._password_field.get_text()
self._settings.http_user = self._http_login_field.get_text()
self._settings.http_password = self._http_password_field.get_text()
self._settings.http_port = self._http_port_field.get_text()
self._settings.http_use_ssl = self._http_use_ssl_check_button.get_active()
self._settings.telnet_user = self._telnet_login_field.get_text()
self._settings.telnet_password = self._telnet_password_field.get_text()
self._settings.telnet_port = self._telnet_port_field.get_text()
self._settings.telnet_timeout = int(self._telnet_timeout_spin_button.get_value())
self._settings.services_path = self._services_field.get_text()
self._settings.user_bouquet_path = self._user_bouquet_field.get_text()
self._settings.satellites_xml_path = self._satellites_xml_field.get_text()
self._settings.picons_path = self._picons_field.get_text()
self._settings.data_local_path = self._data_dir_field.get_text()
self._settings.picons_local_path = self._picons_dir_field.get_text()
self._settings.backup_local_path = self._backup_dir_field.get_text()
def apply_settings(self, item=None):
profile = Profile.ENIGMA_2.value if self._enigma_radio_button.get_active() else Profile.NEUTRINO_MP.value
self._active_profile = profile
self._options["profile"] = profile
options = self._options.get(self._active_profile)
options["host"] = self._host_field.get_text()
options["port"] = self._port_field.get_text()
options["user"] = self._login_field.get_text()
options["password"] = self._password_field.get_text()
options["telnet_user"] = self._telnet_login_field.get_text()
options["telnet_password"] = self._telnet_password_field.get_text()
options["telnet_port"] = self._telnet_port_field.get_text()
options["telnet_timeout"] = int(self._telnet_timeout_spin_button.get_value())
options["services_path"] = self._services_field.get_text()
options["user_bouquet_path"] = self._user_bouquet_field.get_text()
options["satellites_xml_path"] = self._satellites_xml_field.get_text()
options["picons_path"] = self._picons_field.get_text()
options["data_dir_path"] = self._data_dir_field.get_text()
options["picons_dir_path"] = self._picons_dir_field.get_text()
if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.OK:
return
self.on_apply_profile_settings()
self._ext_settings.profiles = self._settings.profiles
self._ext_settings.backup_before_save = self._before_save_switch.get_active()
self._ext_settings.backup_before_downloading = self._before_downloading_switch.get_active()
self._ext_settings.fav_click_mode = self.get_fav_click_mode()
self._ext_settings.play_streams_mode = self.get_play_stream_mode()
self._ext_settings.language = self._lang_combo_box.get_active_id()
self._ext_settings.load_last_config = self._load_on_startup_switch.get_active()
self._ext_settings.show_bq_hints = self._bouquet_hints_switch.get_active()
self._ext_settings.show_srv_hints = self._services_hints_switch.get_active()
self._ext_settings.profile_folder_is_default = self._default_data_paths_switch.get_active()
self._ext_settings.default_data_path = self._default_data_dir_field.get_text()
self._ext_settings.records_path = self._record_data_dir_field.get_text()
self._ext_settings.activate_transcoding = self._transcoding_switch.get_active()
self._ext_settings.active_preset = self._presets_combo_box.get_active_id()
if self._ext_settings.is_darwin:
self._ext_settings.dark_mode = self._dark_mode_switch.get_active()
self._ext_settings.is_themes_support = self._themes_support_switch.get_active()
self._ext_settings.theme = self._theme_combo_box.get_active_id()
self._ext_settings.icon_theme = self._icon_theme_combo_box.get_active_id()
if self._s_type is SettingsType.ENIGMA_2:
self._ext_settings.is_enable_experimental = self._enable_exp_switch.get_active()
self._ext_settings.use_colors = self._set_color_switch.get_active()
self._ext_settings.new_color = self._new_color_button.get_rgba().to_string()
self._ext_settings.extra_color = self._extra_color_button.get_rgba().to_string()
self._ext_settings.v5_support = self._support_ver5_switch.get_active()
self._ext_settings.force_bq_names = self._force_bq_name_switch.get_active()
self._ext_settings.http_api_support = self._support_http_api_switch.get_active()
self._ext_settings.enable_yt_dl = self._enable_yt_dl_switch.get_active()
self._ext_settings.enable_yt_dl_update = self._enable_update_yt_dl_switch.get_active()
self._ext_settings.enable_send_to = self._enable_send_to_switch.get_active()
self._ext_settings.default_profile = list(filter(lambda r: r[1], self._profile_view.get_model()))[0][0]
self._ext_settings.save()
return True
@run_task
def on_connection_test(self, item):
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:
self.test_ftp()
def test_http(self):
user, password = self._http_login_field.get_text(), self._http_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:
self.show_info_message(test_http(host, port, user, password, use_ssl=use_ssl), Gtk.MessageType.INFO)
except TestException as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
except HttpApiException as e:
self.show_info_message(str(e), Gtk.MessageType.WARNING)
finally:
self.show_spinner(False)
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()
try:
self.show_info_message(test_telnet(host, port, user, password, timeout), Gtk.MessageType.INFO)
self.show_spinner(False)
except TestException as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
self.show_spinner(False)
def test_ftp(self):
host, port = self._host_field.get_text(), self._port_field.get_text()
user, password = self._login_field.get_text(), self._password_field.get_text()
try:
self.show_info_message("OK. {}".format(test_ftp(host, port, user, password)), Gtk.MessageType.INFO)
except TestException as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
finally:
self.show_spinner(False)
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(get_message(text))
@run_idle
def show_spinner(self, show):
self._test_spinner.start() if show else self._test_spinner.stop()
self._test_spinner.set_state(Gtk.StateType.ACTIVE if show else Gtk.StateType.NORMAL)
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
def on_set_color_switch(self, switch, state):
self._colors_grid.set_sensitive(state)
def on_http_mode_switch(self, switch, state):
self._click_mode_zap_button.set_sensitive(state)
if any((self._click_mode_play_button.get_active(),
self._click_mode_zap_button.get_active(),
self._click_mode_zap_and_play_button.get_active())):
self._click_mode_disabled_button.set_active(True)
def on_experimental_switch(self, switch, state):
if not state:
self._support_ver5_switch.set_active(state)
self._enable_send_to_switch.set_active(state)
self._enable_yt_dl_switch.set_active(state)
def on_force_bq_name(self, switch, state):
if self._main_stack.get_visible_child_name() != "extra":
return
if state:
msg = "Some images may have problems displaying the favorites list!"
self.show_info_message(msg, Gtk.MessageType.WARNING)
else:
self.on_info_bar_close()
def on_yt_dl_switch(self, switch, state):
self.show_info_message("Not implemented yet!", Gtk.MessageType.WARNING)
def on_default_path_mode_switch(self, switch, state):
self._settings.profile_folder_is_default = state
def on_default_data_path_changed(self, entry):
self._settings.default_data_path = entry.get_text()
def on_profile_add(self, item):
model = self._profile_view.get_model()
count = 0
name = "profile"
while name in self._profiles:
count += 1
name = "profile{}".format(count)
self._profiles[name] = self._s_type.get_default_settings()
model.append((name, None))
scroll_to(len(model) - 1, self._profile_view)
self.on_profile_selected(self._profile_view, False)
self.on_reset()
def on_profile_edit(self, item=None):
model, paths = self._profile_view.get_selection().get_selected_rows()
self._profile_view.set_cursor(paths, self._profile_view.get_column(0), True)
def on_profile_remove(self, item):
model, paths = self._profile_view.get_selection().get_selected_rows()
if paths:
row = model[paths]
is_default = row[1]
self._profiles.pop(row[0], None)
del model[paths]
if is_default:
model.set_value(model.get_iter_first(), 1, DEFAULT_ICON)
def on_profile_deleted(self, model, paths):
self._profile_remove_button.set_sensitive(len(model) > 1)
def on_profile_edited(self, render, path, new_value):
row = self._profile_view.get_model()[path]
old_name = row[0]
if old_name == new_value:
return
if new_value in self._profiles:
show_dialog(DialogType.ERROR, self._dialog, "A profile with that name exists!")
return
p_settings = self._profiles.pop(old_name, None)
if p_settings:
row[0] = new_value
self._profiles[new_value] = p_settings
self.update_local_paths(new_value, old_name)
self.on_profile_selected(self._profile_view, False)
def update_local_paths(self, p_name, old_name, force_rename=False):
data_path = self._settings.data_local_path
picons_path = self._settings.picons_local_path
backup_path = self._settings.backup_local_path
self._settings.data_local_path = p_name.join(data_path.rsplit(old_name, 1))
self._settings.picons_local_path = p_name.join(picons_path.rsplit(old_name, 1))
self._settings.backup_local_path = p_name.join(backup_path.rsplit(old_name, 1))
if force_rename:
try:
if os.path.isdir(picons_path):
os.rename(picons_path, self._settings.picons_local_path)
if os.path.isdir(data_path):
os.rename(data_path, self._settings.data_local_path)
if os.path.isdir(backup_path):
os.rename(backup_path, self._settings.backup_local_path)
except OSError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
def on_profile_selected(self, view, force=True):
if force:
self.on_apply_profile_settings()
model, paths = self._profile_view.get_selection().get_selected_rows()
if paths:
profile = model.get_value(model.get_iter(paths), 0)
self._settings.current_profile = profile
self.set_settings()
def on_profile_set_default(self, item):
model, paths = self._profile_view.get_selection().get_selected_rows()
if paths:
itr = model.get_iter(paths)
model.foreach(lambda m, p, i: model.set_value(i, 1, None))
model.set_value(itr, 1, DEFAULT_ICON)
self._settings.default_profile = model.get_value(itr, 0)
def on_profile_inserted(self, model, path, itr):
self._profile_remove_button.set_sensitive(len(model) > 1)
def on_lang_changed(self, box):
if box.get_active_id() != self._settings.language:
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
def on_main_settings_visible(self, stack, param):
name = stack.get_visible_child_name()
self._apply_profile_button.set_visible(name == "profiles")
self._apply_presets_button.set_visible(name == "streaming")
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
port = "443" if active else "80"
self._http_port_field.set_text(port)
self._settings.http_port = port
def on_click_mode_togged(self, button):
if self._main_stack.get_visible_child_name() != "extra":
return
mode = self.get_fav_click_mode()
if mode is FavClickMode.PLAY:
self.show_info_message("Operates in standby mode or current active transponder!", Gtk.MessageType.WARNING)
else:
self.on_info_bar_close()
@run_idle
def set_fav_click_mode(self, mode):
mode = FavClickMode(mode)
self._click_mode_disabled_button.set_active(mode is FavClickMode.DISABLED)
self._click_mode_stream_button.set_active(mode is FavClickMode.STREAM)
self._click_mode_play_button.set_active(mode is FavClickMode.PLAY)
self._click_mode_zap_button.set_active(mode is FavClickMode.ZAP)
self._click_mode_zap_and_play_button.set_active(mode is FavClickMode.ZAP_PLAY)
def get_fav_click_mode(self):
if self._click_mode_zap_button.get_active():
return FavClickMode.ZAP
if self._click_mode_play_button.get_active():
return FavClickMode.PLAY
if self._click_mode_zap_and_play_button.get_active():
return FavClickMode.ZAP_PLAY
if self._click_mode_stream_button.get_active():
return FavClickMode.STREAM
return FavClickMode.DISABLED
def on_play_mode_changed(self, button):
if self._main_stack.get_visible_child_name() != "streaming":
return
if button.get_active():
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
@run_idle
def set_play_stream_mode(self, mode):
self._play_in_built_radio_button.set_sensitive(not self._settings.is_darwin)
self._play_in_built_radio_button.set_active(mode is PlayStreamsMode.BUILT_IN)
self._play_in_vlc_radio_button.set_active(mode is PlayStreamsMode.VLC)
self._get_m3u_radio_button.set_active(mode is PlayStreamsMode.M3U)
def get_play_stream_mode(self):
if self._play_in_built_radio_button.get_active():
return PlayStreamsMode.BUILT_IN
if self._play_in_vlc_radio_button.get_active():
return PlayStreamsMode.VLC
if self._get_m3u_radio_button.get_active():
return PlayStreamsMode.M3U
return self._settings.play_streams_mode
def on_transcoding_preset_changed(self, button):
presets = self._settings.transcoding_presets
prs = presets.get(button.get_active_id())
self._video_bitrate_field.set_text(prs.get("vb", "0"))
self._video_width_field.set_text(prs.get("width", "0"))
self._video_height_field.set_text(prs.get("height", "0"))
self._audio_bitrate_field.set_text(prs.get("ab", "0"))
self._audio_channels_combo_box.set_active_id(prs.get("channels", "2"))
self._audio_sample_rate_combo_box.set_active_id(prs.get("samplerate", "44100"))
self._audio_codec_combo_box.set_active_id(prs.get("acodec", "mp3"))
def on_apply_presets(self, item):
if not self.is_data_correct(self._digit_elems):
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
presets = self._settings.transcoding_presets
prs = presets.get(self._presets_combo_box.get_active_id())
prs["vb"] = self._video_bitrate_field.get_text()
prs["width"] = self._video_width_field.get_text()
prs["height"] = self._video_height_field.get_text()
prs["ab"] = self._audio_bitrate_field.get_text()
prs["channels"] = self._audio_channels_combo_box.get_active_id()
prs["samplerate"] = self._audio_sample_rate_combo_box.get_active_id()
prs["acodec"] = self._audio_codec_combo_box.get_active_id()
self._ext_settings.transcoding_presets = presets
self._edit_preset_switch.set_active(False)
def on_digit_entry_changed(self, entry):
if self._DIGIT_PATTERN.search(entry.get_text()):
entry.set_name(self._DIGIT_ENTRY_NAME)
else:
entry.set_name("GtkEntry")
def is_data_correct(self, elems):
return not any(elem.get_name() == self._DIGIT_ENTRY_NAME for elem in elems)
def on_view_popup_menu(self, menu, event):
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
menu.popup(None, None, None, None, event.button, event.time)
def on_theme_changed(self, button):
if self._main_stack.get_visible_child_name() != "appearance":
return
self.set_theme_thumbnail_image(button.get_active_id())
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
@run_idle
def set_theme_thumbnail_image(self, theme_name):
img_path = "{}{}/gtk-3.0/thumbnail.png".format(self._ext_settings.themes_path, theme_name)
self._theme_thumbnail_image.set_from_pixbuf(get_picon_pixbuf(img_path, 96))
def on_theme_add(self, button):
self.add_theme(self._ext_settings.themes_path, self._theme_combo_box)
def on_theme_remove(self, button):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.OK:
Gtk.Settings().get_default().set_property("gtk-theme-name", "")
self.remove_theme(self._theme_combo_box, self._ext_settings.themes_path)
def on_icon_theme_changed(self, button, state=False):
if self._main_stack.get_visible_child_name() != "appearance":
return
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
def on_icon_theme_add(self, button):
self.add_theme(self._ext_settings.icon_themes_path, self._icon_theme_combo_box)
def on_icon_theme_remove(self, button):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.OK:
Gtk.Settings().get_default().set_property("gtk-icon-theme-name", "")
self.remove_theme(self._icon_theme_combo_box, self._ext_settings.icon_themes_path)
@run_idle
def add_theme(self, path, button):
response = get_chooser_dialog(self._dialog, self._settings, "Themes Archive [*.xz, *.zip]", ("*.xz", "*.zip"))
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
self._appearance_box.set_sensitive(False)
self.unpack_theme(response, path, button)
@run_task
def unpack_theme(self, src, dst, button):
try:
os.makedirs(os.path.dirname(dst), exist_ok=True)
import subprocess
log("Unpacking '{}' started...".format(src))
p = subprocess.Popen(["tar", "-xvf", src, "-C", dst],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
p.communicate()
log("Unpacking end.")
finally:
self.update_theme_button(button, dst)
self._appearance_box.set_sensitive(True)
@run_idle
def update_theme_button(self, button, dst):
exist = set(os.listdir(dst))
current = {r[0] for r in button.get_model()}
added = exist - current
if added:
theme = added.pop()
if theme not in current:
button.append(theme, theme)
button.set_active_id(theme)
self.show_info_message("Done!", Gtk.MessageType.INFO)
@run_idle
def remove_theme(self, button, path):
theme = button.get_active_id()
if not theme:
self.show_info_message("No selected item!", Gtk.MessageType.ERROR)
return
from shutil import rmtree
try:
rmtree(path + theme, ignore_errors=True)
except OSError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
self.theme_button_remove_active(button)
@run_idle
def theme_button_remove_active(self, button):
button.remove(button.get_active())
button.set_active(0)
@run_idle
def init_appearance(self):
self._dark_mode_switch.set_active(self._ext_settings.dark_mode)
t_support = self._ext_settings.is_themes_support
self._themes_support_switch.set_active(t_support)
if t_support:
# GTK
try:
for t in os.listdir(self._ext_settings.themes_path):
self._theme_combo_box.append(t, t)
self._theme_combo_box.set_active_id(self._ext_settings.theme)
self.set_theme_thumbnail_image(self._ext_settings.theme)
except FileNotFoundError:
pass
except PermissionError as e:
log("{}".format(e))
# Icons
try:
for t in os.listdir(self._ext_settings.icon_themes_path):
self._icon_theme_combo_box.append(t, t)
self._icon_theme_combo_box.set_active_id(self._ext_settings.icon_theme)
except FileNotFoundError:
pass
except PermissionError as e:
log("{}".format(e))
if __name__ == "__main__":

View File

@@ -1,3 +1,58 @@
#digit-entry {
border-color: Red;
border-color: Red;
border-width: 0.15em;
}
#status-bar-button {
margin: 0.1em;
}
.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;
}
#textview-large {
font-size: 14px;
}
.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;
}
.group :last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left-width: 0;
}
.group :not(:first-child):not(:last-child) {
border-radius: 0;
border-left-width: 0;
border-right-width: 1px;
}

250
app/ui/telnet.glade Executable file
View File

@@ -0,0 +1,250 @@
<?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>

256
app/ui/telnet.py Executable file
View File

@@ -0,0 +1,256 @@
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>

233
app/ui/transmitter.glade Normal file
View File

@@ -0,0 +1,233 @@
<?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-css-provider-path style.css -->
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkWindow" id="main_window">
<property name="can_focus">False</property>
<property name="resizable">False</property>
<property name="window_position">mouse</property>
<property name="destroy_with_parent">True</property>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="decorated">False</property>
<property name="gravity">center</property>
<property name="has_resize_grip">True</property>
<child>
<placeholder/>
</child>
<child>
<object class="GtkBox" id="tool_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">1</property>
<child>
<object class="GtkButton" id="previous_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Previous stream in the list</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin_left">1</property>
<property name="margin_top">1</property>
<property name="margin_bottom">1</property>
<signal name="clicked" handler="on_previous" swapped="no"/>
<child>
<object class="GtkImage" id="previous_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-media-previous</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="next_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Next stream in the list</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin_top">1</property>
<property name="margin_bottom">1</property>
<signal name="clicked" handler="on_next" swapped="no"/>
<child>
<object class="GtkImage" id="next_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-media-next</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="url_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="tooltip_text" translatable="yes">Drag or paste the link here</property>
<property name="margin_left">2</property>
<property name="margin_right">2</property>
<property name="margin_top">1</property>
<property name="margin_bottom">1</property>
<property name="primary_icon_stock">gtk-paste</property>
<signal name="activate" handler="on_url_activate" swapped="no"/>
<signal name="changed" handler="on_url_changed" swapped="no"/>
<signal name="drag-data-received" handler="on_drag_data_received" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="play_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Play</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin_top">1</property>
<property name="margin_bottom">1</property>
<signal name="clicked" handler="on_play" swapped="no"/>
<child>
<object class="GtkImage" id="play_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-media-play</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkButton" id="stop_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Stop playback</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin_top">1</property>
<property name="margin_bottom">1</property>
<signal name="clicked" handler="on_stop" swapped="no"/>
<child>
<object class="GtkImage" id="stop_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-media-stop</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkButton" id="clear_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Remove added links in the playlist</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin_right">1</property>
<property name="margin_top">1</property>
<property name="margin_bottom">1</property>
<signal name="clicked" handler="on_clear" swapped="no"/>
<child>
<object class="GtkImage" id="clear_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-clear</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">6</property>
</packing>
</child>
<style>
<class name="primary-toolbar"/>
</style>
</object>
</child>
</object>
<object class="GtkImage" id="show_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">view-restore</property>
</object>
<object class="GtkMenu" id="staus_popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="show_menu_item">
<property name="label" translatable="yes">Show/Hide</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">show_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_status_icon_activate" object="main_window" swapped="no"/>
</object>
</child>
</object>
<object class="GtkStatusIcon" id="status_icon">
<property name="icon_name">demon-editor</property>
<property name="has_tooltip">True</property>
<signal name="activate" handler="on_status_icon_activate" object="main_window" swapped="no"/>
<signal name="popup-menu" handler="on_popup_menu" object="staus_popup_menu" swapped="no"/>
</object>
</interface>

179
app/ui/transmitter.py Normal file
View File

@@ -0,0 +1,179 @@
from pathlib import Path
from urllib.parse import urlparse
import gi
from gi.repository import GLib
from app.commons import log
from app.settings import IS_DARWIN
from app.connections import HttpAPI
from app.tools.yt import YouTube
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..
It used for direct playback of media links by the enigma2 media player.
"""
__STREAM_PREFIX = "4097:0:1:0:0:0:0:0:0:0:"
def __init__(self, http_api, app_window, settings):
handlers = {"on_popup_menu": self.on_popup_menu,
"on_status_icon_activate": self.on_status_icon_activate,
"on_url_changed": self.on_url_changed,
"on_url_activate": self.on_url_activate,
"on_drag_data_received": self.on_drag_data_received,
"on_previous": self.on_previous,
"on_next": self.on_next,
"on_stop": self.on_stop,
"on_clear": self.on_clear,
"on_play": self.on_play}
self._http_api = http_api
self._app_window = app_window
self._is_status_icon = True
builder = Gtk.Builder()
builder.add_from_file(UI_RESOURCES_PATH + "transmitter.glade")
builder.connect_signals(handlers)
self._main_window = builder.get_object("main_window")
self._url_entry = builder.get_object("url_entry")
self._tool_bar = builder.get_object("tool_bar")
self._popup_menu = builder.get_object("staus_popup_menu")
self._restore_menu_item = builder.get_object("restore_menu_item")
self._status_active = None
self._status_passive = None
self._yt = YouTube.get_instance(settings)
if IS_DARWIN:
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
category = AppIndicator3.IndicatorCategory.APPLICATION_STATUS
path = Path(UI_RESOURCES_PATH + "/icons/hicolor/scalable/apps/demon-editor.svg")
path = str(path.resolve()) if path.is_file() else "demon-editor"
self._tray = AppIndicator3.Indicator.new("DemonEditor", path, category)
self._tray.set_status(self._status_active)
self._tray.set_secondary_activate_target(builder.get_object("show_menu_item"))
self._tray.set_menu(self._popup_menu)
style_provider = Gtk.CssProvider()
style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
def show(self, show):
if self._is_status_icon:
self._tray.set_visible(show)
elif self._status_active:
self._tray.set_status(self._status_active if show else self._status_passive)
if not show:
self.hide()
def hide(self):
self._main_window.hide()
def on_popup_menu(self, menu, button, time):
menu.popup(None, None, None, None, button, time)
def on_status_icon_activate(self, window):
visible = window.get_visible()
window.hide() if visible else window.show()
self._app_window.present() if visible else self._app_window.iconify()
def on_url_changed(self, entry):
entry.set_name("GtkEntry" if self.is_url(entry.get_text()) else "digit-entry")
def on_url_activate(self, entry):
gen = self.activate_url(entry.get_text())
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def on_drag_data_received(self, entry, drag_context, x, y, data, info, time):
url = data.get_text()
GLib.idle_add(entry.set_text, url)
gen = self.activate_url(url)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def activate_url(self, url):
self._url_entry.set_name("GtkEntry")
self._url_entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None)
if self.is_url(url):
self._tool_bar.set_sensitive(False)
yt_id = YouTube.get_yt_id(url)
yield True
if yt_id:
self._url_entry.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, get_yt_icon("youtube", 32))
links, title = self._yt.get_yt_link(yt_id, url)
yield True
if links:
url = links[sorted(links, key=lambda x: int(x.rstrip("p")), reverse=True)[0]]
else:
self.on_done(links)
return
else:
self._url_entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None)
self._http_api.send(HttpAPI.Request.PLAY, url, self.on_done, self.__STREAM_PREFIX)
yield True
def on_done(self, res):
""" Play callback """
res = res.get("e2state", None) if res else res
self._url_entry.set_name("GtkEntry" if res else "digit-entry")
GLib.idle_add(self._tool_bar.set_sensitive, True)
def on_previous(self, item):
self._http_api.send(HttpAPI.Request.PLAYER_PREV, None, self.on_done)
def on_next(self, item):
self._http_api.send(HttpAPI.Request.PLAYER_NEXT, None, self.on_done)
def on_play(self, item):
self._http_api.send(HttpAPI.Request.PLAYER_PLAY, None, self.on_done)
def on_stop(self, item):
self._http_api.send(HttpAPI.Request.PLAYER_STOP, None, self.on_done)
def on_clear(self, item):
""" Remove added links in the playlist. """
GLib.idle_add(self._tool_bar.set_sensitive, False)
self._http_api.send(HttpAPI.Request.PLAYER_LIST, None, self.clear_playlist)
def clear_playlist(self, res):
GLib.idle_add(self._tool_bar.set_sensitive, not res)
if "error_code" in res:
log("Error clearing playlist. There may be no http connection.")
self.on_done(res)
return
for ref in res:
GLib.idle_add(self._tool_bar.set_sensitive, False)
self._http_api.send(HttpAPI.Request.PLAYER_REMOVE,
ref.get("e2servicereference", ""),
self.on_done,
self.__STREAM_PREFIX)
@staticmethod
def is_url(text):
""" Simple url checking. """
result = urlparse(text)
return result.scheme and result.netloc
if __name__ == "__main__":
pass

245
app/ui/uicommons.py Normal file
View File

@@ -0,0 +1,245 @@
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
# Setting mod mask for keyboard depending on 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
IS_GNOME_SESSION = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
# Translation.
TEXT_DOMAIN = "demon-editor"
try:
settings = Settings.get_instance()
except SettingsException:
pass
else:
os.environ["LANGUAGE"] = settings.language
st = Gtk.Settings().get_default()
st.set_property("gtk-application-prefer-dark-theme", settings.dark_mode)
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")
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)
@lru_cache(maxsize=1)
def get_yt_icon(icon_name, size=24):
""" Getting YouTube icon.
If the icon is not found in the icon themes, the "Info" icon is returned by default!
"""
default_theme = Gtk.IconTheme.get_default()
if default_theme.has_icon(icon_name):
return default_theme.load_icon(icon_name, size, 0)
n_theme = Gtk.IconTheme.new()
import glob
for theme_name in map(os.path.basename, filter(os.path.isdir, glob.glob("/usr/share/icons/*"))):
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)
def show_notification(message, timeout=10000, urgency=1):
""" Shows notification.
@param message: text to display
@param timeout: milliseconds
@param urgency: 0 - low, 1 - normal, 2 - critical
"""
if IS_DARWIN:
# Since NSUserNotification has been deprecated, osascript will be used.
os.system("""osascript -e 'display notification "{}" with title "DemonEditor"'""".format(message))
elif NOTIFY_IS_INIT:
notify = Notify.Notification.new("DemonEditor", message, "demon-editor")
notify.set_urgency(urgency)
notify.set_timeout(timeout)
notify.show()
class KeyboardKey(Enum):
""" The raw(hardware) codes of the keyboard keys. """
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
# Laptop codes
HOME_KP = 79
END_KP = 87
PAGE_UP_KP = 81
PAGE_DOWN_KP = 89
@classmethod
def value_exist(cls, value):
return value in (val.value for val in cls.__members__.values())
# Keys for move in lists. KEY_KP_(NAME) for laptop!!!
MOVE_KEYS = (KeyboardKey.UP, KeyboardKey.PAGE_UP, KeyboardKey.DOWN, KeyboardKey.PAGE_DOWN, KeyboardKey.HOME,
KeyboardKey.END, KeyboardKey.HOME_KP, KeyboardKey.END_KP, KeyboardKey.PAGE_UP_KP, KeyboardKey.PAGE_DOWN_KP)
class FavClickMode(IntEnum):
""" Double click mode on the service in the bouquet(FAV) list. """
DISABLED = 0
STREAM = 1
PLAY = 2
ZAP = 3
ZAP_PLAY = 4
class ViewTarget(Enum):
""" Used for set target view. """
BOUQUET = 0
FAV = 1
SERVICES = 2
class BqGenType(Enum):
""" Bouquet generation type. """
SAT = 0
EACH_SAT = 1
PACKAGE = 2
EACH_PACKAGE = 3
TYPE = 4
EACH_TYPE = 5
class Column(IntEnum):
""" Column nums in the views """
# main view
SRV_CAS_FLAGS = 0
SRV_STANDARD = 1
SRV_CODED = 2
SRV_SERVICE = 3
SRV_LOCKED = 4
SRV_HIDE = 5
SRV_PACKAGE = 6
SRV_TYPE = 7
SRV_PICON = 8
SRV_PICON_ID = 9
SRV_SSID = 10
SRV_FREQ = 11
SRV_RATE = 12
SRV_POL = 13
SRV_FEC = 14
SRV_SYSTEM = 15
SRV_POS = 16
SRV_DATA_ID = 17
SRV_FAV_ID = 18
SRV_TRANSPONDER = 19
SRV_TOOLTIP = 20
SRV_BACKGROUND = 21
# fav view
FAV_NUM = 0
FAV_CODED = 1
FAV_SERVICE = 2
FAV_LOCKED = 3
FAV_HIDE = 4
FAV_TYPE = 5
FAV_POS = 6
FAV_ID = 7
FAV_PICON = 8
FAV_TOOLTIP = 9
FAV_BACKGROUND = 10
# bouquets view
BQ_NAME = 0
BQ_LOCKED = 1
BQ_HIDDEN = 2
BQ_TYPE = 3
def __index__(self):
""" Overridden to get the index in slices directly """
return self.value
if __name__ == "__main__":
pass

View File

@@ -1,19 +0,0 @@
#!/bin/env bash
VER="0.2.3_Pre-alpha"
B_PATH="dist/DemonEditor"
DEB_PATH="$B_PATH/usr/share/demoneditor"
mkdir -p $B_PATH
cp -TRv deb $B_PATH
cp -Rv app $DEB_PATH
cp -Rv start.py $DEB_PATH
cd dist
fakeroot dpkg-deb --build DemonEditor
mv DemonEditor.deb DemonEditor_$VER.deb
rm -R DemonEditor

View File

@@ -1,9 +0,0 @@
Package: DemonEditor
Version: 0.2.3-Pre-alpha
Section: utils
Priority: optional
Architecture: all
Essential: no
Depends: python3 (>= 3.5)
Maintainer: Dmitriy Yefremov <dmitry.v.yefremov@gmail.com>
Description: Enigma2 channels and satellites list editor

View File

@@ -1,26 +0,0 @@
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 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

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

View File

@@ -1,11 +0,0 @@
[Desktop Entry]
Version=1.0
Name=DemonEditor
Comment=Channels and satellites list editor for Enigma2
Comment[ru]=Редактор списка каналов и спутников для Enigma2
Icon=accessories-text-editor
Exec=/usr/bin/demoneditor.sh
Terminal=false
Type=Application
Categories=Utility;Application;
StartupNotify=true

View File

@@ -1,21 +0,0 @@
MIT License
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
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

@@ -1,25 +0,0 @@
# 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)
Keyboard shortcuts:
Ctrl + X, C, V, Up, Down, PageUp, PageDown, S, T, E, L, H, Space; Insert, Delete, F2.
Insert - copies the selected channels from the main list to the bouquet or inserts (creates) a new bouquet.
Ctrl + X - only in bouquet list. Ctrl + C - only in services list.
Clipboard is "rubber". There is an accumulation before the insertion!
Ctrl + E, F2 - edit/rename.
Ctrl + S, T, E in Satellites edit tool for create and edit satellite or transponder.
Ctrl + L - parental lock.
Ctrl + H - hide/skip.
Ability to import IPTV into bouquet from m3u files!
Tests only in image based on OpenPLi or last BPanther(neutrino) images with GM 990 Spark Reloaded receiver
in my preferred linux distro (Last Linux Mint 18.* - MATE 64-bit)!
Minimum requirements: Python >= 3.5.2 and GTK+ 3 with PyGObject bindings.
Terrestrial and cable channels at the moment are not supported!

BIN
icon.icns Normal file

Binary file not shown.

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

File diff suppressed because it is too large Load Diff

3
po/build.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/bash
#xgettext --keyword=translatable --sort-output -L Glade -o po/demon-editor.po app/ui/main_window.glade
#msgfmt demon-editor.po -o demon-editor.mo

1174
po/de/demon-editor.po Normal file

File diff suppressed because it is too large Load Diff

1027
po/es/demon-editor.po Normal file

File diff suppressed because it is too large Load Diff

1015
po/nl/demon-editor.po Normal file

File diff suppressed because it is too large Load Diff

1004
po/pl/demon-editor.po Executable file

File diff suppressed because it is too large Load Diff

1012
po/pt/demon-editor.po Normal file

File diff suppressed because it is too large Load Diff

1161
po/ru/demon-editor.po Normal file

File diff suppressed because it is too large Load Diff

990
po/tr/demon-editor.po Normal file
View File

@@ -0,0 +1,990 @@
msgid ""
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"
"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"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Language: tr\n"
msgid "translator-credits"
msgstr "audi06_19 <info@dreamosat-forum.com>"
# Main
msgid "Service"
msgstr "Hizmet"
msgid "Package"
msgstr "Paket"
msgid "Type"
msgstr "Tür"
msgid "Picon"
msgstr "Picon"
msgid "Freq"
msgstr "Frekans"
msgid "Rate"
msgstr "Oran"
msgid "Pol"
msgstr "Pol"
msgid "System"
msgstr "Sistem"
msgid "Pos"
msgstr "Pos"
msgid "Num"
msgstr "Num"
msgid "Current IP:"
msgstr "Geçerli IP:"
msgid "Assign"
msgstr "Ata"
msgid "Bouquet details"
msgstr "Buket detayları"
msgid "Bouquets"
msgstr "Buketler"
msgid "Copy"
msgstr "Kopya"
msgid "Copy reference"
msgstr "Referansı kopyala"
msgid "Download"
msgstr "İndir"
msgid "Edit"
msgstr "Düzelt"
msgid "Edit mаrker text"
msgstr "Marker metnini Düzenle"
msgid "FTP-transfer"
msgstr "FTP aktarımı"
msgid "Global search"
msgstr "Global arama"
msgid "Hide"
msgstr "Gizle"
msgid "Hide/Skip On/Off Ctrl + H"
msgstr "Gizle/Atla Açık/Kapalı Ctrl + H"
msgid "Add IPTV or stream service"
msgstr "IPTV veya akış hizmeti ekle"
msgid "Import m3u"
msgstr "M3u aktar"
msgid "Import m3u file"
msgstr "İçe aktar M3U"
msgid "List configuration"
msgstr "Liste yapılandırması"
msgid "Rename for this bouquet"
msgstr "Bu buketi yeniden adlandır"
msgid "Set default name"
msgstr "Varsayılan adı ayarla"
msgid "Insert marker"
msgstr "İşaretçi ekle"
msgid "Locate in services"
msgstr "Hizmetlerde bulun"
msgid "Locked"
msgstr "Kilitli"
msgid "Move"
msgstr "Taçı"
msgid "New"
msgstr "Yeni"
msgid "New bouquet"
msgstr "Yeni buket"
msgid "Create bouquet"
msgstr "Buket oluştur"
msgid "For current satellite"
msgstr "Mevcut uydu için"
msgid "For current package"
msgstr "Mevcut paket için"
msgid "For current type"
msgstr "Mevcut tip için"
msgid "For each satellite"
msgstr "Her uydu için"
msgid "For each package"
msgstr "Her paket için"
msgid "For each type"
msgstr "Her tip için"
msgid "Open"
msgstr "Aç"
msgid "Parent lock On/Off Ctrl + L"
msgstr "Ebeveyn kilidi Açık/Kapalı Ctrl + L"
msgid "Picons"
msgstr "Piconlar"
msgid "Picons downloader"
msgstr "Piconları Güncelle/indir"
msgid "Satellites downloader"
msgstr "Satellites Güncelle/indir"
msgid "Remove"
msgstr "Kaldır"
msgid "Remove all unavailable"
msgstr "Tüm mevcut olmayanları kaldır"
msgid "Satellites editor"
msgstr "Uydular editörü"
msgid "Save"
msgstr "Kaydet"
msgid "Search"
msgstr "Arama"
msgid "Services"
msgstr "Hizmetler"
msgid "Services filter"
msgstr "Hizmet filtresi"
msgid "Settings"
msgstr "Ayarlar"
msgid "Up"
msgstr "Yukarı"
msgid "Down"
msgstr "Aşağı"
msgid "Active profile:"
msgstr "Etkin profil:"
msgid "All"
msgstr "Tümü"
msgid "Are you sure?"
msgstr "Emin misin?"
msgid "Current data path:"
msgstr "Mevcut veri yolu:"
msgid "Data:"
msgstr "Veri:"
msgid "Enigma2 channel and satellite list editor for GNU/Linux."
msgstr "GNU/Linux için Enigma2 kanalı ve uydu listesi editörü."
msgid "Host:"
msgstr "Ana bilgisayar:"
msgid "Loading data..."
msgstr "Veriler yükleniyor ..."
msgid "Receive"
msgstr "Cihazdan Al"
msgid "Receive files from receiver"
msgstr "Alıcıdan dosya al"
msgid "Receiver IP:"
msgstr "Alıcı IP'si:"
msgid "Remove unused bouquets"
msgstr "Kullanılmayan buketleri kaldır"
msgid "Reset profile"
msgstr "Profili sıfırla"
msgid "Satellites"
msgstr "Uydular"
msgid "Satellites.xml file:"
msgstr "Satellites.xml dosyası:"
msgid "Selected"
msgstr "Seçildi"
msgid "Send"
msgstr "Cihaza Gönder"
msgid "Send files to receiver"
msgstr "Alıcıya dosya gönder"
msgid "Services and Bouquets files:"
msgstr "Hizmetler ve Buketler dosyaları:"
msgid "User bouquet files:"
msgstr "Kullanıcı buketi dosyaları:"
msgid "Extra:"
msgstr "Ekstra:"
# Filter bar
msgid "Only free"
msgstr "Sadece ücretsiz"
msgid "All positions"
msgstr "Tüm pozisyonlar"
msgid "All types"
msgstr "Tüm türler"
# Streams player
msgid "Play"
msgstr "Oynat"
msgid "Stop playback"
msgstr "Oynatmayı durdur"
msgid "Previous stream in the list"
msgstr "Listedeki önceki akış"
msgid "Next stream in the list"
msgstr "Listedeki sonraki akış"
msgid "Toggle in fullscreen"
msgstr "Tam ekranda geçiş yap"
msgid "Close"
msgstr "Kapat"
# Picons dialog
msgid "Load providers"
msgstr "Yük sağlayıcılar"
msgid "Providers"
msgstr "Yayıncılar"
msgid "Receive picons"
msgstr "Piconları al"
msgid "Picons name format:"
msgstr "Piconların adı biçimi:"
msgid "Resize:"
msgstr "Yeniden boyutlandır:"
msgid "Current picons path:"
msgstr "Geçerli picon yolları:"
msgid "Receiver picons path:"
msgstr "Alıcı picon yolu:"
msgid "Picons download tool"
msgstr "Picon indirme aracı"
msgid "Transfer to receiver"
msgstr "Alıcıya aktar"
msgid "Downloader"
msgstr "İndirici"
msgid "Converter"
msgstr "Dönüştürücü"
msgid "Convert"
msgstr "Dönüştür"
msgid "Path to save:"
msgstr "Kaydetme yolu:"
msgid "Path to Enigma2 picons:"
msgstr "Enigma2 piconların yolu:"
msgid "Specify the correct position value for the provider!"
msgstr "Sağlayıcı için doğru pozisyon değerini belirtin!"
msgid "Converter between name formats"
msgstr "İsim formatları arasında dönüştürücü"
msgid "Receive picons for providers"
msgstr "Sağlayıcılar için picon alma"
msgid "Load satellite providers."
msgstr "Uydu sağlayıcılarını yükle."
msgid ""
"To automatically set the identifiers for picons,\n"
"first load the required services list into the main application window."
msgstr ""
"Picon tanımlayıcılarını otomatik olarak ayarlamak için \n"
"önce gerekli uygulama listesini ana uygulama penceresine yükleyin."
# Satellites editor
msgid "Satellites edit tool"
msgstr "Uydular düzenleme aracı"
msgid "Add"
msgstr "Ekle"
msgid "Satellite"
msgstr "Uydu Ekle"
msgid "Transponder"
msgstr "Transponder Ekle"
msgid "Satellite properties:"
msgstr "Uydu özellikleri:"
msgid "Transponder properties:"
msgstr "Transponder özellikleri:"
msgid "Name"
msgstr "Ad"
msgid "Position"
msgstr "Konum"
# Satellites update dialog
msgid "Satellites update"
msgstr "Uydular güncelleme"
msgid "Remove selection"
msgstr "Seçimi kaldır"
# Service details dialog
msgid "Service data:"
msgstr "Servis verileri:"
msgid "Transponder data:"
msgstr "Transponder verileri:"
msgid "Service data"
msgstr "Servis verileri"
msgid "Transponder details"
msgstr "Transponder detayları"
msgid ""
"Changes will be applied to all services of this transponder!\n"
"Continue?"
msgstr ""
"Değişiklikler bu transponderin tüm servislerine uygulanacak!\n"
"Devam edilsinmi?"
msgid "Reference"
msgstr "Referans"
msgid "Namespace"
msgstr "Ad alanı"
msgid "Flags:"
msgstr "Bayraklar:"
msgid "Delays (ms):"
msgstr "Gecikmeler (ms):"
msgid "Bitstream"
msgstr "Bit akımı"
msgid "Description"
msgstr "Açıklama"
msgid "Source:"
msgstr "Kaynak:"
msgid "Cancel"
msgstr "İptal"
msgid "Update"
msgstr "Güncelle"
msgid "Filter"
msgstr "Filtre"
msgid "Find"
msgstr "Bul"
# IPTV dialog
msgid "Stream data"
msgstr "Veri akışı"
# IPTV list configuration dialog
msgid "Starting values"
msgstr "Başlangıç değerleri"
msgid "Reset to default"
msgstr "Varsayılana sıfırla"
msgid "IPTV streams list configuration"
msgstr "IPTV akış listesi yapılandırması"
# Settings dialog
msgid "Preferences"
msgstr "Tercihler"
msgid "Profile:"
msgstr "Profil:"
msgid "Timeout between commands in seconds"
msgstr "Komutlar arasında saniye cinsinden zaman aşımı"
msgid "Timeout:"
msgstr "Zaman aşımı:"
msgid "Login:"
msgstr "Giriş:"
msgid "Options"
msgstr "Seçenekler"
msgid "Password:"
msgstr "Parola:"
msgid "Picons:"
msgstr "Piconlar:"
msgid "Port:"
msgstr "Port:"
msgid "Data path:"
msgstr "Veri yolu:"
msgid "Picons path:"
msgstr "Picon yolu:"
msgid "Network settings:"
msgstr "Ağ ayarları:"
msgid "STB file paths:"
msgstr "STB dosya yolları:"
msgid "Local file paths:"
msgstr "Yerel dosya yolları:"
# Dialogs messages
msgid "Error. No bouquet is selected!"
msgstr "Hata. Buket seçilmedi!"
msgid "This item is not allowed to be removed!"
msgstr "Bu öğenin kaldırılmasına izin verilmiyor!"
msgid "This item is not allowed to edit!"
msgstr "Bu öğenin düzenlenmesine izin verilmiyor!"
msgid "Not allowed in this context!"
msgstr "Bu bağlamda izin verilmiyor!"
msgid "Please, download files from receiver or setup your path for read data!"
msgstr "Lütfen, alıcıdan dosya indirin veya veri okumak için yolunuzu ayarlayın!"
msgid "Reading data error!"
msgstr "Veri hatası okunuyor!"
msgid "No m3u file is selected!"
msgstr "Hiçbir m3u dosyası seçilmedi!"
msgid "Not implemented yet!"
msgstr "Henüz uygulanmadı!"
msgid "The text of marker is empty, please try again!"
msgstr "İşaretçi metni boş, lütfen tekrar deneyin!"
msgid "Please, select only one item!"
msgstr "Lütfen sadece bir ürün seçiniz!"
msgid "No png file is selected!"
msgstr "Hiçbir png dosyası seçilmedi!"
msgid "No reference is present!"
msgstr "Referans yok!"
msgid "No selected item!"
msgstr "Seçili öğe yok!"
msgid "The task is already running!"
msgstr "Görev zaten çalışıyor!"
msgid "Done!"
msgstr "Bitti!"
msgid "Please, wait..."
msgstr "Lütfen bekleyin ..."
msgid "Resizing..."
msgstr "Yeniden boyutlandırılıyor ..."
msgid "Select paths!"
msgstr "Yolları seç!"
msgid "No satellite is selected!"
msgstr "Uydu seçilmedi!"
msgid "Please, select only one satellite!"
msgstr "Lütfen sadece bir uydu seçin!"
msgid "Please check your parameters and try again."
msgstr "Lütfen parametrelerinizi kontrol edip tekrar deneyin."
msgid "No satellites.xml file is selected!"
msgstr "Hiçbir satellites.xml dosyası seçilmedi!"
msgid "Error. Verify the data!"
msgstr "Hata. Verileri doğrulayın!"
msgid "Operation not allowed in this context!"
msgstr "Bu bağlamda işleme izin verilmiyor!"
msgid "No VLC is found. Check that it is installed!"
msgstr "VLC bulunamadı. Yüklü olduğundan emin olun!"
# Search unavailable streams dialog
msgid "Please wait, streams testing in progress..."
msgstr "Lütfen bekleyin, akış testi devam ediyor ..."
msgid "Found"
msgstr "Bulundu"
msgid "unavailable streams."
msgstr "mevcut olmayan akışlar."
msgid "No changes required!"
msgstr "Hiçbir değişiklik gerekli!"
msgid "This list does not contains IPTV streams!"
msgstr "Bu liste IPTV akışı içermiyor!"
msgid "New empty configuration"
msgstr "Yeni boş yapılandırma"
msgid "No data to save!"
msgstr "Kaydedilecek veri yok!"
msgid "Network"
msgstr "Ağ"
msgid "Paths"
msgstr "Yollar"
msgid "Program"
msgstr "Program"
msgid "Backup:"
msgstr "Yedekleme:"
msgid "Backup"
msgstr "Yedekleme"
msgid "Backups"
msgstr "Yedeklemeler"
msgid "Backup path:"
msgstr "Yedekleme yolu:"
msgid "Restore bouquets"
msgstr "Buketleri geri yükle"
msgid "Restore all"
msgstr "Tümünü geri yükle"
msgid "Before saving"
msgstr "Kaydetmeden önce"
msgid "Before downloading from the receiver"
msgstr "Alıcıdan indirmeden önce"
msgid "Set background color for the services"
msgstr "Hizmetler için arka plan rengini ayarla"
msgid "Marked as new:"
msgstr "Yeni olarak işaretlendi:"
msgid "With an extra name in the bouquet:"
msgstr "Bukette fazladan bir isim ile:"
msgid "Select"
msgstr "Seç"
msgid "About"
msgstr "Hakkında"
msgid "Exit"
msgstr "Çıkış"
msgid "Tools"
msgstr "Araçlar"
# Import
msgid "Import"
msgstr "İçe aktar"
msgid "Bouquet"
msgstr "Buket"
msgid "Bouquets and services"
msgstr "Buketler ve hizmetler"
msgid "The main list does not contain services for this bouquet!"
msgstr "Ana liste bu buket için hizmet içermiyor!"
msgid "No bouquet file is selected!"
msgstr "Hiçbir buket dosyası seçilmedi!"
msgid "Remove all unused"
msgstr "Kullanılmayanların tümünü kaldır"
msgid "Test"
msgstr "Test"
msgid "Test connection"
msgstr "Bağlantıyı test et"
msgid "Double click on the service in the bouquet list:"
msgstr "Buket listesindeki hizmete çift tıklayın:"
msgid "Zap"
msgstr "Zap"
msgid "Play stream"
msgstr "Akışı oynat"
msgid "Disabled"
msgstr "Devre dışı"
msgid "Enable lamedb ver. 5 support"
msgstr "Sürüm 5 desteğini etkinleştir"
msgid "Enable HTTP API"
msgstr "HTTP API'sini etkinleştir"
msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Kanalı değiştir (zap) (Ctrl + Z)"
msgid "Switch the channel and watch in the program(Ctrl + W)"
msgstr "Kanal değiştirme ve programda izleme (Ctrl + W)"
msgid "Play IPTV or other stream in the program(Ctrl + P)"
msgstr "Programdaki IPTV veya diğer akışları oynat (Ctrl + P)"
msgid "Export to m3u"
msgstr "Dışa aktar M3U"
msgid "EPG configuration"
msgstr "EPG yapılandırması"
msgid "Apply"
msgstr "Uygula"
msgid "EPG source"
msgstr "EPG kaynağı"
msgid "Service names source:"
msgstr "Hizmet adları kaynağı:"
msgid "Main service list"
msgstr "Ana hizmet listesi"
msgid "XML file"
msgstr "XML dosyası"
msgid "Use web source"
msgstr "Web kaynağını kullan"
msgid "Url to *.xml.gz file:"
msgstr "URL. * .xml.gz dosyasına:"
msgid "Enable filtering"
msgstr "Filtrelemeyi etkinleştir"
msgid "Filter by presence in the epg.dat file."
msgstr "Epg.dat dosyasındaki varlığına göre filtreleyin."
msgid "Paths to the epg.dat file:"
msgstr "Epg.dat dosyasının yolları:"
msgid "Local path:"
msgstr "Yerel yol:"
msgid "STB path:"
msgstr "STB yolu:"
msgid "Update on start"
msgstr "Başlangıçta güncelleme"
msgid "Auto configuration by service names."
msgstr "Hizmet adlarına göre otomatik yapılandırma."
msgid "Save list to xml."
msgstr "Listeyi xml'ye kaydet."
msgid "Download XML file error."
msgstr "XML dosyası indir hatası."
msgid "Unsupported file type:"
msgstr "Desteklenmeyen dosya türü:"
msgid "Unpacking data error."
msgstr "Veri paketi açılırken hata oluştu."
msgid "XML parsing error:"
msgstr "XML ayrıştırma hatası:"
msgid "Count of successfully configured services:"
msgstr "Başarılı bir şekilde yapılandırılmış hizmetlerin sayısı:"
msgid "Current epg.dat file does not contains references for the services of this bouquet!"
msgstr "Şu anki epg.dat dosyası bu buketin hizmetleri için referans içermiyor!"
msgid "Use HTTP"
msgstr "HTTP kullan"
msgid "Close playback"
msgstr "Oynatmayı kapat"
msgid "Import YouTube playlist"
msgstr "YouTube oynatma listesini içe aktar"
msgid ""
"Found a link to the YouTube resource!\n"
"Try to get a direct link to the video?"
msgstr ""
"YouTube kaynağına bir bağlantı bulundu!\n"
"Videoya doğrudan bağlantı almaya çalışılsın mı?"
msgid "Playlist import"
msgstr "Oynatma listesi içe aktarma"
msgid "Getting link error:"
msgstr "Bağlantı hatası alınıyor:"
msgid "Extra"
msgstr "Ekstra"
msgid "Apply profile settings"
msgstr "Profil ayarlarını uygula"
msgid "Settings type:"
msgstr "Ayarlar türü:"
msgid "Set default"
msgstr "Varsayılanı ayarla"
msgid "Language:"
msgstr "Dil:"
msgid "Load the last open configuration at program startup"
msgstr "Program açılışında son açık yapılandırmayı yükle"
msgid "Enable direct playback bar"
msgstr "Doğrudan oynatma çubuğunu etkinleştir"
msgid "Enables direct sending and playback of media links on the receiver"
msgstr "Alıcıdaki medya bağlantılarının doğrudan gönderilmesini ve oynatılmasını sağlar"
msgid "Watch the channel in the program"
msgstr "Programdaki kanalı izle"
msgid "Zap and Play"
msgstr "Zap ve Oyna"
msgid "Drag or paste the link here"
msgstr "Bağlantıyı buraya sürükleyin veya yapıştırın"
msgid "Remove added links in the playlist"
msgstr "Oynatma listesine eklenen bağlantıları kaldır"
msgid "A bouquet with that name exists!"
msgstr "Bu isimli bir buket var!"
msgid "Details"
msgstr "Detaylar"
msgid "Profile"
msgstr "Profil"
msgid "Reset"
msgstr "Yeniden Başlat"
msgid "File"
msgstr "Dosya"
msgid "Picons manager"
msgstr "Picon yöneticisi"
msgid "Explorer"
msgstr "Explorer"
msgid "Satellite url:"
msgstr "Uydu url:"
msgid "Cut"
msgstr "Kes"
msgid "Paste"
msgstr "Yapıştır"
msgid "To the top"
msgstr "Başlangıçta"
msgid "To the end"
msgstr "Sonunda"
msgid "View"
msgstr "Görünüm"
msgid "Lock"
msgstr "Kilit"
msgid "Parent lock"
msgstr "Ebeveyn kilidi"
msgid "Hide/Skip"
msgstr "Gizle/Atla"
msgid "IPTV tools"
msgstr "IPTV araçları"
msgid "Make profile folder as default for the additional data"
msgstr "Ek veriler için profil klasörünü varsayılan yap"
msgid "Default data path:"
msgstr "Varsayılan veri yolu:"
msgid "Streams record path:"
msgstr "Akış kayıt yolu:"
msgid "Record"
msgstr "Kayıt"
msgid "Record:"
msgstr "Kayıt:"
msgid "Record to disk:"
msgstr "Diske kaydet:"
msgid "Streaming"
msgstr "Yayın Akışı"
msgid "Activate transcoding"
msgstr "Kod dönüştürmeyi etkinleştir"
msgid "Presets:"
msgstr "Ön ayarlar:"
msgid "Video options:"
msgstr "Video seçenekleri:"
msgid "Audio options:"
msgstr "Ses seçenekleri:"
msgid "Bitrate (kb/s):"
msgstr "Bit hızı (kb/s):"
msgid "Codec:"
msgstr "Codec:"
msgid "Width (px):"
msgstr "Genişlik (px):"
msgid "Height (px):"
msgstr "Yükseklik (px):"
msgid "Channels:"
msgstr "Kanallar:"
msgid "Sample rate (Hz):"
msgstr "Örnekleme hızı (Hz):"
msgid "Play streams mode:"
msgstr "Akışları oynatma modu:"
msgid "Built-in player"
msgstr "Dahili oynatıcı"
msgid "VLC media player"
msgstr "VLC media player"
msgid "Only get m3u file"
msgstr "Sadece m3u dosyası al"
msgid "Save and restart the program to apply the settings."
msgstr "Ayarları uygulamak için programı kaydedin ve yeniden başlatın."
msgid "Some images may have problems displaying the favorites list!"
msgstr "Bazı resimler sık kullanılanlar listesini görüntülerken sorun yaşayabilir!"
msgid "Operates in standby mode or current active transponder!"
msgstr "Bekleme modunda veya geçerli aktif transponderde çalışır!"
msgid "No connection to the receiver!"
msgstr "Alıcıya bağlantı yok!"
msgid "Signal level"
msgstr "Sinyal seviyesi"
msgid "Receiver info"
msgstr "Alıcı bilgisi"
msgid "A profile with that name exists!"
msgstr "Bu ada sahip bir profil var!"
msgid "Show short info as hints in the main services list"
msgstr "Kısa bilgileri ana hizmetler listesinde ipuçları olarak göster"
msgid "Show detailed info as hints in the bouquet list"
msgstr "Ayrıntılı bilgileri buket listesinde ipuçları olarak göster"
msgid "Enable alternate bouquet file naming"
msgstr "Alternatif buket dosyası adlandırmasını etkinleştir"
msgid "Allows you to name bouquet files using their names."
msgstr "Buket dosyalarını isimlerini kullanarak isimlendirmenizi sağlar."
msgid "Appearance"
msgstr "Görünüm"
msgid "Enable Themes support"
msgstr "Temalar desteğini etkinleştir"
msgid "Gtk3 Theme:"
msgstr "Gtk3 Teması:"
msgid "Icon Theme:"
msgstr "Simge Teması:"
msgid "Gtk3 Themes and Icons:"
msgstr "Gtk3 Tema ve Simgeler:"
msgid "Deleting data..."
msgstr "Veriler siliniyor..."
msgid "Download from the receiver"
msgstr "Alıcıdan indir"
msgid "Remove all picons from the receiver"
msgstr "Alıcıdaki tüm piconları kaldırın"
msgid "Service reference"
msgstr "Servis referansı"

View File

@@ -1,4 +1,19 @@
#!/usr/bin/env python3
from app.ui.main_app_window import start_app
start_app()
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]
start_app()