Compare commits

...

593 Commits
v.0.1 ... 0.4.5

Author SHA1 Message Date
DYefremov
3550f58603 update spanish, dutch and portuguese 2019-06-11 21:24:06 +03:00
DYefremov
729c85be77 fix type for radio bouquets 2019-06-11 21:03:51 +03:00
DYefremov
f8aee1b807 update russian 2019-06-08 16:04:47 +03:00
DYefremov
75d93f6a19 improved xml download for the epg dialog 2019-06-08 15:45:41 +03:00
DYefremov
4581cc7d4f upd README 2019-06-05 11:53:52 +03:00
DYefremov
2f8ea069e1 bouquet name for xml file when saving from the epg dialog 2019-06-04 13:31:54 +03:00
DYefremov
8afcec6b7e changed date format in the header 2019-06-04 13:06:02 +03:00
DYefremov
b5a9321c5c small refactoring of getting refs from xml 2019-06-04 01:22:26 +03:00
DYefremov
7ee781c39b lazy init of epg data 2019-06-03 15:47:04 +03:00
DYefremov
48f3c1a4d6 added export to m3u for neutrino 2019-05-30 15:56:04 +03:00
DYefremov
717bac6446 fix on open 2019-05-30 12:57:31 +03:00
DYefremov
fe1323f8cf deleted extra dialog 2019-05-30 11:12:22 +03:00
DYefremov
0f30d74edc changing header bar elements 2019-05-29 14:31:44 +03:00
DYefremov
0686c91a5d fix provider name for neutrino 2019-05-29 12:57:03 +03:00
DYefremov
a84090cda7 added support of coupled satellites 2019-05-27 22:17:29 +03:00
DYefremov
291b3aa289 minor appearance changes 2019-05-27 11:02:41 +03:00
DYefremov
dd92ffc9b1 fix getting some transponders 2019-05-27 00:17:38 +03:00
DYefremov
97a8f793c3 lazy loading of satellites list 2019-05-26 22:11:52 +03:00
DYefremov
3ad2e3d6b6 disable cache 2019-05-26 21:50:02 +03:00
DYefremov
7d6763ffb5 fix show iptv services in the info box of import dialog 2019-05-20 20:01:28 +03:00
DYefremov
6582be7a0d added arg for the start script 2019-05-19 14:22:43 +03:00
DYefremov
1e45621bd8 added data recovery if download error 2019-05-19 00:37:07 +03:00
DYefremov
61bcb85bbc global update settings from the download dialog 2019-05-14 22:12:36 +03:00
DYefremov
9eee9ac424 small refactoring of the init of dynamic elems 2019-05-13 14:42:23 +03:00
DYefremov
3f720afedc added command line params support 2019-05-12 16:26:58 +03:00
DYefremov
cd19c5fd9c optional logging 2019-05-12 16:26:19 +03:00
DYefremov
61ca2f3e8b minor changes for the input dialog 2019-05-11 13:27:46 +03:00
DYefremov
75fc7adc88 input dialog refactoring 2019-05-11 00:09:20 +03:00
DYefremov
3678a9d29d satellite dialogs refactoring 2019-05-10 14:42:32 +03:00
DYefremov
a5927dd2b6 service details refactoring 2019-05-10 14:41:33 +03:00
DYefremov
34e0ed4748 setting selected bouquet after rename 2019-05-09 23:51:47 +03:00
DYefremov
d7a214b445 iptv dialogs refactoring 2019-05-09 14:48:29 +03:00
DYefremov
e194827af7 get about dialog 2019-05-09 12:53:11 +03:00
DYefremov
e9e53da5cc dialogs refactoring 2019-05-09 11:11:54 +03:00
DYefremov
822497317d minor changes 2019-05-09 00:01:49 +03:00
DYefremov
c2047bd7b5 question dialog refactoring 2019-05-08 23:35:42 +03:00
DYefremov
3636da60d6 input dialog refactoring 2019-05-08 23:05:32 +03:00
DYefremov
2eebd55b77 updating counter on reset 2019-05-07 22:08:04 +03:00
DYefremov
12f76f8e28 added reset for the epg dialog 2019-05-07 17:22:18 +03:00
DYefremov
28e6cca919 update spanish, dutch and portuguese 2019-05-07 13:25:17 +03:00
DYefremov
9b53538da6 new impl of data mapping for the epg dialog 2019-05-07 00:04:53 +03:00
DYefremov
994541bad5 update russian 2019-05-05 11:49:24 +03:00
DYefremov
3cbb16febe minor gui changes 2019-05-05 11:26:11 +03:00
DYefremov
2b61fa07b9 update russian 2019-05-05 11:08:16 +03:00
DYefremov
406f4bd0f0 little mapping improvements for services with cyrillic names 2019-05-04 23:54:58 +03:00
DYefremov
1ec6b817e9 support of epg.dat download from the receiver 2019-05-04 20:13:57 +03:00
DYefremov
7c55692c99 small decoupling of dialogs 2019-05-04 11:21:20 +03:00
DYefremov
3aa29a788d added groups support by export to m3u 2019-05-01 17:21:51 +03:00
DYefremov
55b0dccc80 added info dialog 2019-05-01 17:19:31 +03:00
DYefremov
edb97cbf8c added keyboard shortcuts for the epg dialog 2019-05-01 13:11:19 +03:00
DYefremov
7620f03e2b added info bars for the epg dialog 2019-04-30 14:17:45 +03:00
DYefremov
cced856297 added base support of xml sources for epg dialog 2019-04-27 19:05:37 +03:00
DYefremov
3bcfd66971 added elements in the epg options widget 2019-04-26 22:07:21 +03:00
DYefremov
e7e7c667e9 added options widget for the epg dialog 2019-04-25 00:18:49 +03:00
DYefremov
6de0bc4201 added popup menus for epg dialog 2019-04-24 21:53:01 +03:00
DYefremov
878520b7f9 epg config dialog skeleton 2019-04-24 20:27:47 +03:00
DYefremov
63ac413982 saving list to xml 2019-04-22 20:25:19 +03:00
DYefremov
171c58c546 epg assignment by drag 2019-04-22 00:12:04 +03:00
DYefremov
6758ae3d16 assign epg data 2019-04-21 21:48:47 +03:00
DYefremov
329513d2a7 epg config dialog skeleton 2019-04-21 01:18:54 +03:00
DYefremov
be195e9001 added epg icon 2019-04-20 20:44:56 +03:00
DYefremov
635a3fb966 added epg skeleton 2019-04-18 23:05:19 +03:00
DYefremov
281f7a28f3 added export to m3u 2019-04-18 21:43:35 +03:00
DYefremov
507f5817c2 update version 2019-04-18 19:12:52 +03:00
DYefremov
d3822474ba small internal refactoring of iptv list config dialog 2019-04-14 20:24:57 +03:00
DYefremov
e1ce9f3006 added non-rec stream types for iptv 2019-04-14 00:03:52 +03:00
DYefremov
c2b0768857 minor cleaning 2019-04-13 15:23:24 +03:00
DYefremov
283d85ef8e fix reading of bouquet names for some configs 2019-04-12 23:29:04 +03:00
DYefremov
f5656d8d5f update spanish, dutch and portuguese 2019-04-08 11:28:49 +03:00
DYefremov
5dd5a09bfc show error message if no item is selected by import 2019-04-05 13:46:37 +03:00
DYefremov
1b5f3372b4 update russian 2019-04-04 21:01:58 +03:00
DYefremov
974e964f42 minor gui changes 2019-04-04 20:38:30 +03:00
DYefremov
8cb6ed02d2 update russian 2019-04-01 10:11:57 +03:00
DYefremov
8bb3b780d1 minor tooltips changes 2019-04-01 10:09:55 +03:00
DYefremov
3000c8830c minor gui changes 2019-03-31 21:51:53 +03:00
DYefremov
ac550e016d update russian 2019-03-31 21:40:55 +03:00
DYefremov
7420751806 Merge remote-tracking branch 'origin/development' into development 2019-03-31 21:10:41 +03:00
DYefremov
f35889e8e4 minor changes in the gui of the settings dialog 2019-03-31 21:10:27 +03:00
DYefremov
857b252f4c upd README 2019-03-28 10:34:13 +03:00
DYefremov
572584a14f fix transponders duplication 2019-03-23 11:16:43 +03:00
DYefremov
7e4ac3e69c fix pls mode 2019-03-22 00:54:44 +03:00
DYefremov
0d73ffa79d show error dialog refactoring 2019-03-19 21:44:05 +03:00
DYefremov
5e2f1ddb84 added extra method for error dialog showing 2019-03-19 00:12:33 +03:00
DYefremov
6c4040901f upd README 2019-03-18 23:37:37 +03:00
DYefremov
103e09b900 added download/upload data using Ctrl + D/U/B shortcuts 2019-03-18 23:04:05 +03:00
DYefremov
8ddc517ab7 added skip message for http test 2019-03-18 23:03:42 +03:00
DYefremov
b26d982db4 added skip message for http test 2019-03-18 22:54:59 +03:00
DYefremov
3733bc395b fix insert stream 2019-03-14 13:43:13 +03:00
DYefremov
26bfbafc0e little cleaning 2019-03-14 12:40:32 +03:00
DYefremov
84d1a18111 added single import for neutrino 2019-03-14 12:37:48 +03:00
DYefremov
1cdacd5276 added support of multistream transponders by update from web 2019-03-12 13:39:30 +03:00
DYefremov
354715558c fix set pls mode for transponder dialog 2019-03-12 10:39:55 +03:00
DYefremov
bca1613bff Added Ctrl + O/Q shortcuts 2019-03-10 18:11:38 +03:00
DYefremov
36b533b890 added double click mode option for bouquet list 2019-03-10 15:33:28 +03:00
DYefremov
2eabccc1a9 fix single import for empty bouquets list 2019-03-06 08:40:38 +03:00
DYefremov
75cd78277e added remove all unused picons 2019-03-03 12:50:40 +03:00
DYefremov
5181b732ed added confirmation dialog before import 2019-02-27 20:12:41 +03:00
DYefremov
513c0e8d3d update dutch, spanish and portuguese 2019-02-26 20:52:01 +03:00
DYefremov
f932feb305 update russian 2019-02-25 23:37:05 +03:00
DYefremov
6a2fda5ec0 changes for single bouquet import 2019-02-25 23:35:50 +03:00
DYefremov
86c30dd2c1 changes for single bouquet import 2019-02-25 23:35:20 +03:00
DYefremov
0ed41c473d minor gui changes 2019-02-24 15:47:19 +03:00
DYefremov
5078a854d2 import single bouquet skeleton 2019-02-23 13:54:00 +03:00
DYefremov
353bf04924 added import elements 2019-02-23 13:53:16 +03:00
DYefremov
474ff8e303 changed data opening from import dialog 2019-02-15 13:04:52 +03:00
DYefremov
5834bd4a0b added selection with space key 2019-02-09 15:25:44 +03:00
DYefremov
81f31e5d8d added space key 2019-02-09 15:16:03 +03:00
DYefremov
267f645c16 little refactoring of working with models 2019-02-09 12:46:06 +03:00
DYefremov
8c10d7d6a5 added columns for bouquets model 2019-02-09 12:43:27 +03:00
DYefremov
bbdb47ee7a enabled import for neutrino 2019-02-09 10:25:49 +03:00
DYefremov
7f6856e6aa import for empty config 2019-02-09 09:55:35 +03:00
DYefremov
3439a3ad0a base implementation of import 2019-02-08 19:11:30 +03:00
DYefremov
ee2e2ac49d added bouquet details for import dialog 2019-02-06 00:19:04 +03:00
DYefremov
a745167fb7 update version 2019-02-05 17:02:23 +03:00
DYefremov
8551bc2459 added import bouquets dialog 2019-02-05 16:58:54 +03:00
DYefremov
38ceb6f65d fix switching(zap) channel 2019-02-03 23:44:53 +03:00
DYefremov
33fe78911a changed download icon 2019-01-31 16:57:42 +03:00
DYefremov
3552415054 changed moving up and down in lists 2019-01-30 14:39:58 +03:00
DYefremov
a2ec6f1e1f small changes of moving from keyboard 2019-01-30 09:10:43 +03:00
DYefremov
5f90d07853 fix markers reading in some configs 2019-01-28 23:47:05 +03:00
DYefremov
d88548eece fix opening some ext configs 2019-01-28 23:10:35 +03:00
DYefremov
9ead5b3918 minor fix 2019-01-28 01:04:11 +03:00
DYefremov
1a376e6922 minor fix 2019-01-28 00:28:46 +03:00
DYefremov
eb29f2e1b2 fix parsing for some m3u 2019-01-27 23:59:06 +03:00
DYefremov
bfaab7b2fb fix get iptv url 2019-01-27 23:28:53 +03:00
DYefremov
3e6a7f8a42 changed parsing for iptv services 2019-01-27 23:20:07 +03:00
DYefremov
aff34d7627 minor gui changes 2019-01-27 09:10:56 +03:00
DYefremov
8ef0a451ff upd README 2019-01-24 10:30:47 +03:00
DYefremov
8678f02fd6 added rate lp setup for terrestrial transponder 2019-01-24 10:04:27 +03:00
DYefremov
a0a64606cd enabled transponder edit for terrestrial services 2019-01-23 16:36:50 +03:00
DYefremov
6574296278 little refactoring 2019-01-23 16:35:19 +03:00
DYefremov
d8414f56ee update data for terrestrial services 2019-01-23 13:14:06 +03:00
DYefremov
bb9499392b enabled transponder edit for cable services 2019-01-23 00:43:02 +03:00
DYefremov
069ea9348f ui changes for terrestrial transponder editing 2019-01-21 18:04:46 +03:00
DYefremov
8c932c8913 ui changes for cable transponder edit 2019-01-20 00:25:02 +03:00
DYefremov
562501dda8 added system for terrestrial services 2019-01-19 23:31:52 +03:00
DYefremov
79583869e4 added portuguese 2019-01-19 11:34:19 +03:00
DYefremov
5c9abcee21 updated spanish and dutch 2019-01-19 11:33:09 +03:00
DYefremov
e8377ed174 skeleton of transponder edit for cable and terrestrial services 2019-01-18 18:35:22 +03:00
DYefremov
16f8df0238 added elems for cable and terrestrial services 2019-01-18 18:26:48 +03:00
DYefremov
dab772e25c changed header menu 2019-01-14 10:37:18 +03:00
DYefremov
37791a5537 keyboard support for backup tool 2019-01-12 20:08:17 +03:00
DYefremov
eb55cc76be completion of the path to backups 2019-01-12 19:17:20 +03:00
DYefremov
f728a01963 update version 2019-01-12 18:12:06 +03:00
DYefremov
8aae503e35 support of setting backup path 2019-01-12 18:10:04 +03:00
DYefremov
fe499d6c94 added extra info to the backup tool 2019-01-12 13:57:40 +03:00
DYefremov
cd2c820324 fix iptv dialog show 2019-01-08 18:06:32 +03:00
DYefremov
b4c5af4c04 minor icons changes 2019-01-06 14:43:07 +03:00
DYefremov
f239957ca2 header bar icons changes 2019-01-06 14:05:03 +03:00
DYefremov
3b835e6f34 updating russian translation 2019-01-05 23:31:16 +03:00
DYefremov
4ab3d2d0e1 updated russian translation 2019-01-05 23:17:06 +03:00
DYefremov
3781686213 revert 2019-01-05 22:58:34 +03:00
DYefremov
0247aeed52 fix user bouquets restore for neutrino 2019-01-05 22:53:51 +03:00
DYefremov
3d34539c16 added backup options 2019-01-03 23:32:28 +03:00
DYefremov
0a06b36f60 setting background color for the services 2018-12-23 16:15:48 +03:00
DYefremov
ea1d536c6c added elems for color changing 2018-12-22 23:37:35 +03:00
DYefremov
5087266693 Merge branch 'master' into experimental 2018-12-22 15:35:32 +03:00
DYefremov
3e617e329c Merge branch 'testing' into experimental 2018-12-22 15:34:49 +03:00
DYefremov
1ab0b3d3f0 Merge branch 'testing' 2018-12-22 15:33:56 +03:00
DYefremov
cb3f2d71d1 fix getting the max marker number 2018-12-22 15:33:32 +03:00
DYefremov
62bcb64640 basic implementation of bouquets restore 2018-12-22 15:26:47 +03:00
DYefremov
300ea3b6d9 base implementation of backup restoring 2018-12-21 00:48:45 +03:00
DYefremov
3ae1901757 simple implementation of backups removing 2018-12-20 22:59:01 +03:00
DYefremov
2b63a59c91 added prototype of backup tool gui 2018-12-20 18:14:19 +03:00
DYefremov
ec69bae2a6 compressing backups to zip 2018-12-19 19:22:07 +03:00
DYefremov
c07e0b606b new setting dialog prototype 2018-12-19 18:21:20 +03:00
DYefremov
61745df2a7 little refactoring 2018-12-19 14:43:43 +03:00
DYefremov
146c59e0db сhanged the style of services highlighting 2018-12-19 14:42:29 +03:00
DYefremov
c7c0a4055b fix getting the maximum marker number, little refactoring 2018-12-18 19:23:44 +03:00
DYefremov
6c7d39889d Column refactoring 2018-12-17 18:31:57 +03:00
DYefremov
9d9626d065 added background for extra names in fav list 2018-12-16 22:44:45 +03:00
DYefremov
9bf7a10bf1 Merge branch 'master' into experimental 2018-12-16 17:50:39 +03:00
DYefremov
9659514feb changed version 2018-12-16 17:49:11 +03:00
DYefremov
f39ddd4315 updating service background after edit 2018-12-16 17:47:55 +03:00
DYefremov
90bd9e0211 added background for new services 2018-12-16 17:28:07 +03:00
DYefremov
48f419fec0 Merge remote-tracking branch 'origin/testing' into testing 2018-12-11 20:46:20 +03:00
DYefremov
b993eba2df Merge remote-tracking branch 'origin/experimental' into experimental 2018-12-11 20:45:55 +03:00
DYefremov
dbab024778 bouquet selection refactoring 2018-12-11 19:09:55 +03:00
DYefremov
7999fc6893 Merge branch 'testing' into experimental 2018-12-11 15:26:46 +03:00
DYefremov
b2eecf6cd9 Merge branch 'testing' 2018-12-11 15:26:23 +03:00
DYefremov
919e1c66ba Added spanish translation 2018-12-11 15:25:47 +03:00
DYefremov
3fc5a3fd68 Added services column data function prototypes 2018-12-11 14:10:44 +03:00
Dmitriy Yefremov
345ebc0983 Update _config.yml 2018-12-10 13:38:45 +03:00
Dmitriy Yefremov
8e14e78847 Update _config.yml 2018-12-10 13:37:32 +03:00
DYefremov
856b09bb4b Merge remote-tracking branch 'origin/master' 2018-12-10 13:25:17 +03:00
DYefremov
c303cd3683 upd README 2018-12-10 13:24:26 +03:00
Dmitriy Yefremov
097bb82af2 Set theme jekyll-theme-cayman 2018-12-10 13:16:31 +03:00
Dmitriy Yefremov
e780448be5 Set theme jekyll-theme-midnight 2018-12-10 13:14:08 +03:00
DYefremov
732076e60e minor changes to the player 2018-12-09 21:09:51 +03:00
DYefremov
6bcbb48993 Added dutch translation 2018-12-07 07:19:02 +03:00
DYefremov
2b80704063 added README to deb 2018-12-06 12:08:16 +03:00
DYefremov
215010e2a4 upd README 2018-12-02 19:57:44 +03:00
DYefremov
d9a035d399 upd README 2018-12-02 13:16:47 +03:00
DYefremov
f9008c62d0 upd README 2018-12-02 01:02:20 +03:00
DYefremov
6c28ce29a9 added the channel watching in the program 2018-12-02 00:45:55 +03:00
DYefremov
6ac88317cc minor gui changes for the satellites editor 2018-12-01 16:16:39 +03:00
DYefremov
332b2342cd delete repo files 2018-12-01 14:27:27 +03:00
DYefremov
3703ab4427 little refactoring 2018-12-01 13:34:26 +03:00
DYefremov
a1586944b7 minor changes for menus 2018-12-01 00:29:43 +03:00
DYefremov
22f93c0b89 added creation of empty configuration 2018-12-01 00:17:21 +03:00
DYefremov
803b26ea02 added creation of empty configuration 2018-12-01 00:13:19 +03:00
DYefremov
d86a566668 upd README 2018-11-29 08:59:11 +03:00
DYefremov
a1011c7516 added basic support of lamedb ver. 3 2018-11-28 22:47:57 +03:00
DYefremov
40df14ace8 changed version 2018-11-28 22:47:14 +03:00
DYefremov
96f9e4cca8 minor fix for the cable channel save 2018-11-25 22:55:15 +03:00
DYefremov
cd1a3c5df2 added basic support of cable services 2018-11-25 19:34:28 +03:00
DYefremov
9f9db9257e upd README 2018-11-25 10:26:53 +03:00
DYefremov
f748fae549 minor gui changes for dialogs 2018-11-23 21:05:20 +03:00
DYefremov
c303162c09 little refactoring for ZAP function 2018-11-23 15:06:36 +03:00
DYefremov
6d4434c652 added Z key 2018-11-23 15:03:10 +03:00
Dmitriy
a92b7526eb some compatibility fix 2018-11-20 22:21:26 +03:00
DYefremov
c33f95e0b5 init http api only if enabled in settings 2018-11-19 17:53:33 +03:00
DYefremov
7bfeef9d73 added http api support option in the settings 2018-11-19 13:37:10 +03:00
DYefremov
506207f2e5 added basic http api skeleton 2018-11-17 23:19:17 +03:00
DYefremov
f9ef03b827 fix reference 2018-11-17 13:48:34 +03:00
DYefremov
b5dabf65c7 added groups import support for m3u 2018-11-16 23:08:40 +03:00
DYefremov
3fb3d04161 skip options for the lists 2018-11-16 22:35:47 +03:00
DYefremov
d76e379542 Added accelerators for popup menus 2018-11-16 21:46:27 +03:00
DYefremov
d046bd7c28 minor gui changes for the download dialog 2018-11-13 21:23:37 +03:00
DYefremov
bc2ec7fff1 refactoring of data uploading 2018-11-13 14:17:59 +03:00
DYefremov
e3c3f20da7 little refactoring 2018-11-12 13:43:05 +03:00
DYefremov
2254164f40 F2 code fix 2018-11-12 11:26:11 +03:00
DYefremov
2ae4ac6383 new implementation skeleton of data uploading 2018-11-11 18:35:45 +03:00
DYefremov
80cd4c89b0 little style changes for the iptv dialogs 2018-11-11 15:33:45 +03:00
DYefremov
3df6fd7d0e some style changes for the service details dialog 2018-11-10 23:31:35 +03:00
DYefremov
f3243c9e40 width changes for some dialogs elements 2018-11-09 16:57:47 +03:00
DYefremov
528df9602b checking if value exist for the keyboard key 2018-11-09 14:14:24 +03:00
DYefremov
cd83539855 authentication for the http test 2018-11-09 12:41:36 +03:00
DYefremov
ecc17fa4b2 added default bouquet name 2018-11-08 17:48:51 +03:00
DYefremov
4c555bb7f4 moving the tests functions 2018-11-08 13:07:24 +03:00
DYefremov
e2592bb87a changing settings from the download dialog 2018-11-06 23:43:49 +03:00
DYefremov
631564b22c writing transfer state to the callback 2018-11-06 23:43:13 +03:00
DYefremov
a8bc81fb13 simple output during data transferring 2018-11-06 21:21:47 +03:00
DYefremov
fecb77090d settings dialog minor changes 2018-11-05 23:24:13 +03:00
DYefremov
692495283c scrolling to the first item when copying to the fav list beginning 2018-11-05 14:23:33 +03:00
DYefremov
5ee6615d6f added keyboard keys for laptops 2018-11-05 11:09:15 +03:00
DYefremov
4e34057d16 new variant of working with keyboard keys 2018-11-05 00:31:44 +03:00
DYefremov
f16b47ebf1 added http default settings 2018-11-04 00:37:19 +03:00
DYefremov
3ac16cb7af http properties dialog prototype 2018-11-04 00:36:07 +03:00
DYefremov
2b3c357657 new download dialog 2018-11-04 00:33:50 +03:00
DYefremov
5658a3cfc3 cyrillic keyboard keys map 2018-11-02 23:13:51 +03:00
DYefremov
06c3cb6c42 little refactoring 2018-11-02 23:13:31 +03:00
DYefremov
b60bb6b3b1 added home, end move keys for laptops 2018-11-02 21:58:10 +03:00
DYefremov
b3648a2dae updating elements for download dialog 2018-10-29 21:49:31 +03:00
DYefremov
04efdab63a set default port for telnet 2018-10-29 21:23:31 +03:00
DYefremov
966dd13c23 new download dialog skeleton 2018-10-29 18:46:46 +03:00
DYefremov
2f6450f2b4 translation for context error message 2018-10-26 20:00:11 +03:00
DYefremov
0db2080e2b little improvements for rename 2018-10-26 19:28:40 +03:00
DYefremov
4210c44ee9 fixed cropping of some bouquets names 2018-10-25 16:39:54 +03:00
DYefremov
9d7fef36c2 renaming for the IPTV and MARKER types 2018-10-25 16:00:25 +03:00
DYefremov
303d4d6107 set elems for edit transponder data for terrestrial services as hidden 2018-10-22 17:41:57 +03:00
DYefremov
9ac847ce19 set picons resizing after all providers downloading 2018-10-21 19:20:27 +03:00
DYefremov
d2c7c297b1 base implementation of terrestrial channels editing 2018-10-21 18:34:14 +03:00
DYefremov
995def4be8 minor changes in bouquets generation 2018-10-21 17:24:58 +03:00
DYefremov
0f08ba67e7 catching connection errors when getting a satellites list 2018-10-21 16:09:57 +03:00
DYefremov
499c1aa104 satellites selective loading in the picons downloader 2018-10-21 11:12:57 +03:00
DYefremov
348b0743b4 refactoring for on popup menu function 2018-10-21 11:10:45 +03:00
DYefremov
769445fafe namespace for single channel 2018-10-21 00:17:22 +03:00
DYefremov
61af6e50ce onid for the single channels parsing 2018-10-20 07:27:12 +03:00
DYefremov
4fd7295762 deleting files when closing the picons downloader 2018-10-19 11:18:55 +03:00
DYefremov
b0ad01da6c added picons downloading for the single channels 2018-10-18 19:19:40 +03:00
DYefremov
4320adc46d show single channels in the picons downloader 2018-10-17 21:36:02 +03:00
DYefremov
e2ec359327 added satellites loading in the picons dialog 2018-10-17 00:12:31 +03:00
DYefremov
dc773339ab little fix for fav end copy 2018-10-16 14:12:07 +03:00
DYefremov
e5f8ebbf37 Merge branch 'master' into experimental 2018-10-15 14:09:14 +03:00
DYefremov
1489b3ba4f skip iptv stream deletion for 403 error code 2018-10-15 14:08:33 +03:00
DYefremov
e871e88f46 fix names saving for the neutrino webtv 2018-10-15 13:37:40 +03:00
DYefremov
ea9ce5dfaf Merge branch 'master' into experimental 2018-10-15 12:36:26 +03:00
DYefremov
a1dada9a55 fix showing iptv dialog for neutrino 2018-10-15 12:36:03 +03:00
DYefremov
b137a790a0 added terrestrial services reading support 2018-10-15 11:39:33 +03:00
DYefremov
9ef776d8ab moving download dialog in the separate file 2018-10-14 20:27:06 +03:00
DYefremov
e5b726cbe6 little refactoring for the download dialog 2018-10-14 16:01:05 +03:00
DYefremov
fcfac6c20b skip receiver reboot during uploading bouquets if reachable webif 2018-10-14 12:45:12 +03:00
DYefremov
e7a4b06945 little refactoring for dynamic elements 2018-10-14 11:37:53 +03:00
DYefremov
3fe84f82f9 upd README 2018-10-13 22:28:21 +03:00
DYefremov
3eee824160 added copying from the main list to the bouquet end 2018-10-13 22:27:32 +03:00
DYefremov
bfcab961d5 minor gui changes for picons dialog 2018-10-13 12:57:37 +03:00
Dmitriy
c3f4390d11 little optimisation 2018-10-13 10:48:39 +03:00
DYefremov
dce3f0104d fix saving in fav list after paste 2018-10-10 22:46:36 +03:00
DYefremov
0c4c0da17f upd README.md 2018-10-10 21:56:38 +03:00
DYefremov
1dec2c8fc1 added url checking before iptv stream save 2018-10-10 21:54:56 +03:00
DYefremov
6eb112eb38 little refactoring for delete and copy functions 2018-10-09 13:16:49 +03:00
DYefremov
8307f153cd added logger for the satellites parser 2018-10-08 23:51:02 +03:00
DYefremov
4796a558bb modulation value fix 2018-10-08 21:41:02 +03:00
DYefremov
61d257f801 fix for compatibility with xenial for some functions 2018-10-08 00:30:09 +03:00
DYefremov
af74e7c32c updating player navigation buttons state 2018-10-06 18:28:59 +03:00
DYefremov
bf474ee8d0 fix opening lamedb5 2018-10-06 16:29:41 +03:00
DYefremov
26e6c40a1b fix rename by Ctrl + R, F2 2018-10-06 15:14:29 +03:00
DYefremov
5723f29b60 translation for the streams player elements 2018-10-05 15:28:15 +03:00
DYefremov
bc65e1b446 added prev and next buttons for the stream player 2018-10-05 15:12:13 +03:00
DYefremov
1842bec2aa minor translation fix 2018-10-04 16:31:05 +03:00
DYefremov
9cc33b9a33 fixed remove all for iptv 2018-10-04 16:24:53 +03:00
DYefremov
c8fefae571 Merge remote-tracking branch 'origin/experimental' into experimental 2018-10-04 09:14:36 +03:00
DYefremov
6d6616425c added repo files 2018-10-04 09:14:14 +03:00
DYefremov
cb4ed7ebd1 update readme 2018-10-02 09:13:49 +03:00
DYefremov
a51929068a stream player minor changes 2018-10-01 20:16:05 +03:00
DYefremov
dff2071fa3 redesign ui elements for the player 2018-09-30 23:16:30 +03:00
DYefremov
fdd2d61a28 new player prototype 2018-09-30 00:12:15 +03:00
DYefremov
1532b213e4 little refactoring and optimization of hide, lock functionality 2018-09-29 21:57:17 +03:00
DYefremov
d2b76b08e1 fix editing from the bouquet list if main list in the filter mode 2018-09-28 10:09:36 +03:00
DYefremov
af40443730 translation for the filter bar elements 2018-09-27 22:12:27 +03:00
DYefremov
62d9e21433 positions update optimisation for the filter 2018-09-27 22:02:35 +03:00
DYefremov
ddfd3db7fa little translation correction 2018-09-26 15:07:26 +03:00
DYefremov
a50a7c426e little optimisation on data loading 2018-09-25 21:26:03 +03:00
DYefremov
e93760b2ac improved functionality of the config dialog for the IPTV streams list 2018-09-23 19:19:34 +03:00
DYefremov
4a80da7515 moving iptv dialogs in the separate file 2018-09-23 00:17:58 +03:00
DYefremov
962db5f736 added selecting row under the cursor for all views 2018-09-22 21:14:56 +03:00
DYefremov
1c0ca0dbeb selecting row under the cursor at the dragging begin 2018-09-22 21:08:28 +03:00
DYefremov
40faa4029d changes of button press handling in the view 2018-09-22 19:14:47 +03:00
DYefremov
86351c61ae Skipping terrestrial and cable channels during parsing 2018-09-22 19:06:23 +03:00
DYefremov
1b56899636 minor changes 2018-09-21 11:15:20 +03:00
DYefremov
b92a7f1bdb bouquets deletion fix 2018-09-21 11:09:40 +03:00
Dmitriy Yefremov
7b1db69867 Merge branch 'master' into experimental 2018-09-21 10:31:02 +03:00
DYefremov
8ae34fa6e6 minor changes 2018-09-21 10:16:30 +03:00
DYefremov
a507f9d401 Updatating bouquets type in the model and dict 2018-09-20 18:37:47 +03:00
DYefremov
ac724fc36f cut-copy-paste impl for the bouquets list 2018-09-20 16:36:03 +03:00
DYefremov
90564859b2 DnD implementation for the bouquets list 2018-09-20 11:06:33 +03:00
DYefremov
562a5e5c6d providers parsing optimisation for picons 2018-09-19 23:02:26 +03:00
DYefremov
49605b87f9 DnD skeleton for bouquets list 2018-09-19 11:46:41 +03:00
DYefremov
27bdac7b4f Changes in handling keystrokes. 2018-09-18 14:40:24 +03:00
DYefremov
b92c02fd63 added copy possibility for fav list 2018-09-18 10:35:10 +03:00
DYefremov
aec3874e0b new player prototype 2018-09-18 07:13:32 +03:00
DYefremov
08a31e9aa0 added verification on service type before rename for the bouquet 2018-09-16 23:40:02 +03:00
DYefremov
8850ff910c added verification on service type before rename for the bouquet 2018-09-16 23:39:31 +03:00
DYefremov
5f79f5b523 little changes for fav popup menu 2018-09-16 16:59:34 +03:00
DYefremov
62bf6eadac fixed removing picon for the iptv service 2018-09-16 15:41:06 +03:00
DYefremov
c7575c4646 translation for some elements 2018-09-15 17:26:01 +03:00
DYefremov
57ae0c8d53 parsing fix of some satellites during update from the web 2018-09-15 16:04:08 +03:00
DYefremov
c37838d2c0 little gui changes for satellite and transponder dialogs 2018-09-15 10:50:40 +03:00
DYefremov
fa9949d562 revert buttons 2018-09-14 14:39:27 +03:00
DYefremov
bf54187f28 settings dialog moved to a separate file 2018-09-14 14:23:25 +03:00
DYefremov
696bce8201 little changes for the filter bar 2018-09-12 17:49:28 +03:00
DYefremov
5d01bd5479 little changes for the filter bar 2018-09-12 17:46:45 +03:00
DYefremov
403426ba75 added free services filter in the main list 2018-09-12 17:26:22 +03:00
DYefremov
43a884159b simple implementation of filtering support by type and position in main list 2018-09-12 14:05:28 +03:00
DYefremov
f925aa5642 setting default name for the service in bouquet 2018-09-11 16:25:12 +03:00
DYefremov
8bdbe45a57 support for extra names in the bouquet list 2018-09-11 15:21:05 +03:00
DYefremov
b2a24974a3 redesign of the satellites update dialog 2018-09-10 22:37:42 +03:00
DYefremov
9dd6633c6a little style changes of service details dialog 2018-09-10 08:46:01 +03:00
DYefremov
1dc5461f90 new elements for the fav popup menu 2018-09-10 00:19:45 +03:00
DYefremov
15418d234d support of opening bouquets with different names of services in bouquet and main list 2018-09-09 23:38:00 +03:00
DYefremov
0ac439cc84 GUI redesign of the some dialogs 2018-09-08 09:58:54 +03:00
DYefremov
b4eda74c6d GUI redesign of the service details dialog 2018-09-08 00:19:20 +03:00
DYefremov
35d598f1f4 redesign of GUI of the IPTV dialog 2018-09-07 23:42:59 +03:00
DYefremov
d2272e5715 redesign of GUI of the picons dialog 2018-09-07 23:10:48 +03:00
DYefremov
252c2245f7 translation some elements 2018-09-02 23:16:32 +03:00
DYefremov
93fd3cd2c3 stream play fix for enigma2 2018-09-01 23:17:03 +03:00
DYefremov
0e3d5df4bf picons support for iptv (enigma2) 2018-09-01 00:49:11 +03:00
DYefremov
e476c26bb5 changed popdown to the hide for compatibility 2018-08-31 17:45:52 +03:00
DYefremov
fb0996f94e full screen mode prototype for the stream player 2018-08-31 17:26:36 +03:00
DYefremov
1da72666d7 fix play 2018-08-25 23:53:53 +03:00
DYefremov
f790f7d0b5 new prototype of the streams player 2018-08-25 15:30:12 +03:00
DYefremov
76a0f43485 added delay for the search and filter functions 2018-08-24 12:03:51 +03:00
DYefremov
0dcbf98d1f added TID for iptv list config dialog 2018-08-22 00:09:52 +03:00
DYefremov
bc0eb37775 info bar close implementations 2018-08-21 17:28:29 +03:00
DYefremov
d494702257 base implementation of iptv list config dialog for enigma2 2018-08-21 17:19:44 +03:00
DYefremov
db60217474 iptv list configuration dialog skeleton 2018-08-19 23:27:13 +03:00
DYefremov
a399660a15 fixed get url for iptv 2018-08-19 10:55:41 +03:00
DYefremov
2eeb53537a little changes for gui elements 2018-08-18 17:36:57 +03:00
DYefremov
ab5f98a2b6 skeleton for IPTV lists configuration dialog 2018-08-18 17:35:30 +03:00
DYefremov
6d92ed667f append picon while adding a service 2018-08-18 12:31:12 +03:00
DYefremov
ff123b579f data loading refactoring 2018-08-18 11:35:44 +03:00
DYefremov
f9a66f8f75 data loading optimisation 2018-08-18 10:21:40 +03:00
DYefremov
32f33815c2 little gui changes 2018-08-15 10:51:19 +03:00
DYefremov
ea9ea98e1a replaced the dialog with a window 2018-08-05 00:54:30 +03:00
DYefremov
9c32d24a20 webtv download fix 2018-08-04 11:38:38 +03:00
DYefremov
25f483d760 clean 2018-08-01 22:34:30 +03:00
DYefremov
d9e471eaec download fix 2018-08-01 22:10:00 +03:00
DYefremov
e4f8a075f4 removed preview mode elements for IPTV 2018-08-01 11:05:29 +03:00
DYefremov
0a3dc8f79d translation for some elements 2018-07-30 22:48:48 +03:00
DYefremov
ad69df0b63 new popup menu for satellites editor 2018-07-13 17:12:02 +03:00
DYefremov
2a3a9e124b file name format fix. for bouquets 2018-07-13 12:28:13 +03:00
DYefremov
8afd1e8a80 select all, including the filter 2018-07-12 19:44:27 +03:00
DYefremov
ee8cc5b139 added (un)select all in the satellites update dialog 2018-07-12 19:11:32 +03:00
DYefremov
bed490f491 minor gui changes 2018-07-12 11:57:02 +03:00
DYefremov
620ff4bd60 added label of the current bouquet name 2018-07-09 19:00:05 +03:00
DYefremov
99ecb0f22e updating dynamic elements before popup menu 2018-07-09 11:38:36 +03:00
DYefremov
31603bfd41 little ui elements changes 2018-07-08 22:05:26 +03:00
DYefremov
abd803a58c lock/hide fix 2018-07-08 14:58:41 +03:00
DYefremov
f84e77cbce new gui for main window 2018-07-08 00:09:26 +03:00
DYefremov
170c8ffc55 new gui for satellites update dialog 2018-07-06 22:26:48 +03:00
DYefremov
bf3ba96fb9 new gui for satellites update dialog 2018-07-06 22:26:27 +03:00
DYefremov
b3e057a5a3 new gui for satellites editor 2018-07-05 17:59:17 +03:00
DYefremov
78c07f3934 added extra dialog for searching unavailable iptv streams 2018-07-03 19:30:45 +03:00
DYefremov
249a49aff5 test implementation of remove all unavailable iptv streams 2018-06-29 22:43:04 +03:00
DYefremov
03c291a61e added lamedb5 to ftp 2018-06-05 20:45:47 +03:00
DYefremov
97d9ce8b68 lamedb5 reading fix 2018-06-01 18:41:59 +03:00
DYefremov
1a55df6674 lamedb5 writing 2018-06-01 17:45:26 +03:00
DYefremov
6ac10c1380 opening lamedb5 2018-06-01 11:16:30 +03:00
DYefremov
13270b6152 v5 support skeleton 2018-05-28 18:45:31 +03:00
DYefremov
b2c0359017 applying filter after values change 2018-05-22 21:59:17 +03:00
DYefremov
8a6dd1da93 added full screen mode for the player 2018-05-19 16:24:20 +03:00
DYefremov
2e1410ca36 satellites list update in the separate thread 2018-05-12 14:36:38 +03:00
DYefremov
3d96181450 translation for some elements 2018-05-12 12:42:42 +03:00
DYefremov
fe749ca594 added checking that vlc is installed 2018-05-12 12:21:34 +03:00
DYefremov
1b6cd58112 added transponder validity checking 2018-05-12 11:47:19 +03:00
DYefremov
8d405d223a new src for the sat update dialog 2018-05-10 23:28:51 +03:00
DYefremov
81ad19043a new source for sat update dialog 2018-05-10 00:44:42 +03:00
DYefremov
34db58f8e0 selection fix in satellites update dialog 2018-05-07 23:55:22 +03:00
DYefremov
890163af4a added search feature for satellites update dialog 2018-05-07 18:19:00 +03:00
DYefremov
c4e8a6646d added simple filter for satellites update dialog 2018-05-07 15:19:05 +03:00
DYefremov
639c8511bf added ui elements for find and filter for update dialog 2018-05-07 00:44:46 +03:00
DYefremov
5e082fc5d7 new header variant for sat update dialog 2018-05-06 00:08:56 +03:00
DYefremov
7f393ff9ba getting of satellites in separate processes 2018-05-05 22:01:50 +03:00
DYefremov
b37aac0cd9 satellite data update in the main model 2018-05-02 21:23:09 +03:00
DYefremov
d857c4b786 append output for sat update dialog 2018-05-01 21:05:18 +03:00
DYefremov
6ffd1d7926 receive_satellites skeleton 2018-04-30 18:37:02 +03:00
DYefremov
415ad79c80 new satellites update dialog skeleton 2018-04-30 11:11:55 +03:00
DYefremov
da4fef7f6b test implementation of IPTV preview 2018-04-29 15:36:35 +03:00
DYefremov
76c034435d iptv preview mode skeleton 2018-04-29 01:44:28 +03:00
Dmitriy Yefremov
f9239f0642 new variant of satellites download skeleton 2018-04-25 17:26:29 +03:00
DYefremov
7f096df998 satellites dialog skeleton 2018-04-23 23:14:07 +03:00
Dmitriy Yefremov
3f0738d874 skeleton of satellites downloader 2018-04-23 14:42:41 +03:00
Dmitriy Yefremov
b310b640b4 little fix for move in tree model 2018-04-16 19:42:48 +03:00
Dmitriy Yefremov
18caa58336 Update README.md 2018-04-16 18:53:49 +03:00
Dmitriy Yefremov
03e5909c23 support of "Home" and "End" keys, new variant of move in lists 2018-04-16 18:50:48 +03:00
Dmitriy Yefremov
d9071632d2 grouping the scattered rows on move 2018-04-14 00:05:08 +03:00
DYefremov
694269113a iptv reference fix 2018-04-10 16:03:36 +03:00
DYefremov
78f347a505 bouquet creation place depending on the profile 2018-04-10 13:32:43 +03:00
DYefremov
eb9be7b190 fav view update after service edit 2018-04-10 13:04:21 +03:00
DYefremov
952aeb4d22 little refactoring for bq selection 2018-04-10 11:15:50 +03:00
Dmitriy Yefremov
8a865513b3 translation for services popup menu 2018-04-09 21:28:19 +03:00
DYefremov
30e38dde3f bouquet auto generation by type 2018-04-07 23:49:36 +03:00
DYefremov
5f68eb0f1a service deletion refactoring 2018-04-06 17:57:04 +03:00
DYefremov
ce92134a00 refactoring for multiple selection in the bouquets 2018-04-06 16:02:16 +03:00
DYefremov
2c80d13170 basic implementation of bouquets generation 2018-04-04 16:52:58 +03:00
Dmitriy Yefremov
a49d6490c5 bouquets gen skeleton 2018-04-03 22:47:29 +03:00
DYefremov
dc76a7801e Skeleton of the bouquets auto-generation feature 2018-04-02 23:55:41 +03:00
DYefremov
0b0d3ded8c IPTV elements translation 2018-04-01 09:54:50 +03:00
DYefremov
c05dd026fb Merge remote-tracking branch 'origin/experimental' into experimental 2018-04-01 09:39:29 +03:00
DYefremov
dca94271b0 checking data in IPTV dialog 2018-04-01 09:39:14 +03:00
Dmitriy Yefremov
8d7aa8736e Update README.md 2018-03-30 09:41:05 +03:00
Dmitriy Yefremov
1d693670f4 Merge branch 'experimental' into testing 2018-03-30 09:07:41 +03:00
Dmitriy Yefremov
52f50cdaf5 upd readme 2018-03-30 09:07:17 +03:00
DYefremov
d2ac5d5ac4 impl of checking the CAID field 2018-03-29 13:49:01 +03:00
Dmitriy Yefremov
fe579358f6 Merge branch 'experimental' into testing 2018-03-28 17:30:00 +03:00
Dmitriy Yefremov
db942ee10b set defaults for dvb-s2 after system change 2018-03-28 17:29:07 +03:00
DYefremov
b4648a6efd Merge remote-tracking branch 'origin/testing' into testing 2018-03-27 23:05:39 +03:00
DYefremov
647f468feb Merge branch 'experimental' into testing 2018-03-27 23:05:21 +03:00
DYefremov
ef608df76b picon name update after service edit 2018-03-27 23:03:56 +03:00
DYefremov
8e32373a99 picon name update after service edit 2018-03-27 23:03:01 +03:00
DYefremov
7f817944fa reference update in the service dialog 2018-03-26 17:33:20 +03:00
Dmitriy Yefremov
7752da92b1 Merge branch 'experimental' into testing 2018-03-25 10:47:33 +03:00
Dmitriy Yefremov
24023d438d ui changes for the service dialog 2018-03-25 10:46:30 +03:00
DYefremov
9e0d8840f3 init neutrino ui elms for service dialog 2018-03-23 21:42:54 +03:00
Dmitriy Yefremov
d762f097d0 state update for navigation buttons 2018-03-20 23:42:06 +03:00
DYefremov
336aa47177 added default init in service dialog 2018-03-20 00:33:00 +03:00
DYefremov
1eeccd654a added validation for non empty entries in service dialog 2018-03-18 18:11:16 +03:00
DYefremov
e37abef359 decoupling for [on save] in service dialog 2018-03-18 17:13:21 +03:00
Dmitriy Yefremov
c120f42ee1 skeleton of getting transponder data for neutrino 2018-03-17 22:18:10 +03:00
DYefremov
1531548e51 add impl for iptv 2018-03-16 00:10:33 +03:00
DYefremov
72ebdceb6e edit impl for iptv 2018-03-15 23:10:22 +03:00
Dmitriy Yefremov
791fa2b5f6 init neutrino data skeleton for iptv dialog 2018-03-13 23:27:55 +03:00
Dmitriy Yefremov
47df44c202 little optimisation 2018-03-13 10:42:56 +03:00
Dmitriy Yefremov
d7635370ba iptv dialog init data 2018-03-12 22:47:43 +03:00
DYefremov
1d577750c0 Iptv dialog skeleton 2018-03-11 21:52:10 +03:00
DYefremov
b56685edb1 Ui skeleton for IPTV dialog 2018-03-10 19:12:56 +03:00
DYefremov
f7f230f40e Separation of service creation and editing 2018-03-10 17:49:53 +03:00
Dmitriy Yefremov
88e19e2fd1 added warning message to the picons dialog 2018-03-09 17:40:26 +03:00
DYefremov
ae6f0e1ae2 pos fix for picons dialog 2018-03-07 23:56:21 +03:00
DYefremov
c3e880890e little picons dialog changes 2018-03-07 22:43:42 +03:00
Dmitriy Yefremov
a7edb6d0f6 Merge branch 'experimental' into testing 2018-03-06 20:46:25 +03:00
DYefremov
03e18401cc global search navigation skeleton 2018-03-06 19:06:16 +03:00
DYefremov
1d6b8c2558 Dialogs translation 2018-03-06 11:34:06 +03:00
Dmitriy Yefremov
ccd111cd94 added ui elements for search navigation 2018-03-05 22:45:21 +03:00
Dmitriy Yefremov
e8f3b5df8a convert to impl 2018-03-04 19:37:41 +03:00
DYefremov
c4c9c73809 neutrino data init skeleton for service dialog 2018-03-03 23:24:43 +03:00
DYefremov
a4514ebb2b fix tv counter 2018-03-03 20:55:08 +03:00
Dmitriy Yefremov
7b44df9afd added converter tab for picons dialog 2018-03-03 13:38:33 +03:00
DYefremov
3a018e9654 added local translation 2018-03-02 17:08:40 +03:00
DYefremov
9d4e571d89 added local translation 2018-03-02 17:06:53 +03:00
DYefremov
f62184c96c type entry for service dialog 2018-03-02 15:12:21 +03:00
Dmitriy Yefremov
320183554c status bar changes 2018-03-01 20:39:00 +03:00
Dmitriy Yefremov
a525816eca russian translation 2018-03-01 00:18:05 +03:00
DYefremov
0e11a223ad russian translate 2018-02-27 22:05:09 +03:00
Dmitriy Yefremov
6e4b992a79 russian translation skeleton 2018-02-27 14:55:03 +03:00
DYefremov
1164d38e5c e2 impl for set transponder 2018-02-26 16:26:53 +03:00
Dmitriy Yefremov
4288d62a53 set transponder for services 2018-02-25 22:59:36 +03:00
Dmitriy Yefremov
b17bd13fb5 revert transponder services dialog 2018-02-25 17:52:21 +03:00
Dmitriy Yefremov
7124fd6a92 new ui variant for services details dialog 2018-02-24 16:12:05 +03:00
DYefremov
81f354207d ServicesListDialog skeleton 2018-02-23 23:56:19 +03:00
Dmitriy Yefremov
15cb611764 fix flags 2018-02-23 17:36:49 +03:00
Dmitriy Yefremov
6115433aba skeleton of implementation of service editing for Enigma 2018-02-23 17:14:08 +03:00
Dmitriy Yefremov
0c5b9165ef intermediate impl of save in service dialog 2018-02-20 23:34:07 +03:00
DYefremov
c432646f30 on save skeleton for service details dialog 2018-02-20 00:20:32 +03:00
DYefremov
f70913832c showing flags in the service details dialog 2018-02-19 23:18:01 +03:00
DYefremov
25ee7f3538 added simple wait dialog 2018-02-18 17:14:02 +03:00
DYefremov
5a76601ae6 little refactoring in UI for edit/rename 2018-02-18 11:23:31 +03:00
Dmitriy Yefremov
4367fe6ead skeleton implementation of service details show 2018-02-17 17:07:32 +03:00
Dmitriy Yefremov
242642a7ed Update README.md 2018-02-17 16:40:40 +03:00
Dmitriy Yefremov
074fc960e5 skeleton implementation of service details show 2018-02-17 16:23:41 +03:00
DYefremov
e26d08ca8e filling pids in service dialog 2018-02-16 07:41:19 +03:00
DYefremov
8c433680a9 little refactoring ) 2018-02-16 01:58:43 +03:00
DYefremov
547046bddb service details dialog data filling 2018-02-16 01:56:28 +03:00
Dmitriy Yefremov
12983bb1a6 new variant of service details dialog 2018-02-15 18:05:13 +03:00
Dmitriy Yefremov
5dc20232ef Update README.md 2018-02-15 15:22:09 +03:00
Dmitriy Yefremov
0040ecee32 Update README.md 2018-02-15 15:21:08 +03:00
Dmitriy Yefremov
e8f30b667d service dialog elements update 2018-02-15 15:16:34 +03:00
DYefremov
99a9f081fa service details dialog ui changes 2018-02-14 19:06:08 +03:00
Dmitriy Yefremov
51605ae680 service details dialog skeleton 2018-02-14 00:00:52 +03:00
Dmitriy Yefremov
ca06400071 little ui changes 2018-02-13 17:21:25 +03:00
Dmitriy Yefremov
588df32b2f added IPTV icon 2018-02-12 14:27:21 +03:00
Dmitriy Yefremov
ab7f560b4f webtv download/upload 2018-02-12 13:34:00 +03:00
DYefremov
ffce103eae m3u gui elements for neutrino 2018-02-11 23:14:22 +03:00
Dmitriy Yefremov
90418f0e28 webtv parse for neutrino 2018-02-11 16:47:20 +03:00
Dmitriy Yefremov
0daaf6d1e5 neutrino save services fix 2018-02-11 10:50:21 +03:00
DYefremov
47f26b0f4c little fix 2018-02-10 15:49:44 +03:00
DYefremov
e67ce41667 copy/paste fix 2018-02-06 14:20:59 +03:00
DYefremov
0cff24486a insert marker to empty bouquet fix 2018-02-06 13:56:17 +03:00
DYefremov
850ba0d96a little ui changes 2018-02-06 09:49:51 +03:00
Dmitriy Yefremov
303c9a0267 write bouquet fix 2018-02-05 22:17:06 +03:00
Dmitriy Yefremov
0f95165088 set encoding for ftp, fix file names for bouquets 2018-02-05 18:24:49 +03:00
Dmitriy Yefremov
105cf9c90c new algorithm for picons retrieving 2018-02-05 14:44:42 +03:00
Dmitriy Yefremov
cb40a8d0de name space for picons 2018-02-04 18:09:37 +03:00
DYefremov
f19ab37bc8 sat position for picons dialog 2018-02-03 23:25:15 +03:00
DYefremov
d716bd6a86 Displaying picons in lists for Neutrino 2018-02-03 21:07:01 +03:00
Dmitriy Yefremov
5b13b22823 lamedb fix 2018-02-03 17:23:16 +03:00
Dmitriy Yefremov
49076fe477 bouquet names fix 2018-02-03 16:45:54 +03:00
DYefremov
50a517b6f1 simple skeleton for global search 2018-02-02 12:45:58 +03:00
Dmitriy Yefremov
184c3b18ba assign picon skeleton 2018-02-01 21:43:44 +03:00
Dmitriy Yefremov
2f8dcaf47b data backup before save 2018-02-01 14:02:59 +03:00
Dmitriy Yefremov
dbe18b345f skeleton for picon remove 2018-02-01 13:10:06 +03:00
Dmitriy Yefremov
5d68ec8176 added search bar 2018-01-31 16:02:26 +03:00
DYefremov
83e58f9375 little decoupling, filter bar 2018-01-31 00:13:42 +03:00
Dmitriy Yefremov
fe199d78a4 picon reference 2018-01-30 12:37:04 +03:00
DYefremov
d5f7acb019 popup menus for picons 2018-01-29 18:07:47 +03:00
DYefremov
a141c34ee7 Display picons in the fav list 2018-01-29 14:46:34 +03:00
Dmitriy Yefremov
25c9189e1a Display picons in the services list 2018-01-28 23:10:54 +03:00
Dmitriy Yefremov
d9390aa7be Update README.md 2018-01-26 21:26:03 +03:00
Dmitriy Yefremov
e12cc86e5f Update readme 2018-01-26 21:21:40 +03:00
DYefremov
f1ef9fe4aa decoupling deletion, deletion with considering filter 2018-01-26 15:15:39 +03:00
Dmitriy Yefremov
728bfd0b20 added remove selection keys 2018-01-25 21:43:48 +03:00
Dmitriy Yefremov
1d6022b6db fix lock\hide 2018-01-25 21:05:24 +03:00
DYefremov
8609d30ac9 bouquets fix, new implementation of services filter 2018-01-25 16:11:52 +03:00
DYefremov
fde06dca89 filter revert 2018-01-24 13:39:11 +03:00
Dmitriy Yefremov
e41bf5f58f little gui changes 2018-01-24 00:05:15 +03:00
Dmitriy Yefremov
b1488df9ce little gui changes 2018-01-23 22:58:43 +03:00
DYefremov
c6e4b3624b services filter skeleton 2018-01-23 16:18:28 +03:00
DYefremov
26b843921b force ctrl for fav and services lists 2018-01-22 14:51:34 +03:00
Dmitriy Yefremov
e73638d006 resize option for picons 2018-01-20 22:17:18 +03:00
Dmitriy Yefremov
cf3c05f324 mkdir if not exist for picons 2018-01-20 14:04:07 +03:00
Dmitriy Yefremov
030b7c4957 upload data fix 2018-01-20 11:29:34 +03:00
Dmitriy Yefremov
d7ed3e20a4 Update README.md 2018-01-19 13:15:49 +03:00
Dmitriy Yefremov
c69b0ac9e1 neutrino fav id fix 2018-01-19 11:15:35 +03:00
Dmitriy Yefremov
c5c88a8958 telnet options 2018-01-18 12:38:58 +03:00
Dmitriy Yefremov
24c064b450 telnet options 2018-01-18 00:57:58 +03:00
Dmitriy Yefremov
240d724b59 picons name format for neutrino 2018-01-17 01:18:02 +03:00
Dmitriy Yefremov
5b410241a9 upload picons impl 2018-01-16 18:51:08 +03:00
Dmitriy Yefremov
a6ffe4999a termination of process for picons 2018-01-16 12:11:54 +03:00
Dmitriy Yefremov
f67a79e869 base implementation of picons parser 2018-01-16 01:16:03 +03:00
Dmitriy Yefremov
d37c088112 load providers skeleton for picons 2018-01-15 14:56:17 +03:00
Dmitriy Yefremov
adf117c88d picons parser skeleton 2018-01-12 14:32:36 +03:00
Dmitriy Yefremov
98da7acd96 changes in dialogues 2018-01-11 17:59:59 +03:00
Dmitriy Yefremov
c82763081a added telnet options 2018-01-10 22:13:25 +03:00
Dmitriy Yefremov
1a39557964 properties for picons 2018-01-10 18:09:44 +03:00
Dmitriy Yefremov
c274c9e91d skeleton for picons dialog 2018-01-10 12:15:41 +03:00
Dmitriy Yefremov
dd1ec89592 GUI skeleton for picons 2018-01-08 22:00:48 +03:00
Dmitriy Yefremov
8ce9823a0c Update README.md 2018-01-08 14:17:47 +03:00
Dmitriy Yefremov
cc08fa8096 simple script to create deb 2018-01-08 13:52:05 +03:00
Dmitriy Yefremov
dfdf0f9d3a little changes for dynamic elements 2018-01-08 00:11:07 +03:00
Dmitriy Yefremov
a3cf34ba2a telnet for neutrino 2018-01-07 16:33:18 +03:00
Dmitriy Yefremov
0f02055c0c Update README.md 2018-01-06 15:30:46 +03:00
Dmitriy Yefremov
347dd15233 clear data fix 2018-01-06 15:24:56 +03:00
Dmitriy Yefremov
6dccdc258a fix bouquet creation 2018-01-05 14:53:53 +03:00
Dmitriy Yefremov
c8c9a0bbf0 hide, lock for neutrino 2018-01-05 14:32:14 +03:00
Dmitriy Yefremov
4dfa126795 write services for neutrino 2018-01-04 20:58:22 +03:00
Dmitriy Yefremov
9a0aa1e28f get services skeleton for neutrino (v3) 2018-01-04 01:23:22 +03:00
Dmitriy Yefremov
0fb708ca9b write bouquets skeleton for neutrino 2018-01-02 22:33:05 +03:00
Dmitriy Yefremov
74d4c9e038 write services skeleton for neutrino 2018-01-02 17:38:01 +03:00
Dmitriy Yefremov
f229169d29 fav id for neutrino 2018-01-02 01:50:01 +03:00
Dmitriy Yefremov
8263f39591 get services skeleton for neutrino 2018-01-01 23:42:40 +03:00
Dmitriy Yefremov
cf25057658 little refactoring 2018-01-01 17:28:19 +03:00
Dmitriy Yefremov
300fedf684 paths fix 2017-12-30 23:08:37 +03:00
Dmitriy Yefremov
5e954c7ec9 paths fix 2017-12-30 22:58:47 +03:00
Dmitriy Yefremov
d723ecd7f7 Neutrino-MP settings profile skeleton 2017-12-30 21:51:57 +03:00
Dmitriy Yefremov
37d4cbe1f4 Update README.md 2017-12-29 21:24:27 +03:00
Dmitriy Yefremov
beabac5c2c added hide/skip for bouquet list 2017-12-29 19:49:01 +03:00
Dmitriy Yefremov
4399664bd4 fix path 2017-12-25 21:39:53 +03:00
Dmitriy Yefremov
de497d1adf some ui changes 2017-12-25 19:50:35 +03:00
Dmitriy Yefremov
3077a1c536 added locate in service list from fav list 2017-12-24 20:54:56 +03:00
Dmitriy Yefremov
f793666c88 new implementation of hide/skip 2017-12-24 16:43:05 +03:00
Dmitriy Yefremov
5aade90d96 prototype of edit 2017-12-24 01:40:30 +03:00
Dmitriy Yefremov
3f226e0090 added up, down for satellites tool 2017-12-23 22:25:29 +03:00
Dmitriy Yefremov
86131b2a66 added deb 2017-12-21 12:38:45 +03:00
DYefremov
9d13961d3c marker insertion impl 2017-12-20 16:46:15 +03:00
DYefremov
e92a412ffb m3u import fix 2017-12-20 11:27:18 +03:00
DYefremov
e9850c4244 marker edit impl 2017-12-20 10:54:45 +03:00
Dmitriy Yefremov
b694834ee7 ui prototype to support markers 2017-12-19 22:57:04 +03:00
DYefremov
f2839e3968 DECT impl skeleton 2017-12-16 19:28:57 +03:00
DYefremov
54c7f32d53 little iptv and ui changes 2017-12-16 10:29:51 +03:00
Dmitriy Yefremov
9f71b59f9b little changes for IPTV 2017-12-15 23:55:07 +03:00
Dmitriy Yefremov
8a57d2b2e3 import fix 2017-12-15 14:43:42 +03:00
Dmitriy Yefremov
d7b9aa3766 force loading icons if not exist in theme 2017-12-15 14:33:36 +03:00
74 changed files with 32968 additions and 4300 deletions

