Compare commits

...

232 Commits
0.2.3 ... 0.4.0

Author SHA1 Message Date
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
a1dada9a55 fix showing iptv dialog for neutrino 2018-10-15 12:36:03 +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
53 changed files with 21843 additions and 4838 deletions

View File

@@ -8,4 +8,4 @@ Exec=bash -c 'cd $(dirname %k) && ./start.py'
Terminal=false
Type=Application
Categories=Utility;Application;
StartupNotify=true
StartupNotify=false

View File

@@ -1,32 +1,34 @@
# DemonEditor
Enigma2 channel and satellites list editor for GNU/Linux.
## 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:
* **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** - edit.
* **Ctrl + R, F2** - rename.
* **Ctrl + S, T** in Satellites edit tool for create satellite or transponder.
* **Ctrl + L** - parental lock.
* **Ctrl + H** - hide/skip.
* **Ctrl + P** - start play IPTV or other stream in the bouquet list.
* **Space** - select/deselect.
* **Left/Right** - remove selection.
* **Ctrl + Up, Down, PageUp, PageDown, Home, End** - move selected items in the list.
### Extra:
* Multiple selections in lists only with Space key (as in file managers).
* Ability to import IPTV into bouquet (Neutrino WEBTV) from m3u files.
* Ability to download picons and update satellites (transponders) from web.
* Preview (playing) IPTV or other streams directly from the bouquet list(should be installed VLC).
### Minimum requirements:
Python >= 3.5.2 and GTK+ >= 3.16 with PyGObject bindings.
#### Note.
To create a simple debian package, you can use the build-deb.sh
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.
Tests only with openATV image and Formuler F1 receiver in my preferred Linux distros
(latest Linux Mint 18.* and 19 MATE 64-bit)!
Multiple selections in lists only with Space key (as in file managers)!
**Terrestrial and cable channels at the moment are not supported!**
Extra:
Ability to import IPTV into bouquet from m3u files(Enigma2 only)!
Tool for downloading picons from lyngsat.com.
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!
Note. To create a simple debian package, you can use the build-deb.sh

View File

@@ -1,6 +1,6 @@
import logging
from functools import wraps
from threading import Thread
from threading import Thread, Timer
from gi.repository import GLib
@@ -43,5 +43,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

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 .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,17 +10,17 @@ 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):
def get_services(data_path, profile, format_version):
if profile is Profile.ENIGMA_2:
return get_enigma_services(data_path)
return get_enigma_services(data_path, format_version)
elif profile is Profile.NEUTRINO_MP:
return get_neutrino_services(data_path)
@run_task
def write_services(path, channels, profile):
def write_services(path, channels, profile, format_version):
if profile is Profile.ENIGMA_2:
write_enigma_services(path, channels)
write_enigma_services(path, channels, format_version)
elif profile is Profile.NEUTRINO_MP:
write_neutrino_services(path, channels)

View File

@@ -27,15 +27,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"
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 +57,48 @@ 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"
ROLL_OFF = {"0": "35%", "1": "25%", "2": "20%", "3": "Auto"}
POLARIZATION = {"0": "H", "1": "V", "2": "L", "3": "R"}
@@ -56,13 +109,15 @@ 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"}
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",
@@ -71,3 +126,42 @@ CAS = {"C:2600": "BISS", "C:0b00": "Conax", "C:0b01": "Conax", "C:0b02": "Conax"
# '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

@@ -1,43 +1,49 @@
""" Module for parsing bouquets """
from app.eparser.ecommons import BqServiceType, BouquetService, Bouquets, Bouquet
import re
from app.eparser.ecommons import BqServiceType, BouquetService, Bouquets, Bouquet, BqType
_TV_ROOT_FILE_NAME = "bouquets.tv"
_RADIO_ROOT_FILE_NAME = "bouquets.radio"
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'
line = []
pattern = re.compile("[^\w_()]+")
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)
bq_name = re.sub(pattern, "_", bq.name)
line.append(srv_line.format(bq_name, bq.type))
write_bouquet(path + "userbouquet.{}.{}".format(bq_name, bq.type), bq.name, bq.services)
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, channels):
bouquet = ["#NAME {}\n".format(name)]
for ch in channels:
if not ch: # if was duplicate
continue
if ch.service_type == BqServiceType.IPTV.name or ch.service_type == BqServiceType.MARKER.name:
bouquet.append("#SERVICE {}\n".format(ch.fav_id.strip()))
else:
bouquet.append("#SERVICE {}\n".format(to_bouquet_id(ch)))
data = to_bouquet_id(ch)
if ch.service:
bouquet.append("#SERVICE {}:{}\n#DESCRIPTION {}\n".format(data, ch.service, ch.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)
@@ -52,7 +58,7 @@ def to_bouquet_id(ch):
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:
with open(path + "userbouquet.{}.{}".format(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 ['']
@@ -64,13 +70,16 @@ def get_bouquet(path, name, bq_type):
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))
name = None
if len(ch_data) == 12:
name, desc = str(ch_data[-1]).split("\n#DESCRIPTION")
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id, 0))
return srvs[0].strip("#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"

View File

@@ -1,24 +1,32 @@
""" This module used for parsing lamedb file
""" This module used for parsing and write lamedb file
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
_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 """
lines = [_HEADER.format(4), "\ntransponders\n"]
tr_lines = []
services_lines = ["end\nservices\n"]
tr_set = set()
@@ -36,14 +44,46 @@ 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:
file.writelines(lines)
def parse(path):
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, 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_v4(path):
""" Parsing version 4 """
with open(path + _FILE_NAME, "r", encoding="utf-8", errors="replace") as file:
try:
data = str(file.read())
@@ -58,7 +98,33 @@ def parse(path):
transponders, sep, services = services.partition("services") # 2 step
services, sep, _ = services.partition("\nend") # 3 step
return parse_services(services.split("\n"), transponders.split("/"), path)
return parse_services(services.split("\n"), parse_transponders(transponders.split("/")), path)
def parse_v5(path):
""" Parsing version 5 """
with open(path + "lamedb5", "r", encoding="utf-8", errors="replace") as file:
lns = file.readlines()
if lns and not lns[0].endswith("/5/\n"):
raise SyntaxError("lamedb v.5 parsing error: unsupported format.")
trs, srvs = {}, [""]
for l in lns:
if l.startswith("s:"):
srv_data = l.strip("s:").split(",", 2)
srv_data[1] = srv_data[1].strip("\"")
data_len = len(srv_data)
if data_len == 3:
srv_data[2] = srv_data[2].strip()
elif data_len == 2:
srv_data.append("p:")
srvs.extend(srv_data)
elif l.startswith("t:"):
tr, srv = l.split(",")
trs[tr.strip("t:")] = srv.strip().replace(":", " ", 1)
return parse_services(srvs, trs, path)
def parse_transponders(arg):
@@ -75,9 +141,7 @@ def parse_transponders(arg):
def parse_services(services, transponders, path):
""" Parsing channels """
channels = []
transponders = parse_transponders(transponders)
blacklist = str(get_blacklist(path))
srv = split(services, 3)
if srv[0][0] == "": # remove first empty element
srv.remove(srv[0])
@@ -101,14 +165,18 @@ def parse_services(services, transponders, path):
all_flags = ch[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
hide = HIDE_ICON if flags and Flag.is_hide(int(flags[0][2:])) else None
locked = LOCKED_ICON if fav_id in blacklist else None
package = list(filter(lambda x: x.startswith("p:"), all_flags))
package = package[0][2:] if package else None
package = package[0][2:] if package else ""
if transponder is not None:
tr_type, sp, tr = str(transponder).partition(" ")
# Skipping terrestrial and cable channels
if tr_type in "tc":
continue
tr = tr.split(_SEP)
service_type = SERVICE_TYPE.get(data[4], SERVICE_TYPE["-2"])
# removing all non printable symbols!
@@ -128,7 +196,7 @@ def parse_services(services, transponders, path):
rate=tr[1],
pol=POLARIZATION[tr[2]],
fec=FEC[tr[3]],
system=SYSTEM[tr[6]],
system="DVB-S2" if len(tr) > 7 else "DVB-S",
pos="{}.{}".format(tr[4][:-1], tr[4][-1:]),
data_id=ch[0],
fav_id=fav_id,

View File

@@ -1,21 +1,39 @@
""" Module for IPTV and streams support """
from enum import Enum
from app.properties import Profile
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"
def parse_m3u(path):
class StreamType(Enum):
DVB_TS = "1"
NONE_TS = "4097"
def parse_m3u(path, profile):
with open(path) as file:
aggr = [None] * 10
channels = []
count = 0
name = None
fav_id = 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)
if profile is Profile.ENIGMA_2:
fav_id = ENIGMA2_FAV_ID_FORMAT.format(StreamType.NONE_TS.value, 1, 0, 0, 0, 0,
line.strip().replace(":", "%3a"), name, name, None)
elif profile is Profile.NEUTRINO_MP:
fav_id = NEUTRINO_FAV_ID_FORMAT.format(line.strip(), "", 0, None, None, None, None, "", "", 1)
srv = Service(None, None, IPTV_ICON, name, *aggr[0:3], BqServiceType.IPTV.name, *aggr, fav_id, None)
channels.append(srv)
return channels

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):
@@ -61,22 +59,62 @@ def parse_bouquets(file, name, bq_type):
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 +140,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

@@ -2,9 +2,13 @@
For more info see __COMMENT
"""
from functools import lru_cache
from xml.dom.minidom import parse, Document
from .ecommons import POLARIZATION, FEC, SYSTEM, MODULATION, PLS_MODE, Transponder, Satellite
import os
from app.commons import log
from .ecommons import POLARIZATION, FEC, SYSTEM, MODULATION, PLS_MODE, Transponder, Satellite, get_key_by_value
__COMMENT = (" File was created in DemonEditor\n\n"
"usable flags are\n"
@@ -18,7 +22,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"
@@ -29,7 +33,7 @@ __COMMENT = (" File was created in DemonEditor\n\n"
def get_satellites(path):
return parse_satellites(path)
return parse_satellites(path, os.path.getsize(path))
def write_satellites(satellites, data_path):
@@ -71,34 +75,42 @@ 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],
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)
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):
@lru_cache(maxsize=1)
def parse_satellites(path, file_size):
""" Parsing satellites from xml"""
dom = parse(path)
satellites = []
@@ -110,11 +122,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

