Compare commits

..

740 Commits

Author SHA1 Message Date
DYefremov
2dc8611294 bump version 2023-01-21 16:27:08 +03:00
DYefremov
72bfd21056 fix input for settings dialog 2023-01-21 15:49:18 +03:00
DYefremov
65ef018f81 modifiers fix for yt dialog 2023-01-21 15:47:47 +03:00
DYefremov
1236c5ebc9 filtering by satellite position for the EPG dialog (#148) 2023-01-21 15:06:27 +03:00
DYefremov
f0011ebcf2 fixed copy/paste refs from some satellites (#146) 2023-01-20 12:17:59 +03:00
DYefremov
392e94e7ba bump version 2023-01-19 22:05:40 +03:00
DYefremov
c6de18271d setting EPG path active by default 2023-01-19 21:57:52 +03:00
DYefremov
71a65242c1 ui correction for EPG tab 2023-01-19 21:04:53 +03:00
DYefremov
4efc956870 minor ui correction for remote control 2023-01-19 16:02:11 +03:00
DYefremov
a605fdd545 minor ui corrections 2023-01-18 18:52:54 +03:00
DYefremov
285c1cae69 minor style changes 2023-01-14 23:02:24 +03:00
DYefremov
2e937a42a3 updated tr *.mo file 2023-01-14 00:30:46 +03:00
audi06_19
e208cf4656 Turkish translation update (#147) 2023-01-14 00:26:01 +03:00
DYefremov
3db82e3e18 don't hide filter popups after items selection (#145) 2023-01-13 16:11:57 +03:00
DYefremov
38e9a85694 remote control improvement 2023-01-12 21:46:04 +03:00
DYefremov
438e9c10d4 corrected reference assignment for IPTV services (#146) 2023-01-08 01:59:46 +03:00
DYefremov
920fa01159 bump version 2023-01-06 12:00:46 +03:00
DYefremov
d2787364cd fixed *.xml files deletion skip when saving data 2023-01-06 11:24:10 +03:00
DYefremov
839c0fae23 compressed picons path correction 2023-01-06 11:01:39 +03:00
DYefremov
f87548e12e minor style correction for filter items (#145) 2023-01-01 21:33:05 +03:00
DYefremov
463702c371 IPTV description tag correction (#144) 2022-12-27 00:26:47 +03:00
DYefremov
0b84a81439 version update 2022-12-25 14:56:22 +03:00
DYefremov
6b68740961 skipping existing channels when grouping by satellites (#127) 2022-12-25 14:49:02 +03:00
DYefremov
92aa2400f6 mpv module correction
* fixed work with API ver. 2
2022-12-20 14:36:14 +03:00
DYefremov
2cf4e5b756 updated it *.mo file 2022-12-14 13:23:37 +03:00
mapi68
6cfa68e219 Italian translation update (#141) 2022-12-14 13:10:36 +03:00
DYefremov
138aa54b44 Russian, Belarusian and German translations update 2022-12-13 14:33:51 +03:00
DYefremov
3e1a3d1595 fixed display of tooltip icon 2022-12-13 14:04:28 +03:00
DYefremov
b833458c45 version update 2022-12-11 11:29:37 +03:00
DYefremov
d1e88be1cc grouping by satellites in the import dialog (#127) 2022-12-10 21:34:19 +03:00
DYefremov
72e128aeb9 Dutch and Spanish translations update 2022-12-08 22:14:16 +03:00
DYefremov
457b4e4645 support for SNP picons filtering (#128) 2022-12-08 21:56:14 +03:00
DYefremov
076243b0ac changed picon reference creation
* Reverted stream type (important for some images).
2022-12-04 16:45:46 +03:00
DYefremov
e5ff185791 snr display in dB (#140) 2022-11-28 15:15:40 +03:00
DYefremov
2b6b0dd827 version update 2022-11-24 21:38:33 +03:00
DYefremov
a11fdd683e updated it *.mo file 2022-11-24 11:53:47 +03:00
mapi68
636bc5c52f Italian translation update (#137) 2022-11-24 11:47:52 +03:00
DYefremov
90d64d46c7 Russian, Belarusian and German translations update 2022-11-24 00:46:49 +03:00
DYefremov
115f77108c Russian, Belarusian and German translations update 2022-11-22 09:48:50 +03:00
DYefremov
2082f8e973 IPTV tab update after EPG configuration (#132) 2022-11-21 22:12:56 +03:00
DYefremov
7e586bf0a6 redesigned refs assignment for EPG dialog (#132) 2022-11-19 00:20:59 +03:00
DYefremov
c5c4823534 support for cleaning flag "new" (#136) 2022-11-17 00:27:39 +03:00
DYefremov
6235519cf9 increased chars width for service column (#126) 2022-11-15 22:19:00 +03:00
DYefremov
7d3a9f768c version update 2022-11-12 00:03:48 +03:00
DYefremov
37bea3a93c setting service type in EPG configuration (#132) 2022-11-11 23:38:06 +03:00
DYefremov
33a22fdca7 reference assigning with a service type (#132) 2022-11-11 21:52:01 +03:00
DYefremov
d54e97b1b8 picon reference corrections for IPTV dialogs 2022-11-11 21:38:24 +03:00
DYefremov
12c2a449ea picon reference formation change (#131) 2022-11-10 18:21:29 +03:00
DYefremov
7164f54773 picons display by service name (#128) 2022-11-10 00:12:07 +03:00
DYefremov
84adeb994e added power mode option 2022-11-09 21:23:14 +03:00
DYefremov
69c3b4a6c1 uploading data to multiple hosts 2022-11-05 00:58:39 +03:00
DYefremov
cbec74c2a4 added multiple hosts option 2022-11-04 01:06:18 +03:00
DYefremov
8d856bc989 updated it *.mo file 2022-10-28 10:32:40 +03:00
mapi68
a1ec7600da Italian translation corrections (#135) 2022-10-28 10:28:04 +03:00
DYefremov
2fd71c3645 assign refs refactoring 2022-10-26 10:42:34 +03:00
DYefremov
a0f6b1f651 finalization of selective import 2022-10-21 18:53:32 +03:00
DYefremov
671c2204bf paths setting refactoring (#123) 2022-10-20 09:03:58 +03:00
DYefremov
d2a0419c06 highlighting existing services for the import dialog 2022-10-15 22:22:05 +03:00
DYefremov
fb48395d1e added service details for import dialog 2022-10-14 23:08:10 +03:00
DYefremov
44fb760241 prototype of selective services import (#127) 2022-10-05 19:32:15 +03:00
DYefremov
428a240416 restored open data button 2022-10-03 23:15:21 +03:00
DYefremov
8c58b9395b version update 2022-10-03 16:53:23 +03:00
DYefremov
021b2b08cf added compression option for *.deb (#125) 2022-10-01 18:36:06 +03:00
DYefremov
0d68e43212 version change 2022-09-25 20:04:19 +03:00
DYefremov
751e633a51 send/receive elements dynamic activity 2022-09-25 19:59:17 +03:00
DYefremov
9ff96f2e1d minor logging improvement 2022-09-22 20:09:03 +03:00
DYefremov
3e85fa0149 backup fix 2022-09-20 23:58:32 +03:00
DYefremov
ab5620f9d1 updated tr *.mo file 2022-08-28 07:33:13 +03:00
audi06_19
bceddc199b Turkish translation update (#124) 2022-08-28 07:29:59 +03:00
DYefremov
4f26855ec3 xmltv reading correction 2022-08-25 21:31:16 +03:00
DYefremov
15b8483107 minor improvement to the FTP tab
* Involved send-receive toolbar buttons.
2022-08-24 23:47:31 +03:00
DYefremov
f4dac57d06 improved recordings download 2022-08-24 23:07:18 +03:00
DYefremov
71f7b3a570 improved list font change (#119) 2022-08-24 11:16:04 +03:00
DYefremov
265fb59f0b minor refactoring 2022-08-24 10:48:34 +03:00
DYefremov
5b090672d9 updated it *.mo file 2022-08-21 22:17:14 +03:00
mapi68
aa8f2a8df0 Italian translation improvement (#114)
Fixed errors, and many improvements. Now Italian version of DemonEditor is less verbose!
2022-08-21 22:13:09 +03:00
DYefremov
6782da5f83 Russian, Belarusian and German translations update 2022-08-19 16:34:37 +03:00
DYefremov
26d7b22c3a minor fixes 2022-08-18 16:00:48 +03:00
DYefremov
69ec1f8359 updated *.spec files 2022-08-16 14:09:37 +03:00
DYefremov
c5aac859b0 loading xml EPG as background task 2022-08-16 11:36:57 +03:00
DYefremov
8d0b241ca3 updated tr *.mo file 2022-08-14 14:38:13 +03:00
audi06_19
8943eab99d Turkish translation updateo (#111) 2022-08-14 14:33:21 +03:00
DYefremov
a5a4f267cc some improvements for recordings tab
* logos support
 * download support
 * meta data removing
2022-08-12 09:26:26 +03:00
DYefremov
a69127f0cc background tasks prototype 2022-08-12 09:14:13 +03:00
DYefremov
858b2ae2d6 minor ui corrections 2022-08-05 19:51:47 +03:00
DYefremov
13a08c98de decoupling of processing DVB data 2022-08-04 00:02:33 +03:00
DYefremov
272e3786dc getting data from transponder dialogs 2022-08-03 00:23:24 +03:00
DYefremov
3b510e6935 terrestrial and cable dialogs skeleton 2022-08-01 22:55:38 +03:00
DYefremov
f3a6d2bd9c transponder dialog refactoring 2022-08-01 18:19:15 +03:00
DYefremov
ca20852bfe minor fix for EPG tab 2022-08-01 17:57:44 +03:00
DYefremov
5e1bd8e1c9 DVB dialogs refactoring 2022-07-31 00:10:19 +03:00
DYefremov
0b87f4f143 remote control improvement 2022-07-28 00:17:44 +03:00
DYefremov
00cbe43aa7 minor improvements for timers tab 2022-07-27 00:44:40 +03:00
DYefremov
deb161a153 improved reference assignment for IPTV 2022-07-27 00:03:28 +03:00
DYefremov
2a2611abde minor improvements for EPG tab 2022-07-20 23:50:31 +03:00
DYefremov
dbcfb71224 minor correction 2022-07-17 14:15:15 +03:00
DYefremov
2441d3726b basic Multi EPG support 2022-07-16 23:56:02 +03:00
DYefremov
681b43b164 changed format for time columns (#110) 2022-07-14 10:32:58 +03:00
DYefremov
31025777a3 changed network grid position 2022-07-13 14:19:26 +03:00
DYefremov
33e39d2f25 improved sorting by recording time 2022-07-11 08:52:15 +03:00
DYefremov
14e200f262 improved sorting for EPG time column (#110) 2022-07-10 18:05:52 +03:00
DYefremov
3cef75e765 minor timer time format correction (#110) 2022-07-08 22:21:22 +03:00
DYefremov
a973f8e636 added popup menu for recordings tab 2022-07-08 21:59:35 +03:00
DYefremov
c6bea94ff5 improved timer start time setting (#110) 2022-07-08 01:05:14 +03:00
DYefremov
736655542c minor improvements for the timers tab (#110) 2022-07-07 08:39:53 +03:00
DYefremov
073521de75 deletion support for DVB-T/C 2022-07-04 21:47:05 +03:00
DYefremov
46748d3fc4 corrected satellites web update 2022-07-03 12:44:03 +03:00
DYefremov
f2e571185d xml write support for DVB-T/C 2022-07-02 23:14:32 +03:00
DYefremov
c2fd116252 xml data rendering refactoring 2022-07-02 18:40:26 +03:00
DYefremov
c1c5e866ad some correction of recordings deletion 2022-06-25 00:13:57 +03:00
DYefremov
d4a2e78a09 minor ui corrections 2022-06-22 22:38:36 +03:00
DYefremov
0f68d5b292 transponders display for DVB-T/C 2022-06-21 22:58:47 +03:00
DYefremov
c3d9159822 list display for DVB-T/C 2022-06-21 01:19:35 +03:00
DYefremov
b5a508ef54 ui prototype for *.xml [dvb-t\c] editing 2022-06-20 23:30:26 +03:00
DYefremov
14459b8e7e satellite tools decoupling 2022-06-18 21:29:10 +03:00
DYefremov
a9998b9d17 fixed xmltv loading on Windows 2022-06-17 22:46:00 +03:00
DYefremov
6d05a6ec20 lazy xmltv loading 2022-06-17 17:53:39 +03:00
DYefremov
b821fd54be some epg load changes 2022-06-16 23:30:28 +03:00
DYefremov
c3bc3a1160 basic XMLTV support 2022-06-14 20:14:08 +03:00
DYefremov
748b41e31c epg display in bouquet list 2022-06-06 20:33:37 +03:00
DYefremov
a4cbe00e96 updated it *.mo file 2022-06-02 20:23:38 +03:00
mapi68
9f0ad72d42 Italian translation сorrection (#109)
Typo fix
2022-06-02 20:17:47 +03:00
DYefremov
673a8547ff minor start script correction 2022-05-31 22:37:52 +03:00
mapi68
f6058dafb9 *.desktop file update (#108) 2022-05-31 22:29:09 +03:00
DYefremov
0140fb4eb4 added epg path option 2022-05-31 18:27:11 +03:00
DYefremov
97f04999c4 base tools decoupling 2022-05-22 23:55:13 +03:00
DYefremov
e879c8db18 style correction 2022-05-19 21:50:27 +03:00
DYefremov
d41ceb6bbc updated it *.mo file 2022-05-18 22:10:11 +03:00
mapi68
8e82d8562a Italian translation сorrection (#107) 2022-05-18 21:58:35 +03:00
DYefremov
4cba9a6754 minor fix 2022-05-17 18:48:16 +03:00
DYefremov
d568a9429d minor improvements for data upload 2022-05-16 16:09:05 +03:00
DYefremov
8b39fedaed minor refactoring of ftp dialogs 2022-05-11 10:38:06 +03:00
DYefremov
c3351b43cc attributes change support for FTP (#105) 2022-05-10 23:10:38 +03:00
DYefremov
58156dd4c1 backup dialog improvement 2022-05-09 23:57:50 +03:00
DYefremov
d09c14518e added simple network explorer (#60) 2022-05-07 23:13:17 +03:00
DYefremov
a8739be31d data upload correction 2022-05-06 23:06:35 +03:00
DYefremov
67a75e5ffa corrected tooltip for picons option 2022-05-06 23:06:03 +03:00
mapi68
7d31a050fe changed tooltip for picons option (#103) 2022-05-06 18:22:01 +03:00
DYefremov
5f4cee759f bouquets reading correction (#104) 2022-05-05 23:21:26 +03:00
DYefremov
f7ed1736c5 bump version 2022-05-05 22:06:33 +03:00
DYefremov
4bdbb511ee added FTP submenu 2022-05-05 21:57:34 +03:00
DYefremov
e49d32f931 improvement of picons unzipping (#102) 2022-05-05 18:07:07 +03:00
DYefremov
42feb8d5f6 small fix 2022-05-05 09:33:41 +03:00
DYefremov
e408f88afd added FTP transfer settings 2022-05-04 23:03:18 +03:00
DYefremov
eaf0434e19 added compress picons option 2022-05-04 20:49:33 +03:00
DYefremov
079cf6a482 picons compression support on upload (#101) 2022-05-04 17:28:57 +03:00
DYefremov
d3ae2187c8 restore backup creation 2022-05-03 23:08:49 +03:00
DYefremov
eac0cc47a9 added URL column to IPTV view (#92) 2022-05-03 18:21:44 +03:00
DYefremov
1972374505 refactoring of send and receive data (#101) 2022-05-03 01:12:49 +03:00
DYefremov
d8626e63cc minor correction for IPTV list dialog 2022-04-29 20:13:42 +03:00
DYefremov
528f59d990 updated it *.mo file 2022-04-26 17:46:53 +03:00
mapi68
28802957fc Italian translation update (#99) 2022-04-26 17:42:33 +03:00
DYefremov
b763d9785d minor translation improvement (#97) 2022-04-26 16:52:48 +03:00
DYefremov
601a81beb9 bump version 2022-04-25 21:00:08 +03:00
DYefremov
ace38433a1 fixed layout switching for the control tab 2022-04-25 20:48:22 +03:00
DYefremov
536b23a845 minor fix 2022-04-25 20:19:36 +03:00
DYefremov
d71a1d5dac removed unused option 2022-04-25 20:18:34 +03:00
DYefremov
4a92084c75 updated pl *.mo file 2022-04-18 13:09:32 +03:00
lareq
e7b8412c11 Polish translation update (#96)
added missing polish translations
2022-04-18 12:51:37 +03:00
audi06_19
ef53de1796 Turkish translation update (#95) 2022-04-17 22:56:10 +03:00
DYefremov
8e7a116db7 multiple selection for picons explorer 2022-04-12 23:33:17 +03:00
DYefremov
285014480f win style correction 2022-04-12 13:42:44 +03:00
DYefremov
279c255ad0 minor fixes in the control panel 2022-04-11 15:04:47 +03:00
DYefremov
90a3053192 bump version 2022-04-11 11:23:11 +03:00
DYefremov
19c6a5bef9 disabling FlySat source (#55) 2022-04-11 10:52:20 +03:00
DYefremov
945ee13058 hooks config for *.spec file 2022-04-10 10:39:59 +03:00
DYefremov
c65b6c540c transponder menu fix 2022-04-09 12:57:45 +03:00
DYefremov
62091dfa96 mac style correction 2022-04-07 22:29:00 +03:00
DYefremov
6ca06fd2cd minor start script correction 2022-04-07 16:29:46 +03:00
DYefremov
4cab05fc09 minor mac style correction 2022-04-06 14:16:50 +03:00
DYefremov
81e714ebab updated it *.mo file 2022-04-04 15:26:15 +03:00
DYefremov
147430d4f3 README update 2022-04-04 15:14:32 +03:00
DYefremov
f8f209d288 build instruction for Windows (#87) 2022-04-04 14:58:29 +03:00
DYefremov
bd0e08e90b changed ssl context for Windows package (#89) 2022-04-03 19:15:27 +03:00
DYefremov
57020423d7 minor fix 2022-04-03 19:08:08 +03:00
mapi68
1fd3e45dd3 Italian translation сorrection (#88) 2022-04-02 09:17:36 +03:00
DYefremov
ac1725b3ef changed path for 7zip on Windows 2022-04-01 12:12:02 +03:00
DYefremov
39a592fd4d bump version 2022-03-30 17:42:09 +03:00
DYefremov
e40e0f2458 added force external themes option 2022-03-30 14:46:57 +03:00
DYefremov
d5889cd96c minor mac style changes 2022-03-30 02:17:10 +03:00
DYefremov
77a8bfe2c6 fixed name parsing for KingOfSat source 2022-03-27 11:33:09 +03:00
DYefremov
470d2d843b fixed translation missing (#85)
* Fixed translation missing for dropdown lists on Windows.
2022-03-26 12:12:22 +03:00
DYefremov
a908845b4e filtering support for recordings tab 2022-03-25 21:25:30 +03:00
DYefremov
8fee5033a4 updated it *.mo file 2022-03-24 21:20:56 +03:00
mapi68
a9b1f8b26c Italian translation сorrection (#84) 2022-03-24 21:13:23 +03:00
DYefremov
8fa306a9d1 updated it *.mo file 2022-03-23 11:02:15 +03:00
mapi68
383ea2b9b3 Italian translation update (#82) 2022-03-23 10:55:20 +03:00
DYefremov
a40ba2ff68 minor correction for settings dialog 2022-03-21 12:19:00 +03:00
DYefremov
421d9b1c96 control file correction 2022-03-20 11:57:26 +03:00
DYefremov
7357939241 bump version 2022-03-20 11:36:58 +03:00
DYefremov
08ef7bc451 ftp bookmark activation via single click 2022-03-20 11:31:21 +03:00
DYefremov
8b255ec824 Russian, Belarusian and German translations update 2022-03-20 11:26:12 +03:00
DYefremov
c81084015d correction of iptv streams check (#69) 2022-03-20 01:02:19 +03:00
mapi68
66c8e9e916 Update control (#80)
Added homepage and extended info.
2022-03-20 00:57:51 +03:00
DYefremov
4b93ae6950 updated it *.mo file 2022-03-19 21:48:42 +03:00
mapi68
e2cafef113 Italian translation correction (#79)
Fixed column labels
2022-03-19 21:44:16 +03:00
DYefremov
c35be2aa24 added newline parameter 2022-03-19 21:07:49 +03:00
mapi68
852404bae6 Update control (#78)
Added p7zip-full to Depends (to extract Picons from picon,cz)
2022-03-19 13:44:22 +03:00
mapi68
f6d2765137 Italian translation correction (#77) 2022-03-19 13:43:55 +03:00
DYefremov
182c7a9cc7 updated it *.mo file 2022-03-14 20:52:48 +03:00
mapi68
271ea97040 Italian translation update (#73)
Fixed and updated italian language.
2022-03-14 20:34:36 +03:00
DYefremov
364fb68743 minor correction 2022-03-14 14:18:05 +03:00
DYefremov
85a9d5e67e added comment in italian to *.desktop file 2022-03-14 10:10:47 +03:00
mapi68
50c0a0cf37 Update DemonEditor.desktop (#72) 2022-03-14 10:06:29 +03:00
DYefremov
25fd6df967 playback support from the main list 2022-03-13 21:08:33 +03:00
DYefremov
6106e86d18 minor settings dialog improvements 2022-03-13 17:57:06 +03:00
DYefremov
1c5f7fab11 tooltips support for main iptv list 2022-03-13 00:07:59 +03:00
DYefremov
024f90d23f t2mi plp id support for satellite editing tool (#70) 2022-03-11 11:40:23 +03:00
DYefremov
ee3041174c streams playback from the main list 2022-03-10 23:06:06 +03:00
DYefremov
6f28aae40c style correction 2022-03-06 12:22:39 +03:00
DYefremov
1b2de795a2 minor rework of settings dialog 2022-03-06 12:19:54 +03:00
DYefremov
8d485a9993 minor style corrections 2022-02-27 09:03:39 +03:00
Víctor Pont
1721567731 Spanish translation fixes and new strings (#68) 2022-02-24 13:46:21 +03:00
DYefremov
fd4325961c minor style changes 2022-02-23 14:22:29 +03:00
DYefremov
294d32c705 Russian, Belarusian and German translations update 2022-02-22 19:55:39 +03:00
DYefremov
aa961030ce changing path by link for FTP client 2022-02-22 14:44:55 +03:00
DYefremov
bc4c6746c9 minor corrections 2022-02-22 14:18:40 +03:00
DYefremov
61282b0cc8 copied tr *.mo file 2022-02-22 10:21:10 +03:00
audi06_19
07e855f99d Turkish translation update (#66) 2022-02-22 10:15:12 +03:00
DYefremov
0d0f19122b updated comments in *.desktop file 2022-02-21 20:33:24 +03:00
DYefremov
4789688efd updated es *.mo file 2022-02-21 20:09:40 +03:00
Víctor Pont
b13c2c3be0 Spanish translation update (#65) 2022-02-21 20:03:21 +03:00
DYefremov
d72189abc4 bump version 2022-02-21 15:03:17 +03:00
DYefremov
35d194100b added keyboard shortcut for renaming 2022-02-21 14:28:47 +03:00
DYefremov
aa0b97b9ae added extra tab for IPTV 2022-02-21 12:22:44 +03:00
DYefremov
5f54452ee2 fixed getting satellites list from FlySat 2022-02-21 00:23:07 +03:00
DYefremov
580e8ca82c basic bookmarks support for the FTP client 2022-02-13 00:29:44 +03:00
DYefremov
4ba2fb1a04 skip importing groups from m3u for Neutrino 2022-02-12 14:22:48 +03:00
DYefremov
b4612c26cb changed file names reading for FTP client 2022-02-09 21:56:45 +03:00
DYefremov
9a8b1e871d bump version 2022-02-05 15:20:28 +03:00
DYefremov
3a53a95f86 fixed current channel recording on Windows 2022-02-05 15:11:29 +03:00
DYefremov
24a94cfe9a improved MPV support 2022-02-05 13:50:59 +03:00
DYefremov
0ca08e3a1d fixed bouquets DND for macOS (#64) 2022-02-04 02:12:48 +03:00
DYefremov
db4e9d2696 fixed single bouquets import on macOS (#63) 2022-02-04 01:16:37 +03:00
DYefremov
3be9b374c8 bump version 2022-01-29 18:43:36 +03:00
DYefremov
0b9fd37ee9 minor revision and improvement of the FTP client 2022-01-28 16:31:12 +03:00
DYefremov
be90d9694a fixed removal of unused picons (#62) 2022-01-28 00:37:02 +03:00
DYefremov
a5144e8e34 fixed multiple selection with the Shift key (#61) 2022-01-27 22:46:34 +03:00
DYefremov
04e0a25956 minor fixes for picons 2022-01-26 23:07:41 +03:00
DYefremov
dc24c899af version update 2022-01-25 18:16:00 +03:00
DYefremov
a55495fd7c reworked built-in function for getting yt links 2022-01-25 18:07:34 +03:00
DYefremov
ed3aea42f5 minor corrections for EPG dialog 2022-01-24 19:17:11 +03:00
DYefremov
a6904360f9 improvement of EPG refs mapping (#59) 2022-01-24 16:39:59 +03:00
DYefremov
1e42d693cc added info panel auto closing for playback (#58) 2022-01-23 14:52:34 +03:00
DYefremov
5e64605be6 minor changes in saving the EPG link mapping list (#59) 2022-01-23 14:24:50 +03:00
DYefremov
63c55ea2ed Russian, Belarusian and German translations update 2022-01-19 22:32:47 +03:00
DYefremov
c7f85b027d bump version 2022-01-05 15:10:10 +03:00
DYefremov
b24910a9a5 fixed command to open file for FTP 2022-01-05 14:57:39 +03:00
DYefremov
1bdb4f123f minor data cleaning optimization 2022-01-05 12:16:16 +03:00
DYefremov
8f8d7633b8 added display picons option 2022-01-05 00:41:06 +03:00
DYefremov
c997724300 refactoring of picon assignment 2022-01-04 20:39:59 +03:00
DYefremov
727a3fa8a2 data rendering optimization 2022-01-03 00:08:31 +03:00
DYefremov
4009f5c2a2 multi-stream support for services web import 2022-01-01 23:09:31 +03:00
DYefremov
ca5d648032 multi-stream transponders support for LyngSat 2021-12-30 15:54:09 +03:00
DYefremov
938fe297c5 corrected satellite update from KingOfSat 2021-12-30 00:02:32 +03:00
DYefremov
db29b78fd7 basic editing support for FTP client 2021-12-29 00:21:11 +03:00
DYefremov
e599ea04c7 improved bouquet export to *.m3u (#56) 2021-12-28 15:17:39 +03:00
DYefremov
50a2f66fc3 bump version 2021-12-24 11:42:33 +03:00
DYefremov
34056b1006 minor rework of recordings tab 2021-12-24 00:15:17 +03:00
DYefremov
6188fecda9 reduced waiting time for web import 2021-12-23 22:07:06 +03:00
DYefremov
eb41b9629e minor revision of satellite update dialog 2021-12-22 14:19:10 +03:00
DYefremov
7694754919 fixed satellites web update from FlySat (#55) 2021-12-21 22:08:54 +03:00
DYefremov
11f240a81f minor rework of ftp client 2021-12-19 20:37:23 +03:00
DYefremov
1b75034317 basic alternate layout support 2021-12-19 13:15:58 +03:00
DYefremov
5b6900cae7 tooltips support for the bouquets list 2021-12-11 16:48:13 +03:00
DYefremov
eeeca881e8 added default id value for iptv service 2021-12-10 12:09:27 +03:00
DYefremov
ed16fb0195 minor correction for settings dialog 2021-12-07 15:05:58 +03:00
DYefremov
833b386356 win style correction 2021-12-07 14:54:12 +03:00
DYefremov
83424124d3 fix saving sub-bouquets 2021-12-07 14:39:31 +03:00
DYefremov
d77aa68a39 Russian, Belarusian and German translations update 2021-12-04 14:53:47 +03:00
DYefremov
94266e13b8 bump version 2021-12-03 21:11:00 +03:00
DYefremov
8db6fb1b0b improved picons filtering and assignment (#49) 2021-12-03 20:05:46 +03:00
DYefremov
e07c5d4bf7 minor code cleanup 2021-12-02 17:28:25 +03:00
DYefremov
6f3090a7e1 sub-bouquets creation support 2021-12-02 15:39:33 +03:00
DYefremov
b73c1d1118 sub bouquets saving support 2021-12-01 11:37:23 +03:00
DYefremov
df127c05f3 services marking not presented in bouquets 2021-11-28 23:41:16 +03:00
DYefremov
9b579af528 added filter option "not in bouquets" 2021-11-28 18:55:37 +03:00
DYefremov
8290e723c9 state saving for the main paned widgets 2021-11-27 10:20:10 +03:00
DYefremov
b12c29be84 minor correction for tid and nid values 2021-11-24 23:54:16 +03:00
DYefremov
1780fbadbd fixed signal update for control tab 2021-11-22 19:41:30 +03:00
DYefremov
4fe4e92442 fixed some folders display for the recording tab 2021-11-22 16:33:24 +03:00
DYefremov
bfad5cf9ac bump version 2021-11-22 14:19:52 +03:00
DYefremov
5d285f61c0 fixed a typo in the Italian translation 2021-11-18 19:52:40 +03:00
DYefremov
f61d9a1f61 fix tid and nid order for KingOfSat web source 2021-11-18 11:31:01 +03:00
DYefremov
8d115677d1 minor fix for picon downloader 2021-11-17 22:42:09 +03:00
DYefremov
f7c6cd6908 preventing gen bouquets for an empty config 2021-11-17 13:05:15 +03:00
DYefremov
2d2a90542c main icons initialization refactoring 2021-11-16 13:21:52 +03:00
DYefremov
535c9c9102 fix themes unpacking on Windows 2021-11-16 12:09:29 +03:00
DYefremov
866e18762d enabled http api setting for Neutrino 2021-11-14 23:37:56 +03:00
DYefremov
aef5027d23 bump version 2021-11-14 17:28:38 +03:00
DYefremov
3a142eca4a README update 2021-11-14 17:16:40 +03:00
DYefremov
606bad7716 fix getting youtube-dl for Windows 2021-11-13 13:13:52 +03:00
DYefremov
fa07f8bf85 fix picon path on profile change 2021-11-12 19:11:19 +03:00
DYefremov
92280162c6 added logging for transponder validation 2021-11-12 16:23:20 +03:00
DYefremov
5d285e88d8 copied tr *.mo file 2021-11-12 11:10:56 +03:00
audi06_19
0355714e92 Turkish translation update (#53) 2021-11-12 11:05:37 +03:00
DYefremov
b06e877a0c update of pl *.mo file 2021-11-11 22:37:58 +03:00
Wieslaw Weglowski
9b479b051d Polish translation update (#52) 2021-11-11 22:28:23 +03:00
DYefremov
b953ee8762 minor fix of playback window title 2021-11-08 11:06:31 +03:00
DYefremov
0e7d6bec69 fixed some configs loading with dvb-t 2021-11-07 23:34:47 +03:00
DYefremov
cb9824d404 changed audio menu icon 2021-11-07 21:47:11 +03:00
DYefremov
c87adb256f fix sid config for IPTV lists 2021-11-07 21:20:06 +03:00
DYefremov
899d05a186 added "Return" key support for FTP client 2021-11-07 00:10:15 +03:00
DYefremov
bd4f86e91e Russian, Belarusian and German translations update 2021-11-06 15:45:58 +03:00
DYefremov
42fb365b45 fix picon assignment by drag for mac 2021-11-06 14:42:13 +03:00
DYefremov
67d6ea861e minor playback fixes 2021-11-06 12:11:05 +03:00
DYefremov
d887a61636 fix settings saving for Ubuntu [18.04] 2021-11-05 23:01:07 +03:00
DYefremov
9d9efb7577 Russian, Belarusian and German translations update 2021-11-03 18:14:41 +03:00
DYefremov
b6d331a311 redesigned info output for download dialog 2021-11-03 18:09:46 +03:00
DYefremov
1060e169a1 minor refactoring 2021-11-03 12:30:23 +03:00
DYefremov
562c1a5955 modifiers correction 2021-11-01 11:09:45 +03:00
DYefremov
3c4dec323f minor mac style correction 2021-11-01 00:44:12 +03:00
DYefremov
3bafe08030 adapting dialogs for Gnome 2021-10-31 20:26:19 +03:00
DYefremov
722f8df813 header bar for Gnome in epg dialog 2021-10-31 16:09:07 +03:00
DYefremov
8f6984dbaf added src and pos display for epg (#51) 2021-10-31 13:34:48 +03:00
DYefremov
bf6e9617ec header bar changes in the backup dialog 2021-10-30 18:25:20 +03:00
DYefremov
ec27c32d35 bump version 2021-10-29 17:26:34 +03:00
DYefremov
bfa3b1aa66 added dialog call before reset of settings 2021-10-29 17:21:30 +03:00
DYefremov
a1e32abd07 minor settings dialog changes for Gnome 2021-10-29 15:57:28 +03:00
DYefremov
f93370293b Russian, Belarusian and German translations update 2021-10-29 11:31:59 +03:00
DYefremov
79a2a034eb fix app menu translation for Windows 2021-10-29 10:45:13 +03:00
DYefremov
791c073d1a redesigned info output for sat update dialog 2021-10-26 12:24:46 +03:00
DYefremov
de5ec53a18 Russian, Belarusian and German translations update 2021-10-26 11:47:26 +03:00
DYefremov
156ac7d364 bump version 2021-10-25 20:29:14 +03:00
DYefremov
5680423f14 added picons extraction from archives 2021-10-25 20:06:33 +03:00
DYefremov
fa256c5a0b added picon copying menu from ext path 2021-10-25 17:06:23 +03:00
DYefremov
d9a5d9a972 added tool menu for Gnome session 2021-10-23 19:25:19 +03:00
DYefremov
4e59bdf38e redesigned info displaying for the picons tab 2021-10-23 11:48:20 +03:00
DYefremov
7edb03836a added support for setting id for iptv 2021-10-23 00:18:51 +03:00
DYefremov
0ea0c889d4 fixed language selection for *.deb 2021-10-22 10:51:15 +03:00
DYefremov
e494a34bc4 fix for display dvb-t2 system (#50) 2021-10-21 22:50:35 +03:00
DYefremov
4a57234293 added logs display in the gui 2021-10-21 18:53:57 +03:00
DYefremov
aca4875ee6 fixed data loading with hex flag values (#50) 2021-10-20 11:46:22 +03:00
DYefremov
a289b9fb53 starting edit sats by double click 2021-10-19 09:00:14 +03:00
DYefremov
a735b3db13 minor picons loading optimization 2021-10-18 19:36:19 +03:00
DYefremov
e57e8f9dd7 added simple telnet client 2021-10-17 23:05:29 +03:00
DYefremov
300e0a38c3 added build scripts 2021-10-17 11:29:23 +03:00
DYefremov
5eb6f8d63f minor rework of yt import dialog 2021-10-16 14:37:21 +03:00
DYefremov
3ec4817665 minor dnd refactoring for the picon explorer 2021-10-14 21:32:56 +03:00
DYefremov
55bb7b2f45 picons auto filtering on channel selection (#49) 2021-10-14 17:37:53 +03:00
DYefremov
a3cf5edd5e changed 7-zip extract cmd 2021-10-14 11:56:30 +03:00
DYefremov
4f8f6cfcc9 small refactoring of management via http api 2021-10-13 01:41:24 +03:00
DYefremov
ef97cd365c support of adding custom path to picons 2021-10-11 00:31:29 +03:00
DYefremov
4b27e81f5e added win style 2021-10-10 14:22:07 +03:00
DYefremov
408753f124 deb start fix 2021-10-09 20:50:41 +03:00
DYefremov
c3bd3cd2bb mac style correction 2021-10-09 18:20:01 +03:00
DYefremov
a15666204c minor ui rework of the picon downloader 2021-10-09 16:55:06 +03:00
DYefremov
1e5c47c3cc minor rework of the picon manager 2021-10-07 13:08:50 +03:00
DYefremov
bc94bb3505 added player and signal elems for control tab 2021-10-05 12:39:34 +03:00
DYefremov
6cfb72b1d7 corrected screenshots creation 2021-10-01 23:47:14 +03:00
DYefremov
3c5144134c minor http api correction for neutrino 2021-10-01 11:44:38 +03:00
DYefremov
4fd2939d2e basic kingofsat support for channels web import 2021-09-30 21:41:48 +03:00
DYefremov
68b5fcfd39 minor adjustments of bouquets parsing 2021-09-28 00:07:30 +03:00
DYefremov
9fcc1e5f90 minor fix for extract dialog 2021-09-27 19:58:34 +03:00
DYefremov
92b028dc16 stream playback support for neutrino 2021-09-27 18:09:17 +03:00
DYefremov
665de22549 support data reloading via http for neutrino 2021-09-26 13:53:55 +03:00
DYefremov
5036ecee37 added extra playback menus 2021-09-25 14:43:43 +03:00
DYefremov
e755e9eb6e epg filtering support 2021-09-23 21:34:38 +03:00
DYefremov
305323726f enabled dark mode support 2021-09-23 18:21:55 +03:00
DYefremov
7b3ae01253 crud support for timers 2021-09-23 17:40:03 +03:00
DYefremov
08fb55ea54 preventing duplicate data loading for last config 2021-09-20 15:50:03 +03:00
DYefremov
f1ee406818 minor ui correction 2021-09-19 20:38:19 +03:00
DYefremov
8aa9d0797f search rework 2021-09-19 00:23:23 +03:00
DYefremov
8d61663d3e auto resizing screenshot 2021-09-18 09:07:27 +03:00
DYefremov
3a9ca26619 added Italian translation 2021-09-17 16:43:44 +03:00
DYefremov
228466d523 import dialog rework 2021-09-15 23:43:02 +03:00
DYefremov
e9b3b3f374 epg dialog rework 2021-09-15 13:12:25 +03:00
DYefremov
05b1619d1e basic markers support in the bouquets list 2021-09-14 16:30:27 +03:00
DYefremov
9a2a2b49f6 streams playback rework 2021-09-13 16:52:19 +03:00
DYefremov
a034d0476d picons page rework 2021-09-05 13:58:26 +03:00
DYefremov
0cf2f070b0 rework of keyboard keys support 2021-09-04 21:45:41 +03:00
DYefremov
dd70dffa1c hiding some items for neutrino mode 2021-09-04 14:46:11 +03:00
DYefremov
5902b207e1 moved iptv menu 2021-09-03 18:09:26 +03:00
DYefremov
b23319ba52 decoupling for neutrino xml handling 2021-09-02 18:29:59 +03:00
DYefremov
cb8ec264cc added recordings page 2021-09-02 12:20:29 +03:00
DYefremov
9fc07308ab modifiers fix 2021-09-01 22:46:24 +03:00
DYefremov
5f0f1e4e64 added timers page 2021-09-01 14:15:39 +03:00
DYefremov
3fe78e0292 added epg page 2021-09-01 00:05:23 +03:00
DYefremov
0ae9123d98 satellite editor rework 2021-08-31 14:16:14 +03:00
DYefremov
7da3c0fd94 paths settings rework 2021-08-30 15:04:15 +03:00
DYefremov
b584126bff network settings rework 2021-08-27 00:19:16 +03:00
DYefremov
fb173449c6 minor rework of download dialog 2021-08-25 14:05:59 +03:00
DYefremov
64de141807 minor refactoring for neutrino 2021-08-25 14:05:27 +03:00
DYefremov
cab20f744f optimization of bouquets generation 2021-08-24 16:19:39 +03:00
DYefremov
9d44b8002c edit dialogs rework 2021-08-23 23:52:51 +03:00
DYefremov
d4389e8b0a base dialogs rework 2021-08-23 18:02:48 +03:00
DYefremov
57696a2460 base dialogs rework 2021-08-23 16:19:46 +03:00
DYefremov
8383214e15 basic dvb-t and cable support for neutrino 2021-08-23 14:34:56 +03:00
DYefremov
56f1c75a41 rework of view menu 2021-08-21 14:50:05 +03:00
DYefremov
f7be210b85 added services.xml for neutrino 2021-08-20 21:24:27 +03:00
DYefremov
7e0b694a4c title bar depending on the platform 2021-08-20 17:24:48 +03:00
DYefremov
9a24cae626 added displaying sub bouquets support 2021-08-18 19:46:47 +03:00
DYefremov
6e459f80bd some corrections 2021-08-18 00:24:51 +03:00
DYefremov
c55645e5db added mac app menu 2021-08-17 16:19:42 +03:00
DYefremov
d6796bc5a5 locale setting refactoring 2021-08-17 11:00:13 +03:00
DYefremov
4ac04e5401 rework of view headers 2021-08-15 17:24:30 +03:00
DYefremov
3c72f0cc3c added picons page 2021-08-15 15:42:27 +03:00
DYefremov
4b940b7135 gui rework 2021-08-15 14:37:21 +03:00
DYefremov
0aa9eaa401 bump version 2021-08-15 09:05:08 +03:00
DYefremov
de8445d55a Russian, Belarusian and German translations update 2021-08-12 13:55:01 +03:00
DYefremov
8153c5e6d6 fixed switching to full screen mode 2021-08-12 13:48:14 +03:00
DYefremov
51fd013e0f minor style changes of the control panel 2021-08-08 11:51:43 +03:00
DYefremov
872f3f0f81 added recordings tab to the control panel 2021-08-06 13:23:06 +03:00
DYefremov
95aa8aaed6 minor filtering optimization 2021-08-04 08:37:52 +03:00
DYefremov
b18207b376 Chinese translation correction 2021-08-02 19:09:26 +03:00
DYefremov
8f840ed3f5 added Chinese translation 2021-08-01 16:16:25 +03:00
DYefremov
6841090cc0 added "Save as" feature 2021-07-26 11:11:42 +03:00
DYefremov
4e700ca1e8 minor optimization of fav list loading 2021-07-26 09:45:05 +03:00
DYefremov
3312576416 data deletion optimization 2021-07-25 22:33:00 +03:00
DYefremov
b047153591 bump version 2021-07-25 21:47:57 +03:00
DYefremov
b15cae8d79 loading services list in the background 2021-07-11 23:29:19 +03:00
DYefremov
c5e8e19941 copy tr *.mo file 2021-06-13 17:23:42 +03:00
audi06_19
50d8e3365e Turkish translations update (#48)
* Turkish translations update

* Update: Turkish translations update

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

* Update demon-editor.po

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

* added a Polish translation

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

105
README.md
View File

@@ -1,27 +1,33 @@
# <img src="app/ui/icons/hicolor/96x96/apps/demon-editor.png" width="32" /> DemonEditor
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) ![platform](https://img.shields.io/badge/platform-windows-lightgrey)
## Enigma2 channel and satellite list editor for MS Windows (experimental).
[<img src="https://user-images.githubusercontent.com/7511379/116426009-6bd3fa80-a84b-11eb-81ea-3407396a0c4b.png" width="850"/>](https://user-images.githubusercontent.com/7511379/116426009-6bd3fa80-a84b-11eb-81ea-3407396a0c4b.png)
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc).
**The functionality and performance of this version may be different from the [Linux version](https://github.com/DYefremov/DemonEditor)!**
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) ![platform](https://img.shields.io/badge/platform-linux%20|%20macos-lightgrey)
### Enigma2 channel and satellite list editor for GNU/Linux.
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
## Main features of the program
* Editing bouquets, channels, satellites.
* Import function.
* Backup function.
* Editing bouquets, channels, satellites.
[<img src="https://user-images.githubusercontent.com/7511379/141680963-9b8eb6cc-c712-46b2-aefe-19769e21a7d5.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141680963-9b8eb6cc-c712-46b2-aefe-19769e21a7d5.png)
* Import function.
[<img src="https://user-images.githubusercontent.com/7511379/141681059-68bc1b55-6fab-436c-aa73-ef24e2e5113b.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681059-68bc1b55-6fab-436c-aa73-ef24e2e5113b.png)
* Backup function.
[<img src="https://user-images.githubusercontent.com/7511379/141681104-ed9b5d35-25de-426f-b9bb-2a6e4db022bb.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681104-ed9b5d35-25de-426f-b9bb-2a6e4db022bb.png)
* Support of picons.
[<img src="https://user-images.githubusercontent.com/7511379/141681115-957c63a3-4113-422d-bb27-2d96b1463cd1.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681115-957c63a3-4113-422d-bb27-2d96b1463cd1.png)
* Importing services, downloading picons and updating satellites from the Web.
[<img src="https://user-images.githubusercontent.com/7511379/141681075-28f18ea5-e456-4e84-bf64-1b7d9a95324d.png" width="262"/>](https://user-images.githubusercontent.com/7511379/141681075-28f18ea5-e456-4e84-bf64-1b7d9a95324d.png)
[<img src="https://user-images.githubusercontent.com/7511379/141681040-b1ad190a-6bc2-4741-bb42-1fb219a0fcab.png" width="250"/>](https://user-images.githubusercontent.com/7511379/141681040-b1ad190a-6bc2-4741-bb42-1fb219a0fcab.png)
* Extended support of IPTV.
* Support of picons.
* Importing services, downloading picons and updating satellites from the Web.
* Import to bouquet(Neutrino WEBTV) from m3u.
* Export of bouquets with IPTV services in m3u.
* Assignment of EPGs from DVB or XML for IPTV services (only Enigma2, experimental).
* Preview (playback) of IPTV or other streams directly from the bouquet list.
* Control panel with the ability to view EPG and manage timers (via HTTP API, experimental).
* Simple FTP client (experimental).
* Assignment of EPG from DVB or XML for IPTV services (Enigma2 only).
[<img src="https://user-images.githubusercontent.com/7511379/141681187-fae4e784-c9e0-43df-b499-4d38e83d6560.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681187-fae4e784-c9e0-43df-b499-4d38e83d6560.png)
* Playback of IPTV or other streams directly from the bouquet list.
[<img src="https://user-images.githubusercontent.com/7511379/141681129-98f78cdc-9a98-46ef-b738-618a327634d4.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681129-98f78cdc-9a98-46ef-b738-618a327634d4.png)
* Control panel (via HTTP API).
[<img src="https://user-images.githubusercontent.com/7511379/141684475-4511ea4f-b152-42d5-b9c8-f3e1e9a160d0.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141684475-4511ea4f-b152-42d5-b9c8-f3e1e9a160d0.png)
* Ability to view EPG and manage timers (via HTTP API).
* Simple FTP client (experimental).
[<img src="https://user-images.githubusercontent.com/7511379/141681165-5679c331-72e7-4044-b365-dcdb30b1433c.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681165-5679c331-72e7-4044-b365-dcdb30b1433c.png)
#### Keyboard shortcuts
* **Ctrl + X** - only in bouquet list.
* **Ctrl + C** - only in services list.
@@ -30,7 +36,8 @@ Clipboard is **"rubber"**. There is an accumulation before the insertion!
beginning or inserts (creates) a new bouquet.
* **Ctrl + BackSpace** - copies the selected channels from the main list to the bouquet end.
* **Ctrl + E** - edit.
* **Ctrl + R, F2** - rename.
* **Ctrl + R, F2** - rename.
* **Ctrl + Alt + R** - rename for bouquet.
* **Ctrl + S, T** in Satellites edit tool for create satellite or transponder.
* **Ctrl + L** - parental lock.
* **Ctrl + H** - hide/skip.
@@ -44,30 +51,68 @@ Clipboard is **"rubber"**. There is an accumulation before the insertion!
* **Ctrl + D** - load data from receiver.
* **Ctrl + U/B** - upload data/bouquets to receiver.
* **Ctrl + I** - extra info, details.
* **Ctrl + F** - show/hide search bar.
* **Ctrl + F** - show search bar.
* **Ctrl + Shift + F** - show/hide filter bar.
* **Ctrl + T** - show/hide built-in Telnet client.
* **Ctrl + Shift + L** - show/hide logging panel.
For **multiple** selection with the mouse, press and hold the **Ctrl** key!
## Minimum requirements
*Python >= 3.5.2, GTK+ >= 3.22 with PyGObject bindings, python3-requests.*
## Important
**This version is not fully tested and has experimental status!**
*Python >= 3.6, GTK+ >= 3.22, python3-gi, python3-gi-cairo, python3-requests.*
Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2.
Main supported *lamedb* format is version **4**. Versions **3** and **5** has only **experimental** support!
For version **3** is only read mode available. When saving, version **4** format is used instead.
***Optional:** python3-pil, python3-chardet.*
## Installation and Launch
* ### Linux
To start the program, in most cases it is enough to download the [archive](https://github.com/DYefremov/DemonEditor/archive/master.zip), unpack
and run it by double clicking on DemonEditor.desktop in the root directory,
or launching from the console with the command:```./start.py```
Extra folders can be deleted, excluding the *app* folder and root files like *DemonEditor.desktop* and *start.py*!
To create a simple **debian package**, you can use the *build-deb.sh.* You can also download a ready-made *.deb package from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
Users of **LTS** versions of [Ubuntu](https://ubuntu.com/) or distributions based on them can use [PPA](https://launchpad.net/~dmitriy-yefremov/+archive/ubuntu/demon-editor) repository.
A ready-made [package](https://aur.archlinux.org/packages/demoneditor-bin) is also available for [Arch Linux](https://archlinux.org/) users in the [AUR](https://aur.archlinux.org/) repository.
* ### macOS
**This program can be run on macOS.**
To run the program on macOS, you need to install [brew](https://brew.sh/).
Then install the required components via terminal:
```brew install python3 gtk+3 pygobject3 adwaita-icon-theme```
```pip3 install requests, pillow```
Launch is similar to Linux.
You can also download the ready-made package as a ***.dmg** file from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
Recommended copy the package to the **Application** directory.
Perhaps in the security settings it will be necessary to allow the launch of this application!
* ### MS Windows
**Windows users can also run this program.**
One way is to use the [MSYS2](https://www.msys2.org/) platform. You can use [this](https://github.com/DYefremov/DemonEditor/blob/master/build/BUILD_WIN.md) quick guide.
In addition, you can download a ready-made build (**64-bit**) from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
**All builds may contain components distributed under the GPL [v3](http://www.gnu.org/licenses/gpl-3.0.html) or lower license.
By downloading and using this packages you agree to the terms of this [license](http://www.gnu.org/licenses/gpl-3.0.html) and the possible inconvenience associated with this!**
THIS SOFTWARE COMES WITH ABSOLUTELY NO WARRANTY.
AUTHOR IS NOT LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY CONNECTION WITH THIS SOFTWARE.
## Important
The program is tested only with [openATV](https://www.opena.tv/) image and **Formuler F1** receiver in [Linux Mint](https://linuxmint.com/) (MATE 64-bit) distribution!
Support for DVB-T/T2 and DVB-C channels for Neutrino is not fully implemented and has an experimental status.
Main supported *lamedb* format is version **4**. Versions **3** and **5** has only **experimental** support! For version **3** is only read mode available. When saving, version **4** format is used instead.
When using the multiple import feature, from *lamedb* will be taken data **only for channels that are in the selected bouquets!**
If you need full set of the data, including *[satellites, terrestrial, cables].xml* (current files will be overwritten),
just load your data via *"File/Open"* and press *"Save"*. When importing separate bouquet files, only those services
(excluding IPTV) that are in the **current open lamedb** (main list of services) will be imported.
**The built-in Telnet client does not support ANSI escape sequences!**
For streams playback, this app supports [VLC](https://www.videolan.org/vlc/), [MPV](https://mpv.io/) and [GStreamer](https://gstreamer.freedesktop.org/). Depending on your distro, you may need to install additional packages and libraries.
#### Command line arguments:
* **-l** - write logs to file.
* **-d on/off** - turn on/off debug mode. Allows to display more information in the logs.
* **-t on/off** - show/hide simple built-in **telnet** client (experimental). **ANSI escape sequences are not supported!**
## License
Licensed under the [MIT](LICENSE) license.
Licensed under the [MIT](LICENSE) license.

View File

@@ -1,28 +1,28 @@
import logging
from collections import defaultdict
from functools import wraps
from threading import Thread, Timer
from gi.repository import GLib
_LOG_FILE = "demon-editor.log"
_DATE_FORMAT = "%d-%m-%y %H:%M:%S"
_LOGGER_NAME = None
LOG_DATE_FORMAT = "%d-%m-%y %H:%M:%S"
LOGGER_NAME = "main_logger"
LOG_FORMAT = "%(asctime)s %(message)s"
def init_logger():
global _LOGGER_NAME
_LOGGER_NAME = "main_logger"
logging.Logger(_LOGGER_NAME)
logging.Logger(LOGGER_NAME)
logging.basicConfig(level=logging.INFO,
format="%(asctime)s %(message)s",
datefmt=_DATE_FORMAT,
format=LOG_FORMAT,
datefmt=LOG_DATE_FORMAT,
handlers=[logging.FileHandler(_LOG_FILE), logging.StreamHandler()])
log("Logging is enabled.", level=logging.INFO)
def log(message, level=logging.ERROR, debug=False, fmt_message="{}"):
""" The main logging function. """
logger = logging.getLogger(_LOGGER_NAME)
logger = logging.getLogger(LOGGER_NAME)
if debug:
from traceback import format_exc
logger.log(level, fmt_message.format(format_exc()))
@@ -67,7 +67,7 @@ def run_with_delay(timeout=5):
timer.cancel()
def run():
GLib.idle_add(func, priority=GLib.PRIORITY_LOW, *args, **kwargs)
GLib.idle_add(func, *args, **kwargs, priority=GLib.PRIORITY_LOW)
timer = Timer(interval=timeout, function=run)
timer.start()
@@ -77,5 +77,37 @@ def run_with_delay(timeout=5):
return run_with
def get_size_from_bytes(size):
""" Simple convert function from bytes to other units like K, M or G. """
try:
b = float(size)
except ValueError:
return size
else:
kb, mb, gb = 1024.0, 1048576.0, 1073741824.0
if b < kb:
return str(b)
elif kb <= b < mb:
return f"{b / kb:.1f} K"
elif mb <= b < gb:
return f"{b / mb:.1f} M"
elif gb <= b:
return f"{b / gb:.1f} G"
class DefaultDict(defaultdict):
""" Extended to support functions with params as default factory. """
def __missing__(self, key):
if self.default_factory:
value = self[key] = self.default_factory(key)
return value
return super().__missing__(key)
def get(self, key, default=None):
return self[key]
if __name__ == "__main__":
pass

View File

@@ -1,3 +1,31 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2023 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
import os
import re
import socket
@@ -5,24 +33,27 @@ import time
import urllib
import xml.etree.ElementTree as ETree
from enum import Enum
from ftplib import FTP, CRLF, Error, error_perm
from ftplib import FTP, CRLF, Error, all_errors
from http.client import RemoteDisconnected
from pathlib import Path
from telnetlib import Telnet
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.parse import urlencode, quote
from urllib.request import (urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener,
install_opener, Request)
from app.commons import log, run_task
from app.settings import SettingsType
BQ_FILES_LIST = ("tv", "radio", # enigma 2
"myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino
BQ_FILES_LIST = ("tv", "radio", # Enigma2.
"services.xml", "myservices.xml", "bouquets.xml", "ubouquets.xml") # Neutrino.
DATA_FILES_LIST = ("lamedb", "lamedb5", "blacklist", "whitelist",)
STC_XML_FILE = ("satellites.xml", "terrestrial.xml", "cables.xml")
WEB_TV_XML_FILE = ("webtv.xml",)
PICONS_SUF = (".jpg", ".png")
PICONS_MAX_NUM = 1000 # Maximum picon number for sending without compression.
class DownloadType(Enum):
@@ -59,11 +90,11 @@ class UtfFTP(FTP):
while 1:
line = fp.readline(self.maxline + 1)
if len(line) > self.maxline:
msg = "UtfFTP [retrlines] error: got more than {} bytes".format(self.maxline)
msg = f"UtfFTP [retrlines] error: got more than {self.maxline} bytes"
log(msg)
raise Error(msg)
if self.debugging > 2:
log('UtfFTP [retrlines] *retr* {}'.format(repr(line)))
log(f"UtfFTP [retrlines] *retr* {repr(line)}")
if not line:
break
if line[-2:] == CRLF:
@@ -82,20 +113,21 @@ class UtfFTP(FTP):
def download_file(self, name, save_path, callback=None):
with open(save_path + name, "wb") as f:
msg = "Downloading file: {}. Status: {}\n"
try:
resp = str(self.retrbinary("RETR " + name, f.write))
except error_perm as e:
resp = str(e)
msg = msg.format(name, e)
log(msg.rstrip())
else:
msg = msg.format(name, resp)
resp = self.download_binary(name, f)
msg = f"Downloading file: {name}. Status: {resp}"
callback(msg) if callback else log(msg.rstrip())
return resp
def download_binary(self, src, fo):
try:
resp = str(self.retrbinary(f"RETR {src}", fo.write))
except all_errors as e:
resp = str(e)
log(f"Error. {e}")
return resp
def download_dir(self, path, save_path, callback=None):
""" Downloads directory from FTP with all contents.
@@ -106,8 +138,8 @@ class UtfFTP(FTP):
files = []
self.dir(path, files.append)
for f in files:
f_data = f.split()
f_path = os.path.join(path, " ".join(f_data[8:]))
f_data = self.get_file_data(f)
f_path = f_data[8]
if f_data[0][0] == "d":
try:
@@ -141,7 +173,7 @@ class UtfFTP(FTP):
def download_picons(self, src, dest, callback, files_filter=None):
try:
self.cwd(src)
except error_perm as e:
except all_errors as e:
callback(str(e))
return
@@ -171,7 +203,7 @@ class UtfFTP(FTP):
def upload_picons(self, src, dest, callback, files_filter=None):
try:
self.cwd(dest)
except error_perm as e:
except all_errors as e:
if str(e).startswith("550"):
self.mkd(dest) # if not exist
self.cwd(dest)
@@ -180,7 +212,7 @@ class UtfFTP(FTP):
self.send_file(file_name, src, callback)
def remove_unused_bouquets(self, callback):
bq_files = ("userbouquet.", "bouquets.xml", "ubouquets.xml")
bq_files = ("userbouquet.", "subbouquet.", "bouquets.xml", "ubouquets.xml")
for file in filter(lambda f: f.startswith(bq_files), self.nlst()):
self.delete_file(file, callback)
@@ -194,18 +226,17 @@ class UtfFTP(FTP):
return resp + " File not found."
with open(file_src, "rb") as f:
msg = "Uploading file: {}. Status: {}\n"
msg = "Uploading file: {}. Status: {}"
try:
resp = str(self.storbinary("STOR " + file_name, f))
except Error as e:
except all_errors as e:
resp = str(e)
msg = msg.format(file_name, resp)
log(msg)
else:
msg = msg.format(file_name, resp)
if callback:
callback(msg)
if callback:
callback(msg)
return resp
@@ -229,12 +260,12 @@ class UtfFTP(FTP):
elif os.path.isdir(file):
try:
self.mkd(f)
except Error:
except all_errors:
pass # NOP
try:
self.cwd(f)
except Error as e:
except all_errors as e:
resp = str(e)
log(msg.format(f, resp))
else:
@@ -254,7 +285,7 @@ class UtfFTP(FTP):
if dest:
try:
self.cwd(dest)
except Error as e:
except all_errors as e:
callback(str(e))
return
@@ -262,10 +293,10 @@ class UtfFTP(FTP):
self.delete_file(file, callback)
def delete_file(self, file, callback=log):
msg = "Deleting file: {}. Status: {}\n"
msg = "Deleting file: {}. Status: {}"
try:
resp = self.delete(file)
except Error as e:
except all_errors as e:
resp = str(e)
msg = msg.format(file, resp)
log(msg)
@@ -281,19 +312,18 @@ class UtfFTP(FTP):
files = []
self.dir(path, files.append)
for f in files:
f_data = f.split()
name = " ".join(f_data[8:])
f_path = path + "/" + name
f_data = self.get_file_data(f)
f_path = f"{path}/{f_data[8]}"
if f_data[0][0] == "d":
self.delete_dir(f_path, callback)
else:
self.delete_file(f_path, callback)
msg = "Remove directory {}. Status: {}\n"
msg = "Remove directory {}. Status: {}"
try:
resp = self.rmd(path)
except Error as e:
except all_errors as e:
msg = msg.format(path, e)
log(msg)
return "500"
@@ -307,10 +337,10 @@ class UtfFTP(FTP):
return resp
def rename_file(self, from_name, to_name, callback=None):
msg = "File rename: {}. Status: {}\n"
msg = "File rename: {}. Status: {}"
try:
resp = self.rename(from_name, to_name)
except Error as e:
except all_errors as e:
resp = str(e)
msg = msg.format(from_name, resp)
log(msg)
@@ -322,12 +352,21 @@ class UtfFTP(FTP):
return resp
@staticmethod
def get_file_data(file):
""" Returns a prepared list of file data from a file string. """
f_data = file.split()
# Ignoring space in file name.
f_data = f_data[0:9]
f_data[8] = file[file.index(f_data[8]):]
return f_data
def download_data(*, settings, download_type=DownloadType.ALL, callback=log, files_filter=None):
with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
ftp.encoding = "utf-8"
callback("FTP OK.\n")
save_path = settings.data_local_path
callback("FTP OK.")
save_path = settings.profile_data_path
os.makedirs(os.path.dirname(save_path), exist_ok=True)
# bouquets
if download_type is DownloadType.ALL or download_type is DownloadType.BOUQUETS:
@@ -341,69 +380,66 @@ def download_data(*, settings, download_type=DownloadType.ALL, callback=log, fil
ftp.download_xml(save_path, settings.satellites_xml_path, WEB_TV_XML_FILE, callback)
if download_type is DownloadType.PICONS:
picons_path = settings.picons_local_path
picons_path = settings.profile_picons_path
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
ftp.download_picons(settings.picons_path, picons_path, callback, files_filter)
# epg.dat
if download_type is DownloadType.EPG:
stb_path = settings.services_path
epg_options = settings.epg_options
if epg_options:
stb_path = epg_options.get("epg_dat_stb_path", stb_path)
save_path = epg_options.get("epg_dat_path", save_path)
ftp.cwd(settings.epg_dat_path)
ftp.download_files(f"{settings.profile_data_path}epg{os.sep}", "epg.dat", callback)
ftp.cwd(stb_path)
ftp.download_files(save_path, "epg.dat", callback)
callback("\nDone.\n")
callback("*** Done. ***")
def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False,
callback=log, done_callback=None, use_http=False, files_filter=None):
def upload_data(*, settings, download_type=DownloadType.ALL, callback=log, done_callback=None,
files_filter=None, ext_host=None):
s_type = settings.setting_type
data_path = settings.data_local_path
host = settings.host
base_url = "http{}://{}:{}".format("s" if settings.http_use_ssl else "", host, settings.http_port)
url = "{}/web/".format(base_url)
tn, ht = None, None # telnet, http
use_http = s_type is SettingsType.ENIGMA_2 and settings.use_http
data_path = settings.profile_data_path
host, port, use_ssl = ext_host or settings.host, settings.http_port, settings.http_use_ssl
user, password = settings.user, settings.password
base_url = f"http{'s' if use_ssl else ''}://{host}:{port}"
base = "web" if s_type is SettingsType.ENIGMA_2 else "control"
url = f"{base_url}/{base}/"
tn, ht = None, None # Telnet, HTTP.
try:
if s_type is SettingsType.ENIGMA_2 and use_http:
ht = http(settings.user, settings.password, base_url, callback, settings.http_use_ssl)
use_http = use_http and test_http(host, port, user, password, use_ssl=use_ssl, skip_message=True, s_type=s_type)
except TestException:
log("HTTP test failed.")
use_http = False
try:
if use_http:
ht = http(user, password, base_url, callback, use_ssl, s_type)
next(ht)
message = ""
if download_type is DownloadType.BOUQUETS:
message = "User bouquets will be updated!"
elif download_type is DownloadType.ALL:
message = "All user data will be reloaded!"
elif download_type is DownloadType.SATELLITES:
message = "Satellites.xml file will be updated!"
elif download_type is DownloadType.PICONS:
message = "Picons will be updated!"
message = get_upload_info_message(download_type)
params = urlencode({"text": message, "type": 2, "timeout": 5})
ht.send((url + "message?{}".format(params), "Sending info message... "))
if s_type is SettingsType.ENIGMA_2:
params = urlencode({"text": message, "type": 2, "timeout": 5})
else:
params = urlencode({"nmsg": message, "timeout": 5}, quote_via=quote)
if download_type is DownloadType.ALL:
ht.send((f"{url}message?{params}", "Sending info message... "))
if s_type is SettingsType.ENIGMA_2 and download_type is DownloadType.ALL:
time.sleep(5)
ht.send((url + "powerstate?newstate=0", "Toggle Standby "))
if not settings.keep_power_mode:
ht.send((f"{url}powerstate?newstate=0", "Toggle Standby "))
time.sleep(2)
else:
if download_type is not DownloadType.PICONS:
# telnet
tn = telnet(host=host,
user=settings.user,
password=settings.password,
timeout=settings.telnet_timeout)
# Telnet
tn = telnet(host=host, user=user, password=password, timeout=settings.telnet_timeout)
next(tn)
# terminate enigma or neutrino
callback("Telnet initialization ...\n")
# Terminate Enigma2 or Neutrino.
callback("Telnet initialization ...")
tn.send("init 4")
callback("Stopping GUI...\n")
callback("Stopping GUI...")
with UtfFTP(host=host, user=settings.user, passwd=settings.password) as ftp:
with UtfFTP(host=host, user=user, passwd=password) as ftp:
ftp.encoding = "utf-8"
callback("FTP OK.\n")
callback("FTP OK.")
sat_xml_path = settings.satellites_xml_path
services_path = settings.services_path
@@ -415,7 +451,7 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
if download_type is DownloadType.BOUQUETS:
ftp.cwd(services_path)
ftp.upload_bouquets(data_path, remove_unused, callback)
ftp.upload_bouquets(data_path, settings.remove_unused_bouquets, callback)
if download_type is DownloadType.ALL:
ftp.upload_xml(data_path, sat_xml_path, STC_XML_FILE, callback)
@@ -423,22 +459,67 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
ftp.upload_xml(data_path, sat_xml_path, WEB_TV_XML_FILE, callback)
ftp.cwd(services_path)
ftp.upload_bouquets(data_path, remove_unused, callback)
ftp.upload_bouquets(data_path, settings.remove_unused_bouquets, callback)
ftp.upload_files(data_path, DATA_FILES_LIST, callback)
if download_type is DownloadType.PICONS:
ftp.upload_picons(settings.picons_local_path, settings.picons_path, callback, files_filter)
p_src, p_dst = settings.profile_picons_path, settings.picons_path
compress = all((settings.compress_picons, files_filter, len(files_filter) > PICONS_MAX_NUM))
if compress:
from zipfile import ZipFile
if tn and not use_http:
# resume enigma or restart neutrino
z_name = "picons.zip"
zip_file = f"{p_src}{z_name}"
p_dst = Path(p_dst).parent.as_posix()
if files_filter and z_name in files_filter:
files_filter.remove(z_name)
if os.path.isfile(zip_file):
try:
os.unlink(zip_file)
except OSError:
pass # NOP
log("Compressing picons...")
with ZipFile(zip_file, "w") as zf:
list(map(lambda p: zf.write(os.path.join(p_src, p), arcname=p), files_filter))
files_filter = {z_name}
log("Uploading...")
ftp.upload_picons(p_src, p_dst, callback, files_filter)
if compress:
if not tn:
callback("Telnet initialization...")
tn = telnet(host=host, user=user, password=password, timeout=settings.telnet_timeout)
next(tn)
callback("Extracting...")
cmd = f"mkdir -p {settings.picons_path} && unzip -o -q {p_dst}/{z_name} -d {settings.picons_path}"
tn.send(cmd)
ftp.delete_file(z_name)
try:
os.unlink(zip_file)
except OSError:
pass # NOP
if all((tn, download_type is not DownloadType.PICONS, not use_http)):
# Resume Enigma2 or restart Neutrino.
tn.send("init 3" if s_type is SettingsType.ENIGMA_2 else "init 6")
callback("Starting...\n" if s_type is SettingsType.ENIGMA_2 else "Rebooting...\n")
callback("Starting..." if s_type is SettingsType.ENIGMA_2 else "Rebooting...")
elif ht and use_http:
if download_type is DownloadType.BOUQUETS:
ht.send((url + "servicelistreload?mode=2", "Reloading Userbouquets."))
elif download_type is DownloadType.ALL:
ht.send((url + "servicelistreload?mode=0", "Reloading lamedb and Userbouquets."))
ht.send((url + "powerstate?newstate=4", "Wakeup from Standby."))
if s_type is SettingsType.ENIGMA_2:
if download_type is DownloadType.BOUQUETS:
ht.send((f"{url}servicelistreload?mode=2", "Reloading Userbouquets."))
elif download_type is DownloadType.ALL:
ht.send((f"{url}servicelistreload?mode=0", "Reloading lamedb and Userbouquets."))
if not settings.keep_power_mode:
ht.send((f"{url}powerstate?newstate=4", "Wakeup from Standby."))
else:
ht.send((f"{url}reloadchannels", "Reloading channels..."))
if done_callback is not None:
done_callback()
@@ -449,12 +530,24 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
ht.close()
def get_upload_info_message(download_type):
if download_type is DownloadType.BOUQUETS:
return "User bouquets will be updated!"
elif download_type is DownloadType.ALL:
return "All user data will be reloaded!"
elif download_type is DownloadType.SATELLITES:
return "Satellites.xml file will be updated!"
elif download_type is DownloadType.PICONS:
return "Picons will be updated!"
return ""
# ***************** Picons *******************#
def remove_picons(*, settings, callback, done_callback=None, files_filter=None):
def remove_picons(*, settings, callback=log, done_callback=None, files_filter=None):
with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
ftp.encoding = "utf-8"
callback("FTP OK.\n")
callback("FTP OK.")
ftp.delete_picons(callback, settings.picons_path, files_filter)
if done_callback:
done_callback()
@@ -464,14 +557,17 @@ def picons_filter_function(files_filter=None):
return lambda f: f in files_filter if files_filter else f.endswith(PICONS_SUF)
def http(user, password, url, callback, use_ssl=False):
init_auth(user, password, url, use_ssl)
data = get_post_data(url, password, url)
def http(user, password, url, callback, use_ssl=False, s_type=SettingsType.ENIGMA_2):
HttpAPI.init_auth(user, password, url, use_ssl)
data = HttpAPI.get_post_data(url, password, url) if s_type is SettingsType.ENIGMA_2 else None
while True:
url, message = yield
resp = get_response(HttpAPI.Request.TEST, url, data).get("e2statetext", None)
callback("HTTP: {} {}\n".format(message, "Successful." if resp and message else ""))
resp = HttpAPI.get_response(HttpAPI.Request.TEST, url, data, s_type)
if s_type is SettingsType.ENIGMA_2:
resp = resp.get("e2statetext", None)
callback(f"HTTP: {message} {'Successful.' if resp and message else ''}")
def telnet(host, port=23, user="", password="", timeout=5):
@@ -490,21 +586,29 @@ def telnet(host, port=23, user="", password="", timeout=5):
tn.read_until(b"Password: ", timeout)
tn.write(password.encode("utf-8") + b"\n")
time.sleep(timeout)
tn.write("{}\r\n".format(command).encode("utf-8"))
time.sleep(timeout)
command = f"{command}\r\n".encode("utf-8")
tn.write(command)
msg = tn.read_until(command, timeout)
while msg.endswith(command) or not msg:
time.sleep(timeout)
msg = tn.read_until(command, timeout)
command = yield
time.sleep(timeout)
tn.write("{}\r\n".format(command).encode("utf-8"))
tn.write(f"{command}\r\n".encode("utf-8"))
time.sleep(timeout)
yield
# ***************** HTTP API *******************#
# ***************** HTTP API ******************* #
class HttpAPI:
__MAX_WORKERS = 4
_MAX_WORKERS = 4
_TIMEOUT = 10
class Request(Enum):
class Request(str, Enum):
ZAP = "zap?sRef="
INFO = "about"
SIGNAL = "signal"
@@ -528,6 +632,8 @@ class HttpAPI:
VOL = "vol?set=set"
# EPG
EPG = "epgservice?sRef="
EPG_NOW = "epgnow?bRef="
EPG_MULTI = "epgmulti?bRef="
# Timer
TIMER = ""
TIMER_LIST = "timerlist"
@@ -537,9 +643,23 @@ class HttpAPI:
REC_CURRENT = "getcurrlocation"
# Screenshot
GRUB = "grab?format=jpg&"
# Neutrino requests.
N_INFO = "info"
N_ZAP = "zapto"
N_STREAM = "build_playlist?id="
class Remote(str, Enum):
""" Args for HttpRequestType [REMOTE] class. """
ONE = "2"
TWO = "3"
THREE = "4"
FOUR = "5"
FIVE = "6"
SIX = "7"
SEVEN = "8"
EIGHT = "9"
NINE = "10"
ZERO = "11"
UP = "103"
LEFT = "105"
RIGHT = "106"
@@ -547,10 +667,20 @@ class HttpAPI:
MENU = "139"
EXIT = "174"
OK = "352"
INFO = "358"
EPG = "365"
TV = "377"
RADIO = "385"
AUDIO = "392"
FAV = "393"
RED = "398"
GREEN = "399"
YELLOW = "400"
BLUE = "401"
CH_UP = "402"
CH_DOWN = "403"
NEXT = "407"
BACK = "412"
class Power(str, Enum):
""" Args for HttpRequestType [POWER] class. """
@@ -565,16 +695,20 @@ class HttpAPI:
Request.POWER,
Request.VOL,
Request.EPG,
Request.EPG_NOW,
Request.EPG_MULTI,
Request.TIMER,
Request.RECORDINGS}
Request.RECORDINGS,
Request.N_ZAP}
STREAM_REQUESTS = {Request.STREAM,
Request.STREAM_CURRENT,
Request.STREAM_TS}
Request.STREAM_TS,
Request.N_STREAM}
def __init__(self, settings):
from concurrent.futures import ThreadPoolExecutor as PoolExecutor
self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS)
self._executor = PoolExecutor(max_workers=self._MAX_WORKERS)
self._settings = settings
self._shutdown = False
@@ -583,42 +717,45 @@ class HttpAPI:
self._base_url = None
self._data = None
self._is_owif = True
self._s_type = SettingsType.ENIGMA_2
self.init()
def send(self, req_type, ref, callback=print, ref_prefix=""):
def send(self, req_type, ref, callback=print, ref_prefix="", timeout=_TIMEOUT):
if self._shutdown:
return
url = self._base_url + req_type.value
url = self._base_url + req_type
data = self._data
if req_type is self.Request.ZAP or req_type in self.STREAM_REQUESTS:
url += urllib.parse.quote(ref)
url += quote(ref)
elif req_type is self.Request.PLAY or req_type is self.Request.PLAYER_REMOVE:
url += "{}{}".format(ref_prefix, urllib.parse.quote(ref).replace("%3A", "%253A"))
url = f"{url}{ref_prefix}{quote(ref).replace('%3A', '%253A')}"
elif req_type is self.Request.GRUB:
data = None # Must be disabled for token-based security.
url = "{}/{}{}".format(self._main_url, req_type.value, ref)
url = f"{self._main_url}/{req_type}{ref}"
elif req_type in self.PARAM_REQUESTS:
url += ref
def done_callback(f):
callback(f.result())
future = self._executor.submit(get_response, req_type, url, data)
future = self._executor.submit(self.get_response, req_type, url, data, self._s_type, timeout)
future.add_done_callback(done_callback)
@run_task
def init(self):
user, password = self._settings.user, self._settings.password
use_ssl = self._settings.http_use_ssl
self._main_url = "http{}://{}:{}".format("s" if use_ssl else "", self._settings.host, self._settings.http_port)
self._base_url = "{}/web/".format(self._main_url)
init_auth(user, password, self._main_url, use_ssl)
url = "{}/web/{}".format(self._main_url, self.Request.TOKEN.value)
s_id = get_session_id(user, password, url)
if s_id != "0":
self._data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
self._s_type = self._settings.setting_type
user, password, use_ssl = self._settings.user, self._settings.password, self._settings.http_use_ssl
self._main_url = f"http{'s' if use_ssl else ''}://{self._settings.host}:{self._settings.http_port}"
self._base_url = f"{self._main_url}/{'web' if self._s_type is SettingsType.ENIGMA_2 else 'control'}/"
self.init_auth(user, password, self._main_url, use_ssl)
self._data = None
if self._s_type is SettingsType.ENIGMA_2:
s_id = self.get_session_id(user, password, f"{self._main_url}/web/{self.Request.TOKEN}")
if s_id != "0":
self._data = urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
self.send(self.Request.INFO, None, self.init_callback)
@@ -639,74 +776,91 @@ class HttpAPI:
self._shutdown = True
self._executor.shutdown()
@staticmethod
def get_response(req_type, url, data=None, s_type=SettingsType.ENIGMA_2, timeout=_TIMEOUT):
try:
with urlopen(Request(url, data=data), timeout=timeout) as f:
if s_type is SettingsType.ENIGMA_2:
return HttpAPI.get_e2_response_data(req_type, f)
elif s_type is SettingsType.NEUTRINO_MP:
return HttpAPI.get_neutrino_response_data(req_type, f)
else:
return f.read().decode("utf-8")
except HTTPError as e:
if req_type is HttpAPI.Request.TEST:
raise e
return {"error_code": e.code}
except OSError as e:
if req_type is HttpAPI.Request.TEST:
raise e
except ETree.ParseError as e:
log("Parsing response error: {}".format(e))
def get_response(req_type, url, data=None):
try:
with urlopen(Request(url, data=data), timeout=10) as f:
if req_type in HttpAPI.STREAM_REQUESTS:
return {"m3u": f.read().decode("utf-8")}
elif req_type is HttpAPI.Request.GRUB:
return {"img_data": f.read()}
elif req_type is HttpAPI.Request.CURRENT:
for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2event"):
return {el.tag: el.text for el in el.iter()} # return first[current] event from the list
elif req_type is HttpAPI.Request.PLAYER_LIST:
return [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2file")]
elif req_type is HttpAPI.Request.EPG:
return {"event_list": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2event")]}
elif req_type is HttpAPI.Request.TIMER_LIST:
return {"timer_list": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2timer")]}
elif req_type is HttpAPI.Request.REC_DIRS:
return {"rec_dirs": [el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2location")]}
elif req_type is HttpAPI.Request.RECORDINGS:
return {"recordings": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2movie")]}
else:
return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()}
except HTTPError as e:
if req_type is HttpAPI.Request.TEST:
raise e
return {"error_code": e.code}
except (URLError, ConnectionResetError) as e:
if req_type is HttpAPI.Request.TEST:
raise e
except ETree.ParseError as e:
log("Parsing response error: {}".format(e))
return {"error_code": -1}
return {"error_code": -1}
@staticmethod
def get_e2_response_data(req_type, f):
if req_type in HttpAPI.STREAM_REQUESTS:
return {"m3u": f.read().decode("utf-8")}
elif req_type is HttpAPI.Request.GRUB:
return {"img_data": f.read()}
elif req_type is HttpAPI.Request.CURRENT:
for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2event"):
return {el.tag: el.text for el in el.iter()} # return first[current] event from the list
elif req_type is HttpAPI.Request.PLAYER_LIST:
return [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2file")]
elif req_type in (HttpAPI.Request.EPG, HttpAPI.Request.EPG_NOW, HttpAPI.Request.EPG_MULTI):
return {"event_list": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2event")]}
elif req_type is HttpAPI.Request.TIMER_LIST:
return {"timer_list": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2timer")]}
elif req_type is HttpAPI.Request.REC_DIRS:
return {"rec_dirs": [el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2location")]}
elif req_type is HttpAPI.Request.RECORDINGS:
return {"recordings": [{el.tag: el.text for el in el.iter()} for el in
ETree.fromstring(f.read().decode("utf-8")).iter("e2movie")]}
else:
return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()}
@staticmethod
def get_neutrino_response_data(req_type, f):
if req_type is HttpAPI.Request.N_INFO:
return {"info": f.read().decode("utf-8").strip()}
elif req_type is HttpAPI.Request.N_STREAM:
return {"m3u": f.read().decode("utf-8")}
return {"data": f.read().decode("utf-8")}
def init_auth(user, password, url, use_ssl=False):
""" Init authentication """
pass_mgr = HTTPPasswordMgrWithDefaultRealm()
pass_mgr.add_password(None, url, user, password)
auth_handler = HTTPBasicAuthHandler(pass_mgr)
@staticmethod
def init_auth(user, password, url, use_ssl=False):
""" Init authentication """
pass_mgr = HTTPPasswordMgrWithDefaultRealm()
pass_mgr.add_password(None, url, user, password)
auth_handler = HTTPBasicAuthHandler(pass_mgr)
if use_ssl:
import ssl
from urllib.request import HTTPSHandler
if use_ssl:
import ssl
from urllib.request import HTTPSHandler
opener = build_opener(auth_handler, HTTPSHandler(context=ssl._create_unverified_context()))
else:
opener = build_opener(auth_handler)
opener = build_opener(auth_handler, HTTPSHandler(context=ssl._create_unverified_context()))
else:
opener = build_opener(auth_handler)
install_opener(opener)
install_opener(opener)
@staticmethod
def get_session_id(user, password, url):
data = urllib.parse.urlencode(dict(user=user, password=password)).encode("utf-8")
return HttpAPI.get_response(HttpAPI.Request.TOKEN, url, data=data).get("e2sessionid", "0")
def get_session_id(user, password, url):
data = urllib.parse.urlencode(dict(user=user, password=password)).encode("utf-8")
return get_response(HttpAPI.Request.TOKEN, url, data=data).get("e2sessionid", "0")
def get_post_data(base_url, password, user):
s_id = get_session_id(user, password, "{}/web/{}".format(base_url, HttpAPI.Request.TOKEN.value))
data = None
if s_id != "0":
data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
return data
@staticmethod
def get_post_data(base_url, password, user):
s_id = HttpAPI.get_session_id(user, password, "{}/web/{}".format(base_url, HttpAPI.Request.TOKEN))
data = None
if s_id != "0":
data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
return data
# ***************** Connections testing *******************#
@@ -715,21 +869,36 @@ 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:
except all_errors as e:
raise TestException(e)
def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message=False):
params = urlencode({"text": "Connection test", "type": 2, "timeout": timeout})
params = "statusinfo" if skip_message else "message?{}".format(params)
base_url = "http{}://{}:{}".format("s" if use_ssl else "", host, port)
# authentication
init_auth(user, password, base_url, use_ssl)
data = get_post_data(base_url, password, user)
def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message=False, s_type=SettingsType.ENIGMA_2):
t_msg = "Connection test!"
if s_type is SettingsType.ENIGMA_2:
params = urlencode({"text": t_msg, "type": 2, "timeout": timeout})
params = "deviceinfo" if skip_message else f"message?{params}"
elif s_type is SettingsType.NEUTRINO_MP:
params = urlencode({"nmsg": t_msg, "timeout": 5}, quote_via=quote)
params = "info" if skip_message else f"message?{params}"
else:
raise TestException("This type of settings is not supported!")
base_url = f"http{'s' if use_ssl else ''}://{host}:{port}"
base = "web" if s_type is SettingsType.ENIGMA_2 else "control"
url = f"{base_url}/{base}/{params}"
# Authentication
HttpAPI.init_auth(user, password, base_url, use_ssl)
data = HttpAPI.get_post_data(base_url, password, user) if s_type is SettingsType.ENIGMA_2 else None
try:
return get_response(HttpAPI.Request.TEST, "{}/web/{}".format(base_url, params), data).get("e2statetext", "")
except (URLError, HTTPError) as e:
log("Testing HTTP connection...")
resp = HttpAPI.get_response(HttpAPI.Request.TEST, url, data, s_type)
if s_type is SettingsType.ENIGMA_2:
return resp.get("e2enigmaversion", "")
return resp
except (RemoteDisconnected, URLError, HTTPError) as e:
raise TestException(e)

View File

@@ -62,7 +62,7 @@ def get_bouquets(path, s_type):
def write_bouquet(path, bq, s_type):
if s_type is SettingsType.ENIGMA_2:
writer = BouquetsWriter(path, None)
writer.write_bouquet(path + "userbouquet.{}.{}".format(bq.name, bq.type), bq.name, bq.services)
writer.write_bouquet(f"{path}userbouquet.{bq.name}.{bq.type}", bq.name, bq.services)
elif s_type is SettingsType.NEUTRINO_MP:
from .neutrino.bouquets import write_bouquet
write_bouquet(path, bq)

View File

@@ -1,7 +1,37 @@
""" Common elements module """
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" Common elements module. """
from collections import namedtuple
from enum import Enum
from app.commons import log
Service = namedtuple("Service", ["flags_cas", "transponder_type", "coded", "service", "locked", "hide", "package",
"service_type", "picon", "picon_id", "ssid", "freq", "rate", "pol", "fec",
"system", "pos", "data_id", "fav_id", "transponder"])
@@ -15,6 +45,11 @@ class BqServiceType(Enum):
MARKER = "MARKER" # 64
SPACE = "SPACE" # 832 [hidden marker]
ALT = "ALT" # Service with alternatives
BOUQUET = "BOUQUET" # Sub bouquet.
@classmethod
def _missing_(cls, value):
return cls.DEFAULT
Bouquet = namedtuple("Bouquet", ["name", "type", "services", "locked", "hidden", "file"])
@@ -22,12 +57,18 @@ Bouquet.__new__.__defaults__ = (None, BqServiceType.DEFAULT, [], None, None, Non
Bouquets = namedtuple("Bouquets", ["name", "type", "bouquets"])
BouquetService = namedtuple("BouquetService", ["name", "type", "data", "num"])
# ***************** Satellites *******************#
# *************** *.xml [Satellites, Terrestrial, Cable] ***************** #
Satellite = namedtuple("Satellite", ["name", "flags", "position", "transponders"])
Terrestrial = namedtuple("Terrestrial", ["name", "flags", "countrycode", "transponders"])
Cable = namedtuple("Cable", ["name", "flags", "satfeed", "countrycode", "transponders"])
Transponder = namedtuple("Transponder", ["frequency", "symbol_rate", "polarization", "fec_inner",
"system", "modulation", "pls_mode", "pls_code", "is_id"])
Transponder = namedtuple("Transponder", ["frequency", "symbol_rate", "polarization", "fec_inner", "system",
"modulation", "pls_mode", "pls_code", "is_id", "t2mi_plp_id"])
TerTransponder = namedtuple("TerTransponder", ["centre_frequency", "system", "bandwidth", "constellation",
"code_rate_hp", "code_rate_lp", "guard_interval", "transmission_mode",
"hierarchy_information", "inversion", "plp_id"])
CableTransponder = namedtuple("CableTransponder", ["frequency", "symbol_rate", "fec_inner", "modulation"])
class TrType(Enum):
@@ -39,11 +80,16 @@ class TrType(Enum):
class BqType(Enum):
""" Bouquet type"""
""" Bouquet type. """
BOUQUET = "bouquet"
TV = "tv"
RADIO = "radio"
WEBTV = "webtv"
MARKER = "marker"
@classmethod
def _missing_(cls, value):
return cls.TV
class Flag(Enum):
@@ -76,6 +122,21 @@ class Flag(Enum):
def is_new(value: int):
return value & 1 << 5
@staticmethod
def parse(value: str) -> int:
""" Returns an int representation of the flag value.
The flag value is usually represented by the number [int],
but can also be appear in hex format.
"""
if len(value) < 3:
return 0
value = value[2:]
if value.isdigit():
return int(value)
return int(value, 16)
class Pids(Enum):
VIDEO = "c:00"
@@ -95,12 +156,20 @@ class Inversion(Enum):
On = "1"
Auto = "2"
@classmethod
def _missing_(cls, value):
return cls.Auto
class Pilot(Enum):
Off = "0"
On = "1"
Auto = "2"
@classmethod
def _missing_(cls, value):
return cls.Auto
class SystemCable(Enum):
""" System of cable service """
@@ -132,6 +201,8 @@ SERVICE_TYPE = {"-2": "Data", "1": "TV", "2": "Radio", "3": "Data", "10": "Radio
# Terrestrial
BANDWIDTH = {"0": "8MHz", "1": "7MHz", "2": "6MHz", "3": "Auto", "4": "5MHz", "5": "1/712MHz", "6": "10MHz"}
CONSTELLATION = {"0": "QPSK", "1": "16-QAM", "2": "64-QAM", "3": "Auto"}
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"}
@@ -146,7 +217,7 @@ T_FEC = {"0": "1/2", "1": "2/3", "2": "3/4", "3": "5/6", "4": "7/8", "5": "Auto"
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"}
C_MODULATION = {"0": "QPSK", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM128", "5": "QAM256", "6": "Auto"}
# ATSC
A_MODULATION = {"0": "Auto", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM128", "5": "QAM256", "6": "8VSB",
@@ -177,23 +248,25 @@ def get_value_by_name(en, name):
def is_transponder_valid(tr: Transponder):
""" Checks transponder validity """
""" Checks transponder validity. """
try:
int(tr.frequency)
int(tr.symbol_rate)
tr.pls_mode is None or int(tr.pls_mode)
tr.pls_code is None or int(tr.pls_code)
tr.is_id is None or int(tr.is_id)
except TypeError:
tr.t2mi_plp_id is None or int(tr.t2mi_plp_id)
except (TypeError, ValueError) as e:
log(f"Transponder validation error: {e}\n{tr}")
return False
if tr.polarization not in POLARIZATION.values():
if tr.polarization not in POLARIZATION:
return False
if tr.fec_inner not in FEC.values():
if tr.fec_inner not in FEC:
return False
if tr.system not in SYSTEM.values():
if tr.system not in SYSTEM:
return False
if tr.modulation not in MODULATION.values():
if tr.modulation not in MODULATION:
return False
return True

View File

@@ -1,3 +1,31 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" This module used for parsing blacklist file
Parent Lock/Unlock
@@ -11,9 +39,8 @@ def get_blacklist(path):
with suppress(FileNotFoundError):
with open(path + __FILE_NAME, "r", encoding="utf-8") as file:
# filter empty values and "\n"
return set(filter(None, (x.strip() for x in file.readlines())))
return set()
return {*list(filter(None, (x.strip() for x in file.readlines())))}
return {}
def write_blacklist(path, channels):

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,6 +29,7 @@
""" Module for working with Enigma2 bouquets. """
import re
from collections import Counter
from enum import Enum
from pathlib import Path
from app.commons import log
@@ -37,10 +38,11 @@ from app.eparser.ecommons import BqServiceType, BouquetService, Bouquets, Bouque
_TV_FILE = "bouquets.tv"
_RADIO_FILE = "bouquets.radio"
_DEFAULT_BOUQUET_NAME = "favourites"
_MARKER_PREFIX = "[MARKER!] "
class BouquetsWriter:
""" Class for creating and writing bouquet files..
""" Class for creating and writing bouquet files.
If "force_bq_names" then naming the files using the name of the bouquet.
Some images may have problems displaying the favorites list!
@@ -58,42 +60,54 @@ class BouquetsWriter:
self._marker_index = 1
self._space_index = 0
self._alt_names = set()
self._NAME_PATTERN = re.compile("[^\\w_()]+")
def write(self):
line = []
pattern = re.compile("[^\\w_()]+")
for bqs in self._bouquets:
line.clear()
line.append("#NAME {}\n".format(bqs.name))
line.append(f"#NAME {bqs.name}\n")
bq_file_names = {b.file for b in bqs.bouquets}
count = 1
m_count = 0
for bq in bqs.bouquets:
bq_name = bq.file
if not bq_name:
if self._force_bq_names:
bq_name = re.sub(pattern, "_", bq.name)
bq_name = re.sub(self._NAME_PATTERN, "_", bq.name)
else:
bq_name = "de{0:02d}".format(count)
bq_name = f"de{count:02d}"
while bq_name in bq_file_names:
count += 1
bq_name = "de{0:02d}".format(count)
bq_name = f"de{count:02d}"
bq_file_names.add(bq_name)
line.append(self._SERVICE.format(2 if bq.type == BqType.RADIO.value else 1, bq_name, bq.type))
self.write_bouquet(self._path + "userbouquet.{}.{}".format(bq_name, bq.type), bq.name, bq.services)
bq_type = BqType(bq.type)
if bq_type is BqType.MARKER:
m_data = bq.file.split(":") if bq.file else None
b_name = m_data[-1].strip() if m_data else bq.name.lstrip(_MARKER_PREFIX)
line.append(self._MARKER.format(m_count, b_name))
m_count += 1
else:
if bq_type is BqType.BOUQUET:
bq_name = re.sub(self._NAME_PATTERN, "_", bq.name)
self.write_sub_bouquet(self._path, bq_name, bq, bqs.type)
else:
self.write_bouquet(f"{self._path}userbouquet.{bq_name}.{bqs.type}", bq.name, bq.services)
line.append(self._SERVICE.format(2 if bqs.type == BqType.RADIO.value else 1, bq_name, bqs.type))
with open(self._path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file:
with open(f"{self._path}bouquets.{bqs.type}", "w", encoding="utf-8", newline="\n") as file:
file.writelines(line)
def write_bouquet(self, path, name, services):
""" Writes single bouquet file. """
bouquet = ["#NAME {}\n".format(name)]
bouquet = [f"#NAME {name}\n"]
for srv in services:
s_type = srv.service_type
if s_type == BqServiceType.IPTV.name:
bouquet.append("#SERVICE {}\n".format(srv.fav_id.strip()))
bouquet.append(f"#SERVICE {srv.fav_id.strip()}\n")
elif s_type == BqServiceType.MARKER.name:
m_data = srv.fav_id.strip().split(":")
m_data[2] = self._marker_index
@@ -107,30 +121,56 @@ class BouquetsWriter:
if services:
p = Path(path)
alt_name = srv.data_id
f_name = "alternatives.{}{}".format(alt_name, p.suffix)
f_name = f"alternatives.{alt_name}{p.suffix}"
if self._force_bq_names:
alt_name = re.sub(self._ALT_PAT, "_", srv.service).lower()
f_name = "alternatives.{}{}".format(alt_name, p.suffix)
f_name = f"alternatives.{alt_name}{p.suffix}"
alt_path = "{}/{}".format(p.parent, f_name)
bouquet.append(self._ALT.format(f_name))
self.write_bouquet(alt_path, srv.service, services)
self.write_bouquet(f"{p.parent}/{f_name}", srv.service, services)
else:
data = to_bouquet_id(srv)
if srv.service:
bouquet.append("#SERVICE {}:{}\n#DESCRIPTION {}\n".format(data, srv.service, srv.service))
bouquet.append(f"#SERVICE {data}:{srv.service}\n#DESCRIPTION {srv.service}\n")
else:
bouquet.append("#SERVICE {}\n".format(data))
bouquet.append(f"#SERVICE {data}\n")
with open(path, "w", encoding="utf-8") as file:
with open(path, "w", encoding="utf-8", newline="\n") as file:
file.writelines(bouquet)
def write_sub_bouquet(self, path, file_name, bq, bq_type):
bouquet = [f"#NAME {bq.name}\n"]
sb_type = 2 if bq_type == BqType.RADIO.value else 1
for sb in bq.services:
bq_name = f"subbouquet.{re.sub(self._NAME_PATTERN, '_', sb.name)}.{sb.type}"
self.write_bouquet(f"{path}{bq_name}", sb.name, sb.services)
bouquet.append(f"#SERVICE 1:7:{sb_type}:0:0:0:0:0:0:0:FROM BOUQUET \"{bq_name}\" ORDER BY bouquet\n")
with open(f"{self._path}userbouquet.{file_name}.{bq_type}", "w", encoding="utf-8", newline="\n") as file:
file.writelines(bouquet)
class ServiceType(Enum):
SERVICE = "0"
BOUQUET = "7" # Sub bouquet.
MARKER = "64"
SPACE = "832" # Hidden marker.
ALT = "134" # Alternatives.
UDP = "256"
@classmethod
def _missing_(cls, value):
log("Error. No matching service type [{} {}] was found.".format(cls.__name__, value))
return cls.SERVICE
class BouquetsReader:
""" Class for reading and parsing bouquets. """
_ALT_PAT = re.compile(".*alternatives\\.+(.*)\\.([tv|radio]+).*")
_BQ_PAT = re.compile(".*userbouquet\\.+(.*)\\.+[tv|radio].*")
_ALT_PAT = re.compile(r".*alternatives\.+(.*)\.([tv|radio]+).*")
_BQ_PAT = re.compile(r".*\s+\W(.*bouquet)\.+(.*)\.+[tv|radio].*")
_SUB_BQ_PAT = re.compile(r".*subbouquet\.+(.*)\.([tv|radio]+).*")
_STREAM_TYPES = {"4097", "5001", "5002", "8193", "8739"}
__slots__ = ["_path"]
@@ -144,51 +184,58 @@ class BouquetsReader:
def parse_bouquets(self, bq_name, bq_type):
with open(self._path + bq_name, encoding="utf-8", errors="replace") as file:
lines = file.readlines()
bouquets = None
nm_sep = "#NAME"
line = file.readline()
_, _, bqs_name = line.partition("#NAME")
if not bqs_name:
log(f"No bouquets name found in '{bq_name}'")
bqs_name = "Bouquets (TV)" if bq_type == BqType.TV.value else "Bouquets (Radio)"
bouquets = Bouquets(bqs_name.strip(), bq_type, [])
b_names = set()
real_b_names = Counter()
for line in lines:
if nm_sep in line:
_, _, name = line.partition(nm_sep)
bouquets = Bouquets(name.strip(), bq_type, [])
if bouquets and "#SERVICE" in line:
for line in file.readlines():
if "#SERVICE" in line:
name = re.match(self._BQ_PAT, line)
if name:
b_name = name.group(1)
prefix, b_name = name.group(1), name.group(2)
if b_name in b_names:
log("The list of bouquets contains duplicate [{}] names!".format(b_name))
log(f"The list of bouquets contains duplicate [{b_name}] names!")
else:
b_names.add(b_name)
rb_name, services = self.get_bouquet(self._path, b_name, bq_type)
rb_name, services = self.get_bouquet(self._path, b_name, bq_type, prefix)
if rb_name in real_b_names:
log("Bouquet file 'userbouquet.{}.{}' has duplicate name: {}".format(b_name, bq_type,
rb_name))
log(f"Bouquet file '{prefix}.{b_name}.{bq_type}' has duplicate name: {rb_name}")
real_b_names[rb_name] += 1
rb_name = "{} {}".format(rb_name, real_b_names[rb_name])
rb_name = f"{rb_name} {real_b_names[rb_name]}"
else:
real_b_names[rb_name] = 0
bouquets[2].append(Bouquet(rb_name, bq_type, services, None, None, b_name))
else:
raise ValueError("No bouquet name found for: {}".format(line))
s_data = line.split(":")
if len(s_data) == 12 and s_data[1] == ServiceType.MARKER.value:
b_name = f"{_MARKER_PREFIX}{s_data[-1].strip()}"
bouquets[2].append(Bouquet(b_name, BqType.MARKER.value, [], None, None, line.strip()))
else:
log(f"Unsupported or invalid data format: [{line}].")
else:
log(f"Unsupported or invalid line format: [{line}].")
return bouquets
@staticmethod
def get_bouquet(path, bq_name, bq_type, prefix="userbouquet"):
""" Parsing services ids from bouquet file. """
with open(path + "{}.{}.{}".format(prefix, bq_name, bq_type), encoding="utf-8", errors="replace") as file:
with open(f"{path}{prefix}.{bq_name}.{bq_type}", encoding="utf-8", errors="replace") as file:
chs_list = file.read()
services = []
srvs = list(filter(None, chs_list.split("\n#SERVICE"))) # filtering ['']
# May come across empty[wrong] files!
if not srvs:
log("Bouquet file 'userbouquet.{}.{}' is empty or wrong!".format(bq_name, bq_type))
return "{} [empty]".format(bq_name), services
log(f"Bouquet file 'userbouquet.{bq_name}.{bq_type}' is empty or wrong!")
return f"{bq_name} [empty]", services
bq_name = srvs.pop(0)
@@ -196,28 +243,35 @@ class BouquetsReader:
srv_data = srv.strip().split(":")
data_len = len(srv_data)
if data_len < 10:
log("The bouquet [{}] service [{}] has the wrong data format: [{}]".format(bq_name, num, srv))
log(f"The bouquet [{bq_name}] service [{num}] has the wrong data format: [{srv}]")
continue
s_type = srv_data[1]
if s_type == "64":
s_type = ServiceType(srv_data[1])
if s_type is ServiceType.MARKER:
m_data, sep, desc = srv.partition("#DESCRIPTION")
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.MARKER, srv, num))
elif s_type == "832":
elif s_type is ServiceType.SPACE:
m_data, sep, desc = srv.partition("#DESCRIPTION")
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.SPACE, srv, num))
elif s_type == "134":
elif s_type is ServiceType.ALT:
alt = re.match(BouquetsReader._ALT_PAT, srv)
if alt:
alt_name, alt_type = alt.group(1), alt.group(2)
alt_bq_name, alt_srvs = BouquetsReader.get_bouquet(path, alt_name, alt_type, "alternatives")
services.append(BouquetService(alt_bq_name, BqServiceType.ALT, alt_name, tuple(alt_srvs)))
elif s_type is ServiceType.BOUQUET:
sub = re.match(BouquetsReader._SUB_BQ_PAT, srv)
if sub:
sub_name, sub_type = sub.group(1), sub.group(2)
sub_bq_name, sub_srvs = BouquetsReader.get_bouquet(path, sub_name, sub_type, "subbouquet")
bq = Bouquet(sub_bq_name, sub_type, tuple(sub_srvs), None, None, sub_name)
services.append(BouquetService(sub_bq_name, BqServiceType.BOUQUET, bq, num))
elif srv_data[0].strip() in BouquetsReader._STREAM_TYPES or srv_data[10].startswith(("http", "rtsp")):
stream_data, sep, desc = srv.partition("#DESCRIPTION")
desc = desc.lstrip(":").strip() if desc else srv_data[-1].strip()
services.append(BouquetService(desc, BqServiceType.IPTV, srv, num))
else:
fav_id = "{}:{}:{}:{}".format(srv_data[3], srv_data[4], srv_data[5], srv_data[6])
fav_id = f"{srv_data[3]}:{srv_data[4]}:{srv_data[5]}:{srv_data[6]}"
name = None
if data_len == 12:
name, sep, desc = str(srv_data[-1]).partition("\n#DESCRIPTION")

View File

@@ -1,3 +1,31 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" This module used for parsing and write lamedb file """
import re
@@ -24,7 +52,7 @@ class LameDbReader:
""" Lamedb parser class.
Reads and parses the Enigma2 lamedb[5] file.
Supports versions 3, 4 and 5..
Supports versions 3, 4 and 5.
"""
__slots__ = ["_path", "_fmt"]
@@ -71,7 +99,7 @@ class LameDbReader:
try:
data = str(file.read())
except UnicodeDecodeError as e:
log("lamedb parse error: " + str(e))
log(f"lamedb parse error: {e}")
else:
return self.get_services_list(data)
@@ -87,7 +115,7 @@ class LameDbReader:
for line in lns:
if line.startswith("s:"):
srv_data = line.strip("s:").split(",", 2)
srv_data[1] = srv_data[1].strip("\"")
srv_data[1] = srv_data[1].strip("\"\n")
data_len = len(srv_data)
if data_len == 3:
srv_data[2] = srv_data[2].strip()
@@ -101,7 +129,7 @@ class LameDbReader:
tr, srv = data[0].strip("t:"), data[1].strip().replace(":", " ", 1)
trs[tr] = srv
else:
log("Error while parsing transponder data [ver. 5] for line: {}".format(line))
log(f"Error while parsing transponder data [ver. 5] for line: {line}")
return self.parse_services(srvs, trs)
@@ -123,33 +151,33 @@ class LameDbReader:
is_v3 = False
if len(tid) < 4:
is_v3 = True
tid = "{:0>4}".format(tid)
tid = f"{tid:0>4}"
data[2] = tid
if len(nid) < 4:
is_v3 = True
nid = "{:0>4}".format(nid)
nid = f"{nid:0>4}"
data[3] = nid
if is_v3:
data[0] = "{:0>4}".format(data[0])
data[0] = f"{data[0]:0>4}"
data_id = _SEP.join(data)
srv_type = int(data[4])
transponder_id = "{}:{}:{}".format(data[1], tid, nid)
transponder_id = f"{data[1]}:{tid}:{nid}"
transponder = transponders.get(transponder_id, None)
tid = tid.lstrip(sp).upper()
nid = nid.lstrip(sp).upper()
# The tid and nid values can be 0.
tid = tid.lstrip(sp).upper() or "0"
nid = nid.lstrip(sp).upper() or "0"
ssid = str(data[0]).lstrip(sp).upper()
onid = str(data[1]).lstrip(sp).upper()
# For comparison in bouquets. Needed in upper case!!!
fav_id = "{}:{}:{}:{}".format(ssid, tid, nid, onid)
picon_id = "1_0_{:X}_{}_{}_{}_{}_0_0_0.png".format(srv_type, ssid, tid, nid, onid)
s_id = "1:0:{:X}:{}:{}:{}:{}:0:0:0:".format(srv_type, ssid, tid, nid, onid)
fav_id = f"{ssid}:{tid}:{nid}:{onid}"
picon_id = f"1_0_{srv_type:X}_{ssid}_{tid}_{nid}_{onid}_0_0_0.png"
s_id = f"1:0:{srv_type:X}:{ssid}:{tid}:{nid}:{onid}:0:0:0:"
all_flags = srv[2].split(",")
coded = CODED_ICON if list(filter(lambda x: x.startswith("C:"), all_flags)) else None
flags = list(filter(lambda x: x.startswith("f:"), all_flags))
hide = HIDE_ICON if flags and Flag.is_hide(int(flags[0][2:])) else None
hide = HIDE_ICON if flags and Flag.is_hide(Flag.parse(flags[0])) else None
locked = LOCKED_ICON if s_id in blacklist else None
package = list(filter(lambda x: x.startswith("p:"), all_flags))
@@ -160,7 +188,7 @@ class LameDbReader:
tr_type = TrType(tr_type)
tr = tr.split(_SEP)
service_type = SERVICE_TYPE.get(data[4], SERVICE_TYPE["-2"])
# Removing all non printable symbols!
# Removing all non-printable symbols!
srv_name = "".join(c for c in srv[1] if c.isprintable())
freq = tr[0]
rate = tr[1]
@@ -175,7 +203,7 @@ class LameDbReader:
system = "DVB-S2" if len(tr) > 7 else "DVB-S"
pos = tr[4]
if tr_type is TrType.Terrestrial:
system = T_SYSTEM.get(tr[9], None)
system = T_SYSTEM.get(tr[10] if len(tr) > 10 else "0", None)
pos = "T"
fec = T_FEC.get(tr[3], None)
elif tr_type is TrType.Cable:
@@ -189,13 +217,13 @@ class LameDbReader:
# Formatting displayed values.
try:
freq = "{}".format(int(freq) // 1000)
rate = "{}".format(int(rate) // 1000)
freq = f"{int(freq) // 1000}"
rate = f"{int(rate) // 1000}"
if tr_type is TrType.Satellite:
pos = int(pos)
pos = "{:0.1f}{}".format(abs(pos / 10), "W" if pos < 0 else "E")
pos = f"{abs(pos / 10):0.1f}{'W' if pos < 0 else 'E'}"
except ValueError as e:
log("Parse error [parse_services]: {}".format(e))
log(f"Parse error [parse_services]: {e}")
s = Service(srv[2], tr_type.value, coded, srv_name, locked, hide, package, service_type, None,
picon_id, data[0], freq, rate, pol, fec, system, pos, data_id, fav_id, transponder)
@@ -230,18 +258,17 @@ class LameDbReader:
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_id = f"{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_lines.append(f"{tr_id}\n\t{srv.transponder}\n/\n")
tr_set.add(tr_id)
# Services
services_lines.append("{}\n{}\n{}\n".format(srv.data_id, srv.service, srv.flags_cas))
services_lines.append(f"{srv.data_id}\n{srv.service}\n{srv.flags_cas}\n")
tr_lines.sort()
lines.extend(tr_lines)
lines.extend(services_lines)
lines.append("end\n" + _END_LINE)
lines.append(f"end\n{_END_LINE}")
return lines
@@ -283,7 +310,7 @@ class LameDbWriter:
def write(self):
if self._fmt == 4:
# Writing lamedb file ver.4
with open(self._path + _FILE_NAME, "w", encoding="utf-8") as file:
with open(self._path + _FILE_NAME, "w", encoding="utf-8", newline="\n") as file:
file.writelines(LameDbReader.get_services_lines(self._services))
elif self._fmt == 5:
self.write_to_lamedb5()
@@ -296,19 +323,19 @@ class LameDbWriter:
for srv in self._services:
data_id = str(srv.data_id).split(_SEP)
tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3])
tr_set.add("t:{},{}\n".format(tr_id, srv.transponder.replace(" ", ":", 1)))
tr_id = f"{data_id[1]}:{data_id[2]}:{data_id[3]}"
tr_set.add(f"t:{tr_id},{srv.transponder.replace(' ', ':', 1)}\n")
# Removing empty packages
flags = list(filter(lambda x: x != "p:", srv.flags_cas.split(",")))
flags = ",".join(flags)
flags = "," + flags if flags else ""
services_lines.append("s:{},\"{}\"{}\n".format(srv.data_id, srv.service, flags))
services_lines.append(f"s:{srv.data_id},\"{srv.service}\"{flags}\n")
lines.extend(sorted(tr_set))
lines.extend(services_lines)
lines.append(_END_LINE)
with open(self._path + "lamedb5", "w", encoding="utf-8") as file:
with open(self._path + "lamedb5", "w", encoding="utf-8", newline="\n") as file:
file.writelines(lines)

View File

@@ -1,3 +1,31 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" Module for IPTV and streams support """
import re
from enum import Enum
@@ -10,8 +38,9 @@ from app.ui.uicommons import IPTV_ICON
# url, description, urlkey, account, usrname, psw, s_type, iconsrc, iconsrc_b, group
NEUTRINO_FAV_ID_FORMAT = "{}::{}::{}::{}::{}::{}::{}::{}::{}::{}"
ENIGMA2_FAV_ID_FORMAT = " {}:0:{}:{:X}:{:X}:{:X}:{:X}:0:0:0:{}:{}\n#DESCRIPTION: {}\n"
ENIGMA2_FAV_ID_FORMAT = " {}:{}:{}:{:X}:{:X}:{:X}:{:X}:0:0:0:{}:{}\n#DESCRIPTION {}\n"
MARKER_FORMAT = " 1:64:{}:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n"
PICON_FORMAT = "{}_{}_{:X}_{:X}_{:X}_{:X}_{:X}_0_0_0.png"
class StreamType(Enum):
@@ -21,6 +50,11 @@ class StreamType(Enum):
NONE_REC_2 = "5002"
E_SERVICE_URI = "8193"
E_SERVICE_HLS = "8739"
UNKNOWN = "0"
@classmethod
def _missing_(cls, value):
return cls.UNKNOWN
def parse_m3u(path, s_type, detect_encoding=True, params=None):
@@ -48,12 +82,10 @@ def parse_m3u(path, s_type, detect_encoding=True, params=None):
p_id = "1_0_1_0_0_0_0_0_0_0.png"
st = BqServiceType.IPTV.name
params = params or [0, 0, 0, 0]
m_name = BqServiceType.MARKER.name
for line in str(data, encoding=encoding, errors="ignore").splitlines():
if line.startswith("#EXTINF"):
inf, sep, line = line.partition(" ")
if not line:
line = inf
line, sep, name = line.rpartition(",")
data = re.split('"', line)
@@ -63,39 +95,40 @@ def parse_m3u(path, s_type, detect_encoding=True, params=None):
d = {data[i].lower().strip(" ="): data[i + 1] for i in range(0, len(data) - 1, 2)}
picon = d.get("tvg-logo", None)
grp_name = d.get("group-title", None)
if grp_name not in groups:
groups.add(grp_name)
fav_id = MARKER_FORMAT.format(marker_counter, grp_name, grp_name)
marker_counter += 1
mr = Service(None, None, None, grp_name, None, None, None, BqServiceType.MARKER.name, None, None,
None, None, None, None, None, None, None, None, fav_id, None)
services.append(mr)
if s_type is SettingsType.ENIGMA_2:
grp_name = d.get("group-title", None)
if grp_name not in groups:
groups.add(grp_name)
fav_id = MARKER_FORMAT.format(marker_counter, grp_name, grp_name)
marker_counter += 1
mr = Service(None, None, None, grp_name, *aggr[0:3], m_name, *aggr, fav_id, None)
services.append(mr)
elif line.startswith("#EXTGRP") and s_type is SettingsType.ENIGMA_2:
grp_name = line.strip("#EXTGRP:").strip()
if grp_name not in groups:
groups.add(grp_name)
fav_id = MARKER_FORMAT.format(marker_counter, grp_name, grp_name)
marker_counter += 1
mr = Service(None, None, None, grp_name, None, None, None, BqServiceType.MARKER.name, None, None,
None, None, None, None, None, None, None, None, fav_id, None)
mr = Service(None, None, None, grp_name, *aggr[0:3], m_name, *aggr, fav_id, None)
services.append(mr)
elif not line.startswith("#"):
url = line.strip()
params[0] = sid_counter
sid_counter += 1
fav_id = get_fav_id(url, name, s_type, params)
if s_type is SettingsType.ENIGMA_2:
p_id = get_picon_id(params)
if all((name, url, fav_id)):
srv = Service(None, None, IPTV_ICON, name, None, None, None, st, picon, p_id, None, None, None,
None, None, None, None, url, fav_id, None)
srv = Service(None, None, IPTV_ICON, name, *aggr[0:3], st, picon, p_id, *s_aggr, url, fav_id, None)
services.append(srv)
else:
log("*.m3u* parse error ['{}']: name[{}], url[{}], fav id[{}]".format(path, name, url, fav_id))
log(f"*.m3u* parse error ['{path}']: name[{name}], url[{url}], fav id[{fav_id}]")
return services
def export_to_m3u(path, bouquet, s_type):
def export_to_m3u(path, bouquet, s_type, url=None):
pattern = re.compile(".*:(http.*):.*") if s_type is SettingsType.ENIGMA_2 else re.compile("(http.*?)::::.*")
lines = ["#EXTM3U\n"]
current_grp = None
@@ -106,29 +139,35 @@ def export_to_m3u(path, bouquet, s_type):
res = re.match(pattern, s.data)
if not res:
continue
data = res.group(1)
lines.append("#EXTINF:-1,{}\n".format(s.name))
if current_grp:
lines.append(current_grp)
lines.append("{}\n".format(unquote(data.strip())))
lines.append(f"#EXTINF:-1,{s.name}\n")
lines.append(current_grp) if current_grp else None
lines.append(f"{unquote(res.group(1).strip())}\n")
elif s_type is BqServiceType.MARKER:
current_grp = "#EXTGRP:{}\n".format(s.name)
current_grp = f"#EXTGRP:{s.name}\n"
elif s_type is BqServiceType.DEFAULT and url:
lines.append(f"#EXTINF:-1,{s.name}\n")
lines.append(current_grp) if current_grp else None
lines.append(f"{url}{s.data}\n")
with open(path + "{}.m3u".format(bouquet.name), "w", encoding="utf-8") as file:
with open(f"{path}{bouquet.name}.m3u", "w", encoding="utf-8") as file:
file.writelines(lines)
def get_fav_id(url, service_name, settings_type, params=None, stream_type=None, s_type=1):
def get_fav_id(url, name, settings_type, params=None, st_type=None, s_id=0, srv_type=1):
""" Returns fav id depending on the profile. """
if settings_type is SettingsType.ENIGMA_2:
stream_type = stream_type or StreamType.NONE_TS.value
st_type = st_type or StreamType.NONE_TS.value
params = params or (0, 0, 0, 0)
v1, v2, v3, v4 = params
return ENIGMA2_FAV_ID_FORMAT.format(stream_type, s_type, v1, v2, v3, v4, quote(url),
service_name, service_name, None)
return ENIGMA2_FAV_ID_FORMAT.format(st_type, s_id, srv_type, *params, quote(url), name, name, None)
elif settings_type is SettingsType.NEUTRINO_MP:
return NEUTRINO_FAV_ID_FORMAT.format(url, "", 0, None, None, None, None, "", "", 1)
def get_picon_id(params=None, st_type=None, s_id=0, srv_type=1):
st_type = st_type or StreamType.NONE_TS.value
params = params or (0, 0, 0, 0)
return PICON_FORMAT.format(st_type, s_id, srv_type, *params)
if __name__ == "__main__":
pass

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
SP = "_:::_"
KSP = "_::_"
API_VER = "4"
def get_attributes(data):
return {el[0]: el[1] for el in (e.split(KSP) for e in data.split(SP))}
def get_xml_attributes(attr):
attrs = attr.attributes
return {t: attrs[t].value for t in attrs.keys()}

View File

@@ -1,7 +1,36 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
import os
from xml.dom.minidom import parse, Document
from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT
from app.eparser.neutrino import KSP, SP, get_xml_attributes, get_attributes, API_VER
from app.eparser.neutrino.nxml import XmlHandler, NeutrinoDocument
from app.ui.uicommons import LOCKED_ICON, HIDE_ICON
from ..ecommons import Bouquets, Bouquet, BouquetService, BqServiceType, PROVIDER, BqType
@@ -23,29 +52,29 @@ def parse_bouquets(file, name, bq_type):
if not os.path.exists(file):
return bouquets
dom = parse(file)
dom = XmlHandler.parse(file)
for elem in dom.getElementsByTagName("Bouquet"):
if elem.hasAttributes():
bq_name = elem.attributes["name"].value
hidden = elem.attributes.get("hidden")
hidden = hidden.value if hidden else hidden
locked = elem.attributes.get("locked")
locked = locked.value if locked else locked
# epg = elem.attributes["epg"].value
bq_attrs = get_xml_attributes(elem)
bq_name = bq_attrs.get("name", "")
hidden = bq_attrs.get("hidden", "0")
locked = bq_attrs.get("locked", "0")
services = []
for srv_elem in elem.getElementsByTagName("S"):
if srv_elem.hasAttributes():
ssid = srv_elem.attributes["i"].value
on = srv_elem.attributes["on"].value
tr_id = srv_elem.attributes["t"].value
s_attrs = get_xml_attributes(srv_elem)
ssid = s_attrs.get("i", "0")
on = s_attrs.get("on", "0")
tr_id = s_attrs.get("t", "0")
fav_id = "{}:{}:{}".format(tr_id, on, ssid)
services.append(BouquetService(None, BqServiceType.DEFAULT, fav_id, 0))
bouquets[2].append(Bouquet(name=bq_name,
type=bq_type,
services=services,
locked=LOCKED_ICON if locked == "1" else None,
hidden=HIDE_ICON if hidden == "1" else None))
hidden=HIDE_ICON if hidden == "1" else None,
file=SP.join("{}{}{}".format(k, KSP, v) for k, v in bq_attrs.items())))
if BqType(bq_type) is BqType.BOUQUET:
for bq in bouquets.bouquets:
@@ -63,35 +92,27 @@ def parse_webtv(path, name, bq_type):
if not os.path.exists(path):
return bouquets
dom = parse(path)
dom = XmlHandler.parse(path)
services = []
for elem in dom.getElementsByTagName("webtv"):
if elem.hasAttributes():
title = elem.attributes["title"].value
url = elem.attributes["url"].value
description = elem.attributes.get("description")
description = description.value if description else description
urlkey = elem.attributes.get("urlkey", None)
urlkey = urlkey.value if urlkey else urlkey
account = elem.attributes.get("account", None)
account = account.value if account else account
usrname = elem.attributes.get("usrname", None)
usrname = usrname.value if usrname else usrname
psw = elem.attributes.get("psw", None)
psw = psw.value if psw else psw
s_type = elem.attributes.get("type", None)
s_type = s_type.value if s_type else s_type
iconsrc = elem.attributes.get("iconsrc", None)
iconsrc = iconsrc.value if iconsrc else iconsrc
iconsrc_b = elem.attributes.get("iconsrc_b", None)
iconsrc_b = iconsrc_b.value if iconsrc_b else iconsrc_b
group = elem.attributes.get("group", None)
group = group.value if group else group
web_attrs = get_xml_attributes(elem)
title = web_attrs.get("title", "")
url = web_attrs.get("url", "")
description = web_attrs.get("description", "")
urlkey = web_attrs.get("urlkey", None)
account = web_attrs.get("account", None)
usrname = web_attrs.get("usrname", None)
psw = web_attrs.get("psw", None)
s_type = web_attrs.get("type", None)
iconsrc = web_attrs.get("iconsrc", None)
iconsrc_b = web_attrs.get("iconsrc_b", None)
group = web_attrs.get("group", None)
fav_id = NEUTRINO_FAV_ID_FORMAT.format(url, description, urlkey, account, usrname, psw, s_type, iconsrc,
iconsrc_b, group)
services.append(BouquetService(name=title, type=BqServiceType.IPTV, data=fav_id, num=0))
bouquet = Bouquet(name="default", type=bq_type, services=services, locked=None, hidden=None)
bouquet = Bouquet(name="default", type=bq_type, services=services, locked=None, hidden=None, file=None)
bouquets[2].append(bouquet)
return bouquets
@@ -107,38 +128,47 @@ def write_bouquets(path, bouquets):
def write_bouquet(file, bouquet):
doc = Document()
doc = NeutrinoDocument()
root = doc.createElement("zapit")
root.setAttribute("api", API_VER)
doc.appendChild(root)
comment = doc.createComment(_COMMENT)
doc.appendChild(comment)
for bq in bouquet.bouquets:
attrs = get_attributes(bq.file) if bq.file else {}
attrs["name"] = bq.name
if bq.hidden:
attrs["hidden"] = "1"
else:
attrs.pop("hidden", None)
if bq.locked:
attrs["locked"] = "1"
else:
attrs.pop("locked", None)
bq_elem = doc.createElement("Bouquet")
bq_elem.setAttribute("name", bq.name)
bq_elem.setAttribute("hidden", "1" if bq.hidden else "0")
bq_elem.setAttribute("locked", "1" if bq.locked else "0")
bq_elem.setAttribute("epg", "0")
for k, v in attrs.items():
bq_elem.setAttribute(k, v)
root.appendChild(bq_elem)
for srv in bq.services:
f_data = srv.flags_cas.split(":")
tr_id, on, ssid = srv.fav_id.split(":")
srv_elem = doc.createElement("S")
srv_elem.setAttribute("i", ssid)
srv_elem.setAttribute("n", srv.service)
srv_elem.setAttribute("t", tr_id)
srv_elem.setAttribute("on", on)
srv_elem.setAttribute("s", f_data[1])
srv_elem.setAttribute("frq", srv.freq)
srv_elem.setAttribute("l", "0") # temporary !!!
srv_elem.setAttribute("s", get_attributes(srv.flags_cas).get("position", "0"))
bq_elem.appendChild(srv_elem)
doc.writexml(open(file, "w"), addindent=" ", newl="\n", encoding="UTF-8")
doc.write_xml(file)
def write_webtv(file, bouquet):
doc = Document()
doc = NeutrinoDocument()
root = doc.createElement("webtvs")
doc.appendChild(root)
comment = doc.createComment(_COMMENT)
@@ -172,7 +202,7 @@ def write_webtv(file, bouquet):
root.appendChild(srv_elem)
doc.writexml(open(file, "w"), addindent=" ", newl="\n", encoding="UTF-8")
doc.write_xml(file)
if __name__ == "__main__":

View File

@@ -0,0 +1,114 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" Additional module for working with Neutrino xml files. """
import re
from xml.dom.minidom import parseString, Document, Element, Node
from xml.parsers.expat import ExpatError
from app.commons import log
class XmlHandler:
""" Utility class for handling Neutrino xml files. """
__slots__ = ()
ERROR_MESSAGE = "The file [{}] is not formatted correctly or contains invalid characters! Cause: {}"
@staticmethod
def parse(path):
""" Parses a file into the DOM by filename. """
try:
return parseString(open(path, "r", encoding="utf-8", errors="ignore").read())
except ExpatError as e:
# Some neutrino configuration files may contain text data with invalid character ['&'].
# https://www.w3.org/TR/xml/#syntax
# Apparently there is an error in Neutrino itself and the document is not initially formed correctly.
log(XmlHandler.ERROR_MESSAGE.format(path, e))
return XmlHandler.preprocess(path)
@staticmethod
def preprocess(path):
""" Pre-processing xml [for '&' symbol] for correct parsing. """
with open(path, "r", encoding="utf-8", errors="ignore") as f:
pat = re.compile("&([^;\\W]*([^;\\w]|$))")
log("Processing the file '{}'...".format(path))
try:
dom = parseString(re.sub(pat, "&amp;", f.read()))
except ExpatError as e:
msg = XmlHandler.ERROR_MESSAGE.format(path, e)
log(msg)
raise ValueError(e)
else:
log("Done!")
return dom
class NeutrinoDocument(Document):
def createElement(self, tag_name):
e = NElement(tag_name)
e.ownerDocument = self
return e
def write_xml(self, path):
self.writexml(open(path, "w", encoding="utf-8"), addindent=" ", newl="\n", encoding="UTF-8")
class NElement(Element):
def writexml(self, writer, indent="", add_indent="", new_line=""):
""" Overridden specifically for neutrino for more correct [&apos; -> optional] xml attrs generation. """
writer.write(indent + "<" + self.tagName)
attrs = self._get_attributes()
for a_name in attrs.keys():
writer.write(" %s=\"" % a_name)
self.write_data(writer, attrs[a_name].value)
writer.write("\"")
if self.childNodes:
writer.write(">")
if len(self.childNodes) == 1 and self.childNodes[0].nodeType in (Node.TEXT_NODE, Node.CDATA_SECTION_NODE):
self.childNodes[0].writexml(writer, '', '', '')
else:
writer.write(new_line)
for node in self.childNodes:
node.writexml(writer, indent + add_indent, add_indent, new_line)
writer.write(indent)
writer.write("</%s>%s" % (self.tagName, new_line))
else:
writer.write("/>%s" % new_line)
@staticmethod
def write_data(writer, data):
""" Writes data chars to writer."""
if data:
data = data.replace("&", "&amp;").replace("<", "&lt;").replace("\"", "&quot;").replace(">", "&gt;")
data = data.replace("'", "&apos;")
writer.write(data)

View File

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

View File

@@ -1,120 +1,195 @@
""" Module foe parsing Satellites.xml
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
For more info see __COMMENT
""" Module for working with *.xml files.
For more info see comments.
"""
from xml.dom.minidom import parse, Document
import xml.etree.ElementTree as ETree
from app.commons import log
from .ecommons import POLARIZATION, FEC, SYSTEM, MODULATION, Transponder, Satellite, get_key_by_value
from .ecommons import Satellite, Terrestrial, Cable, Transponder, TerTransponder, CableTransponder
__COMMENT = (" File was created in DemonEditor\n\n"
"usable flags are\n"
" 1: Network Scan\n"
" 2: use BAT\n"
" 4: use ONIT\n"
" 8: skip NITs of known networks\n"
" and combinations of this.\n\n"
_SAT_COMMENT = ("\tFile was created in DemonEditor.\n\n"
"Usable flags are:\n"
" 1: Network Scan\n"
" 2: use BAT\n"
" 4: use ONIT\n"
" 8: skip NITs of known networks\n"
" This is a bitmap and combinations can be used.\n\n"
"Transponder parameters:\n"
"\tpolarization: 0 - Horizontal, 1 - Vertical, 2 - Left Circular, 3 - Right Circular\n"
"\tfec_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"
"\t8 - 4/5, 9 - 9/10, 15 - None\n"
"\tmodulation: 0 - Auto, 1 - QPSK, 2 - 8PSK, 4 - 16APSK, 5 - 32APSK\n"
"\trolloff: 0 - 0.35, 1 - 0.25, 2 - 0.20, 3 - Auto\n"
"\tpilot: 0 - Off, 1 - On, 2 - Auto\n"
"\tinversion: 0 = Off, 1 = On, 2 = Auto (default)\n"
"\tsystem: 0 = DVB-S, 1 = DVB-S2\n"
"\tis_id: 0 - 255\n"
"\tpls_mode: 0 - Root, 1 - Gold, 2 - Combo\n"
"\tpls_code: 0 - 262142\n\n")
"transponder parameters:\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, 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"
"system: 0 = DVB-S, 1 = DVB-S2\n"
"is_id: 0 - 255\n"
"pls_mode: 0 - Root, 1 - Gold, 2 - Combo\n"
"pls_code: 0 - 262142\n\n")
_TERRESTRIAL_COMMENT = ("\tFile was created in DemonEditor.\n\n"
"Usable flags are:\n"
" 1: Network Scan\n"
" 2: use BAT\n"
" 4: use ONIT\n"
" 8: skip NITs of known networks\n"
" This is a bitmap and combinations can be used.\n\n")
_CABLE_COMMENT = ("\tFile was created in DemonEditor.\n\n"
"Transponder parameters:\n"
"\tmodulation:\n"
"\t3: QAM64\n"
"\t5: QAM256\n")
def get_satellites(path):
return parse_satellites(path)
""" Returns data [Satellite] list from *.xml. """
return [Satellite(e.get("name", None),
e.get("flags", None),
e.get("position", None) or "0",
get_sat_transponders(e)) for e in ETree.parse(path).iter("sat")]
def write_satellites(satellites, data_path):
""" Creation satellites.xml file """
doc = Document()
comment = doc.createComment(__COMMENT)
doc.appendChild(comment)
root = doc.createElement("satellites")
doc.appendChild(root)
for sat in satellites:
# Create Element
sat_child = doc.createElement("sat")
sat_child.setAttribute("name", sat.name)
sat_child.setAttribute("flags", sat.flags)
sat_child.setAttribute("position", sat.position)
for tr in sat.transponders:
transponder_child = doc.createElement("transponder")
transponder_child.setAttribute("frequency", tr.frequency)
transponder_child.setAttribute("symbol_rate", tr.symbol_rate)
transponder_child.setAttribute("polarization", get_key_by_value(POLARIZATION, tr.polarization))
transponder_child.setAttribute("fec_inner", get_key_by_value(FEC, tr.fec_inner) or "0")
transponder_child.setAttribute("system", get_key_by_value(SYSTEM, tr.system) or "0")
transponder_child.setAttribute("modulation", get_key_by_value(MODULATION, tr.modulation) or "0")
if tr.pls_mode:
transponder_child.setAttribute("pls_mode", tr.pls_mode)
if tr.pls_code:
transponder_child.setAttribute("pls_code", tr.pls_code)
if tr.is_id:
transponder_child.setAttribute("is_id", tr.is_id)
sat_child.appendChild(transponder_child)
root.appendChild(sat_child)
doc.writexml(open(data_path, "w"),
# indent="",
addindent=" ",
newl='\n',
encoding="iso-8859-1")
doc.unlink()
def get_sat_transponders(elem):
""" Returns satellite transponders list. """
return [Transponder(e.get("frequency", "0"),
e.get("symbol_rate", "0"),
e.get("polarization", None),
e.get("fec_inner", None),
e.get("system", None),
e.get("modulation", None),
e.get("pls_mode", None),
e.get("pls_code", None),
e.get("is_id", None),
e.get("t2mi_plp_id", None)) for e in elem.iter("transponder")]
def parse_transponders(elem, sat_name):
""" Parsing satellite transponders """
transponders = []
for el in elem.getElementsByTagName("transponder"):
if el.hasAttributes():
atr = el.attributes
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))
log(message)
else:
transponders.append(tr)
return transponders
def get_terrestrial(path):
""" Returns data [Terrestrial] list from *.xml. """
return [Terrestrial(e.get("name", None),
e.get("flags", None),
e.get("countrycode", None),
[get_ter_transponder(e) for e in e.iter("transponder")]
) for e in ETree.parse(path).iter("terrestrial")]
def parse_sat(elem):
""" Parsing satellite """
sat_name = elem.attributes["name"].value
return Satellite(sat_name,
elem.attributes["flags"].value,
elem.attributes["position"].value,
parse_transponders(elem, sat_name))
def get_ter_transponder(elem):
""" Returns terrestrial transponder. """
return TerTransponder(elem.get("centre_frequency", "0"),
elem.get("system", None),
elem.get("bandwidth", None),
elem.get("constellation", None),
elem.get("code_rate_hp", None),
elem.get("code_rate_lp", None),
elem.get("guard_interval", None),
elem.get("transmission_mode", None),
elem.get("hierarchy_information", None),
elem.get("inversion", None),
elem.get("plp_id", None))
def parse_satellites(path):
""" Parsing satellites from xml"""
dom = parse(path)
satellites = []
def get_cable(path):
""" Returns data [Cable] list from *.xml. """
return [Cable(e.get("name", None),
e.get("flags", None),
e.get("satfeed", None),
e.get("countrycode", None),
get_cable_transponders(e)) for e in ETree.parse(path).iter("cable")]
for elem in dom.getElementsByTagName("sat"):
if elem.hasAttributes():
satellites.append(parse_sat(elem))
return satellites
def get_cable_transponders(elem):
""" Returns cable transponders list. """
return [CableTransponder(e.get("frequency", "0"),
e.get("symbol_rate", "0"),
e.get("fec_inner", None),
e.get("modulation", None)) for e in elem.iter("transponder")]
def write_satellites(satellites, data_path, encoding="UTF-8"):
""" Creates satellites.xml file. """
write_xml("satellites", "sat", satellites, data_path, _SAT_COMMENT, encoding)
def write_terrestrial(terrestrial, data_path, encoding="UTF-8"):
""" Creates terrestrial.xml file. """
write_xml("locations", "terrestrial", terrestrial, data_path, _TERRESTRIAL_COMMENT, encoding)
def write_cable(cables, data_path, encoding="UTF-8"):
""" Creates cables.xml file. """
write_xml("cables", "cable", cables, data_path, _CABLE_COMMENT, encoding)
def write_xml(root_name, sub_name, data, data_path, comment="", encoding="UTF-8"):
""" Creates *.xml files. """
xml = ETree.Element(root_name)
[write_element(sub_name, "transponder", t, xml) for t in data]
tree = ETree.ElementTree(xml)
indent(tree.getroot())
with open(data_path, "wb") as f:
# To put comment on top.
f.write(f'<?xml version="1.0" encoding="{encoding}"?>\n<!--\n{comment}-->\n\n'.encode("utf-8"))
tree.write(f, encoding=encoding)
def write_element(e_name, ch_name, e_data, root):
""" Writes element with sub elements.
@param e_name: Element name.
@param ch_name: Child element name.
@param e_data: Element data -> defaultdict
@param root: Parent of the element.
"""
t = e_data._asdict()
subs = t.pop("transponders")
root_sub = ETree.SubElement(root, e_name, {k: v for k, v in t.items() if v})
[ETree.SubElement(root_sub, ch_name, {k: v for k, v in tr._asdict().items() if v}) for tr in subs]
def indent(elem, parent=None, index=-1, level=0, space=" "):
""" Appends whitespace to the subtree to indent the tree visually.
Since the minimum supported version < 3.9, we will use our own implementation.
"""
for i, sub in enumerate(elem):
indent(sub, elem, i, level + 1)
if parent:
if index == 0:
parent.text = f"\n{space * level}"
else:
parent[index - 1].tail = f"\n{space * level}"
if index == len(parent) - 1:
elem.tail = f"\n{space * (level - 1)}"
if __name__ == "__main__":

View File

@@ -1,3 +1,31 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
import copy
import json
import locale
@@ -5,27 +33,56 @@ import os
import sys
from enum import Enum, IntEnum
from functools import lru_cache
from pathlib import Path
from pprint import pformat
from textwrap import dedent
SEP = os.sep
HOME_PATH = os.path.expanduser("~")
CONFIG_PATH = HOME_PATH + "{}.config{}demon-editor{}".format(SEP, SEP, SEP)
HOME_PATH = str(Path.home())
CONFIG_PATH = HOME_PATH + f"{SEP}.config{SEP}demon-editor{SEP}"
CONFIG_FILE = CONFIG_PATH + "config.json"
DATA_PATH = HOME_PATH + "{}DemonEditor{}data{}".format(SEP, SEP, SEP)
DATA_PATH = HOME_PATH + f"{SEP}DemonEditor{SEP}"
GTK_PATH = os.environ.get("GTK_PATH", None)
IS_DARWIN = sys.platform == "darwin"
IS_WIN = sys.platform == "win32"
IS_LINUX = sys.platform == "linux"
class Defaults(Enum):
""" Default program settings """
USER = "root"
PASSWORD = ""
HOST = "127.0.0.1"
FTP_PORT = "21"
HTTP_PORT = "80"
TELNET_PORT = "23"
HTTP_USE_SSL = False
# Enigma2.
BOX_SERVICES_PATH = "/etc/enigma2/"
BOX_SATELLITE_PATH = "/etc/tuxbox/"
BOX_EPG_PATH = "/etc/enigma2/"
BOX_PICON_PATH = "/usr/share/enigma2/picon/"
BOX_PICON_PATHS = ("/usr/share/enigma2/picon/",
"/media/hdd/picon/",
"/media/usb/picon/",
"/media/mmc/picon/",
"/media/cf/picon/")
# Neutrino.
NEUTRINO_BOX_SERVICES_PATH = "/var/tuxbox/config/zapit/"
NEUTRINO_BOX_SATELLITE_PATH = "/var/tuxbox/config/"
NEUTRINO_BOX_PICON_PATH = "/usr/share/tuxbox/neutrino/icons/logo/"
NEUTRINO_BOX_PICON_PATHS = ("/usr/share/tuxbox/neutrino/icons/logo/",)
# Paths.
BACKUP_PATH = f"{DATA_PATH}backup{SEP}"
PICON_PATH = f"{DATA_PATH}picons{SEP}"
DEFAULT_PROFILE = "default"
BACKUP_BEFORE_DOWNLOADING = True
BACKUP_BEFORE_SAVE = True
V5_SUPPORT = False
FORCE_BQ_NAMES = False
HTTP_API_SUPPORT = IS_WIN
HTTP_API_SUPPORT = True
ENABLE_YT_DL = False
ENABLE_SEND_TO = False
USE_COLORS = True
@@ -36,62 +93,11 @@ class Defaults(Enum):
FAV_CLICK_MODE = 0
PLAY_STREAMS_MODE = 1 if IS_DARWIN else 0
STREAM_LIB = "mpv" if IS_WIN else "vlc"
MAIN_LIST_PLAYBACK = False
PROFILE_FOLDER_DEFAULT = False
RECORDS_PATH = DATA_PATH + "records{}".format(SEP)
RECORDINGS_PATH = f"{DATA_PATH}recordings{SEP}"
ACTIVATE_TRANSCODING = False
ACTIVE_TRANSCODING_PRESET = "720p TV{}device".format(SEP)
def get_settings():
if not os.path.isfile(CONFIG_FILE) or os.stat(CONFIG_FILE).st_size == 0:
write_settings(get_default_settings())
with open(CONFIG_FILE, "r", encoding="utf-8") as config_file:
return json.load(config_file)
def get_default_settings(profile_name="default"):
def_settings = SettingsType.ENIGMA_2.get_default_settings()
set_local_paths(def_settings, profile_name)
return {
"version": 1,
"default_profile": Defaults.DEFAULT_PROFILE.value,
"profiles": {profile_name: def_settings},
"v5_support": Defaults.V5_SUPPORT.value,
"http_api_support": Defaults.HTTP_API_SUPPORT.value,
"enable_yt_dl": Defaults.ENABLE_YT_DL.value,
"enable_send_to": Defaults.ENABLE_SEND_TO.value,
"use_colors": Defaults.USE_COLORS.value,
"new_color": Defaults.NEW_COLOR.value,
"extra_color": Defaults.EXTRA_COLOR.value,
"fav_click_mode": Defaults.FAV_CLICK_MODE.value,
"profile_folder_is_default": Defaults.PROFILE_FOLDER_DEFAULT.value,
"records_path": Defaults.RECORDS_PATH.value
}
def get_default_transcoding_presets():
return {"720p TV/device": {"vcodec": "h264", "vb": "1500", "width": "1280", "height": "720", "acodec": "mp3",
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"},
"1080p TV/device": {"vcodec": "h264", "vb": "3500", "width": "1920", "height": "1080", "acodec": "mp3",
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"}}
def write_settings(config):
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
with open(CONFIG_FILE, "w", encoding="utf-8") as config_file:
json.dump(config, config_file, indent=" ")
def set_local_paths(settings, profile_name, data_path=DATA_PATH, use_profile_folder=False):
settings["data_local_path"] = "{}{}{}".format(data_path, profile_name, SEP)
if use_profile_folder:
settings["picons_local_path"] = "{}{}{}{}{}".format(data_path, profile_name, SEP, "picons", SEP)
settings["backup_local_path"] = "{}{}{}{}{}".format(data_path, profile_name, SEP, "backup", SEP)
else:
settings["picons_local_path"] = "{}{}{}{}{}".format(data_path, "picons", SEP, profile_name, SEP)
settings["backup_local_path"] = "{}{}{}{}{}".format(data_path, "backup", SEP, profile_name, SEP)
ACTIVE_TRANSCODING_PRESET = f"720p TV{SEP}device"
class SettingsType(IntEnum):
@@ -100,30 +106,38 @@ class SettingsType(IntEnum):
NEUTRINO_MP = 1
def get_default_settings(self):
""" Returns default settings for current type """
if self is SettingsType.ENIGMA_2:
return {"setting_type": self.value,
"host": "127.0.0.1", "port": "21", "timeout": 5,
"user": "root", "password": "root",
"http_port": "80", "http_timeout": 5, "http_use_ssl": False,
"telnet_port": "23", "telnet_timeout": 5,
"services_path": "/etc/enigma2/", "user_bouquet_path": "/etc/enigma2/",
"satellites_xml_path": "/etc/tuxbox/", "data_local_path": "{}enigma2{}".format(DATA_PATH, SEP),
"picons_path": "/usr/share/enigma2/picon/",
"picons_local_path": "{}enigma2{}picons{}".format(DATA_PATH, SEP, SEP),
"backup_local_path": "{}enigma2{}backup{}".format(DATA_PATH, SEP, SEP)}
elif self is SettingsType.NEUTRINO_MP:
return {"setting_type": self,
"host": "127.0.0.1", "port": "21", "timeout": 5,
"user": "root", "password": "root",
"http_port": "80", "http_timeout": 2, "http_use_ssl": False,
"telnet_port": "23", "telnet_timeout": 1,
"services_path": "/var/tuxbox/config/zapit/", "user_bouquet_path": "/var/tuxbox/config/zapit/",
"satellites_xml_path": "/var/tuxbox/config/",
"data_local_path": "{}neutrino{}".format(DATA_PATH, SEP),
"picons_path": "/usr/share/tuxbox/neutrino/icons/logo/",
"picons_local_path": "{}neutrino{}picons{}".format(DATA_PATH, SEP, SEP),
"backup_local_path": "{}neutrino{}backup{}".format(DATA_PATH, SEP, SEP)}
""" Returns default settings for current type. """
if self is self.ENIGMA_2:
srv_path = Defaults.BOX_SERVICES_PATH.value
sat_path = Defaults.BOX_SATELLITE_PATH.value
picons_path = Defaults.BOX_PICON_PATH.value
epg_path = Defaults.BOX_EPG_PATH.value
http_timeout = 5
telnet_timeout = 5
else:
srv_path = Defaults.NEUTRINO_BOX_SERVICES_PATH.value
sat_path = Defaults.NEUTRINO_BOX_SATELLITE_PATH.value
picons_path = Defaults.NEUTRINO_BOX_PICON_PATH.value
epg_path = ""
http_timeout = 2
telnet_timeout = 1
return {"setting_type": self.value,
"host": Defaults.HOST.value,
"port": Defaults.FTP_PORT.value,
"timeout": 5,
"user": Defaults.USER.value,
"password": Defaults.PASSWORD.value,
"http_port": Defaults.HTTP_PORT.value,
"http_timeout": http_timeout,
"http_use_ssl": Defaults.HTTP_USE_SSL.value,
"telnet_port": Defaults.TELNET_PORT.value,
"telnet_timeout": telnet_timeout,
"services_path": srv_path,
"user_bouquet_path": srv_path,
"satellites_xml_path": sat_path,
"epg_dat_path": epg_path,
"picons_path": picons_path}
class SettingsException(Exception):
@@ -141,13 +155,19 @@ class PlayStreamsMode(IntEnum):
M3U = 2
class EpgSource(IntEnum):
HTTP = 0 # HTTP API -> WebIf
DAT = 1 # epg.dat file
XML = 2 # XML TV
class Settings:
__INSTANCE = None
__VERSION = 1
__VERSION = 2
def __init__(self, ext_settings=None):
try:
settings = ext_settings or get_settings()
settings = ext_settings or self.get_settings()
except PermissionError as e:
raise SettingsReadException(e)
@@ -178,22 +198,18 @@ class Settings:
return cls.__INSTANCE
def save(self):
write_settings(self._settings)
self.write_settings(self._settings)
def reset(self, force_write=False):
for k, v in self.setting_type.get_default_settings().items():
self._cp_settings[k] = v
def_path = self.default_data_path
def_path += "enigma2{}".format(SEP) if self.setting_type is SettingsType.ENIGMA_2 else "neutrino{}".format(SEP)
set_local_paths(self._cp_settings, self._current_profile, def_path, self.profile_folder_is_default)
if force_write:
self.save()
@staticmethod
def reset_to_default():
write_settings(get_default_settings())
Settings.write_settings(Settings.get_default_settings())
def get_default(self, p_name):
""" Returns default value for current settings type """
@@ -203,9 +219,9 @@ class Settings:
""" Adds extra options """
self._settings[name] = value
def get(self, name):
def get(self, name, default=None):
""" Returns extra options or None """
return self._settings.get(name, None)
return self._settings.get(name, default)
@property
def settings(self):
@@ -234,6 +250,10 @@ class Settings:
def default_profile(self, value):
self._settings["default_profile"] = value
@property
def current_profile_settings(self):
return self._cp_settings
@property
def profiles(self):
return self._profiles
@@ -261,6 +281,14 @@ class Settings:
def host(self, value):
self._cp_settings["host"] = value
@property
def hosts(self):
return self._cp_settings.get("hosts", [self.host, ])
@hosts.setter
def hosts(self, value):
self._cp_settings["hosts"] = value
@property
def port(self):
return self._cp_settings.get("port", self.get_default("port"))
@@ -349,6 +377,14 @@ class Settings:
def satellites_xml_path(self, value):
self._cp_settings["satellites_xml_path"] = value
@property
def epg_dat_path(self):
return self._cp_settings.get("epg_dat_path", self.get_default("epg_dat_path"))
@epg_dat_path.setter
def epg_dat_path(self, value):
self._cp_settings["epg_dat_path"] = value
@property
def picons_path(self):
return self._cp_settings.get("picons_path", self.get_default("picons_path"))
@@ -357,6 +393,20 @@ class Settings:
def picons_path(self, value):
self._cp_settings["picons_path"] = value
@property
def picons_paths(self):
if self.setting_type is SettingsType.NEUTRINO_MP:
return self._settings.get("neutrino_picon_paths", Defaults.NEUTRINO_BOX_PICON_PATHS.value)
else:
return self._settings.get("picon_paths", Defaults.BOX_PICON_PATHS.value)
@picons_paths.setter
def picons_paths(self, value):
if self.setting_type is SettingsType.NEUTRINO_MP:
self._settings["neutrino_picon_paths"] = value
else:
self._settings["picon_paths"] = value
# ***** Local paths ***** #
@property
@@ -376,36 +426,56 @@ class Settings:
self._settings["default_data_path"] = value
@property
def data_local_path(self):
return self._cp_settings.get("data_local_path", self.get_default("data_local_path"))
def default_backup_path(self):
return self._settings.get("default_backup_path", Defaults.BACKUP_PATH.value)
@data_local_path.setter
def data_local_path(self, value):
self._cp_settings["data_local_path"] = value
@default_backup_path.setter
def default_backup_path(self, value):
self._settings["default_backup_path"] = value
@property
def picons_local_path(self):
return self._cp_settings.get("picons_local_path", self.get_default("picons_local_path"))
def default_picon_path(self):
return self._settings.get("default_picon_path", Defaults.PICON_PATH.value)
@picons_local_path.setter
def picons_local_path(self, value):
self._cp_settings["picons_local_path"] = value
@default_picon_path.setter
def default_picon_path(self, value):
self._settings["default_picon_path"] = value
@property
def backup_local_path(self):
return self._cp_settings.get("backup_local_path", self.get_default("backup_local_path"))
def profile_data_path(self):
return f"{self.default_data_path}data{SEP}{self._current_profile}{SEP}"
@backup_local_path.setter
def backup_local_path(self, value):
self._cp_settings["backup_local_path"] = value
@profile_data_path.setter
def profile_data_path(self, value):
self._cp_settings["profile_data_path"] = value
@property
def records_path(self):
return self._settings.get("records_path", Defaults.RECORDS_PATH.value)
def profile_picons_path(self):
if self.profile_folder_is_default:
return f"{self.profile_data_path}picons{SEP}"
return f"{self.default_picon_path}{self._current_profile}{SEP}"
@records_path.setter
def records_path(self, value):
self._settings["records_path"] = value
@profile_picons_path.setter
def profile_picons_path(self, value):
self._cp_settings["profile_picons_path"] = value
@property
def profile_backup_path(self):
if self.profile_folder_is_default:
return f"{self.profile_data_path}backup{SEP}"
return f"{self.default_backup_path}{self._current_profile}{SEP}"
@profile_backup_path.setter
def profile_backup_path(self, value):
self._cp_settings["profile_backup_path"] = value
@property
def recordings_path(self):
return self._settings.get("recordings_path", Defaults.RECORDINGS_PATH.value)
@recordings_path.setter
def recordings_path(self, value):
self._settings["recordings_path"] = value
# ******** Streaming ********* #
@@ -427,7 +497,7 @@ class Settings:
@property
def transcoding_presets(self):
return self._settings.get("transcoding_presets", get_default_transcoding_presets())
return self._settings.get("transcoding_presets", self.get_default_transcoding_presets())
@transcoding_presets.setter
def transcoding_presets(self, value):
@@ -449,6 +519,22 @@ class Settings:
def stream_lib(self, value):
self._settings["stream_lib"] = value
@property
def fav_click_mode(self):
return self._settings.get("fav_click_mode", Defaults.FAV_CLICK_MODE.value)
@fav_click_mode.setter
def fav_click_mode(self, value):
self._settings["fav_click_mode"] = value
@property
def main_list_playback(self):
return self._settings.get("main_list_playback", Defaults.MAIN_LIST_PLAYBACK.value)
@main_list_playback.setter
def main_list_playback(self, value):
self._settings["main_list_playback"] = value
# *********** EPG ************ #
@property
@@ -460,6 +546,40 @@ class Settings:
def epg_options(self, value):
self._cp_settings["epg_options"] = value
@property
def epg_source(self):
return EpgSource(self._cp_settings.get("epg_source", EpgSource.HTTP))
@epg_source.setter
def epg_source(self, value):
self._cp_settings["epg_source"] = value
@property
def epg_update_interval(self):
return self._cp_settings.get("epg_update_interval", 5)
@epg_update_interval.setter
def epg_update_interval(self, value):
self._cp_settings["epg_update_interval"] = value
@property
def epg_xml_source(self):
return self._cp_settings.get("epg_xml_source", "")
@epg_xml_source.setter
def epg_xml_source(self, value):
self._cp_settings["epg_xml_source"] = value
# *********** FTP ************ #
@property
def ftp_bookmarks(self):
return self._cp_settings.get("ftp_bookmarks", [])
@ftp_bookmarks.setter
def ftp_bookmarks(self, value):
self._cp_settings["ftp_bookmarks"] = value
# ***** Program settings ***** #
@property
@@ -526,14 +646,6 @@ class Settings:
def enable_send_to(self, value):
self._settings["enable_send_to"] = value
@property
def fav_click_mode(self):
return self._settings.get("fav_click_mode", Defaults.FAV_CLICK_MODE.value)
@fav_click_mode.setter
def fav_click_mode(self, value):
self._settings["fav_click_mode"] = value
@property
def language(self):
return self._settings.get("language", locale.getlocale()[0] or "en_US")
@@ -620,12 +732,35 @@ class Settings:
@property
def dark_mode(self):
if IS_DARWIN:
import subprocess
cmd = ["defaults", "read", "-g", "AppleInterfaceStyle"]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
return "Dark" in str(p[0])
return self._settings.get("dark_mode", False)
@dark_mode.setter
def dark_mode(self, value):
self._settings["dark_mode"] = value
@property
def display_picons(self):
return self._settings.get("display_picons", True)
@display_picons.setter
def display_picons(self, value):
self._settings["display_picons"] = value
@property
def display_epg(self):
return self._settings.get("display_epg", False)
@display_epg.setter
def display_epg(self, value):
self._settings["display_epg"] = value
@property
def alternate_layout(self):
return self._settings.get("alternate_layout", IS_DARWIN)
@@ -661,7 +796,7 @@ class Settings:
@property
@lru_cache(1)
def themes_path(self):
return "{}{}.themes{}".format(HOME_PATH, SEP, SEP)
return f"{HOME_PATH}{SEP}.themes{SEP}"
@property
def icon_theme(self):
@@ -674,13 +809,13 @@ class Settings:
@property
@lru_cache(1)
def icon_themes_path(self):
return "{}{}.icons{}".format(HOME_PATH, SEP, SEP)
return f"{HOME_PATH}{SEP}.icons{SEP}"
@property
def is_darwin(self):
return IS_DARWIN
# *********** Download dialog *********** #
# ************* Download ************** #
@property
def use_http(self):
@@ -698,6 +833,22 @@ class Settings:
def remove_unused_bouquets(self, value):
self._settings["remove_unused_bouquets"] = value
@property
def keep_power_mode(self):
return self._settings.get("keep_power_mode", False)
@keep_power_mode.setter
def keep_power_mode(self, value):
self._settings["keep_power_mode"] = value
@property
def compress_picons(self):
return self._settings.get("compress_picons", False)
@compress_picons.setter
def compress_picons(self, value):
self._settings["compress_picons"] = value
# **************** Debug **************** #
@property
@@ -719,6 +870,52 @@ class Settings:
def is_enable_experimental(self, value):
self._settings["enable_experimental"] = value
# **************** Get-Set settings **************** #
@staticmethod
def get_settings():
if not os.path.isfile(CONFIG_FILE) or os.stat(CONFIG_FILE).st_size == 0:
Settings.write_settings(Settings.get_default_settings())
with open(CONFIG_FILE, "r", encoding="utf-8") as config_file:
try:
return json.load(config_file)
except ValueError as e:
raise SettingsReadException(e)
@staticmethod
def get_default_settings(profile_name="default"):
def_settings = SettingsType.ENIGMA_2.get_default_settings()
return {
"version": Settings.__VERSION,
"default_profile": Defaults.DEFAULT_PROFILE.value,
"profiles": {profile_name: def_settings},
"v5_support": Defaults.V5_SUPPORT.value,
"http_api_support": Defaults.HTTP_API_SUPPORT.value,
"enable_yt_dl": Defaults.ENABLE_YT_DL.value,
"enable_send_to": Defaults.ENABLE_SEND_TO.value,
"use_colors": Defaults.USE_COLORS.value,
"new_color": Defaults.NEW_COLOR.value,
"extra_color": Defaults.EXTRA_COLOR.value,
"fav_click_mode": Defaults.FAV_CLICK_MODE.value,
"profile_folder_is_default": Defaults.PROFILE_FOLDER_DEFAULT.value,
"records_path": Defaults.RECORDINGS_PATH.value
}
@staticmethod
def get_default_transcoding_presets():
return {"720p TV/device": {"vcodec": "h264", "vb": "1500", "width": "1280", "height": "720", "acodec": "mp3",
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"},
"1080p TV/device": {"vcodec": "h264", "vb": "3500", "width": "1920", "height": "1080", "acodec": "mp3",
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"}}
@staticmethod
def write_settings(config):
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
with open(CONFIG_FILE, "w", encoding="utf-8") as config_file:
json.dump(config, config_file, indent=" ")
if __name__ == "__main__":
pass

View File

@@ -1,45 +1,367 @@
""" Module for working with epg.dat file """
import struct
from datetime import datetime
from xml.dom.minidom import parse, Node, Document
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" Module for working with epg.dat file. """
import abc
import os
import shutil
import struct
import sys
from collections import namedtuple
from datetime import datetime, timezone
from tempfile import NamedTemporaryFile
from urllib.parse import urlparse
from xml.dom.minidom import parse, Node, Document
import xml.etree.ElementTree as ET
import requests
from app.commons import log
from app.eparser.ecommons import BqServiceType, BouquetService
from app.settings import IS_WIN
ENCODING = "utf-8"
DETECT_ENCODING = False
try:
import chardet
except ModuleNotFoundError:
pass
else:
DETECT_ENCODING = True
EpgEvent = namedtuple("EpgEvent", ["service_name", "title", "time", "desc", "event_data"])
EpgEvent.__new__.__defaults__ = ("N/A", "N/A", "N/A", "N/A", None) # For Python3 < 3.7
class Reader(metaclass=abc.ABCMeta):
@abc.abstractmethod
def download(self, clb=None): pass
@abc.abstractmethod
def get_current_events(self, ids: set) -> dict: pass
class EPG:
""" Base EPG class. """
# DVB/EPG count days with a 'modified Julian calendar' where day 1 is 17 November 1858.
# datetime.datetime.toordinal(1858,11,17) => 678576
ZERO_DAY = 678576
@staticmethod
def get_epg_refs(path):
""" The read algorithm was taken from the eEPGCache::load() function from this source:
Event = namedtuple("EpgEvent", ["id", "data", "start", "duration", "title", "desc", "ext_desc"])
class EventData:
""" Event data representation class. """
__slots__ = ["raw_data", "crc", "size", "type"]
def __init__(self, size=0, e_type=0):
self.raw_data = None
self.crc = None
self.size = size
self.type = e_type
def get_event_id(self):
return self.raw_data[0] << 8 | self.raw_data[1]
def get_start_time(self):
""" Returns start time [sec.]. """
# Date
start_date = datetime.fromordinal((self.raw_data[2] << 8 | self.raw_data[3]) + EPG.ZERO_DAY).timestamp()
# Time
tm_hour = EPG.get_from_bcd(self.raw_data[4])
tm_min = EPG.get_from_bcd(self.raw_data[5])
tm_sec = EPG.get_from_bcd(self.raw_data[6])
# UTC.
s_time = start_date + tm_hour * 3600 + tm_min * 60 + tm_sec
# Time zone correction.
s_time += datetime.now(timezone.utc).astimezone().utcoffset().seconds
return s_time
def get_duration(self):
""" Returns duration [sec.]."""
return EPG.get_from_bcd(self.raw_data[7]) * 3600 + EPG.get_from_bcd(
self.raw_data[8]) * 60 + EPG.get_from_bcd(self.raw_data[9])
class DatReader(Reader):
""" The epd.dat file reading class.
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!")
def __init__(self, path):
self._path = path
self._refs = {}
self._desc = {}
header = f.read(13).decode()
if header != "ENIGMA_EPG_V7":
raise ValueError("Unsupported format of epd.dat file!")
def download(self, clb=None):
pass
channels_count = struct.unpack("<I", f.read(4))[0]
def get_current_events(self, ids: set) -> dict:
pass
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)
def get_refs(self):
return self._refs.keys()
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)]
def get_services(self):
return self._refs
refs.add(service_id)
def get_event(self, evd):
title, desc, ext_desc = None, None, None
e_id, start, duration = evd.get_event_id(), evd.get_start_time(), evd.get_duration()
return refs
for c in evd.crc:
data = self._desc.get(c, None)
if not data:
continue
encoding = ENCODING
if DETECT_ENCODING:
# May be slow.
encoding = chardet.detect(data).get("encoding", "utf-8") or encoding
desc_type = data[0]
if desc_type == 77: # Short event descriptor -> 0x4d -> 77
size = data[6]
txt = data[7:-1].decode(encoding, errors="ignore")
t_len = len(txt)
st = 0
if size and size < t_len:
st = abs(size - t_len)
if size < 32:
title = txt
else:
desc = txt[st:]
elif desc_type == 78: # Extended event descriptor -> 0x4e -> 78
ext_desc = data[9:].decode(encoding, errors="ignore") if data[7] and data[8] < 32 else None
return EPG.Event(e_id, evd, start, duration, title, desc, ext_desc)
def get_events(self, ref):
return self._refs.get(ref, {})
def read(self):
with open(self._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":
epg_ver = 7
elif header == "ENIGMA_EPG_V8":
epg_ver = 8
else:
raise ValueError("Unsupported format of epd.dat file!")
channels_count = struct.unpack("I", f.read(4))[0]
_len_read_size = 3 if epg_ver == 8 else 2
_type_read_str = f"{'H' if epg_ver == 8 else 'B'}B"
for i in range(channels_count):
sid, nid, tsid, events_size = struct.unpack("IIII", f.read(16))
service_id = f"{sid:X}:{tsid:X}:{nid:X}"
events = {}
for j in range(events_size):
_type, _len = struct.unpack(_type_read_str, f.read(_len_read_size))
event = EPG.EventData(size=_len, e_type=_type)
event.raw_data = f.read(10)
n_crc = (_len - 10) // 4
if n_crc > 0:
event.crc = [struct.unpack("I", f.read(4))[0] for n in range(n_crc)]
events[event.get_event_id()] = event
self._refs[service_id] = events
for i in range(struct.unpack("I", f.read(4))[0]):
_id, ref_count = struct.unpack("II", f.read(8))
header = struct.unpack("BB", f.read(2))
_bytes = header[1] + 2
f.seek(-2, os.SEEK_CUR)
self._desc[_id] = f.read(_bytes)
@staticmethod
def get_from_bcd(value: int):
""" Converts a BCD to an integer. """
if ((value & 0xF0) >= 0xA0) or ((value & 0xF) >= 0xA):
return -1
return ((value & 0xF0) >> 4) * 10 + (value & 0xF)
class XmlTvReader(Reader):
PR_TAG = "programme"
CH_TAG = "channel"
DSP_NAME_TAG = "display-name"
ICON_TAG = "icon"
TITLE_TAG = "title"
DESC_TAG = "desc"
TIME_FORMAT_STR = "%Y%m%d%H%M%S %z"
Service = namedtuple("Service", ["id", "names", "logo", "events"])
Event = namedtuple("EpgEvent", ["start", "duration", "title", "desc"])
def __init__(self, path, url):
self._path = path
self._url = url
self._ids = {}
def download(self, clb=None):
""" Downloads an XMLTV file. """
res = urlparse(self._url)
if not all((res.scheme, res.netloc)):
log(f"{self.__class__.__name__} [download] error: Invalid URL {self._url}")
return
with requests.get(url=self._url, stream=True) as request:
if request.reason == "OK":
suf = self._url[self._url.rfind("."):]
if suf not in (".gz", ".xz", ".lzma"):
log(f"{self.__class__.__name__} [download] error: Unsupported file extension.")
return
data_len = request.headers.get("content-length")
with NamedTemporaryFile(suffix=suf, delete=not IS_WIN) as tf:
downloaded = 0
data_len = int(data_len)
log("Downloading XMLTV file...")
for data in request.iter_content(chunk_size=1024):
downloaded += len(data)
tf.write(data)
done = int(50 * downloaded / data_len)
sys.stdout.write(f"\rDownloading XMLTV file [{'=' * done}{' ' * (50 - done)}]")
sys.stdout.flush()
tf.seek(0)
sys.stdout.write("\n")
os.makedirs(os.path.dirname(self._path), exist_ok=True)
if suf.endswith(".gz"):
try:
shutil.copyfile(tf.name, self._path)
except OSError as e:
log(f"{self.__class__.__name__} [download *.gz] error: {e}")
elif self._url.endswith((".xz", ".lzma")):
import lzma
try:
with lzma.open(tf, "rb") as lzf:
shutil.copyfileobj(lzf, self._path)
except (lzma.LZMAError, OSError) as e:
log(f"{self.__class__.__name__} [download *.xz] error: {e}")
if IS_WIN and os.path.isfile(tf.name):
tf.close()
os.remove(tf.name)
else:
log(f"{self.__class__.__name__} [download] error: {request.reason}")
if clb:
clb()
def get_current_events(self, names: set) -> dict:
events = {}
dt = datetime.utcnow()
utc = dt.timestamp()
offset = datetime.now() - dt
for srv in filter(lambda s: any(name in names for name in s.names), self._ids.values()):
ev = list(filter(lambda s: s.start < utc, srv.events))
if ev:
ev = ev[-1]
start = datetime.fromtimestamp(ev.start) + offset
end_time = datetime.fromtimestamp(ev.duration) + offset
tm = f"{start.strftime('%H:%M')} - {end_time.strftime('%H:%M')}"
for n in srv.names:
events[n] = EpgEvent(n, ev.title, tm, ev.desc, ev)
return events
def parse(self):
""" Parses XML. """
try:
import gzip
with gzip.open(self._path, "rb") as gzf:
log("Processing XMLTV data...")
list(map(self.process_node, ET.iterparse(gzf)))
log("XMLTV data parsing is complete.")
except OSError as e:
log(f"{self.__class__.__name__} [parse] error: {e}")
def process_node(self, node):
event, element = node
if element.tag == self.CH_TAG:
ch_id = element.get("id", None)
logo = None # Currently not in use.
# Since a service can have several names, we will store a set of names in the "names" field!
self._ids[ch_id] = self.Service(ch_id, {c.text for c in element if c.tag == self.DSP_NAME_TAG}, logo, [])
elif element.tag == self.PR_TAG:
channel = self._ids.get(element.get(self.CH_TAG, None), None)
if channel:
events = channel[-1]
start = element.get("start", None)
if start:
start = self.get_utc_time(start)
stop = element.get("stop", None)
if stop:
stop = self.get_utc_time(stop)
title, desc = None, None
for c in element:
if c.tag == self.TITLE_TAG:
title = c.text
elif c.tag == self.DESC_TAG:
desc = c.text
if all((start, stop, title)):
events.append(self.Event(start, stop, title, desc))
def to_epg_dat(self):
""" Converts and saves imported data to 'epg.dat' file. """
raise ValueError("Not implemented yet!")
@staticmethod
def get_utc_time(time_str):
""" Returns the UTC time in seconds. """
t, sep, delta = time_str.partition(" ")
t = datetime(*map(int, (t[:4], t[4:6], t[6:8], t[8:10], t[10:12], t[12:]))).timestamp()
if delta:
t -= (3600 * int(delta) // 100)
return t
class ChannelsParser:
@@ -90,14 +412,14 @@ class ChannelsParser:
srv_type = srv.type
if srv_type is BqServiceType.IPTV:
channel_child = doc.createElement("channel")
channel_child.setAttribute("id", str(srv.num))
channel_child.setAttribute("id", srv.name)
data = srv.data.strip().split(":")
channel_child.appendChild(doc.createTextNode(":".join(data[:10])))
comment = doc.createComment(srv.name)
lines.append("{} {}\n".format(str(channel_child.toxml()), str(comment.toxml())))
lines.append(f"{channel_child.toxml()} {comment.toxml()}\n")
elif srv_type is BqServiceType.MARKER:
comment = doc.createComment(srv.name)
lines.append("{}\n".format(str(comment.toxml())))
lines.append(f"{comment.toxml()}\n")
lines.append("</channels>")
doc.unlink()

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -28,86 +28,115 @@
import os
import sys
from abc import ABC, abstractmethod
from datetime import datetime
from gi.repository import Gdk, Gtk
from gi.repository import Gdk, Gtk, GObject
from app.commons import run_task, log, _DATE_FORMAT, run_with_delay
from app.commons import run_task, log, LOG_DATE_FORMAT, run_with_delay
from app.settings import IS_DARWIN, IS_LINUX, IS_WIN
class Player(ABC):
class Player(Gtk.DrawingArea):
""" Base player class. Also used as a factory. """
@abstractmethod
def __init__(self, mode, widget, **kwargs):
super().__init__(**kwargs)
GObject.signal_new("error", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("message", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("position", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("played", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("audio-track", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("subtitle-track", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
self.connect("draw", self.on_draw)
self.connect("motion-notify-event", self.on_mouse_motion)
self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)
widget.add(self)
parent = widget.get_parent()
parent.connect("play", self.on_play)
parent.connect("stop", self.on_stop)
self.show()
def get_play_mode(self):
pass
@abstractmethod
def play(self, mrl=None):
pass
@abstractmethod
def stop(self):
pass
@abstractmethod
def pause(self):
pass
@abstractmethod
def set_time(self, time):
pass
@abstractmethod
def release(self):
pass
@abstractmethod
def is_playing(self):
pass
@abstractmethod
def get_instance(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
def set_audio_track(self, track):
pass
def get_window_handle(self, widget):
def get_audio_track(self):
pass
def set_subtitle_track(self, track):
pass
def set_aspect_ratio(self, ratio):
pass
def get_instance(self, mode, widget):
pass
def on_play(self, widget, url):
self.play(url)
def on_stop(self, widget, state):
self.stop()
def on_release(self, widget, state):
self.release()
def get_window_handle(self):
""" Returns the identifier [pointer] for the window.
Based on gtkvlc.py[get_window_pointer] example from here:
https://github.com/oaubert/python-vlc/tree/master/examples
"""
if sys.platform == "linux":
return widget.get_window().get_xid()
if IS_LINUX:
return self.get_window().get_xid()
else:
is_darwin = sys.platform == "darwin"
try:
import ctypes
libgdk = ctypes.CDLL("libgdk-3.0.dylib" if is_darwin else "libgdk-3-0.dll")
libgdk = ctypes.CDLL("libgdk-3.0.dylib" if IS_DARWIN else "libgdk-3-0.dll")
except OSError as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
log(f"{__class__.__name__}: Load library error: {e}")
else:
# https://gitlab.gnome.org/GNOME/pygobject/-/issues/112
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object]
gpointer = ctypes.pythonapi.PyCapsule_GetPointer(widget.get_window().__gpointer__, None)
get_pointer = libgdk.gdk_quartz_window_get_nsview if is_darwin else libgdk.gdk_win32_window_get_handle
gpointer = ctypes.pythonapi.PyCapsule_GetPointer(self.get_window().__gpointer__, None)
get_pointer = libgdk.gdk_quartz_window_get_nsview if IS_DARWIN else libgdk.gdk_win32_window_get_handle
get_pointer.restype = ctypes.c_void_p
get_pointer.argtypes = [ctypes.c_void_p]
return get_pointer(gpointer)
def get_video_widget(self, widget):
area = Gtk.DrawingArea(visible=True)
area.connect("draw", self.on_drawing_area_draw)
area.connect("motion-notify-event", self.on_mouse_motion)
area.set_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)
widget.add(area)
return area
def on_drawing_area_draw(self, widget, cr):
def on_draw(self, widget, cr):
""" Used for black background drawing in the player drawing area. """
cr.set_source_rgb(0, 0, 0)
cr.paint()
@@ -126,27 +155,23 @@ class Player(ABC):
window.set_cursor(cursor)
@staticmethod
def make(name, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None):
def make(name, mode, widget):
""" Factory method. We will not use a separate factory to return a specific implementation.
@param name: implementation name.
@param mode: current player mode [Built-in or windowed].
@param widget: parent of video widget.
@param buf_cb: buffering callback.
@param position_cb: time (position) callback.
@param error_cb: error callback.
@param playing_cb: playing state callback.
Throws a NameError if there is no implementation for the given name.
"""
if name == "mpv":
return MpvPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
return MpvPlayer.get_instance(mode, widget)
elif name == "gst":
return GstPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
return GstPlayer.get_instance(mode, widget)
elif name == "vlc":
return VlcPlayer.get_instance(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
return VlcPlayer.get_instance(mode, widget)
else:
raise NameError("There is no such [{}] implementation.".format(name))
raise NameError(f"There is no such [{name}] implementation.")
class MpvPlayer(Player):
@@ -156,16 +181,17 @@ class MpvPlayer(Player):
"""
__INSTANCE = None
def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
def __init__(self, mode, widget):
super().__init__(mode, widget)
try:
from app.tools import mpv
self._player = mpv.MPV(wid=str(self.get_window_handle(self.get_video_widget(widget), )),
self._player = mpv.MPV(wid=str(self.get_window_handle()),
input_default_bindings=False,
input_cursor=False,
cursor_autohide="no")
except OSError as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
log(f"{__class__.__name__}: Load library error: {e}")
raise ImportError("No libmpv is found. Check that it is installed!")
else:
self._mode = mode
@@ -174,25 +200,35 @@ class MpvPlayer(Player):
@self._player.event_callback(mpv.MpvEventID.FILE_LOADED)
def on_open(event):
log("Starting playback...")
playing_cb()
self.emit("played", 0)
t_list = self._player._get_property("track-list")
if t_list:
# Audio tracks.
a_tracks = filter(lambda t: t.get("type", "") == "audio", t_list)
self.emit("audio-track", ((t.get("id", 1), t.get("lang", "Unknown")) for t in a_tracks))
# Subtitle.
sub_tracks = [(0, "no")]
tracks = filter(lambda t: t.get("type", "") == "sub", t_list)
[sub_tracks.append((t.get("id", 1), t.get("lang", "Unknown"))) for t in tracks]
self.emit("subtitle-track", sub_tracks)
@self._player.event_callback(mpv.MpvEventID.END_FILE)
def on_end(event):
event = event.get("event", {})
if event.get("reason", mpv.MpvEventEndFile.ERROR) == mpv.MpvEventEndFile.ERROR:
log("Stream playback error: {}".format(event.get("error", mpv.ErrorCode.GENERIC)))
error_cb()
log(f"Stream playback error: {event.get('error', mpv.ErrorCode.GENERIC)}")
self.emit("error", "Can't Playback!")
@classmethod
def get_instance(cls, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
def get_instance(cls, mode, widget):
if not cls.__INSTANCE:
cls.__INSTANCE = MpvPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
cls.__INSTANCE = MpvPlayer(mode, widget)
return cls.__INSTANCE
def get_play_mode(self):
return self._mode
@run_task
def play(self, mrl=None):
if not mrl:
return
@@ -200,7 +236,6 @@ class MpvPlayer(Player):
self._player.play(mrl)
self._is_playing = True
@run_task
def stop(self):
self._player.stop()
self._is_playing = True
@@ -219,13 +254,23 @@ class MpvPlayer(Player):
def is_playing(self):
return self._is_playing
def set_audio_track(self, track):
self._player._set_property("aid", track)
def set_subtitle_track(self, track):
self._player._set_property("sub", track)
def set_aspect_ratio(self, ratio):
self._player._set_property("aspect", ratio or "-1.0")
class GstPlayer(Player):
""" Simple wrapper for GStreamer playbin. """
__INSTANCE = None
def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
def __init__(self, mode, widget):
super().__init__(mode, widget)
try:
import gi
@@ -235,23 +280,16 @@ class GstPlayer(Player):
# Initialization of GStreamer.
Gst.init(sys.argv)
except (OSError, ValueError) as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
log(f"{__class__.__name__}: Load library error: {e}")
raise ImportError("No GStreamer is found. Check that it is installed!")
else:
self._error_cb = error_cb
self._playing_cb = playing_cb
self.STATE = Gst.State
self.STAT_RETURN = Gst.StateChangeReturn
self._mode = mode
self._is_playing = False
self._player = Gst.ElementFactory.make("playbin", "player")
# Initialization of the playback widget.
vid_widget = self.get_video_widget(widget)
widget.add(vid_widget)
vid_widget.show()
self._player.set_window_handle(self.get_window_handle(vid_widget))
self._player.set_window_handle(self.get_window_handle())
bus = self._player.get_bus()
bus.add_signal_watch()
@@ -260,9 +298,9 @@ class GstPlayer(Player):
bus.connect("message::eos", self.on_eos)
@classmethod
def get_instance(cls, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None):
def get_instance(cls, mode, widget):
if not cls.__INSTANCE:
cls.__INSTANCE = GstPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
cls.__INSTANCE = GstPlayer(mode, widget)
return cls.__INSTANCE
def get_play_mode(self):
@@ -275,12 +313,15 @@ class GstPlayer(Player):
self._player.set_property("uri", mrl)
log("Setting the URL for playback: {}".format(mrl))
log(f"Setting the URL for playback: {mrl}")
ret = self._player.set_state(self.STATE.PLAYING)
if ret == self.STAT_RETURN.FAILURE:
log("ERROR: Unable to set the 'PLAYING' state for '{}'.".format(mrl))
msg = f"ERROR: Unable to set the 'PLAYING' state for '{mrl}'."
log(msg)
self.emit("error", msg)
else:
self.emit("played", 0)
self._is_playing = True
def stop(self):
@@ -309,7 +350,7 @@ class GstPlayer(Player):
def on_error(self, bus, msg):
err, dbg = msg.parse_error()
log(err)
self._error_cb()
self.emit("error", "Can't Playback!")
def on_state_changed(self, bus, msg):
if not msg.src == self._player:
@@ -319,7 +360,7 @@ class GstPlayer(Player):
old_state, new_state, pending = msg.parse_state_changed()
if new_state is self.STATE.PLAYING:
log("Starting playback...")
self._playing_cb()
self.emit("played", 0)
self.get_stream_info()
def on_eos(self, bus, msg):
@@ -335,7 +376,7 @@ class GstPlayer(Player):
tags = self._player.emit("get-video-tags", i)
if tags:
_, cod = tags.get_string("video-codec")
log("Video codec: {}".format(cod or "unknown"))
log(f"Video codec: {cod or 'unknown'}")
nr_audio = self._player.get_property("n-audio")
for i in range(nr_audio):
@@ -343,7 +384,7 @@ class GstPlayer(Player):
tags = self._player.emit("get-audio-tags", i)
if tags:
_, cod = tags.get_string("audio-codec")
log("Audio codec: {}".format(cod or "unknown"))
log(f"Audio codec: {cod or 'unknown'}")
class VlcPlayer(Player):
@@ -354,65 +395,49 @@ class VlcPlayer(Player):
__VLC_INSTANCE = None
def __init__(self, mode, widget, buf_cb, position_cb, error_cb, playing_cb):
def __init__(self, mode, widget):
super().__init__(mode, widget)
try:
if sys.platform == "win32":
if IS_WIN:
os.add_dll_directory(r"C:\Program Files\VideoLAN\VLC")
from app.tools import vlc
from app.tools.vlc import EventType
args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib")
args = f"--quiet {'' if IS_DARWIN else '--no-xlib'}"
self._player = vlc.Instance(args).media_player_new()
vlc.libvlc_video_set_key_input(self._player, False)
vlc.libvlc_video_set_mouse_input(self._player, False)
except (OSError, AttributeError, NameError) as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
log(f"{__class__.__name__}: Load library error: {e}")
raise ImportError("No VLC is found. Check that it is installed!")
else:
self._mode = mode
self._is_playing = False
ev_mgr = self._player.event_manager()
if buf_cb:
# TODO look other EventType options
ev_mgr.event_attach(EventType.MediaPlayerBuffering,
lambda et, p: buf_cb(p.get_media().get_duration()),
self._player)
if position_cb:
ev_mgr.event_attach(EventType.MediaPlayerTimeChanged,
lambda et, p: position_cb(p.get_time()),
self._player)
if error_cb:
ev_mgr.event_attach(EventType.MediaPlayerEncounteredError,
lambda et, p: error_cb(),
self._player)
if playing_cb:
ev_mgr.event_attach(EventType.MediaPlayerPlaying,
lambda et, p: playing_cb(),
self._player)
ev_mgr.event_attach(EventType.MediaPlayerVout, self.on_playback_start)
ev_mgr.event_attach(EventType.MediaPlayerTimeChanged,
lambda et: self.emit("position", self._player.get_time()))
ev_mgr.event_attach(EventType.MediaPlayerEncounteredError, lambda et: self.emit("error", "Can't Playback!"))
self.init_video_widget(widget)
@classmethod
def get_instance(cls, mode, widget, buf_cb=None, position_cb=None, error_cb=None, playing_cb=None):
def get_instance(cls, mode, widget):
if not cls.__VLC_INSTANCE:
cls.__VLC_INSTANCE = VlcPlayer(mode, widget, buf_cb, position_cb, error_cb, playing_cb)
cls.__VLC_INSTANCE = VlcPlayer(mode, widget)
return cls.__VLC_INSTANCE
def get_play_mode(self):
return self._mode
@run_task
def play(self, mrl=None):
if mrl:
self._player.set_mrl(mrl)
self._player.play()
self._is_playing = True
@run_task
def stop(self):
if self._is_playing:
self._player.stop()
@@ -438,14 +463,34 @@ class VlcPlayer(Player):
def is_playing(self):
return self._is_playing
def set_audio_track(self, track):
self._player.audio_set_track(track)
def get_audio_track(self):
return self._player.audio_get_track()
def set_subtitle_track(self, track):
self._player.video_set_spu(track)
def set_aspect_ratio(self, ratio):
self._player.video_set_aspect_ratio(ratio)
def on_playback_start(self, event):
self.emit("played", self._player.get_media().get_duration())
# Audio tracks.
a_desc = self._player.audio_get_track_description()
self.emit("audio-track", [(t[0], t[1].decode(encoding="utf-8", errors="ignore")) for t in a_desc])
# Subtitle.
s_desc = self._player.video_get_spu_description()
self.emit("subtitle-track", [(s[0], s[1].decode(encoding="utf-8", errors="ignore")) for s in s_desc])
def init_video_widget(self, widget):
video_widget = self.get_video_widget(widget)
if sys.platform == "linux":
self._player.set_xwindow(video_widget.get_window().get_xid())
elif sys.platform == "darwin":
self._player.set_nsobject(self.get_window_handle(video_widget))
if IS_LINUX:
self._player.set_xwindow(self.get_window_handle())
elif IS_DARWIN:
self._player.set_nsobject(self.get_window_handle())
else:
self._player.set_hwnd(self.get_window_handle(video_widget))
self._player.set_hwnd(self.get_window_handle())
class Recorder:
@@ -456,18 +501,18 @@ class Recorder:
def __init__(self, settings):
try:
if sys.platform == "win32":
if IS_WIN:
os.add_dll_directory(r"C:\Program Files\VideoLAN\VLC")
from app.tools import vlc
from app.tools.vlc import EventType
except OSError as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
log(f"{__class__.__name__}: Load library error: {e}")
raise ImportError
else:
self._settings = settings
self._is_record = False
args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib")
args = f"--quiet {'' if IS_DARWIN else '--no-xlib'}"
self._recorder = vlc.Instance(args).media_player_new()
@classmethod
@@ -481,10 +526,11 @@ class Recorder:
if self._recorder:
self._recorder.stop()
path = self._settings.records_path
path = self._settings.recordings_path
os.makedirs(os.path.dirname(path), exist_ok=True)
d_now = datetime.now().strftime(_DATE_FORMAT)
path = "{}{}_{}".format(path, name.replace(" ", "_"), d_now.replace(" ", "_").replace(":", "-"))
d_now = datetime.now().strftime(LOG_DATE_FORMAT)
d_now = d_now.replace(" ", "_").replace(":", "-") if IS_WIN else d_now.replace(" ", "_")
path = f"{path}{name.replace(' ', '_')}_{d_now}"
cmd = self.get_transcoding_cmd(path) if self._settings.activate_transcoding else self._CMD.format(path)
media = self._recorder.get_instance().media_new(url, cmd)
media.get_mrl()
@@ -492,7 +538,7 @@ class Recorder:
self._recorder.set_media(media)
self._is_record = True
self._recorder.play()
log("Record started {}".format(d_now))
log(f"Record started {d_now}")
@run_task
def stop(self):
@@ -514,7 +560,7 @@ class Recorder:
def get_transcoding_cmd(self, path):
presets = self._settings.transcoding_presets
prs = presets.get(self._settings.active_preset)
return self._TR_CMD.format(",".join("{}={}".format(k, v) for k, v in prs.items()), path)
return self._TR_CMD.format(",".join(f"{k}={v}" for k, v in prs.items()), path)
if __name__ == "__main__":

View File

@@ -16,17 +16,17 @@
# <http://www.gnu.org/licenses/>.
#
from ctypes import *
import ctypes.util
import threading
import os
import sys
from warnings import warn
from functools import partial, wraps
from contextlib import contextmanager
import collections
import ctypes.util
import os
import re
import sys
import threading
import traceback
from contextlib import contextmanager
from ctypes import *
from functools import partial, wraps
from warnings import warn
if os.name == 'nt':
dll = ctypes.util.find_library('mpv-1.dll')
@@ -569,10 +569,13 @@ _mpv_free_node_contents = backend.mpv_free_node_contents
backend.mpv_create.restype = MpvHandle
_mpv_create = backend.mpv_create
_API_VER = _mpv_client_api_version()[0]
_handle_func('mpv_destroy' if _API_VER > 1 else 'mpv_detach_destroy', [], None, errcheck=None)
_handle_func('mpv_create_client', [c_char_p], MpvHandle, notnull_errcheck)
_handle_func('mpv_client_name', [], c_char_p, errcheck=None)
_handle_func('mpv_initialize', [], c_int, ec_errcheck)
_handle_func('mpv_detach_destroy', [], None, errcheck=None)
_handle_func('mpv_terminate_destroy', [], None, errcheck=None)
_handle_func('mpv_load_config_file', [c_char_p], c_int, ec_errcheck)
_handle_func('mpv_get_time_us', [], c_ulonglong, errcheck=None)
@@ -608,28 +611,6 @@ _handle_func('mpv_get_wakeup_pipe', [], c_int, errcheck=None)
_handle_func('mpv_stream_cb_add_ro', [c_char_p, c_void_p, StreamOpenFn], c_int, ec_errcheck)
# Disabled for compatibility with the old version of mpv!!!
# _handle_func('mpv_render_context_create', [MpvRenderCtxHandle, MpvHandle, POINTER(MpvRenderParam)], c_int, ec_errcheck, ctx=None)
# _handle_func('mpv_render_context_set_parameter', [MpvRenderParam], c_int, ec_errcheck, ctx=MpvRenderCtxHandle)
# _handle_func('mpv_render_context_get_info', [MpvRenderParam], c_int, ec_errcheck, ctx=MpvRenderCtxHandle)
# _handle_func('mpv_render_context_set_update_callback', [RenderUpdateFn, c_void_p], None, errcheck=None, ctx=MpvRenderCtxHandle)
# _handle_func('mpv_render_context_update', [], c_int64, errcheck=None, ctx=MpvRenderCtxHandle)
# _handle_func('mpv_render_context_render', [POINTER(MpvRenderParam)], c_int, ec_errcheck, ctx=MpvRenderCtxHandle)
# _handle_func('mpv_render_context_report_swap', [], None, errcheck=None, ctx=MpvRenderCtxHandle)
# _handle_func('mpv_render_context_free', [], None, errcheck=None, ctx=MpvRenderCtxHandle)
# Deprecated in v0.29.0 and may disappear eventually
if hasattr(backend, 'mpv_get_sub_api'):
_handle_func('mpv_get_sub_api', [MpvSubApi], c_void_p, notnull_errcheck, deprecated=True)
_handle_gl_func('mpv_opengl_cb_set_update_callback', [OpenGlCbUpdateFn, c_void_p], deprecated=True)
_handle_gl_func('mpv_opengl_cb_init_gl', [c_char_p, OpenGlCbGetProcAddrFn, c_void_p], c_int, deprecated=True)
_handle_gl_func('mpv_opengl_cb_draw', [c_int, c_int, c_int], c_int, deprecated=True)
_handle_gl_func('mpv_opengl_cb_render', [c_int, c_int], c_int, deprecated=True)
_handle_gl_func('mpv_opengl_cb_report_flip', [c_ulonglong], c_int, deprecated=True)
_handle_gl_func('mpv_opengl_cb_uninit_gl', [], c_int, deprecated=True)
def _mpv_coax_proptype(value, proptype=str):
"""Intelligently coax the given python value into something that can be understood as a proptype property."""
@@ -934,7 +915,7 @@ class MPV(object):
self._message_handlers[target](*args)
if eid == MpvEventID.SHUTDOWN:
_mpv_detach_destroy(self._event_handle)
_mpv_destroy(self._event_handle) if _API_VER > 1 else _mpv_detach_destroy(self._event_handle)
return
except Exception as e:

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -37,7 +37,7 @@ from html.parser import HTMLParser
import requests
from app.commons import run_task, log
from app.settings import SettingsType, IS_WIN
from app.settings import SettingsType, IS_LINUX, IS_WIN, IS_DARWIN, GTK_PATH
from .satellites import _HEADERS
_ENIGMA2_PICON_KEY = "{:X}:{:X}:{}"
@@ -57,9 +57,9 @@ class PiconsCzDownloader:
_PERM_URL = "https://picon.cz/download/7337"
_BASE_URL = "https://picon.cz/download/"
_BASE_LOGO_URL = "https://picon.cz/picon/0/"
_HEADER = {"User-Agent": "DemonEditor/1.0.10", "Referer": ""}
_HEADER = {"User-Agent": "DemonEditor/3.0.0", "Referer": ""}
_LINK_PATTERN = re.compile(r"((.*)-\d+x\d+)-(.*)_by_chocholousek.7z$")
_FILE_PATTERN = re.compile("\\s+(1_.*\\.png).*")
_FILE_PATTERN = re.compile(b"\\s+(\\w+\\.png).*")
def __init__(self, picon_ids=set(), appender=log):
self._perm_links = {}
@@ -81,7 +81,11 @@ class PiconsCzDownloader:
name_map = self.get_name_map()
for line in request.iter_lines():
l_id, perm_link = line.decode(encoding="utf-8", errors="ignore").split(maxsplit=1)
data = line.decode(encoding="utf-8", errors="ignore").split(maxsplit=1)
if len(data) != 2:
continue
l_id, perm_link = data
self._perm_links[str(l_id)] = str(perm_link)
data = re.match(self._LINK_PATTERN, perm_link)
if data:
@@ -89,7 +93,7 @@ class PiconsCzDownloader:
# Logo url.
logo = logo_map.get(data.group(2), None)
l_name = name_map.get(sat_pos, None) or sat_pos.replace(".", "")
logo_url = "{}{}/{}.png".format(self._BASE_LOGO_URL, logo, l_name) if logo else None
logo_url = f"{self._BASE_LOGO_URL}{logo}/{l_name}.png" if logo else None
prv = Provider(None, data.group(1), sat_pos, self._BASE_URL + l_id, l_id, logo_url, None, False)
if sat_pos in self._providers:
@@ -97,7 +101,7 @@ class PiconsCzDownloader:
else:
self._providers[sat_pos] = [prv]
else:
log("{} [get permalinks] error: {}".format(self.__class__.__name__, request.reason))
log(f"{self.__class__.__name__} [get permalinks] error: {request.reason}")
raise PiconsError(request.reason)
@property
@@ -111,35 +115,39 @@ class PiconsCzDownloader:
self._HEADER["Referer"] = provider.url
with requests.get(url=provider.url, headers=self._HEADER, stream=True) as request:
if request.reason == "OK":
dest = "{}{}.7z".format(picons_path, provider.on_id)
self._appender("Downloading: {}\n".format(provider.url))
dest = f"{picons_path}{provider.on_id}.7z"
self._appender(f"Downloading: {provider.url}")
with open(dest, mode="bw") as f:
for data in request.iter_content(chunk_size=1024):
f.write(data)
self._appender("Extracting: {}\n".format(provider.on_id))
self._appender(f"Extracting: {provider.on_id}")
self.extract(dest, picons_path, picon_ids)
else:
log("{} [download] error: {}".format(self.__class__.__name__, request.reason))
log(f"{self.__class__.__name__} [download] error: {request.reason}")
def extract(self, src, dest, picon_ids=None):
""" Extracts 7z archives. """
# TODO: think about https://github.com/miurahr/py7zr
exe = "7zr"
exe = "7z"
if IS_DARWIN and GTK_PATH:
exe = "./7zr"
if IS_LINUX and not os.path.isfile(f"/usr/bin/{exe}"):
raise PiconsError("7-zip [7z] archiver not found!")
if IS_WIN:
exe = "C:\\Program Files\\7-Zip\\7z.exe"
exe = f"{exe}.exe" if GTK_PATH else f"C:{os.sep}Program Files{os.sep}7-Zip{os.sep}{exe}.exe"
if not os.path.isfile(exe):
raise PiconsError("7-Zip executable not found!")
cmd = [exe, "l", src]
try:
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8").communicate()
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
if err:
log("{} [extract] error: {}".format(self.__class__.__name__, err))
log(f"{self.__class__.__name__} [extract] error: {err}")
raise PiconsError(err)
except OSError as e:
log("{} [extract] error: {}".format(self.__class__.__name__, e))
log(f"{self.__class__.__name__} [extract] error: {e}")
raise PiconsError(e)
is_filter = bool(picon_ids)
@@ -147,7 +155,7 @@ class PiconsCzDownloader:
to_extract = []
for o in re.finditer(self._FILE_PATTERN, out):
p_id = o.group(1)
p_id = o.group(1).decode("utf-8", errors="ignore")
if p_id in ids:
to_extract.append(p_id)
@@ -159,11 +167,9 @@ class PiconsCzDownloader:
cmd = [exe, "e", src, "-o{}".format(dest), "-y", "-r"]
cmd.extend(to_extract)
try:
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
encoding="utf-8").communicate()
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
if err:
log("{} [extract] error: {}".format(self.__class__.__name__, err))
log(f"{self.__class__.__name__} [extract] error: {err}")
raise PiconsError(err)
else:
if os.path.isfile(src):
@@ -190,9 +196,9 @@ class PiconsCzDownloader:
self._provider_logos[url] = data
return data
else:
log("Downloading package logo error: {}".format(logo_request.reason))
log(f"Downloading package logo error: {logo_request.reason}")
except requests.exceptions.ConnectionError as e:
log("{} error [get provider logo]: {}".format(self.__class__.__name__, e))
log(f"{self.__class__.__name__} error [get provider logo]: {e}")
def get_logos_map(self):
return {"piconblack": "b50",
@@ -213,7 +219,9 @@ class PiconsCzDownloader:
"picontransparentdark": "td220",
"piconoled": "o96",
"piconblack80": "b50",
"piconblack3d": "b50"
"piconblack3d": "b50",
"piconwin11": "win11220",
"piconSNPtransparent": "t50"
}
def get_name_map(self):
@@ -479,20 +487,16 @@ def parse_providers(url):
return providers
def download_picon(src_url, dest_path, callback):
def download_picon(src_url, dest_path):
""" Downloads and saves the picon to file. """
err_msg = "Picon download error: {} [{}]"
timeout = (3, 5) # connect and read timeouts
if callback:
callback("Downloading: {}.\n".format(os.path.basename(dest_path)))
log("Downloading: {}.".format(os.path.basename(dest_path)))
req = requests.get(src_url, timeout=timeout, stream=True)
if req.status_code != 200:
err_msg = err_msg.format(src_url, req.reason)
log(err_msg)
if callback:
callback(err_msg + "\n")
else:
try:
with open(dest_path, "wb") as f:
@@ -501,12 +505,10 @@ def download_picon(src_url, dest_path, callback):
except OSError as e:
err_msg = "Saving picon [{}] error: {}".format(dest_path, e)
log(err_msg)
if callback:
callback(err_msg + "\n")
@run_task
def convert_to(src_path, dest_path, s_type, callback, done_callback):
def convert_to(src_path, dest_path, s_type, done_callback):
""" Converts names format of picons.
Copies resulting files from src to dest and writes state to callback.
@@ -517,7 +519,7 @@ def convert_to(src_path, dest_path, s_type, callback, done_callback):
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))
log('Converting "{}" to "{}"'.format(base_name, dest_file))
shutil.copyfile(file, dest)
done_callback()

View File

@@ -1,6 +1,34 @@
""" Module for downloading satellites, transponders ans services from the web.
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
Sources: www.flysat.com, www.lyngsat.com.
""" Module for downloading satellites, transponders and services from the Web.
Sources: www.flysat.com, www.lyngsat.com, www.kingofsat.net.
Replaces or updates the current satellites.xml file.
"""
import re
@@ -14,11 +42,12 @@ from app.eparser import Satellite, Transponder, is_transponder_valid
from app.eparser.ecommons import (PLS_MODE, get_key_by_value, FEC, SYSTEM, POLARIZATION, MODULATION, SERVICE_TYPE,
Service, CAS)
_HEADERS = {"User-Agent": "Mozilla/5.0 (Linux x86_64; rv:85.0) Gecko/20100101 Firefox/85.0"}
_HEADERS = {"User-Agent": "Mozilla/5.0 (Linux x86_64; rv:92.0) Gecko/20100101 Firefox/92.0"}
_TIMEOUT = 10
class SatelliteSource(Enum):
FLYSAT = ("https://www.flysat.com/satlist.php",)
FLYSAT = ("https://www.flysat.com/en/satellitelist",)
LYNGSAT = ("https://www.lyngsat.com/asia.html", "https://www.lyngsat.com/europe.html",
"https://www.lyngsat.com/atlantic.html", "https://www.lyngsat.com/america.html")
KINGOFSAT = ("https://en.kingofsat.net/satellites.php",)
@@ -38,10 +67,10 @@ class Cell:
self._img = img
def __repr__(self):
return "Cell({}, {}, {})".format(self._text, self._url, self._img)
return f"Cell({self._text}, {self._url}, {self._img})"
def __str__(self):
return "<Cell(text={}, link={}, img={})>".format(self._text, self._url, self._img)
return f"<Cell(text={self._text}, link={self._url}, img={self._img})>"
def __iter__(self):
return (x for x in (self._text, self._url, self._img))
@@ -93,6 +122,12 @@ class SatellitesParser(HTMLParser):
self._rows = []
self._source = source
self.PLS_MODES = {v: k for k, v in PLS_MODE.items()}
self.POLARIZATION = {v: k for k, v in POLARIZATION.items()}
self.FEC = {v: k for k, v in FEC.items()}
self.SYSTEM = {v: k for k, v in SYSTEM.items()}
self.MODULATION = {v: k for k, v in MODULATION.items()}
def handle_starttag(self, tag, attrs):
if tag == "td":
self._is_td = True
@@ -134,76 +169,114 @@ class SatellitesParser(HTMLParser):
for src in SatelliteSource.get_sources(self._source):
try:
request = requests.get(url=src, headers=_HEADERS)
except requests.exceptions.ConnectionError as e:
log(repr(e))
return []
resp = requests.get(url=src, headers=_HEADERS, timeout=_TIMEOUT)
except requests.exceptions.RequestException as e:
log(f"Getting satellite list error: {repr(e)}")
else:
reason = request.reason
reason = resp.reason
if reason == "OK":
self.feed(request.text)
self.feed(resp.text)
else:
log(reason)
log(f"Getting satellite list error: {reason} -> {resp.url}")
if self._rows:
if self._source is SatelliteSource.FLYSAT:
def get_sat(r):
return r[1], self.parse_position(r[2]), r[3], r[0], False
return list(map(get_sat, filter(lambda x: all(x) and len(x) == 5, self._rows)))
return self.get_satellites_for_fly_sat()
elif self._source is SatelliteSource.LYNGSAT:
base_url = "https://www.lyngsat.com/"
sats = []
cur_pos = "0"
for row in filter(lambda x: 3 < len(x) < 8, self._rows):
if not row[0]:
row = row[1:]
pos = self.parse_position(row[1])
if not self.POS_PAT.match(pos):
if len(row) == 4 and row[0].endswith(".html"):
sats.append((row[1], cur_pos, row[-2], base_url + row[0], False))
continue
sats.append((row[-3], pos, row[-2], base_url + row[0], False))
cur_pos = pos
return sats
return self.get_satellites_for_lyng_sat()
elif source is SatelliteSource.KINGOFSAT:
def get_sat(r):
return r[3], self.parse_position(r[1]), None, r[0], False
return self.get_satellites_for_king_of_sat()
return list(map(get_sat, filter(lambda x: len(x) == 17, self._rows)))
return []
def get_satellite(self, sat):
pos = sat[1]
return Satellite(name="{} {}".format(pos, sat[0]),
flags="0",
return Satellite(name=f"{pos} {sat[0]}", flags="0",
position=self.get_position(pos.replace(".", "")),
transponders=self.get_transponders(sat[3]))
def get_satellites_for_fly_sat(self):
sat_pat = re.compile(r"https://.*/satellite/.+")
pos_pat = re.compile(r"https://.*/satellite/position/.+")
names = []
pos = ""
pos_url = ""
satellites = []
def normalize_pos(p):
return f"{float(p[:-1])}{p[-1]}" if "." not in p else p
for row in filter(lambda x: len(x) > 6, self._rows):
if re.match(sat_pat, row[1]):
row.pop(0)
if re.match(sat_pat, row[0]) and row[-2]: # r[-2] -> skip EMPTY satellites!
if re.match(pos_pat, row[0]):
names.clear()
pos_url = row[0]
name = row[3]
pos = normalize_pos(self.parse_position(row[-4]))
names.append(name)
satellites.append((name, pos, row[-2], row[2], False))
if len(row) == 7:
single_pos = normalize_pos(self.parse_position(row[-4]))
name = row[1]
if pos == single_pos:
names.append(name)
else:
# Uniting satellites in position.
if len(names) > 1:
satellites.append(("/".join(names), pos, None, pos_url, False))
names.clear()
satellites.append((name, single_pos, row[-2], row[0], False))
return satellites
def get_satellites_for_lyng_sat(self):
base_url = "https://www.lyngsat.com/"
sats = []
cur_pos = "0"
for row in filter(lambda x: 3 < len(x) < 8, self._rows):
if not row[0]:
row = row[1:]
pos = self.parse_position(row[1])
if not self.POS_PAT.match(pos):
if len(row) == 4 and row[0].endswith(".html"):
sats.append((row[1], cur_pos, row[-2], base_url + row[0], False))
continue
sats.append((row[-3], pos, row[-2], base_url + row[0], False))
cur_pos = pos
return sats
def get_satellites_for_king_of_sat(self):
def get_sat(r):
return r[3], self.parse_position(r[1]), None, r[2], False
return list(map(get_sat, filter(lambda x: len(x) == 17, self._rows)))
@staticmethod
def parse_position(pos_str):
return "".join(c for c in pos_str if c.isdigit() or c.isalpha() or c == ".")
@staticmethod
def get_position(pos):
return "{}{}".format("-" if pos[-1] == "W" else "", pos[:-1])
return f"{'-' if pos[-1] == 'W' else ''}{pos[:-1]}"
def get_transponders(self, sat_url):
""" Getting transponders(sorted by frequency). """
self._rows.clear()
trs = []
url = sat_url
if self._source is SatelliteSource.FLYSAT:
url = "https://www.flysat.com/" + sat_url
elif self._source is SatelliteSource.KINGOFSAT:
url = "https://en.kingofsat.net/" + sat_url
if self._source is SatelliteSource.KINGOFSAT:
sat_url = "https://en.kingofsat.net/" + sat_url
try:
request = requests.get(url=url, headers=_HEADERS)
except requests.exceptions.ConnectionError as e:
log("Getting transponders error: {}".format(e))
request = requests.get(url=sat_url, headers=_HEADERS, timeout=_TIMEOUT)
except requests.exceptions.RequestException as e:
log(f"Getting transponders error: {e}")
else:
if request.status_code == 200:
self.feed(request.text)
@@ -214,87 +287,99 @@ class SatellitesParser(HTMLParser):
elif self._source is SatelliteSource.KINGOFSAT:
self.get_transponders_for_king_of_sat(trs)
else:
log("SatellitesParser [get transponders] error: {} {}".format(url, request.reason))
log(f"SatellitesParser [get transponders] error: {sat_url} {request.reason}")
return sorted(trs, key=lambda x: int(x.frequency))
def get_transponders_for_fly_sat(self, trs):
""" Parsing transponders for FlySat """
pls_pattern = re.compile("(PLS:)+ (Root|Gold|Combo)+ (\\d+)?")
is_id_pattern = re.compile("(Stream) (\\d+)")
pls_modes = {v: k for k, v in PLS_MODE.items()}
""" Parsing transponders for FlySat. """
frq_pol_pattern = re.compile(r"(\d{4,5})+\s+([RLHV]).*(DVB-S[2]?)/(.+PSK)?.*")
pls_pattern = re.compile(r".*PLS\s+(Root|Gold|Combo)+\s(\d+)?")
is_id_pattern = re.compile(r"Stream\s(\d+)")
sr_fec_pattern = re.compile(r"(\d{4,5})+\s+(\d+/\d+).*")
n_trs = []
if self._rows:
zeros = "000"
is_ids = []
for r in self._rows:
if len(r) == 1:
row_len = len(r)
if row_len == 1:
is_ids.extend(re.findall(is_id_pattern, r[0]))
continue
if len(r) < 3:
if row_len < 12:
continue
data = r[2].split(" ")
if len(data) != 2:
continue
sr, fec = data
data = r[1].split(" ")
if len(data) < 3:
continue
freq, pol, sys = data[0], data[1], data[2]
sys = sys.split("/")
if len(sys) != 2:
continue
sys, mod = sys
mod = "QPSK" if sys == "DVB-S" else mod
pls = re.findall(pls_pattern, r[1])
freq = re.findall(frq_pol_pattern, r[2])
if not freq:
continue
freq, pol, sys, mod = freq[0]
sr_fec = re.match(sr_fec_pattern, r[3])
if not sr_fec:
continue
sr, fec = sr_fec.group(1), sr_fec.group(2)
pls = re.match(pls_pattern, r[2])
pls_code = None
pls_mode = None
if pls:
pls_code = pls[0][2]
pls_mode = pls_modes.get(pls[0][1], None)
pls_mode = self.PLS_MODES.get(pls.group(1), None)
pls_code = pls.group(2)
if is_ids:
tr = trs.pop()
for index, is_id in enumerate(is_ids):
tr = tr._replace(is_id=is_id[1])
tr = tr._replace(is_id=is_id)
if is_transponder_valid(tr):
n_trs.append(tr)
else:
tr = Transponder(freq + zeros, sr + zeros, pol, fec, sys, mod, pls_mode, pls_code, None)
if is_transponder_valid(tr):
trs.append(tr)
tr = Transponder(f"{freq}000", f"{sr}000",
self.POLARIZATION.get(pol, None),
self.FEC.get(fec, None),
self.SYSTEM.get(sys, None),
self.MODULATION.get(mod, None),
pls_mode, pls_code, None, None)
if is_transponder_valid(tr):
trs.append(tr)
is_ids.clear()
trs.extend(n_trs)
def get_transponders_for_lyng_sat(self, trs):
""" Parsing transponders for LyngSat. """
frq_pol_pattern = re.compile("(\\d{4,5})\\s+([RLHV]).*")
sr_fec_pattern = re.compile(r"(DVB-S[2]?)\s+(.+PSK)?.*?(\d+)\s+(\d/\d)\s*(?:T2-MI\s+PLP\s+(\d+))?.*")
zeros = "000"
pls_mode, pls_code, pls_id = None, None, None
frq_pol_pattern = re.compile(r"(\d{4,5})\s+([RLHV]).*")
sr_fec_pattern = re.compile((r"(DVB-S[2]?)\s+(.+PSK)?.*?(\d+)\s+(\d/\d)\s?"
r"(?:T2-MI\s+PLP\s+(\d+))?.*"
r"?(?:PLS\s+(Root|Gold|Combo)\s+(\d+))?"
r"(?:.*Stream\s+(\d+))?.*"))
for row in filter(lambda x: len(x) > 8, self._rows):
for frq in row[1], row[2], row[3]:
freq = re.match(frq_pol_pattern, frq)
if freq:
for freq in row[1], row[2], row[3]:
res = re.match(frq_pol_pattern, freq)
if res:
break
if not freq:
if not res:
continue
frq, pol = freq.group(1), freq.group(2)
srf = " ".join(row[3:5])
sr_fec = re.search(sr_fec_pattern, srf)
if not sr_fec:
freq, pol = res.group(1), res.group(2)
res = re.search(sr_fec_pattern, row[3])
if not res:
continue
sys, mod, sr, fec = sr_fec.group(1), sr_fec.group(2), sr_fec.group(3), sr_fec.group(4)
sys, mod, sr, fec = res.group(1), res.group(2), res.group(3), res.group(4)
mod = mod.strip() if mod else "Auto"
pls_id = sr_fec.group(5)
plp, pls_mode, pls_code, is_id = res.group(5), res.group(6), res.group(7), res.group(8)
pls_mode = self.PLS_MODES.get(pls_mode, None)
tr = Transponder(frq + zeros, sr + zeros, pol, fec, sys, mod, pls_mode, pls_code, pls_id)
if plp is not None:
log(f"Detected T2-MI transponder! [{freq} {sr} {pol}] ")
tr = Transponder(f"{freq}000", f"{sr}000",
self.POLARIZATION.get(pol, None),
self.FEC.get(fec, None),
self.SYSTEM.get(sys, None),
self.MODULATION.get(mod, None),
pls_mode, pls_code, is_id, None)
if is_transponder_valid(tr):
trs.append(tr)
@@ -303,19 +388,42 @@ class SatellitesParser(HTMLParser):
Since the *.ini file contains incomplete information, it is not used.
"""
zeros = "000"
pat = re.compile(
r"(\d+).00\s+([RLHV])\s+(DVB-S[2]?)\s+(?:T2-MI, PLP (\d+)\s+)?(.*PSK).*?(?:Stream\s+(\d+))?\s+(\d+)\s+(\d+/\d+)$")
sys_pat = re.compile(r"(DVB-S[2]?)\s?(?:T2-MI,\s+PLP\s+(\d+))?.*?(?:PLS:\s+(Root|Gold|Combo)\+(\d+))?")
mod_pat = re.compile(r"(.*PSK).*?(?:.*Stream\s+(\d+))?.*")
sr_fec_pattern = re.compile(r"(\d{4,5})+\s+(\d+/\d+).*")
for row in filter(lambda r: len(r) == 16 and self.POS_PAT.match(r[0]), self._rows):
res = pat.search(" ".join((row[0], row[2], row[3], row[8], row[9], row[10])))
if res:
freq, sr, pol, fec, sys = res.group(1), res.group(7), res.group(2), res.group(8), res.group(3)
mod, pls_id, pls_code = res.group(5), res.group(4), res.group(6)
freq, pol = row[2].replace(".", "0"), row[3]
if not freq.isdigit() or pol not in "VHLR":
continue
tr = Transponder(freq + zeros, sr + zeros, pol, fec, sys, mod, None, pls_code, pls_id)
if is_transponder_valid(tr):
trs.append(tr)
res = re.match(sys_pat, row[8])
if not res:
continue
sys, t2_mi, pls_id, pls_code = res.group(1), res.group(2), res.group(3), res.group(4)
pls_id = self.PLS_MODES.get(pls_id, None)
res = re.match(mod_pat, row[9])
if not res:
continue
mod, is_id = res.group(1), res.group(2)
res = re.match(sr_fec_pattern, row[10])
if not res:
continue
sr, fec = res.group(1), res.group(2)
if t2_mi:
log(f"Detected T2-MI transponder! [{freq} {sr} {pol}] ")
tr = Transponder(freq, f"{sr}000",
self.POLARIZATION.get(pol, None),
self.FEC.get(fec, None),
self.SYSTEM.get(sys, None),
self.MODULATION.get(mod, None),
pls_id, pls_code, is_id, None)
if is_transponder_valid(tr):
trs.append(tr)
class ServicesParser(HTMLParser):
@@ -328,22 +436,53 @@ class ServicesParser(HTMLParser):
self._S_TYPES = {"": "2", "MPEG-2 SD": "1", "MPEG-2/SD": "1", "SD": "1", "MPEG-4 SD": "22", "MPEG-4/SD": "22",
"MPEG-4": "22", "HEVC SD": "22", "MPEG-4/HD": "25", "MPEG-4 HD": "25", "MPEG-4 HD 1080": "25",
"MPEG-4 HD 720": "25", "HEVC HD": "25", "HEVC/HD": "25", "HEVC": "31", "HEVC/UHD": "31",
"HEVC UHD": "31", "HEVC UHD 4K": "31"}
self._TR_PAT = re.compile(
r".*?(\d+)\s+([RLHV]).*(DVB-S[2]?)/?(.*PSK)?\s(T2-MI)?\s?SR-FEC:\s(\d+)-(\d/\d)\s+.*ONID-TID:\s+(\d+)-(\d+).*")
self._POS_PAT = re.compile(r".*?(\d+\.\d°[EW]).*")
"HEVC UHD": "31", "HEVC UHD 4K": "31", "3": "Data"}
self._TR = "s {}000:{}000:{}:{}:{}:{}:{}:{}"
self._S2_TR = "{}:{}:{}:{}"
self._POS_PAT = re.compile(r".*?(\d+\.\d°[EW]).*")
# LyngSat.
self._TR_PAT = re.compile((r".*?(\d+)\.?\d?\s+([RLHV]).*(DVB-S[2]?)/?(.*PSK)?\s"
r"?(T2-MI)?\s?(PLS\s+Multistream)?\s?"
r"SR-FEC:\s(\d+)-(\d/\d)\s+.*ONID-TID:\s+(\d+)-(\d+).*"))
self._MULTI_PAT = re.compile(r"PLS\s+(Root|Gold|Combo)+\s(\d+)?\s+(?:Stream\s(\d+))")
# KingOfSat.
self._KING_TR_PAT = re.compile((r"(DVB-S[2]?)\s?(?:T2-MI,\s+PLP\s+(\d+))?.*"
r"?(?:PLS:\s+(Root|Gold|Combo)\+(\d+))?"
r"\s+(.*PSK).*?(?:.*Stream\s+(\d+))?.*"))
self._parse_html_entities = entities
self._separator = separator
self._is_td = False
self._is_th = False
self._is_mux_div = False
self._current_row = []
self._current_cell_text = []
self._current_cell = Cell()
self._rows = []
self._source = source
self._t_url = ""
self._use_short_names = True
self._pls_modes = {v: k for k, v in PLS_MODE.items()}
@property
def source(self):
return self._source
@source.setter
def source(self, value):
self._source = value
self.reset()
@property
def use_short_names(self):
return self._use_short_names
@use_short_names.setter
def use_short_names(self, value):
self._use_short_names = value
def handle_starttag(self, tag, attrs):
if tag == "td":
@@ -351,17 +490,39 @@ class ServicesParser(HTMLParser):
elif tag == "tr":
self._is_th = True
elif tag == "a" and not self._current_cell.url:
self._current_cell.url = attrs[0][1]
if attrs:
for a in attrs:
if a[0] == "href":
self._current_cell.url = a[1]
if self._source is SatelliteSource.KINGOFSAT and self._use_short_names:
if a[0] != "title":
continue
txt = a[1]
sep = "Id: "
if txt and txt.startswith(sep):
# Saving the 'short' name.
_, sep, name = txt.partition(sep)
self._current_cell.text = name
elif tag == "img":
img_link = attrs[0][1]
if img_link.startswith("/logo/"):
if self._source is SatelliteSource.LYNGSAT:
if img_link.startswith("/logo/"):
self._current_cell.img = img_link
elif self._source is SatelliteSource.KINGOFSAT:
self._current_cell.img = img_link
elif tag == "div" and self._source is SatelliteSource.LYNGSAT:
self._is_mux_div = bool(list(filter(lambda at: at[-1] == "mux-header", attrs)))
def handle_data(self, data):
""" Save content to a cell """
if self._is_td or self._is_th:
self._current_cell_text.append(data.strip())
if self._is_mux_div:
self._current_cell.url = data.strip()
self._is_mux_div = False
def handle_endtag(self, tag):
if tag == "td":
self._is_td = False
@@ -369,8 +530,9 @@ class ServicesParser(HTMLParser):
self._is_th = False
if tag in ("td", "th"):
final_cell = self._separator.join(self._current_cell_text).strip()
self._current_cell.text = final_cell
if not self._current_cell.text:
txt = self._separator.join(self._current_cell_text).strip()
self._current_cell.text = txt
self._current_row.append(self._current_cell)
self._current_cell_text = []
self._current_cell = Cell()
@@ -380,32 +542,48 @@ class ServicesParser(HTMLParser):
self._current_row = []
def error(self, message):
log("ServicesParser error: {}".format(message))
log(f"ServicesParser error: {message}")
def init_data(self, url):
""" Initializes data for the given URL. """
if self._source is not SatelliteSource.LYNGSAT:
raise ValueError("Unsupported source: {}!".format(self._source.name))
if self._source not in (SatelliteSource.LYNGSAT, SatelliteSource.KINGOFSAT):
raise ValueError(f"Unsupported source: {self._source.name}!")
self._rows.clear()
request = requests.get(url=url, headers=_HEADERS)
reason = request.reason
if reason == "OK":
self.feed(request.text)
try:
request = requests.get(url=url, headers=_HEADERS, timeout=_TIMEOUT)
except requests.exceptions.RequestException as e:
raise ValueError(e)
else:
raise ValueError(reason)
reason = request.reason
if reason == "OK":
self.feed(request.text)
else:
raise ValueError(reason)
def get_transponders_links(self, sat_url):
""" Returns transponder links. """
try:
if self._source is SatelliteSource.KINGOFSAT:
sat_url = "https://en.kingofsat.net/" + sat_url
self.init_data(sat_url)
except ValueError as e:
log(e)
else:
url = "https://www.lyngsat.com/muxes/"
return [row[0] for row in
filter(lambda x: x and len(x) > 8 and x[0].url and x[0].url.startswith(url), self._rows)]
if self._source is SatelliteSource.LYNGSAT:
url = "https://www.lyngsat.com/muxes/"
return [row[0] for row in
filter(lambda x: x and len(x) > 8 and x[0].url and x[0].url.startswith(url), self._rows)]
elif self._source is SatelliteSource.KINGOFSAT:
trs = []
for r in self._rows:
if len(r) == 13 and SatellitesParser.POS_PAT.match(r[0].text):
t_cell = r[4]
if t_cell.url and t_cell.url.startswith("tp.php?tp="):
t_cell.url = f"https://en.kingofsat.net/{t_cell.url}"
t_cell.text = f"{r[2].text} {r[3].text} {r[6].text} {r[8].text}"
trs.append(t_cell)
return trs
return []
def get_transponder_services(self, tr_url, sat_position=None, use_pids=False):
@@ -415,90 +593,189 @@ class ServicesParser(HTMLParser):
@param sat_position: custom satellite position. Sometimes required to adjust the namespace.
@param use_pids: if possible use additional pids [video, audio].
"""
services = []
try:
self._t_url = tr_url
self.init_data(tr_url)
except ValueError as e:
log(e)
return []
else:
pos, freq, sr, fec, pol, namespace, tid, nid = sat_position or 0, 0, 0, 0, 0, 0, 0, 0
sys = "DVB-S"
pos_found = False
tr = None
# Transponder
for r in filter(lambda x: x and 6 < len(x) < 9, self._rows):
if not pos_found:
pos_tr = re.match(self._POS_PAT, r[0].text)
if not pos_tr:
continue
if self._source is SatelliteSource.LYNGSAT:
return self.get_lyngsat_services(sat_position, use_pids)
elif self._source is SatelliteSource.KINGOFSAT:
return self.get_kingofsat_services(sat_position, use_pids)
return []
if not sat_position:
pos = int(SatellitesParser.get_position(
"".join(c for c in pos_tr.group(1) if c.isdigit() or c.isalpha())))
def get_lyngsat_services(self, sat_position=None, use_pids=False):
services = []
pos, freq, sr, fec, pol, nsp, tid, nid = sat_position or 0, 0, 0, 0, 0, 0, 0, 0
sys = "DVB-S"
pos_found = False
tr = None
# Multi-stream.
multi_tr = None
multi = False
# Transponder.
for r in filter(lambda x: x and 6 < len(x) < 9, self._rows):
if not pos_found:
pos_tr = re.match(self._POS_PAT, r[0].text)
if not pos_tr:
continue
pos_found = True
if not sat_position:
pos = self.get_position(pos_tr.group(1))
pos_found = True
if pos_found:
text = " ".join(c.text for c in r[1:])
td = re.match(self._TR_PAT, text)
if td:
freq, pol = int(td.group(1)), get_key_by_value(POLARIZATION, td.group(2))
if td.group(5):
log("Detected T2-MI transponder!")
continue
if pos_found:
text = " ".join(c.text for c in r[1:])
td = re.match(self._TR_PAT, text)
if td:
freq, pol = int(td.group(1)), get_key_by_value(POLARIZATION, td.group(2))
sys, mod, sr, _fec, = td.group(3), td.group(4), td.group(7), td.group(8)
nid, tid = td.group(9), td.group(10)
sys, mod, fec, nsp, s2_flags, roll_off, pilot, inv = self.get_transponder_data(pos, _fec, sys, mod)
nid, tid = int(nid), int(tid)
sys, mod, sr, _fec, = td.group(3), td.group(4), td.group(6), td.group(7)
nid, tid = td.group(8), td.group(9)
if td.group(5):
log(f"Detected T2-MI transponder! [{freq} {sr} {pol}]")
neg_pos = False # POS = W
# For negative (West) positions: 3600 - numeric position value!!!
namespace = "{:04x}0000".format(3600 - pos if neg_pos else pos)
inv = 2 # Default
fec = get_key_by_value(FEC, _fec)
sys = get_key_by_value(SYSTEM, sys)
tr_flag = 1
mod = get_key_by_value(MODULATION, mod)
roll_off = 0 # 35% DVB-S2/DVB-S (default)
pilot = 2 # Auto
s2_flags = "" if sys == "DVB-S" else self._S2_TR.format(tr_flag, mod or 0, roll_off, pilot)
nid, tid = int(nid), int(tid)
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
if td.group(6):
log(f"Detected multi-stream transponder! [{freq} {sr} {pol}]")
multi = True
if not tr:
msg = "ServicesParser error [get transponder services]: {}"
er = "Transponder [{}] not found or its type [T2-MI, etc] not supported yet.".format(freq)
log(msg.format(er))
return []
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
# Services
for r in filter(lambda x: x and len(x) == 12 and (x[0].text.isdigit()), self._rows):
if not tr:
er = f"Transponder [{self._t_url}] not found or its type [T2-MI, etc] not supported yet."
log(f"ServicesParser error [get transponder services]: {er}")
return services
# Services.
for r in filter(None, self._rows):
if multi and r[0].url:
res = re.match(self._MULTI_PAT, r[0].url)
if res:
pls_mode, is_code, is_id = self._pls_modes.get(res.group(1), None), res.group(2), res.group(3)
multi_tr = f"{tr}:{is_id}:{is_code}:{pls_mode}" if all((pls_mode, is_code, is_id)) else None
tid = int(is_id) if multi_tr else tid
if len(r) == 12 and r[0].text.isdigit():
sid, name, s_type, v_pid, a_pid, cas, pkg = r[0].text, r[2].text, r[4].text, r[
5].text.strip(), r[6].text.split(), r[9].text, r[10].text.strip()
try:
s_type = self._S_TYPES.get(s_type, "3") # 3 = Data
_s_type = SERVICE_TYPE.get(s_type, SERVICE_TYPE.get("3")) # str repr
sid = int(sid)
data_id = "{:04x}:{}:{:04x}:{:04x}:{}:0:0".format(sid, namespace, tid, nid, s_type)
fav_id = "{}:{}:{}:{}".format(sid, tid, nid, namespace)
picon_id = "1_0_{:X}_{}_{}_{}_{}_0_0_0.png".format(int(s_type), sid, tid, nid, namespace)
# Flags.
flags = "p:{}".format(pkg)
cas = ",".join(get_key_by_value(CAS, c) or "C:0000" for c in cas.split()) if cas else None
if use_pids:
v_pid = "c:00{:04x}".format(int(v_pid)) if v_pid else None
a_pid = ",".join(["c:01{:04x}".format(int(p)) for p in a_pid]) if a_pid else None
flags = ",".join(filter(None, (flags, v_pid, a_pid, cas)))
else:
flags = ",".join(filter(None, (flags, cas)))
flags, sid, fav_id, picon_id, data_id = self.get_service_data(s_type, pkg, sid, tid, nid, nsp,
v_pid, a_pid, cas, use_pids)
services.append(Service(flags, "s", None, name, None, None, pkg, _s_type, r[1].img, picon_id,
sid, freq, sr, pol, fec, sys, pos, data_id, fav_id, tr))
sid, freq, sr, pol, fec, sys, pos, data_id, fav_id, multi_tr or tr))
except ValueError as e:
log("ServicesParser error [get transponder services]: {}".format(e))
log(f"ServicesParser error [get transponder services]: {e}")
return services
def get_kingofsat_services(self, sat_position=None, use_pids=False):
services = []
# Transponder
tr = list(filter(lambda r: len(r) == 13 and r[4].url and r[4].url.startswith("tp.php?tp="), self._rows))
if not tr:
log(f"ServicesParser error [get transponder services]: Transponder [{self._t_url}] not found!")
return services
tr, multi_tr, tid, nid, nsp = None, None, None, None, None
freq, sr, pol, fec, sys, pos = None, None, None, None, None, None
for r in filter(lambda x: len(x) > 12, self._rows):
r_size = len(r)
if r_size == 13 and r[4].url and r[4].url.startswith("tp.php?tp="):
res = re.match(self._KING_TR_PAT, f"{r[6].text} {r[7].text}")
if not res:
continue
sys, mod = res.group(1), res.group(5)
s_pos, freq, pol, sr_fec = r[0].text, r[2].text, r[3].text, r[8].text
nid, tid = r[10].text, r[11].text
pos = sat_position
if not sat_position:
pos_tr = re.match(self._POS_PAT, s_pos)
if pos_tr:
pos = self.get_position(pos_tr.group(1))
sr, fec = sr_fec.split()
pol = get_key_by_value(POLARIZATION, pol)
sys, mod, fec, nsp, s2_flags, roll_off, pilot, inv = self.get_transponder_data(pos, fec, sys, mod)
freq, nid, tid = int(float(freq)), int(nid), int(tid)
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
pls_mode, is_code, is_id = self._pls_modes.get(res.group(3), None), res.group(4), res.group(6)
multi_tr = f"{tr}:{is_id}:{is_code}:{pls_mode}" if all((pls_mode, is_code, is_id)) else None
tid = int(is_id) if multi_tr else tid
if res.group(2):
log(f"Detected T2-MI transponder! [{freq} {sr}]")
if multi_tr:
log(f"Detected multi-stream transponder! [{freq} {sr}]")
if tr and r_size == 14 and not r[1].text and r[7].text and r[7].text.isdigit():
if r[1].img == "/radio.gif":
s_type = ""
elif r[8].img == "/hd.gif":
s_type = "HEVC HD"
elif r[1].img == "/data.gif":
s_type = "Data"
else:
s_type = "SD"
s_type = self._S_TYPES.get(s_type, "3")
_s_type = SERVICE_TYPE.get(s_type, SERVICE_TYPE.get("3"))
name, pkg, cas, sid, v_pid, a_pid = r[2].text, r[5].text, r[6].text, r[7].text, None, None
flags, sid, fav_id, picon_id, data_id = self.get_service_data(s_type, pkg, sid, tid, nid, nsp,
v_pid, a_pid, cas, use_pids)
services.append(Service(flags, "s", None, name, None, None, pkg, _s_type, None, picon_id,
sid, str(freq), sr, pol, fec, sys, pos, data_id, fav_id, multi_tr or tr))
return services
def get_transponder_data(self, pos, fec, sys, mod):
""" Returns converted transponder data. """
sys = get_key_by_value(SYSTEM, sys)
mod = get_key_by_value(MODULATION, mod)
fec = get_key_by_value(FEC, fec)
# For negative (West) positions: 3600 - numeric position value!!!
namespace = f"{3600 - pos if pos < 0 else pos:04x}0000"
tr_flag = 1
roll_off = 0 # 35% DVB-S2/DVB-S (default)
pilot = 2 # Auto
s2_flags = "" if sys == "DVB-S" else self._S2_TR.format(tr_flag, mod or 0, roll_off, pilot)
inv = 2 # Default
return sys, mod, fec, namespace, s2_flags, roll_off, pilot, inv
@staticmethod
def get_service_data(s_type, pkg, sid, tid, nid, namespace, v_pid, a_pid, cas, use_pids=False):
sid = int(sid)
data_id = f"{sid:04x}:{namespace}:{tid:04x}:{nid:04x}:{s_type}:0:0"
fav_id = f"{sid}:{tid}:{nid}:{namespace}"
picon_id = f"1_0_{int(s_type):X}_{sid}_{tid}_{nid}_{namespace}_0_0_0.png"
# Flags.
flags = f"p:{pkg}"
cas = ",".join(get_key_by_value(CAS, c) or "C:0000" for c in cas.split()) if cas else None
if use_pids:
v_pid = f"c:00{int(v_pid):04x}" if v_pid else None
a_pid = ",".join([f"c:01{int(p):04x}" for p in a_pid]) if a_pid else None
flags = ",".join(filter(None, (flags, v_pid, a_pid, cas)))
else:
flags = ",".join(filter(None, (flags, cas)))
return flags, sid, fav_id, picon_id, data_id
@staticmethod
def get_position(pos):
return int(SatellitesParser.get_position("".join(c for c in pos if c.isdigit() or c.isalpha())))
if __name__ == "__main__":
pass

View File

@@ -1,4 +1,32 @@
""" Module for working with YouTube service """
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" Module for working with YouTube service. """
import gzip
import json
import os
@@ -6,20 +34,22 @@ import re
import shutil
import sys
from html.parser import HTMLParser
from json import JSONDecodeError
from urllib import parse
from urllib.error import URLError
from urllib.parse import unquote
from urllib.request import Request, urlopen, urlretrieve
from app.commons import log
from app.settings import SEP
from app.ui.uicommons import show_notification
_TIMEOUT = 5
_HEADERS = {"User-Agent": "Mozilla/5.0 (Linux x86_64; rv:92.0) Gecko/20100101 Firefox/92.0",
"DNT": "1",
"Accept-Encoding": "gzip, deflate"}
_YT_PATTERN = re.compile(r"https://www.youtube.com/.+(?:v=)([\w-]{11}).*")
_YT_LIST_PATTERN = re.compile(r"https://www.youtube.com/.+?(?:list=)([\w-]{18,})?.*")
_YT_VIDEO_PATTERN = re.compile(r"https://r\d+---sn-[\w]{10}-[\w]{3,5}.googlevideo.com/videoplayback?.*")
_HEADERS = {"User-Agent": "Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/69.0",
"DNT": "1",
"Accept-Encoding": "gzip, deflate"}
Quality = {137: "1080p", 136: "720p", 135: "480p", 134: "360p",
133: "240p", 160: "144p", 0: "0p", 18: "360p", 22: "720p"}
@@ -80,6 +110,8 @@ class YouTube:
if self._settings.enable_yt_dl and url:
if not self._yt_dl:
self._yt_dl = YouTubeDL.get_instance(self._settings, self._callback)
if not self._yt_dl:
raise YouTubeException("youtube-dl initialization error.")
return self._yt_dl.get_yt_link(url, skip_errors)
return self.get_yt_link_by_id(video_id)
@@ -90,61 +122,117 @@ class YouTube:
Returns tuple from the video links dict and title.
"""
req = Request(YouTube._VIDEO_INFO_LINK.format(video_id), headers=_HEADERS)
info = InnerTube().player(video_id)
det = info.get("videoDetails", None)
title = det.get("title", None) if det else None
streaming_data = info.get("streamingData", None)
fmts = streaming_data.get("formats", None) if streaming_data else None
with urlopen(req, timeout=2) as resp:
data = unquote(gzip.decompress(resp.read()).decode("utf-8")).split("&")
out = {k: v for k, sep, v in (str(d).partition("=") for d in map(unquote, data))}
player_resp = out.get("player_response", None)
if fmts:
links = {Quality[i["itag"]]: i["url"] for i in filter(
lambda i: i.get("itag", -1) in Quality, fmts) if "url" in i}
if player_resp:
try:
resp = json.loads(player_resp)
except Exception as e:
log("{}: Parsing player response error: {}".format(__class__.__name__, e))
else:
det = resp.get("videoDetails", None)
title = det.get("title", None) if det else None
streaming_data = resp.get("streamingData", None)
fmts = streaming_data.get("formats", None) if streaming_data else None
if links and title:
return links, title.replace("+", " ")
if fmts:
urls = {Quality[i["itag"]]: i["url"] for i in
filter(lambda i: i.get("itag", -1) in Quality, fmts) if "url" in i}
cause = None
status = info.get("playabilityStatus", None)
if status:
cause = f"[{status.get('status', '')}] {status.get('reason', '')}"
if urls and title:
return urls, title.replace("+", " ")
log(f"{__class__.__name__}: Getting link to video with id '{video_id}' filed! Cause: {cause}")
stream_map = out.get("url_encoded_fmt_stream_map", None)
if stream_map:
s_map = {k: v for k, sep, v in (str(d).partition("=") for d in stream_map.split("&"))}
url, title = s_map.get("url", None), out.get("title", None)
url, title = unquote(url) if url else "", title.replace("+", " ") if title else ""
if url and title:
return {Quality[0]: url}, title.replace("+", " ")
rsn = out.get("reason", None)
rsn = rsn.replace("+", " ") if rsn else ""
log("{}: Getting link to video with id {} filed! Cause: {}".format(__class__.__name__, video_id, rsn))
return None, rsn
return None, cause
def get_yt_playlist(self, list_id, url=None):
""" Returns tuple from the playlist header and list of tuples (title, video id). """
if self._settings.enable_yt_dl and url:
try:
if not self._yt_dl:
raise YouTubeException("youtube-dl is not initialized!")
self._yt_dl.update_options({"noplaylist": False, "extract_flat": True})
info = self._yt_dl.get_info(url, skip_errors=False)
if "url" in info:
info = self._yt_dl.get_info(info.get("url"), skip_errors=False)
return info.get("title", ""), [(e.get("title", ""), e.get("id", "")) for e in info.get("entries", [])]
finally:
# Restoring default options
self._yt_dl.update_options({"noplaylist": True, "extract_flat": False})
if self._yt_dl:
self._yt_dl.update_options({"noplaylist": True, "extract_flat": False})
return PlayListParser.get_yt_playlist(list_id)
class InnerTube:
""" Object for interacting with the innertube API.
Based on InnerTube class from pytube [https://github.com/pytube/pytube] project!
"""
_BASE_URI = "https://www.youtube.com/youtubei/v1"
_DEFAULT_CLIENTS = {
"ANDROID": {
"context": {"client": {"clientName": "ANDROID", "clientVersion": "16.20"}},
"api_key": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
},
"ANDROID_EMBED": {
"context": {"client": {"clientName": "ANDROID", "clientVersion": "16.20", "clientScreen": "EMBED"}},
"api_key": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
}
}
def __init__(self, client="ANDROID"):
""" Initialize an InnerTube object.
@param client: Client to use for the object. Default to web because it returns the most playback types.
"""
self.context = self._DEFAULT_CLIENTS[client]["context"]
self.api_key = self._DEFAULT_CLIENTS[client]["api_key"]
@property
def base_data(self):
"""Return the base json data to transmit to the innertube API."""
return {"context": self.context}
@property
def base_params(self):
"""Return the base query parameters to transmit to the innertube API."""
return {"key": self.api_key, "contentCheckOk": True, "racyCheckOk": True}
def player(self, video_id):
""" Make a request to the player endpoint. Returns raw player info results. """
endpoint = f"{self._BASE_URI}/player"
query = {"videoId": video_id}
query.update(self.base_params)
return self._call_api(endpoint, query, self.base_data) or {}
@staticmethod
def _call_api(endpoint, query, data):
""" Make a request to a given endpoint with the provided query parameters and data."""
headers = {"Content-Type": "application/json", }
response = InnerTube._execute(f"{endpoint}?{parse.urlencode(query)}", "POST", headers=headers, data=data)
try:
resp = json.loads(response.read())
except JSONDecodeError as e:
log(f"{__class__.__name__}: Parsing response error: {e}")
else:
return resp
@staticmethod
def _execute(url, method=None, headers=None, data=None, timeout=_TIMEOUT):
base_headers = {"User-Agent": "Mozilla/5.0", "accept-language": "en-US,en"}
if headers:
base_headers.update(headers)
if data:
# Encoding data for request.
if not isinstance(data, bytes):
data = bytes(json.dumps(data), encoding="utf-8")
return urlopen(Request(url, headers=base_headers, method=method, data=data), timeout=timeout)
class PlayListParser(HTMLParser):
""" Very simple parser to handle YouTube playlist pages. """
@@ -170,8 +258,8 @@ class PlayListParser(HTMLParser):
try:
resp = json.loads(data)
except YouTubeException as e:
log("{}: Parsing data error: {}".format(__class__.__name__, e))
except JSONDecodeError as e:
log(f"{__class__.__name__}: Parsing data error: {e}")
else:
sb = resp.get("sidebar", None)
if sb:
@@ -189,7 +277,7 @@ class PlayListParser(HTMLParser):
self._is_script = False
def error(self, message):
log("{} Parsing error: {}".format(__class__.__name__, message))
log(f"{__class__.__name__} Parsing error: {message}")
@property
def header(self):
@@ -205,9 +293,9 @@ class PlayListParser(HTMLParser):
returns tuple from the playlist header and list of tuples (title, video id)
"""
request = Request("https://www.youtube.com/playlist?list={}&hl=en".format(play_list_id), headers=_HEADERS)
request = Request(f"https://www.youtube.com/playlist?list={play_list_id}&hl=en", headers=_HEADERS)
with urlopen(request, timeout=2) as resp:
with urlopen(request, timeout=_TIMEOUT) as resp:
data = gzip.decompress(resp.read()).decode("utf-8")
parser = PlayListParser()
parser.feed(data)
@@ -230,7 +318,7 @@ class YouTubeDL:
"cookiefile": "cookies.txt"} # File name where cookies should be read from and dumped to.
def __init__(self, settings, callback):
self._path = settings.default_data_path + "tools{}".format(SEP)
self._path = f"{settings.default_data_path}tools{SEP}"
self._update = settings.enable_yt_dl_update
self._supported = {"22", "18"}
self._dl = None
@@ -247,7 +335,7 @@ class YouTubeDL:
return cls._DL_INSTANCE
def init(self):
if not os.path.isfile(self._path + "youtube_dl{}version.py".format(SEP)):
if not os.path.isfile(f"{self._path}youtube_dl{SEP}version.py"):
self.get_latest_release()
if self._path not in sys.path:
@@ -259,17 +347,22 @@ class YouTubeDL:
try:
import youtube_dl
except ModuleNotFoundError as e:
log("YouTubeDLHelper error: {}".format(str(e)))
log(f"YouTubeDLHelper error: {e}")
raise YouTubeException(e)
except ImportError as e:
log("YouTubeDLHelper error: {}".format(str(e)))
log(f"YouTubeDLHelper error: {e}")
else:
if self._path not in youtube_dl.__file__:
msg = "Another version of youtube-dl was found on your system!"
log(msg)
raise YouTubeException(msg)
if self._update:
if hasattr(youtube_dl.version, "__version__"):
l_ver = self.get_last_release_id()
cur_ver = youtube_dl.version.__version__
if l_ver and youtube_dl.version.__version__ < l_ver:
msg = "youtube-dl has new release!\nCurrent: {}. Last: {}.".format(cur_ver, l_ver)
msg = f"youtube-dl has new release!\nCurrent: {cur_ver}. Last: {l_ver}."
show_notification(msg)
log(msg)
self._callback(msg, False)
@@ -289,7 +382,7 @@ class YouTubeDL:
with urlopen(url, timeout=10) as resp:
return json.loads(resp.read().decode("utf-8")).get("tag_name", "0")
except URLError as e:
log("YouTubeDLHelper error [get last release id]: {}".format(e))
log(f"YouTubeDLHelper error [get last release id]: {e}")
def get_latest_release(self):
try:
@@ -311,10 +404,10 @@ class YouTubeDL:
with zipfile.ZipFile(f_name) as arch:
for info in arch.infolist():
pref, sep, f = info.filename.partition("{}youtube_dl{}".format("/", "/"))
pref, sep, f = info.filename.partition("/youtube_dl/")
if sep:
arch.extract(info.filename)
shutil.move(info.filename, "{}{}{}".format(self._path, sep, f))
shutil.move(info.filename, f"{self._path}{sep}{f}")
shutil.rmtree(pref)
msg = "Getting the last youtube-dl release is done!"
show_notification(msg)
@@ -325,7 +418,7 @@ class YouTubeDL:
os.remove(zip_file)
return True
except URLError as e:
log("YouTubeDLHelper error: {}".format(e))
log(f"YouTubeDLHelper error: {e}")
raise YouTubeException(e)
finally:
self._is_update_process = False
@@ -348,10 +441,10 @@ class YouTubeDL:
try:
return self._dl.extract_info(url, download=False)
except URLError as e:
log(str(e))
log(f"YouTubeDLHelper error [get info]: {e}")
raise YouTubeException(e)
except self._DownloadError as e:
log(str(e))
log(f"YouTubeDLHelper error [get info]: {e}")
if not skip_errors:
raise YouTubeException(e)

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

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

View File

@@ -1,175 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="menu_bar">
<submenu>
<attribute name="label" translatable="yes">File</attribute>
<attribute name="action">app.hide_menu_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<section>
<submenu>
<attribute name="label" translatable="yes">Import</attribute>
<section>
<item>
<attribute name="label" translatable="yes">Bouquet</attribute>
<attribute name="action">app.on_import_bouquet</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Bouquets and services</attribute>
<attribute name="action">app.on_import_bouquets</attribute>
</item>
</section>
</submenu>
<item>
<attribute name="label" translatable="yes">Import from Web</attribute>
<attribute name="action">app.on_import_from_web</attribute>
</item>
<item>
<attribute name="label" translatable="yes">New empty configuration</attribute>
<attribute name="action">app.on_new_configuration</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Open</attribute>
<attribute name="action">app.on_data_open</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Extract...</attribute>
<attribute name="action">app.on_archive_open</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Save</attribute>
<attribute name="action">app.on_data_save</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Save as</attribute>
<attribute name="action">app.on_data_save_as</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">FTP-transfer</attribute>
<attribute name="action">app.on_download</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Settings</attribute>
<attribute name="action">app.on_settings</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Exit</attribute>
<attribute name="action">app.on_close_app</attribute>
</item>
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">Edit</attribute>
<attribute name="action">app.hide_menu_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<section>
<item>
<attribute name="label" translatable="yes">Lock</attribute>
<attribute name="action">app.on_locked</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Hide</attribute>
<attribute name="action">app.on_hide</attribute>
</item>
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">View</attribute>
<attribute name="action">app.hide_menu_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<section>
<item>
<attribute name="label" translatable="yes">Search</attribute>
<attribute name="action">win.search</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Filter</attribute>
<attribute name="action">win.filter</attribute>
</item>
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">Tools</attribute>
<attribute name="action">app.hide_menu_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<section>
<item>
<attribute name="label" translatable="yes">Satellites editor</attribute>
<attribute name="action">app.on_satellite_editor_show</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Picons manager</attribute>
<attribute name="action">app.on_picons_manager_show</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Backups</attribute>
<attribute name="action">app.on_backup_tool_show</attribute>
</item>
</section>
<section id="telnet_section">
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">IPTV</attribute>
<attribute name="action">app.hide_menu_bar</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<item>
<attribute name="label" translatable="yes">Add IPTV or stream service</attribute>
<attribute name="action">app.on_iptv</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Import YouTube playlist</attribute>
<attribute name="action">app.on_import_yt_list</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Import m3u</attribute>
<attribute name="action">app.on_import_m3u</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Export to m3u</attribute>
<attribute name="action">app.on_export_to_m3u</attribute>
</item>
<section>
<item>
<attribute name="label" translatable="yes">EPG configuration</attribute>
<attribute name="action">app.on_epg_list_configuration</attribute>
</item>
<item>
<attribute name="label" translatable="yes">List configuration</attribute>
<attribute name="action">app.on_iptv_list_configuration</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Remove all unavailable</attribute>
<attribute name="action">app.on_remove_all_unavailable</attribute>
</item>
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">FTP client</attribute>
<attribute name="action">app.show_ftp_menu</attribute>
<attribute name="hidden-when">action-disabled</attribute>
<item>
<attribute name="label" translatable="yes">Close</attribute>
<attribute name="action">app.on_ftp_client_close</attribute>
</item>
</submenu>
<submenu>
<attribute name="label" translatable="yes">Help</attribute>
<section>
<item>
<attribute name="label" translatable="yes">About</attribute>
<attribute name="action">app.on_about_app</attribute>
</item>
</section>
</submenu>
</menu>
</interface>

View File

@@ -1,3 +1,31 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
import os
import shutil
import tempfile
@@ -5,12 +33,13 @@ import time
import zipfile
from datetime import datetime
from enum import Enum
from pathlib import Path
from app.commons import run_idle
from app.settings import SettingsType
from app.commons import run_idle, get_size_from_bytes
from app.settings import SettingsType, SEP
from app.ui.dialogs import show_dialog, DialogType, get_builder
from app.ui.main_helper import append_text_to_tview
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, IS_GNOME_SESSION
class RestoreType(Enum):
@@ -34,8 +63,8 @@ class BackupDialog:
self._settings = settings
self._s_type = settings.setting_type
self._data_path = self._settings.data_local_path
self._backup_path = self._settings.backup_local_path or self._data_path + "backup/"
self._data_path = self._settings.profile_data_path
self._backup_path = self._settings.profile_backup_path or f"{self._data_path}backup{os.sep}"
self._open_data_callback = callback
self._dialog_window = builder.get_object("dialog_window")
self._dialog_window.set_transient_for(transient)
@@ -46,6 +75,25 @@ class BackupDialog:
self._info_check_button = builder.get_object("info_check_button")
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("message_label")
self._file_count_label = builder.get_object("file_count_label")
if IS_GNOME_SESSION:
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True)
self._dialog_window.set_titlebar(header_bar)
button_box = builder.get_object("main_button_box")
button_box.set_margin_top(0)
button_box.set_margin_bottom(0)
button_box.set_margin_left(0)
button_box.reparent(header_bar)
ch_button = builder.get_object("info_check_button")
ch_button.set_margin_right(0)
h_bar = builder.get_object("header_bar")
h_bar.remove(ch_button)
h_bar.set_visible(False)
header_bar.pack_end(ch_button)
# Setting the last size of the dialog window if it was saved
window_size = self._settings.get("backup_tool_window_size")
if window_size:
@@ -60,10 +108,14 @@ class BackupDialog:
def init_data(self):
if os.path.isdir(self._backup_path):
for file in filter(lambda x: x.endswith(".zip"), os.listdir(self._backup_path)):
self._model.append((file.rstrip(".zip"), False))
p = Path(os.path.join(self._backup_path, file))
if p.is_file():
self._model.append((p.stem, get_size_from_bytes(p.stat().st_size)))
else:
os.makedirs(os.path.dirname(self._backup_path), exist_ok=True)
self._file_count_label.set_text(str(len(self._model)))
def on_restore_bouquets(self, item):
self.restore(RestoreType.BOUQUETS)
@@ -83,13 +135,15 @@ class BackupDialog:
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"))
os.remove(f"{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))
self._file_count_label.set_text(str(len(self._model)))
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)
@@ -119,7 +173,7 @@ class BackupDialog:
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))
f"Created: {created}\n********** Files: **********\n")
with zipfile.ZipFile(file_name) as zip_file:
for name in zip_file.namelist():
append_text_to_tview(name + "\n", self._text_view)
@@ -149,7 +203,7 @@ class BackupDialog:
clear_data_path(self._data_path)
shutil.unpack_archive(full_file_name, self._data_path)
elif restore_type is RestoreType.BOUQUETS:
tmp_dir = tempfile.gettempdir() + "/" + file_name
tmp_dir = tempfile.gettempdir() + SEP + file_name
cond = (".tv", ".radio") if self._s_type is SettingsType.ENIGMA_2 else "bouquets.xml"
shutil.unpack_archive(full_file_name, tmp_dir)
for file in filter(lambda f: f.endswith(cond), os.listdir(self._data_path)):
@@ -188,15 +242,15 @@ def backup_data(path, backup_path, move=True):
Returns full path to the compressed file.
"""
backup_path = "{}{}/".format(backup_path, datetime.now().strftime("%Y-%m-%d_%H-%M-%S"))
backup_path = f"{backup_path}{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}{SEP}"
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
os.makedirs(os.path.dirname(path), exist_ok=True)
# backup files in data dir(skipping dirs and satellites.xml)
for file in filter(lambda f: f != "satellites.xml" and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
# Backup files in data dir(skipping dirs and *.xml).
for file in filter(lambda f: not f.endswith(".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)
# Compressing to zip and delete remaining files.
zip_file = shutil.make_archive(backup_path.rstrip(SEP), "zip", backup_path)
shutil.rmtree(backup_path)
return zip_file
@@ -209,8 +263,8 @@ def restore_data(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)):
""" Clearing data at the specified path excluding *.xml file. """
for file in filter(lambda f: not f.endswith(".xml") and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
os.remove(os.path.join(path, file))

View File

@@ -41,10 +41,10 @@ Author: Dmitriy Yefremov
</object>
<object class="GtkListStore" id="main_list_store">
<columns>
<!-- column-name date -->
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name size -->
<column type="gchararray"/>
<!-- column-name selected -->
<column type="gboolean"/>
</columns>
</object>
<object class="GtkMenu" id="popup_menu">
@@ -57,7 +57,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_restore_bouquets" swapped="no"/>
<accelerator key="r" signal="activate" modifiers="GDK_CONTROL_MASK"/>
<accelerator key="r" signal="activate" modifiers="Primary"/>
</object>
</child>
<child>
@@ -67,7 +67,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_restore_all" swapped="no"/>
<accelerator key="e" signal="activate" modifiers="GDK_CONTROL_MASK"/>
<accelerator key="e" signal="activate" modifiers="Primary"/>
</object>
</child>
<child>
@@ -123,106 +123,17 @@ Author: Dmitriy Yefremov
<object class="GtkBox" id="main_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="margin_top">1</property>
<property name="margin_bottom">1</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkPaned" id="main_paned">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="wide_handle">True</property>
<child>
<object class="GtkScrolledWindow" id="main_view_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="main_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="model">main_list_store</property>
<property name="headers_visible">False</property>
<property name="search_column">0</property>
<property name="rubber_banding">True</property>
<property name="activate_on_single_click">True</property>
<signal name="button-press-event" handler="on_view_popup_menu" object="popup_menu" swapped="no"/>
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
<signal name="key-release-event" handler="on_key_release" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="main_view_selection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="backup_date_column">
<property name="title" translatable="yes">Backup</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<property name="reorderable">True</property>
<property name="sort_column_id">0</property>
<child>
<object class="GtkCellRendererText" id="date_render">
<property name="xpad">10</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="text_view_scrolled_window">
<property name="can_focus">False</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="text_view">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="pixels_above_lines">5</property>
<property name="editable">False</property>
<property name="left_margin">10</property>
<property name="right_margin">10</property>
<property name="indent">10</property>
<property name="cursor_visible">False</property>
<property name="accepts_tab">False</property>
</object>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<object class="GtkBox" id="header_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">15</property>
<property name="margin_right">15</property>
<child>
<object class="GtkButtonBox">
<object class="GtkButtonBox" id="main_button_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="margin_left">15</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="layout_style">expand</property>
@@ -230,7 +141,7 @@ Author: Dmitriy Yefremov
<object class="GtkButton" id="restore_bouquets_header_button">
<property name="label" translatable="yes">Restore bouquets</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
<property name="image">restore_bouquets_image</property>
@@ -247,7 +158,7 @@ Author: Dmitriy Yefremov
<object class="GtkButton" id="restore_all_header_button">
<property name="label" translatable="yes">Restore all</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
<property name="image">restore_all_image</property>
@@ -264,7 +175,7 @@ Author: Dmitriy Yefremov
<object class="GtkButton" id="remove_header_button">
<property name="label" translatable="yes">Remove</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
<property name="image">remove_image</property>
@@ -287,16 +198,24 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkCheckButton" id="info_check_button">
<property name="label" translatable="yes">Details</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_focus">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Details</property>
<property name="valign">center</property>
<property name="image">details_image</property>
<property name="margin_right">15</property>
<property name="always_show_image">True</property>
<property name="draw_indicator">False</property>
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
<accelerator key="i" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
<child>
<object class="GtkImage" id="details_image1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">emblem-important-symbolic</property>
<property name="icon_size">1</property>
</object>
</child>
<accelerator key="i" signal="clicked" modifiers="Primary"/>
</object>
<packing>
<property name="expand">False</property>
@@ -305,10 +224,186 @@ Author: Dmitriy Yefremov
<property name="position">1</property>
</packing>
</child>
<style>
<class name="primary-toolbar"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="main_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</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkPaned" id="main_paned">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="wide_handle">True</property>
<child>
<object class="GtkBox" id="backups_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="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="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_name_column">
<property name="min_width">75</property>
<property name="title" translatable="yes">Name</property>
<property name="expand">True</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="name_renderer">
<property name="xpad">10</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="backup_size_column">
<property name="sizing">fixed</property>
<property name="fixed_width">120</property>
<property name="title" translatable="yes">Size</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="size_renderer">
<property name="xalign">0.49000000953674316</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="status_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">2</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="count_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-properties</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="file_count_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">0</property>
<property name="width_chars">4</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="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>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
@@ -337,6 +432,7 @@ Author: Dmitriy Yefremov
<object class="GtkLabel" id="message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="label" translatable="yes">message</property>
</object>
<packing>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@
The MIT License (MIT)
Copyright (c) 2018-2021 Dmitriy Yefremov
Copyright (c) 2018-2023 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -30,8 +30,8 @@ Author: Dmitriy Yefremov
<requires lib="gtk+" version="3.16"/>
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor for macOS. -->
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
<!-- interface-description Enigma2 channel and satellites list editor. -->
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkAboutDialog" id="about_dialog">
<property name="can_focus">False</property>
@@ -40,16 +40,15 @@ Author: Dmitriy Yefremov
<property name="icon_name">system-help</property>
<property name="type_hint">normal</property>
<property name="program_name">DemonEditor</property>
<property name="version">1.0.10 Alpha</property>
<property name="copyright">2018-2021 Dmitriy Yefremov
</property>
<property name="comments" translatable="yes">Enigma2 channel and satellite list editor for MS Windows.
(Experimental)</property>
<property name="website">https://github.com/DYefremov/DemonEditor/tree/experimental-win</property>
<property name="version">3.2.4 Beta</property>
<property name="copyright">2018-2023 Dmitriy Yefremov
</property>
<property name="comments" translatable="yes">Enigma2 channel and satellite list editor.</property>
<property name="website">https://dyefremov.github.io/DemonEditor/</property>
<property name="license" translatable="yes">Это приложение распространяется без каких-либо гарантий.
Подробнее в &lt;a href="http://opensource.org/licenses/mit-license.php"&gt;The MIT License (MIT)&lt;/a&gt;.</property>
Подробнее в &lt;a href="http://opensource.org/licenses/mit-license.php"&gt;The MIT License (MIT)&lt;/a&gt;.</property>
<property name="authors">Dmitriy Yefremov
</property>
</property>
<property name="translator_credits" translatable="yes">translator-credits</property>
<property name="artists">Program logo: &lt;a href="http://ihad.tv"&gt;mfgeg&lt;/a&gt;</property>
<property name="logo_icon_name">demon-editor</property>
@@ -92,52 +91,36 @@ Author: Dmitriy Yefremov
<property name="type_hint">utility</property>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<child type="titlebar">
<placeholder/>
<child type="action">
<object class="GtkButton" id="input_dialog_cancel_button">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
</object>
</child>
<child type="action">
<object class="GtkButton" id="input_dialog_ok_button">
<property name="label" translatable="yes">OK</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
<accelerator key="Return" signal="activate"/>
</object>
</child>
<child internal-child="vbox">
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">4</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="input_dialog_cancel_button">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="input_dialog_ok_button">
<property name="label" translatable="yes">OK</property>
<property name="width_request">100</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
<accelerator key="Return" signal="activate"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -149,11 +132,11 @@ Author: Dmitriy Yefremov
<object class="GtkEntry" id="input_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_left">2</property>
<property name="margin_right">2</property>
<property name="margin_top">2</property>
<property name="margin_bottom">2</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="secondary_icon_sensitive">False</property>

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -28,10 +28,10 @@
""" Common module for showing dialogs """
import gettext
import xml.etree.ElementTree as ET
from enum import Enum
from functools import lru_cache
from pathlib import Path
import xml.etree.ElementTree as ET
from app.commons import run_idle
from app.settings import SEP, IS_WIN
@@ -118,11 +118,12 @@ def show_dialog(dialog_type, transient, text=None, settings=None, action_type=No
return get_about_dialog(transient)
def get_chooser_dialog(transient, settings, name, patterns, title=None):
file_filter = Gtk.FileFilter()
file_filter.set_name(name)
for p in patterns:
file_filter.add_pattern(p)
def get_chooser_dialog(transient, settings, name, patterns, title=None, file_filter=None):
if not file_filter:
file_filter = Gtk.FileFilter()
file_filter.set_name(name)
for p in patterns:
file_filter.add_pattern(p)
return show_dialog(dialog_type=DialogType.CHOOSER,
transient=transient,
@@ -133,20 +134,18 @@ def get_chooser_dialog(transient, settings, name, patterns, title=None):
def get_file_chooser_dialog(transient, text, settings, action_type, file_filter, buttons=None, title=None, dirs=False):
text = get_message(text) if text else ""
action_type = Gtk.FileChooserAction.SELECT_FOLDER if action_type is None else action_type
buttons = buttons or (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
dialog = Gtk.FileChooserDialog(text, transient, action_type, buttons, use_header_bar=IS_GNOME_SESSION)
dialog.set_title(get_message(title) if title else "")
dialog = Gtk.FileChooserNative.new(get_message(title) if title else "", transient, action_type)
dialog.set_create_folders(dirs)
dialog.set_modal(True)
if file_filter is not None:
dialog.add_filter(file_filter)
dialog.set_current_folder(settings.data_local_path)
dialog.set_current_folder(settings.profile_data_path)
response = dialog.run()
if response not in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
if response == Gtk.ResponseType.ACCEPT:
path = Path(dialog.get_filename() or dialog.get_current_folder())
if path.is_dir():
response = "{}{}".format(path.resolve(), SEP)
@@ -239,15 +238,16 @@ def get_builder(path, handlers=None, use_str=False, objects=None, tag="property"
def translate_xml(path, tag="property"):
"""
Used to translate GUI from * .glade files in MS Windows.
""" Used to translate GUI from * .glade files in MS Windows.
More info: https://gitlab.gnome.org/GNOME/gtk/-/issues/569
"""
et = ET.parse(path)
root = et.getroot()
for e in root.iter(tag):
if e.attrib.get("translatable", None) == "yes":
for e in root.iter():
if e.tag == tag and e.attrib.get("translatable", None) == "yes":
e.text = get_message(e.text)
elif e.tag == "item" and e.attrib.get("translatable", None) == "yes":
e.text = get_message(e.text)
return ET.tostring(root, encoding="unicode", method="xml")

View File

@@ -1,553 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2
The MIT License (MIT)
Copyright (c) 2018-2020 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface domain="demon-editor">
<requires lib="gtk+" version="3.16"/>
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor for macOS. -->
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkImage" id="download_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">network-receive-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="send_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">network-transmit-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkWindow" id="download_dialog_window">
<property name="width_request">550</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">FTP-transfer</property>
<property name="resizable">False</property>
<property name="modal">True</property>
<property name="window_position">center-on-parent</property>
<property name="icon_name">mail-send-receive</property>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<child type="titlebar">
<placeholder/>
</child>
<child>
<object class="GtkBox" id="main_dialog_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkFrame" id="main_settings_box_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_top">10</property>
<property name="margin_bottom">5</property>
<property name="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkBox" id="main_settings_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkGrid" id="main_settings_bo">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">2</property>
<property name="column_spacing">2</property>
<property name="column_homogeneous">True</property>
<child>
<object class="GtkLabel" id="ip_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Receiver IP:</property>
<property name="xalign">0.10000000149011612</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="host_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="max_width_chars">10</property>
<property name="text">127.0.0.1</property>
<property name="caps_lock_warning">False</property>
<property name="primary_icon_name">network-transmit-receive-symbolic</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="data_path_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Current data path:</property>
<property name="xalign">0.10000000149011612</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="data_path_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="text">data/</property>
<property name="caps_lock_warning">False</property>
<property name="primary_icon_name">folder-open-symbolic</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="extra_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<child>
<object class="GtkCheckButton" id="remove_unused_check_button">
<property name="label" translatable="yes">Remove unused bouquets</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="on_remove_unused_bouquets_toggled" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="use_http_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel" id="use_http_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Use HTTP</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="use_http_switch">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="tooltip_text" translatable="yes">Use http to reload data in the receiver.</property>
<property name="active">True</property>
<signal name="state-set" handler="on_use_http_state_set" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkFrame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="selection_data_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_top">10</property>
<property name="margin_bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="label10">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="all_radio_button">
<property name="label" translatable="yes">All</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">satellites_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="bouquets_radio_button">
<property name="label" translatable="yes">Bouquets</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">satellites_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="satellites_radio_button">
<property name="label" translatable="yes">Satellites</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">all_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="webtv_radio_button">
<property name="label" translatable="yes">WebTV</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">True</property>
<property name="group">all_radio_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Profile:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="profile_combo_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="focus_on_click">False</property>
<property name="active">0</property>
<property name="has_frame">False</property>
<signal name="changed" handler="on_profile_changed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="options_button">
<property name="label" translatable="yes">Options</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Options</property>
<signal name="clicked" handler="on_settings" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkButtonBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="margin_left">20</property>
<property name="margin_right">20</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="homogeneous">True</property>
<property name="layout_style">expand</property>
<child>
<object class="GtkButton" id="receive_button">
<property name="label" translatable="yes">Receive</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Receive</property>
<property name="valign">center</property>
<property name="image">download_image</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_receive" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="send_button">
<property name="label" translatable="yes">Send</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Send</property>
<property name="valign">center</property>
<property name="image">send_image</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_send" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">6</property>
</packing>
</child>
<child>
<object class="GtkExpander" id="expander">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="margin_bottom">1</property>
<property name="resize_toplevel">True</property>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="height_request">120</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="text_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="left_margin">5</property>
<property name="right_margin">5</property>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="expander_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Extra:</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">7</property>
</packing>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="can_focus">False</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="margin_bottom">1</property>
<property name="show_close_button">True</property>
<signal name="response" handler="on_info_bar_close" swapped="no"/>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="homogeneous">True</property>
<property name="layout_style">expand</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="spacing">16</property>
<child>
<object class="GtkLabel" id="info_bar_message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Info</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">8</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View File

@@ -1,171 +0,0 @@
import os
from gi.repository import GLib
from app.commons import run_idle, run_task, log
from app.connections import download_data, DownloadType, upload_data
from app.settings import SettingsType
from app.ui.backup import backup_data, restore_data
from app.ui.main_helper import append_text_to_tview
from app.ui.settings_dialog import show_settings_dialog
from .dialogs import show_dialog, DialogType, get_message, get_builder
from .uicommons import Gtk, UI_RESOURCES_PATH
class DownloadDialog:
def __init__(self, transient, settings, open_data_callback, update_settings_callback):
self._s_type = settings.setting_type
self._settings = settings
self._open_data_callback = open_data_callback
self._update_settings_callback = update_settings_callback
handlers = {"on_receive": self.on_receive,
"on_send": self.on_send,
"on_settings": self.on_settings,
"on_profile_changed": self.on_profile_changed,
"on_use_http_state_set": self.on_use_http_state_set,
"on_remove_unused_bouquets_toggled": self.on_remove_unused_bouquets_toggled,
"on_info_bar_close": self.on_info_bar_close}
builder = get_builder(UI_RESOURCES_PATH + "download_dialog.glade", handlers)
self._dialog_window = builder.get_object("download_dialog_window")
self._dialog_window.set_transient_for(transient)
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label")
self._text_view = builder.get_object("text_view")
self._expander = builder.get_object("expander")
self._host_entry = builder.get_object("host_entry")
self._data_path_entry = builder.get_object("data_path_entry")
self._remove_unused_check_button = builder.get_object("remove_unused_check_button")
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._webtv_radio_button = builder.get_object("webtv_radio_button")
self._use_http_switch = builder.get_object("use_http_switch")
self._http_radio_button = builder.get_object("http_radio_button")
self._use_http_box = builder.get_object("use_http_box")
self._profile_combo_box = builder.get_object("profile_combo_box")
self.init_settings()
def show(self):
self._dialog_window.show()
def init_settings(self):
self.update_profiles()
self.init_ui_settings()
def init_ui_settings(self):
self._host_entry.set_text(self._settings.host)
self._data_path_entry.set_text(self._settings.data_local_path)
is_enigma = self._s_type is SettingsType.ENIGMA_2
self._webtv_radio_button.set_visible(not is_enigma)
self._use_http_box.set_visible(is_enigma)
self._use_http_switch.set_active(is_enigma and self._settings.use_http)
self._remove_unused_check_button.set_active(self._settings.remove_unused_bouquets)
def update_profiles(self):
self._profile_combo_box.remove_all()
for p in self._settings.profiles:
self._profile_combo_box.append(p, p)
self._profile_combo_box.set_active_id(self._settings.current_profile)
@run_idle
def on_receive(self, item):
self.download(True, self.get_download_type())
@run_idle
def on_send(self, item):
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 = DownloadType.ALL
if self._bouquets_radio_button.get_active():
download_type = DownloadType.BOUQUETS
elif self._satellites_radio_button.get_active():
download_type = DownloadType.SATELLITES
elif self._webtv_radio_button.get_active():
download_type = DownloadType.WEBTV
return download_type
def destroy(self):
self._dialog_window.destroy()
def on_settings(self, item):
response = show_settings_dialog(self._dialog_window, self._settings)
if response != Gtk.ResponseType.CANCEL:
self._s_type = self._settings.setting_type
self.update_profiles()
gen = self._update_settings_callback()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def on_profile_changed(self, box):
active = box.get_active_text()
if active in self._settings.profiles:
self._settings.current_profile = active
self._profile_combo_box.set_active_id(active)
self._s_type = self._settings.setting_type
self.init_ui_settings()
def on_use_http_state_set(self, button, state):
self._settings.use_http = state
def on_remove_unused_bouquets_toggled(self, button):
self._settings.remove_unused_bouquets = button.get_active()
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@run_task
def download(self, download, d_type):
""" Download/upload data from/to receiver """
GLib.idle_add(self._expander.set_expanded, True)
self.clear_output()
backup, backup_src, data_path = self._settings.backup_before_downloading, None, None
try:
if download:
if backup and d_type is not DownloadType.SATELLITES:
data_path = self._settings.data_local_path or self._data_path_entry.get_text()
os.makedirs(os.path.dirname(data_path), exist_ok=True)
backup_path = self._settings.backup_local_path or data_path + "backup/"
backup_src = backup_data(data_path, backup_path, d_type is DownloadType.ALL)
download_data(settings=self._settings, download_type=d_type, callback=self.append_output)
else:
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
upload_data(settings=self._settings,
download_type=d_type,
remove_unused=self._remove_unused_check_button.get_active(),
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:
msg = "Downloading data error: {}"
log(msg.format(e), debug=self._settings.debug_mode, fmt_message=msg)
self.show_info_message(str(e), Gtk.MessageType.ERROR)
if all((download, backup, data_path)):
restore_data(backup_src, data_path)
else:
if download and d_type is not DownloadType.SATELLITES:
GLib.idle_add(self._open_data_callback)
@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 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

0
app/ui/epg/__init__.py Normal file
View File

1557
app/ui/epg/dialog.glade Normal file

File diff suppressed because it is too large Load Diff

967
app/ui/epg/epg.py Normal file
View File

@@ -0,0 +1,967 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2023 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" Module for working with EPG. """
import gzip
import locale
import os
import re
import shutil
import urllib.request
from datetime import datetime
from enum import Enum
from urllib.error import HTTPError, URLError
from urllib.parse import quote
from gi.repository import GLib
from app.commons import run_idle, run_task, run_with_delay
from app.connections import download_data, DownloadType, HttpAPI
from app.eparser.ecommons import BouquetService, BqServiceType
from app.settings import SEP, EpgSource
from app.tools.epg import EPG, ChannelsParser, EpgEvent, XmlTvReader
from app.ui.dialogs import get_message, show_dialog, DialogType, get_builder
from app.ui.tasks import BGTaskWidget
from app.ui.timers import TimerTool
from ..main_helper import on_popup_menu, update_entry_data, scroll_to, update_toggle_model, update_filter_sat_positions
from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column, EPG_ICON, KeyboardKey, IS_GNOME_SESSION, Page
class RefsSource(Enum):
SERVICES = 0
XML = 1
class EpgCache(dict):
def __init__(self, app):
super().__init__()
self._current_bq = None
self._reader = None
self._canceled = False
self._settings = app.app_settings
self._src = self._settings.epg_source
self._app = app
self._app.connect("bouquet-changed", self.on_bouquet_changed)
self._app.connect("profile-changed", self.on_profile_changed)
self._app.connect("task-canceled", self.on_xml_load_cancel)
self.init()
@run_idle
def init(self):
if self._src is EpgSource.XML:
url = self._settings.epg_xml_source
gz_file = f"{self._settings.profile_data_path}epg{os.sep}epg.gz"
self._reader = XmlTvReader(gz_file, url)
def process_data():
t = BGTaskWidget(self._app, "Processing XMLTV data...", self._reader.parse, )
self._app.emit("add-background-task", t)
if os.path.isfile(gz_file):
# Difference calculation between the current time and file modification.
dif = datetime.now() - datetime.fromtimestamp(os.path.getmtime(gz_file))
# We will update daily. -> Temporarily!!!
if dif.days > 0 and not self._canceled:
task = BGTaskWidget(self._app, "Downloading EPG...", self._reader.download, process_data, )
self._app.emit("add-background-task", task)
else:
process_data()
else:
if not self._canceled:
task = BGTaskWidget(self._app, "Downloading EPG...", self._reader.download, process_data, )
self._app.emit("add-background-task", task)
elif self._src is EpgSource.DAT:
self._reader = EPG.DatReader(f"{self._settings.profile_data_path}epg{os.sep}epg.dat")
self._reader.download()
GLib.timeout_add_seconds(self._settings.epg_update_interval, self.update_epg_data, priority=GLib.PRIORITY_LOW)
def on_bouquet_changed(self, app, bq):
self._current_bq = bq
def on_profile_changed(self, app, p):
self.clear()
def on_xml_load_cancel(self, app, widget):
self._canceled = True
def update_epg_data(self):
if self._src is EpgSource.HTTP:
api = self._app.http_api
bq = self._app.current_bouquet_files.get(self._current_bq, None)
if bq and api:
req = quote(f'FROM BOUQUET "userbouquet.{bq}.{self._current_bq.split(":")[-1]}"')
api.send(HttpAPI.Request.EPG_NOW, f'1:7:1:0:0:0:0:0:0:0:{req}', self.update_http_data)
elif self._src is EpgSource.XML:
self.update_xml_data()
return self._app.display_epg
def update_http_data(self, epg):
for e in (EpgTool.get_event(e, False) for e in epg.get("event_list", []) if e.get("e2eventid", "").isdigit()):
self[e.event_data.get("e2eventservicename", "")] = e
@run_task
def update_xml_data(self):
services = self._app.current_services
names = {services[s].service for s in self._app.current_bouquets.get(self._current_bq, [])}
for name, e in self._reader.get_current_events(names).items():
self[name] = e
def get_current_event(self, service_name):
return self.get(service_name, EpgEvent())
class EpgSettingsPopover(Gtk.Popover):
def __init__(self, app, **kwarg):
super().__init__(**kwarg)
self._app = app
self._app.connect("profile-changed", self.on_profile_changed)
handlers = {"on_apply": self.on_apply,
"on_close": lambda b: self.popdown()}
builder = get_builder(f"{UI_RESOURCES_PATH}epg{SEP}settings.glade", handlers)
self.add(builder.get_object("main_box"))
self._http_src_button = builder.get_object("http_src_button")
self._xml_src_button = builder.get_object("xml_src_button")
self._dat_src_button = builder.get_object("dat_src_button")
self._interval_button = builder.get_object("interval_button")
self._url_entry = builder.get_object("url_entry")
self._dat_path_box = builder.get_object("dat_path_box")
self.init()
def init(self):
settings = self._app.app_settings
src = settings.epg_source
if src is EpgSource.HTTP:
self._http_src_button.set_active(True)
elif src is EpgSource.XML:
self._xml_src_button.set_active(True)
else:
self._dat_src_button.set_active(True)
self._interval_button.set_value(settings.epg_update_interval)
self._url_entry.set_text(settings.epg_xml_source)
self._dat_path_box.set_active_id(settings.epg_dat_path)
def on_apply(self, button):
settings = self._app.app_settings
if self._http_src_button.get_active():
settings.epg_source = EpgSource.HTTP
elif self._xml_src_button.get_active():
settings.epg_source = EpgSource.XML
else:
settings.epg_source = EpgSource.DAT
settings.epg_update_interval = self._interval_button.get_value()
settings.epg_xml_source = self._url_entry.get_text()
settings.epg_dat_path = self._dat_path_box.get_active_id()
self.popdown()
self._app.change_action_state("display_epg", GLib.Variant.new_boolean(True))
def on_profile_changed(self, app, p):
self.init()
class EpgTool(Gtk.Box):
def __init__(self, app, *args, **kwargs):
super().__init__(*args, **kwargs)
self._current_bq = None
self._app = app
self._app.connect("fav-changed", self.on_service_changed)
self._app.connect("bouquet-changed", self.on_bouquet_changed)
handlers = {"on_epg_press": self.on_epg_press,
"on_timer_add": self.on_timer_add,
"on_epg_filter_changed": self.on_epg_filter_changed,
"on_epg_filter_toggled": self.on_epg_filter_toggled,
"on_view_query_tooltip": self.on_view_query_tooltip,
"on_multi_epg_toggled": self.on_multi_epg_toggled}
builder = get_builder(f"{UI_RESOURCES_PATH}epg{SEP}tab.glade", handlers)
self._view = builder.get_object("epg_view")
self._model = builder.get_object("epg_model")
self._filter_model = builder.get_object("epg_filter_model")
self._filter_model.set_visible_func(self.epg_filter_function)
self._filter_entry = builder.get_object("epg_filter_entry")
self._multi_epg_button = builder.get_object("multi_epg_button")
self._event_count_label = builder.get_object("event_count_label")
self.pack_start(builder.get_object("epg_frame"), True, True, 0)
# Custom sort function.
self._view.get_model().set_sort_func(2, self.time_sort_func, 2)
self.show()
def on_timer_add(self, action=None, value=None):
model, paths = self._view.get_selection().get_selected_rows()
p_count = len(paths)
if p_count == 1:
dialog = TimerTool.TimerDialog(self._app.app_window, TimerTool.TimerAction.EVENT, model[paths][-1])
response = dialog.run()
if response == Gtk.ResponseType.OK:
gen = self.write_timers_list([dialog.get_request()])
GLib.idle_add(lambda: next(gen, False))
dialog.destroy()
elif p_count > 1:
if show_dialog(DialogType.QUESTION, self._app.app_window,
"Add timers for selected events?") != Gtk.ResponseType.OK:
return True
self.add_timers_list((model[p][-1] for p in paths))
else:
self._app.show_error_message("No selected item!")
def add_timers_list(self, paths):
ref_str = "timeraddbyeventid?sRef={}&eventid={}&justplay=0"
refs = [ref_str.format(quote(ev.get("e2eventservicereference", "")), ev.get("e2eventid", "")) for ev in paths]
gen = self.write_timers_list(refs)
GLib.idle_add(lambda: next(gen, False))
def write_timers_list(self, refs):
self._app.wait_dialog.show()
tasks = list(refs)
for ref in refs:
self._app.send_http_request(HttpAPI.Request.TIMER, ref, lambda x: tasks.pop())
yield True
while tasks:
yield True
self._app.emit("change-page", Page.TIMERS.value)
def on_epg_press(self, view, event):
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(view.get_model()) > 0:
self.on_timer_add()
def on_service_changed(self, app, ref):
if app.page is Page.EPG:
if self._multi_epg_button.get_active():
ref += ":"
path = next((r.path for r in self._model if r[-1].get("e2eventservicereference", None) == ref), None)
scroll_to(path, self._view) if path else None
else:
self._app.wait_dialog.show()
self._app.send_http_request(HttpAPI.Request.EPG, quote(ref), self.update_epg_data)
@run_idle
def update_epg_data(self, epg):
self._event_count_label.set_text("0")
self._model.clear()
list(map(self._model.append, (self.get_event(e) for e in epg.get("event_list", [])
if e.get("e2eventid", "").isdigit())))
self._event_count_label.set_text(str(len(self._model)))
self._app.wait_dialog.hide()
@staticmethod
def get_event(event, show_day=True):
t_str = f"{'%a, ' if show_day else ''}%x, %H:%M"
s_name = event.get("e2eventservicename", "")
title = event.get("e2eventtitle", "") or ""
desc = event.get("e2eventdescription", "") or ""
desc = desc.strip()
start = int(event.get("e2eventstart", "0"))
start_time = datetime.fromtimestamp(start)
end_time = datetime.fromtimestamp(start + int(event.get("e2eventduration", "0")))
ev_time = f"{start_time.strftime(t_str)} - {end_time.strftime('%H:%M')}"
return EpgEvent(s_name, title, ev_time, desc, event)
def on_epg_filter_changed(self, entry):
self._filter_model.refilter()
def on_epg_filter_toggled(self, button):
if not button.get_active():
self._filter_entry.set_text("")
def epg_filter_function(self, model, itr, data):
txt = self._filter_entry.get_text().upper()
return next((s for s in model.get(itr, 0, 1, 2, 3) if txt in s.upper()), False)
def time_sort_func(self, model, iter1, iter2, column):
""" Custom sort function for time column. """
event1 = model.get_value(iter1, 4)
event2 = model.get_value(iter2, 4)
return int(event1.get("e2eventstart", "0")) - int(event2.get("e2eventstart", "0"))
def on_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
dst = view.get_dest_row_at_pos(x, y)
if not dst:
return False
path, pos = dst
model = view.get_model()
data = model[path][-1]
desc = data.get("e2eventdescription", "") or ""
ext_desc = data.get("e2eventdescriptionextended", "") or ""
tooltip.set_text(ext_desc if ext_desc else desc)
view.set_tooltip_row(tooltip, path)
return True
def on_multi_epg_toggled(self, button):
self._model.clear()
self._event_count_label.set_text("0")
if button.get_active():
self.get_multi_epg()
def on_bouquet_changed(self, app, bq):
self._current_bq = bq
if app.page is Page.EPG and self._multi_epg_button.get_active():
self.get_multi_epg()
def get_multi_epg(self):
if not self._current_bq:
return
self._app.wait_dialog.show()
bq = self._app.current_bouquet_files.get(self._current_bq, None)
api = self._app.http_api
if bq and api:
tm = datetime.now().timestamp()
req = quote(f'FROM BOUQUET "userbouquet.{bq}.{self._current_bq.split(":")[-1]}"&time={tm}')
api.send(HttpAPI.Request.EPG_MULTI, f'1:7:1:0:0:0:0:0:0:0:{req}', self.update_epg_data, timeout=15)
class EpgDialog:
def __init__(self, app, 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_satellite_toggled": self.on_filter_satellite_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_press": self.on_key_press,
"on_bq_cursor_changed": self.on_bq_cursor_changed}
self._app = app
self._ex_services = self._app.current_services
self._ex_fav_model = self._app.fav_view.get_model()
self._settings = self._app.app_settings
self._bouquet_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._download_xml_is_active = False
self._sat_positions = None
builder = get_builder(f"{UI_RESOURCES_PATH}epg{SEP}dialog.glade", handlers)
self._dialog = builder.get_object("epg_dialog_window")
self._dialog.set_transient_for(self._app.app_window)
self._source_view = builder.get_object("source_view")
self._bouquet_view = builder.get_object("bouquet_view")
self._bouquet_model = builder.get_object("bouquet_list_store")
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_action_box = builder.get_object("left_action_box")
self._xml_download_progress_bar = builder.get_object("xml_download_progress_bar")
self._src_load_spinner = builder.get_object("src_load_spinner")
# Filter
self._filter_bar = builder.get_object("filter_bar")
self._filter_entry = builder.get_object("filter_entry")
self._filter_auto_switch = builder.get_object("filter_auto_switch")
self._services_filter_model = builder.get_object("services_filter_model")
self._services_filter_model.set_visible_func(self.services_filter_function)
self._sat_pos_filter_model = builder.get_object("sat_pos_filter_model")
# 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")
if IS_GNOME_SESSION:
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True, title="EPG",
subtitle=get_message("List configuration"))
self._dialog.set_titlebar(header_bar)
builder.get_object("left_action_box").reparent(header_bar)
right_box = builder.get_object("right_action_box")
builder.get_object("main_actions_box").remove(right_box)
header_bar.pack_end(right_box)
builder.get_object("toolbar_box").set_visible(False)
self._app.connect("epg-dat-downloaded", self.on_epg_dat_downloaded)
# Setting the last size of the dialog window
window_size = self._settings.get("epg_tool_window_size")
if window_size:
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.OK:
return
p = re.compile(r"\d+")
updated = {}
for i, row in enumerate(self._bouquet_model):
if row[Column.FAV_LOCKED]:
fav_id = self._ex_fav_model[row.path][Column.FAV_ID]
srv = self._ex_services.pop(fav_id, None)
if srv:
new_fav_id, picon_id = row[Column.FAV_ID], row[Column.FAV_POS]
if picon_id:
picon_id = re.sub(p, re.search(p, srv.picon_id).group(), picon_id, count=1)
else:
picon_id = srv.picon_id
new = srv._replace(fav_id=new_fav_id, data_id=new_fav_id.strip(), picon_id=picon_id)
self._ex_services[new_fav_id] = new
updated[fav_id] = (srv, new)
if updated:
self._app.emit("iptv-service-edited", updated)
self._dialog.destroy()
@run_idle
def on_update(self, item=None):
self.clear_data()
self.init_options()
if self._update_epg_data_on_start:
self.download_epg_from_stb()
else:
gen = self.init_data()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def clear_data(self):
self._services_model.clear()
self._bouquet_model.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:
try:
epg_reader = EPG.DatReader(f"{self._epg_dat_path_entry.get_text()}epg.dat")
epg_reader.read()
refs = epg_reader.get_refs()
except (OSError, ValueError) as e:
self.show_info_message(f"Read data error: {e}", Gtk.MessageType.ERROR)
return
yield True
self._src_load_spinner.start()
if self._refs_source is RefsSource.SERVICES:
yield from 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)
self._src_load_spinner.stop()
yield True
def init_bouquet_data(self):
for r in self._ex_fav_model:
row = [*r[:]]
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())
factor = self._app.DEL_FACTOR / 4
for index, srv in enumerate(filtered):
self._services_model.append((srv.service, srv.pos, srv.fav_id, srv.picon_id))
if index % factor == 0:
yield True
self.update_source_count_info()
yield True
def init_xml_source(self, refs):
path = self._epg_dat_path_entry.get_text() if self._use_web_source else self._xml_chooser_button.get_filename()
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(f"{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(f"{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(f"{get_message('XML parsing error:')} {e}")
else:
if refs:
s_refs = filter(lambda x: x.num in refs, s_refs)
factor = self._app.DEL_FACTOR / 4
for index, srv in enumerate(s_refs):
self._services_model.append((srv.name, " ", srv.data, ""))
if index % factor == 0:
yield True
self.update_source_info(info)
self.update_source_count_info()
yield True
def on_key_press(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()
def on_bq_cursor_changed(self, view):
if self._filter_bar.get_visible() and self._filter_auto_switch.get_active():
path, column = view.get_cursor()
model = view.get_model()
if path:
self._filter_entry.set_text(model[path][Column.FAV_SERVICE] or "")
@run_idle
def on_save_to_xml(self, item):
response = show_dialog(DialogType.CHOOSER, self._dialog, settings=self._settings)
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
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_refs(self, model, paths, data):
[self.assign_data(model[p], data) for p in paths]
self.update_epg_count()
def assign_data(self, row, data, show_error=False):
if row[Column.FAV_TYPE] != BqServiceType.IPTV.value:
if not show_error:
self.show_info_message(get_message("Not allowed in this context!"), Gtk.MessageType.ERROR)
return
fav_id = row[Column.FAV_ID]
fav_id_data = fav_id.split(":")
fav_id_data[3:7] = data[-2].split(":")
if data[-1]:
row[Column.FAV_POS] = data[-1]
p_data = data[-1].split("_")
if p_data:
fav_id_data[2] = p_data[2]
new_fav_id = ":".join(fav_id_data)
row[Column.FAV_ID] = new_fav_id
row[Column.FAV_LOCKED] = EPG_ICON
pos = f"({data[1] if self._refs_source is RefsSource.SERVICES else 'XML'})"
src = f"{get_message('EPG source')}: {(GLib.markup_escape_text(data[0] or ''))} {pos}"
row[Column.FAV_TOOLTIP] = f"{get_message('Service reference')}: {':'.join(fav_id_data[:10])}\n{src}"
def on_filter_toggled(self, button):
self._filter_bar.set_visible(button.get_active())
if button.get_active():
self._sat_positions = {r[1] for r in self._services_model}
update_filter_sat_positions(self._sat_pos_filter_model, self._sat_positions)
else:
self._sat_positions = None
self._filter_entry.set_text("") if self._filter_entry.get_text() else self.on_filter_changed()
def on_filter_satellite_toggled(self, toggle, path):
update_toggle_model(self._sat_pos_filter_model, path, toggle)
self._sat_positions.clear()
self._sat_positions.update({r[0] for r in self._sat_pos_filter_model if r[1]})
self.on_filter_changed()
@run_with_delay(2)
def on_filter_changed(self, entry=None):
self._services_filter_model.refilter()
def services_filter_function(self, model, itr, data):
txt = self._filter_entry.get_text().upper()
pos = model.get_value(itr, 1)
pos = self._sat_positions is None or self._xml_radiobutton.get_active() or pos in self._sat_positions
return model is None or model == "None" or (txt in model.get_value(itr, 0).upper() and pos)
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][:])
def on_assign_ref(self, item=None):
if self._current_ref:
model, paths = self._bouquet_view.get_selection().get_selected_rows()
self.assign_refs(model, paths, self._current_ref.pop())
@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):
row[Column.FAV_LOCKED], row[Column.FAV_TOOLTIP], row[Column.FAV_POS] = None, 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_action_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, drag_context, data, info, time):
model, paths = view.get_selection().get_selected_rows()
if paths:
s_data = model[paths][:]
if all(s_data[:-1]):
data.set_text("::::".join(s_data), -1)
else:
self.show_info_message(get_message("Source error!"), Gtk.MessageType.ERROR)
def on_drag_data_received(self, view, drag_context, x, y, data, info, time):
path, pos = view.get_dest_row_at_pos(x, y)
model = view.get_model()
data = data.get_text()
if data:
data = data.split("::::")
self.assign_refs(model, path, data)
return False
# ***************** Options *********************#
def init_options(self):
epg_dat_path = "{}epg{}".format(self._settings.profile_data_path, SEP)
self._epg_dat_path_entry.set_text(epg_dat_path)
default_epg_data_stb_path = "/etc/enigma2"
epg_options = self._settings.epg_options
if epg_options:
self._refs_source = RefsSource.XML if epg_options.get("xml_source", False) else RefsSource.SERVICES
self._xml_radiobutton.set_active(self._refs_source is RefsSource.XML)
self._use_web_source = epg_options.get("use_web_source", False)
self._use_web_source_switch.set_active(self._use_web_source)
self._url_to_xml_entry.set_text(epg_options.get("url_to_xml", ""))
self._enable_dat_filter = epg_options.get("enable_filtering", False)
self._enable_filtering_switch.set_active(self._enable_dat_filter)
epg_dat_path = epg_options.get("epg_dat_path", epg_dat_path)
self._epg_dat_path_entry.set_text(epg_dat_path)
self._epg_dat_stb_path_entry.set_text(epg_options.get("epg_dat_stb_path", default_epg_data_stb_path))
self._update_epg_data_on_start = epg_options.get("epg_data_update_on_start", False)
self._update_on_start_switch.set_active(self._update_epg_data_on_start)
local_xml_path = epg_options.get("local_path_to_xml", None)
if local_xml_path:
self._xml_chooser_button.set_filename(local_xml_path)
os.makedirs(os.path.dirname(self._epg_dat_path_entry.get_text()), exist_ok=True)
def on_options_save(self, item=None):
self._settings.epg_options = {"xml_source": self._xml_radiobutton.get_active(),
"use_web_source": self._use_web_source_switch.get_active(),
"local_path_to_xml": self._xml_chooser_button.get_filename(),
"url_to_xml": self._url_to_xml_entry.get_text(),
"enable_filtering": self._enable_filtering_switch.get_active(),
"epg_dat_path": self._epg_dat_path_entry.get_text(),
"epg_dat_stb_path": self._epg_dat_stb_path_entry.get_text(),
"epg_data_update_on_start": self._update_on_start_switch.get_active()}
def on_resize(self, window):
if self._settings:
self._settings.add("epg_tool_window_size", window.get_size())
def on_names_source_changed(self, button):
self._refs_source = RefsSource.XML if button.get_active() else RefsSource.SERVICES
self._names_source_box.set_sensitive(button.get_active())
def on_enable_filtering_switch(self, switch, state):
self._epg_dat_source_box.set_sensitive(state)
self._update_on_start_switch.set_active(False if not state else self._update_epg_data_on_start)
def on_update_on_start_switch(self, switch, state):
pass
def on_use_web_source_switch(self, switch, state):
self._web_source_box.set_sensitive(state)
self._xml_chooser_button.set_sensitive(not state)
def on_field_icon_press(self, entry, icon, event_button):
update_entry_data(entry, self._dialog, self._settings)
# ***************** Downloads *********************#
def on_epg_dat_downloaded(self, app, value):
gen = self.init_data()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
@run_task
def download_epg_from_stb(self):
""" Download the epg.dat file via ftp from the receiver. """
try:
download_data(settings=self._settings, download_type=DownloadType.EPG, callback=print)
except Exception as e:
GLib.idle_add(self.show_info_message, f"Download epg.dat file error: {e}", Gtk.MessageType.ERROR)
else:
GLib.idle_add(self._app.emit, "epg-dat-downloaded", None)
if __name__ == "__main__":
pass

356
app/ui/epg/settings.glade Normal file
View File

@@ -0,0 +1,356 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2
The MIT License (MIT)
Copyright (c) 2018-2022 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<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-2022 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkAdjustment" id="interval_adjustment">
<property name="lower">3</property>
<property name="upper">60</property>
<property name="value">3</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkBox" id="main_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="label" translatable="yes">Source:</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="src_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkButtonBox" id="source_selection_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="layout_style">expand</property>
<child>
<object class="GtkRadioButton" id="http_src_button">
<property name="label" translatable="yes">Receiver</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">dat_src_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="xml_src_button">
<property name="label" translatable="yes">XML TV</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">dat_src_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="dat_src_button">
<property name="label" translatable="yes">*.dat file</property>
<property name="visible">False</property>
<property name="sensitive">False</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">http_src_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="interval_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="interval_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Update interval (sec):</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="interval_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="max_width_chars">4</property>
<property name="adjustment">interval_adjustment</property>
<property name="climb_rate">1</property>
<property name="numeric">True</property>
<property name="value">3</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="xml_source_box">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<property name="sensitive" bind-source="xml_src_button" bind-property="active"/>
<child>
<object class="GtkLabel" id="url_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Url to *.xml.gz file:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="url_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">network-transmit-receive-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="input_purpose">url</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="download_interval_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="download_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Update:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="download_interval_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="focus_on_click">False</property>
<property name="halign">end</property>
<property name="active">0</property>
<property name="active_id">daily</property>
<items>
<item id="daily" translatable="yes">Daily</item>
</items>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox" id="dat_source_box">
<property name="visible">False</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<property name="sensitive" bind-source="dat_src_button" bind-property="active"/>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">STB path:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="dat_path_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="active">0</property>
<property name="active_id">/etc/enigma2</property>
<items>
<item id="/etc/enigma2/">/etc/enigma2/</item>
<item id="/media/hdd/">/media/hdd/</item>
<item id="/media/usb/">/media/usb/</item>
<item id="/media/mmc/">/media/mmc/</item>
<item id="/media/cf/">/media/cf/</item>
</items>
</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">4</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButtonBox" id="actions_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="spacing">5</property>
<property name="homogeneous">True</property>
<property name="layout_style">expand</property>
<child>
<object class="GtkButton" id="apply_button">
<property name="label" translatable="yes">Apply</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="on_apply" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="close_button">
<property name="label" translatable="yes">Close</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="on_close" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">16</property>
</packing>
</child>
</object>
</interface>

426
app/ui/epg/tab.glade Normal file
View File

@@ -0,0 +1,426 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2
The MIT License (MIT)
Copyright (c) 2018-2023 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface domain="demon-editor">
<requires lib="gtk+" version="3.22"/>
<!-- interface-css-provider-path style.css -->
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkListStore" id="epg_model">
<columns>
<!-- column-name service -->
<column type="gchararray"/>
<!-- column-name title -->
<column type="gchararray"/>
<!-- column-name time -->
<column type="gchararray"/>
<!-- column-name description -->
<column type="gchararray"/>
<!-- column-name data -->
<column type="PyObject"/>
</columns>
</object>
<object class="GtkTreeModelFilter" id="epg_filter_model">
<property name="child-model">epg_model</property>
</object>
<object class="GtkTreeModelSort" id="epg_sort_model">
<property name="model">epg_filter_model</property>
</object>
<object class="GtkFrame" id="epg_frame">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label-xalign">0.49000000953674316</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkBox" id="epg_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-top">2</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="epg_action_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="border-width">5</property>
<property name="spacing">5</property>
<child type="center">
<object class="GtkToggleButton" id="multi_epg_button">
<property name="label" translatable="yes">Multi EPG</property>
<property name="name">header-button</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<signal name="toggled" handler="on_multi_epg_toggled" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="src_combo_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">EPG source</property>
<property name="active">0</property>
<property name="has-entry">True</property>
<property name="active-id">0</property>
<items>
<item id="0" translatable="yes">Receiver</item>
</items>
<child internal-child="entry">
<object class="GtkEntry">
<property name="name">header-entry</property>
<property name="can-focus">False</property>
<property name="editable">False</property>
<property name="width-chars">10</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="GtkToggleButton" id="epg_filter_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Filter</property>
<signal name="toggled" handler="on_epg_filter_toggled" swapped="no"/>
<child>
<object class="GtkImage" id="epg_filter_button_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">edit-find-replace-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="epg_add_timer_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Add timer</property>
<property name="always-show-image">True</property>
<signal name="clicked" handler="on_timer_add" swapped="no"/>
<child>
<object class="GtkImage" id="add_timer_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">alarm-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="epg_fs_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-bottom">5</property>
<property name="spacing">10</property>
<child>
<object class="GtkSearchEntry" id="epg_filter_entry">
<property name="visible" bind-source="epg_filter_button" bind-property="active">False</property>
<property name="can-focus">True</property>
<property name="primary-icon-name">edit-find-replace-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="primary-icon-sensitive">False</property>
<signal name="search-changed" handler="on_epg_filter_changed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="fav_search_box">
<property name="can-focus">False</property>
<property name="valign">center</property>
<child>
<object class="GtkSearchEntry" id="epg_search_entry">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="primary-icon-name">edit-find-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="primary-icon-sensitive">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="epg_search_down_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkArrow" id="epg_down_arrow">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="arrow-type">down</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="epg_search_up_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkArrow" id="epg_up_arrow">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="arrow-type">up</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
<style>
<class name="group"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="epg_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="epg_view">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="model">epg_sort_model</property>
<property name="fixed-height-mode">True</property>
<property name="rubber-banding">True</property>
<property name="enable-grid-lines">both</property>
<property name="tooltip-column">3</property>
<signal name="button-press-event" handler="on_epg_press" swapped="no"/>
<signal name="query-tooltip" handler="on_view_query_tooltip" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="epg_selection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="epg_service_column">
<property name="visible" bind-source="multi_epg_button" bind-property="active">False</property>
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="fixed-width">100</property>
<property name="min-width">40</property>
<property name="title" translatable="yes">Service</property>
<property name="alignment">0.49000000953674316</property>
<property name="sort-column-id">0</property>
<child>
<object class="GtkCellRendererText" id="epg_service_renderer">
<property name="xpad">5</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="epg_title_column">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="fixed-width">170</property>
<property name="min-width">50</property>
<property name="title" translatable="yes">Title</property>
<property name="alignment">0.49000000953674316</property>
<property name="sort-column-id">1</property>
<child>
<object class="GtkCellRendererText" id="epg_title_renderer">
<property name="xpad">5</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="epg_time_column">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="fixed-width">210</property>
<property name="min-width">50</property>
<property name="title" translatable="yes">Time</property>
<property name="alignment">0.49000000953674316</property>
<property name="sort-column-id">2</property>
<child>
<object class="GtkCellRendererText" id="epg_time_renderer">
<property name="xpad">5</property>
<property name="xalign">0.49000000953674316</property>
</object>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="epg_desc_column">
<property name="sizing">fixed</property>
<property name="fixed-width">100</property>
<property name="min-width">50</property>
<property name="title" translatable="yes">Description</property>
<property name="expand">True</property>
<property name="alignment">0.49000000953674316</property>
<property name="sort-column-id">3</property>
<child>
<object class="GtkCellRendererText" id="epg_desc_renderer">
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">3</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>
<child>
<object class="GtkBox" id="status_box">
<property name="height-request">26</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="event_count_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">document-properties</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="event_count_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">0</property>
<property name="width-chars">4</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="epg_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">EPG</property>
</object>
</child>
</object>
</interface>

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,215 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" Simple FTP client module. """
import stat
import subprocess
from collections import namedtuple
from datetime import datetime
from enum import IntEnum
from ftplib import all_errors
from io import TextIOWrapper, BytesIO
from pathlib import Path
from shutil import rmtree
from urllib.parse import urlparse, unquote
from gi.repository import GLib
from app.commons import log, run_task, run_idle
from app.commons import log, run_task, run_idle, get_size_from_bytes
from app.connections import UtfFTP
from app.settings import IS_WIN, SEP
from app.ui.dialogs import show_dialog, DialogType, get_builder
from app.settings import IS_LINUX, IS_DARWIN, IS_WIN, SEP
from app.ui.dialogs import show_dialog, DialogType, get_builder, get_message
from app.ui.main_helper import on_popup_menu
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, IS_GNOME_SESSION, Page
File = namedtuple("File", ["icon", "name", "size", "date", "attr", "extra"])
class BaseDialog(Gtk.Dialog):
""" Base class for additional FTP dialogs. """
def __init__(self, title, use_header_bar=0, *args, **kwargs):
super().__init__(title=title, use_header_bar=use_header_bar, *args, **kwargs)
self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK)
self.set_modal(True)
self.set_skip_pager_hint(True)
self.set_skip_taskbar_hint(True)
self.set_position(Gtk.PositionType.BOTTOM)
self.set_default_icon_name("document-properties-symbolic")
class TextEditDialog(BaseDialog):
""" Simple text edit dialog. """
def __init__(self, path, use_header_bar=0, *args, **kwargs):
super().__init__(title=f"DemonEditor [{path}]", use_header_bar=use_header_bar, *args, **kwargs)
content_box = self.get_content_area()
self._search_entry = Gtk.SearchEntry(visible=True, primary_icon_name="system-search-symbolic")
self._search_entry.connect("search-changed", self.on_search_changed)
if use_header_bar:
bar = self.get_header_bar()
bar.pack_start(self._search_entry)
bar.set_title("DemonEditor")
bar.set_subtitle(path)
else:
search_bar = Gtk.SearchBar(visible=True)
search_bar.add(self._search_entry)
search_bar.set_search_mode(True)
content_box.pack_start(search_bar, False, False, 0)
scrolled_window = Gtk.ScrolledWindow(hexpand=True, vexpand=True,
min_content_width=720,
min_content_height=320)
content_box.pack_start(scrolled_window, True, True, 0)
try:
import gi
gi.require_version("GtkSource", "3.0")
from gi.repository import GtkSource
except (ImportError, ValueError) as e:
self._text_view = Gtk.TextView()
self._buf = self._text_view.get_buffer()
log(e)
else:
self._text_view = GtkSource.View(show_line_numbers=True, show_line_marks=True)
self._buf = self._text_view.get_buffer()
self._buf.set_highlight_syntax(True)
self._buf.set_highlight_matching_brackets(True)
lang_manager = GtkSource.LanguageManager.new()
self._buf.set_language(lang_manager.guess_language(path))
# Style
self._buf.set_style_scheme(GtkSource.StyleSchemeManager().get_default().get_scheme("tango"))
self._tag_found = self._buf.create_tag("found", background="yellow")
scrolled_window.add(self._text_view)
self.show_all()
@property
def text(self):
return self._buf.get_text(self._buf.get_start_iter(), self._buf.get_end_iter(), include_hidden_chars=True)
@text.setter
def text(self, value):
self._buf.set_text(value)
def on_search_changed(self, entry):
self._buf.remove_tag(self._tag_found, self._buf.get_start_iter(), self._buf.get_end_iter())
cursor_mark = self._buf.get_insert()
start = self._buf.get_iter_at_mark(cursor_mark)
if start.get_offset() == self._buf.get_char_count():
start = self._buf.get_start_iter()
self.search_and_mark(entry.get_text(), start)
def search_and_mark(self, text, start, first=True):
end = self._buf.get_end_iter()
match = start.forward_search(text, 0, end)
if match is not None:
match_start, match_end = match
self._buf.apply_tag(self._tag_found, match_start, match_end)
if first:
self._text_view.scroll_to_iter(match_start, 0.0, False, 0.0, 0.0)
GLib.idle_add(self.search_and_mark, text, match_end, False)
class AttributesDialog(BaseDialog):
""" Dialog for editing file attributes (permissions). """
def __init__(self, attrs, use_header_bar=0, *args, **kwargs):
super().__init__(title=get_message("Permissions"), use_header_bar=use_header_bar, *args, **kwargs)
self.set_default_size(360, 100)
self.set_resizable(False)
builder = get_builder(f"{UI_RESOURCES_PATH}ftp.glade", use_str=True, objects=("attributes_box",))
content_box = self.get_content_area()
content_box.pack_start(builder.get_object("attributes_box"), True, True, 0)
self._num_value_entry = builder.get_object("num_value_entry")
# Buttons.
self._owner_read_button = builder.get_object("owner_read_button")
self._group_read_button = builder.get_object("group_read_button")
self._others_read_button = builder.get_object("others_read_button")
self._owner_write_button = builder.get_object("owner_write_button")
self._group_write_button = builder.get_object("group_write_button")
self._others_write_button = builder.get_object("others_write_button")
self._owner_exec_button = builder.get_object("owner_exec_button")
self._group_exec_button = builder.get_object("group_exec_button")
self._others_exec_button = builder.get_object("others_exec_button")
self.init_attrs(attrs)
for b in (self._owner_read_button, self._group_read_button, self._others_read_button, self._owner_write_button,
self._group_write_button, self._others_write_button, self._owner_exec_button, self._group_exec_button,
self._others_exec_button):
b.connect("toggled", self.update_num_value)
self.show_all()
@property
def permissions(self):
return self._num_value_entry.get_text()
def init_attrs(self, attrs):
# Owner.
self._owner_read_button.set_active(attrs[1] != "-")
self._owner_write_button.set_active(attrs[2] != "-")
self._owner_exec_button.set_active(attrs[3] != "-")
# Group.
self._group_read_button.set_active(attrs[4] != "-")
self._group_write_button.set_active(attrs[5] != "-")
self._group_exec_button.set_active(attrs[6] != "-")
# Others.
self._others_read_button.set_active(attrs[7] != "-")
self._others_write_button.set_active(attrs[8] != "-")
self._others_exec_button.set_active(attrs[9] != "-")
self.update_num_value()
def update_num_value(self, button=None):
val = 0
val |= stat.S_IRUSR if self._owner_read_button.get_active() else val
val |= stat.S_IWUSR if self._owner_write_button.get_active() else val
val |= stat.S_IXUSR if self._owner_exec_button.get_active() else val
val |= stat.S_IRGRP if self._group_read_button.get_active() else val
val |= stat.S_IWGRP if self._group_write_button.get_active() else val
val |= stat.S_IXGRP if self._group_exec_button.get_active() else val
val |= stat.S_IROTH if self._others_read_button.get_active() else val
val |= stat.S_IWOTH if self._others_write_button.get_active() else val
val |= stat.S_IXOTH if self._others_exec_button.get_active() else val
self._num_value_entry.set_text(f"{val:o}")
class FtpClientBox(Gtk.HBox):
""" Simple FTP client base class. """
ROOT = ".."
@@ -42,6 +232,8 @@ class FtpClientBox(Gtk.HBox):
self.set_orientation(Gtk.Orientation.VERTICAL)
self._app = app
self._app.connect("data-receive", self.on_receive)
self._app.connect("data-send", self.on_send)
self._settings = settings
self._ftp = None
self._select_enabled = True
@@ -50,12 +242,18 @@ class FtpClientBox(Gtk.HBox):
"on_disconnect": self.on_disconnect,
"on_ftp_row_activated": self.on_ftp_row_activated,
"on_file_row_activated": self.on_file_row_activated,
"on_bookmark_activated": self.on_bookmark_activated,
"on_ftp_edit": self.on_ftp_edit,
"on_ftp_edited": self.on_ftp_edited,
"on_file_edit": self.on_file_edit,
"on_file_edited": self.on_file_edited,
"on_ftp_rename": self.on_ftp_rename,
"on_ftp_renamed": self.on_ftp_renamed,
"on_ftp_attr_change": self.on_ftp_attr_change,
"on_ftp_copy": self.on_ftp_copy,
"on_file_rename": self.on_file_rename,
"on_file_renamed": self.on_file_renamed,
"on_file_copy": self.on_file_copy,
"on_file_remove": self.on_file_remove,
"on_ftp_remove": self.on_ftp_file_remove,
"on_bookmark_remove": self.on_bookmark_remove,
"on_file_create_folder": self.on_file_create_folder,
"on_ftp_create_folder": self.on_ftp_create_folder,
"on_view_drag_begin": self.on_view_drag_begin,
@@ -64,14 +262,16 @@ class FtpClientBox(Gtk.HBox):
"on_file_drag_data_get": self.on_file_drag_data_get,
"on_file_drag_data_received": self.on_file_drag_data_received,
"on_view_drag_end": self.on_view_drag_end,
"on_bookmark_add": self.on_bookmark_add,
"on_view_popup_menu": on_popup_menu,
"on_view_key_press": self.on_view_key_press,
"on_view_press": self.on_view_press,
"on_view_release": self.on_view_release}
"on_view_release": self.on_view_release,
"on_paned_size_allocate": self.on_paned_size_allocate}
builder = get_builder(UI_RESOURCES_PATH + "ftp.glade", handlers)
builder = get_builder(f"{UI_RESOURCES_PATH}ftp.glade", handlers)
self.add(builder.get_object("main_frame"))
self.add(builder.get_object("main_ftp_box"))
self._ftp_info_label = builder.get_object("ftp_info_label")
self._ftp_view = builder.get_object("ftp_view")
self._ftp_model = builder.get_object("ftp_list_store")
@@ -79,13 +279,20 @@ class FtpClientBox(Gtk.HBox):
self._file_view = builder.get_object("file_view")
self._file_model = builder.get_object("file_list_store")
self._file_name_renderer = builder.get_object("file_name_column_renderer")
self._bookmark_view = builder.get_object("bookmarks_view")
self._bookmark_model = builder.get_object("bookmarks_list_store")
# Buttons
self._connect_button = builder.get_object("connect_button")
disconnect_button = builder.get_object("disconnect_button")
disconnect_button.bind_property("visible", builder.get_object("ftp_actions_box"), "sensitive")
disconnect_button.bind_property("visible", builder.get_object("ftp_create_folder_menu_item"), "sensitive")
disconnect_button.bind_property("visible", builder.get_object("ftp_edit_menu_item"), "sensitive")
disconnect_button.bind_property("visible", builder.get_object("ftp_rename_menu_item"), "sensitive")
disconnect_button.bind_property("visible", builder.get_object("ftp_remove_menu_item"), "sensitive")
disconnect_button.bind_property("visible", builder.get_object("add_ftp_bookmark_button"), "sensitive")
self._connect_button.bind_property("visible", builder.get_object("disconnect_button"), "visible", 4)
self._bookmarks_button = builder.get_object("bookmarks_button")
self._bookmarks_button.bind_property("active", builder.get_object("bookmarks_box"), "visible")
# Force Ctrl
self._ftp_view.connect("key-press-event", self._app.force_ctrl)
self._file_view.connect("key-press-event", self._app.force_ctrl)
@@ -101,8 +308,17 @@ class FtpClientBox(Gtk.HBox):
self.init_file_data()
self.show()
def on_receive(self, app, page):
if page is Page.FTP:
self.on_ftp_copy()
def on_send(self, app, page):
if page is Page.FTP:
self.on_file_copy()
@run_task
def init_ftp(self):
self.init_bookmarks()
GLib.idle_add(self._ftp_model.clear)
try:
if self._ftp:
@@ -141,7 +357,7 @@ class FtpClientBox(Gtk.HBox):
@run_task
def init_file_data(self, path=None):
self.append_file_data(Path(path if path else self._settings.data_local_path))
self.append_file_data(Path(path if path else self._settings.profile_data_path))
@run_idle
def append_file_data(self, path: Path):
@@ -166,7 +382,7 @@ class FtpClientBox(Gtk.HBox):
r_size = self.LINK
icon = self._link_icon
else:
r_size = self.get_size_from_bytes(size)
r_size = get_size_from_bytes(size)
self._file_model.append(File(icon, p.name, r_size, date, str(p.resolve()), size))
@@ -176,7 +392,7 @@ class FtpClientBox(Gtk.HBox):
self._ftp_model.append(File(None, self.ROOT, None, None, self._ftp.pwd(), "0"))
for f in files:
f_data = f.split()
f_data = self._ftp.get_file_data(f)
f_type = f_data[0][0]
is_dir = f_type == "d"
is_link = f_type == "l"
@@ -190,10 +406,10 @@ class FtpClientBox(Gtk.HBox):
r_size = self.LINK
icon = self._link_icon
else:
r_size = self.get_size_from_bytes(size)
r_size = get_size_from_bytes(size)
date = "{}, {} {}".format(f_data[5], f_data[6], f_data[7])
self._ftp_model.append(File(icon, " ".join(f_data[8:]), r_size, date, f_data[0], size))
date = f"{f_data[5]}, {f_data[6]} {f_data[7]}"
self._ftp_model.append(File(icon, f_data[8], r_size, date, f_data[0], size))
def on_connect(self, item=None):
self.init_ftp()
@@ -211,10 +427,14 @@ class FtpClientBox(Gtk.HBox):
if size == self.FOLDER or f_path == self.ROOT:
self.init_ftp_data(f_path)
elif size == self.LINK:
name, sep, f_path = f_path.partition("->")
if f_path:
self.init_ftp_data(f_path.strip())
else:
b_size = row[self.Column.EXTRA]
if b_size.isdigit() and int(b_size) > self.MAX_SIZE:
self._app.show_error_dialog("The file size is too large!")
self._app.show_error_message("The file size is too large!")
else:
self.open_ftp_file(f_path)
@@ -232,7 +452,7 @@ class FtpClientBox(Gtk.HBox):
def open_file(self, path):
GLib.idle_add(self._file_view.set_sensitive, False)
try:
cmd = ["start" if IS_WIN else "xdg-open", path]
cmd = [self.get_open_file_cmd(), path]
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=IS_WIN).communicate()
finally:
GLib.idle_add(self._file_view.set_sensitive, True)
@@ -244,10 +464,10 @@ class FtpClientBox(Gtk.HBox):
try:
import tempfile
import os
path = os.path.expanduser("~/Desktop") if IS_WIN else None
path = os.path.expanduser("~/Desktop") if not IS_LINUX else None
with tempfile.NamedTemporaryFile(mode="wb", dir=path, delete=not IS_WIN) as tf:
msg = "Downloading file: {}. Status: {}"
with tempfile.NamedTemporaryFile(mode="wb", dir=path, delete=IS_LINUX) as tf:
msg = "Downloading file: {}. Status: {}"
try:
status = self._ftp.retrbinary("RETR " + f_path, tf.write)
self.update_ftp_info(msg.format(f_path, status))
@@ -255,24 +475,84 @@ class FtpClientBox(Gtk.HBox):
self.update_ftp_info(msg.format(f_path, e))
tf.flush()
cmd = ["start" if IS_WIN else "xdg-open", tf.name]
cmd = [self.get_open_file_cmd(), tf.name]
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=IS_WIN).communicate()
finally:
GLib.idle_add(self._ftp_view.set_sensitive, True)
def on_ftp_edit(self, renderer):
@staticmethod
def get_open_file_cmd():
if IS_DARWIN:
return "open"
elif IS_WIN:
return "start"
return "xdg-open"
@run_task
def on_ftp_edit(self, item=None):
path = self.get_ftp_edit_path()
if path:
row = self._ftp_model[path]
f_path = row[self.Column.NAME]
size = row[self.Column.SIZE]
if size == self.FOLDER or f_path == self.ROOT:
self._app.show_error_message("Not allowed in this context!")
else:
b_size = row[self.Column.EXTRA]
if b_size.isdigit() and int(b_size) > self.MAX_SIZE / 5:
self._app.show_error_message("The file size is too large!")
else:
msg = "Retrieving file: {}. Status: {}"
io = BytesIO()
try:
status = self._ftp.retrbinary("RETR " + f_path, io.write)
self.update_ftp_info(msg.format(f_path, status))
except all_errors as e:
self.update_ftp_info(msg.format(f_path, e))
else:
io.seek(0)
self.show_edit_dialog(f_path, TextIOWrapper(io, errors="ignore").read())
def on_ftp_edited(self, f_path, txt_data):
buf = BytesIO()
buf.write(txt_data.encode())
buf.seek(0)
msg = "Uploading file: {}. Status: {}"
try:
status = self._ftp.storbinary(f"STOR {f_path}", buf)
self.update_ftp_info(msg.format(f_path, status))
except all_errors as e:
self.update_ftp_info(msg.format(f_path, e))
@run_idle
def show_edit_dialog(self, f_path, data):
dialog = TextEditDialog(f_path, IS_GNOME_SESSION)
dialog.text = data
ok = Gtk.ResponseType.OK
if dialog.run() == ok and show_dialog(DialogType.QUESTION, self._app.app_window) == ok:
self.on_ftp_edited(f_path, dialog.text)
dialog.destroy()
def on_ftp_rename(self, renderer):
path = self.get_ftp_edit_path()
if path:
renderer.set_property("editable", True)
self._ftp_view.set_cursor(path, self._ftp_view.get_column(0), True)
def get_ftp_edit_path(self):
model, paths = self._ftp_view.get_selection().get_selected_rows()
if not paths:
return
if len(paths) > 1:
self._app.show_error_dialog("Please, select only one item!")
self._app.show_error_message("Please, select only one item!")
return
return paths
renderer.set_property("editable", True)
self._ftp_view.set_cursor(paths, self._ftp_view.get_column(0), True)
def on_ftp_edited(self, renderer, path, new_value):
def on_ftp_renamed(self, renderer, path, new_value):
renderer.set_property("editable", False)
row = self._ftp_model[path]
old_name = row[self.Column.NAME]
@@ -280,20 +560,42 @@ class FtpClientBox(Gtk.HBox):
return
resp = self._ftp.rename_file(old_name, new_value)
self.update_ftp_info("{} Status: {}".format(old_name, resp))
self.update_ftp_info(f"{old_name} Status: {resp}")
if resp[0] == "2":
row[self.Column.NAME] = new_value
def on_file_edit(self, renderer):
def on_ftp_attr_change(self, item):
path = self.get_ftp_edit_path()
if path:
row = self._ftp_model[path]
file = row[self.Column.NAME]
if file == self.ROOT:
self._app.show_error_message("Not allowed in this context!")
return
attrs = row[self.Column.ATTR]
if len(attrs) != 10:
log(f"Init attributes error [{attrs}]. Invalid length!")
return
dialog = AttributesDialog(attrs, IS_GNOME_SESSION)
ok = Gtk.ResponseType.OK
if dialog.run() == ok and show_dialog(DialogType.QUESTION, self._app.app_window) == ok:
log(self._ftp.sendcmd(f"SITE CHMOD {dialog.permissions} {file}"))
f_data = self._ftp.sendcmd(f"STAT {file}").split()
row[self.Column.ATTR] = f_data[2] if len(f_data) > 3 else attrs
dialog.destroy()
def on_file_rename(self, renderer):
model, paths = self._file_view.get_selection().get_selected_rows()
if len(paths) > 1:
self._app.show_error_dialog("Please, select only one item!")
self._app.show_error_message("Please, select only one item!")
return
renderer.set_property("editable", True)
self._file_view.set_cursor(paths, self._file_view.get_column(0), True)
def on_file_edited(self, renderer, path, new_value):
def on_file_renamed(self, renderer, path, new_value):
renderer.set_property("editable", False)
row = self._file_model[path]
old_name = row[self.Column.NAME]
@@ -303,17 +605,25 @@ class FtpClientBox(Gtk.HBox):
path = Path(row[self.Column.ATTR])
if path.exists():
try:
new_path = path.rename("{}/{}".format(path.parent, new_value))
new_path = path.rename(f"{path.parent}/{new_value}")
except ValueError as e:
log(e)
self._app.show_error_dialog(str(e))
self._app.show_error_message(str(e))
else:
if new_path.name == new_value:
row[self.Column.NAME] = new_value
row[self.Column.ATTR] = str(new_path.resolve())
def on_file_copy(self, item=None):
uris = self.get_file_uris()
self.copy_to_ftp(uris) if uris else None
def on_ftp_copy(self, item=None):
uris = self.get_ftp_uris()
self.copy_to_pc(uris) if uris else None
def on_file_remove(self, item=None):
if show_dialog(DialogType.QUESTION, self._app._main_window) != Gtk.ResponseType.OK:
if show_dialog(DialogType.QUESTION, self._app.app_window) != Gtk.ResponseType.OK:
return
model, paths = self._file_view.get_selection().get_selected_rows()
@@ -331,7 +641,7 @@ class FtpClientBox(Gtk.HBox):
list(map(model.remove, to_delete))
def on_ftp_file_remove(self, item=None):
if show_dialog(DialogType.QUESTION, self._app._main_window) != Gtk.ResponseType.OK:
if show_dialog(DialogType.QUESTION, self._app.app_window) != Gtk.ResponseType.OK:
return
model, paths = self._ftp_view.get_selection().get_selected_rows()
@@ -357,13 +667,13 @@ class FtpClientBox(Gtk.HBox):
name = self.get_new_folder_name(self._file_model)
cur_path = self._file_model.get_value(itr, self.Column.ATTR)
path = Path("{}/{}".format(cur_path, name))
path = Path(f"{cur_path}/{name}")
try:
path.mkdir()
except OSError as e:
log(e)
self._app.show_error_dialog(str(e))
self._app.show_error_message(str(e))
else:
itr = self._file_model.append(File(self._folder_icon, path.name, self.FOLDER, "", str(path.resolve()), "0"))
renderer.set_property("editable", True)
@@ -378,13 +688,13 @@ class FtpClientBox(Gtk.HBox):
name = self.get_new_folder_name(self._ftp_model)
try:
folder = "{}/{}".format(cur_path, name)
folder = f"{cur_path}/{name}"
resp = self._ftp.mkd(folder)
except all_errors as e:
self.update_ftp_info(str(e))
log(e)
else:
if resp == "{}/{}".format(cur_path, name):
if resp == f"{cur_path}/{name}":
itr = self._ftp_model.append(File(self._folder_icon, name, self.FOLDER, "", "drwxr-xr-x", "0"))
renderer.set_property("editable", True)
self._ftp_view.set_cursor(self._ftp_model.get_path(itr), self._ftp_view.get_column(0), True)
@@ -396,7 +706,7 @@ class FtpClientBox(Gtk.HBox):
count = 0
while name in names:
count += 1
name = "{}{}".format(name, count)
name = f"{name}{count}"
return name
# ***************** Drag-and-drop ********************* #
@@ -425,66 +735,84 @@ class FtpClientBox(Gtk.HBox):
return True
def on_ftp_drag_data_get(self, view, context, data, info, time):
model, paths = view.get_selection().get_selected_rows()
uris = self.get_ftp_uris()
data.set_uris(uris) if uris else None
def get_ftp_uris(self):
""" Returns the selected paths in FTP view as a list containing uris string or None. """
model, paths = self._ftp_view.get_selection().get_selected_rows()
if len(paths) > 0:
sep = self.URI_SEP if IS_WIN else "\n"
sep = self.URI_SEP if self._settings.is_darwin else "\n"
uris = []
for r in [model[p][:] for p in paths]:
if r[self.Column.SIZE] != self.LINK and r[self.Column.NAME] != self.ROOT:
path = Path("/{}:{}".format(r[self.Column.NAME], r[self.Column.ATTR]))
path = Path(f"/{r[self.Column.NAME]}:{r[self.Column.ATTR]}")
uris.append(str(path.resolve()) if IS_WIN else path.as_uri())
data.set_uris([sep.join(uris)])
return [sep.join(uris)]
@run_task
def on_ftp_drag_data_received(self, view, context, x, y, data: Gtk.SelectionData, info, time):
if not self._ftp:
return
self.copy_to_ftp(data.get_uris())
Gtk.drag_finish(context, True, False, time)
return True
@run_task
def copy_to_ftp(self, uris):
resp = "2"
try:
GLib.idle_add(self._app._wait_dialog.show)
GLib.idle_add(self._app.wait_dialog.show)
uris = data.get_uris()
if IS_WIN and len(uris) == 1:
uris = uris[0].split(self.URI_SEP)
if len(uris) == 1:
uris = uris[0].split(self.URI_SEP if self._settings.is_darwin else "\n")
for uri in uris:
uri = urlparse(unquote(uri)).path.strip()
path = Path(uri.lstrip("/") if IS_WIN else uri)
uri = urlparse(unquote(uri)).path
if IS_WIN:
uri = uri.lstrip("/")
path = Path(uri)
if path.is_dir():
try:
self._ftp.mkd(path.name)
except all_errors as e:
pass # NOP
self._ftp.cwd(path.name)
resp = self._ftp.upload_dir(str(path.resolve()) + "/", self.update_ftp_info)
resp = self._ftp.upload_dir(str(path.resolve()) + SEP, self.update_ftp_info)
else:
resp = self._ftp.send_file(path.name, str(path.parent) + "/", callback=self.update_ftp_info)
resp = self._ftp.send_file(path.name, str(path.parent) + SEP, callback=self.update_ftp_info)
finally:
GLib.idle_add(self._app._wait_dialog.hide)
GLib.idle_add(self._app.wait_dialog.hide)
if resp and resp[0] == "2":
itr = self._ftp_model.get_iter_first()
if itr:
self.init_ftp_data(self._ftp_model.get_value(itr, self.Column.ATTR))
def on_file_drag_data_get(self, view, context, data, info, time):
uris = self.get_file_uris()
data.set_uris(uris) if uris else None
def get_file_uris(self):
""" Returns the selected paths in the file view as a list containing uris string or None. """
model, paths = self._file_view.get_selection().get_selected_rows()
if len(paths) > 0:
sep = self.URI_SEP if self._settings.is_darwin else "\n"
return [sep.join([Path(model[p][self.Column.ATTR]).as_uri() for p in paths])]
def on_file_drag_data_received(self, view, context, x, y, data, info, time):
self.copy_to_pc(data.get_uris())
Gtk.drag_finish(context, True, False, time)
return True
def on_file_drag_data_get(self, view, context, data: Gtk.SelectionData, info, time):
model, paths = view.get_selection().get_selected_rows()
if len(paths) > 0:
sep = self.URI_SEP if IS_WIN else "\n"
uris = [sep.join([Path(model[p][self.Column.ATTR]).as_uri() for p in paths])]
data.set_uris(uris)
@run_task
def on_file_drag_data_received(self, view, context, x, y, data, info, time):
cur_path = self._file_model.get_value(self._file_model.get_iter_first(), self.Column.ATTR) + SEP
def copy_to_pc(self, uris):
cur_path = self._file_model.get_value(self._file_model.get_iter_first(), self.Column.ATTR) + "/"
try:
GLib.idle_add(self._app._wait_dialog.show)
GLib.idle_add(self._app.wait_dialog.show)
uris = data.get_uris()
if IS_WIN and len(uris) == 1:
uris = uris[0].split(self.URI_SEP)
if len(uris) == 1:
uris = uris[0].split(self.URI_SEP if self._settings.is_darwin else "\n")
for uri in uris:
name, sep, attr = unquote(Path(uri).name).partition(":")
@@ -498,12 +826,9 @@ class FtpClientBox(Gtk.HBox):
except OSError as e:
log(e)
finally:
GLib.idle_add(self._app._wait_dialog.hide)
GLib.idle_add(self._app.wait_dialog.hide)
self.init_file_data(cur_path)
Gtk.drag_finish(context, True, False, time)
return True
def on_view_drag_end(self, view, context):
self._select_enabled = True
view.get_selection().unselect_all()
@@ -514,6 +839,27 @@ class FtpClientBox(Gtk.HBox):
self._ftp_info_label.set_text(message)
self._ftp_info_label.set_tooltip_text(message)
# **************** Bookmarks ***************** #
@run_idle
def init_bookmarks(self):
self._bookmark_model.clear()
list(map(lambda b: self._bookmark_model.append((b,)), self._settings.ftp_bookmarks))
def on_bookmark_activated(self, view, path, column):
self.init_ftp_data(self._bookmark_model[path][0])
def on_bookmark_add(self, item=None):
self._bookmarks_button.set_active(True)
self._bookmark_model.append((self._ftp_model.get_value(self._ftp_model.get_iter_first(), 4),))
self._settings.ftp_bookmarks = [r[0] for r in self._bookmark_model]
def on_bookmark_remove(self, item=None):
model, paths = self._bookmark_view.get_selection().get_selected_rows()
if paths and show_dialog(DialogType.QUESTION, self._app.app_window) == Gtk.ResponseType.OK:
list(map(lambda p: model.remove(model.get_iter(p)), paths))
self._settings.ftp_bookmarks = [r[0] for r in self._bookmark_model]
def on_view_key_press(self, view, event):
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
@@ -529,14 +875,28 @@ class FtpClientBox(Gtk.HBox):
self.on_file_create_folder(self._file_name_renderer)
elif key is KeyboardKey.F2 or ctrl and KeyboardKey.R:
if self._ftp_view.is_focus():
self.on_ftp_edit(self._ftp_name_renderer)
self.on_ftp_rename(self._ftp_name_renderer)
elif self._file_view.is_focus():
self.on_file_edit(self._file_name_renderer)
self.on_file_rename(self._file_name_renderer)
elif key is KeyboardKey.F4:
if self._ftp_view.is_focus():
self.on_ftp_edit()
elif key is KeyboardKey.F5:
if self._ftp_view.is_focus():
self.on_ftp_copy()
elif self._file_view.is_focus():
self.on_file_copy()
elif key is KeyboardKey.DELETE:
if self._ftp_view.is_focus():
self.on_ftp_file_remove()
elif self._file_view.is_focus():
self.on_file_remove()
elif self._bookmark_view.is_focus():
self.on_bookmark_remove()
elif key is KeyboardKey.RETURN:
path, column = view.get_cursor()
if path:
view.emit("row-activated", path, column)
def on_view_press(self, view, event):
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_PRIMARY:
@@ -550,24 +910,11 @@ class FtpClientBox(Gtk.HBox):
# Enable selection.
self._select_enabled = True
def get_size_from_bytes(self, size):
""" Simple convert function from bytes to other units like K, M or G. """
try:
b = float(size)
except ValueError:
return size
else:
kb, mb, gb = 1024.0, 1048576.0, 1073741824.0
if b < kb:
return str(b)
elif kb <= b < mb:
return "{0:.1f} K".format(b / kb)
elif mb <= b < gb:
return "{0:.1f} M".format(b / mb)
elif gb <= b:
return "{0:.1f} G".format(b / gb)
@staticmethod
def on_paned_size_allocate(paned, allocation):
""" Sets default homogeneous sizes. """
paned.set_position(0.5 * allocation.width)
if __name__ == '__main__':
if __name__ == "__main__":
pass

View File

@@ -1,414 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1
The MIT License (MIT)
Copyright (c) 2018-2020 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface domain="demon-editor">
<requires lib="gtk+" version="3.16"/>
<!-- interface-css-provider-path style.css -->
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor for macOS. -->
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkImage" id="details_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">emblem-important-symbolic</property>
</object>
<object class="GtkImage" id="import_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-revert-symbolic-rtl</property>
<property name="icon_size">1</property>
</object>
<object class="GtkListStore" id="main_list_store">
<columns>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name type -->
<column type="gchararray"/>
<!-- column-name selected -->
<column type="gboolean"/>
</columns>
</object>
<object class="GtkImage" id="remove_selection_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-undo</property>
</object>
<object class="GtkMenu" id="popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="select_all_popup_item">
<property name="label">gtk-select-all</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<signal name="activate" handler="on_select_all" object="main_view" swapped="no"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="unselect_all_popup_item">
<property name="label" translatable="yes">Remove selection</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">remove_selection_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_unselect_all" object="main_view" swapped="no"/>
</object>
</child>
</object>
<object class="GtkListStore" id="services_list_store">
<columns>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name type -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkWindow" id="dialog_window">
<property name="can_focus">False</property>
<property name="title" translatable="yes">Import</property>
<property name="modal">True</property>
<property name="window_position">center-on-parent</property>
<property name="default_width">480</property>
<property name="default_height">320</property>
<property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property>
<signal name="check-resize" handler="on_resize" swapped="no"/>
<child>
<placeholder/>
</child>
<child>
<object class="GtkBox" id="main_box">
<property name="width_request">480</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="margin_bottom">1</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkPaned" id="main_paned">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="wide_handle">True</property>
<child>
<object class="GtkBox" id="bouquets_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="label" translatable="yes">Bouquets</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="bouquets_screlled_window">
<property name="width_request">200</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="main_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">main_list_store</property>
<property name="headers_clickable">False</property>
<property name="search_column">0</property>
<signal name="button-press-event" handler="on_popup_menu" object="popup_menu" swapped="no"/>
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
<signal name="select-all" handler="on_select_all" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="bouquet_name_column">
<property name="title" translatable="yes">Name</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="bq_name_renderer">
<property name="xalign">0.019999999552965164</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="bouquet_type_column">
<property name="title" translatable="yes">Type</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="bq_type_renderer">
<property name="xalign">0.50999999046325684</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="bouquet_selected_column">
<property name="title" translatable="yes">Selected</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererToggle" id="bq_selected_renderer">
<property name="xalign">0.50999999046325684</property>
<signal name="toggled" handler="on_selected_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">2</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<object class="GtkBox" id="services_box">
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="label" translatable="yes">Bouquet details</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="services_view_scrolled_window">
<property name="width_request">150</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="services_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">services_list_store</property>
<property name="headers_clickable">False</property>
<property name="search_column">0</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="service_name_column">
<property name="title" translatable="yes">Service</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="info_name_renderer">
<property name="xalign">0.019999999552965164</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="service_type_column">
<property name="title" translatable="yes">Type</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="info_type_renderer">
<property name="xalign">0.50999999046325684</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButtonBox" id="actions_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="margin_left">15</property>
<property name="margin_right">15</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="layout_style">start</property>
<child>
<object class="GtkButton" id="import_button">
<property name="label" translatable="yes">Import</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Bouquets and services</property>
<property name="valign">center</property>
<property name="image">import_image</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_import" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="info_check_button">
<property name="label" translatable="yes">Details</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Details</property>
<property name="valign">center</property>
<property name="image">details_image</property>
<property name="always_show_image">True</property>
<property name="draw_indicator">False</property>
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
<accelerator key="i" signal="clicked"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
<property name="secondary">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="can_focus">False</property>
<property name="show_close_button">True</property>
<signal name="response" handler="on_info_bar_close" swapped="no"/>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="spacing">6</property>
<property name="layout_style">end</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="spacing">16</property>
<child>
<object class="GtkLabel" id="message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">message</property>
<property name="wrap">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

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

@@ -0,0 +1,851 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2
The MIT License (MIT)
Copyright (c) 2018-2022 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface domain="demon-editor">
<requires lib="gtk+" version="3.20"/>
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellite list editor. -->
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkListStore" id="bq_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="details_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">emblem-important-symbolic</property>
</object>
<object class="GtkImage" id="import_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">document-revert-symbolic-rtl</property>
<property name="icon_size">1</property>
</object>
<object class="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="bq_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="bq_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="bq_view" swapped="no"/>
</object>
</child>
</object>
<object class="GtkImage" id="remove_selection_image1">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">edit-undo</property>
</object>
<object class="GtkMenu" id="sat_popup_menu">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkImageMenuItem" id="sat_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="sat_view" swapped="no"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="sat_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_image1</property>
<property name="use-stock">False</property>
<signal name="activate" handler="on_unselect_all" object="sat_view" swapped="no"/>
</object>
</child>
</object>
<object class="GtkImage" id="remove_services_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="services_popup_menu">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkImageMenuItem" id="select_all_services_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="services_view" swapped="no"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="unselect_all_services_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_services_selection_image</property>
<property name="use-stock">False</property>
<signal name="activate" handler="on_unselect_all" object="services_view" swapped="no"/>
</object>
</child>
</object>
<object class="GtkListStore" id="sat_list_store">
<columns>
<!-- column-name position -->
<column type="gchararray"/>
<!-- column-name type -->
<column type="gchararray"/>
<!-- column-name selected -->
<column type="gboolean"/>
</columns>
</object>
<object class="GtkListStore" id="services_list_store">
<columns>
<!-- column-name name -->
<column type="gchararray"/>
<!-- column-name type -->
<column type="gchararray"/>
<!-- column-name selected -->
<column type="gboolean"/>
<!-- column-name background -->
<column type="GdkRGBA"/>
<!-- column-name id -->
<column type="gchararray"/>
</columns>
<signal name="row-changed" handler="on_services_model_changed" swapped="no"/>
</object>
<object class="GtkWindow" id="dialog_window">
<property name="can-focus">False</property>
<property name="title" translatable="yes">Import</property>
<property name="modal">True</property>
<property name="window-position">center-on-parent</property>
<property name="default-width">480</property>
<property name="default-height">320</property>
<property name="destroy-with-parent">True</property>
<property name="type-hint">dialog</property>
<signal name="check-resize" handler="on_resize" swapped="no"/>
<child>
<object class="GtkBox" id="main_box">
<property name="width-request">480</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="toolbar_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkBox" id="actions_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="valign">center</property>
<property name="margin-start">15</property>
<property name="margin-end">15</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="spacing">5</property>
<child type="center">
<object class="GtkStackSwitcher" id="stack_switcher">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Group by</property>
<property name="stack">stack</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="import_button">
<property name="label" translatable="yes">Import</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Bouquets and services</property>
<property name="valign">center</property>
<property name="image">import_image</property>
<property name="always-show-image">True</property>
<signal name="clicked" handler="on_import" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkToggleButton" id="details_button">
<property name="label" translatable="yes">Details</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="image">details_image</property>
<property name="always-show-image">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<style>
<class name="primary-toolbar"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkPaned" id="paned">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-bottom">5</property>
<property name="wide-handle">True</property>
<signal name="realize" handler="on_main_paned_realize" swapped="no"/>
<child>
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<signal name="notify::visible-child-name" handler="on_visible_page" swapped="no"/>
<child>
<object class="GtkFrame" id="bouquets_box_frame">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label-xalign">0.49000000953674316</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkBox" id="bouquets_box">
<property name="width-request">100</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="bouquets_screlled_window">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkTreeView" id="bq_view">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="model">bq_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="bq_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"/>
</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>
<property name="ellipsize">end</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.5099999904632568</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">
<signal name="toggled" handler="on_bq_selected_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">2</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="bouquets_status_box">
<property name="height-request">26</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="bouquets_count_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">document-properties</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="bouquets_count_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="label" translatable="yes">Bouquets</property>
</object>
</child>
</object>
<packing>
<property name="name">services</property>
<property name="title" translatable="yes">Bouquets</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="sat_box_frame">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label-xalign">0.49000000953674316</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkBox" id="satellites_box">
<property name="width-request">100</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="satellites_screlled_window">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkTreeView" id="sat_view">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="model">sat_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="sat_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="realize" handler="on_sat_view_realize" swapped="no"/>
<signal name="select-all" handler="on_select_all" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="sat_position_column">
<property name="title" translatable="yes">Position</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="sat_position_renderer">
<property name="xalign">0.019999999552965164</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="sat_selected_column">
<property name="title" translatable="yes">Selected</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererToggle" id="sat_selected_renderer">
<signal name="toggled" handler="on_sat_selected_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">2</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="sat_status_box">
<property name="height-request">26</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="sat_count_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">document-properties</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="sat_count_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="label" translatable="yes">Satellites</property>
</object>
</child>
</object>
<packing>
<property name="name">satellite</property>
<property name="title" translatable="yes">Satellites</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="resize">False</property>
<property name="shrink">False</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="bq_services_box_frame">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label-xalign">0.49000000953674316</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkBox" id="services_box">
<property name="width-request">100</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="services_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="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>
<signal name="button-press-event" handler="on_popup_menu" object="services_popup_menu" swapped="no"/>
<signal name="cursor-changed" handler="on_service_changed" swapped="no"/>
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
<signal name="realize" handler="on_services_view_realize" swapped="no"/>
<signal name="select-all" handler="on_select_all" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="service_name_column">
<property name="sizing">fixed</property>
<property name="min-width">50</property>
<property name="title" translatable="yes">Name</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>
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">0</attribute>
<attribute name="background-rgba">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="service_type_column">
<property name="sizing">fixed</property>
<property name="min-width">75</property>
<property name="title" translatable="yes">Type</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="info_type_renderer">
<property name="xpad">5</property>
<property name="xalign">0.5099999904632568</property>
</object>
<attributes>
<attribute name="text">1</attribute>
<attribute name="background-rgba">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="service_selected_column">
<property name="sizing">fixed</property>
<property name="min-width">75</property>
<property name="title" translatable="yes">Selected</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererToggle" id="service_selected_renderer">
<property name="xpad">5</property>
<signal name="toggled" handler="on_service_selected_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="cell-background-rgba">3</attribute>
<attribute name="active">2</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="service_info_box_frame">
<property name="visible" bind-source="details_button" bind-property="active">False</property>
<property name="can-focus">False</property>
<property name="margin-top">5</property>
<property name="label-xalign">0</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkBox" id="service_info_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="service_info_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="ellipsize">end</property>
</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">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="services_status_box">
<property name="height-request">26</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="services_count_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">document-properties</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="services_count_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="service_exists_frame">
<property name="width-request">32</property>
<property name="height-request">16</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="label-xalign">0</property>
<property name="shadow-type">in</property>
<child>
<placeholder/>
</child>
<child type="label_item">
<placeholder/>
</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>
<child>
<object class="GtkLabel" id=" service_exists_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Already exists</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">3</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">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="label" translatable="yes">Services</property>
</object>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="can-focus">False</property>
<property name="show-close-button">True</property>
<signal name="response" handler="on_info_bar_close" swapped="no"/>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can-focus">False</property>
<property name="spacing">6</property>
<property name="layout-style">end</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can-focus">False</property>
<property name="spacing">16</property>
<child>
<object class="GtkLabel" id="message_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">message</property>
<property name="wrap">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View File

@@ -1,3 +1,32 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
from collections import defaultdict
from contextlib import suppress
from pathlib import Path
@@ -5,22 +34,25 @@ from app.commons import run_idle, log
from app.eparser import get_bouquets, get_services, BouquetsReader
from app.eparser.ecommons import BqType, BqServiceType, Bouquet
from app.eparser.neutrino.bouquets import parse_webtv, parse_bouquets as get_neutrino_bouquets
from app.settings import SettingsType
from app.settings import SettingsType, IS_DARWIN, SEP
from app.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, get_message, get_builder
from app.ui.main_helper import on_popup_menu
from .uicommons import Gtk, UI_RESOURCES_PATH, KeyboardKey, Column
from app.ui.main_helper import on_popup_menu, get_iptv_data
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, Column, IS_GNOME_SESSION, Page
def import_bouquet(transient, model, path, settings, services, appender, file_path=None):
def import_bouquet(app, model, path, appender, file_path=None):
""" Import of single bouquet """
itr = model.get_iter(path)
bq_type = BqType(model.get(itr, Column.BQ_TYPE)[0])
pattern, f_pattern = None, None
settings = app.app_settings
transient = app.app_window
services = app.current_services
profile = settings.setting_type
if profile is SettingsType.ENIGMA_2:
pattern = ".{}".format(bq_type.value)
f_pattern = "*" + pattern if settings.is_darwin else "userbouquet.*{}".format(pattern)
pattern = f".{bq_type.value}"
f_pattern = f"{'' if IS_DARWIN else 'userbouquet.'}*{pattern}"
elif profile is SettingsType.NEUTRINO_MP:
pattern = "webtv.xml" if bq_type is BqType.WEBTV else "bouquets.xml"
f_pattern = "bouquets.xml"
@@ -33,13 +65,13 @@ def import_bouquet(transient, model, path, settings, services, appender, file_pa
if file_path == Gtk.ResponseType.CANCEL:
return
if not file_path.endswith(pattern):
if not str(file_path).endswith(pattern):
show_dialog(DialogType.ERROR, transient, text="No bouquet file is selected!")
return
if profile is SettingsType.ENIGMA_2:
if settings.is_darwin and file_path.rfind("userbouquet.") < 0:
show_dialog(DialogType.ERROR, transient, text="No bouquet file is selected!")
if IS_DARWIN and file_path.rfind("userbouquet.") < 0:
show_dialog(DialogType.ERROR, transient, text="Not allowed in this context!")
return
bq = get_enigma2_bouquet(file_path)
@@ -59,8 +91,8 @@ def import_bouquet(transient, model, path, settings, services, appender, file_pa
bqs = parse_webtv(file_path, "WEBTV", bq_type.value)
else:
bqs = get_neutrino_bouquets(file_path, "", bq_type.value)
file_path = "{}/".format(Path(file_path).parent)
ImportDialog(transient, file_path, settings, services.keys(), lambda b, s: appender(b), (bqs,)).show()
file_path = f"{Path(file_path).parent}{SEP}"
ImportDialog(app, file_path, lambda b, s: appender(b), (bqs,)).show()
def get_enigma2_bouquet(path):
@@ -72,38 +104,75 @@ def get_enigma2_bouquet(path):
class ImportDialog:
def __init__(self, transient, path, settings, service_ids, appender, bouquets=None):
def __init__(self, app, path, 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_service_changed": self.on_service_changed,
"on_bq_selected_toggled": self.on_bq_selected_toggled,
"on_sat_selected_toggled": self.on_sat_selected_toggled,
"on_service_selected_toggled": self.on_service_selected_toggled,
"on_services_model_changed": self.on_services_model_changed,
"on_info_bar_close": self.on_info_bar_close,
"on_select_all": self.on_select_all,
"on_unselect_all": self.on_unselect_all,
"on_sat_view_realize": self.on_sat_view_realize,
"on_services_view_realize": self.on_services_view_realize,
"on_popup_menu": on_popup_menu,
"on_resize": self.on_resize,
"on_main_paned_realize": self.on_main_paned_realize,
"on_visible_page": self.on_visible_page,
"on_key_press": self.on_key_press}
builder = get_builder(UI_RESOURCES_PATH + "import_dialog.glade", handlers)
builder = get_builder(f"{UI_RESOURCES_PATH}imports.glade", handlers)
self._bq_services = {}
self._app = app
self._services = {}
self._service_ids = service_ids
self._bq_services = {}
self._sat_services = defaultdict(list)
self._ids = self._app.current_services.keys()
self._skip_import = defaultdict(set)
self._append = appender
self._profile = settings.setting_type
self._settings = settings
self._profile = app.app_settings.setting_type
self._settings = app.app_settings
self._bouquets = bouquets
self._current_bq = None
self._current_sat = None
self._existing_srv_background = None
self._page = Page.SERVICES
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._dialog_window.set_transient_for(app.app_window)
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("message_label")
# Bouquets page.
self._bq_model = builder.get_object("bq_list_store")
self._bq_view = builder.get_object("bq_view")
self._services_view = builder.get_object("services_view")
self._services_model = builder.get_object("services_list_store")
self._bouquets_count_label = builder.get_object("bouquets_count_label")
self._services_count_label = builder.get_object("services_count_label")
self._service_info_label = builder.get_object("service_info_label")
self._service_exists_frame = builder.get_object("service_exists_frame")
# Satellites page.
self._sat_view = builder.get_object("sat_view")
self._sat_model = builder.get_object("sat_list_store")
self._sat_count_label = builder.get_object("sat_count_label")
if IS_GNOME_SESSION:
actions_box = builder.get_object("actions_box")
builder.get_object("toolbar_box").set_visible(False)
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True)
stack_switcher = builder.get_object("stack_switcher")
actions_box.remove(stack_switcher)
header_bar.set_custom_title(stack_switcher)
button = builder.get_object("import_button")
actions_box.remove(button)
header_bar.pack_start(button)
button = builder.get_object("details_button")
actions_box.remove(button)
header_bar.pack_end(button)
self._dialog_window.set_titlebar(header_bar)
window_size = self._settings.get("import_dialog_window_size")
if window_size:
self._dialog_window.resize(*window_size)
@@ -115,7 +184,7 @@ class ImportDialog:
@run_idle
def init_data(self, path):
self._main_model.clear()
self._bq_model.clear()
self._services_model.clear()
try:
if not self._bouquets:
@@ -123,8 +192,9 @@ class ImportDialog:
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_model.append((bq.name, bq.type, True))
self._bq_services[(bq.name, bq.type)] = bq.services
self._bouquets_count_label.set_text(str(len(self._bq_model)))
if self._profile is SettingsType.ENIGMA_2:
services = get_services(path, self._profile, 5 if self._settings.v5_support else 4)
@@ -137,21 +207,24 @@ class ImportDialog:
for srv in services:
self._services[srv.fav_id] = srv
except FileNotFoundError as e:
log("Import error [init data]: {}".format(e))
log(f"Import error [init data]: {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 self._page is Page.SERVICES:
if not any(r[-1] for r in self._bq_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
if not self._bouquets or show_dialog(DialogType.QUESTION, self._dialog_window) != Gtk.ResponseType.OK:
return
self.import_data()
self.import_bouquets_data()
else:
self.import_satellites_data()
@run_idle
def import_data(self):
def import_bouquets_data(self):
""" Importing data into models. """
if not self._bouquets:
return
@@ -159,12 +232,13 @@ class ImportDialog:
log("Importing data...")
services = set()
to_delete = set()
for row in self._main_model:
for row in self._bq_model:
bq = (row[0], row[1])
if row[-1]:
skip = self._skip_import[bq]
for bq_srv in self._bq_services.get(bq, []):
srv = self._services.get(bq_srv.data, None)
if srv:
if srv and srv.fav_id not in skip:
services.add(srv)
else:
to_delete.add(bq)
@@ -173,39 +247,111 @@ class ImportDialog:
for bq in bqs.bouquets:
if (bq.name, bq.type) in to_delete:
bqs_to_delete.append(bq)
else:
skip = self._skip_import[(bq.name, bq.type)]
bq_services = [srv for srv in bq.services if srv.data not in skip]
bq.services.clear()
bq.services.extend(bq_services)
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)))
services = list(filter(lambda s: s.fav_id not in self._ids, services))
self._append(self._bouquets, services)
self._dialog_window.destroy()
def import_satellites_data(self):
if show_dialog(DialogType.QUESTION, self._dialog_window) != Gtk.ResponseType.OK:
return
replace_existing = False
services = []
current_services = self._app.current_services
for row in self._sat_model:
if row[-1]:
sat = (row[0], row[1])
skip = self._skip_import[sat]
for s in filter(lambda srv: srv.fav_id not in skip, self._sat_services.get(sat[0], ())):
if replace_existing and s.fav_id in self._ids:
current_services[s.fav_id] = s
elif s.fav_id not in self._ids:
services.append(s)
self._append((), 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()
self._service_info_label.set_text("")
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))
if self._page is Page.SERVICES:
self._current_bq = model.get(model.get_iter(paths[0]), 0, 1)
self.update_bq_services()
else:
self._current_sat = model.get(model.get_iter(paths[0]), 0, 1)
self.update_sat_services()
self._services_count_label.set_text(str(len(self._services_model)))
def update_bq_services(self):
bq_services = self._bq_services.get(self._current_bq)
skip = self._skip_import[self._current_bq]
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))
bg = self._existing_srv_background if srv.fav_id in self._ids else None
self._services_model.append((srv.service, srv.service_type, srv.fav_id not in skip, bg, srv.fav_id))
else:
self._services_model.append((bq_srv.name, bq_srv.type.value))
bg = self._existing_srv_background if bq_srv.data in self._ids else None
self._services_model.append((bq_srv.name, bq_srv.type.value, bq_srv.data not in skip, bg, bq_srv.data))
def on_info_button_toggled(self, button):
active = button.get_active()
self._services_box.set_visible(active)
def update_sat_services(self):
sat_services = self._sat_services.get(self._current_sat[0])
skip = self._skip_import[self._current_sat]
for srv in sat_services:
bg = self._existing_srv_background if srv.fav_id in self._ids else None
self._services_model.append((srv.service, srv.service_type, srv.fav_id not in skip, bg, srv.fav_id))
def on_selected_toggled(self, toggle, path):
self._main_model.set_value(self._main_model.get_iter(path), 2, not toggle.get_active())
def on_service_changed(self, view):
path, column = view.get_cursor()
if path:
row = self._services_model[path][:]
if row[1] == "IPTV":
ref, url = get_iptv_data(row[-1])
ref = f"{get_message('Service reference')}: {ref}"
info = f"{get_message('Name')}: {row[0]}\n{ref}\nURL: {url}"
self._service_info_label.set_text(info)
else:
srv = self._services.get(row[-1], None)
self._service_info_label.set_text(self._app.get_hint_for_fav_list(srv) if srv else "")
def on_bq_selected_toggled(self, toggle, path):
self._bq_model.set_value(self._bq_model.get_iter(path), 2, not toggle.get_active())
def on_sat_selected_toggled(self, toggle, path):
self._sat_model.set_value(self._sat_model.get_iter(path), 2, not toggle.get_active())
def on_service_selected_toggled(self, toggle, path):
self._services_model.set_value(self._services_model.get_iter(path), 2, not toggle.get_active())
def on_services_model_changed(self, model, path, itr):
row = model[itr][:]
fav_id = row[-1]
skip = self._skip_import[self._current_bq if self._page is Page.SERVICES else self._current_sat]
if row[2]:
if fav_id in skip:
skip.remove(fav_id)
else:
skip.add(fav_id)
@run_idle
def show_info_message(self, text, message_type):
@@ -226,10 +372,33 @@ class ImportDialog:
def update_selection(self, view, select):
view.get_model().foreach(lambda mod, path, itr: mod.set_value(itr, 2, select))
def on_sat_view_realize(self, view):
if not self._services:
return True
for srv in self._services.values():
self._sat_services[srv.pos].append(srv)
list(map(lambda s: self._sat_model.append((s, None, True)), self._sat_services))
self._sat_count_label.set_text(str(len(self._sat_model)))
def on_services_view_realize(self, view):
if self._settings.use_colors:
background = Gdk.RGBA()
self._existing_srv_background = background if background.parse(self._settings.extra_color) else None
self._service_exists_frame.modify_bg(Gtk.StateType.NORMAL, background.to_color())
def on_resize(self, window):
if self._settings:
self._settings.add("import_dialog_window_size", window.get_size())
def on_main_paned_realize(self, paned):
width = paned.get_allocated_width()
paned.set_position(width * 0.35)
def on_visible_page(self, stack, param):
self._page = Page(stack.get_visible_child_name())
def on_key_press(self, view, event):
""" Handling keystrokes """
key_code = event.hardware_keycode
@@ -238,10 +407,11 @@ class ImportDialog:
key = KeyboardKey(key_code)
if key is KeyboardKey.SPACE:
model = view.get_model()
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)
itr = model.get_iter(path)
selected = model.get_value(itr, 2)
model.set_value(itr, 2, not selected)
if __name__ == "__main__":

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -40,15 +40,15 @@ from gi.repository import GLib, Gio, GdkPixbuf
from app.commons import run_idle, run_task, log
from app.eparser.ecommons import BqServiceType, Service
from app.eparser.iptv import (NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT, get_fav_id, MARKER_FORMAT,
parse_m3u)
parse_m3u, PICON_FORMAT)
from app.settings import SettingsType
from app.tools.yt import YouTubeException, YouTube
from app.ui.dialogs import Action, show_dialog, DialogType, get_message, get_builder
from app.ui.main_helper import get_base_model, get_iptv_url, on_popup_menu, get_picon_pixbuf
from app.ui.main_helper import get_iptv_url, on_popup_menu, get_picon_pixbuf
from app.ui.uicommons import (Gtk, Gdk, UI_RESOURCES_PATH, IPTV_ICON, Column, KeyboardKey, get_yt_icon)
_DIGIT_ENTRY_NAME = "digit-entry"
_ENIGMA2_REFERENCE = "{}:0:{}:{:X}:{:X}:{:X}:{:X}:0:0:0"
_ENIGMA2_REFERENCE = "{}:{}:{}:{:X}:{:X}:{:X}:{:X}:0:0:0"
_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
_UI_PATH = UI_RESOURCES_PATH + "iptv.glade"
@@ -77,7 +77,7 @@ def get_stream_type(box):
class IptvDialog:
def __init__(self, transient, view, services, bouquet, settings, action=Action.ADD):
def __init__(self, app, view, bouquet=None, service=None, action=Action.ADD):
handlers = {"on_response": self.on_response,
"on_entry_changed": self.on_entry_changed,
"on_url_changed": self.on_url_changed,
@@ -86,11 +86,11 @@ class IptvDialog:
"on_yt_quality_changed": self.on_yt_quality_changed,
"on_info_bar_close": self.on_info_bar_close}
self._app = app
self._action = action
self._s_type = settings.setting_type
self._settings = settings
self._settings = app.app_settings
self._s_type = self._settings.setting_type
self._bouquet = bouquet
self._services = services
self._yt_links = None
self._yt_dl = None
@@ -98,12 +98,13 @@ class IptvDialog:
objects=("iptv_dialog", "stream_type_liststore", "yt_quality_liststore"))
self._dialog = builder.get_object("iptv_dialog")
self._dialog.set_transient_for(transient)
self._dialog.set_transient_for(app.app_window)
self._name_entry = builder.get_object("name_entry")
self._description_entry = builder.get_object("description_entry")
self._url_entry = builder.get_object("url_entry")
self._reference_entry = builder.get_object("reference_entry")
self._srv_type_entry = builder.get_object("srv_type_entry")
self._srv_id_entry = builder.get_object("srv_id_entry")
self._sid_entry = builder.get_object("sid_entry")
self._tr_id_entry = builder.get_object("tr_id_entry")
self._net_id_entry = builder.get_object("net_id_entry")
@@ -119,8 +120,8 @@ class IptvDialog:
# style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._digit_elems = (self._srv_type_entry, self._sid_entry, self._tr_id_entry, self._net_id_entry,
self._namespace_entry)
self._digit_elems = (self._srv_id_entry, self._srv_type_entry, self._sid_entry, self._tr_id_entry,
self._net_id_entry, self._namespace_entry)
for el in self._digit_elems:
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
@@ -141,7 +142,7 @@ class IptvDialog:
self.update_reference_entry()
self._stream_type_combobox.set_active(1)
elif self._action is Action.EDIT:
self._current_srv = get_base_model(self._model)[self._paths][:]
self._current_srv = service
self.init_data(self._current_srv)
def show(self):
@@ -166,8 +167,8 @@ class IptvDialog:
self._dialog.destroy()
def init_data(self, srv):
name, fav_id = srv[2], srv[7]
self._name_entry.set_text(name)
fav_id = srv.fav_id
self._name_entry.set_text(srv.service)
self.init_enigma2_data(fav_id) if self._s_type is SettingsType.ENIGMA_2 else self.init_neutrino_data(fav_id)
def init_enigma2_data(self, fav_id):
@@ -195,6 +196,7 @@ class IptvDialog:
except ValueError:
self.show_info_message("Unknown stream type {}".format(s_type), Gtk.MessageType.ERROR)
self._srv_id_entry.set_text(data[1])
self._srv_type_entry.set_text(data[2])
self._sid_entry.set_text(str(int(data[3], 16)))
self._tr_id_entry.set_text(str(int(data[4], 16)))
@@ -212,6 +214,7 @@ class IptvDialog:
if self._s_type is SettingsType.ENIGMA_2 and is_data_correct(self._digit_elems):
self.on_url_changed(self._url_entry)
self._reference_entry.set_text(_ENIGMA2_REFERENCE.format(self.get_type(),
self._srv_id_entry.get_text(),
self._srv_type_entry.get_text(),
int(self._sid_entry.get_text()),
int(self._tr_id_entry.get_text()),
@@ -296,6 +299,7 @@ class IptvDialog:
def save_enigma2_data(self):
name = self._name_entry.get_text().strip()
fav_id = ENIGMA2_FAV_ID_FORMAT.format(self.get_type(),
self._srv_id_entry.get_text(),
self._srv_type_entry.get_text(),
int(self._sid_entry.get_text()),
int(self._tr_id_entry.get_text()),
@@ -303,11 +307,12 @@ class IptvDialog:
int(self._namespace_entry.get_text()),
quote(self._url_entry.get_text()),
name, name)
self.update_bouquet_data(name, fav_id)
def save_neutrino_data(self):
if self._action is Action.EDIT:
id_data = self._current_srv[7].split("::")
id_data = self._current_srv.fav_id.split("::")
else:
id_data = ["", "", "0", None, None, None, None, "", "", "1"]
id_data[0] = self._url_entry.get_text()
@@ -316,20 +321,25 @@ class IptvDialog:
self._dialog.destroy()
def update_bouquet_data(self, name, fav_id):
picon_id = f"{self._reference_entry.get_text().replace(':', '_')}.png"
if self._action is Action.EDIT:
old_srv = self._services.pop(self._current_srv[7])
self._services[fav_id] = old_srv._replace(service=name, fav_id=fav_id)
self._bouquet[self._paths[0][0]] = fav_id
self._model.set(self._model.get_iter(self._paths), {Column.FAV_SERVICE: name, Column.FAV_ID: fav_id})
services = self._app.current_services
old_srv = services.pop(self._current_srv.fav_id)
new_service = old_srv._replace(service=name, fav_id=fav_id, picon_id=picon_id)
services[fav_id] = new_service
self._app.emit("iptv-service-edited", {self._current_srv.fav_id: (old_srv, new_service)})
else:
aggr = [None] * 8
s_type = BqServiceType.IPTV.name
srv = (None, None, name, None, None, s_type, None, fav_id, None, None, None)
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, None, None, None, s_type, None,
None, None, None, None, None, None, None, None, None, fav_id, None)
service = Service(None, None, IPTV_ICON, name, *aggr[0:3], s_type, None, picon_id, *aggr, fav_id, None)
self._app.current_services[fav_id] = service
self._app.emit("iptv-service-added", (service,))
@run_idle
def on_info_bar_close(self, bar=None, resp=None):
@@ -369,9 +379,13 @@ class SearchUnavailableDialog:
@run_task
def do_search(self):
import concurrent.futures
import ssl
import certifi
context = ssl.create_default_context(cafile=certifi.where())
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
futures = {executor.submit(self.get_unavailable, row): row for row in self._iptv_rows}
futures = {executor.submit(self.get_unavailable, row, context): row for row in self._iptv_rows}
for future in concurrent.futures.as_completed(futures):
if not self._download_task:
executor.shutdown()
@@ -380,13 +394,13 @@ class SearchUnavailableDialog:
self._download_task = False
self.on_close()
def get_unavailable(self, row):
def get_unavailable(self, row, context):
if not self._download_task:
return
try:
req = Request(get_iptv_url(row, self._s_type))
self.update_bar()
urlopen(req, timeout=2)
urlopen(req, context=context, timeout=2)
except HTTPError as e:
if e.code != 403:
self.append_data(row)
@@ -432,6 +446,7 @@ class IptvListDialog:
"on_response": self.on_response,
"on_stream_type_default_togged": self.on_stream_type_default_togged,
"on_stream_type_changed": self.on_stream_type_changed,
"on_default_id_toggled": self.on_default_id_toggled,
"on_default_type_toggled": self.on_default_type_toggled,
"on_auto_sid_toggled": self.on_auto_sid_toggled,
"on_default_tid_toggled": self.on_default_tid_toggled,
@@ -453,12 +468,14 @@ class IptvListDialog:
self._info_bar = builder.get_object("list_configuration_info_bar")
self._reference_label = builder.get_object("reference_label")
self._stream_type_check_button = builder.get_object("stream_type_default_check_button")
self._id_default_check_button = builder.get_object("id_default_check_button")
self._type_check_button = builder.get_object("type_default_check_button")
self._sid_auto_check_button = builder.get_object("sid_auto_check_button")
self._tid_check_button = builder.get_object("tid_default_check_button")
self._nid_check_button = builder.get_object("nid_default_check_button")
self._namespace_check_button = builder.get_object("namespace_default_check_button")
self._stream_type_combobox = builder.get_object("stream_type_list_combobox")
self._list_srv_id_entry = builder.get_object("list_srv_id_entry")
self._list_srv_type_entry = builder.get_object("list_srv_type_entry")
self._list_sid_entry = builder.get_object("list_sid_entry")
self._list_tid_entry = builder.get_object("list_tid_entry")
@@ -467,13 +484,16 @@ class IptvListDialog:
self._apply_button = builder.get_object("list_configuration_apply_button")
self._cancel_button = builder.get_object("cancel_config_list_button")
self._ok_button = builder.get_object("list_configuration_ok_button")
self._ok_button.bind_property("visible", self._apply_button, "visible", 4)
self._ok_button.bind_property("visible", self._cancel_button, "visible", 4)
# Style
style_provider = Gtk.CssProvider()
style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._default_elems = (self._stream_type_check_button, self._type_check_button, self._sid_auto_check_button,
self._tid_check_button, self._nid_check_button, self._namespace_check_button)
self._digit_elems = (self._list_srv_type_entry, self._list_sid_entry, self._list_tid_entry,
self._list_nid_entry, self._list_namespace_entry)
self._default_elems = (self._stream_type_check_button, self._id_default_check_button, self._type_check_button,
self._sid_auto_check_button, self._tid_check_button, self._nid_check_button,
self._namespace_check_button)
self._digit_elems = (self._list_srv_id_entry, self._list_srv_type_entry, self._list_sid_entry,
self._list_tid_entry, self._list_nid_entry, self._list_namespace_entry)
for el in self._digit_elems:
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
@@ -495,30 +515,28 @@ class IptvListDialog:
self._stream_type_combobox.set_active(1)
self._stream_type_combobox.set_sensitive(not button.get_active())
def on_default_id_toggled(self, button):
self.set_default(button, self._list_srv_id_entry, "0")
def on_default_type_toggled(self, button):
if button.get_active():
self._list_srv_type_entry.set_text("1")
self._list_srv_type_entry.set_sensitive(not button.get_active())
self.set_default(button, self._list_srv_type_entry, "1")
def on_auto_sid_toggled(self, button):
if button.get_active():
self._list_sid_entry.set_text("0")
self._list_sid_entry.set_sensitive(not button.get_active())
self.set_default(button, self._list_sid_entry, "0")
def on_default_tid_toggled(self, button):
if button.get_active():
self._list_tid_entry.set_text("0")
self._list_tid_entry.set_sensitive(not button.get_active())
self.set_default(button, self._list_tid_entry, "0")
def on_default_nid_toggled(self, button):
if button.get_active():
self._list_nid_entry.set_text("0")
self._list_nid_entry.set_sensitive(not button.get_active())
self.set_default(button, self._list_nid_entry, "0")
def on_default_namespace_toggled(self, button):
self.set_default(button, self._list_namespace_entry, "0")
def set_default(self, button, entry, value):
if button.get_active():
self._list_namespace_entry.set_text("0")
self._list_namespace_entry.set_sensitive(not button.get_active())
entry.set_text(value)
entry.set_sensitive(not button.get_active())
@run_idle
def on_reset_to_default(self, item):
@@ -550,7 +568,7 @@ class IptvListDialog:
self.update_reference()
def is_default_values(self):
return any(el.get_text() == "0" for el in self._digit_elems[2:])
return any(el.get_text() == "0" for el in self._digit_elems[3:])
def is_all_data_default(self):
return all(el.get_active() for el in self._default_elems)
@@ -573,41 +591,50 @@ class IptvListConfigurationDialog(IptvListDialog):
return
if self._s_type is SettingsType.ENIGMA_2:
id_default = self._id_default_check_button.get_active()
type_default = self._type_check_button.get_active()
tid_default = self._tid_check_button.get_active()
sid_auto = self._sid_auto_check_button.get_active()
nid_default = self._nid_check_button.get_active()
namespace_default = self._namespace_check_button.get_active()
all_default = self.is_all_data_default()
stream_type = get_stream_type(self._stream_type_combobox)
st_type = get_stream_type(self._stream_type_combobox)
s_id = "0" if id_default else self._list_srv_id_entry.get_text()
srv_type = "1" if type_default else self._list_srv_type_entry.get_text()
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()))
sid = "0" if sid_auto else self._list_sid_entry.get_text()
tid = "0" if tid_default else f"{int(self._list_tid_entry.get_text()):X}"
nid = "0" if nid_default else f"{int(self._list_nid_entry.get_text()):X}"
namespace = "0" if namespace_default else f"{int(self._list_namespace_entry.get_text()):X}"
params = [int(el.get_text()) for el in self._digit_elems[2:]]
for index, row in enumerate(self._rows):
fav_id = row[Column.FAV_ID]
data, sep, desc = fav_id.partition("http")
data = data.split(":")
if self.is_all_data_default():
data[2], data[3], data[4], data[5], data[6] = "10000"
if all_default:
data[1], data[2], data[3], data[4], data[5], data[6] = "010000"
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[0], data[1], data[2], data[4], data[5], data[6] = st_type, s_id, srv_type, tid, nid, namespace
data[3] = f"{index:X}" if sid_auto else sid
if sid_auto:
params[0] = index
picon_id = PICON_FORMAT.format(st_type, int(s_id), int(srv_type), *params)
data = ":".join(data)
new_fav_id = "{}{}{}".format(data, sep, desc)
new_fav_id = f"{data}{sep}{desc}"
row[Column.FAV_ID] = new_fav_id
srv = self._services.pop(fav_id, None)
if srv:
self._services[new_fav_id] = srv._replace(fav_id=new_fav_id)
self._services[new_fav_id] = srv._replace(fav_id=new_fav_id, picon_id=picon_id)
self._bouquet.clear()
list(map(lambda r: self._bouquet.append(r[Column.FAV_ID]), self._fav_model))
self._info_bar.set_visible(True)
self._ok_button.set_visible(True)
class M3uImportDialog(IptvListDialog):
@@ -617,8 +644,8 @@ class M3uImportDialog(IptvListDialog):
super().__init__(transient, s_type)
self._app = app
self._picons = app._picons
self._pic_path = app._settings.picons_local_path
self._picons = app.picons
self._pic_path = app._settings.profile_picons_path
self._services = None
self._url_count = 0
self._errors_count = 0
@@ -628,8 +655,6 @@ class M3uImportDialog(IptvListDialog):
self._dialog.set_title(get_message("Playlist import"))
self._dialog.connect("delete-event", self.on_close)
self._apply_button.set_label(get_message("Import"))
self._ok_button.bind_property("visible", self._apply_button, "visible", 4)
self._ok_button.bind_property("visible", self._cancel_button, "visible", 4)
# Progress
self._progress_bar = Gtk.ProgressBar(visible=False, valign="center")
self._spinner = Gtk.Spinner(active=False)
@@ -645,7 +670,7 @@ class M3uImportDialog(IptvListDialog):
progress_box.pack_start(load_label, False, False, 0)
# Picons
self._picons_switch = Gtk.Switch(visible=True)
self._picon_box = Gtk.HBox(visible=True, sensitive=False, spacing=2)
self._picon_box = Gtk.HBox(visible=True, sensitive=False, spacing=5)
self._picon_box.pack_end(self._picons_switch, False, False, 0)
self._picon_box.pack_end(Gtk.Label(visible=True, label=get_message("Download picons")), False, False, 0)
# Extra box
@@ -654,7 +679,7 @@ class M3uImportDialog(IptvListDialog):
extra_box.pack_start(self._info_label, False, False, 5)
extra_box.pack_end(self._picon_box, True, True, 5)
frame = Gtk.Frame(visible=True)
frame = Gtk.Frame(visible=True, margin_bottom=5)
frame.add(extra_box)
self._data_box.add(frame)
@@ -670,7 +695,7 @@ class M3uImportDialog(IptvListDialog):
GLib.idle_add(self._picon_box.set_sensitive, True)
break
finally:
msg = "{} {}.".format(get_message("Streams detected:"), len(self._services) if self._services else 0)
msg = f"{get_message('Streams detected:')} {len(self._services) if self._services else 0}."
GLib.idle_add(self._info_label.set_text, msg)
GLib.idle_add(self._spinner.set_property, "active", False)
@@ -685,9 +710,12 @@ class M3uImportDialog(IptvListDialog):
if not self.is_all_data_default():
services = []
params = [int(el.get_text()) for el in self._digit_elems]
s_type = params[0]
params = params[1:]
stream_type = get_stream_type(self._stream_type_combobox)
s_id = params[0]
s_type = params[1]
params = params[2:]
st_type = get_stream_type(self._stream_type_combobox)
sid_auto = self._sid_auto_check_button.get_active()
sid = 0 if sid_auto else int(self._list_sid_entry.get_text())
for i, s in enumerate(self._services, start=params[0]):
# Skipping markers.
@@ -695,9 +723,9 @@ class M3uImportDialog(IptvListDialog):
services.append(s)
continue
params[0] = i
picon_id = "{}_0_{:X}_{:X}_{:X}_{:X}_{:X}_0_0_0.png".format(stream_type, s_type, *params)
fav_id = get_fav_id(s.data_id, s.service, self._s_type, params, stream_type, s_type)
params[0] = i if sid_auto else sid
picon_id = PICON_FORMAT.format(st_type, s_id, s_type, *params)
fav_id = get_fav_id(s.data_id, s.service, self._s_type, params, st_type, s_id, s_type)
if s.picon:
picons[s.picon] = picon_id
@@ -755,16 +783,16 @@ class M3uImportDialog(IptvListDialog):
@run_idle
def on_picon_load_done(self, data, user_data):
try:
self._info_label.set_text("Processing: {}".format(user_data))
self._info_label.set_text(f"Processing: {user_data}")
f = Gio.MemoryInputStream.new_from_data(data)
pixbuf = GdkPixbuf.Pixbuf.new_from_stream_at_scale(f, 220, 132, False, self._cancellable)
path = "{}{}".format(self._pic_path, user_data)
path = f"{self._pic_path}{user_data}"
pixbuf.savev(path, "png", [], [])
self._picons[user_data] = get_picon_pixbuf(path)
except GLib.GError as e:
self.update_progress(1)
if e.code != Gio.IOErrorEnum.CANCELLED:
log("Loading picon [{}] data error: {}".format(user_data, e))
log(f"Loading picon [{user_data}] data error: {e}")
else:
self.update_progress()
@@ -780,15 +808,15 @@ class M3uImportDialog(IptvListDialog):
self._progress_bar.set_visible(False)
self._progress_bar.set_fraction(0.0)
self._apply_button.set_sensitive(True)
self._info_label.set_text("Errors: {}.".format(self._errors_count))
self._info_label.set_text(f"Errors: {self._errors_count}.")
self._is_download = False
gen = self.update_fav_model()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def update_fav_model(self):
services = self._app._services
picons = self._app._picons
services = self._app.current_services
picons = self._app.picons
model = self._app.fav_view.get_model()
for r in model:
s = services.get(r[Column.FAV_ID], None)
@@ -819,7 +847,7 @@ class M3uImportDialog(IptvListDialog):
class YtListImportDialog:
def __init__(self, transient, settings, appender):
def __init__(self, app):
handlers = {"on_import": self.on_import,
"on_receive": self.on_receive,
"on_yt_url_entry_changed": self.on_url_entry_changed,
@@ -831,12 +859,13 @@ class YtListImportDialog:
"on_key_press": self.on_key_press,
"on_close": self.on_close}
self.appender = appender
self._s_type = settings.setting_type
# self._main_window, self._settings, self.append_imported_services
self.appender = app.append_imported_services
self._settings = app.app_settings
self._s_type = self._settings.setting_type
self._download_task = False
self._yt_list_id = None
self._yt_list_title = None
self._settings = settings
self._yt = None
builder = get_builder(_UI_PATH, handlers, use_str=True,
@@ -845,7 +874,7 @@ class YtListImportDialog:
"yt_import_image"))
self._dialog = builder.get_object("yt_import_dialog_window")
self._dialog.set_transient_for(transient)
self._dialog.set_transient_for(app.app_window)
self._list_view_scrolled_window = builder.get_object("yt_list_view_scrolled_window")
self._model = builder.get_object("yt_liststore")
self._progress_bar = builder.get_object("yt_progress_bar")
@@ -861,10 +890,14 @@ class YtListImportDialog:
self._import_button.bind_property("visible", self._quality_box, "visible")
self._import_button.bind_property("sensitive", self._quality_box, "sensitive")
self._receive_button.bind_property("sensitive", self._import_button, "sensitive")
# style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
window_size = self._settings.get("yt_import_dialog_size")
if window_size:
self._dialog.resize(*window_size)
# Style
style_provider = Gtk.CssProvider()
style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
def show(self):
@@ -909,7 +942,6 @@ class YtListImportDialog:
self.update_active_elements(True)
def on_receive(self, item):
self.show_invisible_elements()
self.update_active_elements(False)
self._model.clear()
self._yt_count_label.set_text("0")
@@ -943,13 +975,13 @@ class YtListImportDialog:
@run_idle
def append_services(self, links):
aggr = [None] * 9
srvs = []
if self._yt_list_title:
title = self._yt_list_title
fav_id = MARKER_FORMAT.format(0, title, title)
mk = Service(None, None, None, title, None, None, None, BqServiceType.MARKER.name, None,
None, None, None, None, None, None, None, None, 0, fav_id, None)
mk = Service(None, None, None, title, *aggr[0:3], BqServiceType.MARKER.name, *aggr, 0, fav_id, None)
srvs.append(mk)
act = self._quality_model.get_value(self._quality_box.get_active_iter(), 0)
@@ -959,8 +991,7 @@ class YtListImportDialog:
continue
ln = lnk.get(act) if act in lnk else lnk[sorted(lnk, key=lambda x: int(x.rstrip("p")), reverse=True)[0]]
fav_id = get_fav_id(ln, title, self._s_type)
srv = Service(None, None, IPTV_ICON, title, None, None, None, BqServiceType.IPTV.name, None, None, None,
None, None, None, None, None, None, None, fav_id, None)
srv = Service(None, None, IPTV_ICON, title, *aggr[0:3], BqServiceType.IPTV.name, *aggr, None, fav_id, None)
srvs.append(srv)
self.appender(srvs)
@@ -969,11 +1000,6 @@ class YtListImportDialog:
self._url_entry.set_sensitive(sensitive)
self._receive_button.set_sensitive(sensitive)
def show_invisible_elements(self):
self._list_view_scrolled_window.set_visible(True)
self._info_bar_box.set_visible(True)
self._dialog.set_resizable(True)
def on_url_entry_changed(self, entry):
url_str = entry.get_text()
yt_id = YouTube.get_yt_list_id(url_str)
@@ -1029,7 +1055,9 @@ class YtListImportDialog:
def on_close(self, window, event):
if self._download_task and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return True
self._download_task = False
self._settings.add("yt_import_dialog_size", self._dialog.get_size())
if __name__ == "__main__":

Binary file not shown.

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

@@ -0,0 +1,145 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2
The MIT License (MIT)
Copyright (c) 2018-2022 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface domain="demon-editor">
<requires lib="gtk+" version="3.18"/>
<!-- interface-css-provider-path style.css -->
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellite list editor. -->
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkFrame" id="log_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0.49000000953674316</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkBox" id="main_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="header_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">2</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="clear_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Clear</property>
<property name="halign">center</property>
<property name="valign">center</property>
<signal name="clicked" handler="on_clear" swapped="no"/>
<child>
<object class="GtkImage" id="clear_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-clear</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="close_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Close</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_close" swapped="no"/>
<child>
<object class="GtkImage" id="close_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-close</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="log_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="left_margin">5</property>
<property name="right_margin">5</property>
<property name="top_margin">5</property>
<property name="bottom_margin">5</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="log_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Logs</property>
</object>
</child>
</object>
</interface>

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

@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
import logging
from gi.repository import GLib
from app.commons import LOGGER_NAME, LOG_FORMAT, LOG_DATE_FORMAT
from app.ui.dialogs import get_builder
from app.ui.main_helper import append_text_to_tview
from app.ui.uicommons import Gtk, UI_RESOURCES_PATH
class LogsClient(Gtk.Box):
""" Logger GUI client. """
class LogHandler(logging.Handler):
def __init__(self, view):
logging.Handler.__init__(self)
self._view = view
self.setFormatter(logging.Formatter(fmt=LOG_FORMAT, datefmt=LOG_DATE_FORMAT))
def handle(self, rec: logging.LogRecord):
GLib.idle_add(append_text_to_tview, f"{self.format(rec)}\n", self._view)
def __init__(self, app, *args, **kwargs):
super().__init__(*args, **kwargs)
self._app = app
handlers = {"on_clear": self.on_clear, "on_close": self.on_close}
builder = get_builder(UI_RESOURCES_PATH + "logs.glade", handlers)
self._log_view = builder.get_object("log_view")
self.pack_start(builder.get_object("log_frame"), True, True, 0)
logger = logging.getLogger(LOGGER_NAME)
logger.addHandler(LogsClient.LogHandler(self._log_view))
self.show()
def on_clear(self, button):
GLib.idle_add(self._log_view.get_buffer().set_text, "")
def on_close(self, button):
self._app.change_action_state("on_logs_show", GLib.Variant.new_boolean(False))
if __name__ == "__main__":
pass

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

@@ -0,0 +1,71 @@
* {
background-clip: padding-box;
-GtkScrolledWindow-scrollbar-spacing: 0;
-GtkToolItemGroup-expander-size: 11;
-GtkWidget-text-handle-width: 20;
-GtkWidget-text-handle-height: 20;
-GtkDialog-button-spacing: 12;
-GtkDialog-action-area-border: 6;
}
entry {
min-height: 2.0em;
padding: 0.2em;
}
entry > image {
padding-left: 0.3em;
padding-right: 0.3em;
}
button {
min-height: 1.2em;
min-width: 1.5em;
padding-top: 0.3em;
padding-bottom: 0.3em;
}
button:active, button:checked {
color: @theme_selected_fg_color;
background-image: linear-gradient(@theme_selected_bg_color, @theme_selected_bg_color);
}
combobox {
min-height: 2.2em;
}
spinbutton {
min-height: 1.5em;
}
toolbutton {
padding: 0.1em;
}
spinner {
padding-left: 1em;
padding-right: 1em;
}
infobar {
min-height: 2em;
}
revealer > box > button {
padding: 0.2em;
}
switch slider {
min-height: 1.5em;
min-width: 1.5em;
}
.font > box {
min-height: 1.5em;
padding-top: 0.1em;
padding-bottom: 0.1em;
}
.dialog-action-area button {
margin-bottom: 0.6em;
}

5023
app/ui/main.glade Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,56 @@
""" Helper module for the ui. """
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2023 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" Helper module for the GUI. """
__all__ = ("insert_marker", "move_items", "rename", "ViewTarget", "set_flags", "locate_in_services",
"scroll_to", "get_base_model", "get_base_paths", "copy_reference", "assign_picons", "remove_picon",
"is_only_one_item_selected", "gen_bouquets", "BqGenType", "get_selection", "get_service_reference",
"get_model_data", "remove_all_unused_picons", "get_picon_pixbuf", "get_base_itrs", "get_iptv_url",
"get_iptv_data", "update_entry_data", "append_text_to_tview", "on_popup_menu", "get_picon_file_name",
"update_toggle_model", "update_filter_sat_positions", "get_pos_num")
import os
import re
import shutil
import unicodedata
from collections import defaultdict
from functools import lru_cache
from pathlib import Path
from urllib.parse import unquote
from gi.repository import GdkPixbuf, GLib
from app.commons import run_task
from app.eparser import Service
from app.eparser.ecommons import Flag, BouquetService, Bouquet, BqType
from app.eparser.enigma.bouquets import BqServiceType, to_bouquet_id
from app.settings import SettingsType
from .dialogs import show_dialog, DialogType, get_chooser_dialog, WaitDialog
from app.settings import SettingsType, SEP, IS_WIN, IS_DARWIN, IS_LINUX
from .dialogs import show_dialog, DialogType, get_message
from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON, KeyboardKey, Column
@@ -29,7 +69,7 @@ def insert_marker(view, bouquets, selected_bouquet, services, parent_window, m_t
show_dialog(DialogType.ERROR, parent_window, "The text of marker is empty, please try again!")
return
fav_id = "1:64:0:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n".format(response, response)
fav_id = f"1:64:0:0:0:0:0:0:0:0::{response}\n#DESCRIPTION {response}\n"
text = response
s_type = m_type.name
@@ -37,7 +77,7 @@ def insert_marker(view, bouquets, selected_bouquet, services, parent_window, m_t
marker = (None, None, text, None, None, s_type, None, fav_id, None, None, None)
itr = model.insert_before(model.get_iter(paths[0]), marker) if paths else model.insert(0, marker)
bouquets[selected_bouquet].insert(model.get_path(itr)[0], fav_id)
services[fav_id] = Service(None, None, None, text, None, None, None, s_type, None, None, None, None, None, None, None, None, None, 0, fav_id, None)
services[fav_id] = Service(None, None, None, text, None, None, None, s_type, *[None] * 9, 0, fav_id, None)
# ***************** Movement *******************#
@@ -75,8 +115,8 @@ def move_items(key, view: Gtk.TreeView):
children_num = model.iter_n_children(parent_itr)
if key in (KeyboardKey.PAGE_DOWN, KeyboardKey.END, KeyboardKey.END_KP, KeyboardKey.PAGE_DOWN_KP):
children_num -= 1
min_path = Gtk.TreePath.new_from_string("{}:{}".format(parent_index, 0))
max_path = Gtk.TreePath.new_from_string("{}:{}".format(parent_index, children_num))
min_path = Gtk.TreePath.new_from_string(f"{parent_index}:{0}")
max_path = Gtk.TreePath.new_from_string(f"{parent_index}:{children_num}")
is_tree_store = True
if key is KeyboardKey.UP:
@@ -246,7 +286,7 @@ def set_lock(blacklist, services, model, paths, target, services_model):
locked = has_locked_hide(model, paths, col_num)
ids = []
skip_type = {BqServiceType.MARKER.name, BqServiceType.SPACE.name}
skip_type = {BqServiceType.MARKER.name, BqServiceType.SPACE.name, BqServiceType.ALT.name}
for path in paths:
itr = model.get_iter(path)
@@ -280,7 +320,7 @@ def set_hide(services, model, paths):
for path in paths:
itr = model.get_iter(path)
model.set_value(itr, col_num, None if hide else HIDE_ICON)
flags = list(model.get_value(itr, 0).split(","))
flags = [*model.get_value(itr, 0).split(",")]
index, flag = None, None
for i, fl in enumerate(flags):
if fl.startswith("f:"):
@@ -324,7 +364,7 @@ def has_locked_hide(model, paths, col_num):
# ***************** Location *******************#
def locate_in_services(fav_view, services_view, parent_window):
def locate_in_services(fav_view, services_view, column, parent_window):
""" Locating and scrolling to the service """
model, paths = fav_view.get_selection().get_selected_rows()
@@ -336,7 +376,7 @@ def locate_in_services(fav_view, services_view, parent_window):
fav_id = model.get_value(model.get_iter(paths[0]), Column.FAV_ID)
for index, row in enumerate(services_view.get_model()):
if row[Column.SRV_FAV_ID] == fav_id:
if row[column] == fav_id:
scroll_to(index, services_view)
break
@@ -351,26 +391,36 @@ def scroll_to(index, view, paths=None):
selection.select_path(index)
# ***************** Picons *********************#
# ***************** Picons ********************* #
def update_picons_data(path, picons, size=32):
if not os.path.exists(path):
return
def get_picon_dialog(transient, title, button_text, multiple=True):
""" Returns a copy dialog with a preview of images [picons -> *.png]. """
dialog = Gtk.FileChooserNative.new(title, transient, Gtk.FileChooserAction.OPEN, button_text)
dialog.set_select_multiple(multiple)
dialog.set_modal(True)
# Filter.
file_filter = Gtk.FileFilter()
file_filter.set_name("*.png")
file_filter.add_pattern("*.png")
file_filter.add_mime_type("image/png") if IS_DARWIN else None
dialog.add_filter(file_filter)
for file in os.listdir(path):
pf = get_picon_pixbuf(path + file, size)
if pf:
picons[file] = pf
if IS_LINUX:
preview_image = Gtk.Image(margin_right=10)
dialog.set_preview_widget(preview_image)
def update_preview_widget(dlg):
path = dialog.get_preview_filename()
if not path:
return
def append_picons(picons, model):
def append_picons_data(pcs, mod):
for r in mod:
mod.set_value(mod.get_iter(r.path), Column.SRV_PICON, pcs.get(r[Column.SRV_PICON_ID], None))
yield True
pix = get_picon_pixbuf(path, 220)
preview_image.set_from_pixbuf(pix)
dlg.set_preview_widget_active(bool(pix))
app = append_picons_data(picons, model)
GLib.idle_add(lambda: next(app, False), priority=GLib.PRIORITY_LOW)
dialog.connect("update-preview", update_preview_widget)
return dialog
def assign_picons(target, srv_view, fav_view, transient, picons, settings, services, src_path=None, dst_path=None):
@@ -380,10 +430,16 @@ def assign_picons(target, srv_view, fav_view, transient, picons, settings, servi
picons_files = []
if not src_path:
src_path = get_chooser_dialog(transient, settings, "*.png files", ("*.png",))
if src_path == Gtk.ResponseType.CANCEL:
dialog = get_picon_dialog(transient, get_message("Picon selection"), get_message("Open"), False)
if dialog.run() in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT) or not dialog.get_filenames():
return picons_files
src_path = dialog.get_filenames()[0]
if IS_WIN:
src_path = src_path.lstrip("/")
dst_path = dst_path.lstrip("/") if dst_path else dst_path
if not str(src_path).endswith(".png") or not os.path.isfile(src_path):
show_dialog(DialogType.ERROR, transient, text="No png file is selected!")
return picons_files
@@ -402,7 +458,7 @@ def assign_picons(target, srv_view, fav_view, transient, picons, settings, servi
picon_id = services.get(fav_id)[Column.SRV_PICON_ID]
if picon_id:
picons_path = dst_path or settings.picons_local_path
picons_path = dst_path or settings.profile_picons_path
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
picon_file = picons_path + picon_id
try:
@@ -411,7 +467,7 @@ def assign_picons(target, srv_view, fav_view, transient, picons, settings, servi
pass # NOP
else:
picons_files.append(picon_file)
picon = get_picon_pixbuf(picon_file)
picon = get_picon_pixbuf(picon_file, settings.list_picon_size)
picons[picon_id] = picon
model.set_value(itr, p_pos, picon)
if target is ViewTarget.SERVICES:
@@ -471,36 +527,17 @@ def remove_picon(target, srv_view, fav_view, picons, settings):
remove_picons(settings, 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(settings, picons, services):
def remove_all_unused_picons(settings, services):
""" Removes picons from profile picons folder if there are no services for these picons. """
ids = {s.picon_id for s in services}
pcs = list(filter(lambda x: x not in ids, picons))
remove_picons(settings, pcs, picons)
for p in Path(settings.profile_picons_path).glob("*.png"):
if p.name not in ids and p.is_file():
p.unlink()
def remove_picons(settings, picon_ids, picons):
pions_path = settings.picons_local_path
backup_path = settings.backup_local_path + "picons/"
pions_path = settings.profile_picons_path
backup_path = f"{settings.profile_backup_path}picons{SEP}"
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
for p_id in picon_ids:
picons[p_id] = None
@@ -524,14 +561,27 @@ def is_only_one_item_selected(paths, transient):
def get_picon_pixbuf(path, size=32):
try:
return GdkPixbuf.Pixbuf.new_from_file_at_scale(path, width=size, height=size, preserve_aspect_ratio=True)
except GLib.GError as e:
pass
except GLib.GError:
pass # NOP
# ***************** Bouquets *********************#
@lru_cache(50)
def get_picon_file_name(service_name):
""" Returns picon file name by service name. """
name = unicodedata.normalize("NFKD", service_name).encode("ASCII", errors="ignore").decode(errors="ignore")
return f"{re.sub('[^a-z0-9]', '', name.replace('&', 'and').replace('+', 'plus').replace('*', 'star').lower())}.png"
# ***************** Bouquets ********************* #
def gen_bouquets(view, bq_view, transient, gen_type, s_type, callback):
""" Auto-generate and append list of bouquets """
""" Auto-generate and append list of bouquets. """
model, paths = view.get_selection().get_selected_rows()
single_types = (BqGenType.SAT, BqGenType.PACKAGE, BqGenType.TYPE)
if gen_type in single_types:
if not is_only_one_item_selected(paths, transient):
return
fav_id_index = Column.SRV_FAV_ID
index = Column.SRV_TYPE
if gen_type in (BqGenType.PACKAGE, BqGenType.EACH_PACKAGE):
@@ -539,60 +589,79 @@ def gen_bouquets(view, bq_view, transient, gen_type, s_type, callback):
elif gen_type in (BqGenType.SAT, BqGenType.EACH_SAT):
index = Column.SRV_POS
model, paths = view.get_selection().get_selected_rows()
# Splitting services [caching] by column value.
s_data = defaultdict(list)
for row in model:
s_data[row[index]].append(BouquetService(None, BqServiceType.DEFAULT, row[fav_id_index], 0))
bq_type = BqType.BOUQUET.value if s_type is SettingsType.NEUTRINO_MP else BqType.TV.value
if gen_type in (BqGenType.SAT, BqGenType.PACKAGE, BqGenType.TYPE):
if not is_only_one_item_selected(paths, transient):
return
service = Service(*model[paths][:Column.SRV_TOOLTIP])
append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model,
[service.package if gen_type is BqGenType.PACKAGE else
service.pos if gen_type is BqGenType.SAT else service.service_type], s_type)
else:
wait_dialog = WaitDialog(transient)
wait_dialog.show()
append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model,
{row[index] for row in model}, s_type, wait_dialog)
@run_task
def append_bouquets(bq_type, bq_view, callback, fav_id_index, index, model, names, s_type, wait_dialog=None):
bq_index = 0 if s_type is SettingsType.ENIGMA_2 else 1
bq_root_iter = bq_view.get_model().get_iter(bq_index)
srv = Service(*model[paths][:Column.SRV_TOOLTIP])
cond = srv.package if gen_type is BqGenType.PACKAGE else srv.pos if gen_type is BqGenType.SAT else srv.service_type
bq_view.expand_row(Gtk.TreePath(bq_index), 0)
bqs_model = bq_view.get_model()
bouquets_names = get_bouquets_names(bqs_model)
for pos, name in enumerate(sorted(names)):
if name not in bouquets_names:
services = [BouquetService(None, BqServiceType.DEFAULT, row[fav_id_index], 0)
for row in model if row[index] == name]
callback(Bouquet(name=name, type=bq_type, services=services, locked=None, hidden=None),
bqs_model.get_iter(bq_index))
bq_names = get_bouquets_names(bq_view.get_model())
if wait_dialog is not None:
wait_dialog.destroy()
if gen_type in single_types:
if cond in bq_names:
show_dialog(DialogType.ERROR, transient, "A bouquet with that name exists!")
else:
callback(Bouquet(cond, bq_type, s_data.get(cond)), bq_root_iter)
else:
# We add a bouquet only if the given name is missing [keys - names]!
for name in sorted(s_data.keys() - bq_names):
callback(Bouquet(name, BqType.TV.value, s_data.get(name)), bq_root_iter)
def get_bouquets_names(model):
""" Returns all current bouquets names """
bouquets_names = []
bouquets_names = set()
for row in model:
itr = row.iter
if model.iter_has_child(itr):
num_of_children = model.iter_n_children(itr)
for num in range(num_of_children):
child_itr = model.iter_nth_child(itr, num)
bouquets_names.append(model[child_itr][0])
bouquets_names.add(model[child_itr][0])
return bouquets_names
# ***************** Others *********************#
# ***************** Others ********************* #
def copy_reference(view, app):
""" Copying picon id to clipboard. """
model, paths = view.get_selection().get_selected_rows()
if not is_only_one_item_selected(paths, app.app_window):
return
target = app.get_target_view(view)
clipboard = app._clipboard
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:
app.show_error_message("No reference is present!")
elif target is ViewTarget.FAV:
fav_id = model.get_value(model.get_iter(paths), Column.FAV_ID)
srv = app.current_services.get(fav_id, None)
if srv and srv.picon_id:
clipboard.set_text(get_service_reference(srv), -1)
else:
app.show_error_message("No reference is present!")
app.emit("clipboard-changed", clipboard.wait_is_text_available())
def get_service_reference(srv):
return srv.picon_id.rstrip(".png")
def update_entry_data(entry, dialog, settings):
""" Updates value in text entry from chooser dialog. """
response = show_dialog(dialog_type=DialogType.CHOOSER, transient=dialog, settings=settings,
action_type=Gtk.FileChooserAction.CREATE_FOLDER if settings.is_darwin else None,
create_dir=True)
response = show_dialog(dialog_type=DialogType.CHOOSER, transient=dialog, settings=settings, create_dir=True)
if response not in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
entry.set_text(response)
return response
@@ -629,6 +698,40 @@ def get_model_data(view):
return model_name, model
def update_toggle_model(model, path, toggle):
""" Updates the toggle state for the model. """
active = not toggle.get_active()
if path == "0":
model.foreach(lambda m, p, i: m.set_value(i, 1, active))
else:
model.set_value(model.get_iter(path), 1, active)
if active:
model.set_value(model.get_iter_first(), 1, len({r[0] for r in model if r[1]}) == len(model) - 1)
else:
model.set_value(model.get_iter_first(), 1, False)
def update_filter_sat_positions(model, sat_positions):
""" Updates the values for the satellite positions button model. """
first = model[model.get_iter_first()][:]
model.clear()
model.append((first[0], True))
sat_positions.discard(first[0])
list(map(lambda pos: model.append((pos, True)), sorted(sat_positions, key=get_pos_num, reverse=True)))
def get_pos_num(pos):
""" Returns num [float] representation of satellite position. """
if not pos:
return -183.0
if len(pos) > 1:
m = -1 if pos[-1] == "W" else 1
return float(pos[:-1]) * m
return -181.0 if pos == "T" else -182.0
def append_text_to_tview(char, view):
""" Appending text and scrolling to a given line in the text view. """
buf = view.get_buffer()
@@ -637,9 +740,9 @@ def append_text_to_tview(char, view):
view.scroll_to_mark(insert, 0.0, True, 0.0, 1.0)
def get_iptv_url(row, s_type):
def get_iptv_url(row, s_type, column=Column.FAV_ID):
""" Returns url from iptv type row """
data = row[Column.FAV_ID].split(":" if s_type is SettingsType.ENIGMA_2 else "::")
data = row[column].split(":" if s_type is SettingsType.ENIGMA_2 else "::")
if s_type is SettingsType.ENIGMA_2:
data = list(filter(lambda x: "http" in x, data))
if data:
@@ -647,6 +750,15 @@ def get_iptv_url(row, s_type):
return unquote(url) if s_type is SettingsType.ENIGMA_2 else url
def get_iptv_data(fav_id):
""" Returns the reference and URL as a tuple from the fav_id. """
data, sep, desc = fav_id.partition("#DESCRIPTION")
data = data.split(":")
if len(data) < 11:
return None, None, desc
return ":".join(data[:10]), unquote(data[10].strip())
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:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -35,27 +35,36 @@ from urllib.parse import urlparse, unquote
from gi.repository import GLib, GdkPixbuf, Gio
from app.commons import run_idle, run_task, run_with_delay
from app.commons import run_idle, run_task, run_with_delay, log
from app.connections import upload_data, DownloadType, download_data, remove_picons
from app.settings import SettingsType, Settings
from app.settings import SettingsType, Settings, SEP, IS_DARWIN
from app.tools.picons import (PiconsParser, parse_providers, Provider, convert_to, download_picon, PiconsCzDownloader,
PiconsError)
from app.tools.satellites import SatellitesParser, SatelliteSource
from .dialogs import show_dialog, DialogType, get_message, get_builder
from .main_helper import update_entry_data, append_text_to_tview, scroll_to, on_popup_menu, get_base_model, set_picon, \
get_picon_pixbuf
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TV_ICON, Column, KeyboardKey
from .dialogs import show_dialog, DialogType, get_message, get_builder, get_chooser_dialog
from .main_helper import (scroll_to, on_popup_menu, get_base_model, set_picon, get_picon_pixbuf, get_picon_dialog,
get_picon_file_name)
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TV_ICON, Column, KeyboardKey, Page, ViewTarget
class PiconsDialog:
class PiconManager(Gtk.Box):
class DownloadSource(Enum):
LYNG_SAT = "lyngsat"
PICON_CZ = "piconcz"
def __init__(self, transient, settings, picon_ids, sat_positions, app):
def __init__(self, app, settings, picon_ids, sat_positions, *args, **kwargs):
super().__init__(*args, **kwargs)
self._app = app
self._app.connect("data-receive", self.on_download)
self._app.connect("data-send", self.on_send)
self._app.connect("page-changed", self.update_picons_dest)
self._app.connect("filter-toggled", self.on_app_filter_toggled)
self._app.connect("profile-changed", self.on_profile_changed)
self._app.connect("picon-assign", self.on_picon_assign)
self._app.fav_view.connect("row-activated", self.on_fav_changed)
self._picon_ids = picon_ids
self._sat_positions = sat_positions
self._app = app
self._BASE_URL = "www.lyngsat.com/packages/"
self._PATTERN = re.compile(r"^https://www\.lyngsat\.com/[\w-]+\.html$")
self._POS_PATTERN = re.compile(r"^\d+\.\d+[EW]?$")
@@ -72,35 +81,26 @@ class PiconsDialog:
self._download_src = self.DownloadSource.PICON_CZ
self._picon_cz_downloader = None
handlers = {"on_receive": self.on_receive,
handlers = {"on_tool_switched": self.on_tool_switched,
"on_add": self.on_add,
"on_extract": self.on_extract,
"on_receive": self.on_receive,
"on_cancel": self.on_cancel,
"on_close": self.on_close,
"on_send": self.on_send,
"on_download": self.on_download,
"on_remove": self.on_remove,
"on_info_bar_close": self.on_info_bar_close,
"on_picons_dir_open": self.on_picons_dir_open,
"on_selected_toggled": self.on_selected_toggled,
"on_url_changed": self.on_url_changed,
"on_picons_filter_changed": self.on_picons_filter_changed,
"on_position_edited": self.on_position_edited,
"on_visible_page": self.on_visible_page,
"on_convert": self.on_convert,
"on_picons_src_changed": self.on_picons_src_changed,
"on_picons_dest_changed": self.on_picons_dest_changed,
"on_picons_view_drag_data_get": self.on_picons_view_drag_data_get,
"on_picons_src_view_drag_drop": self.on_picons_src_view_drag_drop,
"on_picons_src_view_drag_data_received": self.on_picons_src_view_drag_data_received,
"on_picons_src_view_drag_end": self.on_picons_src_view_drag_end,
"on_picons_view_drag_drop": self.on_picons_view_drag_drop,
"on_picons_view_drag_data_received": self.on_picons_view_drag_data_received,
"on_picons_view_drag_end": self.on_picons_view_drag_end,
"on_picon_info_image_drag_data_received": self.on_picon_info_image_drag_data_received,
"on_send_button_drag_data_received": self.on_send_button_drag_data_received,
"on_download_button_drag_data_received": self.on_download_button_drag_data_received,
"on_remove_button_drag_data_received": self.on_remove_button_drag_data_received,
"on_selective_send": self.on_selective_send,
"on_selective_download": self.on_selective_download,
"on_selective_remove": self.on_selective_remove,
"on_local_remove": self.on_local_remove,
"on_picons_dest_view_realize": self.on_picons_dest_view_realize,
"on_download_source_changed": self.on_download_source_changed,
"on_satellites_view_realize": self.on_satellites_view_realize,
"on_satellite_filter_toggled": self.on_satellite_filter_toggled,
@@ -110,16 +110,15 @@ class PiconsDialog:
"on_unselect_all": self.on_unselect_all,
"on_filter_toggled": self.on_filter_toggled,
"on_fiter_srcs_toggled": self.on_fiter_srcs_toggled,
"on_filter_services_switch": self.on_filter_services_switch,
"on_picon_activated": self.on_picon_activated,
"on_view_query_tooltip": self.on_view_query_tooltip,
"on_tree_view_key_press": self.on_tree_view_key_press,
"on_popup_menu": on_popup_menu}
builder = get_builder(UI_RESOURCES_PATH + "picons_manager.glade", handlers)
builder = get_builder(UI_RESOURCES_PATH + "picons.glade", handlers)
self._dialog = builder.get_object("picons_dialog")
self._dialog.set_transient_for(transient)
self._app_window = app.get_active_window()
self._stack = builder.get_object("stack")
self._picons_src_view = builder.get_object("picons_src_view")
self._picons_dest_view = builder.get_object("picons_dest_view")
self._providers_view = builder.get_object("providers_view")
@@ -128,21 +127,10 @@ class PiconsDialog:
self._picons_src_filter_model.set_visible_func(self.picons_src_filter_function)
self._picons_dst_filter_model = builder.get_object("picons_dst_filter_model")
self._picons_dst_filter_model.set_visible_func(self.picons_dst_filter_function)
self._explorer_src_path_button = builder.get_object("explorer_src_path_button")
self._explorer_dest_path_button = builder.get_object("explorer_dest_path_button")
self._expander = builder.get_object("expander")
self._text_view = builder.get_object("text_view")
self._info_bar = builder.get_object("info_bar")
self._filter_bar = builder.get_object("filter_bar")
self._filter_button = builder.get_object("filter_button")
self._src_filter_button = builder.get_object("src_filter_button")
self._dst_filter_button = builder.get_object("dst_filter_button")
self._picons_filter_entry = builder.get_object("picons_filter_entry")
self._picons_dir_entry = builder.get_object("picons_dir_entry")
self._message_label = builder.get_object("info_bar_message_label")
self._info_toggle_button = builder.get_object("info_toggle_button")
self._picon_info_image = builder.get_object("picon_info_image")
self._picon_info_label = builder.get_object("picon_info_label")
self._current_path_label = builder.get_object("current_path_label")
self._download_source_button = builder.get_object("download_source_button")
self._receive_button = builder.get_object("receive_button")
self._convert_button = builder.get_object("convert_button")
@@ -157,42 +145,60 @@ class PiconsDialog:
self._resize_no_radio_button = builder.get_object("resize_no_radio_button")
self._resize_220_132_radio_button = builder.get_object("resize_220_132_radio_button")
self._resize_100_60_radio_button = builder.get_object("resize_100_60_radio_button")
self._explorer_action_box = builder.get_object("explorer_action_box")
self._satellite_label = builder.get_object("satellite_label")
self._provider_header_label = builder.get_object("provider_header_label")
self._src_link_button = builder.get_object("src_link_button")
self._satellite_filter_switch = builder.get_object("satellite_filter_switch")
self._bouquet_filter_switch = builder.get_object("bouquet_filter_switch")
self._bouquet_filter_grid = builder.get_object("bouquet_filter_grid")
self._providers_header_box = builder.get_object("providers_header_box")
self._header_download_box = builder.get_object("header_download_box")
self._satellite_label.bind_property("visible", builder.get_object("loading_data_label"), "visible", 4)
self._satellite_label.bind_property("visible", builder.get_object("loading_data_spinner"), "visible", 4)
self._satellite_label.bind_property("visible", self._download_source_button, "sensitive")
self._satellite_label.bind_property("visible", self._satellites_view, "sensitive")
self._cancel_button.bind_property("visible", self._header_download_box, "visible", 4)
self._convert_button.bind_property("visible", self._header_download_box, "visible", 4)
self._download_source_button.bind_property("visible", self._receive_button, "visible")
self._cancel_button.bind_property("visible", builder.get_object("receive_button"), "visible", 4)
self._convert_button.bind_property("visible", self._explorer_action_box, "visible", 4)
downloader_action_box = builder.get_object("downloader_action_box")
self._explorer_action_box.bind_property("visible", downloader_action_box, "visible", 4)
self._convert_button.bind_property("visible", downloader_action_box, "visible", 4)
self._filter_bar.bind_property("search-mode-enabled", self._filter_bar, "visible")
self._explorer_src_path_button.bind_property("sensitive", builder.get_object("picons_view_sw"), "sensitive")
self._filter_button.bind_property("active", builder.get_object("filter_service_box"), "visible")
self._filter_button.bind_property("active", builder.get_object("src_title_grid"), "visible")
self._filter_button.bind_property("active", builder.get_object("dst_title_grid"), "visible")
self._filter_button.bind_property("visible", self._info_toggle_button, "visible")
explorer_info_bar = builder.get_object("explorer_info_bar")
explorer_info_bar.bind_property("visible", builder.get_object("explorer_info_bar_frame"), "visible")
self._info_toggle_button.bind_property("active", explorer_info_bar, "visible")
# Info.
self._dst_count_label = builder.get_object("dst_count_label")
self._info_check_button = builder.get_object("info_check_button")
self._picon_info_image = builder.get_object("picon_info_image")
self._picon_info_label = builder.get_object("picon_info_label")
# Filter.
self._filter_bar = builder.get_object("filter_bar")
self._auto_filter_switch = builder.get_object("auto_filter_switch")
self._filter_button = builder.get_object("filter_button")
self._filter_button.bind_property("active", self._filter_bar, "visible")
self._filter_button.bind_property("active", self._src_filter_button, "visible")
self._filter_button.bind_property("active", self._dst_filter_button, "visible")
self._filter_button.bind_property("visible", self._info_check_button, "visible")
self._filter_button.bind_property("visible", self._remove_button, "visible")
self._src_button = builder.get_object("src_button")
self._src_button.bind_property("active", builder.get_object("explorer_dst_label"), "visible")
self._src_button.bind_property("active", builder.get_object("src_picon_box_frame"), "visible")
self._filter_button.bind_property("visible", self._src_button, "visible")
self._info_check_button.bind_property("active", builder.get_object("explorer_info_box_frame"), "visible")
# Header buttons. -> Used instead stack switcher.
self._manager_button = builder.get_object("manager_button")
self._manager_button.bind_property("active", builder.get_object("manager_label"), "visible")
self._downloader_button = builder.get_object("downloader_button")
self._downloader_button.bind_property("active", builder.get_object("downloader_label"), "visible")
self._converter_button = builder.get_object("converter_button")
self._converter_button.bind_property("active", builder.get_object("converter_label"), "visible")
self._manager_button.bind_property("active", builder.get_object("add_menu_button"), "visible")
# Init drag-and-drop
self.init_drag_and_drop()
# Rendering.
column = builder.get_object("dest_picon_column")
column.set_cell_data_func(builder.get_object("picons_dest_renderer"), self.picon_data_func)
column = builder.get_object("src_picon_column")
column.set_cell_data_func(builder.get_object("picons_src_renderer"), self.picon_data_func)
# Settings
self._settings = settings
self._s_type = settings.setting_type
self._picons_dir_entry.set_text(self._settings.picons_local_path)
self._current_path_label.set_text(self._settings.profile_picons_path)
window_size = self._settings.get("picons_downloader_window_size")
if window_size:
self._dialog.resize(*window_size)
self.pack_start(builder.get_object("main_frame"), True, True, 0)
self.show()
if not len(self._picon_ids) and self._s_type is SettingsType.ENIGMA_2:
message = get_message("To automatically set the identifiers for picons,\n"
@@ -200,57 +206,93 @@ class PiconsDialog:
self.show_info_message(message, Gtk.MessageType.WARNING)
self._satellite_label.show()
def show(self):
self._dialog.show()
def on_tool_switched(self, button):
if not button.get_active():
return True
def on_picons_dest_view_realize(self, view):
self._services = {s.picon_id: s for s in self._app.current_services.values() if s.picon_id}
self._explorer_dest_path_button.select_filename(self._settings.picons_local_path)
is_explorer = button is self._manager_button
is_downloader = button is self._downloader_button
is_converter = button is self._converter_button
def on_picons_src_changed(self, button):
self.update_picons_data(self._picons_src_view, button)
name = "explorer"
if is_downloader:
name = "downloader"
elif is_converter:
name = "converter"
def on_picons_dest_changed(self, button):
self.update_picon_info()
self.update_picons_data(self._picons_dest_view, button)
self._stack.set_visible_child_name(name)
def update_picons_data(self, view, button):
path = button.get_filename()
if not path or not os.path.exists(path):
self._convert_button.set_visible(is_converter)
self._download_source_button.set_visible(is_downloader)
self._filter_button.set_visible(is_explorer)
if is_explorer:
self.update_picons_data(self._picons_dest_view)
def on_open(self):
""" Opens picons from local path [in src view]. """
response = show_dialog(DialogType.CHOOSER, self._app.app_window, settings=self._settings, title="Open folder")
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
GLib.idle_add(button.set_sensitive, False)
gen = self.update_picons(path, view, button)
self._src_button.set_active(True)
self.update_picons_data(self._picons_src_view, response)
def update_picons_dest(self, app, page):
if page is Page.PICONS:
self._services = {s.picon_id: s for s in self._app.current_services.values() if s.picon_id}
self.update_picons_data(self._picons_dest_view)
def on_profile_changed(self, app, data):
self._current_path_label.set_text(self._settings.profile_picons_path)
self.update_picons_dest(app, self._app.page)
def on_picon_assign(self, app, target):
if target is ViewTarget.SERVICES:
model, paths = app.services_view.get_selection().get_selected_rows()
ids = {model[p][Column.SRV_FAV_ID] for p in paths}
else:
model, paths = app.fav_view.get_selection().get_selected_rows()
ids = {model[p][Column.FAV_ID] for p in paths}
self._filter_button.set_active(True)
self._dst_filter_button.set_active(True)
self._picons_filter_entry.set_text(
"|".join(s.service for f, s in self._app.current_services.items() if f in ids))
def update_picons_data(self, view, path=None):
if view is self._picons_dest_view:
self.update_picon_info()
gen = self.update_picons(path or self._settings.profile_picons_path, view)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def update_picons(self, path, view, button):
def update_picons(self, path, view):
p_model = view.get_model()
if not p_model:
button.set_sensitive(True)
return
model = get_base_model(p_model)
view.set_model(None)
factor = self._app.DEL_FACTOR
factor = self._app.DEL_FACTOR * 2
for index, itr in enumerate([row.iter for row in model]):
model.remove(itr)
if index % factor == 0:
yield True
for file in os.listdir(path):
self._dst_count_label.set_text("0")
os.makedirs(os.path.dirname(path), exist_ok=True)
for index, file in enumerate(os.listdir(path)):
if self._terminate:
return
p_path = "{}/{}".format(path, file)
p = self.get_pixbuf_at_scale(p_path, 72, 48, True)
if p:
yield model.append((p, file, p_path))
model.append((None, file, f"{path}{SEP}{file}"))
if index % factor == 0:
self._dst_count_label.set_text(str(len(model)))
yield True
view.set_model(p_model)
button.set_sensitive(True)
self._dst_count_label.set_text(str(len(model)))
yield True
def picon_data_func(self, column, renderer, model, itr, data):
renderer.set_property("pixbuf", self.get_pixbuf_at_scale(model.get_value(itr, 2), 72, 48, True))
def update_picons_from_file(self, view, uri):
""" Adds picons in the view on dragging from file system. """
path = Path(urlparse(unquote(uri)).path.strip())
@@ -265,7 +307,7 @@ class PiconsDialog:
if p:
model.append((p, path.name, f_path))
elif path.is_dir():
self._explorer_src_path_button.select_filename(f_path)
self.update_picons_data(view, f_path)
def get_pixbuf_at_scale(self, path, width, height, p_ratio):
try:
@@ -285,31 +327,27 @@ class PiconsDialog:
self._picons_src_view.enable_model_drag_dest([], Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE)
self._picons_src_view.drag_dest_add_text_targets()
self._picons_dest_view.enable_model_drag_dest([], Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE)
self._picons_dest_view.drag_dest_add_text_targets()
self._picon_info_image.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
self._picon_info_image.drag_dest_add_uri_targets()
self._send_button.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
self._send_button.drag_dest_add_uri_targets()
self._download_button.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
self._download_button.drag_dest_add_uri_targets()
self._remove_button.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
self._remove_button.drag_dest_add_uri_targets()
def on_picons_view_drag_data_get(self, view, drag_context, data, info, time):
model, path = view.get_selection().get_selected_rows()
if path:
p_uri = Path(model[path][-1]).as_uri()
dest_uri = Path(self._explorer_dest_path_button.get_filename()).as_uri()
data.set_uris(["{}::::{}".format(p_uri, dest_uri)])
dest_uri = Path(self._settings.profile_picons_path).as_uri()
if IS_DARWIN:
data.set_uris([f"{Path(model[path][-1]).as_uri()}{self._app.DRAG_SEP}{dest_uri}"])
else:
data.set_uris([Path(model[path][-1]).as_uri(), dest_uri])
def on_picons_src_view_drag_drop(self, view, drag_context, x, y, time):
def on_picons_view_drag_drop(self, view, drag_context, x, y, time):
view.stop_emission_by_name("drag_drop")
targets = drag_context.list_targets()
view.drag_get_data(drag_context, targets[-1] if targets else Gdk.atom_intern("text/plain", False), time)
def on_picons_src_view_drag_data_received(self, view, drag_context, x, y, data, info, time):
def on_picons_view_drag_data_received(self, view, drag_context, x, y, data, info, time):
view.stop_emission_by_name("drag_data_received")
txt = data.get_text()
if not txt:
@@ -319,8 +357,8 @@ class PiconsDialog:
self.update_picons_from_file(view, txt)
return
itr_str, sep, src = txt.partition("::::")
if src == self._app.BQ_MODEL_NAME:
itr_str, sep, src = txt.partition(self._app.DRAG_SEP)
if src == self._app.BQ_MODEL:
return
path, pos = view.get_dest_row_at_pos(x, y) or (None, None)
@@ -328,7 +366,7 @@ class PiconsDialog:
return
model = view.get_model()
if src == self._app.FAV_MODEL_NAME:
if src == self._app.FAV_MODEL:
target_view = self._app.fav_view
c_id = Column.FAV_ID
else:
@@ -336,8 +374,8 @@ class PiconsDialog:
c_id = Column.SRV_FAV_ID
t_mod = target_view.get_model()
dest_path = self._explorer_dest_path_button.get_filename() + "/"
self.update_picons_dest_view(self._app.on_assign_picon(target_view, model[path][-1], dest_path))
dest_path = self._settings.profile_picons_path
self.update_picons_dest_view(self._app.on_assign_picon_file(target_view, model[path][-1], dest_path))
self.show_assign_info([t_mod.get_value(t_mod.get_iter_from_string(itr), c_id) for itr in itr_str.split(",")])
@run_idle
@@ -358,17 +396,18 @@ class PiconsDialog:
itr = dest_model.append((p, p_name, p_path))
scroll_to(dest_model.get_path(itr), self._picons_dest_view)
self._dst_count_label.set_text(str(len(dest_model)))
@run_idle
def show_assign_info(self, fav_ids):
self._expander.set_expanded(True)
self._text_view.get_buffer().set_text("")
self._app.change_action_state("on_logs_show", GLib.Variant.new_boolean(True))
for i in fav_ids:
srv = self._app.current_services.get(i, None)
if srv:
info = self._app.get_hint_for_srv_list(srv)
self.append_output("Picon assignment for the service:\n{}\n{}\n".format(info, " * " * 30))
log(f"Picon assignment for the service:\n{info}\n{' * ' * 30}\n")
def on_picons_src_view_drag_end(self, view, drag_context):
def on_picons_view_drag_end(self, view, drag_context):
self.update_picons_dest_view(self._app.picons_buffer)
def on_picon_info_image_drag_data_received(self, img, drag_context, x, y, data, info, time):
@@ -377,11 +416,10 @@ class PiconsDialog:
return
uris = data.get_uris()
if uris:
if len(uris) == 2:
name, fav_id = self._current_picon_info
src, sep, dst = uris[0].partition("::::")
src = urlparse(unquote(src)).path
dst = "{}/{}".format(urlparse(unquote(dst)).path, name)
src = urlparse(unquote(uris[0])).path
dst = f"{urlparse(unquote(uris[1])).path}{SEP}{name}"
if src != dst:
shutil.copy(src, dst)
for row in get_base_model(self._picons_dest_view.get_model()):
@@ -392,26 +430,10 @@ class PiconsDialog:
gen = self.update_picon_in_lists(dst, fav_id)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def on_send_button_drag_data_received(self, button, drag_context, x, y, data, info, time):
path = self.get_path_from_uris(data)
if path and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.OK:
self.on_send(files_filter={path.name}, path=path.parent)
def on_download_button_drag_data_received(self, button, drag_context, x, y, data, info, time):
path = self.get_path_from_uris(data)
if path:
self.on_download(files_filter={path.name})
def on_remove_button_drag_data_received(self, button, drag_context, x, y, data, info, time):
path = self.get_path_from_uris(data)
if path:
self.on_remove(files_filter={path.name})
def get_path_from_uris(self, data):
uris = data.get_uris()
if uris:
src, sep, dst = uris[0].partition("::::")
return Path(urlparse(unquote(src)).path).resolve()
if len(uris) == 2:
return Path(urlparse(unquote(uris[0])).path).resolve()
def update_picon_in_lists(self, dst, fav_id):
picon = get_picon_pixbuf(dst)
@@ -419,17 +441,62 @@ class PiconsDialog:
yield set_picon(fav_id, get_base_model(self._app.services_view.get_model()), picon, Column.SRV_FAV_ID, p_pos)
yield set_picon(fav_id, get_base_model(self._app.fav_view.get_model()), picon, Column.FAV_ID, p_pos)
# ************************ Add/Extract ******************************** #
def on_add(self, item):
""" Adds (copies) picons from an external folder to the profile picons folder. """
dialog = get_picon_dialog(self._app_window, get_message("Add picons"), get_message("Add"))
if dialog.run() in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
self.copy_picons_file(dialog.get_filenames())
def on_extract(self, item):
""" Extracts picons from an archives to the profile picons folder. """
file_filter = None
if IS_DARWIN:
file_filter = Gtk.FileFilter()
file_filter.set_name("*.zip, *.gz")
file_filter.add_mime_type("application/zip")
file_filter.add_mime_type("application/gzip")
response = get_chooser_dialog(self._app_window, self._settings, "*.zip, *.gz files",
("*.zip", "*.gz"), "Extract picons", file_filter)
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
arch_path = self._app.get_archive_path(response)
if arch_path:
self.copy_picons_file(Path(arch_path.name).glob("*.png"), arch_path.cleanup)
def copy_picons_file(self, files, callback=None):
""" Copies files to the profile picons folder. """
picon_path = self._settings.profile_picons_path
os.makedirs(os.path.dirname(picon_path), exist_ok=True)
try:
picons = [shutil.copy(p, picon_path) for p in files]
except shutil.SameFileError as e:
log(e)
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
self.update_picons_dest_view(picons)
self._app.update_picons()
finally:
if callback:
callback()
# ******************** Download/Upload/Remove ************************* #
def on_selective_send(self, view):
path = self.get_selected_path(view)
if path and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.OK:
self.on_send(files_filter={path.name}, path=path.parent)
if path:
self.on_picons_send(files_filter={path.name}, path=path.parent)
def on_selective_download(self, view):
path = self.get_selected_path(view)
if path:
self.on_download(files_filter={path.name})
self.on_picons_download(files_filter={path.name})
def on_selective_remove(self, view):
path = self.get_selected_path(view)
@@ -438,49 +505,63 @@ class PiconsDialog:
def on_local_remove(self, view):
model, paths = view.get_selection().get_selected_rows()
if paths and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.OK:
itr = model.get_iter(paths.pop())
p_path = Path(model.get_value(itr, 2)).resolve()
if p_path.is_file():
p_path.unlink()
base_model = get_base_model(model)
filter_model = model.get_model()
itr = filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr))
base_model.remove(itr)
if paths and show_dialog(DialogType.QUESTION, self._app_window) == Gtk.ResponseType.OK:
base_model = get_base_model(model)
filter_model = model.get_model()
to_del = []
def on_send(self, item=None, files_filter=None, path=None):
dest_path = path or self.check_dest_path()
if not dest_path:
return
for p in paths:
itr = model.get_iter(p)
p_path = Path(model.get_value(itr, 2)).resolve()
if p_path.is_file():
p_path.unlink()
to_del.append(filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr)))
list(map(base_model.remove, to_del))
self._app.update_picons()
if view is self._picons_dest_view:
self._dst_count_label.set_text(str(len(model)))
def on_send(self, app, page):
if page is Page.PICONS:
view = self._picons_src_view if self._picons_src_view.is_focus() else self._picons_dest_view
model, paths = view.get_selection().get_selected_rows()
if paths:
self.on_picons_send(files_filter={Path(model[p][-1]).resolve().name for p in paths})
else:
self._app.show_error_message("No selected item!")
def on_picons_send(self, item=None, files_filter=None, path=None):
dest_path = path or self._settings.profile_picons_path
settings = Settings(self._settings.settings)
settings.picons_local_path = "{}/".format(dest_path)
settings.profile_picons_path = f"{dest_path}{SEP}"
settings.current_profile = self._settings.current_profile
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
self.run_func(lambda: upload_data(settings=settings,
download_type=DownloadType.PICONS,
callback=self.append_output,
done_callback=lambda: self.show_info_message(get_message("Done!"),
Gtk.MessageType.INFO),
files_filter=files_filter))
def on_download(self, item=None, files_filter=None, path=None):
path = path or self.check_dest_path()
if not path:
return
def on_download(self, app, page):
if page is Page.PICONS:
self.on_picons_download()
def on_picons_download(self, item=None, files_filter=None, path=None):
path = path or self._settings.profile_picons_path
settings = Settings(self._settings.settings)
settings.picons_local_path = path + "/"
settings.profile_picons_path = path + SEP
settings.current_profile = self._settings.current_profile
self.run_func(lambda: download_data(settings=settings,
download_type=DownloadType.PICONS,
callback=self.append_output,
files_filter=files_filter), True)
def on_remove(self, item=None, files_filter=None):
if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.OK:
if show_dialog(DialogType.QUESTION, self._app_window) == Gtk.ResponseType.CANCEL:
return
self.run_func(lambda: remove_picons(settings=self._settings,
callback=self.append_output,
done_callback=lambda: self.show_info_message(get_message("Done!"),
Gtk.MessageType.INFO),
files_filter=files_filter))
@@ -490,23 +571,12 @@ class PiconsDialog:
if paths:
return Path(model[paths.pop()][-1]).resolve()
def check_dest_path(self):
""" Checks the destination path and returns if present. """
if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.OK:
return
path = self._explorer_dest_path_button.get_filename()
if not path:
show_dialog(DialogType.ERROR, transient=self._dialog, text="Select paths!")
return
return path
# ******************** Downloader ************************* #
def on_download_source_changed(self, button):
self._download_src = self.DownloadSource(button.get_active_id())
self.set_providers_header()
self._bouquet_filter_grid.set_sensitive(self._download_src is self.DownloadSource.PICON_CZ)
self._providers_header_box.set_sensitive(self._download_src is self.DownloadSource.PICON_CZ)
GLib.idle_add(self._providers_view.get_model().clear)
self.init_satellites(self._satellites_view)
@@ -551,19 +621,19 @@ class PiconsDialog:
@run_idle
def set_providers_header(self):
msg = "{} [{}]"
tooltip = ""
if self._download_src is self.DownloadSource.PICON_CZ:
tooltip = "https://picon.cz (by Chocholoušek)"
msg = msg.format(get_message("Package"), tooltip)
link = "https://picon.cz"
tooltip = f"{link} (by Chocholoušek)"
elif self._download_src is self.DownloadSource.LYNG_SAT:
tooltip = "https://www.lyngsat.com"
msg = msg.format(get_message("Providers"), tooltip)
link = "https://www.lyngsat.com"
tooltip = f"{get_message('Providers')} [{link}]"
else:
msg = ""
link = ""
tooltip = ""
self._provider_header_label.set_text(msg)
self._provider_header_label.set_tooltip_text(tooltip)
self._src_link_button.set_uri(link)
self._src_link_button.set_label(link)
self._src_link_button.set_tooltip_text(tooltip)
@run_task
def get_satellites(self, view):
@@ -572,7 +642,7 @@ class PiconsDialog:
self.show_info_message("Getting satellites list error!", Gtk.MessageType.ERROR)
self._sat_names = {s[1]: s[0] for s in self._sats} # position -> satellite name
self._picon_cz_downloader = PiconsCzDownloader(self._picon_ids, self.append_output)
self._picon_cz_downloader = PiconsCzDownloader(self._picon_ids)
self.init_satellites(view)
@run_task
@@ -599,7 +669,7 @@ class PiconsDialog:
try:
for sat in sorted(sats):
pos = sat[1]
name = "{} ({})".format(sat[0], pos)
name = f"{sat[0]} ({pos})"
if is_filter and pos not in self._sat_positions:
continue
if not model:
@@ -609,7 +679,6 @@ class PiconsDialog:
self._satellite_label.show()
def on_satellite_selection(self, view, path, column):
self.on_info_bar_close()
model = self._providers_view.get_model()
model.clear()
self._satellite_label.set_visible(False)
@@ -646,13 +715,13 @@ class PiconsDialog:
def on_receive(self, item):
if self._is_downloading:
self.show_dialog("The task is already running!", DialogType.ERROR)
self._app.show_error_message("The task is already running!")
return
providers = self.get_selected_providers()
if self._download_src is self.DownloadSource.PICON_CZ and len(providers) > 1:
self.show_dialog("Please, select only one item!", DialogType.ERROR)
self._app.show_error_message("Please, select only one item!")
return
self._cancel_button.show()
@@ -661,7 +730,7 @@ class PiconsDialog:
@run_task
def start_download(self, providers):
self._is_downloading = True
GLib.idle_add(self._expander.set_expanded, True)
self._app.change_action_state("on_logs_show", GLib.Variant.new_boolean(True))
for prv in providers:
if self._download_src is self.DownloadSource.LYNG_SAT and not self._POS_PATTERN.match(prv[2]):
@@ -671,7 +740,7 @@ class PiconsDialog:
return
try:
picons_path = self._picons_dir_entry.get_text()
picons_path = self._current_path_label.get_text()
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
providers = (Provider(*p) for p in providers)
@@ -687,6 +756,7 @@ class PiconsDialog:
if not self._resize_no_radio_button.get_active():
self.resize(picons_path)
finally:
self._app.update_picons()
GLib.idle_add(self._cancel_button.hide)
self._is_downloading = False
@@ -706,7 +776,7 @@ class PiconsDialog:
if pic:
picons.extend(pic)
# Getting picon images.
futures = {executor.submit(download_picon, *pic, self.append_output): pic for pic in picons}
futures = {executor.submit(download_picon, *pic): pic for pic in picons}
done, not_done = concurrent.futures.wait(futures, timeout=0)
while self._is_downloading and not_done:
done, not_done = concurrent.futures.wait(not_done, timeout=5)
@@ -725,10 +795,9 @@ class PiconsDialog:
try:
# We download it sequentially.
for p in providers:
self._picon_cz_downloader.download(p, path, p_ids)
[self._picon_cz_downloader.download(p, path, p_ids) for p in providers]
except PiconsError as e:
self.append_output("Error: {}\n".format(str(e)))
log(f"Error: {str(e)}\n")
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
@@ -741,21 +810,22 @@ class PiconsDialog:
model, paths = self._app.bouquets_view.get_selection().get_selected_rows()
if len(paths) > 1:
self.show_dialog("Please, select only one bouquet!", DialogType.ERROR)
self._app.show_error_message("Please, select only one bouquet!")
return
fav_bouquet = self._app.current_bouquets[bq_selected]
services = self._app.current_services
return {services.get(fav_id).picon_id for fav_id in fav_bouquet}
ids = set()
for s in (services.get(fav_id) for fav_id in fav_bouquet):
ids.add(s.picon_id)
ids.add(get_picon_file_name(s.service))
return ids
def process_provider(self, prv, picons_path):
self.append_output("Getting links to picons for: {}.\n".format(prv.name))
log(f"Getting links to picons for: {prv.name}.\n")
return PiconsParser.parse(prv, picons_path, self._picon_ids, self.get_picons_format())
@run_idle
def append_output(self, char):
append_text_to_tview(char, self._text_view)
@run_task
def resize(self, path):
self.show_info_message(get_message("Resizing..."), Gtk.MessageType.INFO)
@@ -764,7 +834,7 @@ class PiconsDialog:
from pathlib import Path
from PIL import Image
except ImportError as e:
self.show_info_message("{} {}".format(get_message("Conversion error."), e), Gtk.MessageType.ERROR)
self.show_info_message(f"{get_message('Conversion error.')} {e}", Gtk.MessageType.ERROR)
else:
res = (220, 132) if self._resize_220_132_radio_button.get_active() else (100, 60)
@@ -776,7 +846,7 @@ class PiconsDialog:
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
def on_cancel(self, item=None):
if self._is_downloading and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
if self._is_downloading and show_dialog(DialogType.QUESTION, self._app_window) == Gtk.ResponseType.CANCEL:
return True
self.terminate_task()
@@ -787,46 +857,21 @@ class PiconsDialog:
self._is_downloading = False
self.show_info_message(get_message("The task is canceled!"), Gtk.MessageType.WARNING)
def on_close(self, window, event):
if self.on_cancel():
return True
self._terminate = True
self._is_downloading = False
self.save_window_size(window)
self._app.update_picons()
GLib.idle_add(self._dialog.destroy)
def save_window_size(self, window):
size = window.get_size()
height = size.height - self._text_view.get_allocated_height() - self._info_bar.get_allocated_height()
self._settings.add("picons_downloader_window_size", (size.width, height))
@run_task
def run_func(self, func, update=False):
try:
GLib.idle_add(self._expander.set_expanded, True)
GLib.idle_add(self._explorer_action_box.set_sensitive, False)
self._app.change_action_state("on_logs_show", GLib.Variant.new_boolean(True))
GLib.idle_add(self._header_download_box.set_sensitive, False)
func()
except OSError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
finally:
GLib.idle_add(self._explorer_action_box.set_sensitive, True)
GLib.idle_add(self._header_download_box.set_sensitive, True)
if update:
self.on_picons_dest_changed(self._explorer_dest_path_button)
self.update_picons_data(self._picons_dest_view)
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(False)
self._message_label.set_text(get_message(text))
self._info_bar.set_message_type(message_type)
self._info_bar.set_visible(True)
def on_picons_dir_open(self, entry, icon, event_button):
update_entry_data(entry, self._dialog, settings=self._settings)
self._app.show_info_message(text, message_type)
@run_idle
def on_selected_toggled(self, toggle, path):
@@ -846,9 +891,17 @@ class PiconsDialog:
# *********************** Filter **************************** #
def on_app_filter_toggled(self, app, value):
if app.page is Page.PICONS:
self._filter_button.set_active(not self._filter_button.get_active())
def on_fav_changed(self, view, path, column):
if self._app.page is Page.PICONS and self._auto_filter_switch.get_active():
model = view.get_model()
self._picons_filter_entry.set_text(model.get_value(model.get_iter(path), Column.FAV_SERVICE))
def on_filter_toggled(self, button):
active = button.get_active()
self._filter_bar.set_search_mode(active)
active = self._filter_button.get_active()
if not active:
self._picons_filter_entry.set_text("")
@@ -856,22 +909,12 @@ class PiconsDialog:
""" Activates re-filtering for model when filter check-button has toggled. """
GLib.idle_add(filter_model.refilter, priority=GLib.PRIORITY_LOW)
def on_filter_services_switch(self, button, state):
""" Activates or deactivates filtering in the main list of services. """
if state:
self._filter_binding = self._picons_filter_entry.bind_property("text", self._app.filter_entry, "text")
self._app.filter_entry.set_text(self._picons_filter_entry.get_text())
else:
if self._filter_binding:
self._filter_binding.unbind()
self._app.filter_entry.set_text("")
@run_with_delay(0.5)
def on_picons_filter_changed(self, entry):
txt = entry.get_text().upper()
self._filter_cache.clear()
txt = entry.get_text().upper().split("|")
for s in self._app.current_services.values():
self._filter_cache[s.picon_id] = txt in s.service.upper()
self._filter_cache[s.picon_id] = any(t in s.service.upper() or t in str(s.picon_id) for t in txt)
GLib.idle_add(self._picons_src_filter_model.refilter, priority=GLib.PRIORITY_LOW)
GLib.idle_add(self._picons_dst_filter_model.refilter, priority=GLib.PRIORITY_LOW)
@@ -895,7 +938,7 @@ class PiconsDialog:
return txt in t.upper() or self._filter_cache.get(t, False)
def on_picon_activated(self, view):
if self._info_toggle_button.get_active():
if self._info_check_button.get_active():
model, path = view.get_selection().get_selected_rows()
if not path:
return
@@ -955,32 +998,21 @@ class PiconsDialog:
model = self._providers_view.get_model()
model.set_value(model.get_iter(path), 2, value)
@run_idle
def on_visible_page(self, stack: Gtk.Stack, param):
name = stack.get_visible_child_name()
self._convert_button.set_visible(name == "converter")
self._download_source_button.set_visible(name == "downloader")
is_explorer = name == "explorer"
self._explorer_action_box.set_visible(is_explorer)
if is_explorer:
self.on_picons_dest_changed(self._explorer_dest_path_button)
@run_idle
def on_convert(self, item):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
if show_dialog(DialogType.QUESTION, self._app_window) == 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!")
self._app.show_error_message("Select paths!")
return
self._expander.set_expanded(True)
self._app.change_action_state("on_logs_show", GLib.Variant.new_boolean(True))
convert_to(src_path=picons_path,
dest_path=save_path,
s_type=SettingsType.ENIGMA_2,
callback=self.append_output,
done_callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO))
@run_idle
@@ -996,7 +1028,7 @@ class PiconsDialog:
@run_idle
def show_dialog(self, message, dialog_type):
show_dialog(dialog_type, self._dialog, message)
show_dialog(dialog_type, self._app_window, message)
def get_picons_format(self):
picon_format = SettingsType.ENIGMA_2

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

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

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

@@ -0,0 +1,454 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" Additional module for playback. """
from functools import lru_cache
from gi.repository import GLib, GObject, Gio
from app.commons import run_idle, run_with_delay
from app.connections import HttpAPI
from app.eparser.ecommons import BqServiceType
from app.settings import PlayStreamsMode, IS_DARWIN, SettingsType
from app.tools.media import Player
from app.ui.dialogs import get_builder, get_message
from app.ui.main_helper import get_iptv_url
from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, Column, IS_GNOME_SESSION, Page
class PlayerBox(Gtk.Box):
def __init__(self, app, *args, **kwargs):
super().__init__(*args, **kwargs)
# Signals.
GObject.signal_new("playback-full-screen", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("playback-close", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("play", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("stop", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
self._app = app
self._app.connect("fav-clicked", self.on_fav_clicked)
self._app.connect("srv-clicked", self.on_srv_clicked)
self._app.connect("iptv-clicked", self.on_iptv_clicked)
self._app.connect("page-changed", self.on_page_changed)
self._app.connect("play-current", self.on_play_current)
self._app.connect("play-recording", self.on_play_recording)
self._fav_view = app.fav_view
self._player = None
self._current_mrl = None
self._full_screen = False
self._playback_window = None
self._audio_track_menu = None
self._subtitle_track_menu = None
self._play_mode = self._app.app_settings.play_streams_mode
handlers = {"on_realize": self.on_realize,
"on_press": self.on_press,
"on_next": self.on_next,
"on_previous": self.on_previous,
"on_rewind": self.on_rewind,
"on_full_screen": self.on_full_screen,
"on_close": self.on_close}
builder = get_builder(UI_RESOURCES_PATH + "playback.glade", handlers)
self.set_spacing(5)
self.set_orientation(Gtk.Orientation.VERTICAL)
self._event_box = builder.get_object("event_box")
self.pack_start(self._event_box, True, True, 0)
if not IS_DARWIN:
self.pack_end(builder.get_object("tool_bar"), False, True, 0)
self._scale = builder.get_object("scale")
self._full_time_label = builder.get_object("full_time_label")
self._current_time_label = builder.get_object("current_time_label")
self._rewind_box = builder.get_object("rewind_box")
self._tool_bar = builder.get_object("tool_bar")
self._prev_button = builder.get_object("prev_button")
self._next_button = builder.get_object("next_button")
self._audio_menu_button = builder.get_object("audio_menu_button")
self._video_menu_button = builder.get_object("video_menu_button")
self._subtitle_menu_button = builder.get_object("subtitle_menu_button")
self._fav_view.bind_property("sensitive", self._prev_button, "sensitive")
self._fav_view.bind_property("sensitive", self._next_button, "sensitive")
self.connect("delete-event", self.on_delete)
self.connect("show", self.set_player_area_size)
def on_fav_clicked(self, app, mode):
if mode is not FavClickMode.STREAM and not self._app.http_api:
return
self._fav_view.set_sensitive(False)
if mode is FavClickMode.STREAM:
self.on_play_stream()
elif mode is FavClickMode.ZAP_PLAY:
self._app.on_zap(self.on_watch)
elif mode is FavClickMode.PLAY:
self.on_play_service()
def on_srv_clicked(self, app, mode):
if not self._app.http_api:
return
view = self._app.services_view
path, column = view.get_cursor()
if path:
srv = self._app.current_services.get(view.get_model()[path][Column.SRV_FAV_ID], None)
if not srv or not srv.picon_id:
return
ref = self._app.get_service_ref_data(srv)
s_type = self._app.app_settings.setting_type
error_msg = "No connection to the receiver!"
if s_type is SettingsType.ENIGMA_2:
def zap(rq):
self.on_watch() if rq and rq.get("e2state", False) else self.on_error(None, error_msg)
self._app.http_api.send(HttpAPI.Request.ZAP, ref, zap)
elif self._s_type is SettingsType.NEUTRINO_MP:
def zap(rq):
self.on_watch() if rq and rq.get("data", None) == "ok" else self.on_error(None, error_msg)
self._app.http_api.send(HttpAPI.Request.N_ZAP, f"?{ref}", zap)
def on_iptv_clicked(self, app, mode):
if not self._app.http_api:
return
view = self._app.iptv_services_view
path, column = view.get_cursor()
if path:
row = view.get_model()[path][:]
url = get_iptv_url(row, self._app.app_settings.setting_type, Column.IPTV_FAV_ID)
self.play(url, row[Column.IPTV_SERVICE]) if url else self.on_error(None, "No reference is present!")
def on_play_current(self, app, url):
self.on_watch()
def on_play_recording(self, app, url):
self.play(url)
def on_page_changed(self, app, page):
self.on_close()
self.set_visible(False)
def on_realize(self, box):
if not self._player:
settings = self._app.app_settings
try:
self._player = Player.make(settings.stream_lib, settings.play_streams_mode, self._event_box)
except (ImportError, NameError) as e:
self._app.show_error_message(str(e))
return True
else:
self.init_playback_elements()
self.emit("play", self._current_mrl)
finally:
if settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
self.set_player_area_size(box)
def init_playback_elements(self):
self._player.connect("error", self.on_error)
self._player.connect("played", self.on_played)
self._player.connect("audio-track", self.on_audio_track_changed)
self._player.connect("subtitle-track", self.on_subtitle_track_changed)
self._app.app_window.connect("key-press-event", self.on_key_press)
builder = get_builder(UI_RESOURCES_PATH + "app_menu.ui")
self._audio_track_menu = builder.get_object("audio_track_menu")
self._subtitle_track_menu = builder.get_object("subtitle_track_menu")
audio_menu = builder.get_object("audio_menu")
video_menu = builder.get_object("video_menu")
subtitle_menu = builder.get_object("subtitle_menu")
if not IS_GNOME_SESSION:
menu_bar = self._app.get_menubar()
menu_bar.insert_section(1, None, audio_menu)
menu_bar.insert_section(2, None, video_menu)
menu_bar.insert_section(3, None, subtitle_menu)
if not IS_DARWIN:
self._player.connect("position", self.on_time_changed)
self._audio_menu_button.set_menu_model(self._audio_track_menu)
self._video_menu_button.set_menu_model(builder.get_object("aspect_ratio_menu"))
self._subtitle_menu_button.set_menu_model(self._subtitle_track_menu)
# Actions.
self._app.set_action("on_play", self.on_play)
self._app.set_action("on_stop", self.on_stop)
audio_track_action = Gio.SimpleAction.new_stateful("on_set_audio_track", GLib.VariantType.new("i"),
GLib.Variant("i", 0))
audio_track_action.connect("activate", self.on_set_audio_track)
self._app.add_action(audio_track_action)
aspect_action = Gio.SimpleAction.new_stateful("on_set_aspect_ratio", GLib.VariantType.new("s"),
GLib.Variant("s", ""))
aspect_action.connect("activate", self.on_set_aspect_ratio)
self._app.add_action(aspect_action)
subtitle_track_action = Gio.SimpleAction.new_stateful("on_set_subtitle_track", GLib.VariantType.new("i"),
GLib.Variant("i", -1))
subtitle_track_action.connect("activate", self.on_set_subtitle_track)
self._app.add_action(subtitle_track_action)
def on_play(self, action=None, value=None):
self.emit("play", None)
def on_stop(self, action=None, value=None):
self.emit("stop", None)
def on_next(self, button):
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, 1):
self.set_player_action()
def on_previous(self, button):
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, -1):
self.set_player_action()
def on_rewind(self, scale, scroll_type, value):
self._player.set_time(int(value))
def on_full_screen(self, item=None):
self._full_screen = not self._full_screen
if self._play_mode is PlayStreamsMode.BUILT_IN:
self._tool_bar.set_visible(not self._full_screen)
self.emit("playback-full-screen", not self._full_screen)
elif self._playback_window:
if not IS_DARWIN:
self._tool_bar.set_visible(not self._full_screen)
self._playback_window.fullscreen() if self._full_screen else self._playback_window.unfullscreen()
def on_close(self, action=None, value=None):
if self._playback_window:
self._app.app_settings.add("playback_window_size", self._playback_window.get_size())
self._playback_window.hide()
self.on_stop()
self.hide()
self.emit("playback-close", None)
return True
@run_with_delay(1)
def on_audio_track_changed(self, player, tracks):
self._audio_track_menu.remove_all()
for t in tracks:
item = Gio.MenuItem.new(t[1], None)
item.set_action_and_target_value("app.on_set_audio_track", GLib.Variant("i", t[0]))
self._audio_track_menu.append_item(item)
@run_with_delay(1)
def on_subtitle_track_changed(self, player, tracks):
self._subtitle_track_menu.remove_all()
for t in tracks:
item = Gio.MenuItem.new(t[1], None)
item.set_action_and_target_value("app.on_set_subtitle_track", GLib.Variant("i", t[0]))
self._subtitle_track_menu.append_item(item)
def on_set_audio_track(self, action, value):
action.set_state(value)
self._player.set_audio_track(value.get_int32())
def on_set_aspect_ratio(self, action, value):
action.set_state(value)
self._player.set_aspect_ratio(value.get_string())
def on_set_subtitle_track(self, action, value):
action.set_state(value)
self._player.set_subtitle_track(value.get_int32())
def on_press(self, area, event):
if event.button == Gdk.BUTTON_PRIMARY:
if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
self.on_full_screen()
def on_key_press(self, widget, event):
if self._player and self.get_visible():
key = event.keyval
if any((key == Gdk.KEY_F11, key == Gdk.KEY_f, self._full_screen and key == Gdk.KEY_Escape)):
self.on_full_screen()
def on_delete(self, box):
if self._player:
self._player.release()
@run_with_delay(1)
def set_player_action(self):
click_mode = self._app.app_settings.fav_click_mode
self._fav_view.set_sensitive(False)
if click_mode is FavClickMode.PLAY:
self.on_play_service()
elif click_mode is FavClickMode.ZAP_PLAY:
self._app.on_zap(self.on_watch)
elif click_mode is FavClickMode.STREAM:
self.on_play_stream()
def update_buttons(self):
if self._player:
path, column = self._fav_view.get_cursor()
current_index = path[0]
self._player_prev_button.set_sensitive(current_index != 0)
self._player_next_button.set_sensitive(len(self._fav_model) != current_index + 1)
@lru_cache(maxsize=1)
def on_duration_changed(self, duration):
self._scale.set_value(0)
self._scale.get_adjustment().set_upper(duration)
GLib.idle_add(self._rewind_box.set_visible, duration > 0, priority=GLib.PRIORITY_LOW)
GLib.idle_add(self._current_time_label.set_text, "0", priority=GLib.PRIORITY_LOW)
GLib.idle_add(self._full_time_label.set_text, self.get_time_str(duration),
priority=GLib.PRIORITY_LOW)
def on_time_changed(self, widget, t):
if not self._full_screen and self._rewind_box.get_visible():
GLib.idle_add(self._current_time_label.set_text, self.get_time_str(t),
priority=GLib.PRIORITY_LOW)
def get_time_str(self, duration):
""" Returns a string representation of time from duration in milliseconds """
m, s = divmod(duration // 1000, 60)
h, m = divmod(m, 60)
return f"{str(h) + ':' if h else ''}{m:02d}:{s:02d}"
def set_player_area_size(self, widget):
w, h = self._app.app_window.get_size()
widget.set_size_request(w * 0.6, -1)
@run_idle
def show_playback_window(self, title=None):
width, height = 480, 240
size = self._app.app_settings.get("playback_window_size")
if size:
width, height = size
if self._playback_window:
self._playback_window.show()
self._playback_window.set_title(title or self.get_playback_title())
else:
self._playback_window = Gtk.Window(title=title or self.get_playback_title(),
window_position=Gtk.WindowPosition.CENTER,
icon_name="demon-editor")
self._playback_window.connect("delete-event", self.on_close)
self._playback_window.connect("key-press-event", self.on_key_press)
self._playback_window.bind_property("visible", self._event_box, "visible")
if not IS_DARWIN:
self._prev_button.set_visible(False)
self._next_button.set_visible(False)
self.reparent(self._playback_window)
self._playback_window.set_application(self._app)
self.show()
self._playback_window.resize(width, height)
self._playback_window.show()
def get_playback_title(self):
if self._app.page is not Page.RECORDINGS:
path, column = self._fav_view.get_cursor()
if path:
return f"DemonEditor [{self._app.fav_view.get_model()[path][:][Column.FAV_SERVICE]}]"
else:
return f"DemonEditor [{get_message('Recordings')}]"
return f"DemonEditor [{get_message('Playback')}]"
def on_play_stream(self):
path, column = self._fav_view.get_cursor()
if path:
row = self._fav_view.get_model()[path][:]
if row[Column.FAV_TYPE] != BqServiceType.IPTV.name:
self.on_error(None, "Not allowed in this context!")
return
url = get_iptv_url(row, self._app.app_settings.setting_type)
self.play(url) if url else self.on_error(None, "No reference is present!")
def on_play_service(self, item=None):
path, column = self._fav_view.get_cursor()
if not path or not self._app.http_api:
return
ref = self._app.get_service_ref(path)
if not ref:
return
if self._player and self._player.is_playing():
self.emit("stop", None)
s_type = self._app.app_settings.setting_type
req = HttpAPI.Request.STREAM if s_type is SettingsType.ENIGMA_2 else HttpAPI.Request.N_STREAM
self._app.http_api.send(req, ref, self.watch)
def on_watch(self, item=None):
""" Switch to the channel and watch in the player. """
s_type = self._app.app_settings.setting_type
if s_type is SettingsType.ENIGMA_2:
self._app.http_api.send(HttpAPI.Request.STREAM_CURRENT, "", self.watch)
elif s_type is SettingsType.NEUTRINO_MP:
self._app.http_api.send(HttpAPI.Request.N_ZAP, "",
lambda rf: self._app.http_api.send(HttpAPI.Request.N_STREAM, rf.get("data", ""),
self.watch))
def watch(self, data):
url = self._app.get_url_from_m3u(data)
GLib.timeout_add_seconds(1, self.play, url) if url else self.on_error(None, "Can't Playback!")
def play(self, url, title=None):
if self._play_mode is PlayStreamsMode.M3U:
self._app.save_stream_to_m3u(url)
return
if self._play_mode is not self._app.app_settings.play_streams_mode:
self.on_error(None, "Play mode has been changed!\nRestart the program to apply the settings.")
return
if self._play_mode is PlayStreamsMode.BUILT_IN:
self.show()
elif self._play_mode is PlayStreamsMode.WINDOW:
self.show_playback_window(title)
if self._player:
self.emit("play", url)
else:
self._current_mrl = url
@run_idle
def on_played(self, player, duration):
self._fav_view.set_sensitive(True)
if not IS_DARWIN:
self.on_duration_changed(duration)
@run_idle
def on_error(self, player, msg):
self._app.show_error_message(msg)
self._fav_view.set_sensitive(True)
if __name__ == "__main__":
pass

581
app/ui/recordings.glade Normal file
View File

@@ -0,0 +1,581 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2
The MIT License (MIT)
Copyright (c) 2018-2022 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface domain="demon-editor">
<requires lib="gtk+" version="3.20"/>
<!-- interface-css-provider-path style.css -->
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor. -->
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkMenu" id="popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="play_menu_item">
<property name="label">gtk-media-play</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_play" swapped="no"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="menu_separator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="remove_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_recording_remove" swapped="no"/>
<accelerator key="Delete" signal="activate"/>
</object>
</child>
</object>
<object class="GtkListStore" id="rec_paths_model">
<columns>
<!-- column-name icon -->
<column type="GdkPixbuf"/>
<!-- column-name title -->
<column type="gchararray"/>
<!-- column-name path -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkListStore" id="recordings_model">
<columns>
<!-- column-name logo -->
<column type="GdkPixbuf"/>
<!-- column-name service -->
<column type="gchararray"/>
<!-- column-name title -->
<column type="gchararray"/>
<!-- column-name time -->
<column type="gchararray"/>
<!-- column-name length -->
<column type="gchararray"/>
<!-- column-name file -->
<column type="gchararray"/>
<!-- column-name desc -->
<column type="gchararray"/>
<!-- column-name data -->
<column type="PyObject"/>
</columns>
</object>
<object class="GtkTreeModelFilter" id="recordings_filter_model">
<property name="child_model">recordings_model</property>
</object>
<object class="GtkTreeModelSort" id="recordings_sort_model">
<property name="model">recordings_filter_model</property>
<signal name="row-deleted" handler="on_recordings_model_changed" swapped="no"/>
<signal name="row-inserted" handler="on_recordings_model_changed" swapped="no"/>
</object>
<object class="GtkBox" id="recordings_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkPaned" id="recordings_paned">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="wide_handle">True</property>
<child>
<object class="GtkFrame" id="recordings_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0.49000000953674316</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkBox" id="recordings_main_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">2</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="recordings_header_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkToggleButton" id="recordings_filter_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="focus_on_click">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Filter</property>
<signal name="toggled" handler="on_recordings_filter_toggled" swapped="no"/>
<child>
<object class="GtkImage" id="recordings_filter_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-find-replace-symbolic</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="recordings_remove_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Remove</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_recording_remove" swapped="no"/>
<child>
<object class="GtkImage" id="remove_recording_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">user-trash-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="recordings_fs_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="spacing">10</property>
<child>
<object class="GtkSearchEntry" id="recordings_filter_entry">
<property name="can_focus">True</property>
<property name="primary_icon_name">edit-find-replace-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="primary_icon_sensitive">False</property>
<property name="visible" bind-source="recordings_filter_button" bind-property="active"/>
<signal name="search-changed" handler="on_recordings_filter_changed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="recordings_search_box">
<property name="can_focus">False</property>
<property name="valign">center</property>
<child>
<object class="GtkSearchEntry" id="recordings_search_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">edit-find-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="primary_icon_sensitive">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="recordings_search_down_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<child>
<object class="GtkArrow" id="recordings_down_arrow">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="arrow_type">down</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="recordings_search_up_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<child>
<object class="GtkArrow" id="recordings_up_arrow">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="arrow_type">up</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
<style>
<class name="group"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="recordings_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="recordings_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">recordings_sort_model</property>
<property name="fixed_height_mode">True</property>
<property name="rubber_banding">True</property>
<property name="enable_grid_lines">both</property>
<property name="tooltip_column">6</property>
<signal name="button-press-event" handler="on_popup_menu" object="popup_menu" swapped="no"/>
<signal name="key-press-event" handler="on_recordings_key_press" swapped="no"/>
<signal name="row-activated" handler="on_recordings_activated" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="recordings_view_selection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_service_column">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="min_width">150</property>
<property name="title" translatable="yes">Service</property>
<property name="alignment">0.5</property>
<property name="sort_column_id">1</property>
<child>
<object class="GtkCellRendererPixbuf" id="rec_log_renderer">
<property name="xpad">5</property>
<property name="ypad">2</property>
</object>
<attributes>
<attribute name="pixbuf">0</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererText" id="rec_service_renderer">
<property name="xalign">0.49000000953674316</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_title_column">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="min_width">150</property>
<property name="title" translatable="yes">Title</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<property name="sort_column_id">2</property>
<child>
<object class="GtkCellRendererText" id="rec_title_renderer">
<property name="xpad">5</property>
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_time_column">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="fixed_width">180</property>
<property name="min_width">100</property>
<property name="title" translatable="yes">Time</property>
<property name="alignment">0.5</property>
<property name="sort_column_id">3</property>
<child>
<object class="GtkCellRendererText" id="rec_time_renderer">
<property name="xpad">5</property>
<property name="xalign">0.49000000953674316</property>
</object>
<attributes>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_len_column">
<property name="min_width">100</property>
<property name="sizing">fixed</property>
<property name="title" translatable="yes">Length</property>
<property name="alignment">0.5</property>
<property name="sort_column_id">4</property>
<child>
<object class="GtkCellRendererText" id="rec_len_renderer">
<property name="xalign">0.49000000953674316</property>
</object>
<attributes>
<attribute name="text">4</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_file_column">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="min_width">100</property>
<property name="title" translatable="yes">File</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<property name="sort_column_id">5</property>
<child>
<object class="GtkCellRendererText" id="rec_file_renderer">
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">5</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_desc_column">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="title" translatable="yes">Description</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<property name="sort_column_id">6</property>
<child>
<object class="GtkCellRendererText" id="rec_desc_renderer">
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">6</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>
<child>
<object class="GtkBox" id="recordings_status_box">
<property name="height_request">26</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="recordings_count_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-properties</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="recordings_count_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="recordings_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Recordings</property>
</object>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="recordings_paths_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0.49000000953674316</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkScrolledWindow" id="paths_view_scrolled_window">
<property name="width_request">250</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="shadow_type">in</property>
<property name="min_content_height">100</property>
<child>
<object class="GtkTreeView" id="recordings_paths_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">rec_paths_model</property>
<property name="headers_visible">False</property>
<property name="search_column">1</property>
<property name="rubber_banding">True</property>
<property name="activate_on_single_click">True</property>
<signal name="button-press-event" handler="on_path_press" swapped="no"/>
<signal name="row-activated" handler="on_path_activated" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="rec_paths_selection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="rec_paths_column">
<property name="resizable">True</property>
<property name="min_width">100</property>
<property name="title" translatable="yes">Paths</property>
<property name="expand">True</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<property name="sort_column_id">1</property>
<child>
<object class="GtkCellRendererPixbuf" id="ftp_icon_column_renderer">
<property name="xalign">0.019999999552965164</property>
</object>
<attributes>
<attribute name="pixbuf">0</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererText" id="ftp_name_column_renderer">
<property name="xalign">0.019999999552965164</property>
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="recordings_path_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Paths</property>
</object>
</child>
</object>
<packing>
<property name="resize">False</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</interface>

337
app/ui/recordings.py Normal file
View File

@@ -0,0 +1,337 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" Module for working with recordings. """
import os
from datetime import datetime
from ftplib import all_errors
from io import BytesIO, TextIOWrapper
from urllib.parse import quote
from app.ui.tasks import BGTaskWidget
from .dialogs import get_builder, show_dialog, DialogType
from .main_helper import get_base_paths, get_base_model, on_popup_menu
from .uicommons import Gtk, Gdk, GLib, UI_RESOURCES_PATH, Column, KeyboardKey, Page
from ..commons import run_task, run_idle, log
from ..connections import UtfFTP, HttpAPI
from ..settings import IS_DARWIN, PlayStreamsMode
class RecordingsTool(Gtk.Box):
ROOT = ".."
DEFAULT_PATH = "/hdd"
def __init__(self, app, settings, *args, **kwargs):
super().__init__(*args, **kwargs)
self._app = app
self._app.connect("layout-changed", self.on_layout_changed)
self._app.connect("data-receive", self.on_data_receive)
self._app.connect("profile-changed", self.init)
self._settings = settings
self._ftp = None
self._logos = {}
# Icon.
theme = Gtk.IconTheme.get_default()
icon = "folder-symbolic" if IS_DARWIN else "folder"
self._icon = theme.load_icon(icon, 24, 0) if theme.lookup_icon(icon, 24, 0) else None
handlers = {"on_path_press": self.on_path_press,
"on_path_activated": self.on_path_activated,
"on_recordings_activated": self.on_recordings_activated,
"on_play": self.on_play,
"on_recording_remove": self.on_recording_remove,
"on_recordings_model_changed": self.on_recordings_model_changed,
"on_recordings_filter_changed": self.on_recordings_filter_changed,
"on_recordings_filter_toggled": self.on_recordings_filter_toggled,
"on_recordings_key_press": self.on_recordings_key_press,
"on_popup_menu": on_popup_menu}
builder = get_builder(f"{UI_RESOURCES_PATH}recordings.glade", handlers)
self._rec_view = builder.get_object("recordings_view")
self._paths_view = builder.get_object("recordings_paths_view")
self._paned = builder.get_object("recordings_paned")
self._model = builder.get_object("recordings_model")
self._filter_model = builder.get_object("recordings_filter_model")
self._filter_model.set_visible_func(self.recordings_filter_function)
self._filter_entry = builder.get_object("recordings_filter_entry")
self._recordings_count_label = builder.get_object("recordings_count_label")
self.pack_start(builder.get_object("recordings_box"), True, True, 0)
self._rec_view.get_model().set_sort_func(3, self.time_sort_func, 3)
srv_column = builder.get_object("rec_service_column")
renderer = builder.get_object("rec_log_renderer")
size = self._app.app_settings.list_picon_size
renderer.set_fixed_size(size, size * 0.65)
srv_column.set_cell_data_func(renderer, self.logo_data_func)
if settings.alternate_layout:
self.on_layout_changed(app, True)
self.init()
self.show()
def clear_data(self):
self._model.clear()
self._paths_view.get_model().clear()
def on_layout_changed(self, app, alt_layout):
ch1 = self._paned.get_child1()
ch2 = self._paned.get_child2()
self._paned.remove(ch1)
self._paned.remove(ch2)
self._paned.add1(ch2)
self._paned.add(ch1)
def on_data_receive(self, app, page):
if page is Page.RECORDINGS:
model, paths = self._rec_view.get_selection().get_selected_rows()
if not paths:
self._app.show_error_message("No selected item!")
return
response = show_dialog(DialogType.CHOOSER, self._app.app_window, settings=self._settings,
title="Open folder", create_dir=True)
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
files = (model[p][5] for p in paths)
bgw = BGTaskWidget(self._app, "Downloading recordings...", self.download_recordings, files, response)
self._app.emit("add-background-task", bgw)
def download_recordings(self, files, dst):
for file in files:
try:
with open(os.path.join(dst, os.path.basename(file)), "wb") as f:
log(f"Downloading recording: {file}. Status: {self._ftp.download_binary(file, f)}".rstrip())
except OSError as e:
log(str(e))
@run_task
def init(self, app=None, arg=None):
GLib.idle_add(self.clear_data)
try:
if self._ftp:
self._ftp.close()
self._ftp = UtfFTP(host=self._settings.host, user=self._settings.user, passwd=self._settings.password)
self._ftp.encoding = "utf-8"
except all_errors:
pass # NOP
else:
self.init_paths(self.DEFAULT_PATH)
@run_idle
def init_paths(self, path=None):
self.clear_data()
if not self._ftp:
return
if path:
try:
self._ftp.cwd(path)
except all_errors as e:
pass
files = []
try:
self._ftp.dir(files.append)
except all_errors as e:
log(e)
else:
self.append_paths(files)
@run_idle
def append_paths(self, files):
model = self._paths_view.get_model()
model.clear()
model.append((None, self.ROOT, self._ftp.pwd()))
for f in files:
f_data = self._ftp.get_file_data(f)
if len(f_data) < 9:
log(f"{__class__.__name__}. Folder data parsing error. [{f}]")
continue
f_type = f_data[0][0]
if f_type == "d":
model.append((self._icon, f_data[8], self._ftp.pwd()))
def on_path_activated(self, view, path, column):
row = view.get_model()[path][:]
path = f"{row[-1]}/{row[1]}/"
self._app.send_http_request(HttpAPI.Request.RECORDINGS, quote(path), self.update_recordings_data)
def on_path_press(self, view, event):
target = view.get_path_at_pos(event.x, event.y)
if not target or event.button != Gdk.BUTTON_PRIMARY:
return
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS:
self.init_paths(self._paths_view.get_model()[target[0]][1])
@run_idle
def update_recordings_data(self, recordings):
self._model.clear()
recs = recordings.get("recordings", [])
list(map(self._model.append, (self.get_recordings_row(r) for r in recs)))
list(map(self.get_rec_service_logo, recs))
def get_recordings_row(self, rec):
service = rec.get("e2servicename")
title = rec.get("e2title", "")
r_time = datetime.fromtimestamp(int(rec.get("e2time", "0"))).strftime("%a, %x, %H:%M")
length = rec.get("e2length", "0")
file = rec.get("e2filename", "")
desc = rec.get("e2description", "")
return None, service, title, r_time, length, file, desc, rec
def get_rec_service_logo(self, rec_data):
if not rec_data.get("e2servicename", None):
return
ref = rec_data.get("e2servicereference", None)
logo = self._logos.get(rec_data.get("e2servicereference", None))
if not logo:
file = rec_data.get("e2filename", None)
if file:
meta = f"RETR {file}.meta"
io = BytesIO()
try:
self._ftp.retrbinary(meta, io.write)
except all_errors:
pass
else:
io.seek(0)
f_ref, sep, name = TextIOWrapper(io, errors="ignore").readline().partition("::")
self._logos[ref] = self._app.picons.get(f"{f_ref.replace(':', '_')}.png")
def on_recordings_activated(self, view, path, column):
rec = view.get_model()[path][-1]
self._app.send_http_request(HttpAPI.Request.STREAM_TS, rec.get("e2filename", ""), self.on_play_recording)
def on_play(self, item):
path, column = self._rec_view.get_cursor()
if not path:
self._app.show_error_message("No selected item!")
return
self.on_recordings_activated(self._rec_view, path, column)
def on_play_recording(self, m3u):
url = self._app.get_url_from_m3u(m3u)
if url:
self._app.emit("play-recording", url)
def on_recording_remove(self, action=None, value=None):
""" Removes recordings via FTP. """
model, paths = self._rec_view.get_selection().get_selected_rows()
if not paths:
self._app.show_error_message("No selected item!")
return
if show_dialog(DialogType.QUESTION, self._app.app_window) != Gtk.ResponseType.OK:
return
paths = get_base_paths(paths, model)
model = get_base_model(model)
to_delete = []
if paths and self._ftp:
for file, itr in ((model[p][-1].get("e2filename", ""), model.get_iter(p)) for p in paths):
resp = self._ftp.delete_file(file)
if resp.startswith("2"):
to_delete.append((itr, file))
else:
self._app.show_error_message(resp)
break
[self.remove_meta_files(f) for i, f in to_delete if model.remove(i) or True]
@run_task
def remove_meta_files(self, file):
name, ex = os.path.splitext(file)
[self._ftp.delete_file(f"{name}{suf}") for suf in (f"{ex}.ap", f"{ex}.cuts", f"{ex}.meta", f"{ex}.sc", ".eit")]
def on_recordings_model_changed(self, model, path, itr=None):
self._recordings_count_label.set_text(str(len(model)))
def on_recordings_filter_changed(self, entry):
self._filter_model.refilter()
def recordings_filter_function(self, model, itr, data):
txt = self._filter_entry.get_text().upper()
return next((s for s in model.get(itr, 1, 2, 3, 5, 6) if s and txt in s.upper()), False)
def on_recordings_filter_toggled(self, button):
if not button.get_active():
self._filter_entry.set_text("")
def on_recordings_key_press(self, view, event):
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
if key is KeyboardKey.DELETE:
self.on_recording_remove()
def on_playback(self, box, state):
""" Updates state of the UI elements for playback mode. """
if self._settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
self._paned.set_orientation(Gtk.Orientation.VERTICAL)
self.update_rec_columns_visibility(False)
def on_playback_close(self, box, state):
""" Restores UI elements state after playback mode. """
self._paned.set_orientation(Gtk.Orientation.HORIZONTAL)
self.update_rec_columns_visibility(True)
def update_rec_columns_visibility(self, state):
for c in (Column.REC_TIME, Column.REC_LEN, Column.REC_FILE, Column.REC_DESC):
self._rec_view.get_column(c).set_visible(state)
def logo_data_func(self, column, renderer, model, itr, data):
rec_data = model.get_value(itr, 7)
renderer.set_property("pixbuf", self._logos.get(rec_data.get("e2servicereference", None)))
def time_sort_func(self, model, iter1, iter2, column):
""" Custom sort function for time column. """
rec1 = model.get_value(iter1, 7)
rec2 = model.get_value(iter2, 7)
return int(rec1.get("e2time", "0")) - int(rec2.get("e2time", "0"))
if __name__ == "__main__":
pass

File diff suppressed because it is too large Load Diff

View File

@@ -1,955 +0,0 @@
import concurrent.futures
import re
import time
from math import fabs
from gi.repository import GLib
from app.commons import run_idle, run_task, log
from app.eparser import get_satellites, write_satellites, Satellite, Transponder
from app.eparser.ecommons import PLS_MODE, get_key_by_value
from app.tools.satellites import SatellitesParser, SatelliteSource, ServicesParser
from .dialogs import show_dialog, DialogType, get_chooser_dialog, get_message, get_builder
from .main_helper import move_items, scroll_to, append_text_to_tview, get_base_model, on_popup_menu
from .search import SearchProvider
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, MOVE_KEYS, KeyboardKey, MOD_MASK
_UI_PATH = UI_RESOURCES_PATH + "satellites_dialog.glade"
def show_satellites_dialog(transient, options):
SatellitesDialog(transient, options).show()
class SatellitesDialog:
_aggr = [None for x in range(9)] # aggregate
def __init__(self, transient, settings):
self._data_path = settings.data_local_path + "satellites.xml"
self._settings = settings
handlers = {"on_open": self.on_open,
"on_remove": self.on_remove,
"on_save": self.on_save,
"on_save_as": self.on_save_as,
"on_update": self.on_update,
"on_up": self.on_up,
"on_down": self.on_down,
"on_popup_menu": on_popup_menu,
"on_satellite_add": self.on_satellite_add,
"on_transponder_add": self.on_transponder_add,
"on_edit": self.on_edit,
"on_key_release": self.on_key_release,
"on_row_activated": self.on_row_activated,
"on_resize": self.on_resize,
"on_quit": self.on_quit}
builder = get_builder(_UI_PATH, handlers, use_str=True,
objects=("satellites_editor_window", "satellites_tree_store", "popup_menu",
"left_header_menu", "popup_menu_add_image", "popup_menu_add_image_2",
"sat_editor_save_image", "sat_editor_update_image"))
self._window = builder.get_object("satellites_editor_window")
self._window.set_transient_for(transient)
self._sat_view = builder.get_object("satellites_editor_tree_view")
# Setting the last size of the dialog window if it was saved
window_size = self._settings.get("sat_editor_window_size")
if window_size:
self._window.resize(*window_size)
self._stores = {3: builder.get_object("pol_store"),
4: builder.get_object("fec_store"),
5: builder.get_object("system_store"),
6: builder.get_object("mod_store")}
self.load_satellites_list(self._sat_view.get_model())
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._settings:
self._settings.add("sat_editor_window_size", window.get_size())
@run_idle
def on_quit(self, *args):
self._window.destroy()
@run_idle
def on_open(self, model):
response = get_chooser_dialog(self._window, self._settings, "satellites.xml", ("*.xml",))
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
if not str(response).endswith("satellites.xml"):
show_dialog(DialogType.ERROR, self._window, text="No satellites.xml file is selected!")
return
self._data_path = response
self.load_satellites_list(model)
@staticmethod
def on_row_activated(view, path, column):
if view.row_expanded(path):
view.collapse_row(path)
else:
view.expand_row(path, column)
def on_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_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK
if key is KeyboardKey.DELETE:
self.on_remove(view)
elif key is KeyboardKey.INSERT:
pass
elif ctrl and key is KeyboardKey.E:
self.on_edit(view)
elif ctrl and key is KeyboardKey.S:
self.on_satellite()
elif ctrl and key is KeyboardKey.T:
self.on_transponder()
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)
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._window, getattr(e, "message", str(e)) +
"\n\nPlease, download files from receiver or setup your path for read data!")
return
else:
model.clear()
for sat in satellites:
append_satellite(model, sat)
yield True
def on_add(self, view):
""" Common adding """
self.on_edit(view, force=True)
def on_satellite_add(self, item):
self.on_satellite(None)
def on_transponder_add(self, item):
self.on_transponder(None)
def on_edit(self, view, force=False):
""" Common edit """
paths = self.check_selection(view, "Please, select only one item!")
if not paths:
return
model = view.get_model()
itr = model.get_iter(paths[0])
row = model.get(itr, *[x for x in range(view.get_n_columns())])
if row[-1]: # satellite
self.on_satellite(None if force else Satellite(row[0], None, row[-1], None), itr)
else:
self.on_transponder(None if force else Transponder(*row[1:-2]), itr)
def on_satellite(self, satellite=None, edited_itr=None):
""" Create or edit satellite"""
sat_dialog = SatelliteDialog(self._window, satellite)
sat = sat_dialog.run()
sat_dialog.destroy()
if sat:
view = self._sat_view
model = view.get_model()
if satellite and edited_itr:
model.set(edited_itr, {0: sat.name, 10: sat.flags, 11: sat.position})
else:
index = self.get_sat_position_index(sat.position, model)
model.insert(None, index,
[sat.name, None, None, None, None, None, None, None, None, None, sat.flags, sat.position])
scroll_to(index, view)
def on_transponder(self, transponder=None, edited_itr=None):
""" Create or edit transponder """
paths = self.check_selection(self._sat_view, "Please, select only one satellite!")
if paths is None:
return
elif len(paths) == 0:
show_dialog(DialogType.ERROR, self._window, "No satellite is selected!")
return
dialog = TransponderDialog(self._window, transponder)
tr = dialog.run()
dialog.destroy()
if tr:
view = self._sat_view
model = view.get_model()
if transponder and edited_itr:
model.set(edited_itr, {1: tr.frequency, 2: tr.symbol_rate, 3: tr.polarization,
4: tr.fec_inner, 5: tr.system, 6: tr.modulation,
7: tr.pls_mode, 8: tr.pls_code, 9: tr.is_id})
else:
row = ["Transponder:", tr.frequency, tr.symbol_rate, tr.polarization, tr.fec_inner,
tr.system, tr.modulation, tr.pls_mode, tr.pls_code, tr.is_id, None, None]
model, paths = view.get_selection().get_selected_rows()
itr = model.get_iter(paths[0])
view.expand_row(paths[0], 0)
# Get parent iter if selected transponder
parent_itr = model.iter_parent(itr)
if parent_itr:
itr = parent_itr
freq = int(tr.frequency if tr.frequency else 0)
tr_itr = model.iter_children(itr)
# Inserting according to frequency value.
while tr_itr:
cur_freq = int(model.get_value(tr_itr, 1))
if freq <= cur_freq:
path = model.get_path(tr_itr)
index = path.get_indices()[1]
model.insert(model.iter_parent(tr_itr), index, row)
scroll_to(path, view)
break
else:
tr_itr = model.iter_next(tr_itr)
else:
itr = model.append(itr, row)
scroll_to(model.get_path(itr), view)
def get_sat_position_index(self, pos, model):
""" Search and returns index after given position """
pos = int(pos)
row = next(filter(lambda r: int(r[-1]) >= pos, model), None)
return row.path[0] if row else len(model)
def check_selection(self, view, message):
""" Checks if any row is selected. Shows error dialog if selected more than one.
returns selected path or None
"""
model, paths = view.get_selection().get_selected_rows()
if len(paths) > 1:
show_dialog(DialogType.ERROR, self._window, message)
return
return paths
@run_idle
def on_remove(self, view):
""" Removal of selected satellites and transponders.
The satellites are removed first! Then transponders.
"""
selection = view.get_selection()
model, paths = selection.get_selected_rows()
itrs = [model.get_iter(path) for path in paths]
satellites = list(filter(model.iter_has_child, itrs))
if len(satellites):
# Removing selected satellites.
list(map(model.remove, satellites))
else:
# Removing selected transponders.
list(map(model.remove, itrs))
@run_idle
def on_save(self, view):
if show_dialog(DialogType.QUESTION, self._window) == Gtk.ResponseType.CANCEL:
return
model = view.get_model()
satellites = []
model.foreach(self.parse_data, satellites)
write_satellites(satellites, self._data_path)
def on_save_as(self, item):
response = self.get_file_dialog_response(Gtk.FileChooserAction.SAVE)
if response == Gtk.ResponseType.CANCEL:
return
show_dialog(DialogType.ERROR, transient=self._window, text="Not implemented yet!")
@run_idle
def on_update(self, item):
SatellitesUpdateDialog(self._window, self._settings, self._sat_view.get_model()).show()
@staticmethod
def parse_data(model, path, itr, sats):
if model.iter_has_child(itr):
num_of_children = model.iter_n_children(itr)
transponders = []
num_columns = model.get_n_columns()
for num in range(num_of_children):
transponder_itr = model.iter_nth_child(itr, num)
transponder = model.get(transponder_itr, *[item for item in range(num_columns)])
transponders.append(Transponder(*transponder[1:-2]))
sat = model.get(itr, *[item for item in range(num_columns)])
satellite = Satellite(sat[0], sat[-2], sat[-1], transponders)
sats.append(satellite)
# ***************** Transponder dialog *******************#
class TransponderDialog:
""" Shows dialog for adding or edit transponder """
def __init__(self, transient, transponder: Transponder = None):
handlers = {"on_entry_changed": self.on_entry_changed}
objects = ("transponder_dialog", "pol_store", "fec_store", "mod_store", "system_store", "pls_mode_store")
builder = get_builder(_UI_PATH, handlers, use_str=True, objects=objects)
self._dialog = builder.get_object("transponder_dialog")
self._dialog.set_transient_for(transient)
self._freq_entry = builder.get_object("freq_entry")
self._rate_entry = builder.get_object("rate_entry")
self._pol_box = builder.get_object("pol_box")
self._fec_box = builder.get_object("fec_box")
self._sys_box = builder.get_object("sys_box")
self._mod_box = builder.get_object("mod_box")
self._pls_mode_box = builder.get_object("pls_mode_box")
self._pls_code_entry = builder.get_object("pls_code_entry")
self._is_id_entry = builder.get_object("is_id_entry")
# pattern for frequency and rate entries (only digits)
self._pattern = re.compile(r"\D")
# style
self._style_provider = Gtk.CssProvider()
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,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
if transponder:
self.init_transponder(transponder)
def run(self):
while self._dialog.run() != Gtk.ResponseType.CANCEL:
tr = self.to_transponder()
if self.is_accept(tr):
return tr
show_dialog(DialogType.ERROR, self._dialog, "Please check your parameters and try again.")
def destroy(self):
self._dialog.destroy()
def init_transponder(self, transponder):
self._freq_entry.set_text(transponder.frequency)
self._rate_entry.set_text(transponder.symbol_rate)
self._pol_box.set_active_id(transponder.polarization)
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(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 "")
def to_transponder(self):
return Transponder(frequency=self._freq_entry.get_text(),
symbol_rate=self._rate_entry.get_text(),
polarization=self._pol_box.get_active_id(),
fec_inner=self._fec_box.get_active_id(),
system=self._sys_box.get_active_id(),
modulation=self._mod_box.get_active_id(),
pls_mode=get_key_by_value(PLS_MODE, self._pls_mode_box.get_active_id()),
pls_code=self._pls_code_entry.get_text(),
is_id=self._is_id_entry.get_text())
def on_entry_changed(self, entry):
entry.set_name("digit-entry" if self._pattern.search(entry.get_text()) else "GtkEntry")
def is_accept(self, tr):
if self._pattern.search(tr.frequency) or not tr.frequency:
return False
elif self._pattern.search(tr.symbol_rate) or not tr.symbol_rate:
return False
elif None in (tr.polarization, tr.fec_inner, tr.system, tr.modulation):
return False
elif self._pattern.search(tr.pls_code) or self._pattern.search(tr.is_id):
return False
return True
# ***************** Satellite dialog *******************#
class SatelliteDialog:
""" Shows dialog for adding or edit satellite """
def __init__(self, transient, satellite: Satellite = None):
builder = get_builder(_UI_PATH, use_str=True, objects=("satellite_dialog", "side_store", "pos_adjustment"))
self._dialog = builder.get_object("satellite_dialog")
self._dialog.set_transient_for(transient)
self._sat_name = builder.get_object("sat_name_entry")
self._sat_position = builder.get_object("sat_position_button")
self._side = builder.get_object("side_box")
if satellite:
self._sat_name.set_text(satellite.name[0:satellite.name.find("(")].strip())
pos = satellite.position
pos = float("{}.{}".format(pos[:-1], pos[-1:]))
self._sat_position.set_value(fabs(pos))
self._side.set_active(0 if pos >= 0 else 1) # E or W
def run(self):
if self._dialog.run() == Gtk.ResponseType.CANCEL:
return
return self.to_satellite()
def destroy(self):
self._dialog.destroy()
def to_satellite(self):
name = self._sat_name.get_text()
pos = round(self._sat_position.get_value(), 1)
side = self._side.get_active()
name = "{} ({}{})".format(name, pos, self._side.get_active_id())
pos = "{}{}{}".format("-" if side == 1 else "", *str(pos).split("."))
return Satellite(name=name, flags="0", position=pos, transponders=None)
# ********************** Update dialogs ************************ #
class UpdateDialog:
""" Base dialog for update satellites, transponders and services from the web."""
def __init__(self, transient, settings, title=None):
handlers = {"on_update_satellites_list": self.on_update_satellites_list,
"on_receive_data": self.on_receive_data,
"on_cancel_receive": self.on_cancel_receive,
"on_satellite_toggled": self.on_satellite_toggled,
"on_transponder_toggled": self.on_transponder_toggled,
"on_info_bar_close": self.on_info_bar_close,
"on_filter_toggled": self.on_filter_toggled,
"on_find_toggled": self.on_find_toggled,
"on_popup_menu": on_popup_menu,
"on_select_all": self.on_select_all,
"on_unselect_all": self.on_unselect_all,
"on_filter": self.on_filter,
"on_search": self.on_search,
"on_search_down": self.on_search_down,
"on_search_up": self.on_search_up,
"on_quit": self.on_quit}
self._settings = settings
self._download_task = False
self._parser = None
self._size_name = "{}_window_size".format("_".join(re.findall("[A-Z][^A-Z]*", self.__class__.__name__))).lower()
builder = get_builder(UI_RESOURCES_PATH + "satellites_dialog.glade", handlers,
objects=("satellites_update_window", "update_source_store", "update_sat_list_store",
"update_sat_list_model_filter", "update_sat_list_model_sort", "side_store",
"pos_adjustment", "pos_adjustment2", "satellites_update_popup_menu",
"remove_selection_image", "sat_update_cancel_image", "sat_receive_image",
"sat_update_filter_image", "sat_update_search_image", "sat_update_image",
"update_transponder_store", "update_service_store"))
self._window = builder.get_object("satellites_update_window")
self._window.set_transient_for(transient)
if title:
self._window.set_title(title)
self._transponder_paned = builder.get_object("sat_update_tr_paned")
self._sat_view = builder.get_object("sat_update_tree_view")
self._transponder_view = builder.get_object("sat_update_tr_view")
self._service_view = builder.get_object("sat_update_srv_view")
self._source_box = builder.get_object("source_combo_box")
self._sat_update_expander = builder.get_object("sat_update_expander")
self._text_view = builder.get_object("text_view")
self._receive_button = builder.get_object("receive_data_button")
self._sat_update_info_bar = builder.get_object("sat_update_info_bar")
self._info_bar_message_label = builder.get_object("info_bar_message_label")
self._receive_button.bind_property("visible", builder.get_object("cancel_data_button"), "visible", 4)
# Filter
self._filter_bar = builder.get_object("sat_update_filter_bar")
self._from_pos_button = builder.get_object("from_pos_button")
self._to_pos_button = builder.get_object("to_pos_button")
self._filter_from_combo_box = builder.get_object("filter_from_combo_box")
self._filter_to_combo_box = builder.get_object("filter_to_combo_box")
self._filter_model = builder.get_object("update_sat_list_model_filter")
self._filter_model.set_visible_func(self.filter_function)
self._filter_positions = (0, 0)
# Search
self._search_bar = builder.get_object("sat_update_search_bar")
self._search_provider = SearchProvider((self._sat_view,),
builder.get_object("sat_update_search_down_button"),
builder.get_object("sat_update_search_up_button"))
window_size = self._settings.get(self._size_name)
if window_size:
self._window.resize(*window_size)
def show(self):
self._window.show()
@property
def is_download(self):
return self._download_task
@is_download.setter
def is_download(self, value):
self._download_task = value
self._receive_button.set_visible(not value)
@run_idle
def on_update_satellites_list(self, item):
if self.is_download:
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return
model = get_base_model(self._sat_view.get_model())
model.clear()
self.is_download = True
src = self._source_box.get_active()
if not self._parser:
self._parser = SatellitesParser()
self.get_sat_list(src, self.append_satellites)
@run_task
def get_sat_list(self, src, callback):
sat_src = SatelliteSource.FLYSAT
if src == 1:
sat_src = SatelliteSource.LYNGSAT
elif src == 2:
sat_src = SatelliteSource.KINGOFSAT
sats = self._parser.get_satellites_list(sat_src)
if sats:
callback(sats)
self.is_download = False
@run_idle
def append_satellites(self, sats):
model = get_base_model(self._sat_view.get_model())
for sat in sats:
model.append(sat)
@run_idle
def on_receive_data(self, item):
if self.is_download:
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return
@run_idle
def update_expander(self):
self._sat_update_expander.set_expanded(True)
self._text_view.get_buffer().set_text("", 0)
def append_output(self):
@run_idle
def append(t):
append_text_to_tview(t, self._text_view)
while True:
text = yield
append(text)
def on_cancel_receive(self, item=None):
self._download_task = False
def on_satellite_toggled(self, toggle, path):
model = self._sat_view.get_model()
self.update_state(model, path, not toggle.get_active())
self.update_receive_button_state(self._filter_model)
def on_transponder_toggled(self, toggle, path):
model = self._transponder_view.get_model()
model.set_value(model.get_iter(path), 2, not toggle.get_active())
@run_idle
def update_receive_button_state(self, model):
self._receive_button.set_sensitive((any(r[4] for r in model)))
@run_idle
def show_info_message(self, text, message_type):
self._sat_update_info_bar.set_visible(True)
self._sat_update_info_bar.set_message_type(message_type)
self._info_bar_message_label.set_text(text)
def on_info_bar_close(self, bar=None, resp=None):
self._sat_update_info_bar.set_visible(False)
def on_find_toggled(self, button: Gtk.ToggleToolButton):
self._search_bar.set_search_mode(button.get_active())
def on_filter_toggled(self, button: Gtk.ToggleToolButton):
self._filter_bar.set_search_mode(button.get_active())
@run_idle
def on_filter(self, item):
self._filter_positions = self.get_positions()
self._filter_model.refilter()
def filter_function(self, model, itr, data):
if self._filter_model is None or self._filter_model == "None":
return True
from_pos, to_pos = self._filter_positions
if from_pos == 0 and to_pos == 0:
return True
if from_pos > to_pos:
from_pos, to_pos = to_pos, from_pos
return from_pos <= float(self._parser.get_position(model.get(itr, 1)[0])) <= to_pos
def get_positions(self):
from_pos = round(self._from_pos_button.get_value(), 1) * (-1 if self._filter_from_combo_box.get_active() else 1)
to_pos = round(self._to_pos_button.get_value(), 1) * (-1 if self._filter_to_combo_box.get_active() else 1)
return from_pos, to_pos
def on_search(self, entry):
self._search_provider.search(entry.get_text())
def on_search_down(self, item):
self._search_provider.on_search_down()
def on_search_up(self, item):
self._search_provider.on_search_up()
def on_select_all(self, view):
self.update_selection(view, True)
def on_unselect_all(self, view):
self.update_selection(view, False)
def update_selection(self, view, select):
model = view.get_model()
view.get_model().foreach(lambda mod, path, itr: self.update_state(model, path, select))
self.update_receive_button_state(self._filter_model)
def update_state(self, model, path, select):
""" Updates checkbox state by given path in the list """
itr = self._filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(model.get_iter(path)))
self._filter_model.get_model().set_value(itr, 4, select)
def on_quit(self, window, event):
self._settings.add(self._size_name, window.get_size())
self.is_download = False
class SatellitesUpdateDialog(UpdateDialog):
""" Dialog for update satellites from the web. """
def __init__(self, transient, settings, main_model):
super().__init__(transient=transient, settings=settings)
self._main_model = main_model
@run_idle
def on_receive_data(self, item):
if self.is_download:
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return
self.receive_satellites()
@run_task
def receive_satellites(self):
self.is_download = True
self.update_expander()
model = self._sat_view.get_model()
start = time.time()
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
text = "Processing: {}\n"
sats = []
appender = self.append_output()
next(appender)
futures = {executor.submit(self._parser.get_satellite, sat[:-1]): sat for sat in [r for r in model if r[4]]}
for future in concurrent.futures.as_completed(futures):
if not self.is_download:
self.is_download = True
executor.shutdown()
appender.send("\nCanceled\n")
appender.close()
self.is_download = False
return
data = future.result()
appender.send(text.format(data[0]))
sats.append(data)
appender.send("-" * 75 + "\n")
appender.send("Consumed: {:0.0f}s, {} satellites received.".format(time.time() - start, len(sats)))
appender.close()
sats = {s[2]: s for s in sats} # key = position, v = satellite
for row in self._main_model:
pos = row[-1]
if pos in sats:
sat = sats.pop(pos)
itr = row.iter
self.update_satellite(itr, row, sat)
for sat in sats.values():
append_satellite(self._main_model, sat)
self.is_download = False
@run_idle
def update_satellite(self, itr, row, sat):
if self._main_model.iter_has_child(itr):
children = row.iterchildren()
for ch in children:
self._main_model.remove(ch.iter)
for tr in sat[3]:
self._main_model.append(itr, ["Transponder:", tr.frequency, tr.symbol_rate, tr.polarization, tr.fec_inner,
tr.system, tr.modulation, tr.pls_mode, tr.pls_code, tr.is_id, None, None])
class ServicesUpdateDialog(UpdateDialog):
""" Dialog for updating services from the web. """
def __init__(self, transient, settings, callback):
super().__init__(transient=transient, settings=settings, title="Services update")
self._callback = callback
self._satellite_paths = {}
self._transponders = {}
self._services = {}
self._selected_transponders = set()
self._services_parser = ServicesParser(source=SatelliteSource.LYNGSAT)
self._transponder_paned.set_visible(True)
self._source_box.remove(0)
self._source_box.remove(1)
self._source_box.set_active(0)
# Transponder view popup menu
tr_popup_menu = Gtk.Menu()
select_all_item = Gtk.ImageMenuItem.new_from_stock("gtk-select-all")
select_all_item.connect("activate", lambda w: self.update_transponder_selection(True))
tr_popup_menu.append(select_all_item)
remove_selection_item = Gtk.ImageMenuItem.new_from_stock("gtk-undo")
remove_selection_item.set_label(get_message("Remove selection"))
remove_selection_item.connect("activate", lambda w: self.update_transponder_selection(False))
tr_popup_menu.append(remove_selection_item)
tr_popup_menu.show_all()
self._sat_view.connect("row-activated", self.on_activate_satellite)
self._transponder_view.connect("row-activated", self.on_activate_transponder)
self._transponder_view.connect("button-press-event", lambda w, e: on_popup_menu(tr_popup_menu, e))
self._transponder_view.connect("select_all", lambda w: self.update_transponder_selection(True))
@run_idle
def on_receive_data(self, item):
if self.is_download:
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return
self.receive_services()
@run_task
def receive_services(self):
self.is_download = True
self.update_expander()
model = self._sat_view.get_model()
appender = self.append_output()
next(appender)
start = time.time()
non_cached_sats = []
sat_names = {}
t_names = {}
t_urls = []
services = []
for r in (r for r in model if r[-1]):
if not self.is_download:
appender.send("\nCanceled\n")
return
sat, url = r[0], r[3]
trs = self._transponders.get(url, None)
if trs:
for t in filter(lambda tp: tp.url in self._selected_transponders, trs):
t_urls.append(t.url)
t_names[t.url] = t.text
else:
non_cached_sats.append(url)
sat_names[url] = sat
if non_cached_sats:
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
futures = {executor.submit(self._services_parser.get_transponders_links, u): u for u in non_cached_sats}
for future in concurrent.futures.as_completed(futures):
if not self.is_download:
appender.send("\nCanceled.\n")
self.is_download = False
return
appender.send("Getting transponders for: {}.\n".format(sat_names.get(futures[future])))
for t in future.result():
t_urls.append(t.url)
t_names[t.url] = t.text
appender.send("-" * 75 + "\n")
appender.send("{} transponders received.\n\n".format(len(t_urls)))
non_cached_ts = []
for tr in t_urls:
srvs = self._services.get(tr)
services.extend(srvs) if srvs else non_cached_ts.append(tr)
if non_cached_ts:
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
futures = {executor.submit(self._services_parser.get_transponder_services, u): u for u in non_cached_ts}
for future in concurrent.futures.as_completed(futures):
if not self.is_download:
appender.send("\nCanceled.\n")
self.is_download = False
return
appender.send("Getting services for: {}.\n".format(t_names.get(futures[future], "")))
list(map(services.append, future.result()))
appender.send("-" * 75 + "\n")
appender.send("Consumed: {:0.0f}s, {} services received.".format(time.time() - start, len(services)))
try:
from app.eparser.enigma.lamedb import LameDbReader
# Used for double checking!
reader = LameDbReader(path=None)
srvs = reader.get_services_list("".join(reader.get_services_lines(services)))
except ValueError as e:
log("ServicesUpdateDialog [on receive data] error: {}".format(e))
else:
self._callback(srvs)
self.is_download = False
@run_task
def get_sat_list(self, src, callback):
sats = self._parser.get_satellites_list(SatelliteSource.LYNGSAT)
if sats:
callback(sats)
self.is_download = False
def on_satellite_toggled(self, toggle, path):
model = self._sat_view.get_model()
self.update_state(model, path, not toggle.get_active())
self.update_receive_button_state(self._filter_model)
url = model.get_value(model.get_iter(path), 3)
selected = toggle.get_active()
transponders = self._transponders.get(url, None)
if transponders:
for t in transponders:
self._selected_transponders.add(t.url) if selected else self._selected_transponders.discard(t.url)
def on_transponder_toggled(self, toggle, path):
model = self._transponder_view.get_model()
itr = model.get_iter(path)
active = not toggle.get_active()
url = self.update_transponder_state(itr, model, active)
s_path = self._satellite_paths.get(url, None)
if s_path:
self.update_sat_state(model, s_path, active)
def update_sat_state(self, model, path, active):
sat_model = self._sat_view.get_model()
if active:
self.update_state(sat_model, path, active)
else:
self.update_state(sat_model, path, any((r[-1] for r in model)))
self.update_receive_button_state(self._filter_model)
def update_transponder_state(self, itr, model, active):
model.set_value(itr, 2, active)
url = model.get_value(itr, 1)
self._selected_transponders.add(url) if active else self._selected_transponders.discard(url)
return url
@run_task
def on_activate_satellite(self, view, path, column):
model = view.get_model()
itr = model.get_iter(path)
url, selected = model.get_value(itr, 3), model.get_value(itr, 4)
transponders = self._transponders.get(url, None)
if transponders is None:
GLib.idle_add(view.set_sensitive, False)
transponders = self._services_parser.get_transponders_links(url)
self._transponders[url] = transponders
for t in transponders:
t_url = t.url
self._satellite_paths[t_url] = path
self._selected_transponders.add(t_url) if selected else self._selected_transponders.discard(t_url)
self.append_transponders(self._transponder_view.get_model(), transponders)
@run_idle
def append_transponders(self, model, trs_list):
model.clear()
list(map(model.append, [(t.text, t.url, t.url in self._selected_transponders) for t in trs_list]))
self._sat_view.set_sensitive(True)
@run_task
def on_activate_transponder(self, view, path, column):
url = view.get_model()[path][1]
services = self._services.get(url, None)
if services is None:
GLib.idle_add(view.set_sensitive, False)
services = self._services_parser.get_transponder_services(url)
self._services[url] = services
self.append_services(self._service_view.get_model(), services)
@run_idle
def append_services(self, model, srv_list):
model.clear()
for s in srv_list:
model.append((None, s.service, s.package, s.service_type, str(s.ssid), None))
self._transponder_view.set_sensitive(True)
def update_transponder_selection(self, select):
m = self._transponder_view.get_model()
if not len(m):
return
s_path = self._satellite_paths.get({self.update_transponder_state(r.iter, m, select) for r in m}.pop(), None)
if s_path:
self.update_sat_state(m, s_path, select)
# ************************* Commons ************************* #
@run_idle
def append_satellite(model, sat):
""" Common function for append satellite to the model """
name, flags, pos, transponders = sat
parent = model.append(None, [name, None, None, None, None, None, None, None, None, None, flags, pos])
for tr in transponders:
model.append(parent, ["Transponder:", tr.frequency, tr.symbol_rate, tr.polarization, tr.fec_inner, tr.system,
tr.modulation, tr.pls_mode, tr.pls_code, tr.is_id, None, None])
if __name__ == "__main__":
pass

View File

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

View File

@@ -275,6 +275,38 @@ Author: Dmitriy Yefremov
<child type="titlebar">
<placeholder/>
</child>
<child type="action">
<object class="GtkButton" id="cancel_button">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
<signal name="clicked" handler="on_cancel" swapped="no"/>
</object>
</child>
<child type="action">
<object class="GtkButton" id="apply_button">
<property name="label" translatable="yes">Apply</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Save current service</property>
<property name="valign">center</property>
<signal name="clicked" handler="on_save" swapped="no"/>
<accelerator key="Return" signal="activate"/>
</object>
</child>
<child type="action">
<object class="GtkButton" id="create_button">
<property name="label" translatable="yes">Create</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Create and save as new service</property>
<property name="valign">center</property>
<signal name="clicked" handler="on_create_new" swapped="no"/>
</object>
</child>
<child internal-child="vbox">
<object class="GtkBox" id="dialog_vbox">
<property name="can_focus">False</property>
@@ -287,53 +319,6 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="cancel_button">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
<signal name="clicked" handler="on_cancel" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="apply_button">
<property name="label" translatable="yes">Apply</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Save current service</property>
<property name="valign">center</property>
<signal name="clicked" handler="on_save" swapped="no"/>
<accelerator key="Return" signal="activate"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="create_button">
<property name="label" translatable="yes">Create</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Create and save as new service</property>
<property name="valign">center</property>
<signal name="clicked" handler="on_create_new" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -1644,6 +1629,9 @@ Author: Dmitriy Yefremov
</child>
</object>
</child>
<action-widgets>
<action-widget response="-6">cancel_button</action-widget>
</action-widgets>
</object>
<object class="GtkListStore" id="transponder_services_liststore">
<columns>
@@ -1676,6 +1664,24 @@ Author: Dmitriy Yefremov
<child>
<placeholder/>
</child>
<child type="action">
<object class="GtkButton" id="tr_services_no_button">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
</object>
</child>
<child type="action">
<object class="GtkButton" id="tr_services_ok_button">
<property name="label" translatable="yes">OK</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
</object>
</child>
<child internal-child="vbox">
<object class="GtkBox" id="tr_services_dialog_vbox">
<property name="can_focus">False</property>
@@ -1685,34 +1691,6 @@ Author: Dmitriy Yefremov
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="valign">center</property>
<child>
<object class="GtkButton" id="tr_services_no_button">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="tr_services_ok_button">
<property name="label" translatable="yes">OK</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -35,6 +35,7 @@ from app.eparser.ecommons import (MODULATION, Inversion, ROLL_OFF, Pilot, Flag,
get_value_by_name, FEC_DEFAULT, PLS_MODE, SERVICE_TYPE, T_MODULATION, C_MODULATION,
TrType, SystemCable, T_SYSTEM, BANDWIDTH, TRANSMISSION_MODE, GUARD_INTERVAL, T_FEC,
HIERARCHY, A_MODULATION)
from app.eparser.neutrino import get_attributes, SP, KSP
from app.settings import SettingsType
from .dialogs import show_dialog, DialogType, Action, get_builder
from .main_helper import get_base_model
@@ -52,8 +53,6 @@ class ServiceDetailsDialog:
_NEUTRINO_FAV_ID = "{:x}:{:x}:{:x}"
_NEUTRINO_TRANSPONDER_DATA = "{:04x}:{:04x}:{}:{}:{}:{}:{}:{}:{}"
_DIGIT_ENTRY_ELEMENTS = ("bitstream_entry", "pcm_entry", "video_pid_entry", "pcr_pid_entry", "srv_type_entry",
"ac3_pid_entry", "ac3plus_pid_entry", "acc_pid_entry", "he_acc_pid_entry",
"teletext_pid_entry", "pls_code_entry", "stream_id_entry", "tr_flag_entry",
@@ -63,7 +62,7 @@ class ServiceDetailsDialog:
_DIGIT_ENTRY_NAME = "digit-entry"
def __init__(self, transient, settings, srv_view, fav_view, services, bouquets, new_color, action=Action.EDIT):
def __init__(self, app, new_color, action=Action.EDIT):
handlers = {"on_system_changed": self.on_system_changed,
"on_save": self.on_save,
"on_create_new": self.on_create_new,
@@ -77,19 +76,19 @@ class ServiceDetailsDialog:
builder = get_builder(_UI_PATH, handlers, use_str=True)
self._builder = builder
settings = app.app_settings
self._dialog = builder.get_object("service_details_dialog")
self._dialog.set_transient_for(transient)
self._dialog.set_transient_for(app.app_window)
self._s_type = settings.setting_type
self._tr_type = TrType.Satellite
self._satellites_xml_path = settings.data_local_path + "satellites.xml"
self._picons_dir_path = settings.picons_local_path
self._services_view = srv_view
self._fav_view = fav_view
self._picons_path = settings.profile_picons_path
self._services_view = app.services_view
self._fav_view = app.fav_view
self._action = action
self._old_service = None
self._services = services
self._bouquets = bouquets
self._services = app.current_services
self._bouquets = app.current_bouquets
self._new_color = new_color
self._transponder_services_iters = None
self._current_model = None
@@ -223,8 +222,7 @@ class ServiceDetailsDialog:
self._package_entry.set_text(srv.package)
self._sid_entry.set_text(str(int(srv.ssid, 16)))
# Transponder
if self._s_type is SettingsType.ENIGMA_2:
self._tr_type = TrType(srv.transponder_type)
self._tr_type = TrType(srv.transponder_type)
self._freq_entry.set_text(srv.freq)
self._rate_entry.set_text(srv.rate)
self.select_active_text(self._pol_combo_box, srv.pol)
@@ -261,7 +259,7 @@ class ServiceDetailsDialog:
def init_enigma2_flags(self, flags):
f_flags = list(filter(lambda x: x.startswith("f:"), flags))
if f_flags:
value = int(f_flags[0][2:])
value = Flag.parse(f_flags[0])
self._keep_check_button.set_active(Flag.is_keep(value))
self._hide_check_button.set_active(Flag.is_hide(value))
self._use_pids_check_button.set_active(Flag.is_pids(value))
@@ -353,10 +351,12 @@ class ServiceDetailsDialog:
# ***************** Init Neutrino data *********************#
def init_neutrino_data(self, srv):
tr_data = srv.transponder.split(":")
self._transponder_id_entry.set_text(str(int(tr_data[0], 16)))
self._network_id_entry.set_text(str(int(tr_data[1], 16)))
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[3]).name)
if self._tr_type is not TrType.Satellite:
return
tr_data = get_attributes(srv.transponder)
self._transponder_id_entry.set_text(str(int(tr_data.get("id", "0"), 16)))
self._network_id_entry.set_text(str(int(tr_data.get("on", "0"), 16)))
self.select_active_text(self._invertion_combo_box, Inversion(tr_data.get("inv", "2")).name)
self.select_active_text(self._service_type_combo_box, srv.service_type)
self.update_reference_entry()
@@ -368,6 +368,7 @@ class ServiceDetailsDialog:
tr_grid.set_margin_bottom(5)
self._builder.get_object("tr_extra_expander").set_visible(False)
self._builder.get_object("srv_separator").set_visible(False)
self._package_entry.set_sensitive(False)
# ***************** Init Sat positions *********************#
@@ -407,6 +408,10 @@ class ServiceDetailsDialog:
self.save_data()
def save_data(self):
if self._s_type is SettingsType.NEUTRINO_MP and self._tr_type is not TrType.Satellite:
show_dialog(DialogType.ERROR, transient=self._dialog, text="Not implemented yet!")
return
if not self.is_data_correct():
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
@@ -419,6 +424,7 @@ class ServiceDetailsDialog:
def on_new(self):
""" Create new service. """
service = self.get_service(*self.get_srv_data(), self.get_satellite_transponder_data())
show_dialog(DialogType.ERROR, transient=self._dialog, text="Not implemented yet!")
return True
@@ -462,7 +468,7 @@ class ServiceDetailsDialog:
extra_data = {Column.SRV_TOOLTIP: None, Column.SRV_BACKGROUND: None}
if self._s_type is SettingsType.ENIGMA_2 and flags:
f_flags = list(filter(lambda x: x.startswith("f:"), flags.split(",")))
if f_flags and Flag.is_new(int(f_flags[0][2:])):
if f_flags and Flag.is_new(Flag.parse(f_flags[0])):
extra_data[Column.SRV_BACKGROUND] = self._new_color
self._current_model.set(self._current_itr, extra_data)
@@ -498,13 +504,13 @@ class ServiceDetailsDialog:
Column.FAV_PICON: new_service.picon})
def update_picon_name(self, old_name, new_name):
if not os.path.isdir(self._picons_dir_path):
if not os.path.isdir(self._picons_path):
return
for file_name in os.listdir(self._picons_dir_path):
for file_name in os.listdir(self._picons_path):
if file_name == old_name:
old_file = os.path.join(self._picons_dir_path, old_name)
new_file = os.path.join(self._picons_dir_path, new_name)
old_file = os.path.join(self._picons_path, old_name)
new_file = os.path.join(self._picons_path, new_name)
os.rename(old_file, new_file)
break
@@ -537,9 +543,9 @@ class ServiceDetailsDialog:
if self._s_type is SettingsType.ENIGMA_2:
return self.get_enigma2_flags()
elif self._s_type is SettingsType.NEUTRINO_MP:
flags = self._old_service.flags_cas.split(":")
flags[1] = self.get_sat_position()
return ":".join(flags)
flags = get_attributes(self._old_service.flags_cas)
flags["position"] = self.get_sat_position()
return SP.join("{}{}{}".format(k, KSP, v) for k, v in flags.items())
def get_enigma2_flags(self):
flags = ["p:{}".format(self._package_entry.get_text())]
@@ -593,10 +599,12 @@ class ServiceDetailsDialog:
fav_id = self._ENIGMA2_FAV_ID.format(ssid, tr_id, net_id, namespace)
return fav_id, data_id
elif self._s_type is SettingsType.NEUTRINO_MP:
data = get_attributes(self._old_service.data_id)
data["n"] = self._name_entry.get_text()
data["t"] = "{:x}".format(int(service_type))
data["i"] = "{:04x}".format(ssid)
fav_id = self._NEUTRINO_FAV_ID.format(tr_id, net_id, ssid)
data_id = self._old_service.data_id.split(":")
data_id[1] = "{:x}".format(int(service_type))
return fav_id, ":".join(data_id)
return fav_id, SP.join("{}{}{}".format(k, KSP, v) for k, v in data.items())
# ***************** Transponder ********************* #
@@ -641,12 +649,19 @@ class ServiceDetailsDialog:
pls_code = self._pls_code_entry.get_text()
st_id = self._stream_id_entry.get_text()
pls = ":{}:{}:{}".format(st_id, pls_code, pls_mode) if pls_mode and pls_code and st_id else ""
return "{}:{}:{}:{}:{}{}".format(dvb_s_tr, flag, mod, roll_off, pilot, pls)
elif self._s_type is SettingsType.NEUTRINO_MP:
on_id, tr_id = int(self._network_id_entry.get_text()), int(self._transponder_id_entry.get_text())
mod = self.get_value_from_combobox_id(self._mod_combo_box, MODULATION) if sys == "DVB-S2" else None
srv_sys = None
return self._NEUTRINO_TRANSPONDER_DATA.format(tr_id, on_id, freq, inv, rate, fec, pol, mod, srv_sys)
tr_data = get_attributes(self._old_service.transponder)
tr_data["frq"] = freq
tr_data["sr"] = rate
tr_data["pol"] = pol
tr_data["fec"] = fec
tr_data["on"] = "{:04x}".format(int(self._network_id_entry.get_text()))
tr_data["id"] = "{:04x}".format(int(self._transponder_id_entry.get_text()))
tr_data["inv"] = inv
return SP.join("{}{}{}".format(k, KSP, v) for k, v in tr_data.items())
def get_sat_position(self):
sat_pos = self._sat_pos_button.get_value() * (-1 if self._pos_side_box.get_active_id() == "W" else 1)
@@ -706,9 +721,9 @@ class ServiceDetailsDialog:
continue
if self._s_type is SettingsType.NEUTRINO_MP:
flags = srv[Column.SRV_CAS_FLAGS].split(":")
flags[1] = sat_pos
srv[Column.SRV_CAS_FLAGS] = ":".join(flags)
flags = get_attributes(srv[Column.SRV_CAS_FLAGS])
flags["position"] = sat_pos
srv[Column.SRV_CAS_FLAGS] = SP.join("{}{}{}".format(k, KSP, v) for k, v in flags.items())
self._services[fav_id] = Service(*srv[:Column.SRV_TOOLTIP])
self._current_model.set_row(itr, srv)

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,41 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
import os
import re
from collections import Counter
from app.commons import run_task, run_idle, log
from app.connections import test_telnet, test_ftp, TestException, test_http, HttpApiException
from app.settings import SettingsType, Settings, PlayStreamsMode, IS_WIN
from app.settings import SettingsType, Settings, PlayStreamsMode, IS_LINUX, SEP, IS_WIN
from app.ui.dialogs import show_dialog, DialogType, get_message, get_chooser_dialog, get_builder
from .main_helper import update_entry_data, scroll_to, get_picon_pixbuf
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON, APP_FONT
def show_settings_dialog(transient, options):
return SettingsDialog(transient, options).show()
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON, APP_FONT, IS_GNOME_SESSION
class SettingsDialog:
@@ -18,12 +43,10 @@ class SettingsDialog:
_DIGIT_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
def __init__(self, transient, settings: Settings):
handlers = {"on_field_icon_press": self.on_field_icon_press,
handlers = {"on_field_button_press": self.on_field_button_press,
"on_settings_type_changed": self.on_settings_type_changed,
"on_reset": self.on_reset,
"on_response": self.on_response,
"apply_settings": self.apply_settings,
"on_apply_profile_settings": self.on_apply_profile_settings,
"on_connection_test": self.on_connection_test,
"on_info_bar_close": self.on_info_bar_close,
"on_set_color_switch": self.on_set_color_switch,
@@ -32,7 +55,6 @@ class SettingsDialog:
"on_experimental_switch": self.on_experimental_switch,
"on_yt_dl_switch": self.on_yt_dl_switch,
"on_default_path_mode_switch": self.on_default_path_mode_switch,
"on_default_data_path_changed": self.on_default_data_path_changed,
"on_profile_add": self.on_profile_add,
"on_profile_edit": self.on_profile_edit,
"on_profile_remove": self.on_profile_remove,
@@ -41,6 +63,12 @@ class SettingsDialog:
"on_profile_edited": self.on_profile_edited,
"on_profile_selected": self.on_profile_selected,
"on_profile_set_default": self.on_profile_set_default,
"on_host_focus_in": self.on_host_focus_in,
"on_host_focus_out": self.on_host_focus_out,
"on_add_host": self.on_add_host,
"on_remove_host": self.on_remove_host,
"on_add_picon_path": self.on_add_picon_path,
"on_remove_picon_path": self.on_remove_picon_path,
"on_lang_changed": self.on_lang_changed,
"on_main_settings_visible": self.on_main_settings_visible,
"on_http_use_ssl_toggled": self.on_http_use_ssl_toggled,
@@ -58,20 +86,25 @@ class SettingsDialog:
"on_icon_theme_add": self.on_icon_theme_add,
"on_icon_theme_remove": self.on_icon_theme_remove}
# Settings
# Settings.
self._ext_settings = settings
self._settings = Settings(settings.settings)
self._profiles = self._settings.profiles
self._s_type = self._settings.setting_type
self._updated = False
builder = get_builder(UI_RESOURCES_PATH + "settings_dialog.glade", handlers)
self._dialog = builder.get_object("settings_dialog")
self._dialog.set_transient_for(transient)
self._header_bar = builder.get_object("header_bar")
self._dialog.set_border_width(0)
self._dialog.set_margin_left(0)
self._main_stack = builder.get_object("main_stack")
# Network
# Network.
self._host_iter = None
self._host_field = builder.get_object("host_field")
self._hosts_box = builder.get_object("hosts_box")
self._remove_host_button = builder.get_object("remove_host_button")
self._port_field = builder.get_object("port_field")
self._login_field = builder.get_object("login_field")
self._password_field = builder.get_object("password_field")
@@ -79,30 +112,33 @@ class SettingsDialog:
self._http_use_ssl_check_button = builder.get_object("http_use_ssl_check_button")
self._telnet_port_field = builder.get_object("telnet_port_field")
self._telnet_timeout_spin_button = builder.get_object("telnet_timeout_spin_button")
# Test
self._reset_button = builder.get_object("reset_button")
# Test.
self._ftp_radio_button = builder.get_object("ftp_radio_button")
self._http_radio_button = builder.get_object("http_radio_button")
# Paths
# Network paths.
self._services_field = builder.get_object("services_field")
self._user_bouquet_field = builder.get_object("user_bouquet_field")
self._satellites_xml_field = builder.get_object("satellites_xml_field")
self._data_dir_field = builder.get_object("data_dir_field")
self._picons_field = builder.get_object("picons_field")
self._picons_dir_field = builder.get_object("picons_dir_field")
self._backup_dir_field = builder.get_object("backup_dir_field")
self._default_data_dir_field = builder.get_object("default_data_dir_field")
self._record_data_dir_field = builder.get_object("record_data_dir_field")
self._epg_dat_box = builder.get_object("epg_dat_box")
self._picons_paths_box = builder.get_object("picons_paths_box")
self._remove_picon_path_button = builder.get_object("remove_picon_path_button")
# Paths.
self._picons_path_field = builder.get_object("picons_path_field")
self._data_path_field = builder.get_object("data_path_field")
self._backup_path_field = builder.get_object("backup_path_field")
self._recordings_path_field = builder.get_object("recordings_path_field")
self._default_data_paths_switch = builder.get_object("default_data_paths_switch")
# Info bar
self._default_data_paths_switch.bind_property("active", builder.get_object("picons_path_box"), "sensitive", 4)
self._default_data_paths_switch.bind_property("active", builder.get_object("backup_path_box"), "sensitive", 4)
# Info bar.
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label")
self._test_spinner = builder.get_object("test_spinner")
# Settings type
# Settings type.
self._enigma_radio_button = builder.get_object("enigma_radio_button")
self._neutrino_radio_button = builder.get_object("neutrino_radio_button")
self._support_ver5_switch = builder.get_object("support_ver5_switch")
self._force_bq_name_switch = builder.get_object("force_bq_name_switch")
# Streaming
# Streaming.
self._apply_presets_button = builder.get_object("apply_presets_button")
self._transcoding_switch = builder.get_object("transcoding_switch")
self._edit_preset_switch = builder.get_object("edit_preset_switch")
@@ -116,22 +152,20 @@ class SettingsDialog:
self._audio_codec_combo_box = builder.get_object("audio_codec_combo_box")
self._transcoding_switch.bind_property("active", builder.get_object("record_box"), "sensitive")
self._edit_preset_switch.bind_property("active", self._apply_presets_button, "sensitive")
self._edit_preset_switch.bind_property("active", builder.get_object("video_options_frame"), "sensitive")
self._edit_preset_switch.bind_property("active", builder.get_object("audio_options_frame"), "sensitive")
self._play_in_built_radio_button = builder.get_object("play_in_built_radio_button")
self._play_in_window_radio_button = builder.get_object("play_in_window_radio_button")
self._get_m3u_radio_button = builder.get_object("get_m3u_radio_button")
self._gst_lib_button = builder.get_object("gst_lib_button")
self._vlc_lib_button = builder.get_object("vlc_lib_button")
self._mpv_lib_button = builder.get_object("mpv_lib_button")
# Program
self._edit_preset_switch.bind_property("active", builder.get_object("video_options_grid"), "sensitive")
self._edit_preset_switch.bind_property("active", builder.get_object("audio_options_grid"), "sensitive")
self._play_streams_combo_box = builder.get_object("play_streams_combo_box")
self._stream_lib_combo_box = builder.get_object("stream_lib_combo_box")
self._double_click_combo_box = builder.get_object("double_click_combo_box")
self._allow_main_list_playback_switch = builder.get_object("allow_main_list_playback_switch")
# Program.
self._before_save_switch = builder.get_object("before_save_switch")
self._before_downloading_switch = builder.get_object("before_downloading_switch")
self._load_on_startup_switch = builder.get_object("load_on_startup_switch")
self._bouquet_hints_switch = builder.get_object("bouquet_hints_switch")
self._services_hints_switch = builder.get_object("services_hints_switch")
self._lang_combo_box = builder.get_object("lang_combo_box")
# Appearance
# Appearance.
self._list_font_button = builder.get_object("list_font_button")
self._picons_size_button = builder.get_object("picons_size_button")
self._tooltip_logo_size_button = builder.get_object("tooltip_logo_size_button")
@@ -139,72 +173,78 @@ class SettingsDialog:
self._set_color_switch = builder.get_object("set_color_switch")
self._new_color_button = builder.get_object("new_color_button")
self._extra_color_button = builder.get_object("extra_color_button")
# Extra
# Extra.
self._use_http_switch = builder.get_object("use_http_switch")
self._remove_unused_bq_switch = builder.get_object("remove_unused_bq_switch")
self._keep_power_mode_switch = builder.get_object("keep_power_mode_switch")
self._compress_picons_switch = builder.get_object("compress_picons_switch")
self._force_bq_name_switch = builder.get_object("force_bq_name_switch")
self._support_ver5_switch = builder.get_object("support_ver5_switch")
self._support_http_api_switch = builder.get_object("support_http_api_switch")
self._enable_yt_dl_switch = builder.get_object("enable_yt_dl_switch")
self._enable_update_yt_dl_switch = builder.get_object("enable_update_yt_dl_switch")
self._enable_send_to_switch = builder.get_object("enable_send_to_switch")
self._click_mode_disabled_button = builder.get_object("click_mode_disabled_button")
self._click_mode_stream_button = builder.get_object("click_mode_stream_button")
self._click_mode_play_button = builder.get_object("click_mode_play_button")
self._click_mode_zap_button = builder.get_object("click_mode_zap_button")
self._click_mode_zap_and_play_button = builder.get_object("click_mode_zap_and_play_button")
self._click_mode_zap_button.bind_property("sensitive", self._click_mode_play_button, "sensitive")
self._click_mode_zap_button.bind_property("sensitive", self._click_mode_zap_and_play_button, "sensitive")
# EXPERIMENTAL
# EXPERIMENTAL.
self._enable_exp_switch = builder.get_object("enable_experimental_switch")
self._enable_exp_switch.bind_property("active", builder.get_object("yt_dl_box"), "sensitive")
self._enable_yt_dl_switch.bind_property("active", builder.get_object("yt_dl_update_box"), "sensitive")
self._enable_exp_switch.bind_property("active", builder.get_object("v5_support_box"), "sensitive")
self._enable_exp_switch.bind_property("active", builder.get_object("enable_direct_playback_box"), "sensitive")
# Enigma2 only
# Enigma2 only.
self._enigma_radio_button.bind_property("active", builder.get_object("bq_naming_grid"), "sensitive")
self._enigma_radio_button.bind_property("active", builder.get_object("enable_http_box"), "sensitive")
self._enigma_radio_button.bind_property("active", builder.get_object("enable_experimental_box"), "sensitive")
self._enigma_radio_button.bind_property("active", builder.get_object("program_frame"), "sensitive")
self._enigma_radio_button.bind_property("active", builder.get_object("experimental_box"), "sensitive")
# Profiles
self._enigma_radio_button.bind_property("active", builder.get_object("allow_double_click_box"), "sensitive")
# Profiles.
self._profile_view = builder.get_object("profile_tree_view")
self._profile_add_button = builder.get_object("profile_add_button")
self._profile_remove_button = builder.get_object("profile_remove_button")
self._apply_profile_button = builder.get_object("apply_profile_button")
self._apply_profile_button.bind_property("visible", builder.get_object("reset_button"), "visible")
# Style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
# Network.
# Separated due to a bug with response (presumably in the builder) in ubuntu 18.04 and derivatives.
builder.get_object("network_settings_frame").add(builder.get_object("network_grid"))
# Style.
style_provider = Gtk.CssProvider()
style_provider.load_from_path(f"{UI_RESOURCES_PATH}style.css")
screen = Gdk.Screen.get_default()
self._digit_elems = (self._port_field, self._http_port_field, self._telnet_port_field, self._video_width_field,
self._video_bitrate_field, self._video_height_field, self._audio_bitrate_field)
for el in self._digit_elems:
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
self.init_ui_elements(self._s_type)
[self.init_element_style(el, screen, style_provider) for el in self._digit_elems]
self.init_element_style(self._host_field, screen, style_provider)
if IS_GNOME_SESSION:
switcher = builder.get_object("main_stack_switcher")
switcher.set_margin_top(0)
switcher.set_margin_bottom(0)
builder.get_object("main_box").remove(switcher)
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True)
header_bar.set_custom_title(switcher)
self._dialog.set_titlebar(header_bar)
self.init_ui_elements()
self.init_profiles()
if IS_WIN:
self._gst_lib_button.set_visible(False)
# Themes
enable_exp = self._settings.is_enable_experimental
builder.get_object("style_frame").set_visible(enable_exp)
builder.get_object("themes_support_frame").set_visible(enable_exp)
if not IS_LINUX:
# Themes.
builder.get_object("style_frame").set_visible(IS_WIN)
builder.get_object("themes_support_frame").set_visible(True)
self._layout_switch = builder.get_object("layout_switch")
self._layout_switch.set_active(self._ext_settings.alternate_layout)
self._theme_frame = builder.get_object("theme_frame")
self._theme_frame.set_visible(enable_exp)
self._theme_frame.set_visible(True)
self._theme_thumbnail_image = builder.get_object("theme_thumbnail_image")
self._theme_combo_box = builder.get_object("theme_combo_box")
self._icon_theme_combo_box = builder.get_object("icon_theme_combo_box")
self._dark_mode_switch = builder.get_object("dark_mode_switch")
self._dark_mode_switch.set_active(self._ext_settings.dark_mode)
self._themes_support_switch = builder.get_object("themes_support_switch")
self._themes_support_switch.bind_property("active", self._theme_frame, "sensitive")
self.init_themes()
@run_idle
def init_ui_elements(self, s_type):
is_enigma_profile = s_type is SettingsType.ENIGMA_2
self._neutrino_radio_button.set_active(s_type is SettingsType.NEUTRINO_MP)
def init_ui_elements(self):
is_enigma_profile = self._s_type is SettingsType.ENIGMA_2
self._neutrino_radio_button.set_active(self._s_type is SettingsType.NEUTRINO_MP)
self.update_picon_paths()
self.update_title()
http_active = self._support_http_api_switch.get_active()
self._click_mode_zap_button.set_sensitive(is_enigma_profile and http_active)
self._lang_combo_box.set_active_id(self._ext_settings.language)
self.on_info_bar_close() if is_enigma_profile else self.show_info_message(
"The Neutrino has only experimental support. Not all features are supported!", Gtk.MessageType.WARNING)
@@ -220,6 +260,9 @@ class SettingsDialog:
self.on_profile_selected(self._profile_view, False)
self._profile_remove_button.set_sensitive(len(self._profile_view.get_model()) > 1)
def init_element_style(self, elem, screen, provider):
elem.get_style_context().add_provider_for_screen(screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
def update_title(self):
title = "{} [{}]"
if self._s_type is SettingsType.ENIGMA_2:
@@ -227,17 +270,27 @@ class SettingsDialog:
elif self._s_type is SettingsType.NEUTRINO_MP:
self._dialog.set_title(title.format(get_message("Options"), self._neutrino_radio_button.get_label()))
def update_picon_paths(self):
model = self._picons_paths_box.get_model()
model.clear()
list(map(lambda p: model.append((p, p)), self._settings.picons_paths))
if self._settings.picons_path in self._settings.picons_paths:
self._picons_paths_box.set_active_id(self._settings.picons_path)
else:
self._picons_paths_box.set_active(0)
def show(self):
self._dialog.run()
return self._dialog.run()
def is_updated(self):
return self._updated
def on_response(self, dialog, resp):
if resp == Gtk.ResponseType.OK and not self.apply_settings():
return
if resp == Gtk.ResponseType.ACCEPT:
self._updated = self.on_save_settings()
dialog.destroy()
self._dialog.destroy()
return resp
def on_field_icon_press(self, entry, icon, event_button):
def on_field_button_press(self, entry):
update_entry_data(entry, self._dialog, self._settings)
def on_settings_type_changed(self, item):
@@ -246,7 +299,7 @@ class SettingsDialog:
self._settings.setting_type = s_type
self._s_type = s_type
self.on_reset()
self.init_ui_elements(s_type)
self.init_ui_elements()
def on_reset(self, item=None):
self._settings.reset()
@@ -254,7 +307,9 @@ class SettingsDialog:
def set_settings(self):
self._s_type = self._settings.setting_type
self._host_field.set_text(self._settings.host)
self._hosts_box.remove_all()
self._remove_host_button.set_sensitive(len([self._hosts_box.append(h, h) for h in self._settings.hosts]) > 1)
self._hosts_box.set_active_id(self._settings.host)
self._port_field.set_text(self._settings.port)
self._login_field.set_text(self._settings.user)
self._password_field.set_text(self._settings.password)
@@ -265,17 +320,18 @@ class SettingsDialog:
self._services_field.set_text(self._settings.services_path)
self._user_bouquet_field.set_text(self._settings.user_bouquet_path)
self._satellites_xml_field.set_text(self._settings.satellites_xml_path)
self._picons_field.set_text(self._settings.picons_path)
self._data_dir_field.set_text(self._settings.data_local_path)
self._picons_dir_field.set_text(self._settings.picons_local_path)
self._backup_dir_field.set_text(self._settings.backup_local_path)
self._default_data_dir_field.set_text(self._settings.default_data_path)
self._record_data_dir_field.set_text(self._settings.records_path)
self._epg_dat_box.set_active_id(self._settings.epg_dat_path)
self._picons_paths_box.set_active_id(self._settings.picons_path)
self._data_path_field.set_text(self._settings.default_data_path)
self._picons_path_field.set_text(self._settings.default_picon_path)
self._backup_path_field.set_text(self._settings.default_backup_path)
self._recordings_path_field.set_text(self._settings.recordings_path)
self._before_save_switch.set_active(self._settings.backup_before_save)
self._before_downloading_switch.set_active(self._settings.backup_before_downloading)
self.set_fav_click_mode(self._settings.fav_click_mode)
self.set_play_stream_mode(self._settings.play_streams_mode)
self.set_stream_lib(self._settings.stream_lib)
self._play_streams_combo_box.set_active_id(str(self._settings.play_streams_mode.value))
self._stream_lib_combo_box.set_active_id(self._settings.stream_lib)
self._double_click_combo_box.set_active_id(str(self._settings.fav_click_mode))
self._allow_main_list_playback_switch.set_active(self._settings.main_list_playback)
self._load_on_startup_switch.set_active(self._settings.load_last_config)
self._bouquet_hints_switch.set_active(self._settings.show_bq_hints)
self._services_hints_switch.set_active(self._settings.show_srv_hints)
@@ -286,12 +342,16 @@ class SettingsDialog:
self._picons_size_button.set_active_id(str(self._settings.list_picon_size))
self._tooltip_logo_size_button.set_active_id(str(self._settings.tooltip_logo_size))
self._list_font_button.set_font(self._settings.list_font)
self._support_http_api_switch.set_active(self._settings.http_api_support)
if self._s_type is SettingsType.ENIGMA_2:
self._enable_exp_switch.set_active(self._settings.is_enable_experimental)
self._support_ver5_switch.set_active(self._settings.v5_support)
self._use_http_switch.set_active(self._settings.use_http)
self._remove_unused_bq_switch.set_active(self._settings.remove_unused_bouquets)
self._keep_power_mode_switch.set_active(self._settings.keep_power_mode)
self._compress_picons_switch.set_active(self._settings.compress_picons)
self._force_bq_name_switch.set_active(self._settings.force_bq_names)
self._support_http_api_switch.set_active(self._settings.http_api_support)
self._enable_yt_dl_switch.set_active(self._settings.enable_yt_dl)
self._enable_update_yt_dl_switch.set_active(self._settings.enable_yt_dl_update)
self._enable_send_to_switch.set_active(self._settings.enable_send_to)
@@ -316,6 +376,7 @@ class SettingsDialog:
self._s_type = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP
self._settings.setting_type = self._s_type
self._settings.host = self._host_field.get_text()
self._settings.hosts = [h[1] for h in self._hosts_box.get_model()]
self._settings.port = self._port_field.get_text()
self._settings.user = self._login_field.get_text()
self._settings.password = self._password_field.get_text()
@@ -325,37 +386,39 @@ class SettingsDialog:
self._settings.telnet_timeout = int(self._telnet_timeout_spin_button.get_value())
self._settings.services_path = self._services_field.get_text()
self._settings.user_bouquet_path = self._user_bouquet_field.get_text()
self._settings.satellites_xml_path = self._satellites_xml_field.get_text()
self._settings.picons_path = self._picons_field.get_text()
self._settings.data_local_path = self._data_dir_field.get_text()
self._settings.picons_local_path = self._picons_dir_field.get_text()
self._settings.backup_local_path = self._backup_dir_field.get_text()
self._settings.epg_dat_path = self._epg_dat_box.get_active_id()
self._settings.picons_path = self._picons_paths_box.get_active_id()
def apply_settings(self, item=None):
def on_save_settings(self, item=None):
if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.OK:
return
return False
self.on_apply_profile_settings()
self._ext_settings.profiles = self._settings.profiles
self._ext_settings.backup_before_save = self._before_save_switch.get_active()
self._ext_settings.backup_before_downloading = self._before_downloading_switch.get_active()
self._ext_settings.fav_click_mode = self.get_fav_click_mode()
self._ext_settings.play_streams_mode = self.get_play_stream_mode()
self._ext_settings.stream_lib = self.get_stream_lib()
self._ext_settings.play_streams_mode = PlayStreamsMode(int(self._play_streams_combo_box.get_active_id()))
self._ext_settings.stream_lib = self._stream_lib_combo_box.get_active_id()
self._ext_settings.fav_click_mode = int(self._double_click_combo_box.get_active_id())
self._ext_settings.main_list_playback = self._allow_main_list_playback_switch.get_active()
self._ext_settings.language = self._lang_combo_box.get_active_id()
self._ext_settings.load_last_config = self._load_on_startup_switch.get_active()
self._ext_settings.show_bq_hints = self._bouquet_hints_switch.get_active()
self._ext_settings.show_srv_hints = self._services_hints_switch.get_active()
self._ext_settings.profile_folder_is_default = self._default_data_paths_switch.get_active()
self._ext_settings.default_data_path = self._default_data_dir_field.get_text()
self._ext_settings.records_path = self._record_data_dir_field.get_text()
self._ext_settings.default_data_path = self._data_path_field.get_text()
self._ext_settings.default_backup_path = self._backup_path_field.get_text()
self._ext_settings.default_picon_path = self._picons_path_field.get_text()
self._ext_settings.recordings_path = self._recordings_path_field.get_text()
self._ext_settings.activate_transcoding = self._transcoding_switch.get_active()
self._ext_settings.active_preset = self._presets_combo_box.get_active_id()
self._ext_settings.list_picon_size = int(self._picons_size_button.get_active_id())
self._ext_settings.tooltip_logo_size = int(self._tooltip_logo_size_button.get_active_id())
self._ext_settings.list_font = self._list_font_button.get_font()
self._ext_settings.http_api_support = self._support_http_api_switch.get_active()
if IS_WIN:
if not IS_LINUX:
self._ext_settings.dark_mode = self._dark_mode_switch.get_active()
self._ext_settings.alternate_layout = self._layout_switch.get_active()
self._ext_settings.is_themes_support = self._themes_support_switch.get_active()
self._ext_settings.theme = self._theme_combo_box.get_active_id()
@@ -367,14 +430,18 @@ class SettingsDialog:
self._ext_settings.new_color = self._new_color_button.get_rgba().to_string()
self._ext_settings.extra_color = self._extra_color_button.get_rgba().to_string()
self._ext_settings.v5_support = self._support_ver5_switch.get_active()
self._ext_settings.use_http = self._use_http_switch.get_active()
self._ext_settings.remove_unused_bouquets = self._remove_unused_bq_switch.get_active()
self._ext_settings.keep_power_mode = self._keep_power_mode_switch.get_active()
self._ext_settings.compress_picons = self._compress_picons_switch.get_active()
self._ext_settings.force_bq_names = self._force_bq_name_switch.get_active()
self._ext_settings.http_api_support = self._support_http_api_switch.get_active()
self._ext_settings.enable_yt_dl = self._enable_yt_dl_switch.get_active()
self._ext_settings.enable_yt_dl_update = self._enable_update_yt_dl_switch.get_active()
self._ext_settings.enable_send_to = self._enable_send_to_switch.get_active()
self._ext_settings.default_profile = list(filter(lambda r: r[1], self._profile_view.get_model()))[0][0]
self._ext_settings.save()
return True
@run_task
@@ -394,7 +461,8 @@ class SettingsDialog:
host, port = self._host_field.get_text(), self._http_port_field.get_text()
use_ssl = self._http_use_ssl_check_button.get_active()
try:
self.show_info_message(test_http(host, port, user, password, use_ssl=use_ssl), Gtk.MessageType.INFO)
self.show_info_message(test_http(host, port, user, password, use_ssl=use_ssl, s_type=self._s_type),
Gtk.MessageType.INFO)
except TestException as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
except HttpApiException as e:
@@ -417,7 +485,7 @@ class SettingsDialog:
host, port = self._host_field.get_text(), self._port_field.get_text()
user, password = self._login_field.get_text(), self._password_field.get_text()
try:
self.show_info_message("OK. {}".format(test_ftp(host, port, user, password)), Gtk.MessageType.INFO)
self.show_info_message(f"OK. {test_ftp(host, port, user, password)}", Gtk.MessageType.INFO)
except TestException as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
finally:
@@ -425,9 +493,10 @@ class SettingsDialog:
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(True)
self._info_bar.set_visible(False)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(get_message(text))
self._info_bar.set_visible(True)
@run_idle
def show_spinner(self, show):
@@ -441,11 +510,8 @@ class SettingsDialog:
self._colors_grid.set_sensitive(state)
def on_http_mode_switch(self, switch, state):
self._click_mode_zap_button.set_sensitive(state)
if any((self._click_mode_play_button.get_active(),
self._click_mode_zap_button.get_active(),
self._click_mode_zap_and_play_button.get_active())):
self._click_mode_disabled_button.set_active(True)
if self._main_stack.get_visible_child_name() == "program" and not state:
self.show_info_message("May affect some features availability! ", Gtk.MessageType.WARNING)
def on_experimental_switch(self, switch, state):
if not state:
@@ -469,16 +535,13 @@ class SettingsDialog:
def on_default_path_mode_switch(self, switch, state):
self._settings.profile_folder_is_default = state
def on_default_data_path_changed(self, entry):
self._settings.default_data_path = entry.get_text()
def on_profile_add(self, item):
model = self._profile_view.get_model()
count = 0
name = "profile"
while name in self._profiles:
count += 1
name = "profile{}".format(count)
name = f"profile{count}"
self._profiles[name] = self._s_type.get_default_settings()
model.append((name, None))
@@ -518,29 +581,8 @@ class SettingsDialog:
if p_settings:
row[0] = new_value
self._profiles[new_value] = p_settings
self.update_local_paths(new_value, old_name)
self.on_profile_selected(self._profile_view, False)
def update_local_paths(self, p_name, old_name, force_rename=False):
data_path = self._settings.data_local_path
picons_path = self._settings.picons_local_path
backup_path = self._settings.backup_local_path
self._settings.data_local_path = p_name.join(data_path.rsplit(old_name, 1))
self._settings.picons_local_path = p_name.join(picons_path.rsplit(old_name, 1))
self._settings.backup_local_path = p_name.join(backup_path.rsplit(old_name, 1))
if force_rename:
try:
if os.path.isdir(picons_path):
os.rename(picons_path, self._settings.picons_local_path)
if os.path.isdir(data_path):
os.rename(data_path, self._settings.data_local_path)
if os.path.isdir(backup_path):
os.rename(backup_path, self._settings.backup_local_path)
except OSError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
def on_profile_selected(self, view, force=True):
if force:
self.on_apply_profile_settings()
@@ -562,14 +604,79 @@ class SettingsDialog:
def on_profile_inserted(self, model, path, itr):
self._profile_remove_button.set_sensitive(len(model) > 1)
def on_host_focus_in(self, entry, event):
self._host_iter = self._hosts_box.get_active_iter()
def on_host_focus_out(self, entry, event=None):
if self._host_iter:
model = self._hosts_box.get_model()
host = entry.get_text()
model.set_value(self._host_iter, 0, host)
model.set_value(self._host_iter, 1, host)
if Counter(r[0] for r in model).get(host, 0) > 1:
self._host_field.set_name(self._DIGIT_ENTRY_NAME)
self.show_info_message("The host already exists!", Gtk.MessageType.WARNING)
else:
self._host_field.set_name("GtkEntry")
self.on_info_bar_close()
def on_add_host(self, button):
model = self._hosts_box.get_model()
count = 1
host = "127.0.0.1"
hosts = {r[0] for r in model}
while host in hosts:
count += 1
host = f"127.0.0.{count}"
self._hosts_box.append(host, host)
self._hosts_box.set_active_id(host)
self._remove_host_button.set_sensitive(len(model) > 1)
def on_remove_host(self, button):
self._hosts_box.remove(self._hosts_box.get_active())
self._hosts_box.set_active(0)
self._remove_host_button.set_sensitive(len(self._hosts_box.get_model()) > 1)
def on_add_picon_path(self, button):
response = show_dialog(DialogType.INPUT, self._dialog, self._settings.picons_path)
if response is Gtk.ResponseType.CANCEL:
return
if response in self._settings.picons_paths:
self.show_info_message("This path already exists!", Gtk.MessageType.ERROR)
return
path = response if response.endswith(SEP) else response + SEP
model = self._picons_paths_box.get_model()
model.append((path, path))
self._picons_paths_box.set_active_id(path)
self._ext_settings.picons_paths = tuple(r[0] for r in model)
def on_remove_picon_path(self, button):
msg = f"{get_message('This may change the settings of other profiles!')}\n\n\t\t{get_message('Are you sure?')}"
if show_dialog(DialogType.QUESTION, self._dialog, msg) != Gtk.ResponseType.OK:
return
model = self._picons_paths_box.get_model()
active = self._picons_paths_box.get_active_iter()
if active:
model.remove(active)
self._picons_paths_box.set_active(0)
self._remove_picon_path_button.set_sensitive(len(model) > 1)
self._ext_settings.picons_paths = tuple(r[0] for r in model)
def on_lang_changed(self, box):
if box.get_active_id() != self._settings.language:
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
def on_main_settings_visible(self, stack, param):
name = stack.get_visible_child_name()
self._apply_profile_button.set_visible(name == "profiles")
self._apply_presets_button.set_visible(name == "streaming")
self._reset_button.set_visible(name == "profiles")
def on_http_use_ssl_toggled(self, button):
active = button.get_active()
@@ -579,78 +686,26 @@ class SettingsDialog:
self._settings.http_port = port
def on_click_mode_togged(self, button):
if self._main_stack.get_visible_child_name() != "extra":
if self._main_stack.get_visible_child_name() != "streaming":
return
mode = self.get_fav_click_mode()
mode = FavClickMode(int(self._double_click_combo_box.get_active_id()))
if mode is FavClickMode.PLAY:
self.show_info_message("Operates in standby mode or current active transponder!", Gtk.MessageType.WARNING)
elif mode is FavClickMode.STREAM:
self.show_info_message("Playback IPTV streams only!", Gtk.MessageType.WARNING)
elif mode is FavClickMode.DISABLED:
self._allow_main_list_playback_switch.set_active(False)
else:
self.on_info_bar_close()
@run_idle
def set_fav_click_mode(self, mode):
mode = FavClickMode(mode)
self._click_mode_disabled_button.set_active(mode is FavClickMode.DISABLED)
self._click_mode_stream_button.set_active(mode is FavClickMode.STREAM)
self._click_mode_play_button.set_active(mode is FavClickMode.PLAY)
self._click_mode_zap_button.set_active(mode is FavClickMode.ZAP)
self._click_mode_zap_and_play_button.set_active(mode is FavClickMode.ZAP_PLAY)
def get_fav_click_mode(self):
if self._click_mode_zap_button.get_active():
return FavClickMode.ZAP
if self._click_mode_play_button.get_active():
return FavClickMode.PLAY
if self._click_mode_zap_and_play_button.get_active():
return FavClickMode.ZAP_PLAY
if self._click_mode_stream_button.get_active():
return FavClickMode.STREAM
return FavClickMode.DISABLED
self._allow_main_list_playback_switch.set_sensitive(mode is not FavClickMode.DISABLED)
def on_play_mode_changed(self, button):
if self._main_stack.get_visible_child_name() != "streaming" or not button.get_active():
if self._main_stack.get_visible_child_name() != "streaming":
return
if self._settings.is_darwin:
is_gst = self._gst_lib_button.get_active()
self._play_in_built_radio_button.set_sensitive(is_gst)
self._play_in_window_radio_button.set_active(not is_gst and self._play_in_built_radio_button.get_active())
if button.get_active():
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
@run_idle
def set_play_stream_mode(self, mode):
self._play_in_built_radio_button.set_active(mode is PlayStreamsMode.BUILT_IN)
self._play_in_window_radio_button.set_active(mode is PlayStreamsMode.WINDOW)
self._get_m3u_radio_button.set_active(mode is PlayStreamsMode.M3U)
if self._settings.is_darwin and self._settings.stream_lib != "gst":
self._play_in_built_radio_button.set_sensitive(False)
def get_play_stream_mode(self):
if self._play_in_built_radio_button.get_active():
return PlayStreamsMode.BUILT_IN
if self._play_in_window_radio_button.get_active():
return PlayStreamsMode.WINDOW
if self._get_m3u_radio_button.get_active():
return PlayStreamsMode.M3U
return self._settings.play_streams_mode
def set_stream_lib(self, mode):
self._vlc_lib_button.set_active(mode == "vlc")
self._gst_lib_button.set_active(mode == "gst")
self._mpv_lib_button.set_active(mode == "mpv")
def get_stream_lib(self):
if self._gst_lib_button.get_active():
return "gst"
elif self._vlc_lib_button.get_active():
return "vlc"
return "mpv"
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
def on_transcoding_preset_changed(self, button):
presets = self._settings.transcoding_presets
@@ -710,7 +765,7 @@ class SettingsDialog:
@run_idle
def set_theme_thumbnail_image(self, theme_name):
img_path = "{}{}/gtk-3.0/thumbnail.png".format(self._ext_settings.themes_path, theme_name)
img_path = "{}{}{}gtk-3.0{}thumbnail.png".format(self._ext_settings.themes_path, theme_name, SEP, SEP)
self._theme_thumbnail_image.set_from_pixbuf(get_picon_pixbuf(img_path, 96))
def on_theme_add(self, button):
@@ -745,15 +800,16 @@ class SettingsDialog:
@run_task
def unpack_theme(self, src, dst, button):
try:
os.makedirs(os.path.dirname(dst), exist_ok=True)
from shutil import unpack_archive
import subprocess
log("Unpacking '{}' started...".format(src))
p = subprocess.Popen(["tar", "-xvf", src, "-C", dst],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
p.communicate()
log(f"Unpacking '{src}' started...")
os.makedirs(os.path.dirname(dst), exist_ok=True)
unpack_archive(src, dst)
log("Unpacking end.")
except (ValueError, OSError) as e:
msg = f"Unpacking error: {e}"
log(msg)
self.show_info_message(msg, Gtk.MessageType.ERROR)
finally:
self.update_theme_button(button, dst)

View File

@@ -1,18 +1,54 @@
* {
-GtkDialog-action-area-border: 6;
}
#digit-entry {
border-color: Red;
}
#status-bar-button {
padding: 1px;
padding-top: 1px;
padding-bottom: 1px;
padding-left: 3px;
padding-right: 3px;
margin: 1px;
}
#task-button {
padding: 0;
}
#header-button {
padding-top: 0;
padding-bottom: 0;
}
#header-entry {
min-height: 0;
}
#header-stack-switcher > button {
padding-top: 0;
padding-bottom: 0;
}
buttonbox {
padding: 0;
}
paned > separator {
background-repeat: no-repeat;
background-position: center;
}
paned.horizontal > separator {
background-size: 2px 24px;
}
paned.vertical > separator {
background-size: 24px 2px;
}
.red-button {
background-image: none;
background-color: red;
@@ -57,9 +93,14 @@ paned > separator {
border-right-width: 1px;
}
.stack-switcher > button {
padding-left: 0.5em;
padding-right: 0.5em;
min-width: 5em;
min-height: 1.5em;
.stack-switcher > button > label {
padding-left: 5px;
padding-right: 5px;
min-width: 50px;
}
.stack-switcher > button.text-button {
padding-left: 5px;
padding-right: 5px;
min-width: 50px;
}

86
app/ui/tasks.py Normal file
View File

@@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
from app.ui.dialogs import get_message
from .uicommons import Gtk, GLib
class BGTaskWidget(Gtk.Box):
""" Widget for displaying and running background tasks. """
TASK_LIMIT = 1
def __init__(self, app, text, target, *args):
super().__init__(spacing=2, orientation=Gtk.Orientation.HORIZONTAL, valign=Gtk.Align.CENTER)
self._app = app
self._label = Gtk.Label(get_message(text))
self.pack_start(self._label, False, False, 0)
self._spinner = Gtk.Spinner(active=True)
self.pack_start(self._spinner, False, False, 0)
close_button = Gtk.Button.new_from_icon_name("window-close", Gtk.IconSize.MENU)
close_button.set_relief(Gtk.ReliefStyle.NONE)
close_button.set_valign(Gtk.Align.CENTER)
close_button.set_tooltip_text(get_message("Cancel"))
close_button.set_name("task-button")
close_button.connect("clicked", lambda b: self._app.emit("task-cancel", self))
self.pack_start(close_button, False, False, 0)
self.show_all()
# Just prototype. -> It may not work properly!
# TODO: Different options need to be tested. Possibly with normal threads.
from concurrent.futures import ThreadPoolExecutor
self._executor = ThreadPoolExecutor(max_workers=self.TASK_LIMIT)
future = self._executor.submit(target, *args)
future.add_done_callback(lambda f: GLib.idle_add(self._app.emit, "task-done", self))
@property
def text(self):
return self._label.get_text()
@text.setter
def text(self, value):
self._label.set_text(value)
@property
def tooltip(self):
return self.get_tooltip_text()
@tooltip.setter
def tooltip(self, value):
self.set_tooltip_text(value)
def cancel(self):
self._executor.shutdown(wait=False)
self._app.emit("task-canceled", None)
if __name__ == '__main__':
pass

269
app/ui/telnet.glade Executable file → Normal file
View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1
<!-- Generated with glade 3.22.2
The MIT License (MIT)
@@ -27,10 +27,10 @@ Author: Dmitriy Yefremov
-->
<interface domain="demon-editor">
<requires lib="gtk+" version="3.16"/>
<requires lib="gtk+" version="3.18"/>
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkTextTagTable" id="tag_table">
<child type="tag">
@@ -43,25 +43,99 @@ Author: Dmitriy Yefremov
<object class="GtkTextBuffer" id="text_buffer">
<property name="tag_table">tag_table</property>
</object>
<object class="GtkWindow" id="dialog_window">
<object class="GtkFrame" id="telnet_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">DemonEditor [Telnet client]</property>
<property name="destroy_with_parent">True</property>
<property name="icon_name">terminal</property>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<signal name="delete-event" handler="on_close" swapped="no"/>
<property name="label_xalign">0.49000000953674316</property>
<property name="shadow_type">in</property>
<child>
<placeholder/>
</child>
<child>
<object class="GtkBox" id="main_box">
<property name="width_request">560</property>
<property name="height_request">320</property>
<object class="GtkBox" id="telnet_main_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="commands_entry">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">2</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="connect_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Connect</property>
<signal name="clicked" handler="on_connect" swapped="no"/>
<child>
<object class="GtkImage" id="connect_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-connect</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="disconnect_button">
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Disconnect</property>
<signal name="clicked" handler="on_disconnect" swapped="no"/>
<child>
<object class="GtkImage" id="disconnect_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-disconnect</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="clear_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Clear</property>
<property name="halign">center</property>
<property name="valign">center</property>
<signal name="clicked" handler="on_clear" swapped="no"/>
<child>
<object class="GtkImage" id="clear_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-clear</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="telnet_scrolled_window">
<property name="visible">True</property>
@@ -87,163 +161,16 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="commands_entry">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">2</property>
<property name="spacing">2</property>
<child>
<object class="GtkComboBoxText" id="profile_combo_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="active">0</property>
<property name="has_frame">False</property>
<signal name="changed" handler="on_profile_changed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="connect_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Connect</property>
<signal name="clicked" handler="on_connect" swapped="no"/>
<child>
<object class="GtkImage" id="connect_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-connect</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="disconnect_button">
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Disconnect</property>
<signal name="clicked" handler="on_disconnect" swapped="no"/>
<child>
<object class="GtkImage" id="disconnect_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-disconnect</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child type="center">
<placeholder/>
</child>
<child>
<object class="GtkButton" id="clear_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Clear</property>
<property name="halign">center</property>
<property name="valign">center</property>
<signal name="clicked" handler="on_clear" swapped="no"/>
<child>
<object class="GtkImage" id="clear_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-clear</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="app_paintable">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<property name="show_close_button">True</property>
<signal name="response" handler="on_info_bar_close" swapped="no"/>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="layout_style">end</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="info_bar_message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">Info</property>
<property name="justify">center</property>
<property name="wrap">True</property>
<property name="wrap_mode">word-char</property>
<property name="lines">2</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Telnet</property>
</object>
</child>
</object>

128
app/ui/telnet.py Executable file → Normal file
View File

@@ -1,3 +1,31 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2021 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
import re
import selectors
import socket
@@ -7,7 +35,6 @@ from telnetlib import Telnet
from gi.repository import GLib
from app.commons import run_task, run_idle, log
from app.settings import Settings
from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK
@@ -18,7 +45,7 @@ class ExtTelnet(Telnet):
self._output_callback = output_callback
def interact(self):
"""Interaction function, emulates a very dumb telnet client."""
""" Interaction function, emulates a very dumb telnet client. """
with selectors.DefaultSelector() as selector:
selector.register(self, selectors.EVENT_READ)
@@ -37,90 +64,62 @@ class ExtTelnet(Telnet):
self._output_callback(text)
class TelnetDialog:
""" Dialog of very simple telnet client. """
class TelnetClient(Gtk.Box):
""" Very simple telnet client. """
_COLOR_PATTERN = re.compile("\x1b.*?m") # Color info
_ERASING_PATTERN = re.compile("\x1b.*?K") # Erase to right
_APP_MODE_PATTERN = re.compile("\x1b.*?(1h)|(1l)") # h - on, l - off
_ALL_PATTERN = re.compile(r'(\x1b\[|\x9b)[0-?]*[@-~]')
_NOT_SUPPORTED = {"mc", "mcedit", "vi", "nano"}
def __init__(self, transient, settings):
self._handlers = {"on_profile_changed": self.on_profile_changed,
"on_clear": self.on_clear,
def __init__(self, app, *args, **kwargs):
super().__init__(*args, **kwargs)
self._app = app
self._app.connect("profile-changed", self.on_profile_changed)
self._tn = None
self._app_mode = False
self._commands = deque(maxlen=10)
self._handlers = {"on_clear": self.on_clear,
"on_text_view_realize": self.on_text_view_realize,
"on_view_key_press": self.on_view_key_press,
"on_info_bar_close": self.on_info_bar_close,
"on_connect": self.on_connect,
"on_disconnect": self.on_disconnect,
"on_close": self.on_close}
"on_disconnect": self.on_disconnect}
builder = Gtk.Builder()
builder.add_from_file(UI_RESOURCES_PATH + "telnet.glade")
builder.connect_signals(self._handlers)
self._dialog_window = builder.get_object("dialog_window")
self._dialog_window.set_transient_for(transient)
self._profile_combo_box = builder.get_object("profile_combo_box")
self._info_bar = builder.get_object("info_bar")
self._info_message_label = builder.get_object("info_bar_message_label")
self._text_view = builder.get_object("text_view")
self._buf = builder.get_object("text_buffer")
self._end_tag = builder.get_object("end_tag")
self._connect_button = builder.get_object("connect_button")
self._connect_button.bind_property("visible", builder.get_object("disconnect_button"), "visible", 4)
main_frame = builder.get_object("telnet_frame")
provider = Gtk.CssProvider()
provider.load_from_path(UI_RESOURCES_PATH + "style.css")
builder.get_object("main_box").get_style_context().add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
main_frame.get_style_context().add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
window_size = settings.get("telnet_dialog_window_size")
if window_size:
self._dialog_window.resize(*window_size)
self.pack_start(main_frame, True, True, 0)
self.show()
self._ext_settings = settings
self._settings = Settings(settings.settings)
self._tn = None
self._app_mode = False
self._commands = deque(maxlen=10)
def show(self):
self._dialog_window.show()
def on_close(self, window, event):
""" Performs shutdown tasks """
self._ext_settings.add("telnet_dialog_window_size", window.get_size())
def on_profile_changed(self, app, data):
self.on_clear()
self.on_disconnect()
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type)
self._info_message_label.set_text(text)
def on_text_view_realize(self, view):
self.init_profiles()
self.on_connect()
@run_idle
def init_profiles(self):
for p in self._settings.profiles:
self._profile_combo_box.append(p, p)
self._profile_combo_box.set_active_id(self._settings.current_profile)
def on_text_view_realize(self, view):
self.on_connect()
@run_task
def on_connect(self, item=None):
try:
GLib.idle_add(self._connect_button.set_visible, False)
GLib.idle_add(self.on_info_bar_close)
user, password = self._settings.user, self._settings.password
timeout = self._settings.telnet_timeout
self._tn = ExtTelnet(self.append_output,
host=self._settings.host,
port=self._settings.telnet_port,
timeout=timeout)
settings = self._app.app_settings
user, password, timeout = settings.user, settings.password, settings.telnet_timeout
self._tn = ExtTelnet(self.append_output, host=settings.host, port=settings.telnet_port, timeout=timeout)
if user != "":
self._tn.read_until(b"login: ")
@@ -131,8 +130,8 @@ class TelnetDialog:
self._tn.interact()
except (OSError, EOFError, socket.timeout, ConnectionRefusedError) as e:
log("{}: {}".format(self.__class__.__name__, e))
self.show_info_message(str(e), Gtk.MessageType.ERROR)
log(f"{self.__class__.__name__}: {e}")
self._app.show_info_message(str(e), Gtk.MessageType.ERROR)
finally:
GLib.idle_add(self._connect_button.set_visible, True)
@@ -142,9 +141,6 @@ class TelnetDialog:
GLib.idle_add(self._connect_button.set_visible, True)
self._tn.close()
def on_profile_changed(self, button):
self._settings.current_profile = button.get_active_id()
def on_command_done(self, entry):
command = entry.get_text()
entry.set_text("")
@@ -155,7 +151,7 @@ class TelnetDialog:
self._buf.delete(self._buf.get_start_iter(), self._buf.get_end_iter())
def on_view_key_press(self, view, event):
""" Handling keystrokes on press """
""" Handling keystrokes on press. """
if event.keyval == Gdk.KEY_Return:
self.do_command()
return True
@@ -170,7 +166,7 @@ class TelnetDialog:
if self._tn and self._tn.sock:
self._tn.write(b"\x03") # interrupt
# last commands navigation
# Last commands navigation.
if key is KeyboardKey.UP:
self.delete_last_command()
if self._commands:
@@ -206,13 +202,13 @@ class TelnetDialog:
else: # if buf is empty
command.append(self._buf.get_text(begin, end, False))
# to preventing duplication of the command in the buf
# To preventing duplication of the command in the buf.
self._buf.delete(end, begin)
if command and self._tn.sock:
cmd = command[0]
if cmd in self._NOT_SUPPORTED:
self.show_info_message("'{}' is not supported by this client.".format(cmd), Gtk.MessageType.ERROR)
self._app.show_info_message(f"'{cmd}' is not supported by this client.", Gtk.MessageType.ERROR)
else:
self._tn.write(cmd.encode("ascii") + b"\r")
self._commands.append(cmd)
@@ -230,7 +226,7 @@ class TelnetDialog:
self._app_mode = False
self.on_clear()
t = re.sub(self._ALL_PATTERN, "", t) # removing [replacing] ascii escape sequences
t = re.sub(self._ALL_PATTERN, "", t) # Removing [replacing] ascii escape sequences.
if self._app_mode:
start, end = self._buf.get_start_iter(), self._buf.get_end_iter()

View File

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

1765
app/ui/timers.glade Normal file

File diff suppressed because it is too large Load Diff

560
app/ui/timers.py Normal file
View File

@@ -0,0 +1,560 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" Module for working with timers. """
from datetime import datetime, timedelta
from enum import Enum
from urllib.parse import quote
from app.ui.main_helper import on_popup_menu
from .dialogs import get_builder, get_message, show_dialog, DialogType
from .uicommons import Gtk, Gdk, GLib, UI_RESOURCES_PATH, Page, Column, KeyboardKey, IS_GNOME_SESSION, MOD_MASK
from ..commons import run_idle, log
from ..connections import HttpAPI
from ..eparser.ecommons import BqServiceType
class TimerTool(Gtk.Box):
TIME_STR = "%Y-%m-%d %H:%M"
ACTION = {"0": "Record", "1": "Zap"}
AFTER_EVENT = {"0": "Do Nothing",
"1": "Standby",
"2": "Shut down",
"3": "Auto"}
class TimerAction(Enum):
ADD = 0
EVENT = 1
CHANGE = 2
class TimerDialog(Gtk.Dialog):
def __init__(self, parent, action=None, timer_data=None, *args, **kwargs):
super().__init__(use_header_bar=IS_GNOME_SESSION, *args, **kwargs)
self._action = action or TimerTool.TimerAction.ADD
self._timer_data = timer_data or {}
self._request = ""
handlers = {"on_timer_begins_set": self.on_timer_begins_set,
"on_timer_ends_set": self.on_timer_ends_set}
builder = get_builder(f"{UI_RESOURCES_PATH}timers.glade", handlers,
objects=("timer_dialog_frame", "timer_ends_popover", "end_hour_adjustment",
"min_end_adjustment", "timer_begins_popover", "begins_hour_adjustment",
"min_begins_adjustment"))
self.set_title(get_message("Timer"))
self.set_modal(True)
self.set_skip_pager_hint(True)
self.set_skip_taskbar_hint(True)
self.set_transient_for(parent)
self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
self.set_resizable(False)
self._timer_name_entry = builder.get_object("timer_name_entry")
self._timer_desc_entry = builder.get_object("timer_desc_entry")
self._timer_service_entry = builder.get_object("timer_service_entry")
self._timer_service_ref_entry = builder.get_object("timer_service_ref_entry")
self._timer_event_id_entry = builder.get_object("timer_event_id_entry")
self._timer_begins_entry = builder.get_object("timer_begins_entry")
self._timer_ends_entry = builder.get_object("timer_ends_entry")
self._timer_begins_calendar = builder.get_object("timer_begins_calendar")
self._timer_begins_hr_button = builder.get_object("timer_begins_hr_button")
self._timer_begins_min_button = builder.get_object("timer_begins_min_button")
self._timer_ends_calendar = builder.get_object("timer_ends_calendar")
self._timer_ends_hr_button = builder.get_object("timer_ends_hr_button")
self._timer_ends_min_button = builder.get_object("timer_ends_min_button")
self._timer_enabled_switch = builder.get_object("timer_enabled_switch")
self._timer_action_combo_box = builder.get_object("timer_action_combo_box")
self._timer_after_combo_box = builder.get_object("timer_after_combo_box")
self._days_buttons = (builder.get_object("timer_mo_check_button"),
builder.get_object("timer_tu_check_button"),
builder.get_object("timer_we_check_button"),
builder.get_object("timer_th_check_button"),
builder.get_object("timer_fr_check_button"),
builder.get_object("timer_sa_check_button"),
builder.get_object("timer_su_check_button"))
self._timer_location_switch = builder.get_object("timer_location_switch")
self._timer_location_entry = builder.get_object("timer_location_entry")
self._timer_location_switch.bind_property("active", self._timer_location_entry, "sensitive")
# Disable DnD for timer entries.
self._timer_name_entry.drag_dest_unset()
self._timer_desc_entry.drag_dest_unset()
self._timer_service_entry.drag_dest_unset()
self.add_buttons(get_message("Cancel"), Gtk.ResponseType.CANCEL, get_message("Save"), Gtk.ResponseType.OK)
self.get_content_area().pack_start(builder.get_object("timer_dialog_frame"), True, True, 5)
if self._action is TimerTool.TimerAction.ADD:
self.set_timer_for_add()
elif self._action is TimerTool.TimerAction.CHANGE:
self.set_timer_for_edit()
elif self._action is TimerTool.TimerAction.EVENT:
self.set_timer_from_event_data()
else:
log(f"{__class__.__name__} error: No action set for timer!")
@property
def request(self):
return self._request
def run(self):
resp = super().run()
if resp == Gtk.ResponseType.OK:
self._request = self.get_request()
return resp
def get_request(self):
""" Constructs str representation of add/update request. """
args = []
t_data = self.get_timer_data()
s_ref = quote(t_data.get("sRef", ""))
if self._action is TimerTool.TimerAction.EVENT:
args.append(f"timeraddbyeventid?sRef={s_ref}")
args.append(f"eventid={t_data.get('eit', '0')}")
args.append(f"justplay={t_data.get('justplay', '')}")
args.append(f"tags={''}")
else:
if self._action is TimerTool.TimerAction.ADD:
args.append(f"timeradd?sRef={s_ref}")
args.append(f"deleteOldOnSave={0}")
elif self._action is TimerTool.TimerAction.CHANGE:
args.append(f"timerchange?sRef={s_ref}")
args.append(f"channelOld={s_ref}")
args.append(f"beginOld={self._timer_data.get('e2timebegin', '0')}")
args.append(f"endOld={self._timer_data.get('e2timeend', '0')}")
args.append(f"deleteOldOnSave={1}")
args.append(f"begin={t_data.get('begin', '')}")
args.append(f"end={t_data.get('end', '')}")
args.append(f"name={quote(t_data.get('name', ''))}")
args.append(f"description={quote(t_data.get('description', ''))}")
args.append(f"tags={''}")
args.append(f"eit={'0'}")
args.append(f"disabled={t_data.get('disabled', '1')}")
args.append(f"justplay={t_data.get('justplay', '1')}")
args.append(f"afterevent={t_data.get('afterevent', '0')}")
args.append(f"repeated={TimerTool.get_repetition_flags(self._days_buttons)}")
if self._timer_location_switch.get_active():
args.append(f"dirname={self._timer_location_entry.get_text()}")
return "&".join(args)
def on_timer_begins_set(self, action, value=None):
b_date = self.get_begins_date()
if b_date > self.get_ends_date():
self.set_ends_date(b_date + timedelta(hours=1))
self.set_begins_date(b_date)
def on_timer_ends_set(self, action, value=None):
self.set_ends_date(self.get_ends_date())
def get_begins_date(self):
date = self._timer_begins_calendar.get_date()
return datetime(year=date.year, month=date.month + 1, day=date.day,
hour=int(self._timer_begins_hr_button.get_value()),
minute=int(self._timer_begins_min_button.get_value()))
def set_begins_date(self, date):
hour = date.hour
minute = date.minute
self._timer_begins_hr_button.set_value(hour)
self._timer_begins_min_button.set_value(minute)
self._timer_begins_calendar.select_day(date.day)
self._timer_begins_calendar.select_month(date.month - 1, date.year)
self._timer_begins_entry.set_text(f"{date.year}-{date.month:02d}-{date.day:02d} {hour:02d}:{minute:02d}")
def get_ends_date(self):
date = self._timer_ends_calendar.get_date()
return datetime(year=date.year, month=date.month + 1, day=date.day,
hour=int(self._timer_ends_hr_button.get_value()),
minute=int(self._timer_ends_min_button.get_value()))
def set_ends_date(self, date):
hour = date.hour
minute = date.minute
self._timer_ends_hr_button.set_value(hour)
self._timer_ends_min_button.set_value(minute)
self._timer_ends_calendar.select_day(date.day)
self._timer_ends_calendar.select_month(date.month - 1, date.year)
self._timer_ends_entry.set_text(f"{date.year}-{date.month:02d}-{date.day:02d} {hour:02d}:{minute:02d}")
def set_timer_for_add(self):
self._timer_service_entry.set_text(self._timer_data.get("e2servicename", ""))
self._timer_service_ref_entry.set_text(self._timer_data.get("e2servicereference", ""))
date = datetime.now()
self.set_begins_date(date)
self.set_ends_date(date + timedelta(hours=1))
self._timer_event_id_entry.set_text("")
self._timer_location_switch.set_active(False)
TimerTool.set_repetition_flags(0, self._days_buttons)
def set_timer_for_edit(self):
self._timer_name_entry.set_text(self._timer_data.get("e2name", ""))
self._timer_desc_entry.set_text(self._timer_data.get("e2description", "") or "")
self._timer_service_entry.set_text(self._timer_data.get("e2servicename", "") or "")
self._timer_service_ref_entry.set_text(self._timer_data.get("e2servicereference", ""))
self._timer_event_id_entry.set_text(self._timer_data.get("e2eit", ""))
self._timer_enabled_switch.set_active((self._timer_data.get("e2disabled", "0") == "0"))
self._timer_action_combo_box.set_active_id(self._timer_data.get("e2justplay", "0"))
self._timer_after_combo_box.set_active_id(self._timer_data.get("e2afterevent", "0"))
self.set_time_data(int(self._timer_data.get("e2timebegin", "0")),
int(self._timer_data.get("e2timeend", "0")))
location = self._timer_data.get("e2location", "")
self._timer_location_entry.set_text("" if location == "None" else location)
TimerTool.set_repetition_flags(int(self._timer_data.get("e2repeated", "0")), self._days_buttons)
def set_timer_from_event_data(self):
self._timer_name_entry.set_text(self._timer_data.get("e2eventtitle", ""))
self._timer_desc_entry.set_text(self._timer_data.get("e2eventdescription", ""))
self._timer_service_entry.set_text(self._timer_data.get("e2eventservicename", ""))
self._timer_service_ref_entry.set_text(self._timer_data.get("e2eventservicereference", ""))
self._timer_event_id_entry.set_text(self._timer_data.get("e2eventid", ""))
self._timer_action_combo_box.set_active_id("1")
self._timer_after_combo_box.set_active_id("3")
start_time = int(self._timer_data.get("e2eventstart", "0"))
self.set_time_data(start_time, start_time + int(self._timer_data.get("e2eventduration", "0")))
def set_time_data(self, start_time, end_time):
""" Sets values for time widgets. """
now = datetime.now()
ev_time_start = datetime.fromtimestamp(start_time) or now
ev_time_end = datetime.fromtimestamp(end_time) or now + timedelta(hours=1)
self._timer_begins_entry.set_text(ev_time_start.strftime(TimerTool.TIME_STR))
self._timer_ends_entry.set_text(ev_time_end.strftime(TimerTool.TIME_STR))
self._timer_begins_calendar.select_day(ev_time_start.day)
self._timer_begins_calendar.select_month(ev_time_start.month - 1, ev_time_start.year)
self._timer_ends_calendar.select_day(ev_time_end.day)
self._timer_ends_calendar.select_month(ev_time_end.month - 1, ev_time_end.year)
self._timer_begins_hr_button.set_value(ev_time_start.hour)
self._timer_begins_min_button.set_value(ev_time_start.minute)
self._timer_ends_hr_button.set_value(ev_time_end.hour)
self._timer_ends_min_button.set_value(ev_time_end.minute)
def get_timer_data(self):
""" Returns timer data as a dict. """
return {"sRef": self._timer_service_ref_entry.get_text(),
"begin": int(
datetime.strptime(self._timer_begins_entry.get_text(), TimerTool.TIME_STR).timestamp()),
"end": int(datetime.strptime(self._timer_ends_entry.get_text(), TimerTool.TIME_STR).timestamp()),
"name": self._timer_name_entry.get_text(),
"description": self._timer_desc_entry.get_text(),
"dirname": "",
"eit": self._timer_event_id_entry.get_text(),
"disabled": int(not self._timer_enabled_switch.get_active()),
"justplay": self._timer_action_combo_box.get_active_id(),
"afterevent": self._timer_after_combo_box.get_active_id(),
"repeated": TimerTool.get_repetition_flags(self._days_buttons)}
def __init__(self, app, *args, **kwargs):
super().__init__(*args, **kwargs)
self._app = app
self._app.connect("page-changed", self.update_timer_list)
# Icon.
theme = Gtk.IconTheme.get_default()
icon = "alarm-symbolic"
self._icon = theme.load_icon(icon, 16, 0) if theme.lookup_icon(icon, 16, 0) else None
handlers = {"on_timer_add": self.on_timer_add,
"on_timer_edit": self.on_timer_edit,
"on_timer_remove": self.on_timer_remove,
"on_model_changed": self.on_model_changed,
"on_timers_press": self.on_timers_press,
"on_timers_key_press": self.on_timers_key_press,
"on_timer_cursor_changed": self.on_timer_cursor_changed,
"on_timers_drag_data_received": self.on_timers_drag_data_received}
builder = get_builder(f"{UI_RESOURCES_PATH}timers.glade", handlers,
objects=("timers_frame", "timer_model", "popup_menu", "popup_menu_add_image"))
self._view = builder.get_object("timer_view")
self._remove_button = builder.get_object("timer_remove_button")
self._remove_button.bind_property("sensitive", builder.get_object("timer_edit_button"), "sensitive")
self._remove_button.bind_property("sensitive", builder.get_object("edit_menu_item"), "sensitive")
self._remove_button.bind_property("sensitive", builder.get_object("remove_menu_item"), "sensitive")
self._info_button = builder.get_object("timer_info_check_button")
self._info_button.bind_property("active", builder.get_object("timer_info_frame"), "visible")
self._info_enabled_switch = builder.get_object("timer_info_enabled_switch")
self._timers_count_label = builder.get_object("timers_count_label")
self._ref_info_label = builder.get_object("timer_ref_value_label")
self._event_id_info_label = builder.get_object("timer_event_id_value_label")
self._begins_info_label = builder.get_object("timer_begins_value_label")
self._ends_info_label = builder.get_object("timer_ends_value_label")
self._action_info_label = builder.get_object("timer_action_value_label")
self._after_info_label = builder.get_object("timer_after_value_label")
self._timer_location_switch = builder.get_object("timer_location_switch")
self._info_location_entry = builder.get_object("timer_info_location_entry")
self._days_buttons = (builder.get_object("timer_info_mo_check_button"),
builder.get_object("timer_info_tu_check_button"),
builder.get_object("timer_info_we_check_button"),
builder.get_object("timer_info_th_check_button"),
builder.get_object("timer_info_fr_check_button"),
builder.get_object("timer_info_sa_check_button"),
builder.get_object("timer_info_su_check_button"))
# Disable button presses.
list(map(lambda b: b.connect("button-press-event", lambda bx, e: True), self._days_buttons))
self._info_enabled_switch.connect("button-press-event", lambda b, e: True)
# DnD initialization for the timer list.
self._view.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
self._view.drag_dest_add_text_targets()
self.pack_start(builder.get_object("timers_frame"), True, True, 0)
self.show()
def update_timer_list(self, app, page):
if page is Page.TIMERS:
self._app.wait_dialog.show()
self._app.send_http_request(HttpAPI.Request.TIMER_LIST, "", self.update_timers_data)
@run_idle
def update_timers_data(self, timers):
model = self._view.get_model()
model.clear()
list(map(model.append, (self.get_timer_row(t) for t in timers.get("timer_list", []))))
self._remove_button.set_sensitive(len(model))
self._app.wait_dialog.hide()
def get_timer_row(self, timer):
disabled = self._icon if timer.get("e2disabled", "0") == "0" else None
name = timer.get("e2name", "") or ""
description = timer.get("e2description", "") or ""
service = timer.get("e2servicename", "") or ""
start_time = datetime.fromtimestamp(int(timer.get("e2timebegin", "0")))
end_time = datetime.fromtimestamp(int(timer.get("e2timeend", "0")))
time = f"{start_time.strftime('%a, %x, %H:%M')} - {end_time.strftime('%H:%M')}"
return disabled, name, service, time, description, timer
def on_timer_add(self, timer=None, value=None):
model, paths = self._app.fav_view.get_selection().get_selected_rows()
p_count = len(paths)
if p_count == 1:
service = self._app.current_services.get(model[paths][Column.FAV_ID], None)
if service:
self.add_timer({"e2servicename": service.service,
"e2servicereference": service.picon_id.rstrip(".png").replace("_", ":")})
elif p_count > 1:
self._app.show_error_message("Please, select only one item!")
else:
self._app.show_error_message("No selected item!")
def add_timer(self, timer_data):
dialog = self.TimerDialog(self._app.app_window, self.TimerAction.ADD, timer_data)
response = dialog.run()
if response == Gtk.ResponseType.OK:
self._app.send_http_request(HttpAPI.Request.TIMER, dialog.request, self.timer_add_edit_callback)
dialog.destroy()
def on_timer_edit(self, action=None, value=None):
model, paths = self._view.get_selection().get_selected_rows()
if len(paths) > 1:
self._app.show_error_message("Please, select only one item!")
return
dialog = self.TimerDialog(self._app.app_window, self.TimerAction.CHANGE, model[paths][-1])
response = dialog.run()
if response == Gtk.ResponseType.OK:
self._app.send_http_request(HttpAPI.Request.TIMER, dialog.request, self.timer_add_edit_callback)
dialog.destroy()
@run_idle
def timer_add_edit_callback(self, resp):
if "error_code" in resp:
msg = f"Error getting timer status.\n{resp.get('error_code')}"
self._app.show_error_message(msg)
log(msg)
return
state = resp.get("e2state", None)
if state == "False":
msg = resp.get("e2statetext", "")
self._app.show_error_message(msg)
log(msg)
if state == "True":
msg = resp.get("e2statetext", "")
log(msg)
self._app.show_info_message(msg, Gtk.MessageType.INFO)
self.update_timer_list(self._app, Page.TIMERS)
else:
log("Error getting timer status. No response!")
def on_timer_remove(self, action=None, value=None):
model, paths = self._view.get_selection().get_selected_rows()
if not paths or show_dialog(DialogType.QUESTION, self._app.app_window) != Gtk.ResponseType.OK:
return
refs = {}
for path in paths:
timer = model[path][-1]
ref = "timerdelete?sRef={}&begin={}&end={}".format(quote(timer.get("e2servicereference", "")),
timer.get("e2timebegin", ""),
timer.get("e2timeend", ""))
refs[ref] = model.get_iter(path)
self._app.wait_dialog.show("Deleting data...")
gen = self.remove_timers(refs)
GLib.idle_add(lambda: next(gen, False))
def remove_timers(self, refs):
tasks = list(refs)
removed = set()
for ref in refs:
yield from self.remove_timer(ref, removed, tasks)
while tasks:
yield True
model = self._view.get_model()
list(map(model.remove, (refs[ref] for ref in refs if ref in removed)))
self._app.wait_dialog.hide()
self._remove_button.set_sensitive(len(model))
yield True
def remove_timer(self, ref, removed, tasks=None):
def callback(resp):
if resp.get("e2state", "") == "True":
log(resp.get("e2statetext", ""))
removed.add(ref)
else:
log(resp.get("e2statetext", None) or "Timer deletion error.")
if tasks:
tasks.pop()
self._app.send_http_request(HttpAPI.Request.TIMER, ref, callback)
yield True
def on_model_changed(self, model, path, itr=None):
self._timers_count_label.set_text(str(len(model)))
def on_timers_press(self, menu, event):
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(self._view.get_model()) > 0:
self.on_timer_edit()
else:
on_popup_menu(menu, event)
def on_timers_key_press(self, view, event):
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK
if key is KeyboardKey.DELETE:
self.on_timer_remove()
elif key is KeyboardKey.INSERT:
self.on_timer_add()
elif ctrl and key is KeyboardKey.E:
self.on_timer_edit()
def on_timer_cursor_changed(self, view):
path, column = view.get_cursor()
if not path:
return
timer = view.get_model()[path][-1]
self._info_enabled_switch.set_active((timer.get("e2disabled", "0") == "0"))
self._ref_info_label.set_text(timer.get("e2servicereference", ""))
self._event_id_info_label.set_text(timer.get("e2eit", ""))
self._action_info_label.set_text(get_message(self.ACTION.get(timer.get("e2justplay", "0"), "0")))
self._after_info_label.set_text(get_message(self.AFTER_EVENT.get(timer.get("e2afterevent", "0"), "0")))
self._begins_info_label.set_text(str(datetime.fromtimestamp(int(timer.get("e2timebegin", "0")))))
self._ends_info_label.set_text(str(datetime.fromtimestamp(int(timer.get("e2timeend", "0")))))
self.set_repetition_flags(int(timer.get("e2repeated", "0")), self._days_buttons)
location = timer.get("e2location", "")
self._info_location_entry.set_text("" if location == "None" else location)
@staticmethod
def get_repetition_flags(boxes):
""" Returns flags for repetition.
@param boxes: Buttons tuple for the days of the week.
"""
day_flags = 0
for i, box in enumerate(boxes):
if box.get_active():
day_flags = day_flags | (1 << i)
return day_flags
@staticmethod
def set_repetition_flags(flags, boxes):
""" Sets flags for repetition.
@param flags: Flags value.
@param boxes: Buttons tuple for the days of the week.
"""
for i, box in enumerate(boxes):
box.set_active(flags & 1 == 1)
flags = flags >> 1
# ***************** Drag-and-drop ********************* #
def on_timers_drag_data_received(self, box, context, x, y, data, info, time):
txt = data.get_text()
if txt:
itr_str, sep, source = txt.partition(self._app.DRAG_SEP)
if not source:
return
itrs = itr_str.split(",")
if len(itrs) > 1:
self._app.show_error_message("Please, select only one item!")
return
fav_id = None
if source == self._app.FAV_MODEL:
model = self._app.fav_view.get_model()
fav_id = model.get_value(model.get_iter_from_string(itrs[0]), Column.FAV_ID)
elif source == self._app.SERVICE_MODEL:
model = self._app.services_view.get_model()
fav_id = model.get_value(model.get_iter_from_string(itrs[0]), Column.SRV_FAV_ID)
service = self._app.current_services.get(fav_id, None)
if service:
if service.service_type == BqServiceType.ALT.name:
msg = "Alternative service.\n\n {get_message('Not implemented yet!')}"
show_dialog(DialogType.ERROR, transient=self._app.app_window, text=msg)
context.finish(False, False, time)
return
self.add_timer({"e2servicename": service.service,
"e2servicereference": service.picon_id.rstrip(".png").replace("_", ":")})
context.finish(True, False, time)
if __name__ == "__main__":
pass

View File

@@ -42,6 +42,7 @@ Author: Dmitriy Yefremov
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<property name="decorated">False</property>
<property name="gravity">center</property>
<property name="has_resize_grip">True</property>
<child>
<placeholder/>

View File

@@ -6,7 +6,6 @@ from gi.repository import GLib
from app.commons import log
from app.connections import HttpAPI
from app.settings import IS_WIN
from app.tools.yt import YouTube
from app.ui.dialogs import get_builder
from app.ui.iptv import get_yt_icon
@@ -14,7 +13,7 @@ from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH
class LinksTransmitter:
""" The main media bar class for the "send to" function..
""" The main class for the "send to" function.
It used for direct playback of media links by the enigma2 media player.
"""
@@ -47,19 +46,16 @@ class LinksTransmitter:
self._status_passive = None
self._yt = YouTube.get_instance(settings)
if IS_WIN:
try:
gi.require_version("AppIndicator3", "0.1")
from gi.repository import AppIndicator3
except (ImportError, ValueError) as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
self._tray = builder.get_object("status_icon")
else:
try:
gi.require_version("AppIndicator3", "0.1")
from gi.repository import AppIndicator3
except (ImportError, ValueError) as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
self._tray = builder.get_object("status_icon")
else:
self._is_status_icon = False
self._status_active = AppIndicator3.IndicatorStatus.ACTIVE
self._status_passive = AppIndicator3.IndicatorStatus.PASSIVE
self._is_status_icon = False
self._status_active = AppIndicator3.IndicatorStatus.ACTIVE
self._status_passive = AppIndicator3.IndicatorStatus.PASSIVE
category = AppIndicator3.IndicatorCategory.APPLICATION_STATUS
path = Path(UI_RESOURCES_PATH + "/icons/hicolor/scalable/apps/demon-editor.svg")

View File

@@ -1,63 +1,127 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
import locale
import os
from enum import Enum, IntEnum
from functools import lru_cache
from app.settings import Settings, SettingsException, IS_WIN, SEP
import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Gdk", "3.0")
from gi.repository import Gtk, Gdk, GLib
from app.settings import Settings, SettingsException, IS_DARWIN, GTK_PATH, IS_LINUX
# Setting mod mask for keyboard depending on platform
MOD_MASK = Gdk.ModifierType.CONTROL_MASK
# Path to *.glade files
UI_PATH = "app{}ui{}".format(SEP, SEP)
UI_RESOURCES_PATH = UI_PATH if os.path.exists(UI_PATH) else "ui{}".format(SEP)
LANG_PATH = UI_RESOURCES_PATH + "lang"
GTK_PATH = os.environ.get("GTK_PATH", None)
NOTIFY_IS_INIT = False
IS_GNOME_SESSION = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
MOD_MASK = Gdk.ModifierType.MOD2_MASK if IS_DARWIN else Gdk.ModifierType.CONTROL_MASK
# Paths.
BASE_PATH = "app/ui/"
EX_PATH = "/usr/share/demoneditor/app/ui/" if IS_LINUX else "ui/"
# Path to *.glade files.
UI_RESOURCES_PATH = BASE_PATH if os.path.exists(BASE_PATH) else EX_PATH
# Translation.
LANG_PATH = UI_RESOURCES_PATH + "lang"
TEXT_DOMAIN = "demon-editor"
NOTIFY_IS_INIT = False
APP_FONT = None
IS_GNOME_SESSION = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
try:
settings = Settings.get_instance()
except SettingsException:
pass
else:
locale.setlocale(locale.LC_NUMERIC, "C")
os.environ["LANGUAGE"] = settings.language
st = Gtk.Settings().get_default()
APP_FONT = st.get_property("gtk-font-name")
st.set_property("gtk-application-prefer-dark-theme", settings.dark_mode)
if settings.is_themes_support:
st.set_property("gtk-theme-name", settings.theme)
st.set_property("gtk-icon-theme-name", settings.icon_theme)
else:
if not IS_LINUX:
if IS_DARWIN:
s_path = f"{GTK_PATH + '/' + UI_RESOURCES_PATH if GTK_PATH else UI_RESOURCES_PATH}mac_style.css"
else:
s_path = f"{UI_RESOURCES_PATH}win_style.css"
theme = Gtk.IconTheme.get_default()
theme.append_search_path(GTK_PATH + "{}share{}icons".format(SEP, SEP) if GTK_PATH else UI_RESOURCES_PATH + "icons")
style_provider = Gtk.CssProvider()
style_provider.load_from_path(s_path)
Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
def get_theme_icon(icon_theme, name, size):
if IS_LINUX:
if UI_RESOURCES_PATH == BASE_PATH:
locale.bindtextdomain(TEXT_DOMAIN, LANG_PATH)
# Init notify
try:
return icon_theme.load_icon(name, size, 0)
gi.require_version("Notify", "0.7")
from gi.repository import Notify
except (ImportError, ValueError):
pass # NOP
else:
NOTIFY_IS_INIT = Notify.init("DemonEditor")
elif IS_DARWIN:
import gettext
if GTK_PATH:
LANG_PATH = GTK_PATH + "/share/locale"
gettext.bindtextdomain(TEXT_DOMAIN, LANG_PATH)
# For launching from the bundle.
if os.getcwd() == "/" and GTK_PATH:
os.chdir(GTK_PATH)
else:
locale.setlocale(locale.LC_NUMERIC, "C")
# Icons.
theme = Gtk.IconTheme.get_default()
theme.append_search_path(UI_RESOURCES_PATH + "icons")
def get_icon(name, size, default=None):
try:
return theme.load_icon(name, size, 0) if theme.lookup_icon(name, size, 0) else default
except GLib.Error:
pass
return default
_IMAGE_MISSING = get_theme_icon(theme, "image-missing", 16)
CODED_ICON = get_theme_icon(theme, "emblem-readonly", 16) or _IMAGE_MISSING
LOCKED_ICON = get_theme_icon(theme, "changes-prevent-symbolic", 16) or _IMAGE_MISSING
HIDE_ICON = get_theme_icon(theme, "go-jump", 16) or _IMAGE_MISSING
TV_ICON = get_theme_icon(theme, "tv-symbolic", 16) or _IMAGE_MISSING
IPTV_ICON = get_theme_icon(theme, "emblem-shared", 16)
EPG_ICON = get_theme_icon(theme, "gtk-index", 16)
DEFAULT_ICON = get_theme_icon(theme, "emblem-default", 16)
_IMAGE_MISSING = get_icon("image-missing", 16)
CODED_ICON = get_icon("emblem-readonly", 16, _IMAGE_MISSING)
LOCKED_ICON = get_icon("changes-prevent-symbolic", 16, _IMAGE_MISSING)
HIDE_ICON = get_icon("go-jump", 16, _IMAGE_MISSING)
TV_ICON = get_icon("tv-symbolic", 16, _IMAGE_MISSING)
IPTV_ICON = get_icon("emblem-shared", 16, _IMAGE_MISSING)
EPG_ICON = get_icon("gtk-index", 16, _IMAGE_MISSING)
DEFAULT_ICON = get_icon("emblem-default", 16, _IMAGE_MISSING)
@lru_cache(maxsize=1)
@@ -71,17 +135,14 @@ def get_yt_icon(icon_name, size=24):
return default_theme.load_icon(icon_name, size, 0)
n_theme = Gtk.IconTheme.new()
p_path = "{}usr{}share{}icons{}*".format(SEP, SEP, SEP, SEP)
import glob
for theme_name in map(os.path.basename, filter(os.path.isdir, glob.glob(p_path))):
theme.set_custom_theme(theme_name)
for theme_name in map(os.path.basename, filter(os.path.isdir, glob.glob("/usr/share/icons/*"))):
n_theme.set_custom_theme(theme_name)
if n_theme.has_icon(icon_name):
return n_theme.load_icon(icon_name, size, 0)
if default_theme.lookup_icon(Gtk.STOCK_APPLY, size, 0):
return default_theme.load_icon(Gtk.STOCK_APPLY, size, 0)
return default_theme.load_icon("emblem-important-symbolic", size, 0)
def show_notification(message, timeout=10000, urgency=1):
@@ -91,52 +152,27 @@ def show_notification(message, timeout=10000, urgency=1):
@param timeout: milliseconds
@param urgency: 0 - low, 1 - normal, 2 - critical
"""
pass
if IS_DARWIN:
# Since NSUserNotification has been deprecated, osascript will be used.
os.system("""osascript -e 'display notification "{}" with title "DemonEditor"'""".format(message))
elif NOTIFY_IS_INIT:
notify = Notify.Notification.new("DemonEditor", message, "demon-editor")
notify.set_urgency(urgency)
notify.set_timeout(timeout)
notify.show()
class KeyboardKey(Enum):
""" The raw(hardware) codes of the keyboard keys. """
E = 69 if IS_WIN else 26
R = 82 if IS_WIN else 27
T = 84 if IS_WIN else 28
P = 80 if IS_WIN else 33
S = 83 if IS_WIN else 39
F = 70 if IS_WIN else 41
X = 88 if IS_WIN else 53
C = 67 if IS_WIN else 54
V = 86 if IS_WIN else 55
W = 87 if IS_WIN else 25
Z = 90 if IS_WIN else 52
INSERT = 45 if IS_WIN else 118
HOME = 36 if IS_WIN else 110
END = 35 if IS_WIN else 115
UP = 38 if IS_WIN else 111
DOWN = 40 if IS_WIN else 116
PAGE_UP = 33 if IS_WIN else 112
PAGE_DOWN = 34 if IS_WIN else 117
LEFT = 37 if IS_WIN else 113
RIGHT = 39 if IS_WIN else 114
F2 = 113 if IS_WIN else 68
F7 = 118 if IS_WIN else 73
SPACE = 32 if IS_WIN else 65
DELETE = 46 if IS_WIN else 119
BACK_SPACE = 8 if IS_WIN else 22
CTRL_L = 17 if IS_WIN else 37
CTRL_R = 163 if IS_WIN else 105
# Laptop codes
HOME_KP = 79
END_KP = 87
PAGE_UP_KP = 81
PAGE_DOWN_KP = 89
@classmethod
def value_exist(cls, value):
return value in (val.value for val in cls.__members__.values())
# Keys for move in lists. KEY_KP_(NAME) for laptop!!!
MOVE_KEYS = (KeyboardKey.UP, KeyboardKey.PAGE_UP, KeyboardKey.DOWN, KeyboardKey.PAGE_DOWN, KeyboardKey.HOME,
KeyboardKey.END, KeyboardKey.HOME_KP, KeyboardKey.END_KP, KeyboardKey.PAGE_UP_KP, KeyboardKey.PAGE_DOWN_KP)
class Page(Enum):
""" Main stack widget page. """
INFO = "info"
SERVICES = "services"
SATELLITE = "satellite"
PICONS = "picons"
EPG = "epg"
TIMERS = "timers"
RECORDINGS = "recordings"
FTP = "ftp"
CONTROL = "control"
class FavClickMode(IntEnum):
@@ -153,6 +189,7 @@ class ViewTarget(Enum):
BOUQUET = 0
FAV = 1
SERVICES = 2
IPTV = 3
class BqGenType(Enum):
@@ -216,11 +253,161 @@ class Column(IntEnum):
ALT_FAV_ID = 5
ALT_ID = 6
ALT_ITER = 7
# Recordings view
REC_SERVICE = 0
REC_TITLE = 1
REC_TIME = 2
REC_LEN = 3
REC_FILE = 4
REC_DESC = 5
# IPTV view
IPTV_SERVICE = 0
IPTV_TYPE = 1
IPTV_PICON = 2
IPTV_REF = 3
IPTV_URL = 4
IPTV_FAV_ID = 5
IPTV_PICON_ID = 6
IPTV_TOOLTIP = 7
def __index__(self):
""" Overridden to get the index in slices directly """
return self.value
# *************** Keyboard keys *************** #
class BaseKeyboardKey(Enum):
@classmethod
def value_exist(cls, value):
return value in (val.value for val in cls.__members__.values())
if IS_LINUX:
class KeyboardKey(BaseKeyboardKey):
""" The raw(hardware) codes [Linux] of the keyboard keys. """
E = 26
R = 27
T = 28
P = 33
S = 39
F = 41
X = 53
C = 54
V = 55
W = 25
Z = 52
INSERT = 118
HOME = 110
END = 115
UP = 111
DOWN = 116
PAGE_UP = 112
PAGE_DOWN = 117
LEFT = 113
RIGHT = 114
F2 = 68
F4 = 70
F5 = 71
F7 = 73
SPACE = 65
DELETE = 119
BACK_SPACE = 22
RETURN = 36
CTRL_L = 37
CTRL_R = 105
# Laptop codes
HOME_KP = 79
END_KP = 87
PAGE_UP_KP = 81
PAGE_DOWN_KP = 89
elif IS_DARWIN:
class KeyboardKey(BaseKeyboardKey):
""" The raw(hardware) codes [macOS] of the keyboard keys. """
F = 3
E = 14
R = 15
T = 17
P = 35
S = 1
H = 4
L = 37
X = 7
C = 8
V = 9
W = 13
Z = 6
INSERT = -1
HOME = -1
END = -1
UP = 126
DOWN = 125
PAGE_UP = -1
PAGE_DOWN = -1
LEFT = 123
RIGHT = 123
F2 = 120
F4 = 118
F5 = 96
F7 = 98
SPACE = 49
DELETE = 51
BACK_SPACE = 76
RETURN = 36
CTRL_L = 55
CTRL_R = 55
# Laptop codes.
HOME_KP = -1
END_KP = -1
PAGE_UP_KP = -1
PAGE_DOWN_KP = -1
else:
class KeyboardKey(BaseKeyboardKey):
""" The raw(hardware) codes [Windows] of the keyboard keys. """
E = 69
R = 82
T = 84
P = 80
S = 83
F = 70
X = 88
C = 67
V = 86
W = 87
Z = 90
INSERT = 45
HOME = 36
END = 35
UP = 38
DOWN = 40
PAGE_UP = 33
PAGE_DOWN = 34
LEFT = 37
RIGHT = 39
F2 = 113
F4 = 115
F5 = 116
F7 = 118
SPACE = 32
DELETE = 46
BACK_SPACE = 8
RETURN = 13
CTRL_L = 17
CTRL_R = 163
# Laptop codes.
HOME_KP = -1
END_KP = -1
PAGE_UP_KP = -1
PAGE_DOWN_KP = -1
# Keys for move in lists. KEY_KP_(NAME) for laptop!
MOVE_KEYS = {KeyboardKey.UP, KeyboardKey.PAGE_UP,
KeyboardKey.DOWN, KeyboardKey.PAGE_DOWN,
KeyboardKey.HOME, KeyboardKey.END,
KeyboardKey.HOME_KP, KeyboardKey.END_KP,
KeyboardKey.PAGE_UP_KP, KeyboardKey.PAGE_DOWN_KP}
if __name__ == "__main__":
pass

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

@@ -0,0 +1,24 @@
* {
-GtkDialog-action-area-border: 12;
}
switch {
margin-right: 2px;
}
spinbutton entry {
min-height: 16px;
}
button > image {
padding: 2px;
}
grid > button {
padding-left: 15px;
padding-right: 15px;
}
popover .view {
background-color: transparent;
}

0
app/ui/xml/__init__.py Normal file
View File

984
app/ui/xml/dialogs.glade Normal file
View File

@@ -0,0 +1,984 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2
The MIT License (MIT)
Copyright (c) 2018-2022 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface>
<requires lib="gtk+" version="3.20"/>
<!-- interface-css-provider-path style.css -->
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkGrid" id="cable_tr_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="column_spacing">5</property>
<child>
<object class="GtkLabel" id="cable_freq_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Freq</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="cable_rate_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Rate</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="cable_rate_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="width_chars">12</property>
<property name="max_width_chars">14</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="placeholder_text">6900000</property>
<property name="input_purpose">digits</property>
<signal name="changed" handler="on_entry_changed" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="cable_fec_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">FEC</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="cable_mod_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Mod</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="cable_freq_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="width_chars">12</property>
<property name="max_width_chars">14</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="secondary_icon_sensitive">False</property>
<property name="placeholder_text">120000</property>
<property name="input_purpose">digits</property>
<signal name="changed" handler="on_entry_changed" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="cable_fec_box">
<property name="width_request">75</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="cable_mod_box">
<property name="width_request">100</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">1</property>
</packing>
</child>
</object>
<object class="GtkListStore" id="fec_store">
<columns>
<!-- column-name fec -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0">1/2</col>
</row>
<row>
<col id="0">2/3</col>
</row>
<row>
<col id="0">3/4</col>
</row>
<row>
<col id="0">5/6</col>
</row>
<row>
<col id="0">6/7</col>
</row>
<row>
<col id="0">7/8</col>
</row>
<row>
<col id="0">8/9</col>
</row>
<row>
<col id="0" translatable="yes">3/5</col>
</row>
<row>
<col id="0" translatable="yes">4/5</col>
</row>
<row>
<col id="0" translatable="yes">9/10</col>
</row>
<row>
<col id="0" translatable="yes">Auto</col>
</row>
</data>
</object>
<object class="GtkListStore" id="mod_store">
<columns>
<!-- column-name mod -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0">Auto</col>
</row>
<row>
<col id="0">QPSK</col>
</row>
<row>
<col id="0">8PSK</col>
</row>
<row>
<col id="0">16APSK</col>
</row>
<row>
<col id="0">32APSK</col>
</row>
</data>
</object>
<object class="GtkListStore" id="pls_mode_store">
<columns>
<!-- column-name pls_mode -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0">Root</col>
</row>
<row>
<col id="0">Gold</col>
</row>
<row>
<col id="0">Combo</col>
</row>
</data>
</object>
<object class="GtkListStore" id="pol_store">
<columns>
<!-- column-name pol -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0">H</col>
</row>
<row>
<col id="0">V</col>
</row>
<row>
<col id="0">R</col>
</row>
<row>
<col id="0">L</col>
</row>
</data>
</object>
<object class="GtkAdjustment" id="pos_adjustment">
<property name="upper">180</property>
<property name="step_increment">0.10000000000000001</property>
<property name="page_increment">10</property>
</object>
<object class="GtkListStore" id="side_store">
<columns>
<!-- column-name side -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0">E</col>
</row>
<row>
<col id="0">W</col>
</row>
</data>
</object>
<object class="GtkGrid" id="sat_dialog_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="column_spacing">5</property>
<child>
<object class="GtkLabel" id="label11">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Name</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label12">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Position</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="sat_name_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="secondary_icon_sensitive">False</property>
<property name="placeholder_text" translatable="yes">satellite name</property>
<property name="input_purpose">name</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="sat_position_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="width_chars">5</property>
<property name="secondary_icon_activatable">False</property>
<property name="secondary_icon_sensitive">False</property>
<property name="input_purpose">number</property>
<property name="adjustment">pos_adjustment</property>
<property name="digits">1</property>
<property name="numeric">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="side_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="model">side_store</property>
<property name="active">0</property>
<property name="id_column">0</property>
<child>
<object class="GtkCellRendererText" id="side_cellrenderertext"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<object class="GtkListStore" id="system_store">
<columns>
<!-- column-name system -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0">DVB-S</col>
</row>
<row>
<col id="0">DVB-S2</col>
</row>
</data>
</object>
<object class="GtkBox" id="sat_tr_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkGrid" id="sat_tr_dialog_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="column_spacing">5</property>
<child>
<object class="GtkLabel" id="sat_freq_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Freq</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="sat_rate_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Rate</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="sat_pol_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Pol</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="sat_fec_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">FEC</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="sat_sys_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">System</property>
</object>
<packing>
<property name="left_attach">4</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="sat_mod_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Mod</property>
</object>
<packing>
<property name="left_attach">5</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="freq_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="width_chars">12</property>
<property name="max_width_chars">14</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="secondary_icon_sensitive">False</property>
<property name="placeholder_text">11700000</property>
<property name="input_purpose">digits</property>
<signal name="changed" handler="on_entry_changed" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="rate_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="width_chars">12</property>
<property name="max_width_chars">14</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="placeholder_text">27500000</property>
<property name="input_purpose">digits</property>
<signal name="changed" handler="on_entry_changed" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="pol_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="model">pol_store</property>
<property name="id_column">0</property>
<child>
<object class="GtkCellRendererText" id="pol_cellrenderertext"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="fec_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="model">fec_store</property>
<property name="id_column">0</property>
<child>
<object class="GtkCellRendererText" id="cellrenderertext4"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="sys_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="model">system_store</property>
<property name="id_column">0</property>
<child>
<object class="GtkCellRendererText" id="cellrenderertext5"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="left_attach">4</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="mod_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="model">mod_store</property>
<property name="id_column">0</property>
<child>
<object class="GtkCellRendererText" id="cellrenderertext6"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="left_attach">5</property>
<property name="top_attach">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkExpander" id="expander">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="resize_toplevel">True</property>
<child>
<object class="GtkGrid" id="tr_dialog_grid2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="column_spacing">5</property>
<child>
<object class="GtkLabel" id="tr_pls_mode_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Pls mode</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="tr_pls_code_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Pls code</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="id_id_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Is ID</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="pls_mode_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="model">pls_mode_store</property>
<property name="id_column">0</property>
<child>
<object class="GtkCellRendererText" id="pls_mode_cellrenderertext1"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="pls_code_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="width_chars">5</property>
<property name="max_width_chars">12</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="placeholder_text" translatable="yes">0 - 262142</property>
<property name="input_purpose">digits</property>
<signal name="changed" handler="on_entry_changed" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="is_id_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="width_chars">5</property>
<property name="max_width_chars">12</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="placeholder_text" translatable="yes">0 - 255</property>
<property name="input_purpose">digits</property>
<signal name="changed" handler="on_entry_changed" swapped="no"/>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="tr_t2mi_plp_id_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">T2-MI PLP ID</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="t2mi_plp_id_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="width_chars">5</property>
<property name="max_width_chars">12</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="placeholder_text" translatable="yes">0 - 255</property>
<property name="input_purpose">digits</property>
<signal name="changed" handler="on_entry_changed" swapped="no"/>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">1</property>
</packing>
</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">4</property>
</packing>
</child>
</object>
<object class="GtkBox" id="ter_tr_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkGrid" id="ter_tr_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="column_spacing">5</property>
<child>
<object class="GtkLabel" id="ter_freq_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Freq</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="ter_freq_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="width_chars">12</property>
<property name="max_width_chars">14</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property>
<property name="secondary_icon_sensitive">False</property>
<property name="placeholder_text">170000000</property>
<property name="input_purpose">digits</property>
<signal name="changed" handler="on_entry_changed" swapped="no"/>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ter_sys_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">System</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="ter_sys_box">
<property name="width_request">100</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="ter_bandwidth_box">
<property name="width_request">110</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ter_bandwidth_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Bandwidth</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ter_constellation_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Constellation</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ter_sr_hp_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">SR (HP)</property>
</object>
<packing>
<property name="left_attach">4</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ter_sr_lp_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">SR (LP)</property>
</object>
<packing>
<property name="left_attach">5</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="ter_sr_hp_box">
<property name="width_request">75</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="left_attach">4</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="ter_sr_lp_box">
<property name="width_request">75</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="left_attach">5</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="ter_constellation_box">
<property name="width_request">100</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="left_attach">3</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="GtkGrid" id="ter_tr_ext_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_bottom">5</property>
<property name="column_spacing">5</property>
<property name="column_homogeneous">True</property>
<child>
<object class="GtkLabel" id="ter_guard_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Guard</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ter_transmission_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Transmission</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ter_hierarchy_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Hierarchy</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ter_inversion_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Inversion</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="ter_guard_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="ter_transmission_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="ter_hierarchy_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="ter_inversion_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="ter_plp_id_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="width_chars">8</property>
<property name="max_width_chars">8</property>
<property name="primary_icon_name">document-edit-symbolic</property>
<property name="placeholder_text">0-255</property>
<property name="input_purpose">digits</property>
<signal name="changed" handler="on_entry_changed" swapped="no"/>
</object>
<packing>
<property name="left_attach">4</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="ter_plp_id_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">PLP ID</property>
</object>
<packing>
<property name="left_attach">4</property>
<property name="top_attach">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>

915
app/ui/xml/dialogs.py Normal file
View File

@@ -0,0 +1,915 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
import concurrent.futures
import os
import re
import time
from math import fabs
from gi.repository import GLib
from app.commons import run_idle, run_task, log
from app.eparser import Satellite, Transponder
from app.eparser.ecommons import (PLS_MODE, get_key_by_value, POLARIZATION, FEC, SYSTEM, MODULATION, Terrestrial, Cable,
T_SYSTEM, BANDWIDTH, CONSTELLATION, T_FEC, GUARD_INTERVAL, TRANSMISSION_MODE,
HIERARCHY, Inversion, C_MODULATION, FEC_DEFAULT, TerTransponder, CableTransponder)
from app.tools.satellites import SatellitesParser, SatelliteSource, ServicesParser
from ..dialogs import show_dialog, DialogType, get_message, get_builder
from ..main_helper import append_text_to_tview, get_base_model, on_popup_menu
from ..search import SearchProvider
from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, IS_GNOME_SESSION
_DIALOGS_UI_PATH = f"{UI_RESOURCES_PATH}xml{os.sep}dialogs.glade"
class DVBDialog(Gtk.Dialog):
""" Base dialog class for editing DVB (-> *.xml) data. """
def __init__(self, parent, title, data=None, *args, **kwargs):
super().__init__(transient_for=parent,
title=get_message(title),
modal=True,
resizable=False,
default_width=320,
skip_taskbar_hint=True,
skip_pager_hint=True,
destroy_with_parent=True,
use_header_bar=IS_GNOME_SESSION,
window_position=Gtk.WindowPosition.CENTER_ON_PARENT,
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK),
*args, **kwargs)
self.frame = Gtk.Frame(margin=5, label_xalign=0.02)
self.get_content_area().pack_start(self.frame, True, True, 0)
self._data = data
@property
def data(self):
return self._data
class TransponderDialog(DVBDialog):
""" Base transponder dialog class. """
def __init__(self, parent, title, data=None, *args, **kwargs):
super().__init__(parent, title, data, *args, **kwargs)
self.frame.set_label(get_message("Transponder properties:"))
# Pattern for digits entries.
self.digit_pattern = re.compile(r"\D")
# Style
self.style_provider = Gtk.CssProvider()
self.style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
def run(self):
resp = super().run()
while resp == Gtk.ResponseType.OK:
if self.is_accept():
return resp
show_dialog(DialogType.ERROR, self, "Please check your parameters and try again.")
resp = super().run()
return resp
def is_accept(self):
return True
def init_transponder_data(self, data):
self._data = data
def to_transponder(self):
return self.data
def on_entry_changed(self, entry):
""" Digit entries handler. """
entry.set_name("digit-entry" if self.digit_pattern.search(entry.get_text()) else "GtkEntry")
def set_style_provider(self, widget):
context = widget.get_style_context()
context.add_provider_for_screen(Gdk.Screen.get_default(), self.style_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
class TCDialog(DVBDialog):
def __init__(self, parent, title=None, data=None, *args, **kwargs):
super().__init__(parent, title, data, *args, **kwargs)
self._entry = Gtk.Entry(margin=5)
self.frame.add(self._entry)
self.frame.set_label(get_message("Name:"))
self.show_all()
if data:
self._entry.set_text(data.name)
class SatelliteDialog(DVBDialog):
""" Dialog for adding or edit satellite. """
def __init__(self, transient, title, satellite=None, *args, **kwargs):
super().__init__(transient, title, *args, **kwargs)
builder = get_builder(_DIALOGS_UI_PATH, use_str=True,
objects=("sat_dialog_box", "side_store", "pos_adjustment"))
self.frame.add(builder.get_object("sat_dialog_box"))
self.frame.set_label(get_message("Satellite properties:"))
self._sat_name = builder.get_object("sat_name_entry")
self._sat_position = builder.get_object("sat_position_button")
self._side = builder.get_object("side_box")
self._transponders = satellite.transponders if satellite else []
self.show_all()
if satellite:
self._sat_name.set_text(satellite.name)
pos = satellite.position
pos = float(f"{pos[:-1]}.{pos[-1:]}")
self._sat_position.set_value(fabs(pos))
self._side.set_active(0 if pos >= 0 else 1) # E or W
@property
def data(self):
return self.to_satellite()
def to_satellite(self):
name = self._sat_name.get_text()
pos = round(self._sat_position.get_value(), 1)
side = self._side.get_active()
pos = "{}{}{}".format("-" if side == 1 else "", *str(pos).split("."))
return Satellite(name=name, flags="0", position=pos, transponders=self._transponders)
class TerrestrialDialog(TCDialog):
""" Dialog for adding or edit terrestrial region. """
@property
def data(self):
name = self._entry.get_text()
return self._data._replace(name=name) if self._data else Terrestrial(name, "5", None, [])
class CableDialog(TCDialog):
""" Dialog for adding or edit cable provider. """
@property
def data(self):
name = self._entry.get_text()
return self._data._replace(name=name) if self._data else Cable(name, "true", "9", None, [])
class SatTransponderDialog(TransponderDialog):
""" Dialog for adding or edit satellite transponder. """
def __init__(self, transient, title, data=None, *args, **kwargs):
super().__init__(transient, title, data, *args, **kwargs)
handlers = {"on_entry_changed": self.on_entry_changed}
objects = ("sat_tr_box", "pol_store", "fec_store", "mod_store", "system_store", "pls_mode_store")
builder = get_builder(_DIALOGS_UI_PATH, handlers, use_str=True, objects=objects)
self.frame.add(builder.get_object("sat_tr_box"))
self._freq_entry = builder.get_object("freq_entry")
self._rate_entry = builder.get_object("rate_entry")
self._pol_box = builder.get_object("pol_box")
self._fec_box = builder.get_object("fec_box")
self._sys_box = builder.get_object("sys_box")
self._mod_box = builder.get_object("mod_box")
self._pls_mode_box = builder.get_object("pls_mode_box")
self._pls_code_entry = builder.get_object("pls_code_entry")
self._is_id_entry = builder.get_object("is_id_entry")
self._t2mi_plp_id_entry = builder.get_object("t2mi_plp_id_entry")
self.set_style_provider(self._freq_entry)
self.set_style_provider(self._rate_entry)
self.show_all()
self.init_transponder_data(data)
@property
def data(self):
return self.to_transponder()
def init_transponder_data(self, transponder):
if transponder:
self._freq_entry.set_text(transponder.frequency)
self._rate_entry.set_text(transponder.symbol_rate)
self._pol_box.set_active_id(POLARIZATION.get(transponder.polarization, None))
self._fec_box.set_active_id(FEC.get(transponder.fec_inner, None))
self._sys_box.set_active_id(SYSTEM.get(transponder.system, None))
self._mod_box.set_active_id(MODULATION.get(transponder.modulation, None))
self._pls_mode_box.set_active_id(PLS_MODE.get(transponder.pls_mode, None))
self._is_id_entry.set_text(transponder.is_id if transponder.is_id else "")
self._pls_code_entry.set_text(transponder.pls_code if transponder.pls_code else "")
self._t2mi_plp_id_entry.set_text(transponder.t2mi_plp_id if transponder.t2mi_plp_id else "")
def to_transponder(self):
return Transponder(frequency=self._freq_entry.get_text(),
symbol_rate=self._rate_entry.get_text(),
polarization=get_key_by_value(POLARIZATION, self._pol_box.get_active_id()),
fec_inner=get_key_by_value(FEC, self._fec_box.get_active_id()),
system=get_key_by_value(SYSTEM, self._sys_box.get_active_id()),
modulation=get_key_by_value(MODULATION, self._mod_box.get_active_id()),
pls_mode=get_key_by_value(PLS_MODE, self._pls_mode_box.get_active_id()),
pls_code=self._pls_code_entry.get_text(),
is_id=self._is_id_entry.get_text(),
t2mi_plp_id=self._t2mi_plp_id_entry.get_text())
def is_accept(self):
tr = self.to_transponder()
if self.digit_pattern.search(tr.frequency) or not tr.frequency:
return False
elif self.digit_pattern.search(tr.symbol_rate) or not tr.symbol_rate:
return False
elif None in (tr.polarization, tr.fec_inner, tr.system, tr.modulation):
return False
elif self.digit_pattern.search(tr.pls_code) or self.digit_pattern.search(tr.is_id):
return False
elif self.digit_pattern.search(tr.t2mi_plp_id):
return False
return True
class TerTransponderDialog(TransponderDialog):
""" Dialog for adding or edit terrestrial transponder. """
def __init__(self, transient, title, data=None, *args, **kwargs):
super().__init__(transient, title, data, *args, **kwargs)
handlers = {"on_entry_changed": self.on_entry_changed}
builder = get_builder(_DIALOGS_UI_PATH, handlers, use_str=True, objects=("ter_tr_box",))
self.frame.add(builder.get_object("ter_tr_box"))
self._freq_entry = builder.get_object("ter_freq_entry")
self._sys_box = builder.get_object("ter_sys_box")
self._bandwidth_box = builder.get_object("ter_bandwidth_box")
self._constellation_box = builder.get_object("ter_constellation_box")
self._sr_hp_box = builder.get_object("ter_sr_hp_box")
self._sr_lp_box = builder.get_object("ter_sr_lp_box")
self._guard_box = builder.get_object("ter_guard_box")
self._transmission_box = builder.get_object("ter_transmission_box")
self._hierarchy_box = builder.get_object("ter_hierarchy_box")
self._inversion_box = builder.get_object("ter_inversion_box")
self._plp_id_entry = builder.get_object("ter_plp_id_entry")
self.set_style_provider(self._freq_entry)
self.set_style_provider(self._plp_id_entry)
self.show_all()
self.init_transponder_data(data)
@property
def data(self):
return self.to_transponder()
def init_transponder_data(self, transponder):
[self._sys_box.append(k, v) for k, v in T_SYSTEM.items()]
[self._bandwidth_box.append(k, v) for k, v in BANDWIDTH.items()]
[self._constellation_box.append(k, v) for k, v in CONSTELLATION.items()]
[self._sr_hp_box.append(k, v) for k, v in T_FEC.items()]
[self._sr_lp_box.append(k, v) for k, v in T_FEC.items()]
[self._guard_box.append(k, v) for k, v in GUARD_INTERVAL.items()]
[self._transmission_box.append(k, v) for k, v in TRANSMISSION_MODE.items()]
[self._hierarchy_box.append(k, v) for k, v in HIERARCHY.items()]
[self._inversion_box.append(k.value, k.name) for k in Inversion]
if transponder:
self._freq_entry.set_text(transponder.centre_frequency)
self._sys_box.set_active_id(transponder.system)
self._bandwidth_box.set_active_id(transponder.bandwidth)
self._constellation_box.set_active_id(transponder.constellation)
self._sr_hp_box.set_active_id(transponder.code_rate_hp)
self._sr_lp_box.set_active_id(transponder.code_rate_lp)
self._guard_box.set_active_id(transponder.guard_interval)
self._transmission_box.set_active_id(transponder.transmission_mode)
self._hierarchy_box.set_active_id(transponder.hierarchy_information)
self._inversion_box.set_active_id(transponder.inversion)
self._plp_id_entry.set_text(transponder.plp_id or "")
def is_accept(self):
tr = self.to_transponder()
if not tr.centre_frequency or self.digit_pattern.search(tr.centre_frequency):
return False
elif tr.plp_id and self.digit_pattern.search(tr.plp_id):
return False
return True
def to_transponder(self):
return TerTransponder(centre_frequency=self._freq_entry.get_text(),
system=self._sys_box.get_active_id(),
bandwidth=self._bandwidth_box.get_active_id(),
constellation=self._constellation_box.get_active_id(),
code_rate_hp=self._sr_hp_box.get_active_id(),
code_rate_lp=self._sr_lp_box.get_active_id(),
guard_interval=self._guard_box.get_active_id(),
transmission_mode=self._transmission_box.get_active_id(),
hierarchy_information=self._hierarchy_box.get_active_id(),
inversion=self._inversion_box.get_active_id(),
plp_id=self._plp_id_entry.get_text() or None)
class CableTransponderDialog(TransponderDialog):
""" Dialog for adding or edit cable transponder. """
def __init__(self, transient, title, data=None, *args, **kwargs):
super().__init__(transient, title, data, *args, **kwargs)
handlers = {"on_entry_changed": self.on_entry_changed}
builder = get_builder(_DIALOGS_UI_PATH, handlers, use_str=True, objects=("cable_tr_box",))
self.frame.add(builder.get_object("cable_tr_box"))
self._freq_entry = builder.get_object("cable_freq_entry")
self._rate_entry = builder.get_object("cable_rate_entry")
self._fec_box = builder.get_object("cable_fec_box")
self._mod_box = builder.get_object("cable_mod_box")
self.set_style_provider(self._freq_entry)
self.set_style_provider(self._rate_entry)
self.show_all()
self.init_transponder_data(data)
@property
def data(self):
return self.to_transponder()
def init_transponder_data(self, transponder):
[self._fec_box.append(k, v) for k, v in FEC_DEFAULT.items()]
[self._mod_box.append(k, v) for k, v in C_MODULATION.items()]
if transponder:
self._freq_entry.set_text(transponder.frequency)
self._rate_entry.set_text(transponder.symbol_rate)
self._fec_box.set_active_id(transponder.fec_inner)
self._mod_box.set_active_id(transponder.modulation)
def is_accept(self):
tr = self.to_transponder()
if not tr.frequency or self.digit_pattern.search(tr.frequency):
return False
elif not tr.symbol_rate or self.digit_pattern.search(tr.symbol_rate):
return False
return True
def to_transponder(self):
return CableTransponder(frequency=self._freq_entry.get_text(),
symbol_rate=self._rate_entry.get_text(),
fec_inner=self._fec_box.get_active_id(),
modulation=self._mod_box.get_active_id())
# ********************** Update dialogs ************************ #
class UpdateDialog:
""" Base dialog for update satellites, transponders and services from the Web."""
def __init__(self, transient, settings, title=None):
handlers = {"on_update_satellites_list": self.on_update_satellites_list,
"on_receive_data": self.on_receive_data,
"on_cancel_receive": self.on_cancel_receive,
"on_satellite_toggled": self.on_satellite_toggled,
"on_satellite_changed": self.on_satellite_changed,
"on_transponder_toggled": self.on_transponder_toggled,
"on_info_bar_close": self.on_info_bar_close,
"on_filter_toggled": self.on_filter_toggled,
"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_quit": self.on_quit}
self._settings = settings
self._download_task = False
self._parser = None
self._size_name = f"{'_'.join(re.findall('[A-Z][^A-Z]*', self.__class__.__name__))}_window_size".lower()
builder = get_builder(f"{UI_RESOURCES_PATH}xml{os.sep}update.glade", handlers)
self._window = builder.get_object("satellites_update_window")
self._window.set_transient_for(transient)
if title:
self._window.set_title(title)
self._transponder_paned = builder.get_object("sat_update_tr_paned")
self._sat_view = builder.get_object("sat_update_tree_view")
self._transponder_view = builder.get_object("sat_update_tr_view")
self._service_view = builder.get_object("sat_update_srv_view")
self._source_box = builder.get_object("source_combo_box")
self._text_view = builder.get_object("text_view")
self._receive_button = builder.get_object("receive_data_button")
self._sat_update_info_bar = builder.get_object("sat_update_info_bar")
self._info_bar_message_label = builder.get_object("info_bar_message_label")
self._satellites_count_label = builder.get_object("satellites_count_label")
self._transponders_count_label = builder.get_object("transponders_count_label")
self._services_count_label = builder.get_object("services_count_label")
self._receive_button.bind_property("visible", builder.get_object("cancel_data_button"), "visible", 4)
update_button = builder.get_object("sat_update_button")
self._sat_view.bind_property("sensitive", update_button, "sensitive")
self._sat_view.bind_property("sensitive", self._source_box, "sensitive")
self._sat_view.bind_property("sensitive", self._source_box, "sensitive")
self._sat_view.bind_property("sensitive", self._receive_button, "sensitive")
self._receive_button.bind_property("visible", update_button, "visible")
# Filter
self._filter_bar = builder.get_object("sat_update_filter_bar")
self._from_pos_button = builder.get_object("from_pos_button")
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)
self._filter_bar.bind_property("search-mode-enabled", self._filter_bar, "visible")
# Log.
self._log_frame = builder.get_object("log_frame")
builder.get_object("log_info_bar").connect("response", lambda b, r: self._log_frame.set_visible(False))
# Search.
self._search_bar = builder.get_object("sat_update_search_bar")
self._search_bar.bind_property("search-mode-enabled", self._search_bar, "visible")
search_provider = SearchProvider(self._sat_view,
builder.get_object("sat_update_search_entry"),
builder.get_object("sat_update_search_down_button"),
builder.get_object("sat_update_search_up_button"))
builder.get_object("sat_update_find_button").connect("toggled", search_provider.on_search_toggled)
window_size = self._settings.get(self._size_name)
if window_size:
self._window.resize(*window_size)
def show(self):
self._window.show()
@property
def is_download(self):
return self._download_task
@is_download.setter
def is_download(self, value):
self._download_task = value
self._receive_button.set_visible(not value)
@run_idle
def on_update_satellites_list(self, item=None):
if self.is_download:
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return
self.clear_data()
self.is_download = True
self._sat_view.set_sensitive(False)
src = self._source_box.get_active()
if not self._parser:
self._parser = SatellitesParser()
self.get_sat_list(src, self.append_satellites)
def clear_data(self):
get_base_model(self._sat_view.get_model()).clear()
self._transponder_view.get_model().clear()
self._service_view.get_model().clear()
self._satellites_count_label.set_text("0")
self._transponders_count_label.set_text("0")
self._services_count_label.set_text("0")
@run_task
def get_sat_list(self, src, callback):
sat_src = SatelliteSource.LYNGSAT
if src == 1:
sat_src = SatelliteSource.KINGOFSAT
elif src == 2:
sat_src = SatelliteSource.FLYSAT
sats = self._parser.get_satellites_list(sat_src)
callback(sats)
self.is_download = False
@run_idle
def append_satellites(self, sats):
model = get_base_model(self._sat_view.get_model())
for sat in sats:
model.append(sat)
self._sat_view.set_sensitive(True)
self._satellites_count_label.set_text(str(len(model)))
@run_idle
def on_receive_data(self, item):
if self.is_download:
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return
@run_idle
def update_log_visibility(self):
self._log_frame.set_visible(True)
self._text_view.get_buffer().set_text("", 0)
def append_output(self):
@run_idle
def append(t):
append_text_to_tview(t, self._text_view)
while True:
text = yield
append(text)
def on_cancel_receive(self, item=None):
self._download_task = False
def on_satellite_changed(self, box):
self.on_update_satellites_list()
def on_satellite_toggled(self, toggle, path):
model = self._sat_view.get_model()
self.update_state(model, path, not toggle.get_active())
self.update_receive_button_state(self._filter_model)
def on_transponder_toggled(self, toggle, path):
model = self._transponder_view.get_model()
model.set_value(model.get_iter(path), 2, not toggle.get_active())
@run_idle
def update_receive_button_state(self, model):
self._receive_button.set_sensitive((any(r[4] for r in model)))
@run_idle
def show_info_message(self, text, message_type):
self._sat_update_info_bar.set_visible(True)
self._sat_update_info_bar.set_message_type(message_type)
self._info_bar_message_label.set_text(text)
def on_info_bar_close(self, bar=None, resp=None):
self._sat_update_info_bar.set_visible(False)
def on_find_toggled(self, button: Gtk.ToggleToolButton):
self._search_bar.set_search_mode(button.get_active())
def on_filter_toggled(self, button: Gtk.ToggleToolButton):
self._filter_bar.set_search_mode(button.get_active())
@run_idle
def on_filter(self, item):
self._filter_positions = self.get_positions()
self._filter_model.refilter()
def filter_function(self, model, itr, data):
if self._filter_model is None or self._filter_model == "None":
return True
from_pos, to_pos = self._filter_positions
if from_pos == 0 and to_pos == 0:
return True
if from_pos > to_pos:
from_pos, to_pos = to_pos, from_pos
return from_pos <= float(self._parser.get_position(model.get(itr, 1)[0])) <= to_pos
def get_positions(self):
from_pos = round(self._from_pos_button.get_value(), 1) * (-1 if self._filter_from_combo_box.get_active() else 1)
to_pos = round(self._to_pos_button.get_value(), 1) * (-1 if self._filter_to_combo_box.get_active() else 1)
return from_pos, to_pos
def on_select_all(self, view):
self.update_selection(view, True)
def on_unselect_all(self, view):
self.update_selection(view, False)
def update_selection(self, view, select):
model = view.get_model()
view.get_model().foreach(lambda mod, path, itr: self.update_state(model, path, select))
self.update_receive_button_state(self._filter_model)
def update_state(self, model, path, select):
""" Updates checkbox state by given path in the list """
itr = self._filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(model.get_iter(path)))
self._filter_model.get_model().set_value(itr, 4, select)
def on_quit(self, window, event):
self._settings.add(self._size_name, window.get_size())
self.is_download = False
class SatellitesUpdateDialog(UpdateDialog):
""" Dialog for update satellites from the Web. """
def __init__(self, transient, settings, main_model):
super().__init__(transient=transient, settings=settings)
self._main_model = main_model
self._source_box.connect("changed", self.on_update_satellites_list)
@run_idle
def on_receive_data(self, item):
if self.is_download:
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return
self.receive_satellites()
@run_task
def receive_satellites(self):
self.is_download = True
self.update_log_visibility()
model = self._sat_view.get_model()
start = time.time()
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
text = "Processing: {}\n"
sats = []
appender = self.append_output()
next(appender)
futures = {executor.submit(self._parser.get_satellite, sat[:-1]): sat for sat in [r for r in model if r[4]]}
for future in concurrent.futures.as_completed(futures):
if not self.is_download:
self.is_download = True
executor.shutdown()
appender.send("\nCanceled\n")
appender.close()
self.is_download = False
return
data = future.result()
appender.send(text.format(data[0]))
sats.append(data)
appender.send("-" * 75 + "\n")
sat_count = len(sats)
sats = {s[0]: s for s in sats} # key = name, v = satellite
for row in self._main_model:
pos = row[0]
if pos in sats:
sat = sats.pop(pos)
appender.send(f"Updating satellite: {row[0]}\n")
GLib.idle_add(self._main_model.set, row.iter, {i: v for i, v in enumerate(sat)})
for p, s in sats.items():
appender.send(f"Adding satellite: {s.name}\n")
self.append_satellite(s)
appender.send("-" * 75 + "\n")
appender.send(f"Consumed: {time.time() - start:0.0f}s, {sat_count} satellites received.\n")
appender.close()
self.is_download = False
@run_idle
def append_satellite(self, sat):
self._main_model.append(sat)
class ServicesUpdateDialog(UpdateDialog):
""" Dialog for updating services from the Web. """
def __init__(self, transient, settings, callback):
super().__init__(transient=transient, settings=settings, title="Services update")
self._callback = callback
self._satellite_paths = {}
self._transponders = {}
self._services = {}
self._selected_transponders = set()
self._services_parser = ServicesParser(source=SatelliteSource.LYNGSAT)
# Transponder view popup menu
tr_popup_menu = Gtk.Menu()
select_all_item = Gtk.ImageMenuItem.new_from_stock("gtk-select-all")
select_all_item.connect("activate", lambda w: self.update_transponder_selection(True))
tr_popup_menu.append(select_all_item)
remove_selection_item = Gtk.ImageMenuItem.new_from_stock("gtk-undo")
remove_selection_item.set_label(get_message("Remove selection"))
remove_selection_item.connect("activate", lambda w: self.update_transponder_selection(False))
tr_popup_menu.append(remove_selection_item)
tr_popup_menu.show_all()
self._sat_view.connect("row-activated", self.on_activate_satellite)
self._transponder_view.connect("row-activated", self.on_activate_transponder)
self._transponder_view.connect("button-press-event", lambda w, e: on_popup_menu(tr_popup_menu, e))
self._transponder_view.connect("select_all", lambda w: self.update_transponder_selection(True))
self._transponder_paned.set_visible(True)
self._source_box.connect("changed", self.on_update_satellites_list)
@run_idle
def on_receive_data(self, item):
if self.is_download:
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
return
self.receive_services()
@run_task
def receive_services(self):
self.is_download = True
self.update_log_visibility()
model = self._sat_view.get_model()
appender = self.append_output()
next(appender)
start = time.time()
non_cached_sats = []
sat_names = {}
t_names = {}
t_urls = set()
services = []
for r in (r for r in model if r[-1]):
if not self.is_download:
appender.send("\nCanceled\n")
return
sat, url = r[0], r[3]
trs = self._transponders.get(url, None)
if trs:
for t in filter(lambda tp: tp.url in self._selected_transponders, trs):
t_urls.add(t.url)
t_names[t.url] = t.text
else:
non_cached_sats.append(url)
sat_names[url] = sat
if non_cached_sats:
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
futures = {executor.submit(self._services_parser.get_transponders_links, u): u for u in non_cached_sats}
for future in concurrent.futures.as_completed(futures):
if not self.is_download:
appender.send("\nCanceled.\n")
self.is_download = False
return
appender.send(f"Getting transponders for: {sat_names.get(futures[future])}.\n")
for t in future.result():
t_urls.add(t.url)
t_names[t.url] = t.text
appender.send("-" * 75 + "\n")
appender.send(f"{len(t_urls)} transponders received.\n\n")
non_cached_ts = []
for tr in t_urls:
srvs = self._services.get(tr)
services.extend(srvs) if srvs else non_cached_ts.append(tr)
if non_cached_ts:
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
futures = {executor.submit(self._services_parser.get_transponder_services, u): u for u in non_cached_ts}
for future in concurrent.futures.as_completed(futures):
if not self.is_download:
appender.send("\nCanceled.\n")
self.is_download = False
return
appender.send(f"Getting services for: {t_names.get(futures[future], '')}.\n")
try:
list(map(services.append, future.result()))
except ValueError as e:
log(f"Getting services error: {e} [{t_names.get(futures[future])}]")
appender.send("-" * 75 + "\n")
appender.send(f"Consumed: {time.time() - start:0.0f}s, {len(services)} services received.")
try:
from app.eparser.enigma.lamedb import LameDbReader
# Used for double check!
reader = LameDbReader(path=None)
srvs = reader.get_services_list("".join(reader.get_services_lines(services)))
except ValueError as e:
log(f"ServicesUpdateDialog [on receive data] error: {e}")
else:
self._callback(srvs)
self.is_download = False
@run_task
def get_sat_list(self, src, callback):
sat_src = SatelliteSource.LYNGSAT
if src == 1:
sat_src = SatelliteSource.KINGOFSAT
self._services_parser.source = sat_src
sats = self._parser.get_satellites_list(sat_src)
callback(sats)
self.is_download = False
def on_satellite_toggled(self, toggle, path):
model = self._sat_view.get_model()
self.update_state(model, path, not toggle.get_active())
self.update_receive_button_state(self._filter_model)
url = model.get_value(model.get_iter(path), 3)
selected = toggle.get_active()
transponders = self._transponders.get(url, None)
if transponders:
for t in transponders:
self._selected_transponders.add(t.url) if selected else self._selected_transponders.discard(t.url)
def on_transponder_toggled(self, toggle, path):
model = self._transponder_view.get_model()
itr = model.get_iter(path)
active = not toggle.get_active()
url = self.update_transponder_state(itr, model, active)
s_path = self._satellite_paths.get(url, None)
if s_path:
self.update_sat_state(model, s_path, active)
def update_sat_state(self, model, path, active):
sat_model = self._sat_view.get_model()
if active:
self.update_state(sat_model, path, active)
else:
self.update_state(sat_model, path, any((r[-1] for r in model)))
self.update_receive_button_state(self._filter_model)
def update_transponder_state(self, itr, model, active):
model.set_value(itr, 2, active)
url = model.get_value(itr, 1)
self._selected_transponders.add(url) if active else self._selected_transponders.discard(url)
return url
@run_task
def on_activate_satellite(self, view, path, column):
GLib.idle_add(self._transponder_view.get_model().clear)
GLib.idle_add(self._service_view.get_model().clear)
model = view.get_model()
itr = model.get_iter(path)
url, selected = model.get_value(itr, 3), model.get_value(itr, 4)
transponders = self._transponders.get(url, None)
if transponders is None:
GLib.idle_add(view.set_sensitive, False)
transponders = self._services_parser.get_transponders_links(url)
self._transponders[url] = transponders
for t in transponders:
t_url = t.url
self._satellite_paths[t_url] = path
self._selected_transponders.add(t_url) if selected else self._selected_transponders.discard(t_url)
self.append_transponders(self._transponder_view.get_model(), transponders)
@run_idle
def append_transponders(self, model, trs_list):
model.clear()
list(map(model.append, [(t.text, t.url, t.url in self._selected_transponders) for t in trs_list]))
self._sat_view.set_sensitive(True)
self._transponders_count_label.set_text(str(len(model)))
@run_task
def on_activate_transponder(self, view, path, column):
url = view.get_model()[path][1]
services = self._services.get(url, None)
if services is None:
GLib.idle_add(view.set_sensitive, False)
services = self._services_parser.get_transponder_services(url)
self._services[url] = services
self.append_services(self._service_view.get_model(), services)
@run_idle
def append_services(self, model, srv_list):
model.clear()
for s in srv_list:
model.append((None, s.service, s.package, s.service_type, str(s.ssid), None))
self._transponder_view.set_sensitive(True)
self._services_count_label.set_text(str(len(model)))
def update_transponder_selection(self, select):
m = self._transponder_view.get_model()
if not len(m):
return
s_path = self._satellite_paths.get({self.update_transponder_state(r.iter, m, select) for r in m}.pop(), None)
if s_path:
self.update_sat_state(m, s_path, select)
if __name__ == "__main__":
pass

585
app/ui/xml/edit.py Normal file
View File

@@ -0,0 +1,585 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
from enum import Enum
from pyexpat import ExpatError
from gi.repository import GLib
from app.commons import run_idle
from app.connections import DownloadType
from app.eparser import get_satellites, write_satellites, Satellite, Transponder
from app.eparser.ecommons import (POLARIZATION, FEC, SYSTEM, MODULATION, T_SYSTEM, BANDWIDTH, CONSTELLATION, T_FEC,
GUARD_INTERVAL, TRANSMISSION_MODE, HIERARCHY, Inversion, FEC_DEFAULT, C_MODULATION,
Terrestrial, Cable, CableTransponder, TerTransponder)
from app.eparser.satxml import get_terrestrial, get_cable, write_terrestrial, write_cable
from .dialogs import SatelliteDialog, SatellitesUpdateDialog, TerrestrialDialog, CableDialog, SatTransponderDialog, \
CableTransponderDialog, TerTransponderDialog
from ..dialogs import show_dialog, DialogType, get_chooser_dialog, get_message, get_builder
from ..main_helper import move_items, on_popup_menu, scroll_to
from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, MOVE_KEYS, KeyboardKey, MOD_MASK, Page
class SatellitesTool(Gtk.Box):
""" Class to processing *.xml data. """
class DVB(str, Enum):
SAT = "satellites"
TERRESTRIAL = "terrestrial"
CABLE = "cable"
def __str__(self):
return self.value
def __init__(self, app, settings, *args, **kwargs):
super().__init__(*args, **kwargs)
self._app = app
self._app.connect("data-save", self.on_save)
self._app.connect("data-save-as", self.on_save_as)
self._app.connect("data-receive", self.on_download)
self._app.connect("data-send", self.on_upload)
self._settings = settings
self._current_sat_path = None
self._current_ter_path = None
self._current_cable_path = None
self._dvb_type = self.DVB.SAT
handlers = {"on_satellite_view_realize": self.on_satellite_view_realize,
"on_terrestrial_view_realize": self.on_terrestrial_view_realize,
"on_cable_view_realize": self.on_cable_view_realize,
"on_update": self.on_update,
"on_up": self.on_up,
"on_down": self.on_down,
"on_button_press": self.on_button_press,
"on_tr_button_press": self.on_tr_button_press,
"on_add": self.on_add,
"on_edit": self.on_edit,
"on_remove": self.on_remove,
"on_transponder_add": self.on_transponder_add,
"on_transponder_edit": self.on_transponder_edit,
"on_transponder_remove": self.on_transponder_remove,
"on_key_press": self.on_key_press,
"on_tr_key_press": self.on_tr_key_press,
"on_visible_page": self.on_visible_page,
"on_satellite_selection": self.on_satellite_selection,
"on_terrestrial_selection": self.on_terrestrial_selection,
"on_cable_selection": self.on_cable_selection,
"on_sat_model_changed": self.on_sat_model_changed,
"on_sat_tr_model_changed": self.on_sat_tr_model_changed,
"on_ter_model_changed": self.on_ter_model_changed,
"on_ter_tr_model_changed": self.on_ter_tr_model_changed,
"on_cable_model_changed": self.on_cable_model_changed,
"on_cable_tr_model_changed": self.on_cable_tr_model_changed}
builder = get_builder(f"{UI_RESOURCES_PATH}xml/editor.glade", handlers)
self._satellite_view = builder.get_object("satellite_view")
self._terrestrial_view = builder.get_object("terrestrial_view")
self._cable_view = builder.get_object("cable_view")
self._sat_tr_view = builder.get_object("sat_tr_view")
self._ter_tr_view = builder.get_object("ter_tr_view")
self._cable_tr_view = builder.get_object("cable_tr_view")
self._sat_count_label = builder.get_object("sat_count_label")
self._sat_tr_count_label = builder.get_object("sat_tr_count_label")
self._ter_count_label = builder.get_object("ter_count_label")
self._ter_tr_count_label = builder.get_object("ter_tr_count_label")
self._cable_count_label = builder.get_object("cable_count_label")
self._cable_tr_count_label = builder.get_object("cable_tr_count_label")
self._transponders_stack = builder.get_object("transponders_stack")
self._add_header_button = builder.get_object("add_header_button")
self._update_header_button = builder.get_object("update_header_button")
self.pack_start(builder.get_object("main_paned"), True, True, 0)
self._app.connect("profile-changed", self.on_profile_changed)
# Custom renderers.
renderer = builder.get_object("sat_pos_renderer")
builder.get_object("sat_pos_column").set_cell_data_func(renderer, self.sat_pos_func)
# Satellite.
renderer = builder.get_object("sat_pol_renderer")
builder.get_object("pol_column").set_cell_data_func(renderer, self.sat_pol_func)
renderer = builder.get_object("sat_fec_renderer")
builder.get_object("fec_column").set_cell_data_func(renderer, self.sat_fec_func)
renderer = builder.get_object("sat_sys_renderer")
builder.get_object("sys_column").set_cell_data_func(renderer, self.sat_sys_func)
renderer = builder.get_object("sat_mod_renderer")
builder.get_object("mod_column").set_cell_data_func(renderer, self.sat_mod_func)
# Terrestrial.
renderer = builder.get_object("ter_system_renderer")
builder.get_object("ter_system_column").set_cell_data_func(renderer, self.ter_sys_func)
renderer = builder.get_object("ter_bandwidth_renderer")
builder.get_object("ter_bandwidth_column").set_cell_data_func(renderer, self.ter_bandwidth_func)
renderer = builder.get_object("ter_constellation_renderer")
builder.get_object("ter_constellation_column").set_cell_data_func(renderer, self.ter_constellation_func)
renderer = builder.get_object("ter_rate_hp_renderer")
builder.get_object("ter_rate_hp_column").set_cell_data_func(renderer, self.ter_fec_hp_func)
renderer = builder.get_object("ter_rate_lp_renderer")
builder.get_object("ter_rate_lp_column").set_cell_data_func(renderer, self.ter_fec_lp_func)
renderer = builder.get_object("ter_guard_renderer")
builder.get_object("ter_guard_column").set_cell_data_func(renderer, self.ter_guard_func)
renderer = builder.get_object("ter_tr_mode_renderer")
builder.get_object("ter_tr_mode_column").set_cell_data_func(renderer, self.ter_transmission_func)
renderer = builder.get_object("ter_hierarchy_renderer")
builder.get_object("ter_hierarchy_column").set_cell_data_func(renderer, self.ter_hierarchy_func)
renderer = builder.get_object("ter_inversion_renderer")
builder.get_object("ter_inversion_column").set_cell_data_func(renderer, self.ter_inversion_func)
# Cable.
renderer = builder.get_object("cable_fec_renderer")
builder.get_object("cable_fec_column").set_cell_data_func(renderer, self.cable_fec_func)
renderer = builder.get_object("cable_mod_renderer")
builder.get_object("cable_mod_column").set_cell_data_func(renderer, self.cable_mod_func)
self.show()
# ******************** Custom renderers ******************** #
def sat_pos_func(self, column, renderer, model, itr, data):
""" Converts and sets the satellite position value to a readable format. """
pos = int(model.get_value(itr, 2))
renderer.set_property("text", f"{abs(pos / 10):0.1f}{'W' if pos < 0 else 'E'}")
def sat_pol_func(self, column, renderer, model, itr, data):
renderer.set_property("text", POLARIZATION.get(model.get_value(itr, 2), None))
def sat_fec_func(self, column, renderer, model, itr, data):
renderer.set_property("text", FEC.get(model.get_value(itr, 3), None))
def sat_sys_func(self, column, renderer, model, itr, data):
renderer.set_property("text", SYSTEM.get(model.get_value(itr, 4), None))
def sat_mod_func(self, column, renderer, model, itr, data):
renderer.set_property("text", MODULATION.get(model.get_value(itr, 5), None))
def ter_sys_func(self, column, renderer, model, itr, data):
renderer.set_property("text", T_SYSTEM.get(model.get_value(itr, 1), None))
def ter_bandwidth_func(self, column, renderer, model, itr, data):
renderer.set_property("text", BANDWIDTH.get(model.get_value(itr, 2), None))
def ter_constellation_func(self, column, renderer, model, itr, data):
renderer.set_property("text", CONSTELLATION.get(model.get_value(itr, 3), None))
def ter_fec_hp_func(self, column, renderer, model, itr, data):
renderer.set_property("text", T_FEC.get(model.get_value(itr, 4), None))
def ter_fec_lp_func(self, column, renderer, model, itr, data):
renderer.set_property("text", T_FEC.get(model.get_value(itr, 5), None))
def ter_guard_func(self, column, renderer, model, itr, data):
renderer.set_property("text", GUARD_INTERVAL.get(model.get_value(itr, 6), None))
def ter_transmission_func(self, column, renderer, model, itr, data):
renderer.set_property("text", TRANSMISSION_MODE.get(model.get_value(itr, 7), None))
def ter_hierarchy_func(self, column, renderer, model, itr, data):
renderer.set_property("text", HIERARCHY.get(model.get_value(itr, 8), None))
def ter_inversion_func(self, column, renderer, model, itr, data):
value = model.get_value(itr, 9)
if value:
value = Inversion(value).name
renderer.set_property("text", value)
def cable_fec_func(self, column, renderer, model, itr, data):
renderer.set_property("text", FEC_DEFAULT.get(model.get_value(itr, 2), None))
def cable_mod_func(self, column, renderer, model, itr, data):
renderer.set_property("text", C_MODULATION.get(model.get_value(itr, 3), None))
def on_satellite_view_realize(self, view):
self.load_satellites_list()
def on_terrestrial_view_realize(self, view):
self.load_terrestrial_list()
def on_cable_view_realize(self, view):
self.load_cable_list()
def load_satellites_list(self, path=None):
gen = self.on_satellites_list_load(path)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def load_terrestrial_list(self, path=None):
gen = self.on_terrestrial_list_load(path)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def load_cable_list(self, path=None):
gen = self.on_cable_list_load(path)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def on_visible_page(self, stack, param):
self._dvb_type = self.DVB(stack.get_visible_child_name())
self._transponders_stack.set_visible_child_name(self._dvb_type)
self._update_header_button.set_sensitive(self._dvb_type is self.DVB.SAT)
if self._dvb_type is self.DVB.SAT:
self._app.on_info_bar_close()
else:
self._app.show_info_message("EXPERIMENTAL!", Gtk.MessageType.WARNING)
def on_satellite_selection(self, view):
model = self._sat_tr_view.get_model()
model.clear()
self._current_sat_path, column = view.get_cursor()
if self._current_sat_path:
sat_model = view.get_model()
list(map(model.append, sat_model[self._current_sat_path][-1]))
def on_terrestrial_selection(self, view):
model = self._ter_tr_view.get_model()
model.clear()
self._current_ter_path, column = view.get_cursor()
if self._current_ter_path:
ter_model = view.get_model()
list(map(model.append, ter_model[self._current_ter_path][-1]))
def on_cable_selection(self, view):
model = self._cable_tr_view.get_model()
model.clear()
self._current_cable_path, column = view.get_cursor()
if self._current_cable_path:
cable_model = view.get_model()
list(map(model.append, cable_model[self._current_cable_path][-1]))
def on_sat_model_changed(self, model, path, itr=None):
self._sat_count_label.set_text(str(len(model)))
def on_sat_tr_model_changed(self, model, path, itr=None):
self._sat_tr_count_label.set_text(str(len(model)))
def on_ter_model_changed(self, model, path, itr=None):
self._ter_count_label.set_text(str(len(model)))
def on_ter_tr_model_changed(self, model, path, itr=None):
self._ter_tr_count_label.set_text(str(len(model)))
def on_cable_model_changed(self, model, path, itr=None):
self._cable_count_label.set_text(str(len(model)))
def on_cable_tr_model_changed(self, model, path, itr=None):
self._cable_tr_count_label.set_text(str(len(model)))
def on_up(self, item):
move_items(KeyboardKey.UP, self._satellite_view)
def on_down(self, item):
move_items(KeyboardKey.DOWN, self._satellite_view)
def on_button_press(self, menu, event):
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS:
self.on_edit()
else:
on_popup_menu(menu, event)
def on_tr_button_press(self, menu, event):
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS:
self.on_transponder_edit()
else:
on_popup_menu(menu, event)
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)
ctrl = event.state & MOD_MASK
if key is KeyboardKey.DELETE:
self.on_remove(view)
elif key is KeyboardKey.INSERT:
self.on_edit(force=True)
elif ctrl and key is KeyboardKey.E:
self.on_edit()
elif ctrl and key in MOVE_KEYS:
move_items(key, view)
elif key is KeyboardKey.LEFT or key is KeyboardKey.RIGHT:
view.do_unselect_all(view)
def on_tr_key_press(self, view, event):
""" Handling transponder view keystrokes. """
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
ctrl = event.state & MOD_MASK
if key is KeyboardKey.DELETE:
self.on_transponder_remove()
elif key is KeyboardKey.INSERT:
self.on_transponder_edit(force=True)
elif ctrl and key is KeyboardKey.E:
self.on_transponder_edit()
elif ctrl and key in MOVE_KEYS:
move_items(key, view)
elif key is KeyboardKey.LEFT or key is KeyboardKey.RIGHT:
view.do_unselect_all(view)
def on_satellites_list_load(self, path=None):
""" Load satellites data into model """
path = path or f"{self._settings.profile_data_path}satellites.xml"
yield from self.load_data(self._satellite_view, get_satellites, path)
def on_terrestrial_list_load(self, path=None):
path = path or f"{self._settings.profile_data_path}terrestrial.xml"
yield from self.load_data(self._terrestrial_view, get_terrestrial, path)
def on_cable_list_load(self, path=None):
path = path or f"{self._settings.profile_data_path}cables.xml"
yield from self.load_data(self._cable_view, get_cable, path)
def load_data(self, view, func, path):
model = view.get_model()
model.clear()
try:
data = func(path)
yield True
except FileNotFoundError as e:
msg = get_message("Please, download files from receiver or setup your path for read data!")
self._app.show_error_message(f"{e}\n{msg}")
except ExpatError as e:
msg = f"The file [{path}] is not formatted correctly or contains invalid characters! Cause: {e}"
self._app.show_error_message(msg)
else:
for d in data:
yield model.append(d)
def on_add(self, item):
""" Common adding. """
self.on_edit(item, force=True)
def on_transponder_add(self, item):
self.on_transponder_edit(force=True)
def on_edit(self, item=None, force=False):
self.on_data_edit(self.get_active_dvb_view(), force)
def on_transponder_edit(self, item=None, force=False):
self.on_data_edit(self.get_active_transponder_view(), force)
def on_data_edit(self, view, force=False):
""" Common edit. """
if force:
model, paths = view.get_selection().get_selected_rows()
else:
paths = self.check_selection(view, "Please, select only one item!")
if not paths:
return
model = view.get_model()
row = model[paths][:] if paths else None
itr = model.get_iter(paths) if paths else None
if view is self._satellite_view:
self.on_dvb_data_edit(SatelliteDialog, "Satellite", view, None if force else Satellite(*row), itr)
elif view is self._terrestrial_view:
self.on_dvb_data_edit(TerrestrialDialog, "Region", view, None if force else Terrestrial(*row), itr)
elif view is self._cable_view:
self.on_dvb_data_edit(CableDialog, "Provider", view, None if force else Cable(*row), itr)
elif view is self._sat_tr_view:
data = None if force else Transponder(*row)
self.on_transponder_data_edit(SatTransponderDialog, "Transponder", view, self._satellite_view, data, itr)
elif view is self._ter_tr_view:
data = None if force else TerTransponder(*row)
self.on_transponder_data_edit(TerTransponderDialog, "Transponder", view, self._terrestrial_view, data, itr)
elif view is self._cable_tr_view:
data = None if force else CableTransponder(*row)
self.on_transponder_data_edit(CableTransponderDialog, "Transponder", view, self._cable_view, data, itr)
else:
self._app.show_error_message("Not implemented yet!")
def on_dvb_data_edit(self, dialog, title, view, data=None, edited_itr=None):
""" Creates or edits DVB data. """
dialog = dialog(self._app.get_active_window(), title, data)
if dialog.run() == Gtk.ResponseType.OK:
dvb_data = dialog.data
if dvb_data:
model, paths = view.get_selection().get_selected_rows()
if data and edited_itr:
model.set(edited_itr, {i: v for i, v in enumerate(dvb_data)})
else:
if paths:
index = paths[0].get_indices()[0] + 1
model.insert(index, dvb_data)
else:
model.append(dvb_data)
scroll_to(len(model) - 1, view)
dialog.destroy()
def on_transponder_data_edit(self, dialog, title, view, src_view, data=None, edited_itr=None):
""" Creates or edits transponder data. """
paths = self.check_selection(src_view, "Please, select only one item!")
if paths is None:
return
elif len(paths) == 0:
self._app.show_error_message("No source selected!")
return
dialog = dialog(self._app.app_window, title, data)
if dialog.run() == Gtk.ResponseType.OK:
tr = dialog.data
if tr:
src_model = src_view.get_model()
transponders = src_model[paths][-1]
tr_model, tr_paths = view.get_selection().get_selected_rows()
if data and edited_itr:
tr_model.set(edited_itr, {i: v for i, v in enumerate(tr)})
transponders[tr_model.get_path(edited_itr).get_indices()[0]] = tr
else:
index = paths[0].get_indices()[0] + 1
tr_model.insert(index, tr)
transponders.insert(index, tr)
dialog.destroy()
def check_selection(self, view, message):
""" Checks if any row is selected. Shows error dialog if selected more than one.
Returns selected path or None.
"""
model, paths = view.get_selection().get_selected_rows()
if len(paths) > 1:
self._app.show_error_message(message)
return
return paths
def on_remove(self, view=None):
""" Removes selected satellites and transponders. """
view = self.get_active_dvb_view()
selection = view.get_selection()
model, paths = selection.get_selected_rows()
list(map(model.remove, [model.get_iter(path) for path in paths]))
def on_transponder_remove(self, item=None):
view = self.get_active_transponder_view()
trs = None
if view is self._sat_tr_view:
if self._current_sat_path:
trs = self._satellite_view.get_model()[self._current_sat_path][-1]
else:
self._app.show_error_message("No satellite is selected!")
elif view is self._ter_tr_view:
if self._current_ter_path:
trs = self._terrestrial_view.get_model()[self._current_ter_path][-1]
else:
self._app.show_error_message("No terrestrial is selected!")
elif view is self._cable_tr_view:
if self._current_cable_path:
trs = self._cable_view.get_model()[self._current_cable_path][-1]
else:
self._app.show_error_message("No cable is selected!")
if trs:
model, paths = view.get_selection().get_selected_rows()
list(map(trs.pop, sorted(map(lambda p: p.get_indices()[0], paths), reverse=True)))
list(map(model.remove, [model.get_iter(path) for path in paths]))
def get_active_dvb_view(self):
if self._dvb_type is self.DVB.SAT:
return self._satellite_view
elif self._dvb_type is self.DVB.TERRESTRIAL:
return self._terrestrial_view
return self._cable_view
def get_active_transponder_view(self):
if self._dvb_type is self.DVB.SAT:
return self._sat_tr_view
elif self._dvb_type is self.DVB.TERRESTRIAL:
return self._ter_tr_view
return self._cable_tr_view
@run_idle
def on_open(self):
xml_file = "satellites.xml"
if self._dvb_type is self.DVB.TERRESTRIAL:
xml_file = "terrestrial.xml"
elif self._dvb_type is self.DVB.CABLE:
xml_file = "cables.xml"
response = get_chooser_dialog(self._app.app_window, self._settings, xml_file, ("*.xml",))
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
if not str(response).endswith(xml_file):
self._app.show_error_message(f"No {xml_file} file is selected!")
return
if self._dvb_type is self.DVB.SAT:
self.load_satellites_list(response)
elif self._dvb_type is self.DVB.TERRESTRIAL:
self.load_terrestrial_list(response)
else:
self.load_cable_list(response)
@run_idle
def on_profile_changed(self, app, profile):
self.load_satellites_list()
self.load_terrestrial_list()
self.load_cable_list()
@run_idle
def on_save(self, app, page):
if page is Page.SATELLITE and show_dialog(DialogType.QUESTION, self._app.app_window) == Gtk.ResponseType.OK:
if self._dvb_type is self.DVB.SAT:
write_satellites((Satellite(*r) for r in self._satellite_view.get_model()),
f"{self._settings.profile_data_path}satellites.xml")
elif self._dvb_type is self.DVB.TERRESTRIAL:
write_terrestrial((Terrestrial(*r) for r in self._terrestrial_view.get_model()),
f"{self._settings.profile_data_path}terrestrial.xml")
else:
write_cable((Cable(*r) for r in self._cable_view.get_model()),
f"{self._settings.profile_data_path}cables.xml")
def on_save_as(self, app, page):
self._app.show_error_message("Not implemented yet!")
def on_download(self, app, page):
if page is Page.SATELLITE:
self._app.on_download_data(DownloadType.SATELLITES)
def on_upload(self, app, page):
if page is Page.SATELLITE:
self._app.upload_data(DownloadType.SATELLITES)
@run_idle
def on_update(self, item):
SatellitesUpdateDialog(self._app.get_active_window(), self._settings, self._satellite_view.get_model()).show()
if __name__ == "__main__":
pass

1399
app/ui/xml/editor.glade Normal file

File diff suppressed because it is too large Load Diff

1173
app/ui/xml/update.glade Normal file

File diff suppressed because it is too large Load Diff

7
build/BUILDING.md Normal file
View File

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

40
build/BUILD_WIN.md Normal file
View File

@@ -0,0 +1,40 @@
## Launch
The best way to run this program from source is using of [MSYS2](https://www.msys2.org/) platform.
1. Download and install the platform as described [here](https://www.msys2.org/) up to point 4.
2. Launch **mingw64** shell.
![mingw64](https://user-images.githubusercontent.com/7511379/161400639-898ceb10-7de8-4557-bde1-25fe32bdfb03.png)
3. Run first `pacman -Suy` After that, you may need to restart the terminal and re-run the update command.
4. Install minimal required packages:
`pacman -S mingw-w64-x86_64-gtk3 mingw-w64-x86_64-python3 mingw-w64-x86_64-python3-gobject mingw-w64-x86_64-python3-pip mingw-w64-x86_64-python3-requests`
Optional: `pacman -S mingw-w64-x86_64-python3-pillow`
To support streams playback, install the following packages (the list may not be complete):
For [MPV](https://mpv.io/) `pacman -S mingw-w64-x86_64-mpv`,
For [GStreamer](https://gstreamer.freedesktop.org/) `pacman -S mingw-w64-x86_64-gst-libav mingw-w64-x86_64-gst-plugins-bad mingw-w64-x86_64-gst-plugins-base mingw-w64-x86_64-gst-plugins-good mingw-w64-x86_64-gstreamer`
5. Download and unzip the archive with sources from preferred branch (e.g. [master](https://github.com/DYefremov/DemonEditor/archive/refs/heads/master.zip)) in to folder where MSYS2 is installed. E.g: `c:\msys64\home\username\`
6. Run mingw64 shell. Go to the folder where the program was unpacked. E.g: `cd DemonEditor/`
And run: `./start.py`
## Building a package
To build a standalone package, we can use [PyInstaller](https://pyinstaller.readthedocs.io/en/stable/).
1. Launch mingw64 shell.
2. Install PyInstaller via pip: `pip3 install pyinstaller`
3. Go to the folder where the program was unpacked. E.g: `c:\msys64\home\username\DemonEditor\`
4. Сopy and replace the files from the /build/win/ folder to the root .
5. Go to the folder with the program in the running terminal: `cd DemonEditor/`
6. Give the following command: `pyinstaller.exe DemonEditor.spec`
7. Wait until the operation end. In the dist folder you will find a ready-made build.
### Appearance
To change the look we can use third party [Gtk3 themes and Icon sets](https://www.gnome-look.org).
To set the default theme:
1. Сreate a folder "`\etc\gtk-3.0\`" in the root of the finished build folder.
2. Create a _settings.ini_ file in this folder with the following content:
```
[Settings]
gtk-icon-theme-name = Adwaita
gtk-theme-name = Windows-10
```
In this case, we are using the default icon theme "Adwaita" and the [third party theme](https://github.com/B00merang-Project/Windows-10) "Windows-10".
Themes and icon sets should be located in the `share\themes` and `share\icons` folders respectively.
To fine-tune the default theme you use, you can use the _win_style.css_ file in the `ui` folder.
You can find more info about changing the appearance of Gtk applications on the Web yourself.

View File

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

View File

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

14
build/linux/build-deb.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/bash
VER="3.2.4_Beta"
B_PATH="dist/DemonEditor"
DEB_PATH="$B_PATH/usr/share/demoneditor"
mkdir -p $B_PATH
cp -TRv deb $B_PATH
rsync --exclude=app/ui/lang --exclude=app/ui/icons --exclude=__pycache__ -arv ../../app $DEB_PATH
cd dist
fakeroot dpkg-deb -Zxz --build DemonEditor
mv DemonEditor.deb DemonEditor_$VER.deb
rm -R DemonEditor

View File

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

View File

@@ -0,0 +1,25 @@
Package: demon-editor
Version: 3.2.4-Beta
Section: utils
Priority: optional
Architecture: all
Essential: no
Depends: python3 (>= 3.6),
python3-requests,
python3-gi,
python3-gi-cairo,
gir1.2-notify-0.7,
p7zip-full
Recommends: libmpv1,
python3-chardet,
libgtksourceview (>= 3.0)
Maintainer: Dmitriy Yefremov <dmitry.v.yefremov@gmail.com>
Homepage: https://dyefremov.github.io/DemonEditor
Description: Enigma2 channel and satellite list editor
Editing bouquets, channels, satellites, importing services,
downloading picons and updating satellites from the Web,
extended support of IPTV, assignment of EPG from DVB or
XML for IPTV services, playback of IPTV or other streams
directly from the bouquet list, control panel (via HTTP API),
ability to view EPG and manage timers (via HTTP API),
simple FTP client (experimental).

View File

@@ -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-2022 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

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

View File

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

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