Compare commits

...

343 Commits

Author SHA1 Message Date
DYefremov
4cf19e5413 minor code adjustment
* Preventing mixins [str -> Enum] bug in Python 3.11 [100458]
2023-04-22 18:33:18 +03:00
audi06_19
cd4a814838 Turkish translation update (#176) 2023-04-22 17:31:42 +03:00
DYefremov
d7c49f50f2 bump version 2023-04-19 14:07:53 +03:00
DYefremov
b61b8e16fa small fix for Web import options init 2023-04-19 09:56:17 +03:00
DYefremov
56d2a3e991 fix dir copy for FTP tab 2023-04-17 23:16:00 +03:00
DYefremov
b65ea9c0d3 vlc module update 2023-04-17 13:53:46 +03:00
DYefremov
a62ee8f378 extension API improvement 2023-04-15 16:49:20 +03:00
DYefremov
0db8ee6d47 prevent focus lack for main views 2023-04-14 10:48:37 +03:00
DYefremov
ed41b01f63 it *.mo file update 2023-04-13 10:27:24 +03:00
mapi68
c2eaecb8b8 Italian translation update (#174) 2023-04-13 10:20:26 +03:00
DYefremov
f5313f2c40 minor translations update 2023-04-12 23:40:50 +03:00
DYefremov
c6a0b80fdd bouquets gen improvement 2023-04-12 23:30:07 +03:00
DYefremov
7813aeb059 reverted lowercase for some prefixes (#170) 2023-04-12 09:34:50 +03:00
DYefremov
3a0e5c09a1 it *.mo file update 2023-04-09 10:42:48 +03:00
mapi68
ab6a44dc3f Italian translation update (#173) 2023-04-09 10:38:27 +03:00
audi06_19
caefb4587d Update: Turkish translation update (#172) 2023-04-09 10:37:42 +03:00
DYefremov
93ff78d7ce version update 2023-04-08 14:32:00 +03:00
DYefremov
1d583ecd99 fix root bouquet rename 2023-04-08 14:24:12 +03:00
DYefremov
d4914ac451 Russian, Belarusian and German translations update 2023-04-08 00:21:16 +03:00
DYefremov
6207b6a10d Spanish and Dutch translations update 2023-04-07 20:51:13 +03:00
DYefremov
1eb8fe621d save satellites selection for web import 2023-04-07 19:01:16 +03:00
DYefremov
79b41b1661 minor refactoring of settings save 2023-04-07 18:37:40 +03:00
DYefremov
7a36ba8148 minor fix 2023-04-07 13:22:39 +03:00
DYefremov
08e970fc96 save source for web import 2023-04-07 08:42:04 +03:00
DYefremov
9681fcbc79 saving settings for web import 2023-04-07 00:27:55 +03:00
DYefremov
7b002b208f added additional yt link checking (#170) 2023-04-06 14:55:30 +03:00
DYefremov
95a1732f01 satellites merge support for web import (#165) 2023-04-06 00:24:45 +03:00
DYefremov
57e4fdff7f changed case for yt links prefixes (#170) 2023-04-04 22:37:06 +03:00
DYefremov
89c456993f allowed to rename root bouquet 2023-04-03 09:12:18 +03:00
DYefremov
2f3fc31023 minor fix 2023-04-02 12:49:00 +03:00
DYefremov
5c18e49cf7 added 'No' prefix for yt links (#170) 2023-04-02 12:42:59 +03:00
DYefremov
64530bcb85 prefix support for yt playlist import (#170) 2023-03-30 18:57:01 +03:00
DYefremov
4d472609b4 url prefix elems for yt dialog 2023-03-30 00:13:25 +03:00
DYefremov
640b995ab8 prefix support for yt links (#170) 2023-03-27 00:01:08 +03:00
DYefremov
42980a988f minor ui changes for IPTV dialogs 2023-03-26 18:18:32 +03:00
DYefremov
64c5f28957 enabled quality change for yt links 2023-03-26 16:22:12 +03:00
DYefremov
a32bf230cf fix yt link double check on edit (#170) 2023-03-25 16:04:11 +03:00
audi06_19
b8cac728a8 Turkish translation update (#166) 2023-03-12 16:28:55 +03:00
DYefremov
079f07cfd2 mpv module update 2023-03-09 08:47:57 +03:00
DYefremov
9a5884cc9a it *.mo file update 2023-03-06 16:49:55 +03:00
mapi68
115237a10f Italian translation update (#163) 2023-03-06 16:46:29 +03:00
DYefremov
994bd0ee1c Russian, Belarusian and German translations update 2023-03-05 14:01:02 +03:00
DYefremov
27e5b373a3 version update 2023-03-04 20:43:15 +03:00
DYefremov
43c05b1739 fix namespace for web import 2023-03-04 20:00:13 +03:00
DYefremov
bd96c286e9 option skip c-band for web import dialog 2023-03-04 18:15:16 +03:00
DYefremov
1bded41eab close options button for import dialog 2023-03-04 16:00:41 +03:00
DYefremov
5f0f51679c sat list init on update dialog start 2023-03-04 12:28:10 +03:00
DYefremov
380bb3150b updated it *.mo file 2023-03-04 11:15:23 +03:00
mapi68
d1a7a486a2 Italian translation update (#162) 2023-03-04 11:07:52 +03:00
DYefremov
1be167bec3 sorting by pos on bouquets generation 2023-03-04 00:27:37 +03:00
DYefremov
dd3e88589c Russian, Belarusian and German translations update 2023-03-03 17:38:29 +03:00
DYefremov
c5a2df6d7d prevent duplicate for web import 2023-03-03 12:04:04 +03:00
DYefremov
c9fc3803c7 fix FEC value for web import 2023-03-03 10:49:06 +03:00
DYefremov
6afd518cfc fix adding duplicates to the main list 2023-03-03 10:36:46 +03:00
DYefremov
02a51c9b56 bouquets generation for kos web source 2023-03-02 17:50:31 +03:00
DYefremov
c96cfa0e1b category and lang for kingofsat 2023-03-01 23:02:11 +03:00
DYefremov
08bc4ff4c4 bouquets only option for import dialog 2023-03-01 13:36:20 +03:00
DYefremov
ae2b78e990 README update 2023-02-28 00:23:00 +03:00
DYefremov
88be9fe49c updated it *.mo file 2023-02-27 23:48:01 +03:00
mapi68
9dae9b7219 Italian translation update (#161) 2023-02-27 23:42:58 +03:00
DYefremov
f296a6c90b paths normalization in settings 2023-02-27 23:40:41 +03:00
DYefremov
0486776d83 minor fix 2023-02-27 10:31:20 +03:00
DYefremov
3e6146d825 .gitignore update 2023-02-23 16:31:03 +03:00
audi06_19
9b97341e70 add .gitignore and Turkish translation update (#160)
* add .gitignore

* Update: Turkish translation update
2023-02-23 14:36:34 +03:00
DYefremov
41714136e6 README update 2023-02-23 14:31:56 +03:00
DYefremov
f781cbb9f6 bump version 2023-02-23 11:05:50 +03:00
DYefremov
177be7679b added version for extensions 2023-02-22 13:43:24 +03:00
DYefremov
a65914a48c duplicate removal support for the fav list (#159) 2023-02-22 11:55:34 +03:00
DYefremov
e3ffc2e24b playback fix for empty list 2023-02-22 11:46:11 +03:00
DYefremov
79415c69c5 minor refactoring 2023-02-21 15:39:22 +03:00
DYefremov
54c4e02cee bump version 2023-02-18 21:15:50 +03:00
DYefremov
f580c5d83c added header bar option on macOS 2023-02-18 18:35:09 +03:00
DYefremov
e0cbdb2f8d header bar option refactoring 2023-02-18 11:30:06 +03:00
DYefremov
839855c076 reworking of the program options tab 2023-02-18 09:37:05 +03:00
audi06_19
87db39590b Update: Turkish translation update (#158) 2023-02-18 08:28:41 +03:00
DYefremov
ebe58903e5 corrected service type setting for IPTV 2023-02-17 21:51:35 +03:00
DYefremov
ff8d4e5321 fix ref assignment from XML source (#154) 2023-02-17 20:17:17 +03:00
DYefremov
907415a2c9 added path to extensions 2023-02-17 13:15:12 +03:00
DYefremov
f9afffbdb3 *.spec file correction 2023-02-17 11:25:32 +03:00
DYefremov
326856b1e3 added config for extensions 2023-02-17 00:37:43 +03:00
DYefremov
865a326fe9 minor translations update 2023-02-16 20:05:27 +03:00
DYefremov
dd2661c6c9 picons location corrections (#157) 2023-02-16 19:36:15 +03:00
DYefremov
623e5a17f5 small fix to set bouquet lock 2023-02-16 16:29:08 +03:00
DYefremov
18c1fa736b display channel id as tooltip for EPG dialog (#154) 2023-02-16 14:42:20 +03:00
DYefremov
0e6142c751 parental lock and hiding for bouquets 2023-02-15 22:41:37 +03:00
DYefremov
c274c265c6 fix set lock for bouquet 2023-02-15 21:59:30 +03:00
DYefremov
777c09c9b8 storing parental lock for bouquets 2023-02-15 14:17:25 +03:00
DYefremov
12a68a4dbb displaying parental lock for bouquets 2023-02-15 13:04:54 +03:00
DYefremov
eab869d4d5 hidden marker for bouquets 2023-02-15 12:34:06 +03:00
DYefremov
aec76eec45 filtering by sat position for XML source (#154) 2023-02-14 17:36:43 +03:00
DYefremov
bdab316ba7 small resizing fix 2023-02-14 10:55:11 +03:00
DYefremov
771ecb696f updated it *.mo file 2023-02-12 14:43:15 +03:00
mapi68
8993fbed5d Italian translation update (#156) 2023-02-12 14:36:32 +03:00
DYefremov
f3cad81a7d build-deb file update 2023-02-11 13:06:13 +03:00
DYefremov
c1cf343f69 logging support for extensions 2023-02-11 12:32:33 +03:00
DYefremov
f998f66a35 version update 2023-02-11 10:09:46 +03:00
DYefremov
9eb4cdc574 option to enable extensions 2023-02-11 10:04:04 +03:00
DYefremov
20120e0db4 README for extensions 2023-02-10 17:42:54 +03:00
DYefremov
3446bb225c moved extensions module 2023-02-10 14:04:42 +03:00
DYefremov
191975bd14 extension as singleton 2023-02-10 11:59:14 +03:00
DYefremov
f4be52a202 basic extensions API 2023-02-09 23:51:14 +03:00
DYefremov
ba2272cf13 updated tr *.mo file 2023-02-06 21:49:56 +03:00
audi06_19
7bd3fcd9a6 Turkish translation update (#155) 2023-02-06 21:46:10 +03:00
DYefremov
e54719ca2c bump version 2023-02-04 12:37:16 +03:00
DYefremov
0c1c44c866 columns resizing for EPG tab 2023-02-04 11:03:12 +03:00
DYefremov
06b82251ef duration format correction for win 2023-02-02 00:09:51 +03:00
DYefremov
fb929ec723 playback pause on mouse click 2023-02-01 13:24:51 +03:00
DYefremov
fd1c1bfd6e layout init correction 2023-01-31 12:45:26 +03:00
DYefremov
c48a08b239 header bar for yt dialog 2023-01-30 17:36:22 +03:00
DYefremov
b647b0a338 delayed cache init 2023-01-30 14:41:16 +03:00
DYefremov
9b608eeb74 added custom header bar widget 2023-01-29 12:59:57 +03:00
DYefremov
07e55b3f1e header bar activation for macOS 2023-01-29 00:33:59 +03:00
DYefremov
7e639f5637 header bar for update dialog 2023-01-29 00:26:28 +03:00
DYefremov
5570d47cae header bar activation for macOS 2023-01-28 21:45:01 +03:00
DYefremov
5c49c0d123 refs assignment from filtered list only (#154) 2023-01-28 17:55:19 +03:00
DYefremov
ee6dd511b5 clearing EPG on profile change 2023-01-28 14:43:13 +03:00
DYefremov
1f847233b3 copyright update 2023-01-28 14:03:04 +03:00
DYefremov
835e1af8e4 README update 2023-01-28 14:00:22 +03:00
DYefremov
50e0d8b66a time display correction for XMLTV 2023-01-28 13:53:15 +03:00
DYefremov
fb0789664a version update 2023-01-27 22:41:49 +03:00
DYefremov
5dd39492f2 updated it *.mo file 2023-01-27 22:30:55 +03:00
mapi68
33be9f21a2 Italian translation update (#153) 2023-01-27 22:16:45 +03:00
DYefremov
7acc9ae74f Russian, Belarusian and German translations update 2023-01-27 17:04:18 +03:00
DYefremov
d492022232 some ui corrections 2023-01-27 13:44:41 +03:00
DYefremov
b034995130 added unlimited buffer option 2023-01-26 22:06:29 +03:00
DYefremov
f037b3554d disabled fixed height for picon views 2023-01-26 20:30:20 +03:00
DYefremov
7f1f27da57 minor adjustment 2023-01-26 00:53:28 +03:00
DYefremov
d7f3afecb0 increased EPG tab columns width (#150) 2023-01-26 00:17:38 +03:00
DYefremov
f309005c52 additional time columns for EPG tab (#150) 2023-01-25 17:54:09 +03:00
DYefremov
25661816e7 'replace existing' for bouquets tab 2023-01-25 01:00:16 +03:00
DYefremov
a2652cef4b fix update EPG from alt service 2023-01-25 00:29:23 +03:00
DYefremov
adbc9ad322 option 'replace existing' for import dialog (#127) 2023-01-24 23:02:28 +03:00
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
102 changed files with 23837 additions and 15311 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
*.pyc
*.pyo
*__pycache__
.idea

View File

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

View File

@@ -26,17 +26,19 @@ Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
[<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)
[<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)
**To increase program functionality you can use [extensions](https://github.com/DYefremov/demoneditor-extensions).**
#### Keyboard shortcuts
* **Ctrl + X** - only in bouquet list.
* **Ctrl + C** - only in services list.
Clipboard is **"rubber"**. There is an accumulation before the insertion!
* **Ctrl + C** - only in services list.
* **Ctrl + Insert** - copies the selected channels from the main list to the bouquet
beginning or inserts (creates) a new bouquet.
* **Ctrl + BackSpace** - copies the selected channels from the main list to the bouquet end.
* **Ctrl + E** - edit.
* **Ctrl + R, F2** - rename.
* **Ctrl + 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.
@@ -85,7 +87,8 @@ 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.
**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.
@@ -107,7 +110,7 @@ just load your data via *"File/Open"* and press *"Save"*. When importing separat
**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.
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.

View File

@@ -6,16 +6,16 @@ from threading import Thread, Timer
from gi.repository import GLib
_LOG_FILE = "demon-editor.log"
_DATE_FORMAT = "%d-%m-%y %H:%M:%S"
LOG_DATE_FORMAT = "%d-%m-%y %H:%M:%S"
LOGGER_NAME = "main_logger"
LOG_FORMAT = "%(asctime)s %(message)s"
def init_logger():
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)
@@ -77,6 +77,25 @@ 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. """

View File

@@ -2,7 +2,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
@@ -33,8 +33,9 @@ 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, quote
@@ -44,14 +45,15 @@ from urllib.request import (urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicA
from app.commons import log, run_task
from app.settings import SettingsType
BQ_FILES_LIST = ("tv", "radio", # enigma 2
"services.xml", "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):
@@ -88,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:
@@ -111,50 +113,55 @@ 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.
Creates a leaf directory and all intermediate ones. This is recursive.
Creates a leaf directory and all intermediate ones. This is recursive.
"""
os.makedirs(os.path.join(save_path, path), exist_ok=True)
dir_path = os.path.join(save_path, path, "")
os.makedirs(dir_path, exist_ok=True)
current_path = self.pwd()
files = []
self.dir(path, files.append)
try:
self.cwd(path)
except all_errors as e:
msg = f"Download dir error: {e}".rstrip()
log(msg)
return f"500 {msg}"
for f in files:
f_data = self.get_file_data(f)
f_path = f_data[8]
if f_data[0][0] == "d":
try:
os.makedirs(os.path.join(save_path, f_path), exist_ok=True)
except OSError as e:
msg = "Download dir error: {}".format(e).rstrip()
log(msg)
return "500 " + msg
else:
self.download_dir(f_path, save_path, callback)
self.download_dir(f_path, dir_path, callback)
else:
try:
self.download_file(f_path, save_path, callback)
self.download_file(f_path, dir_path, callback)
except OSError as e:
log("Download dir error: {}".format(e).rstrip())
log(f"Download dir error: {e}".rstrip())
self.cwd(current_path)
resp = "226 Transfer complete."
msg = "Copy directory {}. Status: {}".format(path, resp)
msg = f"Copying directory: {path}. Status: {resp}"
log(msg)
if callback:
@@ -170,7 +177,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
@@ -200,7 +207,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)
@@ -223,18 +230,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
@@ -258,12 +264,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:
@@ -283,7 +289,7 @@ class UtfFTP(FTP):
if dest:
try:
self.cwd(dest)
except Error as e:
except all_errors as e:
callback(str(e))
return
@@ -291,10 +297,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)
@@ -318,10 +324,10 @@ class UtfFTP(FTP):
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"
@@ -335,10 +341,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)
@@ -363,7 +369,7 @@ class UtfFTP(FTP):
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")
callback("FTP OK.")
save_path = settings.profile_data_path
os.makedirs(os.path.dirname(save_path), exist_ok=True)
# bouquets
@@ -383,41 +389,35 @@ def download_data(*, settings, download_type=DownloadType.ALL, callback=log, fil
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
use_http = s_type is SettingsType.ENIGMA_2 and settings.use_http
data_path = settings.profile_data_path
host, port, use_ssl = settings.host, settings.http_port, settings.http_use_ssl
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
tn, ht = None, None # Telnet, HTTP.
try:
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(settings.user, settings.password, base_url, callback, use_ssl, s_type)
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)
if s_type is SettingsType.ENIGMA_2:
params = urlencode({"text": message, "type": 2, "timeout": 5})
@@ -428,24 +428,22 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
if s_type is SettingsType.ENIGMA_2 and download_type is DownloadType.ALL:
time.sleep(5)
ht.send((f"{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)
tn = telnet(host=host, user=user, password=password, timeout=settings.telnet_timeout)
next(tn)
# Terminate Enigma2 or Neutrino.
callback("Telnet initialization ...\n")
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
@@ -457,7 +455,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)
@@ -465,23 +463,65 @@ 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.profile_picons_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:
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 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."))
ht.send((f"{url}powerstate?newstate=4", "Wakeup from Standby."))
if not settings.keep_power_mode:
ht.send((f"{url}powerstate?newstate=4", "Wakeup from Standby."))
else:
ht.send((f"{url}reloadchannels", "Reloading channels..."))
@@ -494,12 +534,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()
@@ -519,7 +571,7 @@ def http(user, password, url, callback, use_ssl=False, s_type=SettingsType.ENIGM
if s_type is SettingsType.ENIGMA_2:
resp = resp.get("e2statetext", None)
callback(f"HTTP: {message} {'Successful.' if resp and message else ''}\n")
callback(f"HTTP: {message} {'Successful.' if resp and message else ''}")
def telnet(host, port=23, user="", password="", timeout=5):
@@ -538,19 +590,27 @@ 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(str, Enum):
ZAP = "zap?sRef="
@@ -576,6 +636,8 @@ class HttpAPI:
VOL = "vol?set=set"
# EPG
EPG = "epgservice?sRef="
EPG_NOW = "epgnow?bRef="
EPG_MULTI = "epgmulti?bRef="
# Timer
TIMER = ""
TIMER_LIST = "timerlist"
@@ -590,8 +652,21 @@ class HttpAPI:
N_ZAP = "zapto"
N_STREAM = "build_playlist?id="
def __str__(self):
return self.value
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"
@@ -600,12 +675,23 @@ class HttpAPI:
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"
def __str__(self):
return self.value
class Power(str, Enum):
""" Args for HttpRequestType [POWER] class. """
TOGGLE_STANDBY = "0"
@@ -615,10 +701,15 @@ class HttpAPI:
WAKEUP = "4"
STANDBY = "5"
def __str__(self):
return self.value
PARAM_REQUESTS = {Request.REMOTE,
Request.POWER,
Request.VOL,
Request.EPG,
Request.EPG_NOW,
Request.EPG_MULTI,
Request.TIMER,
Request.RECORDINGS,
Request.N_ZAP}
@@ -630,7 +721,7 @@ class HttpAPI:
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
@@ -642,7 +733,7 @@ class HttpAPI:
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
@@ -662,7 +753,7 @@ class HttpAPI:
def done_callback(f):
callback(f.result())
future = self._executor.submit(self.get_response, req_type, url, data, self._s_type)
future = self._executor.submit(self.get_response, req_type, url, data, self._s_type, timeout)
future.add_done_callback(done_callback)
@run_task
@@ -699,9 +790,9 @@ class HttpAPI:
self._executor.shutdown()
@staticmethod
def get_response(req_type, url, data=None, s_type=SettingsType.ENIGMA_2):
def get_response(req_type, url, data=None, s_type=SettingsType.ENIGMA_2, timeout=_TIMEOUT):
try:
with urlopen(Request(url, data=data), timeout=10) as f:
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:
@@ -712,7 +803,7 @@ class HttpAPI:
if req_type is HttpAPI.Request.TEST:
raise e
return {"error_code": e.code}
except (URLError, RemoteDisconnected, ConnectionResetError) as e:
except OSError as e:
if req_type is HttpAPI.Request.TEST:
raise e
except ETree.ParseError as e:
@@ -732,7 +823,7 @@ class HttpAPI:
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:
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:
@@ -791,7 +882,7 @@ 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)
@@ -799,7 +890,7 @@ def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message
t_msg = "Connection test!"
if s_type is SettingsType.ENIGMA_2:
params = urlencode({"text": t_msg, "type": 2, "timeout": timeout})
params = "statusinfo" if skip_message else f"message?{params}"
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}"
@@ -814,9 +905,11 @@ def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message
data = HttpAPI.get_post_data(base_url, password, user) if s_type is SettingsType.ENIGMA_2 else None
try:
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("e2statetext", "")
return resp.get("e2enigmaversion", "")
return resp
except (RemoteDisconnected, URLError, HTTPError) as e:
raise TestException(e)

View File

@@ -2,7 +2,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
@@ -68,10 +68,9 @@ def write_bouquet(path, bq, s_type):
write_bouquet(path, bq)
@run_task
def write_bouquets(path, bouquets, s_type, force_bq_names=False):
def write_bouquets(path, bouquets, s_type, force_bq_names=False, blacklist=None):
if s_type is SettingsType.ENIGMA_2:
BouquetsWriter(path, bouquets, force_bq_names).write()
BouquetsWriter(path, bouquets, force_bq_names, blacklist).write()
elif s_type is SettingsType.NEUTRINO_MP:
write_neutrino_bouquets(path, bouquets)

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
@@ -57,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):
@@ -195,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"}
@@ -209,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",
@@ -247,17 +255,18 @@ def is_transponder_valid(tr: Transponder):
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)
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

@@ -2,7 +2,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
@@ -47,16 +47,19 @@ class BouquetsWriter:
If "force_bq_names" then naming the files using the name of the bouquet.
Some images may have problems displaying the favorites list!
"""
_SERVICE = '#SERVICE 1:7:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
_SERVICE = '#SERVICE 1:{}:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
_MARKER = "#SERVICE 1:64:{:X}:0:0:0:0:0:0:0::{}\n"
_SPACE = "#SERVICE 1:832:D:{}:0:0:0:0:0:0:\n"
_LOCKED = '1:{}:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet'
_ALT = '#SERVICE 1:134:1:0:0:0:0:0:0:0:FROM BOUQUET "{}" ORDER BY bouquet\n'
_ALT_PAT = r"[<>:\"/\\|?*\-\s]"
def __init__(self, path, bouquets, force_bq_names=False):
def __init__(self, path, bouquets, force_bq_names=False, blacklist=None):
self._path = path
self._bouquets = bouquets
self._force_bq_names = force_bq_names
self._black_list = set() if blacklist is None else blacklist
self._marker_index = 1
self._space_index = 0
self._alt_names = set()
@@ -96,9 +99,15 @@ class BouquetsWriter:
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))
bq_type = 2 if bqs.type == BqType.RADIO.value else 1
# Parental lock.
locked = self._LOCKED.format(ServiceType.SERVICE, bq_type, bq_name, bqs.type)
self._black_list.add(locked) if bq.locked else self._black_list.discard(locked)
# Hiding.
s_type = ServiceType.HIDDEN if bq.hidden else ServiceType.BOUQUET
line.append(self._SERVICE.format(s_type, bq_type, bq_name, bqs.type))
with open(f"{self._path}bouquets.{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):
@@ -136,7 +145,7 @@ class BouquetsWriter:
else:
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):
@@ -148,7 +157,7 @@ class BouquetsWriter:
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") as file:
with open(f"{self._path}userbouquet.{file_name}.{bq_type}", "w", encoding="utf-8", newline="\n") as file:
file.writelines(bouquet)
@@ -156,21 +165,25 @@ class ServiceType(Enum):
SERVICE = "0"
BOUQUET = "7" # Sub bouquet.
MARKER = "64"
SPACE = "832" # Hidden marker.
SPACE = "832"
ALT = "134" # Alternatives.
UDP = "256"
HIDDEN = "519" # Skip, hide.
@classmethod
def _missing_(cls, value):
log("Error. No matching service type [{} {}] was found.".format(cls.__name__, value))
return cls.SERVICE
def __str__(self):
return self.value
class BouquetsReader:
""" Class for reading and parsing bouquets. """
_ALT_PAT = re.compile(".*alternatives\\.+(.*)\\.([tv|radio]+).*")
_BQ_PAT = re.compile(".*userbouquet\\.+(.*)\\.+[tv|radio].*")
_SUB_BQ_PAT = re.compile(".*subbouquet\\.+(.*)\\.([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"]
@@ -197,25 +210,30 @@ class BouquetsReader:
for line in file.readlines():
if "#SERVICE" in line:
name = re.match(self._BQ_PAT, line)
s_data = line.split(":")
s_type = ServiceType(s_data[1])
if name:
b_name = name.group(1)
prefix, b_name = name.group(1), name.group(2)
if b_name in b_names:
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(f"Bouquet file 'userbouquet.{b_name}.{bq_type}' has duplicate name: {rb_name}")
log(f"Bouquet file '{prefix}.{b_name}.{bq_type}' has duplicate name: {rb_name}")
real_b_names[rb_name] += 1
rb_name = f"{rb_name} {real_b_names[rb_name]}"
else:
real_b_names[rb_name] = 0
# Locked, hidden.
s_data[:2] = "10"
locked = ":".join(s_data).rstrip()
hidden = s_type is ServiceType.HIDDEN
bouquets[2].append(Bouquet(rb_name, bq_type, services, None, None, b_name))
bouquets[2].append(Bouquet(rb_name, bq_type, services, locked, hidden, b_name))
else:
s_data = line.split(":")
if len(s_data) == 12 and s_data[1] == ServiceType.MARKER.value:
if len(s_data) == 12 and s_type is ServiceType.MARKER:
b_name = f"{_MARKER_PREFIX}{s_data[-1].strip()}"
bouquets[2].append(Bouquet(b_name, BqType.MARKER.value, [], None, None, line.strip()))
else:

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
@@ -30,6 +30,7 @@
import re
from app.commons import log
from app.eparser.satxml import get_pos_str
from app.ui.uicommons import CODED_ICON, LOCKED_ICON, HIDE_ICON
from .blacklist import get_blacklist
from ..ecommons import Service, POLARIZATION, FEC, SERVICE_TYPE, Flag, T_FEC, TrType, FEC_DEFAULT, T_SYSTEM
@@ -220,8 +221,7 @@ class LameDbReader:
freq = f"{int(freq) // 1000}"
rate = f"{int(rate) // 1000}"
if tr_type is TrType.Satellite:
pos = int(pos)
pos = f"{abs(pos / 10):0.1f}{'W' if pos < 0 else 'E'}"
pos = get_pos_str(int(pos))
except ValueError as e:
log(f"Parse error [parse_services]: {e}")
@@ -310,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()
@@ -335,7 +335,7 @@ class LameDbWriter:
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

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 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
@@ -38,7 +38,7 @@ 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 = " {}:{}:{}:{:X}:{:X}:{:X}:{:X}:0:0:0:{}:{}\n#DESCRIPTION: {}\n"
ENIGMA2_FAV_ID_FORMAT = " {}:{}:{:X}:{: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"
@@ -153,12 +153,13 @@ def export_to_m3u(path, bouquet, s_type, url=None):
file.writelines(lines)
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. """
def get_fav_id(url, name, settings_type, params=None, st_type=None, s_id=0, srv_type=1, force_quote=True):
""" Returns fav id depending on the settings type. """
if settings_type is SettingsType.ENIGMA_2:
st_type = st_type or StreamType.NONE_TS.value
params = params or (0, 0, 0, 0)
return ENIGMA2_FAV_ID_FORMAT.format(st_type, s_id, srv_type, *params, quote(url), name, name, None)
url = quote(url) if force_quote else url
return ENIGMA2_FAV_ID_FORMAT.format(st_type, s_id, srv_type, *params, url, name, name, None)
elif settings_type is SettingsType.NEUTRINO_MP:
return NEUTRINO_FAV_ID_FORMAT.format(url, "", 0, None, None, None, None, "", "", 1)

View File

@@ -2,7 +2,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
@@ -31,7 +31,6 @@ import os
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
_FILE = "bouquets.xml"
@@ -72,8 +71,8 @@ def parse_bouquets(file, name, bq_type):
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,
locked=locked == "1",
hidden=hidden == "1",
file=SP.join("{}{}{}".format(k, KSP, v) for k, v in bq_attrs.items())))
if BqType(bq_type) is BqType.BOUQUET:

View File

@@ -1,120 +1,200 @@
""" Module foe parsing Satellites.xml
# -*- 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
#
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)}"
def get_pos_str(pos: int) -> str:
""" Converts satellite position int value to readable string. """
return f"{abs(pos / 10):0.1f}{'W' if pos < 0 else 'E'}"
if __name__ == "__main__":

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 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
@@ -39,15 +39,17 @@ from textwrap import dedent
SEP = os.sep
HOME_PATH = str(Path.home())
CONFIG_PATH = HOME_PATH + "{}.config{}demon-editor{}".format(SEP, SEP, SEP)
CONFIG_PATH = HOME_PATH + f"{SEP}.config{SEP}demon-editor{SEP}"
CONFIG_FILE = CONFIG_PATH + "config.json"
DATA_PATH = HOME_PATH + "{}DemonEditor{}".format(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"
USE_HEADER_BAR = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
class Defaults(Enum):
""" Default program settings """
@@ -61,25 +63,30 @@ class Defaults(Enum):
# 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/")
"/media/cf/picon/",
"/hdd/picon/",
"/usb/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 = "{}backup{}".format(DATA_PATH, SEP)
PICON_PATH = "{}picons{}".format(DATA_PATH, SEP)
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
UNLIMITED_COPY_BUFFER = False
EXTENSIONS_SUPPORT = False
FORCE_BQ_NAMES = False
HTTP_API_SUPPORT = True
ENABLE_YT_DL = False
@@ -92,10 +99,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)
ACTIVE_TRANSCODING_PRESET = f"720p TV{SEP}device"
class SettingsType(IntEnum):
@@ -109,12 +117,14 @@ class SettingsType(IntEnum):
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
@@ -132,6 +142,7 @@ class SettingsType(IntEnum):
"services_path": srv_path,
"user_bouquet_path": srv_path,
"satellites_xml_path": sat_path,
"epg_dat_path": epg_path,
"picons_path": picons_path}
@@ -150,6 +161,12 @@ 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 = 2
@@ -270,6 +287,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"))
@@ -358,6 +383,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"))
@@ -396,7 +429,7 @@ class Settings:
@default_data_path.setter
def default_data_path(self, value):
self._settings["default_data_path"] = value
self._settings["default_data_path"] = Settings.normalize_path(value)
@property
def default_backup_path(self):
@@ -404,7 +437,7 @@ class Settings:
@default_backup_path.setter
def default_backup_path(self, value):
self._settings["default_backup_path"] = value
self._settings["default_backup_path"] = Settings.normalize_path(value)
@property
def default_picon_path(self):
@@ -412,11 +445,11 @@ class Settings:
@default_picon_path.setter
def default_picon_path(self, value):
self._settings["default_picon_path"] = value
self._settings["default_picon_path"] = Settings.normalize_path(value)
@property
def profile_data_path(self):
return "{}data{}{}{}".format(self.default_data_path, SEP, self._current_profile, SEP)
return f"{self.default_data_path}data{SEP}{self._current_profile}{SEP}"
@profile_data_path.setter
def profile_data_path(self, value):
@@ -425,8 +458,8 @@ class Settings:
@property
def profile_picons_path(self):
if self.profile_folder_is_default:
return "{}picons{}".format(self.profile_data_path, SEP)
return "{}{}{}".format(self.default_picon_path, self._current_profile, SEP)
return f"{self.profile_data_path}picons{SEP}"
return f"{self.default_picon_path}{self._current_profile}{SEP}"
@profile_picons_path.setter
def profile_picons_path(self, value):
@@ -435,20 +468,20 @@ class Settings:
@property
def profile_backup_path(self):
if self.profile_folder_is_default:
return "{}backup{}".format(self.profile_data_path, SEP)
return "{}{}{}".format(self.default_backup_path, self._current_profile, SEP)
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 records_path(self):
return self._settings.get("records_path", Defaults.RECORDS_PATH.value)
def recordings_path(self):
return self._settings.get("recordings_path", Defaults.RECORDINGS_PATH.value)
@records_path.setter
def records_path(self, value):
self._settings["records_path"] = value
@recordings_path.setter
def recordings_path(self, value):
self._settings["recordings_path"] = Settings.normalize_path(value)
# ******** Streaming ********* #
@@ -492,6 +525,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
@@ -503,6 +552,30 @@ 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
@@ -539,6 +612,22 @@ class Settings:
def v5_support(self, value):
self._settings["v5_support"] = value
@property
def unlimited_copy_buffer(self):
return self._settings.get("unlimited_copy_buffer", Defaults.UNLIMITED_COPY_BUFFER.value)
@unlimited_copy_buffer.setter
def unlimited_copy_buffer(self, value):
self._settings["unlimited_copy_buffer"] = value
@property
def extensions_support(self):
return self._settings.get("extensions_support", Defaults.EXTENSIONS_SUPPORT.value)
@extensions_support.setter
def extensions_support(self, value):
self._settings["extensions_support"] = value
@property
def force_bq_names(self):
return self._settings.get("force_bq_names", Defaults.FORCE_BQ_NAMES.value)
@@ -579,14 +668,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")
@@ -623,6 +704,14 @@ class Settings:
# *********** Appearance *********** #
@property
def use_header_bar(self):
return self._settings.get("use_header_bar", USE_HEADER_BAR)
@use_header_bar.setter
def use_header_bar(self, value):
self._settings["use_header_bar"] = value
@property
def list_font(self):
return self._settings.get("list_font", "")
@@ -694,6 +783,14 @@ class Settings:
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)
@@ -748,7 +845,7 @@ class Settings:
def is_darwin(self):
return IS_DARWIN
# *********** Download dialog *********** #
# ************* Download ************** #
@property
def use_http(self):
@@ -766,6 +863,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
@@ -790,12 +903,16 @@ class Settings:
# **************** 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())
def get_settings(config_file=CONFIG_FILE, default_settings=None):
if not os.path.isfile(config_file) or os.stat(config_file).st_size == 0:
df = Settings.get_default_settings() if default_settings is None else default_settings
Settings.write_settings(df, config_file=config_file)
with open(CONFIG_FILE, "r", encoding="utf-8") as config_file:
return json.load(config_file)
with open(config_file, "r", encoding="utf-8") as cf:
try:
return json.load(cf)
except ValueError as e:
raise SettingsReadException(e)
@staticmethod
def get_default_settings(profile_name="default"):
@@ -814,7 +931,7 @@ class Settings:
"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
"records_path": Defaults.RECORDINGS_PATH.value
}
@staticmethod
@@ -825,10 +942,14 @@ class Settings:
"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=" ")
def write_settings(config, config_path=CONFIG_PATH, config_file=CONFIG_FILE):
os.makedirs(os.path.dirname(config_path), exist_ok=True)
with open(config_file, "w", encoding="utf-8") as cf:
json.dump(config, cf, indent=" ")
@staticmethod
def normalize_path(path):
return f"{os.path.normpath(path)}{SEP}"
if __name__ == "__main__":

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 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
@@ -27,53 +27,343 @@
""" Module for working with epg.dat file. """
import abc
import os
import re
import shutil
import struct
from datetime import datetime
import sys
import xml.etree.ElementTree as ET
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 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", "start", "end", "length", "desc", "event_data"])
EpgEvent.__new__.__defaults__ = ("N/A", "N/A", 0, 0, 0, "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
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
"""
def __init__(self, path):
self._path = path
self._refs = {}
self._desc = {}
def download(self, clb=None):
pass
def get_current_events(self, ids: set) -> dict:
pass
def get_refs(self):
return self._refs.keys()
def get_services(self):
return self._refs
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()
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_epg_refs(path):
""" The read algorithm was taken from the eEPGCache::load() function from this source:
https://github.com/OpenPLi/enigma2/blob/develop/lib/dvb/epgcache.cpp#L955
"""
refs = set()
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)
with open(path, mode="rb") as f:
crc = struct.unpack("<I", f.read(4))[0]
if crc != int(0x98765432):
raise ValueError("Epg file has incorrect byte order!")
header = f.read(13).decode()
if header == "ENIGMA_EPG_V7":
epg_ver = 7
elif header == "ENIGMA_EPG_V8":
epg_ver = 8
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:
raise ValueError("Unsupported format of epd.dat file!")
log(f"{self.__class__.__name__} [download] error: {request.reason}")
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"
if clb:
clb()
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}"
def get_current_events(self, names: set) -> dict:
events = {}
for j in range(events_size):
_type, _len = struct.unpack(_type_read_str, f.read(_len_read_size))
f.read(10)
n_crc = (_len - 10) // 4
if n_crc > 0:
[f.read(4) for n in range(n_crc)]
dt = datetime.utcnow()
utc = dt.timestamp()
offset = datetime.now() - dt
refs.add(service_id)
for srv in filter(lambda s: any(name in names for name in s.names), self._ids.values()):
ev = max(filter(lambda s: s.start < utc, srv.events), key=lambda x: x.start, default=None)
if ev:
start = datetime.fromtimestamp(ev.start) + offset
end_time = datetime.fromtimestamp(ev.duration) + offset
start = start.timestamp()
end_time = end_time.timestamp()
return refs
for n in srv.names:
events[n] = EpgEvent(n, ev.title, start, end_time, int(ev.duration), 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:
@@ -85,32 +375,39 @@ class ChannelsParser:
refs = []
dom = parse(path)
description = "".join(n.data + "\n" for n in dom.childNodes if n.nodeType == Node.COMMENT_NODE)
pos_pat = re.compile(r"^\d+\.\d+[EW]$")
for elem in dom.getElementsByTagName("channels"):
c_count = 0
comment_count = 0
current_data = ""
data = ""
ch_id = None
pos = None
ch_type = BqServiceType.DEFAULT
if elem.hasChildNodes():
for n in elem.childNodes:
if n.nodeType == Node.ELEMENT_NODE:
ch_id = n.getAttribute("id")
if n.nodeType == Node.COMMENT_NODE:
c_count += 1
comment_count += 1
txt = n.data.strip()
if re.match(pos_pat, txt):
pos = txt
if comment_count:
comment_count -= 1
else:
ref_data = current_data.split(":")
refs.append(BouquetService(name=txt,
type=BqServiceType.DEFAULT,
data="{}:{}:{}:{}".format(*ref_data[3:7]).upper(),
num="{}:{}:{}".format(*ref_data[3:6]).upper()))
refs.append(BouquetService(name=txt, type=ch_type, data=data.upper(), num=(pos, ch_id)))
if n.hasChildNodes():
for s_node in n.childNodes:
if s_node.nodeType == Node.TEXT_NODE:
comment_count -= 1
current_data = s_node.data
data = s_node.data
return refs, description
@staticmethod

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 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
@@ -32,7 +32,7 @@ from datetime import datetime
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
@@ -63,6 +63,7 @@ class Player(Gtk.DrawingArea):
parent = widget.get_parent()
parent.connect("play", self.on_play)
parent.connect("stop", self.on_stop)
parent.connect("pause", self.on_pause)
self.show()
def get_play_mode(self):
@@ -107,6 +108,9 @@ class Player(Gtk.DrawingArea):
def on_stop(self, widget, state):
self.stop()
def on_pause(self, widget, state):
self.pause()
def on_release(self, widget, state):
self.release()
@@ -241,7 +245,7 @@ class MpvPlayer(Player):
self._is_playing = True
def pause(self):
pass
self._player.pause = not self._player.pause
def set_time(self, time):
pass
@@ -330,7 +334,11 @@ class GstPlayer(Player):
self._is_playing = False
def pause(self):
self._player.set_state(self.STATE.PAUSED)
state = self._player.get_state(self.STATE.NULL).state
if state == self.STATE.PLAYING:
self._player.set_state(self.STATE.PAUSED)
elif state == self.STATE.PAUSED:
self._player.set_state(self.STATE.PLAYING)
def set_time(self, time):
pass
@@ -526,9 +534,9 @@ 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)
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)

View File

@@ -16,25 +16,26 @@
# <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')
dll = ctypes.util.find_library('libmpv-2.dll') or ctypes.util.find_library('mpv-1.dll')
if dll is None:
raise OSError('Cannot find mpv-1.dll in your system %PATH%. One way to deal with this is to ship mpv-1.dll '
'with your script and put the directory your script is in into %PATH% before "import mpv": '
'os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] '
'If mpv-1.dll is located elsewhere, you can add that path to os.environ["PATH"].')
raise OSError(
'Cannot find [lib]mpv-*.dll in your system %PATH%. One way to deal with this is to ship [lib]mpv-*.dll '
'with your script and put the directory your script is in into %PATH% before "import mpv": '
'os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] '
'If mpv-1.dll is located elsewhere, you can add that path to os.environ["PATH"].')
backend = CDLL(dll)
fs_enc = 'utf-8'
else:
@@ -569,10 +570,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 +612,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 +916,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

@@ -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/2.2.0", "Referer": ""}
_HEADER = {"User-Agent": "DemonEditor/3.0.0", "Referer": ""}
_LINK_PATTERN = re.compile(r"((.*)-\d+x\d+)-(.*)_by_chocholousek.7z$")
_FILE_PATTERN = re.compile(b"\\s+(1_.*\\.png).*")
_FILE_PATTERN = re.compile(b"\\s+(\\w+\\.png).*")
def __init__(self, picon_ids=set(), appender=log):
self._perm_links = {}
@@ -116,11 +116,11 @@ class PiconsCzDownloader:
with requests.get(url=provider.url, headers=self._HEADER, stream=True) as request:
if request.reason == "OK":
dest = f"{picons_path}{provider.on_id}.7z"
self._appender(f"Downloading: {provider.url}\n")
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(f"Extracting: {provider.on_id}\n")
self._appender(f"Extracting: {provider.on_id}")
self.extract(dest, picons_path, picon_ids)
else:
log(f"{self.__class__.__name__} [download] error: {request.reason}")
@@ -136,7 +136,7 @@ class PiconsCzDownloader:
raise PiconsError("7-zip [7z] archiver not found!")
if IS_WIN:
exe = f"C:{os.sep}Program Files{os.sep}7-Zip{os.sep}{exe}.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!")
@@ -220,7 +220,8 @@ class PiconsCzDownloader:
"piconoled": "o96",
"piconblack80": "b50",
"piconblack3d": "b50",
"piconwin11": "win11220"
"piconwin11": "win11220",
"piconSNPtransparent": "t50"
}
def get_name_map(self):
@@ -486,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:
@@ -508,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.
@@ -524,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

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 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
@@ -121,7 +121,12 @@ class SatellitesParser(HTMLParser):
self._current_cell = []
self._rows = []
self._source = source
self.pls_modes = {v: k for k, v in PLS_MODE.items()}
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":
@@ -164,16 +169,15 @@ class SatellitesParser(HTMLParser):
for src in SatelliteSource.get_sources(self._source):
try:
request = requests.get(url=src, headers=_HEADERS, timeout=_TIMEOUT)
resp = requests.get(url=src, headers=_HEADERS, timeout=_TIMEOUT)
except requests.exceptions.RequestException as e:
log(f"Getting satellite list error: {repr(e)}")
return []
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:
@@ -183,6 +187,8 @@ class SatellitesParser(HTMLParser):
elif source is SatelliteSource.KINGOFSAT:
return self.get_satellites_for_king_of_sat()
return []
def get_satellite(self, sat):
pos = sat[1]
return Satellite(name=f"{pos} {sat[0]}", flags="0",
@@ -317,7 +323,7 @@ class SatellitesParser(HTMLParser):
pls_code = None
pls_mode = None
if pls:
pls_mode = self.pls_modes.get(pls.group(1), None)
pls_mode = self.PLS_MODES.get(pls.group(1), None)
pls_code = pls.group(2)
if is_ids:
@@ -327,7 +333,12 @@ class SatellitesParser(HTMLParser):
if is_transponder_valid(tr):
n_trs.append(tr)
tr = Transponder(f"{freq}000", f"{sr}000", pol, fec, sys, mod, pls_mode, pls_code, None)
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)
@@ -358,12 +369,17 @@ class SatellitesParser(HTMLParser):
sys, mod, sr, fec = res.group(1), res.group(2), res.group(3), res.group(4)
mod = mod.strip() if mod else "Auto"
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)
pls_mode = self.PLS_MODES.get(pls_mode, None)
if plp is not None:
log(f"Detected T2-MI transponder! [{freq} {sr} {pol}] ")
tr = Transponder(f"{freq}000", f"{sr}000", pol, fec, sys, mod, pls_mode, pls_code, is_id)
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)
@@ -385,7 +401,7 @@ class SatellitesParser(HTMLParser):
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)
pls_id = self.PLS_MODES.get(pls_id, None)
res = re.match(mod_pat, row[9])
if not res:
@@ -400,7 +416,12 @@ class SatellitesParser(HTMLParser):
if t2_mi:
log(f"Detected T2-MI transponder! [{freq} {sr} {pol}] ")
tr = Transponder(freq, f"{sr}000", pol, fec, sys, mod, pls_id, pls_code, is_id)
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)
@@ -408,7 +429,7 @@ class SatellitesParser(HTMLParser):
class ServicesParser(HTMLParser):
""" Services parser for LYNGSAT source. """
def __init__(self, source=SatelliteSource.LYNGSAT, entities=False, separator=' '):
def __init__(self, source=SatelliteSource.LYNGSAT, entities=False, separator=' ', lang=None):
HTMLParser.__init__(self)
@@ -431,6 +452,12 @@ class ServicesParser(HTMLParser):
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._lang = "en"
if lang:
langs = {"en", "fr", "nl", "de", "se", "no", "pt", "es", "it", "pl",
"cz", "gr", "fi", "ar", "tr", "ru", "sc", "ro", "hu", "sq"}
lang, _, _ = lang.partition("_")
self._lang = lang if lang in langs else self._lang
self._parse_html_entities = entities
self._separator = separator
@@ -478,9 +505,11 @@ class ServicesParser(HTMLParser):
if a[0] != "title":
continue
txt = a[1]
if txt and txt.startswith("Id: "):
sep = "Id: "
if txt and txt.startswith(sep):
# Saving the 'short' name.
self._current_cell.text = txt.lstrip("Id: ")
_, sep, name = txt.partition(sep)
self._current_cell.text = name
elif tag == "img":
img_link = attrs[0][1]
if self._source is SatelliteSource.LYNGSAT:
@@ -557,7 +586,7 @@ class ServicesParser(HTMLParser):
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.url = f"https://{self._lang}.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
@@ -707,11 +736,12 @@ class ServicesParser(HTMLParser):
s_type = self._S_TYPES.get(s_type, "3")
_s_type = SERVICE_TYPE.get(s_type, SERVICE_TYPE.get("3"))
reg, grp = r[3].text, r[4].text
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,
services.append(Service(flags, "s", None, name, reg, grp, pkg, _s_type, None, picon_id,
sid, str(freq), sr, pol, fec, sys, pos, data_id, fav_id, multi_tr or tr))
return services
@@ -720,9 +750,9 @@ class ServicesParser(HTMLParser):
""" 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)
fec = get_key_by_value(FEC, fec) or "0"
# For negative (West) positions: 3600 - numeric position value!!!
namespace = f"{3600 - pos if pos < 0 else pos:04x}0000"
namespace = f"{3600 - abs(pos) if pos < 0 else pos:04x}0000"
tr_flag = 1
roll_off = 0 # 35% DVB-S2/DVB-S (default)
pilot = 2 # Auto

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 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
@@ -129,8 +129,7 @@ class YouTube:
fmts = streaming_data.get("formats", None) if streaming_data else 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}
links = {Quality[i["itag"]]: i["url"] for i in fmts if i.get("itag", -1) in Quality and "url" in i}
if links and title:
return links, title.replace("+", " ")

View File

@@ -58,21 +58,30 @@
</item>
</section>
<section>
<item>
<submenu>
<attribute name="label" translatable="yes">FTP-transfer</attribute>
<attribute name="action">app.on_download</attribute>
</item>
<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>
<attribute name="action">app.preferences</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Exit</attribute>
<attribute name="action">app.on_close_app</attribute>
<attribute name="action">app.quit</attribute>
</item>
</section>
</submenu>
@@ -138,10 +147,20 @@
<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>
<item>
<attribute name="label" translatable="yes">Alternate window title</attribute>
<attribute name="action">app.set_alternate_title</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
</section>
</submenu>
<submenu id="tools_menu">
@@ -162,6 +181,8 @@
<attribute name="action">app.on_logs_show</attribute>
</item>
</section>
<section id="extension_section">
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">FTP client</attribute>
@@ -177,7 +198,7 @@
<section>
<item>
<attribute name="label" translatable="yes">About</attribute>
<attribute name="action">app.on_about_app</attribute>
<attribute name="action">app.about</attribute>
</item>
</section>
</submenu>
@@ -186,19 +207,19 @@
<section>
<item>
<attribute name="label" translatable="yes">About</attribute>
<attribute name="action">app.on_about_app</attribute>
<attribute name="action">app.about</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Settings</attribute>
<attribute name="action">app.on_settings</attribute>
<attribute name="action">app.preferences</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">Exit</attribute>
<attribute name="action">app.on_close_app</attribute>
<attribute name="action">app.quit</attribute>
</item>
</section>
</menu>
@@ -270,10 +291,19 @@
</item>
</section>
<section>
<item>
<submenu>
<attribute name="label" translatable="yes">FTP-transfer</attribute>
<attribute name="action">app.on_download</attribute>
</item>
<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>
@@ -338,10 +368,20 @@
<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>
<item>
<attribute name="label" translatable="yes">Alternate window title</attribute>
<attribute name="action">app.set_alternate_title</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
</section>
</submenu>
<submenu>
@@ -362,6 +402,8 @@
<attribute name="action">app.on_logs_show</attribute>
</item>
</section>
<section id="mac_extension_section">
</section>
</submenu>
<submenu>
<attribute name="label" translatable="yes">FTP client</attribute>

View File

@@ -2,7 +2,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
@@ -33,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.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, IS_GNOME_SESSION
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, HeaderBar
class RestoreType(Enum):
@@ -63,7 +64,7 @@ class BackupDialog:
self._settings = settings
self._s_type = settings.setting_type
self._data_path = self._settings.profile_data_path
self._backup_path = self._settings.profile_backup_path or "{}backup{}".format(self._data_path, os.sep)
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)
@@ -74,9 +75,10 @@ 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)
if self._settings.use_header_bar:
header_bar = HeaderBar()
self._dialog_window.set_titlebar(header_bar)
button_box = builder.get_object("main_button_box")
@@ -106,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)
@@ -129,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)
@@ -165,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)
@@ -234,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"), SEP)
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
@@ -255,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">
@@ -198,16 +198,23 @@ 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">False</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Details</property>
<property name="valign">center</property>
<property name="margin_right">15</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"/>
<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>
@@ -228,84 +235,170 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkPaned" id="main_paned">
<object class="GtkFrame" id="main_frame">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_top">5</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="wide_handle">True</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkScrolledWindow" id="main_view_scrolled_window">
<object class="GtkPaned" id="main_paned">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</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="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">
<object class="GtkBox" id="backups_box">
<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>
<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>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>

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-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
@@ -28,785 +28,15 @@
""" Receiver control module via HTTP API. """
import os
from datetime import datetime
from enum import Enum
from ftplib import all_errors
from urllib.parse import quote
import re
from gi.repository import GLib
from .dialogs import get_builder, show_dialog, DialogType, get_message
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Page, Column, KeyboardKey, IS_GNOME_SESSION
from .dialogs import get_builder, get_message
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH
from ..commons import run_task, run_with_delay, log, run_idle
from ..connections import HttpAPI, UtfFTP
from ..eparser.ecommons import BqServiceType
from ..settings import IS_DARWIN, PlayStreamsMode, IS_LINUX, IS_WIN
class EpgTool(Gtk.Box):
def __init__(self, app, *args, **kwargs):
super().__init__(*args, **kwargs)
self._app = app
self._app.connect("fav-changed", self.on_service_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}
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers,
objects=("epg_frame", "epg_model", "epg_filter_model", "epg_sort_model"))
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")
builder.get_object("epg_filter_button").bind_property("active", self._filter_entry, "visible")
self.pack_start(builder.get_object("epg_frame"), True, True, 0)
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(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):
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._model.clear()
list(map(self._model.append, (self.get_event_row(e) for e in epg.get("event_list", []))))
self._app.wait_dialog.hide()
def get_event_row(self, event):
title = event.get("e2eventtitle", "") or ""
desc = event.get("e2eventdescription", "") or ""
start = int(event.get("e2eventstart", "0"))
start_time = datetime.fromtimestamp(start)
end_time = datetime.fromtimestamp(start + int(event.get("e2eventduration", "0")))
time = f"{start_time.strftime('%A, %H:%M')} - {end_time.strftime('%H:%M')}"
return title, 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) if txt in s.upper()), False)
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(UI_RESOURCES_PATH + "control.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):
self.set_begins_date(self.get_begins_date())
def on_timer_ends_set(self, action, value=None):
self.set_ends_date(self.get_ends_date())
def get_begins_date(self):
date = self._timer_begins_calendar.get_date()
return datetime(year=date.year, month=date.month + 1, day=date.day,
hour=int(self._timer_begins_hr_button.get_value()),
minute=int(self._timer_begins_min_button.get_value()))
def set_begins_date(self, date):
hour = date.hour
minute = date.minute
self._timer_begins_hr_button.set_value(hour)
self._timer_begins_min_button.set_value(minute)
self._timer_begins_calendar.select_day(date.day)
self._timer_begins_calendar.select_month(date.month - 1, date.year)
self._timer_begins_entry.set_text(f"{date.year}-{date.month}-{date.day} {hour}:{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}-{date.day} {hour}:{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)
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. """
ev_time_start = datetime.fromtimestamp(start_time) or datetime.now()
ev_time_end = datetime.fromtimestamp(end_time) or datetime.now()
self._timer_begins_entry.set_text(ev_time_start.strftime(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_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(UI_RESOURCES_PATH + "control.glade", handlers, objects=("timers_frame", "timer_model"))
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._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._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, %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_timers_press(self, view, event):
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(view.get_model()) > 0:
self.on_timer_edit()
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)
if key is KeyboardKey.DELETE:
self.on_timer_remove()
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._main_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)
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("profile-changed", self.init)
self._settings = settings
self._ftp = None
# 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_recording_remove": self.on_recording_remove,
"on_recordings_model_changed": self.on_recordings_model_changed}
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers,
objects=("recordings_box", "recordings_model", "rec_paths_model"))
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._recordings_count_label = builder.get_object("recordings_count_label")
self.pack_start(builder.get_object("recordings_box"), True, True, 0)
if settings.alternate_layout:
self.on_layout_changed(app, True)
self.init()
self.show()
def clear_data(self):
self._rec_view.get_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)
@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):
model = self._rec_view.get_model()
model.clear()
list(map(model.append, (self.get_recordings_row(r) for r in recordings.get("recordings", []))))
def get_recordings_row(self, rec):
service = rec.get("e2servicename")
title = rec.get("e2title", "")
time = datetime.fromtimestamp(int(rec.get("e2time", "0"))).strftime("%A, %H:%M")
length = rec.get("e2length", "0")
file = rec.get("e2filename", "")
desc = rec.get("e2description", "")
return service, title, time, length, file, desc, rec
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_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, value=None):
""" Removes recordings via FTP. """
if show_dialog(DialogType.QUESTION, self._app.app_window) != Gtk.ResponseType.OK:
return
model, paths = self._rec_view.get_selection().get_selected_rows()
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"):
GLib.idle_add(model.remove, itr)
else:
self._app.show_error_message(resp)
break
def on_recordings_model_changed(self, model, path, itr=None):
self._recordings_count_label.set_text(str(len(model)))
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_SERVICE, Column.REC_TIME, Column.REC_LEN, Column.REC_FILE, Column.REC_DESC):
self._rec_view.get_column(c).set_visible(state)
from ..connections import HttpAPI
from ..settings import IS_DARWIN, IS_LINUX, IS_WIN
class ControlTool(Gtk.Box):
@@ -820,10 +50,11 @@ class ControlTool(Gtk.Box):
self._pix = None
handlers = {"on_volume_changed": self.on_volume_changed,
"on_screenshot_draw": self.on_screenshot_draw}
"on_screenshot_draw": self.on_screenshot_draw,
"on_network_toggled": self.on_network_toggled,
"on_network_view_query_tooltip": self.on_network_view_query_tooltip}
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers,
objects=("control_box", "volume_adjustment"))
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers)
self.pack_start(builder.get_object("control_box"), True, True, 0)
self._remote_box = builder.get_object("remote_box")
@@ -838,7 +69,14 @@ class ControlTool(Gtk.Box):
self._ber_level_bar = builder.get_object("ber_level_bar")
self._agc_level_bar = builder.get_object("agc_level_bar")
self._volume_button = builder.get_object("volume_button")
self._header_box = builder.get_object("control_header_box")
self._screenshot_button_box = builder.get_object("screenshot_button_box")
# Network.
self._network_button = builder.get_object("control_network_button")
self._network_model = builder.get_object("network_model")
self.init_actions(app)
if settings.alternate_layout:
self.on_layout_changed(app, True)
@@ -846,19 +84,37 @@ class ControlTool(Gtk.Box):
def init_actions(self, app):
# Remote controller actions.
app.set_action("on_one", lambda a, v: self.on_remote_action(HttpAPI.Remote.ONE))
app.set_action("on_two", lambda a, v: self.on_remote_action(HttpAPI.Remote.TWO))
app.set_action("on_three", lambda a, v: self.on_remote_action(HttpAPI.Remote.THREE))
app.set_action("on_four", lambda a, v: self.on_remote_action(HttpAPI.Remote.FOUR))
app.set_action("on_five", lambda a, v: self.on_remote_action(HttpAPI.Remote.FIVE))
app.set_action("on_six", lambda a, v: self.on_remote_action(HttpAPI.Remote.SIX))
app.set_action("on_seven", lambda a, v: self.on_remote_action(HttpAPI.Remote.SEVEN))
app.set_action("on_eight", lambda a, v: self.on_remote_action(HttpAPI.Remote.EIGHT))
app.set_action("on_nine", lambda a, v: self.on_remote_action(HttpAPI.Remote.NINE))
app.set_action("on_zero", lambda a, v: self.on_remote_action(HttpAPI.Remote.ZERO))
app.set_action("on_up", lambda a, v: self.on_remote_action(HttpAPI.Remote.UP))
app.set_action("on_down", lambda a, v: self.on_remote_action(HttpAPI.Remote.DOWN))
app.set_action("on_left", lambda a, v: self.on_remote_action(HttpAPI.Remote.LEFT))
app.set_action("on_right", lambda a, v: self.on_remote_action(HttpAPI.Remote.RIGHT))
app.set_action("on_next", lambda a, v: self.on_remote_action(HttpAPI.Remote.NEXT))
app.set_action("on_back", lambda a, v: self.on_remote_action(HttpAPI.Remote.BACK))
app.set_action("on_info", lambda a, v: self.on_remote_action(HttpAPI.Remote.INFO))
app.set_action("on_ok", lambda a, v: self.on_remote_action(HttpAPI.Remote.OK))
app.set_action("on_menu", lambda a, v: self.on_remote_action(HttpAPI.Remote.MENU))
app.set_action("on_exit", lambda a, v: self.on_remote_action(HttpAPI.Remote.EXIT))
app.set_action("on_epg", lambda a, v: self.on_remote_action(HttpAPI.Remote.EPG))
app.set_action("on_ch_up", lambda a, v: self.on_remote_action(HttpAPI.Remote.CH_UP))
app.set_action("on_ch_down", lambda a, v: self.on_remote_action(HttpAPI.Remote.CH_DOWN))
app.set_action("on_red", lambda a, v: self.on_remote_action(HttpAPI.Remote.RED))
app.set_action("on_green", lambda a, v: self.on_remote_action(HttpAPI.Remote.GREEN))
app.set_action("on_yellow", lambda a, v: self.on_remote_action(HttpAPI.Remote.YELLOW))
app.set_action("on_blue", lambda a, v: self.on_remote_action(HttpAPI.Remote.BLUE))
app.set_action("on_audio", lambda a, v: self.on_remote_action(HttpAPI.Remote.AUDIO))
app.set_action("on_tv", lambda a, v: self.on_remote_action(HttpAPI.Remote.TV))
app.set_action("on_radio", lambda a, v: self.on_remote_action(HttpAPI.Remote.RADIO))
app.set_action("on_fav", lambda a, v: self.on_remote_action(HttpAPI.Remote.FAV))
# Playback.
app.set_action("on_prev_media", lambda a, v: self.on_player_action(HttpAPI.Request.PLAYER_PREV))
app.set_action("on_play_media", lambda a, v: self.on_player_action(HttpAPI.Request.PLAYER_PLAY))
@@ -876,7 +132,13 @@ class ControlTool(Gtk.Box):
app.set_action("on_screenshot_osd", self.on_screenshot_osd)
def on_layout_changed(self, app, alt_layout):
self._remote_box.reorder_child(self._remote_box.get_children()[0], 1)
children = self._remote_box.get_children()
self._remote_box.reorder_child(children[0], len(children) - 1)
self._remote_box.reorder_child(children[-1], 0)
pack_type = Gtk.PackType.END if alt_layout else Gtk.PackType.START
self._header_box.set_child_packing(self._network_button, False, False, 0, pack_type)
pack_type = Gtk.PackType.START if alt_layout else Gtk.PackType.END
self._header_box.set_child_packing(self._screenshot_button_box, False, False, 0, pack_type)
# ***************** Remote controller ********************* #
@@ -996,13 +258,86 @@ class ControlTool(Gtk.Box):
def update_signal(self, sig):
snr = sig.get("e2snr", "0 %").strip() if sig else "0 %"
snr_db = sig.get("e2snrdb", "0 dB").strip() if sig else "0 dB"
acg = sig.get("e2acg", "0 %").strip() if sig else "0 %"
ber = (sig.get("e2ber", None) or "").strip() if sig else ""
# Labels.
self._snr_value_label.set_text(snr)
self._snr_value_label.set_text(f"{snr_db} ({snr})")
self._agc_value_label.set_text(acg)
self._ber_value_label.set_text(ber)
# Level bars.
self._snr_level_bar.set_value(int(snr.strip("%N/A") or 0))
self._agc_level_bar.set_value(int(acg.rstrip("%N/A") or 0))
self._ber_level_bar.set_value(int(ber.rstrip("N/A") or 0))
# ***************** Network explorer ********************** #
def on_network_toggled(self, button):
self._network_model.clear()
if button.get_active():
self.update_network()
@run_task
def update_network(self):
pattern = re.compile(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})')
ips = [match for match in re.findall(pattern, os.popen("arp -a").read())]
for ip in ips:
if not self._network_button.get_active():
break
url = f"http://{ip}/web/{HttpAPI.Request.INFO.value}"
try:
resp = HttpAPI.get_response(HttpAPI.Request.INFO, url, timeout=5)
except OSError as e:
log(f"{ip} {e}")
else:
if resp.get("e2distroversion", None):
log(f"Receiver found. Model: {resp.get('e2model', 'N/A')} [{ip} ]")
self.append_box_data(resp)
@run_idle
def append_box_data(self, data):
ip = data.get('e2lanip', 'N/A')
itr = self._network_model.append((data.get("e2model", "N/A"), ip, None, data, None))
GLib.timeout_add_seconds(3, self.check_power_state, itr, priority=GLib.PRIORITY_LOW)
def on_network_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
result = view.get_dest_row_at_pos(x, y)
if not result:
return False
path, pos = result
model = view.get_model()
data = model[path][3]
dist = data.get("e2distroversion", "N/A")
img = data.get("e2imageversion", "N/A")
txt = f"Distro version: {dist}\nImage version: {img}"
tooltip.set_text(txt)
view.set_tooltip_row(tooltip, path)
return True
def check_power_state(self, itr):
active = self._network_button.get_active()
if not active:
return False
data = self._network_model.get_value(itr, 3)
url = f"http://{data.get('e2lanip', 'N/A')}/web/powerstate"
self.update_power_state(itr, url)
return active
@run_task
def update_power_state(self, itr, url):
try:
resp = HttpAPI.get_response(HttpAPI.Request.POWER, url, timeout=2)
except OSError as e:
log(e)
else:
state = get_message("On" if resp.get("e2instandby", "N/A").strip() == "false" else "Standby")
GLib.idle_add(self._network_model.set_value, itr, 2, state)
if __name__ == "__main__":
pass

View File

@@ -3,7 +3,7 @@
The MIT License (MIT)
Copyright (c) 2018-2022 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
@@ -27,11 +27,11 @@ Author: Dmitriy Yefremov
-->
<interface domain="demon-editor">
<requires lib="gtk+" version="3.16"/>
<requires lib="gtk+" version="3.22"/>
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor. -->
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkAboutDialog" id="about_dialog">
<property name="can_focus">False</property>
@@ -40,8 +40,8 @@ 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">2.2.0 Beta</property>
<property name="copyright">2018-2022 Dmitriy Yefremov
<property name="version">3.6.1 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>

View File

@@ -2,7 +2,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
@@ -28,14 +28,14 @@
""" 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
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN, IS_GNOME_SESSION
from app.settings import SEP, IS_WIN, USE_HEADER_BAR
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN
class Dialog(Enum):
@@ -157,7 +157,7 @@ def get_file_chooser_dialog(transient, text, settings, action_type, file_filter,
def get_input_dialog(transient, text):
builder, dialog = get_dialog_from_xml(DialogType.INPUT, transient, use_header=IS_GNOME_SESSION)
builder, dialog = get_dialog_from_xml(DialogType.INPUT, transient, use_header=USE_HEADER_BAR)
entry = builder.get_object("input_entry")
entry.set_text(text if text else "")
response = dialog.run()
@@ -223,9 +223,9 @@ def get_builder(path, handlers=None, use_str=False, objects=None, tag="property"
if use_str:
if objects:
builder.add_objects_from_string(get_dialogs_string(path, tag).format(use_header=IS_GNOME_SESSION), objects)
builder.add_objects_from_string(get_dialogs_string(path, tag).format(use_header=USE_HEADER_BAR), objects)
else:
builder.add_from_string(get_dialogs_string(path, tag).format(use_header=IS_GNOME_SESSION))
builder.add_from_string(get_dialogs_string(path, tag).format(use_header=USE_HEADER_BAR))
else:
if objects:
builder.add_objects_from_string(get_dialogs_string(path, tag), objects)
@@ -238,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,573 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2
The MIT License (MIT)
Copyright (c) 2018-2021 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface domain="demon-editor">
<requires lib="gtk+" version="3.16"/>
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor. -->
<!-- interface-copyright 2018 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkImage" id="download_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">network-receive-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="send_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">network-transmit-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkWindow" id="download_dialog_window">
<property name="width_request">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-symbolic</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="GtkBox" id="header_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="profile_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Profile:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="profile_combo_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="focus_on_click">False</property>
<property name="active">0</property>
<property name="has_frame">False</property>
<signal name="changed" handler="on_profile_changed" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<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">0</property>
</packing>
</child>
<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">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_settings_box_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkBox" id="main_settings_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="orientation">vertical</property>
<property name="spacing">10</property>
<child>
<object class="GtkGrid" id="main_settings_bo">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">5</property>
<property name="column_spacing">10</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">10</property>
<property name="margin_right">10</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">1</property>
</packing>
</child>
<child>
<object class="GtkButtonBox" id="button_box">
<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">2</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="log_bar_frame">
<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="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkInfoBar" id="log_bar">
<property name="can_focus">False</property>
<property name="baseline_position">bottom</property>
<property name="message_type">other</property>
<property name="show_close_button">True</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="log_bar_button_box">
<property name="can_focus">False</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" id="log_bar_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="height_request">100</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>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="can_focus">False</property>
<property name="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,201 +0,0 @@
# -*- 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 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 SettingsDialog
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._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._profile_combo_box = builder.get_object("profile_combo_box")
# Info.
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._log_bar = builder.get_object("log_bar")
self._log_bar.bind_property("visible", builder.get_object("log_bar_frame"), "visible")
self._log_bar.connect("response", lambda b, r: b.set_visible(False))
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.profile_data_path)
is_enigma = self._s_type is SettingsType.ENIGMA_2
self._webtv_radio_button.set_visible(not is_enigma)
self._use_http_switch.set_active(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):
dialog = SettingsDialog(self._dialog_window, self._settings)
dialog.show()
if dialog.is_updated():
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._log_bar.set_visible, 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.profile_data_path or self._data_path_entry.get_text()
os.makedirs(os.path.dirname(data_path), exist_ok=True)
backup_path = self._settings.profile_backup_path or self._settings.default_backup_path
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

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 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
@@ -26,25 +26,30 @@
#
""" 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
from app.connections import download_data, DownloadType, HttpAPI
from app.eparser.ecommons import BouquetService, BqServiceType
from app.settings import SEP
from app.tools.epg import EPG, ChannelsParser
from app.settings import SEP, EpgSource, IS_WIN
from app.tools.epg import EPG, ChannelsParser, EpgEvent, XmlTvReader
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, IS_GNOME_SESSION
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, Page, HeaderBar
class RefsSource(Enum):
@@ -52,9 +57,344 @@ class RefsSource(Enum):
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_with_delay(5)
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)
@run_with_delay(2)
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, **kwargs):
super().__init__(**kwargs)
self._current_bq = None
self._app = app
self._app.connect("fav-changed", self.on_service_changed)
self._app.connect("profile-changed", self.on_profile_changed)
self._app.connect("bouquet-changed", self.on_bouquet_changed)
self._app.connect("filter-toggled", self.on_filter_toggled)
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_button = builder.get_object("epg_filter_button")
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 data functions.
renderer = builder.get_object("epg_start_renderer")
column = builder.get_object("epg_start_column")
column.set_cell_data_func(renderer, self.start_data_func)
renderer = builder.get_object("epg_end_renderer")
column = builder.get_object("epg_end_column")
column.set_cell_data_func(renderer, self.end_data_func)
renderer = builder.get_object("epg_length_renderer")
column = builder.get_object("epg_length_column")
column.set_cell_data_func(renderer, self.duration_data_func)
# Time formats.
self._time_fmt = "%a %x - %H:%M"
self._duration_fmt = f"%{'' if IS_WIN else '-'}Hh %Mm"
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)
def on_profile_changed(self, app, prf):
self.update_epg_data()
@run_idle
def update_epg_data(self, epg=None):
self._event_count_label.set_text("0")
self._model.clear()
if epg:
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):
s_name = event.get("e2eventservicename", "")
title = event.get("e2eventtitle", "") or ""
desc = event.get("e2eventdescription", "") or ""
desc = desc.strip()
start, duration = int(event.get("e2eventstart", "0")), int(event.get("e2eventduration", "0"))
return EpgEvent(s_name, title, start, start + duration, duration, desc, event)
def start_data_func(self, column, renderer, model, itr, data):
value = datetime.fromtimestamp(model.get_value(itr, Column.EPG_START))
renderer.set_property("text", value.strftime(self._time_fmt))
def end_data_func(self, column, renderer, model, itr, data):
value = datetime.fromtimestamp(model.get_value(itr, Column.EPG_END))
renderer.set_property("text", value.strftime(self._time_fmt))
def duration_data_func(self, column, renderer, model, itr, data):
value = datetime.utcfromtimestamp(model.get_value(itr, Column.EPG_LENGTH))
renderer.set_property("text", value.strftime(self._duration_fmt))
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,
Column.EPG_SERVICE,
Column.EPG_TITLE,
Column.EPG_DESC) if txt in s.upper()), False)
def on_filter_toggled(self, app, value):
if self._app.page is Page.EPG:
active = not self._filter_button.get_active()
self._filter_button.set_active(active)
if active:
self._filter_entry.grab_focus()
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, bouquet_name):
def __init__(self, app, bouquet_name):
handlers = {"on_close_dialog": self.on_close_dialog,
"on_apply": self.on_apply,
@@ -62,6 +402,7 @@ class EpgDialog:
"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,
@@ -81,14 +422,13 @@ class EpgDialog:
"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}
"on_bq_cursor_changed": self.on_bq_cursor_changed,
"on_source_view_query_tooltip": self.on_source_view_query_tooltip}
self._app = app
self._services = {}
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 = bouquet
self._bouquet_name = bouquet_name
self._current_ref = []
self._enable_dat_filter = False
@@ -96,8 +436,9 @@ class EpgDialog:
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(UI_RESOURCES_PATH + "epg.glade", handlers)
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)
@@ -108,14 +449,16 @@ class EpgDialog:
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._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")
@@ -134,9 +477,8 @@ class EpgDialog:
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"))
if self._settings.use_header_bar:
header_bar = HeaderBar(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")
@@ -162,19 +504,29 @@ class EpgDialog:
@run_idle
def on_apply(self, item):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.OK:
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)
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:
self._ex_services[fav_id] = srv._replace(fav_id=fav_id)
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
@@ -190,7 +542,6 @@ class EpgDialog:
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()
@@ -202,12 +553,16 @@ class EpgDialog:
refs = None
if self._enable_dat_filter:
try:
refs = EPG.get_epg_refs(self._epg_dat_path_entry.get_text() + "epg.dat")
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:
@@ -218,13 +573,13 @@ class EpgDialog:
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[:]]
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
@@ -237,7 +592,7 @@ class EpgDialog:
factor = self._app.DEL_FACTOR / 4
for index, srv in enumerate(filtered):
self._services_model.append((srv.service, srv.pos, srv.fav_id))
self._services_model.append((srv.service, srv.pos, srv.fav_id, srv.picon_id, srv.picon_id))
if index % factor == 0:
yield True
@@ -309,12 +664,20 @@ class EpgDialog:
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)
refs = refs or {}
factor = self._app.DEL_FACTOR / 4
for index, srv in enumerate(s_refs):
self._services_model.append((srv.name, " ", srv.data))
ref_data = srv.data.split(":")
ref = ":".join(ref_data[3:6])
if ref in refs:
continue
data = ":".join(ref_data[3:7])
pos, ch_id = srv.num
pos = pos or " "
self._services_model.append((srv.name, pos, data, "_".join(ref_data).rstrip("_"), ch_id))
if index % factor == 0:
yield True
@@ -342,6 +705,25 @@ class EpgDialog:
if path:
self._filter_entry.set_text(model[path][Column.FAV_SERVICE] or "")
def on_source_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
result = view.get_dest_row_at_pos(x, y)
if not result:
return False
path, pos = result
ch_id = view.get_model()[path][-1]
if not ch_id:
return False
if self._refs_source is RefsSource.XML:
text = f"ID = {ch_id}"
else:
text = f"{get_message('Service reference')}: {ch_id.rstrip('.png')}"
tooltip.set_text(text)
view.set_tooltip_row(tooltip, path)
return True
@run_idle
def on_save_to_xml(self, item):
response = show_dialog(DialogType.CHOOSER, self._dialog, settings=self._settings)
@@ -374,7 +756,7 @@ class EpgDialog:
tr = {ord(k): ord(v) for k, v in zip(*symbols)}
source = {}
for row in self._services_model:
for row in self._source_view.get_model():
name = re.sub("\\W+", "", str(row[0])).upper()
name = name.translate(tr) if use_cyrillic else name
source[name] = row
@@ -407,6 +789,10 @@ class EpgDialog:
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:
@@ -415,29 +801,46 @@ class EpgDialog:
fav_id = row[Column.FAV_ID]
fav_id_data = fav_id.split(":")
fav_id_data[3:7] = data[-1].split(":")
fav_id_data[3:7] = data[-3].split(":")
if data[-2]:
row[Column.FAV_POS] = data[-2]
p_data = data[-2].split("_")
if p_data:
fav_id_data[2] = p_data[2]
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
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}"
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 not button.get_active():
self._filter_entry.set_text("")
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()
@run_with_delay(1)
def on_filter_changed(self, entry):
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()
return model is None or model == "None" or txt in model.get_value(itr, 0).upper()
pos = model.get_value(itr, 1)
pos = self._sat_positions is None 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)
@@ -451,8 +854,7 @@ class EpgDialog:
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()
self.assign_refs(model, paths, self._current_ref.pop())
@run_idle
def on_reset(self, item):
@@ -468,10 +870,7 @@ class EpgDialog:
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
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):
@@ -483,7 +882,6 @@ class EpgDialog:
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):
@@ -500,7 +898,7 @@ class EpgDialog:
@run_idle
def update_active_header_elements(self, state):
self._left_header_box.set_sensitive(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:")
@@ -532,7 +930,7 @@ class EpgDialog:
model, paths = view.get_selection().get_selected_rows()
if paths:
s_data = model[paths][:]
if all(s_data):
if all(s_data[:-1]):
data.set_text("::::".join(s_data), -1)
else:
self.show_info_message(get_message("Source error!"), Gtk.MessageType.ERROR)
@@ -542,8 +940,8 @@ class EpgDialog:
model = view.get_model()
data = data.get_text()
if data:
self.assign_data(model[path], data.split("::::"))
self.update_epg_count()
data = data.split("::::")
self.assign_refs(model, path, data)
return False
# ***************** Options *********************#

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>

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

@@ -0,0 +1,468 @@
<?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 start -->
<column type="gint"/>
<!-- column-name end -->
<column type="gint"/>
<!-- column-name length -->
<column type="gint"/>
<!-- 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.49</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">6</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.49</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="min-width">150</property>
<property name="title" translatable="yes">Title</property>
<property name="alignment">0.49</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_start_column">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="min-width">50</property>
<property name="title" translatable="yes">Start time</property>
<property name="alignment">0.49</property>
<property name="sort-column-id">2</property>
<child>
<object class="GtkCellRendererText" id="epg_start_renderer">
<property name="xpad">5</property>
<property name="xalign">0.49</property>
<property name="width-chars">27</property>
</object>
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="epg_end_column">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="min-width">50</property>
<property name="title" translatable="yes">End time</property>
<property name="alignment">0.49</property>
<property name="sort-column-id">3</property>
<child>
<object class="GtkCellRendererText" id="epg_end_renderer">
<property name="xpad">5</property>
<property name="xalign">0.49</property>
<property name="width-chars">27</property>
</object>
<attributes>
<attribute name="text">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="epg_length_column">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="fixed-width">100</property>
<property name="min-width">50</property>
<property name="title" translatable="yes">Length</property>
<property name="alignment">0.49</property>
<property name="sort-column-id">4</property>
<child>
<object class="GtkCellRendererText" id="epg_length_renderer">
<property name="xpad">5</property>
<property name="xalign">0.49</property>
</object>
<attributes>
<attribute name="text">4</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="epg_desc_column">
<property name="sizing">fixed</property>
<property name="min-width">100</property>
<property name="title" translatable="yes">Description</property>
<property name="expand">True</property>
<property name="alignment">0.49</property>
<property name="sort-column-id">5</property>
<child>
<object class="GtkCellRendererText" id="epg_desc_renderer">
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">5</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<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>

View File

@@ -32,8 +32,289 @@ Author: Dmitriy Yefremov
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellite list editor for GNU/Linux. -->
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkImage" id="attr_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-dialog-authentication</property>
</object>
<object class="GtkBox" id="attributes_box">
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkFrame" id="attributes_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkGrid" id="attributes_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">5</property>
<property name="row_spacing">5</property>
<property name="column_spacing">5</property>
<child>
<object class="GtkLabel" id="others_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_right">15</property>
<property name="label" translatable="yes">Оthers:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="others_read_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">center</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="others_write_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">center</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="others_exec_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">center</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="group_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_right">15</property>
<property name="label" translatable="yes">Group:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="owner_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_right">15</property>
<property name="label" translatable="yes">Owner:</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="group_read_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">center</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="group_write_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">center</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="group_exec_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">center</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="owner_exec_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">center</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="owner_write_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">center</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="owner_read_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">center</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="read_label">
<property name="width_request">70</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="label" translatable="yes">Read</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="write_label">
<property name="width_request">70</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="label" translatable="yes">Write</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="execute_label">
<property name="width_request">70</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="label" translatable="yes">Execute</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="Permissions_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Permissions:</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="num_value_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="num_value_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Numeric value:</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="num_value_entry">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="editable">False</property>
<property name="width_chars">10</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">2</property>
</packing>
</child>
</object>
<object class="GtkMenu" id="bookmark_popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
@@ -135,15 +416,14 @@ Author: Dmitriy Yefremov
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="ftp_button_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">2</property>
<property name="margin_bottom">10</property>
<property name="spacing">10</property>
<child>
<object class="GtkButton" id="connect_button">
@@ -349,6 +629,7 @@ Author: Dmitriy Yefremov
<property name="headers_visible">False</property>
<property name="search_column">0</property>
<property name="tooltip_column">0</property>
<property name="activate_on_single_click">True</property>
<signal name="button-press-event" handler="on_view_popup_menu" object="bookmark_popup_menu" swapped="no"/>
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
<signal name="row-activated" handler="on_bookmark_activated" swapped="no"/>
@@ -521,11 +802,11 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="ftp_status_bar_box">
<property name="height_request">24</property>
<property name="height_request">26</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="ftp_info_image">
@@ -594,15 +875,14 @@ Author: Dmitriy Yefremov
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkBox" id="pc_header_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="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">2</property>
<property name="margin_bottom">10</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="pc_add_folder_button">
@@ -775,7 +1055,7 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="file_status_bar_box">
<property name="height_request">24</property>
<property name="height_request">26</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
@@ -896,8 +1176,8 @@ Author: Dmitriy Yefremov
<object class="GtkImageMenuItem" id="ftp_rename_menu_item">
<property name="label" translatable="yes">Rename</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="image">rename_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_ftp_rename" object="ftp_name_column_renderer" swapped="no"/>
@@ -905,6 +1185,16 @@ Author: Dmitriy Yefremov
<accelerator key="F2" signal="activate"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="ftp_attr_menu_item">
<property name="label" translatable="yes">Permissions...</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">attr_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_ftp_attr_change" swapped="no"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem">
<property name="visible">True</property>

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 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
@@ -27,6 +27,7 @@
""" Simple FTP client module. """
import stat
import subprocess
from collections import namedtuple
from datetime import datetime
@@ -39,16 +40,176 @@ 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_LINUX, IS_DARWIN, IS_WIN, SEP
from app.ui.dialogs import show_dialog, DialogType, get_builder
from app.settings import IS_LINUX, IS_DARWIN, IS_WIN, SEP, USE_HEADER_BAR
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, IS_GNOME_SESSION
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, 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 = ".."
@@ -65,92 +226,14 @@ class FtpClientBox(Gtk.HBox):
ATTR = 4
EXTRA = 5
class TextEditDialog(Gtk.Dialog):
""" 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)
self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK, )
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)
def __init__(self, app, settings, *args, **kwargs):
super().__init__(*args, **kwargs)
self.set_spacing(2)
self.set_orientation(Gtk.Orientation.VERTICAL)
self._app = app
self._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
@@ -163,6 +246,7 @@ class FtpClientBox(Gtk.HBox):
"on_ftp_edit": self.on_ftp_edit,
"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,
@@ -185,7 +269,7 @@ class FtpClientBox(Gtk.HBox):
"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_ftp_box"))
self._ftp_info_label = builder.get_object("ftp_info_label")
@@ -224,6 +308,14 @@ 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()
@@ -290,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))
@@ -314,7 +406,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)
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))
@@ -437,7 +529,7 @@ class FtpClientBox(Gtk.HBox):
@run_idle
def show_edit_dialog(self, f_path, data):
dialog = self.TextEditDialog(f_path, IS_GNOME_SESSION)
dialog = TextEditDialog(f_path, USE_HEADER_BAR)
dialog.text = data
ok = Gtk.ResponseType.OK
if dialog.run() == ok and show_dialog(DialogType.QUESTION, self._app.app_window) == ok:
@@ -472,6 +564,28 @@ class FtpClientBox(Gtk.HBox):
if resp[0] == "2":
row[self.Column.NAME] = new_value
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, USE_HEADER_BAR)
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:
@@ -801,25 +915,6 @@ class FtpClientBox(Gtk.HBox):
""" Sets default homogeneous sizes. """
paned.set_position(0.5 * allocation.width)
@staticmethod
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"
if __name__ == '__main__':
if __name__ == "__main__":
pass

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-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
@@ -26,6 +26,7 @@
#
from collections import defaultdict
from contextlib import suppress
from pathlib import Path
@@ -35,15 +36,18 @@ 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, 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, Page, HeaderBar
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:
@@ -88,7 +92,7 @@ def import_bouquet(transient, model, path, settings, services, appender, file_pa
else:
bqs = get_neutrino_bouquets(file_path, "", bq_type.value)
file_path = f"{Path(file_path).parent}{SEP}"
ImportDialog(transient, file_path, settings, services.keys(), lambda b, s: appender(b), (bqs,)).show()
ImportDialog(app, file_path, lambda b, s: appender(b), (bqs,)).show()
def get_enigma2_bouquet(path):
@@ -100,37 +104,81 @@ 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_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_bouquets_only_switch": self.on_bouquets_only_switch,
"on_key_press": self.on_key_press}
builder = get_builder(UI_RESOURCES_PATH + "imports.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._info_check_button = builder.get_object("info_check_button")
self._info_check_button.bind_property("active", builder.get_object("services_box_frame"), "visible")
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")
# Options.
self._replace_existing_switch = builder.get_object("replace_existing_switch")
self._bouquets_only_switch = builder.get_object("bouquets_only_switch")
self._bouquets_settings_box = builder.get_object("bouquets_settings_box")
# 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 self._settings.use_header_bar:
actions_box = builder.get_object("actions_box")
builder.get_object("toolbar_box").set_visible(False)
header_bar = HeaderBar()
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)
extra_box = builder.get_object("extra_header_box")
actions_box.remove(extra_box)
header_bar.pack_end(extra_box)
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)
@@ -142,7 +190,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:
@@ -150,8 +198,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)
@@ -164,21 +213,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
@@ -186,12 +238,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)
@@ -200,35 +253,124 @@ 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)))
if self._bouquets_only_switch.get_active():
services = ()
else:
services = list(filter(lambda s: s.fav_id not in self._ids, services))
self._append(self._bouquets, services)
if self._replace_existing_switch.get_active():
self._app.emit("services_update", {s.fav_id: s for s in filter(lambda s: s.fav_id in self._ids, services)})
self._dialog_window.destroy()
def import_satellites_data(self):
if show_dialog(DialogType.QUESTION, self._dialog_window) != Gtk.ResponseType.OK:
return
replace_existing = self._replace_existing_switch.get_active()
services = []
current_services = self._app.current_services
to_replace = {}
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
to_replace[s.fav_id] = s
elif s.fav_id not in self._ids:
services.append(s)
self._append((), services)
if to_replace:
self._app.emit("services_update", to_replace)
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_selected_toggled(self, toggle, path):
self._main_model.set_value(self._main_model.get_iter(path), 2, not toggle.get_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_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):
@@ -249,10 +391,38 @@ 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())
self._bouquets_settings_box.set_sensitive(self._page is Page.SERVICES)
def on_bouquets_only_switch(self, switch, state):
if state:
self._replace_existing_switch.set_active(False)
def on_key_press(self, view, event):
""" Handling keystrokes """
key_code = event.hardware_keycode
@@ -261,10 +431,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-2022 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
@@ -40,17 +40,18 @@ 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_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)
from app.ui.uicommons import (Gtk, Gdk, UI_RESOURCES_PATH, IPTV_ICON, Column, KeyboardKey, get_yt_icon, HeaderBar)
_DIGIT_ENTRY_NAME = "digit-entry"
_ENIGMA2_REFERENCE = "{}:{}:{}:{:X}:{:X}:{:X}:{:X}:0:0:0"
_ENIGMA2_REFERENCE = "{}:{}:{:X}:{:X}:{:X}:{:X}:{:X}:0:0:0"
_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
_UI_PATH = UI_RESOURCES_PATH + "iptv.glade"
_URL_PREFIXES = {"YT-DLP": "YT-DLP://", "YT-DL": "YT-DL://", "STREAMLINK": "streamlink://", "No": None}
def is_data_correct(elems):
@@ -81,6 +82,7 @@ class IptvDialog:
handlers = {"on_response": self.on_response,
"on_entry_changed": self.on_entry_changed,
"on_url_changed": self.on_url_changed,
"on_url_paste": self.on_url_paste,
"on_save": self.on_save,
"on_stream_type_changed": self.on_stream_type_changed,
"on_yt_quality_changed": self.on_yt_quality_changed,
@@ -93,6 +95,7 @@ class IptvDialog:
self._bouquet = bouquet
self._yt_links = None
self._yt_dl = None
self._inserted_url = False
builder = get_builder(_UI_PATH, handlers, use_str=True,
objects=("iptv_dialog", "stream_type_liststore", "yt_quality_liststore"))
@@ -116,8 +119,10 @@ class IptvDialog:
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label")
self._yt_quality_box = builder.get_object("yt_iptv_quality_combobox")
self._url_prefix_box = builder.get_object("iptv_url_prefix_box")
self._url_prefix_combobox = builder.get_object("iptv_url_prefix_combobox")
self._model, self._paths = view.get_selection().get_selected_rows()
# style
# Style.
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._digit_elems = (self._srv_id_entry, self._srv_type_entry, self._sid_entry, self._tr_id_entry,
@@ -134,6 +139,8 @@ class IptvDialog:
else:
self._description_entry.set_visible(False)
builder.get_object("iptv_description_label").set_visible(False)
[self._url_prefix_combobox.append(v, k) for k, v in _URL_PREFIXES.items()]
self._url_prefix_combobox.set_active(0)
if self._action is Action.ADD:
self._save_button.set_visible(False)
@@ -160,6 +167,13 @@ class IptvDialog:
self.show_info_message(get_message("Error. Verify the data!"), Gtk.MessageType.ERROR)
return
url = self._url_entry.get_text()
if all((self._url_prefix_box.get_visible(),
self._url_prefix_combobox.get_active_id(),
url.count("http") > 1 or urlparse(url).scheme.upper() in _URL_PREFIXES)):
self.show_info_message(get_message("Invalid prefix for the given URL!"), Gtk.MessageType.ERROR)
return
if show_dialog(DialogType.QUESTION, self._dialog) in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
@@ -194,15 +208,25 @@ class IptvDialog:
elif stream_type is StreamType.E_SERVICE_HLS:
self._stream_type_combobox.set_active(5)
except ValueError:
self.show_info_message("Unknown stream type {}".format(s_type), Gtk.MessageType.ERROR)
self.show_info_message(f"Unknown stream type {s_type}", Gtk.MessageType.ERROR)
self._srv_id_entry.set_text(data[1])
self._srv_type_entry.set_text(data[2])
self._srv_type_entry.set_text(str(int(data[2], 16)))
self._sid_entry.set_text(str(int(data[3], 16)))
self._tr_id_entry.set_text(str(int(data[4], 16)))
self._net_id_entry.set_text(str(int(data[5], 16)))
self._namespace_entry.set_text(str(int(data[6], 16)))
self._url_entry.set_text(unquote(data[10].strip()))
# URL.
url = unquote(data[10].strip())
sch = urlparse(url).scheme.upper()
if YouTube.get_yt_id(url) and sch in _URL_PREFIXES:
active_prefix = _URL_PREFIXES.get(sch)
url = re.sub(active_prefix, "", url, 1, re.IGNORECASE)
self._url_prefix_combobox.set_active_id(active_prefix)
else:
self._url_prefix_combobox.set_active(len(_URL_PREFIXES) - 1)
self._url_entry.set_text(url)
self.update_reference_entry()
def init_neutrino_data(self, fav_id):
@@ -212,10 +236,9 @@ class IptvDialog:
def update_reference_entry(self):
if self._s_type is SettingsType.ENIGMA_2 and is_data_correct(self._digit_elems):
self.on_url_changed(self._url_entry)
self._reference_entry.set_text(_ENIGMA2_REFERENCE.format(self.get_type(),
self._srv_id_entry.get_text(),
self._srv_type_entry.get_text(),
int(self._srv_type_entry.get_text()),
int(self._sid_entry.get_text()),
int(self._tr_id_entry.get_text()),
int(self._net_id_entry.get_text()),
@@ -242,15 +265,25 @@ class IptvDialog:
if yt_id:
entry.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, get_yt_icon("youtube", 32))
text = "Found a link to the YouTube resource!\nTry to get a direct link to the video?"
if show_dialog(DialogType.QUESTION, self._dialog, text=text) == Gtk.ResponseType.OK:
entry.set_sensitive(False)
gen = self.set_yt_url(entry, yt_id)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
if self._inserted_url and url_str.count("http") == 1:
if show_dialog(DialogType.QUESTION, self._dialog, text=text) == Gtk.ResponseType.OK:
entry.set_sensitive(False)
gen = self.set_yt_url(entry, yt_id)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
else:
self._url_prefix_box.set_visible(self._s_type is SettingsType.ENIGMA_2)
else:
self._url_prefix_box.set_visible(self._s_type is SettingsType.ENIGMA_2)
self._inserted_url = False
elif YouTube.is_yt_video_link(url_str):
entry.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, get_yt_icon("youtube", 32))
else:
entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None)
self._yt_quality_box.set_visible(False)
self._url_prefix_box.set_visible(False)
def on_url_paste(self, entry):
self._inserted_url = True
self._yt_quality_box.set_visible(False)
def set_yt_url(self, entry, video_id):
try:
@@ -264,7 +297,7 @@ class IptvDialog:
links, title = self._yt_dl.get_yt_link(video_id, entry.get_text())
yield True
except urllib.error.URLError as e:
self.show_info_message(get_message("Getting link error:") + (str(e)), Gtk.MessageType.ERROR)
self.show_info_message(f"{get_message('Getting link error:')} {e}", Gtk.MessageType.ERROR)
return
except YouTubeException as e:
self.show_info_message((str(e)), Gtk.MessageType.ERROR)
@@ -279,7 +312,7 @@ class IptvDialog:
entry.set_text(links[sorted(links, key=lambda x: int(x.rstrip("p")), reverse=True)[0]])
self._yt_links = links
else:
msg = get_message("Getting link error:") + " No link received for id: {}".format(video_id)
msg = f"{get_message('Getting link error:')} No link received for id: {video_id}"
self.show_info_message(msg, Gtk.MessageType.ERROR)
finally:
entry.set_sensitive(True)
@@ -291,22 +324,33 @@ class IptvDialog:
self.update_reference_entry()
def on_yt_quality_changed(self, box):
if not self._yt_links:
return
model = box.get_model()
active = model.get_value(box.get_active_iter(), 0)
if self._yt_links and active in self._yt_links:
if active in self._yt_links:
self._url_entry.set_text(self._yt_links[active])
else:
self._url_entry.set_text(self._yt_links.get(max(self._yt_links, default=None), ""))
def save_enigma2_data(self):
name = self._name_entry.get_text().strip()
if self._url_prefix_box.get_visible():
prefix = self._url_prefix_combobox.get_active_id()
url = self._url_entry.get_text().replace(':', '%3A', 1 if prefix else -1)
url = f"{quote(prefix) if prefix else ''}{url}"
else:
url = quote(self._url_entry.get_text())
fav_id = ENIGMA2_FAV_ID_FORMAT.format(self.get_type(),
self._srv_id_entry.get_text(),
self._srv_type_entry.get_text(),
int(self._srv_type_entry.get_text()),
int(self._sid_entry.get_text()),
int(self._tr_id_entry.get_text()),
int(self._net_id_entry.get_text()),
int(self._namespace_entry.get_text()),
quote(self._url_entry.get_text()),
name, name)
url, name, name)
self.update_bouquet_data(name, fav_id)
@@ -328,7 +372,7 @@ class IptvDialog:
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", (old_srv, 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
@@ -379,9 +423,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()
@@ -390,13 +438,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)
@@ -597,11 +645,12 @@ class IptvListConfigurationDialog(IptvListDialog):
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()
srv_type = int("1" if type_default else self._list_srv_type_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]
@@ -611,16 +660,20 @@ class IptvListConfigurationDialog(IptvListDialog):
if all_default:
data[1], data[2], data[3], data[4], data[5], data[6] = "010000"
else:
data[0], data[1], data[2], data[4], data[5], data[6] = st_type, s_id, srv_type, tid, nid, namespace
data[0], data[1], data[4], data[5], data[6] = st_type, s_id, tid, nid, namespace
data[2] = f"{srv_type:X}"
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), srv_type, *params)
data = ":".join(data)
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))
@@ -662,7 +715,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
@@ -716,7 +769,7 @@ class M3uImportDialog(IptvListDialog):
continue
params[0] = i if sid_auto else sid
picon_id = "{}_{}_{:X}_{:X}_{:X}_{:X}_{:X}_0_0_0.png".format(st_type, s_id, s_type, *params)
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
@@ -851,7 +904,6 @@ class YtListImportDialog:
"on_key_press": self.on_key_press,
"on_close": self.on_close}
# 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
@@ -879,26 +931,54 @@ class YtListImportDialog:
self._import_button = builder.get_object("yt_import_button")
self._quality_box = builder.get_object("yt_quality_combobox")
self._quality_model = builder.get_object("yt_quality_liststore")
self._import_button.bind_property("visible", self._quality_box, "visible")
self._import_button.bind_property("sensitive", self._quality_box, "sensitive")
self._receive_button.bind_property("sensitive", self._import_button, "sensitive")
self._extract_switch = builder.get_object("yt_extract_links_switch")
self._url_prefix_combobox = builder.get_object("yt_url_prefix_combobox")
[self._url_prefix_combobox.append(v, k) for k, v in _URL_PREFIXES.items()]
self._url_prefix_combobox.set_active(0)
builder.get_object("yt_extract_links_box").set_visible(self._s_type is SettingsType.ENIGMA_2)
builder.get_object("yt_url_prefix_box").set_visible(self._s_type is SettingsType.ENIGMA_2)
if self._settings.use_header_bar:
header_bar = HeaderBar(title="YouTube", subtitle=get_message("Playlist import"))
self._dialog.set_titlebar(header_bar)
actions_box = builder.get_object("yt_actions_box")
import_box = builder.get_object("yt_import_box")
actions_box.remove(import_box)
header_bar.pack_end(import_box)
actions_box.remove(self._receive_button)
header_bar.pack_start(self._receive_button)
actions_box.set_visible(False)
window_size = self._settings.get("yt_import_dialog_size")
if window_size:
self._dialog.resize(*window_size)
# Style
# Style.
style_provider = Gtk.CssProvider()
style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
style_provider.load_from_path(f"{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):
self._dialog.show()
@run_task
def on_import(self, item):
self.on_info_bar_close()
self.update_active_elements(False)
if self._extract_switch.get_active():
self.extract_direct_links()
else:
prefix = self._url_prefix_combobox.get_active_id()
selected = filter(lambda r: r[2], self._model)
prefix = quote(prefix) if prefix else ''
links = [(f"{prefix}https{quote(':')}//www.youtube.com/watch?v={r[1]}", r[0]) for r in selected]
self.append_services(links)
self.update_active_elements(True)
@run_task
def extract_direct_links(self):
self._download_task = True
try:
@@ -927,7 +1007,6 @@ class YtListImportDialog:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
if self._download_task:
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
self.append_services([done_links[r] for r in rows])
finally:
self._download_task = False
@@ -970,22 +1049,31 @@ class YtListImportDialog:
aggr = [None] * 9
srvs = []
if self._yt_list_title:
if self._yt_list_title and self._s_type is SettingsType.ENIGMA_2:
title = self._yt_list_title
fav_id = MARKER_FORMAT.format(0, title, title)
mk = Service(None, None, None, title, *aggr[0:3], BqServiceType.MARKER.name, *aggr, 0, fav_id, None)
srvs.append(mk)
act = self._quality_model.get_value(self._quality_box.get_active_iter(), 0)
extract = self._extract_switch.get_active()
act = self._quality_model.get_value(self._quality_box.get_active_iter(), 0) if extract else None
for link in links:
lnk, title = link or (None, None)
if not lnk:
continue
ln = lnk.get(act) if act in lnk else lnk[sorted(lnk, key=lambda x: int(x.rstrip("p")), reverse=True)[0]]
fav_id = get_fav_id(ln, title, self._s_type)
if extract:
ln = lnk.get(act) if act in lnk else lnk[sorted(lnk, key=lambda x: int(x.rstrip("p")), reverse=True)[0]]
else:
ln = lnk
fav_id = get_fav_id(ln, title, self._s_type, force_quote=extract)
srv = Service(None, None, IPTV_ICON, title, *aggr[0:3], BqServiceType.IPTV.name, *aggr, None, fav_id, None)
srvs.append(srv)
self.appender(srvs)
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
@run_idle
def update_active_elements(self, sensitive):

View File

@@ -3,7 +3,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
@@ -32,7 +32,7 @@ Author: Dmitriy Yefremov
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellite list editor. -->
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkFrame" id="log_frame">
<property name="visible">True</property>
@@ -80,7 +80,27 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<placeholder/>
<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>

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
@@ -30,7 +30,7 @@ import logging
from gi.repository import GLib
from app.commons import LOGGER_NAME
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
@@ -43,15 +43,16 @@ class LogsClient(Gtk.Box):
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):
GLib.idle_add(append_text_to_tview, f"{rec.msg}\n", self._view)
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}
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")
@@ -64,3 +65,10 @@ class LogsClient(Gtk.Box):
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

View File

@@ -1,19 +1,37 @@
* {
-GtkDialog-action-area-border: 6em;
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.5em;
min-width: 1em;
padding-left: 0.4em;
padding-right: 0.4em;
padding-top: 0.1em;
padding-bottom: 0.1em;
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 {
@@ -33,11 +51,21 @@ 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;
}

View File

@@ -3,7 +3,7 @@
The MIT License (MIT)
Copyright (c) 2018-2022 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
@@ -27,12 +27,12 @@ Author: Dmitriy Yefremov
-->
<interface domain="demon-editor">
<requires lib="gtk+" version="3.18"/>
<requires lib="gtk+" version="3.22"/>
<!-- interface-css-provider-path style.css -->
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellite list editor. -->
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkImage" id="alt_image">
<property name="visible">True</property>
@@ -73,12 +73,10 @@ Author: Dmitriy Yefremov
</object>
</child>
</object>
<object class="GtkImage" id="backups_image">
<object class="GtkImage" id="assign_ref_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="icon_name">document-revert-symbolic</property>
<property name="icon_size">1</property>
<property name="icon_name">insert-link</property>
</object>
<object class="GtkTreeStore" id="bouquets_tree_store">
<columns>
@@ -108,6 +106,11 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="stock">gtk-clear</property>
</object>
<object class="GtkImage" id="clear_new_flag_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-undo</property>
</object>
<object class="GtkImage" id="copy_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
@@ -257,10 +260,7 @@ Author: Dmitriy Yefremov
<property name="width_request">170</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="border_width">5</property>
<property name="hscrollbar_policy">never</property>
<property name="max_content_height">350</property>
<property name="propagate_natural_height">True</property>
@@ -325,10 +325,7 @@ Author: Dmitriy Yefremov
<property name="width_request">135</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="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="border_width">5</property>
<property name="hscrollbar_policy">never</property>
<property name="max_content_height">350</property>
<property name="propagate_natural_height">True</property>
@@ -408,39 +405,49 @@ Author: Dmitriy Yefremov
<object class="GtkPopover" id="filter_type_popover">
<property name="can_focus">False</property>
<child>
<object class="GtkTreeView" id="filter_type_view">
<property name="width_request">135</property>
<object class="GtkBox" id="filter_type_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="model">filter_types_list_store</property>
<property name="headers_visible">False</property>
<property name="enable_search">False</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<property name="border_width">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkTreeViewColumn" id="fiter_type_column">
<property name="title" translatable="yes">Type</property>
<child>
<object class="GtkCellRendererText" id="filter_type_renderer_text"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
<object class="GtkTreeView" id="filter_type_view">
<property name="width_request">135</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">5</property>
<property name="model">filter_types_list_store</property>
<property name="headers_visible">False</property>
<property name="enable_search">False</property>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<child>
<object class="GtkCellRendererToggle" id="filter_type_renderer_toggle">
<property name="xalign">0.98000001907348633</property>
<signal name="toggled" handler="on_filter_type_toggled" swapped="no"/>
<object class="GtkTreeViewColumn" id="fiter_type_column">
<property name="title" translatable="yes">Type</property>
<child>
<object class="GtkCellRendererText" id="filter_type_renderer_text"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
<child>
<object class="GtkCellRendererToggle" id="filter_type_renderer_toggle">
<property name="xalign">0.98000001907348633</property>
<signal name="toggled" handler="on_filter_type_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">1</attribute>
</attributes>
</child>
</object>
<attributes>
<attribute name="active">1</attribute>
</attributes>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
</child>
@@ -450,13 +457,6 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="stock">gtk-find</property>
</object>
<object class="GtkImage" id="ftp_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="icon_name">network-transmit-receive-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="import_bouquet_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
@@ -502,10 +502,14 @@ Author: Dmitriy Yefremov
<column type="GdkPixbuf"/>
<!-- column-name ref -->
<column type="gchararray"/>
<!-- column-name url -->
<column type="gchararray"/>
<!-- column-name fav_id -->
<column type="gchararray"/>
<!-- column-name picon_id -->
<column type="gchararray"/>
<!-- column-name extra -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkTreeModelFilter" id="iptv_services_model_filter">
@@ -654,7 +658,78 @@ Author: Dmitriy Yefremov
</child>
</object>
<packing>
<property name="submenu">submenu1</property>
<property name="submenu">import_submenu</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="ftp_popover_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkModelButton" id="ftp_back_to_main_menu_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="halign">start</property>
<property name="valign">start</property>
<property name="margin_left">5</property>
<property name="margin_top">5</property>
<property name="menu_name">main</property>
<property name="inverted">True</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="ftp_transfer_menu_box">
<property name="visible">True</property>
<property name="can_focus">False</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="GtkModelButton" id="receive_ftp_menu_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="action_name">app.on_receive</property>
<property name="text" translatable="yes">Download from the receiver</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="send_ftp_menu_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="action_name">app.on_send</property>
<property name="text" translatable="yes">Transfer to receiver</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
</object>
<packing>
<property name="submenu">ftp_submenu</property>
<property name="position">1</property>
</packing>
</child>
@@ -679,7 +754,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="text" translatable="yes">Import</property>
<property name="menu_name">submenu1</property>
<property name="menu_name">import_submenu</property>
</object>
<packing>
<property name="expand">False</property>
@@ -797,8 +872,8 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="action_name">app.on_download</property>
<property name="text" translatable="yes">FTP-transfer</property>
<property name="menu_name">ftp_submenu</property>
</object>
<packing>
<property name="expand">False</property>
@@ -823,7 +898,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="margin_bottom">2</property>
<property name="action_name">app.on_settings</property>
<property name="action_name">app.preferences</property>
<property name="text" translatable="yes">Settings</property>
</object>
<packing>
@@ -874,7 +949,7 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="action_name">app.on_about_app</property>
<property name="action_name">app.about</property>
<property name="text" translatable="yes">About</property>
</object>
<packing>
@@ -888,8 +963,8 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="action_name">app.quit</property>
<property name="text" translatable="yes">Exit</property>
<signal name="clicked" handler="on_close_app" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
@@ -928,6 +1003,11 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-find-and-replace</property>
</object>
<object class="GtkImage" id="remove_duplicates_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-select-all</property>
</object>
<object class="GtkImage" id="mark_not_in_bq_image">
<property name="visible">True</property>
@@ -1089,13 +1169,6 @@ Author: Dmitriy Yefremov
</object>
</child>
</object>
<object class="GtkImage" id="open_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="icon_name">folder-open-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkAdjustment" id="player_scale_adjustment">
<property name="upper">100</property>
<property name="step_increment">1</property>
@@ -1138,13 +1211,6 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="stock">gtk-save-as</property>
</object>
<object class="GtkImage" id="save_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="icon_name">document-save-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="select_all_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
@@ -1294,6 +1360,16 @@ Author: Dmitriy Yefremov
<accelerator key="c" signal="activate" modifiers="Primary"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="services_reference_popup_item">
<property name="label" translatable="yes">Copy reference</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">copy_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_reference_copy" object="services_tree_view" swapped="no"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="services_edit_popup_item">
<property name="label">gtk-edit</property>
@@ -1340,6 +1416,15 @@ Author: Dmitriy Yefremov
<signal name="activate" handler="on_services_clear_marked" swapped="no"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="services_clear_new_flag_item">
<property name="label" translatable="yes">Clear "New" flag</property>
<property name="can_focus">False</property>
<property name="image">clear_new_flag_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_services_clear_new_marked" swapped="no"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="services_separatormenuitem_1">
<property name="visible">True</property>
@@ -1393,22 +1478,6 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="services_reference_picon_popup_item">
<property name="label" translatable="yes">Copy reference</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">copy_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_reference_picon" object="services_tree_view" swapped="no"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="services_separatormenuitem_5">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="services_remove_unused_picons_popup_item">
<property name="label" translatable="yes">Remove all unused</property>
@@ -1424,7 +1493,7 @@ Author: Dmitriy Yefremov
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="services_separatormenuitem_6">
<object class="GtkSeparatorMenuItem" id="services_separatormenuitem_5">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
@@ -1543,6 +1612,7 @@ Author: Dmitriy Yefremov
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="hhomogeneous">False</property>
<property name="transition_type">crossfade</property>
<signal name="notify::visible-child-name" handler="on_visible_page" swapped="no"/>
<child>
@@ -1587,7 +1657,7 @@ Author: Dmitriy Yefremov
<object class="GtkLabel" id="app_ver_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">2.2.0 Beta</property>
<property name="label">3.6.1 Beta</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
@@ -1713,11 +1783,11 @@ Author: Dmitriy Yefremov
<child type="center">
<object class="GtkButtonBox" id="services_button_box">
<property name="visible">True</property>
<property name="name">header-stack-switcher</property>
<property name="can_focus">False</property>
<property name="layout_style">expand</property>
<child>
<object class="GtkRadioButton" id="dvb_button">
<property name="name">stack-switch-button</property>
<property name="label" translatable="yes">DVB</property>
<property name="width_request">80</property>
<property name="visible">True</property>
@@ -1735,7 +1805,6 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkRadioButton" id="iptv_button">
<property name="name">stack-switch-button</property>
<property name="label" translatable="yes">IPTV</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
@@ -1782,7 +1851,7 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="enigma_lock_hide_box">
<property name="sensitive">False</property>
<property name="sensitive" bind-source="bouquet_lock_hide_box" bind-property="sensitive" bind-flags="invert-boolean">False</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
<child>
@@ -2133,7 +2202,7 @@ Author: Dmitriy Yefremov
<object class="GtkCellRendererText" id="service_renderer">
<property name="xpad">5</property>
<property name="ellipsize">end</property>
<property name="width_chars">25</property>
<property name="width_chars">50</property>
</object>
<attributes>
<attribute name="cell-background-rgba">21</attribute>
@@ -2783,6 +2852,7 @@ Author: Dmitriy Yefremov
<property name="fixed_height_mode">True</property>
<property name="rubber_banding">True</property>
<property name="enable_grid_lines">both</property>
<property name="tooltip_column">7</property>
<property name="activate_on_single_click">True</property>
<signal name="button-press-event" handler="on_view_popup_menu" object="iptv_popup_menu" swapped="no"/>
<signal name="button-press-event" handler="on_view_press" swapped="no"/>
@@ -2792,6 +2862,7 @@ Author: Dmitriy Yefremov
<signal name="drag-drop" handler="on_services_view_drag_drop" swapped="no"/>
<signal name="drag-end" handler="on_view_drag_end" swapped="no"/>
<signal name="key-press-event" handler="on_tree_view_key_press" swapped="no"/>
<signal name="query-tooltip" handler="on_iptv_view_query_tooltip" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="iptv_services_selection">
<property name="mode">multiple</property>
@@ -2803,7 +2874,6 @@ Author: Dmitriy Yefremov
<property name="sizing">fixed</property>
<property name="min_width">170</property>
<property name="title" translatable="yes">Service</property>
<property name="expand">True</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<property name="sort_column_id">0</property>
@@ -2823,7 +2893,6 @@ Author: Dmitriy Yefremov
<property name="sizing">fixed</property>
<property name="min_width">100</property>
<property name="title" translatable="yes">Type</property>
<property name="expand">True</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<property name="sort_column_id">1</property>
@@ -2842,7 +2911,6 @@ Author: Dmitriy Yefremov
<property name="sizing">fixed</property>
<property name="min_width">25</property>
<property name="title" translatable="yes">Picon</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererPixbuf" id="iptv_picon_renderer">
@@ -2856,10 +2924,10 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkTreeViewColumn" id="iptv_ref_column">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="min_width">150</property>
<property name="title" translatable="yes">Reference</property>
<property name="expand">True</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<property name="sort_column_id">3</property>
@@ -2873,6 +2941,27 @@ Author: Dmitriy Yefremov
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="iptv_url_column">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="min_width">150</property>
<property name="title" translatable="yes">URL</property>
<property name="expand">True</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<property name="sort_column_id">4</property>
<child>
<object class="GtkCellRendererText" id="iptv_url_renderer">
<property name="xpad">5</property>
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">4</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
@@ -2889,7 +2978,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="spacing">2</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="iptv_services_count_image">
<property name="visible">True</property>
@@ -3215,7 +3304,25 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<placeholder/>
<object class="GtkMenuButton" id="epg_menu_button">
<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">EPG source</property>
<child>
<object class="GtkImage" id="epg_menu_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">insert-text-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>
@@ -3311,7 +3418,6 @@ Author: Dmitriy Yefremov
<object class="GtkBox" id="fav_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_bottom">2</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
@@ -3326,7 +3432,6 @@ Author: Dmitriy Yefremov
<property name="model">fav_list_store</property>
<property name="enable_search">False</property>
<property name="search_column">2</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">9</property>
@@ -3696,12 +3801,12 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="fav_bar_box">
<property name="height_request">24</property>
<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">2</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="image2">
<property name="visible">True</property>
@@ -3833,6 +3938,7 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="bouquet_lock_hide_box">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="spacing">5</property>
@@ -3917,7 +4023,6 @@ Author: Dmitriy Yefremov
<object class="GtkScrolledWindow" id="bouquets_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_bottom">2</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="bouquets_tree_view">
@@ -4004,12 +4109,12 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkBox" id="bouquet_bar_box">
<property name="height_request">24</property>
<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">2</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="bq_count_image">
<property name="visible">True</property>
@@ -4073,7 +4178,7 @@ Author: Dmitriy Yefremov
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">False</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
@@ -4109,7 +4214,6 @@ Author: Dmitriy Yefremov
<property name="width_request">150</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<signal name="realize" handler="on_logs_realize" swapped="no"/>
<child>
<placeholder/>
</child>
@@ -4247,6 +4351,37 @@ Author: Dmitriy Yefremov
<property name="position">0</property>
</packing>
</child>
<child type="center">
<object class="GtkBox" id="task_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="spacing">5</property>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkImage" id="http_status_image">
<property name="can_focus">False</property>
@@ -4391,25 +4526,25 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
<property name="stack">stack</property>
</object>
<object class="GtkImage" id="telnet_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">network-workgroup-symbolic</property>
</object>
<object class="GtkButtonBox" id="toolbar_main_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="homogeneous">True</property>
<property name="layout_style">expand</property>
<child>
<object class="GtkButton" id="open_tool_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Open</property>
<property name="action_name">app.on_data_open</property>
<property name="image">open_image</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage" id="open_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-open-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
@@ -4418,16 +4553,22 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkButton" id="download_tool_button">
<property name="label" translatable="yes"> </property>
<property name="width_request">48</property>
<object class="GtkButton" id="receive_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">FTP-transfer</property>
<property name="action_name">app.on_download</property>
<property name="image">ftp_image</property>
<property name="tooltip_text" translatable="yes">Download from the receiver</property>
<property name="action_name">app.on_receive</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage" id="download_image">
<property name="width_request">32</property>
<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>
</child>
</object>
<packing>
<property name="expand">True</property>
@@ -4436,14 +4577,21 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkButton" id="telnet_tool_button">
<property name="label" translatable="yes"> </property>
<property name="can_focus">True</property>
<object class="GtkButton" id="send_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Telnet</property>
<property name="action_name">app.on_telnet_client_show</property>
<property name="image">telnet_image</property>
<property name="tooltip_text" translatable="yes">Transfer to receiver</property>
<property name="action_name">app.on_send</property>
<property name="always_show_image">True</property>
<child>
<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>
</child>
</object>
<packing>
<property name="expand">True</property>
@@ -4453,13 +4601,17 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkButton" id="save_tool_button">
<property name="label" translatable="yes"> </property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Save</property>
<property name="action_name">app.on_data_save</property>
<property name="image">save_image</property>
<property name="always_show_image">True</property>
<child>
<object class="GtkImage" id="save_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-save-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
@@ -4467,23 +4619,6 @@ Author: Dmitriy Yefremov
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkButton" id="backup_tool_button">
<property name="label" translatable="yes"> </property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Backups</property>
<property name="action_name">app.on_backup_tool_show</property>
<property name="image">backups_image</property>
<property name="always_show_image">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
</object>
<object class="GtkImage" id="yt_image">
<property name="visible">True</property>
@@ -4517,6 +4652,16 @@ Author: Dmitriy Yefremov
<accelerator key="c" signal="activate" modifiers="Primary"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="fav_reference_popup_item">
<property name="label" translatable="yes">Copy reference</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">copy_image_2</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_reference_copy" object="fav_tree_view" swapped="no"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="fav_paste_popup_item">
<property name="label">gtk-paste</property>
@@ -4612,7 +4757,18 @@ Author: Dmitriy Yefremov
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="fav_pupup_separator_3">
<object class="GtkImageMenuItem" id="fav_remove_dup_popup_item">
<property name="label" translatable="yes">Remove duplicates</property>
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="image">remove_duplicates_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_remove_duplicates" swapped="no"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_3">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
@@ -4686,7 +4842,7 @@ Author: Dmitriy Yefremov
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="fav_pupup_separator_5">
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_5">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
@@ -4733,11 +4889,22 @@ Author: Dmitriy Yefremov
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="fav_pupup_separator_6">
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_6">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="fav_assign_ref_popup_item">
<property name="label" translatable="yes">Assign reference</property>
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="image">assign_ref_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_reference_assign" object="fav_tree_view" swapped="no"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="fav_epg_configuration_popup_item">
<property name="label" translatable="yes">EPG configuration</property>
@@ -4749,7 +4916,7 @@ Author: Dmitriy Yefremov
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="fav_pupup_separator_7">
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_7">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
@@ -4765,7 +4932,7 @@ Author: Dmitriy Yefremov
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="fav_pupup_separator_8">
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_8">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
@@ -4799,7 +4966,7 @@ Author: Dmitriy Yefremov
<property name="image">insert_image</property>
<property name="use_stock">False</property>
<child type="submenu">
<object class="GtkMenu" id="fav_picon_popoup_menu">
<object class="GtkMenu" id="fav_picon_popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
@@ -4838,22 +5005,6 @@ Author: Dmitriy Yefremov
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="fav_reference_picon_popup_item">
<property name="label" translatable="yes">Copy reference</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">copy_image_2</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_reference_picon" object="fav_tree_view" swapped="no"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="fav_pupup_separator_11">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="fav_remove_unused_picons_popup_item">
<property name="label" translatable="yes">Remove all unused</property>
@@ -4869,7 +5020,7 @@ Author: Dmitriy Yefremov
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_12">
<object class="GtkSeparatorMenuItem" id="fav_popup_separator_11">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 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
@@ -29,14 +29,18 @@
""" Helper module for the GUI. """
__all__ = ("insert_marker", "move_items", "rename", "ViewTarget", "set_flags", "locate_in_services",
"scroll_to", "get_base_model", "copy_picon_reference", "assign_picons", "remove_picon",
"is_only_one_item_selected", "gen_bouquets", "BqGenType", "get_selection",
"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",
"update_entry_data", "append_text_to_tview", "on_popup_menu")
"get_iptv_data", "update_entry_data", "append_text_to_tview", "on_popup_menu", "get_picon_file_name",
"update_toggle_model", "update_popup_filter_model", "update_filter_sat_positions", "get_pos_num")
import os
import re
import shutil
from collections import defaultdict
import unicodedata
from functools import lru_cache
from itertools import groupby
from pathlib import Path
from urllib.parse import unquote
@@ -523,27 +527,6 @@ 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, services):
""" Removes picons from profile picons folder if there are no services for these picons. """
ids = {s.picon_id for s in services}
@@ -563,13 +546,13 @@ def remove_picons(settings, picon_ids, picons):
shutil.move(src, backup_path + p_id)
def is_only_one_item_selected(paths, transient):
def is_only_one_item_selected(paths, app):
if len(paths) > 1:
show_dialog(DialogType.ERROR, transient, "Please, select only one item!")
app.show_error_message("Please, select only one item!")
return False
if not paths:
show_dialog(DialogType.ERROR, transient, "No selected item!")
app.show_error_message("No selected item!")
return False
return True
@@ -578,50 +561,102 @@ 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
@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):
def gen_bouquets(app, gen_type):
""" 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
model, paths = app.services_view.get_selection().get_selected_rows()
single_types = {BqGenType.SAT, BqGenType.PACKAGE, BqGenType.TYPE}
if gen_type in single_types and not is_only_one_item_selected(paths, app):
return
fav_id_index = Column.SRV_FAV_ID
index = Column.SRV_TYPE
if gen_type in (BqGenType.PACKAGE, BqGenType.EACH_PACKAGE):
index = Column.SRV_PACKAGE
elif gen_type in (BqGenType.SAT, BqGenType.EACH_SAT):
index = Column.SRV_POS
# 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))
ids = {row[Column.SRV_FAV_ID] for row in model}
services = [v for k, v in app.current_services.items() if k in ids]
bq_type = BqType.BOUQUET.value if s_type is SettingsType.NEUTRINO_MP else BqType.TV.value
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)
if gen_type is BqGenType.TYPE and cond == "Data":
msg = f"{get_message('Selected type:')} '{cond}'\n\n{get_message('Are you sure?')}"
if show_dialog(DialogType.QUESTION, app.app_window, msg) != Gtk.ResponseType.OK:
return
def grouper(s):
data = s[index]
return data if data else "None"
services = {k: list(v) for k, v in groupby(sorted(services, key=grouper), key=grouper)}
bq_view = app.bouquets_view
bq_type = BqType.TV.value if app.is_enigma else BqType.BOUQUET.value
bq_index = 0 if app.is_enigma else 1
bq_root_iter = bq_view.get_model().get_iter(bq_index)
bq_names = get_bouquets_names(bq_view.get_model())
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)
app.show_error_message("A bouquet with that name exists!")
return
bq_services = get_services_type_groups(services.get(cond, []))
if app.is_enigma:
if srv.service_type == "Radio":
bq_index = 1
bq_type = BqType.RADIO.value
bq_root_iter = bq_view.get_model().get_iter(bq_index)
bq_view.expand_row(Gtk.TreePath(bq_index), 1)
bq_services = bq_services.get("Radio", [])
else:
bq_view.expand_row(Gtk.TreePath(bq_index), 0)
bq_services = bq_services.get("Data" if srv.service_type == "Data" else "TV", [])
app.append_bouquet(Bouquet(cond, bq_type, get_bouquet_services(bq_services)), bq_root_iter)
else:
bq_view.expand_row(Gtk.TreePath(bq_index), 0)
# 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)
if gen_type is BqGenType.EACH_SAT:
bq_names = sorted(services.keys() - bq_names, key=get_pos_num, reverse=True)
else:
bq_names = sorted(services.keys() - bq_names)
tv_bqs = []
radio_bqs = []
for n in bq_names:
bqs = services.get(n, [])
# TV and Radio separation.
bq_grp = get_services_type_groups(bqs)
tv_bq = bq_grp.get("TV", [])
tv_bqs.append(Bouquet(n, BqType.TV.value, get_bouquet_services(tv_bq))) if tv_bq else None
radio_bq = bq_grp.get("Radio", [])
radio_bqs.append(Bouquet(n, BqType.RADIO.value, get_bouquet_services(radio_bq))) if radio_bq else None
[app.append_bouquet(b, bq_root_iter) for b in tv_bqs]
if app.is_enigma:
bq_root_iter = bq_view.get_model().get_iter(bq_index + 1)
bq_view.expand_row(Gtk.TreePath(bq_index + 1), 0)
[app.append_bouquet(b, bq_root_iter) for b in radio_bqs]
def get_bouquet_services(services):
services.sort(key=lambda s: s.service)
return [BouquetService(None, BqServiceType.DEFAULT, s.fav_id, 0) for s in services]
def get_bouquets_names(model):
@@ -637,7 +672,53 @@ def get_bouquets_names(model):
return bouquets_names
# ***************** Others *********************#
def get_services_type_groups(services):
""" Returns services grouped by main types [TV, Radio, Data]. -> dict """
def type_grouper(s):
s_type = s.service_type
if s_type == "Data":
return s_type
elif s_type == "Radio":
return s_type
else:
return "TV"
return {k: list(v) for k, v in groupby(sorted(services, key=type_grouper), key=type_grouper)}
# ***************** 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):
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. """
@@ -678,6 +759,44 @@ 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_popup_filter_model(model, elements: set):
first = model[model.get_iter_first()][:]
model.clear()
model.append((first[0], True))
elements.discard(first[0])
def update_filter_sat_positions(model, sat_positions):
""" Updates the values for the satellite positions button model. """
update_popup_filter_model(model, sat_positions)
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()
@@ -686,9 +805,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:
@@ -696,6 +815,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, 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:

View File

@@ -3,7 +3,7 @@
The MIT License (MIT)
Copyright (c) 2018-2022 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
@@ -31,7 +31,7 @@ Author: Dmitriy Yefremov
<!-- interface-css-provider-path style.css -->
<!-- interface-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkMenu" id="add_menu">
<property name="visible">True</property>
@@ -251,8 +251,8 @@ Author: Dmitriy Yefremov
<object class="GtkBox" id="header_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">15</property>
<property name="margin_right">15</property>
<property name="margin_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>
@@ -348,51 +348,6 @@ Author: Dmitriy Yefremov
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="send_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Transfer to receiver</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_send" swapped="no"/>
<signal name="drag-data-received" handler="on_send_button_drag_data_received" swapped="no"/>
<child>
<object class="GtkImage" id="send_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-go-up</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkButton" id="download_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Download from the receiver</property>
<signal name="clicked" handler="on_download" swapped="no"/>
<signal name="drag-data-received" handler="on_download_button_drag_data_received" swapped="no"/>
<child>
<object class="GtkImage" id="download_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-go-down</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkButton" id="remove_button">
<property name="visible">True</property>
@@ -400,7 +355,6 @@ Author: Dmitriy Yefremov
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Remove all picons from the receiver</property>
<signal name="clicked" handler="on_remove" swapped="no"/>
<signal name="drag-data-received" handler="on_remove_button_drag_data_received" swapped="no"/>
<child>
<object class="GtkImage" id="remove_button_image">
<property name="visible">True</property>
@@ -425,27 +379,20 @@ Author: Dmitriy Yefremov
<child type="center">
<object class="GtkButtonBox" id="header_button_box">
<property name="visible">True</property>
<property name="name">header-stack-switcher</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="layout_style">expand</property>
<child>
<object class="GtkRadioButton" id="manager_button">
<property name="width_request">50</property>
<property name="label" translatable="yes">Explorer</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Explorer</property>
<property name="active">True</property>
<property name="draw_indicator">False</property>
<property name="group">converter_button</property>
<signal name="toggled" handler="on_tool_switched" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">folder-open-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
@@ -456,7 +403,7 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkRadioButton" id="downloader_button">
<property name="width_request">50</property>
<property name="label" translatable="yes">Downloader</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
@@ -465,13 +412,6 @@ Author: Dmitriy Yefremov
<property name="draw_indicator">False</property>
<property name="group">manager_button</property>
<signal name="toggled" handler="on_tool_switched" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">insert-image-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
@@ -482,7 +422,7 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkRadioButton" id="converter_button">
<property name="width_request">50</property>
<property name="label" translatable="yes">Converter</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
@@ -491,13 +431,6 @@ Author: Dmitriy Yefremov
<property name="draw_indicator">False</property>
<property name="group">manager_button</property>
<signal name="toggled" handler="on_tool_switched" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">object-rotate-right-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
@@ -708,7 +641,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">True</property>
<property name="tooltip_text" translatable="yes">Drag the services to the desired picon or picon to the list of selected services.</property>
<property name="model">picons_src_sort_model</property>
<property name="fixed_height_mode">True</property>
<property name="rubber_banding">True</property>
<property name="enable_grid_lines">horizontal</property>
<property name="tooltip_column">0</property>
<property name="activate_on_single_click">True</property>
@@ -720,7 +653,9 @@ Author: Dmitriy Yefremov
<signal name="key-press-event" handler="on_tree_view_key_press" swapped="no"/>
<signal name="query-tooltip" handler="on_view_query_tooltip" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="picons_src_view_selection"/>
<object class="GtkTreeSelection" id="picons_src_view_selection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="src_picon_column">
@@ -730,7 +665,6 @@ Author: Dmitriy Yefremov
<property name="alignment">0.49000000953674316</property>
<child>
<object class="GtkCellRendererPixbuf" id="picons_src_renderer">
<property name="height">50</property>
<property name="ypad">5</property>
</object>
<attributes>
@@ -841,9 +775,9 @@ Author: Dmitriy Yefremov
<object class="GtkTreeView" id="picons_dest_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="tooltip_text" translatable="yes">Drag the services to the desired picon or picon to the list of selected services. </property>
<property name="tooltip_text" translatable="yes">Drag the services to the desired picon or picon to the list of selected services.</property>
<property name="model">picons_dst_sort_model</property>
<property name="fixed_height_mode">True</property>
<property name="rubber_banding">True</property>
<property name="enable_grid_lines">horizontal</property>
<property name="tooltip_column">0</property>
<property name="activate_on_single_click">True</property>
@@ -856,7 +790,9 @@ Author: Dmitriy Yefremov
<signal name="key-press-event" handler="on_tree_view_key_press" swapped="no"/>
<signal name="query-tooltip" handler="on_view_query_tooltip" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection" id="picons_dest_view_selection"/>
<object class="GtkTreeSelection" id="picons_dest_view_selection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="dest_picon_column">
@@ -866,7 +802,6 @@ Author: Dmitriy Yefremov
<property name="alignment">0.49000000953674316</property>
<child>
<object class="GtkCellRendererPixbuf" id="picons_dest_renderer">
<property name="height">50</property>
<property name="ypad">5</property>
</object>
<attributes>
@@ -1730,89 +1665,13 @@ Author: Dmitriy Yefremov
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="info_bar_frame">
<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="GtkInfoBar" id="info_bar">
<property name="can_focus">False</property>
<property name="baseline_position">bottom</property>
<property name="message_type">other</property>
<property name="show_close_button">True</property>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</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" id="info_bar_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkScrolledWindow" id="info_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="info_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">2</property>
<property name="bottom_margin">2</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkBox" id="bottom_bar_box">
<property name="height_request">24</property>
<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="margin_top">2</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="count_image">

View File

@@ -42,8 +42,8 @@ from app.tools.picons import (PiconsParser, parse_providers, Provider, convert_t
PiconsError)
from app.tools.satellites import SatellitesParser, SatelliteSource
from .dialogs import show_dialog, DialogType, get_message, get_builder, get_chooser_dialog
from .main_helper import (update_entry_data, append_text_to_tview, scroll_to, on_popup_menu, get_base_model, set_picon,
get_picon_pixbuf, get_picon_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
@@ -56,6 +56,8 @@ class PiconManager(Gtk.Box):
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)
@@ -84,10 +86,7 @@ class PiconManager(Gtk.Box):
"on_extract": self.on_extract,
"on_receive": self.on_receive,
"on_cancel": self.on_cancel,
"on_send": self.on_send,
"on_download": self.on_download,
"on_remove": self.on_remove,
"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,
@@ -98,9 +97,6 @@ class PiconManager(Gtk.Box):
"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,
@@ -167,10 +163,6 @@ class PiconManager(Gtk.Box):
self._info_check_button = builder.get_object("info_check_button")
self._picon_info_image = builder.get_object("picon_info_image")
self._picon_info_label = builder.get_object("picon_info_label")
self._info_view = builder.get_object("info_view")
self._info_bar = builder.get_object("info_bar")
self._info_bar.bind_property("visible", builder.get_object("info_bar_frame"), "visible")
self._info_bar.connect("response", lambda b, r: b.set_visible(False))
# Filter.
self._filter_bar = builder.get_object("filter_bar")
self._auto_filter_switch = builder.get_object("auto_filter_switch")
@@ -179,8 +171,6 @@ class PiconManager(Gtk.Box):
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._send_button, "visible")
self._filter_button.bind_property("visible", self._download_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")
@@ -343,15 +333,6 @@ class PiconManager(Gtk.Box):
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:
@@ -419,13 +400,12 @@ class PiconManager(Gtk.Box):
@run_idle
def show_assign_info(self, fav_ids):
self._info_bar.show()
self._info_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(f"Picon assignment for the service:\n{info}\n{' * ' * 30}\n")
log(f"Picon assignment for the service:\n{info}\n{' * ' * 30}\n")
def on_picons_view_drag_end(self, view, drag_context):
self.update_picons_dest_view(self._app.picons_buffer)
@@ -450,21 +430,6 @@ class PiconManager(Gtk.Box):
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:
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 len(uris) == 2:
@@ -526,12 +491,12 @@ class PiconManager(Gtk.Box):
def on_selective_send(self, view):
path = self.get_selected_path(view)
if path:
self.on_send(files_filter={path.name}, path=path.parent)
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)
@@ -541,20 +506,33 @@ class PiconManager(Gtk.Box):
def on_local_remove(self, view):
model, paths = view.get_selection().get_selected_rows()
if paths and show_dialog(DialogType.QUESTION, self._app_window) == 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)
self._app.update_picons()
base_model = get_base_model(model)
filter_model = model.get_model()
to_del = []
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, item=None, files_filter=None, path=None):
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.profile_picons_path = f"{dest_path}{SEP}"
@@ -562,19 +540,21 @@ class PiconManager(Gtk.Box):
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):
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.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):
@@ -582,7 +562,6 @@ class PiconManager(Gtk.Box):
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))
@@ -663,7 +642,7 @@ class PiconManager(Gtk.Box):
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
@@ -751,7 +730,7 @@ class PiconManager(Gtk.Box):
@run_task
def start_download(self, providers):
self._is_downloading = True
GLib.idle_add(self._info_bar.set_visible, 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]):
@@ -797,7 +776,7 @@ class PiconManager(Gtk.Box):
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)
@@ -816,10 +795,9 @@ class PiconManager(Gtk.Box):
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(f"Error: {str(e)}\n")
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)
@@ -837,16 +815,17 @@ class PiconManager(Gtk.Box):
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(f"Getting links to picons for: {prv.name}.\n")
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._info_view)
@run_task
def resize(self, path):
self.show_info_message(get_message("Resizing..."), Gtk.MessageType.INFO)
@@ -881,7 +860,7 @@ class PiconManager(Gtk.Box):
@run_task
def run_func(self, func, update=False):
try:
GLib.idle_add(self._info_bar.set_visible, True)
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:
@@ -894,9 +873,6 @@ class PiconManager(Gtk.Box):
def show_info_message(self, text, message_type):
self._app.show_info_message(text, message_type)
def on_picons_dir_open(self, entry, icon, event_button):
update_entry_data(entry, self._app_window, settings=self._settings)
@run_idle
def on_selected_toggled(self, toggle, path):
model = self._providers_view.get_model()
@@ -1033,11 +1009,10 @@ class PiconManager(Gtk.Box):
self._app.show_error_message("Select paths!")
return
self._info_bar.set_visible(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

View File

@@ -2,7 +2,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
@@ -34,17 +34,17 @@ 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.settings import PlayStreamsMode, IS_DARWIN, SettingsType, USE_HEADER_BAR
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
from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, Column, Page
class PlayerBox(Gtk.Box):
def __init__(self, app, *args, **kwargs):
super().__init__(*args, **kwargs)
def __init__(self, app, **kwargs):
super().__init__(**kwargs)
# Signals.
GObject.signal_new("playback-full-screen", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
@@ -54,9 +54,13 @@ class PlayerBox(Gtk.Box):
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("stop", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
GObject.signal_new("pause", 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)
@@ -104,6 +108,9 @@ class PlayerBox(Gtk.Box):
if mode is not FavClickMode.STREAM and not self._app.http_api:
return
if len(self._fav_view.get_model()) == 0:
return
self._fav_view.set_sensitive(False)
if mode is FavClickMode.STREAM:
self.on_play_stream()
@@ -112,6 +119,42 @@ class PlayerBox(Gtk.Box):
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()
@@ -151,7 +194,7 @@ class PlayerBox(Gtk.Box):
video_menu = builder.get_object("video_menu")
subtitle_menu = builder.get_object("subtitle_menu")
if not IS_GNOME_SESSION:
if not USE_HEADER_BAR:
menu_bar = self._app.get_menubar()
menu_bar.insert_section(1, None, audio_menu)
menu_bar.insert_section(2, None, video_menu)
@@ -246,7 +289,9 @@ class PlayerBox(Gtk.Box):
def on_press(self, area, event):
if event.button == Gdk.BUTTON_PRIMARY:
if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
if event.type == Gdk.EventType.BUTTON_PRESS:
self.emit("pause", None)
elif event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
self.on_full_screen()
def on_key_press(self, widget, event):
@@ -266,7 +311,7 @@ class PlayerBox(Gtk.Box):
if click_mode is FavClickMode.PLAY:
self.on_play_service()
elif click_mode is FavClickMode.ZAP_PLAY:
self.on_zap(self.on_watch)
self._app.on_zap(self.on_watch)
elif click_mode is FavClickMode.STREAM:
self.on_play_stream()
@@ -302,7 +347,7 @@ class PlayerBox(Gtk.Box):
widget.set_size_request(w * 0.6, -1)
@run_idle
def show_playback_window(self):
def show_playback_window(self, title=None):
width, height = 480, 240
size = self._app.app_settings.get("playback_window_size")
if size:
@@ -310,9 +355,9 @@ class PlayerBox(Gtk.Box):
if self._playback_window:
self._playback_window.show()
self._playback_window.set_title(self.get_playback_title())
self._playback_window.set_title(title or self.get_playback_title())
else:
self._playback_window = Gtk.Window(title=self.get_playback_title(),
self._playback_window = Gtk.Window(title=title or self.get_playback_title(),
window_position=Gtk.WindowPosition.CENTER,
icon_name="demon-editor")
@@ -381,7 +426,7 @@ class PlayerBox(Gtk.Box):
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):
def play(self, url, title=None):
if self._play_mode is PlayStreamsMode.M3U:
self._app.save_stream_to_m3u(url)
return
@@ -393,7 +438,7 @@ class PlayerBox(Gtk.Box):
if self._play_mode is PlayStreamsMode.BUILT_IN:
self.show()
elif self._play_mode is PlayStreamsMode.WINDOW:
self.show_playback_window()
self.show_playback_window(title)
if self._player:
self.emit("play", url)

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,943 +0,0 @@
# -*- 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 re
import time
from math import fabs
from pyexpat import ExpatError
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, 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.glade"
class SatellitesTool(Gtk.Box):
_aggr = [None for x in range(9)] # aggregate
def __init__(self, app, settings, *args, **kwargs):
super().__init__(*args, **kwargs)
self._app = app
self._settings = settings
self._current_sat_path = None
handlers = {"on_remove": self.on_remove,
"on_update": self.on_update,
"on_up": self.on_up,
"on_down": self.on_down,
"on_button_press": self.on_button_press,
"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_satellite_selection": self.on_satellite_selection}
builder = get_builder(_UI_PATH, handlers, use_str=True,
objects=("satellite_editor_box", "satellite_view_model", "transponder_view_model",
"satellite_popup_menu", "transponder_popup_menu", "left_header_menu",
"popup_menu_add_image", "popup_menu_add_image_2"))
self._satellite_view = builder.get_object("satellite_view")
self._transponder_view = builder.get_object("transponder_view")
builder.get_object("sat_pos_column").set_cell_data_func(builder.get_object("sat_pos_renderer"),
self.sat_pos_func)
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.pack_start(builder.get_object("satellite_editor_box"), True, True, 0)
self._app.connect("profile-changed", lambda a, m: self.load_satellites_list())
self.show()
self.load_satellites_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)
@run_idle
def on_open(self):
response = get_chooser_dialog(self._app.app_window, self._settings, "satellites.xml", ("*.xml",))
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
if not str(response).endswith("satellites.xml"):
self._app.show_error_message("No satellites.xml file is selected!")
return
self.load_satellites_list(response)
def on_satellite_selection(self, view):
model = self._transponder_view.get_model()
model.clear()
self._current_sat_path, column = view.get_cursor()
if self._current_sat_path:
list(map(model.append, view.get_model()[self._current_sat_path][-1]))
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(self._satellite_view if self._satellite_view.is_focus() else self._transponder_view)
else:
on_popup_menu(menu, event)
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._satellite_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 """
model = self._satellite_view.get_model()
model.clear()
try:
path = path or self._settings.profile_data_path + "satellites.xml"
satellites = get_satellites(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 sat in satellites:
yield model.append(sat)
def on_add(self, view):
""" Common adding """
self.on_edit(view, force=True)
def on_satellite_add(self, item):
self.on_satellite()
def on_transponder_add(self, item):
self.on_transponder()
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()
row = model[paths][:]
itr = model.get_iter(paths)
if view is self._satellite_view:
self.on_satellite(None if force else Satellite(*row), itr)
elif view is self._transponder_view:
self.on_transponder(None if force else Transponder(*row), itr)
def on_satellite(self, satellite=None, edited_itr=None):
""" Create or edit satellite"""
sat_dialog = SatelliteDialog(self._app.get_active_window(), satellite)
sat = sat_dialog.run()
sat_dialog.destroy()
if sat:
model, paths = self._satellite_view.get_selection().get_selected_rows()
if satellite and edited_itr:
model.set(edited_itr, {i: v for i, v in enumerate(sat)})
else:
if len(model):
index = paths[0].get_indices()[0] + 1
model.insert(index, sat)
else:
model.append(sat)
def on_transponder(self, transponder=None, edited_itr=None):
""" Create or edit transponder """
paths = self.check_selection(self._satellite_view, "Please, select only one satellite!")
if paths is None:
return
elif len(paths) == 0:
self._app.show_error_message("No satellite is selected!")
return
dialog = TransponderDialog(self._app.get_active_window(), transponder)
tr = dialog.run()
dialog.destroy()
if tr:
sat_model = self._satellite_view.get_model()
transponders = sat_model[paths][-1]
tr_model, tr_paths = self._transponder_view.get_selection().get_selected_rows()
if transponder 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)
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):
""" Removes selected satellites and transponders. """
selection = view.get_selection()
model, paths = selection.get_selected_rows()
if view is self._satellite_view:
list(map(model.remove, [model.get_iter(path) for path in paths]))
elif view is self._transponder_view:
if self._current_sat_path:
trs = self._satellite_view.get_model()[self._current_sat_path][-1]
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]))
else:
self._app.show_error_message("No satellite is selected!")
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'}")
@run_idle
def on_save(self):
if show_dialog(DialogType.QUESTION, self._app.app_window) == Gtk.ResponseType.CANCEL:
return
write_satellites((Satellite(*r) for r in self._satellite_view.get_model()),
self._settings.profile_data_path + "satellites.xml")
def on_save_as(self):
show_dialog(DialogType.ERROR, transient=self._app.app_window, text="Not implemented yet!")
@run_idle
def on_update(self, item):
SatellitesUpdateDialog(self._app.get_active_window(), self._settings, self._satellite_view.get_model()).show()
# ***************** 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=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")
self._transponders = satellite.transponders if satellite else []
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
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()
pos = "{}{}{}".format("-" if side == 1 else "", *str(pos).split("."))
return Satellite(name=name, flags="0", position=pos, transponders=self._transponders)
# ********************** 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(UI_RESOURCES_PATH + "satellites.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_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._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.FLYSAT
if src == 1:
sat_src = SatelliteSource.LYNGSAT
elif src == 2:
sat_src = SatelliteSource.KINGOFSAT
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.remove(0)
self._source_box.connect("changed", self.on_update_satellites_list)
self._source_box.set_active(0)
@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

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 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
@@ -28,17 +28,14 @@
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_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, IS_GNOME_SESSION
def show_settings_dialog(transient, options):
return SettingsDialog(transient, options).show()
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON, APP_FONT, HeaderBar
class SettingsDialog:
@@ -46,7 +43,7 @@ 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,
@@ -66,6 +63,10 @@ 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,
@@ -100,7 +101,10 @@ class SettingsDialog:
self._dialog.set_margin_left(0)
self._main_stack = builder.get_object("main_stack")
# 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")
@@ -116,16 +120,17 @@ class SettingsDialog:
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._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._record_data_path_field = builder.get_object("record_data_path_field")
self._recordings_path_field = builder.get_object("recordings_path_field")
self._default_data_paths_switch = builder.get_object("default_data_paths_switch")
self._default_data_paths_switch.bind_property("active", self._backup_path_field, "sensitive", 4)
self._default_data_paths_switch.bind_property("active", self._picons_path_field, "sensitive", 4)
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")
@@ -133,8 +138,6 @@ class SettingsDialog:
# Settings type.
self._enigma_radio_button = builder.get_object("enigma_radio_button")
self._neutrino_radio_button = builder.get_object("neutrino_radio_button")
self._support_ver5_switch = builder.get_object("support_ver5_switch")
self._force_bq_name_switch = builder.get_object("force_bq_name_switch")
# Streaming.
self._apply_presets_button = builder.get_object("apply_presets_button")
self._transcoding_switch = builder.get_object("transcoding_switch")
@@ -149,14 +152,12 @@ 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")
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")
@@ -173,51 +174,48 @@ class SettingsDialog:
self._new_color_button = builder.get_object("new_color_button")
self._extra_color_button = builder.get_object("extra_color_button")
# 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._unlimited_buffer_switch = builder.get_object("unlimited_buffer_switch")
self._enable_extensions_switch = builder.get_object("enable_extensions_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.
self._enable_exp_switch = builder.get_object("enable_experimental_switch")
self._enable_exp_switch.bind_property("active", builder.get_object("yt_dl_box"), "sensitive")
self._enable_yt_dl_switch.bind_property("active", builder.get_object("yt_dl_update_box"), "sensitive")
self._enable_exp_switch.bind_property("active", builder.get_object("v5_support_box"), "sensitive")
self._enable_exp_switch.bind_property("active", builder.get_object("enable_direct_playback_box"), "sensitive")
# Enigma2 only.
self._enigma_radio_button.bind_property("active", builder.get_object("bq_naming_grid"), "sensitive")
self._enigma_radio_button.bind_property("active", builder.get_object("enable_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")
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")
# 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_box"))
builder.get_object("network_settings_frame").add(builder.get_object("network_grid"))
# Style.
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
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_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:
if self._settings.use_header_bar:
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 = HeaderBar()
header_bar.set_custom_title(switcher)
self._dialog.set_titlebar(header_bar)
self.init_ui_elements()
@@ -245,7 +243,6 @@ class SettingsDialog:
self._neutrino_radio_button.set_active(self._s_type is SettingsType.NEUTRINO_MP)
self.update_picon_paths()
self.update_title()
self._click_mode_zap_button.set_sensitive(self._support_http_api_switch.get_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)
@@ -261,6 +258,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:
@@ -288,7 +288,7 @@ class SettingsDialog:
self._updated = self.on_save_settings()
dialog.destroy()
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):
@@ -305,7 +305,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)
@@ -316,16 +318,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._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._record_data_path_field.set_text(self._settings.records_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)
@@ -341,6 +345,12 @@ class SettingsDialog:
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._unlimited_buffer_switch.set_active(self._settings.unlimited_copy_buffer)
self._enable_extensions_switch.set_active(self._settings.extensions_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._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)
@@ -366,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()
@@ -375,7 +386,7 @@ 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.epg_dat_path = self._epg_dat_box.get_active_id()
self._settings.picons_path = self._picons_paths_box.get_active_id()
def on_save_settings(self, item=None):
@@ -386,9 +397,10 @@ class SettingsDialog:
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()
@@ -397,7 +409,7 @@ class SettingsDialog:
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.records_path = self._record_data_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())
@@ -418,6 +430,12 @@ 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.unlimited_copy_buffer = self._unlimited_buffer_switch.get_active()
self._ext_settings.extensions_support = self._enable_extensions_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.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()
@@ -494,15 +512,13 @@ 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:
self._support_ver5_switch.set_active(state)
self._unlimited_buffer_switch.set_active(state)
self._enable_send_to_switch.set_active(state)
self._enable_yt_dl_switch.set_active(state)
@@ -528,7 +544,7 @@ class SettingsDialog:
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))
@@ -591,16 +607,54 @@ 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:
sep = "/"
path = response if response.endswith(sep) else response + sep
if path 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)
@@ -637,78 +691,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