@@ -5,21 +5,25 @@ from enum import Enum
from ftplib import FTP, error_perm
from telnetlib import Telnet
from app.commons import log
from app.commons import log, run_task
from app.properties import Profile
__DATA_FILES_LIST = ("tv", "radio", "lamedb", "blacklist", "whitelist", # enigma 2
__DATA_FILES_LIST = ("tv", "radio", "lamedb", "lamedb5", "blacklist", "whitelist", # enigma 2
"services.xml", "myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino
_SAT_XML_FILE = "satellites.xml"
_WEBTV_XML_FILE = "webtv.xml"
class DownloadDataType(Enum):
class DownloadType(Enum):
ALL = 0
BOUQUETS = 1
SATELLITES = 2
PICONS = 3
WEBTV = 4
def download_data(*, properties, download_type=DownloadDataType.ALL, callback=None):
def download_data(*, properties, download_type=DownloadType.ALL, callback=None):
with FTP(host=properties["host"]) as ftp:
ftp.login(user=properties["user"], passwd=properties["password"])
ftp.encoding = "utf-8"
@@ -27,7 +31,7 @@ def download_data(*, properties, download_type=DownloadDataType.ALL, callback=No
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:
if download_type is DownloadType.ALL or download_type is DownloadType.BOUQUETS:
ftp.cwd(properties["services_path"])
ftp.dir(files.append)
@@ -35,26 +39,26 @@ def download_data(*, properties, download_type=DownloadDataType.ALL, callback=No
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:
download_file(ftp, name, save_path)
# satellites.xml and webtv section
if download_type in (DownloadType.ALL, DownloadType.SATELLITES, DownloadType.WEBTV):
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 download_type in (DownloadType.ALL, DownloadType.SATELLITES) and name.endswith(_SAT_XML_FILE):
download_file(ftp, _SAT_XML_FILE, save_path)
if download_type in (DownloadType.ALL, DownloadType.WEBTV) and name.endswith(_WEBTV_XML_FILE):
download_file(ftp, _WEBTV_XML_FILE, save_path)
if callback is not None:
callback()
def upload_data(*, properties, download_type=DownloadDataType.ALL, remove_unused=False, profile=Profile.ENIGMA_2,
@run_task
def upload_data(*, properties, download_type=DownloadType.ALL, remove_unused=False, profile=Profile.ENIGMA_2,
callback=None):
data_path = properties["data_dir_path"]
host = properties["host"]
@@ -62,21 +66,32 @@ def upload_data(*, properties, download_type=DownloadDataType.ALL, remove_unused
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
# terminate enigma or neutrino
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:
if download_type is DownloadType.ALL or download_type is DownloadType.SATELLITES:
ftp.cwd(properties["satellites_xml_path"])
file_name = "satellites.xml"
send = send_file(file_name, data_path, ftp)
if download_type == DownloadDataType.SATELLITES:
send = send_file(_SAT_XML_FILE, data_path, ftp)
if download_type is DownloadType.SATELLITES:
tn.send("init 3" if profile is Profile.ENIGMA_2 else "init 6")
if callback is not None:
callback()
return send
if download_type is DownloadDataType.ALL or download_type is DownloadDataType.BOUQUETS:
if profile is Profile.NEUTRINO_MP and download_type in (DownloadType.ALL, DownloadType.WEBTV):
ftp.cwd(properties["satellites_xml_path"])
send = send_file(_WEBTV_XML_FILE, data_path, ftp)
if download_type is DownloadType.WEBTV:
tn.send("init 6")
if callback is not None:
callback()
return send
if download_type is DownloadType.ALL or download_type is DownloadType.BOUQUETS:
ftp.cwd(properties["services_path"])
if remove_unused:
files = []
@@ -88,12 +103,12 @@ def upload_data(*, properties, download_type=DownloadDataType.ALL, remove_unused
ftp.delete(name)
for file_name in os.listdir(data_path):
if file_name == "satellites.xml":
if file_name == _SAT_XML_FILE or file_name == _WEBTV_XML_FILE:
continue
if file_name.endswith(__DATA_FILES_LIST):
send_file(file_name, data_path, ftp)
if download_type is DownloadDataType.PICONS:
if download_type is DownloadType.PICONS:
picons_dir_path = properties.get("picons_dir_path")
picons_path = properties.get("picons_path")
try:
@@ -123,6 +138,11 @@ def upload_data(*, properties, download_type=DownloadDataType.ALL, remove_unused
callback()
def download_file(ftp, name, save_path):
with open(save_path + name, "wb") as f:
ftp.retrbinary("RETR " + name, f.write)
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:

View File

@@ -48,7 +48,8 @@ def get_default_settings():
"satellites_xml_path": "/etc/tuxbox/",
"picons_path": "/usr/share/enigma2/picon",
"data_dir_path": DATA_PATH + "enigma2/",
"picons_dir_path": DATA_PATH + "enigma2/picons/"},
"picons_dir_path": DATA_PATH + "enigma2/picons/",
"v5_support": False},
Profile.NEUTRINO_MP.value: {
"host": "127.0.0.1", "port": "21",
"user": "root", "password": "root",

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

@@ -0,0 +1,53 @@
from app.tools import vlc
class Player:
_VLC_INSTANCE = None
def __init__(self):
self._is_playing = False
self._player = self.get_vlc_instance()
@staticmethod
def get_vlc_instance():
if Player._VLC_INSTANCE:
return Player._VLC_INSTANCE
_VLC_INSTANCE = vlc.Instance("--quiet --no-xlib").media_player_new()
return _VLC_INSTANCE
def play(self, mrl=None):
if not self._is_playing:
if mrl:
self._player.set_mrl(mrl)
self._player.play()
self._is_playing = True
def stop(self):
if self._is_playing:
self._player.stop()
self._is_playing = False
def pause(self):
self._player.pause()
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_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)
if __name__ == "__main__":
pass

View File

@@ -1,9 +1,12 @@
import glob
import os
import re
import shutil
from collections import namedtuple
from html.parser import HTMLParser
from app.commons import log
from app.commons import log, run_task
from app.properties import Profile
_ENIGMA2_PICON_KEY = "{:X}:{:X}:{:X}0000"
@@ -102,6 +105,12 @@ class PiconsParser(HTMLParser):
class ProviderParser(HTMLParser):
""" Parser for satellite html page. (https://www.lyngsat.com/*sat-name*.html) """
_POSITION_PATTERN = re.compile("at\s\d+\..*(?:E|W)']")
_DOMAIN = "https://www.lyngsat.com"
_TV_DOMAIN = _DOMAIN + "/tvchannels/"
_RADIO_DOMAIN = _DOMAIN + "/radiochannels/"
_PKG_DOMAIN = _DOMAIN + "/packages/"
def __init__(self, entities=False, separator=' '):
HTMLParser.__init__(self)
@@ -116,7 +125,7 @@ class ProviderParser(HTMLParser):
self._current_cell = []
self.rows = []
self._ids = set()
self._counter = 0
self._prv_names = set()
self._positon = None
def handle_starttag(self, tag, attrs):
@@ -128,8 +137,9 @@ class ProviderParser(HTMLParser):
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])
url = attrs[0][1]
if url.startswith((self._PKG_DOMAIN, self._TV_DOMAIN, self._RADIO_DOMAIN)):
self._current_row.append(url)
def handle_data(self, data):
""" Save content to a cell """
@@ -147,21 +157,27 @@ class ProviderParser(HTMLParser):
self._current_row.append(final_cell)
self._current_cell = []
elif tag == 'tr':
row = self._current_row
r = 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 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")
if len(row) == 12:
on_id, sep, tid = str(row[-2]).partition("-")
len_row = len(r)
if len_row == 12:
name = r[5]
self._prv_names.add(name)
on_id, sep, tid = str(r[-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)
r[-2] = on_id
self._ids.add(on_id)
row[0] = self._positon
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=r[0], url=r[6], on_id=r[-2], selected=True))
self._current_row = []
def error(self, message):
@@ -169,7 +185,6 @@ class ProviderParser(HTMLParser):
def reset(self):
super().reset()
self._counter = 0
def parse_providers(open_path):
@@ -178,10 +193,26 @@ def parse_providers(open_path):
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]
return parser.rows
@run_task
def convert_to(src_path, dest_path, profile, 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 profile is Profile.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__":

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

@@ -0,0 +1,204 @@
""" Module for download satellites from internet ("flysat.com")
for replace or update current satellites.xml file.
"""
import re
import requests
from enum import Enum
from html.parser import HTMLParser
from app.eparser import Satellite, Transponder, is_transponder_valid
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 SatellitesParser(HTMLParser):
""" Parser for satellite html page. """
_HEADERS = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:45.0) Gecko/20100101 Firefox/59.02"}
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):
request = requests.get(url=src, headers=self._HEADERS)
reason = request.reason
if reason == "OK":
self.feed(request.text)
else:
print(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("^https://www\.lyngsat\.com/[\w-]+\.html")
sats = []
current_pos = "0"
for row in filter(lambda x: len(x) in (5, 7, 8), self._rows):
r_len = len(row)
if r_len == 7:
current_pos = self.parse_position(row[2])
sats.append((row[4], current_pos, row[5], row[1], False))
if r_len == 8: # for a very limited number of satellites
data = list(filter(None, row))
urls = set()
sat_type = ""
for d in data:
url = re.match(extra_pattern, d)
if url:
urls.add(url.group(0))
if d in ("C", "Ku", "CKu"):
sat_type = d
current_pos = self.parse_position(data[1])
for url in urls:
name = url.rsplit("/")[-1].rstrip(".html").replace("-", " ")
sats.append((name, current_pos, sat_type, url, False))
elif r_len == 5:
sats.append((row[2], current_pos, row[3], row[1], False))
return sats
def get_satellite(self, sat):
pos = sat[1]
return Satellite(name=sat[0] + " ({})".format(pos),
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):
self._rows.clear()
url = "https://www.flysat.com/" + sat_url if self._source is SatelliteSource.FLYSAT else sat_url
request = requests.get(url=url, headers=self._HEADERS)
reason = request.reason
trs = []
if reason == "OK":
self.feed(request.text)
if self._source is SatelliteSource.FLYSAT:
self.get_transponders_for_fly_sat(trs)
elif self._source is SatelliteSource.LYNGSAT:
self.get_transponders_for_lyng_sat(trs)
return trs
def get_transponders_for_fly_sat(self, trs):
""" Parsing transponders for FlySat """
if self._rows:
zeros = "000"
for r in self._rows:
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
tr = Transponder(freq + zeros, sr + zeros, pol, fec, sys, mod, None, None, None)
if is_transponder_valid(tr):
trs.append(tr)
def get_transponders_for_lyng_sat(self, trs):
""" Parsing transponders for LyngSat """
frq_pol_pattern = re.compile("(\d{4,5}).*([RLHV])(.*\d$)")
sr_fec_pattern = re.compile("^(\d{4,5})-(\d/\d)(.+PSK)?(.*)?$")
sys_pattern = re.compile("(DVB-S[2]?)(.*)?")
zeros = "000"
for r in filter(lambda x: len(x) > 8, self._rows):
freq = re.match(frq_pol_pattern, r[2])
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"
sys = re.match(sys_pattern, r[-4])
if not sys:
continue
sys = sys.group(1)
tr = Transponder(frq + zeros, sr + zeros, pol, fec, sys, mod, None, None, None)
if is_transponder_valid(tr):
trs.append(tr)
if __name__ == "__main__":
parser = SatellitesParser(source=SatelliteSource.LYNGSAT)
satts = parser.get_satellites_list(SatelliteSource.LYNGSAT)
if satts:
# list(map(print, satts))
print("Parsed: ", len(satts))

8775
app/tools/vlc.py Normal file

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,48 @@
""" Common module for showing dialogs """
import locale
from enum import Enum
from . import Gtk, UI_RESOURCES_PATH
from app.commons import run_idle
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN
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"
WAIT = "wait_dialog"
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)
if text is not None:
builder.get_object("wait_dialog_label").set_text(text)
def show(self):
self._dialog.show()
@run_idle
def hide(self):
self._dialog.hide()
@run_idle
def destroy(self):
self._dialog.destroy()
def show_dialog(dialog_type: DialogType, transient, text=None, options=None, action_type=None, file_filter=None):
""" Shows dialogs by name """
builder = Gtk.Builder()
builder.add_from_file(UI_RESOURCES_PATH + "dialogs.glade")
dialog = builder.get_object(dialog_type.value)
dialog.set_transient_for(transient)
builder, dialog = get_dialog_from_xml(dialog_type, transient)
if dialog_type is DialogType.CHOOSER and options:
if action_type is not None:
@@ -51,17 +75,28 @@ def show_dialog(dialog_type: DialogType, transient, text=None, options=None, act
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_dialog_from_xml(dialog_type, transient):
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "dialogs.glade", (dialog_type.value,))
dialog = builder.get_object(dialog_type.value)
dialog.set_transient_for(transient)
return builder, dialog
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,
@@ -69,5 +104,10 @@ def get_chooser_dialog(transient, options, pattern, name):
file_filter=file_filter)
def get_message(message):
""" returns translated message """
return locale.dgettext(TEXT_DOMAIN, message)
if __name__ == "__main__":
pass