View File

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

View File

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

View File

@@ -1,23 +1,57 @@
# DemonEditor
Enigma2 channel and satellites list editor for GNU/Linux.
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!
F2 - rename the bouquet.
Ctrl + S, T, E in Satellites edit tool for create and edit satellite or transponder.
Ctrl + L - parental lock.
Ctrl + H - hide/skip.
## 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 + Insert** - copies the selected channels from the main list to the the bouquet beginning
or inserts (creates) a new bouquet.
* **Ctrl + BackSpace** - copies the selected channels from the main list to the bouquet end.
* **Ctrl + X** - only in bouquet list. **Ctrl + C** - only in services list.
Clipboard is **"rubber"**. There is an accumulation before the insertion!
* **Ctrl + E** - edit.
* **Ctrl + R, F2** - rename.
* **Ctrl + S, T** in Satellites edit tool for create satellite or transponder.
* **Ctrl + L** - parental lock.
* **Ctrl + H** - hide/skip.
* **Ctrl + P** - start play IPTV or other stream in the bouquet list.
* **Ctrl + Z** - switch(**zap**) the channel(works when the HTTP API is enabled, Enigma2 only).
* **Ctrl + W** - switch to the channel and watch in the program.
* **Space** - select/deselect.
* **Left/Right** - remove selection.
* **Ctrl + Up, Down, PageUp, PageDown, Home, End** - move selected items in the list.
* **Ctrl + O** - (re)load user data from current dir.
* **Ctrl + D** - load data from receiver.
* **Ctrl + U/B** upload data/bouquets to receiver.
### Extra:
* Import feature.
* Multiple selections in lists only with Space key (as in file managers).
* Ability to download picons and update satellites (transponders) from web.
* Ability to import into bouquet (Neutrino WEB TV) from m3u.
* Ability to export bouquets with IPTV services to m3u.
* Assignment EPG from DVB or XML for IPTV services (Enigma2 only).
* 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.
Ability to import IPTV into bouquet from m3u files!
### Launching:
To start the program, in most cases it is enough to download the archive, unpack and run it by
double clicking on DemonEditor.desktop in the root directory, or launching from the console
with the command: ```./start.py```
Extra folders can be deleted, excluding the *app* folder and root files like *DemonEditor.desktop* and *start.py*!
### Note.
To create a simple **debian package**, you can use the *build-deb.sh.*
Tests only on OpenPLi based image with GM 990 Spark Reloaded receiver
in my preferred linux distro (Last Linux Mint 18.* - MATE 64-bit)!
The program is tested only with openATV image and Formuler F1 receiver in my favourite Linux distributions.
(the latest versions of Linux Mint 18.* and 19* MATE 64-bit)!
**Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2!**
**Important:**
Main supported **lamedb** format is version **4**. Versions **3** and **5** has only experimental support!
For version **3** is only read mode available. When saving, version **4** format is used instead!
Minimum requirements: Python >= 3.5.2 and GTK+ 3 with PyGObject bindings.
Terrestrial and cable channels at the moment are not supported!

2
_config.yml Normal file
View File

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

View File

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

359
app/connections.py Normal file
View File

@@ -0,0 +1,359 @@
import json
import os
import socket
import time
import urllib
from enum import Enum
from ftplib import FTP, error_perm
from telnetlib import Telnet
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener, install_opener
from app.commons import log
from app.properties import Profile
_BQ_FILES_LIST = ("tv", "radio", # enigma 2
"myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino
_DATA_FILES_LIST = ("lamedb", "lamedb5", "services.xml", "blacklist", "whitelist",)
_SAT_XML_FILE = "satellites.xml"
_WEBTV_XML_FILE = "webtv.xml"
class DownloadType(Enum):
ALL = 0
BOUQUETS = 1
SATELLITES = 2
PICONS = 3
WEBTV = 4
EPG = 5
class HttpRequestType(Enum):
ZAP = "zap?sRef="
INFO = "about"
SIGNAL = "tunersignal"
STREAM = "streamcurrentm3u"
class TestException(Exception):
pass
def download_data(*, properties, download_type=DownloadType.ALL, callback):
with FTP(host=properties["host"], user=properties["user"], passwd=properties["password"]) as ftp:
ftp.encoding = "utf-8"
callback("FTP OK.\n")
save_path = properties["data_dir_path"]
os.makedirs(os.path.dirname(save_path), exist_ok=True)
files = []
# bouquets
if download_type is DownloadType.ALL or download_type is DownloadType.BOUQUETS:
ftp.cwd(properties["services_path"])
ftp.dir(files.append)
file_list = _BQ_FILES_LIST + _DATA_FILES_LIST if download_type is DownloadType.ALL else _BQ_FILES_LIST
for file in files:
name = str(file).strip()
if name.endswith(file_list):
name = name.split()[-1]
download_file(ftp, name, save_path, callback)
# satellites.xml and webtv
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()
if download_type in (DownloadType.ALL, DownloadType.SATELLITES) and name.endswith(_SAT_XML_FILE):
download_file(ftp, _SAT_XML_FILE, save_path, callback)
if download_type in (DownloadType.ALL, DownloadType.WEBTV) and name.endswith(_WEBTV_XML_FILE):
download_file(ftp, _WEBTV_XML_FILE, save_path, callback)
# epg.dat
if download_type is DownloadType.EPG:
stb_path = properties["services_path"]
epg_options = properties.get("epg_options", None)
if epg_options:
stb_path = epg_options.get("epg_dat_stb_path", stb_path)
save_path = epg_options.get("epg_dat_path", save_path)
ftp.cwd(stb_path)
ftp.dir(files.append)
for file in files:
name = str(file).strip()
if name.endswith("epg.dat"):
name = name.split()[-1]
download_file(ftp, name, save_path, callback)
if callback is not None:
callback("\nDone.\n")
def upload_data(*, properties, download_type=DownloadType.ALL, remove_unused=False, profile=Profile.ENIGMA_2,
callback=None, done_callback=None, use_http=False):
data_path = properties["data_dir_path"]
host = properties["host"]
base_url = "http://{}:{}/api/".format(host, properties.get("http_port", "80"))
tn, ht = None, None # telnet, http
try:
if profile is Profile.ENIGMA_2 and use_http:
ht = http(properties.get("http_user", ""), properties.get("http_password", ""), base_url, callback)
next(ht)
message = ""
if download_type is DownloadType.BOUQUETS:
message = "User bouquets will be updated!"
elif download_type is DownloadType.ALL:
message = "All user data will be reloaded!"
elif download_type is DownloadType.SATELLITES:
message = "Satellites.xml file will be updated!"
params = urlencode({"text": message, "type": 2, "timeout": 5})
url = base_url + "message?{}".format(params)
ht.send(url)
if download_type is DownloadType.ALL:
time.sleep(5)
ht.send(base_url + "/powerstate?newstate=0")
time.sleep(2)
else:
# telnet
tn = telnet(host=host, user=properties.get("telnet_user", "root"),
password=properties.get("telnet_password", ""),
timeout=properties.get("telnet_timeout", 5))
next(tn)
# terminate enigma or neutrino
tn.send("init 4")
with FTP(host=host, user=properties["user"], passwd=properties["password"]) as ftp:
ftp.encoding = "utf-8"
callback("FTP OK.\n")
sat_xml_path = properties["satellites_xml_path"]
services_path = properties["services_path"]
if download_type is DownloadType.SATELLITES:
upload_xml(ftp, data_path, sat_xml_path, _SAT_XML_FILE, callback)
if profile is Profile.NEUTRINO_MP and download_type is DownloadType.WEBTV:
upload_xml(ftp, data_path, sat_xml_path, _WEBTV_XML_FILE, callback)
if download_type is DownloadType.BOUQUETS:
ftp.cwd(services_path)
upload_bouquets(ftp, data_path, remove_unused, callback)
if download_type is DownloadType.ALL:
upload_xml(ftp, data_path, sat_xml_path, _SAT_XML_FILE, callback)
if profile is Profile.NEUTRINO_MP:
upload_xml(ftp, data_path, sat_xml_path, _WEBTV_XML_FILE, callback)
ftp.cwd(services_path)
upload_bouquets(ftp, data_path, remove_unused, callback)
upload_files(ftp, data_path, _DATA_FILES_LIST, callback)
if download_type is DownloadType.PICONS:
upload_picons(ftp, properties.get("picons_dir_path"), properties.get("picons_path"))
if tn and not use_http:
# resume enigma or restart neutrino
tn.send("init 3" if profile is Profile.ENIGMA_2 else "init 6")
elif ht and use_http:
if download_type is DownloadType.BOUQUETS:
ht.send(base_url + "/servicelistreload?mode=2")
elif download_type is DownloadType.ALL:
ht.send(base_url + "/servicelistreload?mode=0")
ht.send(base_url + "/powerstate?newstate=4")
if done_callback is not None:
done_callback()
finally:
if tn:
tn.close()
if ht:
ht.close()
def upload_bouquets(ftp, data_path, remove_unused, callback):
if remove_unused:
remove_unused_bouquets(ftp, callback)
upload_files(ftp, data_path, _BQ_FILES_LIST, callback)
def upload_files(ftp, data_path, file_list, callback):
for file_name in os.listdir(data_path):
if file_name == _SAT_XML_FILE or file_name == _WEBTV_XML_FILE:
continue
if file_name.endswith(file_list):
send_file(file_name, data_path, ftp, callback)
def remove_unused_bouquets(ftp, callback):
files = []
ftp.dir(files.append)
for file in files:
name = str(file).strip()
if name.endswith(("tv", "radio", "bouquets.xml", "ubouquets.xml")):
name = name.split()[-1]
callback("Deleting file: {}. Status: {}\n".format(name, ftp.delete(name)))
def upload_xml(ftp, data_path, xml_path, xml_file, callback):
""" Used for transfer satellites.xml or webtv.xml files """
ftp.cwd(xml_path)
send_file(xml_file, data_path, ftp, callback)
def upload_picons(ftp, src, dest):
try:
ftp.cwd(dest)
except error_perm as e:
if str(e).startswith("550"):
ftp.mkd(dest) # if not exist
ftp.cwd(dest)
files = []
ftp.dir(files.append)
picons_suf = (".jpg", ".png")
for file in files:
name = str(file).strip()
if name.endswith(picons_suf):
name = name.split()[-1]
ftp.delete(name)
for file_name in os.listdir(src):
if file_name.endswith(picons_suf):
send_file(file_name, src, ftp)
def download_file(ftp, name, save_path, callback):
with open(save_path + name, "wb") as f:
callback("Downloading file: {}. Status: {}\n".format(name, str(ftp.retrbinary("RETR " + name, f.write))))
def send_file(file_name, path, ftp, callback):
""" Opens the file in binary mode and transfers into receiver """
with open(path + file_name, "rb") as f:
callback("Uploading file: {}. Status: {}\n".format(file_name, str(ftp.storbinary("STOR " + file_name, f))))
def http(user, password, url, callback):
init_auth(user, password, url)
while True:
url = yield
with urlopen(url, timeout=5) as f:
msg = json.loads(f.read().decode("utf-8")).get("message", None)
if msg:
callback("HTTP: {}\n".format(msg))
def telnet(host, port=23, user="", password="", timeout=5):
try:
tn = Telnet(host=host, port=port, timeout=timeout)
except socket.timeout:
log("telnet error: socket timeout")
else:
time.sleep(1)
command = yield
if user != "":
tn.read_until(b"login: ")
tn.write(user.encode("utf-8") + b"\n")
time.sleep(timeout)
if password != "":
tn.read_until(b"Password: ")
tn.write(password.encode("utf-8") + b"\n")
time.sleep(timeout)
tn.write("{}\r\n".format(command).encode("utf-8"))
time.sleep(timeout)
command = yield
time.sleep(timeout)
tn.write("{}\r\n".format(command).encode("utf-8"))
time.sleep(timeout)
yield
# ***************** http api *******************#
def http_request(host, port, user, password):
base_url = "http://{}:{}/api/".format(host, port)
init_auth(user, password, base_url)
while True:
req_type, ref = yield
url = base_url
if req_type is HttpRequestType.ZAP:
url = base_url + "zap?sRef={}".format(urllib.parse.quote(ref))
elif req_type is HttpRequestType.INFO:
url = base_url + HttpRequestType.INFO.value
elif req_type is HttpRequestType.SIGNAL:
url = base_url + HttpRequestType.SIGNAL.value
elif req_type is HttpRequestType.STREAM:
url = base_url + HttpRequestType.STREAM.value
try:
with urlopen(url, timeout=5) as f:
if req_type is HttpRequestType.STREAM:
yield f.read().decode("utf-8")
else:
yield json.loads(f.read().decode("utf-8"))
except (URLError, HTTPError):
yield None
# ***************** Connections testing *******************#
def test_ftp(host, port, user, password, timeout=5):
try:
with FTP(host=host, user=user, passwd=password, timeout=timeout) as ftp:
return ftp.getwelcome()
except (error_perm, ConnectionRefusedError, OSError) as e:
raise TestException(e)
def test_http(host, port, user, password, timeout=5, skip_message=False):
try:
params = urlencode({"text": "Connection test", "type": 2, "timeout": timeout})
params = "statusinfo" if skip_message else "message?{}".format(params)
url = "http://{}:{}/api/{}".format(host, port, params)
# authentication
init_auth(user, password, url)
with urlopen(url, timeout=5) as f:
return json.loads(f.read().decode("utf-8")).get("message", "")
except (URLError, HTTPError) as e:
raise TestException(e)
def init_auth(user, password, url):
""" Init authentication """
pass_mgr = HTTPPasswordMgrWithDefaultRealm()
pass_mgr.add_password(None, url, user, password)
auth_handler = HTTPBasicAuthHandler(pass_mgr)
opener = build_opener(auth_handler)
install_opener(opener)
def test_telnet(host, port, user, password, timeout=5):
try:
gen = telnet_test(host, port, user, password, timeout)
res = next(gen)
print(res)
res = next(gen)
return res
except (socket.timeout, OSError) as e:
raise TestException(e)
def telnet_test(host, port, user, password, timeout):
tn = Telnet(host=host, port=port, timeout=timeout)
time.sleep(1)
tn.read_until(b"login: ", timeout=2)
tn.write(user.encode("utf-8") + b"\n")
time.sleep(timeout)
tn.read_until(b"Password: ", timeout=2)
tn.write(password.encode("utf-8") + b"\n")
time.sleep(timeout)
yield tn.read_very_eager()
tn.close()
yield "Done"
if __name__ == "__main__":
pass

View File

@@ -1,39 +0,0 @@
""" This module only for common constants """
from enum import Enum
class Type(Enum):
""" Types of DVB transponders """
Satellite = "s"
Terestrial = "t"
Cable = "c"
class FLAG(Enum):
""" Service flags """
HIDE = "f:0002"
LOCK = "f:0008"
NEW = "f:0040"
POLARIZATION = {"0": "H", "1": "V", "2": "L", "3": "R"}
PLS_MODE = {"0": "Root", "1": "Gold", "2": "Combo"}
FEC = {"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", "15": None}
SYSTEM = {"0": "DVB-S", "1": "DVB-S2"}
MODULATION = {"0": "Auto", "1": "QPSK", "2": "8PSK", "3": "16APSK", "5": "32APSK"}
SERVICE_TYPE = {"-2": "Unknown", "1": "TV", "2": "Radio", "3": "Data",
"10": "Radio", "12": "Data", "22": "TV", "25": "TV (HD)",
"136": "Data", "139": "Data"}
CAS = {"C:2600": "BISS", "C:0b00": "Conax", "C:0b01": "Conax", "C:0b02": "Conax", "C:0baa": "Conax", "C:0602": "Irdeto",
"C:0604": "Irdeto", "C:0606": "Irdeto", "C:0608": "Irdeto", "C:0622": "Irdeto", "C:0626": "Irdeto",
"C:0664": "Irdeto", "C:0614": "Irdeto", "C:0692": "Irdeto", "C:1801": "Nagravision", "C:0500": "Viaccess",
"C:0E00": "PowerVu", "C:4ae0": "DRE-Crypt", "C:4ae1": "DRE-Crypt", "C:7be1": "DRE-Crypt"}

View File

@@ -1,10 +1,44 @@
from .lamedb import get_channels, write_channels, Channel
from .bouquets import get_bouquets, write_bouquets, to_bouquet_id, Bouquet, Bouquets
from .satxml import get_satellites, write_satellites, Satellite, Transponder
from .blacklist import get_blacklist, write_blacklist
from app.commons import run_task
from app.properties import Profile
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
from .iptv import parse_m3u
from .neutrino.bouquets import get_bouquets as get_neutrino_bouquets, write_bouquets as write_neutrino_bouquets
from .neutrino.services import get_services as get_neutrino_services, write_services as write_neutrino_services
from .satxml import get_satellites, write_satellites
def get_services(data_path, profile, format_version):
if profile is Profile.ENIGMA_2:
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, format_version):
if profile is Profile.ENIGMA_2:
write_enigma_services(path, channels, format_version)
elif profile is Profile.NEUTRINO_MP:
write_neutrino_services(path, channels)
def get_bouquets(path, profile):
if profile is Profile.ENIGMA_2:
return get_enigma_bouquets(path)
elif profile is Profile.NEUTRINO_MP:
return get_neutrino_bouquets(path)
@run_task
def write_bouquets(path, bouquets, profile):
if profile is Profile.ENIGMA_2:
write_enigma_bouquets(path, bouquets)
elif profile is Profile.NEUTRINO_MP:
write_neutrino_bouquets(path, bouquets)
if __name__ == "__main__":
pass