View File

@@ -14,9 +14,26 @@
margin: 1px;
}
#stack-switch-button {
padding-top: 2px;
padding-bottom: 2px;
#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 {

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

1765
app/ui/timers.glade Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,561 @@
# -*- 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.settings import USE_HEADER_BAR
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, 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=USE_HEADER_BAR, *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

@@ -2,7 +2,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
@@ -52,7 +52,6 @@ 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()
@@ -86,8 +85,8 @@ if IS_LINUX:
try:
gi.require_version("Notify", "0.7")
from gi.repository import Notify
except ImportError:
pass
except (ImportError, ValueError):
pass # NOP
else:
NOTIFY_IS_INIT = Notify.init("DemonEditor")
elif IS_DARWIN:
@@ -162,6 +161,18 @@ def show_notification(message, timeout=10000, urgency=1):
notify.show()
class HeaderBar(Gtk.HeaderBar):
""" Custom header bar widget. """
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.set_visible(True)
self.set_show_close_button(True)
if IS_DARWIN:
self.set_decoration_layout("close,minimize,maximize")
class Page(Enum):
""" Main stack widget page. """
INFO = "info"
@@ -265,8 +276,18 @@ class Column(IntEnum):
IPTV_TYPE = 1
IPTV_PICON = 2
IPTV_REF = 3
IPTV_FAV_ID = 4
IPTV_PICON_ID = 5
IPTV_URL = 4
IPTV_FAV_ID = 5
IPTV_PICON_ID = 6
IPTV_TOOLTIP = 7
# EPG view
EPG_SERVICE = 0
EPG_TITLE = 1
EPG_START = 2
EPG_END = 3
EPG_LENGTH = 4
EPG_DESC = 5
EPG_DATA = 6
def __index__(self):
""" Overridden to get the index in slices directly """