View File

@@ -1,8 +1,8 @@
from app.commons import run_idle, run_task
from app.ftp import download_data, DownloadDataType, upload_data
from app.ftp import download_data, DownloadType, upload_data
from app.properties import Profile
from . import Gtk, UI_RESOURCES_PATH
from .dialogs import show_dialog, DialogType
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN
from .dialogs import show_dialog, DialogType, get_message
def show_download_dialog(transient, options, open_data, profile=Profile.ENIGMA_2):
@@ -22,6 +22,7 @@ class DownloadDialog:
"on_info_bar_close": self.on_info_bar_close}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "dialogs.glade", ("download_dialog",))
builder.connect_signals(handlers)
@@ -35,23 +36,27 @@ class DownloadDialog:
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")
if profile is Profile.NEUTRINO_MP:
self._webtv_radio_button.set_visible(True)
@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())
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):
@@ -64,24 +69,23 @@ class DownloadDialog:
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 """
try:
if download:
download_data(properties=self._properties, download_type=d_type)
else:
self.show_info_message("Please, wait...", Gtk.MessageType.INFO)
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
upload_data(properties=self._properties,
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=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO))
except Exception as e:
message = str(getattr(e, "message", str(e)))
self.show_info_message(message, Gtk.MessageType.ERROR)
else:
if download and d_type is not DownloadDataType.SATELLITES:
if download and d_type is not DownloadType.SATELLITES:
self._open_data()
@run_idle

1191
app/ui/iptv.glade Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,438 @@
import re
from urllib.error import HTTPError
from urllib.parse import urlparse
from urllib.request import Request, urlopen
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
from app.properties import Profile
from .uicommons import Gtk, Gdk, TEXT_DOMAIN, UI_RESOURCES_PATH, IPTV_ICON
from .dialogs import Action, show_dialog, DialogType
from .main_helper import get_base_model, get_iptv_url
_DIGIT_ENTRY_NAME = "digit-entry"
_ENIGMA2_REFERENCE = "{}:0:{}:{:X}:{:X}:{:X}:{:X}:0:0:0"
_PATTERN = re.compile("(?:^[\s]*$|\D)")
def is_data_correct(elems):
for elem in elems:
if elem.get_name() == _DIGIT_ENTRY_NAME:
return False
return True
class IptvDialog:
def __init__(self, transient, view, services, bouquet, profile=Profile.ENIGMA_2, action=Action.ADD):
handlers = {"on_entry_changed": self.on_entry_changed,
"on_url_changed": self.on_url_changed,
"on_save": self.on_save,
"on_cancel": self.on_cancel,
"on_stream_type_changed": self.on_stream_type_changed}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "iptv.glade", ("iptv_dialog", "stream_type_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._action = action
self._profile = profile
self._bouquet = bouquet
self._services = services
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 profile is Profile.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._profile is Profile.ENIGMA_2:
self._update_reference_entry()
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_cancel(self, item):
self._dialog.destroy()
def on_save(self, item):
self.on_url_changed(self._url_entry)
if not is_data_correct(self._digit_elems) or self._url_entry.get_name() == _DIGIT_ENTRY_NAME:
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
self.save_enigma2_data() if self._profile is Profile.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._profile is Profile.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) < 12:
return
self._stream_type_combobox.set_active(0 if StreamType(data[0].strip()) is StreamType.DVB_TS else 1)
self._srv_type_entry.set_text(data[2])
self._sid_entry.set_text(str(int(data[3], 16)))
self._tr_id_entry.set_text(str(int(data[4], 16)))
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(data[10].replace("%3a", ":"))
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._profile is Profile.ENIGMA_2:
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 1 if self._stream_type_combobox.get_active() == 0 else 4097
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 = urlparse(entry.get_text())
entry.set_name("GtkEntry" if all([url.scheme, url.netloc, url.path]) else _DIGIT_ENTRY_NAME)
def on_stream_type_changed(self, item):
self._update_reference_entry()
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()),
self._url_entry.get_text().replace(":", "%3a"),
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), {2: name, 7: fav_id})
else:
aggr = [None] * 10
s_type = BqServiceType.IPTV.name
srv = (None, None, name, None, None, s_type, None, fav_id, None)
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)
class SearchUnavailableDialog:
def __init__(self, transient, model, fav_bouquet, iptv_rows, profile):
handlers = {"on_search_unavailable_close": self.on_close}
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._profile = profile
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._profile))
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()
self._dialog.destroy()
return self._to_delete if response not in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT) else False
@run_idle
def on_close(self, item=None, event=None):
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, profile):
handlers = {"on_apply": self.on_apply,
"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}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "iptv.glade",
("iptv_list_configuration_dialog", "stream_type_liststore"))
builder.connect_signals(handlers)
self._rows = iptv_rows
self._services = services
self._bouquet = bouquet
self._profile = profile
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()
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 len(self._bouquet) != len(self._rows):
return
if self._profile is Profile.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()
for index, row in enumerate(self._rows):
fav_id = row[7]
data, sep, desc = fav_id.partition("http")
data = data.split(":")
if reset:
data[0] = " 4097"
data[2], data[3], data[4], data[5], data[6] = "10000"
else:
data[0] = " 4097" if self._stream_type_combobox.get_active() == 1 else "1"
data[2] = "1" if type_default else self._list_srv_type_entry.get_text()
data[3] = "{:X}".format(index) if sid_auto else "0"
data[4] = "0" if tid_default else "{:X}".format(int(self._list_tid_entry.get_text()))
data[5] = "0" if nid_default else "{:X}".format(int(self._list_nid_entry.get_text()))
data[6] = "0" if namespace_default else "{:X}".format(int(self._list_namespace_entry.get_text()))
data = ":".join(data)
new_fav_id = "{}{}{}".format(data, sep, desc)
row[7] = new_fav_id
self._bouquet[index] = new_fav_id
srv = self._services.pop(fav_id, None)
self._services[new_fav_id] = srv._replace(fav_id=new_fav_id)
self._info_bar.set_visible(True)
@run_idle
def update_reference(self):
if is_data_correct(self._digit_elems):
stream_type = "4097" if self._stream_type_combobox.get_active() == 1 else "1"
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()
if __name__ == "__main__":
pass

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,15 @@
""" This is helper module for ui """
from enum import Enum
import os
import shutil
from gi.repository import GdkPixbuf
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.properties import Profile
from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON
from .dialogs import show_dialog, DialogType, get_chooser_dialog, WaitDialog
# ***************** Markers *******************#
@@ -59,55 +51,100 @@ def edit_marker(view, bouquets, selected_bouquet, channels, parent_window):
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)
channels[new_fav_id] = old_ch._replace(service=response, fav_id=new_fav_id)
bq_services.pop(index)
bq_services.insert(index, new_fav_id)
# ***************** 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 paths:
# for correct down move!
if key in (Gdk.KEY_Down, Gdk.KEY_Page_Down, Gdk.KEY_KP_Page_Down):
paths = reversed(paths)
mod_length = len(model)
if mod_length == len(paths):
return
cursor_path = view.get_cursor()[0]
max_path = Gtk.TreePath.new_from_indices((mod_length,))
min_path = Gtk.TreePath.new_from_indices((0,))
is_tree_store = False
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)
if type(model) is Gtk.TreeStore:
parent_paths = list(filter(lambda p: p.get_depth() == 1, paths))
if parent_paths:
paths = parent_paths
min_path = model.get_path(model.get_iter_first())
view.collapse_all()
if mod_length == len(paths):
return
else:
if not is_some_level(paths):
return
parent_itr = model.iter_parent(model.get_iter(paths[0]))
parent_index = model.get_path(parent_itr)
children_num = model.iter_n_children(parent_itr)
if key in (Gdk.KEY_Page_Down, Gdk.KEY_KP_Page_Down, Gdk.KEY_End):
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 == Gdk.KEY_Up:
top_path = Gtk.TreePath(paths[0])
top_path.prev()
move_up(top_path, model, paths)
elif key == Gdk.KEY_Down:
down_path = Gtk.TreePath(paths[-1])
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 (Gdk.KEY_Page_Up, Gdk.KEY_KP_Page_Up, Gdk.KEY_Home):
move_up(min_path if is_tree_store else cursor_path, model, paths)
elif key in (Gdk.KEY_Page_Down, Gdk.KEY_KP_Page_Down, Gdk.KEY_End):
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):
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!")
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
# ***************** Rename *******************#
def rename(view, parent_window, target, fav_view=None, service_view=None, channels=None):
selection = get_selection(view, parent_window)
if not selection:
return
model, paths = selection
itr = model.get_iter(paths)
f_id = None
channel_name = None
@@ -143,12 +180,26 @@ def edit(view, parent_window, target, fav_view=None, service_view=None, channels
old_ch = channels.get(f_id, None)
if old_ch:
channels[f_id] = Service(*old_ch[0:3], channel_name, *old_ch[4:])
channels[f_id] = old_ch._replace(service=channel_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, "Please, select only one item!")
return
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:
@@ -166,21 +217,28 @@ def set_flags(flag, services_view, fav_view, channels, blacklist):
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]
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()))
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):
def update_fav_model(fav_view, services):
for row in get_base_model(fav_view.get_model()):
srv = services.get(row[7], None)
if srv:
row[3], row[4] = srv.locked, srv.hide
def set_lock(blacklist, services, model, paths, target, services_model):
col_num = 4 if target is ViewTarget.SERVICES else 3
locked = has_locked_hide(model, paths, col_num)
@@ -189,23 +247,29 @@ def set_lock(blacklist, channels, model, paths, target, services_model):
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)
srv = services.get(fav_id, None)
if srv:
bq_id = 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):
def update_services_model(ids, locked, services_model):
for srv in services_model:
if srv[18] in ids:
srv[4] = None if locked else LOCKED_ICON
yield True
def set_hide(services, model, paths):
col_num = 5
hide = has_locked_hide(model, paths, col_num)
@@ -223,18 +287,18 @@ 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:
@@ -242,9 +306,9 @@ def set_hide(channels, model, paths):
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:])
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):
@@ -285,15 +349,22 @@ 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))
def append_picons(picons, model):
def append_picons_data(pcs, mod):
for r in mod:
mod.set_value(mod.get_iter(r.path), 8, pcs.get(r[9], None))
yield True
app = append_picons_data(picons, model)
GLib.idle_add(lambda: next(app, False), priority=GLib.PRIORITY_LOW)
def assign_picon(target, srv_view, fav_view, transient, picons, options, services):
@@ -352,13 +423,17 @@ def remove_picon(target, srv_view, fav_view, picons, options):
fav_ids.append(model.get_value(itr, 18))
picon_ids.append(model.get_value(itr, 9))
else:
fav_ids.append(model.get_value(itr, 7))
srv_type, fav_id = model.get(itr, 5, 7)
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)
def remove(md, path, it):
if md.get_value(it, 7 if target is ViewTarget.SERVICES else 18) 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, 9))
fav_view.get_model().foreach(remove) if target is ViewTarget.SERVICES else get_base_model(
srv_view.get_model()).foreach(remove)
@@ -411,30 +486,65 @@ def get_picon_pixbuf(path):
return GdkPixbuf.Pixbuf.new_from_file_at_scale(filename=path, width=32, height=32, preserve_aspect_ratio=True)
# ***************** 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, profile, callback):
""" Auto-generate and append list of bouquets """
fav_id_index = 18
index = 6 if gen_type in (BqGenType.PACKAGE, BqGenType.EACH_PACKAGE) else 16 if gen_type in (
BqGenType.SAT, BqGenType.EACH_SAT) else 7
model, paths = view.get_selection().get_selected_rows()
model = get_base_model(model)
bq_type = BqType.BOUQUET.value if profile is Profile.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][:])
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], profile)
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}, profile, wait_dialog)
if paths:
view.scroll_to_cell(paths[0], None)
@run_task
def append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model, names, profile, wait_dialog=None):
bq_index = 0 if profile is Profile.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):
""" Updates value in text entry from chooser dialog """
response = show_dialog(dialog_type=DialogType.CHOOSER, transient=dialog, options=options)
@@ -451,5 +561,23 @@ def get_base_model(model):
return 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, profile):
""" Returns url from iptv type row """
data = row[7].split(":" if profile is Profile.ENIGMA_2 else "::")
if profile is Profile.ENIGMA_2:
data = list(filter(lambda x: "http" in x, data))
if data:
url = data[0]
return url.replace("%3a", ":") if profile is Profile.ENIGMA_2 else url
if __name__ == "__main__":
pass

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6,12 +6,12 @@ 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.ftp import upload_data, DownloadType
from app.tools.picons import PiconsParser, parse_providers, Provider, convert_to
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
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, TV_ICON
from .dialogs import show_dialog, DialogType, get_message
from .main_helper import update_entry_data, append_text_to_tview
class PiconsDialog:
@@ -21,7 +21,6 @@ class PiconsDialog:
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,
@@ -33,12 +32,15 @@ class PiconsDialog:
"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}
"on_position_edited": self.on_position_edited,
"on_notebook_switch_page": self.on_notebook_switch_page,
"on_convert": self.on_convert}
builder = Gtk.Builder()
builder.add_objects_from_file(UI_RESOURCES_PATH + "picons_dialog.glade",
("picons_dialog", "receive_image", "providers_list_store"))
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_from_file(UI_RESOURCES_PATH + "picons_dialog.glade")
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")
@@ -52,8 +54,12 @@ class PiconsDialog:
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._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._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")
@@ -64,13 +70,19 @@ class PiconsDialog:
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._properties = options.get(profile.value)
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._ip_entry.set_text(self._properties.get("host", ""))
self._picons_entry.set_text(self._properties.get("picons_path", ""))
self._picons_path = self._properties.get("picons_dir_path", "")
self._picons_dir_entry.set_text(self._picons_path)
self._enigma2_picons_path = self._picons_path
if profile is Profile.NEUTRINO_MP:
self._enigma2_picons_path = options.get(Profile.ENIGMA_2.value).get("picons_dir_path", "")
if not len(self._picon_ids) and self._profile is Profile.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)
def show(self):
self._dialog.run()
@@ -85,17 +97,18 @@ class PiconsDialog:
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.update_receive_button_state()
self.append_providers(url, model)
@run_task
def append_providers(self, url, model):
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))
model.append((self.get_pixbuf(p[0]) if p[0] else TV_ICON, p.name, p.pos, p.url, p.on_id, p.selected))
self.update_receive_button_state()
def get_pixbuf(self, img_url):
@@ -115,14 +128,21 @@ class PiconsDialog:
self._terminate = False
self._expander.set_expanded(True)
for prv in self.get_selected_providers():
providers = self.get_selected_providers()
for prv in providers:
if not prv[2] and prv[2][:-2].isdigit():
self.show_info_message(
get_message("Specify the correct position value for the provider!"), Gtk.MessageType.ERROR)
return
for prv in 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.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
self._current_process = subprocess.Popen(["wget", "-pkP", self._TMP_DIR, url],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
@@ -134,7 +154,8 @@ class PiconsDialog:
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)
if not self._terminate:
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
def write_to_buffer(self, fd, condition):
if condition == GLib.IO_IN:
@@ -146,26 +167,24 @@ class PiconsDialog:
@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)
append_text_to_tview(char, self._text_view)
def resize(self, path):
if self._resize_no_radio_button.get_active():
return
self.show_info_message("Resizing...", Gtk.MessageType.INFO)
self.show_info_message(get_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()
try:
self._current_process = subprocess.Popen(command, universal_newlines=True, cwd=path)
self._current_process.wait()
except FileNotFoundError as e:
self.show_info_message("Conversion error. " + str(e), Gtk.MessageType.ERROR)
self.on_cancel()
@run_task
def on_cancel(self, item):
def on_cancel(self, item=None):
if self._current_process:
self._terminate = True
self._current_process.terminate()
@@ -180,7 +199,7 @@ class PiconsDialog:
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
self.show_info_message("Please, wait...", Gtk.MessageType.INFO)
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
self.upload_picons()
@run_task
@@ -189,10 +208,13 @@ class PiconsDialog:
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))
try:
upload_data(properties=self._properties,
download_type=DownloadType.PICONS,
profile=self._profile,
callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO))
except OSError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@@ -215,15 +237,43 @@ class PiconsDialog:
def on_url_changed(self, entry):
suit = self._PATTERN.search(entry.get_text())
entry.set_name("GtkEntry" if suit else "digit-entry")
self._load_providers_tool_button.set_sensitive(suit if suit else False)
self._load_providers_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 on_notebook_switch_page(self, nb, box, tab_num):
self._load_providers_button.set_visible(not tab_num)
self._receive_button.set_visible(not tab_num)
self._convert_button.set_visible(tab_num)
self._send_button.set_visible(not tab_num)
if self._enigma2_path_button.get_filename() is None:
self._enigma2_path_button.set_current_folder(self._enigma2_picons_path)
@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,
profile=Profile.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):
self._receive_tool_button.set_sensitive(len(self.get_selected_providers()) > 0)
self._receive_button.set_sensitive(len(self.get_selected_providers()) > 0)
def get_selected_providers(self):
""" returns selected providers """

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,22 @@
import re
import time
import concurrent.futures
from math import fabs
from app.commons import run_idle
from app.commons import run_idle, run_task
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.tools.satellites import SatellitesParser, SatelliteSource
from .search import SearchProvider
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, MOVE_KEYS
from .dialogs import show_dialog, DialogType, WaitDialog
from .main_helper import move_items, scroll_to, append_text_to_tview, get_base_model
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):
@@ -26,6 +26,8 @@ class SatellitesDialog:
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,
@@ -33,27 +35,27 @@ class SatellitesDialog:
"on_transponder_add": self.on_transponder_add,
"on_edit": self.on_edit,
"on_key_release": self.on_key_release,
"on_popover_release": self.on_popover_release,
"on_row_activated": self.on_row_activated,
"on_resize": self.on_resize,
"on_quit": self.on_quit}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
("satellites_editor_dialog", "satellites_tree_store",
"popup_menu", "add_popup_menu", "add_menu_icon"))
("satellites_editor_window", "satellites_tree_store", "popup_menu",
"left_header_menu", "add_header_popover_menu"))
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._dialog.get_content_area().set_border_width(0) # The width of the border around the app dialog area!
self._sat_view = builder.get_object("satellites_editor_tree_view")
self._wait_dialog = WaitDialog(self._window)
# Setting the last size of the dialog window if it was saved
window_size = self._options.get("sat_editor_window_size", None)
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"),
@@ -61,38 +63,41 @@ class SatellitesDialog:
6: builder.get_object("mod_store")}
self.on_satellites_list_load(self._sat_view.get_model())
def run(self):
self._dialog.run()
def destroy(self):
self._dialog.destroy()
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()
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)
response = self.get_file_dialog_response(Gtk.FileChooserAction.OPEN)
if response == Gtk.ResponseType.CANCEL:
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)
def get_file_dialog_response(self, action: Gtk.FileChooserAction):
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._window,
options=self._options,
action_type=action,
file_filter=file_filter)
return response
@staticmethod
def on_row_activated(view, path, column):
if view.row_expanded(path):
@@ -123,29 +128,33 @@ class SatellitesDialog:
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 == Gdk.KEY_Left or key == Gdk.KEY_Right:
view.do_unselect_all(view)
def on_popover_release(self, menu, event):
menu.hide()
@run_idle
def on_satellites_list_load(self, model):
""" Load satellites data into model """
try:
self._wait_dialog.show()
satellites = get_satellites(self._data_path)
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!")
else:
model.clear()
self.append_data(model, satellites)
finally:
self._wait_dialog.hide()
@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)
def on_add(self, view):
""" Common adding """
@@ -174,7 +183,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 +204,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 +257,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 +267,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 +281,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._sat_view.get_model()).show()
@staticmethod
def parse_data(model, path, itr, sats):
if model.iter_has_child(itr):
@@ -296,6 +313,8 @@ class SatellitesDialog:
menu.popup(None, None, None, None, event.button, event.time)
# ***************** Transponder dialog *******************#
class TransponderDialog:
""" Shows dialog for adding or edit transponder """
@@ -304,10 +323,9 @@ class TransponderDialog:
handlers = {"on_entry_changed": self.on_entry_changed}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
("transponder_dialog",
"pol_store", "fec_store",
"mod_store", "system_store",
("transponder_dialog", "pol_store", "fec_store", "mod_store", "system_store",
"pls_mode_store"))
builder.connect_signals(handlers)
@@ -382,11 +400,14 @@ 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.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
("satellite_dialog", "side_store", "pos_adjustment"))
@@ -422,5 +443,265 @@ class SatelliteDialog:
return Satellite(name=name, flags="0", position=pos, transponders=None)
# ***************** Satellite update dialog *******************#
class SatellitesUpdateDialog:
""" Dialog for update satellites over internet """
def __init__(self, transient, main_model):
handlers = {"on_update_satellites_list": self.on_update_satellites_list,
"on_receive_satellites_list": self.on_receive_satellites_list,
"on_cancel_receive": self.on_cancel_receive,
"on_selected_toggled": self.on_selected_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": self.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}
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"))
builder.connect_signals(handlers)
self._window = builder.get_object("satellites_update_window")
self._window.set_transient_for(transient)
self._main_model = main_model
# self._dialog.get_content_area().set_border_width(0)
self._sat_view = builder.get_object("sat_update_tree_view")
self._source_box = builder.get_object("source_combo_box")
self._sat_update_expander = builder.get_object("sat_update_expander")
self._text_view = builder.get_object("text_view")
self._receive_button = builder.get_object("receive_sat_list_tool_button")
self._sat_update_info_bar = builder.get_object("sat_update_info_bar")
self._info_bar_message_label = builder.get_object("info_bar_message_label")
# 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"))
self._download_task = False
self._parser = None
def show(self):
self._window.show()
@run_idle
def on_update_satellites_list(self, item):
if self._download_task:
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return
model = get_base_model(self._sat_view.get_model())
model.clear()
self._download_task = True
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._download_task = 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_satellites_list(self, item):
if self._download_task:
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return
self.receive_satellites()
@run_task
def receive_satellites(self):
self._download_task = True
self.update_expander()
model = self._sat_view.get_model()
start = time.time()
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
text = "Processing: {}\n"
sats = []
appender = self.append_output()
next(appender)
futures = {executor.submit(self._parser.get_satellite, sat[:-1]): sat for sat in [r for r in model if r[4]]}
for future in concurrent.futures.as_completed(futures):
if not self._download_task:
self._download_task = True
executor.shutdown()
appender.send("\nCanceled\n")
appender.close()
self._download_task = False
return
data = future.result()
appender.send(text.format(data[0]))
sats.append(data)
appender.send("-" * 75 + "\n")
appender.send("Consumed : {:0.0f}s, {} satellites received.".format(start - time.time(), len(sats)))
appender.close()
sats = {s[2]: s for s in sats} # key = position, v = satellite
for row in self._main_model:
pos = row[-1]
if pos in sats:
sat = sats.pop(pos)
itr = row.iter
self.update_satellite(itr, row, sat)
for sat in sats.values():
append_satellite(self._main_model, sat)
self._download_task = False
@run_idle
def update_expander(self):
self._sat_update_expander.set_expanded(True)
self._text_view.get_buffer().set_text("", 0)
@run_idle
def update_satellite(self, itr, row, sat):
if self._main_model.iter_has_child(itr):
children = row.iterchildren()
for ch in children:
self._main_model.remove(ch.iter)
for tr in sat[3]:
self._main_model.append(itr, ["Transponder:", *tr, None, None])
def append_output(self):
@run_idle
def append(t):
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_selected_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)
@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, iter, 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(iter, 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_popup_menu(self, 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)
def on_quit(self, window, event):
self._download_task = False
# ***************** 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

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

@@ -0,0 +1,55 @@
""" 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()
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,626 @@
import re
import os
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
from app.properties import Profile
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, TEXT_DOMAIN, CODED_ICON
from .dialogs import show_dialog, DialogType, Action
from .main_helper import get_base_model
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, options, srv_view, fav_view, services, bouquets, 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_file(UI_RESOURCES_PATH + "service_details_dialog.glade")
builder.connect_signals(handlers)
self._builder = builder
self._dialog = builder.get_object("service_details_dialog")
self._dialog.set_transient_for(transient)
self._profile = Profile(options["profile"])
self._satellites_xml_path = options.get(self._profile.value)["data_dir_path"] + "satellites.xml"
self._picons_dir_path = options.get(self._profile.value)["picons_dir_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._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-z]{4})(,C:[0-9a-z]{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._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)
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][:])
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
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)
self.set_sat_positions(srv.pos)
if self._profile is Profile.ENIGMA_2:
self.init_enigma2_service_data(srv)
self.init_enigma2_transponder_data(srv)
elif self._profile is Profile.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(":")
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]))
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)))
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[5]).name)
# Should be called last to properly initialize the reference
self._srv_type_entry.set_text(data[4])
# ***************** Init Neutrino data *********************#
def init_neutrino_data(self, srv):
tr_data = srv.transponder.split(":")
self._transponder_id_entry.set_text(str(int(tr_data[0], 16)))
self._network_id_entry.set_text(str(int(tr_data[1], 16)))
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[3]).name)
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)
self._builder.get_object("tr_grid").remove_column(7)
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_edit(self):
fav_id, data_id = self.get_srv_data()
# transponder
transponder = self._old_service.transponder
if self._tr_edit_switch.get_active():
transponder = self.get_transponder_data()
if self._transponder_services_iters:
self.update_transponder_services(transponder)
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)
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):
model.set(row.iter, {1: new_service.coded,
2: new_service.service,
3: new_service.locked,
4: new_service.hide,
5: new_service.service_type,
6: new_service.pos,
7: new_service.fav_id,
8: new_service.picon})
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
def on_new(self):
service = self.get_service(*self.get_srv_data(), self.get_transponder_data())
print(service)
show_dialog(DialogType.ERROR, transient=self._dialog, text="Not implemented yet!")
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="s",
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._profile is Profile.ENIGMA_2:
return self.get_enigma2_flags()
elif self._profile is Profile.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._profile is Profile.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._profile is Profile.NEUTRINO_MP:
fav_id = self._NEUTRINO_FAV_ID.format(tr_id, net_id, ssid)
return fav_id, self._old_service.data_id
def get_transponder_values(self):
freq = self._freq_entry.get_text()
rate = self._rate_entry.get_text()
pol = self._pol_combo_box.get_active_id()
fec = self._fec_combo_box.get_active_id()
system = self._sys_combo_box.get_active_id()
pos = str(round(self._sat_pos_button.get_value(), 1))
return freq, rate, pol, fec, system, pos
def get_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._profile is Profile.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._profile is Profile.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 update_transponder_services(self, transponder):
for itr in self._transponder_services_iters:
srv = self._current_model[itr][:]
srv[-9], srv[-8], srv[-7], srv[-6], srv[-5], srv[-4] = self.get_transponder_values()
srv[-1] = 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: Gtk.ComboBox, 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: Gtk.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")
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._profile is Profile.NEUTRINO_MP):
return
self.update_reference_entry()
def update_reference_entry(self):
srv_type = 0 if self._srv_type_entry.get_text() == "2" else 1
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._profile is Profile.ENIGMA_2:
on_id = int(self._namespace_entry.get_text())
ref = "1:0:{}:{: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))
class TransponderServicesDialog:
def __init__(self, transient, model, transponder, tr_iters):
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "service_details_dialog.glade",
("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)
def append_services(self, model, transponder, tr_iters):
for row in model:
if row[-1] == transponder:
self._srv_model.append((row[3], row[6], row[7], row[10], row[11], row[16]))
tr_iters.append(model.get_iter(row.path))
def show(self):
response = self._dialog.run()
self._dialog.destroy()
return response
if __name__ == "__main__":
pass

View File

@@ -0,0 +1,778 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1
The MIT License (MIT)
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.
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 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkImage" id="reset_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-revert-to-saved</property>
</object>
<object class="GtkAdjustment" id="telnet_timeout_adjustment">
<property name="lower">1</property>
<property name="upper">11</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkDialog" id="settings_dialog">
<property name="can_focus">False</property>
<property name="resizable">False</property>
<property name="modal">True</property>
<property name="destroy_with_parent">True</property>
<property name="icon_name">preferences-desktop</property>
<property name="type_hint">dialog</property>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<child type="titlebar">
<object class="GtkHeaderBar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Options</property>
<property name="spacing">2</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
</object>
</child>
<action-widgets>
<action-widget response="-6">cancel_button</action-widget>
<action-widget response="-5">ok_button</action-widget>
</action-widgets>
<child internal-child="vbox">
<object class="GtkBox" id="main_box">
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="halign">end</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="cancel_button">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="apply_button">
<property name="label">gtk-apply</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="apply_settings" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="ok_button">
<property name="label">gtk-ok</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
</object>
<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">False</property>
<property name="position">6</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="network_settings_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkNotebook" id="notebook">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_left">2</property>
<property name="margin_right">2</property>
<property name="margin_top">2</property>
<property name="margin_bottom">2</property>
<child>
<object class="GtkGrid" id="ftp_settings_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="row_spacing">2</property>
<property name="column_spacing">2</property>
<property name="column_homogeneous">True</property>
<child>
<object class="GtkLabel" id="label4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Host:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="host_field">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="text">127.0.0.1</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="label13">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Login:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label14">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Password:</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="port_field">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="text">21</property>
<property name="primary_icon_name">network-workgroup-symbolic</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label15">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Port:</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="login_field">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="text">root</property>
<property name="primary_icon_name">avatar-default-symbolic</property>
<property name="primary_icon_activatable">False</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="password_field">
<property name="visible">True</property>
<property name="can_focus">True</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="primary_icon_activatable">False</property>
<property name="input_purpose">password</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">3</property>
</packing>
</child>
</object>
</child>
<child type="tab">
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">FTP</property>
</object>
<packing>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="telnet_settings_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="row_spacing">2</property>
<property name="column_spacing">2</property>
<property name="column_homogeneous">True</property>
<child>
<object class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Port:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="telnet_password_field">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">emblem-readonly</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="telnet_login_field">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">avatar-default-symbolic</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label16">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Login:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label17">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Password:</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="telnet_port_field">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="text" translatable="yes">23</property>
<property name="primary_icon_name">network-workgroup-symbolic</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label19">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Timeout:</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="telnet_timeout_spin_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="tooltip_text" translatable="yes">Timeout between commands in seconds</property>
<property name="max_length">2</property>
<property name="primary_icon_name">alarm-symbolic</property>
<property name="input_purpose">number</property>
<property name="adjustment">telnet_timeout_adjustment</property>
<property name="numeric">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel" id="label2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Telnet</property>
</object>
<packing>
<property name="position">1</property>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child type="tab">
<placeholder/>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Network settings:</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="GtkBox" id="settings_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">5</property>
<property name="spacing">2</property>
<child>
<object class="GtkFrame" id="stb_paths_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkGrid" id="stb_dirs_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="row_spacing">2</property>
<property name="column_homogeneous">True</property>
<child>
<object class="GtkLabel" id="label5">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Services and Bouquets files:</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="services_field">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="text">/etc/enigma2/</property>
<property name="primary_icon_stock">gtk-edit</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label6">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">User bouquet files:</property>
<property name="xalign">2.2351741291171123e-10</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="user_bouquet_field">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="text">/etc/enigma2/</property>
<property name="primary_icon_stock">gtk-edit</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label7">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Satellites.xml file:</property>
<property name="xalign">0.019999999552965164</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="satellites_xml_field">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="text">/etc/tuxbox/</property>
<property name="primary_icon_stock">gtk-edit</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">5</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label20">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Picons:</property>
<property name="xalign">2.2351741291171123e-10</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">6</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="picons_field">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="text">/usr/share/enigma2/picon</property>
<property name="primary_icon_stock">gtk-edit</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">7</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="stb_paths_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">STB file paths:</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="settings_profile_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0.20000000298023224</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkBox" id="settings_profile_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkRadioButton" id="enigma_radio_button">
<property name="label">Enigma2 </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">neutrino_radio_button</property>
<signal name="toggled" 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="GtkCheckButton" id="support_ver5_check_button">
<property name="label" translatable="yes">Ver. 5 support
(experimental)</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkSeparator" id="settings_prof_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">2</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="neutrino_radio_button">
<property name="label">Neutrino-MP
(experimental)</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">enigma_radio_button</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkSeparator" id="settings_prof_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="padding">2</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkButton" id="reset_button">
<property name="label" translatable="yes">Reset profile</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="margin_top">3</property>
<property name="image">reset_image</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_reset" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">5</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="label12">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Active profile:</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="local_file_paths_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="margin_top">5</property>
<property name="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkGrid" id="local_paths_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="row_spacing">2</property>
<property name="column_homogeneous">True</property>
<child>
<object class="GtkEntry" id="data_dir_field">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="text">/data</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="secondary_icon_name">folder-open-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_tooltip_text" translatable="yes">Select</property>
<property name="secondary_icon_tooltip_markup" translatable="yes">Select</property>
<signal name="icon-press" handler="on_data_dir_field_icon_press" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label8">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Data path:</property>
<property name="lines">0</property>
<property name="xalign">0.019999999552965164</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label18">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Picons path:</property>
<property name="xalign">0.019999999552965164</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="picons_dir_field">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="text">/data/picons</property>
<property name="primary_icon_stock">gtk-edit</property>
<property name="secondary_icon_name">folder-open-symbolic</property>
<signal name="icon-press" handler="on_picons_dir_field_icon_press" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Local file paths:</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
</object>
</child>
<action-widgets>
<action-widget response="-6">cancel_button</action-widget>
<action-widget response="-5">ok_button</action-widget>
</action-widgets>
</object>
</interface>

View File

@@ -1,5 +1,5 @@
from app.properties import write_config, Profile, get_default_settings
from . import Gtk, UI_RESOURCES_PATH
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN
from .main_helper import update_entry_data
@@ -8,6 +8,7 @@ def show_settings_dialog(transient, options):
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,
@@ -16,8 +17,8 @@ class SettingsDialog:
"apply_settings": self.apply_settings}
builder = Gtk.Builder()
builder.add_objects_from_file(UI_RESOURCES_PATH + "dialogs.glade",
("settings_dialog", "telnet_timeout_adjustment"))
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_from_file(UI_RESOURCES_PATH + "settings_dialog.glade")
builder.connect_signals(handlers)
self._dialog = builder.get_object("settings_dialog")
@@ -38,11 +39,14 @@ class SettingsDialog:
self._picons_dir_field = builder.get_object("picons_dir_field")
self._enigma_radio_button = builder.get_object("enigma_radio_button")
self._neutrino_radio_button = builder.get_object("neutrino_radio_button")
self._support_ver5_check_button = builder.get_object("support_ver5_check_button")
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)
profile = Profile(self._active_profile)
self._neutrino_radio_button.set_active(profile is Profile.NEUTRINO_MP)
self._support_ver5_check_button.set_sensitive(profile is not Profile.NEUTRINO_MP)
def show(self):
response = self._dialog.run()
@@ -60,7 +64,9 @@ class SettingsDialog:
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)
profile = Profile.ENIGMA_2 if self._enigma_radio_button.get_active() else Profile.NEUTRINO_MP
self.set_profile(profile)
self._support_ver5_check_button.set_sensitive(profile is Profile.ENIGMA_2)
def set_profile(self, profile):
self._active_profile = profile.value
@@ -93,11 +99,13 @@ class SettingsDialog:
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", ""))
if Profile(self._active_profile) is Profile.ENIGMA_2:
self._support_ver5_check_button.set_active(options.get("v5_support", False))
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
profile = Profile.ENIGMA_2 if self._enigma_radio_button.get_active() else Profile.NEUTRINO_MP
self._active_profile = profile.value
self._options["profile"] = self._active_profile
options = self._options.get(self._active_profile)
options["host"] = self._host_field.get_text()
options["port"] = self._port_field.get_text()
@@ -113,6 +121,8 @@ class SettingsDialog:
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 profile is Profile.ENIGMA_2:
options["v5_support"] = self._support_ver5_check_button.get_active()
if __name__ == "__main__":

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