View File

@@ -1,92 +0,0 @@
""" Module for parsing bouquets """
from collections import namedtuple
_BOUQUETS_PATH = "../data/"
_TV_ROOT_FILE_NAME = "bouquets.tv"
_RADIO_ROOT_FILE_NAME = "bouquets.radio"
Bouquet = namedtuple("Bouquet", ["name", "type", "services"])
Bouquets = namedtuple("Bouquets", ["name", "type", "bouquets"])
def get_bouquets(path):
return parse_bouquets(path, "bouquets.tv", "tv"), parse_bouquets(path, "bouquets.radio", "radio")
def write_bouquets(path, bouquets, bouquets_services):
srv_line = '#SERVICE 1:7:1:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
line = []
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, bq.type))
write_bouquet(path, bq.name, bq.type, bq.services)
with open(path + "bouquets.{}".format(bqs.type), "w") as file:
file.writelines(line)
def write_bouquet(path, name, bq_type, channels):
bouquet = ["#NAME {}\n".format(name)]
for ch in channels:
if not ch: # if was duplicate
continue
if ch.service_type == "IPTV":
bouquet.append(ch.fav_id)
else:
bouquet.append("#SERVICE {}\n".format(to_bouquet_id(ch)))
with open(path + "userbouquet.{}.{}".format(name, bq_type), "w") as file:
file.writelines(bouquet)
def to_bouquet_id(ch):
""" Creates bouquet channel id """
data_type = int(ch.data_id.split(":")[-2])
if data_type == 22:
data_type = 16
elif data_type == 25:
data_type = 19
service = "{}:0:{}:{}:0:0:0:".format(1, data_type, ch.fav_id)
return service
def get_bouquet(path, name, bq_type):
""" Parsing services ids from bouquet file """
with open(path + "userbouquet.{}.{}".format(name, bq_type)) as file:
chs_list = file.read()
ids = []
for ch in list(filter(lambda x: len(x) > 1, chs_list.split("#SERVICE")[1:])): # filtering ['']
if "#DESCRIPTION" in ch: # IPTV
ids.append("#SERVICE{}".format(ch))
else:
ch_data = ch.strip().split(":")
ids.append("{}:{}:{}:{}".format(ch_data[3], ch_data[4], ch_data[5], ch_data[6]))
return ids
def parse_bouquets(path, bq_name, bq_type):
with open(path + bq_name) as file:
lines = file.readlines()
bouquets = None
nm_sep = "#NAME"
for line in lines:
if nm_sep in line:
_, _, name = line.partition(nm_sep)
bouquets = Bouquets(name.strip(), bq_type, [])
if bouquets and "#SERVICE" in line:
name = line.split(".")[1]
bouquets[2].append(Bouquet(name=name, type=bq_type, services=get_bouquet(path, name, bq_type)))
return bouquets
if __name__ == "__main__":
pass

193
app/eparser/ecommons.py Normal file
View File

@@ -0,0 +1,193 @@
""" Common elements module """
from collections import namedtuple
from enum import Enum
Service = namedtuple("Service", ["flags_cas", "transponder_type", "coded", "service", "locked", "hide", "package",
"service_type", "picon", "picon_id", "ssid", "freq", "rate", "pol", "fec",
"system", "pos", "data_id", "fav_id", "transponder"])
# ***************** Bouquets *******************#
class BqServiceType(Enum):
DEFAULT = "DEFAULT"
IPTV = "IPTV"
MARKER = "MARKER" # 64
Bouquet = namedtuple("Bouquet", ["name", "type", "services", "locked", "hidden"])
Bouquets = namedtuple("Bouquets", ["name", "type", "bouquets"])
BouquetService = namedtuple("BouquetService", ["name", "type", "data", "num"])
# ***************** Satellites *******************#
Satellite = namedtuple("Satellite", ["name", "flags", "position", "transponders"])
Transponder = namedtuple("Transponder", ["frequency", "symbol_rate", "polarization", "fec_inner",
"system", "modulation", "pls_mode", "pls_code", "is_id"])
class TrType(Enum):
""" Transponders type """
Satellite = "s"
Terrestrial = "t"
Cable = "c"
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.
LOCK = 8
NEW = 40 # Marked as new at the last scan
@staticmethod
def is_hide(value: int):
return value & 1 << 1
@staticmethod
def is_keep(value: int):
return value & 1 << 0
@staticmethod
def is_pids(value: int):
return value & 1 << 2
@staticmethod
def is_new(value: int):
return value & 1 << 5
class Pids(Enum):
VIDEO = "c:00"
AUDIO = "c:01"
TELETEXT = "c:02"
PCR = "c:03"
AC3 = "c:04"
VIDEO_TYPE = "c:05"
AUDIO_CHANNEL = "c:06"
BIT_STREAM_DELAY = "c:07" # in ms
PCM_DELAY = "c:08" # in ms
SUBTITLE = "c:09"
class Inversion(Enum):
Off = "0"
On = "1"
Auto = "2"
class Pilot(Enum):
Off = "0"
On = "1"
Auto = "2"
class SystemCable(Enum):
""" System of cable service """
ANNEX_A = "0"
ANNEX_C = "1"
ROLL_OFF = {"0": "35%", "1": "25%", "2": "20%", "3": "Auto"}
POLARIZATION = {"0": "H", "1": "V", "2": "L", "3": "R"}
PLS_MODE = {"0": "Root", "1": "Gold", "2": "Combo"}
FEC = {"0": "Auto", "1": "1/2", "2": "2/3", "3": "3/4", "4": "5/6", "5": "7/8", "6": "8/9", "7": "3/5", "8": "4/5",
"9": "9/10", "10": "1/2", "11": "2/3", "12": "3/4", "13": "5/6", "14": "7/8", "15": "8/9", "16": "3/5",
"17": "4/5", "18": "9/10", "19": "1/2", "20": "2/3", "21": "3/4", "22": "5/6", "23": "7/8", "24": "8/9",
"25": "3/5", "26": "4/5", "27": "9/10", "28": "Auto"}
FEC_DEFAULT = {"0": "Auto", "1": "1/2", "2": "2/3", "3": "3/4", "4": "5/6", "5": "7/8", "6": "8/9", "7": "3/5",
"8": "4/5", "9": "9/10", "10": "6/7", "15": "None"}
SYSTEM = {"0": "DVB-S", "1": "DVB-S2"}
MODULATION = {"0": "Auto", "1": "QPSK", "2": "8PSK", "4": "16APSK", "5": "32APSK"}
SERVICE_TYPE = {"-2": "Data", "1": "TV", "2": "Radio", "3": "Data", "10": "Radio", "22": "TV (H264)",
"25": "TV (HD)", "31": "TV (UHD)"}
# Terrestrial
BANDWIDTH = {"0": "8MHz", "1": "7MHz", "2": "6MHz", "3": "Auto", "4": "5MHz", "5": "1/712MHz", "6": "10MHz"}
T_MODULATION = {"0": "QPSK", "1": "QAM16", "2": "QAM64", "3": "Auto", "4": "QAM256"}
TRANSMISSION_MODE = {"0": "2k", "1": "8k", "2": "Auto", "3": "4k", "4": "1k", "5": "16k", "6": "32k"}
GUARD_INTERVAL = {"0": "1/32", "1": "1/16", "2": "1/8", "3": "1/4", "4": "Auto", "5": "1/128", "6": "19/128",
"7": "19/256"}
HIERARCHY = {"0": "None", "1": "1", "2": "2", "3": "4", "4": "Auto"}
T_FEC = {"0": "1/2", "1": "2/3", "2": "3/4", "3": "5/6", "4": "7/8", "5": "Auto", "6": "6/7", "7": "8/9"}
T_SYSTEM = {"0": "DVB-T", "1": "DVB-T2", "-1": "DVB-T/T2"}
# Cable
C_MODULATION = {"0": "Auto", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM128", "5": "QAM256"}
# CAS
CAS = {"C:2600": "BISS", "C:0b00": "Conax", "C:0b01": "Conax", "C:0b02": "Conax", "C:0baa": "Conax", "C:0602": "Irdeto",
"C:0604": "Irdeto", "C:0606": "Irdeto", "C:0608": "Irdeto", "C:0622": "Irdeto", "C:0626": "Irdeto",
"C:0664": "Irdeto", "C:0614": "Irdeto", "C:0692": "Irdeto", "C:1801": "Nagravision", "C:0500": "Viaccess",
"C:0E00": "PowerVu", "C:4ae0": "DRE-Crypt", "C:4ae1": "DRE-Crypt", "C:7be1": "DRE-Crypt"}
# '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

View File

@@ -0,0 +1,115 @@
""" Module for parsing bouquets """
import re
from app.eparser.ecommons import BqServiceType, BouquetService, Bouquets, Bouquet, BqType
_TV_ROOT_FILE_NAME = "bouquets.tv"
_RADIO_ROOT_FILE_NAME = "bouquets.radio"
_DEFAULT_BOUQUET_NAME = "favourites"
def get_bouquets(path):
return parse_bouquets(path, "bouquets.tv", BqType.TV.value), parse_bouquets(path, "bouquets.radio",
BqType.RADIO.value)
def write_bouquets(path, bouquets):
srv_line = '#SERVICE 1:7:{}: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:
bq_name = bq.name
if bq_name == "Favourites (TV)" or bq_name == "Favourites (Radio)":
bq_name = _DEFAULT_BOUQUET_NAME
else:
bq_name = re.sub(pattern, "_", bq.name)
line.append(srv_line.format(2 if bq.type == BqType.RADIO.value else 1, bq_name, bq.type))
write_bouquet(path + "userbouquet.{}.{}".format(bq_name, bq.type), bq.name, bq.services)
with open(path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file:
file.writelines(line)
def write_bouquet(path, name, channels):
bouquet = ["#NAME {}\n".format(name)]
for ch in channels:
if ch.service_type == BqServiceType.IPTV.name or ch.service_type == BqServiceType.MARKER.name:
bouquet.append("#SERVICE {}\n".format(ch.fav_id.strip()))
else:
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, "w", encoding="utf-8") as file:
file.writelines(bouquet)
def to_bouquet_id(ch):
""" Creates bouquet channel id """
data_type = ch.data_id
if data_type and len(data_type) > 4:
data_type = int(ch.data_id.split(":")[4])
return "{}:0:{:X}:{}:0:0:0:".format(1, data_type, ch.fav_id)
def get_bouquet(path, name, bq_type):
""" Parsing services ids from bouquet file """
with open(path + "userbouquet.{}.{}".format(name, bq_type), encoding="utf-8", errors="replace") as file:
chs_list = file.read()
services = []
srvs = list(filter(None, chs_list.split("\n#SERVICE"))) # filtering ['']
for ch in srvs[1:]:
ch_data = ch.strip().split(":")
if ch_data[1] == "64":
marker_data = ch.split("#DESCRIPTION", 1)
services.append(BouquetService(marker_data[1].strip(), BqServiceType.MARKER, ch, ch_data[2]))
elif "http" in ch:
stream_data = ch.split("#DESCRIPTION", 1)
services.append(BouquetService(stream_data[-1].strip(":").strip(), BqServiceType.IPTV, ch, 0))
else:
fav_id = "{}:{}:{}:{}".format(ch_data[3], ch_data[4], ch_data[5], ch_data[6])
name = None
if len(ch_data) == 12:
name, desc = str(ch_data[-1]).split("\n#DESCRIPTION")
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id.upper(), 0))
return srvs[0].lstrip("#NAME").strip(), services
def parse_bouquets(path, bq_name, bq_type):
with open(path + bq_name, encoding="utf-8", errors="replace") as file:
lines = file.readlines()
bouquets = None
nm_sep = "#NAME"
bq_pattern = re.compile(".*userbouquet\\.+(.*)\\.+[tv|radio].*")
for line in lines:
if nm_sep in line:
_, _, name = line.partition(nm_sep)
bouquets = Bouquets(name.strip(), bq_type, [])
if bouquets and "#SERVICE" in line:
name = re.match(bq_pattern, line)
if name:
b_name, services = get_bouquet(path, name.group(1), bq_type)
bouquets[2].append(Bouquet(name=b_name,
type=bq_type,
services=services,
locked=None,
hidden=None))
else:
raise ValueError("No bouquet name found for: {}".format(line))
return bouquets
if __name__ == "__main__":
pass

View File

@@ -0,0 +1,284 @@
""" This module used for parsing and write lamedb file """
import re
from app.commons import log
from app.ui.uicommons import CODED_ICON, LOCKED_ICON, HIDE_ICON
from .blacklist import get_blacklist
from ..ecommons import Service, POLARIZATION, FEC, SERVICE_TYPE, Flag, T_FEC, TrType, FEC_DEFAULT, T_SYSTEM
_HEADER = "eDVB services /{}/"
_SEP = ":" # separator
_FILE_NAME = "lamedb"
_END_LINE = "# File was created in DemonEditor.\n# ....Enjoy watching!....\n"
def get_services(path, format_version):
return parse(path, format_version)
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()
for srv in services:
data_id = str(srv.data_id).split(_SEP)
tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3])
if tr_id not in tr_set:
transponder = "{}\n\t{}\n/\n".format(tr_id, srv.transponder)
tr_lines.append(transponder)
tr_set.add(tr_id)
# Services
services_lines.append("{}\n{}\n{}\n".format(srv.data_id, srv.service, srv.flags_cas))
tr_lines.sort()
lines.extend(tr_lines)
lines.extend(services_lines)
lines.append("end\n" + _END_LINE)
with open(path + _FILE_NAME, "w") as file:
file.writelines(lines)
def write_to_lamedb5(path, services):
""" Writing lamedb5 file """
lines = [_HEADER.format(5) + "\n"]
services_lines = []
tr_set = set()
for srv in services:
data_id = str(srv.data_id).split(_SEP)
tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3])
tr_set.add("t:{},{}\n".format(tr_id, srv.transponder.replace(" ", ":", 1)))
# Removing empty packages
flags = list(filter(lambda x: x != "p:", srv.flags_cas.split(",")))
flags = ",".join(flags)
flags = "," + flags if flags else ""
services_lines.append("s:{},\"{}\"{}\n".format(srv.data_id, srv.service, flags))
lines.extend(sorted(tr_set))
lines.extend(services_lines)
lines.append(_END_LINE)
with open(path + "lamedb5", "w") as file:
file.writelines(lines)
def parse(path, version=4):
""" Parsing lamedb """
if version == 4:
return parse_v4(path)
elif version == 5:
return parse_v5(path)
raise SyntaxError("Unsupported version of the format.")
def parse_v3(services, transponders, path):
""" Parsing version 3 """
for t in transponders:
tr = transponders[t].lower()
tr_type = tr[0:1]
if tr_type == "c":
tr += ":0:0:0"
elif tr_type == "t":
tr += ":0:0"
else:
tr_data = tr.split(_SEP)
len_data = len(tr_data)
if len_data == 6:
tr_data.append("0")
elif len_data == 9:
tr_data.insert(6, "0")
tr_data.append("0")
tr_data.append("2")
tr = _SEP.join(tr_data)
transponders[t] = tr
return parse_services(services, transponders, path)
def parse_v4(path):
""" Parsing version 4 """
with open(path + _FILE_NAME, "r", encoding="utf-8", errors="replace") as file:
try:
data = str(file.read())
except UnicodeDecodeError as e:
log("lamedb parse error: " + str(e))
else:
transponders, sep, services = data.partition("transponders") # 1 step
pattern = re.compile("/[34]/$")
match = re.search(pattern, transponders)
if not match:
msg = "lamedb parsing error: unsupported format."
log(msg)
raise SyntaxError(msg)
transponders, sep, services = services.partition("services") # 2 step
services, sep, _ = services.partition("\nend") # 3 step
if match.group() == "/3/":
return parse_v3(services.split("\n"), parse_transponders(transponders.split("/")), path)
return parse_services(services.split("\n"), parse_transponders(transponders.split("/")), path)
def parse_v5(path):
""" Parsing version 5 """
with open(path + "lamedb5", "r", encoding="utf-8", errors="replace") as file:
lns = file.readlines()
if lns and not lns[0].endswith("/5/\n"):
raise SyntaxError("lamedb v.5 parsing error: unsupported format.")
trs, srvs = {}, [""]
for l in lns:
if l.startswith("s:"):
srv_data = l.strip("s:").split(",", 2)
srv_data[1] = srv_data[1].strip("\"")
data_len = len(srv_data)
if data_len == 3:
srv_data[2] = srv_data[2].strip()
elif data_len == 2:
srv_data.append("p:")
srvs.extend(srv_data)
elif l.startswith("t:"):
tr, srv = l.split(",")
trs[tr.strip("t:")] = srv.strip().replace(":", " ", 1)
return parse_services(srvs, trs, path)
def parse_transponders(arg):
""" Parsing transponders """
transponders = {}
for ar in arg:
tr = ar.replace("\n", "").split("\t")
if len(tr) == 2:
transponders[tr[0]] = tr[1]
return transponders
def parse_services(services, transponders, path):
""" Parsing services """
services_list = []
blacklist = str(get_blacklist(path))
srvs = split(services, 3)
if srvs[0][0] == "": # remove first empty element
srvs.remove(srvs[0])
for srv in srvs:
data_id = str(srv[0]).lower() # lower is for lamedb ver.3
data = data_id.split(_SEP)
sp = "0"
tid = data[2]
nid = data[3]
# For lamedb ver.3
is_v3 = False
if len(tid) < 4:
is_v3 = True
tid = "{:0>4}".format(tid)
data[2] = tid
if len(nid) < 4:
is_v3 = True
nid = "{:0>4}".format(nid)
data[3] = nid
if is_v3:
data[0] = "{:0>4}".format(data[0])
data_id = _SEP.join(data)
srv_type = int(data[4])
transponder_id = "{}:{}:{}".format(data[1], tid, nid)
transponder = transponders.get(transponder_id, None)
tid = tid.lstrip(sp).upper()
nid = nid.lstrip(sp).upper()
ssid = str(data[0]).lstrip(sp).upper()
onid = str(data[1]).lstrip(sp).upper()
# For comparison in bouquets. Needed in upper case!!!
fav_id = "{}:{}:{}:{}".format(ssid, tid, nid, onid)
picon_id = "1_0_{:X}_{}_{}_{}_{}_0_0_0.png".format(srv_type, ssid, tid, nid, onid)
all_flags = srv[2].split(",")
coded = CODED_ICON if list(filter(lambda x: x.startswith("C:"), all_flags)) else None
flags = list(filter(lambda x: x.startswith("f:"), all_flags))
hide = HIDE_ICON if flags and Flag.is_hide(int(flags[0][2:])) else None
locked = LOCKED_ICON if fav_id in blacklist else None
package = list(filter(lambda x: x.startswith("p:"), all_flags))
package = package[0][2:] if package else ""
if transponder is not None:
tr_type, sp, tr = str(transponder).partition(" ")
tr_type = TrType(tr_type)
tr = tr.split(_SEP)
service_type = SERVICE_TYPE.get(data[4], SERVICE_TYPE["-2"])
# removing all non printable symbols!
srv_name = "".join(c for c in srv[1] if c.isprintable())
pol = None
fec = None
system = None
pos = None
if tr_type is TrType.Satellite:
pol = POLARIZATION.get(tr[2], None)
fec = FEC.get(tr[3], None)
system = "DVB-S2" if len(tr) > 7 else "DVB-S"
pos = "{}.{}".format(tr[4][:-1], tr[4][-1:])
if tr_type is TrType.Terrestrial:
system = T_SYSTEM.get(tr[9], None)
pos = "T"
fec = T_FEC.get(tr[3], None)
elif tr_type is TrType.Cable:
system = "DVB-C"
pos = "C"
fec = FEC_DEFAULT.get(tr[4])
services_list.append(Service(flags_cas=srv[2],
transponder_type=tr_type.value,
coded=coded,
service=srv_name,
locked=locked,
hide=hide,
package=package,
service_type=service_type,
picon=None,
picon_id=picon_id,
ssid=data[0],
freq=tr[0],
rate=tr[1],
pol=pol,
fec=fec,
system=system,
pos=pos,
data_id=data_id,
fav_id=fav_id,
transponder=transponder))
return services_list
def split(itr, size):
""" Divide the iterable. """
srv = []
tmp = []
for i, line in enumerate(itr):
tmp.append(line)
if i % size == 0:
srv.append(tuple(tmp))
tmp.clear()
return srv
if __name__ == "__main__":
pass

View File

@@ -1,23 +1,80 @@
from . import Channel
""" Module for IPTV and streams support """
import re
import urllib.request
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"
MARKER_FORMAT = " 1:64:{}:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n"
def parse_m3u(path):
class StreamType(Enum):
DVB_TS = "1"
NONE_TS = "4097"
NONE_REC_1 = "5001"
NONE_REC_2 = "5002"
def parse_m3u(path, profile):
with open(path) as file:
aggr = [None] * 8
channels = []
count = 0
aggr = [None] * 10
services = []
groups = set()
counter = 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 = "#SERVICE 1:0:1:0:0:0:0:0:0:0:{}:{}\n#DESCRIPTION: {}\n".format(
line.strip().replace(":", "%3a"), name, name)
channels.append(Channel(*aggr[0:3], name, *aggr[0:3], "IPTV", *aggr, fav_id, None))
elif line.startswith("#EXTGRP") and profile is Profile.ENIGMA_2:
grp_name = line.strip("#EXTGRP:").strip()
if grp_name not in groups:
groups.add(grp_name)
fav_id = MARKER_FORMAT.format(counter, grp_name, grp_name)
counter += 1
mr = Service(None, None, None, grp_name, *aggr[0:3], BqServiceType.MARKER.name, *aggr, fav_id, None)
services.append(mr)
elif not line.startswith("#"):
url = line.strip()
if profile is Profile.ENIGMA_2:
url = urllib.request.quote(line.strip())
stream_type = StreamType.NONE_TS.value
fav_id = ENIGMA2_FAV_ID_FORMAT.format(stream_type, 1, 0, 0, 0, 0, url, name, name, None)
elif profile is Profile.NEUTRINO_MP:
fav_id = NEUTRINO_FAV_ID_FORMAT.format(url, "", 0, None, None, None, None, "", "", 1)
if name and url:
srv = Service(None, None, IPTV_ICON, name, *aggr[0:3], BqServiceType.IPTV.name, *aggr, fav_id, None)
services.append(srv)
return channels
return services
def export_to_m3u(path, bouquet, profile):
pattern = re.compile(".*:(http.*):.*") if profile is Profile.ENIGMA_2 else re.compile("(http.*?)::::.*")
lines = ["#EXTM3U\n"]
current_grp = None
for s in bouquet.services:
s_type = s.type
if s_type is BqServiceType.IPTV:
res = re.match(pattern, s.data)
if not res:
continue
data = res.group(1)
lines.append("#EXTINF:-1,{}\n".format(s.name))
if current_grp:
lines.append(current_grp)
lines.append("{}\n".format(urllib.request.unquote(data.strip())))
elif s_type is BqServiceType.MARKER:
current_grp = "#EXTGRP:{}\n".format(s.name)
with open(path + "{}.m3u".format(bouquet.name), "w", encoding="utf-8") as file:
file.writelines(lines)
if __name__ == "__main__":

View File