View File

@@ -9,3 +9,16 @@ switch {
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>

1105
app/ui/xml/dialogs.py Normal file

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,584 @@
# -*- 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
#
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, get_pos_str
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, **kwargs):
super().__init__(**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. """
renderer.set_property("text", get_pos_str(int(model.get_value(itr, 2))))
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

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

File diff suppressed because it is too large Load Diff

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

@@ -1,18 +1,15 @@
#!/bin/bash
VER="2.2.0_Beta"
VER="3.6.1_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
rsync --exclude=__pycache__ -arv ../../extensions $DEB_PATH
cd dist
fakeroot dpkg-deb --build DemonEditor
fakeroot dpkg-deb -Zxz --build DemonEditor
mv DemonEditor.deb DemonEditor_$VER.deb
rm -R DemonEditor

View File

@@ -1,5 +1,5 @@
Package: demon-editor
Version: 2.2.0-Beta
Version: 3.6.1-Beta
Section: utils
Priority: optional
Architecture: all
@@ -8,9 +8,18 @@ Depends: python3 (>= 3.6),
python3-requests,
python3-gi,
python3-gi-cairo,
gir1.2-notify-0.7
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

@@ -1,10 +1,13 @@
[Desktop Entry]
Version=1.0
Name=DemonEditor
GenericName=Enigma2 bouquets editor
GenericName[it]=Editor di bouquet per Enigma2
Comment=Channel and satellite list editor for Enigma2
Comment[ru]=Редактор списка каналов и спутников для Enigma2
Comment[be]=Рэдактар спіса каналаў і спадарожнікаў для Enigma2
Comment[de]=Programm- und Satellitenlisten-Editor für Enigma2
Comment[it]=Editor di liste canali e satelliti per Enigma2
Comment[tr]=Enigma2 için kanal ve uydu listesi editörü
Comment[es]=Editor de listas de canales y satélites para Enigma2
Icon=demon-editor

View File

@@ -21,8 +21,11 @@ excludes = ['app.tools.mpv',
ui_files = [('app/ui/*.glade', 'ui'),
('app/ui/*.css', 'ui'),
('app/ui/*.ui', 'ui'),
('app/ui/epg/*.glade', 'ui/epg'),
('app/ui/xml/*.glade', 'ui/xml'),
('app/ui/lang*', 'share/locale'),
('app/ui/icons*', 'share/icons')
('app/ui/icons*', 'share/icons'),
('extensions/*', 'extensions')
]
a = Analysis([EXE_NAME],
@@ -32,6 +35,15 @@ a = Analysis([EXE_NAME],
hiddenimports=['fileinput', 'uuid'],
hookspath=[],
runtime_hooks=[],
hooksconfig={
"gi": {
"languages": ["en", "be", "es", "it", "nl",
"pl", "pt", "ru", "tr", "zh_CN"],
"module-versions": {
"Gtk": "3.0"
},
},
},
excludes=excludes,
win_no_prefer_redirects=False,
win_private_assemblies=False,
@@ -69,7 +81,8 @@ app = BUNDLE(coll,
'CFBundleGetInfoString': "Enigma2 channel and satellite editor",
'LSApplicationCategoryType': 'public.app-category.utilities',
'LSMinimumSystemVersion': '10.13',
'CFBundleShortVersionString': f"2.2.0.{BUILD_DATE} Beta",
'NSHumanReadableCopyright': u"Copyright © 2022, Dmitriy Yefremov",
'NSRequiresAquaSystemAppearance': 'false'
'CFBundleShortVersionString': f"3.6.1.{BUILD_DATE} Beta",
'NSHumanReadableCopyright': u"Copyright © 2023, Dmitriy Yefremov",
'NSRequiresAquaSystemAppearance': 'false',
'NSHighResolutionCapable': 'true'
})

View File

@@ -18,8 +18,11 @@ excludes = ['app.tools.mpv',
ui_files = [('app\\ui\\*.glade', 'ui'),
('app\\ui\\*.css', 'ui'),
('app\\ui\\*.ui', 'ui'),
('app\\ui\\epg\\*.glade', 'ui\\epg'),
('app\\ui\\xml\\*.glade', 'ui\\xml'),
('app\\ui\\lang*', 'share\\locale'),
('app\\ui\\icons*', 'share\\icons')
('app\\ui\\icons*', 'share\\icons'),
('extensions\\*', 'extensions')
]
@@ -30,6 +33,16 @@ a = Analysis([EXE_NAME],
hiddenimports=['fileinput', 'uuid', 'ctypes.wintypes'],
hookspath=[],
runtime_hooks=[],
hooksconfig={
"gi": {
"languages": ["en", "be", "es", "it", "nl",
"pl", "pt", "ru", "tr", "zh_CN"],
"module-versions": {
"Gtk": "3.0",
"GtkSource": "3",
},
},
},
excludes=excludes,
win_no_prefer_redirects=False,
win_private_assemblies=False,

View File

@@ -1,8 +1,14 @@
#!/usr/bin/env python3
import os
import ssl
if __name__ == "__main__":
from multiprocessing import freeze_support
from app.ui.main import start_app
os.environ["PYTHONUTF8"] = "1"
# TODO There needs to be a more "correct" way.
ssl._create_default_https_context = ssl._create_unverified_context
freeze_support()
start_app()

View File

@@ -1,10 +1,13 @@
[Desktop Entry]
Version=1.0
Name=DemonEditor
GenericName=Enigma2 bouquets editor
GenericName[it]=Editor di bouquet per Enigma2
Comment=Channel and satellite list editor for Enigma2
Comment[ru]=Редактор списка каналов и спутников для Enigma2
Comment[be]=Рэдактар спіса каналаў і спадарожнікаў для Enigma2
Comment[de]=Programm- und Satellitenlisten-Editor für Enigma2
Comment[it]=Editor di liste canali e satelliti per Enigma2
Comment[tr]=Enigma2 için kanal ve uydu listesi editörü
Comment[es]=Editor de listas de canales y satélites para Enigma2
Icon=demon-editor

10
extensions/README.md Normal file
View File

@@ -0,0 +1,10 @@
Extension packages must be located in the following paths:
``` app/ui/extensions```, ``` your data path/tools/extensions ```.
For builds:
``` Program Root\extensions ```
Extensions and examples can be found [here](https://github.com/DYefremov/demoneditor-extensions).
The possibilities of extending the API, as well as the creation and publication of the necessary extensions, can be discussed there.
### Pull requests for extensions are not accepted here!

104
extensions/__init__.py Normal file
View File

@@ -0,0 +1,104 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 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 json
import logging
import os
from pathlib import Path
CONFIG_PATH = f"{Path.home()}{os.sep}.config{os.sep}demon-editor{os.sep}extensions{os.sep}"
class Singleton(type):
_INSTANCE = None
def __call__(cls, *args, **kwargs):
if not cls._INSTANCE:
cls._INSTANCE = type.__call__(cls, *args, **kwargs)
return cls._INSTANCE
class BaseExtension(metaclass=Singleton):
""" Base extension (plugin) class. """
# The label that will be displayed in the "Tools" menu.
LABEL = "Base extension"
VERSION = "1.0"
# Additional flags.
EMBEDDED = False
SWITCHABLE = False
_LOGGER_NAME = "main_logger"
def __init__(self, app):
# Current application instance.
# It can be used all public methods, properties or signals.
self.app = app
self._config_path = f"{CONFIG_PATH}{self.__class__.__name__}{os.sep}config"
self.log(f"Extension initialized...")
def exec(self):
""" Triggers an action for the given extension.
E.g. shows a dialog or runs an external script.
"""
self.app.show_info_message(f"Hello from {self.__class__.__name__} class!")
def stop(self):
""" Stops (terminates) the task or the extension itself. """
self.log("Terminating a task...")
def log(self, message, level=logging.ERROR):
""" Shows log messages. """
logging.getLogger(self._LOGGER_NAME).log(level, f"[{self.__class__.__name__}] {message}")
def reset_config(self):
path = Path(self._config_path)
if path.is_file():
path.unlink()
@property
def config(self) -> dict:
if not Path(self._config_path).is_file():
return {}
with open(self._config_path, "r", encoding="utf-8") as config_file:
try:
return json.load(config_file)
except ValueError as e:
self.log(f"Configuration load error: {e}")
return {}
@config.setter
def config(self, value: dict):
Path(self._config_path).parent.mkdir(parents=True, exist_ok=True)
with open(self._config_path, "w", encoding="utf-8") as config_file:
json.dump(value, config_file, indent=" ")
if __name__ == "__main__":
pass

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2018-2021 Dmitriy Yefremov
# Copyright (C) 2018-2023 Dmitriy Yefremov
# This file is distributed under the MIT license.
#
#
@@ -1253,6 +1253,9 @@ msgstr "Выконваецца загрузка дадзеных!"
msgid "Recordings"
msgstr "Запісы"
msgid "Recordings:"
msgstr "Запісы:"
msgid "Help"
msgstr "Даведка"
@@ -1283,6 +1286,9 @@ msgstr "Аўтаматычная ўсталёўка імя са спіса аб
msgid "Playback"
msgstr "Прайграванне"
msgid "Playback:"
msgstr "Прайграванне:"
msgid "Audio"
msgstr "Аўдыё"
@@ -1348,3 +1354,137 @@ msgstr "Дадаць закладку"
msgid "All bouquets"
msgstr "Усе букеты"
msgid "Playback from the main list"
msgstr "Прайграванне з асноўнага спіса"
msgid "Enables URL parsing using youtube-dl to get direct links to media."
msgstr "Улучае аналіз URL-адрасоў з дапамогай youtube-dl для атрымання прамых спасылак на медыя."
msgid "Permissions..."
msgstr "Дазволы..."
msgid "Display EPG in bouquet list"
msgstr "Адлюстроўваць EPG у спісе букета"
msgid "EPG *.dat file:"
msgstr "Файл EPG *.dat:"
msgid "Use HTTP to reload data in the receiver"
msgstr "Скарыстаць HTTP для перазагрузкі дадзеных у рэсіверы"
msgid "Enable picons compression"
msgstr "Уключыць сціск пiконаў"
msgid "Update interval (sec):"
msgstr "Інтэрвал абнаўлення (сек):"
msgid "Update:"
msgstr "Абнаўляць:"
msgid "Daily"
msgstr "Штодня"
msgid "Assign reference"
msgstr "Прысвоіць спасылку"
msgid "Specify hostname or IP address"
msgstr "Задайце імя хоста або IP-адрас"
msgid "Default selection"
msgstr "Выбар па змаўчанні"
msgid "Don't change power state"
msgstr "Не змяняць стан сілкавання"
msgid "Don't toggle standby mode when updating bouquets and services."
msgstr "Не перамыкаць у рэжым чакання пры абнаўленні букетаў і сэрвісаў."
msgid "Region"
msgstr "Рэгіён"
msgid "Provider"
msgstr "Правайдар"
msgid "Enables upload as an archive if a large number of picon (> 1000) is selected.\n"
" Recommended only if you have external storage."
msgstr "Улучае загрузку ў выглядзе архіва, калі абрана вялікая колькасць пiконаў (> 1000).\n"
" Рэкамендуецца, толькі калі ў вас ёсць вонкавае сховішча."
msgid "Clear \"New\" flag"
msgstr "Здаліць сцяжок \"New\""
msgid "Group by"
msgstr "Групаваць па"
msgid "Replace existing"
msgstr "Замяніць існуючыя"
msgid "Already exists"
msgstr "Ужо існуе"
msgid "Enable unlimited copy buffer"
msgstr "Уключыць неабмежаваны буфер капіявання"
msgid "Enables unlimited copy buffer for the bouquets tab."
msgstr "Улучае неабмежаваны буфер капіявання для ўкладкі букетаў."
msgid "Start time"
msgstr "Пачатак"
msgid "End time"
msgstr "Сканчэнне"
msgid "Enable extensions support"
msgstr "Уключыць падтрымку пашырэнняў"
msgid "After uploading the changes you may need to completely reboot the receiver!"
msgstr "Пасля загрузкі змен можа запатрабавацца перазапуск рэсівера!"
msgid "Remove duplicates"
msgstr "Выдаліць дублікаты"
msgid "Removed"
msgstr "Выдалена"
msgid "Enables overwriting existing main list services."
msgstr "Улучае перазапіс існых сэрвісаў асноўнага спіса."
msgid "Enables skipping services import from lamedb."
msgstr "Улучае пропуск імпарту сэрвісаў з lamedb."
msgid "Bouquets data only"
msgstr "Толькі дадзеныя букетаў"
msgid "Create Category bouquets"
msgstr "Стварыць букеты па катэгорыях"
msgid "Create Regional bouquets"
msgstr "Стварыць букеты па рэгіёнах"
msgid "Skip C-band"
msgstr "Прапусціць C-дыяпазон"
msgid "Automatically set the name selected in the bouquet list."
msgstr "Аўтаматычная ўсталёўка імя, абранага ў спісе букетаў."
msgid "Merge satellites by positions"
msgstr "Аб'яднаць спадарожнікі па пазіцыях"
msgid "Save satellite selection"
msgstr "Захоўваць выбар спадарожнікаў"
msgid "Extract direct links"
msgstr "Выняць простыя спасылкі"
msgid "URL prefix:"
msgstr "Префикс URL:"
msgid "Invalid prefix for the given URL!"
msgstr "Недапушчальны прэфікс для дадзенага URL!"
msgid "Alternate window title"
msgstr "Альтэрнатыўны загаловак акна"
msgid "Selected type:"
msgstr "Абраны тып:"

View File

@@ -2,8 +2,8 @@
# This file is distributed under the MIT license.
#
# Charly, 2019.
# Dmitriy Yefremov, 2020-2021.
# Thomas Schmidt, 2021
# Dmitriy Yefremov, 2020-2023.
# Thomas Schmidt, 2021.
msgid ""
msgstr ""
"Last-Translator: Dmitriy Yefremov\n"
@@ -935,7 +935,7 @@ msgid "Built-in player"
msgstr "Integrierter Player"
msgid "In a separate window"
msgstr "In einem separaten Fenster"
msgstr "In separatem Fenster"
msgid "Only get m3u file"
msgstr "Nur m3u-Datei erhalten"
@@ -1267,6 +1267,9 @@ msgstr "Daten werden geladen!"
msgid "Recordings"
msgstr "Aufnahmen"
msgid "Recordings:"
msgstr "Aufnahmen:"
msgid "Help"
msgstr "Hilfe"
@@ -1297,8 +1300,11 @@ msgstr "Automatisch den in der Favoritenliste ausgewählten Namen einstellen."
msgid "Playback"
msgstr "Wiedergabe"
msgid "Playback:"
msgstr "Wiedergabe:"
msgid "Audio"
msgstr ""
msgstr "Audio"
msgid "Audio Track"
msgstr "Audio"
@@ -1363,3 +1369,136 @@ msgstr "Lesezeichen hinzufügen"
msgid "All bouquets"
msgstr "Alle Bouquets"
msgid "Playback from the main list"
msgstr "Wiedergabe aus der Hauptliste"
msgid "Enables URL parsing using youtube-dl to get direct links to media."
msgstr "Aktiviert URL-Parsing mit youtube-dl, um direkte Links zu Medien zu erhalten."
msgid "Permissions..."
msgstr "Berechtigungen..."
msgid "Display EPG in bouquet list"
msgstr "EPG in Bouquet-Liste anzeigen"
msgid "EPG *.dat file:"
msgstr "EPG *.dat Datei:"
msgid "Use HTTP to reload data in the receiver"
msgstr "HTTP verwenden, um Daten in den Receiver updaten"
msgid "Enable picons compression"
msgstr "Picon-Komprimierung aktivieren"
msgid "Update interval (sec):"
msgstr "Aktualisierungsintervall (sec):"
msgid "Update:"
msgstr "Aktualisieren:"
msgid "Daily"
msgstr "Täglich"
msgid "Assign reference"
msgstr "Referenz zuweisen"
msgid "Specify hostname or IP address"
msgstr "Hostname oder IP-Adresse angeben"
msgid "Default selection"
msgstr "Standardauswahl"
msgid "Don't change power state"
msgstr "Energiezustand nicht ändern"
msgid "Don't toggle standby mode when updating bouquets and services."
msgstr "Schalten beim Aktualisieren von Bouquets und Services nicht in den Standby-Modus um."
msgid "Region"
msgstr "Region"
msgid "Provider"
msgstr "Provider"
msgid "Enables upload as an archive if a large number of picon (> 1000) is selected.\n"
" Recommended only if you have external storage."
msgstr "Ermöglicht das Hochladen als Archiv, wenn eine große Anzahl von Picons (> 1000)"
" ausgewählt ist. Empfohlen nur, wenn einen externen Speicher verfügt wird."
msgid "Clear \"New\" flag"
msgstr "Das \"Neu\"-Flag entfernen"
msgid "Group by"
msgstr "Gruppieren nach"
msgid "Replace existing"
msgstr "Vorhandene ersetzen"
msgid "Already exists"
msgstr "Bereits vorhanden"
msgid "Enable unlimited copy buffer"
msgstr "Unbegrenzte Zwischenablage aktivieren"
msgid "Enables unlimited copy buffer for the bouquets tab."
msgstr "Aktiviert unbegrenzte Zwischenablage für die Bouquets-Tab."
msgid "Start time"
msgstr "Anfangszeit"
msgid "End time"
msgstr "Endzeit"
msgid "Enable extensions support"
msgstr "Erweiterungen Unterstützung aktivieren"
msgid "After uploading the changes you may need to completely reboot the receiver!"
msgstr "Nach dem Hochladen der Änderungen muss den Receiver eventuell komplett neu starten!"
msgid "Remove duplicates"
msgstr "Duplikate entfernen"
msgid "Removed"
msgstr "Entfernt"
msgid "Enables overwriting existing main list services."
msgstr "Ermöglicht das Überschreiben vorhandener Hauptlistendienste."
msgid "Enables skipping services import from lamedb."
msgstr "Ermöglicht das Überspringen von Dienstimporten aus lamedb."
msgid "Bouquets data only"
msgstr "Nur Bouquets-Daten"
msgid "Create Category bouquets"
msgstr "Erstellen Kategories-Bouquets"
msgid "Create Regional bouquets"
msgstr "Erstellen Regionale-Bouquets"
msgid "Skip C-band"
msgstr "C-Band überspringen"
msgid "Automatically set the name selected in the bouquet list."
msgstr "Stellen in der Bouquet-Liste ausgewählten Namen automatisch ein."
msgid "Merge satellites by positions"
msgstr "Satelliten nach Pos. zusammenführen"
msgid "Save satellite selection"
msgstr "Satellitenauswahl speichern"
msgid "Extract direct links"
msgstr "Direktlinks extrahieren"
msgid "URL prefix:"
msgstr "URL-Präfix:"
msgid "Invalid prefix for the given URL!"
msgstr "Ungültiges Präfix für die angegebene URL!"
msgid "Alternate window title"
msgstr "Alternativer Fenstertitel"
msgid "Selected type:"
msgstr "Ausgewählt Typ:"

View File

@@ -1,14 +1,14 @@
# Copyright (C) 2018-2022 Víctor Pont
# Copyright (C) 2018-2023 Frank Neirynck
# This file is distributed under the MIT license.
#
# Frank Neirynck <frank@insink.be>, 2018-2020.
# Frank Neirynck <frank@insink.be>, 2018-2023.
# Víctor Pont <victor@pont.cat>, 2021-2022.
msgid ""
msgstr ""
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: Víctor Pont\n"
"Last-Translator: Frank Neirynck\n"
"Language-Team: \n"
"Language: es\n"
"MIME-Version: 1.0\n"
@@ -1382,3 +1382,116 @@ msgstr "Añadir marcador"
msgid "All bouquets"
msgstr "Todos los bouquets"
msgid "Playback from the main list"
msgstr "Reproducción desde la lista principal"
msgid "Enables URL parsing using youtube-dl to get direct links to media."
msgstr "Habilita el análisis de URL usando youtube-dl para obtener enlaces directos a los medios."
msgid "Permissions..."
msgstr "Permisos..."
msgid "Display EPG in bouquet list"
msgstr "Mostrar EPG en la lista de bouquet"
msgid "EPG *.dat file:"
msgstr "Archivo EPG *.dat:"
msgid "Use HTTP to reload data in the receiver"
msgstr "Use HTTP para recargar datos en el receptor"
msgid "Enable picons compression"
msgstr "Habilitar la compresión de picons"
msgid "Update interval (sec):"
msgstr "Intervalo de actualización (sec):"
msgid "Update:"
msgstr "Actualización:"
msgid "Daily"
msgstr "Diariamente"
msgid "Assign reference"
msgstr "Asignar referencia"
msgid "Specify hostname or IP address"
msgstr "Especifique el nombre de host o la dirección IP"
msgid "Default selection"
msgstr "Selección predeterminada"
msgid "Don't change power state"
msgstr "No apagues el dispositivo"
msgid "Don't toggle standby mode when updating bouquets and services."
msgstr "No cambie el modo de espera al actualizar bouquets y servicios."
msgid "Region"
msgstr "Región"
msgid "Provider"
msgstr "Proveedor"
msgid "Enables upload as an archive if a large number of picon (> 1000) is selected.\n"
" Recommended only if you have external storage."
msgstr "Habilita la carga como un archivo si se selecciona una gran cantidad de picon (> 1000).\n"
" Recomendado solo si tienes almacenamiento externo."
msgid "Clear \"New\" flag"
msgstr "Limpiar \"Nuevo\" flag"
msgid "Group by"
msgstr "Agrupar por"
msgid "Replace existing"
msgstr "Sustituir los existentes"
msgid "Already exists"
msgstr "Ya existe"
msgid "Enable unlimited copy buffer"
msgstr "Activar búfer de copia ilimitado"
msgid "Enables unlimited copy buffer for the bouquets tab."
msgstr "Activa el búfer de copia ilimitado para la pestaña de bouquets."
msgid "Start time"
msgstr "Hora de inicio"
msgid "End time"
msgstr "Hora final"
msgid "Enable extensions support"
msgstr "Activar el soporte de extensiones"
msgid "After uploading the changes you may need to completely reboot the receiver!"
msgstr "Después de cargar los cambios, es posible que tenga que reiniciar completamente el receptor."
msgid "Remove duplicates"
msgstr "Eliminar duplicados"
msgid "Removed"
msgstr "Eliminado"
msgid "Enables overwriting existing main list services."
msgstr "Permite sobrescribir los servicios existentes de la lista principal."
msgid "Enables skipping services import from lamedb."
msgstr "Permite omitir la importación de servicios desde lamedb."
msgid "Bouquets data only"
msgstr "Sólo datos de bouquets"
msgid "Create Category bouquets"
msgstr "Crear categoría de bouquets"
msgid "Create Regional bouquets"
msgstr "Crear bouquets regionales"
msgid "Skip C-band"
msgstr "Omitir banda C"
msgid "Automatically set the name selected in the bouquet list."
msgstr "Establece automáticamente el nombre seleccionado en la lista de bouquets."

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
# Copyright (C) 2018-2020 Frank Neirynck
# Copyright (C) 2018-2023 Frank Neirynck
# This file is distributed under the MIT license.
#
# Frank Neirynck <frank@insink.be>, 2018-2020.
# Frank Neirynck <frank@insink.be>, 2018-2022.
#
msgid ""
msgstr ""
@@ -25,7 +25,7 @@ msgid "Type"
msgstr "Type"
msgid "Picon"
msgstr "Picon"
msgstr "Pict."
msgid "Freq"
msgstr "Freq."
@@ -148,10 +148,10 @@ msgid "Parent lock On/Off Ctrl + L"
msgstr "Ouderlijk slot aan/uit Ctrl + L"
msgid "Picons"
msgstr "Picons"
msgstr "Pictogrammen"
msgid "Picons downloader"
msgstr "Picons downloader"
msgstr "Pictogrammen downloader"
msgid "Satellites downloader"
msgstr "Satellieten downloader"
@@ -286,22 +286,22 @@ msgid "Providers"
msgstr "Leveranciers"
msgid "Receive picons"
msgstr "Ontvang picons"
msgstr "Ontvang pictogrammen"
msgid "Picons name format:"
msgstr "Picons naam formaat:"
msgstr "Pictogrammen naam formaat:"
msgid "Resize:"
msgstr "Verklein/vergroot:"
msgid "Current picons path:"
msgstr "Huidig pad picons:"
msgstr "Huidig pad Pictogrammen:"
msgid "Receiver picons path:"
msgstr "Ontvanger picons pad:"
msgstr "Ontvanger Pictogrammen pad:"
msgid "Picons download tool"
msgstr "Picons download gereedschap"
msgstr "Pictogrammen download gereedschap"
msgid "Transfer to receiver"
msgstr "Transfereren naar ontvanger"
@@ -319,7 +319,7 @@ msgid "Path to save:"
msgstr "Pad om op te slaan:"
msgid "Path to Enigma2 picons:"
msgstr "Pad naar Enigma2 picons:"
msgstr "Pad naar Enigma2 Pictogrammen:"
msgid "Specify the correct position value for the provider!"
msgstr "Geeft de juiste positie waarde voor de provider!"
@@ -328,16 +328,16 @@ msgid "Converter between name formats"
msgstr "Omvormer tussen naam formaten"
msgid "Receive picons for providers"
msgstr "Ontvang picons voor leveranciers"
msgstr "Ontvang Pictogrammen voor providers"
msgid "Load satellite providers."
msgstr "Laad satelliet leveranciers."
msgstr "Laad satelliet providers."
msgid ""
"To automatically set the identifiers for picons,\n"
"first load the required services list into the main application window."
msgstr ""
"Om automatisch de ID in te stellen voor picons,\n"
"Om automatisch de ID in te stellen voor pictogrammen,\n"
"laad eerst de vereiste serviceslijst in via het hoofdvenster van het programma."
# Satellites editor
@@ -462,7 +462,7 @@ msgid "Password:"
msgstr "Paswoord:"
msgid "Picons:"
msgstr "Picons:"
msgstr "Pictogrammen:"
msgid "Port:"
msgstr "Poort:"
@@ -471,7 +471,7 @@ msgid "Data path:"
msgstr "Gegevenspad:"
msgid "Picons path:"
msgstr "Picons pad:"
msgstr "Pictogrammen pad:"
msgid "Network settings:"
msgstr "Netwerk instellingen:"
@@ -825,7 +825,7 @@ msgid "File"
msgstr "File"
msgid "Picons manager"
msgstr "Picons manager"
msgstr "Pictogrammen manager"
msgid "Explorer"
msgstr "Explorer"
@@ -1013,3 +1013,445 @@ msgstr "Schermafbeelding"
msgid "Video"
msgstr "Vidео"
msgid "The Neutrino has only experimental support. Not all features are supported!"
msgstr "Neutrino heeft alleen experimentele ondersteuning. Niet alle functies worden ondersteund!"
msgid "Enable experimental features"
msgstr "Schakel experimentele functies in"
msgid "Can't Playback!"
msgstr "Kan niet afspelen!"
msgid "Enable Dark Mode"
msgstr "Schakel de donkere modus in"
msgid "Extract..."
msgstr "Extract..."
msgid "Unsupported format!"
msgstr "Niet ondersteund formaat!"
msgid "Combine with the current data?"
msgstr "Combineren met de huidige gegevens?"
msgid "Importing data done!"
msgstr "Gegevens importeren klaar!"
msgid "Current service"
msgstr "Huidige dienst"
msgid "Open folder"
msgstr "Open Map"
msgid "Open archive"
msgstr "Open Archief"
msgid "Import from Web"
msgstr "Importeren van internet"
msgid "Control"
msgstr "Controle"
msgid "Timers"
msgstr "Timers"
msgid "Timer"
msgstr "Timer"
msgid "Add timer"
msgstr "Timer toevoegen"
msgid "Hr."
msgstr "U."
msgid "Min."
msgstr "мin."
msgid "Power"
msgstr "Power"
msgid "Standby"
msgstr "Standby"
msgid "Wake Up"
msgstr "Wake Up"
msgid "Reboot"
msgstr "Herstart"
msgid "Restart GUI"
msgstr "Herstart GUI"
msgid "Shutdown"
msgstr "Uitschakelen"
msgid "Shut down"
msgstr "Uitschakelen"
msgid "Do Nothing"
msgstr "Doe niets"
msgid "Auto"
msgstr "Auto"
msgid "Grab screenshot"
msgstr "Maak schermafbeelding"
msgid "Enabled:"
msgstr "Ingeschakeld:"
msgid "Name:"
msgstr "Naam:"
msgid "Description:"
msgstr "Omschrijving:"
msgid "Service:"
msgstr "Dienst:"
msgid "Service reference:"
msgstr "Service referentie:"
msgid "Event ID:"
msgstr "Event ID:"
msgid "Begins:"
msgstr "Begint:"
msgid "Ends:"
msgstr "Stopt:"
msgid "Repeated:"
msgstr "Herhaald:"
msgid "Action:"
msgstr "Actie:"
msgid "After event:"
msgstr "Na Event:"
msgid "Location:"
msgstr "Locatiе:"
msgid "Mo"
msgstr "Ma"
msgid "Tu"
msgstr "Di"
msgid "We"
msgstr "Wo"
msgid "Th"
msgstr "Do"
msgid "Fr"
msgstr "Vr"
msgid "Sa"
msgstr "Za"
msgid "Su"
msgstr "Zo"
msgid "Set"
msgstr "Set"
msgid "Services update"
msgstr "Services-update"
msgid "Create folder"
msgstr "Map aanmaken"
msgid "FTP client"
msgstr "FTP client"
msgid "The file size is too large!"
msgstr "De bestandsgrootte is te groot!"
msgid "Connect"
msgstr "Verbind"
msgid "Disconnect"
msgstr "Verbreek Verbinding"
msgid "Size"
msgstr "Grootte"
msgid "Date"
msgstr "Datum"
msgid "Toggle display position"
msgstr "Weergavepositie wisselen"
msgid "Alternatives"
msgstr "Alternatieven"
msgid "Add alternatives"
msgstr "Alternatieven toevoegen"
msgid "DreamOS only!"
msgstr "Enkel DreamOS!"
msgid "A similar service is already in this list!"
msgstr "Een vergelijkbare service staat al in deze lijst!"
msgid "Play mode has been changed!\nRestart the program to apply the settings."
msgstr "De afspeelmodus is gewijzigd!\nStart het programma opnieuw om de instellingen toe te passen."
msgid "Set values for TID, NID and Namespace for correct naming of the picons!"
msgstr "Stel waarden in voor TID, NID en Namespace voor correcte naamgeving van de pictogrammen!"
msgid "Streams detected:"
msgstr "Streams gedetecteerd:"
msgid "Download picons"
msgstr "Pictogrammen downloaden"
msgid "Errors:"
msgstr "Fouten:"
msgid "Use to play streams:"
msgstr "Gebruik om streams af te spelen:"
msgid "Font in the lists:"
msgstr "Lettertype in de lijsten:"
msgid "Picons size in the lists:"
msgstr "Pictogramgrootte in de lijsten:"
msgid "Logo size in tooltips:"
msgstr "Logogrootte in knopinfo:"
msgid "Save as"
msgstr "Opslaan als"
msgid "Mark duplicates"
msgstr "Markeer duplicaten"
msgid "Load only for selected bouquet"
msgstr "Laad alleen voor geselecteerd boeket"
msgid "The task is canceled!"
msgstr "De taak is geannuleerd!"
msgid "Data loading in progress!"
msgstr "Gegevens worden geladen!"
msgid "Recordings"
msgstr "Opnames"
msgid "Recordings:"
msgstr "Opnames:"
msgid "Help"
msgstr "Help"
msgid "HTTP API is not activated. Check your settings!"
msgstr "HTTP-API is niet geactiveerd. Controleer uw instellingen!"
msgid "Add picons"
msgstr "Pictogrammen toevoegen"
msgid "Logs"
msgstr "Logs"
msgid "Title"
msgstr "Titel"
msgid "Time"
msgstr "Tijd"
msgid "Length"
msgstr "Lengte"
msgid "Additional source"
msgstr "Extra bron"
msgid "Automatically set the name selected in the favorites list."
msgstr "Stel automatisch de geselecteerde naam in de favorietenlijst in."
msgid "Playback"
msgstr "Weergave"
msgid "Playback:"
msgstr "Weergave:"
msgid "Audio"
msgstr "Audio"
msgid "Audio Track"
msgstr "Audio Spoor"
msgid "Subtitle"
msgstr "Ondertitel"
msgid "Subtitle Track"
msgstr "Ondertitel Spoor"
msgid "Aspect ratio"
msgstr "Beeldverhouding"
msgid "This may change the settings of other profiles!"
msgstr "Hierdoor kunnen de instellingen van andere profielen veranderen!"
msgid "Drag the services to the desired picon or picon to the list of selected services."
msgstr "Sleep de services naar het gewenste pictogram of het pictogram naar de lijst met geselecteerde services."
msgid "Sets the profile folder as default to store picons, backups, etc."
msgstr "Stelt de profielmap in als standaard om pictogrammen, back-ups, etc. op te slaan."
msgid "New sub-bouquet"
msgstr "Nieuw sub-boeket"
msgid "Mark not presented in Bouquets"
msgstr "Stel in als niet in Boeketten"
msgid "Not in Bouquets"
msgstr "Niet in Boeketten"
msgid "Do not show services present in Bouquets."
msgstr "Laat geen services zien die aanwezig zijn in Boeketten."
msgid "IPTV services only"
msgstr "Enkel IPTV diensten"
msgid "Display picons"
msgstr "Toon pictogrammen"
msgid "Alternate layout"
msgstr "Alternatieve indeling"
msgid "Layout of elements has been changed!"
msgstr "Lay-out van elementen is gewijzigd!"
msgid "Restart the program to apply all changes."
msgstr "Start het programma opnieuw om alle wijzigingen toe te passen."
msgid "New folder"
msgstr "Nieuwe map"
msgid "Rename"
msgstr "Hernoem"
msgid "Bookmarks"
msgstr "Bladwijzers"
msgid "Add bookmark"
msgstr "Bladwijzer toevoegen"
msgid "All bouquets"
msgstr "Alle boeketten"
msgid "Playback from the main list"
msgstr "Afspelen vanuit de hoofdlijst"
msgid "Enables URL parsing using youtube-dl to get direct links to media."
msgstr "Schakelt URL-parsing met behulp van youtube-dl in, om directe links naar media te krijgen."
msgid "Permissions..."
msgstr "Rechten..."
msgid "Display EPG in bouquet list"
msgstr "Toon EPG in boeketlijst"
msgid "EPG *.dat file:"
msgstr "EPG *.dat file:"
msgid "Use HTTP to reload data in the receiver"
msgstr "Gebruik HTTP om gegevens opnieuw in de ontvanger te laden"
msgid "Enable picons compression"
msgstr "Pictogrammen-compressie inschakelen"
msgid "Update interval (sec):"
msgstr "Update-interval (sec):"
msgid "Update:"
msgstr "Update:"
msgid "Daily"
msgstr "Dagelijks"
msgid "Assign reference"
msgstr "Referentie toewijzen"
msgid "Specify hostname or IP address"
msgstr "Geef de hostnaam of het IP-adres op"
msgid "Default selection"
msgstr "Standaard selectie"
msgid "Don't change power state"
msgstr "Schakel het toestel niet uit"
msgid "Don't toggle standby mode when updating bouquets and services."
msgstr "Schakel de stand-bymodus niet in bij het updaten van boeketten en services."
msgid "Region"
msgstr "Regio"
msgid "Provider"
msgstr "Provider"
msgid "Enables upload as an archive if a large number of picon (> 1000) is selected.\n"
" Recommended only if you have external storage."
msgstr "Maakt uploaden als archief mogelijk als een groot aantal pictogrammen (> 1000) is geselecteerd.\n"
" Alleen aanbevolen als u externe opslag heeft."
msgid "Clear \"New\" flag"
msgstr "Opruimen \"Niew\" flag"
msgid "Group by"
msgstr "Groeperen per"
msgid "Replace existing"
msgstr "Vervang bestaande"
msgid "Already exists"
msgstr "Bestaat reeds"
msgid "Enable unlimited copy buffer"
msgstr "Onbeperkte kopieerbuffer inschakelen"
msgid "Enables unlimited copy buffer for the bouquets tab."
msgstr "Schakelt een onbeperkte kopieerbuffer in voor het boeketten tabblad."
msgid "Start time"
msgstr "Starttijd"
msgid "End time"
msgstr "Eindtijd"
msgid "Enable extensions support"
msgstr "Ondersteuning voor extensies inschakelen"
msgid "After uploading the changes you may need to completely reboot the receiver!"
msgstr "Na het uploaden van de wijzigingen kan het nodig zijn de ontvanger volledig opnieuw op te starten!"
msgid "Remove duplicates"
msgstr "Duplicaten verwijderen"
msgid "Removed"
msgstr "Verwijderd"
msgid "Enables overwriting existing main list services."
msgstr "Maakt het overschrijven van bestaande hoofdlijst diensten mogelijk."
msgid "Enables skipping services import from lamedb."
msgstr "Maakt het overslaan van dienstenimport uit lamedb mogelijk."
msgid "Bouquets data only"
msgstr "Alleen gegevens over boeketten"
msgid "Create Category bouquets"
msgstr "Maak categorie boeketten"
msgid "Create Regional bouquets"
msgstr "Maak regionale boeketten"
msgid "Skip C-band"
msgstr "C-band overslaan"
msgid "Automatically set the name selected in the bouquet list."
msgstr "Stel automatisch de naam in die is geselecteerd in de boeketlijst."

View File

@@ -4,7 +4,7 @@
#
msgid ""
msgstr ""
"Last-Translator: wwns <https://github.com/wwns>\n"
"Last-Translator: lareq <lareq@lareq.eu>\n"
"Language: pl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -13,10 +13,12 @@ msgstr ""
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Language-Team: \n"
"X-Generator: Poedit 3.0\n"
"X-Generator: Poedit 2.3\n"
msgid "translator-credits"
msgstr "wwns"
msgstr ""
"lareq <lareq@lareq.eu>\n"
"wwns <https://github.com/wwns>"
# Main
msgid "Service"
@@ -215,7 +217,7 @@ msgid "Host:"
msgstr "Host:"
msgid "Loading data..."
msgstr "Ładowanie danych"
msgstr "Ładowanie danych..."
msgid "Receive"
msgstr "Pobierz"
@@ -542,10 +544,10 @@ msgid "Done!"
msgstr "Zrobione!"
msgid "Please, wait..."
msgstr "Proszę czekać"
msgstr "Proszę czekać..."
msgid "Resizing..."
msgstr "Zmiana rozmiaru"
msgstr "Zmiana rozmiaru..."
msgid "Select paths!"
msgstr "Wybierz ścieżki!"
@@ -573,7 +575,7 @@ msgstr "Nie znaleziono VLC. Sprawdź, czy jest zainstalowany!"
# Search unavailable streams dialog
msgid "Please wait, streams testing in progress..."
msgstr "Proszę czekać, trwa testowanie strumieni"
msgstr "Proszę czekać, trwa testowanie strumieni..."
msgid "Found"
msgstr "Znaleziono"
@@ -989,7 +991,7 @@ msgid "Gtk3 Themes and Icons:"
msgstr "Gtk3 motywy i ikony:"
msgid "Deleting data..."
msgstr "Usuwanie danych"
msgstr "Usuwanie danych..."
msgid "Download from the receiver"
msgstr "Pobierz z odbiornika"
@@ -1019,7 +1021,7 @@ msgid "EXPERIMENTAL!"
msgstr "EKSPERYMENTALNE!"
msgid "Sorting data..."
msgstr "Sortowanie danych"
msgstr "Sortowanie danych..."
msgid ""
"There are unsaved changes.\n"
@@ -1059,7 +1061,7 @@ msgid "Enable Dark Mode"
msgstr "Włącz tryb ciemny"
msgid "Extract..."
msgstr "Rozpakuj"
msgstr "Rozpakuj..."
msgid "Unsupported format!"
msgstr "Format nieobsługiwany!"
@@ -1331,3 +1333,45 @@ msgstr "Przeciągnij usługi na żądaną ikonę lub na listę wybranych usług.
msgid "Sets the profile folder as default to store picons, backups, etc."
msgstr "Ustawia folder profilu jako domyślny do przechowywania pikonów, kopii zapasowych itp."
msgid "New sub-bouquet"
msgstr "Nowy sub-bukiet"
msgid "Mark not presented in Bouquets"
msgstr "Zaznacz te, których nie ma w bukietach"
msgid "Not in Bouquets"
msgstr "Nie ma w bukietach"
msgid "Do not show services present in Bouquets."
msgstr "Nie pokazuj usług obecnych w bukietach."
msgid "IPTV services only"
msgstr "Tylko serwisy IPTV"
msgid "Display picons"
msgstr "Wyświetl pikony"
msgid "Alternate layout"
msgstr "Alternatywny wygląd"
msgid "Layout of elements has been changed!"
msgstr "Zmieniono układ elementów!"
msgid "Restart the program to apply all changes."
msgstr "Uruchom ponownie program, aby zastosować wszystkie zmiany."
msgid "New folder"
msgstr "Nowy katalog"
msgid "Bookmarks"
msgstr "Zakładki"
msgid "Add bookmark"
msgstr "Dodaj zakładkę"
msgid "All bouquets"
msgstr "Wszystkie bukiety"
msgid "Playback from the main list"
msgstr "Odtwarzanie z listy głównej"

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2018-2022 Dmitriy Yefremov
# Copyright (C) 2018-2023 Dmitriy Yefremov
# This file is distributed under the MIT license.
#
#
@@ -1250,6 +1250,9 @@ msgstr "Выполняется загрузка данных!"
msgid "Recordings"
msgstr "Записи"
msgid "Recordings:"
msgstr "Записи:"
msgid "Help"
msgstr "Справка"
@@ -1280,6 +1283,9 @@ msgstr "Автоматическая установка имени из спис
msgid "Playback"
msgstr "Воспроизведение"
msgid "Playback:"
msgstr "Воспроизведение:"
msgid "Audio"
msgstr "Аудио"
@@ -1345,3 +1351,137 @@ msgstr "Добавить в закладки"
msgid "All bouquets"
msgstr "Все букеты"
msgid "Playback from the main list"
msgstr "Воспроизведение из основного списка"
msgid "Enables URL parsing using youtube-dl to get direct links to media."
msgstr "Включает анализ URL-адресов с помощью youtube-dl для получения прямых ссылок на медиа."
msgid "Permissions..."
msgstr "Разрешения..."
msgid "Display EPG in bouquet list"
msgstr "Отображать EPG в списке букета"
msgid "EPG *.dat file:"
msgstr "Файл EPG *.dat:"
msgid "Use HTTP to reload data in the receiver"
msgstr "Использовать HTTP для перезагрузки данных в ресивере"
msgid "Enable picons compression"
msgstr "Включить сжатие пиконов"
msgid "Update interval (sec):"
msgstr "Интервал обновления (сек):"
msgid "Update:"
msgstr "Обновлять:"
msgid "Daily"
msgstr "Ежедневно"
msgid "Assign reference"
msgstr "Присвоить ссылку"
msgid "Specify hostname or IP address"
msgstr "Укажите имя хоста или IP-адрес"
msgid "Default selection"
msgstr "Выбор по умолчанию"
msgid "Don't change power state"
msgstr "Не изменять состояние питания"
msgid "Don't toggle standby mode when updating bouquets and services."
msgstr "Не переключать в режим ожидания при обновлении букетов и сервисов."
msgid "Region"
msgstr "Регион"
msgid "Provider"
msgstr "Провайдер"
msgid "Enables upload as an archive if a large number of picon (> 1000) is selected.\n"
" Recommended only if you have external storage."
msgstr "Включает загрузку в виде архива, если выбрано большое количество пиконов (> 1000).\n"
" Рекомендуется, только если у вас есть внешнее хранилище."
msgid "Clear \"New\" flag"
msgstr "Очистить флаг \"New\""
msgid "Group by"
msgstr "Группировать по"
msgid "Replace existing"
msgstr "Заменить существующие"
msgid "Already exists"
msgstr "Уже существует"
msgid "Enable unlimited copy buffer"
msgstr "Включить неограниченный буфер копирования"
msgid "Enables unlimited copy buffer for the bouquets tab."
msgstr "Включает неограниченный буфер копирования для вкладки букетов."
msgid "Start time"
msgstr "Начало"
msgid "End time"
msgstr "Окончание"
msgid "Enable extensions support"
msgstr "Включить поддержку расширений"
msgid "After uploading the changes you may need to completely reboot the receiver!"
msgstr "После загрузки изменений может потребоваться перезапуск ресивера!"
msgid "Remove duplicates"
msgstr "Удалить дубликаты"
msgid "Removed"
msgstr "Удалено"
msgid "Enables overwriting existing main list services."
msgstr "Включает перезапись существующих сервисов основного списка."
msgid "Enables skipping services import from lamedb."
msgstr "Включает пропуск импорта сервисов из lamedb."
msgid "Bouquets data only"
msgstr "Только данные букетов"
msgid "Create Category bouquets"
msgstr "Создать букеты по категориям"
msgid "Create Regional bouquets"
msgstr "Создать букеты по регионам"
msgid "Skip C-band"
msgstr "Пропустить C-диапазон"
msgid "Automatically set the name selected in the bouquet list."
msgstr "Автоматическая установка имени, выбранного в списке букетов."
msgid "Merge satellites by positions"
msgstr "Объединять спутники по позициям"
msgid "Save satellite selection"
msgstr "Сохранять выбор спутников"
msgid "Extract direct links"
msgstr "Извлечь прямые ссылки"
msgid "URL prefix:"
msgstr "Префикс URL:"
msgid "Invalid prefix for the given URL!"
msgstr "Недопустимый префикс для данного URL!"
msgid "Alternate window title"
msgstr "Альтернативный заголовок окна"
msgid "Selected type:"
msgstr "Выбран тип:"

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