@@ -0,0 +1,52 @@
import locale
import os
import gi
from enum import Enum
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/"
# translation
TEXT_DOMAIN = "demon-editor"
if UI_RESOURCES_PATH == "app/ui/":
LANG_DIR = UI_RESOURCES_PATH + "lang"
locale.bindtextdomain(TEXT_DOMAIN, UI_RESOURCES_PATH + "lang")
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("changes-prevent-symbolic", 16, 0) if theme.lookup_icon(
"system-lock-screen", 16, 0) else _IMAGE_MISSING
HIDE_ICON = theme.load_icon("go-jump", 16, 0) if theme.lookup_icon("go-jump", 16, 0) else _IMAGE_MISSING
TV_ICON = theme.load_icon("tv-symbolic", 16, 0) if theme.lookup_icon("tv-symbolic", 16, 0) else _IMAGE_MISSING
IPTV_ICON = theme.load_icon("emblem-shared", 16, 0) if theme.load_icon("emblem-shared", 16, 0) else None
# keys for move in lists
MOVE_KEYS = (Gdk.KEY_Up, Gdk.KEY_Page_Up, Gdk.KEY_Down, Gdk.KEY_Page_Down, Gdk.KEY_Home, Gdk.KEY_End,
Gdk.KEY_KP_Page_Up, Gdk.KEY_KP_Page_Down) # KEY_KP_Page_Up(Down) for laptop!
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
if __name__ == "__main__":
pass