@@ -1,145 +0,0 @@
""" This module used for parsing 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 collections import namedtuple
from app.commons import log
from app.eparser.__constants import POLARIZATION, SYSTEM, FEC, SERVICE_TYPE
from app.ui import CODED_ICON, LOCKED_ICON, HIDE_ICON
from .blacklist import get_blacklist
_HEADER = "eDVB services /4/"
_FILE_PATH = "../data/lamedb"
_SEP = ":" # separator
_FILE_NAME = "lamedb"
Channel = namedtuple("Channel", ["flags_cas", "transponder_type", "coded", "service", "locked", "hide",
"package", "service_type", "ssid", "freq", "rate", "pol", "fec",
"system", "pos", "data_id", "fav_id", "transponder"])
def get_channels(path):
return parse(path)
def write_channels(path, channels):
lines = [_HEADER, "\ntransponders\n"]
tr_lines = []
services_lines = ["end\nservices\n"]
tr_set = set()
for ch in channels:
data_id = str(ch.data_id).split(_SEP)
tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3])
if tr_id not in tr_set:
transponder = "{}\n\t{}\n/\n".format(tr_id, ch.transponder)
tr_lines.append(transponder)
tr_set.add(tr_id)
# Services
services_lines.append("{}\n{}\n{}\n".format(ch.data_id, ch.service, ch.flags_cas))
tr_lines.sort()
lines.extend(tr_lines)
lines.extend(services_lines)
lines.append("end\nFile was created in DemonEditor.\n....Enjoy watching!....\n")
with open(path + _FILE_NAME, "w") as file:
file.writelines(lines)
def parse(path):
""" Parsing lamedb """
with open(path + _FILE_NAME, "r", encoding="utf-8", errors="replace") as file:
try:
data = str(file.read())
except UnicodeDecodeError as e:
log("lamedb parse error: " + str(e))
else:
transponders, sep, services = data.partition("transponders") # 1 step
transponders, sep, services = services.partition("services") # 2 step
services, sep, _ = services.partition("end") # 3 step
return parse_channels(services.split("\n"), transponders.split("/"), path)
def parse_transponders(arg):
""" Parsing transponders """
transponders = {}
for ar in arg:
tr = ar.replace("\n", "").split("\t")
if len(tr) == 2:
transponders[tr[0]] = tr[1]
return transponders
def parse_channels(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])
for ch in srv:
data = str(ch[0]).split(_SEP)
sp = "0"
# For comparison in bouquets. Needed in upper case!!!
fav_id = "{}:{}:{}:{}".format(str(data[0]).lstrip(sp), str(data[2]).lstrip(sp),
str(data[3]).lstrip(sp), str(data[1]).lstrip(sp)).upper()
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:]) == 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
transponder_id = "{}:{}:{}".format(data[1], data[2], data[3])
transponder = transponders.get(transponder_id, None)
if transponder is not None:
tr_type, sp, tr = str(transponder).partition(" ")
tr = tr.split(_SEP)
service_type = SERVICE_TYPE.get(data[4], SERVICE_TYPE["-2"])
channels.append(Channel(flags_cas=ch[2],
transponder_type=tr_type,
coded=coded,
service=ch[1],
locked=locked,
hide=hide,
package=package,
service_type=service_type,
ssid=data[0],
freq=tr[0],
rate=tr[1],
pol=POLARIZATION[tr[2]],
fec=FEC[tr[3]],
system=SYSTEM[tr[6]],
pos="{}.{}".format(tr[4][:-1], tr[4][-1:]),
data_id=ch[0],
fav_id=fav_id,
transponder=transponder))
return channels
def split(itr, size):
""" Divide the iterable. """
srv = []
tmp = []
for i, line in enumerate(itr):
tmp.append(line)
if i % size == 0:
srv.append(tuple(tmp))
tmp.clear()
return srv
if __name__ == "__main__":
pass

View File

View File

@@ -0,0 +1,181 @@
import os
from xml.dom.minidom import parse, Document
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"
_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_webtv(path + _W_FILE, "WEBTV", BqType.WEBTV.value))
def parse_bouquets(file, name, bq_type):
bouquets = Bouquets(name=name, type=bq_type, bouquets=[])
if not os.path.exists(file):
return bouquets
dom = parse(file)
for elem in dom.getElementsByTagName("Bouquet"):
if elem.hasAttributes():
bq_name = elem.attributes["name"].value
hidden = elem.attributes.get("hidden")
hidden = hidden.value if hidden else hidden
locked = elem.attributes.get("locked")
locked = locked.value if locked else locked
# epg = elem.attributes["epg"].value
services = []
for srv_elem in elem.getElementsByTagName("S"):
if srv_elem.hasAttributes():
ssid = srv_elem.attributes["i"].value
on = srv_elem.attributes["on"].value
tr_id = srv_elem.attributes["t"].value
fav_id = "{}:{}:{}".format(tr_id, on, ssid)
services.append(BouquetService(None, BqServiceType.DEFAULT, fav_id, 0))
bouquets[2].append(Bouquet(name=bq_name,
type=bq_type,
services=services,
locked=LOCKED_ICON if locked == "1" else None,
hidden=HIDE_ICON if hidden == "1" else None))
if BqType(bq_type) is BqType.BOUQUET:
for bq in bouquets.bouquets:
if bq.services:
key = int(bq.services[0].data.split(":")[1], 16)
if key not in PROVIDER:
pos, sep, name = bq.name.partition("]")
PROVIDER[key] = name
return bouquets
def 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)
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(_COMMENT)
doc.appendChild(comment)
for bq in bouquet.bouquets:
bq_elem = doc.createElement("Bouquet")
bq_elem.setAttribute("name", bq.name)
bq_elem.setAttribute("hidden", "1" if bq.hidden else "0")
bq_elem.setAttribute("locked", "1" if bq.locked else "0")
bq_elem.setAttribute("epg", "0")
root.appendChild(bq_elem)
for srv in bq.services:
tr_id, on, ssid = srv.fav_id.split(":")
srv_elem = doc.createElement("S")
srv_elem.setAttribute("i", ssid)
srv_elem.setAttribute("n", srv.service)
srv_elem.setAttribute("t", tr_id)
srv_elem.setAttribute("on", on)
srv_elem.setAttribute("s", srv.pos.replace(".", ""))
srv_elem.setAttribute("frq", srv.freq[:-3])
srv_elem.setAttribute("l", "0") # temporary !!!
bq_elem.appendChild(srv_elem)
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

@@ -0,0 +1,169 @@
from xml.dom.minidom import parse, Document
from ..ecommons import Service, POLARIZATION, FEC, SYSTEM, SERVICE_TYPE, PROVIDER
_FILE = "services.xml"
_TR_ATTR_NAMES = ("id", "on", "frq", "inv", "sr", "fec", "pol", "mod", "sys") # transponder attributes
_SRV_ATTR_NAMES = ("t", "s", "num", "f", "v", "a", "p", "pmt", "tx", "vt") # service attributes
def write_services(path, services):
doc = Document()
root = doc.createElement("zapit")
root.setAttribute("api", "4")
doc.appendChild(root)
comment = doc.createComment(" File was created in DemonEditor. Enjoy watching! ")
doc.appendChild(comment)
sats = {}
for srv in services:
flag = srv[0]
if flag in sats:
sats.get(flag).append(srv)
else:
srv_list = [srv]
sats[flag] = srv_list
for sat in sats:
tr_atr = sat.split(":")
sat_elem = doc.createElement("sat")
sat_elem.setAttribute("name", tr_atr[0])
sat_elem.setAttribute("position", tr_atr[1].replace(".", ""))
sat_elem.setAttribute("diseqc", tr_atr[2])
sat_elem.setAttribute("uncommited", tr_atr[3])
root.appendChild(sat_elem)
transponers = {}
for srv in sats.get(sat):
flag = srv[-1]
if flag in transponers:
transponers.get(flag).append(srv)
else:
srv_list = [srv]
transponers[flag] = srv_list
for tr in transponers:
tr_elem = doc.createElement("TS")
tr_atr = tr.split(":")
for i, value in enumerate(tr_atr):
if value == "None":
continue
tr_elem.setAttribute(_TR_ATTR_NAMES[i], value)
sat_elem.appendChild(tr_elem)
for srv in transponers.get(tr):
srv_elem = doc.createElement("S")
srv_elem.setAttribute("i", srv.ssid)
srv_elem.setAttribute("n", srv.service)
srv_attrs = srv.data_id.split(":")
api = srv_attrs.pop(0)
if api == "3":
root.setAttribute("api", "3") # !!!
for i, value in enumerate(srv_attrs):
if value == "None":
continue
srv_elem.setAttribute(_SRV_ATTR_NAMES[i], value)
tr_elem.appendChild(srv_elem)
doc.writexml(open(path + _FILE, "w"), addindent=" ", newl="\n", encoding="UTF-8")
doc.unlink()
def get_services(path):
return parse_services(path)
def parse_services(path):
""" Parsing services from xml"""
dom = parse(path + _FILE)
services = []
for root in dom.getElementsByTagName("zapit"):
api = root.attributes["api"].value
for elem in root.getElementsByTagName("sat"):
if elem.hasAttributes():
sat_name = elem.attributes["name"].value
sat_pos = elem.attributes["position"].value
sat_pos = "{}.{}".format(sat_pos[:-1], sat_pos[-1:])
diseqc = elem.attributes.get("diseqc")
diseqc = diseqc.value if diseqc else diseqc
uncommited = elem.attributes.get("uncommited")
uncommited = uncommited.value if uncommited else uncommited
sat = "{}:{}:{}:{}".format(sat_name, sat_pos, diseqc, uncommited)
for tr_elem in elem.getElementsByTagName("TS"):
if tr_elem.hasAttributes():
parse_transponder(api, sat, sat_pos, services, tr_elem)
return services
def parse_transponder(api, sat, sat_pos, services, tr_elem):
tr_id = tr_elem.attributes["id"].value
on = tr_elem.attributes["on"].value
freq = tr_elem.attributes["frq"].value
rate = tr_elem.attributes["sr"].value
inv = tr_elem.attributes["inv"].value
fec = tr_elem.attributes["fec"].value
pol = tr_elem.attributes["pol"].value
mod = tr_elem.attributes.get("mod")
mod = mod.value if mod else mod
sys = tr_elem.attributes.get("sys")
sys = sys.value if sys else sys
tr = "{}:{}:{}:{}:{}:{}:{}:{}:{}".format(tr_id, on, freq, inv, rate, fec, pol, mod, sys)
tr_id = tr_id.lstrip("0")
for srv_elem in tr_elem.getElementsByTagName("S"):
if srv_elem.hasAttributes():
ssid = srv_elem.attributes["i"].value
name = srv_elem.attributes["n"].value
srv_type = srv_elem.attributes["t"].value
sys = srv_elem.attributes["s"].value
num = srv_elem.attributes.get("num")
num = num.value if num else num
f = srv_elem.attributes.get("f")
f = f.value if f else f
v, a, p, pmt, tx, vt = [None] * 6
# For v3 is possible so: '<S i="0001" n="name" t="1" s="0" num="770" f="4"/>' (equals v4 api)
if api == "3" and len(srv_elem.attributes) > 6:
v = srv_elem.attributes["v"].value
a = srv_elem.attributes["a"].value
p = srv_elem.attributes["p"].value
pmt = srv_elem.attributes["pmt"].value
tx = srv_elem.attributes["tx"].value
vt = srv_elem.attributes["vt"].value
data_id = "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}".format(api, srv_type, sys, num, f, v, a, p, pmt, tx, vt)
fav_id = "{}:{}:{}".format(tr_id, on.lstrip("0"), ssid.lstrip("0"))
picon_id = "{}{}{}.png".format(tr_id, on, ssid)
srv = Service(flags_cas=sat,
transponder_type=None,
coded=None,
service=name,
locked=None,
hide=None,
package=PROVIDER.get(int(on, 16)),
service_type=SERVICE_TYPE.get(str(int(srv_type, 16))),
picon=None,
picon_id=picon_id,
ssid=ssid,
freq=freq,
rate=rate,
pol=POLARIZATION.get(pol),
fec=FEC.get(fec),
system=SYSTEM.get(sys),
pos=sat_pos,
data_id=data_id,
fav_id=fav_id,
transponder=tr)
services.append(srv)
if __name__ == "__main__":
pass

View File

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

View File

@@ -1,110 +0,0 @@
import os
import socket
import time
from enum import Enum
from ftplib import FTP
from telnetlib import Telnet
__DATA_FILES_LIST = ("tv", "radio", "lamedb", "blacklist", "whitelist")
class DownloadDataType(Enum):
ALL = 0
BOUQUETS = 1
SATELLITES = 2
def download_data(*, properties, download_type=DownloadDataType.ALL):
with FTP(host=properties["host"]) as ftp:
ftp.login(user=properties["user"], passwd=properties["password"])
save_path = properties["data_dir_path"]
files = []
# bouquets section
if download_type is DownloadDataType.ALL or download_type is DownloadDataType.BOUQUETS:
ftp.cwd(properties["services_path"])
ftp.dir(files.append)
for file in files:
name = str(file).strip()
if name.endswith(__DATA_FILES_LIST):
name = name.split()[-1]
with open(save_path + name, "wb") as f:
ftp.retrbinary("RETR " + name, f.write)
# satellites.xml section
if download_type is DownloadDataType.ALL or download_type is DownloadDataType.SATELLITES:
ftp.cwd(properties["satellites_xml_path"])
files.clear()
ftp.dir(files.append)
for file in files:
name = str(file).strip()
xml_file = "satellites.xml"
if name.endswith(xml_file):
with open(save_path + xml_file, 'wb') as f:
ftp.retrbinary("RETR " + xml_file, f.write)
def upload_data(*, properties, download_type=DownloadDataType.ALL, remove_unused=False):
data_path = properties["data_dir_path"]
host = properties["host"]
# telnet
tn = telnet(host=host)
next(tn)
# terminate enigma
tn.send("init 4")
with FTP(host=host) as ftp:
ftp.login(user=properties["user"], passwd=properties["password"])
if download_type is DownloadDataType.ALL or download_type is DownloadDataType.SATELLITES:
ftp.cwd(properties["satellites_xml_path"])
file_name = "satellites.xml"
send = send_file(file_name, data_path, ftp)
if download_type == DownloadDataType.SATELLITES:
return send
if download_type is DownloadDataType.ALL or download_type is DownloadDataType.BOUQUETS:
ftp.cwd(properties["services_path"])
if remove_unused:
files = []
ftp.dir(files.append)
for file in files:
name = str(file).strip()
if name.endswith(__DATA_FILES_LIST):
name = name.split()[-1]
ftp.delete(name)
for file_name in os.listdir(data_path):
if file_name == "satellites.xml":
continue
file_name, send_file(file_name, data_path, ftp)
# resume enigma
tn.send("init 3")
def send_file(file_name, path, ftp):
""" Opens the file in binary mode and transfers into receiver """
with open(path + file_name, "rb") as f:
return ftp.storbinary("STOR " + file_name, f)
def telnet(host, port=23, user="root", password="root", timeout=5):
try:
tn = Telnet(host=host, port=port, timeout=timeout)
except socket.timeout:
print("socket timeout")
else:
time.sleep(1)
command = yield
tn.write("{}\r\n".format(command).encode("utf-8"))
time.sleep(timeout)
command = yield
time.sleep(timeout)
tn.write("{}\r\n".format(command).encode("utf-8"))
time.sleep(timeout)
tn.close()
yield
if __name__ == "__main__":
pass

View File

@@ -1,25 +1,35 @@
import json
import os
from enum import Enum
from pathlib import Path
CONFIG_PATH = str(Path.home()) + "/.config/demon-editor/"
CONFIG_FILE = CONFIG_PATH + "config.json"
DATA_PATH = "data/"
class Profile(Enum):
""" Profiles for settings """
ENIGMA_2 = "0"
NEUTRINO_MP = "1"
def get_config():
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True) # create dir if not exist
os.makedirs(os.path.dirname(DATA_PATH), exist_ok=True)
if not os.path.isfile(CONFIG_FILE) or os.stat(CONFIG_FILE).st_size == 0:
with open(CONFIG_FILE, "w") as default_config_file:
json.dump(get_default_settings(), default_config_file)
reset_config()
with open(CONFIG_FILE, "r") as config_file:
return json.load(config_file)
def reset_config():
with open(CONFIG_FILE, "w") as default_config_file:
json.dump(get_default_settings(), default_config_file)
def write_config(config):
assert isinstance(config, dict)
with open(CONFIG_FILE, "w") as config_file:
@@ -27,12 +37,30 @@ def write_config(config):
def get_default_settings():
return {"host": "127.0.0.1", "port": "21",
"user": "root", "password": "root",
"services_path": "/etc/enigma2/",
"user_bouquet_path": "/etc/enigma2/",
"satellites_xml_path": "/etc/tuxbox/",
"data_dir_path": DATA_PATH}
return {
Profile.ENIGMA_2.value: {
"host": "127.0.0.1", "port": "21", "user": "root", "password": "root",
"http_user": "root", "http_password": "", "http_port": "80", "http_timeout": 5,
"telnet_user": "root", "telnet_password": "", "telnet_port": "23", "telnet_timeout": 5,
"services_path": "/etc/enigma2/", "user_bouquet_path": "/etc/enigma2/",
"satellites_xml_path": "/etc/tuxbox/", "data_dir_path": DATA_PATH + "enigma2/",
"picons_path": "/usr/share/enigma2/picon", "picons_dir_path": DATA_PATH + "enigma2/picons/",
"backup_dir_path": DATA_PATH + "enigma2/backup/",
"backup_before_save": True, "backup_before_downloading": True,
"v5_support": False, "http_api_support": False,
"use_colors": True, "new_color": "rgb(255,230,204)", "extra_color": "rgb(179,230,204)",
"fav_click_mode": 0},
Profile.NEUTRINO_MP.value: {
"host": "127.0.0.1", "port": "21", "user": "root", "password": "root",
"http_user": "", "http_password": "", "http_port": "80", "http_timeout": 2,
"telnet_user": "root", "telnet_password": "", "telnet_port": "23", "telnet_timeout": 1,
"services_path": "/var/tuxbox/config/zapit/", "user_bouquet_path": "/var/tuxbox/config/zapit/",
"satellites_xml_path": "/var/tuxbox/config/", "data_dir_path": DATA_PATH + "neutrino/",
"picons_path": "/usr/share/tuxbox/neutrino/icons/logo/", "picons_dir_path": DATA_PATH + "neutrino/picons/",
"backup_dir_path": DATA_PATH + "neutrino/backup/",
"backup_before_save": True, "backup_before_downloading": True,
"fav_click_mode": 0},
"profile": Profile.ENIGMA_2.value}
if __name__ == "__main__":

0
app/tools/__init__.py Normal file
View File

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

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

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

@@ -0,0 +1,56 @@
from app.commons import run_task
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
@run_task
def play(self, mrl=None):
if mrl:
self._player.set_mrl(mrl)
self._player.play()
self._is_playing = True
@run_task
def stop(self):
if self._is_playing:
self._player.stop()
self._is_playing = False
def pause(self):
self._player.pause()
@run_task
def release(self):
if self._player:
self._is_playing = False
self._player.stop()
self._player.release()
def set_xwindow(self, xid):
self._player.set_xwindow(xid)
def set_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

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

@@ -0,0 +1,270 @@
import glob
import os
import re
import shutil
from collections import namedtuple
from html.parser import HTMLParser
from app.commons import run_task
from app.properties import Profile
_ENIGMA2_PICON_KEY = "{:X}:{:X}:{}"
_NEUTRINO_PICON_KEY = "{:x}{:04x}{:04x}.png"
Provider = namedtuple("Provider", ["logo", "name", "pos", "url", "on_id", "ssid", "single", "selected"])
Picon = namedtuple("Picon", ["ref", "ssid", "v_pid"])
class PiconsParser(HTMLParser):
""" Parser for package html page. (https://www.lyngsat.com/packages/*provider-name*.html) """
def __init__(self, entities=False, separator=' ', single=None):
HTMLParser.__init__(self)
self._parse_html_entities = entities
self._separator = separator
self._single = single
self._is_td = False
self._is_th = False
self._current_row = []
self._current_cell = []
self.picons = []
def handle_starttag(self, tag, attrs):
if tag == 'td':
self._is_td = True
if tag == 'th':
self._is_th = True
if tag == "img":
self._current_row.append(attrs[0][1])
def handle_data(self, data):
""" Save content to a cell """
if self._is_td or self._is_th:
self._current_cell.append(data.strip())
def handle_endtag(self, tag):
if tag == 'td':
self._is_td = False
elif tag == 'th':
self._is_th = False
if tag in ('td', 'th'):
final_cell = self._separator.join(self._current_cell).strip()
self._current_row.append(final_cell)
self._current_cell = []
elif tag == 'tr':
row = self._current_row
ln = len(row)
if self._single and ln == 4 and row[0].startswith("../../logo/"):
self.picons.append(Picon(row[0].strip("../"), "0", "0"))
else:
if 9 < ln < 13:
url = None
if row[0].startswith("../logo/"):
url = row[0]
elif row[1].startswith("../logo/"):
url = row[1]
ssid = row[-4]
if url and len(ssid) > 2:
self.picons.append(Picon(url, ssid, row[-3]))
self._current_row = []
def error(self, message):
pass
@staticmethod
def parse(open_path, picons_path, tmp_path, provider, picon_ids, profile=Profile.ENIGMA_2):
with open(open_path, encoding="utf-8", errors="replace") as f:
on_id, pos, ssid, single = provider.on_id, provider.pos, provider.ssid, provider.single
neg_pos = pos.endswith("W")
pos = int("".join(c for c in pos if c.isdigit()))
# For negative (West) positions 3600 - numeric position value!!!
if neg_pos:
pos = 3600 - pos
parser = PiconsParser(single=single)
parser.reset()
parser.feed(f.read())
picons = parser.picons
if picons:
os.makedirs(picons_path, exist_ok=True)
for p in picons:
try:
if single:
on_id, freq = on_id.strip().split("::")
namespace = "{:X}{:X}".format(int(pos), int(freq))
else:
namespace = "{:X}0000".format(int(pos))
name = PiconsParser.format(ssid if single else p.ssid, on_id, namespace, picon_ids, profile)
p_name = picons_path + (name if name else os.path.basename(p.ref))
shutil.copyfile(tmp_path + "www.lyngsat.com/" + p.ref.lstrip("."), p_name)
except (TypeError, ValueError) as e:
msg = "Picons format parse error: {}".format(p) + "\n" + str(e)
# log(msg)
print(msg)
@staticmethod
def format(ssid, on_id, namespace, picon_ids, profile: Profile):
if profile is Profile.ENIGMA_2:
return picon_ids.get(_ENIGMA2_PICON_KEY.format(int(ssid), int(on_id), namespace), None)
elif profile is Profile.NEUTRINO_MP:
tr_id = int(ssid[:-2] if len(ssid) < 4 else ssid[:2])
return _NEUTRINO_PICON_KEY.format(tr_id, int(on_id), int(ssid))
else:
return "{}.png".format(ssid)
class ProviderParser(HTMLParser):
""" Parser for satellite html page. (https://www.lyngsat.com/*sat-name*.html) """
_POSITION_PATTERN = re.compile("at\s\d+\..*(?:E|W)']")
_ONID_TID_PATTERN = re.compile("^\d+-\d+.*")
_TRANSPONDER_FREQUENCY_PATTERN = re.compile("^\d+ [HVLR]+")
_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)
self.convert_charrefs = False
self._parse_html_entities = entities
self._separator = separator
self._is_td = False
self._is_th = False
self._is_onid_tid = False
self._is_provider = False
self._current_row = []
self._current_cell = []
self.rows = []
self._ids = set()
self._prv_names = set()
self._positon = None
self._on_id = None
self._freq = None
def handle_starttag(self, tag, attrs):
if tag == 'td':
self._is_td = True
if tag == 'tr':
self._is_th = True
if tag == "img":
if attrs[0][1].startswith("logo/"):
self._current_row.append(attrs[0][1])
if tag == "a":
url = attrs[0][1]
if url.startswith((self._PKG_DOMAIN, self._TV_DOMAIN, self._RADIO_DOMAIN)):
self._current_row.append(url)
if tag == "font" and len(attrs) == 1:
atr = attrs[0]
if len(atr) == 2 and atr[1] == "darkgreen":
self._is_onid_tid = True
def handle_data(self, data):
""" Save content to a cell """
if self._is_td or self._is_th:
self._current_cell.append(data.strip())
if self._is_onid_tid:
m = self._ONID_TID_PATTERN.match(data)
if m:
self._on_id, tid = m.group().split("-")
self._is_onid_tid = False
def handle_endtag(self, tag):
if tag == 'td':
self._is_td = False
elif tag == 'tr':
self._is_th = False
if tag in ('td', 'th'):
final_cell = self._separator.join(self._current_cell).strip()
self._current_row.append(final_cell)
self._current_cell = []
elif tag == 'tr':
r = self._current_row
# Satellite position
if not self._positon:
pos = re.findall(self._POSITION_PATTERN, str(r))
if pos:
self._positon = "".join(c for c in str(pos) if c.isdigit() or c in ".EW")
len_row = len(r)
if len_row > 2:
m = self._TRANSPONDER_FREQUENCY_PATTERN.match(r[1])
if m:
self._freq = m.group().split()[0]
if len_row == 12:
# Providers
name = r[5]
self._prv_names.add(name)
m = self._ONID_TID_PATTERN.match(str(r[-2]))
if m:
on_id, tid = m.group().split("-")
if on_id not in self._ids:
r[-2] = on_id
self._ids.add(on_id)
r[0] = self._positon
if name + on_id not in self._prv_names:
self._prv_names.add(name + on_id)
self.rows.append(Provider(logo=r[2], name=name, pos=self._positon, url=r[6], on_id=on_id,
ssid=None, single=False, selected=True))
elif 6 < len_row < 10:
# Single services
name, url, ssid = None, None, None
if r[0].startswith("http"):
name, url, ssid = r[1], r[0], r[4]
elif r[1].startswith("http"):
name, url, ssid = r[2], r[1], r[5]
if name and url:
on_id = "{}::{}".format(self._on_id if self._on_id else "1", self._freq)
self.rows.append(Provider(logo=None, name=name, pos=self._positon, url=url, on_id=on_id,
ssid=ssid, single=True, selected=False))
self._current_row = []
def error(self, message):
pass
def reset(self):
super().reset()
def parse_providers(open_path):
parser = ProviderParser()
parser.reset()
with open(open_path, encoding="utf-8", errors="replace") as f:
parser.feed(f.read())
return parser.rows
@run_task
def convert_to(src_path, dest_path, 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__":
pass

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

@@ -0,0 +1,250 @@
""" 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.commons import log
from app.eparser import Satellite, Transponder, is_transponder_valid
from app.eparser.ecommons import PLS_MODE
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):
try:
request = requests.get(url=src, headers=self._HEADERS)
except requests.exceptions.ConnectionError as e:
log(repr(e))
return []
else:
reason = request.reason
if reason == "OK":
self.feed(request.text)
else:
log(reason)
if self._rows:
if self._source is SatelliteSource.FLYSAT:
def get_sat(r):
return r[1], self.parse_position(r[2]), r[3], r[0], False
return list(map(get_sat, filter(lambda x: all(x) and len(x) == 5, self._rows)))
elif self._source is SatelliteSource.LYNGSAT:
extra_pattern = re.compile("^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])
name = row[1].rsplit("/")[-1].rstrip(".html").replace("-", " ")
sats.append((name, current_pos, row[5], row[1], False)) # coupled [all in one] satellites
sats.append((row[4], current_pos, row[5], row[3], False))
if r_len == 8: # for a very limited number of satellites
data = list(filter(None, row))
urls = set()
sat_type = ""
for d in data:
url = re.match(extra_pattern, d)
if url:
urls.add(url.group(0))
if d in ("C", "Ku", "CKu"):
sat_type = d
current_pos = self.parse_position(data[1])
for url in urls:
name = url.rsplit("/")[-1].rstrip(".html").replace("-", " ")
sats.append((name, current_pos, sat_type, 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="{} {}".format(pos, sat[0]),
flags="0",
position=self.get_position(pos.replace(".", "")),
transponders=self.get_transponders(sat[3]))
@staticmethod
def parse_position(pos_str):
return "".join(c for c in pos_str if c.isdigit() or c.isalpha() or c == ".")
@staticmethod
def get_position(pos):
return "{}{}".format("-" if pos[-1] == "W" else "", pos[:-1])
def get_transponders(self, sat_url):
""" Getting transponders(sorted by frequency). """
self._rows.clear()
url = "https://www.flysat.com/" + sat_url if self._source is SatelliteSource.FLYSAT else sat_url
request = requests.get(url=url, headers=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 sorted(trs, key=lambda x: int(x.frequency))
def get_transponders_for_fly_sat(self, trs):
""" Parsing transponders for FlySat """
pls_pattern = re.compile("(PLS:)+ (Root|Gold|Combo)+ (\\d+)?")
is_id_pattern = re.compile("(Stream) (\\d+)")
pls_modes = {v: k for k, v in PLS_MODE.items()}
n_trs = []
if self._rows:
zeros = "000"
is_ids = []
for r in self._rows:
if len(r) == 1:
is_ids.extend(re.findall(is_id_pattern, r[0]))
continue
if len(r) < 3:
continue
data = r[2].split(" ")
if len(data) != 2:
continue
sr, fec = data
data = r[1].split(" ")
if len(data) < 3:
continue
freq, pol, sys = data[0], data[1], data[2]
sys = sys.split("/")
if len(sys) != 2:
continue
sys, mod = sys
mod = "QPSK" if sys == "DVB-S" else mod
pls = re.findall(pls_pattern, r[1])
pls_code = None
pls_mode = None
if pls:
pls_code = pls[0][2]
pls_mode = pls_modes.get(pls[0][1], None)
if is_ids:
tr = trs.pop()
for index, is_id in enumerate(is_ids):
tr = tr._replace(is_id=is_id[1])
if is_transponder_valid(tr):
n_trs.append(tr)
else:
tr = Transponder(freq + zeros, sr + zeros, pol, fec, sys, mod, pls_mode, pls_code, None)
if is_transponder_valid(tr):
trs.append(tr)
is_ids.clear()
trs.extend(n_trs)
def get_transponders_for_lyng_sat(self, trs):
""" Parsing transponders for LyngSat """
frq_pol_pattern = re.compile("(\\d{4,5})\\s+([RLHV]).*")
sr_fec_pattern = re.compile("^(\\d{4,5})-(\\d/\\d)(.+PSK)?(.*)?$")
sys_pattern = re.compile("(DVB-S[2]?) ?(PLS+ (Root|Gold|Combo)+ (\\d+))* ?(multistream stream (\\d+))?",
re.IGNORECASE)
zeros = "000"
pls_modes = {v: k for k, v in PLS_MODE.items()}
for r in filter(lambda x: len(x) > 8, self._rows):
for frq in r[1], r[2], r[3]:
freq = re.match(frq_pol_pattern, frq)
if freq:
break
if not freq:
continue
frq, pol = freq.group(1), freq.group(2)
sr_fec = re.match(sr_fec_pattern, r[-3])
if not sr_fec:
continue
sr, fec, mod = sr_fec.group(1), sr_fec.group(2), sr_fec.group(3)
mod = mod.strip() if mod else "Auto"
res = re.match(sys_pattern, r[-4])
if not res:
continue
sys = res.group(1)
pls_mode = res.group(3)
pls_mode = pls_modes.get(pls_mode.capitalize(), None) if pls_mode else pls_mode
pls_code = res.group(4)
pls_id = res.group(6)
tr = Transponder(frq + zeros, sr + zeros, pol, fec, sys, mod, pls_mode, pls_code, pls_id)
if is_transponder_valid(tr):
trs.append(tr)
if __name__ == "__main__":
pass

8775
app/tools/vlc.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1 @@
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk
CODED_ICON = Gtk.IconTheme.get_default().load_icon("gtk-dialog-authentication-panel", 16, 0)
LOCKED_ICON = Gtk.IconTheme.get_default().load_icon("system-lock-screen", 16, 0)
HIDE_ICON = Gtk.IconTheme.get_default().load_icon("go-jump", 16, 0)
TV_ICON = Gtk.IconTheme.get_default().load_icon("tv-symbolic", 16, 0)
if __name__ == "__main__":
pass

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

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

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

@@ -0,0 +1,355 @@
<?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="GtkListStore" id="main_list_store">
<columns>
<!-- column-name date -->
<column type="gchararray"/>
<!-- column-name selected -->
<column type="gboolean"/>
</columns>
</object>
<object class="GtkWindow" id="dialog_window">
<property name="width_request">560</property>
<property name="height_request">320</property>
<property name="can_focus">False</property>
<property name="modal">True</property>
<property name="window_position">center-on-parent</property>
<property name="destroy_with_parent">True</property>
<property name="icon_name">document-revert</property>
<property name="gravity">center</property>
<signal name="check-resize" handler="on_resize" swapped="no"/>
<child type="titlebar">
<object class="GtkHeaderBar" id="header_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Backups</property>
<property name="spacing">2</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkBox" id="header_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkButton" id="restore_bouquets_header_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Restore bouquets</property>
<signal name="clicked" handler="on_restore_bouquets" swapped="no"/>
<child>
<object class="GtkImage" id="restore_bouquets_header_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-revert</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="restore_all_header_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Restore all</property>
<signal name="clicked" handler="on_restore_all" swapped="no"/>
<child>
<object class="GtkImage" id="restore_all_header_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-select-all</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="remove_header_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Remove</property>
<signal name="clicked" handler="on_remove" swapped="no"/>
<child>
<object class="GtkImage" id="remove_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">user-trash</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkCheckButton" id="info_check_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">False</property>
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
<child>
<object class="GtkImage" id="info_check_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-dialog-info</property>
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="main_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkPaned" id="main_paned">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="wide_handle">True</property>
<child>
<object class="GtkScrolledWindow" id="main_view_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="main_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="model">main_list_store</property>
<property name="headers_visible">False</property>
<property name="search_column">0</property>
<property name="rubber_banding">True</property>
<property name="activate_on_single_click">True</property>
<signal name="button-press-event" handler="on_view_popup_menu" object="popup_menu" swapped="no"/>
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
<signal name="key-release-event" handler="on_key_release" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="main_view_selection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="backup_date_column">
<property name="title" translatable="yes">Backup</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<property name="reorderable">True</property>
<property name="sort_column_id">0</property>
<child>
<object class="GtkCellRendererText" id="date_render">
<property name="xpad">10</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="text_view_scrolled_window">
<property name="can_focus">False</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="text_view">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="pixels_above_lines">5</property>
<property name="editable">False</property>
<property name="left_margin">10</property>
<property name="right_margin">10</property>
<property name="indent">10</property>
<property name="cursor_visible">False</property>
<property name="accepts_tab">False</property>
</object>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="can_focus">False</property>
<property name="show_close_button">True</property>
<signal name="response" handler="on_info_bar_close" swapped="no"/>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="spacing">6</property>
<property name="layout_style">end</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="spacing">16</property>
<child>
<object class="GtkLabel" id="message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">message</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkImage" id="restore_popup_menu_item_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-revert-to-saved</property>
</object>
<object class="GtkImage" id="restore_popup_menu_item_image2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-select-all</property>
</object>
<object class="GtkMenu" id="popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="restore_bouquets_popup_menu_item">
<property name="label" translatable="yes">Restore bouquets</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">restore_popup_menu_item_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_restore_bouquets" swapped="no"/>
<accelerator key="r" signal="activate" modifiers="GDK_CONTROL_MASK"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="restore_all_popup_menu_item">
<property name="label" translatable="yes">Restore all</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">restore_popup_menu_item_image2</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_restore_all" swapped="no"/>
<accelerator key="e" signal="activate" modifiers="GDK_CONTROL_MASK"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="popup_menu_separator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="remove_popup_menu_item">
<property name="label">gtk-remove</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<signal name="activate" handler="on_remove" swapped="no"/>
<accelerator key="Delete" signal="activate"/>
</object>
</child>
</object>
</interface>

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,694 @@
<?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="GtkWindow" id="download_dialog_window">
<property name="width_request">500</property>
<property name="can_focus">False</property>
<property name="resizable">False</property>
<property name="modal">True</property>
<property name="window_position">center-on-parent</property>
<property name="icon_name">mail-send-receive</property>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="gravity">center</property>
<child type="titlebar">
<object class="GtkHeaderBar" id="header_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkBox" id="header_left_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkButton" id="receive_button">
<property name="width_request">48</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Receive</property>
<signal name="clicked" handler="on_receive" swapped="no"/>
<child>
<object class="GtkImage" id="receive_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-goto-bottom</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="send_button">
<property name="width_request">48</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Send</property>
<signal name="clicked" handler="on_send" swapped="no"/>
<child>
<object class="GtkImage" id="send_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-goto-top</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="title">
<object class="GtkBox" id="header_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="margin_bottom">2</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="header_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">FTP-transfer</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="header_data_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="label10">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="all_radio_button">
<property name="label" translatable="yes">All</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">satellites_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="bouquets_radio_button">
<property name="label" translatable="yes">Bouquets</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">satellites_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="satellites_radio_button">
<property name="label" translatable="yes">Satellites</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">all_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="webtv_radio_button">
<property name="label" translatable="yes">WebTV</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">all_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="options_button">
<property name="width_request">48</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Options</property>
<signal name="clicked" handler="on_preferences" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-properties</property>
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="main_dialog_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="margin_bottom">1</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkFrame" id="main_settings_box_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkBox" id="main_settings_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkGrid" id="main_settings_bo">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">2</property>
<property name="column_spacing">2</property>
<property name="column_homogeneous">True</property>
<child>
<object class="GtkLabel" id="ip_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Receiver IP:</property>
<property name="xalign">0.10000000149011612</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="host_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="max_width_chars">10</property>
<property name="text">127.0.0.1</property>
<property name="caps_lock_warning">False</property>
<property name="primary_icon_name">network-transmit-receive-symbolic</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="data_path_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Current data path:</property>
<property name="xalign">0.10000000149011612</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="data_path_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="text">data/</property>
<property name="caps_lock_warning">False</property>
<property name="primary_icon_name">folder-open-symbolic</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="extra_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<child>
<object class="GtkCheckButton" id="remove_unused_check_button">
<property name="label" translatable="yes">Remove unused bouquets</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="use_http_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel" id="use_http_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Use HTTP</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="use_http_switch">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="tooltip_text" translatable="yes">Use http to reload data in the receiver.</property>
<property name="active">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="settings_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkGrid" id="settings_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="row_spacing">2</property>
<property name="column_spacing">2</property>
<child>
<object class="GtkLabel" id="login_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Login:</property>
<property name="xalign">0.10000000149011612</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="login_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="text">root</property>
<property name="caps_lock_warning">False</property>
<property name="primary_icon_name">avatar-default-symbolic</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="password_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Password:</property>
<property name="xalign">0.10000000149011612</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="password_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="visibility">False</property>
<property name="invisible_char">●</property>
<property name="text">root</property>
<property name="primary_icon_name">emblem-readonly</property>
<property name="input_purpose">password</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="port_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Port:</property>
<property name="xalign">0.10000000149011612</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="port_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="width_chars">8</property>
<property name="max_width_chars">8</property>
<property name="text" translatable="yes">21</property>
<property name="caps_lock_warning">False</property>
<property name="primary_icon_name">network-workgroup-symbolic</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="timeout_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="width_chars">8</property>
<property name="max_width_chars">8</property>
<property name="caps_lock_warning">False</property>
<property name="primary_icon_name">alarm-symbolic</property>
<property name="input_purpose">digits</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="timeout_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Timeout:</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">0</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkBox" id="settings_buttons_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkRadioButton" id="ftp_radio_button">
<property name="label" translatable="yes">FTP</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">False</property>
<property name="group">telnet_radio_button</property>
<signal name="toggled" handler="on_settings_button" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="http_radio_button">
<property name="label" translatable="yes">HTTP</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">False</property>
<property name="group">telnet_radio_button</property>
<signal name="toggled" handler="on_settings_button" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="telnet_radio_button">
<property name="label" translatable="yes">Telnet</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">False</property>
<property name="group">ftp_radio_button</property>
<signal name="toggled" handler="on_settings_button" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkExpander" id="expander">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="margin_bottom">1</property>
<property name="resize_toplevel">True</property>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="height_request">120</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="text_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="left_margin">5</property>
<property name="right_margin">5</property>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="expander_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Extra:</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="can_focus">False</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="margin_bottom">1</property>
<property name="show_close_button">True</property>
<signal name="response" handler="on_info_bar_close" swapped="no"/>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="homogeneous">True</property>
<property name="layout_style">expand</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="spacing">16</property>
<child>
<object class="GtkLabel" id="info_bar_message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Info</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View File

@@ -1,85 +1,160 @@
from gi.repository import GLib
from app.commons import run_idle, run_task
from app.ftp import download_data, DownloadDataType, upload_data
from . import Gtk
from .dialogs import show_dialog, DialogType
def show_download_dialog(transient, options, open_data):
dialog = DownloadDialog(transient, options, open_data)
dialog.run()
dialog.destroy()
from app.connections import download_data, DownloadType, upload_data
from app.properties import Profile, get_config
from app.ui.backup import backup_data, restore_data
from app.ui.main_helper import append_text_to_tview
from app.ui.settings_dialog import show_settings_dialog
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN
from .dialogs import show_dialog, DialogType, get_message
class DownloadDialog:
def __init__(self, transient, properties, open_data):
def __init__(self, transient, properties, open_data_callback, update_settings_callback, profile=Profile.ENIGMA_2):
self._profile_properties = properties.get(profile.value)
self._properties = properties
self._open_data = open_data
self._open_data_callback = open_data_callback
self._update_settings_callback = update_settings_callback
self._profile = profile
handlers = {"on_receive": self.on_receive,
"on_send": self.on_send,
"on_settings_button": self.on_settings_button,
"on_preferences": self.on_preferences,
"on_info_bar_close": self.on_info_bar_close}
builder = Gtk.Builder()
builder.add_objects_from_file("app/ui/dialogs.glade", ("download_dialog",))
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_from_file(UI_RESOURCES_PATH + "download_dialog.glade")
builder.connect_signals(handlers)
self._dialog = builder.get_object("download_dialog")
self._dialog.set_transient_for(transient)
self._current_property = "FTP"
self._dialog_window = builder.get_object("download_dialog_window")
self._dialog_window.set_transient_for(transient)
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label")
self._host_entry = builder.get_object("host_entry").set_text(properties["host"])
self._data_path_entry = builder.get_object("data_path_entry").set_text(properties["data_dir_path"])
self._text_view = builder.get_object("text_view")
self._expander = builder.get_object("expander")
self._host_entry = builder.get_object("host_entry")
self._data_path_entry = builder.get_object("data_path_entry")
self._remove_unused_check_button = builder.get_object("remove_unused_check_button")
self._all_radio_button = builder.get_object("all_radio_button")
self._bouquets_radio_button = builder.get_object("bouquets_radio_button")
self._satellites_radio_button = builder.get_object("satellites_radio_button")
# self._dialog.get_content_area().set_border_width(0)
self._webtv_radio_button = builder.get_object("webtv_radio_button")
self._login_entry = builder.get_object("login_entry")
self._password_entry = builder.get_object("password_entry")
self._host_entry = builder.get_object("host_entry")
self._port_entry = builder.get_object("port_entry")
self._timeout_entry = builder.get_object("timeout_entry")
self._settings_buttons_box = builder.get_object("settings_buttons_box")
self._use_http_switch = builder.get_object("use_http_switch")
self._http_radio_button = builder.get_object("http_radio_button")
self._use_http_box = builder.get_object("use_http_box")
self.init_properties()
def show(self):
self._dialog_window.show()
def init_properties(self):
self._host_entry.set_text(self._profile_properties["host"])
self._data_path_entry.set_text(self._profile_properties["data_dir_path"])
is_enigma = self._profile is Profile.ENIGMA_2
self._webtv_radio_button.set_visible(not is_enigma)
self._http_radio_button.set_visible(is_enigma)
self._use_http_box.set_visible(is_enigma)
self._use_http_switch.set_active(is_enigma)
@run_idle
def on_receive(self, item):
self.download(True, d_type=self.get_download_type())
self.download(True, self.get_download_type())
@run_idle
def on_send(self, item):
if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.CANCEL:
self.download(d_type=self.get_download_type())
if show_dialog(DialogType.QUESTION, self._dialog_window) != Gtk.ResponseType.CANCEL:
self.download(False, self.get_download_type())
def get_download_type(self):
download_type = DownloadDataType.ALL
download_type = DownloadType.ALL
if self._bouquets_radio_button.get_active():
download_type = DownloadDataType.BOUQUETS
download_type = DownloadType.BOUQUETS
elif self._satellites_radio_button.get_active():
download_type = DownloadDataType.SATELLITES
download_type = DownloadType.SATELLITES
elif self._webtv_radio_button.get_active():
download_type = DownloadType.WEB_TV
return download_type
def run(self):
return self._dialog.run()
def destroy(self):
self._dialog.destroy()
self._dialog_window.destroy()
def on_info_bar_close(self, *args):
def on_settings_button(self, button):
if button.get_active():
label = button.get_label()
if label == "Telnet":
self._login_entry.set_text(self._profile_properties.get("telnet_user", ""))
self._password_entry.set_text(self._profile_properties.get("telnet_password", ""))
self._port_entry.set_text(self._profile_properties.get("telnet_port", ""))
self._timeout_entry.set_text(str(self._profile_properties.get("telnet_timeout", 0)))
elif label == "HTTP":
self._login_entry.set_text(self._profile_properties.get("http_user", "root"))
self._password_entry.set_text(self._profile_properties.get("http_password", ""))
self._port_entry.set_text(self._profile_properties.get("http_port", ""))
self._timeout_entry.set_text(str(self._profile_properties.get("http_timeout", 0)))
elif label == "FTP":
self._login_entry.set_text(self._profile_properties.get("user", ""))
self._password_entry.set_text(self._profile_properties.get("password", ""))
self._port_entry.set_text(self._profile_properties.get("port", ""))
self._timeout_entry.set_text("")
self._current_property = label
def on_preferences(self, item):
show_settings_dialog(self._dialog_window, self._properties)
self._profile = Profile(self._properties.get("profile", Profile.ENIGMA_2.value))
self._profile_properties = get_config().get(self._profile.value)
self.init_properties()
self._update_settings_callback()
for button in self._settings_buttons_box.get_children():
if button.get_active():
self.on_settings_button(button)
break
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@run_idle
@run_task
def download(self, download=False, d_type=DownloadDataType.ALL):
def download(self, download, d_type):
""" Download/upload data from/to receiver """
self._expander.set_expanded(True)
self.clear_output()
backup, backup_src, data_path = self._profile_properties.get("backup_before_downloading", True), None, None
try:
if download:
download_data(properties=self._properties, download_type=d_type)
if backup and d_type is not DownloadType.SATELLITES:
data_path = self._profile_properties.get("data_dir_path", self._data_path_entry.get_text())
backup_path = self._profile_properties.get("backup_dir_path", data_path + "backup/")
backup_src = backup_data(data_path, backup_path, d_type is DownloadType.ALL)
download_data(properties=self._profile_properties, download_type=d_type, callback=self.append_output)
else:
self.show_info_message("Please, wait...", Gtk.MessageType.INFO)
upload_data(properties=self._properties,
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
upload_data(properties=self._profile_properties,
download_type=d_type,
remove_unused=self._remove_unused_check_button.get_active())
remove_unused=self._remove_unused_check_button.get_active(),
profile=self._profile,
callback=self.append_output,
done_callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO),
use_http=self._use_http_switch.get_active())
except Exception as e:
message = str(getattr(e, "message", str(e)))
self.show_info_message(message, Gtk.MessageType.ERROR)
if all((download, backup, data_path)):
restore_data(backup_src, data_path)
else:
self.show_info_message("Done!", Gtk.MessageType.INFO)
if download and d_type is not DownloadDataType.SATELLITES:
self._open_data()
if download and d_type is not DownloadType.SATELLITES:
GLib.idle_add(self._open_data_callback)
@run_idle
def show_info_message(self, text, message_type):
@@ -87,6 +162,14 @@ class DownloadDialog:
self._info_bar.set_message_type(message_type)
self._message_label.set_text(text)
@run_idle
def append_output(self, text):
append_text_to_tview(text, self._text_view)
@run_idle
def clear_output(self):
self._text_view.get_buffer().set_text("")
if __name__ == "__main__":
pass

1283
app/ui/epg_dialog.glade Normal file

File diff suppressed because it is too large Load Diff

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

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

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

@@ -0,0 +1,388 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1
The MIT License (MIT)
Copyright (c) 2018-2019 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-2019 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkListStore" id="main_list_store">
<columns>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name type -->
<column type="gchararray"/>
<!-- column-name selected -->
<column type="gboolean"/>
</columns>
</object>
<object class="GtkImage" id="remove_selection_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-undo</property>
</object>
<object class="GtkMenu" id="popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="select_all_popup_item">
<property name="label">gtk-select-all</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<signal name="activate" handler="on_select_all" object="main_view" swapped="no"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="unselect_all_popup_item">
<property name="label" translatable="yes">Remove selection</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">remove_selection_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_unselect_all" object="main_view" swapped="no"/>
</object>
</child>
</object>
<object class="GtkListStore" id="services_list_store">
<columns>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name type -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkWindow" id="dialog_window">
<property name="can_focus">False</property>
<property name="modal">True</property>
<property name="window_position">center-on-parent</property>
<property name="default_width">480</property>
<property name="default_height">320</property>
<property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property>
<property name="gravity">center</property>
<child type="titlebar">
<object class="GtkHeaderBar" id="header_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Import</property>
<property name="subtitle" translatable="yes">Bouquets and services</property>
<property name="spacing">2</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkButton" id="import_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Import</property>
<signal name="clicked" handler="on_import" swapped="no"/>
<child>
<object class="GtkImage" id="import_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-revert-to-saved</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkCheckButton" id="info_check_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">False</property>
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
<child>
<object class="GtkImage" id="info_check_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-dialog-info</property>
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="main_box">
<property name="width_request">480</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="margin_bottom">1</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkPaned" id="main_paned">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="wide_handle">True</property>
<child>
<object class="GtkBox" id="bouquets_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">2</property>
<property name="margin_bottom">2</property>
<property name="label" translatable="yes">Bouquets</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="bouquets_screlled_window">
<property name="width_request">200</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="main_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">main_list_store</property>
<property name="headers_clickable">False</property>
<property name="search_column">0</property>
<signal name="button-press-event" handler="on_popup_menu" object="popup_menu" swapped="no"/>
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
<signal name="select-all" handler="on_select_all" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="bouquet_name_column">
<property name="title" translatable="yes">Name</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="bq_name_renderer">
<property name="xalign">0.019999999552965164</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="bouquet_type_column">
<property name="title" translatable="yes">Type</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="bq_type_renderer">
<property name="xalign">0.50999999046325684</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="bouquet_selected_column">
<property name="title" translatable="yes">Selected</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererToggle" id="bq_selected_renderer">
<property name="xalign">0.50999999046325684</property>
<signal name="toggled" handler="on_selected_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">2</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<object class="GtkBox" id="services_box">
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">2</property>
<property name="margin_bottom">2</property>
<property name="label" translatable="yes">Bouquet details</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="services_view_scrolled_window">
<property name="width_request">150</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="services_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">services_list_store</property>
<property name="headers_clickable">False</property>
<property name="search_column">0</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="service_name_column">
<property name="title" translatable="yes">Service</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="info_name_renderer">
<property name="xalign">0.019999999552965164</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="service_type_column">
<property name="title" translatable="yes">Type</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="info_type_renderer">
<property name="xalign">0.50999999046325684</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="can_focus">False</property>
<property name="show_close_button">True</property>
<signal name="response" handler="on_info_bar_close" swapped="no"/>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="spacing">6</property>
<property name="layout_style">end</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="spacing">16</property>
<child>
<object class="GtkLabel" id="message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">message</property>
<property name="wrap">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

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

@@ -0,0 +1,224 @@
from contextlib import suppress
from pathlib import Path
from app.commons import run_idle
from app.eparser import get_bouquets, get_services
from app.eparser.ecommons import BqType, BqServiceType, Bouquet
from app.eparser.enigma.bouquets import get_bouquet
from app.eparser.neutrino.bouquets import parse_webtv, parse_bouquets as get_neutrino_bouquets
from app.properties import Profile
from app.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, get_message
from app.ui.main_helper import on_popup_menu
from .uicommons import Gtk, UI_RESOURCES_PATH, KeyboardKey, Column
def import_bouquet(transient, profile, model, path, options, services, appender):
""" Import of single bouquet """
itr = model.get_iter(path)
bq_type = BqType(model.get(itr, Column.BQ_TYPE)[0])
pattern, f_pattern = None, None
if profile is Profile.ENIGMA_2:
pattern = ".{}".format(bq_type.value)
f_pattern = "userbouquet.*{}".format(pattern)
elif profile is Profile.NEUTRINO_MP:
pattern = "webtv.xml" if bq_type is BqType.WEBTV else "bouquets.xml"
f_pattern = "bouquets.xml"
if bq_type is BqType.TV:
f_pattern = "ubouquets.xml"
elif bq_type is BqType.WEBTV:
f_pattern = "webtv.xml"
file_path = get_chooser_dialog(transient, options, f_pattern, "bouquet files")
if file_path == Gtk.ResponseType.CANCEL:
return
if not str(file_path).endswith(pattern):
show_dialog(DialogType.ERROR, transient, text="No bouquet file is selected!")
return
if profile is Profile.ENIGMA_2:
bq = get_enigma2_bouquet(file_path)
imported = list(filter(lambda x: x.data in services or x.type is BqServiceType.IPTV, bq.services))
if len(imported) == 0:
show_dialog(DialogType.ERROR, transient, text="The main list does not contain services for this bouquet!")
return
if model.iter_n_children(itr):
appender(bq, itr)
else:
p_itr = model.iter_parent(itr)
appender(bq, p_itr) if p_itr else appender(bq, itr)
elif profile is Profile.NEUTRINO_MP:
if bq_type is BqType.WEBTV:
bqs = parse_webtv(file_path, "WEBTV", bq_type.value)
else:
bqs = get_neutrino_bouquets(file_path, "", bq_type.value)
file_path = "{}/".format(Path(file_path).parent)
ImportDialog(transient, file_path, profile, services.keys(), lambda b, s: appender(b), (bqs,)).show()
def get_enigma2_bouquet(path):
path, sep, f_name = path.rpartition("userbouquet.")
name, sep, suf = f_name.rpartition(".")
bq = get_bouquet(path, name, suf)
bouquet = Bouquet(name=bq[0], type=BqType(suf).value, services=bq[1], locked=None, hidden=None)
return bouquet
class ImportDialog:
def __init__(self, transient, path, profile, service_ids, appender, bouquets=None):
handlers = {"on_import": self.on_import,
"on_cursor_changed": self.on_cursor_changed,
"on_info_button_toggled": self.on_info_button_toggled,
"on_selected_toggled": self.on_selected_toggled,
"on_info_bar_close": self.on_info_bar_close,
"on_select_all": self.on_select_all,
"on_unselect_all": self.on_unselect_all,
"on_popup_menu": on_popup_menu,
"on_key_press": self.on_key_press}
builder = Gtk.Builder()
builder.set_translation_domain("demon-editor")
builder.add_from_file(UI_RESOURCES_PATH + "import_dialog.glade")
builder.connect_signals(handlers)
self._bq_services = {}
self._services = {}
self._service_ids = service_ids
self._append = appender
self._profile = profile
self._bouquets = bouquets
self._dialog_window = builder.get_object("dialog_window")
self._dialog_window.set_transient_for(transient)
self._main_model = builder.get_object("main_list_store")
self._main_view = builder.get_object("main_view")
self._services_view = builder.get_object("services_view")
self._services_model = builder.get_object("services_list_store")
self._services_box = builder.get_object("services_box")
self._info_check_button = builder.get_object("info_check_button")
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("message_label")
self.init_data(path)
def show(self):
self._dialog_window.show()
@run_idle
def init_data(self, path):
self._main_model.clear()
self._services_model.clear()
try:
if not self._bouquets:
self._bouquets = get_bouquets(path, self._profile)
for bqs in self._bouquets:
for bq in bqs.bouquets:
self._main_model.append((bq.name, bq.type, True))
self._bq_services[(bq.name, bq.type)] = bq.services
# Note! Getting default format ver. 4
services = get_services(path, self._profile, 4 if self._profile is Profile.ENIGMA_2 else 0)
for srv in services:
self._services[srv.fav_id] = srv
except FileNotFoundError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
def on_import(self, item):
if not any(r[-1] for r in self._main_model):
self.show_info_message(get_message("No selected item!"), Gtk.MessageType.ERROR)
return
if not self._bouquets or show_dialog(DialogType.QUESTION, self._dialog_window) == Gtk.ResponseType.CANCEL:
return
services = set()
to_delete = set()
for row in self._main_model:
bq = (row[0], row[1])
if row[-1]:
for bq_srv in self._bq_services.get(bq, []):
srv = self._services.get(bq_srv.data, None)
if srv:
services.add(srv)
else:
to_delete.add(bq)
bqs_to_delete = []
for bqs in self._bouquets:
for bq in bqs.bouquets:
if (bq.name, bq.type) in to_delete:
bqs_to_delete.append(bq)
for bqs in self._bouquets:
bq = bqs.bouquets
for b in bqs_to_delete:
with suppress(ValueError):
bq.remove(b)
self._append(self._bouquets, list(filter(lambda s: s.fav_id not in self._service_ids, services)))
self._dialog_window.destroy()
@run_idle
def on_cursor_changed(self, view):
if not self._info_check_button.get_active():
return
self._services_model.clear()
model, paths = view.get_selection().get_selected_rows()
if not paths:
return
bq_services = self._bq_services.get(model.get(model.get_iter(paths[0]), 0, 1))
for bq_srv in bq_services:
if bq_srv.type is BqServiceType.DEFAULT:
srv = self._services.get(bq_srv.data, None)
if srv:
self._services_model.append((srv.service, srv.service_type))
else:
self._services_model.append((bq_srv.name, bq_srv.type.value))
def on_info_button_toggled(self, button):
active = button.get_active()
self._services_box.set_visible(active)
def on_selected_toggled(self, toggle, path):
self._main_model.set_value(self._main_model.get_iter(path), 2, not toggle.get_active())
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(text)
@run_idle
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
def on_select_all(self, view):
self.update_selection(view, True)
def on_unselect_all(self, view):
self.update_selection(view, False)
def update_selection(self, view, select):
view.get_model().foreach(lambda mod, path, itr: mod.set_value(itr, 2, select))
def on_key_press(self, view, event):
""" Handling keystrokes """
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
if key is KeyboardKey.SPACE:
path, column = view.get_cursor()
itr = self._main_model.get_iter(path)
selected = self._main_model.get_value(itr, 2)
self._main_model.set_value(itr, 2, not selected)
if __name__ == "__main__":
pass

1141
app/ui/iptv.glade Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,474 @@
import re
import urllib
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 .dialogs import Action, show_dialog, DialogType, get_dialogs_string
from .main_helper import get_base_model, get_iptv_url
from .uicommons import Gtk, Gdk, TEXT_DOMAIN, UI_RESOURCES_PATH, IPTV_ICON, Column, IS_GNOME_SESSION
_DIGIT_ENTRY_NAME = "digit-entry"
_ENIGMA2_REFERENCE = "{}:0:{}:{:X}:{:X}:{:X}:{:X}:0:0:0"
_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
_UI_PATH = UI_RESOURCES_PATH + "iptv.glade"
def is_data_correct(elems):
for elem in elems:
if elem.get_name() == _DIGIT_ENTRY_NAME:
return False
return True
def get_stream_type(box):
active = box.get_active()
if active == 0:
return StreamType.DVB_TS.value
elif active == 1:
return StreamType.NONE_TS.value
elif active == 2:
return StreamType.NONE_REC_1.value
return StreamType.NONE_REC_2.value
class IptvDialog:
def __init__(self, transient, view, services, bouquet, profile=Profile.ENIGMA_2, action=Action.ADD):
handlers = {"on_response": self.on_response,
"on_entry_changed": self.on_entry_changed,
"on_url_changed": self.on_url_changed,
"on_save": self.on_save,
"on_stream_type_changed": self.on_stream_type_changed}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("iptv_dialog", "stream_type_liststore"))
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_response(self, dialog, response):
if response == Gtk.ResponseType.CANCEL:
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) < 11:
return
s_type = data[0].strip()
try:
stream_type = StreamType(s_type)
if stream_type is StreamType.DVB_TS:
self._stream_type_combobox.set_active(0)
elif stream_type is StreamType.NONE_TS:
self._stream_type_combobox.set_active(1)
elif stream_type is StreamType.NONE_REC_1:
self._stream_type_combobox.set_active(2)
elif stream_type is StreamType.NONE_REC_2:
self._stream_type_combobox.set_active(3)
except ValueError:
show_dialog(DialogType.ERROR, "Unknown stream type {}".format(s_type))
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(urllib.request.unquote(data[10].strip()))
self._update_reference_entry()
def init_neutrino_data(self, fav_id):
data = fav_id.split("::")
self._url_entry.set_text(data[0])
self._description_entry.set_text(data[1])
def _update_reference_entry(self):
if self._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 get_stream_type(self._stream_type_combobox)
def on_entry_changed(self, entry):
if _PATTERN.search(entry.get_text()):
entry.set_name(_DIGIT_ENTRY_NAME)
else:
entry.set_name("GtkEntry")
self._update_reference_entry()
def on_url_changed(self, entry):
url = 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()),
urllib.request.quote(self._url_entry.get_text()),
name, name)
self.update_bouquet_data(name, fav_id)
def save_neutrino_data(self):
if self._action is Action.EDIT:
id_data = self._current_srv[7].split("::")
else:
id_data = ["", "", "0", None, None, None, None, "", "", "1"]
id_data[0] = self._url_entry.get_text()
id_data[1] = self._description_entry.get_text()
self.update_bouquet_data(self._name_entry.get_text(), NEUTRINO_FAV_ID_FORMAT.format(*id_data))
self._dialog.destroy()
def update_bouquet_data(self, name, fav_id):
if self._action is Action.EDIT:
old_srv = self._services.pop(self._current_srv[7])
self._services[fav_id] = old_srv._replace(service=name, fav_id=fav_id)
self._bouquet[self._paths[0][0]] = fav_id
self._model.set(self._model.get_iter(self._paths), {Column.FAV_SERVICE: name, Column.FAV_ID: fav_id})
else:
aggr = [None] * 10
s_type = BqServiceType.IPTV.name
srv = (None, None, name, None, None, s_type, None, fav_id, *aggr[0:3])
itr = self._model.insert_after(self._model.get_iter(self._paths[0]),
srv) if self._paths else self._model.insert(0, srv)
self._model.set_value(itr, 1, IPTV_ICON)
self._bouquet.insert(self._model.get_path(itr)[0], fav_id)
self._services[fav_id] = Service(None, None, IPTV_ICON, name, *aggr[0:3], s_type, *aggr, fav_id, None)
class SearchUnavailableDialog:
def __init__(self, transient, model, fav_bouquet, iptv_rows, profile):
handlers = {"on_response": self.on_response}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "iptv.glade", ("search_unavailable_streams_dialog",))
builder.connect_signals(handlers)
self._dialog = builder.get_object("search_unavailable_streams_dialog")
self._dialog.set_transient_for(transient)
self._model = model
self._counter_label = builder.get_object("streams_rows_counter_label")
self._level_bar = builder.get_object("unavailable_streams_level_bar")
self._bouquet = fav_bouquet
self._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()
return self._to_delete if response not in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT) else False
def on_response(self, dialog, response):
if response == Gtk.ResponseType.CANCEL:
self.on_close()
@run_idle
def on_close(self):
if self._download_task and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
self._download_task = False
self._dialog.destroy()
class IptvListConfigurationDialog:
def __init__(self, transient, services, iptv_rows, bouquet, fav_model, profile):
handlers = {"on_apply": self.on_apply,
"on_response": self.on_response,
"on_stream_type_default_togged": self.on_stream_type_default_togged,
"on_stream_type_changed": self.on_stream_type_changed,
"on_default_type_toggled": self.on_default_type_toggled,
"on_auto_sid_toggled": self.on_auto_sid_toggled,
"on_default_tid_toggled": self.on_default_tid_toggled,
"on_default_nid_toggled": self.on_default_nid_toggled,
"on_default_namespace_toggled": self.on_default_namespace_toggled,
"on_reset_to_default": self.on_reset_to_default,
"on_entry_changed": self.on_entry_changed,
"on_info_bar_close": self.on_info_bar_close}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("iptv_list_configuration_dialog", "stream_type_liststore"))
builder.connect_signals(handlers)
self._rows = iptv_rows
self._services = services
self._bouquet = bouquet
self._fav_model = fav_model
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()
def on_response(self, dialog, response):
if response == Gtk.ResponseType.CANCEL:
self._dialog.destroy()
def on_stream_type_changed(self, box):
self.update_reference()
def on_stream_type_default_togged(self, button):
if button.get_active():
self._stream_type_combobox.set_active(1)
self._stream_type_combobox.set_sensitive(not button.get_active())
def on_default_type_toggled(self, button):
if button.get_active():
self._list_srv_type_entry.set_text("1")
self._list_srv_type_entry.set_sensitive(not button.get_active())
def on_auto_sid_toggled(self, button):
if button.get_active():
self._list_sid_entry.set_text("0")
self._list_sid_entry.set_sensitive(not button.get_active())
def on_default_tid_toggled(self, button):
if button.get_active():
self._list_tid_entry.set_text("0")
self._list_tid_entry.set_sensitive(not button.get_active())
def on_default_nid_toggled(self, button):
if button.get_active():
self._list_nid_entry.set_text("0")
self._list_nid_entry.set_sensitive(not button.get_active())
def on_default_namespace_toggled(self, button):
if button.get_active():
self._list_namespace_entry.set_text("0")
self._list_namespace_entry.set_sensitive(not button.get_active())
@run_idle
def on_reset_to_default(self, item, active):
item.set_sensitive(not active)
self._stream_type_combobox.set_active(1)
self._list_srv_type_entry.set_text("1")
for el in (self._list_sid_entry, self._list_nid_entry, self._list_tid_entry, self._list_namespace_entry):
el.set_text("0")
for el in (self._stream_type_check_button, self._type_check_button, self._sid_auto_check_button,
self._tid_check_button, self._nid_check_button, self._namespace_check_button):
el.set_active(True)
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@run_idle
def on_apply(self, item):
if not is_data_correct(self._digit_elems):
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
if self._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()
stream_type = StreamType.NONE_TS.value if reset else get_stream_type(self._stream_type_combobox)
srv_type = "1" if type_default else self._list_srv_type_entry.get_text()
tid = "0" if tid_default else "{:X}".format(int(self._list_tid_entry.get_text()))
nid = "0" if nid_default else "{:X}".format(int(self._list_nid_entry.get_text()))
namespace = "0" if namespace_default else "{:X}".format(int(self._list_namespace_entry.get_text()))
for index, row in enumerate(self._rows):
fav_id = row[Column.FAV_ID]
data, sep, desc = fav_id.partition("http")
data = data.split(":")
if reset:
data[2], data[3], data[4], data[5], data[6] = "10000"
else:
data[0], data[2], data[4], data[5], data[6] = stream_type, srv_type, tid, nid, namespace
data[3] = "{:X}".format(index) if sid_auto else "0"
data = ":".join(data)
new_fav_id = "{}{}{}".format(data, sep, desc)
row[Column.FAV_ID] = new_fav_id
srv = self._services.pop(fav_id, None)
self._services[new_fav_id] = srv._replace(fav_id=new_fav_id)
self._bouquet.clear()
list(map(lambda r: self._bouquet.append(r[Column.FAV_ID]), self._fav_model))
self._info_bar.set_visible(True)
@run_idle
def update_reference(self):
if is_data_correct(self._digit_elems):
stream_type = get_stream_type(self._stream_type_combobox)
self._reference_label.set_text(
_ENIGMA2_REFERENCE.format(stream_type, *[int(elem.get_text()) for elem in self._digit_elems]))
def on_entry_changed(self, entry):
if _PATTERN.search(entry.get_text()):
entry.set_name(_DIGIT_ENTRY_NAME)
else:
entry.set_name("GtkEntry")
self.update_reference()
if __name__ == "__main__":
pass

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

605
app/ui/main_helper.py Normal file
View File

@@ -0,0 +1,605 @@
""" This is helper module for ui """
import os
import shutil
import urllib.request
from gi.repository import GdkPixbuf, GLib
from app.commons import run_task
from app.eparser import Service
from app.eparser.ecommons import Flag, BouquetService, Bouquet, BqType
from app.eparser.enigma.bouquets import BqServiceType, to_bouquet_id
from app.properties import Profile
from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON, KeyboardKey, Column
from .dialogs import show_dialog, DialogType, get_chooser_dialog, WaitDialog
# ***************** Markers *******************#
def insert_marker(view, bouquets, selected_bouquet, channels, parent_window):
"""" Inserts marker into bouquet services list. """
response = show_dialog(DialogType.INPUT, parent_window)
if response == Gtk.ResponseType.CANCEL:
return
if not response.strip():
show_dialog(DialogType.ERROR, parent_window, "The text of marker is empty, please try again!")
return
# Searching for max num value in all marker services (if empty default = 0)
max_num = max(map(lambda num: int(num.data_id, 16),
filter(lambda ch: ch.service_type == BqServiceType.MARKER.name, channels.values())), default=0)
max_num = '{:X}'.format(max_num + 1)
fav_id = "1:64:{}:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n".format(max_num, response, response)
s_type = BqServiceType.MARKER.name
model, paths = view.get_selection().get_selected_rows()
marker = (None, None, response, None, None, s_type, None, fav_id, None, None, None)
itr = model.insert_before(model.get_iter(paths[0]), marker) if paths else model.insert(0, marker)
bouquets[selected_bouquet].insert(model.get_path(itr)[0], fav_id)
channels[fav_id] = Service(None, None, None, response, None, None, None, s_type, *[None] * 9, max_num, fav_id, None)
# ***************** Movement *******************#
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:
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
if type(model) is Gtk.TreeStore:
parent_paths = list(filter(lambda p: p.get_depth() == 1, paths))
if parent_paths:
paths = parent_paths
min_path = model.get_path(model.get_iter_first())
view.collapse_all()
if mod_length == len(paths):
return
else:
if not is_some_level(paths):
return
parent_itr = model.iter_parent(model.get_iter(paths[0]))
parent_index = model.get_path(parent_itr)
children_num = model.iter_n_children(parent_itr)
if key in (KeyboardKey.PAGE_DOWN, KeyboardKey.END, KeyboardKey.END_KP, KeyboardKey.PAGE_DOWN_KP):
children_num -= 1
min_path = Gtk.TreePath.new_from_string("{}:{}".format(parent_index, 0))
max_path = Gtk.TreePath.new_from_string("{}:{}".format(parent_index, children_num))
is_tree_store = True
if key is KeyboardKey.UP:
top_path = Gtk.TreePath(paths[0])
set_cursor(top_path, paths, selection, view)
top_path.prev()
move_up(top_path, model, paths)
elif key is KeyboardKey.DOWN:
down_path = Gtk.TreePath(paths[-1])
set_cursor(down_path, paths, selection, view)
down_path.next()
if down_path < max_path:
move_down(down_path, model, paths)
else:
max_path.prev()
move_down(max_path, model, paths)
elif key in (KeyboardKey.PAGE_UP, KeyboardKey.HOME, KeyboardKey.PAGE_UP_KP, KeyboardKey.HOME_KP):
move_up(min_path if is_tree_store else cursor_path, model, paths)
elif key in (KeyboardKey.PAGE_DOWN, KeyboardKey.END, KeyboardKey.END_KP, KeyboardKey.PAGE_DOWN_KP):
move_down(max_path if is_tree_store else cursor_path, model, paths)
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 move_down(down_path, model, paths):
top_iter = model.get_iter(down_path)
for path in reversed(paths):
itr = model.get_iter(path)
model.move_after(itr, top_iter)
down_path.prev()
top_iter = model.get_iter(down_path)
def is_some_level(paths):
for i in range(1, len(paths)):
prev = paths[i - 1]
current = paths[i]
if len(prev) != len(current) or (len(prev) == 2 and len(current) == 2 and prev[0] != current[0]):
return
return True
def set_cursor(dest_path, paths, selection, view):
view.set_cursor(dest_path, view.get_column(0), False)
for p in paths:
selection.select_path(p)
# ***************** Rename *******************#
def rename(view, parent_window, target, fav_view=None, service_view=None, services=None):
selection = get_selection(view, parent_window)
if not selection:
return
model, paths = selection
itr = model.get_iter(paths)
f_id, srv_name, srv_type = None, None, None
if target is ViewTarget.SERVICES:
name, fav_id = model.get(itr, Column.SRV_SERVICE, Column.SRV_FAV_ID)
f_id = fav_id
response = show_dialog(DialogType.INPUT, parent_window, name)
if response == Gtk.ResponseType.CANCEL:
return
srv_name = response
model.set_value(itr, Column.SRV_SERVICE, response)
if fav_view is not None:
for row in fav_view.get_model():
if row[Column.FAV_ID] == fav_id:
row[Column.FAV_SERVICE] = response
break
elif target is ViewTarget.FAV:
name, srv_type, fav_id = model.get(itr, Column.FAV_SERVICE, Column.FAV_TYPE, Column.FAV_ID)
f_id = fav_id
response = show_dialog(DialogType.INPUT, parent_window, name)
if response == Gtk.ResponseType.CANCEL:
return
srv_name = response
model.set_value(itr, Column.FAV_SERVICE, response)
if service_view is not None:
for row in get_base_model(service_view.get_model()):
if row[Column.SRV_FAV_ID] == fav_id:
row[Column.SRV_SERVICE] = response
break
old_srv = services.get(f_id, None)
if old_srv:
if srv_type == BqServiceType.IPTV.name or srv_type == BqServiceType.MARKER.name:
l, sep, r = f_id.partition("#DESCRIPTION")
old_name = old_srv.service.strip()
new_name = srv_name.strip()
new_fav_id = "".join((new_name.join(l.rsplit(old_name, 1)), sep, new_name.join(r.rsplit(old_name, 1))))
services[f_id] = old_srv._replace(service=srv_name, fav_id=new_fav_id)
else:
services[f_id] = old_srv._replace(service=srv_name)
def get_selection(view, parent):
""" Returns (model, paths) if possible """
model, paths = view.get_selection().get_selected_rows()
model = get_base_model(model)
if not paths:
return
elif len(paths) > 1:
show_dialog(DialogType.ERROR, parent, "Please, select only one item!")
return
return model, paths
# ***************** Flags *******************#
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:
return
model, paths = None, None
if target is ViewTarget.SERVICES:
model, paths = services_view.get_selection().get_selected_rows()
elif target is ViewTarget.FAV:
model, paths = fav_view.get_selection().get_selected_rows()
if not paths:
return
model = get_base_model(model)
if flag is Flag.HIDE:
if target is ViewTarget.SERVICES:
set_hide(services, model, paths)
else:
fav_ids = [model.get_value(model.get_iter(path), Column.FAV_ID) for path in paths]
srv_model = get_base_model(services_view.get_model())
srv_paths = [row.path for row in srv_model if row[Column.SRV_FAV_ID] in fav_ids]
set_hide(services, srv_model, srv_paths)
elif flag is Flag.LOCK:
set_lock(blacklist, services, model, paths, target, services_model=get_base_model(services_view.get_model()))
update_fav_model(fav_view, services)
def update_fav_model(fav_view, services):
for row in get_base_model(fav_view.get_model()):
srv = services.get(row[Column.FAV_ID], None)
if srv:
row[Column.FAV_LOCKED], row[Column.FAV_HIDE] = srv.locked, srv.hide
def set_lock(blacklist, services, model, paths, target, services_model):
col_num = Column.SRV_LOCKED if target is ViewTarget.SERVICES else Column.FAV_LOCKED
locked = has_locked_hide(model, paths, col_num)
ids = []
for path in paths:
itr = model.get_iter(path)
fav_id = model.get_value(itr, Column.SRV_FAV_ID if target is ViewTarget.SERVICES else Column.FAV_ID)
srv = services.get(fav_id, None)
if srv:
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)
services[fav_id] = srv._replace(locked=None if locked else LOCKED_ICON)
ids.append(fav_id)
if target is ViewTarget.FAV and ids:
gen = update_services_model(ids, locked, services_model)
GLib.idle_add(lambda: next(gen, False))
def update_services_model(ids, locked, services_model):
for srv in services_model:
if srv[Column.SRV_FAV_ID] in ids:
srv[Column.SRV_LOCKED] = None if locked else LOCKED_ICON
yield True
def set_hide(services, model, paths):
col_num = Column.SRV_HIDE
hide = has_locked_hide(model, paths, col_num)
for path in paths:
itr = model.get_iter(path)
model.set_value(itr, col_num, None if hide else HIDE_ICON)
flags = [*model.get_value(itr, 0).split(",")]
index, flag = None, None
for i, fl in enumerate(flags):
if fl.startswith("f:"):
index = i
flag = fl
break
value = int(flag[2:]) if flag else 0
if not hide:
if Flag.is_hide(value):
continue # skip if already hidden
value += Flag.HIDE.value
else:
if not Flag.is_hide(value):
continue # skip if already allowed to show
value -= Flag.HIDE.value
if value == 0 and index is not None:
del flags[index]
else:
value = "f:{:02d}".format(value)
if index is not None:
flags[index] = value
else:
flags.append(value)
model.set_value(itr, 0, (",".join(reversed(sorted(flags)))))
fav_id = model.get_value(itr, Column.SRV_FAV_ID)
srv = services.get(fav_id, None)
if srv:
services[fav_id] = srv._replace(hide=None if hide else HIDE_ICON)
def has_locked_hide(model, paths, col_num):
for path in paths:
if model.get_value(model.get_iter(path), col_num):
return True
return False
# ***************** Location *******************#
def locate_in_services(fav_view, services_view, parent_window):
""" Locating and scrolling to the service """
model, paths = fav_view.get_selection().get_selected_rows()
if not paths:
return
elif len(paths) > 1:
show_dialog(DialogType.ERROR, parent_window, "Please, select only one item!")
return
fav_id = model.get_value(model.get_iter(paths[0]), Column.FAV_ID)
for index, row in enumerate(services_view.get_model()):
if row[Column.SRV_FAV_ID] == fav_id:
scroll_to(index, services_view)
break
def scroll_to(index, view, paths=None):
""" Scrolling to and selecting given index(path) """
if paths is not None:
view.expand_row(paths[0], 0)
view.scroll_to_cell(index, None)
selection = view.get_selection()
selection.unselect_all()
selection.select_path(index)
# ***************** Picons *********************#
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)
def append_picons(picons, model):
def append_picons_data(pcs, mod):
for r in mod:
mod.set_value(mod.get_iter(r.path), Column.SRV_PICON, pcs.get(r[Column.SRV_PICON_ID], None))
yield True
app = append_picons_data(picons, model)
GLib.idle_add(lambda: next(app, False), priority=GLib.PRIORITY_LOW)
def assign_picon(target, srv_view, fav_view, transient, picons, options, services):
view = srv_view if target is ViewTarget.SERVICES else fav_view
model, paths = view.get_selection().get_selected_rows()
if not is_only_one_item_selected(paths, transient):
return
response = get_chooser_dialog(transient, options, "*.png", "png files")
if response == Gtk.ResponseType.CANCEL:
return
if not str(response).endswith(".png"):
show_dialog(DialogType.ERROR, transient, text="No png file is selected!")
return
picon_pos = Column.SRV_PICON
model = get_base_model(model)
itr = model.get_iter(paths)
fav_id = model.get_value(itr, Column.SRV_FAV_ID if target is ViewTarget.SERVICES else Column.FAV_ID)
picon_id = services.get(fav_id)[Column.SRV_PICON_ID]
if picon_id:
picon_file = options.get("picons_dir_path") + picon_id
if os.path.isfile(response):
shutil.copy(response, picon_file)
picon = get_picon_pixbuf(picon_file)
picons[picon_id] = picon
model.set_value(itr, picon_pos, picon)
if target is ViewTarget.SERVICES:
set_picon(fav_id, fav_view.get_model(), picon, Column.FAV_ID, picon_pos)
else:
set_picon(fav_id, get_base_model(srv_view.get_model()), picon, Column.SRV_FAV_ID, picon_pos)
def set_picon(fav_id, model, picon, fav_id_pos, picon_pos):
for row in model:
if row[fav_id_pos] == fav_id:
row[picon_pos] = picon
break
def remove_picon(target, srv_view, fav_view, picons, options):
view = srv_view if target is ViewTarget.SERVICES else fav_view
model, paths = view.get_selection().get_selected_rows()
model = get_base_model(model)
fav_ids = []
picon_ids = []
picon_pos = Column.SRV_PICON # picon position is equal for services and fav
for path in paths:
itr = model.get_iter(path)
model.set_value(itr, picon_pos, None)
if target is ViewTarget.SERVICES:
fav_ids.append(model.get_value(itr, Column.SRV_FAV_ID))
picon_ids.append(model.get_value(itr, Column.SRV_PICON_ID))
else:
srv_type, fav_id = model.get(itr, Column.FAV_TYPE, Column.FAV_ID)
if srv_type == BqServiceType.IPTV.name:
picon_ids.append("{}_{}_{}_{}_{}_{}_{}_{}_{}_{}.png".format(*fav_id.split(":")[0:10]).strip())
else:
fav_ids.append(fav_id)
def remove(md, path, it):
if md.get_value(it, Column.FAV_ID if target is ViewTarget.SERVICES else Column.SRV_FAV_ID) in fav_ids:
md.set_value(it, picon_pos, None)
if target is ViewTarget.FAV:
picon_ids.append(md.get_value(it, Column.SRV_PICON_ID))
fav_view.get_model().foreach(remove) if target is ViewTarget.SERVICES else get_base_model(
srv_view.get_model()).foreach(remove)
remove_picons(options, picon_ids, picons)
def copy_picon_reference(target, view, services, clipboard, transient):
""" Copying picon id to clipboard """
model, paths = view.get_selection().get_selected_rows()
if not is_only_one_item_selected(paths, transient):
return
if target is ViewTarget.SERVICES:
picon_id = model.get_value(model.get_iter(paths), Column.SRV_PICON_ID)
if picon_id:
clipboard.set_text(picon_id.rstrip(".png"), -1)
else:
show_dialog(DialogType.ERROR, transient, "No reference is present!")
elif target is ViewTarget.FAV:
fav_id = model.get_value(model.get_iter(paths), Column.FAV_ID)
srv = services.get(fav_id, None)
if srv and srv.picon_id:
clipboard.set_text(srv.picon_id.rstrip(".png"), -1)
else:
show_dialog(DialogType.ERROR, transient, "No reference is present!")
def remove_all_unused_picons(options, picons, services):
ids = {s.picon_id for s in services}
pcs = list(filter(lambda x: x not in ids, picons))
remove_picons(options, pcs, picons)
def remove_picons(options, picon_ids, picons):
pions_path = options.get("picons_dir_path")
backup_path = options.get("backup_dir_path") + "picons/"
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
for p_id in picon_ids:
picons[p_id] = None
src = pions_path + p_id
if os.path.isfile(src):
shutil.move(src, backup_path + p_id)
def is_only_one_item_selected(paths, transient):
if len(paths) > 1:
show_dialog(DialogType.ERROR, transient, "Please, select only one item!")
return False
if not paths:
show_dialog(DialogType.ERROR, transient, "No selected item!")
return False
return True
def get_picon_pixbuf(path):
return GdkPixbuf.Pixbuf.new_from_file_at_scale(filename=path, width=32, height=32, preserve_aspect_ratio=True)
# ***************** Bouquets *********************#
def gen_bouquets(view, bq_view, transient, gen_type, tv_types, profile, callback):
""" Auto-generate and append list of bouquets """
fav_id_index = Column.SRV_FAV_ID
index = Column.SRV_TYPE
if gen_type in (BqGenType.PACKAGE, BqGenType.EACH_PACKAGE):
index = Column.SRV_PACKAGE
elif gen_type in (BqGenType.SAT, BqGenType.EACH_SAT):
index = Column.SRV_POS
model, paths = view.get_selection().get_selected_rows()
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][:Column.SRV_TOOLTIP])
if service.service_type not in tv_types:
bq_type = BqType.RADIO.value
append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model,
[service.package if gen_type is BqGenType.PACKAGE else
service.pos if gen_type is BqGenType.SAT else service.service_type], 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)
@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)
if response not in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
entry.set_text(response)
return response
return False
def get_base_model(model):
""" Returns base tree model if has wrappers ("TreeModelSort" and "TreeModelFilter") """
if type(model) is Gtk.TreeModelSort:
return model.get_model().get_model()
return model
def get_model_data(view):
""" Returns model name and base model from the given view """
model = get_base_model(view.get_model())
model_name = model.get_name()
return model_name, model
def append_text_to_tview(char, view):
""" Appending text and scrolling to a given line in the text view. """
buf = view.get_buffer()
buf.insert_at_cursor(char)
insert = buf.get_insert()
view.scroll_to_mark(insert, 0.0, True, 0.0, 1.0)
def get_iptv_url(row, profile):
""" Returns url from iptv type row """
data = row[Column.FAV_ID].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 urllib.request.unquote(url) if profile is Profile.ENIGMA_2 else url
def on_popup_menu(menu, event):
""" Shows popup menu for the view """
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
menu.popup(None, None, None, None, event.button, event.time)
if __name__ == "__main__":
pass