View File

@@ -1,11 +1,11 @@
#!/bin/env bash
VER="0.2.3_Pre-alpha"
#!/bin/bash
VER="0.4.0_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
rsync --exclude=app/ui/lang -arv app $DEB_PATH
cp -Rv start.py $DEB_PATH
cd dist

View File

@@ -1,9 +1,9 @@
Package: DemonEditor
Version: 0.2.3-Pre-alpha
Version: 0.4.0-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
Description: Enigma2 channel and satellites list editor

View File

@@ -8,4 +8,4 @@ Exec=/usr/bin/demoneditor.sh
Terminal=false
Type=Application
Categories=Utility;Application;
StartupNotify=true
StartupNotify=false

View File

@@ -1,25 +1,36 @@
# DemonEditor
Enigma2 channel and satellites list editor for GNU/Linux.
## 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!
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, Home, End, S, T, E, L, H, Space; Insert, Delete, F2, Enter, P.**
* **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** - edit.
* **Ctrl + R, F2** - rename.
* **Ctrl + S, T** in Satellites edit tool for create satellite or transponder.
* **Ctrl + L** - parental lock.
* **Ctrl + H** - hide/skip.
* **P** - enable/disable preview mode for IPTV in the bouquet list.
* **Enter** - start play IPTV or other stream in the bouquet list.
* **Space** - select/deselect.
* **Left/Right** - remove selection.
* **Ctrl + Up, Down, PageUp, PageDown, Home, End** - move selected items in the list.
### Extra:
* Multiple selections in lists only with Space key (as in file managers).
* Ability to import IPTV into bouquet (Neutrino WEBTV) from m3u files.
* Ability to download picons and update satellites (transponders) from web.
* Preview (playing) IPTV or other streams directly from the bouquet list(should be installed VLC).
### Minimum requirements:
Python >= 3.5.2 and GTK+ 3 with PyGObject bindings.
#### Note.
To create a simple debian package, you can use the build-deb.sh
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!**
Terrestrial and cable channels at the moment are not supported!