File diff suppressed because it is too large Load Diff

1088
app/ui/picons_dialog.glade Normal file

File diff suppressed because it is too large Load Diff

340
app/ui/picons_downloader.py Normal file
View File

@@ -0,0 +1,340 @@
import os
import re
import shutil
import subprocess
import tempfile
import time
from gi.repository import GLib, GdkPixbuf
from app.commons import run_idle, run_task
from app.connections import upload_data, DownloadType
from app.tools.picons import PiconsParser, parse_providers, Provider, convert_to
from app.properties import Profile
from app.tools.satellites import SatellitesParser, SatelliteSource
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, scroll_to, on_popup_menu
class PiconsDialog:
def __init__(self, transient, options, picon_ids, sat_positions, profile=Profile.ENIGMA_2):
self._picon_ids = picon_ids
self._sat_positions = sat_positions
self._TMP_DIR = tempfile.gettempdir() + "/"
self._BASE_URL = "www.lyngsat.com/packages/"
self._PATTERN = re.compile("^https://www\.lyngsat\.com/[\w-]+\.html$")
self._POS_PATTERN = re.compile("^\d+\.\d+[EW]?$")
self._current_process = None
self._terminate = False
handlers = {"on_receive": self.on_receive,
"on_load_providers": self.on_load_providers,
"on_cancel": self.on_cancel,
"on_close": self.on_close,
"on_send": self.on_send,
"on_info_bar_close": self.on_info_bar_close,
"on_picons_dir_open": self.on_picons_dir_open,
"on_selected_toggled": self.on_selected_toggled,
"on_url_changed": self.on_url_changed,
"on_position_edited": self.on_position_edited,
"on_notebook_switch_page": self.on_notebook_switch_page,
"on_convert": self.on_convert,
"on_satellites_view_realize": self.on_satellites_view_realize,
"on_satellite_selection": self.on_satellite_selection,
"on_select_all": self.on_select_all,
"on_unselect_all": self.on_unselect_all,
"on_popup_menu": on_popup_menu}
builder = Gtk.Builder()
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")
self._satellites_tree_view = builder.get_object("satellites_tree_view")
self._expander = builder.get_object("expander")
self._text_view = builder.get_object("text_view")
self._info_bar = builder.get_object("info_bar")
self._ip_entry = builder.get_object("ip_entry")
self._picons_entry = builder.get_object("picons_entry")
self._url_entry = builder.get_object("url_entry")
self._picons_dir_entry = builder.get_object("picons_dir_entry")
self._info_bar = builder.get_object("info_bar")
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label")
self._load_providers_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")
self._resize_220_132_radio_button = builder.get_object("resize_220_132_radio_button")
self._resize_100_60_radio_button = builder.get_object("resize_100_60_radio_button")
# style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
self._properties = options.get(profile.value)
self._profile = profile
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()
self._dialog.destroy()
def on_satellites_view_realize(self, view):
self.get_satellites(view)
@run_task
def get_satellites(self, view):
sats = SatellitesParser().get_satellites_list(SatelliteSource.LYNGSAT)
gen = self.append_satellites(view.get_model(), sats)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def append_satellites(self, model, sats):
for sat in sats:
pos = sat[1]
name = "{} ({})".format(sat[0], pos)
pos = "{}{}".format("-" if pos[-1] == "W" else "", pos[:-1])
if not self._terminate and model:
if pos in self._sat_positions:
model.append((name, sat[3], pos))
yield True
def on_satellite_selection(self, view, path, column):
model = view.get_model()
self._url_entry.set_text(model.get(model.get_iter(path), 1)[0])
@run_idle
def on_load_providers(self, item):
self._expander.set_expanded(True)
url = self._url_entry.get_text()
self._current_process = subprocess.Popen(["wget", "-pkP", self._TMP_DIR, url],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
GLib.io_add_watch(self._current_process.stderr, GLib.IO_IN, self.write_to_buffer)
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]) if p[0] else TV_ICON, *p[1:]))
self.update_receive_button_state()
def get_pixbuf(self, img_url):
return GdkPixbuf.Pixbuf.new_from_file_at_scale(filename=self._TMP_DIR + "www.lyngsat.com/" + img_url,
width=48, height=48, preserve_aspect_ratio=True)
@run_idle
def on_receive(self, item):
self.start_download()
@run_task
def start_download(self):
if self._current_process.poll() is None:
self.show_dialog("The task is already running!", DialogType.ERROR)
return
self._terminate = False
self._expander.set_expanded(True)
providers = self.get_selected_providers()
for prv in providers:
if not self._POS_PATTERN.match(prv[2]):
self.show_info_message(
get_message("Specify the correct position value for the provider!"), Gtk.MessageType.ERROR)
scroll_to(prv.path, self._providers_tree_view)
return
for prv in providers:
if self._terminate:
break
self.process_provider(Provider(*prv))
self.resize(self._picons_path)
if not self._terminate:
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
def process_provider(self, prv):
url = prv.url
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
self._current_process = subprocess.Popen(["wget", "-pkP", self._TMP_DIR, url],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
GLib.io_add_watch(self._current_process.stderr, GLib.IO_IN, self.write_to_buffer)
self._current_process.wait()
path = self._TMP_DIR + (url[url.find("//") + 2:] if prv.single else self._BASE_URL + url[url.rfind("/") + 1:])
PiconsParser.parse(path, self._picons_path, self._TMP_DIR, prv, self._picon_ids, self.get_picons_format())
def write_to_buffer(self, fd, condition):
if condition == GLib.IO_IN:
char = fd.read(1)
self.append_output(char)
return True
else:
return False
@run_idle
def append_output(self, char):
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(get_message("Resizing..."), Gtk.MessageType.INFO)
command = "mogrify -resize {}! *.png".format(
"320x240" if self._resize_220_132_radio_button.get_active() else "100x60").split()
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=None):
if self._current_process:
self._terminate = True
self._current_process.terminate()
time.sleep(1)
@run_idle
def on_close(self, item):
self.on_cancel(item)
path = self._TMP_DIR + "www.lyngsat.com"
if os.path.exists(path):
shutil.rmtree(path)
def on_send(self, item):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
self.upload_picons()
@run_task
def upload_picons(self):
if self._current_process is not None and self._current_process.poll() is None:
self.show_dialog("The task is already running!", DialogType.ERROR)
return
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)
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(text)
def on_picons_dir_open(self, entry, icon, event_button):
update_entry_data(entry, self._dialog, options={"data_dir_path": self._picons_path})
@run_idle
def on_selected_toggled(self, toggle, path):
model = self._providers_tree_view.get_model()
model.set_value(model.get_iter(path), 7, not toggle.get_active())
self.update_receive_button_state()
def on_select_all(self, view):
self.update_selection(view, True)
def on_unselect_all(self, view):
self.update_selection(view, False)
def update_selection(self, view, select):
view.get_model().foreach(lambda mod, path, itr: mod.set_value(itr, 7, select))
def on_url_changed(self, entry):
suit = self._PATTERN.search(entry.get_text())
entry.set_name("GtkEntry" if suit else "digit-entry")
self._load_providers_button.set_sensitive(suit if suit else False)
def on_position_edited(self, render, path, value):
model = self._providers_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_button.set_sensitive(len(self.get_selected_providers()) > 0)
def get_selected_providers(self):
""" returns selected providers """
return [r for r in self._providers_tree_view.get_model() if r[7]]
@run_idle
def show_dialog(self, message, dialog_type):
show_dialog(dialog_type, self._dialog, message)
def get_picons_format(self):
picon_format = Profile.ENIGMA_2
if self._neutrino_mp_radio_button.get_active():
picon_format = Profile.NEUTRINO_MP
return picon_format
if __name__ == "__main__":
pass

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +1,41 @@
import re
import time
import concurrent.futures
from math import fabs
from app.commons import run_idle
from gi.repository import GLib
from app.commons import run_idle, run_task
from app.eparser import get_satellites, write_satellites, Satellite, Transponder
from . import Gtk, Gdk
from .dialogs import show_dialog, DialogType
from app.eparser.ecommons import PLS_MODE, get_key_by_value
from app.tools.satellites import SatellitesParser, SatelliteSource
from .search import SearchProvider
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, MOVE_KEYS, KeyboardKey, IS_GNOME_SESSION
from .dialogs import show_dialog, DialogType, get_dialogs_string
from .main_helper import move_items, scroll_to, append_text_to_tview, get_base_model, on_popup_menu
_UI_PATH = UI_RESOURCES_PATH + "satellites_dialog.glade"
def show_satellites_dialog(transient, options):
dialog = SatellitesDialog(transient, options)
dialog.run()
dialog.destroy()
SatellitesDialog(transient, options).show()
class SatellitesDialog:
__slots__ = ["_dialog", "_data_path", "_stores", "_options", "_sat_view"]
_aggr = [None for x in range(9)] # aggregate
def __init__(self, transient, options):
self._data_path = options["data_dir_path"] + "satellites.xml"
self._data_path = options.get("data_dir_path") + "satellites.xml"
self._options = options
handlers = {"on_open": self.on_open,
"on_remove": self.on_remove,
"on_save": self.on_save,
"on_popup_menu": self.on_popup_menu,
"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": on_popup_menu,
"on_satellite_add": self.on_satellite_add,
"on_transponder_add": self.on_transponder_add,
"on_edit": self.on_edit,
@@ -35,60 +45,65 @@ class SatellitesDialog:
"on_quit": self.on_quit}
builder = Gtk.Builder()
builder.add_objects_from_file("app/ui/satellites_dialog.glade",
("satellites_editor_dialog", "satellites_tree_store",
"popup_menu", "add_popup_menu", "add_menu_icon"))
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH),
("satellites_editor_window", "satellites_tree_store", "popup_menu",
"left_header_menu", "popup_menu_add_image", "popup_menu_add_image_2"))
builder.connect_signals(handlers)
# Adding custom image for add_menu_tool_button
add_menu_tool_button = builder.get_object("add_menu_tool_button")
add_menu_tool_button.set_image(builder.get_object("add_menu_icon"))
self._dialog = builder.get_object("satellites_editor_dialog")
self._dialog.set_transient_for(transient)
self._dialog.get_content_area().set_border_width(0) # The width of the border around the app dialog area!
self._window = builder.get_object("satellites_editor_window")
self._window.set_transient_for(transient)
self._sat_view = builder.get_object("satellites_editor_tree_view")
# Setting the last size of the dialog window if it was saved
window_size = self._options.get("sat_editor_window_size", None)
if window_size:
self._dialog.resize(*window_size)
self._window.resize(*window_size)
self._stores = {3: builder.get_object("pol_store"),
4: builder.get_object("fec_store"),
5: builder.get_object("system_store"),
6: builder.get_object("mod_store")}
self.on_satellites_list_load(self._sat_view.get_model())
def run(self):
self._dialog.run()
self.load_satellites_list(self._sat_view.get_model())
def destroy(self):
self._dialog.destroy()
def load_satellites_list(self, model):
gen = self.on_satellites_list_load(model)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def show(self):
self._window.show()
def on_resize(self, window):
""" Stores new size properties for dialog window after resize """
if self._options:
self._options["sat_editor_window_size"] = window.get_size()
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)
self.load_satellites_list(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):
@@ -97,43 +112,49 @@ class SatellitesDialog:
else:
view.expand_row(path, column)
def on_up(self, item):
move_items(KeyboardKey.UP, self._sat_view)
def on_down(self, item):
move_items(KeyboardKey.DOWN, self._sat_view)
def on_key_release(self, view, event):
""" Handling keystrokes """
key = event.keyval
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
if key == Gdk.KEY_Delete:
if key is KeyboardKey.DELETE:
self.on_remove(view)
elif key == Gdk.KEY_Insert:
elif key is KeyboardKey.INSERT:
pass
# self.on_add(view)
elif ctrl and key == Gdk.KEY_E or key == Gdk.KEY_e:
elif ctrl and key is KeyboardKey.E:
self.on_edit(view)
elif ctrl and key == Gdk.KEY_s or key == Gdk.KEY_S:
elif ctrl and key is KeyboardKey.S:
self.on_satellite()
elif ctrl and key == Gdk.KEY_t or key == Gdk.KEY_T:
elif ctrl and key is KeyboardKey.T:
self.on_transponder()
elif key == Gdk.KEY_space:
pass
elif ctrl and key in MOVE_KEYS:
move_items(key, self._sat_view)
elif key is KeyboardKey.LEFT or key is KeyboardKey.RIGHT:
view.do_unselect_all(view)
@run_idle
def on_satellites_list_load(self, model):
""" Load satellites data into model """
try:
satellites = get_satellites(self._data_path)
yield True
except FileNotFoundError as e:
show_dialog(DialogType.ERROR, self._dialog, getattr(e, "message", str(e)) +
show_dialog(DialogType.ERROR, self._window, getattr(e, "message", str(e)) +
"\n\nPlease, download files from receiver or setup your path for read data!")
return
else:
model.clear()
self.append_data(model, satellites)
@run_idle
def append_data(self, model, satellites):
for name, flags, pos, transponders in satellites:
parent = model.append(None, [name, *self._aggr, flags, pos])
for transponder in transponders:
model.append(parent, ["Transponder:", *transponder, None, None])
for sat in satellites:
append_satellite(model, sat)
yield True
def on_add(self, view):
""" Common adding """
@@ -162,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()
@@ -174,7 +195,7 @@ class SatellitesDialog:
else:
index = self.get_sat_position_index(sat.position, model)
model.insert(None, index, [sat.name, *self._aggr, sat.flags, sat.position])
self.scroll_to(index, view)
scroll_to(index, view)
def on_transponder(self, transponder=None, edited_itr=None):
""" Create or edit transponder """
@@ -183,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()
@@ -215,20 +236,13 @@ class SatellitesDialog:
path = model.get_path(tr_itr)
index = path.get_indices()[1]
model.insert(model.iter_parent(tr_itr), index, row)
self.scroll_to(path, view)
scroll_to(path, view)
break
else:
tr_itr = model.iter_next(tr_itr)
else:
itr = model.append(itr, row)
self.scroll_to(model.get_path(itr), view)
def scroll_to(self, index, view):
""" Scrolling to and selecting given index(path) """
view.scroll_to_cell(index, None)
selection = view.get_selection()
selection.unselect_all()
selection.select_path(index)
scroll_to(model.get_path(itr), view)
def get_sat_position_index(self, pos, model):
""" Search and returns index after given position """
@@ -243,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
@@ -255,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()
@@ -269,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):
@@ -285,11 +307,8 @@ class SatellitesDialog:
satellite = Satellite(sat[0], sat[-2], sat[-1], transponders)
sats.append(satellite)
@staticmethod
def on_popup_menu(menu, event):
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
menu.popup(None, None, None, None, event.button, event.time)
# ***************** Transponder dialog *******************#
class TransponderDialog:
""" Shows dialog for adding or edit transponder """
@@ -299,11 +318,10 @@ class TransponderDialog:
handlers = {"on_entry_changed": self.on_entry_changed}
builder = Gtk.Builder()
builder.add_objects_from_file("app/ui/satellites_dialog.glade",
("transponder_dialog",
"pol_store", "fec_store",
"mod_store", "system_store",
"pls_mode_store"))
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("transponder_dialog", "pol_store", "fec_store", "mod_store", "system_store",
"pls_mode_store"))
builder.connect_signals(handlers)
self._dialog = builder.get_object("transponder_dialog")
@@ -321,7 +339,7 @@ class TransponderDialog:
self._pattern = re.compile("\D")
# style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path("app/ui/style.css")
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._freq_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
self._rate_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
@@ -346,7 +364,7 @@ class TransponderDialog:
self._fec_box.set_active_id(transponder.fec_inner)
self._sys_box.set_active_id(transponder.system)
self._mod_box.set_active_id(transponder.modulation)
self._pls_mode_box.set_active_id(transponder.pls_mode)
self._pls_mode_box.set_active_id(PLS_MODE.get(transponder.pls_mode, None))
self._is_id_entry.set_text(transponder.is_id if transponder.is_id else "")
self._pls_code_entry.set_text(transponder.pls_code if transponder.pls_code else "")
@@ -357,7 +375,7 @@ class TransponderDialog:
fec_inner=self._fec_box.get_active_id(),
system=self._sys_box.get_active_id(),
modulation=self._mod_box.get_active_id(),
pls_mode=self._pls_mode_box.get_active_id(),
pls_mode=get_key_by_value(PLS_MODE, self._pls_mode_box.get_active_id()),
pls_code=self._pls_code_entry.get_text(),
is_id=self._is_id_entry.get_text())
@@ -377,13 +395,16 @@ class TransponderDialog:
return True
# ***************** Satellite dialog *******************#
class SatelliteDialog:
""" Shows dialog for adding or edit satellite """
def __init__(self, transient, satellite: Satellite = None):
builder = Gtk.Builder()
builder.add_objects_from_file("app/ui/satellites_dialog.glade",
("satellite_dialog", "side_store", "pos_adjustment"))
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("satellite_dialog", "side_store", "pos_adjustment"))
self._dialog = builder.get_object("satellite_dialog")
self._dialog.set_transient_for(transient)
@@ -417,5 +438,260 @@ 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": 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_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,841 @@
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, T_MODULATION, C_MODULATION, TrType, \
SystemCable, T_SYSTEM, BANDWIDTH, TRANSMISSION_MODE, GUARD_INTERVAL, HIERARCHY, T_FEC
from app.properties import Profile
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, TEXT_DOMAIN, CODED_ICON, Column, IS_GNOME_SESSION
from .dialogs import show_dialog, DialogType, Action, get_dialogs_string
from .main_helper import get_base_model
_UI_PATH = UI_RESOURCES_PATH + "service_details_dialog.glade"
class ServiceDetailsDialog:
_ENIGMA2_DATA_ID = "{:04x}:{:08x}:{:04x}:{:04x}:{}:{}"
_ENIGMA2_FAV_ID = "{:X}:{:X}:{:X}:{:X}"
_ENIGMA2_TRANSPONDER_DATA = "{} {}:{}:{}:{}:{}:{}:{}"
_NEUTRINO_FAV_ID = "{:x}:{:x}:{:x}"
_NEUTRINO_TRANSPONDER_DATA = "{:04x}:{:04x}:{}:{}:{}:{}:{}:{}:{}"
_DIGIT_ENTRY_ELEMENTS = ("bitstream_entry", "pcm_entry", "video_pid_entry", "pcr_pid_entry", "srv_type_entry",
"ac3_pid_entry", "ac3plus_pid_entry", "acc_pid_entry", "he_acc_pid_entry",
"teletext_pid_entry", "pls_code_entry", "stream_id_entry", "tr_flag_entry",
"audio_pid_entry")
_NOT_EMPTY_DIGIT_ELEMENTS = ("sid_entry", "freq_entry", "rate_entry", "transponder_id_entry", "network_id_entry",
"namespace_entry", "srv_type_entry")
_DIGIT_ENTRY_NAME = "digit-entry"
def __init__(self, transient, options, srv_view, fav_view, services, bouquets, new_color, action=Action.EDIT):
handlers = {"on_system_changed": self.on_system_changed,
"on_save": self.on_save,
"on_create_new": self.on_create_new,
"on_tr_edit_toggled": self.on_tr_edit_toggled,
"update_reference": self.update_reference,
"on_cas_entry_changed": self.on_cas_entry_changed,
"on_digit_entry_changed": self.on_digit_entry_changed,
"on_non_empty_entry_changed": self.on_non_empty_entry_changed}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION))
builder.connect_signals(handlers)
self._builder = builder
self._dialog = builder.get_object("service_details_dialog")
self._dialog.set_transient_for(transient)
self._profile = Profile(options["profile"])
self._tr_type = None
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._new_color = new_color
self._transponder_services_iters = None
self._current_model = None
self._current_itr = None
# Patterns
self._DIGIT_PATTERN = re.compile("\\D")
self._NON_EMPTY_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
self._CAID_PATTERN = re.compile("(?:^[\\s]*$)|(C:[0-9a-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._rate_lp_combo_box = builder.get_object("rate_lp_combo_box")
self._sys_combo_box = builder.get_object("sys_combo_box")
self._mod_combo_box = builder.get_object("mod_combo_box")
self._invertion_combo_box = builder.get_object("invertion_combo_box")
self._rolloff_combo_box = builder.get_object("rolloff_combo_box")
self._pilot_combo_box = builder.get_object("pilot_combo_box")
self._pls_mode_combo_box = builder.get_object("pls_mode_combo_box")
self._tr_edit_switch = builder.get_object("tr_edit_switch")
self._tr_extra_expander = builder.get_object("tr_extra_expander")
self._DVB_S2_ELEMENTS = (self._mod_combo_box, self._rolloff_combo_box, self._pilot_combo_box,
self._pls_mode_combo_box, self._pls_code_entry, self._stream_id_entry)
self._TRANSPONDER_ELEMENTS = (self._sat_pos_button, self._pol_combo_box, self._invertion_combo_box,
self._sys_combo_box, self._freq_entry, self._transponder_id_entry,
self._network_id_entry, self._namespace_entry, self._fec_combo_box,
self._rate_entry, self._rate_lp_combo_box)
if self._action is Action.EDIT:
self.update_data_elements()
elif self._action is Action.ADD:
self.init_default_data_elements()
def show(self):
response = self._dialog.run()
if response == Gtk.ResponseType.OK:
pass
self._dialog.destroy()
return response
@run_idle
def init_default_data_elements(self):
self._apply_button.set_visible(False)
self._create_button.set_visible(True)
self._tr_edit_switch.set_sensitive(False)
self.on_tr_edit_toggled(self._tr_edit_switch.set_active(True), True)
for elem in self._non_empty_elements.values():
elem.set_text(" ")
elem.set_text("")
self._new_check_button.set_active(True)
self._tr_extra_expander.activate()
self._service_type_combo_box.set_active(0)
self._pol_combo_box.set_active(0)
self._fec_combo_box.set_active(0)
self._sys_combo_box.set_active(0)
self._invertion_combo_box.set_active(2)
def update_data_elements(self):
model, paths = self._services_view.get_selection().get_selected_rows()
# Unpacking to search for an iterator for the base model
filter_model = model.get_model()
self._current_model = get_base_model(model)
itr = None
if not paths:
# If editing from bouquet list and services list in the filter mode
fav_model, paths = self._fav_view.get_selection().get_selected_rows()
fav_id = fav_model[paths][7]
for row in self._current_model:
if row[-2] == fav_id:
itr = row.iter
break
else:
itr = model.get_iter(paths)
itr = filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr))
if not itr:
return
srv = Service(*self._current_model[itr][: Column.SRV_TOOLTIP])
self._old_service = srv
self._current_itr = itr
# Service
self._name_entry.set_text(srv.service)
self._package_entry.set_text(srv.package)
self._sid_entry.set_text(str(int(srv.ssid, 16)))
# Transponder
if self._profile is Profile.ENIGMA_2:
self._tr_type = TrType(srv.transponder_type)
self._freq_entry.set_text(srv.freq)
self._rate_entry.set_text(srv.rate)
self.select_active_text(self._pol_combo_box, srv.pol)
self.select_active_text(self._fec_combo_box, srv.fec)
self.select_active_text(self._sys_combo_box, srv.system)
if self._tr_type is TrType.Terrestrial:
self.update_ui_for_terrestrial()
elif self._tr_type is TrType.Cable:
self.update_ui_for_cable()
else:
self.set_sat_positions(srv.pos)
if self._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(":")
tr_type = TrType(srv.transponder_type)
self._namespace_entry.set_text(str(int(data[1], 16)))
self._transponder_id_entry.set_text(str(int(data[2], 16)))
self._network_id_entry.set_text(str(int(data[3], 16)))
if tr_type is TrType.Satellite:
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[5]).name)
if srv.system == "DVB-S2":
self.select_active_text(self._mod_combo_box, MODULATION.get(tr_data[8]))
self.select_active_text(self._rolloff_combo_box, ROLL_OFF.get(tr_data[9]))
self.select_active_text(self._pilot_combo_box, Pilot(tr_data[10]).name)
self._tr_flag_entry.set_text(tr_data[7])
if len(tr_data) > 12:
self._stream_id_entry.set_text(tr_data[11])
self._pls_code_entry.set_text(tr_data[12])
self.select_active_text(self._pls_mode_combo_box, PLS_MODE.get(tr_data[13]))
elif tr_type is TrType.Cable:
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[2]).name)
self.select_active_text(self._mod_combo_box, C_MODULATION.get(tr_data[3]))
self.select_active_text(self._fec_combo_box, FEC_DEFAULT.get(tr_data[4]))
self.select_active_text(self._sys_combo_box, SystemCable(tr_data[5]).name)
elif tr_type is TrType.Terrestrial:
self.select_active_text(self._fec_combo_box, T_FEC.get(tr_data[2]))
self.select_active_text(self._rate_lp_combo_box, T_FEC.get(tr_data[3]))
# Pol -> Bandwidth
self.select_active_text(self._pol_combo_box, BANDWIDTH.get(tr_data[1]))
self.select_active_text(self._mod_combo_box, T_MODULATION.get(tr_data[4]))
# Transmission Mode -> Roll off
self.select_active_text(self._rolloff_combo_box, TRANSMISSION_MODE.get(tr_data[5]))
# GuardInterval -> Pilot
self.select_active_text(self._pilot_combo_box, GUARD_INTERVAL.get(tr_data[6]))
# Hierarchy -> Pls Mode
self.select_active_text(self._pls_mode_combo_box, HIERARCHY.get(tr_data[7]))
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[8]).name)
self.select_active_text(self._sys_combo_box, T_SYSTEM.get(tr_data[9]))
# Should be called last to properly initialize the reference
self._srv_type_entry.set_text(data[4])
# ***************** Init Neutrino data *********************#
def init_neutrino_data(self, srv):
tr_data = srv.transponder.split(":")
self._transponder_id_entry.set_text(str(int(tr_data[0], 16)))
self._network_id_entry.set_text(str(int(tr_data[1], 16)))
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[3]).name)
self.select_active_text(self._service_type_combo_box, srv.service_type)
self.update_reference_entry()
def init_neutrino_ui_elements(self):
self._builder.get_object("flags_box").set_visible(False)
self._builder.get_object("pids_grid").set_visible(False)
tr_grid = self._builder.get_object("tr_grid")
tr_grid.remove_column(7)
tr_grid.set_margin_bottom(5)
self._builder.get_object("tr_extra_expander").set_visible(False)
self._builder.get_object("srv_separator").set_visible(False)
# ***************** Init Sat positions *********************#
def set_sat_positions(self, sat_pos):
""" Sat positions initialisation """
self._sat_pos_button.set_value(float(sat_pos))
def on_system_changed(self, box):
if not self._tr_edit_switch.get_active():
return
active = box.get_active()
self.update_dvb_s2_elements(active)
def update_dvb_s2_elements(self, active):
for elem in self._DVB_S2_ELEMENTS:
elem.set_sensitive(active)
self._pls_code_entry.set_name("GtkEntry")
self._stream_id_entry.set_name("GtkEntry")
if active:
if not self._mod_combo_box.get_active_id():
self._mod_combo_box.set_active_id(MODULATION["2"])
if not self._rolloff_combo_box.get_active_id():
self._rolloff_combo_box.set_active_id(ROLL_OFF["0"])
if not self._pilot_combo_box.get_active_id():
self._pilot_combo_box.set_active_id(Pilot.Auto.name)
if not self._pls_mode_combo_box.get_active_id():
self._pls_mode_combo_box.set_active_id(PLS_MODE["0"])
# ***************** Save data *********************#
def on_save(self, item):
self.save_data()
def on_create_new(self, item):
self.save_data()
def save_data(self):
if not self.is_data_correct():
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
self.on_edit() if self._action is Action.EDIT else self.on_new()
self._dialog.destroy()
def on_new(self):
""" Create new service. """
service = self.get_service(*self.get_srv_data(), self.get_satellite_transponder_data())
show_dialog(DialogType.ERROR, transient=self._dialog, text="Not implemented yet!")
def on_edit(self):
""" Edit current service. """
fav_id, data_id = self.get_srv_data()
# transponder
transponder = self._old_service.transponder
if self._tr_edit_switch.get_active():
try:
if self._tr_type is TrType.Satellite:
transponder = self.get_satellite_transponder_data()
elif self._tr_type is TrType.Terrestrial:
transponder = self.get_terrestrial_transponder_data()
elif self._tr_type is TrType.Cable:
transponder = self.get_cable_transponder_data()
except Exception as e:
print(e)
show_dialog(DialogType.ERROR, transient=self._dialog, text="Error getting transponder parameters!")
else:
if self._transponder_services_iters:
self.update_transponder_services(transponder)
# service
service = self.get_service(fav_id, data_id, transponder)
old_fav_id = self._old_service.fav_id
if old_fav_id != fav_id:
self.update_bouquets(fav_id, old_fav_id)
self._services[fav_id] = service
if self._old_service.picon_id != service.picon_id:
self.update_picon_name(self._old_service.picon_id, service.picon_id)
flags = service.flags_cas
extra_data = {Column.SRV_TOOLTIP: None, Column.SRV_BACKGROUND: None}
if flags:
f_flags = list(filter(lambda x: x.startswith("f:"), flags.split(",")))
if f_flags and Flag.is_new(int(f_flags[0][2:])):
extra_data[Column.SRV_BACKGROUND] = self._new_color
self._current_model.set(self._current_itr, extra_data)
self._current_model.set(self._current_itr, {i: v for i, v in enumerate(service)})
self.update_fav_view(self._old_service, service)
self._old_service = service
def update_bouquets(self, fav_id, old_fav_id):
self._services.pop(old_fav_id, None)
for bq in self._bouquets.values():
indexes = []
for i, f_id in enumerate(bq):
if old_fav_id == f_id:
indexes.append(i)
for i in indexes:
bq[i] = fav_id
@run_idle
def update_fav_view(self, old_service, new_service):
model = self._fav_view.get_model()
for row in filter(lambda r: old_service.fav_id == r[7], model):
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
# ***************** Service ********************* #
def get_service(self, fav_id, data_id, transponder):
freq, rate, pol, fec, system, pos = self.get_transponder_values()
return Service(flags_cas=self.get_flags(),
transponder_type=self._old_service.transponder_type,
coded=CODED_ICON if self._cas_entry.get_text() else None,
service=self._name_entry.get_text(),
locked=self._old_service.locked,
hide=HIDE_ICON if self._hide_check_button.get_active() else None,
package=self._package_entry.get_text(),
service_type=SERVICE_TYPE.get(self._srv_type_entry.get_text(), SERVICE_TYPE["3"]),
picon=self._old_service.picon,
picon_id=self._reference_entry.get_text().replace(":", "_") + ".png",
ssid="{:04x}".format(int(self._sid_entry.get_text())),
freq=freq,
rate=rate,
pol=pol,
fec=fec,
system=system,
pos=pos,
data_id=data_id,
fav_id=fav_id,
transponder=transponder)
def get_flags(self):
if self._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
# ***************** Transponder ********************* #
def get_transponder_values(self):
freq = self._freq_entry.get_text()
fec = self._fec_combo_box.get_active_id()
system = self._sys_combo_box.get_active_id()
if self._tr_type is TrType.Satellite or self._profile is Profile.NEUTRINO_MP:
freq = self._freq_entry.get_text()
rate = self._rate_entry.get_text()
pol = self._pol_combo_box.get_active_id()
pos = str(round(self._sat_pos_button.get_value(), 1))
return freq, rate, pol, fec, system, pos
elif self._tr_type is TrType.Terrestrial:
o_srv = self._old_service
return freq, o_srv.rate, o_srv.pol, fec, system, o_srv.pos
elif self._tr_type is TrType.Cable:
o_srv = self._old_service
return freq, self._rate_entry.get_text(), o_srv.pol, fec, o_srv.system, o_srv.pos
def get_satellite_transponder_data(self):
sys = self._sys_combo_box.get_active_id()
freq = self._freq_entry.get_text()
rate = self._rate_entry.get_text()
pol = self.get_value_from_combobox_id(self._pol_combo_box, POLARIZATION)
fec = self.get_value_from_combobox_id(self._fec_combo_box, FEC_DEFAULT)
sat_pos = str(round(self._sat_pos_button.get_value(), 1)).replace(".", "")
inv = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
srv_sys = "0" # !!!
if self._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 get_terrestrial_transponder_data(self):
tr_data = re.split("\s|:", self._old_service.transponder)
# frequency, bandwidth, code rate HP, code rate LP, modulation, transmission mode, guard interval, hierarchy,
# inversion, system, plp_id
# Bandwidth -> Pol, Rate HP -> FEC, TransmissionMode -> Roll off, GuardInterval -> Pilot, Hierarchy -> Pls Mode
tr_data[1] = self._freq_entry.get_text()
tr_data[2] = self.get_value_from_combobox_id(self._pol_combo_box, BANDWIDTH)
tr_data[3] = self.get_value_from_combobox_id(self._fec_combo_box, T_FEC)
tr_data[4] = self.get_value_from_combobox_id(self._rate_lp_combo_box, T_FEC)
tr_data[5] = self.get_value_from_combobox_id(self._mod_combo_box, T_MODULATION)
tr_data[6] = self.get_value_from_combobox_id(self._rolloff_combo_box, TRANSMISSION_MODE)
tr_data[7] = self.get_value_from_combobox_id(self._pilot_combo_box, GUARD_INTERVAL)
tr_data[8] = self.get_value_from_combobox_id(self._pls_mode_combo_box, HIERARCHY)
tr_data[9] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
tr_data[10] = self.get_value_from_combobox_id(self._sys_combo_box, T_SYSTEM)
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
def get_cable_transponder_data(self):
tr_data = re.split("\s|:", self._old_service.transponder)
# frequency, symbol_rate, modulation, inversion, fec_inner, system;
tr_data[1] = self._freq_entry.get_text()
tr_data[2] = self._rate_entry.get_text()
tr_data[3] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
tr_data[4] = self.get_value_from_combobox_id(self._mod_combo_box, C_MODULATION)
tr_data[5] = self.get_value_from_combobox_id(self._fec_combo_box, FEC_DEFAULT)
tr_data[6] = get_value_by_name(SystemCable, self._sys_combo_box.get_active_id())
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
def update_transponder_services(self, transponder):
for itr in self._transponder_services_iters:
srv = self._current_model[itr][:Column.SRV_TOOLTIP]
srv[Column.SRV_FREQ], srv[Column.SRV_RATE], srv[Column.SRV_POL], srv[Column.SRV_FEC], srv[
Column.SRV_SYSTEM], srv[Column.SRV_POS] = self.get_transponder_values()
srv[Column.SRV_TRANSPONDER] = transponder
srv = Service(*srv)
self._services[srv.fav_id] = self._services.pop(srv.fav_id)._replace(transponder=transponder)
self._current_model.set(itr, {i: v for i, v in enumerate(srv)})
# ***************** Others *********************#
def select_active_text(self, box, text):
model = box.get_model()
for index, row in enumerate(model):
if row[0] == text:
box.set_active(index)
break
def on_digit_entry_changed(self, entry):
entry.set_name(self._DIGIT_ENTRY_NAME if self._DIGIT_PATTERN.search(entry.get_text()) else "GtkEntry")
def on_non_empty_entry_changed(self, entry):
entry.set_name(self._DIGIT_ENTRY_NAME if self._NON_EMPTY_PATTERN.search(entry.get_text()) else "GtkEntry")
def on_cas_entry_changed(self, entry):
entry.set_name("GtkEntry" if self._CAID_PATTERN.fullmatch(entry.get_text()) else self._DIGIT_ENTRY_NAME)
def get_value_from_combobox_id(self, box: Gtk.ComboBox, dc: dict):
cb_id = box.get_active_id()
return get_key_by_value(dc, cb_id)
@run_idle
def on_tr_edit_toggled(self, switch, active):
if active and self._action is Action.EDIT:
self._transponder_services_iters = []
response = TransponderServicesDialog(self._dialog,
self._current_model,
self._old_service.transponder,
self._transponder_services_iters).show()
if response == Gtk.ResponseType.CANCEL or response == -4:
switch.set_active(False)
self._transponder_services_iters = None
return
self.update_dvb_s2_elements(active and (self._sys_combo_box.get_active_id() == "DVB-S2"
or self._old_service.transponder_type in "tc"))
for elem in self._TRANSPONDER_ELEMENTS:
elem.set_sensitive(active)
def is_data_correct(self):
for elem in self._digit_elements.values():
if elem.get_name() == self._DIGIT_ENTRY_NAME:
return False
for elem in self._non_empty_elements.values():
if elem.get_name() == self._DIGIT_ENTRY_NAME:
return False
if self._cas_entry.get_name() == self._DIGIT_ENTRY_NAME:
return False
return True
def update_reference(self, entry, event=None):
if not self.is_data_correct() or (event is None and self._profile is Profile.NEUTRINO_MP):
return
self.update_reference_entry()
def update_reference_entry(self):
srv_type = int(self._srv_type_entry.get_text())
ssid = int(self._sid_entry.get_text())
tid = int(self._transponder_id_entry.get_text())
nid = int(self._network_id_entry.get_text())
if self._profile is Profile.ENIGMA_2:
on_id = int(self._namespace_entry.get_text())
ref = "1:0:{:X}:{:X}:{:X}:{:X}:{:X}:0:0:0".format(srv_type, ssid, tid, nid, on_id)
self._reference_entry.set_text(ref)
else:
self._reference_entry.set_text("{:x}{:04x}{:04x}".format(tid, nid, ssid))
def update_ui_for_terrestrial(self):
tr_grid = self.get_transponder_grid_for_non_satellite()
tr_grid.remove_column(1)
tr_grid.insert_column(1)
extra_tr_grid = self._builder.get_object("extra_transponder_grid")
for i in range(4):
extra_tr_grid.remove_column(6)
# Bandwidth -> Pol
pol_label = self._builder.get_object("pol_label")
pol_label.set_text("Bandwidth")
tr_grid.attach(pol_label, 1, 0, 1, 1)
tr_grid.attach(self._pol_combo_box, 1, 1, 1, 1)
# Rate HP -> FEC
self._builder.get_object("fec_label").set_text("Rate HP")
# Rate LP
tr_grid.insert_column(3)
rate_lp_label = self._builder.get_object("pls_code_label")
rate_lp_label.set_text("Rate LP")
tr_grid.attach(rate_lp_label, 3, 0, 1, 1)
tr_grid.attach(self._rate_lp_combo_box, 3, 1, 1, 1)
# Modulation
tr_grid.insert_column(4)
extra_tr_grid.remove_column(1)
tr_grid.attach(self._builder.get_object("mod_label"), 4, 0, 1, 1)
tr_grid.attach(self._mod_combo_box, 4, 1, 1, 1)
# TransmissionMode -> Roll off
rolloff_label = self._builder.get_object("rolloff_label")
rolloff_label.set_text("T mode")
# GuardInterval -> Pilot
pilot_label = self._builder.get_object("pilot_label")
pilot_label.set_text("Guard Interval")
# Hierarchy -> Pls Mode
pls_mode_label = self._builder.get_object("pls_mode_label")
pls_mode_label.set_text("Hierarchy")
# Models
fec_model, modulation_model, sys_model = self.get_models_for_non_satellite()
pol_model = self._pol_combo_box.get_model()
roll_off_model = self._rolloff_combo_box.get_model()
pilot_model = self._pilot_combo_box.get_model()
pls_model = self._pls_mode_combo_box.get_model()
# Models clearing
for m in pol_model, roll_off_model, pilot_model, pls_model:
m.clear()
self.init_terrestrial_models((pol_model, modulation_model, roll_off_model, pilot_model, pls_model, sys_model),
(BANDWIDTH, T_MODULATION, TRANSMISSION_MODE, GUARD_INTERVAL, HIERARCHY, T_SYSTEM))
# Removing the latest FEC elements from the model
for itr in [fec_model.get_iter(Gtk.TreePath.new_from_string(str(i))) for i in range(7, 11)]:
fec_model.remove(itr)
# Extra
self._namespace_entry.set_max_width_chars(15)
self._sys_combo_box.set_hexpand(False)
def init_terrestrial_models(self, models, properties):
for index, model in enumerate(models):
for v in properties[index].values():
model.append((v,))
def update_ui_for_cable(self):
tr_grid = self.get_transponder_grid_for_non_satellite()
tr_box = self._builder.get_object("tr_box")
# Models
fec_model, modulation_model, system_model = self.get_models_for_non_satellite()
extra_tr_grid = self._builder.get_object("extra_transponder_grid")
for child in extra_tr_grid.get_children():
extra_tr_grid.remove(child)
tr_grid.remove(extra_tr_grid)
tr_grid.insert_column(3)
tr_grid.insert_column(4)
tr_grid.insert_column(5)
# Modulation
tr_grid.attach(self._builder.get_object("mod_label"), 3, 0, 1, 1)
tr_grid.attach(self._mod_combo_box, 3, 1, 1, 1)
for v in C_MODULATION.values():
modulation_model.append((v,))
# Inversion
tr_grid.attach(self._builder.get_object("inversion_label"), 4, 0, 1, 1)
tr_grid.attach(self._invertion_combo_box, 4, 1, 1, 1)
# System
tr_grid.attach(self._builder.get_object("system_label"), 5, 0, 1, 1)
tr_grid.attach(self._sys_combo_box, 5, 1, 1, 1)
system_model.append((SystemCable.ANNEX_A.name,))
system_model.append((SystemCable.ANNEX_C.name,))
# FEC
fec_model.append(("None",))
# Extra
tr_box.remove(self._tr_extra_expander)
tr_grid.set_margin_bottom(5)
self._freq_entry.set_width_chars(10)
self._freq_entry.set_max_width_chars(10)
self._rate_entry.set_width_chars(10)
self._rate_entry.set_max_width_chars(10)
self._transponder_id_entry.set_max_width_chars(8)
self._network_id_entry.set_max_width_chars(8)
def get_transponder_grid_for_non_satellite(self):
self._pids_grid.set_visible(False)
tr_grid = self._builder.get_object("tr_grid")
tr_grid.remove_column(0)
tr_grid.remove_column(2)
return tr_grid
def get_models_for_non_satellite(self):
fec_model = self._fec_combo_box.get_model()
modulation_model = self._mod_combo_box.get_model()
modulation_model.clear()
system_model = self._sys_combo_box.get_model()
system_model.clear()
return fec_model, modulation_model, system_model
class TransponderServicesDialog:
def __init__(self, transient, model, transponder, tr_iters):
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("tr_services_dialog", "transponder_services_liststore"))
self._dialog = builder.get_object("tr_services_dialog")
self._dialog.set_transient_for(transient)
self._srv_model = builder.get_object("transponder_services_liststore")
self.append_services(model, transponder, tr_iters)
builder.get_object("srv_list_dialog_info_bar").connect("response", lambda bar, resp: bar.hide())
def append_services(self, model, transponder, tr_iters):
for row in model:
if row[Column.SRV_TRANSPONDER] == transponder:
self._srv_model.append((row[Column.SRV_SERVICE], row[Column.SRV_PACKAGE], row[Column.SRV_TYPE],
row[Column.SRV_SSID], row[Column.SRV_FREQ], row[Column.SRV_POS]))
tr_iters.append(model.get_iter(row.path))
def show(self):
response = self._dialog.run()
self._dialog.destroy()
return response
if __name__ == "__main__":
pass