Binary file not shown.

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

BIN
po/ru/demon-editor.mo Normal file

Binary file not shown.

576
po/ru/demon-editor.po Normal file
View File

@@ -0,0 +1,576 @@
# Copyright (C) 2018 Dmitriy Yefremov
# This file is distributed under the MIT license.
# Dmitriy Yefremov , 2018.
msgid ""
msgstr ""
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
# Main
msgid "Service"
msgstr "Сервис"
msgid "Package"
msgstr "Пакет"
msgid "Type"
msgstr "Тип"
msgid "Picon"
msgstr "Пикон"
msgid "Freq"
msgstr "Частота"
msgid "Rate"
msgstr "Сим. скорость"
msgid "Pol"
msgstr "Пол."
msgid "System"
msgstr "Система"
msgid "Pos"
msgstr "Поз."
msgid "Num"
msgstr "№"
msgid "Current IP:"
msgstr "Текущий IP:"
msgid "Assign"
msgstr "Привязать"
msgid "Bouquet details"
msgstr "Сервисы букета"
msgid "Bouquets"
msgstr "Букеты"
msgid "Copy"
msgstr "Копировать"
msgid "Copy reference"
msgstr "Копировать ссылку"
msgid "Download"
msgstr "Загрузить"
msgid "Edit"
msgstr "Изменить"
msgid "Edit "
msgstr "Изменить"
msgid "Edit mаrker text"
msgstr "Изменить текст маркера"
msgid "FTP-transfer"
msgstr "Передача установок по FTP"
msgid "Global search"
msgstr "Глобальный поиск"
msgid "Hide"
msgstr "Пропустить"
msgid "Hide/Skip On/Off Ctrl + H"
msgstr "Скрыть/Пропустить Вкл/Выкл Ctrl + H"
msgid "Add IPTV or stream service"
msgstr "Добавить IPTV или поток"
msgid "Import m3u"
msgstr "Импортировать m3u"
msgid "Import m3u file"
msgstr "Импортировать файл m3u"
msgid "List configuration"
msgstr "Конфигурация списка"
msgid "Rename for this bouquet"
msgstr "Переименовать для букета"
msgid "Set default name"
msgstr "Имя по умолчанию"
msgid "Insert marker"
msgstr "Вставить маркер"
msgid "Locate in services"
msgstr "Найти в списке сервисов"
msgid "Locked"
msgstr "Заблокирован"
msgid "Move"
msgstr "Переместить"
msgid "New"
msgstr "Новый"
msgid "New bouquet"
msgstr "Новый букет"
msgid "Create bouquet"
msgstr "Создать букет"
msgid "For current satellite"
msgstr "Для текущего спутника"
msgid "For current package"
msgstr "Для текущего пакета"
msgid "For current type"
msgstr "Для текущего типа"
msgid "For each satellite"
msgstr "Для каждого спутника"
msgid "For each package"
msgstr "Для каждого пакета"
msgid "For each type"
msgstr "Для каждого типа"
msgid "Open"
msgstr "Открыть"
msgid "Parent lock On/Off Ctrl + L"
msgstr "Родительский замок Вкл/Выкл Ctrl + L"
msgid "Picons"
msgstr "Пиконы"
msgid "Picons downloader"
msgstr "Загрузчик пиконов"
msgid "Satellites downloader"
msgstr "Загрузчик спутников"
msgid "Remove"
msgstr "Удалить"
msgid "Remove all unavailable"
msgstr "Удалить все недоступные"
msgid "Satellites editor"
msgstr "Редактор спутников"
msgid "Save"
msgstr "Сохранить"
msgid "Search"
msgstr "Поиск"
msgid "Services"
msgstr "Сервисы"
msgid "Services filter"
msgstr "Фильтр сервисов"
msgid "Settings"
msgstr "Настройки"
msgid "Up"
msgstr "Переместить вверх"
msgid "Down"
msgstr "Переместить вниз"
msgid "Active profile:"
msgstr "Активный профиль:"
msgid "All"
msgstr "Все"
msgid "Are you sure?"
msgstr "Вы уверены?"
msgid "Current data path:"
msgstr "Текущий путь к данным:"
msgid "Data:"
msgstr "Данные:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux"
msgstr "Редактор списка каналов и спутников Enigma2\n для GNU/Linux"
msgid "Host:"
msgstr "Адрес ресивера:"
msgid "Loading data..."
msgstr "Загрузка данных..."
msgid "Receive"
msgstr "Получить"
msgid "Receive files from receiver"
msgstr "Получить файлы из ресивера"
msgid "Receiver IP:"
msgstr "IP адрес ресивера:"
msgid "Remove unused bouquets"
msgstr "Удалить неиспользуемые букеты"
msgid "Reset profile"
msgstr "Сброс профиля"
msgid "Satellites"
msgstr "Спутники"
msgid "Satellites.xml file:"
msgstr "Файл satellites.xml:"
msgid "Selected"
msgstr "Выбор"
msgid "Send"
msgstr "Отправить"
msgid "Send files to receiver"
msgstr "Отправить файлы в ресивер"
msgid "Services and Bouquets files:"
msgstr "Файлы сервисов и букетов:"
msgid "User bouquet files:"
msgstr "Файлы букетов:"
msgid "Extra:"
msgstr "Дополнительно:"
# Filter bar
msgid "Only free"
msgstr "Только открытые"
msgid "All positions"
msgstr "Все позиции"
msgid "All types"
msgstr "Все типы"
# Streams player
msgid "Play"
msgstr "Воспроизведение"
msgid "Stop playback"
msgstr "Останов"
msgid "Previous stream in the list"
msgstr "Предыдущий поток в списке"
msgid "Next stream in the list"
msgstr "Следующий поток в списке"
msgid "Toggle in fullscreen"
msgstr "Показать во весь экран"
msgid "Close"
msgstr "Закрыть"
# Picons dialog
msgid "Load providers"
msgstr "Загрузить провайдеры"
msgid "Providers"
msgstr "Провайдеры"
msgid "Receive picons"
msgstr "Загрузить пиконы"
msgid "Picons name format:"
msgstr "Формат имени пиконов:"
msgid "Resize:"
msgstr "Обрезать:"
msgid "Current picons path:"
msgstr "Текущий путь к пиконам:"
msgid "Receiver picons path:"
msgstr "Путь к пиконам ресивера:"
msgid "Picons download tool"
msgstr "Загрузчик пиконов"
msgid "Transfer to receiver"
msgstr "Загрузить в ресивер"
msgid "Downloader"
msgstr "Загрузчик"
msgid "Converter"
msgstr "Конвертер"
msgid "Convert"
msgstr "Конвертировать"
msgid "Path to save:"
msgstr "Путь для сохранения:"
msgid "Path to Enigma2 picons:"
msgstr "Путь к пиконам формата Enigma2:"
msgid "Specify the correct position value for the provider!"
msgstr "Укажите правильное значение позиции для провайдера!"
msgid "Converter between name formats"
msgstr "Конвертер формата имен"
msgid "Receive picons for providers"
msgstr "Получение пиконов для провайдеров"
msgid "Load satellite providers."
msgstr "Загрузка провайдеров"
msgid "To automatically set the identifiers for picons,\nfirst load the required services list into the main application window."
msgstr "Для автоматического именования пиконов,\nзагрузите список необходимых сервисов в главное окно программы."
# Satellites editor
msgid "Satellites edit tool"
msgstr "Редактор спутников"
msgid "Add"
msgstr "Добавить"
msgid "Satellite"
msgstr "Спутник"
msgid "Transponder"
msgstr "Транспондер"
msgid "Satellite properties:"
msgstr "Параметры спутника:"
msgid "Transponder properties:"
msgstr "Параметры транспондера:"
msgid "Name"
msgstr "Имя"
msgid "Position"
msgstr "Позиция"
# Satellites update dialog
msgid "Satellites update"
msgstr "Обновление спутников"
msgid "Remove selection"
msgstr "Снять выделение"
# Service details dialog
msgid "Service data:"
msgstr "Данные сервиса:"
msgid "Transponder data:"
msgstr "Данные транспондера:"
msgid "Service data"
msgstr "Данные сервиса"
msgid "Transponder details"
msgstr "Данные транспондера"
msgid "Changes will be applied to all services of this transponder!\nContinue?"
msgstr "Изменения будут применены ко всем сервисам данного транспондера!\nПродолжить?"
msgid "Reference"
msgstr "Ссылка"
msgid "Namespace"
msgstr "Пр. имен"
msgid "Flags:"
msgstr "Флаги:"
msgid "Delays (ms):"
msgstr "Задержки (mc)"
msgid "Bitstream"
msgstr "Поток"
msgid "Description"
msgstr "Описание"
msgid "Source:"
msgstr "Источник:"
msgid "Cancel"
msgstr "Отмена"
msgid "Update"
msgstr "Обновить"
msgid "Filter"
msgstr "Фильтр"
msgid "Find"
msgstr "Поиск"
# IPTV dialog
msgid "Stream data"
msgstr "Данные потока"
# IPTV list configuration dialog
msgid "Starting values"
msgstr "Стартовые значения"
msgid "Reset to default"
msgstr "Сбросить по умолчанию"
msgid "IPTV streams list configuration"
msgstr "Конфигурация списка IPTV потоков"
#Settings dialog
msgid "Preferences"
msgstr "Настройки"
msgid "Profile:"
msgstr "Профиль:"
msgid "Timeout between commands in seconds"
msgstr "Пауза между коммандами в сек."
msgid "Timeout:"
msgstr "Тайм-аут:"
msgid "Login:"
msgstr "Логин:"
msgid "Options"
msgstr "Настройки"
msgid "Password:"
msgstr "Пароль:"
msgid "Picons:"
msgstr "Пиконы:"
msgid "Port:"
msgstr "Порт:"
msgid "Data path:"
msgstr "Путь к данным:"
msgid "Picons path:"
msgstr "Путь к пиконам:"
msgid "Network settings:"
msgstr "Настройки сети:"
msgid "STB file paths:"
msgstr "Пути к файлам ресивера:"
msgid "Local file paths:"
msgstr "Пути к локальным файлам:"
# Dialogs messages
msgid "Error. No bouquet is selected!"
msgstr "Ошибка. Не выбран букет!"
msgid "This item is not allowed to be removed!"
msgstr "Этот элемент не разрешен к удалению!"
msgid "This item is not allowed to edit!"
msgstr "Элемент не предназначен для редактирования!"
msgid "Please, download files from receiver or setup your path for read data!"
msgstr "Пожалуйста, загрузите файлы из приемника или настройте путь для чтения данных!"
msgid "Reading data error!"
msgstr "Ошибка чтения данных!"
msgid "No m3u file is selected!"
msgstr "Не выбран m3u файл!"
msgid "Not implemented yet!"
msgstr "Пока не реализовано!"
msgid "The text of marker is empty, please try again!"
msgstr "Текст маркера пуст, попробуйте еще!"
msgid "Please, select only one item!"
msgstr "Пожалуйста, выберите только один элемент!"
msgid "No png file is selected!"
msgstr "Не выбран png файл!"
msgid "No reference is present!"
msgstr "Ссылка не найдена!"
msgid "No selected item!"
msgstr "Не выбран элемент!"
msgid "The task is already running!"
msgstr "Задача уже запущена!"
msgid "Done!"
msgstr "Готово!"
msgid "Please, wait..."
msgstr "Пожалуйста, подождите..."
msgid "Resizing..."
msgstr "Изменение размера..."
msgid "Select paths!"
msgstr "Укажите пути!"
msgid "No satellite is selected!"
msgstr "Не выбран спутник!"
msgid "Please, select only one satellite!"
msgstr "Пожалуйста, выберите только один спутник!"
msgid "Please check your parameters and try again."
msgstr "Пожалуйста, проверте параметры и попробуйте снова!"
msgid "No satellites.xml file is selected!"
msgstr "Не выбран файл satellites.xml!"
msgid "Error. Verify the data!"
msgstr "Ошибка. Проверьте данные!"
msgid "Operation not allowed in this context!"
msgstr "Недопустимая операция в данном контексте!"
msgid "No VLC is found. Check that it is installed!"
msgstr "VLC не найден. Проверьте, что он установлен!"
# Search unavailable streams dialog
msgid "Please wait, streams testing in progress..."
msgstr "Подождите, идет тестирование потоков ..."
msgid "Found"
msgstr "Найдено"
msgid "unavailable streams."
msgstr "недоступных потоков."
msgid "No changes required!"
msgstr "Изменений не требуется!"
msgid "This list does not contains IPTV streams!"
msgstr "Текущий список не содержит потоков IPTV!"