1492
app/ui/settings_dialog.glade Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,54 +1,291 @@
from app.properties import write_config
from app.ui.dialogs import show_dialog, DialogType
from . import Gtk
from enum import Enum
from app.commons import run_task, run_idle
from app.connections import test_telnet, test_ftp, TestException, test_http
from app.properties import write_config, Profile, get_default_settings
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, NEW_COLOR, EXTRA_COLOR, FavClickMode
from .main_helper import update_entry_data
def show_settings_dialog(transient, options):
SettingsDialog(transient, options)
return SettingsDialog(transient, options).show()
class Property(Enum):
FTP = "ftp"
HTTP = "http"
TELNET = "telnet"
class SettingsDialog:
def __init__(self, transient, options):
handlers = {"on_data_dir_field_icon_press": self.on_data_dir_field_icon_press}
handlers = {"on_field_icon_press": self.on_field_icon_press,
"on_profile_changed": self.on_profile_changed,
"on_reset": self.on_reset,
"apply_settings": self.apply_settings,
"on_connection_test": self.on_connection_test,
"on_info_bar_close": self.on_info_bar_close,
"on_set_color_switch_state": self.on_set_color_switch_state,
"on_http_mode_switch_state": self.on_http_mode_switch_state}
builder = Gtk.Builder()
builder.add_objects_from_file("app/ui/dialogs.glade", ("settings_dialog", ))
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_from_file(UI_RESOURCES_PATH + "settings_dialog.glade")
builder.connect_signals(handlers)
self._options = options
self._dialog = builder.get_object("settings_dialog")
self._dialog.set_transient_for(transient)
self._header_bar = builder.get_object("header_bar")
# Network
self._host_field = builder.get_object("host_field")
self._host_field.set_text(options["host"])
self._port_field = builder.get_object("port_field")
self._port_field.set_text(options["port"])
self._login_field = builder.get_object("login_field")
self._login_field.set_text(options["user"])
self._password_field = builder.get_object("password_field")
self._password_field.set_text(options["password"])
self._http_login_field = builder.get_object("http_login_field")
self._http_password_field = builder.get_object("http_password_field")
self._http_port_field = builder.get_object("http_port_field")
self._telnet_login_field = builder.get_object("telnet_login_field")
self._telnet_password_field = builder.get_object("telnet_password_field")
self._telnet_port_field = builder.get_object("telnet_port_field")
self._telnet_timeout_spin_button = builder.get_object("telnet_timeout_spin_button")
self._settings_stack = builder.get_object("settings_stack")
# Paths
self._services_field = builder.get_object("services_field")
self._services_field.set_text(options["services_path"])
self._user_bouquet_field = builder.get_object("user_bouquet_field")
self._user_bouquet_field.set_text(options["user_bouquet_path"])
self._satellites_xml_field = builder.get_object("satellites_xml_field")
self._satellites_xml_field.set_text(options["satellites_xml_path"])
self._data_dir_field = builder.get_object("data_dir_field")
self._data_dir_field.set_text(options["data_dir_path"])
self._picons_field = builder.get_object("picons_field")
self._picons_dir_field = builder.get_object("picons_dir_field")
self._backup_dir_field = builder.get_object("backup_dir_field")
# Info bar
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label")
self._test_spinner = builder.get_object("test_spinner")
# Profile
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._support_http_api_check_button = builder.get_object("support_http_api_check_button")
# Program
self._before_save_switch = builder.get_object("before_save_switch")
self._before_downloading_switch = builder.get_object("before_downloading_switch")
self._program_frame = builder.get_object("program_frame")
self._extra_support_grid = builder.get_object("extra_support_grid")
self._colors_grid = builder.get_object("colors_grid")
self._set_color_switch = builder.get_object("set_color_switch")
self._new_color_button = builder.get_object("new_color_button")
self._extra_color_button = builder.get_object("extra_color_button")
self._click_mode_disabled_button = builder.get_object("click_mode_disabled_button")
self._click_mode_stream_button = builder.get_object("click_mode_stream_button")
self._click_mode_play_button = builder.get_object("click_mode_play_button")
self._click_mode_zap_button = builder.get_object("click_mode_zap_button")
# Options
self._options = options
self._active_profile = options.get("profile")
self.set_settings()
self.init_ui_elements(Profile(self._active_profile))
if self._dialog.run() == Gtk.ResponseType.OK:
options["host"] = self._host_field.get_text()
options["port"] = self._port_field.get_text()
options["user"] = self._login_field.get_text()
options["password"] = self._password_field.get_text()
options["services_path"] = self._services_field.get_text()
options["user_bouquet_path"] = self._user_bouquet_field.get_text()
options["satellites_xml_path"] = self._satellites_xml_field.get_text()
options["data_dir_path"] = self._data_dir_field.get_text()
write_config(options)
def init_ui_elements(self, profile):
is_enigma_profile = profile is Profile.ENIGMA_2
self._neutrino_radio_button.set_active(profile is Profile.NEUTRINO_MP)
self._settings_stack.get_child_by_name(Property.HTTP.value).set_visible(is_enigma_profile)
self._program_frame.set_sensitive(is_enigma_profile)
self._extra_support_grid.set_sensitive(is_enigma_profile)
http_active = self._support_http_api_check_button.get_active()
self._click_mode_zap_button.set_sensitive(is_enigma_profile and http_active)
self._click_mode_play_button.set_sensitive(is_enigma_profile and http_active)
def show(self):
response = self._dialog.run()
if response == Gtk.ResponseType.OK:
self.apply_settings()
self._dialog.destroy()
def on_data_dir_field_icon_press(self, entry, icon, event_button):
response = show_dialog(dialog_type=DialogType.CHOOSER, transient=self._dialog, options=self._options)
if response != Gtk.ResponseType.CANCEL:
entry.set_text(response)
return response
def on_field_icon_press(self, entry, icon, event_button):
update_entry_data(entry, self._dialog, self._options.get(self._options.get("profile")))
def on_profile_changed(self, item):
profile = Profile.ENIGMA_2 if self._enigma_radio_button.get_active() else Profile.NEUTRINO_MP
self._active_profile = profile.value
self.set_settings()
self.init_ui_elements(profile)
def set_profile(self, profile):
self._active_profile = profile.value
self.set_settings()
def on_reset(self, item):
def_settings = get_default_settings()
for key in def_settings:
current = self._options.get(key)
if type(current) is str:
continue
default = def_settings.get(key)
for k in default:
current[k] = default.get(k)
self.set_settings()
def set_settings(self):
def_settings = get_default_settings().get(self._active_profile)
options = self._options.get(self._active_profile)
self._host_field.set_text(options.get("host", def_settings["host"]))
self._port_field.set_text(options.get("port", def_settings["port"]))
self._login_field.set_text(options.get("user", def_settings["user"]))
self._password_field.set_text(options.get("password", def_settings["password"]))
self._http_login_field.set_text(options.get("http_user", def_settings["http_user"]))
self._http_password_field.set_text(options.get("http_password", def_settings["http_password"]))
self._http_port_field.set_text(options.get("http_port", def_settings["http_port"]))
self._telnet_login_field.set_text(options.get("telnet_user", def_settings["telnet_user"]))
self._telnet_password_field.set_text(options.get("telnet_password", def_settings["telnet_password"]))
self._telnet_port_field.set_text(options.get("telnet_port", def_settings["telnet_port"]))
self._telnet_timeout_spin_button.set_value(options.get("telnet_timeout", def_settings["telnet_timeout"]))
self._services_field.set_text(options.get("services_path", def_settings["services_path"]))
self._user_bouquet_field.set_text(options.get("user_bouquet_path", def_settings["user_bouquet_path"]))
self._satellites_xml_field.set_text(options.get("satellites_xml_path", def_settings["satellites_xml_path"]))
self._picons_field.set_text(options.get("picons_path", def_settings["picons_path"]))
self._data_dir_field.set_text(options.get("data_dir_path", def_settings["data_dir_path"]))
self._picons_dir_field.set_text(options.get("picons_dir_path", def_settings["picons_dir_path"]))
self._backup_dir_field.set_text(options.get("backup_dir_path", def_settings["backup_dir_path"]))
self._before_save_switch.set_active(options.get("backup_before_save", def_settings["backup_before_save"]))
self._before_downloading_switch.set_active(options.get("backup_before_downloading",
def_settings["backup_before_downloading"]))
self.set_fav_click_mode(options.get("fav_click_mode", def_settings["fav_click_mode"]))
if Profile(self._active_profile) is Profile.ENIGMA_2:
self._support_ver5_check_button.set_active(options.get("v5_support", False))
self._support_http_api_check_button.set_active(options.get("http_api_support", False))
self._set_color_switch.set_active(options.get("use_colors", False))
new_rgb = Gdk.RGBA()
new_rgb.parse(options.get("new_color", NEW_COLOR))
extra_rgb = Gdk.RGBA()
extra_rgb.parse(options.get("extra_color", EXTRA_COLOR))
self._new_color_button.set_rgba(new_rgb)
self._extra_color_button.set_rgba(extra_rgb)
def apply_settings(self, item=None):
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()
options["user"] = self._login_field.get_text()
options["password"] = self._password_field.get_text()
options["http_user"] = self._http_login_field.get_text()
options["http_password"] = self._http_password_field.get_text()
options["http_port"] = self._http_port_field.get_text()
options["telnet_user"] = self._telnet_login_field.get_text()
options["telnet_password"] = self._telnet_password_field.get_text()
options["telnet_port"] = self._telnet_port_field.get_text()
options["telnet_timeout"] = int(self._telnet_timeout_spin_button.get_value())
options["services_path"] = self._services_field.get_text()
options["user_bouquet_path"] = self._user_bouquet_field.get_text()
options["satellites_xml_path"] = self._satellites_xml_field.get_text()
options["picons_path"] = self._picons_field.get_text()
options["data_dir_path"] = self._data_dir_field.get_text()
options["picons_dir_path"] = self._picons_dir_field.get_text()
options["backup_dir_path"] = self._backup_dir_field.get_text()
options["backup_before_save"] = self._before_save_switch.get_active()
options["backup_before_downloading"] = self._before_downloading_switch.get_active()
options["fav_click_mode"] = self.get_fav_click_mode()
if profile is Profile.ENIGMA_2:
options["v5_support"] = self._support_ver5_check_button.get_active()
options["http_api_support"] = self._support_http_api_check_button.get_active()
options["use_colors"] = self._set_color_switch.get_active()
options["new_color"] = self._new_color_button.get_rgba().to_string()
options["extra_color"] = self._extra_color_button.get_rgba().to_string()
write_config(self._options)
@run_task
def on_connection_test(self, item):
if self._test_spinner.get_state() is Gtk.StateType.ACTIVE:
return
self.show_spinner(True)
current_property = Property(self._settings_stack.get_visible_child_name())
if current_property is Property.HTTP:
self.test_http()
elif current_property is Property.TELNET:
self.test_telnet()
elif current_property is Property.FTP:
self.test_ftp()
def test_http(self):
user, password = self._http_login_field.get_text(), self._http_password_field.get_text()
host, port = self._host_field.get_text(), self._http_port_field.get_text()
try:
self.show_info_message(test_http(host, port, user, password), Gtk.MessageType.INFO)
except TestException as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
finally:
self.show_spinner(False)
def test_telnet(self):
timeout = int(self._telnet_timeout_spin_button.get_value())
host, port = self._host_field.get_text(), self._telnet_port_field.get_text()
user, password = self._telnet_login_field.get_text(), self._telnet_password_field.get_text()
try:
self.show_info_message(test_telnet(host, port, user, password, timeout), Gtk.MessageType.INFO)
self.show_spinner(False)
except TestException as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
self.show_spinner(False)
def test_ftp(self):
host, port = self._host_field.get_text(), self._port_field.get_text()
user, password = self._login_field.get_text(), self._password_field.get_text()
try:
self.show_info_message("OK. {}".format(test_ftp(host, port, user, password)), Gtk.MessageType.INFO)
except TestException as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
finally:
self.show_spinner(False)
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(text)
@run_idle
def show_spinner(self, show):
self._test_spinner.start() if show else self._test_spinner.stop()
self._test_spinner.set_state(Gtk.StateType.ACTIVE if show else Gtk.StateType.NORMAL)
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
def on_set_color_switch_state(self, switch, state):
self._colors_grid.set_sensitive(state)
def on_http_mode_switch_state(self, switch, state):
self._click_mode_play_button.set_sensitive(state)
self._click_mode_zap_button.set_sensitive(state)
if self._click_mode_play_button.get_active() or self._click_mode_zap_button.get_active():
self._click_mode_disabled_button.set_active(True)
@run_idle
def set_fav_click_mode(self, mode):
mode = FavClickMode(mode)
self._click_mode_disabled_button.set_active(mode is FavClickMode.DISABLED)
self._click_mode_stream_button.set_active(mode is FavClickMode.STREAM)
self._click_mode_play_button.set_active(mode is FavClickMode.PLAY)
self._click_mode_zap_button.set_active(mode is FavClickMode.ZAP)
def get_fav_click_mode(self):
if self._click_mode_zap_button.get_active():
return FavClickMode.ZAP
if self._click_mode_play_button.get_active():
return FavClickMode.PLAY
if self._click_mode_stream_button.get_active():
return FavClickMode.STREAM
return FavClickMode.DISABLED
if __name__ == "__main__":

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