5
repo/debian/changelog Normal file
View File

@@ -0,0 +1,5 @@
demon-editor (0.4.0-1~ppa1) bionic; urgency=low
* Initial release
-- Dmitriy Yefremov <dmitry.v.yefremov@gmail.com> Tue, 02 Oct 2018 12:41:40 +0300

1
repo/debian/compat Normal file
View File

@@ -0,0 +1 @@
10

12
repo/debian/control Normal file
View File

@@ -0,0 +1,12 @@
Source: demon-editor
Section: utils
Priority: optional
Maintainer: Dmitriy Yefremov <dmitry.v.yefremov@gmail.com>
Build-Depends: python3 (>= 3.5), debhelper (>= 10)
Standards-Version: 4.1.2
Package: demon-editor
Architecture: all
Depends: python3 (>= 3.5)
Description: Enigma2 channel and satellites list editor

26
repo/debian/copyright Normal file
View File

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

2
repo/debian/install Normal file
View File

@@ -0,0 +1,2 @@
usr/share /usr
usr/bin /usr

6
repo/debian/rules Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/make -f
export PYBUILD_NAME=demon-editor
%:
dh $@

View File

@@ -0,0 +1 @@
3.0 (native)

View File

@@ -0,0 +1 @@
extend-diff-ignore = "^[^/]*[.]egg-info/"