@@ -0,0 +1,161 @@
import locale
import os
import gi
from enum import Enum, IntEnum
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/"
IS_GNOME_SESSION = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
# 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.lookup_icon("emblem-shared", 16, 0) else None
EPG_ICON = theme.load_icon("gtk-index", 16, 0) if theme.lookup_icon("gtk-index", 16, 0) else None
# Colors
NEW_COLOR = "rgb(255,230,204)" # Color for new services in the main list
EXTRA_COLOR = "rgb(179,230,204)" # Color for services with a extra name for the bouquet
class KeyboardKey(Enum):
""" The raw(hardware) codes of the keyboard keys. """
Q = 24
E = 26
R = 27
T = 28
U = 30
O = 32
P = 33
S = 39
D = 40
H = 43
L = 46
X = 53
C = 54
V = 55
B = 56
W = 25
Z = 52
INSERT = 118
HOME = 110
END = 115
UP = 111
DOWN = 116
PAGE_UP = 112
PAGE_DOWN = 117
LEFT = 113
RIGHT = 114
F2 = 68
SPACE = 65
DELETE = 119
BACK_SPACE = 22
CTRL_L = 37
CTRL_R = 105
# Laptop codes
HOME_KP = 79
END_KP = 87
PAGE_UP_KP = 81
PAGE_DOWN_KP = 89
@classmethod
def value_exist(cls, value):
return value in (val.value for val in cls.__members__.values())
# Keys for move in lists. KEY_KP_(NAME) for laptop!!!
MOVE_KEYS = (KeyboardKey.UP, KeyboardKey.PAGE_UP, KeyboardKey.DOWN, KeyboardKey.PAGE_DOWN, KeyboardKey.HOME,
KeyboardKey.END, KeyboardKey.HOME_KP, KeyboardKey.END_KP, KeyboardKey.PAGE_UP_KP, KeyboardKey.PAGE_DOWN_KP)
class FavClickMode(IntEnum):
""" Double click mode on the service in the bouquet(FAV) list. """
DISABLED = 0
STREAM = 1
PLAY = 2
ZAP = 3
class ViewTarget(Enum):
""" Used for set target view. """
BOUQUET = 0
FAV = 1
SERVICES = 2
class BqGenType(Enum):
""" Bouquet generation type. """
SAT = 0
EACH_SAT = 1
PACKAGE = 2
EACH_PACKAGE = 3
TYPE = 4
EACH_TYPE = 5
class Column(IntEnum):
""" Column nums in the views """
# main view
SRV_CAS_FLAGS = 0
SRV_STANDARD = 1
SRV_CODED = 2
SRV_SERVICE = 3
SRV_LOCKED = 4
SRV_HIDE = 5
SRV_PACKAGE = 6
SRV_TYPE = 7
SRV_PICON = 8
SRV_PICON_ID = 9
SRV_SSID = 10
SRV_FREQ = 11
SRV_RATE = 12
SRV_POL = 13
SRV_FEC = 14
SRV_SYSTEM = 15
SRV_POS = 16
SRV_DATA_ID = 17
SRV_FAV_ID = 18
SRV_TRANSPONDER = 19
SRV_TOOLTIP = 20
SRV_BACKGROUND = 21
# fav view
FAV_NUM = 0
FAV_CODED = 1
FAV_SERVICE = 2
FAV_LOCKED = 3
FAV_HIDE = 4
FAV_TYPE = 5
FAV_POS = 6
FAV_ID = 7
FAV_PICON = 8
FAV_TOOLTIP = 9
FAV_BACKGROUND = 10
# bouquets view
BQ_NAME = 0
BQ_LOCKED = 1
BQ_HIDDEN = 2
BQ_TYPE = 3
def __index__(self):
""" Overridden to get the index in slices directly """
return self.value
if __name__ == "__main__":
pass

19
build-deb.sh Executable file
View File

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

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

@@ -0,0 +1,48 @@
demon-editor for Debian
----------------------
DemonEditor
Enigma2 channel and satellites list editor for GNU/Linux.
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc)
Keyboard shortcuts:
Ctrl + Insert - copies the selected channels from the main list to the the bouquet beginning or inserts (creates) a new bouquet.
Ctrl + BackSpace - copies the selected channels from the main list to the bouquet end.
Ctrl + X - only in bouquet list. Ctrl + C - only in services list.
Clipboard is "rubber". There is an accumulation before the insertion!
Ctrl + E - edit.
Ctrl + R, F2 - rename.
Ctrl + S, T in Satellites edit tool for create satellite or transponder.
Ctrl + L - parental lock.
Ctrl + H - hide/skip.
Ctrl + P - start play IPTV or other stream in the bouquet list.
Ctrl + Z - switch (zap) the channel (works when the HTTP API is enabled, Enigma2 only).
Ctrl + W - switch to the channel and watch in the program.
Space - select/deselect.
Left/Right - remove selection.
Ctrl + Up, Down, PageUp, PageDown, Home, End - move selected items in the list.
Ctrl + O - (re)load user data from current dir.
Ctrl + D - load data from receiver.
Ctrl + U/B upload data/bouquets to receiver.
Extra:
Import feature.
Multiple selections in lists only with Space key (as in file managers).
Ability to download picons and update satellites (transponders) from web.
Ability to import into bouquet (Neutrino WEB TV) from m3u.
Ability to export bouquets with IPTV services to m3u.
Assignment EPG from DVB or XML for IPTV services(Enigma2 only).
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.
Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2!
Important:
Main supported lamedb format is version 4. Versions 3 and 5 has only experimental support!
For version **3** is only read mode available. When saving, version **4** format is used instead!

9
deb/DEBIAN/control Normal file
View File

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

26
deb/DEBIAN/copyright Normal file
View File

@@ -0,0 +1,26 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Contact: Dmitriy Yefremov <dmitry.v.yefremov@gmail.com>
Source: https://github.com/DYefremov/DemonEditor
Files: *
MIT License
Copyright (c) 2018-2019 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

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

2
deb/usr/bin/demoneditor.sh Executable file
View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

765
po/es/demon-editor.po Normal file
View File

@@ -0,0 +1,765 @@
# Copyright (C) 2018-2019 Frank Neirynck
# This file is distributed under the MIT license.
#
# Frank Neirynck <frank@insink.be>, 2018-2019.
#
msgid ""
msgstr ""
"Last-Translator: Frank Neirynck\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Language-Team: \n"
"X-Generator: Poedit 2.2.1\n"
msgid "translator-credits"
msgstr "Frank Neirynck <frank@insink.be>"
# Main
msgid "Service"
msgstr "Servicio"
msgid "Package"
msgstr "Paquete"
msgid "Type"
msgstr "Tipo"
msgid "Picon"
msgstr "Picon"
msgid "Freq"
msgstr "Frec."
msgid "Rate"
msgstr "Ratio"
msgid "Pol"
msgstr "Pol."
msgid "System"
msgstr "Sistema"
msgid "Pos"
msgstr "Pos."
msgid "Num"
msgstr "Núm"
msgid "Current IP:"
msgstr "IP actual:"
msgid "Assign"
msgstr "Assignar"
msgid "Bouquet details"
msgstr "Detalles Ramo"
msgid "Bouquets"
msgstr "Ramos"
msgid "Copy"
msgstr "Copiar"
msgid "Copy reference"
msgstr "Copia de Referencia"
msgid "Download"
msgstr "Descargar"
msgid "Edit"
msgstr "Editar"
msgid "Edit "
msgstr "Editar "
msgid "Edit mаrker text"
msgstr "Editar texto del mаrcador"
msgid "FTP-transfer"
msgstr "Transferencia-FTP"
msgid "Global search"
msgstr "Búsqueda Global"
msgid "Hide"
msgstr "Esconder"
msgid "Hide/Skip On/Off Ctrl + H"
msgstr "Esconder/Saltar Encender/Apagar Ctrl + H"
msgid "Add IPTV or stream service"
msgstr "Añadir servicio IPTV"
msgid "Import m3u"
msgstr "Importar m3u"
msgid "Import m3u file"
msgstr "Importar fichero m3u"
msgid "List configuration"
msgstr "Lista configuración"
msgid "Rename for this bouquet"
msgstr "Renombrar para este ramo"
msgid "Set default name"
msgstr "Establecer nombre predeterminado"
msgid "Insert marker"
msgstr "Insertar marcador"
msgid "Locate in services"
msgstr "Buscar en servicios"
msgid "Locked"
msgstr "Cerrado"
msgid "Move"
msgstr "Mover"
msgid "New"
msgstr "Nuevo"
msgid "New bouquet"
msgstr "Ramo nuevo"
msgid "Create bouquet"
msgstr "Crear ramo"
msgid "For current satellite"
msgstr "Para el Satélite actual"
msgid "For current package"
msgstr "Para el paquete actual"
msgid "For current type"
msgstr "Para el tipo actual"
msgid "For each satellite"
msgstr "Para cada Satélite"
msgid "For each package"
msgstr "Para cada paquete"
msgid "For each type"
msgstr "Para cada tipo"
msgid "Open"
msgstr "Abrir"
msgid "Parent lock On/Off Ctrl + L"
msgstr "Bloqueo parentesco Encender/Apagar Ctrl + L"
msgid "Picons"
msgstr "Picons"
msgid "Picons downloader"
msgstr "Picons descargar"
msgid "Satellites downloader"
msgstr "Satélites descargar"
msgid "Remove"
msgstr "Remover"
msgid "Remove all unavailable"
msgstr "Remover todo lo indisponible"
msgid "Satellites editor"
msgstr "Editor Satélites"
msgid "Save"
msgstr "Guardar"
msgid "Search"
msgstr "Buscar"
msgid "Services"
msgstr "Servicios"
msgid "Services filter"
msgstr "Filtro servicios"
msgid "Settings"
msgstr "Configuraciones"
msgid "Up"
msgstr "Arriba"
msgid "Down"
msgstr "Abajo"
msgid "Active profile:"
msgstr "Perfil activo:"
msgid "All"
msgstr "Todo"
msgid "Are you sure?"
msgstr "Estás seguro?"
msgid "Current data path:"
msgstr "Ruta de datos actual:"
msgid "Data:"
msgstr "Datos:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux"
msgstr "Editor de Canales y Satélites Enigma2 para GNU/Linux"
msgid "Host:"
msgstr "Anfitrión:"
msgid "Loading data..."
msgstr "Cargar datos..."
msgid "Receive"
msgstr "Recibir"
msgid "Receive files from receiver"
msgstr "Recibir ficheros de su receptor"
msgid "Receiver IP:"
msgstr "Receptor IP:"
msgid "Remove unused bouquets"
msgstr "Remover ramos sin usar"
msgid "Reset profile"
msgstr "Restablecer perfil"
msgid "Satellites"
msgstr "Satélites"
msgid "Satellites.xml file:"
msgstr "Fichero Satellites.xml:"
msgid "Selected"
msgstr "Seleccionado"
msgid "Send"
msgstr "Enviar"
msgid "Send files to receiver"
msgstr "Enviar ficheros al receptor"
msgid "Services and Bouquets files:"
msgstr "Ficheros de Servicios y ramos:"
msgid "User bouquet files:"
msgstr "Importar ficheros de ramos:"
msgid "Extra:"
msgstr "Extra:"
# Filter bar
msgid "Only free"
msgstr "Solamente gratis"
msgid "All positions"
msgstr "Todas las posiciones"
msgid "All types"
msgstr "Todos los tipos"
# Streams player
msgid "Play"
msgstr "Play"
msgid "Stop playback"
msgstr "Detener la reproducción"
msgid "Previous stream in the list"
msgstr "Secuencia anterior en la lista"
msgid "Next stream in the list"
msgstr "Secuencia siguiente en la lista"
msgid "Toggle in fullscreen"
msgstr "Cambiar a pantalla completa"
msgid "Close"
msgstr "Cerrar"
# Picons dialog
msgid "Load providers"
msgstr "Cargar Proveedores"
msgid "Providers"
msgstr "Proveedores"
msgid "Receive picons"
msgstr "Recibir picons"
msgid "Picons name format:"
msgstr "Picons formato nombres:"
msgid "Resize:"
msgstr "Redimensionar:"
msgid "Current picons path:"
msgstr "Ruta actual Picons:"
msgid "Receiver picons path:"
msgstr "Ruta picons receptor:"
msgid "Picons download tool"
msgstr "Picons herramiento de descarga"
msgid "Transfer to receiver"
msgstr "Transferir al receptor"
msgid "Downloader"
msgstr "Descargador"
msgid "Converter"
msgstr "Convertidor"
msgid "Convert"
msgstr "Convertir"
msgid "Path to save:"
msgstr "Ruta para guardar:"
msgid "Path to Enigma2 picons:"
msgstr "Ruta a picons Enigma2:"
msgid "Specify the correct position value for the provider!"
msgstr "Especifique la posición correcta para el proveedor!"
msgid "Converter between name formats"
msgstr "Conversor entre formatos de nombre"
msgid "Receive picons for providers"
msgstr "Recibir picons para proovedor"
msgid "Load satellite providers."
msgstr "Cargar proovedores Satélite."
msgid ""
"To automatically set the identifiers for picons,\n"
"first load the required services list into the main application window."
msgstr ""
"Para configurar automáticamente los identificadores para picons, \n"
"primero cargue la lista de serviços requeridos en la ventana principal."
# Satellites editor
msgid "Satellites edit tool"
msgstr "Editor de Satélites"
msgid "Add"
msgstr "Añadir"
msgid "Satellite"
msgstr "Satélite"
msgid "Transponder"
msgstr "Transpondedor"
msgid "Satellite properties:"
msgstr "Propiedades del Satélite:"
msgid "Transponder properties:"
msgstr "Propiedades del Transpondedor:"
msgid "Name"
msgstr "Nombre"
msgid "Position"
msgstr "Posición"
# Satellites update dialog
msgid "Satellites update"
msgstr "Actualisar Satélite"
msgid "Remove selection"
msgstr "Remover selección"
# Service details dialog
msgid "Service data:"
msgstr "Datos servicio:"
msgid "Transponder data:"
msgstr "Datos Transpondedor:"
msgid "Service data"
msgstr "Datos servicio"
msgid "Transponder details"
msgstr "Detalles Transpondedor"
msgid ""
"Changes will be applied to all services of this transponder!\n"
"Continue?"
msgstr ""
"Los cambios se aplicarán a todos los servicios de este transpondedor!\n"
"Continuar?"
msgid "Reference"
msgstr "Referencia"
msgid "Namespace"
msgstr "Namespace"
msgid "Flags:"
msgstr "Flags:"
msgid "Delays (ms):"
msgstr "Retraso (mc)"
msgid "Bitstream"
msgstr "Secuencia de Bits"
msgid "Description"
msgstr "Descripción"
msgid "Source:"
msgstr "Fuente:"
msgid "Cancel"
msgstr "Annular"
msgid "Update"
msgstr "Actualisar"
msgid "Filter"
msgstr "Filtrar"
msgid "Find"
msgstr "Buscar"
# IPTV dialog
msgid "Stream data"
msgstr "Datos de la Secuencia"
# IPTV list configuration dialog
msgid "Starting values"
msgstr "Valores iniciales"
msgid "Reset to default"
msgstr "Restablecer a predeterminado"
msgid "IPTV streams list configuration"
msgstr "Configurar lista de Secuencias IPTV"
# Settings dialog
msgid "Preferences"
msgstr "Preferencias"
msgid "Profile:"
msgstr "Perfil:"
msgid "Timeout between commands in seconds"
msgstr "Tiempo de espera entre comandos en segundos"
msgid "Timeout:"
msgstr "Time-out:"
msgid "Login:"
msgstr "Usuario:"
msgid "Options"
msgstr "Opciones"
msgid "Password:"
msgstr "Contraseña:"
msgid "Picons:"
msgstr "Picons:"
msgid "Port:"
msgstr "Puerta:"
msgid "Data path:"
msgstr "Ruta de datos:"
msgid "Picons path:"
msgstr "Ruta de Picons:"
msgid "Network settings:"
msgstr "Configuración de red:"
msgid "STB file paths:"
msgstr "Ruta de ficherors STB:"
msgid "Local file paths:"
msgstr "Ruta de ficheros local:"
# Dialogs messages
msgid "Error. No bouquet is selected!"
msgstr "Error. Ningún ramo está seleccionado!"
msgid "This item is not allowed to be removed!"
msgstr "Este artículo no puede ser eliminado!"
msgid "This item is not allowed to edit!"
msgstr "Este artículo no puede ser editado!"
msgid "Not allowed in this context!"
msgstr "No permitido en este contexto!"
msgid "Please, download files from receiver or setup your path for read data!"
msgstr "Por favor, descargue archivos desde el receptor o configure su ruta para leer los datos!"
msgid "Reading data error!"
msgstr "Error de lectura de datos!"
msgid "No m3u file is selected!"
msgstr "Ningún archivo m3u ha sido seleccionado!"
msgid "Not implemented yet!"
msgstr "Aun no implementado!"
msgid "The text of marker is empty, please try again!"
msgstr "El texto del marcador está vacío, inténtalo de nuevo!"
msgid "Please, select only one item!"
msgstr "Por favor, seleccione solo un elemento!"
msgid "No png file is selected!"
msgstr "Ningún fichero png seleccionado!"
msgid "No reference is present!"
msgstr "Ninguna referencia presente!"
msgid "No selected item!"
msgstr "Ningún elemento seleccionado!"
msgid "The task is already running!"
msgstr "La tarea ya se está ejecutando!"
msgid "Done!"
msgstr "Hecho!"
msgid "Please, wait..."
msgstr "Por favor, espere..."
msgid "Resizing..."
msgstr "Redimensionando..."
msgid "Select paths!"
msgstr "Seleccione rutas!"
msgid "No satellite is selected!"
msgstr "Ningún Satélite seleccionado!"
msgid "Please, select only one satellite!"
msgstr "Seleccione solo un Satélite!"
msgid "Please check your parameters and try again."
msgstr "Por favor revise sus parámetros y vuelva a intentar!"
msgid "No satellites.xml file is selected!"
msgstr "Ningún satellites.xml seleccionado!"
msgid "Error. Verify the data!"
msgstr "Error. Revise sus datos!"
msgid "Operation not allowed in this context!"
msgstr "Operación no permitida en este contexto!"
msgid "No VLC is found. Check that it is installed!"
msgstr "VLC no encontrado. Verifica si está instalado!"
# Search unavailable streams dialog
msgid "Please wait, streams testing in progress..."
msgstr "Por favor espera una prueba de las secuencias..."
msgid "Found"
msgstr "Encontrado"
msgid "unavailable streams."
msgstr "Secuencias no presentes"
msgid "No changes required!"
msgstr "ningún cambio requerido!"
msgid "This list does not contains IPTV streams!"
msgstr "La lista no contiene secuencias IPTV!"
msgid "New empty configuration"
msgstr "Nueva configuración vacía"
msgid "No data to save!"
msgstr "No hay datos para guardar!"
msgid "Network"
msgstr "Red"
msgid "Paths"
msgstr "Rutas"
msgid "Program"
msgstr "Programa"
msgid "Backup:"
msgstr "Backup:"
msgid "Backup"
msgstr "Backup"
msgid "Backups"
msgstr "Backups"
msgid "Backup path:"
msgstr "Ruta del backup:"
msgid "Restore bouquets"
msgstr "Restaurar ramos"
msgid "Restore all"
msgstr "Restaurar todo"
msgid "Before saving"
msgstr "Antes de guardar"
msgid "Before downloading from the receiver"
msgstr "Antes de recibir del receptor"
msgid "Set background color for the services"
msgstr "Determinar color de fondo para servicios"
msgid "Marked as new:"
msgstr "Marcado como nuevo:"
msgid "With an extra name in the bouquet:"
msgstr "Con nombre adicional en ramo:"
msgid "Select"
msgstr "Seleccione"
msgid "About"
msgstr "Sobre"
msgid "Exit"
msgstr "Salir"
msgid "Tools"
msgstr "Herramientas"
# Import
msgid "Import"
msgstr "Importar"
msgid "Bouquet"
msgstr "Ramo"
msgid "Bouquets and services"
msgstr "Ramos y servicios"
msgid "The main list does not contain services for this bouquet!"
msgstr "La lista principal no contiene servicios para este ramo!"
msgid "No bouquet file is selected!"
msgstr "Nigún fichero de ramo ha sido seleccionado!"
msgid "Remove all unused"
msgstr "Quite todos los"
msgid "Test"
msgstr "Prueba"
msgid "Test connection"
msgstr "Probar conexión"
msgid "Double click on the service in the bouquet list:"
msgstr "Haga doble clic en el servicio en la lista de bouquet:"
msgid "Zap"
msgstr "Zapear"
msgid "Play stream"
msgstr "Reproducir secuencia"
msgid "Disabled"
msgstr "Desactivado"
msgid "Enable ver. 5 support (experimental)"
msgstr "Soporte para ver. 5 (experimental)"
msgid "Enable HTTP API (experimental)"
msgstr "Habilitar API HTTP (experimental)"
msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Cambiar (ZAP) el canal (Ctrl + Z)"
msgid "Switch the channel and watch in the program(Ctrl + W)"
msgstr "Cambiar el canal y ver en el programa (Ctrl + W)"
msgid "Play IPTV or other stream in the program(Ctrl + P)"
msgstr "Reproducir IPTV u otro flujo en el programa (Ctrl + P)"
msgid "Export to m3u"
msgstr "Exportar hacia m3u"
msgid "EPG configuration"
msgstr "Configuración EPG"
msgid "Apply"
msgstr "Aplicar"
msgid "EPG source"
msgstr "Fuente EPG"
msgid "Service names source:"
msgstr "Nombre de servicio fuente:"
msgid "Main service list"
msgstr "Lista principal de servicios:"
msgid "XML file"
msgstr "Archivo XML"
msgid "Use web source"
msgstr "Usar fuente web"
msgid "Url to *.xml.gz file:"
msgstr "URL del archivo *.xml.gz:"
msgid "Enable filtering"
msgstr "Habilitar filtrar"
msgid "Filter by presence in the epg.dat file."
msgstr "Filtrar por presencia del archivo epg.dat."
msgid "Paths to the epg.dat file:"
msgstr "Ruta al archivo epg.dat:"
msgid "Local path:"
msgstr "Ruta local:"
msgid "STB path:"
msgstr "Ruta STB:"
msgid "Update on start"
msgstr "Actualisar al iniciar"
msgid "Auto configuration by service names."
msgstr "Auto configuración por nombres de servicios."
msgid "Save list to xml."
msgstr "Guardar como XML."
msgid "Download XML file error."
msgstr "Error bajando archivo XML."
msgid "Unsupported file type:"
msgstr "Archivo no supportado:"
msgid "Unpacking data error."
msgstr "Error abriende datos."
msgid "XML parsing error:"
msgstr "Error analisando XML:"
msgid "Count of successfully configured services:"
msgstr "Número de servicios configurados con éxito:"
msgid "Current epg.dat file does not contains references for the services of this bouquet!"
msgstr "Archivo epg.dat actual no tiene referencias a servicios de este ramo!"

765
po/nl/demon-editor.po Normal file
View File

@@ -0,0 +1,765 @@
# Copyright (C) 2018-2019 Frank Neirynck
# This file is distributed under the MIT license.
#
# Frank Neirynck <frank@insink.be>, 2018-2019.
#
msgid ""
msgstr ""
"Last-Translator: Frank Neirynck\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Language-Team: \n"
"X-Generator: Poedit 2.2.1\n"
msgid "translator-credits"
msgstr "Frank Neirynck <frank@insink.be>"
# Main
msgid "Service"
msgstr "Dienst"
msgid "Package"
msgstr "Pakket"
msgid "Type"
msgstr "Type"
msgid "Picon"
msgstr "Picon"
msgid "Freq"
msgstr "Freq."
msgid "Rate"
msgstr "Rate"
msgid "Pol"
msgstr "Pol."
msgid "System"
msgstr "Systeem"
msgid "Pos"
msgstr "Pos."
msgid "Num"
msgstr "Nr"
msgid "Current IP:"
msgstr "Huidig IP:"
msgid "Assign"
msgstr "Toekennen"
msgid "Bouquet details"
msgstr "Details Boeket"
msgid "Bouquets"
msgstr "Boeketten"
msgid "Copy"
msgstr "Kopieer"
msgid "Copy reference"
msgstr "Referentiekopie"
msgid "Download"
msgstr "Download"
msgid "Edit"
msgstr "Wijzig"
msgid "Edit "
msgstr "Wijzig "
msgid "Edit mаrker text"
msgstr "Wijzig mаrker tekst"
msgid "FTP-transfer"
msgstr "FTP-overdracht"
msgid "Global search"
msgstr "Globale zoekopdracht"
msgid "Hide"
msgstr "Verberg"
msgid "Hide/Skip On/Off Ctrl + H"
msgstr "Verberg/Overslaan Aan/Uit Ctrl + H"
msgid "Add IPTV or stream service"
msgstr "Voeg IPTV of stream dienst toe"
msgid "Import m3u"
msgstr "Importeer m3u"
msgid "Import m3u file"
msgstr "Importeer m3u file"
msgid "List configuration"
msgstr "Lijst configuratie"
msgid "Rename for this bouquet"
msgstr "Hernoem voor dit boeket"
msgid "Set default name"
msgstr "Stel standaard naam in"
msgid "Insert marker"
msgstr "Inserteer marker"
msgid "Locate in services"
msgstr "Zoek in diensten"
msgid "Locked"
msgstr "Gesloten"
msgid "Move"
msgstr "Verplaats"
msgid "New"
msgstr "Nieuw"
msgid "New bouquet"
msgstr "Nieuw boeket"
msgid "Create bouquet"
msgstr "Creeer boeket"
msgid "For current satellite"
msgstr "Voor huidige satelliet"
msgid "For current package"
msgstr "Voor huidig pakket"
msgid "For current type"
msgstr "Voor huidig type"
msgid "For each satellite"
msgstr "Voor elke satelliet"
msgid "For each package"
msgstr "Voor elk pakket"
msgid "For each type"
msgstr "Voor elk type"
msgid "Open"
msgstr "Open"
msgid "Parent lock On/Off Ctrl + L"
msgstr "Ouderlijk slot aan/uit Ctrl + L"
msgid "Picons"
msgstr "Picons"
msgid "Picons downloader"
msgstr "Picons downloader"
msgid "Satellites downloader"
msgstr "Satellieten downloader"
msgid "Remove"
msgstr "Verwijder"
msgid "Remove all unavailable"
msgstr "Verwijder alle onbeschikbare"
msgid "Satellites editor"
msgstr "Satelliet editor"
msgid "Save"
msgstr "Opslaan"
msgid "Search"
msgstr "Zoek"
msgid "Services"
msgstr "Diensten"
msgid "Services filter"
msgstr "Diensten filter"
msgid "Settings"
msgstr "Instellingen"
msgid "Up"
msgstr "Boven"
msgid "Down"
msgstr "Onder"
msgid "Active profile:"
msgstr "Actief profiel:"
msgid "All"
msgstr "Alle"
msgid "Are you sure?"
msgstr "Ben je zeker?"
msgid "Current data path:"
msgstr "Huidig datapad:"
msgid "Data:"
msgstr "Data:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux"
msgstr "Enigma2 kanaal and satelliet lijst editor voor GNU/Linux"
msgid "Host:"
msgstr "Host:"
msgid "Loading data..."
msgstr "Laden data..."
msgid "Receive"
msgstr "Ontvangen"
msgid "Receive files from receiver"
msgstr "Ophalen files van uw ontvanger"
msgid "Receiver IP:"
msgstr "Ontvanger IP:"
msgid "Remove unused bouquets"
msgstr "Verwijder ongebruikte boeketten"
msgid "Reset profile"
msgstr "Profiel terugzetten"
msgid "Satellites"
msgstr "Satellieten"
msgid "Satellites.xml file:"
msgstr "Satellites.xml bestand:"
msgid "Selected"
msgstr "Geselecteerd"
msgid "Send"
msgstr "Verzend"
msgid "Send files to receiver"
msgstr "Verzend bestanden naar ontvanger"
msgid "Services and Bouquets files:"
msgstr "Diensten en Boeketten bestanden:"
msgid "User bouquet files:"
msgstr "Importeer boeket bestanden:"
msgid "Extra:"
msgstr "Extra:"
# Filter bar
msgid "Only free"
msgstr "Enkel gratis"
msgid "All positions"
msgstr "Alle posities"
msgid "All types"
msgstr "Alle types"
# Streams player
msgid "Play"
msgstr "Play"
msgid "Stop playback"
msgstr "Stop afspelen"
msgid "Previous stream in the list"
msgstr "Vorige stream in de lijst"
msgid "Next stream in the list"
msgstr "Volgende stream in de lijst"
msgid "Toggle in fullscreen"
msgstr "Wissel naar volledig scherm"
msgid "Close"
msgstr "Afsluiten"
# Picons dialog
msgid "Load providers"
msgstr "Laad leveranciers"
msgid "Providers"
msgstr "Leveranciers"
msgid "Receive picons"
msgstr "Ontvang picons"
msgid "Picons name format:"
msgstr "Picons naam formaat:"
msgid "Resize:"
msgstr "Verklein/vergroot:"
msgid "Current picons path:"
msgstr "Huidig pad picons:"
msgid "Receiver picons path:"
msgstr "Ontvanger picons pad:"
msgid "Picons download tool"
msgstr "Picons download gereedschap"
msgid "Transfer to receiver"
msgstr "Transfereren naar ontvanger"
msgid "Downloader"
msgstr "Downloader"
msgid "Converter"
msgstr "Omvormer"
msgid "Convert"
msgstr "Converteer"
msgid "Path to save:"
msgstr "Pad om op te slaan:"
msgid "Path to Enigma2 picons:"
msgstr "Pad naar Enigma2 picons:"
msgid "Specify the correct position value for the provider!"
msgstr "Geeft de juiste positie waarde voor de provider!"
msgid "Converter between name formats"
msgstr "Omvormer tussen naam formaten"
msgid "Receive picons for providers"
msgstr "Ontvang picons voor leveranciers"
msgid "Load satellite providers."
msgstr "Laad satelliet leveranciers."
msgid ""
"To automatically set the identifiers for picons,\n"
"first load the required services list into the main application window."
msgstr ""
"Om automatisch de ID in te stellen voor picons,\n"
"laad eerst de vereiste serviceslijst in via het hoofdvenster van het programma."
# Satellites editor
msgid "Satellites edit tool"
msgstr "Satelliet wijzingingsgereedschap"
msgid "Add"
msgstr "Voeg toe"
msgid "Satellite"
msgstr "Satelliet"
msgid "Transponder"
msgstr "Transponder"
msgid "Satellite properties:"
msgstr "Satelliet eigenschappen:"
msgid "Transponder properties:"
msgstr "Transponder eigenschappen:"
msgid "Name"
msgstr "Naam"
msgid "Position"
msgstr "Positie"
# Satellites update dialog
msgid "Satellites update"
msgstr "Actualiseren Satelliet"
msgid "Remove selection"
msgstr "Verwijder selectie"
# Service details dialog
msgid "Service data:"
msgstr "Gegevens Dienst:"
msgid "Transponder data:"
msgstr "Gegevens Transponder:"
msgid "Service data"
msgstr "Gegevens Dienst"
msgid "Transponder details"
msgstr "Details Transponder"
msgid ""
"Changes will be applied to all services of this transponder!\n"
"Continue?"
msgstr ""
"Wijzigingen zullen worden doorgevoerd op alle diensten van deze transponder!\n"
"Doorgaan?"
msgid "Reference"
msgstr "Referentie"
msgid "Namespace"
msgstr "Namespace"
msgid "Flags:"
msgstr "Flags:"
msgid "Delays (ms):"
msgstr "Vertraging (mc)"
msgid "Bitstream"
msgstr "Bitstream"
msgid "Description"
msgstr "Omschrijving"
msgid "Source:"
msgstr "Bron:"
msgid "Cancel"
msgstr "Annuleren"
msgid "Update"
msgstr "Bijwerken"
msgid "Filter"
msgstr "Filteren"
msgid "Find"
msgstr "Zoeken"
# IPTV dialog
msgid "Stream data"
msgstr "Gegevens stream"
# IPTV list configuration dialog
msgid "Starting values"
msgstr "Start waardes"
msgid "Reset to default"
msgstr "Reset naar standaard"
msgid "IPTV streams list configuration"
msgstr "Configureren IPTV Streamlijst"
# Settings dialog
msgid "Preferences"
msgstr "Voorkeuren"
msgid "Profile:"
msgstr "Profiel:"
msgid "Timeout between commands in seconds"
msgstr "Time-out tussen opdrachten in seconden"
msgid "Timeout:"
msgstr "Time-out:"
msgid "Login:"
msgstr "Log in:"
msgid "Options"
msgstr "Opties"
msgid "Password:"
msgstr "Paswoord:"
msgid "Picons:"
msgstr "Picons:"
msgid "Port:"
msgstr "Poort:"
msgid "Data path:"
msgstr "Gegevenspad:"
msgid "Picons path:"
msgstr "Picons pad:"
msgid "Network settings:"
msgstr "Netwerk instellingen:"
msgid "STB file paths:"
msgstr "STB bestandspad:"
msgid "Local file paths:"
msgstr "Lokaal bestandspad:"
# Dialogs messages
msgid "Error. No bouquet is selected!"
msgstr "Fout. Er is geen boeket geselecteerd!"
msgid "This item is not allowed to be removed!"
msgstr "Dit item kan niet verwijderd worden!"
msgid "This item is not allowed to edit!"
msgstr "Dit item kan niet gewijzigd worden!"
msgid "Not allowed in this context!"
msgstr "Niet toegelaten in deze contekst!"
msgid "Please, download files from receiver or setup your path for read data!"
msgstr "Download alstublieft bestanden van de ontvanger of stel het pad in voor het lezen van gegevens!"
msgid "Reading data error!"
msgstr "Fout tijdens het legezen van gegevens!"
msgid "No m3u file is selected!"
msgstr "Geen m3u bestand geslecteerd!"
msgid "Not implemented yet!"
msgstr "Nog niet geïmplementeerd!"
msgid "The text of marker is empty, please try again!"
msgstr "De tekst van de marker is leeg. Probeer opnieuw!"
msgid "Please, select only one item!"
msgstr "Kies maar 1 item graag!"
msgid "No png file is selected!"
msgstr "Geen png bestand geselecteerd!"
msgid "No reference is present!"
msgstr "Geen referentie aanwezig!"
msgid "No selected item!"
msgstr "Geen item geseleceerd!"
msgid "The task is already running!"
msgstr "Deze taak is al bezig!"
msgid "Done!"
msgstr "Gedaan!"
msgid "Please, wait..."
msgstr "Even geduld, aub..."
msgid "Resizing..."
msgstr "Verkleinen/Vergroten..."
msgid "Select paths!"
msgstr "Selecteer paden!"
msgid "No satellite is selected!"
msgstr "Geen satelliet geselecteerd!"
msgid "Please, select only one satellite!"
msgstr "Selecteer alstublieft slechts één satelliet!"
msgid "Please check your parameters and try again."
msgstr "Controleer aub uw parameters en probeer opnieuw!"
msgid "No satellites.xml file is selected!"
msgstr "Geen satellites.xml geselecteerd!"
msgid "Error. Verify the data!"
msgstr "Fout. Controleer de gegevens!"
msgid "Operation not allowed in this context!"
msgstr "Opdracht niet toegestaan in deze context!"
msgid "No VLC is found. Check that it is installed!"
msgstr "Geen VLC gevonden. Controleer of VLC gïnstalleerd is!"
# Search unavailable streams dialog
msgid "Please wait, streams testing in progress..."
msgstr "Even geduld, test van streams bezig ..."
msgid "Found"
msgstr "Gevonden"
msgid "unavailable streams."
msgstr "Steams niet aanwezig."
msgid "No changes required!"
msgstr "Geen wijzigingen vereist!"
msgid "This list does not contains IPTV streams!"
msgstr "Deze lijst bevat geen IPTV streams!"
msgid "New empty configuration"
msgstr "Nieuwe lege configuratie"
msgid "No data to save!"
msgstr "Geen gegevens om op te slaan!"
msgid "Network"
msgstr "Netwerk"
msgid "Paths"
msgstr "Paden"
msgid "Program"
msgstr "Programma"
msgid "Backup:"
msgstr "Backup:"
msgid "Backup"
msgstr "Backup"
msgid "Backups"
msgstr "Backups"
msgid "Backup path:"
msgstr "Backup pad:"
msgid "Restore bouquets"
msgstr "Herstel boeketten"
msgid "Restore all"
msgstr "Herstel alles"
msgid "Before saving"
msgstr "Voor opslaan"
msgid "Before downloading from the receiver"
msgstr "Voor download uit de ontvanger"
msgid "Set background color for the services"
msgstr "Stel achtergrond kleur in voor diensten"
msgid "Marked as new:"
msgstr "Gemarkeerd als nieuw:"
msgid "With an extra name in the bouquet:"
msgstr "Met een extra naam in het boeket:"
msgid "Select"
msgstr "Over"
msgid "About"
msgstr "Over"
msgid "Exit"
msgstr "Exit"
msgid "Tools"
msgstr "Tools"
# Import
msgid "Import"
msgstr "Importeer"
msgid "Bouquet"
msgstr "Boeket"
msgid "Bouquets and services"
msgstr "Boeketten en diensten"
msgid "The main list does not contain services for this bouquet!"
msgstr "De hoofdlijst bevat geen diensten voor dit boeket!"
msgid "No bouquet file is selected!"
msgstr "Geen boeket geselecteerd!"
msgid "Remove all unused"
msgstr "Verwijder alle ongebruikte"
msgid "Test"
msgstr "Test"
msgid "Test connection"
msgstr "Test verbinding"
msgid "Double click on the service in the bouquet list:"
msgstr "Dubbelklik op de dienst in de boeket lijst:"
msgid "Zap"
msgstr "Zap"
msgid "Play stream"
msgstr "Speel stream af"
msgid "Disabled"
msgstr "Uitgeschakeld"
msgid "Enable ver. 5 support (experimental)"
msgstr "Ondersteuning voor ver. 5 inschakelen (experimenteel)"
msgid "Enable HTTP API (experimental)"
msgstr "HTTP API inschakelen (experimenteel)"
msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Schakelaar (ZAP) naar het kanaal (CTRL + Z)"
msgid "Switch the channel and watch in the program(Ctrl + W)"
msgstr "Schakel het kanaal in en bekijk het programma (CTRL + W)"
msgid "Play IPTV or other stream in the program(Ctrl + P)"
msgstr "Speel IPTV of andere stream af en bekijk het programma (CTRL + P)"
msgid "Export to m3u"
msgstr "Uitvoeren naar m3u"
msgid "EPG configuration"
msgstr "Configureer EPG"
msgid "Apply"
msgstr "Toepassen"
msgid "EPG source"
msgstr "Bron EPG"
msgid "Service names source:"
msgstr "Naam van bron van de dienst:"
msgid "Main service list"
msgstr "Hoofdlijst diensten:"
msgid "XML file"
msgstr "XML file"
msgid "Use web source"
msgstr "Gebruik web bron"
msgid "Url to *.xml.gz file:"
msgstr "URL van de *.xml.gz file:"
msgid "Enable filtering"
msgstr "Zet filteren aan"
msgid "Filter by presence in the epg.dat file."
msgstr "Filter op aanwezigheid van epg.dat. file"
msgid "Paths to the epg.dat file:"
msgstr "Pad naar epg.dat:"
msgid "Local path:"
msgstr "Lokaal pad:"
msgid "STB path:"
msgstr "STB pad:"
msgid "Update on start"
msgstr "Actualiseer bij start"
msgid "Auto configuration by service names."
msgstr "Auto configuratie door dienst namen."
msgid "Save list to xml."
msgstr "Opslaan als XML."
msgid "Download XML file error."
msgstr "Fout bij downloaden XML."
msgid "Unsupported file type:"
msgstr "Ongesupporteerd archieftype:"
msgid "Unpacking data error."
msgstr "Fout bij uitpakken van de data."
msgid "XML parsing error:"
msgstr "XML parsingfout:"
msgid "Count of successfully configured services:"
msgstr "Aantal succesvol geconfigureerde diensten:"
msgid "Current epg.dat file does not contains references for the services of this bouquet!"
msgstr "Huisige epg.dat bestand heeft geen referenties naar de diensten van dit boeket!"

751
po/pt/demon-editor.po Normal file
View File

@@ -0,0 +1,751 @@
# Copyright (C) 2018-2019 Frank Neirynck
# This file is distributed under the MIT license.
#
#Frank Neirynck <frank@insink.be>, 2018-2019.
#
msgid ""
msgstr ""
"Last-Translator: Frank Neirynck\n"
"Language: pt\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "translator-credits"
msgstr "Frank Neirynck <frank@insink.be>"
# Main
msgid "Service"
msgstr "Serviço"
msgid "Package"
msgstr "Pacote"
msgid "Type"
msgstr "Tipo"
msgid "Picon"
msgstr "Picon"
msgid "Freq"
msgstr "Freq."
msgid "Rate"
msgstr "Ratio"
msgid "Pol"
msgstr "Pol."
msgid "System"
msgstr "Sistema"
msgid "Pos"
msgstr "Pos."
msgid "Num"
msgstr "Núm"
msgid "Current IP:"
msgstr "IP atual:"
msgid "Assign"
msgstr "Atribuir"
msgid "Bouquet details"
msgstr "Detalhes do Ramo"
msgid "Bouquets"
msgstr "Ramos"
msgid "Copy"
msgstr "Copiar"
msgid "Copy reference"
msgstr "Copia de Referência"
msgid "Download"
msgstr "Descarregar"
msgid "Edit"
msgstr "Editar"
msgid "Edit "
msgstr "Editar "
msgid "Edit mаrker text"
msgstr "Editar texto do mаrcador"
msgid "FTP-transfer"
msgstr "Transferência-FTP"
msgid "Global search"
msgstr "Pesquisa Global"
msgid "Hide"
msgstr "Esconder"
msgid "Hide/Skip On/Off Ctrl + H"
msgstr "Esconder/Pular Acender/Desligar Ctrl + H"
msgid "Add IPTV or stream service"
msgstr "Acrescentar serviço IPTV"
msgid "Import m3u"
msgstr "Importar m3u"
msgid "Import m3u file"
msgstr "Importar ficheiro m3u"
msgid "List configuration"
msgstr "Lista configuração"
msgid "Rename for this bouquet"
msgstr "Renomear para este ramo"
msgid "Set default name"
msgstr "Estabelecer nome predeterminado"
msgid "Insert marker"
msgstr "Inserir marcador"
msgid "Locate in services"
msgstr "Procurar em serviços"
msgid "Locked"
msgstr "Fechado"
msgid "Move"
msgstr "Mover"
msgid "New"
msgstr "Novo"
msgid "New bouquet"
msgstr "Ramo novo"
msgid "Create bouquet"
msgstr "Criar ramo"
msgid "For current satellite"
msgstr "Para o satélite atual"
msgid "For current package"
msgstr "Para o pacote atual"
msgid "For current type"
msgstr "Para o tipo atual"
msgid "For each satellite"
msgstr "Para cada Satélite"
msgid "For each package"
msgstr "Para cada pacote"
msgid "For each type"
msgstr "Para cada tipo"
msgid "Open"
msgstr "Abrir"
msgid "Parent lock On/Off Ctrl + L"
msgstr "Bloqueio parentesco Acender/Desligar Ctrl + L"
msgid "Picons"
msgstr "Picons"
msgid "Picons downloader"
msgstr "Picons descargar"
msgid "Satellites downloader"
msgstr "Satélites descarregar"
msgid "Remove"
msgstr "Remover"
msgid "Remove all unavailable"
msgstr "Remover tudo lo indisponível"
msgid "Satellites editor"
msgstr "Editor Satélites"
msgid "Save"
msgstr "Guardar"
msgid "Search"
msgstr "Procurar"
msgid "Services"
msgstr "Serviços"
msgid "Services filter"
msgstr "Filtro serviços"
msgid "Settings"
msgstr "Configurações"
msgid "Up"
msgstr "Arriba"
msgid "Down"
msgstr "Abaixo"
msgid "Active profile:"
msgstr "Perfil ativo:"
msgid "All"
msgstr "Tudo"
msgid "Are you sure?"
msgstr "Tem certeza?"
msgid "Current data path:"
msgstr "Rota de dados atual:"
msgid "Data:"
msgstr "Dados:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux"
msgstr "Editor de Canais e Satélites Enigma2 para GNU/Linux"
msgid "Host:"
msgstr "Anfitrião:"
msgid "Loading data..."
msgstr "Carregar dados..."
msgid "Receive"
msgstr "Receber"
msgid "Receive files from receiver"
msgstr "Receber ficheiros do seu receptor"
msgid "Receiver IP:"
msgstr "Receptor IP:"
msgid "Remove unused bouquets"
msgstr "Remover ramos sem usar"
msgid "Reset profile"
msgstr "Restabelecer perfil"
msgid "Satellites"
msgstr "Satélites"
msgid "Satellites.xml file:"
msgstr "Ficheiro Satellites.xml:"
msgid "Selected"
msgstr "Selecionado"
msgid "Send"
msgstr "Enviar"
msgid "Send files to receiver"
msgstr "Enviar ficheiros ao recetor"
msgid "Services and Bouquets files:"
msgstr "Ficheiros de Serviços e ramos:"
msgid "User bouquet files:"
msgstr "Importar ficheiros de ramos:"
msgid "Extra:"
msgstr "Extra:"
# Filter bar
msgid "Only free"
msgstr "Somente de graça"
msgid "All positions"
msgstr "Todas as posições"
msgid "All types"
msgstr "Todos os tipos"
# Streams player
msgid "Play"
msgstr "Play"
msgid "Stop playback"
msgstr "Deter a reprodução"
msgid "Previous stream in the list"
msgstr "Sequência anterior na lista"
msgid "Next stream in the list"
msgstr "Sequência siguinte na lista"
msgid "Toggle in fullscreen"
msgstr "Trocar na ecrã completa"
msgid "Close"
msgstr "Fexar"
# Picons dialog
msgid "Load providers"
msgstr "Carregar provedores"
msgid "Providers"
msgstr "Fornecedores"
msgid "Receive picons"
msgstr "Recever picons"
msgid "Picons name format:"
msgstr "Picons formato nomes:"
msgid "Resize:"
msgstr "Redimensionar:"
msgid "Current picons path:"
msgstr "Rota atual Picons:"
msgid "Receiver picons path:"
msgstr "Rota picons recetor:"
msgid "Picons download tool"
msgstr "Picons ferramenta de descarga"
msgid "Transfer to receiver"
msgstr "Transferir ao recetor"
msgid "Downloader"
msgstr "Descarregador"
msgid "Converter"
msgstr "Conversor"
msgid "Convert"
msgstr "Converter"
msgid "Path to save:"
msgstr "Rota para guardar:"
msgid "Path to Enigma2 picons:"
msgstr "Rota a picons Enigma2:"
msgid "Specify the correct position value for the provider!"
msgstr "Especifique a posição correta para o provedor!"
msgid "Converter between name formats"
msgstr "Conversor entre formatos de nomes"
msgid "Receive picons for providers"
msgstr "Recever picons para provedor"
msgid "Load satellite providers."
msgstr "Carregar provedores Satélite."
msgid "To automatically set the identifiers for picons,\nfirst load the required services list into the main application window."
msgstr "Para configurar automaticamente os identificadores para picons, \nprimeiro carregue a lista de serviços necessários na janela principal."
# Satellites editor
msgid "Satellites edit tool"
msgstr "Editor de Satélites"
msgid "Add"
msgstr "Adicionar"
msgid "Satellite"
msgstr "Satélite"
msgid "Transponder"
msgstr "Transponder"
msgid "Satellite properties:"
msgstr "Propriedades do Satélite:"
msgid "Transponder properties:"
msgstr "Propriedades do Transponder:"
msgid "Name"
msgstr "Nome"
msgid "Position"
msgstr "Posição"
# Satellites update dialog
msgid "Satellites update"
msgstr "Atualisar Satélite"
msgid "Remove selection"
msgstr "Remover seleçáo"
# Service details dialog
msgid "Service data:"
msgstr "Dados serviço:"
msgid "Transponder data:"
msgstr "Dados Transponder:"
msgid "Service data"
msgstr "Dados serviço"
msgid "Transponder details"
msgstr "Detalhes Transponder"
msgid "Changes will be applied to all services of this transponder!\nContinue?"
msgstr "Os mudanças serão aplicadas a todos os serviços deste transponder!\nContinuar?"
msgid "Reference"
msgstr "Referência"
msgid "Namespace"
msgstr "Namespace"
msgid "Flags:"
msgstr "Flags:"
msgid "Delays (ms):"
msgstr "Atraso (mc)"
msgid "Bitstream"
msgstr "Sequência de Bits"
msgid "Description"
msgstr "Descrição"
msgid "Source:"
msgstr "Fonte:"
msgid "Cancel"
msgstr "Annular"
msgid "Update"
msgstr "Atualizar"
msgid "Filter"
msgstr "Filtrar"
msgid "Find"
msgstr "Procurar"
# IPTV dialog
msgid "Stream data"
msgstr "Dados da sequência"
# IPTV list configuration dialog
msgid "Starting values"
msgstr "Valores iniciais"
msgid "Reset to default"
msgstr "Restabelecer a predeterminado"
msgid "IPTV streams list configuration"
msgstr "Configurar lista de sequências IPTV"
#Settings dialog
msgid "Preferences"
msgstr "Preferências"
msgid "Profile:"
msgstr "Perfil:"
msgid "Timeout between commands in seconds"
msgstr "Tempo de espera entre comandos em segundos"
msgid "Timeout:"
msgstr "Time-out:"
msgid "Login:"
msgstr "Usuário:"
msgid "Options"
msgstr "Opções"
msgid "Password:"
msgstr "Palavra-passe:"
msgid "Picons:"
msgstr "Picons:"
msgid "Port:"
msgstr "Porta:"
msgid "Data path:"
msgstr "Rota de dados:"
msgid "Picons path:"
msgstr "Rota de Picons:"
msgid "Network settings:"
msgstr "Configuração de rede:"
msgid "STB file paths:"
msgstr "Rota de ficheirors STB:"
msgid "Local file paths:"
msgstr "Rota de ficheiros local:"
# Dialogs messages
msgid "Error. No bouquet is selected!"
msgstr "Erro. Nenhum ramo está selecionado!"
msgid "This item is not allowed to be removed!"
msgstr "Este artigo não pode ser eliminado!"
msgid "This item is not allowed to edit!"
msgstr "Este artigo não pode ser editado!"
msgid "Not allowed in this context!"
msgstr "Náo permitido neste contexto!"
msgid "Please, download files from receiver or setup your path for read data!"
msgstr "Por favor, descarregue arquivos desde o recetor ou configure a sua rota para ler os dados!"
msgid "Reading data error!"
msgstr "Erro de leitura de dados!"
msgid "No m3u file is selected!"
msgstr "Nenhum ficheiro m3u foi selecionado!"
msgid "Not implemented yet!"
msgstr "Ainda não implementado!"
msgid "The text of marker is empty, please try again!"
msgstr "O texto do marcador está vazio, tente de novo!"
msgid "Please, select only one item!"
msgstr "Por favor, selecione só um elemento!"
msgid "No png file is selected!"
msgstr "Nenhum ficheiro png foi selecionado!"
msgid "No reference is present!"
msgstr "Nenhuma referência presente!"
msgid "No selected item!"
msgstr "Nenhum elemento selecionado!"
msgid "The task is already running!"
msgstr "A tarefa ya está en execuçáo!"
msgid "Done!"
msgstr "Feito!"
msgid "Please, wait..."
msgstr "Por favor, espere..."
msgid "Resizing..."
msgstr "A redimensionar..."
msgid "Select paths!"
msgstr "Selecione as rotas!"
msgid "No satellite is selected!"
msgstr "Nemhun Satélite selecionado!"
msgid "Please, select only one satellite!"
msgstr "Selecione só um Satélite!"
msgid "Please check your parameters and try again."
msgstr "Por favor rever seus parâmetros e volte a tentar!"
msgid "No satellites.xml file is selected!"
msgstr "Nemhun satellites.xml selecionado!"
msgid "Error. Verify the data!"
msgstr "Erro. Rever os dados!"
msgid "Operation not allowed in this context!"
msgstr "Operação não permitida neste contexto!"
msgid "No VLC is found. Check that it is installed!"
msgstr "VLC não achado. Verifique se está instalado!"
# Search unavailable streams dialog
msgid "Please wait, streams testing in progress..."
msgstr "Por favor aguarde uma prova das sequências..."
msgid "Found"
msgstr "Achado"
msgid "unavailable streams."
msgstr "Sequências não presentes"
msgid "No changes required!"
msgstr "Nemhuma mudança requerida!"
msgid "This list does not contains IPTV streams!"
msgstr "A lista não tem sequências IPTV!"
msgid "New empty configuration"
msgstr "Nova configuração vazia"
msgid "No data to save!"
msgstr "Não há dados para guardar!"
msgid "Network"
msgstr "Rede"
msgid "Paths"
msgstr "Rotas"
msgid "Program"
msgstr "Programa"
msgid "Backup:"
msgstr "Backup:"
msgid "Backup"
msgstr "Backup"
msgid "Backups"
msgstr "Backups"
msgid "Backup path:"
msgstr "Rota do backup:"
msgid "Restore bouquets"
msgstr "Restaurar ramos"
msgid "Restore all"
msgstr "Restaurar tudo"
msgid "Before saving"
msgstr "Antes de guardar"
msgid "Before downloading from the receiver"
msgstr "Antes de recever do recetor"
msgid "Set background color for the services"
msgstr "Determinar a cor de fundo para serviços"
msgid "Marked as new:"
msgstr "Marcado como novo:"
msgid "With an extra name in the bouquet:"
msgstr "Com nome adicional em ramo:"
msgid "Select"
msgstr "Selecione"
msgid "About"
msgstr "Acerca"
msgid "Exit"
msgstr "Sair"
msgid "Tools"
msgstr "Tools"
#Import
msgid "Import"
msgstr "Importar"
msgid "Bouquet"
msgstr "Ramo"
msgid "Bouquets and services"
msgstr "Ramos e serviços"
msgid "The main list does not contain services for this bouquet!"
msgstr "A lista pricipal no tem serviços em esta ramo!"
msgid "No bouquet file is selected!"
msgstr "Nemhuma ficheiro de ramo foi selecionado!"
msgid "Remove all unused"
msgstr "Remova todos os não utilizados"
msgid "Test"
msgstr "Test"
msgid "Test connection"
msgstr "Testar a conexão"
msgid "Double click on the service in the bouquet list:"
msgstr "Clique duas vezes no serviço na lista de ramos:"
msgid "Zap"
msgstr "Zap"
msgid "Play stream"
msgstr "Play stream"
msgid "Disabled"
msgstr "Desativado"
msgid "Enable ver. 5 support (experimental)"
msgstr "Ativar ver. 5 suporte (experimental)"
msgid "Enable HTTP API (experimental)"
msgstr "Ativar HTTP API (experimental)"
msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Mudar(zap) o canal(Ctrl + Z)"
msgid "Switch the channel and watch in the program(Ctrl + W)"
msgstr "Troque o canal e ver no programa(Ctrl + W)."
msgid "Play IPTV or other stream in the program(Ctrl + P)"
msgstr "Tocar IPTV ou outro fluxo no programa(Ctrl + P)"
msgid "Export to m3u"
msgstr "Exportar na m3u"
msgid "EPG configuration"
msgstr "Configuraçao EPG"
msgid "Apply"
msgstr "Aplicar"
msgid "EPG source"
msgstr "Fonte EPG"
msgid "Service names source:"
msgstr "Fonte de nomes de serviço:"
msgid "Main service list"
msgstr "Lista de serviço principal:"
msgid "XML file"
msgstr "Arquivo XML"
msgid "Use web source"
msgstr "Usar fonte web"
msgid "Url to *.xml.gz file:"
msgstr "Url para o arquivo *.xml.gz:"
msgid "Enable filtering"
msgstr "Ativar filtragem"
msgid "Filter by presence in the epg.dat file."
msgstr "Filtrar por presença no arquivo epg.dat."
msgid "Paths to the epg.dat file:"
msgstr "Ruta para o arquivo epg.dat:"
msgid "Local path:"
msgstr "Ruta local:"
msgid "STB path:"
msgstr "Ruta STB:"
msgid "Update on start"
msgstr "Atualizar no início"
msgid "Auto configuration by service names."
msgstr "Configuração automática por nomes de serviço."
msgid "Save list to xml."
msgstr "Salvar lista para XML."
msgid "Download XML file error."
msgstr "Baixe o erro de arquivo XML."
msgid "Unsupported file type:"
msgstr "Tipo de arquivo não suportado:"
msgid "Unpacking data error."
msgstr "Descompactando o erro de dados."
msgid "XML parsing error:"
msgstr "Erro de análise XML:"
msgid "Count of successfully configured services:"
msgstr "Contagem de serviços configurados com sucesso:"
msgid "Current epg.dat file does not contains references for the services of this bouquet!"
msgstr "O arquivo epg.dat não contém referências para os serviços deste buquê!"

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

@@ -0,0 +1,775 @@
# Copyright (C) 2018-2019 Dmitriy Yefremov
# This file is distributed under the MIT license.
#
#
msgid ""
msgstr ""
"Last-Translator: Dmitriy Yefremov\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "translator-credits"
msgstr ""
# 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 "Not allowed in this context!"
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!"
msgid "New empty configuration"
msgstr "Новая конфигурация"
msgid "No data to save!"
msgstr "Нет данных для сохранения!"
msgid "Network"
msgstr "Сеть"
msgid "Paths"
msgstr "Пути"
msgid "Program"
msgstr "Программа"
msgid "Backup:"
msgstr "Резервное копирование:"
msgid "Backup"
msgstr "Резервное копирование"
msgid "Backups"
msgstr "Резервные копии"
msgid "Backup path:"
msgstr "Путь к резервным копиям:"
msgid "Restore bouquets"
msgstr "Восстановить букеты"
msgid "Restore all"
msgstr "Восстановить все"
msgid "Before saving"
msgstr "Перед сохранением"
msgid "Before downloading from the receiver"
msgstr "Перед загрузкой с ресивера"
msgid "Set background color for the services"
msgstr "Установить цвет фона для сервисов"
msgid "Marked as new:"
msgstr "Помеченные как новые:"
msgid "With an extra name in the bouquet:"
msgstr "С пользовательским именем в букете:"
msgid "Select"
msgstr "Выбрать"
msgid "About"
msgstr "О программе"
msgid "Exit"
msgstr "Выход"
msgid "Tools"
msgstr "Инструменты"
#Import
msgid "Import"
msgstr "Импорт"
msgid "Bouquet"
msgstr "Букета"
msgid "Bouquets and services"
msgstr "Букетов и сервисов"
msgid "The main list does not contain services for this bouquet!"
msgstr "Основной список не содержит сервисов для данного букета!"
msgid "No bouquet file is selected!"
msgstr "Не выбран файл букета!"
msgid "Remove all unused"
msgstr "Удалить все неиспользуемые"
msgid "Test"
msgstr "Тестировать"
msgid "Test connection"
msgstr "Тестировать соединение"
msgid "Double click on the service in the bouquet list:"
msgstr "Двойной клик по сервису в списке букетов:"
msgid "Zap"
msgstr "Переключить"
msgid "Play stream"
msgstr "Воспр. потока"
msgid "Disabled"
msgstr "Выкл."
msgid "Enable ver. 5 support (experimental)"
msgstr "Включить поддержку вер. 5 (экспериментально)"
msgid "Enable HTTP API (experimental)"
msgstr "Включить HTTP API (экспериментально)"
msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Переключить канал(Ctrl + Z)"
msgid "Switch the channel and watch in the program(Ctrl + W)"
msgstr "Переклють канал и просмотр в программе(Ctrl + W)."
msgid "Play IPTV or other stream in the program(Ctrl + P)"
msgstr "Воспроизведение IPTV или другого потока в программе(Ctrl + P)"
msgid "Export to m3u"
msgstr "Экспорт в m3u"
msgid "EPG configuration"
msgstr "Конфигурация EPG"
msgid "Apply"
msgstr "Применить"
msgid "EPG source"
msgstr "Источник EPG"
msgid "Service names source:"
msgstr "Источник имен сервисов:"
msgid "Main service list"
msgstr "Основной список сервисов:"
msgid "XML file"
msgstr "Файл XML"
msgid "Use web source"
msgstr "Использовать веб-источник"
msgid "Url to *.xml.gz file:"
msgstr "URL к файлу *.xml.gz:"
msgid "Enable filtering"
msgstr "Включить фильтрацию"
msgid "Filter by presence in the epg.dat file."
msgstr "Фильтровать по наличию в файле epg.dat."
msgid "Paths to the epg.dat file:"
msgstr "Пути к файлу epg.dat:"
msgid "Local path:"
msgstr "Локальный путь:"
msgid "STB path:"
msgstr "Путь в ресивере:"
msgid "Update on start"
msgstr "Обновлять при запуске"
msgid "Auto configuration by service names."
msgstr "Автонастройка по именам сервисов."
msgid "Save list to xml."
msgstr "Сохранить список в XML."
msgid "Download XML file error."
msgstr "Ошибка загрузки XML-файла."
msgid "Unsupported file type:"
msgstr "Неподдерживаемый тип файла:"
msgid "Unpacking data error."
msgstr "Ошибка распаковки данных."
msgid "XML parsing error:"
msgstr "Ошибка парсинга XML:"
msgid "Count of successfully configured services:"
msgstr "Количество успешно сконфигурированных сервисов:"
msgid "Current epg.dat file does not contains references for the services of this bouquet!"
msgstr "Текущий файл epg.dat не содержит ссылок на сервисы данного букета!"