Compare commits

...

244 Commits

Author SHA1 Message Date
DYefremov
b012fccd1a minor code cleanup 2024-11-03 14:53:24 +03:00
DYefremov
4062d206b8 minor code adjustment 2024-11-03 13:50:22 +03:00
DYefremov
a1f656fbca add additional reload data cmds 2024-11-01 14:40:05 +03:00
DYefremov
84afaee1d0 add encoding blacklist for *.m3u import 2024-10-29 22:25:43 +03:00
DYefremov
08619dd182 refactoring of getting service reference
* fix wrong duplicated services filtering #208
2024-10-26 13:13:55 +03:00
DYefremov
04f27eff88 hide error status on new data open 2024-10-26 10:15:15 +03:00
DYefremov
6e706dec2d README update 2024-10-25 20:52:34 +03:00
DYefremov
3bf787b9fb version update -> 3.11.1 2024-10-03 20:49:36 +03:00
audi06_19
3b1bb80d3c Turkish translation update (#207) 2024-09-30 07:44:12 +03:00
DYefremov
05fa5eaf11 kos web source correction 2024-09-28 16:49:24 +03:00
DYefremov
b558a17d9d deb control update 2024-09-20 08:36:25 +03:00
DYefremov
0ee248a24f README update 2024-09-19 13:06:33 +03:00
DYefremov
3a368427fd web import options correction 2024-09-19 13:03:01 +03:00
DYefremov
384c30ea18 add additional events 2024-08-29 20:38:36 +03:00
DYefremov
05cf047127 export to *.m3u correction 2024-08-18 20:44:44 +03:00
DYefremov
621b090a1a it *.mo file update 2024-08-16 10:02:39 +03:00
mapi68
a8d3f39442 translation update -> [it] 2024-08-16 10:00:48 +03:00
DYefremov
02c261b4dd translations update -> [be, de, ru] 2024-08-15 12:59:54 +03:00
DYefremov
5c3532db65 add confirm dialog to bootlogo manager 2024-08-15 12:33:07 +03:00
DYefremov
fda9780de9 README update 2024-08-14 21:49:41 +03:00
DYefremov
6c5bd5d576 add support for *.mvi transfer for bootlogo manager 2024-08-14 21:03:09 +03:00
DYefremov
9c5b7a3901 add file selection for bootlogo manager 2024-08-13 16:20:51 +03:00
DYefremov
b7f312a35d add settings menu for bootlogo 2024-08-12 23:34:45 +03:00
DYefremov
9401b2a7f7 add format selection for bootlogo 2024-08-11 16:31:43 +03:00
DYefremov
682fa341d0 add load bootlogo image from local file 2024-08-10 11:32:30 +03:00
DYefremov
c9daa8a599 small refactoring 2024-08-10 10:54:31 +03:00
DYefremov
94d3d0d9ac loading logo from the box 2024-08-10 00:16:09 +03:00
DYefremov
2189997122 add boot logo manager prototype 2024-08-09 13:48:45 +03:00
DYefremov
8397efa324 streamrelay adjustment 2024-08-05 21:24:43 +03:00
DYefremov
d21f9410cd streamrelay support improvement 2024-08-04 18:33:54 +03:00
DYefremov
be9b3178e0 version update -> 3.11.0 2024-08-02 22:47:53 +03:00
DYefremov
2a8ddc093c minor fix for kos web source 2024-08-02 22:04:53 +03:00
DYefremov
fa1ec4cdcf save streamrelay change 2024-08-01 07:14:24 +03:00
DYefremov
384da95988 add streamrelay to context menu 2024-08-01 00:46:16 +03:00
DYefremov
960541b56a add basic streamrelay support (#199)
* marking services used with streamrelay in the fav list with an additional icon
2024-07-31 22:46:04 +03:00
DYefremov
396d10a805 extension manager ui adjustment 2024-07-27 23:41:07 +03:00
DYefremov
30e1c63a47 extension manager ui adjustment 2024-07-26 14:23:25 +03:00
DYefremov
ef7e35378d backup dialog ui adjustment 2024-07-26 14:18:54 +03:00
DYefremov
0a1bbab7d0 web import dialog redesign 2024-07-12 00:10:58 +03:00
DYefremov
65502018a0 add split satellites by band for web import (#204) 2024-07-02 19:28:03 +03:00
DYefremov
cc20042001 web import adjustments 2024-07-02 15:51:12 +03:00
DYefremov
50c2e831ce extension manager adjustment 2024-07-02 14:38:19 +03:00
DYefremov
ea91c39769 fix bouquet hiding status 2024-06-20 15:32:47 +03:00
DYefremov
3dab8ef7b7 version update -> 3.10.2 2024-06-17 16:16:41 +03:00
DYefremov
dd1a543e5c allowed data extraction from startup page 2024-06-16 18:12:32 +03:00
DYefremov
0966489024 fix reading some bouquets (#202)
* additional format for reading bouquet file name
2024-06-16 18:06:22 +03:00
DYefremov
052187359d version update -> 3.10.1 2024-06-08 17:44:11 +03:00
DYefremov
6ca6867ea9 small adjustment (#201) 2024-06-08 17:38:06 +03:00
DYefremov
d9cdc6458c fix services duplication (#201)
* Fixes service double loading when changing profile on start page.
2024-06-08 15:48:55 +03:00
DYefremov
70b9851324 minor ui correction
* IPTV list config dialog adjustment for macOS
2024-05-17 22:52:46 +03:00
DYefremov
2a3b558d83 adjustment for LyngSat web source
* default values for ONID-TID if not present (#200)
2024-05-09 19:13:52 +03:00
DYefremov
21ea841f34 version update -> 3.10.0 2024-05-07 18:53:52 +03:00
DYefremov
1db0ce3fc5 README update 2024-05-07 18:42:01 +03:00
DYefremov
2804a9bc54 change playback keyboard shortcuts 2024-05-07 18:41:36 +03:00
DYefremov
8976f42974 enabled playback mode for the main services list (#198) 2024-05-07 17:56:00 +03:00
DYefremov
8330104f3c add last config load flag 2024-03-29 22:22:05 +03:00
DYefremov
3ede2e2b07 fix -> mark not present in bouquets (#197) 2024-03-29 20:35:48 +03:00
DYefremov
dd796c0f88 version update -> 3.9.2-b 2024-03-16 17:58:52 +03:00
DYefremov
f3a432c002 add optional keep for backup
* prevent *.xml deletion (#196)
2024-03-11 00:38:25 +03:00
DYefremov
4c1cdc4850 increase pos width for sat dialog 2024-03-06 20:53:14 +03:00
DYefremov
a062d74e0e BUILD_WIN.md update 2024-03-05 22:53:41 +03:00
DYefremov
7bf36c8d6d minor fix to add a timer 2024-03-04 13:38:36 +03:00
DYefremov
6aad0344c8 timer dialog adjustment 2024-03-03 19:47:54 +03:00
DYefremov
bb4665d180 default emblem adjustment 2024-03-03 11:53:57 +03:00
DYefremov
a455c4569d version update -> 3.9.1-b 2024-03-03 10:05:23 +03:00
DYefremov
8d5af301fb dialogs ui adjustments 2024-03-02 13:08:54 +03:00
DYefremov
f342b99769 update *.desktop file for deb package 2024-03-02 12:29:42 +03:00
DYefremov
a0612e6a98 separate data loading (#196) 2024-03-02 11:57:20 +03:00
DYefremov
60b8f7642d lamedb5 reading correction (#194) 2024-03-01 22:19:34 +03:00
DYefremov
8730cbdb7c README update 2024-02-26 22:19:01 +03:00
mapi68
cefb96ea20 Update languages in demon-editor.desktop (#195) 2024-02-26 10:39:50 +03:00
DYefremov
b383c2572a minor fixes for EPG cache 2024-02-25 14:02:36 +03:00
DYefremov
ee2fcf8082 changed some icons for picons tab 2024-02-25 13:35:37 +03:00
DYefremov
44234fa534 set "Save as" element visibility 2024-02-22 22:58:53 +03:00
DYefremov
4bb66b5cc1 version update -> 3.9.0-b 2024-02-22 15:33:59 +03:00
DYefremov
3b1ecbfbbf get webtv name for neutrino 2024-02-22 15:28:44 +03:00
DYefremov
a7f7a59c8a enable ctrl+u shortcut globally 2024-02-21 22:08:51 +03:00
DYefremov
9068028662 it *.mo file update 2024-02-21 10:14:08 +03:00
mapi68
40fbc7809f Italian translation update (#193)
* Update demon-editor.po

* small corrections
2024-02-21 10:10:26 +03:00
DYefremov
6397c2c7f3 translations update -> [be, de, ru] 2024-02-20 17:14:21 +03:00
DYefremov
81e8e30682 add manual port configuration for *.m3u export 2024-02-20 15:24:46 +03:00
DYefremov
9b4c6ab14a fix *.m3u export for neutrino 2024-02-20 14:00:35 +03:00
DYefremov
cad1437c33 small fix 2024-02-18 23:36:41 +03:00
DYefremov
4799a0d464 add additional bouquet name checking (#192) 2024-02-17 19:04:55 +03:00
DYefremov
ce890353a4 win *.spec file update 2024-02-17 13:27:34 +03:00
DYefremov
506e07e3f4 fix multi EPG request 2024-02-17 00:44:49 +03:00
DYefremov
e7a61e3f05 minor ui correction 2024-02-15 11:35:17 +03:00
DYefremov
274abec3b8 add EPG warning messages 2024-02-15 11:22:57 +03:00
DYefremov
77a5f55522 fix extensions loading -> python 3.12 2024-02-14 22:38:39 +03:00
DYefremov
a7f334682f minor corrections for EPG tab 2024-02-14 14:41:18 +03:00
DYefremov
a9b9e5865e minor fix for m3u export 2024-02-13 09:58:20 +03:00
DYefremov
d59458a84b export to m3u improvement
* multiple bouquets export
  * grouping
2024-02-12 23:58:12 +03:00
DYefremov
076bcb0cce removed sub bouquets warning 2024-02-11 20:09:59 +03:00
DYefremov
8cbf03eb51 improved *m3u import
* added split by groups
2024-02-11 18:33:35 +03:00
DYefremov
abed7bf9cb bouquet name gen refactoring 2024-02-11 16:30:40 +03:00
DYefremov
388e748673 adjustment of EPG cache init 2024-02-10 23:55:23 +03:00
DYefremov
ba3a3ae0aa add EPG cache import on init 2024-02-10 22:10:52 +03:00
DYefremov
9bb4f6d75d fix XMLTV double load 2024-02-10 21:05:47 +03:00
DYefremov
06242ce611 add data size check for XMLTV download 2024-02-10 20:47:46 +03:00
DYefremov
e4b1c98b2a improvements for EPG tab and cache 2024-02-10 19:08:22 +03:00
DYefremov
6a8426e6ef enabled recording and playback for the current IPTV service 2024-02-07 12:52:56 +03:00
DYefremov
583927f1b1 epg data loading improvements
* separate load of xmltv files
  * cache properties for readers
2024-01-31 15:48:02 +03:00
DYefremov
185a9b0082 fix epg event data 2024-01-28 22:37:04 +03:00
DYefremov
ee7eb01af5 add EPG source support to m3u import 2024-01-28 13:58:29 +03:00
DYefremov
07c8034393 fix bouquet file name for new config 2024-01-24 09:07:20 +03:00
DYefremov
8ec73bc0f9 add event to EPG tool 2024-01-23 15:15:12 +03:00
DYefremov
940b28ff6d data items state adjustment 2024-01-22 23:47:41 +03:00
DYefremov
316260703c save as for satellites tab 2024-01-22 23:14:46 +03:00
DYefremov
e6c7b6572c minor clean 2024-01-22 22:55:13 +03:00
DYefremov
0e50cf4927 add extraction support for EPG tab 2024-01-22 14:19:43 +03:00
DYefremov
f55dff1618 toolbar items state update refactoring 2024-01-22 13:47:07 +03:00
DYefremov
669916a5a9 add m3u export dialog 2024-01-21 23:03:56 +03:00
DYefremov
4154f4d2f5 display info messages refactoring 2024-01-20 19:04:20 +03:00
DYefremov
f3d133b7a3 m3u parsing improvement 2024-01-20 15:32:45 +03:00
DYefremov
a432da60e5 add m3u ui file 2024-01-19 23:40:45 +03:00
DYefremov
d34128b701 add short service info to picon explorer 2024-01-19 18:00:41 +03:00
DYefremov
d16d0e62e2 small refactoring 2024-01-19 17:24:12 +03:00
DYefremov
1a6270478a small revert 2024-01-18 00:10:05 +03:00
DYefremov
4144e37049 m3u elements redesign 2024-01-18 00:00:36 +03:00
DYefremov
61faced24d fix EPG request 2024-01-11 17:31:40 +03:00
DYefremov
add73558f3 update -> about 2024-01-11 14:23:28 +03:00
DYefremov
259f20794c minor corrections for EPG tab 2024-01-11 14:16:38 +03:00
DYefremov
afdb9a3c6b EPG data load optimization 2024-01-10 14:34:26 +03:00
DYefremov
8581e7be1f fix some warnings 2024-01-08 17:45:44 +03:00
DYefremov
5496aec95f enabled filtering hotkey for recordings tab 2024-01-08 17:11:10 +03:00
DYefremov
5441bb0c02 enable upload button for EPG tab 2024-01-06 12:40:04 +03:00
DYefremov
3991d2935f support for opening local xmltv files 2024-01-06 00:12:48 +03:00
DYefremov
c42115ba7d add a separate data opening event 2024-01-05 15:59:00 +03:00
DYefremov
929987b2f1 adjustments for EPG tab 2024-01-05 15:03:36 +03:00
DYefremov
9a0bead39c loading services by first selection of the tab 2024-01-04 14:09:57 +03:00
DYefremov
d18e9c116b custom repo support for extension manager 2024-01-02 12:58:22 +03:00
DYefremov
e477e54dcd EPG file update on XML src change 2023-12-31 10:50:05 +03:00
DYefremov
48b9cb23eb fix EPG filtering when missing values 2023-12-30 12:52:06 +03:00
DYefremov
5db027c1cf add settings popover for EPG tab 2023-12-29 15:16:58 +03:00
DYefremov
128fd8a792 version update -> 3.9.0-a 2023-12-29 14:18:44 +03:00
DYefremov
32043b7df0 XMLTV display support for EPG tab 2023-12-29 14:09:13 +03:00
DYefremov
bb847cd94c fix getting current events for xmltv 2023-12-29 13:02:40 +03:00
DYefremov
64b836363b EPG cache optimization 2023-12-28 23:05:01 +03:00
DYefremov
5f34652905 add support for multiple xmltv sources 2023-12-23 19:28:45 +03:00
DYefremov
32078bc7e3 EPG cache refactoring 2023-12-21 01:01:08 +03:00
DYefremov
9c582c26db it *.mo file update 2023-12-19 10:51:51 +03:00
mapi68
aa5addb280 Italian translation update 2023-12-19 10:47:05 +03:00
DYefremov
2d8ae1bbe2 fav selection refactoring 2023-12-15 18:07:02 +03:00
DYefremov
5405d3a9a8 EPG tab toolbar redesign 2023-11-20 21:36:44 +03:00
DYefremov
d91cbc395c allowed uncompressed *.xml for EPG src link (#189) 2023-11-17 11:00:21 +03:00
DYefremov
e44aedebad fix multiple selection for IPTV tab 2023-11-13 12:47:27 +03:00
DYefremov
9ccbd46b71 sat dialogs ui adjustment
* some corrections for macOS
2023-11-09 17:51:15 +03:00
DYefremov
3f0e9e44a9 changed URL extraction for IPTV (#187) 2023-11-05 17:17:49 +03:00
DYefremov
32a847fc5d fix single bouquet import 2023-11-05 14:02:38 +03:00
DYefremov
795ad51098 bouquets parsing refactoring
* allowed custom  file names for bouquets (#185)
2023-10-16 11:11:57 +03:00
audi06_19
64827a60d8 Turkish translation update (#184) 2023-10-08 20:07:45 +03:00
DYefremov
b98d55ffb6 EPG dialog header redesign 2023-10-06 22:00:20 +03:00
DYefremov
aa21862303 version update 2023-09-29 22:24:15 +03:00
DYefremov
f205e4fa66 minor fix for extension manager
* version update on download
2023-09-22 17:57:05 +03:00
DYefremov
4ffecb9ce4 skip getting picon for the marker 2023-09-22 16:47:06 +03:00
DYefremov
ecc95e9c82 it *.mo file update 2023-09-20 10:48:10 +03:00
mapi68
4c5cbcb514 Italian translation update (#183) 2023-09-20 10:19:08 +03:00
DYefremov
8bd24e9642 translations update [be, de, ru] 2023-09-20 00:27:25 +03:00
DYefremov
64234c26e0 pids pattern adjustment 2023-09-20 00:07:35 +03:00
DYefremov
0b1d31fb8a EPG dialog redesign 2023-09-10 15:10:00 +03:00
DYefremov
a638a6b1fc minor corrections 2023-09-10 14:53:32 +03:00
DYefremov
917e184486 sat dialogs adjustment 2023-07-25 16:44:31 +03:00
DYefremov
bc17a6720a allowed *.xml for backup 2023-07-22 20:42:49 +03:00
DYefremov
838cdbd350 webtv parsing refactoring 2023-07-21 19:08:51 +03:00
DYefremov
e795b21499 add IPTV support for favorites in Neutrino 2023-07-20 23:10:18 +03:00
DYefremov
2482e1a424 fix reading lamedb5 2023-07-20 12:16:28 +03:00
DYefremov
3fd9e3ce9d Dutch translation update 2023-07-14 21:57:30 +03:00
DYefremov
67524f7ede Portuguese translation update 2023-07-14 21:57:05 +03:00
DYefremov
721a585eb9 Spanish translation update 2023-07-14 21:55:47 +03:00
DYefremov
45e210dade minor fix 2023-07-14 08:28:06 +03:00
DYefremov
1d98803763 version update 2023-07-13 22:58:25 +03:00
DYefremov
568a68916c timers tab adjustment 2023-07-13 22:49:15 +03:00
DYefremov
89ed1c0715 IPTV dialog ui adjustment 2023-07-13 22:19:43 +03:00
DYefremov
a1ca26c47e EPG tab ui adjustment 2023-07-13 22:04:08 +03:00
DYefremov
27392d6000 extension manager redesign 2023-07-13 21:23:18 +03:00
DYefremov
f5ebd06fc0 backup dialog redesign 2023-07-13 21:01:48 +03:00
DYefremov
7b933188f8 fix selection 2023-07-07 23:46:15 +03:00
DYefremov
ec5a89e716 import dialogs redesign 2023-07-07 23:35:36 +03:00
DYefremov
f8b1c29638 service details dialog redesign 2023-07-07 21:33:26 +03:00
DYefremov
cdbee2b429 IPTV dialogs redesign 2023-07-06 17:19:15 +03:00
DYefremov
ad748bf14c FTP tab redesign 2023-07-05 17:36:49 +03:00
DYefremov
a7ca2ed0fc timers tab redesign 2023-07-05 15:03:32 +03:00
DYefremov
e32042af35 EPG tab redesign 2023-07-05 14:37:10 +03:00
DYefremov
40d2ac9ecf recordings tab redesign 2023-07-05 09:52:40 +03:00
DYefremov
324b7bf103 minor playback fix 2023-07-05 09:51:53 +03:00
DYefremov
3dd3b1df34 control tab redesign 2023-07-04 15:58:27 +03:00
DYefremov
bbd0b07540 telnet tool redesign 2023-07-03 23:19:05 +03:00
DYefremov
9a37280d14 logging tool redesign 2023-07-03 21:22:59 +03:00
DYefremov
1b83c0f7f6 picons tab redesign 2023-07-03 19:41:03 +03:00
DYefremov
c83310b98e satellite dialogs redesign 2023-07-03 00:15:25 +03:00
DYefremov
6a4bd4e5da satellites tab redesign 2023-07-02 22:16:07 +03:00
DYefremov
bca50a5d30 bouquets tab redesign 2023-06-30 19:00:04 +03:00
DYefremov
45b856946d common path default for picons 2023-06-30 10:03:16 +03:00
DYefremov
3fcc96cd02 settings dialog redesign 2023-06-29 21:26:02 +03:00
DYefremov
0894cb5a47 *.spec files update 2023-06-25 18:08:32 +03:00
DYefremov
1ac6537496 win style adjustment 2023-06-23 23:37:16 +03:00
DYefremov
285ea66197 lazy filtering 2023-06-23 23:33:50 +03:00
DYefremov
1edb313e2c minor filter optimization 2023-06-19 23:20:53 +03:00
DYefremov
c9755e0116 win style update 2023-06-18 12:30:41 +03:00
DYefremov
367a4a2e36 alternate title option for Windows 2023-06-14 17:31:29 +03:00
DYefremov
fc65ee29ee minor changes in settings dialog 2023-06-14 17:18:26 +03:00
DYefremov
f0e9684f51 version update 2023-06-14 12:49:51 +03:00
DYefremov
7084eec407 translations update 2023-06-14 12:16:23 +03:00
DYefremov
ef79dac603 skipping services with empty params for kos 2023-06-13 23:04:09 +03:00
audi06_19
89ee80e4ef Update: Turkish translation update (#181) 2023-06-10 19:08:07 +03:00
DYefremov
73df75a519 switching to yt-dlp 2023-06-06 17:41:23 +03:00
DYefremov
7ed0d6d355 playback adjustment for macOS 2023-06-06 11:39:16 +03:00
DYefremov
b4a0a72db3 debug message output adjustment 2023-06-06 10:05:49 +03:00
DYefremov
63e0f1ea14 deb script adjustment 2023-06-06 09:49:36 +03:00
DYefremov
d446240d91 minor adjustment for picons tab 2023-06-03 20:40:25 +03:00
DYefremov
3d0f010798 fix data reading (#180) 2023-06-03 14:00:16 +03:00
DYefremov
4fc4cb7e2a it *.mo file update 2023-06-02 18:53:32 +03:00
mapi68
80147b4cc0 Italian translation update (#179) 2023-06-02 18:49:26 +03:00
DYefremov
6270c03376 small translations update 2023-06-01 22:45:11 +03:00
DYefremov
2285211100 minor fix for picons download 2023-06-01 22:16:47 +03:00
DYefremov
642bca81c2 stream playback adjustments 2023-05-30 11:07:16 +03:00
DYefremov
198cb3867d status icons for extension manager 2023-05-25 18:57:42 +03:00
DYefremov
d0db68acb4 ver column for extension manager 2023-05-25 15:39:56 +03:00
DYefremov
43544a9df3 extension manager improvement
* loading/removing via status toggle
2023-05-23 20:41:57 +03:00
DYefremov
ebf6454181 yt API correction 2023-05-22 20:54:41 +03:00
DYefremov
ea09cef837 version update
* -> 3.7.0 Alpha
2023-05-22 13:47:42 +03:00
DYefremov
d7853c31ff playback improvement 2023-05-22 13:43:47 +03:00
DYefremov
3dcc942c25 playback code refactoring 2023-05-22 00:41:24 +03:00
DYefremov
ffdd98d406 extension manager minor improvements 2023-05-18 23:56:36 +03:00
DYefremov
4d9ae8c23a pl *.mo file update 2023-05-16 19:00:50 +03:00
lareq
e2c97169fb Polish translation update (#177) 2023-05-16 18:54:27 +03:00
DYefremov
414fd22f71 extension manager improvement 2023-05-15 21:38:51 +03:00
DYefremov
4ff7129750 translate refactoring 2023-05-13 13:31:42 +03:00
DYefremov
1546baab30 small playback refactoring 2023-05-13 09:19:35 +03:00
DYefremov
444df51706 deb build update 2023-05-09 22:38:59 +03:00
DYefremov
5e84656c20 it *.mo file update 2023-05-09 21:01:45 +03:00
DYefremov
245d10fb03 remove/download extensions support 2023-05-07 23:55:03 +03:00
DYefremov
b9f2e5cb3a add extension manager dialog 2023-05-07 00:53:36 +03:00
DYefremov
c040c1145c fix http test for neutrino 2023-05-06 10:21:05 +03:00
DYefremov
92a91cd995 add play button 2023-05-04 23:51:50 +03:00
DYefremov
6255b60453 loading screen to playback
* some playback refactoring
2023-05-04 21:55:58 +03:00
DYefremov
30aa967f82 playback bar redesign 2023-04-29 17:55:13 +03:00
DYefremov
b90040f473 fix kos web source 2023-04-28 22:07:27 +03:00
DYefremov
43bf5ac44b bump version 2023-04-25 20:53:42 +03:00
DYefremov
e714b10431 changed columns for filtering 2023-04-25 20:52:44 +03:00
DYefremov
f0d0813e75 refactoring of getting picon pixbuf 2023-04-25 01:04:38 +03:00
DYefremov
9dc4df73c4 added piconSNPblack logo 2023-04-25 00:32:44 +03:00
DYefremov
f2da1e4cd4 minor refactoring of player 2023-04-25 00:12:29 +03:00
DYefremov
3d4588833b vlc module update 2023-04-24 22:49:26 +03:00
100 changed files with 21046 additions and 16144 deletions

View File

@@ -41,10 +41,7 @@ Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
* **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.
* **Ctrl + P** - start play IPTV or other stream in the bouquet list.
* **Ctrl + Z** - switch(**zap**) the channel(works when the HTTP API is enabled, Enigma2 only).
* **Ctrl + W** - switch to the channel and watch in the program.
* **Ctrl + H** - hide/skip.
* **Space** - select/deselect.
* **Left/Right** - remove selection.
* **Ctrl + Up, Down, PageUp, PageDown, Home, End**- move selected items in the list.
@@ -56,13 +53,16 @@ Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
* **Ctrl + Shift + F** - show/hide filter bar.
* **Ctrl + T** - show/hide built-in Telnet client.
* **Ctrl + Shift + L** - show/hide logging panel.
* **Shift + P** - start play IPTV or other stream in the bouquet list.
* **Shift + Z** - switch(**zap**) the channel(works when the HTTP API is enabled, Enigma2 only).
* **Shift + W** - switch to the channel and watch in the program.
For **multiple** selection with the mouse, press and hold the **Ctrl** key!
## Minimum requirements
*Python >= 3.6, GTK+ >= 3.22, python3-gi, python3-gi-cairo, python3-requests.*
***Optional:** python3-pil, python3-chardet.*
***Optional:** python3-pil, python3-chardet, ffmpeg.*
## Installation and Launch
* ### Linux
To start the program, in most cases it is enough to download the [archive](https://github.com/DYefremov/DemonEditor/archive/master.zip), unpack
@@ -75,10 +75,13 @@ Users of **LTS** versions of [Ubuntu](https://ubuntu.com/) or distributions base
A ready-made [package](https://aur.archlinux.org/packages/demoneditor-bin) is also available for [Arch Linux](https://archlinux.org/) users in the [AUR](https://aur.archlinux.org/) repository.
* ### macOS
**This program can be run on macOS.**
To run the program on macOS, you need to install [brew](https://brew.sh/).
To run the program on macOS, you need to install [Homebrew](https://brew.sh/).
Then install the required components via terminal:
```brew install python3 gtk+3 pygobject3 adwaita-icon-theme```
```pip3 install requests, pillow```
```brew install python3 gtk+3 pygobject3 adwaita-icon-theme gtksourceview3```
```pip3 install requests telnetlib-313-and-up --break-system-packages```
*Optional:* ```brew install pillow python-chardet ffmpeg```
Launch is similar to Linux.
@@ -98,7 +101,6 @@ THIS SOFTWARE COMES WITH ABSOLUTELY NO WARRANTY.
AUTHOR IS NOT LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY CONNECTION WITH THIS SOFTWARE.
## Important
The program is tested only with [openATV](https://www.opena.tv/) image and **Formuler F1** receiver in [Linux Mint](https://linuxmint.com/) (MATE 64-bit) distribution!
Support for DVB-T/T2 and DVB-C channels for Neutrino is not fully implemented and has an experimental status.
Main supported *lamedb* format is version **4**. Versions **3** and **5** has only **experimental** support! For version **3** is only read mode available. When saving, version **4** format is used instead.

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2023 Dmitriy Yefremov
# Copyright (c) 2018-2024 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
@@ -48,10 +48,10 @@ from app.settings import SettingsType
BQ_FILES_LIST = ("tv", "radio", # Enigma2.
"services.xml", "myservices.xml", "bouquets.xml", "ubouquets.xml") # Neutrino.
DATA_FILES_LIST = ("lamedb", "lamedb5", "blacklist", "whitelist",)
DATA_FILES_LIST = ("lamedb", "lamedb5", "blacklist", "whitelist", "whitelist_streamrelay")
STC_XML_FILE = ("satellites.xml", "terrestrial.xml", "cables.xml")
WEB_TV_XML_FILE = ("webtv.xml",)
WEB_TV_XML_FILE = ("webtv.xml", "webtv_usr.xml")
PICONS_SUF = (".jpg", ".png")
PICONS_MAX_NUM = 1000 # Maximum picon number for sending without compression.
@@ -59,10 +59,15 @@ PICONS_MAX_NUM = 1000 # Maximum picon number for sending without compression.
class DownloadType(Enum):
ALL = 0
BOUQUETS = 1
SATELLITES = 2
PICONS = 3
WEBTV = 4
EPG = 5
SERVICES = 2
SATELLITES = 3
PICONS = 4
WEBTV = 5
EPG = 6
@classmethod
def _missing_(cls, value):
return cls.ALL
class TestException(Exception):
@@ -373,9 +378,11 @@ def download_data(*, settings, download_type=DownloadType.ALL, callback=log, fil
save_path = settings.profile_data_path
os.makedirs(os.path.dirname(save_path), exist_ok=True)
# bouquets
if download_type is DownloadType.ALL or download_type is DownloadType.BOUQUETS:
if download_type in (DownloadType.ALL, DownloadType.BOUQUETS, DownloadType.SERVICES):
ftp.cwd(settings.services_path)
file_list = BQ_FILES_LIST + DATA_FILES_LIST if download_type is DownloadType.ALL else BQ_FILES_LIST
file_list = BQ_FILES_LIST
if download_type is DownloadType.ALL or DownloadType.SERVICES:
file_list += DATA_FILES_LIST
ftp.download_files(save_path, file_list, callback)
# *.xml and webtv
if download_type in (DownloadType.ALL, DownloadType.SATELLITES):
@@ -426,7 +433,7 @@ def upload_data(*, settings, download_type=DownloadType.ALL, callback=log, done_
ht.send((f"{url}message?{params}", "Sending info message... "))
if s_type is SettingsType.ENIGMA_2 and download_type is DownloadType.ALL:
if s_type is SettingsType.ENIGMA_2 and download_type in (DownloadType.ALL, DownloadType.SERVICES):
time.sleep(5)
if not settings.keep_power_mode:
ht.send((f"{url}powerstate?newstate=0", "Toggle Standby "))
@@ -457,8 +464,10 @@ def upload_data(*, settings, download_type=DownloadType.ALL, callback=log, done_
ftp.cwd(services_path)
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)
if download_type is DownloadType.ALL or download_type is DownloadType.SERVICES:
if download_type is DownloadType.ALL:
ftp.upload_xml(data_path, sat_xml_path, STC_XML_FILE, callback)
if s_type is SettingsType.NEUTRINO_MP:
ftp.upload_xml(data_path, sat_xml_path, WEB_TV_XML_FILE, callback)
@@ -518,10 +527,14 @@ def upload_data(*, settings, download_type=DownloadType.ALL, callback=log, done_
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:
elif download_type is DownloadType.ALL or download_type is DownloadType.SERVICES:
ht.send((f"{url}servicelistreload?mode=0", "Reloading lamedb and Userbouquets."))
time.sleep(1)
ht.send((f"{url}servicelistreload?mode=4", "Updating parental control."))
if not settings.keep_power_mode:
ht.send((f"{url}powerstate?newstate=4", "Wakeup from Standby."))
elif download_type is DownloadType.SATELLITES:
ht.send((f"{url}servicelistreload?mode=3", "Reloading transponders."))
else:
ht.send((f"{url}reloadchannels", "Reloading channels..."))
@@ -537,6 +550,8 @@ def upload_data(*, settings, download_type=DownloadType.ALL, callback=log, done_
def get_upload_info_message(download_type):
if download_type is DownloadType.BOUQUETS:
return "User bouquets will be updated!"
if download_type is DownloadType.SERVICES:
return "User bouquets and services list will be updated!"
elif download_type is DownloadType.ALL:
return "All user data will be reloaded!"
elif download_type is DownloadType.SATELLITES:
@@ -907,10 +922,7 @@ def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message
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("e2enigmaversion", "")
return resp
return resp.get("e2enigmaversion" if s_type is SettingsType.ENIGMA_2 else "data", "")
except (RemoteDisconnected, URLError, HTTPError) as e:
raise TestException(e)

View File

@@ -29,7 +29,7 @@ from app.commons import run_task
from app.settings import SettingsType
from .ecommons import Service, Satellite, Transponder, Bouquet, Bouquets, is_transponder_valid
from .enigma.blacklist import get_blacklist, write_blacklist
from .enigma.bouquets import to_bouquet_id, BouquetsWriter, BouquetsReader
from .enigma.bouquets import BouquetsWriter, BouquetsReader
from .enigma.lamedb import get_services as get_enigma_services, write_services as write_enigma_services
from .iptv import parse_m3u
from .neutrino.bouquets import get_bouquets as get_neutrino_bouquets, write_bouquets as write_neutrino_bouquets

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2023 Dmitriy Yefremov
# Copyright (c) 2018-2024 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,10 +47,10 @@ 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:{}:{}: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 "{}" 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'
_LOCKED = '1:{}:{}:0:0:0:0:0:0:0:FROM BOUQUET "{}" 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]"
@@ -76,18 +76,18 @@ class BouquetsWriter:
m_count = 0
for bq in bqs.bouquets:
bq_name = bq.file
if not bq_name:
if self._force_bq_names:
bq_name = re.sub(self._NAME_PATTERN, "_", bq.name)
else:
bq_name = f"de{count:02d}"
while bq_name in bq_file_names:
count += 1
bq_name = f"de{count:02d}"
bq_file_names.add(bq_name)
f_name = bq.file
bq_type = BqType(bq.type)
if not f_name:
if self._force_bq_names or bq_type is BqType.BOUQUET:
f_name = f"userbouquet.{re.sub(self._NAME_PATTERN, '_', bq.name)}.{bqs.type}"
else:
f_name = f"userbouquet.de{count:02d}.{bqs.type}"
while f_name in bq_file_names:
count += 1
f_name = f"userbouquet.de{count:02d}.{bqs.type}"
bq_file_names.add(f_name)
if bq_type is BqType.MARKER:
m_data = bq.file.split(":") if bq.file else None
b_name = m_data[-1].strip() if m_data else bq.name.lstrip(_MARKER_PREFIX)
@@ -95,17 +95,16 @@ class BouquetsWriter:
m_count += 1
else:
if bq_type is BqType.BOUQUET:
bq_name = re.sub(self._NAME_PATTERN, "_", bq.name)
self.write_sub_bouquet(self._path, bq_name, bq, bqs.type)
self.write_sub_bouquet(self._path, f_name, bq, bqs.type)
else:
self.write_bouquet(f"{self._path}userbouquet.{bq_name}.{bqs.type}", bq.name, bq.services)
self.write_bouquet(f"{self._path}{f_name}", bq.name, bq.services)
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)
locked = self._LOCKED.format(ServiceType.SERVICE, bq_type, f_name)
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))
line.append(self._SERVICE.format(s_type, bq_type, f_name))
with open(f"{self._path}bouquets.{bqs.type}", "w", encoding="utf-8", newline="\n") as file:
file.writelines(line)
@@ -139,11 +138,10 @@ class BouquetsWriter:
bouquet.append(self._ALT.format(f_name))
self.write_bouquet(f"{p.parent}/{f_name}", srv.service, services)
else:
data = to_bouquet_id(srv)
if srv.service:
bouquet.append(f"#SERVICE {data}:{srv.service}\n#DESCRIPTION {srv.service}\n")
bouquet.append(f"#SERVICE {srv.fav_id}:{srv.service}\n#DESCRIPTION {srv.service}\n")
else:
bouquet.append(f"#SERVICE {data}\n")
bouquet.append(f"#SERVICE {srv.fav_id}\n")
with open(path, "w", encoding="utf-8", newline="\n") as file:
file.writelines(bouquet)
@@ -153,11 +151,11 @@ class BouquetsWriter:
sb_type = 2 if bq_type == BqType.RADIO.value else 1
for sb in bq.services:
bq_name = f"subbouquet.{re.sub(self._NAME_PATTERN, '_', sb.name)}.{sb.type}"
self.write_bouquet(f"{path}{bq_name}", sb.name, sb.services)
bouquet.append(f"#SERVICE 1:7:{sb_type}:0:0:0:0:0:0:0:FROM BOUQUET \"{bq_name}\" ORDER BY bouquet\n")
sb_file = sb.file or f"subbouquet.{re.sub(self._NAME_PATTERN, '_', sb.name)}.{sb.type}"
self.write_bouquet(f"{path}{sb_file}", sb.name, sb.services)
bouquet.append(f"#SERVICE 1:7:{sb_type}:0:0:0:0:0:0:0:FROM BOUQUET \"{sb_file}\" ORDER BY bouquet\n")
with open(f"{self._path}userbouquet.{file_name}.{bq_type}", "w", encoding="utf-8", newline="\n") as file:
with open(f"{self._path}{file_name}", "w", encoding="utf-8", newline="\n") as file:
file.writelines(bouquet)
@@ -181,9 +179,9 @@ class ServiceType(Enum):
class BouquetsReader:
""" Class for reading and parsing bouquets. """
_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]+).*")
_BQ_PAT = re.compile(r".*FROM BOUQUET\s+\"((.*bouquet|alternatives)?\.?([\w-]+)\.?(\w+)?)\"\s+.*$", re.IGNORECASE)
_BQ_PAT2 = re.compile(r"#SERVICE:+\s+(?:[0-9a-f]+:+)+([^:]+[.](?:tv|radio))$", re.IGNORECASE)
_BQ_POST_PAT = re.compile(r".*FROM BOUQUET\s+\"((.*bouquet|alternatives)?\.?(.*)\.?(\w+)?)\"\s+.*$", re.IGNORECASE)
_STREAM_TYPES = {"4097", "5001", "5002", "8193", "8739"}
__slots__ = ["_path"]
@@ -209,29 +207,41 @@ 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:
prefix, b_name = name.group(1), name.group(2)
s_type = ServiceType.BOUQUET
mt = re.match(self._BQ_PAT, line) or re.match(self._BQ_PAT2, line)
if not mt:
# Additional file name checking.
mt = re.match(self._BQ_POST_PAT, line)
if mt:
log(f"Warning: The bouquet file name may be formed incorrectly. -> {mt.group(1)}")
if mt:
if len(mt.groups()) > 1:
file_name, prefix, b_name = mt.group(1), mt.group(2), mt.group(3)
s_type = ServiceType(s_data[1])
s_data[:2] = "10"
else:
file_name, prefix, b_name = mt.group(1), "", ""
s_type = ServiceType(s_data[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, prefix)
rb_name, services = self.get_bouquet(self._path, file_name, b_name)
if rb_name in real_b_names:
log(f"Bouquet file '{prefix}.{b_name}.{bq_type}' has duplicate name: {rb_name}")
log(f"Bouquet file '{file_name}' 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, locked, hidden, b_name))
bouquets[2].append(Bouquet(rb_name, bq_type, services, locked, hidden, file_name))
else:
if len(s_data) == 12 and s_type is ServiceType.MARKER:
b_name = f"{_MARKER_PREFIX}{s_data[-1].strip()}"
@@ -244,15 +254,15 @@ class BouquetsReader:
return bouquets
@staticmethod
def get_bouquet(path, bq_name, bq_type, prefix="userbouquet"):
def get_bouquet(path, f_name, bq_name):
""" Parsing services ids from bouquet file. """
with open(f"{path}{prefix}.{bq_name}.{bq_type}", encoding="utf-8", errors="replace") as file:
with open(f"{path}{f_name}", encoding="utf-8", errors="replace") as file:
chs_list = file.read()
services = []
srvs = list(filter(None, chs_list.split("\n#SERVICE"))) # filtering ['']
# May come across empty[wrong] files!
if not srvs:
log(f"Bouquet file 'userbouquet.{bq_name}.{bq_type}' is empty or wrong!")
log(f"Bouquet file '{f_name}' is empty or wrong!")
return f"{bq_name} [empty]", services
bq_name = srvs.pop(0)
@@ -272,40 +282,31 @@ class BouquetsReader:
m_data, sep, desc = srv.partition("#DESCRIPTION")
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.SPACE, srv, num))
elif s_type is ServiceType.ALT:
alt = re.match(BouquetsReader._ALT_PAT, srv)
alt = re.match(BouquetsReader._BQ_PAT, srv)
if alt:
alt_name, alt_type = alt.group(1), alt.group(2)
alt_bq_name, alt_srvs = BouquetsReader.get_bouquet(path, alt_name, alt_type, "alternatives")
af_name, alt_name = alt.group(1), alt.group(3)
alt_bq_name, alt_srvs = BouquetsReader.get_bouquet(path, af_name, alt_name)
services.append(BouquetService(alt_bq_name, BqServiceType.ALT, alt_name, tuple(alt_srvs)))
elif s_type is ServiceType.BOUQUET:
sub = re.match(BouquetsReader._SUB_BQ_PAT, srv)
sub = re.match(BouquetsReader._BQ_PAT, srv)
if sub:
sub_name, sub_type = sub.group(1), sub.group(2)
sub_bq_name, sub_srvs = BouquetsReader.get_bouquet(path, sub_name, sub_type, "subbouquet")
bq = Bouquet(sub_bq_name, sub_type, tuple(sub_srvs), None, None, sub_name)
sf_name, sub_name, sub_type = sub.group(1), sub.group(3), sub.group(4)
sub_bq_name, sub_srvs = BouquetsReader.get_bouquet(path, sf_name, sub_name)
bq = Bouquet(sub_bq_name, sub_type, tuple(sub_srvs), None, None, sf_name)
services.append(BouquetService(sub_bq_name, BqServiceType.BOUQUET, bq, num))
elif srv_data[0].strip() in BouquetsReader._STREAM_TYPES or srv_data[10].startswith(("http", "rtsp")):
stream_data, sep, desc = srv.partition("#DESCRIPTION")
desc = desc.lstrip(":").strip() if desc else srv_data[-1].strip()
services.append(BouquetService(desc, BqServiceType.IPTV, srv, num))
else:
fav_id = f"{srv_data[3]}:{srv_data[4]}:{srv_data[5]}:{srv_data[6]}"
fav_id = srv.strip().upper()
name = None
if data_len == 12:
name, sep, desc = str(srv_data[-1]).partition("\n#DESCRIPTION")
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id.upper(), num))
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id, num))
return bq_name.lstrip("#NAME").strip(), services
def to_bouquet_id(srv):
""" Creates bouquet channel id. """
data_type = srv.data_id
if data_type and len(data_type) > 4:
data_type = int(srv.data_id.split(":")[4])
return "{}:0:{:X}:{}:0:0:0:".format(1, data_type, srv.fav_id)
if __name__ == "__main__":
pass

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
# Copyright (c) 2018-2024 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
@@ -69,7 +69,7 @@ class LameDbReader:
return self.parse_v5()
raise SyntaxError("Unsupported version of the format.")
def parse_v3(self, services, transponders):
def parse_v3(self, services_data, transponders):
""" Parsing version 3. """
for t in transponders:
tr = transponders[t].lower()
@@ -92,7 +92,7 @@ class LameDbReader:
transponders[t] = tr
return self.parse_services(services, transponders)
return self.parse_services(services_data, transponders)
def parse_v4(self):
""" Parsing version 4. """
@@ -112,14 +112,17 @@ class LameDbReader:
if lns and not lns[0].endswith("/5/\n"):
raise SyntaxError("lamedb ver.5 parsing error: unsupported format.")
trs, srvs = {}, [""]
trs, srvs = {}, []
for line in lns:
if line.startswith("s:"):
srv_data = line.strip("s:").split(",", 2)
srv_data[1] = srv_data[1].strip("\"\n")
data_len = len(srv_data)
if data_len == 3:
srv_data[2] = srv_data[2].strip()
s_data = srv_data[2].strip()
if not s_data.startswith("p:"):
s_data = f"p:,{s_data}"
srv_data[2] = s_data
elif data_len == 2:
srv_data.append("p:")
srvs.extend(srv_data)
@@ -134,15 +137,12 @@ class LameDbReader:
return self.parse_services(srvs, trs)
def parse_services(self, services, transponders):
def parse_services(self, services_data, transponders):
""" Parsing services. """
services_list = []
blacklist = get_blacklist(self._path) if self._path else {}
srvs = self.split(services, 3)
if srvs[0][0] == "": # Remove first empty element.
srvs.remove(srvs[0])
for srv in srvs:
for srv in self.get_services(services_data):
data_id = str(srv[0]).lower() # Lower is for lamedb ver.3.
data = data_id.split(_SEP)
sp = "0"
@@ -171,15 +171,14 @@ class LameDbReader:
ssid = str(data[0]).lstrip(sp).upper()
onid = str(data[1]).lstrip(sp).upper()
# For comparison in bouquets. Needed in upper case!!!
fav_id = f"{ssid}:{tid}:{nid}:{onid}"
fav_id = f"1:0:{srv_type:X}:{ssid}:{tid}:{nid}:{onid}:0:0:0:"
picon_id = f"1_0_{srv_type:X}_{ssid}_{tid}_{nid}_{onid}_0_0_0.png"
s_id = f"1:0:{srv_type:X}:{ssid}:{tid}:{nid}:{onid}:0:0:0:"
all_flags = srv[2].split(",")
coded = CODED_ICON if list(filter(lambda x: x.startswith("C:"), all_flags)) else None
flags = list(filter(lambda x: x.startswith("f:"), all_flags))
hide = HIDE_ICON if flags and Flag.is_hide(Flag.parse(flags[0])) else None
locked = LOCKED_ICON if s_id in blacklist else None
locked = LOCKED_ICON if fav_id in blacklist else None
package = list(filter(lambda x: x.startswith("p:"), all_flags))
package = package[0][2:] if package else ""
@@ -243,11 +242,12 @@ class LameDbReader:
transponders, sep, services = services.partition("services") # 2 step
services, sep, _ = services.partition("\nend") # 3 step
services = services.strip()
if match.group() == "/3/":
return self.parse_v3(services.split("\n"), self.parse_transponders(transponders.split("/")))
return self.parse_v3(services.splitlines(), self.parse_transponders(transponders.split("/")))
return self.parse_services(services.split("\n"), self.parse_transponders(transponders.split("/")))
return self.parse_services(services.splitlines(), self.parse_transponders(transponders.split("/")))
@staticmethod
def get_services_lines(services):
@@ -282,17 +282,25 @@ class LameDbReader:
return transponders
def split(self, itr, size):
""" Divide the iterable. """
srv = []
def get_services(self, itr, size=3):
""" Separates and extract services data. """
services = []
tmp = []
for i, line in enumerate(itr):
i = 0
for line in itr:
i += 1
tmp.append(line)
if i % size == 0:
srv.append(tuple(tmp))
tmp.clear()
return srv
if i == size:
if not line.startswith("p:"):
# To prevent cases of incorrect service data formation
# (e.g. the name contains a line break)
tmp.pop()
i -= 1
else:
services.append(tuple(tmp))
tmp.clear()
i = 0
return services
class LameDbWriter:

View File

@@ -0,0 +1,78 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2024 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" Additional module to use stream relay functionality.
Reads/Writes 'whitelist_streamrelay' file.
"""
import os.path
from contextlib import suppress
from app.commons import log
_FILE_NAME = "whitelist_streamrelay"
class StreamRelay(dict):
""" Class to hold/process service references used by a stream relay. """
def refresh(self, path):
self.clear()
f_path = f"{path}{_FILE_NAME}"
if os.path.isfile(f_path):
log("Updating stream relay cache...")
with suppress(FileNotFoundError):
with open(f"{path}{_FILE_NAME}", "r", encoding="utf-8") as file:
refs = filter(None, (x.rstrip("\n") for x in file.readlines()))
self.update(self.get_ref_data(ref) for ref in refs)
def get_ref_data(self, ref):
""" Returns tuple from FAV ID and ref or ref and None for comments. """
data = ref.split(":")
if len(data) == 11:
if "http" in data[-1]:
return ref.replace("%3a", "%3A"), ref
return f"{data[3]}:{data[4]}:{data[5]}:{data[6]}", ref
return ref, None
def save(self, path):
""" Saves current refs to a file.
If no refs is present, delites current relay file.
"""
f_name = f"{path}{_FILE_NAME}"
if len(self):
with open(f_name, "w", encoding="utf-8") as file:
file.writelines([f"{v if v else k}\n\n" for k, v in self.items()])
else:
if os.path.exists(f_name):
os.remove(f_name)
if __name__ == "__main__":
pass

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2023 Dmitriy Yefremov
# Copyright (c) 2018-2024 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
@@ -42,6 +42,7 @@ ENIGMA2_FAV_ID_FORMAT = " {}:{}:{:X}:{:X}:{:X}:{:X}:{:X}:0:0:0:{}:{}\n#DESCRIPTI
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"
ENCODING_BLACKLIST = {"MacRoman"}
class StreamType(Enum):
DVB_TS = "1"
@@ -58,6 +59,9 @@ class StreamType(Enum):
def parse_m3u(path, s_type, detect_encoding=True, params=None):
""" Parses *m3u* file and returns tuple with EPG src URLs and services list. """
pattern = re.compile(r'(\S+)="(.*?)"')
with open(path, "rb") as file:
data = file.read()
encoding = "utf-8"
@@ -70,11 +74,14 @@ def parse_m3u(path, s_type, detect_encoding=True, params=None):
else:
enc = chardet.detect(data)
encoding = enc.get("encoding", "utf-8")
encoding = "utf-8" if encoding in ENCODING_BLACKLIST else encoding
aggr = [None] * 10
s_aggr = aggr[: -3]
services = []
epg_src = None
group = None
groups = set()
services = []
marker_counter = 1
sid_counter = 1
name = None
@@ -85,66 +92,66 @@ def parse_m3u(path, s_type, detect_encoding=True, params=None):
m_name = BqServiceType.MARKER.name
for line in str(data, encoding=encoding, errors="ignore").splitlines():
if line.startswith("#EXTM3U"):
data = dict(pattern.findall(line))
epg_src = data.get("x-tvg-url", data.get("url-tvg", None))
epg_src = epg_src.split(",") if epg_src else None
if line.startswith("#EXTINF"):
line, sep, name = line.rpartition(",")
data = re.split('"', line)
size = len(data)
if size < 3:
continue
d = {data[i].lower().strip(" ="): data[i + 1] for i in range(0, len(data) - 1, 2)}
picon = d.get("tvg-logo", None)
data = dict(pattern.findall(line))
name = data.get("tvg-name", name)
picon = data.get("tvg-logo", None)
if s_type is SettingsType.ENIGMA_2:
grp_name = d.get("group-title", None)
if grp_name not in groups:
groups.add(grp_name)
fav_id = MARKER_FORMAT.format(marker_counter, grp_name, grp_name)
marker_counter += 1
mr = Service(None, None, None, grp_name, *aggr[0:3], m_name, *aggr, fav_id, None)
services.append(mr)
group = data.get("group-title", None)
elif line.startswith("#EXTGRP") and s_type is SettingsType.ENIGMA_2:
grp_name = line.strip("#EXTGRP:").strip()
if grp_name not in groups:
groups.add(grp_name)
fav_id = MARKER_FORMAT.format(marker_counter, grp_name, grp_name)
marker_counter += 1
mr = Service(None, None, None, grp_name, *aggr[0:3], m_name, *aggr, fav_id, None)
services.append(mr)
elif not line.startswith("#"):
group = line.strip("#EXTGRP:").strip()
elif not line.startswith("#") and "://" in line:
url = line.strip()
params[0] = sid_counter
sid_counter += 1
fav_id = get_fav_id(url, name, s_type, params)
if s_type is SettingsType.ENIGMA_2:
p_id = get_picon_id(params)
if group not in groups:
# Some playlists have "random" of group names.
# We will take only the first one we found on the list!
groups.add(group)
m_id = MARKER_FORMAT.format(marker_counter, group, group)
marker_counter += 1
services.append(Service(None, None, None, group, *aggr[0:3], m_name, *aggr, m_id, None))
if all((name, url, fav_id)):
srv = Service(None, None, IPTV_ICON, name, *aggr[0:3], st, picon, p_id, *s_aggr, url, fav_id, None)
services.append(srv)
services.append(Service(None, None, IPTV_ICON, name, *aggr[0:2], group,
st, picon, p_id, *s_aggr, url, fav_id, None))
else:
log(f"*.m3u* parse error ['{path}']: name[{name}], url[{url}], fav id[{fav_id}]")
return services
return epg_src, services
def export_to_m3u(path, bouquet, s_type, url=None):
pattern = re.compile(".*:(http.*):.*") if s_type is SettingsType.ENIGMA_2 else re.compile("(http.*?)::::.*")
pattern = re.compile(".*:(http.*).*") if s_type is SettingsType.ENIGMA_2 else re.compile("(http.*?)::::.*")
lines = ["#EXTM3U\n"]
current_grp = None
for s in bouquet.services:
s_type = s.type
if s_type is BqServiceType.IPTV:
srv_type = s.type
if srv_type is BqServiceType.IPTV:
res = re.match(pattern, s.data)
if not res:
continue
lines.append(f"#EXTINF:-1,{s.name}\n")
lines.append(current_grp) if current_grp else None
lines.append(f"{unquote(res.group(1).strip())}\n")
elif s_type is BqServiceType.MARKER:
u = res.group(1)
if s_type is SettingsType.ENIGMA_2:
index = u.rfind(":")
lines.append(f"{unquote(u[:index] if index > 0 else u)}\n")
else:
lines.append(f"{u}\n")
elif srv_type is BqServiceType.MARKER:
current_grp = f"#EXTGRP:{s.name}\n"
elif s_type is BqServiceType.DEFAULT and url:
elif srv_type is BqServiceType.DEFAULT and url:
lines.append(f"#EXTINF:-1,{s.name}\n")
lines.append(current_grp) if current_grp else None
lines.append(f"{url}{s.data}\n")

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2023 Dmitriy Yefremov
# Copyright (c) 2018-2024 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,6 +28,7 @@
import os
from app.commons import log
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
@@ -35,7 +36,8 @@ from ..ecommons import Bouquets, Bouquet, BouquetService, BqServiceType, PROVIDE
_FILE = "bouquets.xml"
_U_FILE = "ubouquets.xml"
_W_FILE = "webtv.xml"
_W_FILE = "webtv_usr.xml"
_WEB_TV_NAME = "[Web TV]"
_COMMENT = " File was created in DemonEditor. Enjoy watching! "
@@ -60,20 +62,27 @@ def parse_bouquets(file, name, bq_type):
hidden = bq_attrs.get("hidden", "0")
locked = bq_attrs.get("locked", "0")
services = []
for srv_elem in elem.getElementsByTagName("S"):
if srv_elem.hasAttributes():
s_attrs = get_xml_attributes(srv_elem)
ssid = s_attrs.get("i", "0")
on = s_attrs.get("on", "0")
tr_id = s_attrs.get("t", "0")
fav_id = "{}:{}:{}".format(tr_id, on, ssid)
services.append(BouquetService(None, BqServiceType.DEFAULT, fav_id, 0))
if "i" in s_attrs:
ssid = s_attrs.get("i", "0")
on = s_attrs.get("on", "0")
tr_id = s_attrs.get("t", "0")
fav_id = f"{tr_id}:{on}:{ssid}"
services.append(BouquetService(None, BqServiceType.DEFAULT, fav_id, 0))
elif "u" in s_attrs:
services.append(get_webtv_service(s_attrs))
else:
log(f"Parse bouquets [Neutrino] error: Unknown service type. -> {s_attrs}")
bouquets[2].append(Bouquet(name=bq_name,
type=bq_type,
services=services,
locked=locked == "1",
hidden=hidden == "1",
file=SP.join("{}{}{}".format(k, KSP, v) for k, v in bq_attrs.items())))
file=SP.join(f"{k}{KSP}{v}" for k, v in bq_attrs.items())))
if BqType(bq_type) is BqType.BOUQUET:
for bq in bouquets.bouquets:
@@ -92,31 +101,40 @@ def parse_webtv(path, name, bq_type):
return bouquets
dom = XmlHandler.parse(path)
# Display name.
name = None
for e in dom.childNodes:
if e.nodeType == e.ELEMENT_NODE:
name = e.getAttribute("name")
break
services = []
for elem in dom.getElementsByTagName("webtv"):
if elem.hasAttributes():
web_attrs = get_xml_attributes(elem)
title = web_attrs.get("title", "")
url = web_attrs.get("url", "")
description = web_attrs.get("description", "")
urlkey = web_attrs.get("urlkey", None)
account = web_attrs.get("account", None)
usrname = web_attrs.get("usrname", None)
psw = web_attrs.get("psw", None)
s_type = web_attrs.get("type", None)
iconsrc = web_attrs.get("iconsrc", None)
iconsrc_b = web_attrs.get("iconsrc_b", None)
group = web_attrs.get("group", None)
fav_id = NEUTRINO_FAV_ID_FORMAT.format(url, description, urlkey, account, usrname, psw, s_type, iconsrc,
iconsrc_b, group)
services.append(BouquetService(name=title, type=BqServiceType.IPTV, data=fav_id, num=0))
services.append(get_webtv_service(web_attrs))
bouquet = Bouquet(name="default", type=bq_type, services=services, locked=None, hidden=None, file=None)
bouquet = Bouquet(name=name or _WEB_TV_NAME, type=bq_type, services=services, locked=None, hidden=None, file=None)
bouquets[2].append(bouquet)
return bouquets
def get_webtv_service(web_attrs):
title = web_attrs.get("title", web_attrs.get("n", ""))
fav_id = NEUTRINO_FAV_ID_FORMAT.format(web_attrs.get("url", web_attrs.get("u", )),
web_attrs.get("description", ""),
web_attrs.get("urlkey", None),
web_attrs.get("account", None),
web_attrs.get("usrname", None),
web_attrs.get("psw", None),
web_attrs.get("type", None),
web_attrs.get("iconsrc", None),
web_attrs.get("iconsrc_b", None),
web_attrs.get("group", None))
return BouquetService(name=title, type=BqServiceType.IPTV, data=fav_id, num=0)
def write_bouquets(path, bouquets):
for bq in bouquets:
bq_type = BqType(bq.type)
@@ -153,14 +171,25 @@ def write_bouquet(file, bouquet):
root.appendChild(bq_elem)
for srv in bq.services:
tr_id, on, ssid = srv.fav_id.split(":")
srv_elem = doc.createElement("S")
srv_elem.setAttribute("i", ssid)
srv_elem.setAttribute("n", srv.service)
srv_elem.setAttribute("t", tr_id)
srv_elem.setAttribute("on", on)
srv_elem.setAttribute("frq", srv.freq)
srv_elem.setAttribute("s", get_attributes(srv.flags_cas).get("position", "0"))
s_type = BqServiceType(srv.service_type)
if s_type is BqServiceType.DEFAULT:
tr_id, on, ssid = srv.fav_id.split(":")
srv_elem.setAttribute("i", ssid)
srv_elem.setAttribute("t", tr_id)
srv_elem.setAttribute("on", on)
srv_elem.setAttribute("frq", srv.freq)
srv_elem.setAttribute("s", get_attributes(srv.flags_cas).get("position", "0"))
elif s_type is BqServiceType.IPTV:
s_data = srv.fav_id.split("::")
if s_data:
srv_elem.setAttribute("n", srv.service)
srv_elem.setAttribute("u", s_data[0])
else:
log(f"Write bouquet [Neutrino] error: Unsupported service type. -> {s_type.value}")
bq_elem.appendChild(srv_elem)
doc.write_xml(file)
@@ -174,6 +203,7 @@ def write_webtv(file, bouquet):
doc.appendChild(comment)
for bq in bouquet.bouquets:
root.setAttribute("name", bq.name or _WEB_TV_NAME)
for srv in bq.services:
url, description, urlkey, account, usrname, psw, s_type, iconsrc, iconsrc_b, group = srv.fav_id.split("::")
srv_elem = doc.createElement("webtv")

View File

@@ -31,7 +31,7 @@ import json
import locale
import os
import sys
from enum import Enum, IntEnum
from enum import IntEnum
from functools import lru_cache
from pathlib import Path
from pprint import pformat
@@ -51,8 +51,8 @@ IS_LINUX = sys.platform == "linux"
USE_HEADER_BAR = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
class Defaults(Enum):
""" Default program settings """
class Defaults:
""" Default program settings. """
USER = "root"
PASSWORD = ""
HOST = "127.0.0.1"
@@ -114,30 +114,30 @@ class SettingsType(IntEnum):
def get_default_settings(self):
""" Returns default settings for current type. """
if self is self.ENIGMA_2:
srv_path = Defaults.BOX_SERVICES_PATH.value
sat_path = Defaults.BOX_SATELLITE_PATH.value
picons_path = Defaults.BOX_PICON_PATH.value
epg_path = Defaults.BOX_EPG_PATH.value
srv_path = Defaults.BOX_SERVICES_PATH
sat_path = Defaults.BOX_SATELLITE_PATH
picons_path = Defaults.BOX_PICON_PATH
epg_path = Defaults.BOX_EPG_PATH
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
srv_path = Defaults.NEUTRINO_BOX_SERVICES_PATH
sat_path = Defaults.NEUTRINO_BOX_SATELLITE_PATH
picons_path = Defaults.NEUTRINO_BOX_PICON_PATH
epg_path = ""
http_timeout = 2
telnet_timeout = 1
return {"setting_type": self.value,
"host": Defaults.HOST.value,
"port": Defaults.FTP_PORT.value,
"host": Defaults.HOST,
"port": Defaults.FTP_PORT,
"timeout": 5,
"user": Defaults.USER.value,
"password": Defaults.PASSWORD.value,
"http_port": Defaults.HTTP_PORT.value,
"user": Defaults.USER,
"password": Defaults.PASSWORD,
"http_port": Defaults.HTTP_PORT,
"http_timeout": http_timeout,
"http_use_ssl": Defaults.HTTP_USE_SSL.value,
"telnet_port": Defaults.TELNET_PORT.value,
"http_use_ssl": Defaults.HTTP_USE_SSL,
"telnet_port": Defaults.TELNET_PORT,
"telnet_timeout": telnet_timeout,
"services_path": srv_path,
"user_bouquet_path": srv_path,
@@ -161,6 +161,15 @@ class PlayStreamsMode(IntEnum):
M3U = 2
class PlaybackMode(IntEnum):
""" Playback mode by double click of mouse in the bouquet (FAV) list. """
DISABLED = 0
STREAM = 1
PLAY = 2
ZAP = 3
ZAP_PLAY = 4
class EpgSource(IntEnum):
HTTP = 0 # HTTP API -> WebIf
DAT = 1 # epg.dat file
@@ -402,9 +411,9 @@ class Settings:
@property
def picons_paths(self):
if self.setting_type is SettingsType.NEUTRINO_MP:
return self._settings.get("neutrino_picon_paths", Defaults.NEUTRINO_BOX_PICON_PATHS.value)
return self._settings.get("neutrino_picon_paths", Defaults.NEUTRINO_BOX_PICON_PATHS)
else:
return self._settings.get("picon_paths", Defaults.BOX_PICON_PATHS.value)
return self._settings.get("picon_paths", Defaults.BOX_PICON_PATHS)
@picons_paths.setter
def picons_paths(self, value):
@@ -417,12 +426,20 @@ class Settings:
@property
def profile_folder_is_default(self):
return self._settings.get("profile_folder_is_default", Defaults.PROFILE_FOLDER_DEFAULT.value)
return self._settings.get("profile_folder_is_default", Defaults.PROFILE_FOLDER_DEFAULT)
@profile_folder_is_default.setter
def profile_folder_is_default(self, value):
self._settings["profile_folder_is_default"] = value
@property
def use_common_picon_path(self):
return self._settings.get("use_common_picon_path", False)
@use_common_picon_path.setter
def use_common_picon_path(self, value):
self._settings["use_common_picon_path"] = value
@property
def default_data_path(self):
return self._settings.get("default_data_path", DATA_PATH)
@@ -433,7 +450,7 @@ class Settings:
@property
def default_backup_path(self):
return self._settings.get("default_backup_path", Defaults.BACKUP_PATH.value)
return self._settings.get("default_backup_path", Defaults.BACKUP_PATH)
@default_backup_path.setter
def default_backup_path(self, value):
@@ -441,7 +458,7 @@ class Settings:
@property
def default_picon_path(self):
return self._settings.get("default_picon_path", Defaults.PICON_PATH.value)
return self._settings.get("default_picon_path", Defaults.PICON_PATH)
@default_picon_path.setter
def default_picon_path(self, value):
@@ -457,6 +474,9 @@ class Settings:
@property
def profile_picons_path(self):
if self.use_common_picon_path:
return self.default_picon_path
if self.profile_folder_is_default:
return f"{self.profile_data_path}picons{SEP}"
return f"{self.default_picon_path}{self._current_profile}{SEP}"
@@ -477,7 +497,7 @@ class Settings:
@property
def recordings_path(self):
return self._settings.get("recordings_path", Defaults.RECORDINGS_PATH.value)
return self._settings.get("recordings_path", Defaults.RECORDINGS_PATH)
@recordings_path.setter
def recordings_path(self, value):
@@ -487,7 +507,7 @@ class Settings:
@property
def activate_transcoding(self):
return self._settings.get("activate_transcoding", Defaults.ACTIVATE_TRANSCODING.value)
return self._settings.get("activate_transcoding", Defaults.ACTIVATE_TRANSCODING)
@activate_transcoding.setter
def activate_transcoding(self, value):
@@ -495,7 +515,7 @@ class Settings:
@property
def active_preset(self):
return self._settings.get("active_preset", Defaults.ACTIVE_TRANSCODING_PRESET.value)
return self._settings.get("active_preset", Defaults.ACTIVE_TRANSCODING_PRESET)
@active_preset.setter
def active_preset(self, value):
@@ -511,7 +531,7 @@ class Settings:
@property
def play_streams_mode(self):
return PlayStreamsMode(self._settings.get("play_streams_mode", Defaults.PLAY_STREAMS_MODE.value))
return PlayStreamsMode(self._settings.get("play_streams_mode", Defaults.PLAY_STREAMS_MODE))
@play_streams_mode.setter
def play_streams_mode(self, value):
@@ -519,7 +539,7 @@ class Settings:
@property
def stream_lib(self):
return self._settings.get("stream_lib", Defaults.STREAM_LIB.value)
return self._settings.get("stream_lib", Defaults.STREAM_LIB)
@stream_lib.setter
def stream_lib(self, value):
@@ -527,7 +547,7 @@ class Settings:
@property
def fav_click_mode(self):
return self._settings.get("fav_click_mode", Defaults.FAV_CLICK_MODE.value)
return self._settings.get("fav_click_mode", Defaults.FAV_CLICK_MODE)
@fav_click_mode.setter
def fav_click_mode(self, value):
@@ -535,7 +555,7 @@ class Settings:
@property
def main_list_playback(self):
return self._settings.get("main_list_playback", Defaults.MAIN_LIST_PLAYBACK.value)
return self._settings.get("main_list_playback", Defaults.MAIN_LIST_PLAYBACK)
@main_list_playback.setter
def main_list_playback(self, value):
@@ -576,6 +596,14 @@ class Settings:
def epg_xml_source(self, value):
self._cp_settings["epg_xml_source"] = value
@property
def epg_xml_sources(self):
return self._cp_settings.get("epg_xml_sources", [self.epg_xml_source])
@epg_xml_sources.setter
def epg_xml_sources(self, value):
self._cp_settings["epg_xml_sources"] = value
# *********** FTP ************ #
@property
@@ -590,7 +618,7 @@ class Settings:
@property
def backup_before_save(self):
return self._settings.get("backup_before_save", Defaults.BACKUP_BEFORE_SAVE.value)
return self._settings.get("backup_before_save", Defaults.BACKUP_BEFORE_SAVE)
@backup_before_save.setter
def backup_before_save(self, value):
@@ -598,7 +626,7 @@ class Settings:
@property
def backup_before_downloading(self):
return self._settings.get("backup_before_downloading", Defaults.BACKUP_BEFORE_DOWNLOADING.value)
return self._settings.get("backup_before_downloading", Defaults.BACKUP_BEFORE_DOWNLOADING)
@backup_before_downloading.setter
def backup_before_downloading(self, value):
@@ -606,7 +634,7 @@ class Settings:
@property
def v5_support(self):
return self._settings.get("v5_support", Defaults.V5_SUPPORT.value)
return self._settings.get("v5_support", Defaults.V5_SUPPORT)
@v5_support.setter
def v5_support(self, value):
@@ -614,7 +642,7 @@ class Settings:
@property
def unlimited_copy_buffer(self):
return self._settings.get("unlimited_copy_buffer", Defaults.UNLIMITED_COPY_BUFFER.value)
return self._settings.get("unlimited_copy_buffer", Defaults.UNLIMITED_COPY_BUFFER)
@unlimited_copy_buffer.setter
def unlimited_copy_buffer(self, value):
@@ -622,7 +650,7 @@ class Settings:
@property
def extensions_support(self):
return self._settings.get("extensions_support", Defaults.EXTENSIONS_SUPPORT.value)
return self._settings.get("extensions_support", Defaults.EXTENSIONS_SUPPORT)
@extensions_support.setter
def extensions_support(self, value):
@@ -630,7 +658,7 @@ class Settings:
@property
def force_bq_names(self):
return self._settings.get("force_bq_names", Defaults.FORCE_BQ_NAMES.value)
return self._settings.get("force_bq_names", Defaults.FORCE_BQ_NAMES)
@force_bq_names.setter
def force_bq_names(self, value):
@@ -638,7 +666,7 @@ class Settings:
@property
def http_api_support(self):
return self._settings.get("http_api_support", Defaults.HTTP_API_SUPPORT.value)
return self._settings.get("http_api_support", Defaults.HTTP_API_SUPPORT)
@http_api_support.setter
def http_api_support(self, value):
@@ -646,7 +674,7 @@ class Settings:
@property
def enable_yt_dl(self):
return self._settings.get("enable_yt_dl", Defaults.ENABLE_YT_DL.value)
return self._settings.get("enable_yt_dl", Defaults.ENABLE_YT_DL)
@enable_yt_dl.setter
def enable_yt_dl(self, value):
@@ -654,7 +682,7 @@ class Settings:
@property
def enable_yt_dl_update(self):
return self._settings.get("enable_yt_dl_update", Defaults.ENABLE_YT_DL.value)
return self._settings.get("enable_yt_dl_update", Defaults.ENABLE_YT_DL)
@enable_yt_dl_update.setter
def enable_yt_dl_update(self, value):
@@ -662,7 +690,7 @@ class Settings:
@property
def enable_send_to(self):
return self._settings.get("enable_send_to", Defaults.ENABLE_SEND_TO.value)
return self._settings.get("enable_send_to", Defaults.ENABLE_SEND_TO)
@enable_send_to.setter
def enable_send_to(self, value):
@@ -722,7 +750,7 @@ class Settings:
@property
def list_picon_size(self):
return self._settings.get("list_picon_size", Defaults.LIST_PICON_SIZE.value)
return self._settings.get("list_picon_size", Defaults.LIST_PICON_SIZE)
@list_picon_size.setter
def list_picon_size(self, value):
@@ -730,7 +758,7 @@ class Settings:
@property
def tooltip_logo_size(self):
return self._settings.get("tooltip_logo_size", Defaults.TOOLTIP_LOGO_SIZE.value)
return self._settings.get("tooltip_logo_size", Defaults.TOOLTIP_LOGO_SIZE)
@tooltip_logo_size.setter
def tooltip_logo_size(self, value):
@@ -738,7 +766,7 @@ class Settings:
@property
def use_colors(self):
return self._settings.get("use_colors", Defaults.USE_COLORS.value)
return self._settings.get("use_colors", Defaults.USE_COLORS)
@use_colors.setter
def use_colors(self, value):
@@ -746,7 +774,7 @@ class Settings:
@property
def new_color(self):
return self._settings.get("new_color", Defaults.NEW_COLOR.value)
return self._settings.get("new_color", Defaults.NEW_COLOR)
@new_color.setter
def new_color(self, value):
@@ -754,7 +782,7 @@ class Settings:
@property
def extra_color(self):
return self._settings.get("extra_color", Defaults.EXTRA_COLOR.value)
return self._settings.get("extra_color", Defaults.EXTRA_COLOR)
@extra_color.setter
def extra_color(self, value):
@@ -920,18 +948,18 @@ class Settings:
return {
"version": Settings.__VERSION,
"default_profile": Defaults.DEFAULT_PROFILE.value,
"default_profile": Defaults.DEFAULT_PROFILE,
"profiles": {profile_name: def_settings},
"v5_support": Defaults.V5_SUPPORT.value,
"http_api_support": Defaults.HTTP_API_SUPPORT.value,
"enable_yt_dl": Defaults.ENABLE_YT_DL.value,
"enable_send_to": Defaults.ENABLE_SEND_TO.value,
"use_colors": Defaults.USE_COLORS.value,
"new_color": Defaults.NEW_COLOR.value,
"extra_color": Defaults.EXTRA_COLOR.value,
"fav_click_mode": Defaults.FAV_CLICK_MODE.value,
"profile_folder_is_default": Defaults.PROFILE_FOLDER_DEFAULT.value,
"records_path": Defaults.RECORDINGS_PATH.value
"v5_support": Defaults.V5_SUPPORT,
"http_api_support": Defaults.HTTP_API_SUPPORT,
"enable_yt_dl": Defaults.ENABLE_YT_DL,
"enable_send_to": Defaults.ENABLE_SEND_TO,
"use_colors": Defaults.USE_COLORS,
"new_color": Defaults.NEW_COLOR,
"extra_color": Defaults.EXTRA_COLOR,
"fav_click_mode": Defaults.FAV_CLICK_MODE,
"profile_folder_is_default": Defaults.PROFILE_FOLDER_DEFAULT,
"records_path": Defaults.RECORDINGS_PATH
}
@staticmethod

View File

@@ -34,7 +34,7 @@ import shutil
import struct
import sys
import xml.etree.ElementTree as ET
from collections import namedtuple
from collections import namedtuple, defaultdict
from datetime import datetime, timezone
from tempfile import NamedTemporaryFile
from urllib.parse import urlparse
@@ -61,6 +61,10 @@ EpgEvent.__new__.__defaults__ = ("N/A", "N/A", 0, 0, 0, "N/A", None) # For Pyth
class Reader(metaclass=abc.ABCMeta):
@property
@abc.abstractmethod
def cache(self) -> dict: pass
@abc.abstractmethod
def download(self, clb=None): pass
@@ -121,6 +125,10 @@ class EPG:
self._refs = {}
self._desc = {}
@property
def cache(self) -> dict:
return self._refs
def download(self, clb=None):
pass
@@ -229,13 +237,19 @@ class XmlTvReader(Reader):
TIME_FORMAT_STR = "%Y%m%d%H%M%S %z"
SUFFIXES = {".gz", ".xz", ".lzma", ".xml"}
Service = namedtuple("Service", ["id", "names", "logo", "events"])
Event = namedtuple("EpgEvent", ["start", "duration", "title", "desc"])
def __init__(self, path, url):
def __init__(self, path, url=None):
self._path = path
self._url = url
self._ids = {}
self._cache = {}
@property
def cache(self) -> dict:
return self._cache
def download(self, clb=None):
""" Downloads an XMLTV file. """
@@ -244,23 +258,28 @@ class XmlTvReader(Reader):
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":
with requests.get(url=self._url, stream=True) as resp:
if resp.reason == "OK":
suf = self._url[self._url.rfind("."):]
if suf not in (".gz", ".xz", ".lzma"):
if suf not in self.SUFFIXES:
log(f"{self.__class__.__name__} [download] error: Unsupported file extension.")
return
data_len = request.headers.get("content-length")
data_size = resp.headers.get("content-length")
if not data_size:
log(f"{self.__class__.__name__} [download *.{suf}] error: Error getting data size.")
if clb:
clb()
return
with NamedTemporaryFile(suffix=suf, delete=not IS_WIN) as tf:
downloaded = 0
data_len = int(data_len)
data_size = int(data_size)
log("Downloading XMLTV file...")
for data in request.iter_content(chunk_size=1024):
for data in resp.iter_content(chunk_size=1024):
downloaded += len(data)
tf.write(data)
done = int(50 * downloaded / data_len)
done = int(50 * downloaded / data_size)
sys.stdout.write(f"\rDownloading XMLTV file [{'=' * done}{' ' * (50 - done)}]")
sys.stdout.flush()
tf.seek(0)
@@ -281,47 +300,70 @@ class XmlTvReader(Reader):
shutil.copyfileobj(lzf, self._path)
except (lzma.LZMAError, OSError) as e:
log(f"{self.__class__.__name__} [download *.xz] error: {e}")
else:
try:
import gzip
with gzip.open(self._path, "wb") as f_out:
shutil.copyfileobj(tf, f_out)
except OSError as e:
log(f"{self.__class__.__name__} [download *.xml] error: {e}")
if IS_WIN and os.path.isfile(tf.name):
tf.close()
os.remove(tf.name)
else:
log(f"{self.__class__.__name__} [download] error: {request.reason}")
log(f"{self.__class__.__name__} [download] error: {resp.reason}")
if clb:
clb()
def get_current_events(self, names: set) -> dict:
events = {}
events = defaultdict(list)
dt = datetime.utcnow()
utc = dt.timestamp()
offset = datetime.now() - dt
for srv in filter(lambda s: any(name in names for name in s.names), self._ids.values()):
ev = 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()
for n in srv.names:
events[n] = EpgEvent(n, ev.title, start, end_time, int(ev.duration), ev.desc, ev)
for srv in filter(lambda s: any(name in names for name in s.names), self._cache.values()):
[self.process_event(ev, events, offset, srv) for ev in filter(lambda s: s.duration > utc, srv.events)]
return events
@staticmethod
def process_event(ev, events, offset, srv):
start = datetime.fromtimestamp(ev.start) + offset
end_time = datetime.fromtimestamp(ev.duration) + offset
start = start.timestamp()
end_time = end_time.timestamp()
duration = end_time - start
for n in srv.names:
data = {"e2eventservicename": n,
"e2eventtitle": ev.title,
"e2eventdescription": ev.desc,
"e2eventstart": start,
"e2eventduration": duration}
events[n].append(EpgEvent(n, ev.title, start, end_time, duration, ev.desc, data))
def parse(self):
""" Parses XML. """
try:
import gzip
log("Processing XMLTV data...")
suf = os.path.splitext(self._path)[1]
if suf == ".gz":
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.")
with gzip.open(self._path, "rb") as gzf:
list(map(self.process_node, ET.iterparse(gzf)))
elif suf == ".xml":
with open(self._path, "rb") as xml:
list(map(self.process_node, ET.iterparse(xml)))
else:
log(f"{self.__class__.__name__} [parse] error: Unsupported file type [{suf}].")
except OSError as e:
log(f"{self.__class__.__name__} [parse] error: {e}")
else:
log("XMLTV data parsing is complete.")
def process_node(self, node):
event, element = node
@@ -329,9 +371,9 @@ class XmlTvReader(Reader):
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, [])
self._cache[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)
channel = self._cache.get(element.get(self.CH_TAG, None), None)
if channel:
events = channel[-1]
start = element.get("start", None)

View File

@@ -30,17 +30,20 @@ import os
import sys
from datetime import datetime
from gi.repository import Gdk, Gtk, GObject
from gi.repository import GObject
from app.commons import run_task, log, LOG_DATE_FORMAT, run_with_delay
from app.commons import run_task, log, LOG_DATE_FORMAT
from app.settings import IS_DARWIN, IS_LINUX, IS_WIN
class Player(Gtk.DrawingArea):
class Player(GObject.GObject):
""" Base player class. Also used as a factory. """
def __init__(self, mode, widget, **kwargs):
super().__init__(**kwargs)
self._mode = mode
self._is_playing = False
self._handle = self.get_window_handle(widget.playback_widget)
GObject.signal_new("error", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
@@ -55,16 +58,9 @@ class Player(Gtk.DrawingArea):
GObject.signal_new("subtitle-track", self, GObject.SIGNAL_RUN_LAST,
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
self.connect("draw", self.on_draw)
self.connect("motion-notify-event", self.on_mouse_motion)
self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)
widget.add(self)
parent = widget.get_parent()
parent.connect("play", self.on_play)
parent.connect("stop", self.on_stop)
parent.connect("pause", self.on_pause)
self.show()
widget.connect("play", self.on_play)
widget.connect("stop", self.on_stop)
widget.connect("pause", self.on_pause)
def get_play_mode(self):
pass
@@ -114,14 +110,14 @@ class Player(Gtk.DrawingArea):
def on_release(self, widget, state):
self.release()
def get_window_handle(self):
def get_window_handle(self, widget):
""" Returns the identifier [pointer] for the window.
Based on gtkvlc.py[get_window_pointer] example from here:
https://github.com/oaubert/python-vlc/tree/master/examples
"""
if IS_LINUX:
return self.get_window().get_xid()
return widget.get_window().get_xid()
else:
try:
import ctypes
@@ -133,31 +129,13 @@ class Player(Gtk.DrawingArea):
# https://gitlab.gnome.org/GNOME/pygobject/-/issues/112
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object]
gpointer = ctypes.pythonapi.PyCapsule_GetPointer(self.get_window().__gpointer__, None)
gpointer = ctypes.pythonapi.PyCapsule_GetPointer(widget.get_window().__gpointer__, None)
get_pointer = libgdk.gdk_quartz_window_get_nsview if IS_DARWIN else libgdk.gdk_win32_window_get_handle
get_pointer.restype = ctypes.c_void_p
get_pointer.argtypes = [ctypes.c_void_p]
return get_pointer(gpointer)
def on_draw(self, widget, cr):
""" Used for black background drawing in the player drawing area. """
cr.set_source_rgb(0, 0, 0)
cr.paint()
def on_mouse_motion(self, widget, event):
display = widget.get_display()
window = widget.get_window()
cursor = Gdk.Cursor.new_from_name(display, "default")
window.set_cursor(cursor)
self.hide_mouse_cursor(window, display)
@run_with_delay(3)
def hide_mouse_cursor(self, window, display):
cursor = Gdk.Cursor.new_for_display(display, Gdk.CursorType.BLANK_CURSOR)
window.set_cursor(cursor)
@staticmethod
def make(name, mode, widget):
""" Factory method. We will not use a separate factory to return a specific implementation.
@@ -190,7 +168,7 @@ class MpvPlayer(Player):
try:
from app.tools import mpv
self._player = mpv.MPV(wid=str(self.get_window_handle()),
self._player = mpv.MPV(wid=str(self._handle),
input_default_bindings=False,
input_cursor=False,
cursor_autohide="no")
@@ -198,9 +176,6 @@ class MpvPlayer(Player):
log(f"{__class__.__name__}: Load library error: {e}")
raise ImportError("No libmpv is found. Check that it is installed!")
else:
self._mode = mode
self._is_playing = False
@self._player.event_callback(mpv.MpvEventID.FILE_LOADED)
def on_open(event):
log("Starting playback...")
@@ -241,8 +216,9 @@ class MpvPlayer(Player):
self._is_playing = True
def stop(self):
self._player.stop()
self._is_playing = True
if self._is_playing:
self._player.stop()
self._is_playing = False
def pause(self):
self._player.pause = not self._player.pause
@@ -252,8 +228,9 @@ class MpvPlayer(Player):
@run_task
def release(self):
self._player.terminate()
self.__INSTANCE = None
if self._player:
self._player.terminate()
self.__INSTANCE = None
def is_playing(self):
return self._is_playing
@@ -290,10 +267,8 @@ class GstPlayer(Player):
self.STATE = Gst.State
self.STAT_RETURN = Gst.StateChangeReturn
self._mode = mode
self._is_playing = False
self._player = Gst.ElementFactory.make("playbin", "player")
self._player.set_window_handle(self.get_window_handle())
self._player.set_window_handle(self._handle)
bus = self._player.get_bus()
bus.add_signal_watch()
@@ -329,8 +304,9 @@ class GstPlayer(Player):
self._is_playing = True
def stop(self):
log("Stop playback...")
self._player.set_state(self.STATE.READY)
if self._is_playing:
log("Stop playback...")
self._player.set_state(self.STATE.READY)
self._is_playing = False
def pause(self):
@@ -345,9 +321,10 @@ class GstPlayer(Player):
@run_task
def release(self):
self._is_playing = False
self._player.set_state(self.STATE.NULL)
self.__INSTANCE = None
if self._player:
self._is_playing = False
self._player.set_state(self.STATE.NULL)
self.__INSTANCE = None
def set_mrl(self, mrl):
self._player.set_property("uri", mrl)
@@ -412,7 +389,7 @@ class VlcPlayer(Player):
from app.tools import vlc
from app.tools.vlc import EventType
args = f"--quiet {'' if IS_DARWIN else '--no-xlib'}"
args = f"--quiet {'--no-xlib' if IS_LINUX else ''}"
self._player = vlc.Instance(args).media_player_new()
vlc.libvlc_video_set_key_input(self._player, False)
vlc.libvlc_video_set_mouse_input(self._player, False)
@@ -420,16 +397,13 @@ class VlcPlayer(Player):
log(f"{__class__.__name__}: Load library error: {e}")
raise ImportError("No VLC is found. Check that it is installed!")
else:
self._mode = mode
self._is_playing = False
ev_mgr = self._player.event_manager()
ev_mgr.event_attach(EventType.MediaPlayerVout, self.on_playback_start)
ev_mgr.event_attach(EventType.MediaPlayerTimeChanged,
lambda et: self.emit("position", self._player.get_time()))
ev_mgr.event_attach(EventType.MediaPlayerEncounteredError, lambda et: self.emit("error", "Can't Playback!"))
self.init_video_widget(widget)
self.init_video_widget()
@classmethod
def get_instance(cls, mode, widget):
@@ -449,7 +423,7 @@ class VlcPlayer(Player):
def stop(self):
if self._is_playing:
self._player.stop()
self._is_playing = False
self._is_playing = False
def pause(self):
self._player.pause()
@@ -492,13 +466,13 @@ class VlcPlayer(Player):
s_desc = self._player.video_get_spu_description()
self.emit("subtitle-track", [(s[0], s[1].decode(encoding="utf-8", errors="ignore")) for s in s_desc])
def init_video_widget(self, widget):
def init_video_widget(self):
if IS_LINUX:
self._player.set_xwindow(self.get_window_handle())
self._player.set_xwindow(self._handle)
elif IS_DARWIN:
self._player.set_nsobject(self.get_window_handle())
self._player.set_nsobject(self._handle)
else:
self._player.set_hwnd(self.get_window_handle())
self._player.set_hwnd(self._handle)
class Recorder:

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
@@ -221,7 +221,8 @@ class PiconsCzDownloader:
"piconblack80": "b50",
"piconblack3d": "b50",
"piconwin11": "win11220",
"piconSNPtransparent": "t50"
"piconSNPtransparent": "t50",
"piconSNPblack": "b50",
}
def get_name_map(self):

View File

@@ -50,7 +50,7 @@ class SatelliteSource(Enum):
FLYSAT = ("https://www.flysat.com/en/satellitelist",)
LYNGSAT = ("https://www.lyngsat.com/asia.html", "https://www.lyngsat.com/europe.html",
"https://www.lyngsat.com/atlantic.html", "https://www.lyngsat.com/america.html")
KINGOFSAT = ("https://en.kingofsat.net/satellites.php",)
KINGOFSAT = ("https://en.kingofsat.tv/satellites.php",)
@staticmethod
def get_sources(src):
@@ -271,7 +271,7 @@ class SatellitesParser(HTMLParser):
trs = []
if self._source is SatelliteSource.KINGOFSAT:
sat_url = "https://en.kingofsat.net/" + sat_url
sat_url = f"https://en.kingofsat.tv/{sat_url}"
try:
request = requests.get(url=sat_url, headers=_HEADERS, timeout=_TIMEOUT)
@@ -443,10 +443,8 @@ class ServicesParser(HTMLParser):
self._POS_PAT = re.compile(r".*?(\d+\.\d°[EW]).*")
# LyngSat.
self._TR_PAT = re.compile((r".*?(\d+)\.?\d?\s+([RLHV]).*(DVB-S[2]?)/?(.*PSK)?\s"
r"?(T2-MI)?\s?(PLS\s+Multistream)?\s?"
r"SR-FEC:\s(\d+)-(\d/\d)\s+.*ONID-TID:\s+(\d+)-(\d+).*"))
self._TR_PAT = re.compile(r".*?(\d{4,5})\.?\d?\s+([RLHV]).*(DVB-S2?X?)/?(.*PSK)?.*SR-FEC:\s(\d+)-(\d+/\d+).*")
self._ID_PAT = re.compile(r"C/N lock:.*?(?:.*ONID-TID:\s+(\d+)-(\d+))?.*")
self._MULTI_PAT = re.compile(r"PLS\s+(Root|Gold|Combo)+\s(\d+)?\s+(?:Stream\s(\d+))")
# KingOfSat.
self._KING_TR_PAT = re.compile((r"(DVB-S[2]?)\s?(?:T2-MI,\s+PLP\s+(\d+))?.*"
@@ -571,7 +569,7 @@ class ServicesParser(HTMLParser):
""" Returns transponder links. """
try:
if self._source is SatelliteSource.KINGOFSAT:
sat_url = "https://en.kingofsat.net/" + sat_url
sat_url = f"https://en.kingofsat.tv/{sat_url}"
self.init_data(sat_url)
except ValueError as e:
log(e)
@@ -586,7 +584,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://{self._lang}.kingofsat.net/{t_cell.url}"
t_cell.url = f"https://{self._lang}.kingofsat.tv/{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
@@ -616,13 +614,12 @@ class ServicesParser(HTMLParser):
services = []
pos, freq, sr, fec, pol, nsp, tid, nid = sat_position or 0, 0, 0, 0, 0, 0, 0, 0
sys = "DVB-S"
pos_found = False
tr = None
pos_found, tr, td, t_id = False, None, None, None
# Multi-stream.
multi_tr = None
multi = False
# Transponder.
for r in filter(lambda x: x and 6 < len(x) < 9, self._rows):
for r in self._rows:
if not pos_found:
pos_tr = re.match(self._POS_PAT, r[0].text)
if not pos_tr:
@@ -632,22 +629,23 @@ class ServicesParser(HTMLParser):
pos = self.get_position(pos_tr.group(1))
pos_found = True
if pos_found:
text = " ".join(c.text for c in r[1:])
td = re.match(self._TR_PAT, text)
if td:
if pos_found and not td:
td = re.match(self._TR_PAT, " ".join(c.text for c in r))
if td and not t_id:
t_id = re.match(self._ID_PAT, " ".join(c.text for c in r))
if t_id:
# The ONID-TID values may not present!
_nid, _tid = t_id.group(1), t_id.group(2)
if _nid and _tid:
nid, tid = int(_nid), int(_tid)
else:
log((f"Values 'ONID-TID' for transponder [{self._t_url}] are not present."
" Default values are used."))
freq, pol = int(td.group(1)), get_key_by_value(POLARIZATION, td.group(2))
sys, mod, sr, _fec, = td.group(3), td.group(4), td.group(7), td.group(8)
nid, tid = td.group(9), td.group(10)
sys, mod, sr, _fec = td.group(3), td.group(4), td.group(5), td.group(6)
sys, mod, fec, nsp, s2_flags, roll_off, pilot, inv = self.get_transponder_data(pos, _fec, sys, mod)
nid, tid = int(nid), int(tid)
if td.group(5):
log(f"Detected T2-MI transponder! [{freq} {sr} {pol}]")
if td.group(6):
log(f"Detected multi-stream transponder! [{freq} {sr} {pol}]")
multi = True
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
@@ -711,6 +709,10 @@ class ServicesParser(HTMLParser):
sr, fec = sr_fec.split()
pol = get_key_by_value(POLARIZATION, pol)
sys, mod, fec, nsp, s2_flags, roll_off, pilot, inv = self.get_transponder_data(pos, fec, sys, mod)
if not all((freq, nid, tid)):
log(f"Error. Not enough parameters [Frequency={freq}, NID={nid}, TID={tid}].")
continue
freq, nid, tid = int(float(freq)), int(nid), int(tid)
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
@@ -765,11 +767,12 @@ class ServicesParser(HTMLParser):
def get_service_data(s_type, pkg, sid, tid, nid, namespace, v_pid, a_pid, cas, use_pids=False):
sid = int(sid)
data_id = f"{sid:04x}:{namespace}:{tid:04x}:{nid:04x}:{s_type}:0:0"
fav_id = f"{sid}:{tid}:{nid}:{namespace}"
fav_id = f"1:0:{int(s_type):X}:{sid}:{tid}:{nid}:{namespace}:0:0:0:"
picon_id = f"1_0_{int(s_type):X}_{sid}_{tid}_{nid}_{namespace}_0_0_0.png"
# Flags.
flags = f"p:{pkg}"
cas = ",".join(get_key_by_value(CAS, c) or "C:0000" for c in cas.split()) if cas else None
cas = ",".join(get_key_by_value(CAS, c) or "" for c in cas.split()) if cas else None
if use_pids:
v_pid = f"c:00{int(v_pid):04x}" if v_pid else None
a_pid = ",".join([f"c:01{int(p):04x}" for p in a_pid]) if a_pid else None

View File

@@ -41,20 +41,21 @@ C{get_instance} method of L{MediaPlayer} and L{MediaListPlayer}.
"""
import ctypes
import functools
import logging
from ctypes.util import find_library
import os
import sys
from ctypes.util import find_library
import functools
# Used by EventManager in override.py
from inspect import getargspec, signature
import inspect as _inspect
import logging
logger = logging.getLogger(__name__)
__version__ = "3.0.12118"
__libvlc_version__ = "3.0.12"
__generator_version__ = "1.18"
build_date = "Tue Apr 20 20:46:07 2021 3.0.12"
__version__ = "3.0.18122"
__libvlc_version__ = "3.0.18"
__generator_version__ = "1.22"
build_date = "Wed Apr 19 17:27:23 2023 3.0.18"
# The libvlc doc states that filenames are expected to be in UTF8, do
# not rely on sys.getfilesystemencoding() which will be confused,
@@ -85,6 +86,13 @@ if sys.version_info[0] > 2:
return b.decode(DEFAULT_ENCODING)
else:
return b
def len_args(func):
"""Return number of positional arguments.
"""
return len(_inspect.signature(func).parameters)
else:
str = str
unicode = unicode
@@ -110,6 +118,12 @@ else:
else:
return b
def len_args(func):
"""Return number of positional arguments.
"""
return len(_inspect.getargspec(func).args)
# Internal guard to prevent internal classes to be directly
# instanciated.
_internal_guard = object()
@@ -1158,6 +1172,29 @@ MediaPlayerRole._None = MediaPlayerRole(0)
# End of generated enum types #
class EventUnion(ctypes.Union):
_fields_ = [
('meta_type', ctypes.c_uint),
('new_child', ctypes.c_uint),
('new_duration', ctypes.c_longlong),
('new_status', ctypes.c_int),
('media', ctypes.c_void_p),
('new_state', ctypes.c_uint),
# FIXME: Media instance
('new_cache', ctypes.c_float),
('new_position', ctypes.c_float),
('new_time', ctypes.c_longlong),
('new_title', ctypes.c_int),
('new_seekable', ctypes.c_longlong),
('new_pausable', ctypes.c_longlong),
('new_scrambled', ctypes.c_longlong),
('new_count', ctypes.c_longlong),
# FIXME: Skipped MediaList and MediaListView...
('filename', ctypes.c_char_p),
('new_length', ctypes.c_longlong),
]
# Generated structs #
class LogMessage(ctypes.Structure):
'''N/A
@@ -1203,13 +1240,11 @@ class Event(ctypes.Structure):
'''A libvlc event.
'''
pass
Event._fields_ = (
('type', ctypes.c_int),
('obj', ctypes.c_void_p),
('meta_type', Meta),
)
_fields_ = [
('type', EventType),
('object', ctypes.c_void_p),
('u', EventUnion),
]
class MediaStats(ctypes.Structure):
@@ -1894,7 +1929,7 @@ class EventManager(_Ctype):
@note: Only a single notification can be registered
for each event type in an EventManager instance.
'''
_callback_handler = None
@@ -1927,7 +1962,7 @@ class EventManager(_Ctype):
if not hasattr(callback, '__call__'): # callable()
raise VLCException("%s required: %r" % ('callable', callback))
# check that the callback expects arguments
if not any(getargspec(callback)[:2]): # list(...)
if len_args(callback) < 1: # list(...)
raise VLCException("%s required: %r" % ('argument', callback))
if self._callback_handler is None:
@@ -1979,7 +2014,7 @@ class Instance(_Ctype):
- a string
- a list of strings as first parameters
- the parameters given as the constructor parameters (must be strings)
'''
def __new__(cls, *args):
@@ -2077,11 +2112,9 @@ class Instance(_Ctype):
"""
# API 3 vs 4: libvlc_media_list_new does not take any
# parameter as input anymore.
if len(signature(libvlc_media_list_new).parameters) == 1:
# API <= 3
if len_args(libvlc_media_list_new) == 1: # API <= 3
l = libvlc_media_list_new(self)
else:
# API >= 4
else: # API >= 4
l = libvlc_media_list_new()
# We should take the lock, but since we did not leak the
# reference, nobody else can access it.
@@ -2592,7 +2625,7 @@ class Instance(_Ctype):
class LogIterator(_Ctype):
'''Create a new VLC log iterator.
'''
def __new__(cls, ptr=_internal_guard):
@@ -2632,7 +2665,7 @@ class Media(_Ctype):
Usage: Media(MRL, *options)
See vlc.Instance.media_new documentation for details.
'''
def __new__(cls, *args):
@@ -3059,7 +3092,7 @@ class MediaList(_Ctype):
Usage: MediaList(list_of_MRLs)
See vlc.Instance.media_list_new documentation for details.
'''
def __new__(cls, *args):
@@ -3197,7 +3230,7 @@ class MediaListPlayer(_Ctype):
It may take as parameter either:
- a vlc.Instance
- nothing
'''
def __new__(cls, arg=None):
@@ -3337,7 +3370,7 @@ class MediaPlayer(_Ctype):
It may take as parameter either:
- a string (media URI), options... In this case, a vlc.Instance will be created.
- a vlc.Instance, a string (media URI), options...
'''
def __new__(cls, *args):
@@ -3413,7 +3446,8 @@ class MediaPlayer(_Ctype):
@version: LibVLC 3.0.0 and later.
'''
chapterDescription_pp = ctypes.POINTER(ChapterDescription)()
n = libvlc_media_player_get_full_chapter_descriptions(self, ctypes.byref(chapterDescription_pp))
n = libvlc_media_player_get_full_chapter_descriptions(self, i_chapters_of_title,
ctypes.byref(chapterDescription_pp))
info = ctypes.cast(chapterDescription_pp, ctypes.POINTER(ctypes.POINTER(ChapterDescription) * n))
try:
contents = info.contents
@@ -3677,12 +3711,12 @@ class MediaPlayer(_Ctype):
If you want to use it along with Qt see the QMacCocoaViewContainer. Then
the following code should work:
@code.mm
NSView *video = [[NSView alloc] init];
QMacCocoaViewContainer *container = new QMacCocoaViewContainer(video, parent);
L{set_nsobject}(mp, video);
[video release];
@endcode
You can find a live example in VLCVideoView in VLCKit.framework.
@param drawable: the drawable that is either an NSView or an object following the VLCOpenGLVideoViewEmbedding protocol.
@@ -4920,6 +4954,19 @@ def libvlc_playlist_play(p_instance, i_id, i_options, ppsz_options):
return f(p_instance, i_id, i_options, ppsz_options)
def libvlc_errmsg():
'''A human-readable error message for the last LibVLC error in the calling
thread. The resulting string is valid until another error occurs (at least
until the next LibVLC call).
@warning
This will be None if there was no error.
'''
f = _Cfunctions.get('libvlc_errmsg', None) or \
_Cfunction('libvlc_errmsg', (), None,
ctypes.c_char_p)
return f()
def libvlc_clearerr():
'''Clears the LibVLC error status for the current thread. This is optional.
By default, the error status is automatically overridden when a new error
@@ -6580,12 +6627,12 @@ def libvlc_media_player_set_nsobject(p_mi, drawable):
If you want to use it along with Qt see the QMacCocoaViewContainer. Then
the following code should work:
@code.mm
NSView *video = [[NSView alloc] init];
QMacCocoaViewContainer *container = new QMacCocoaViewContainer(video, parent);
L{libvlc_media_player_set_nsobject}(mp, video);
[video release];
@endcode
You can find a live example in VLCVideoView in VLCKit.framework.
@param p_mi: the Media Player.
@@ -8651,7 +8698,7 @@ def libvlc_vlm_get_event_manager(p_instance):
# libvlc_printerr
# libvlc_set_exit_handler
# 39 function(s) not wrapped as methods:
# 40 function(s) not wrapped as methods:
# libvlc_audio_equalizer_get_band_count
# libvlc_audio_equalizer_get_band_frequency
# libvlc_audio_equalizer_get_preset_count
@@ -8668,6 +8715,7 @@ def libvlc_vlm_get_event_manager(p_instance):
# libvlc_dialog_post_action
# libvlc_dialog_post_login
# libvlc_dialog_set_context
# libvlc_errmsg
# libvlc_event_type_name
# libvlc_free
# libvlc_get_changeset
@@ -8771,6 +8819,40 @@ def debug_callback(event, *args, **kwds):
print('Debug callback (%s)' % ', '.join(l))
def print_python():
from platform import architecture, machine, mac_ver, uname, win32_ver
if 'intelpython' in sys.executable:
t = 'Intel-'
# elif 'PyPy ' in sys.version:
# t = 'PyPy-'
else:
t = ''
t = '%sPython: %s (%s)' % (t, sys.version.split()[0], architecture()[0])
if win32_ver()[0]:
t = t, 'Windows', win32_ver()[0]
elif mac_ver()[0]:
t = t, ('iOS' if sys.platform == 'ios' else 'macOS'), mac_ver()[0], machine()
else:
try:
import distro # <http://GitHub.com/nir0s/distro>
t = t, bytes_to_str(distro.name()), bytes_to_str(distro.version())
except ImportError:
t = (t,) + uname()[0:3:2]
print(' '.join(t))
def print_version():
"""Print version of this vlc.py and of the libvlc"""
try:
print('%s: %s (%s)' % (os.path.basename(__file__), __version__, build_date))
print('libVLC: %s (%#x)' % (bytes_to_str(libvlc_get_version()), libvlc_hex_version()))
# print('libVLC %s' % bytes_to_str(libvlc_get_compiler()))
if plugin_path:
print('plugins: %s' % plugin_path)
except Exception:
print('Error: %s' % sys.exc_info()[1])
if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG)
try:
@@ -8807,40 +8889,6 @@ if __name__ == '__main__':
sys.stdout.flush()
def print_python():
from platform import architecture, mac_ver, uname, win32_ver
if 'intelpython' in sys.executable:
t = 'Intel-'
# elif 'PyPy ' in sys.version:
# t = 'PyPy-'
else:
t = ''
t = '%sPython: %s (%s)' % (t, sys.version.split()[0], architecture()[0])
if win32_ver()[0]:
t = t, 'Windows', win32_ver()[0]
elif mac_ver()[0]:
t = t, ('iOS' if sys.platform == 'ios' else 'macOS'), mac_ver()[0]
else:
try:
import distro # <http://GitHub.com/nir0s/distro>
t = t, bytes_to_str(distro.name()), bytes_to_str(distro.version())
except ImportError:
t = (t,) + uname()[0:3:2]
print(' '.join(t))
def print_version():
"""Print version of this vlc.py and of the libvlc"""
try:
print('%s: %s (%s)' % (os.path.basename(__file__), __version__, build_date))
print('LibVLC version: %s (%#x)' % (bytes_to_str(libvlc_get_version()), libvlc_hex_version()))
print('LibVLC compiler: %s' % bytes_to_str(libvlc_get_compiler()))
if plugin_path:
print('Plugin path: %s' % plugin_path)
except Exception:
print('Error: %s' % sys.exc_info()[1])
if '-h' in sys.argv[:2] or '--help' in sys.argv[:2]:
print('Usage: %s [options] <movie_filename>' % sys.argv[0])
print('Once launched, type ? for help.')
@@ -8874,7 +8922,8 @@ if __name__ == '__main__':
# Instance() call above, see <http://www.videolan.org/doc/play-howto/en/ch04.html>
player.video_set_marquee_int(VideoMarqueeOption.Enable, 1)
player.video_set_marquee_int(VideoMarqueeOption.Size, 24) # pixels
player.video_set_marquee_int(VideoMarqueeOption.Position, Position.Bottom)
# FIXME: This crashes the module - it should be investigated
# player.video_set_marquee_int(VideoMarqueeOption.Position, Position.bottom)
if False: # only one marquee can be specified
player.video_set_marquee_int(VideoMarqueeOption.Timeout, 5000) # millisec, 0==forever
t = media.get_mrl() # movie

View File

@@ -111,7 +111,7 @@ class YouTube:
if not self._yt_dl:
self._yt_dl = YouTubeDL.get_instance(self._settings, self._callback)
if not self._yt_dl:
raise YouTubeException("youtube-dl initialization error.")
raise YouTubeException("yt-dlp initialization error.")
return self._yt_dl.get_yt_link(url, skip_errors)
return self.get_yt_link_by_id(video_id)
@@ -148,7 +148,7 @@ class YouTube:
if self._settings.enable_yt_dl and url:
try:
if not self._yt_dl:
raise YouTubeException("youtube-dl is not initialized!")
raise YouTubeException("yt-dlp is not initialized!")
self._yt_dl.update_options({"noplaylist": False, "extract_flat": True})
info = self._yt_dl.get_info(url, skip_errors=False)
@@ -172,17 +172,22 @@ class InnerTube:
_BASE_URI = "https://www.youtube.com/youtubei/v1"
_DEFAULT_CLIENTS = {
"ANDROID": {
"context": {"client": {"clientName": "ANDROID", "clientVersion": "16.20"}},
"api_key": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
},
"ANDROID_EMBED": {
"context": {"client": {"clientName": "ANDROID", "clientVersion": "16.20", "clientScreen": "EMBED"}},
"api_key": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
}
"WEB_EMBED": {"context": {"client": {"clientName": "WEB_EMBEDDED_PLAYER",
"clientVersion": "2.20210721.00.00",
"clientScreen": "EMBED"}},
"header": {"User-Agent": "Mozilla/5.0"},
"api_key": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"},
"ANDROID_EMBED": {"context": {"client": {"clientName": "ANDROID_EMBEDDED_PLAYER",
"clientVersion": "17.31.35",
"clientScreen": "EMBED",
"androidSdkVersion": 30}},
"header": {"User-Agent": "com.google.android.youtube/"},
"api_key": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"}
}
def __init__(self, client="ANDROID"):
def __init__(self, client="ANDROID_EMBED"):
""" Initialize an InnerTube object.
@param client: Client to use for the object. Default to web because it returns the most playback types.
@@ -302,14 +307,14 @@ class PlayListParser(HTMLParser):
class YouTubeDL:
""" Utility class [experimental] for working with youtube-dl.
""" Utility class [experimental] for working with yt-dlp.
[https://github.com/ytdl-org/youtube-dl]
[https://github.com/yt-dlp/yt-dlp]
"""
_DL_INSTANCE = None
_DownloadError = None
_LATEST_RELEASE_URL = "https://api.github.com/repos/ytdl-org/youtube-dl/releases/latest"
_LATEST_RELEASE_URL = "https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest"
_OPTIONS = {"noplaylist": True, # Single video instead of a playlist [ignoring playlist in URL].
"extract_flat": False, # Do not resolve URLs, return the immediate result.
"quiet": True, # Do not print messages to stdout.
@@ -334,7 +339,7 @@ class YouTubeDL:
return cls._DL_INSTANCE
def init(self):
if not os.path.isfile(f"{self._path}youtube_dl{SEP}version.py"):
if not os.path.isfile(f"{self._path}yt_dlp{SEP}version.py"):
self.get_latest_release()
if self._path not in sys.path:
@@ -344,39 +349,39 @@ class YouTubeDL:
def init_dl(self):
try:
import youtube_dl
import yt_dlp
except ModuleNotFoundError as e:
log(f"YouTubeDLHelper error: {e}")
raise YouTubeException(e)
except ImportError as e:
log(f"YouTubeDLHelper error: {e}")
else:
if self._path not in youtube_dl.__file__:
msg = "Another version of youtube-dl was found on your system!"
if self._path not in yt_dlp.__file__:
msg = "Another version of yt-dlp was found on your system!"
log(msg)
raise YouTubeException(msg)
if self._update:
if hasattr(youtube_dl.version, "__version__"):
if hasattr(yt_dlp.version, "__version__"):
l_ver = self.get_last_release_id()
cur_ver = youtube_dl.version.__version__
if l_ver and youtube_dl.version.__version__ < l_ver:
msg = f"youtube-dl has new release!\nCurrent: {cur_ver}. Last: {l_ver}."
cur_ver = yt_dlp.version.__version__
if l_ver and yt_dlp.version.__version__ < l_ver:
msg = f"yt-dlp has new release!\nCurrent: {cur_ver}. Last: {l_ver}."
show_notification(msg)
log(msg)
self._callback(msg, False)
self.get_latest_release()
self._DownloadError = youtube_dl.utils.DownloadError
self._dl = youtube_dl.YoutubeDL(self._OPTIONS)
msg = "youtube-dl initialized..."
self._DownloadError = yt_dlp.utils.DownloadError
self._dl = yt_dlp.YoutubeDL(self._OPTIONS)
msg = "yt-dlp initialized..."
show_notification(msg)
log(msg)
@staticmethod
def get_last_release_id():
""" Getting last release id. """
url = "https://api.github.com/repos/ytdl-org/youtube-dl/releases/latest"
url = "https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest"
try:
with urlopen(url, timeout=10) as resp:
return json.loads(resp.read().decode("utf-8")).get("tag_name", "0")
@@ -386,7 +391,7 @@ class YouTubeDL:
def get_latest_release(self):
try:
self._is_update_process = True
log("Getting the last youtube-dl release...")
log("Getting the last yt-dlp release...")
with urlopen(YouTubeDL._LATEST_RELEASE_URL, timeout=10) as resp:
r = json.loads(resp.read().decode("utf-8"))
@@ -395,7 +400,7 @@ class YouTubeDL:
if os.path.isdir(self._path):
shutil.rmtree(self._path)
zip_file = self._path + "yt.zip"
zip_file = f"{self._path}yt.zip"
os.makedirs(os.path.dirname(self._path), exist_ok=True)
f_name, headers = urlretrieve(zip_url, filename=zip_file)
@@ -403,12 +408,12 @@ class YouTubeDL:
with zipfile.ZipFile(f_name) as arch:
for info in arch.infolist():
pref, sep, f = info.filename.partition("/youtube_dl/")
pref, sep, f = info.filename.partition("/yt_dlp/")
if sep:
arch.extract(info.filename)
shutil.move(info.filename, f"{self._path}{sep}{f}")
shutil.rmtree(pref)
msg = "Getting the last youtube-dl release is done!"
msg = "Getting the last yt-dlp release is done!"
show_notification(msg)
log(msg)
self._callback(msg, False)

View File

@@ -172,6 +172,11 @@
<attribute name="label" translatable="yes">Backups</attribute>
<attribute name="action">app.on_backup_tool_show</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Boot Logo</attribute>
<attribute name="action">app.on_boot_logo_tool_show</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Telnet</attribute>
<attribute name="action">app.on_telnet_show</attribute>
@@ -393,6 +398,11 @@
<attribute name="label" translatable="yes">Backups</attribute>
<attribute name="action">app.on_backup_tool_show</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Boot Logo</attribute>
<attribute name="action">app.on_boot_logo_tool_show</attribute>
<attribute name="hidden-when">action-disabled</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Telnet</attribute>
<attribute name="action">app.on_telnet_show</attribute>
@@ -424,14 +434,6 @@
<attribute name="label" translatable="yes">Import YouTube playlist</attribute>
<attribute name="action">app.on_import_yt_list</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Import m3u</attribute>
<attribute name="action">app.on_import_m3u</attribute>
</item>
<item>
<attribute name="label" translatable="yes">Export to m3u</attribute>
<attribute name="action">app.on_export_iptv_to_m3u</attribute>
</item>
<section>
<item>
<attribute name="label" translatable="yes">EPG configuration</attribute>

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2023 Dmitriy Yefremov
# Copyright (c) 2018-2024 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,9 +38,15 @@ from pathlib import Path
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 app.ui.main_helper import append_text_to_tview, show_info_bar_message
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, HeaderBar
KEEP_DATA = {"satellites.xml",
"terrestrial.xml",
"cables.xml",
"whitelist",
"whitelist_streamrelay"}
class RestoreType(Enum):
BOUQUETS = 0
@@ -71,7 +77,6 @@ class BackupDialog:
self._model = builder.get_object("main_list_store")
self._main_view = builder.get_object("main_view")
self._text_view = builder.get_object("text_view")
self._text_view_scrolled_window = builder.get_object("text_view_scrolled_window")
self._info_check_button = builder.get_object("info_check_button")
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("message_label")
@@ -149,16 +154,12 @@ class BackupDialog:
menu.popup(None, None, None, None, event.button, event.time)
def on_info_button_toggled(self, button):
active = button.get_active()
self._text_view_scrolled_window.set_visible(active)
if active:
if button.get_active():
self.on_cursor_changed(self._main_view)
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(text)
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@@ -196,7 +197,7 @@ class BackupDialog:
return
file_name = model.get_value(model.get_iter(paths[0]), 0)
full_file_name = self._backup_path + file_name + ".zip"
full_file_name = f"{self._backup_path}{file_name}.zip"
try:
if restore_type is RestoreType.ALL:
@@ -237,18 +238,19 @@ class BackupDialog:
self.restore(RestoreType.BOUQUETS)
def backup_data(path, backup_path, move=True):
def backup_data(path, backup_path, move=True, keep=None):
""" Creating data backup from a folder at the specified path
Returns full path to the compressed file.
"""
keep = keep or KEEP_DATA
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 *.xml).
for file in filter(lambda f: not f.endswith(".xml") and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
# Backup files in data dir.
for file in filter(lambda f: 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)
shutil.move(src, dst) if move and file not in keep else shutil.copy(src, dst)
# Compressing to zip and delete remaining files.
zip_file = shutil.make_archive(backup_path.rstrip(SEP), "zip", backup_path)
shutil.rmtree(backup_path)
@@ -264,7 +266,7 @@ def restore_data(src, dst):
def clear_data_path(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)):
for file in filter(lambda f: f not in KEEP_DATA and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
os.remove(os.path.join(path, file))

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1
<!-- Generated with glade 3.38.2
The MIT License (MIT)
Copyright (c) 2018-2020 Dmitriy Yefremov
Copyright (c) 2018-2024 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,18 +27,13 @@ Author: Dmitriy Yefremov
-->
<interface>
<requires lib="gtk+" version="3.16"/>
<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-2020 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2024 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkImage" id="details_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">emblem-important-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkListStore" id="main_list_store">
<columns>
<!-- column-name name -->
@@ -47,106 +42,51 @@ Author: Dmitriy Yefremov
<column type="gchararray"/>
</columns>
</object>
<object class="GtkMenu" id="popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="restore_bouquets_popup_menu_item">
<property name="label" translatable="yes">Restore bouquets</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_restore_bouquets" swapped="no"/>
<accelerator key="r" signal="activate" modifiers="Primary"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="restore_all_popup_menu_item">
<property name="label" translatable="yes">Restore all</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_restore_all" swapped="no"/>
<accelerator key="e" signal="activate" modifiers="Primary"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="popup_menu_separator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="remove_popup_menu_item">
<property name="label">gtk-remove</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<signal name="activate" handler="on_remove" swapped="no"/>
<accelerator key="Delete" signal="activate"/>
</object>
</child>
</object>
<object class="GtkImage" id="remove_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">user-trash-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="restore_all_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-select-all-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="restore_bouquets_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-revert-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkWindow" id="dialog_window">
<property name="width_request">560</property>
<property name="height_request">320</property>
<property name="can_focus">False</property>
<property name="width-request">560</property>
<property name="height-request">320</property>
<property name="can-focus">False</property>
<property name="title" translatable="yes">Backups</property>
<property name="modal">True</property>
<property name="window_position">center-on-parent</property>
<property name="destroy_with_parent">True</property>
<property name="icon_name">document-revert</property>
<property name="window-position">center-on-parent</property>
<property name="destroy-with-parent">True</property>
<property name="icon-name">document-revert-symbolic</property>
<signal name="check-resize" handler="on_resize" swapped="no"/>
<child>
<placeholder/>
</child>
<child>
<object class="GtkBox" id="main_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="header_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<child>
<object class="GtkButtonBox" id="main_button_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="valign">center</property>
<property name="margin_left">15</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="layout_style">expand</property>
<property name="margin-start">15</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="layout-style">expand</property>
<child>
<object class="GtkButton" id="restore_bouquets_header_button">
<property name="label" translatable="yes">Restore bouquets</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Restore bouquets</property>
<property name="valign">center</property>
<property name="image">restore_bouquets_image</property>
<property name="always_show_image">True</property>
<property name="always-show-image">True</property>
<signal name="clicked" handler="on_restore_bouquets" swapped="no"/>
<child>
<object class="GtkImage" id="restore_bouquets_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">document-revert-symbolic</property>
<property name="icon_size">1</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -156,14 +96,21 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkButton" id="restore_all_header_button">
<property name="label" translatable="yes">Restore all</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Restore all</property>
<property name="valign">center</property>
<property name="image">restore_all_image</property>
<property name="always_show_image">True</property>
<property name="always-show-image">True</property>
<signal name="clicked" handler="on_restore_all" swapped="no"/>
<child>
<object class="GtkImage" id="restore_all_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">edit-select-all-symbolic</property>
<property name="icon_size">1</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -173,14 +120,21 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkButton" id="remove_header_button">
<property name="label" translatable="yes">Remove</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Remove</property>
<property name="valign">center</property>
<property name="image">remove_image</property>
<property name="always_show_image">True</property>
<property name="always-show-image">True</property>
<signal name="clicked" handler="on_remove" swapped="no"/>
<child>
<object class="GtkImage" id="remove_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">user-trash-symbolic</property>
<property name="icon_size">1</property>
</object>
</child>
<accelerator key="Delete" signal="clicked"/>
</object>
<packing>
@@ -199,19 +153,19 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkCheckButton" id="info_check_button">
<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="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="always_show_image">True</property>
<property name="draw_indicator">False</property>
<property name="margin-end">15</property>
<property name="always-show-image">True</property>
<property name="draw-indicator">False</property>
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
<child>
<object class="GtkImage" id="details_image1">
<object class="GtkImage" id="details_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">emblem-important-symbolic</property>
<property name="can-focus">False</property>
<property name="icon-name">emblem-important-symbolic</property>
<property name="icon_size">1</property>
</object>
</child>
@@ -220,7 +174,7 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
@@ -237,121 +191,130 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkFrame" id="main_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="label-xalign">0</property>
<property name="shadow-type">none</property>
<child>
<object class="GtkPaned" id="main_paned">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="wide_handle">True</property>
<property name="can-focus">True</property>
<property name="wide-handle">True</property>
<child>
<object class="GtkBox" id="backups_box">
<object class="GtkViewport" id="backups_viewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<property name="can-focus">False</property>
<child>
<object class="GtkScrolledWindow" id="main_view_scrolled_window">
<object class="GtkBox" id="backups_box">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkTreeView" id="main_view">
<object class="GtkScrolledWindow" id="main_view_scrolled_window">
<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>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<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 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="backup_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>
<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="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="file_count_label">
<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="label" translatable="yes">0</property>
<property name="width_chars">4</property>
<property name="xalign">0</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="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>
@@ -360,12 +323,10 @@ Author: Dmitriy Yefremov
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="view"/>
</style>
</object>
<packing>
<property name="resize">True</property>
@@ -373,22 +334,36 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="text_view_scrolled_window">
<property name="can_focus">False</property>
<property name="shadow_type">in</property>
<object class="GtkViewport" id="text_viewport">
<property name="visible" bind-source="info_check_button" bind-property="active">False</property>
<property name="can-focus">False</property>
<child>
<object class="GtkTextView" id="text_view">
<object class="GtkScrolledWindow" id="text_view_scrolled_window">
<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="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</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>
</child>
<style>
<class name="view"/>
</style>
</object>
<packing>
<property name="resize">True</property>
@@ -409,14 +384,14 @@ Author: Dmitriy Yefremov
</child>
<child>
<object class="GtkInfoBar" id="info_bar">
<property name="can_focus">False</property>
<property name="show_close_button">True</property>
<property name="can-focus">False</property>
<property name="show-close-button">True</property>
<signal name="response" handler="on_info_bar_close" swapped="no"/>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="spacing">6</property>
<property name="layout_style">end</property>
<property name="layout-style">end</property>
</object>
<packing>
<property name="expand">False</property>
@@ -426,13 +401,13 @@ Author: Dmitriy Yefremov
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="spacing">16</property>
<child>
<object class="GtkLabel" id="message_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="label" translatable="yes">message</property>
</object>
<packing>
@@ -458,4 +433,41 @@ Author: Dmitriy Yefremov
</object>
</child>
</object>
<object class="GtkMenu" id="popup_menu">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkMenuItem" id="restore_bouquets_popup_menu_item">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Restore bouquets</property>
<signal name="activate" handler="on_restore_bouquets" swapped="no"/>
<accelerator key="r" signal="activate" modifiers="Primary"/>
</object>
</child>
<child>
<object class="GtkMenuItem" id="restore_all_popup_menu_item">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Restore all</property>
<signal name="activate" handler="on_restore_all" swapped="no"/>
<accelerator key="e" signal="activate" modifiers="Primary"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="popup_menu_separator">
<property name="visible">True</property>
<property name="can-focus">False</property>
</object>
</child>
<child>
<object class="GtkMenuItem" id="remove_popup_menu_item">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Remove</property>
<signal name="activate" handler="on_remove" swapped="no"/>
<accelerator key="Delete" signal="activate"/>
</object>
</child>
</object>
</interface>

324
app/ui/bootlogo.py Normal file
View File

@@ -0,0 +1,324 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2024 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
import os
import subprocess
import sys
from datetime import datetime
from ftplib import all_errors
from pathlib import Path
from app.commons import log, run_task
from app.connections import UtfFTP
from app.settings import IS_DARWIN
from app.ui.dialogs import translate, get_chooser_dialog, show_dialog, DialogType
from app.ui.main_helper import get_picon_pixbuf, redraw_image
from app.ui.uicommons import HeaderBar
from .uicommons import Gtk, GLib
_OUTPUT_FILES = ("bootlogo",
"bootlogo_wait",
"backdrop",
"reboot",
"shutdown",
"radio")
_E2_STB_PATHS = ("/usr/share", "/usr/share/enigma2")
class BootLogoManager(Gtk.Window):
def __init__(self, app, **kwargs):
super().__init__(title=translate("Boot Logo"), icon_name="demon-editor", application=app,
transient_for=app.app_window, destroy_with_parent=True,
window_position=Gtk.WindowPosition.CENTER_ON_PARENT,
default_width=560, default_height=320, modal=False, **kwargs)
self._app = app
self._exe = f"{'./' if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS') else ''}ffmpeg"
self._pix = None
self._img_path = None
margin = {"margin_start": 5, "margin_end": 5, "margin_top": 5, "margin_bottom": 5}
base_margin = {"margin_start": 10, "margin_end": 10, "margin_top": 10, "margin_bottom": 10}
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
frame = Gtk.Frame(shadow_type=Gtk.ShadowType.IN, **base_margin)
frame.get_style_context().add_class("view")
data_box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.VERTICAL, **base_margin)
data_box.set_margin_bottom(margin.get("margin_bottom", 5))
data_box.set_margin_start(10)
frame.add(data_box)
self._image_area = Gtk.DrawingArea()
self._image_area.connect("draw", self.on_image_draw)
data_box.pack_end(self._image_area, True, True, 5)
self.add(main_box)
# Buttons
add_button = Gtk.Button.new_from_icon_name("insert-image-symbolic", Gtk.IconSize.BUTTON)
add_button.set_tooltip_text(translate("Add image"))
add_button.set_always_show_image(True)
add_button.connect("clicked", self.on_add_image)
receive_button = Gtk.Button.new_from_icon_name("network-receive-symbolic", Gtk.IconSize.BUTTON)
receive_button.set_tooltip_text(translate("Download from the receiver"))
receive_button.set_always_show_image(True)
receive_button.connect("clicked", self.on_receive)
transmit_button = Gtk.Button.new_from_icon_name("network-transmit-symbolic", Gtk.IconSize.BUTTON)
transmit_button.set_tooltip_text(translate("Transfer to receiver"))
transmit_button.set_sensitive(False)
transmit_button.set_always_show_image(True)
transmit_button.connect("clicked", self.on_transmit)
self._convert_button = Gtk.Button.new_from_icon_name("object-rotate-right-symbolic", Gtk.IconSize.BUTTON)
self._convert_button.set_tooltip_text(translate("Convert"))
self._convert_button.set_always_show_image(True)
self._convert_button.set_sensitive(False)
self._convert_button.connect("clicked", self.on_convert)
self._convert_button.bind_property("sensitive", transmit_button, "sensitive", 4)
settings_close_button = Gtk.ModelButton(label=translate("Close"), centered=True, margin_top=5)
# Formats.
self._format_button = Gtk.ComboBoxText()
self._format_button.set_tooltip_text(translate("TV Format"))
self._format_button.append("hd720", "HD-Ready (720)")
self._format_button.append("hd1080", "Full HD (1080)")
self._format_button.set_active_id("hd720")
action_box = Gtk.ButtonBox()
action_box.set_layout(Gtk.ButtonBoxStyle.EXPAND)
action_box.add(add_button)
action_box.add(self._convert_button)
action_box.add(self._format_button)
data_box.pack_start(action_box, False, False, 0)
# Settings.
popover = Gtk.Popover()
settings_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5, **base_margin)
file_name_box = Gtk.Box(spacing=5)
file_name_box.add(Gtk.Label(f"{translate('File')}:"))
self._file_combo_box = Gtk.ComboBoxText()
[self._file_combo_box.append(f"{f}.mvi", f) for f in _OUTPUT_FILES]
self._file_combo_box.set_active(0)
file_name_box.pack_start(self._file_combo_box, True, True, 0)
settings_box.add(file_name_box)
paths_box = Gtk.Box(spacing=5)
paths_box.add(Gtk.Label(translate("STB path:")))
self._path_combo_box = Gtk.ComboBoxText()
[self._path_combo_box.append(p, p) for p in _E2_STB_PATHS]
self._path_combo_box.set_active_id(_E2_STB_PATHS[0])
paths_box.pack_start(self._path_combo_box, True, True, 0)
settings_box.add(paths_box)
settings_box.pack_end(settings_close_button, False, False, 0)
settings_box.show_all()
popover.add(settings_box)
settings_button = Gtk.MenuButton(popover=popover, valign=Gtk.Align.CENTER, tooltip_text=translate("Options"))
settings_button.add(Gtk.Image.new_from_icon_name("applications-system-symbolic", Gtk.IconSize.BUTTON))
# Header and toolbar.
if app.app_settings.use_header_bar:
header = HeaderBar(title=translate("Boot Logo"))
header.pack_start(receive_button)
header.pack_start(transmit_button)
header.pack_end(settings_button)
self.set_titlebar(header)
header.show_all()
else:
toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
toolbar.get_style_context().add_class("primary-toolbar")
margin["margin_start"] = 15
margin["margin_top"] = 5
button_box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.HORIZONTAL, **margin)
button_box.pack_start(receive_button, False, False, 0)
button_box.pack_start(transmit_button, False, False, 0)
toolbar.pack_start(button_box, True, True, 0)
toolbar.pack_end(settings_button, False, False, 0)
main_box.pack_start(toolbar, False, False, 0)
settings_button.set_margin_end(15)
main_box.pack_start(frame, True, True, 0)
main_box.show_all()
ws_property = "boot_logo_manager_window_size"
window_size = self._app.app_settings.get(ws_property, None)
if window_size:
self.resize(*window_size)
self.connect("delete-event", lambda w, e: self._app.app_settings.add(ws_property, w.get_size()))
self.connect("realize", self.init)
def init(self, *args):
log(f"{self.__class__.__name__} [init] Checking FFmpeg...")
try:
out = subprocess.check_output([self._exe, "-version"], stderr=subprocess.STDOUT)
except FileNotFoundError as e:
msg = translate("Check if FFmpeg is installed!")
self._app.show_error_message(f"Error. {e} {msg}")
log(e)
else:
lines = out.decode(errors="ignore").splitlines()
log(lines[0] if lines else lines)
def on_add_image(self, button):
file_filter = None
if IS_DARWIN:
file_filter = Gtk.FileFilter()
file_filter.set_name("*.jpg, *.jpeg, *.png")
file_filter.add_mime_type("image/jpeg")
file_filter.add_mime_type("image/png")
response = get_chooser_dialog(self._app.app_window, self._app.app_settings, "*.jpg, *.jpeg, *.png files",
("*.jpg", "*.jpeg", "*.png"), "Select image", file_filter)
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
self._img_path = response
self._pix = get_picon_pixbuf(response, -1)
self._convert_button.set_sensitive(True)
self._image_area.queue_draw()
def on_receive(self, button):
self.download_data(self._file_combo_box.get_active_id())
def on_transmit(self, button):
if show_dialog(DialogType.QUESTION, self) != Gtk.ResponseType.OK:
return True
mvi_file = Path(self._img_path).parent.joinpath(self._file_combo_box.get_active_id())
if not mvi_file.is_file():
log(self._app.show_error_message(translate("No *.mvi file found for the selected image!")))
return
self.transfer_data(mvi_file)
def on_convert(self, button):
self.convert_to_mvi()
def convert_to_mvi(self, frame_rate=25, bit_rate=2000):
path = Path(self._img_path)
if not path.is_file():
self._app.show_error_message(translate("No image selected!"))
return
output = path.parent.joinpath(self._file_combo_box.get_active_id())
ffmpeg_output = path.parent.joinpath(f"{self._file_combo_box.get_active_text()}.m1v")
cmd = [self._exe,
"-i", self._img_path,
"-r", str(frame_rate),
"-b", str(bit_rate),
"-s", self._format_button.get_active_id(),
ffmpeg_output]
try:
from PIL import Image
except ImportError as e:
self._app.show_error_message(f"{translate('Conversion error.')} {e}")
else:
with Image.open(self._img_path) as img:
width, height = img.size
if width != 1280 and height != 720:
log(f"{self.__class__.__name__} [convert] Resizing image...")
img.resize((1280, 720), Image.Resampling.LANCZOS)
tmp = path.parent.joinpath(f"{path.name}.tmp{path.suffix}").absolute()
cmd[2] = tmp
img.save(tmp)
# Processing image.
log(f"{self.__class__.__name__} [convert] Converting...")
subprocess.run(cmd)
if Path(ffmpeg_output).exists():
os.rename(ffmpeg_output, output)
log(f"{self.__class__.__name__} [convert] -> '{output}'. Done!")
if cmd[2] != self._img_path:
tmp_path = Path(cmd[2])
if tmp_path.exists():
tmp_path.unlink()
self._convert_button.set_sensitive(False)
def convert_to_image(self, video_path, img_path):
cmd = [self._exe, "-y", "-i", video_path, img_path]
subprocess.run(cmd)
@run_task
def download_data(self, f_name):
try:
settings = self._app.app_settings
with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
ftp.encoding = "utf-8"
ftp.cwd(self._path_combo_box.get_active_id())
dest = Path(settings.profile_data_path).joinpath("bootlogo")
dest.mkdir(parents=True, exist_ok=True)
path = f"{dest}{os.sep}"
ftp.download_file(f_name, path)
vp = Path(f"{path}{f_name}")
img_path = f"{path}{f_name}.jpg"
if vp.exists():
rn_path = f"{path}{self._file_combo_box.get_active_text()}.m1v"
vp.rename(rn_path)
self.convert_to_image(rn_path, img_path)
self._pix = get_picon_pixbuf(img_path, -1)
GLib.idle_add(self._image_area.queue_draw)
except all_errors as e:
log(f"{self.__class__.__name__} [download error] {e}")
GLib.idle_add(self._app.show_error_message, f"{translate('Failed to download data:')} {e}")
@run_task
def transfer_data(self, f_path):
try:
settings = self._app.app_settings
with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
ftp.encoding = "utf-8"
ftp.cwd(self._path_combo_box.get_active_id())
log(f"{self.__class__.__name__} [transfer data] Creating backup...")
backup_path = Path(settings.profile_backup_path).joinpath("bootlogo")
backup_path.mkdir(parents=True, exist_ok=True)
ftp.download_file(f_path.name, f"{backup_path}{os.sep}")
backup_file = backup_path.joinpath(f_path.name)
if backup_file.exists():
target = backup_path.joinpath(f"{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}_{f_path.name}")
backup_file.rename(target)
ftp.send_file(f_path.name, f"{f_path.parent}{os.sep}")
except all_errors as e:
log(f"{self.__class__.__name__} [upload error] {e}")
GLib.idle_add(self._app.show_error_message, f"{translate('Data transfer error:')} {e}")
else:
self._app.show_info_message("Done!")
def on_image_draw(self, area, cr):
if self._pix:
redraw_image(area, cr, self._pix)
if __name__ == "__main__":
pass

View File

@@ -336,10 +336,10 @@ Author: Dmitriy Yefremov
<object class="GtkBox" id="network_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">5</property>
<property name="margin-bottom">5</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="network_scrolled_window">
@@ -422,6 +422,9 @@ Author: Dmitriy Yefremov
<child type="label_item">
<placeholder/>
</child>
<style>
<class name="view"/>
</style>
</object>
<packing>
<property name="expand">False</property>
@@ -439,8 +442,8 @@ Author: Dmitriy Yefremov
<object class="GtkBox" id="info_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-start">5</property>
<property name="margin-end">5</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="orientation">vertical</property>
@@ -448,10 +451,10 @@ Author: Dmitriy Yefremov
<object class="GtkViewport" id="screenshot_view_port">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-left">2</property>
<property name="margin-right">2</property>
<property name="margin-top">2</property>
<property name="margin-bottom">2</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<child>
<object class="GtkDrawingArea" id="screenshot_area">
<property name="can-focus">False</property>
@@ -469,8 +472,8 @@ Author: Dmitriy Yefremov
<object class="GtkBox" id="remote_signal_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-left">25</property>
<property name="margin-right">25</property>
<property name="margin-start">25</property>
<property name="margin-end">25</property>
<property name="margin-top">10</property>
<property name="margin-bottom">5</property>
<property name="orientation">vertical</property>
@@ -620,6 +623,9 @@ Author: Dmitriy Yefremov
<child type="label_item">
<placeholder/>
</child>
<style>
<class name="view"/>
</style>
</object>
<packing>
<property name="expand">True</property>
@@ -652,8 +658,8 @@ Author: Dmitriy Yefremov
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin-left">5</property>
<property name="margin-right">5</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="row-spacing">5</property>
@@ -1336,6 +1342,9 @@ audio-volume-medium-symbolic</property>
<child type="label_item">
<placeholder/>
</child>
<style>
<class name="view"/>
</style>
</object>
<packing>
<property name="expand">False</property>

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2023 Dmitriy Yefremov
# Copyright (c) 2018-2024 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,8 +32,9 @@ import re
from gi.repository import GLib
from .dialogs import get_builder, get_message
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH
from .main_helper import redraw_image
from .dialogs import get_builder, translate
from .uicommons import Gtk, UI_RESOURCES_PATH
from ..commons import run_task, run_with_delay, log, run_idle
from ..connections import HttpAPI
from ..settings import IS_DARWIN, IS_LINUX, IS_WIN
@@ -41,8 +42,8 @@ from ..settings import IS_DARWIN, IS_LINUX, IS_WIN
class ControlTool(Gtk.Box):
def __init__(self, app, settings, *args, **kwargs):
super().__init__(*args, **kwargs)
def __init__(self, app, settings, **kwargs):
super().__init__(**kwargs)
self._settings = settings
self._app = app
@@ -201,11 +202,7 @@ class ControlTool(Gtk.Box):
def on_screenshot_draw(self, area, cr):
""" Called to automatically resize the screenshot. """
if self._pix:
cr.scale(area.get_allocated_width() / self._pix.get_width(),
area.get_allocated_height() / self._pix.get_height())
img_surface = Gdk.cairo_surface_create_from_pixbuf(self._pix, 1, None)
cr.set_source_surface(img_surface, 0, 0)
cr.paint()
redraw_image(area, cr, self._pix)
def on_screenshot_all(self, action, value=None):
if self._app.http_api:
@@ -335,7 +332,7 @@ class ControlTool(Gtk.Box):
except OSError as e:
log(e)
else:
state = get_message("On" if resp.get("e2instandby", "N/A").strip() == "false" else "Standby")
state = translate("On" if resp.get("e2instandby", "N/A").strip() == "false" else "Standby")
GLib.idle_add(self._network_model.set_value, itr, 2, state)

View File

@@ -3,7 +3,7 @@
The MIT License (MIT)
Copyright (c) 2018-2023 Dmitriy Yefremov
Copyright (c) 2018-2024 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-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor. -->
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
<!-- interface-copyright 2018-2024 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">3.6.1 Beta</property>
<property name="copyright">2018-2023 Dmitriy Yefremov
<property name="version">3.11.1 Beta</property>
<property name="copyright">2018-2024 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-2023 Dmitriy Yefremov
# Copyright (c) 2018-2024 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,16 +38,35 @@ from app.settings import SEP, IS_WIN, USE_HEADER_BAR
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN
class BaseDialog(Gtk.Dialog):
""" Base dialog class for editing DVB (-> *.xml) data. """
DEFAULT_BUTTONS = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK)
def __init__(self, parent, title, buttons=None, *args, **kwargs):
super().__init__(transient_for=parent,
title=translate(title),
modal=True,
resizable=False,
default_width=255,
skip_taskbar_hint=True,
skip_pager_hint=True,
destroy_with_parent=True,
use_header_bar=USE_HEADER_BAR,
window_position=Gtk.WindowPosition.CENTER_ON_PARENT,
buttons=buttons or self.DEFAULT_BUTTONS,
*args, **kwargs)
class Dialog(Enum):
MESSAGE = """
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<requires lib="gtk+" version="3.16"/>
<requires lib="gtk+" version="3.22"/>
<object class="GtkMessageDialog" id="message_dialog">
<property name="use-header-bar">{use_header}</property>
<property name="can_focus">False</property>
<property name="modal">True</property>
<property name="width_request">250</property>
<property name="width_request">255</property>
<property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property>
<property name="skip_taskbar_hint">True</property>
@@ -91,7 +110,7 @@ class WaitDialog:
@run_idle
def set_text(self, text):
self._label.set_text(get_message(text or self._default_text))
self._label.set_text(translate(text or self._default_text))
@run_idle
def hide(self):
@@ -135,7 +154,7 @@ def get_chooser_dialog(transient, settings, name, patterns, title=None, file_fil
def get_file_chooser_dialog(transient, text, settings, action_type, file_filter, buttons=None, title=None, dirs=False):
action_type = Gtk.FileChooserAction.SELECT_FOLDER if action_type is None else action_type
dialog = Gtk.FileChooserNative.new(get_message(title) if title else "", transient, action_type)
dialog = Gtk.FileChooserNative.new(translate(title) if title else "", transient, action_type)
dialog.set_create_folders(dirs)
dialog.set_modal(True)
@@ -174,7 +193,7 @@ def get_message_dialog(transient, message_type, buttons_type, text):
builder.add_from_string(dialog_str)
dialog = builder.get_object("message_dialog")
dialog.set_transient_for(transient)
dialog.set_markup(get_message(text))
dialog.set_markup(translate(text))
response = dialog.run()
dialog.destroy()
@@ -202,7 +221,7 @@ def get_dialog_from_xml(dialog_type, transient, use_header=0, title=""):
return builder, dialog
def get_message(message):
def translate(message):
""" returns translated message """
return gettext.dgettext(TEXT_DOMAIN, message)
@@ -246,9 +265,9 @@ def translate_xml(path, tag="property"):
root = et.getroot()
for e in root.iter():
if e.tag == tag and e.attrib.get("translatable", None) == "yes":
e.text = get_message(e.text)
e.text = translate(e.text)
elif e.tag == "item" and e.attrib.get("translatable", None) == "yes":
e.text = get_message(e.text)
e.text = translate(e.text)
return ET.tostring(root, encoding="unicode", method="xml")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2
<!-- Generated with glade 3.38.2
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,33 +26,33 @@ THE SOFTWARE.
Author: Dmitriy Yefremov
-->
<interface>
<requires lib="gtk+" version="3.16"/>
<interface domain="demon-editor">
<requires lib="gtk+" version="3.22"/>
<!-- 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-copyright 2018-2023 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>
<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="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">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>
<object class="GtkLabel" id="src_label">
<property name="visible" bind-source="source_selection_box" bind-property="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="label" translatable="yes">Source:</property>
</object>
@@ -65,22 +65,22 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkBox" id="src_box">
<property name="visible">True</property>
<property name="can_focus">False</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>
<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="can-focus">True</property>
<property name="receives-default">False</property>
<property name="active">True</property>
<property name="draw_indicator">False</property>
<property name="draw-indicator">False</property>
<property name="group">dat_src_button</property>
</object>
<packing>
@@ -93,10 +93,10 @@ Author: Dmitriy Yefremov
<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="can-focus">True</property>
<property name="receives-default">False</property>
<property name="active">True</property>
<property name="draw_indicator">False</property>
<property name="draw-indicator">False</property>
<property name="group">dat_src_button</property>
</object>
<packing>
@@ -108,12 +108,11 @@ Author: Dmitriy Yefremov
<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="can-focus">True</property>
<property name="receives-default">False</property>
<property name="active">True</property>
<property name="draw_indicator">False</property>
<property name="draw-indicator">False</property>
<property name="group">http_src_button</property>
</object>
<packing>
@@ -132,12 +131,12 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkBox" id="interval_box">
<property name="visible">True</property>
<property name="can_focus">False</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="can-focus">False</property>
<property name="label" translatable="yes">Update interval (sec):</property>
</object>
<packing>
@@ -149,10 +148,10 @@ Author: Dmitriy Yefremov
<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="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="climb-rate">1</property>
<property name="numeric">True</property>
<property name="value">3</property>
</object>
@@ -172,15 +171,14 @@ Author: Dmitriy Yefremov
<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="sensitive" bind-source="xml_src_button" bind-property="active">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="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Url to *.xml.gz file:</property>
</object>
@@ -191,15 +189,130 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkEntry" id="url_entry">
<object class="GtkBox" id="url_box">
<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>
<property name="can-focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkComboBoxText" id="url_combo_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">0</property>
<property name="has-entry">True</property>
<child internal-child="entry">
<object class="GtkEntry" id="url_entry">
<property name="can-focus">False</property>
<signal name="focus-out-event" handler="on_url_entry_focus_out" swapped="no"/>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButtonBox" id="url_action_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="homogeneous">True</property>
<property name="layout-style">expand</property>
<child>
<object class="GtkButton" id="remove_url_button">
<property name="visible" bind-source="apply_url_button" bind-property="visible" bind-flags="invert-boolean">True</property>
<property name="sensitive">False</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Remove</property>
<signal name="clicked" handler="on_remove_url" swapped="no"/>
<child>
<object class="GtkImage" id="remove_url_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-remove-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="add_url_button">
<property name="visible" bind-source="apply_url_button" bind-property="visible" bind-flags="invert-boolean">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Add</property>
<signal name="clicked" handler="on_add_url" swapped="no"/>
<child>
<object class="GtkImage" id="add_url_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">list-add-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="cancel_url_button">
<property name="visible" bind-source="apply_url_button" bind-property="visible">False</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Cancel</property>
<child>
<object class="GtkImage" id="cancel_url_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">edit-undo-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="apply_url_button">
<property name="visible" bind-source="url_entry" bind-property="has-focus">False</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Add</property>
<signal name="clicked" handler="on_apply_url" swapped="no"/>
<child>
<object class="GtkImage" id="apply_url_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Apply</property>
<property name="icon-name">insert-link-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
@@ -207,12 +320,12 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkBox" id="download_interval_box">
<property name="visible">True</property>
<property name="can_focus">False</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="can-focus">False</property>
<property name="label" translatable="yes">Update:</property>
</object>
<packing>
@@ -224,11 +337,11 @@ Author: Dmitriy Yefremov
<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="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="halign">end</property>
<property name="active">0</property>
<property name="active_id">daily</property>
<property name="active-id">daily</property>
<items>
<item id="daily" translatable="yes">Daily</item>
</items>
@@ -243,7 +356,7 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
<property name="position">3</property>
</packing>
</child>
</object>
@@ -255,16 +368,14 @@ Author: Dmitriy Yefremov
</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="sensitive" bind-source="dat_src_button" bind-property="active">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="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">STB path:</property>
</object>
@@ -277,9 +388,9 @@ Author: Dmitriy Yefremov
<child>
<object class="GtkComboBoxText" id="dat_path_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">True</property>
<property name="active">0</property>
<property name="active_id">/etc/enigma2</property>
<property name="active-id">/etc/enigma2</property>
<items>
<item id="/etc/enigma2/">/etc/enigma2/</item>
<item id="/media/hdd/">/media/hdd/</item>
@@ -311,18 +422,18 @@ Author: Dmitriy Yefremov
<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="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>
<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>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_apply" swapped="no"/>
</object>
<packing>
@@ -335,8 +446,8 @@ Author: Dmitriy Yefremov
<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>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_close" swapped="no"/>
</object>
<packing>

View File

@@ -3,7 +3,7 @@
The MIT License (MIT)
Copyright (c) 2018-2023 Dmitriy Yefremov
Copyright (c) 2018-2024 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
@@ -62,148 +62,90 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label-xalign">0.49</property>
<property name="shadow-type">in</property>
<property name="shadow-type">none</property>
<child>
<object class="GtkBox" id="epg_box">
<object class="GtkViewport" id="viewport">
<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">
<object class="GtkBox" id="epg_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="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">5</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="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="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-top">5</property>
<property name="spacing">5</property>
<child type="center">
<object class="GtkButtonBox" id="src_box">
<property name="name">header-stack-switcher</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">edit-find-replace-symbolic</property>
<property name="tooltip-text" translatable="yes">EPG source</property>
<property name="halign">center</property>
<property name="layout-style">expand</property>
<child>
<object class="GtkRadioButton" id="src_receiver_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">src_xmltv_button</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="src_xmltv_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">src_receiver_button</property>
<signal name="toggled" handler="on_xmltv_toggled" 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">4</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="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">
<object class="GtkToggleButton" id="epg_filter_button">
<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>
<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>
@@ -212,256 +154,420 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkButton" id="epg_search_down_button">
<object class="GtkButton" id="epg_add_timer_button">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="sensitive" bind-source="src_xmltv_button" bind-property="active" bind-flags="invert-boolean">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="GtkArrow" id="epg_down_arrow">
<object class="GtkImage" id="add_timer_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="arrow-type">down</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">1</property>
</packing>
</child>
<child>
<object class="GtkToggleButton" id="multi_epg_button">
<property name="name">header-button</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Multi EPG</property>
<signal name="toggled" handler="on_multi_epg_toggled" swapped="no"/>
<child>
<object class="GtkImage" id="multi_epg_button_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">edit-select-all-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkMenuButton" id="epg_options_button">
<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">Options</property>
<child>
<object class="GtkImage" id="epg_options_button_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">applications-system-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>
<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-top">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="margin-top">5</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkButton" id="epg_search_up_button">
<object class="GtkTreeView" id="epg_view">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">True</property>
<property name="receives-default">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_view_selection">
<property name="mode">multiple</property>
</object>
</child>
<child>
<object class="GtkArrow" id="epg_up_arrow">
<object class="GtkTreeViewColumn" id="epg_service_column">
<property name="visible" bind-source="multi_epg_button" bind-property="active">False</property>
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="fixed-width">100</property>
<property name="min-width">40</property>
<property name="title" translatable="yes">Service</property>
<property name="alignment">0.49000000953674316</property>
<property name="sort-column-id">0</property>
<child>
<object class="GtkCellRendererText" id="epg_service_renderer">
<property name="xpad">5</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="epg_title_column">
<property name="resizable">True</property>
<property name="sizing">fixed</property>
<property name="min-width">150</property>
<property name="title" translatable="yes">Title</property>
<property name="alignment">0.49000000953674316</property>
<property name="sort-column-id">1</property>
<child>
<object class="GtkCellRendererText" id="epg_title_renderer">
<property name="xpad">5</property>
<property name="ellipsize">end</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.49000000953674316</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.49000000953674316</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.49000000953674316</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.49000000953674316</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.49000000953674316</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.49000000953674316</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.49000000953674316</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>
<child>
<object class="GtkBox" id="epg_cache_info_box">
<property name="visible" bind-source="src_xmltv_button" bind-property="active">False</property>
<property name="can-focus">False</property>
<property name="tooltip-text" translatable="yes">Current EPG cache contents.</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="arrow-type">up</property>
<property name="icon-name">edit-select-all-symbolic</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="cache_info_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>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="fill">True</property>
<property name="pack-type">end</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>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<style>
<class name="view"/>
</style>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="epg_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-bottom">2</property>
<property name="label" translatable="yes">EPG</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>

View File

View File

@@ -0,0 +1,368 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2023-2024 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 os
import shutil
from enum import IntEnum
from pathlib import Path
import requests
from gi.repository import Gtk, Gdk, GLib, Pango, GObject
from app.commons import log, run_task, run_idle
from app.ui.dialogs import translate
from app.ui.uicommons import HeaderBar
EXT_URL = "https://api.github.com/repos/DYefremov/demoneditor-extensions/contents/extensions/"
EXT_LIST_FILE = "https://raw.githubusercontent.com/DYefremov/demoneditor-extensions/main/extensions/extension-list"
# Config file name. The config file must be in json format!
# E.g. -> {"EXT_URL": "repo URL", "EXT_LIST_FILE": "URL to 'extension-list' file."}
EXT_CONFIG_FILE = "ext_sources"
HEADERS = {"User-Agent": "Mozilla/5.0 (X11; Linux i686; rv:112.0) Gecko/20100101 Firefox/112.0",
"Accept": "application/json"}
class ExtensionManager(Gtk.Window):
ICON_INFO = "emblem-important-symbolic"
ICON_UPDATE = "network-receive-symbolic"
class Column(IntEnum):
TITLE = 0
DESC = 1
VER = 2
INFO = 3
STATUS = 4
NAME = 5
URL = 6
PATH = 7
def __init__(self, app, **kwargs):
super().__init__(title=translate("Extensions"), icon_name="demon-editor", application=app,
transient_for=app.app_window, destroy_with_parent=True,
window_position=Gtk.WindowPosition.CENTER_ON_PARENT,
default_width=560, default_height=320, modal=True, **kwargs)
self._app = app
self._ext_path = f"{self._app.app_settings.default_data_path}tools{os.sep}extensions"
margin = {"margin_start": 5, "margin_end": 5, "margin_top": 5, "margin_bottom": 5}
base_margin = {"margin_start": 10, "margin_end": 10, "margin_top": 10, "margin_bottom": 10}
# Title, Description, Version, Info, Status, Name, URL, Path.
self._model = Gtk.ListStore.new((str, str, str, str, bool, str, str, object))
self._model.connect("row-deleted", self.on_model_changed)
self._model.connect("row-inserted", self.on_model_changed)
self._view = Gtk.TreeView(activate_on_single_click=True, enable_grid_lines=Gtk.TreeViewGridLines.BOTH)
self._view.set_model(self._model)
self._view.set_tooltip_column(self.Column.DESC)
self._view.connect("row-activated", self.on_row_activated)
# Title
renderer = Gtk.CellRendererText(xalign=0.05, ellipsize=Pango.EllipsizeMode.END)
column = Gtk.TreeViewColumn(title=translate("Title"), cell_renderer=renderer, text=self.Column.TITLE)
column.set_alignment(0.5)
column.set_min_width(170)
column.set_resizable(True)
self._view.append_column(column)
# Description
renderer = Gtk.CellRendererText(xalign=0.05, ellipsize=Pango.EllipsizeMode.END)
column = Gtk.TreeViewColumn(title=translate("Description"), cell_renderer=renderer, text=self.Column.DESC)
column.set_alignment(0.5)
column.set_resizable(True)
column.set_expand(True)
self._view.append_column(column)
# Version
column = Gtk.TreeViewColumn(translate("Ver."))
column.set_alignment(0.5)
column.set_fixed_width(70)
renderer = Gtk.CellRendererText(xalign=0.5)
column.pack_start(renderer, True)
column.add_attribute(renderer, "text", self.Column.VER)
renderer = Gtk.CellRendererPixbuf()
column.pack_start(renderer, True)
column.add_attribute(renderer, "icon_name", self.Column.INFO)
self._view.append_column(column)
# Status
renderer = Gtk.CellRendererToggle(xalign=0.5)
column = Gtk.TreeViewColumn(title=translate("Installed"), cell_renderer=renderer, active=self.Column.STATUS)
column.set_alignment(0.5)
column.set_fixed_width(100)
self._view.append_column(column)
self._status_column = column
main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
frame = Gtk.Frame(shadow_type=Gtk.ShadowType.IN, **base_margin)
frame.get_style_context().add_class("view")
data_box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.VERTICAL, **base_margin)
data_box.set_margin_bottom(margin.get("margin_bottom", 5))
# Status bar.
status_box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.HORIZONTAL, margin_start=5, margin_end=5)
count_icon = Gtk.Image.new_from_icon_name("document-properties", Gtk.IconSize.SMALL_TOOLBAR)
status_box.pack_start(count_icon, False, False, 0)
self._count_label = Gtk.Label(label="0", width_chars=4, xalign=0)
status_box.pack_start(self._count_label, False, False, 0)
status_box.show_all()
load_box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.HORIZONTAL, margin_end=10, no_show_all=True)
load_box.pack_start(Gtk.Label(label=translate("Loading data..."), visible=True), False, False, 0)
self._load_spinner = Gtk.Spinner(visible=True)
self._load_spinner.bind_property("active", load_box, "visible")
self._load_spinner.bind_property("active", self._view, "sensitive", GObject.BindingFlags.INVERT_BOOLEAN)
load_box.pack_end(self._load_spinner, False, False, 0)
status_box.pack_end(load_box, False, False, 0)
data_box.pack_end(status_box, False, True, 0)
scrolled = Gtk.ScrolledWindow(shadow_type=Gtk.ShadowType.IN)
scrolled.add(self._view)
data_box.pack_start(scrolled, True, True, 0)
data_box.set_margin_start(10)
frame.add(data_box)
self.add(main_box)
# Popup menu.
menu = Gtk.Menu()
download_menu_item = Gtk.MenuItem.new_with_label(translate("Download"))
download_menu_item.connect("activate", self.on_download)
menu.append(download_menu_item)
remove_menu_item = Gtk.MenuItem.new_with_label(translate("Remove"))
remove_menu_item.connect("activate", self.on_remove)
menu.append(remove_menu_item)
menu.show_all()
self._view.connect("button-press-event", self.on_view_popup_menu, menu)
# Header and toolbar.
self._download_button = Gtk.Button.new_from_icon_name("go-bottom-symbolic", Gtk.IconSize.BUTTON)
self._download_button.set_tooltip_text(translate("Download"))
self._download_button.set_always_show_image(True)
self._download_button.connect("clicked", self.on_download)
remove_button = Gtk.Button.new_from_icon_name("user-trash-symbolic", Gtk.IconSize.BUTTON)
remove_button.set_tooltip_text(translate("Remove"))
remove_button.set_always_show_image(True)
remove_button.connect("clicked", self.on_remove)
if app.app_settings.use_header_bar:
header = HeaderBar()
header.pack_start(self._download_button)
header.pack_start(remove_button)
self.set_titlebar(header)
header.show_all()
else:
toolbar = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
toolbar.get_style_context().add_class("primary-toolbar")
margin["margin_start"] = 15
margin["margin_top"] = 10
button_box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.HORIZONTAL, **margin)
button_box.pack_start(self._download_button, False, False, 0)
button_box.pack_start(remove_button, False, False, 0)
toolbar.pack_start(button_box, True, True, 0)
main_box.pack_start(toolbar, False, False, 0)
main_box.pack_start(frame, True, True, 0)
main_box.show_all()
# Connection status.
self._connection_status_image = Gtk.Image.new_from_icon_name("network-offline-symbolic", Gtk.IconSize.BUTTON)
status_box.pack_end(self._connection_status_image, False, False, 0)
self._download_button.bind_property("visible", self._connection_status_image, "visible", 4)
self._download_button.bind_property("visible", download_menu_item, "visible")
ws_property = "extension_manager_window_size"
window_size = self._app.app_settings.get(ws_property, None)
if window_size:
self.resize(*window_size)
self.connect("delete-event", lambda w, e: self._app.app_settings.add(ws_property, w.get_size()))
self.connect("realize", self.init)
def init(self, widget):
self._load_spinner.start()
scf = f"{os.path.dirname(__file__)}{os.sep}{EXT_CONFIG_FILE}"
if os.path.isfile(scf):
with (open(scf, "r", encoding="utf-8", errors="ignore") as cf):
config = json.load(cf)
global EXT_URL, EXT_LIST_FILE
EXT_URL = config.get("EXT_URL", EXT_URL)
EXT_LIST_FILE = config.get("EXT_LIST_FILE", EXT_LIST_FILE)
self.update()
def get_installed(self):
import pkgutil
from importlib.util import module_from_spec
ext_paths = [f"{os.path.dirname(__file__)}{os.sep}", self._ext_path, "extensions"]
installed = {}
for importer, name, is_package in pkgutil.iter_modules(ext_paths):
if is_package:
spec = importer.find_spec(name)
if spec is None:
log(f"{self.__class__.__name__} [get installed]: Module {name} not found.")
continue
m = module_from_spec(spec)
spec.loader.exec_module(m)
cls_name = name.capitalize()
if hasattr(m, cls_name):
cls = getattr(m, cls_name)
path = Path(spec.origin).parent
installed[name] = (cls, path)
return installed
@run_task
def update(self):
error_msg = None
try:
with requests.get(url=EXT_LIST_FILE, stream=True) as resp:
if resp.status_code == 200:
try:
self.update_data(resp.json())
except ValueError as e:
error_msg = f"{self.__class__.__name__} [update] error: {e}"
else:
error_msg = f"{self.__class__.__name__} [update] error: {resp.reason}"
GLib.idle_add(self._app.show_error_message, "Data loading error!")
except OSError as e:
error_msg = f"{self.__class__.__name__} [update] error: Connection error. {e}"
if error_msg:
log(error_msg)
self.update_local_data()
@run_idle
def update_data(self, data):
self._model.clear()
gen = self.append_data(data)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
@run_idle
def update_local_data(self):
self._download_button.set_visible(False)
self._load_spinner.stop()
self._model.clear()
for ext, d in self.get_installed().items():
e, path = d
self._model.append((e.LABEL, None, e.VERSION, None, path, ext, None, path))
def append_data(self, data):
installed = self.get_installed()
for e, d in data.items():
url = f"{EXT_URL}{d.get('ref', '')}"
desc = d.get("description", "")
ver = d.get("version", "1.0")
info = self.ICON_UPDATE
path = None
ext = installed.get(e)
if ext:
info = None
ext_ver = ext[0].VERSION
path = ext[1]
if ext_ver < ver:
ver = ext_ver
info = self.ICON_INFO
yield self._model.append((d.get('label'), desc, ver, info, path, e, url, path))
self._load_spinner.stop()
def on_remove(self, item=None):
model, paths = self._view.get_selection().get_selected_rows()
if not paths:
return
itr = model.get_iter(paths)
path = model[itr][self.Column.PATH]
if path:
try:
shutil.rmtree(path)
except OSError as e:
log(f"{self.__class__.__name__} [remove] error: {e}")
else:
model.set(itr, {self.Column.INFO: self.ICON_UPDATE, self.Column.STATUS: None, self.Column.PATH: None})
msg = translate('Restart the program to apply all changes.')
self._app.show_info_message(msg, Gtk.MessageType.WARNING)
@run_task
def on_download(self, item=None):
model, paths = self._view.get_selection().get_selected_rows()
if not paths:
return
itr = model.get_iter(paths)
url = model[itr][self.Column.URL]
ver = model[itr][self.Column.VER]
if not url:
return
GLib.idle_add(self._load_spinner.start)
urls = {}
with requests.get(url=url, headers=HEADERS, stream=True) as resp:
if resp.status_code == 200:
try:
for f in resp.json():
url = f.get("download_url", None)
ver = f.get("version", ver)
if url:
urls[url] = f.get("name", None)
except ValueError as e:
log(f"{self.__class__.__name__} [download] error: {e}")
else:
log(f"{self.__class__.__name__} [download] error: {resp.reason}")
if urls:
path = f"{self._ext_path}{os.sep}{model[paths][self.Column.NAME]}{os.sep}"
os.makedirs(os.path.dirname(path), exist_ok=True)
if all((self.download_file(u, f"{path}{n}") for u, n in urls.items())):
data = {self.Column.VER: ver, self.Column.INFO: None, self.Column.STATUS: True, self.Column.PATH: path}
GLib.idle_add(model.set, itr, data)
msg = translate('Restart the program to apply all changes.')
self._app.show_info_message(msg, Gtk.MessageType.WARNING)
GLib.idle_add(self._load_spinner.stop)
def download_file(self, url, path):
with requests.get(url=url, headers=HEADERS, stream=True) as resp:
if resp.status_code == 200:
with open(path, mode="bw") as f:
for data in resp.iter_content(chunk_size=1024):
f.write(data)
return True
def on_model_changed(self, model, path, itr=None):
self._count_label.set_text(str(len(model)))
def on_row_activated(self, view, path, column):
if column is self._status_column:
self.on_remove() if view.get_model()[path][self.Column.STATUS] else self.on_download()
def on_view_popup_menu(self, view, event, menu):
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)
return True
return False
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-2023 Dmitriy Yefremov
# Copyright (c) 2018-2024 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
@@ -43,9 +43,9 @@ from gi.repository import GLib
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, USE_HEADER_BAR
from app.ui.dialogs import show_dialog, DialogType, get_builder, get_message
from app.ui.dialogs import show_dialog, DialogType, get_builder, translate
from app.ui.main_helper import on_popup_menu
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, Page
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, Page, LINK_ICON, FOLDER_ICON
File = namedtuple("File", ["icon", "name", "size", "date", "attr", "extra"])
@@ -147,7 +147,7 @@ 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)
super().__init__(title=translate("Permissions"), use_header_bar=use_header_bar, *args, **kwargs)
self.set_default_size(360, 100)
self.set_resizable(False)
@@ -296,12 +296,6 @@ class FtpClientBox(Gtk.HBox):
# Force Ctrl
self._ftp_view.connect("key-press-event", self._app.force_ctrl)
self._file_view.connect("key-press-event", self._app.force_ctrl)
# Icons
theme = Gtk.IconTheme.get_default()
folder_icon = "folder-symbolic" if settings.is_darwin else "folder"
self._folder_icon = theme.load_icon(folder_icon, 16, 0) if theme.lookup_icon(folder_icon, 16, 0) else None
self._link_icon = theme.load_icon("emblem-symbolic-link", 16, 0) if theme.lookup_icon("emblem-symbolic-link",
16, 0) else None
# Initialization
self.init_drag_and_drop()
self.init_ftp()
@@ -377,10 +371,10 @@ class FtpClientBox(Gtk.HBox):
icon = None
if is_dir:
r_size = self.FOLDER
icon = self._folder_icon
icon = FOLDER_ICON
elif p.is_symlink():
r_size = self.LINK
icon = self._link_icon
icon = LINK_ICON
else:
r_size = get_size_from_bytes(size)
@@ -401,10 +395,10 @@ class FtpClientBox(Gtk.HBox):
icon = None
if is_dir:
r_size = self.FOLDER
icon = self._folder_icon
icon = FOLDER_ICON
elif is_link:
r_size = self.LINK
icon = self._link_icon
icon = LINK_ICON
else:
r_size = get_size_from_bytes(size)
@@ -675,7 +669,7 @@ class FtpClientBox(Gtk.HBox):
log(e)
self._app.show_error_message(str(e))
else:
itr = self._file_model.append(File(self._folder_icon, path.name, self.FOLDER, "", str(path.resolve()), "0"))
itr = self._file_model.append(File(FOLDER_ICON, path.name, self.FOLDER, "", str(path.resolve()), "0"))
renderer.set_property("editable", True)
self._file_view.set_cursor(self._file_model.get_path(itr), self._file_view.get_column(0), True)
@@ -695,7 +689,7 @@ class FtpClientBox(Gtk.HBox):
log(e)
else:
if resp == f"{cur_path}/{name}":
itr = self._ftp_model.append(File(self._folder_icon, name, self.FOLDER, "", "drwxr-xr-x", "0"))
itr = self._ftp_model.append(File(FOLDER_ICON, name, self.FOLDER, "", "drwxr-xr-x", "0"))
renderer.set_property("editable", True)
self._ftp_view.set_cursor(self._ftp_model.get_path(itr), self._ftp_view.get_column(0), True)

View File

@@ -434,9 +434,9 @@ Author: Dmitriy Yefremov
<object class="GtkPaned" id="paned">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-bottom">5</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-bottom">10</property>
<property name="wide-handle">True</property>
<signal name="realize" handler="on_main_paned_realize" swapped="no"/>
<child>
@@ -449,112 +449,124 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label-xalign">0.49000000953674316</property>
<property name="shadow-type">in</property>
<property name="shadow-type">none</property>
<child>
<object class="GtkBox" id="bouquets_box">
<property name="width-request">100</property>
<object class="GtkViewport" id="bouquets_viewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="bouquets_screlled_window">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkTreeView" id="bq_view">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="model">bq_list_store</property>
<property name="headers-clickable">False</property>
<property name="search-column">0</property>
<signal name="button-press-event" handler="on_popup_menu" object="bq_popup_menu" swapped="no"/>
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
<signal name="select-all" handler="on_select_all" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="bouquet_name_column">
<property name="title" translatable="yes">Name</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="bq_name_renderer">
<property name="xalign">0.019999999552965164</property>
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="bouquet_type_column">
<property name="title" translatable="yes">Type</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="bq_type_renderer">
<property name="xalign">0.5099999904632568</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="bouquet_selected_column">
<property name="title" translatable="yes">Selected</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererToggle" id="bq_selected_renderer">
<signal name="toggled" handler="on_bq_selected_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">2</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="bouquets_status_box">
<property name="height-request">26</property>
<object class="GtkBox" id="bouquets_box">
<property name="width-request">100</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="spacing">5</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkImage" id="bouquets_count_image">
<object class="GtkScrolledWindow" id="bouquets_screlled_window">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">document-properties</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkTreeView" id="bq_view">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="model">bq_list_store</property>
<property name="headers-clickable">False</property>
<property name="search-column">0</property>
<signal name="button-press-event" handler="on_popup_menu" object="bq_popup_menu" swapped="no"/>
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
<signal name="select-all" handler="on_select_all" swapped="no"/>
<child>
<object class="GtkTreeViewColumn" id="bouquet_name_column">
<property name="title" translatable="yes">Name</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="bq_name_renderer">
<property name="xalign">0.019999999552965164</property>
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="bouquet_type_column">
<property name="title" translatable="yes">Type</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="bq_type_renderer">
<property name="xalign">0.5099999904632568</property>
</object>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="bouquet_selected_column">
<property name="title" translatable="yes">Selected</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererToggle" id="bq_selected_renderer">
<signal name="toggled" handler="on_bq_selected_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">2</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="bouquets_count_label">
<object class="GtkBox" id="bouquets_status_box">
<property name="height-request">26</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label">0</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="bouquets_count_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">document-properties</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="bouquets_count_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -562,25 +574,22 @@ Author: Dmitriy Yefremov
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="view"/>
</style>
</object>
</child>
<child type="label">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="margin-bottom">2</property>
<property name="label" translatable="yes">Bouquets</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>
@@ -594,98 +603,110 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label-xalign">0.49000000953674316</property>
<property name="shadow-type">in</property>
<property name="shadow-type">none</property>
<child>
<object class="GtkBox" id="satellites_box">
<property name="width-request">100</property>
<object class="GtkViewport" id="sat_viewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="satellites_screlled_window">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkTreeView" id="sat_view">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="model">sat_list_store</property>
<property name="headers-clickable">False</property>
<property name="search-column">0</property>
<signal name="button-press-event" handler="on_popup_menu" object="sat_popup_menu" swapped="no"/>
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
<signal name="realize" handler="on_sat_view_realize" swapped="no"/>
<signal name="select-all" handler="on_select_all" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="sat_position_column">
<property name="title" translatable="yes">Position</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="sat_position_renderer">
<property name="xalign">0.019999999552965164</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="sat_selected_column">
<property name="title" translatable="yes">Selected</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererToggle" id="sat_selected_renderer">
<signal name="toggled" handler="on_sat_selected_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">2</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="sat_status_box">
<property name="height-request">26</property>
<object class="GtkBox" id="satellites_box">
<property name="width-request">100</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="spacing">5</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkImage" id="sat_count_image">
<object class="GtkScrolledWindow" id="satellites_screlled_window">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">document-properties</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkTreeView" id="sat_view">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="model">sat_list_store</property>
<property name="headers-clickable">False</property>
<property name="search-column">0</property>
<signal name="button-press-event" handler="on_popup_menu" object="sat_popup_menu" swapped="no"/>
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
<signal name="realize" handler="on_sat_view_realize" swapped="no"/>
<signal name="select-all" handler="on_select_all" swapped="no"/>
<child>
<object class="GtkTreeViewColumn" id="sat_position_column">
<property name="title" translatable="yes">Position</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="sat_position_renderer">
<property name="xalign">0.019999999552965164</property>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="sat_selected_column">
<property name="title" translatable="yes">Selected</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererToggle" id="sat_selected_renderer">
<signal name="toggled" handler="on_sat_selected_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">2</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="sat_count_label">
<object class="GtkBox" id="sat_status_box">
<property name="height-request">26</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label">0</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="sat_count_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">document-properties</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="sat_count_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
@@ -693,25 +714,22 @@ Author: Dmitriy Yefremov
<property name="position">1</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="view"/>
</style>
</object>
</child>
<child type="label">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="margin-bottom">2</property>
<property name="label" translatable="yes">Satellites</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>
@@ -732,183 +750,130 @@ Author: Dmitriy Yefremov
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label-xalign">0.49000000953674316</property>
<property name="shadow-type">in</property>
<property name="shadow-type">none</property>
<child>
<object class="GtkBox" id="services_box">
<property name="width-request">100</property>
<object class="GtkViewport" id="services_viewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkScrolledWindow" id="services_view_scrolled_window">
<object class="GtkBox" id="services_box">
<property name="width-request">100</property>
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkTreeView" id="services_view">
<object class="GtkScrolledWindow" id="services_view_scrolled_window">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="model">services_list_store</property>
<property name="headers-clickable">False</property>
<property name="search-column">0</property>
<signal name="button-press-event" handler="on_popup_menu" object="services_popup_menu" swapped="no"/>
<signal name="cursor-changed" handler="on_service_changed" swapped="no"/>
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
<signal name="realize" handler="on_services_view_realize" swapped="no"/>
<signal name="select-all" handler="on_select_all" swapped="no"/>
<child internal-child="selection">
<object class="GtkTreeSelection"/>
</child>
<property name="shadow-type">in</property>
<child>
<object class="GtkTreeViewColumn" id="service_name_column">
<property name="sizing">fixed</property>
<property name="min-width">50</property>
<property name="title" translatable="yes">Name</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="info_name_renderer">
<property name="xalign">0.019999999552965164</property>
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">0</attribute>
<attribute name="background-rgba">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="service_type_column">
<property name="sizing">fixed</property>
<property name="min-width">75</property>
<property name="title" translatable="yes">Type</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="info_type_renderer">
<property name="xpad">5</property>
<property name="xalign">0.5099999904632568</property>
</object>
<attributes>
<attribute name="text">1</attribute>
<attribute name="background-rgba">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="service_selected_column">
<property name="sizing">fixed</property>
<property name="min-width">75</property>
<property name="title" translatable="yes">Selected</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererToggle" id="service_selected_renderer">
<property name="xpad">5</property>
<signal name="toggled" handler="on_service_selected_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="cell-background-rgba">3</attribute>
<attribute name="active">2</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="service_info_box_frame">
<property name="visible" bind-source="details_button" bind-property="active">False</property>
<property name="can-focus">False</property>
<property name="margin-top">5</property>
<property name="label-xalign">0</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkBox" id="service_info_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="service_info_label">
<object class="GtkTreeView" id="services_view">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="ellipsize">end</property>
<property name="can-focus">True</property>
<property name="model">services_list_store</property>
<property name="headers-clickable">False</property>
<property name="search-column">0</property>
<signal name="button-press-event" handler="on_popup_menu" object="services_popup_menu" swapped="no"/>
<signal name="cursor-changed" handler="on_service_changed" swapped="no"/>
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
<signal name="realize" handler="on_services_view_realize" swapped="no"/>
<signal name="select-all" handler="on_select_all" swapped="no"/>
<child>
<object class="GtkTreeViewColumn" id="service_name_column">
<property name="sizing">fixed</property>
<property name="min-width">50</property>
<property name="title" translatable="yes">Name</property>
<property name="expand">True</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="info_name_renderer">
<property name="xalign">0.019999999552965164</property>
<property name="ellipsize">end</property>
</object>
<attributes>
<attribute name="text">0</attribute>
<attribute name="background-rgba">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="service_type_column">
<property name="sizing">fixed</property>
<property name="min-width">75</property>
<property name="title" translatable="yes">Type</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererText" id="info_type_renderer">
<property name="xpad">5</property>
<property name="xalign">0.5099999904632568</property>
</object>
<attributes>
<attribute name="text">1</attribute>
<attribute name="background-rgba">3</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="service_selected_column">
<property name="sizing">fixed</property>
<property name="min-width">75</property>
<property name="title" translatable="yes">Selected</property>
<property name="alignment">0.5</property>
<child>
<object class="GtkCellRendererToggle" id="service_selected_renderer">
<property name="xpad">5</property>
<signal name="toggled" handler="on_service_selected_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="cell-background-rgba">3</attribute>
<attribute name="active">2</attribute>
</attributes>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="services_status_box">
<property name="height-request">26</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="services_count_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">document-properties</property>
</object>
<packing>
<property name="expand">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="services_count_label">
<property name="visible">True</property>
<object class="GtkFrame" id="service_info_box_frame">
<property name="visible" bind-source="details_button" bind-property="active">False</property>
<property name="can-focus">False</property>
<property name="label">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="service_exists_frame">
<property name="width-request">32</property>
<property name="height-request">16</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin-top">5</property>
<property name="label-xalign">0</property>
<property name="shadow-type">in</property>
<child>
<placeholder/>
<object class="GtkBox" id="service_info_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="service_info_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="ellipsize">end</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
@@ -917,39 +882,101 @@ Author: Dmitriy Yefremov
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">2</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id=" service_exists_label">
<object class="GtkBox" id="services_status_box">
<property name="height-request">26</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Already exists</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="services_count_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">document-properties</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="services_count_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkFrame" id="service_exists_frame">
<property name="width-request">32</property>
<property name="height-request">16</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="label-xalign">0</property>
<property name="shadow-type">in</property>
<child>
<placeholder/>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id=" service_exists_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Already exists</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">3</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>
<style>
<class name="view"/>
</style>
</object>
</child>
<child type="label">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="margin-bottom">2</property>
<property name="label" translatable="yes">Services</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2023 Dmitriy Yefremov
# Copyright (c) 2018-2024 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -35,8 +35,8 @@ from app.eparser import get_bouquets, get_services, BouquetsReader
from app.eparser.ecommons import BqType, BqServiceType, Bouquet
from app.eparser.neutrino.bouquets import parse_webtv, parse_bouquets as get_neutrino_bouquets
from app.settings import SettingsType, 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, get_iptv_data
from app.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, translate, get_builder
from app.ui.main_helper import on_popup_menu, get_iptv_data, show_info_bar_message
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, Column, Page, HeaderBar
@@ -52,7 +52,7 @@ def import_bouquet(app, model, path, appender, file_path=None):
if profile is SettingsType.ENIGMA_2:
pattern = f".{bq_type.value}"
f_pattern = f"{'' if IS_DARWIN else 'userbouquet.'}*{pattern}"
f_pattern = f"*{pattern}"
elif profile is SettingsType.NEUTRINO_MP:
pattern = "webtv.xml" if bq_type is BqType.WEBTV else "bouquets.xml"
f_pattern = "bouquets.xml"
@@ -96,10 +96,9 @@ def import_bouquet(app, model, path, appender, file_path=None):
def get_enigma2_bouquet(path):
path, sep, f_name = path.rpartition("userbouquet.")
name, sep, suf = f_name.rpartition(".")
bq = BouquetsReader.get_bouquet(path, name, suf)
bouquet = Bouquet(name=bq[0], type=BqType(suf).value, services=bq[1], locked=None, hidden=None)
p = Path(path)
bq = BouquetsReader.get_bouquet(f"{p.parent}{SEP}", f"{p.stem}{p.suffix}", p.stem)
bouquet = Bouquet(name=bq[0], type=BqType(p.suffix.lstrip(".")).value, services=bq[1], locked=None, hidden=None)
return bouquet
@@ -219,7 +218,7 @@ class ImportDialog:
def on_import(self, item):
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)
self.show_info_message(translate("No selected item!"), Gtk.MessageType.ERROR)
return
if not self._bouquets or show_dialog(DialogType.QUESTION, self._dialog_window) != Gtk.ResponseType.OK:
@@ -346,8 +345,8 @@ class ImportDialog:
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}"
ref = f"{translate('Service reference')}: {ref}"
info = f"{translate('Name')}: {row[0]}\n{ref}\nURL: {url}"
self._service_info_label.set_text(info)
else:
srv = self._services.get(row[-1], None)
@@ -374,9 +373,7 @@ class ImportDialog:
@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)
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
@run_idle
def on_info_bar_close(self, bar=None, resp=None):

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2023 Dmitriy Yefremov
# Copyright (c) 2018-2024 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,8 @@ import concurrent.futures
import os
import re
import urllib
from datetime import date
from itertools import groupby, chain
from urllib.error import HTTPError
from urllib.parse import urlparse, unquote, quote
from urllib.request import Request, urlopen
@@ -38,19 +40,20 @@ import requests
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.ecommons import BqServiceType, BouquetService, Service
from app.eparser.iptv import (NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT, get_fav_id, MARKER_FORMAT,
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.dialogs import Action, show_dialog, DialogType, translate, get_builder, BaseDialog
from app.ui.main_helper import get_iptv_url, on_popup_menu, get_picon_pixbuf, show_info_bar_message, gen_bouquet_name
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}:{:X}:0:0:0"
_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
_UI_PATH = UI_RESOURCES_PATH + "iptv.glade"
_UI_PATH = f"{UI_RESOURCES_PATH}iptv.glade"
_CSS_PATH = f"{UI_RESOURCES_PATH}style.css"
_URL_PREFIXES = {"YT-DLP": "YT-DLP://", "YT-DL": "YT-DL://", "STREAMLINK": "streamlink://", "No": None}
@@ -105,7 +108,7 @@ class IptvDialog:
self._name_entry = builder.get_object("name_entry")
self._description_entry = builder.get_object("description_entry")
self._url_entry = builder.get_object("url_entry")
self._reference_entry = builder.get_object("reference_entry")
self._reference_label = builder.get_object("iptv_reference_label")
self._srv_type_entry = builder.get_object("srv_type_entry")
self._srv_id_entry = builder.get_object("srv_id_entry")
self._sid_entry = builder.get_object("sid_entry")
@@ -124,7 +127,7 @@ class IptvDialog:
self._model, self._paths = view.get_selection().get_selected_rows()
# Style.
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._style_provider.load_from_path(_CSS_PATH)
self._digit_elems = (self._srv_id_entry, self._srv_type_entry, self._sid_entry, self._tr_id_entry,
self._net_id_entry, self._namespace_entry)
for el in self._digit_elems:
@@ -133,8 +136,7 @@ class IptvDialog:
if self._s_type is SettingsType.NEUTRINO_MP:
builder.get_object("iptv_dialog_ts_data_frame").set_visible(False)
builder.get_object("iptv_type_label").set_visible(False)
builder.get_object("reference_entry").set_visible(False)
builder.get_object("iptv_reference_label").set_visible(False)
builder.get_object("iptv_ref_box").set_visible(False)
self._stream_type_combobox.set_visible(False)
else:
self._description_entry.set_visible(False)
@@ -164,14 +166,14 @@ class IptvDialog:
self.on_url_changed(self._url_entry)
if not is_data_correct(self._digit_elems) or self._url_entry.get_name() == _DIGIT_ENTRY_NAME:
self.show_info_message(get_message("Error. Verify the data!"), Gtk.MessageType.ERROR)
self.show_info_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)
self.show_info_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):
@@ -236,7 +238,7 @@ class IptvDialog:
def update_reference_entry(self):
if self._s_type is SettingsType.ENIGMA_2 and is_data_correct(self._digit_elems):
self._reference_entry.set_text(_ENIGMA2_REFERENCE.format(self.get_type(),
self._reference_label.set_text(_ENIGMA2_REFERENCE.format(self.get_type(),
self._srv_id_entry.get_text(),
int(self._srv_type_entry.get_text()),
int(self._sid_entry.get_text()),
@@ -248,11 +250,8 @@ class IptvDialog:
return get_stream_type(self._stream_type_combobox)
def on_entry_changed(self, entry):
if _PATTERN.search(entry.get_text()):
entry.set_name(_DIGIT_ENTRY_NAME)
else:
entry.set_name("GtkEntry")
self.update_reference_entry()
entry.set_name(_DIGIT_ENTRY_NAME if _PATTERN.search(entry.get_text()) else "GtkEntry")
self.update_reference_entry()
def on_url_changed(self, entry):
url_str = entry.get_text()
@@ -297,7 +296,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(f"{get_message('Getting link error:')} {e}", Gtk.MessageType.ERROR)
self.show_info_message(f"{translate('Getting link error:')} {e}", Gtk.MessageType.ERROR)
return
except YouTubeException as e:
self.show_info_message((str(e)), Gtk.MessageType.ERROR)
@@ -312,7 +311,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 = f"{get_message('Getting link error:')} No link received for id: {video_id}"
msg = f"{translate('Getting link error:')} No link received for id: {video_id}"
self.show_info_message(msg, Gtk.MessageType.ERROR)
finally:
entry.set_sensitive(True)
@@ -365,7 +364,7 @@ class IptvDialog:
self._dialog.destroy()
def update_bouquet_data(self, name, fav_id):
picon_id = f"{self._reference_entry.get_text().replace(':', '_')}.png"
picon_id = f"{self._reference_label.get_text().replace(':', '_')}.png"
if self._action is Action.EDIT:
services = self._app.current_services
@@ -391,9 +390,7 @@ class IptvDialog:
@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)
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
class SearchUnavailableDialog:
@@ -510,6 +507,7 @@ class IptvListDialog:
self._data_box = builder.get_object("iptv_list_data_box")
self._start_values_grid = builder.get_object("start_values_grid")
self._info_bar = builder.get_object("list_configuration_info_bar")
self._message_label = builder.get_object("list_configuration_message_label")
self._reference_label = builder.get_object("reference_label")
self._stream_type_check_button = builder.get_object("stream_type_default_check_button")
self._id_default_check_button = builder.get_object("id_default_check_button")
@@ -532,7 +530,7 @@ class IptvListDialog:
self._ok_button.bind_property("visible", self._cancel_button, "visible", 4)
# Style
style_provider = Gtk.CssProvider()
style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
style_provider.load_from_path(_CSS_PATH)
self._default_elems = (self._stream_type_check_button, self._id_default_check_button, self._type_check_button,
self._sid_auto_check_button, self._tid_check_button, self._nid_check_button,
self._namespace_check_button)
@@ -591,6 +589,10 @@ class IptvListDialog:
for el in self._default_elems:
el.set_active(True)
@run_idle
def show_info_message(self, text, message_type=Gtk.MessageType.INFO):
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@@ -631,7 +633,7 @@ class IptvListConfigurationDialog(IptvListDialog):
@run_idle
def on_apply(self, item):
if not is_data_correct(self._digit_elems):
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
self.show_info_message("Error. Verify the data!", Gtk.MessageType.ERROR)
return
if self._s_type is SettingsType.ENIGMA_2:
@@ -678,7 +680,7 @@ class IptvListConfigurationDialog(IptvListDialog):
self._bouquet.clear()
list(map(lambda r: self._bouquet.append(r[Column.FAV_ID]), self._fav_model))
self._info_bar.set_visible(True)
self.show_info_message("Done!", Gtk.MessageType.INFO)
self._ok_button.set_visible(True)
@@ -692,61 +694,77 @@ class M3uImportDialog(IptvListDialog):
self._picons = app.picons
self._pic_path = app._settings.profile_picons_path
self._services = None
self._epg_src = None
self._url_count = 0
self._errors_count = 0
self._max_count = 0
self._is_download = False
self._cancellable = Gio.Cancellable()
self._dialog.set_title(get_message("Playlist import"))
self._dialog.set_title(translate("Playlist import"))
self._dialog.connect("delete-event", self.on_close)
self._apply_button.set_label(get_message("Import"))
# Progress
self._progress_bar = Gtk.ProgressBar(visible=False, valign="center")
self._spinner = Gtk.Spinner(active=False)
self._info_label = Gtk.Label(visible=True, ellipsize="end", max_width_chars=30)
load_label = Gtk.Label(label=get_message("Loading data..."))
self._spinner.bind_property("active", self._spinner, "visible")
self._spinner.bind_property("visible", load_label, "visible")
self._apply_button.set_label(translate("Import"))
# Extra box.
builder = get_builder(f"{UI_RESOURCES_PATH}m3u.glade", use_str=True, objects=("import_m3u_box",))
self._info_label = builder.get_object("info_label")
self._progress_bar = builder.get_object("progress_bar")
self._spinner = builder.get_object("spinner")
self._spinner.bind_property("active", self._start_values_grid, "sensitive", 4)
self._picon_switch = builder.get_object("picon_switch")
self._picon_box = builder.get_object("picon_box")
# Type import buttons.
self._current_bq_button = builder.get_object("current_bq_button")
self._single_bq_button = builder.get_object("single_bq_button")
self._group_bq_button = builder.get_object("group_bq_button")
self._sub_bq_button = builder.get_object("sub_bq_button")
# EPG src.
self._epg_links_button = builder.get_object("epg_links_box")
self._add_epg_src_switch = builder.get_object("add_epg_src_switch")
progress_box = Gtk.HBox(visible=True, spacing=2)
progress_box.add(self._progress_bar)
progress_box.pack_end(self._spinner, False, False, 0)
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=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
extra_box = Gtk.HBox(visible=True, spacing=2, margin_bottom=5, margin_top=5)
extra_box.set_center_widget(progress_box)
extra_box.pack_start(self._info_label, False, False, 5)
extra_box.pack_end(self._picon_box, True, True, 5)
frame = Gtk.Frame(visible=True, margin_bottom=5)
frame.add(extra_box)
self._data_box.add(frame)
m3u_box = builder.get_object("import_m3u_box")
if s_type is SettingsType.ENIGMA_2:
self._data_box.add(m3u_box)
else:
self._data_box.set_visible(False)
self._group_bq_button.set_sensitive(False)
self._sub_bq_button.set_sensitive(False)
m3u_box.set_margin_start(5)
m3u_box.set_margin_end(5)
area = self._dialog.get_content_area()
area.pack_start(m3u_box, True, True, 0)
area.reorder_child(m3u_box, 0)
self.get_m3u(m3_path, s_type)
@run_task
def get_m3u(self, path, s_type):
try:
GLib.idle_add(self._spinner.set_property, "active", True)
self._services = parse_m3u(path, s_type)
GLib.idle_add(self._spinner.start)
self._epg_src, self._services = parse_m3u(path, s_type)
for s in self._services:
if s.picon:
GLib.idle_add(self._picon_box.set_sensitive, True)
break
finally:
msg = f"{get_message('Streams detected:')} {len(self._services) if self._services else 0}."
GLib.idle_add(self._info_label.set_text, msg)
GLib.idle_add(self._spinner.set_property, "active", False)
self.update_info()
@run_idle
def update_info(self):
msg = f"{translate('Streams detected:')} {len(self._services) if self._services else 0}."
self._info_label.set_text(msg)
self._spinner.stop()
if self._epg_src:
self._epg_links_button.set_visible(True)
[self._epg_links_button.append(u, u) for u in self._epg_src]
self._epg_links_button.set_active(0)
def on_apply(self, item):
if self._current_bq_button.get_active() and not self._app.current_bouquet:
self.show_info_message("Error. No bouquet is selected!", Gtk.MessageType.ERROR)
return
if not is_data_correct(self._digit_elems):
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
self.show_info_message("Error. Verify the data!", Gtk.MessageType.ERROR)
return
picons = {}
@@ -776,18 +794,76 @@ class M3uImportDialog(IptvListDialog):
services.append(s._replace(picon=None, picon_id=picon_id, data_id=None, fav_id=fav_id))
if self._picons_switch.get_active():
if self._add_epg_src_switch.get_active():
self.on_add_epg_source()
if self._picon_switch.get_active():
if self.is_default_values():
show_dialog(DialogType.ERROR, self._dialog,
"Set values for TID, NID and Namespace for correct naming of the picons!")
msg = "Set values for TID, NID and Namespace for correct naming of the picons!"
self.show_info_message(msg, Gtk.MessageType.ERROR)
return
self.download_picons(picons)
else:
GLib.idle_add(self._ok_button.set_visible, True)
GLib.idle_add(self._info_bar.set_visible, True, priority=GLib.PRIORITY_LOW)
self.on_apply_done()
self._app.append_imported_services(services)
self.import_services(services)
def import_services(self, services):
if self._current_bq_button.get_active():
self._app.append_imported_services(services)
return
s_type = self._app.app_settings.setting_type
model = self._app.bouquets_view.get_model()
if s_type is SettingsType.ENIGMA_2:
itr = model.get_iter_first()
else:
# We will use the 'FAV' section for Neutrino!
itr = model.get_iter(Gtk.TreePath.new_from_indices([1]))
bqs = self._app.current_bouquets
bq_type = model.get_value(itr, Column.BQ_TYPE)
def_bq_name = gen_bouquet_name(bqs, f"IPTV {date.today()} ", bq_type)
if self._single_bq_button.get_active():
self.append_bouquet(def_bq_name, bq_type, bqs, model, itr, services)
else:
# Sub-bouquets.
if self._sub_bq_button.get_active():
itr = self.append_bouquet(gen_bouquet_name(bqs, def_bq_name, bq_type), bq_type, bqs, model, itr, ())
# Generating groups with skipping markers.
m_name = BqServiceType.MARKER.value
def_bq_name = f"{def_bq_name} [No group]"
gr = self.get_services_groups(filter(lambda s: s.service_type != m_name, services), def_bq_name)
[self.append_bouquet(gen_bouquet_name(bqs, g, bq_type), bq_type, bqs, model, itr, s) for g, s in gr.items()]
def append_bouquet(self, bq_name, bq_type, bqs, model, itr, services):
""" Adds new bouquet and returns iter of appended row. """
cur_services = self._app.current_services
bqs[f"{bq_name}:{bq_type}"] = [s.fav_id for s in services]
cur_services.update({s.fav_id: s for s in services})
bq = (bq_name, None, None, bq_type)
return model.append(itr, bq)
def get_services_groups(self, services, def_gr_name="No group"):
def grouper(s):
return s.package or def_gr_name
return {k: list(v) for k, v in groupby(sorted(services, key=grouper), key=grouper)}
def on_add_epg_source(self):
active_src = self._epg_links_button.get_active_id()
settings = self._app.app_settings
sources = settings.epg_xml_sources
log(f"Adding an EPG source -> {active_src}")
if active_src not in set(sources):
sources.append(active_src)
settings.epg_xml_sources = sources
self._app.emit("epg-settings-changed", None)
else:
log(f"{translate('This URL already exists!')}")
@run_task
def download_picons(self, picons):
@@ -869,10 +945,15 @@ class M3uImportDialog(IptvListDialog):
model.set_value(r.iter, Column.FAV_PICON, picons.get(s.picon_id, None))
yield True
self._info_bar.set_visible(True)
self._ok_button.set_visible(True)
self.on_apply_done()
yield True
@run_idle
def on_apply_done(self):
self.show_info_message("Done!", Gtk.MessageType.INFO)
self._ok_button.set_visible(True)
self._picon_box.set_sensitive(False)
def on_response(self, dialog, response):
if response == Gtk.ResponseType.APPLY:
return True
@@ -891,6 +972,134 @@ class M3uImportDialog(IptvListDialog):
return False
class ExportM3uDialog(BaseDialog):
def __init__(self, app, bouquets):
super().__init__(app.app_window, "Export to m3u",
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, translate("Save"), Gtk.ResponseType.OK))
self._app = app
self._bouquets = bouquets
self._url = None
self._default_port = "8001"
builder = get_builder(f"{UI_RESOURCES_PATH}m3u.glade", use_str=True, objects=("export_m3u_box",))
self._main_grid = builder.get_object("export_m3u_grid")
self._port_entry = builder.get_object("export_port_entry")
self._port_auto_button = builder.get_object("export_auto_button")
self._all_type_button = builder.get_object("export_all_button")
self._iptv_type_button = builder.get_object("export_iptv_button")
self._grp_bq_button = builder.get_object("export_grp_bq_button")
self._grp_marker_button = builder.get_object("export_grp_markers_button")
self._bq_count_label = builder.get_object("export_bq_count_label")
self._services_count_label = builder.get_object("export_services_count_label")
self.get_content_area().pack_start(builder.get_object("export_m3u_box"), False, False, 0)
is_enigma = self._app.is_enigma
self._port_auto_button.set_active(True) if is_enigma else self._main_grid.remove_row(0)
self._grp_marker_button.set_visible(is_enigma)
self._all_type_button.set_active(True) if is_enigma else self._iptv_type_button.set_active(True)
self._all_type_button.set_sensitive(is_enigma)
self.connect("response", self.on_response)
self.connect("realize", self.init)
def init(self, widget=None):
self._bq_count_label.set_text(str(len(self._bouquets)))
self._services_count_label.set_text(str(len(list(chain.from_iterable(self._bouquets.values())))))
if self._app.is_enigma:
self._port_entry.connect("changed", self.on_port_changed)
self._port_auto_button.connect("toggled", self.on_port_auto_toggled)
# Add style for the port entry.
style_provider = Gtk.CssProvider()
style_provider.load_from_path(_CSS_PATH)
context = self._port_entry.get_style_context()
context.add_provider_for_screen(Gdk.Screen.get_default(), style_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
def on_port_changed(self, entry):
entry.set_name(_DIGIT_ENTRY_NAME if _PATTERN.search(entry.get_text()) else "GtkEntry")
def on_port_auto_toggled(self, button):
if not button.get_active() and not self._port_entry.get_text():
self._port_entry.set_text(self._default_port)
def on_response(self, dialog, response):
if response != Gtk.ResponseType.OK:
self.destroy()
else:
if self._app.is_enigma:
if self._port_auto_button.get_active():
self.do_export_auto()
else:
if self._port_entry.get_name() == _DIGIT_ENTRY_NAME:
self._app.show_error_message("Error. Verify the data!")
else:
st = self._app.app_settings
self._url = f"http{'s' if st.http_use_ssl else ''}://{st.host}:{self._port_entry.get_text()}/"
self.do_export()
else:
self.do_export()
return True
def do_export_auto(self, button=None):
""" Retrieves streaming port from Receiver via HTTP API and starts export.
Since the streaming port can be changed by the user,
we're getting base link to the stream -> http(s)://IP:PORT/
"""
from app.connections import HttpAPI
sent = self._app.send_http_request(HttpAPI.Request.STREAM, "", self.start_export)
self._port_auto_button.set_active(sent)
self._port_auto_button.set_sensitive(sent)
def start_export(self, data):
self._port_auto_button.set_active("error_code" not in data)
url = self._app.get_url_from_m3u(data)
url = urlparse(url)
if all((url.scheme, url.port)):
self._url = url.geturl()
self._port_entry.set_text(str(url.port))
self.do_export()
@run_idle
def do_export(self):
self.destroy()
services = self._app.current_services
def get_service(fav_id, num=0):
srv = services.get(fav_id, None)
if srv:
s_type = BqServiceType(srv.service_type)
if s_type is BqServiceType.DEFAULT:
srv = services.get(fav_id, None)
s_data = srv.picon_id.rstrip(".png").replace("_", ":") if srv.picon_id else None
return BouquetService(srv.service, s_type, s_data, num)
return BouquetService(srv.service, s_type, fav_id, num)
return BouquetService("N/A", BqServiceType.MARKER, fav_id, num)
# Preparing bouquets data.
bouquets = {b[:b.rindex(":")]: [get_service(i) for i in s] for b, s in self._bouquets.items()}
bq_services = []
s_types = {BqServiceType.IPTV}
if self._all_type_button.get_active():
s_types.add(BqServiceType.DEFAULT)
if self._grp_bq_button.get_active():
for b, bs in bouquets.items():
bq_services.append(BouquetService(b, BqServiceType.MARKER, None, 0))
bq_services.extend(filter(lambda s: s.type in s_types, bs))
elif self._grp_marker_button.get_active():
bq_services = chain.from_iterable(bouquets.values())
else:
bq_services = filter(lambda s: s.type in s_types, chain.from_iterable(bouquets.values()))
file_name = f"{'_'.join(list(bouquets)[:10])}__{date.today().strftime('%Y_%m_%d')}"
self._app.save_bouquet_to_m3u(bq_services, self._url, file_name)
class YtListImportDialog:
def __init__(self, app):
handlers = {"on_import": self.on_import,
@@ -941,7 +1150,7 @@ class YtListImportDialog:
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"))
header_bar = HeaderBar(title="YouTube", subtitle=translate("Playlist import"))
self._dialog.set_titlebar(header_bar)
actions_box = builder.get_object("yt_actions_box")
import_box = builder.get_object("yt_import_box")
@@ -956,7 +1165,7 @@ class YtListImportDialog:
self._dialog.resize(*window_size)
# Style.
style_provider = Gtk.CssProvider()
style_provider.load_from_path(f"{UI_RESOURCES_PATH}style.css")
style_provider.load_from_path(_CSS_PATH)
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
@@ -1073,7 +1282,7 @@ class YtListImportDialog:
srvs.append(srv)
self.appender(srvs)
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
self.show_info_message("Done!", Gtk.MessageType.INFO)
@run_idle
def update_active_elements(self, sensitive):
@@ -1104,9 +1313,7 @@ class YtListImportDialog:
@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)
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
def on_selected_toggled(self, toggle, path):
self._model.set_value(self._model.get_iter(path), 2, not toggle.get_active())

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2
<!-- Generated with glade 3.38.2
The MIT License (MIT)
@@ -36,41 +36,77 @@ Author: Dmitriy Yefremov
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkFrame" id="log_frame">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0.49000000953674316</property>
<property name="shadow_type">in</property>
<property name="can-focus">False</property>
<property name="label-xalign">0.49000000953674316</property>
<property name="shadow-type">none</property>
<child>
<object class="GtkBox" id="main_box">
<object class="GtkViewport">
<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>
<property name="can-focus">False</property>
<child>
<object class="GtkBox" id="header_box">
<object class="GtkBox" id="main_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">2</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</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="GtkButton" id="clear_button">
<object class="GtkBox" id="header_box">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Clear</property>
<property name="halign">center</property>
<property name="valign">center</property>
<signal name="clicked" handler="on_clear" swapped="no"/>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="clear_button_image">
<object class="GtkButton" id="clear_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-clear</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Clear</property>
<property name="halign">center</property>
<property name="valign">center</property>
<signal name="clicked" handler="on_clear" swapped="no"/>
<child>
<object class="GtkImage" id="clear_button_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="stock">gtk-clear</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="close_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Close</property>
<property name="always-show-image">True</property>
<signal name="clicked" handler="on_close" swapped="no"/>
<child>
<object class="GtkImage" id="close_button_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="stock">gtk-close</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack-type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
@@ -80,65 +116,47 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkButton" id="close_button">
<object class="GtkScrolledWindow" id="scrolled_window">
<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"/>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkImage" id="close_button_image">
<object class="GtkTextView" id="log_view">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-close</property>
<property name="can-focus">True</property>
<property name="editable">False</property>
<property name="left-margin">5</property>
<property name="right-margin">5</property>
<property name="top-margin">5</property>
<property name="bottom-margin">5</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="view"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="log_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="editable">False</property>
<property name="left_margin">5</property>
<property name="right_margin">5</property>
<property name="top_margin">5</property>
<property name="bottom_margin">5</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="view"/>
</style>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="log_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="margin-bottom">2</property>
<property name="label" translatable="yes">Logs</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>

783
app/ui/m3u.glade Normal file
View File

@@ -0,0 +1,783 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.40.0
The MIT License (MIT)
Copyright (c) 2018-2024 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-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellite list editor. -->
<!-- interface-copyright 2018-2024 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkBox" id="export_m3u_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">10</property>
<property name="margin-bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkViewport" id="export_viewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<!-- n-columns=2 n-rows=3 -->
<object class="GtkGrid" id="export_m3u_grid">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="row-spacing">5</property>
<property name="column-spacing">5</property>
<child>
<object class="GtkLabel" id="export_port_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Port:</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="export_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Export:</property>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkButtonBox" id="export_types_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="layout-style">expand</property>
<child>
<object class="GtkRadioButton" id="export_all_button">
<property name="label" translatable="yes">All types</property>
<property name="name">header-button</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>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="export_iptv_button">
<property name="label" translatable="yes">IPTV only</property>
<property name="name">header-button</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">export_all_button</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left-attach">1</property>
<property name="top-attach">1</property>
</packing>
</child>
<child>
<object class="GtkButtonBox" id="export_grp_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="layout-style">expand</property>
<child>
<object class="GtkRadioButton" id="export_grp_bq_button">
<property name="label" translatable="yes">Bouquets</property>
<property name="name">header-button</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>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="export_grp_markers_button">
<property name="label" translatable="yes">Markers</property>
<property name="name">header-button</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">export_grp_bq_button</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="export_grp_no_button">
<property name="label" translatable="yes">No</property>
<property name="name">header-button</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">export_grp_markers_button</property>
</object>
<packing>
<property name="expand">True</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">2</property>
</packing>
</child>
<child>
<object class="GtkBox" id="export_port_box">
<property name="visible">True</property>
<property name="sensitive" bind-source="export_iptv_button" bind-property="active" bind-flags="invert-boolean">True</property>
<property name="can-focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkEntry" id="export_port_entry">
<property name="visible">True</property>
<property name="sensitive" bind-source="export_auto_button" bind-property="active" bind-flags="invert-boolean">True</property>
<property name="can-focus">True</property>
<property name="width-chars">10</property>
<property name="primary-icon-name">document-edit-symbolic</property>
<property name="placeholder-text" translatable="yes">8001</property>
<property name="input-purpose">digits</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkToggleButton" id="export_auto_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">emblem-synchronizing-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Auto</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</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="left-attach">1</property>
<property name="top-attach">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<child>
<object class="GtkLabel" id="export_group_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Group by</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label">:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left-attach">0</property>
<property name="top-attach">2</property>
</packing>
</child>
</object>
</child>
<style>
<class name="view"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="export_status_box">
<property name="height-request">26</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkImage" id="export_info_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="icon-name">document-properties</property>
<property name="icon_size">2</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="export_bq_info_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">2</property>
<child>
<object class="GtkLabel" id="export_bq_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Bouquets</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label">:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="export_bq_count_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="label">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="export_service_info_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="spacing">1</property>
<child>
<object class="GtkLabel" id="export_services_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Services</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label">:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="export_services_count_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="label">0</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">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">1</property>
</packing>
</child>
</object>
<object class="GtkBox" id="import_m3u_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkFrame" id="import_m3u_frame">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="label-xalign">0.02</property>
<property name="shadow-type">none</property>
<child>
<object class="GtkViewport" id="import_viewport">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkBox" id="import_main_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="orientation">vertical</property>
<property name="spacing">10</property>
<child>
<object class="GtkButtonBox" id="import_type_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="layout-style">expand</property>
<child>
<object class="GtkRadioButton" id="current_bq_button">
<property name="label" translatable="yes">Current bouquet</property>
<property name="name">header-button</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">sub_bq_button</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="single_bq_button">
<property name="label" translatable="yes">Single bouquet</property>
<property name="name">header-button</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">current_bq_button</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="group_bq_button">
<property name="label" translatable="yes">Split by groups</property>
<property name="name">header-button</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">sub_bq_button</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="sub_bq_button">
<property name="label" translatable="yes">Create sub-bouquets</property>
<property name="name">header-button</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">current_bq_button</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="load_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">15</property>
<child type="center">
<object class="GtkProgressBar" id="progress_bar">
<property name="can-focus">False</property>
<property name="valign">center</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox" id="info_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="info_label">
<property name="visible" bind-source="spinner" bind-property="visible" bind-flags="invert-boolean">True</property>
<property name="can-focus">False</property>
<property name="ellipsize">end</property>
<property name="max-width-chars">30</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="load_label">
<property name="visible" bind-source="spinner" bind-property="visible">False</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Loading data...</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkSpinner" id="spinner">
<property name="visible" bind-source="spinner" bind-property="active">False</property>
<property name="can-focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</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="picon_box">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="picon_switch_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Download picons</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="picon_switch">
<property name="visible">True</property>
<property name="can-focus">True</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="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">1</property>
</packing>
</child>
<child>
<object class="GtkBox" id="epg_src_box">
<property name="height-request">30</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkLabel" id="epg_source_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">EPG source</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label">:</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkComboBoxText" id="epg_links_box">
<property name="can-focus">True</property>
<property name="halign">start</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="active">0</property>
<property name="has-entry">True</property>
<child internal-child="entry">
<object class="GtkEntry">
<property name="can-focus">False</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="epg_info_label">
<property name="visible" bind-source="epg_links_box" bind-property="visible" bind-flags="invert-boolean">True</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="label" translatable="yes">Not found.</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkBox" id="add_epg_src_box">
<property name="visible" bind-source="epg_links_box" bind-property="visible">False</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkLabel" id="add_epg_src_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">Add to EPG sources list</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="add_epg_src_switch">
<property name="visible">True</property>
<property name="can-focus">True</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="pack-type">end</property>
<property name="position">4</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
<style>
<class name="view"/>
</style>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="import_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-bottom">2</property>
<property name="label" translatable="yes">Import</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
</interface>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2023 Dmitriy Yefremov
# Copyright (c) 2018-2024 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,7 +33,8 @@ __all__ = ("insert_marker", "move_items", "rename", "ViewTarget", "set_flags", "
"is_only_one_item_selected", "gen_bouquets", "BqGenType", "get_selection", "get_service_reference",
"get_model_data", "remove_all_unused_picons", "get_picon_pixbuf", "get_base_itrs", "get_iptv_url",
"get_iptv_data", "update_entry_data", "append_text_to_tview", "on_popup_menu", "get_picon_file_name",
"update_toggle_model", "update_popup_filter_model", "update_filter_sat_positions", "get_pos_num")
"update_toggle_model", "update_popup_filter_model", "update_filter_sat_positions", "get_pos_num",
"show_info_bar_message", "gen_bouquet_name")
import os
import re
@@ -44,13 +45,13 @@ from itertools import groupby
from pathlib import Path
from urllib.parse import unquote
from gi.repository import GdkPixbuf, GLib
from gi.repository import GdkPixbuf, GLib, Gio
from app.eparser import Service
from app.eparser.ecommons import Flag, BouquetService, Bouquet, BqType
from app.eparser.enigma.bouquets import BqServiceType, to_bouquet_id
from app.eparser.enigma.bouquets import BqServiceType
from app.settings import SettingsType, SEP, IS_WIN, IS_DARWIN, IS_LINUX
from .dialogs import show_dialog, DialogType, get_message
from .dialogs import show_dialog, DialogType, translate
from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON, KeyboardKey, Column
@@ -293,7 +294,7 @@ def set_lock(blacklist, services, model, paths, target, services_model):
fav_id = model.get_value(itr, Column.SRV_FAV_ID if target is ViewTarget.SERVICES else Column.FAV_ID)
srv = services.get(fav_id, None)
if srv and srv.service_type not in skip_type:
bq_id = srv.data_id if srv.service_type == BqServiceType.IPTV.name else to_bouquet_id(srv)
bq_id = srv.data_id if srv.service_type == BqServiceType.IPTV.name else srv.fav_id
if not bq_id:
continue
blacklist.discard(bq_id) if locked else blacklist.add(bq_id)
@@ -430,7 +431,7 @@ def assign_picons(target, srv_view, fav_view, transient, picons, settings, servi
picons_files = []
if not src_path:
dialog = get_picon_dialog(transient, get_message("Picon selection"), get_message("Open"), False)
dialog = get_picon_dialog(transient, translate("Picon selection"), translate("Open"), False)
if dialog.run() in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT) or not dialog.get_filenames():
return picons_files
@@ -565,6 +566,19 @@ def get_picon_pixbuf(path, size=32):
pass # NOP
def get_pixbuf_from_data(img_data, w=48, h=32):
if img_data:
f = Gio.MemoryInputStream.new_from_data(img_data)
return GdkPixbuf.Pixbuf.new_from_stream_at_scale(f, w, h, True, None)
def get_pixbuf_at_scale(path, width, height, p_ratio):
try:
return GdkPixbuf.Pixbuf.new_from_file_at_scale(path, width, height, p_ratio)
except GLib.GError:
pass
@lru_cache(50)
def get_picon_file_name(service_name):
""" Returns picon file name by service name. """
@@ -594,7 +608,7 @@ def gen_bouquets(app, gen_type):
cond = srv.package if gen_type is BqGenType.PACKAGE else srv.pos if gen_type is BqGenType.SAT else srv.service_type
if gen_type is BqGenType.TYPE and cond == "Data":
msg = f"{get_message('Selected type:')} '{cond}'\n\n{get_message('Are you sure?')}"
msg = f"{translate('Selected type:')} '{cond}'\n\n{translate('Are you sure?')}"
if show_dialog(DialogType.QUESTION, app.app_window, msg) != Gtk.ResponseType.OK:
return
@@ -672,6 +686,19 @@ def get_bouquets_names(model):
return bouquets_names
def gen_bouquet_name(bouquets, base_name, bq_type):
""" Generates a name for new bouquets. """
count = 0
key = f"{base_name}:{bq_type}"
bq_name = base_name
while key in bouquets:
count += 1
bq_name = f"{base_name}{count}"
key = f"{bq_name}:{bq_type}"
return bq_name
def get_services_type_groups(services):
""" Returns services grouped by main types [TV, Radio, Data]. -> dict """
@@ -806,13 +833,16 @@ def append_text_to_tview(char, view):
def get_iptv_url(row, s_type, column=Column.FAV_ID):
""" Returns url from iptv type row """
""" Returns URL from IPTV type row. """
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:
url = data[0]
return unquote(url) if s_type is SettingsType.ENIGMA_2 else url
if s_type is SettingsType.ENIGMA_2:
if len(data) > 10 and "http" in data[10]:
url, sep, desc = data[10].partition("#DESCRIPTION")
return unquote(url.strip())
else:
return data[0]
def get_iptv_data(fav_id):
@@ -825,10 +855,27 @@ def get_iptv_data(fav_id):
def on_popup_menu(menu, event):
""" Shows popup menu for the view """
""" Shows popup menu for the view. """
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
menu.popup(None, None, None, None, event.button, event.time)
def show_info_bar_message(bar, label, text, message_type=Gtk.MessageType.INFO):
""" Shows a message for info bars. """
bar.set_visible(False)
label.set_text(translate(text))
bar.set_message_type(message_type)
bar.set_visible(True)
def redraw_image(area, cr, pixbuf):
""" Helper method to redraw (auto resize) image in the Gtk DrawingArea. """
cr.scale(area.get_allocated_width() / pixbuf.get_width(),
area.get_allocated_height() / pixbuf.get_height())
img_surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, 1, None)
cr.set_source_surface(img_surface, 0, 0)
cr.paint()
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-2024 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,10 +30,11 @@ import os
import re
import shutil
from enum import Enum
from html import escape
from pathlib import Path
from urllib.parse import urlparse, unquote
from gi.repository import GLib, GdkPixbuf, Gio
from gi.repository import GLib
from app.commons import run_idle, run_task, run_with_delay, log
from app.connections import upload_data, DownloadType, download_data, remove_picons
@@ -41,9 +42,9 @@ from app.settings import SettingsType, Settings, SEP, IS_DARWIN
from app.tools.picons import (PiconsParser, parse_providers, Provider, convert_to, download_picon, PiconsCzDownloader,
PiconsError)
from app.tools.satellites import SatellitesParser, SatelliteSource
from .dialogs import show_dialog, DialogType, get_message, get_builder, get_chooser_dialog
from .dialogs import show_dialog, DialogType, translate, get_builder, get_chooser_dialog
from .main_helper import (scroll_to, on_popup_menu, get_base_model, set_picon, get_picon_pixbuf, get_picon_dialog,
get_picon_file_name)
get_picon_file_name, get_pixbuf_from_data, get_pixbuf_at_scale)
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TV_ICON, Column, KeyboardKey, Page, ViewTarget
@@ -52,10 +53,11 @@ class PiconManager(Gtk.Box):
LYNG_SAT = "lyngsat"
PICON_CZ = "piconcz"
def __init__(self, app, settings, picon_ids, sat_positions, *args, **kwargs):
super().__init__(*args, **kwargs)
def __init__(self, app, settings, picon_ids, sat_positions, **kwargs):
super().__init__(**kwargs)
self._app = app
self._app.connect("data-open", self.on_open)
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)
@@ -192,6 +194,8 @@ class PiconManager(Gtk.Box):
column.set_cell_data_func(builder.get_object("picons_dest_renderer"), self.picon_data_func)
column = builder.get_object("src_picon_column")
column.set_cell_data_func(builder.get_object("picons_src_renderer"), self.picon_data_func)
column = builder.get_object("dest_title_column")
column.set_cell_data_func(builder.get_object("title_dest_renderer"), self.title_data_func)
# Settings
self._settings = settings
self._s_type = settings.setting_type
@@ -201,8 +205,8 @@ class PiconManager(Gtk.Box):
self.show()
if not len(self._picon_ids) and self._s_type is SettingsType.ENIGMA_2:
message = get_message("To automatically set the identifiers for picons,\n"
"first load the required services list into the main application window.")
message = translate("To automatically set the identifiers for picons,\n"
"first load the required services list into the main application window.")
self.show_info_message(message, Gtk.MessageType.WARNING)
self._satellite_label.show()
@@ -228,8 +232,11 @@ class PiconManager(Gtk.Box):
if is_explorer:
self.update_picons_data(self._picons_dest_view)
def on_open(self):
def on_open(self, app, page):
""" Opens picons from local path [in src view]. """
if page is not Page.PICONS:
return
response = show_dialog(DialogType.CHOOSER, self._app.app_window, settings=self._settings, title="Open folder")
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
@@ -291,7 +298,18 @@ class PiconManager(Gtk.Box):
yield True
def picon_data_func(self, column, renderer, model, itr, data):
renderer.set_property("pixbuf", self.get_pixbuf_at_scale(model.get_value(itr, 2), 72, 48, True))
renderer.set_property("pixbuf", get_pixbuf_at_scale(model.get_value(itr, 2), 72, 48, True))
def title_data_func(self, column, renderer, model, itr, data):
srv = self._services.get(model[itr][1], None)
if srv:
renderer.set_property("markup", self.get_picon_info_markup(srv))
def get_picon_info_markup(self, srv):
ext_info = "" if srv.service_type == "IPTV" else f" {srv.pos} {srv.freq}"
return (f'{escape(srv.picon_id)}\n\n'
f'<span size="small" weight="bold">{translate("Service")}: {escape(srv.service)}</span>\n'
f'<span size="small" style="italic">{srv.service_type}{ext_info}</span>')
def update_picons_from_file(self, view, uri):
""" Adds picons in the view on dragging from file system. """
@@ -303,18 +321,12 @@ class PiconManager(Gtk.Box):
model = get_base_model(view.get_model())
if path.is_file():
p = self.get_pixbuf_at_scale(f_path, 72, 48, True)
p = get_pixbuf_at_scale(f_path, 72, 48, True)
if p:
model.append((p, path.name, f_path))
elif path.is_dir():
self.update_picons_data(view, f_path)
def get_pixbuf_at_scale(self, path, width, height, p_ratio):
try:
return GdkPixbuf.Pixbuf.new_from_file_at_scale(path, width, height, p_ratio)
except GLib.GError:
pass
# ***************** Drag-and-drop ********************* #
def init_drag_and_drop(self):
@@ -386,7 +398,7 @@ class PiconManager(Gtk.Box):
paths = {r[1]: r.iter for r in dest_model}
for p_path in picons:
p = self.get_pixbuf_at_scale(p_path, 72, 48, True)
p = get_pixbuf_at_scale(p_path, 72, 48, True)
if p:
p_name = Path(p_path).name
itr = paths.get(p_name, None)
@@ -424,8 +436,8 @@ class PiconManager(Gtk.Box):
shutil.copy(src, dst)
for row in get_base_model(self._picons_dest_view.get_model()):
if name == row[1]:
row[0] = self.get_pixbuf_at_scale(row[-1], 72, 48, True)
img.set_from_pixbuf(self.get_pixbuf_at_scale(row[-1], 100, 60, True))
row[0] = get_pixbuf_at_scale(row[-1], 72, 48, True)
img.set_from_pixbuf(get_pixbuf_at_scale(row[-1], 100, 60, True))
gen = self.update_picon_in_lists(dst, fav_id)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
@@ -445,7 +457,7 @@ class PiconManager(Gtk.Box):
def on_add(self, item):
""" Adds (copies) picons from an external folder to the profile picons folder. """
dialog = get_picon_dialog(self._app_window, get_message("Add picons"), get_message("Add"))
dialog = get_picon_dialog(self._app_window, translate("Add picons"), translate("Add"))
if dialog.run() in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
@@ -537,15 +549,16 @@ class PiconManager(Gtk.Box):
settings = Settings(self._settings.settings)
settings.profile_picons_path = f"{dest_path}{SEP}"
settings.current_profile = self._settings.current_profile
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
self.show_info_message(translate("Please, wait..."), Gtk.MessageType.INFO)
self.run_func(lambda: upload_data(settings=settings,
download_type=DownloadType.PICONS,
done_callback=lambda: self.show_info_message(get_message("Done!"),
done_callback=lambda: self.show_info_message(translate("Done!"),
Gtk.MessageType.INFO),
files_filter=files_filter))
def on_download(self, app, page):
if page is Page.PICONS:
self._app.picons.clear()
self.on_picons_download()
def on_picons_download(self, item=None, files_filter=None, path=None):
@@ -562,7 +575,7 @@ class PiconManager(Gtk.Box):
return
self.run_func(lambda: remove_picons(settings=self._settings,
done_callback=lambda: self.show_info_message(get_message("Done!"),
done_callback=lambda: self.show_info_message(translate("Done!"),
Gtk.MessageType.INFO),
files_filter=files_filter))
@@ -602,10 +615,10 @@ class PiconManager(Gtk.Box):
if logo_url:
pix_data = self._picon_cz_downloader.get_logo_data(logo_url)
if pix_data:
pix = self.get_pixbuf(pix_data)
pix = get_pixbuf_from_data(pix_data)
model.set_value(itr, 0, pix if pix else TV_ICON)
size = self._settings.tooltip_logo_size
tooltip.set_icon(self.get_pixbuf(pix_data, size, size))
tooltip.set_icon(get_pixbuf_from_data(pix_data, size, size))
else:
self.update_logo_data(itr, model, logo_url)
tooltip.set_text(model.get_value(itr, 1))
@@ -616,7 +629,7 @@ class PiconManager(Gtk.Box):
def update_logo_data(self, itr, model, url):
pix_data = self._picon_cz_downloader.get_provider_logo(url)
if pix_data:
pix = self.get_pixbuf(pix_data)
pix = get_pixbuf_from_data(pix_data)
GLib.idle_add(model.set_value, itr, 0, pix if pix else TV_ICON)
@run_idle
@@ -626,7 +639,7 @@ class PiconManager(Gtk.Box):
tooltip = f"{link} (by Chocholoušek)"
elif self._download_src is self.DownloadSource.LYNG_SAT:
link = "https://www.lyngsat.com"
tooltip = f"{get_message('Providers')} [{link}]"
tooltip = f"{translate('Providers')} [{link}]"
else:
link = ""
tooltip = ""
@@ -699,20 +712,15 @@ class PiconManager(Gtk.Box):
def append_providers(self, providers, model):
if self._download_src is self.DownloadSource.LYNG_SAT:
for p in providers:
model.append(p._replace(logo=self.get_pixbuf(p.logo) if p.logo else TV_ICON))
model.append(p._replace(logo=get_pixbuf_from_data(p.logo) if p.logo else TV_ICON))
elif self._download_src is self.DownloadSource.PICON_CZ:
for p in providers:
logo_data = self._picon_cz_downloader.get_logo_data(p.ssid)
model.append(p._replace(logo=self.get_pixbuf(logo_data) if logo_data else TV_ICON))
model.append(p._replace(logo=get_pixbuf_from_data(logo_data) if logo_data else TV_ICON))
self.update_receive_button_state()
GLib.idle_add(self._satellite_label.set_visible, True)
def get_pixbuf(self, img_data, w=48, h=32):
if img_data:
f = Gio.MemoryInputStream.new_from_data(img_data)
return GdkPixbuf.Pixbuf.new_from_stream_at_scale(f, w, h, True, None)
def on_receive(self, item):
if self._is_downloading:
self._app.show_error_message("The task is already running!")
@@ -735,14 +743,14 @@ class PiconManager(Gtk.Box):
for prv in providers:
if self._download_src is self.DownloadSource.LYNG_SAT and not self._POS_PATTERN.match(prv[2]):
self.show_info_message(
get_message("Specify the correct position value for the provider!"), Gtk.MessageType.ERROR)
translate("Specify the correct position value for the provider!"), Gtk.MessageType.ERROR)
scroll_to(prv.path, self._providers_view)
return
try:
picons_path = self._current_path_label.get_text()
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
self.show_info_message(translate("Please, wait..."), Gtk.MessageType.INFO)
providers = (Provider(*p) for p in providers)
if self._download_src is self.DownloadSource.LYNG_SAT:
@@ -784,7 +792,7 @@ class PiconManager(Gtk.Box):
for future in not_done:
future.cancel()
concurrent.futures.wait(not_done)
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
self.show_info_message(translate("Done!"), Gtk.MessageType.INFO)
def get_picons_for_picon_cz(self, path, providers):
p_ids = None
@@ -800,7 +808,7 @@ class PiconManager(Gtk.Box):
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)
self.show_info_message(translate("Done!"), Gtk.MessageType.INFO)
def get_bouquet_picon_ids(self):
""" Returns picon ids for selected bouquet or None. """
@@ -828,13 +836,13 @@ class PiconManager(Gtk.Box):
@run_task
def resize(self, path):
self.show_info_message(get_message("Resizing..."), Gtk.MessageType.INFO)
self.show_info_message(translate("Resizing..."), Gtk.MessageType.INFO)
try:
from pathlib import Path
from PIL import Image
except ImportError as e:
self.show_info_message(f"{get_message('Conversion error.')} {e}", Gtk.MessageType.ERROR)
self.show_info_message(f"{translate('Conversion error.')} {e}", Gtk.MessageType.ERROR)
else:
res = (220, 132) if self._resize_220_132_radio_button.get_active() else (100, 60)
@@ -843,7 +851,7 @@ class PiconManager(Gtk.Box):
img = img.resize(res, Image.ANTIALIAS)
img.save(img_file, "PNG", optimize=True)
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
self.show_info_message(translate("Done!"), Gtk.MessageType.INFO)
def on_cancel(self, item=None):
if self._is_downloading and show_dialog(DialogType.QUESTION, self._app_window) == Gtk.ResponseType.CANCEL:
@@ -855,7 +863,7 @@ class PiconManager(Gtk.Box):
def terminate_task(self):
self._terminate = True
self._is_downloading = False
self.show_info_message(get_message("The task is canceled!"), Gtk.MessageType.WARNING)
self.show_info_message(translate("The task is canceled!"), Gtk.MessageType.WARNING)
@run_task
def run_func(self, func, update=False):
@@ -949,7 +957,7 @@ class PiconManager(Gtk.Box):
self.update_picon_info(name, path, srv)
def update_picon_info(self, name=None, path=None, srv=None):
self._picon_info_image.set_from_pixbuf(self.get_pixbuf_at_scale(path, 100, 60, True) if path else None)
self._picon_info_image.set_from_pixbuf(get_pixbuf_at_scale(path, 100, 60, True) if path else None)
self._picon_info_label.set_text(self.get_service_info(srv))
self._current_picon_info = (name, srv.fav_id) if srv else None
@@ -962,8 +970,8 @@ class PiconManager(Gtk.Box):
return self._app.get_hint_for_srv_list(srv)
header, ref = self._app.get_hint_header_info(srv)
return "{} {}: {}\n{}: {} {}: {}\n{}".format(header.rstrip(), get_message("Package"), srv.package,
get_message("System"), srv.system, get_message("Freq"), srv.freq,
return "{} {}: {}\n{}: {} {}: {}\n{}".format(header.rstrip(), translate("Package"), srv.package,
translate("System"), srv.system, translate("Freq"), srv.freq,
ref)
def on_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
@@ -1013,7 +1021,7 @@ class PiconManager(Gtk.Box):
convert_to(src_path=picons_path,
dest_path=save_path,
s_type=SettingsType.ENIGMA_2,
done_callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO))
done_callback=lambda: self.show_info_message(translate("Done!"), Gtk.MessageType.INFO))
@run_idle
def update_receive_button_state(self):
@@ -1031,12 +1039,7 @@ class PiconManager(Gtk.Box):
show_dialog(dialog_type, self._app_window, message)
def get_picons_format(self):
picon_format = SettingsType.ENIGMA_2
if self._neutrino_mp_radio_button.get_active():
picon_format = SettingsType.NEUTRINO_MP
return picon_format
return SettingsType.NEUTRINO_MP if self._neutrino_mp_radio_button.get_active() else SettingsType.ENIGMA_2
if __name__ == "__main__":

View File

@@ -1,241 +1,370 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkEventBox" id="event_box">
<!-- 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 satellite list editor. -->
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<signal name="button-press-event" handler="on_press" swapped="no"/>
<signal name="realize" handler="on_realize" swapped="no"/>
<child>
<placeholder/>
<object class="GtkDrawingArea" id="playback_area">
<property name="visible">True</property>
<property name="can-focus">False</property>
<signal name="draw" handler="on_draw" swapped="no"/>
<signal name="realize" handler="on_realize" swapped="no"/>
</object>
<packing>
<property name="name">playback</property>
<property name="title" translatable="yes">Playback</property>
</packing>
</child>
<child>
<object class="GtkSpinner" id="spinner">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="active">True</property>
</object>
<packing>
<property name="name">load</property>
<property name="title" translatable="yes">Load</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="playback"/>
</style>
</object>
<object class="GtkToolbar" id="tool_bar">
<object class="GtkBox" id="tool_bar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">end</property>
<property name="margin-start">12</property>
<property name="margin-end">12</property>
<property name="margin-top">12</property>
<property name="margin-bottom">12</property>
<property name="spacing">5</property>
<child>
<object class="GtkToolButton" id="prev_button">
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Previous stream in the list</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-media-previous</property>
<object class="GtkButton" id="prev_button">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Previous stream in the list</property>
<signal name="clicked" handler="on_previous" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="play_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Play</property>
<property name="action_name">app.on_play</property>
<property name="stock_id">gtk-media-play</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="stop_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Stop playback</property>
<property name="action_name">app.on_stop</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-media-stop</property>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="next_button">
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Next stream in the list</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-media-next</property>
<signal name="clicked" handler="on_next" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkToolItem" id="player_item">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkBox" id="rewind_box">
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel" id="current_time_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScale" id="scale">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="restrict_to_fill_level">False</property>
<property name="fill_level">0</property>
<property name="draw_value">False</property>
<property name="has_origin">False</property>
<signal name="change-value" handler="on_rewind" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="full_time_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">0</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<object class="GtkImage" id="prev_button_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">media-skip-backward-symbolic</property>
<property name="icon_size">2</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="play_button">
<property name="visible" bind-source="stop_button" bind-property="visible" bind-flags="invert-boolean">False</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<signal name="clicked" handler="on_play" swapped="no"/>
<child>
<object class="GtkImage" id="play_button_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">media-playback-start-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="stop_button">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Stop playback</property>
<signal name="clicked" handler="on_stop" swapped="no"/>
<child>
<object class="GtkImage" id="stop_button_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">media-playback-stop-symbolic</property>
<property name="icon_size">2</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="pause_button">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Pause</property>
<signal name="clicked" handler="on_pause" swapped="no"/>
<child>
<object class="GtkImage" id="pause_button_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">media-playback-pause-symbolic</property>
<property name="icon_size">2</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkButton" id="next_button">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Next stream in the list</property>
<signal name="clicked" handler="on_next" swapped="no"/>
<child>
<object class="GtkImage" id="next_button_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">media-skip-forward-symbolic</property>
<property name="icon_size">2</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="GtkBox" id="rewind_box">
<property name="width-request">175</property>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="spacing">2</property>
<child>
<object class="GtkLabel" id="current_time_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">0</property>
<attributes>
<attribute name="foreground" value="#ffffffffffff"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScale" id="scale">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="restrict-to-fill-level">False</property>
<property name="fill-level">0</property>
<property name="draw-value">False</property>
<property name="has-origin">False</property>
<signal name="change-value" handler="on_rewind" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="full_time_label">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="label" translatable="yes">0</property>
<attributes>
<attribute name="foreground" value="#ffffffffffff"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="homogeneous">True</property>
<property name="fill">True</property>
<property name="position">5</property>
</packing>
</child>
<child>
<object class="GtkToolItem" id="extras_item">
<object class="GtkBox" id="extras_box">
<property name="visible">True</property>
<property name="can_focus">False</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="GtkBox" id="extras_box">
<object class="GtkMenuButton" id="audio_menu_button">
<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="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Audio Track</property>
<child>
<object class="GtkMenuButton" id="audio_menu_button">
<object class="GtkImage" id="audio_menu_button_image">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="focus_on_click">False</property>
<property name="receives_default">True</property>
<property name="relief">none</property>
<child>
<object class="GtkImage" id="audio_menu_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Audio Track</property>
<property name="icon_name">audio-volume-high</property>
</object>
</child>
<property name="can-focus">False</property>
<property name="icon-name">audio-card-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkMenuButton" id="video_menu_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Aspect ratio</property>
<child>
<object class="GtkMenuButton" id="video_menu_button">
<object class="GtkImage" id="video_menu_button_image">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="focus_on_click">False</property>
<property name="receives_default">True</property>
<property name="relief">none</property>
<child>
<object class="GtkImage" id="video_menu_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Aspect ratio</property>
<property name="icon_name">view-restore</property>
</object>
</child>
<property name="can-focus">False</property>
<property name="icon-name">zoom-best-fit-symbolic</property>
<property name="icon_size">2</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="GtkMenuButton" id="subtitle_menu_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="focus-on-click">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Subtitle Track</property>
<child>
<object class="GtkMenuButton" id="subtitle_menu_button">
<object class="GtkImage" id="subtitle_menu_button_image">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="focus_on_click">False</property>
<property name="receives_default">True</property>
<property name="relief">none</property>
<child>
<object class="GtkImage" id="subtitle_menu_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Subtitle Track</property>
<property name="icon_name">format-text-underline</property>
</object>
</child>
<property name="can-focus">False</property>
<property name="icon-name">format-text-underline-symbolic</property>
<property name="icon_size">2</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">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">6</property>
</packing>
</child>
<child>
<object class="GtkButton" id="full_button">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Toggle in fullscreen</property>
<signal name="clicked" handler="on_full_screen" swapped="no"/>
<child>
<object class="GtkImage" id="full_button_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">view-fullscreen-symbolic</property>
<property name="icon_size">2</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
<property name="fill">True</property>
<property name="position">7</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="full_button">
<object class="GtkButton" id="close_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Toggle in fullscreen</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-fullscreen</property>
<signal name="clicked" handler="on_full_screen" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
<object class="GtkToolButton" id="close_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Close playback</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-close</property>
<property name="can-focus">False</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Close playback</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="icon-name">window-close-symbolic</property>
<property name="icon_size">2</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
<property name="fill">True</property>
<property name="position">8</property>
</packing>
</child>
</object>

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2023 Dmitriy Yefremov
# Copyright (c) 2018-2024 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 @@
""" Additional module for playback. """
from enum import Enum
from functools import lru_cache
from gi.repository import GLib, GObject, Gio
@@ -34,14 +35,20 @@ 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, USE_HEADER_BAR
from app.settings import PlayStreamsMode, PlaybackMode, IS_DARWIN, SettingsType, USE_HEADER_BAR
from app.tools.media import Player
from app.ui.dialogs import get_builder, get_message
from app.ui.dialogs import get_builder, translate
from app.ui.main_helper import get_iptv_url
from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, Column, Page
from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column, Page
class PlayerBox(Gtk.Box):
class PlayerBox(Gtk.Overlay):
class Page(str, Enum):
LOAD = "load"
PLAYBACK = "playback"
def __str__(self):
return self.value
def __init__(self, app, **kwargs):
super().__init__(**kwargs)
@@ -64,35 +71,48 @@ class PlayerBox(Gtk.Box):
self._app.connect("page-changed", self.on_page_changed)
self._app.connect("play-current", self.on_play_current)
self._app.connect("play-recording", self.on_play_recording)
self._s_type = self._app.app_settings.setting_type
self._fav_view = app.fav_view
self._page = None
self._player = None
self._current_mrl = None
self._full_screen = False
self._playback_window = None
self._audio_track_menu = None
self._subtitle_track_menu = None
self._play_mode = self._app.app_settings.play_streams_mode
self._is_cursor_visible = True
self._play_mode = PlayStreamsMode(self._app.app_settings.play_streams_mode)
handlers = {"on_realize": self.on_realize,
"on_draw": self.on_draw,
"on_mouse_motion": self.on_mouse_motion,
"on_press": self.on_press,
"on_play": self.on_play,
"on_pause": self.on_pause,
"on_stop": self.on_stop,
"on_next": self.on_next,
"on_previous": self.on_previous,
"on_rewind": self.on_rewind,
"on_full_screen": self.on_full_screen,
"on_close": self.on_close}
builder = get_builder(UI_RESOURCES_PATH + "playback.glade", handlers)
self.set_spacing(5)
self.set_orientation(Gtk.Orientation.VERTICAL)
self._event_box = builder.get_object("event_box")
self.pack_start(self._event_box, True, True, 0)
builder = get_builder(f"{UI_RESOURCES_PATH}playback.glade", handlers)
self._stack = builder.get_object("stack")
self._playback_area = builder.get_object("playback_area")
self._playback_area.set_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)
self.connect("motion-notify-event", self.on_mouse_motion)
self.add(self._stack)
if not IS_DARWIN:
self.pack_end(builder.get_object("tool_bar"), False, True, 0)
self.add_overlay(builder.get_object("tool_bar"))
self._scale = builder.get_object("scale")
self._full_time_label = builder.get_object("full_time_label")
self._current_time_label = builder.get_object("current_time_label")
self._rewind_box = builder.get_object("rewind_box")
self._tool_bar = builder.get_object("tool_bar")
self.bind_property("is_cursor_visible", self._tool_bar, "visible")
self._stop_button = builder.get_object("stop_button")
self._prev_button = builder.get_object("prev_button")
self._next_button = builder.get_object("next_button")
self._audio_menu_button = builder.get_object("audio_menu_button")
@@ -103,21 +123,28 @@ class PlayerBox(Gtk.Box):
self.connect("delete-event", self.on_delete)
self.connect("show", self.set_player_area_size)
self.connect("unrealize", self.on_unrealize)
@property
def playback_widget(self):
return self._playback_area
@GObject.Property(type=bool, default=True)
def is_cursor_visible(self):
return self._is_cursor_visible
@is_cursor_visible.setter
def is_cursor_hidden(self, value):
self._is_cursor_visible = value
def on_fav_clicked(self, app, mode):
if mode is not FavClickMode.STREAM and not self._app.http_api:
if mode is not PlaybackMode.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()
elif mode is FavClickMode.ZAP_PLAY:
self._app.on_zap(self.on_watch)
elif mode is FavClickMode.PLAY:
self.on_play_service()
self.start_playback(mode)
def on_srv_clicked(self, app, mode):
if not self._app.http_api:
@@ -131,18 +158,14 @@ class PlayerBox(Gtk.Box):
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)
if mode is PlaybackMode.PLAY:
self.play_service(ref)
elif mode is PlaybackMode.ZAP:
self.zap(ref)
elif mode is PlaybackMode.ZAP_PLAY:
self.zap(ref, self.play_current)
elif mode is PlaybackMode.STREAM:
self._app.show_error_message("Not allowed in this context!")
def on_iptv_clicked(self, app, mode):
if not self._app.http_api:
@@ -156,29 +179,34 @@ class PlayerBox(Gtk.Box):
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()
self.play_current()
def on_play_recording(self, app, url):
self.play(url)
def on_page_changed(self, app, page):
self.on_close()
self.set_visible(False)
self._page = page
if self._player and self.is_visible():
self.update_buttons() if not IS_DARWIN else None
self.on_close()
self.set_visible(False)
def on_realize(self, box):
def on_realize(self, area):
if not self._player:
settings = self._app.app_settings
self._stack.set_visible_child_name(self.Page.LOAD)
try:
self._player = Player.make(settings.stream_lib, settings.play_streams_mode, self._event_box)
self._player = Player.make(settings.stream_lib, settings.play_streams_mode, self)
except (ImportError, NameError) as e:
self._app.show_error_message(str(e))
return True
else:
self.init_playback_elements()
self.emit("play", self._current_mrl)
finally:
if settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
self.set_player_area_size(box)
self.on_play()
def on_unrealize(self, box):
if self._player:
self._player.release()
def init_playback_elements(self):
self._player.connect("error", self.on_error)
@@ -187,7 +215,7 @@ class PlayerBox(Gtk.Box):
self._player.connect("subtitle-track", self.on_subtitle_track_changed)
self._app.app_window.connect("key-press-event", self.on_key_press)
builder = get_builder(UI_RESOURCES_PATH + "app_menu.ui")
builder = get_builder(f"{UI_RESOURCES_PATH}app_menu.ui")
self._audio_track_menu = builder.get_object("audio_track_menu")
self._subtitle_track_menu = builder.get_object("subtitle_track_menu")
audio_menu = builder.get_object("audio_menu")
@@ -221,18 +249,28 @@ class PlayerBox(Gtk.Box):
subtitle_track_action.connect("activate", self.on_set_subtitle_track)
self._app.add_action(subtitle_track_action)
@run_idle
def on_play(self, action=None, value=None):
self.emit("play", None)
self._stack.set_visible_child_name(self.Page.LOAD)
self.emit("play", self._current_mrl)
def on_pause(self, action=None, value=None):
self.emit("pause", None)
def on_stop(self, action=None, value=None):
self._stop_button.set_visible(False) if not IS_DARWIN else None
self.emit("stop", None)
def on_next(self, button):
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, 1):
self.set_player_action()
self.switch_service(1)
def on_previous(self, button):
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, -1):
self.switch_service(-1)
def switch_service(self, count):
self._fav_view.grab_focus()
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, count):
self.update_buttons() if not IS_DARWIN else None
self.set_player_action()
def on_rewind(self, scale, scroll_type, value):
@@ -241,11 +279,8 @@ class PlayerBox(Gtk.Box):
def on_full_screen(self, item=None):
self._full_screen = not self._full_screen
if self._play_mode is PlayStreamsMode.BUILT_IN:
self._tool_bar.set_visible(not self._full_screen)
self.emit("playback-full-screen", not self._full_screen)
elif self._playback_window:
if not IS_DARWIN:
self._tool_bar.set_visible(not self._full_screen)
self._playback_window.fullscreen() if self._full_screen else self._playback_window.unfullscreen()
def on_close(self, action=None, value=None):
@@ -253,6 +288,9 @@ class PlayerBox(Gtk.Box):
self._app.app_settings.add("playback_window_size", self._playback_window.get_size())
self._playback_window.hide()
if self._full_screen:
GLib.idle_add(self.on_full_screen)
self.on_stop()
self.hide()
self.emit("playback-close", None)
@@ -306,21 +344,18 @@ class PlayerBox(Gtk.Box):
@run_with_delay(1)
def set_player_action(self):
click_mode = self._app.app_settings.fav_click_mode
self._fav_view.set_sensitive(False)
if click_mode is FavClickMode.PLAY:
self.on_play_service()
elif click_mode is FavClickMode.ZAP_PLAY:
self._app.on_zap(self.on_watch)
elif click_mode is FavClickMode.STREAM:
self.on_play_stream()
self.start_playback(PlaybackMode(self._app.app_settings.fav_click_mode))
def update_buttons(self):
if self._player:
path, column = self._fav_view.get_cursor()
current_index = path[0]
self._player_prev_button.set_sensitive(current_index != 0)
self._player_next_button.set_sensitive(len(self._fav_model) != current_index + 1)
if path:
current_index = path[0]
self._prev_button.set_sensitive(current_index != 0)
self._next_button.set_sensitive(len(self._fav_view.get_model()) != current_index + 1)
self._prev_button.set_visible(self._page is Page.SERVICES)
self._next_button.set_visible(self._page is Page.SERVICES)
@lru_cache(maxsize=1)
def on_duration_changed(self, duration):
@@ -345,6 +380,7 @@ class PlayerBox(Gtk.Box):
def set_player_area_size(self, widget):
w, h = self._app.app_window.get_size()
widget.set_size_request(w * 0.6, -1)
self._stack.set_visible_child_name(self.Page.PLAYBACK)
@run_idle
def show_playback_window(self, title=None):
@@ -363,7 +399,7 @@ class PlayerBox(Gtk.Box):
self._playback_window.connect("delete-event", self.on_close)
self._playback_window.connect("key-press-event", self.on_key_press)
self._playback_window.bind_property("visible", self._event_box, "visible")
self._playback_window.bind_property("visible", self._stack, "visible")
if not IS_DARWIN:
self._prev_button.set_visible(False)
@@ -382,8 +418,21 @@ class PlayerBox(Gtk.Box):
if path:
return f"DemonEditor [{self._app.fav_view.get_model()[path][:][Column.FAV_SERVICE]}]"
else:
return f"DemonEditor [{get_message('Recordings')}]"
return f"DemonEditor [{get_message('Playback')}]"
return f"DemonEditor [{translate('Recordings')}]"
return f"DemonEditor [{translate('Playback')}]"
def start_playback(self, mode):
self.on_stop() if mode is not PlaybackMode.ZAP else None
self._stack.set_visible_child_name(self.Page.LOAD)
if mode is PlaybackMode.PLAY:
self.on_play_service()
elif mode is PlaybackMode.ZAP:
self.on_zap()
elif mode is PlaybackMode.ZAP_PLAY:
self.on_zap(self.play_current)
elif mode is PlaybackMode.STREAM:
self.on_play_stream()
def on_play_stream(self):
path, column = self._fav_view.get_cursor()
@@ -397,34 +446,62 @@ class PlayerBox(Gtk.Box):
self.play(url) if url else self.on_error(None, "No reference is present!")
def on_play_service(self, item=None):
path, column = self._fav_view.get_cursor()
if not path or not self._app.http_api:
return
ref = self._app.get_service_ref(path)
""" Playback without switching channel on the Box."""
ref, path = self.get_ref()
if not ref:
return
if self._player and self._player.is_playing():
self.emit("stop", None)
self.play_service(ref)
def play_service(self, ref):
s_type = self._app.app_settings.setting_type
req = HttpAPI.Request.STREAM if s_type is SettingsType.ENIGMA_2 else HttpAPI.Request.N_STREAM
self._app.http_api.send(req, ref, self.watch)
def on_watch(self, item=None):
""" Switch to the channel and watch in the player. """
s_type = self._app.app_settings.setting_type
if s_type is SettingsType.ENIGMA_2:
self._app.http_api.send(HttpAPI.Request.STREAM_CURRENT, "", self.watch)
elif s_type is SettingsType.NEUTRINO_MP:
self._app.http_api.send(HttpAPI.Request.N_ZAP, "",
lambda rf: self._app.http_api.send(HttpAPI.Request.N_STREAM, rf.get("data", ""),
self.watch))
def on_zap(self, callback=None):
""" Switch(zap) the channel. """
ref, path = self.get_ref()
if not ref:
return
# IPTV type checking
row = self._fav_view.get_model()[path][:]
if row[Column.FAV_TYPE] == BqServiceType.IPTV.name and callback:
callback = self.play(get_iptv_url(row, self._s_type))
self.zap(ref, callback)
def get_ref(self):
""" Returns reference and currently selected path as a tuple. """
path, column = self._fav_view.get_cursor()
if not path or not self._app.http_api:
return
return self._app.get_service_ref(path), path
def zap(self, ref, callback=None):
if self._s_type is SettingsType.ENIGMA_2:
def zp(rq):
if rq and rq.get("e2state", False):
if callback:
callback()
else:
self._app.show_error_message("No connection to the receiver!")
self._app.http_api.send(HttpAPI.Request.ZAP, ref, zp)
elif self._s_type is SettingsType.NEUTRINO_MP:
def zp(rq):
if rq and rq.get("data", None) == "ok":
if callback:
callback()
else:
self._app.show_error_message("No connection to the receiver!")
self._app.http_api.send(HttpAPI.Request.N_ZAP, f"?{ref}", zp)
else:
self._app.show_error_message("This type of settings is not supported!")
def watch(self, data):
url = self._app.get_url_from_m3u(data)
GLib.timeout_add_seconds(1, self.play, url) if url else self.on_error(None, "Can't Playback!")
self.play(self._app.get_url_from_m3u(data))
def play(self, url, title=None):
if self._play_mode is PlayStreamsMode.M3U:
@@ -440,21 +517,49 @@ class PlayerBox(Gtk.Box):
elif self._play_mode is PlayStreamsMode.WINDOW:
self.show_playback_window(title)
self._current_mrl = url
if self._player:
self.emit("play", url)
else:
self._current_mrl = url
@run_idle
def play_current(self):
if self._s_type is SettingsType.ENIGMA_2:
self._app.http_api.send(HttpAPI.Request.STREAM_CURRENT, "", self.watch)
elif self._s_type is SettingsType.NEUTRINO_MP:
self._app.http_api.send(HttpAPI.Request.N_ZAP, "",
lambda rf: self._app.http_api.send(HttpAPI.Request.N_STREAM, rf.get("data", ""),
self.watch))
@run_with_delay(1)
def on_played(self, player, duration):
self._fav_view.set_sensitive(True)
self._stack.set_visible_child_name(self.Page.PLAYBACK)
if not IS_DARWIN:
self._stop_button.set_visible(True)
self.on_duration_changed(duration)
@run_idle
def on_error(self, player, msg):
self._app.show_error_message(msg)
self._fav_view.set_sensitive(True)
self._stack.set_visible_child_name(self.Page.PLAYBACK)
def on_draw(self, widget, cr):
""" Used for black background drawing in the player drawing area. """
cr.set_source_rgb(0, 0, 0)
cr.paint()
def on_mouse_motion(self, widget, event):
display = widget.get_display()
window = widget.get_window()
cursor = Gdk.Cursor.new_from_name(display, "default")
window.set_cursor(cursor)
self.hide_mouse_cursor(window, display)
self.is_cursor_visible = True
@run_with_delay(3)
def hide_mouse_cursor(self, window, display):
cursor = Gdk.Cursor.new_for_display(display, Gdk.CursorType.BLANK_CURSOR)
window.set_cursor(cursor)
self.is_cursor_visible = False
if __name__ == "__main__":

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2
<!-- Generated with glade 3.38.2
The MIT License (MIT)
@@ -36,30 +36,30 @@ Author: Dmitriy Yefremov
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkMenu" id="popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</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>
<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>
<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>
<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>
@@ -96,7 +96,7 @@ Author: Dmitriy Yefremov
</columns>
</object>
<object class="GtkTreeModelFilter" id="recordings_filter_model">
<property name="child_model">recordings_model</property>
<property name="child-model">recordings_model</property>
</object>
<object class="GtkTreeModelSort" id="recordings_sort_model">
<property name="model">recordings_filter_model</property>
@@ -105,121 +105,55 @@ Author: Dmitriy Yefremov
</object>
<object class="GtkBox" id="recordings_box">
<property name="visible">True</property>
<property name="can_focus">False</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>
<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>
<property name="can-focus">False</property>
<property name="label-xalign">0.49000000953674316</property>
<property name="shadow-type">none</property>
<child>
<object class="GtkBox" id="recordings_main_box">
<object class="GtkViewport" id="recordings_viewport">
<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>
<property name="can-focus">False</property>
<child>
<object class="GtkBox" id="recordings_header_box">
<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">5</property>
<property name="margin_bottom">5</property>
<property name="spacing">5</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">5</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkToggleButton" id="recordings_filter_button">
<object class="GtkBox" id="recordings_header_box">
<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"/>
<property name="can-focus">False</property>
<property name="margin-start">5</property>
<property name="margin-end">5</property>
<property name="margin-top">5</property>
<property name="margin-bottom">5</property>
<property name="spacing">5</property>
<child>
<object class="GtkImage" id="recordings_filter_button_image">
<object class="GtkToggleButton" id="recordings_filter_button">
<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>
<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>
@@ -228,227 +162,27 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkButton" id="recordings_search_down_button">
<object class="GtkButton" id="recordings_remove_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="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="GtkArrow" id="recordings_down_arrow">
<object class="GtkImage" id="remove_recording_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="arrow_type">down</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">False</property>
<property name="fill">True</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>
@@ -457,10 +191,96 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkLabel" id="recordings_count_label">
<object class="GtkBox" id="recordings_fs_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label">0</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="recordings_filter_entry">
<property name="visible" bind-source="recordings_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_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>
@@ -469,22 +289,210 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<placeholder/>
<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>
<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="sizing">fixed</property>
<property name="min-width">100</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-start">5</property>
<property name="margin-end">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>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
<style>
<class name="view"/>
</style>
</object>
</child>
<child type="label">
<object class="GtkLabel" id="recordings_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="margin-bottom">2</property>
<property name="label" translatable="yes">Recordings</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>
@@ -496,72 +504,81 @@ Author: Dmitriy Yefremov
<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>
<property name="can-focus">False</property>
<property name="label-xalign">0.49000000953674316</property>
<property name="shadow-type">none</property>
<child>
<object class="GtkScrolledWindow" id="paths_view_scrolled_window">
<property name="width_request">250</property>
<object class="GtkViewport" id="paths_viewport">
<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>
<property name="can-focus">False</property>
<child>
<object class="GtkTreeView" id="recordings_paths_view">
<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="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>
<property name="can-focus">True</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="shadow-type">in</property>
<property name="min-content-height">100</property>
<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>
<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>
<object class="GtkCellRendererPixbuf" id="ftp_icon_column_renderer">
<property name="xalign">0.019999999552965164</property>
<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>
<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>
<style>
<class name="view"/>
</style>
</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="can-focus">False</property>
<property name="margin-bottom">2</property>
<property name="label" translatable="yes">Paths</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</object>

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
# Copyright (c) 2018-2024 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
@@ -46,15 +46,16 @@ class RecordingsTool(Gtk.Box):
ROOT = ".."
DEFAULT_PATH = "/hdd"
def __init__(self, app, settings, *args, **kwargs):
super().__init__(*args, **kwargs)
def __init__(self, app, **kwargs):
super().__init__(**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._app.connect("filter-toggled", self.on_filter_toggled)
self._settings = settings
self._settings = app.app_settings
self._ftp = None
self._logos = {}
# Icon.
@@ -82,6 +83,7 @@ class RecordingsTool(Gtk.Box):
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_filter_button = builder.get_object("recordings_filter_button")
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)
@@ -92,7 +94,7 @@ class RecordingsTool(Gtk.Box):
renderer.set_fixed_size(size, size * 0.65)
srv_column.set_cell_data_func(renderer, self.logo_data_func)
if settings.alternate_layout:
if self._settings.alternate_layout:
self.on_layout_changed(app, True)
self.init()
@@ -293,9 +295,12 @@ class RecordingsTool(Gtk.Box):
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_filter_toggled(self, app, value):
if self._app.page is Page.RECORDINGS:
self._recordings_filter_button.set_active(not self._recordings_filter_button.get_active())
def on_recordings_filter_toggled(self, button):
if not button.get_active():
self._filter_entry.set_text("")
self._filter_entry.grab_focus() if button.get_active() else self._filter_entry.set_text("")
def on_recordings_key_press(self, view, event):
key_code = event.hardware_keycode

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-2024 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,7 +47,7 @@ _UI_PATH = UI_RESOURCES_PATH + "service_details_dialog.glade"
class ServiceDetailsDialog:
_ENIGMA2_DATA_ID = "{:04x}:{:08x}:{:04x}:{:04x}:{}:{}"
_ENIGMA2_FAV_ID = "{:X}:{:X}:{:X}:{:X}"
_ENIGMA2_FAV_ID = "1:0:{:X}:{:X}:{:X}:{:X}:{:X}:0:0:0"
_ENIGMA2_TRANSPONDER_DATA = "{} {}:{}:{}:{}:{}:{}:{}"
@@ -97,7 +97,7 @@ class ServiceDetailsDialog:
self._DIGIT_PATTERN = re.compile("\\D")
self._NON_EMPTY_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
self._CAID_PATTERN = re.compile("(?:^[\\s]*$)|(C:[0-9a-fA-F]{1,4})(,C:[0-9a-fA-F]{1,4})*")
self._PIDS_PATTERN = re.compile("(?:^[\\s]*$)|(c:[0-9]{2}[0-9a-fA-F]{4})(,c:[0-9]{2}[0-9a-fA-F]{4})*")
self._PIDS_PATTERN = re.compile("(?:^[\\s]*$)|(c:[0-9]{2}[0-9a-fA-F]{1,4})(,c:[0-9]{2}[0-9a-fA-F]{1,4})*?")
# Buttons
self._apply_button = builder.get_object("apply_button")
self._create_button = builder.get_object("create_button")
@@ -140,7 +140,7 @@ class ServiceDetailsDialog:
self._srv_type_entry = self._non_empty_elements.get("srv_type_entry")
self._service_type_combo_box = builder.get_object("service_type_combo_box")
self._cas_entry = builder.get_object("cas_entry")
self._reference_entry = builder.get_object("reference_entry")
self._reference_label = builder.get_object("reference_label")
self._keep_check_button = builder.get_object("keep_check_button")
self._hide_check_button = builder.get_object("hide_check_button")
self._use_pids_check_button = builder.get_object("use_pids_check_button")
@@ -159,7 +159,6 @@ class ServiceDetailsDialog:
self._pilot_combo_box = builder.get_object("pilot_combo_box")
self._pls_mode_combo_box = builder.get_object("pls_mode_combo_box")
self._tr_edit_switch = builder.get_object("tr_edit_switch")
self._tr_extra_expander = builder.get_object("tr_extra_expander")
self._DVB_S2_ELEMENTS = (self._mod_combo_box, self._rolloff_combo_box, self._pilot_combo_box,
self._pls_mode_combo_box, self._pls_code_entry, self._stream_id_entry)
@@ -186,7 +185,6 @@ class ServiceDetailsDialog:
elem.set_text(" ")
elem.set_text("")
self._new_check_button.set_active(True)
self._tr_extra_expander.activate()
self._service_type_combo_box.set_active(0)
self._pol_combo_box.set_active(0)
self._fec_combo_box.set_active(0)
@@ -366,8 +364,7 @@ class ServiceDetailsDialog:
tr_grid = self._builder.get_object("tr_grid")
tr_grid.remove_column(7)
tr_grid.set_margin_bottom(5)
self._builder.get_object("tr_extra_expander").set_visible(False)
self._builder.get_object("srv_separator").set_visible(False)
self._builder.get_object("extra_transponder_grid").set_visible(False)
self._package_entry.set_sensitive(False)
# ***************** Init Sat positions *********************#
@@ -527,7 +524,7 @@ class ServiceDetailsDialog:
package=self._package_entry.get_text(),
service_type=SERVICE_TYPE.get(self._srv_type_entry.get_text(), SERVICE_TYPE["3"]),
picon=self._old_service.picon,
picon_id=self._reference_entry.get_text().replace(":", "_") + ".png",
picon_id=self._reference_label.get_text().replace(":", "_") + ".png",
ssid="{:04x}".format(int(self._sid_entry.get_text())),
freq=freq,
rate=rate,
@@ -596,7 +593,7 @@ class ServiceDetailsDialog:
if self._s_type is SettingsType.ENIGMA_2:
namespace = int(self._namespace_entry.get_text())
data_id = self._ENIGMA2_DATA_ID.format(ssid, namespace, tr_id, net_id, service_type, 0)
fav_id = self._ENIGMA2_FAV_ID.format(ssid, tr_id, net_id, namespace)
fav_id = f"{self._reference_label.get_text()}:"
return fav_id, data_id
elif self._s_type is SettingsType.NEUTRINO_MP:
data = get_attributes(self._old_service.data_id)
@@ -618,7 +615,7 @@ class ServiceDetailsDialog:
freq = self._freq_entry.get_text()
rate = self._rate_entry.get_text()
pol = self._pol_combo_box.get_active_id()
pos = "{}{}".format(round(self._sat_pos_button.get_value(), 1), self._pos_side_box.get_active_id())
pos = f"{round(self._sat_pos_button.get_value(), 1)}{self._pos_side_box.get_active_id()}"
return freq, rate, pol, fec, system, pos
elif self._tr_type in (TrType.Terrestrial, TrType.ATSC):
return freq, o_srv.rate, o_srv.pol, fec, system, o_srv.pos
@@ -627,8 +624,8 @@ class ServiceDetailsDialog:
def get_satellite_transponder_data(self):
sys = self._sys_combo_box.get_active_id()
freq = "{}000".format(self._freq_entry.get_text())
rate = "{}000".format(self._rate_entry.get_text())
freq = f"{self._freq_entry.get_text()}000"
rate = f"{self._rate_entry.get_text()}000"
pol = self.get_value_from_combobox_id(self._pol_combo_box, POLARIZATION)
fec = self.get_value_from_combobox_id(self._fec_combo_box, FEC_DEFAULT)
sat_pos = self.get_sat_position()
@@ -648,9 +645,10 @@ class ServiceDetailsDialog:
pls_mode = self.get_value_from_combobox_id(self._pls_mode_combo_box, PLS_MODE)
pls_code = self._pls_code_entry.get_text()
st_id = self._stream_id_entry.get_text()
pls = ":{}:{}:{}".format(st_id, pls_code, pls_mode) if pls_mode and pls_code and st_id else ""
pls = f":{st_id}:{pls_code}:{pls_mode}" if pls_mode and pls_code and st_id else ""
return f"{dvb_s_tr}:{flag}:{mod}:{roll_off}:{pilot}{pls}"
return "{}:{}:{}:{}:{}{}".format(dvb_s_tr, flag, mod, roll_off, pilot, pls)
elif self._s_type is SettingsType.NEUTRINO_MP:
tr_data = get_attributes(self._old_service.transponder)
tr_data["frq"] = freq
@@ -661,7 +659,7 @@ class ServiceDetailsDialog:
tr_data["id"] = "{:04x}".format(int(self._transponder_id_entry.get_text()))
tr_data["inv"] = inv
return SP.join("{}{}{}".format(k, KSP, v) for k, v in tr_data.items())
return SP.join(f"{k}{KSP}{v}" for k, v in tr_data.items())
def get_sat_position(self):
sat_pos = self._sat_pos_button.get_value() * (-1 if self._pos_side_box.get_active_id() == "W" else 1)
@@ -669,11 +667,11 @@ class ServiceDetailsDialog:
return sat_pos
def get_terrestrial_transponder_data(self):
tr_data = re.split("\s|:", self._old_service.transponder)
tr_data = re.split(r"\s|:", self._old_service.transponder)
# frequency, bandwidth, code rate HP, code rate LP, modulation, transmission mode, guard interval, hierarchy,
# inversion, system, plp_id
# Bandwidth -> Pol, Rate HP -> FEC, TransmissionMode -> Roll off, GuardInterval -> Pilot, Hierarchy -> Pls Mode
tr_data[1] = "{}000".format(self._freq_entry.get_text())
tr_data[1] = f"{self._freq_entry.get_text()}000"
tr_data[2] = self.get_value_from_combobox_id(self._pol_combo_box, BANDWIDTH)
tr_data[3] = self.get_value_from_combobox_id(self._fec_combo_box, T_FEC)
tr_data[4] = self.get_value_from_combobox_id(self._rate_lp_combo_box, T_FEC)
@@ -684,28 +682,28 @@ class ServiceDetailsDialog:
tr_data[9] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
tr_data[10] = self.get_value_from_combobox_id(self._sys_combo_box, T_SYSTEM)
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
return f"{tr_data[0]} {':'.join(tr_data[1:])}"
def get_cable_transponder_data(self):
tr_data = re.split("\s|:", self._old_service.transponder)
tr_data = re.split(r"\s|:", self._old_service.transponder)
# frequency, symbol_rate, modulation, inversion, fec_inner, system;
tr_data[1] = "{}000".format(self._freq_entry.get_text())
tr_data[2] = "{}000".format(self._rate_entry.get_text())
tr_data[1] = f"{self._freq_entry.get_text()}000"
tr_data[2] = f"{self._rate_entry.get_text()}000"
tr_data[3] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
tr_data[4] = self.get_value_from_combobox_id(self._mod_combo_box, C_MODULATION)
tr_data[5] = self.get_value_from_combobox_id(self._fec_combo_box, FEC_DEFAULT)
tr_data[6] = get_value_by_name(SystemCable, self._sys_combo_box.get_active_id())
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
return f"{tr_data[0]} {':'.join(tr_data[1:])}"
def get_atsc_transponder_data(self):
tr_data = re.split("\s|:", self._old_service.transponder)
tr_data = re.split(r"\s|:", self._old_service.transponder)
# frequency, inversion, modulation, system
tr_data[1] = "{}000".format(self._freq_entry.get_text())
tr_data[1] = f"{self._freq_entry.get_text()}000"
tr_data[2] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
tr_data[3] = self.get_value_from_combobox_id(self._mod_combo_box, A_MODULATION)
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
return f"{tr_data[0]} {':'.join(tr_data[1:])}"
def update_transponder_services(self, transponder, sat_pos):
for itr in self._transponder_services_iters:
@@ -717,13 +715,13 @@ class ServiceDetailsDialog:
fav_id = srv[Column.SRV_FAV_ID]
old_srv = self._services.pop(fav_id, None)
if not old_srv:
log("Update transponder services error: No service found for ID {}".format(srv[Column.SRV_FAV_ID]))
log(f"Update transponder services error: No service found for ID {srv[Column.SRV_FAV_ID]}")
continue
if self._s_type is SettingsType.NEUTRINO_MP:
flags = get_attributes(srv[Column.SRV_CAS_FLAGS])
flags["position"] = sat_pos
srv[Column.SRV_CAS_FLAGS] = SP.join("{}{}{}".format(k, KSP, v) for k, v in flags.items())
srv[Column.SRV_CAS_FLAGS] = SP.join(f"{k}{KSP}{v}" for k, v in flags.items())
self._services[fav_id] = Service(*srv[:Column.SRV_TOOLTIP])
self._current_model.set_row(itr, srv)
@@ -797,10 +795,9 @@ class ServiceDetailsDialog:
nid = int(self._network_id_entry.get_text())
if self._s_type is SettingsType.ENIGMA_2:
on_id = int(self._namespace_entry.get_text())
ref = "1:0:{:X}:{:X}:{:X}:{:X}:{:X}:0:0:0".format(srv_type, ssid, tid, nid, on_id)
self._reference_entry.set_text(ref)
self._reference_label.set_text(self._ENIGMA2_FAV_ID.format(srv_type, ssid, tid, nid, on_id))
else:
self._reference_entry.set_text("{:x}{:04x}{:04x}".format(tid, nid, ssid))
self._reference_label.set_text(f"{tid:x}{nid:04x}{ssid:04x}")
def update_ui_for_terrestrial(self):
tr_grid = self.get_transponder_grid_for_non_satellite()
@@ -891,7 +888,6 @@ class ServiceDetailsDialog:
# FEC
fec_model.append(("None",))
# Extra
tr_box.remove(self._tr_extra_expander)
tr_grid.set_margin_bottom(5)
self._freq_entry.set_width_chars(10)
self._freq_entry.set_max_width_chars(10)

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2023 Dmitriy Yefremov
# Copyright (c) 2018-2024 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,10 +32,10 @@ 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, HeaderBar
from app.settings import SettingsType, Settings, PlayStreamsMode, PlaybackMode, IS_LINUX, SEP, IS_WIN
from app.ui.dialogs import show_dialog, DialogType, translate, get_chooser_dialog, get_builder
from .main_helper import update_entry_data, scroll_to, get_picon_pixbuf, show_info_bar_message
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, DEFAULT_ICON, APP_FONT, HeaderBar
class SettingsDialog:
@@ -53,7 +53,6 @@ class SettingsDialog:
"on_force_bq_name": self.on_force_bq_name,
"on_http_mode_switch": self.on_http_mode_switch,
"on_experimental_switch": self.on_experimental_switch,
"on_yt_dl_switch": self.on_yt_dl_switch,
"on_default_path_mode_switch": self.on_default_path_mode_switch,
"on_profile_add": self.on_profile_add,
"on_profile_edit": self.on_profile_edit,
@@ -129,13 +128,13 @@ class SettingsDialog:
self._backup_path_field = builder.get_object("backup_path_field")
self._recordings_path_field = builder.get_object("recordings_path_field")
self._default_data_paths_switch = builder.get_object("default_data_paths_switch")
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)
self._use_common_picon_path_switch = builder.get_object("use_common_picon_path_switch")
# Info bar.
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label")
self._test_spinner = builder.get_object("test_spinner")
# Settings type.
self._settings_type_box = builder.get_object("settings_type_combo_box")
self._enigma_radio_button = builder.get_object("enigma_radio_button")
self._neutrino_radio_button = builder.get_object("neutrino_radio_button")
# Streaming.
@@ -187,18 +186,10 @@ class SettingsDialog:
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._enable_exp_switch = builder.get_object("enable_experimental_switch")
# 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("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_grid"))
# Style.
style_provider = Gtk.CssProvider()
style_provider.load_from_path(f"{UI_RESOURCES_PATH}style.css")
@@ -223,29 +214,27 @@ class SettingsDialog:
if not IS_LINUX:
# Themes.
builder.get_object("style_frame").set_visible(IS_WIN)
builder.get_object("themes_support_frame").set_visible(True)
self._layout_switch = builder.get_object("layout_switch")
self._layout_switch.set_active(self._ext_settings.alternate_layout)
self._theme_frame = builder.get_object("theme_frame")
self._theme_frame.set_visible(True)
builder.get_object("dark_mode_box").set_visible(IS_WIN)
builder.get_object("style_box_view").set_visible(True)
self._theme_view = builder.get_object("theme_view")
self._theme_view.set_visible(True)
self._theme_thumbnail_image = builder.get_object("theme_thumbnail_image")
self._theme_combo_box = builder.get_object("theme_combo_box")
self._icon_theme_combo_box = builder.get_object("icon_theme_combo_box")
self._dark_mode_switch = builder.get_object("dark_mode_switch")
self._dark_mode_switch.set_active(self._ext_settings.dark_mode)
self._themes_support_switch = builder.get_object("themes_support_switch")
self._themes_support_switch.bind_property("active", self._theme_frame, "sensitive")
self._themes_support_switch.bind_property("active", self._theme_view, "sensitive")
self.init_themes()
def init_ui_elements(self):
is_enigma_profile = self._s_type is SettingsType.ENIGMA_2
self._neutrino_radio_button.set_active(self._s_type is SettingsType.NEUTRINO_MP)
self.update_picon_paths()
self.update_title()
self._dialog.set_title(f"{translate('Options')} [{self._settings_type_box.get_active_text()}]")
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(
is_enigma = self._s_type is SettingsType.ENIGMA_2
self.on_info_bar_close() if is_enigma else self.show_info_message(
"The Neutrino has only experimental support. Not all features are supported!", Gtk.MessageType.WARNING)
self._epg_dat_box.set_sensitive(is_enigma)
def init_profiles(self):
p_def = self._settings.default_profile
@@ -261,13 +250,6 @@ class SettingsDialog:
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:
self._dialog.set_title(title.format(get_message("Options"), self._enigma_radio_button.get_label()))
elif self._s_type is SettingsType.NEUTRINO_MP:
self._dialog.set_title(title.format(get_message("Options"), self._neutrino_radio_button.get_label()))
def update_picon_paths(self):
model = self._picons_paths_box.get_model()
model.clear()
@@ -292,7 +274,7 @@ class SettingsDialog:
update_entry_data(entry, self._dialog, self._settings)
def on_settings_type_changed(self, item):
s_type = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP
s_type = SettingsType(int(self._settings_type_box.get_active_id()))
if s_type is not self._s_type:
self._settings.setting_type = s_type
self._s_type = s_type
@@ -334,6 +316,7 @@ class SettingsDialog:
self._bouquet_hints_switch.set_active(self._settings.show_bq_hints)
self._services_hints_switch.set_active(self._settings.show_srv_hints)
self._default_data_paths_switch.set_active(self._settings.profile_folder_is_default)
self._use_common_picon_path_switch.set_active(self._settings.use_common_picon_path)
self._transcoding_switch.set_active(self._settings.activate_transcoding)
self._presets_combo_box.set_active_id(self._settings.active_preset)
self.on_transcoding_preset_changed(self._presets_combo_box)
@@ -363,17 +346,14 @@ class SettingsDialog:
self._new_color_button.set_rgba(new_rgb)
self._extra_color_button.set_rgba(extra_rgb)
if self._s_type is SettingsType.ENIGMA_2:
self._enigma_radio_button.activate()
else:
self._neutrino_radio_button.activate()
self._settings_type_box.set_active_id(str(self._s_type.value))
def on_apply_profile_settings(self, item=None):
if not self.is_data_correct(self._digit_elems):
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
self._s_type = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP
self._s_type = SettingsType(int(self._settings_type_box.get_active_id()))
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()]
@@ -406,6 +386,7 @@ class SettingsDialog:
self._ext_settings.show_bq_hints = self._bouquet_hints_switch.get_active()
self._ext_settings.show_srv_hints = self._services_hints_switch.get_active()
self._ext_settings.profile_folder_is_default = self._default_data_paths_switch.get_active()
self._ext_settings.use_common_picon_path = self._use_common_picon_path_switch.get_active()
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()
@@ -419,7 +400,6 @@ class SettingsDialog:
if not IS_LINUX:
self._ext_settings.dark_mode = self._dark_mode_switch.get_active()
self._ext_settings.alternate_layout = self._layout_switch.get_active()
self._ext_settings.is_themes_support = self._themes_support_switch.get_active()
self._ext_settings.theme = self._theme_combo_box.get_active_id()
self._ext_settings.icon_theme = self._icon_theme_combo_box.get_active_id()
@@ -495,10 +475,7 @@ class SettingsDialog:
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(False)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(get_message(text))
self._info_bar.set_visible(True)
show_info_bar_message(self._info_bar, self._message_label, text, message_type)
@run_idle
def show_spinner(self, show):
@@ -532,11 +509,8 @@ class SettingsDialog:
else:
self.on_info_bar_close()
def on_yt_dl_switch(self, switch, state):
self.show_info_message("Not implemented yet!", Gtk.MessageType.WARNING)
def on_default_path_mode_switch(self, switch, state):
self._settings.profile_folder_is_default = state
self._use_common_picon_path_switch.set_active(False) if state else None
def on_profile_add(self, item):
model = self._profile_view.get_model()
@@ -661,7 +635,7 @@ class SettingsDialog:
self._ext_settings.picons_paths = tuple(r[0] for r in model)
def on_remove_picon_path(self, button):
msg = f"{get_message('This may change the settings of other profiles!')}\n\n\t\t{get_message('Are you sure?')}"
msg = f"{translate('This may change the settings of other profiles!')}\n\n\t\t{translate('Are you sure?')}"
if show_dialog(DialogType.QUESTION, self._dialog, msg) != Gtk.ResponseType.OK:
return
@@ -694,17 +668,17 @@ class SettingsDialog:
if self._main_stack.get_visible_child_name() != "streaming":
return
mode = FavClickMode(int(self._double_click_combo_box.get_active_id()))
if mode is FavClickMode.PLAY:
mode = PlaybackMode(int(self._double_click_combo_box.get_active_id()))
if mode is PlaybackMode.PLAY:
self.show_info_message("Operates in standby mode or current active transponder!", Gtk.MessageType.WARNING)
elif mode is FavClickMode.STREAM:
elif mode is PlaybackMode.STREAM:
self.show_info_message("Playback IPTV streams only!", Gtk.MessageType.WARNING)
elif mode is FavClickMode.DISABLED:
elif mode is PlaybackMode.DISABLED:
self._allow_main_list_playback_switch.set_active(False)
else:
self.on_info_bar_close()
self._allow_main_list_playback_switch.set_sensitive(mode is not FavClickMode.DISABLED)
self._allow_main_list_playback_switch.set_sensitive(mode is not PlaybackMode.DISABLED)
def on_play_mode_changed(self, button):
if self._main_stack.get_visible_child_name() != "streaming":
@@ -799,7 +773,7 @@ class SettingsDialog:
response = get_chooser_dialog(self._dialog, self._settings, "Themes Archive [*.xz, *.zip]", ("*.xz", "*.zip"))
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
self._theme_frame.set_sensitive(False)
self._theme_view.set_sensitive(False)
self.unpack_theme(response, path, button)
@run_task
@@ -829,7 +803,7 @@ class SettingsDialog:
button.append(theme, theme)
button.set_active_id(theme)
self.show_info_message("Done!", Gtk.MessageType.INFO)
self._theme_frame.set_sensitive(True)
self._theme_view.set_sensitive(True)
@run_idle
def remove_theme(self, button, path):

View File

@@ -104,3 +104,8 @@ paned.vertical > separator {
padding-right: 5px;
min-width: 50px;
}
.playback {
background-color: #000000;
color: #ffffff;
}

View File

@@ -24,7 +24,7 @@
#
# Author: Dmitriy Yefremov
#
from app.ui.dialogs import get_message
from app.ui.dialogs import translate
from .uicommons import Gtk, GLib
@@ -37,7 +37,7 @@ class BGTaskWidget(Gtk.Box):
super().__init__(spacing=2, orientation=Gtk.Orientation.HORIZONTAL, valign=Gtk.Align.CENTER)
self._app = app
self._label = Gtk.Label(get_message(text))
self._label = Gtk.Label(translate(text))
self.pack_start(self._label, False, False, 0)
self._spinner = Gtk.Spinner(active=True)
@@ -46,7 +46,7 @@ class BGTaskWidget(Gtk.Box):
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_tooltip_text(translate("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)

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.2
<!-- Generated with glade 3.38.2
The MIT License (MIT)
@@ -41,43 +41,98 @@ Author: Dmitriy Yefremov
</child>
</object>
<object class="GtkTextBuffer" id="text_buffer">
<property name="tag_table">tag_table</property>
<property name="tag-table">tag_table</property>
</object>
<object class="GtkFrame" id="telnet_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>
<property name="can-focus">False</property>
<property name="label-xalign">0.49000000953674316</property>
<property name="shadow-type">none</property>
<child>
<object class="GtkBox" id="telnet_main_box">
<object class="GtkViewport" id="viewport">
<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>
<property name="can-focus">False</property>
<child>
<object class="GtkBox" id="commands_entry">
<object class="GtkBox" id="telnet_main_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_bottom">2</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">10</property>
<property name="margin-bottom">5</property>
<property name="orientation">vertical</property>
<property name="spacing">5</property>
<child>
<object class="GtkButton" id="connect_button">
<object class="GtkBox" id="commands_entry">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Connect</property>
<signal name="clicked" handler="on_connect" swapped="no"/>
<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">5</property>
<child>
<object class="GtkImage" id="connect_button_image">
<object class="GtkButton" id="connect_button">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-connect</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Connect</property>
<signal name="clicked" handler="on_connect" swapped="no"/>
<child>
<object class="GtkImage" id="connect_button_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="stock">gtk-connect</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="disconnect_button">
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Disconnect</property>
<signal name="clicked" handler="on_disconnect" swapped="no"/>
<child>
<object class="GtkImage" id="disconnect_button_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="stock">gtk-disconnect</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="clear_button">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">True</property>
<property name="tooltip-text" translatable="yes">Clear</property>
<property name="halign">center</property>
<property name="valign">center</property>
<signal name="clicked" handler="on_clear" swapped="no"/>
<child>
<object class="GtkImage" id="clear_button_image">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="stock">gtk-clear</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
@@ -87,90 +142,49 @@ Author: Dmitriy Yefremov
</packing>
</child>
<child>
<object class="GtkButton" id="disconnect_button">
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Disconnect</property>
<signal name="clicked" handler="on_disconnect" swapped="no"/>
<object class="GtkScrolledWindow" id="telnet_scrolled_window">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="shadow-type">in</property>
<child>
<object class="GtkImage" id="disconnect_button_image">
<object class="GtkTextView" id="text_view">
<property name="name">textview-large</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-disconnect</property>
<property name="can-focus">True</property>
<property name="wrap-mode">char</property>
<property name="left-margin">5</property>
<property name="right-margin">5</property>
<property name="buffer">text_buffer</property>
<property name="overwrite">True</property>
<property name="input-hints">GTK_INPUT_HINT_WORD_COMPLETION | GTK_INPUT_HINT_NONE</property>
<property name="monospace">True</property>
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
<signal name="realize" handler="on_text_view_realize" swapped="no"/>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="clear_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Clear</property>
<property name="halign">center</property>
<property name="valign">center</property>
<signal name="clicked" handler="on_clear" swapped="no"/>
<child>
<object class="GtkImage" id="clear_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-clear</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="telnet_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="text_view">
<property name="name">textview-large</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="wrap_mode">char</property>
<property name="left_margin">5</property>
<property name="right_margin">5</property>
<property name="buffer">text_buffer</property>
<property name="overwrite">True</property>
<property name="input_hints">GTK_INPUT_HINT_WORD_COMPLETION | GTK_INPUT_HINT_NONE</property>
<property name="monospace">True</property>
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
<signal name="realize" handler="on_text_view_realize" swapped="no"/>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="view"/>
</style>
</object>
</child>
<child type="label">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="can-focus">False</property>
<property name="margin-bottom">2</property>
<property name="label" translatable="yes">Telnet</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
</child>
</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-2024 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,9 +31,8 @@ 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 .dialogs import get_builder, translate, show_dialog, DialogType, BaseDialog
from .uicommons import Gtk, Gdk, GLib, UI_RESOURCES_PATH, Page, Column, KeyboardKey, MOD_MASK
from ..commons import run_idle, log
from ..connections import HttpAPI
@@ -55,9 +54,11 @@ class TimerTool(Gtk.Box):
EVENT = 1
CHANGE = 2
class TimerDialog(Gtk.Dialog):
class TimerDialog(BaseDialog):
def __init__(self, parent, action=None, timer_data=None, *args, **kwargs):
super().__init__(use_header_bar=USE_HEADER_BAR, *args, **kwargs)
super().__init__(parent=parent, title="Timer",
buttons=(translate("Cancel"), Gtk.ResponseType.CANCEL,
translate("Save"), Gtk.ResponseType.OK), *args, **kwargs)
self._action = action or TimerTool.TimerAction.ADD
self._timer_data = timer_data or {}
@@ -71,14 +72,6 @@ class TimerTool(Gtk.Box):
"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")
@@ -111,8 +104,7 @@ class TimerTool(Gtk.Box):
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)
self.get_content_area().pack_start(builder.get_object("timer_dialog_frame"), True, True, 0)
if self._action is TimerTool.TimerAction.ADD:
self.set_timer_for_add()
@@ -236,15 +228,15 @@ class TimerTool(Gtk.Box):
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_name_entry.set_text(self._timer_data.get("e2eventtitle", None) or "")
self._timer_desc_entry.set_text(self._timer_data.get("e2eventdescription", None) or "")
self._timer_service_entry.set_text(self._timer_data.get("e2eventservicename", None) or "")
self._timer_service_ref_entry.set_text(self._timer_data.get("e2eventservicereference", None) or "")
self._timer_event_id_entry.set_text(self._timer_data.get("e2eventid", None) or "")
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")))
start_time = int(self._timer_data.get("e2eventstart", "0") or "0")
self.set_time_data(start_time, start_time + int(self._timer_data.get("e2eventduration", "0") or "0"))
def set_time_data(self, start_time, end_time):
""" Sets values for time widgets. """
@@ -292,7 +284,7 @@ class TimerTool(Gtk.Box):
"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_timers_key_release": self.on_timers_key_release,
"on_timer_cursor_changed": self.on_timer_cursor_changed,
"on_timers_drag_data_received": self.on_timers_drag_data_received}
@@ -363,9 +355,11 @@ class TimerTool(Gtk.Box):
if p_count == 1:
service = self._app.current_services.get(model[paths][Column.FAV_ID], None)
if service:
if service and service.picon_id:
self.add_timer({"e2servicename": service.service,
"e2servicereference": service.picon_id.rstrip(".png").replace("_", ":")})
else:
self._app.show_error_message("Not allowed in this context!")
elif p_count > 1:
self._app.show_error_message("Please, select only one item!")
else:
@@ -465,7 +459,7 @@ class TimerTool(Gtk.Box):
else:
on_popup_menu(menu, event)
def on_timers_key_press(self, view, event):
def on_timers_key_release(self, view, event):
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
@@ -475,10 +469,10 @@ class TimerTool(Gtk.Box):
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()
elif ctrl and key is KeyboardKey.INSERT:
self.on_timer_add()
def on_timer_cursor_changed(self, view):
path, column = view.get_cursor()
@@ -489,8 +483,8 @@ class TimerTool(Gtk.Box):
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._action_info_label.set_text(translate(self.ACTION.get(timer.get("e2justplay", "0"), "0")))
self._after_info_label.set_text(translate(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)

View File

@@ -37,7 +37,7 @@ gi.require_version("Gtk", "3.0")
gi.require_version("Gdk", "3.0")
from gi.repository import Gtk, Gdk, GLib
from app.settings import Settings, SettingsException, IS_DARWIN, GTK_PATH, IS_LINUX
from app.settings import Settings, SettingsException, IS_DARWIN, IS_LINUX, GTK_PATH
# Setting mod mask for keyboard depending on platform
MOD_MASK = Gdk.ModifierType.MOD2_MASK if IS_DARWIN else Gdk.ModifierType.CONTROL_MASK
@@ -119,8 +119,10 @@ LOCKED_ICON = get_icon("changes-prevent-symbolic", 16, _IMAGE_MISSING)
HIDE_ICON = get_icon("go-jump", 16, _IMAGE_MISSING)
TV_ICON = get_icon("tv-symbolic", 16, _IMAGE_MISSING)
IPTV_ICON = get_icon("emblem-shared", 16, _IMAGE_MISSING)
LINK_ICON = get_icon("emblem-symbolic-link", 16, _IMAGE_MISSING)
FOLDER_ICON = get_icon("folder-symbolic" if IS_DARWIN else "folder", 16, _IMAGE_MISSING)
EPG_ICON = get_icon("gtk-index", 16, _IMAGE_MISSING)
DEFAULT_ICON = get_icon("emblem-default", 16, _IMAGE_MISSING)
DEFAULT_ICON = get_icon("emblem-default", 16, get_icon("emblem-default-symbolic", 16, _IMAGE_MISSING))
@lru_cache(maxsize=1)
@@ -186,15 +188,6 @@ class Page(Enum):
CONTROL = "control"
class FavClickMode(IntEnum):
""" Double click mode on the service in the bouquet(FAV) list. """
DISABLED = 0
STREAM = 1
PLAY = 2
ZAP = 3
ZAP_PLAY = 4
class ViewTarget(Enum):
""" Used for set target view. """
BOUQUET = 0

View File

@@ -22,3 +22,7 @@ grid > button {
popover .view {
background-color: transparent;
}
headerbar .titlebutton > image {
padding: 0;
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2023 Dmitriy Yefremov
# Copyright (c) 2018-2024 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
@@ -43,9 +43,9 @@ from app.eparser.ecommons import (PLS_MODE, get_key_by_value, POLARIZATION, FEC,
HIERARCHY, Inversion, C_MODULATION, FEC_DEFAULT, TerTransponder, CableTransponder,
Bouquet, BouquetService, BqServiceType, Bouquets, BqType)
from app.eparser.satxml import get_pos_str
from app.settings import USE_HEADER_BAR, Settings, CONFIG_PATH
from app.settings import Settings, CONFIG_PATH
from app.tools.satellites import SatellitesParser, SatelliteSource, ServicesParser
from ..dialogs import show_dialog, DialogType, get_message, get_builder
from ..dialogs import show_dialog, BaseDialog, DialogType, translate, get_builder
from ..main_helper import append_text_to_tview, get_base_model, on_popup_menu, get_services_type_groups
from ..search import SearchProvider
from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HeaderBar
@@ -53,25 +53,19 @@ from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HeaderBar
_DIALOGS_UI_PATH = f"{UI_RESOURCES_PATH}xml{os.sep}dialogs.glade"
class DVBDialog(Gtk.Dialog):
class DVBDialog(BaseDialog):
""" Base dialog class for editing DVB (-> *.xml) data. """
def __init__(self, parent, title, data=None, *args, **kwargs):
super().__init__(transient_for=parent,
title=get_message(title),
modal=True,
resizable=False,
default_width=320,
skip_taskbar_hint=True,
skip_pager_hint=True,
destroy_with_parent=True,
use_header_bar=USE_HEADER_BAR,
window_position=Gtk.WindowPosition.CENTER_ON_PARENT,
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK),
*args, **kwargs)
super().__init__(parent=parent, title=title, *args, **kwargs)
self.frame = Gtk.Frame(margin=5, label_xalign=0.02)
self.get_content_area().pack_start(self.frame, True, True, 0)
self._viewport = Gtk.Viewport(margin_top=2)
self._viewport.get_style_context().add_class("view")
self._frame = Gtk.Frame(margin=5, label_xalign=0.02, shadow_type=Gtk.ShadowType.NONE)
self._label = Gtk.Label(margin_bottom=2, use_markup=True)
self._frame.set_label_widget(self._label)
self._frame.add(self._viewport)
self.get_content_area().pack_start(self._frame, True, True, 0)
self._data = data
@@ -79,13 +73,19 @@ class DVBDialog(Gtk.Dialog):
def data(self):
return self._data
def set_content(self, widget):
self._viewport.add(widget)
def set_label_text(self, text):
self._label.set_markup(f"<b>{text}</b>")
class TransponderDialog(DVBDialog):
""" Base transponder dialog class. """
def __init__(self, parent, title, data=None, *args, **kwargs):
super().__init__(parent, title, data, *args, **kwargs)
self.frame.set_label(get_message("Transponder properties:"))
self.set_label_text(translate("Transponder properties:"))
# Pattern for digits entries.
self.digit_pattern = re.compile(r"\D")
# Style
@@ -124,8 +124,8 @@ class TCDialog(DVBDialog):
super().__init__(parent, title, data, *args, **kwargs)
self._entry = Gtk.Entry(margin=5)
self.frame.add(self._entry)
self.frame.set_label(get_message("Name:"))
self.set_content(self._entry)
self.set_label_text(translate("Name:"))
self.show_all()
if data:
@@ -140,8 +140,8 @@ class SatelliteDialog(DVBDialog):
builder = get_builder(_DIALOGS_UI_PATH, use_str=True,
objects=("sat_dialog_box", "side_store", "pos_adjustment"))
self.frame.add(builder.get_object("sat_dialog_box"))
self.frame.set_label(get_message("Satellite properties:"))
self.set_content(builder.get_object("sat_dialog_box"))
self.set_label_text(translate("Satellite properties:"))
self._sat_name = builder.get_object("sat_name_entry")
self._sat_position = builder.get_object("sat_position_button")
self._side = builder.get_object("side_box")
@@ -196,7 +196,7 @@ class SatTransponderDialog(TransponderDialog):
objects = ("sat_tr_box", "pol_store", "fec_store", "mod_store", "system_store", "pls_mode_store")
builder = get_builder(_DIALOGS_UI_PATH, handlers, use_str=True, objects=objects)
self.frame.add(builder.get_object("sat_tr_box"))
self.set_content(builder.get_object("sat_tr_box"))
self._freq_entry = builder.get_object("freq_entry")
self._rate_entry = builder.get_object("rate_entry")
self._pol_box = builder.get_object("pol_box")
@@ -268,7 +268,7 @@ class TerTransponderDialog(TransponderDialog):
handlers = {"on_entry_changed": self.on_entry_changed}
builder = get_builder(_DIALOGS_UI_PATH, handlers, use_str=True, objects=("ter_tr_box",))
self.frame.add(builder.get_object("ter_tr_box"))
self.set_content(builder.get_object("ter_tr_box"))
self._freq_entry = builder.get_object("ter_freq_entry")
self._sys_box = builder.get_object("ter_sys_box")
self._bandwidth_box = builder.get_object("ter_bandwidth_box")
@@ -346,7 +346,7 @@ class CableTransponderDialog(TransponderDialog):
handlers = {"on_entry_changed": self.on_entry_changed}
builder = get_builder(_DIALOGS_UI_PATH, handlers, use_str=True, objects=("cable_tr_box",))
self.frame.add(builder.get_object("cable_tr_box"))
self.set_content(builder.get_object("cable_tr_box"))
self._freq_entry = builder.get_object("cable_freq_entry")
self._rate_entry = builder.get_object("cable_rate_entry")
@@ -401,8 +401,6 @@ class UpdateDialog:
"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,
@@ -441,7 +439,7 @@ class UpdateDialog:
self._left_action_box = builder.get_object("sat_update_left_action_box")
self._right_action_box = builder.get_object("sat_update_right_action_box")
# Filter
self._filter_bar = builder.get_object("sat_update_filter_bar")
self._filter_bar_box = builder.get_object("filter_bar_box")
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")
@@ -449,18 +447,16 @@ class UpdateDialog:
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")
self._search_bar_box = builder.get_object("search_bar_box")
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)
builder.get_object("search_button").connect("toggled", search_provider.on_search_toggled)
# Satellite lists init on dialog start.
self._sat_view.connect("realize", self.on_update_satellites_list)
# Options.
@@ -545,6 +541,8 @@ class UpdateDialog:
@run_idle
def append_satellites(self, sats):
model = get_base_model(self._sat_view.get_model())
if not model:
return
for sat in sats:
itr = model.append(sat)
@@ -593,20 +591,14 @@ class UpdateDialog:
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())
self._search_bar_box.set_visible(button.get_active())
def on_filter_toggled(self, button: Gtk.ToggleToolButton):
self._filter_bar.set_search_mode(button.get_active())
self._filter_bar_box.set_visible(button.get_active())
@run_idle
def on_filter(self, item):
@@ -687,11 +679,18 @@ class SatellitesUpdateDialog(UpdateDialog):
self._merge_sat_switch = Gtk.Switch(active=self._dialog_settings.get("merge_satellites", False))
self._merge_sat_switch.connect("state-set", lambda b, s: self._dialog_settings.update({"merge_satellites": s}))
box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.HORIZONTAL)
box.pack_start(Gtk.Label(get_message("Merge satellites by positions")), False, True, 0)
box.pack_start(Gtk.Label(translate("Merge satellites by positions")), False, True, 0)
box.pack_end(self._merge_sat_switch, False, True, 0)
self._general_options_box.pack_start(box, True, True, 0)
self._general_options_box.show_all()
self._split_band_switch = Gtk.Switch(active=self._dialog_settings.get("split_by_band", False))
self._split_band_switch.connect("state-set", lambda b, s: self._dialog_settings.update({"split_by_band": s}))
box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.HORIZONTAL)
box.pack_start(Gtk.Label(translate("Split satellites by bands (C/KU)")), False, True, 0)
box.pack_end(self._split_band_switch, False, True, 0)
self._general_options_box.pack_start(box, True, True, 0)
self._general_options_box.show_all()
self._skip_c_band_switch.get_parent().set_visible(False)
@run_idle
@@ -762,6 +761,28 @@ class SatellitesUpdateDialog(UpdateDialog):
else:
sats = {s.name: s for s in sats} # key = name, v = satellite
# Post-processing if band separation is active.
if self._split_band_switch.get_active():
appender.send(f"Checking and splitting satellites by band...\n")
to_remove = []
new_sats = {}
for name, sat in sats.items():
# Checking for C/KU-transponders.
c_tr = []
ku_tr = []
[c_tr.append(t) if int(t.frequency) < 10000000 else ku_tr.append(t) for t in sat.transponders]
if ku_tr and c_tr:
c_sat = Satellite(f"{name} (C)", sat.flags, sat.position, c_tr)
ku_sat = Satellite(f"{name} (KU)", sat.flags, sat.position, ku_tr)
new_sats[c_sat.name] = c_sat
new_sats[ku_sat.name] = ku_sat
to_remove.append(name)
[sats.pop(n) for n in to_remove]
sats.update(new_sats)
appender.send("-" * _len + "\n")
for row in self._main_model:
pos = row[0]
if pos in sats:
@@ -829,7 +850,7 @@ class ServicesUpdateDialog(UpdateDialog):
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.set_label(translate("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()
@@ -849,11 +870,11 @@ class ServicesUpdateDialog(UpdateDialog):
self._kos_bq_lang_switch.connect("state-set", lambda b, s: self._dialog_settings.update({"kos_bq_lang": s}))
self._kos_options_box = Gtk.Box(spacing=5, orientation=Gtk.Orientation.VERTICAL)
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5, margin_top=5)
box.pack_start(Gtk.Label(get_message("Create Category bouquets")), False, True, 0)
box.pack_start(Gtk.Label(translate("Create Category bouquets")), False, True, 0)
box.pack_end(self._kos_bq_groups_switch, False, True, 0)
self._kos_options_box.add(box)
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5, margin_bottom=5)
box.pack_start(Gtk.Label(get_message("Create Regional bouquets")), False, True, 0)
box.pack_start(Gtk.Label(translate("Create Regional bouquets")), False, True, 0)
box.pack_end(self._kos_bq_lang_switch, False, True, 0)
self._kos_options_box.add(box)
self._kos_options_box.connect("realize", self.on_source_changed)
@@ -874,7 +895,7 @@ class ServicesUpdateDialog(UpdateDialog):
if not is_kos:
self._kos_bq_groups_switch.set_active(False)
self._kos_bq_lang_switch.set_active(False)
self._kos_options_box.set_tooltip_text(None if is_kos else get_message("KingOfSat only!"))
self._kos_options_box.set_tooltip_text(None if is_kos else translate("KingOfSat only!"))
@run_task
def receive_services(self):
@@ -978,14 +999,14 @@ class ServicesUpdateDialog(UpdateDialog):
no_lb = "No Category"
if self._kos_bq_groups_switch.get_active():
self.gen_bouquet_group(tv_services, tv_bouquets, lambda s: s[4] or no_lb)
self.gen_bouquet_group(rd_services, radio_bouquets, lambda s: s[4] or no_lb, bq_type=BqType.RADIO.value)
self.gen_bouquet_group(tv_services, tv_bouquets, lambda s: s[5] or no_lb)
self.gen_bouquet_group(rd_services, radio_bouquets, lambda s: s[5] or no_lb, bq_type=BqType.RADIO.value)
if self._kos_bq_lang_switch.get_active():
lb = "" if no_lb in {b.name for b in tv_bouquets} else "No Region"
self.gen_bouquet_group(tv_services, tv_bouquets, lambda s: s[5] or lb)
self.gen_bouquet_group(tv_services, tv_bouquets, lambda s: s[4] or lb)
lb = "" if no_lb in {b.name for b in radio_bouquets} else "No Region"
self.gen_bouquet_group(rd_services, radio_bouquets, lambda s: s[5] or lb, bq_type=BqType.RADIO.value)
self.gen_bouquet_group(rd_services, radio_bouquets, lambda s: s[4] or lb, bq_type=BqType.RADIO.value)
return Bouquets("", BqType.TV.value, tv_bouquets), Bouquets("", BqType.RADIO.value, radio_bouquets)

View File

@@ -2,7 +2,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2023 Dmitriy Yefremov
# Copyright (c) 2018-2024 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,7 +40,7 @@ from app.eparser.ecommons import (POLARIZATION, FEC, SYSTEM, MODULATION, T_SYSTE
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 ..dialogs import show_dialog, DialogType, get_chooser_dialog, translate, 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
@@ -60,6 +60,7 @@ class SatellitesTool(Gtk.Box):
super().__init__(**kwargs)
self._app = app
self._app.connect("data-open", self.on_open)
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)
@@ -368,7 +369,7 @@ class SatellitesTool(Gtk.Box):
data = func(path)
yield True
except FileNotFoundError as e:
msg = get_message("Please, download files from receiver or setup your path for read data!")
msg = translate("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}"
@@ -522,8 +523,10 @@ class SatellitesTool(Gtk.Box):
return self._ter_tr_view
return self._cable_tr_view
@run_idle
def on_open(self):
def on_open(self, app, page):
if page is not Page.SATELLITE:
return
xml_file = "satellites.xml"
if self._dvb_type is self.DVB.TERRESTRIAL:
xml_file = "terrestrial.xml"
@@ -554,18 +557,26 @@ class SatellitesTool(Gtk.Box):
@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")
self.save_data(self._settings.profile_data_path)
def save_data(self, path):
if self._dvb_type is self.DVB.SAT:
write_satellites((Satellite(*r) for r in self._satellite_view.get_model()), f"{path}satellites.xml")
elif self._dvb_type is self.DVB.TERRESTRIAL:
write_terrestrial((Terrestrial(*r) for r in self._terrestrial_view.get_model()), f"{path}terrestrial.xml")
else:
write_cable((Cable(*r) for r in self._cable_view.get_model()), f"{path}cables.xml")
def on_save_as(self, app, page):
self._app.show_error_message("Not implemented yet!")
if page is not Page.SATELLITE:
return
buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK)
response = show_dialog(DialogType.CHOOSER, self._app.app_window, settings=self._settings, buttons=buttons)
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
self.save_data(response)
def on_download(self, app, page):
if page is Page.SATELLITE:
@@ -576,7 +587,7 @@ class SatellitesTool(Gtk.Box):
self._app.upload_data(DownloadType.SATELLITES)
@run_idle
def on_update(self, item):
def on_update(self, item=None):
SatellitesUpdateDialog(self._app.get_active_window(), self._settings, self._satellite_view.get_model()).show()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,14 @@ The best way to run this program from source is using of [MSYS2](https://www.msy
`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`
* 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`
* For [MPV](https://mpv.io/) `pacman -S mingw-w64-x86_64-mpv`,
To reduce installation size or try the latest changes, we can install the *libmpv* [build](https://github.com/shinchiro/mpv-winbuild-cmake/releases) (**mpv-dev**-x86_64-v3-*.7z) by [shinchiro](https://github.com/shinchiro).
* Download and extract 7z archive.
* Copy libmpv-2.dll to *C:\msys64\mingw64\bin*
* libmpv.dll.a to *C:\msys64\mingw64\lib*
and folder *include\mpv to *C:\msys64\mingw64\include* path.
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`

View File

@@ -1,10 +1,12 @@
#!/bin/bash
VER="3.6.1_Beta"
VER="3.11.1_Beta"
B_PATH="dist/DemonEditor"
DEB_PATH="$B_PATH/usr/share/demoneditor"
mkdir -p $B_PATH
cp -TRv deb $B_PATH
rsync -arv ../../app/ui/lang/* "$B_PATH/usr/share/locale"
rsync --exclude=app/ui/lang --exclude=app/ui/icons --exclude=__pycache__ -arv ../../app $DEB_PATH
rsync --exclude=__pycache__ -arv ../../extensions $DEB_PATH

View File

@@ -1,5 +1,5 @@
Package: demon-editor
Version: 3.6.1-Beta
Version: 3.11.1-Beta
Section: utils
Priority: optional
Architecture: all
@@ -10,7 +10,8 @@ Depends: python3 (>= 3.6),
python3-gi-cairo,
gir1.2-notify-0.7,
p7zip-full
Recommends: libmpv1,
Recommends: ffmpeg,
libmpv1,
python3-chardet,
libgtksourceview (>= 3.0)
Maintainer: Dmitriy Yefremov <dmitry.v.yefremov@gmail.com>

View File

@@ -5,7 +5,7 @@ Source: https://github.com/DYefremov/DemonEditor
Files: *
MIT License
Copyright (c) 2018-2022 Dmitriy Yefremov
Copyright (c) 2018-2024 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

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

View File

@@ -2,14 +2,27 @@
Version=1.0
Name=DemonEditor
GenericName=Enigma2 bouquets editor
GenericName[it]=Editor di bouquet per Enigma2
GenericName[be]=Рэдактар букетаў Enigma2
GenericName[de]=Enigma2 Bouquet-Editor
GenericName[es]=Editor de ramos de Enigma2
GenericName[it]=Editor di bouquet Enigma2
GenericName[nl]=Enigma2 boeket editor
GenericName[pl]=Edytor bukietów Enigma2
GenericName[pt]=Editor de buquês Enigma2
GenericName[ru]=Редактор букетов Enigma2
GenericName[tr]=Enigma2 buket düzenleyici
GenericName[zh_CN]=Enigma2频道编辑器
Comment=Channel and satellite list editor for Enigma2
Comment[be]=Рэдактар спісу каналаў і супутнікаў для Enigma2
Comment[de]=Kanal- und Satellitenlisten-Editor für Enigma2
Comment[es]=Editor de lista de canales y satélites para Enigma2
Comment[it]=Editor di elenchi di canali e satelliti per Enigma2
Comment[nl]=Kanaal- en satellietlijsteditor voor Enigma2
Comment[pl]=Edytor list kanałów i satelitów dla Enigma2
Comment[pt]=Editor de lista de canais e satélites para 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
Comment[tr]=Enigma2 için Kanal ve uydu listesi düzenleyici
Comment[zh_CN]=Enigma2频道和卫星列表编辑器
Icon=demon-editor
Exec=/usr/bin/demon-editor
Terminal=false

View File

@@ -32,7 +32,7 @@ a = Analysis([EXE_NAME],
pathex=PATH_EXE,
binaries=None,
datas=ui_files,
hiddenimports=['fileinput', 'uuid'],
hiddenimports=['fileinput', 'uuid', 'asyncio'],
hookspath=[],
runtime_hooks=[],
hooksconfig={
@@ -81,8 +81,8 @@ app = BUNDLE(coll,
'CFBundleGetInfoString': "Enigma2 channel and satellite editor",
'LSApplicationCategoryType': 'public.app-category.utilities',
'LSMinimumSystemVersion': '10.13',
'CFBundleShortVersionString': f"3.6.1.{BUILD_DATE} Beta",
'NSHumanReadableCopyright': u"Copyright © 2023, Dmitriy Yefremov",
'CFBundleShortVersionString': f"3.11.1.{BUILD_DATE} Beta",
'NSHumanReadableCopyright': u"Copyright © 2024, Dmitriy Yefremov",
'NSRequiresAquaSystemAppearance': 'false',
'NSHighResolutionCapable': 'true'
})

View File

@@ -30,7 +30,7 @@ a = Analysis([EXE_NAME],
pathex=PATH_EXE,
binaries=[],
datas=ui_files,
hiddenimports=['fileinput', 'uuid', 'ctypes.wintypes'],
hiddenimports=['fileinput', 'uuid', 'ctypes.wintypes', 'asyncio'],
hookspath=[],
runtime_hooks=[],
hooksconfig={
@@ -57,9 +57,11 @@ exe = EXE(pyz,
name='DemonEditor',
debug=False,
bootloader_ignore_signals=False,
contents_directory='.',
strip=False,
upx=True,
console=False, icon='icon.ico')
console=False,
icon='icon.ico')
coll = COLLECT(exe,
a.binaries,
a.zipfiles,

View File

@@ -2,14 +2,27 @@
Version=1.0
Name=DemonEditor
GenericName=Enigma2 bouquets editor
GenericName[it]=Editor di bouquet per Enigma2
GenericName[be]=Рэдактар букетаў Enigma2
GenericName[de]=Enigma2 Bouquet-Editor
GenericName[es]=Editor de ramos de Enigma2
GenericName[it]=Editor di bouquet Enigma2
GenericName[nl]=Enigma2 boeket editor
GenericName[pl]=Edytor bukietów Enigma2
GenericName[pt]=Editor de buquês Enigma2
GenericName[ru]=Редактор букетов Enigma2
GenericName[tr]=Enigma2 buket düzenleyici
GenericName[zh_CN]=Enigma2频道编辑器
Comment=Channel and satellite list editor for Enigma2
Comment[be]=Рэдактар спісу каналаў і супутнікаў для Enigma2
Comment[de]=Kanal- und Satellitenlisten-Editor für Enigma2
Comment[es]=Editor de lista de canales y satélites para Enigma2
Comment[it]=Editor di elenchi di canali e satelliti per Enigma2
Comment[nl]=Kanaal- en satellietlijsteditor voor Enigma2
Comment[pl]=Edytor list kanałów i satelitów dla Enigma2
Comment[pt]=Editor de lista de canais e satélites para 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
Comment[tr]=Enigma2 için Kanal ve uydu listesi düzenleyici
Comment[zh_CN]=Enigma2频道和卫星列表编辑器
Icon=demon-editor
Exec=bash -c 'cd $(dirname %k) && ./start.py'
Terminal=false

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2018-2023 Dmitriy Yefremov
# Copyright (C) 2018-2024 Dmitriy Yefremov
# This file is distributed under the MIT license.
#
#
@@ -1358,8 +1358,8 @@ 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 "Enables URL parsing using yt-dlp to get direct links to media."
msgstr "Улучае аналіз URL-адрасоў з дапамогай yt-dlp для атрымання прамых спасылак на медыя."
msgid "Permissions..."
msgstr "Дазволы..."
@@ -1488,3 +1488,69 @@ msgstr "Альтэрнатыўны загаловак акна"
msgid "Selected type:"
msgstr "Абраны тып:"
msgid "Extension Manager"
msgstr "Менеджар пашырэнняў"
msgid "Ver."
msgstr "Вер."
msgid "Installed"
msgstr "Усталявана"
msgid "Use common folder for picons"
msgstr "Скарыстаць агульную тэчку для піконаў"
msgid "Activates single folder use for several profiles."
msgstr "Актывуе выкарыстанне адзінай тэчкі для некалькіх профіляў."
msgid "Events"
msgstr "Падзеі"
msgid "Markers"
msgstr "Маркеры"
msgid "IPTV only"
msgstr "Толькі IPTV"
msgid "No"
msgstr "Не"
msgid "Not found."
msgstr "Не знойдзена."
msgid "Current EPG cache contents."
msgstr "Змесціва бягучага EPG кэша."
msgid "Source error!"
msgstr "Памылка крыніцы!"
msgid "The EPG source for the favorites list is not set!"
msgstr "Не ўсталявана крыніца EPG для спіса абранага!"
msgid "Add to EPG sources list"
msgstr "Дадаць у спіс крыніц EPG"
msgid "Current bouquet"
msgstr "Абраны букет"
msgid "Single bouquet"
msgstr "Адзінкавы букет"
msgid "Split by groups"
msgstr "Падзел па групах"
msgid "Create sub-bouquets"
msgstr "Стварыць падбукеты"
msgid "Add image"
msgstr "Дадаць выяву"
msgid "TV Format"
msgstr "ТБ-фармат"
msgid "Use with Streamrelay"
msgstr "Скарыстаць Streamrelay"
msgid "Remove use with Streamrelay"
msgstr "Не выкарыстоўваць Streamrelay"

6
po/build.sh Normal file → Executable file
View File

@@ -1,3 +1,7 @@
#!/bin/bash
#xgettext --keyword=translatable --sort-output -L Glade -o po/demon-editor.po app/ui/main_window.glade
#msgfmt demon-editor.po -o demon-editor.mo
for dir in */;
do
msgfmt $dir* -o ../app/ui/lang/${dir%/}/LC_MESSAGES/demon-editor.mo
done

View File

@@ -1,8 +1,8 @@
# Copyright (C) 2018-2021 Dmitriy Yefremov
# Copyright (C) 2018-2024 Dmitriy Yefremov
# This file is distributed under the MIT license.
#
# Charly, 2019.
# Dmitriy Yefremov, 2020-2023.
# Dmitriy Yefremov, 2020-2024.
# Thomas Schmidt, 2021.
msgid ""
msgstr ""
@@ -797,7 +797,7 @@ msgid "Apply profile settings"
msgstr "Profileinstellungen anwenden"
msgid "Settings type:"
msgstr "Art der Einstellungen:"
msgstr "Einstellungstyp:"
msgid "Set default"
msgstr "Standard wiederherstellen"
@@ -1372,8 +1372,8 @@ 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 "Enables URL parsing using yt-dlp to get direct links to media."
msgstr "Aktiviert URL-Parsing mit yt-dlp, um direkte Links zu Medien zu erhalten."
msgid "Permissions..."
msgstr "Berechtigungen..."
@@ -1502,3 +1502,69 @@ msgstr "Alternativer Fenstertitel"
msgid "Selected type:"
msgstr "Ausgewählt Typ:"
msgid "Extension Manager"
msgstr "Erweiterungs-Manager"
msgid "Ver."
msgstr "Ver."
msgid "Installed"
msgstr "Installiert"
msgid "Use common folder for picons"
msgstr "Gemeinsamen Ordner für Picons verwenden"
msgid "Activates single folder use for several profiles."
msgstr "Aktiviert die Verwendung eines einzelnen Ordners für mehrere Profile."
msgid "Events"
msgstr "Events"
msgid "Markers"
msgstr "Markers"
msgid "IPTV only"
msgstr "Nur IPTV"
msgid "No"
msgstr "Nein"
msgid "Not found."
msgstr "Nicht gefunden."
msgid "Current EPG cache contents."
msgstr "Aktueller EPG-Cache-Inhalt."
msgid "Source error!"
msgstr "Quellfehler!"
msgid "The EPG source for the favorites list is not set!"
msgstr "Die EPG-Quelle für die Favoritenliste ist nicht eingestellt!"
msgid "Add to EPG sources list"
msgstr "Zur EPG-Quellenliste hinzufügen"
msgid "Current bouquet"
msgstr "Aktuelles Bouquet"
msgid "Single bouquet"
msgstr "Einzel Bouquet"
msgid "Split by groups"
msgstr "Aufteilung nach Gruppen"
msgid "Create sub-bouquets"
msgstr "Sub-Bouquets erstellen"
msgid "Add image"
msgstr "Bild hinzufügen"
msgid "TV Format"
msgstr "TV-Format"
msgid "Use with Streamrelay"
msgstr "Benutzung mit Streamrelay"
msgid "Remove use with Streamrelay"
msgstr "Streamrelay nutzung löschen"

View File

@@ -1386,8 +1386,8 @@ 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 "Enables URL parsing using yt-dlp to get direct links to media."
msgstr "Habilita el análisis de URL usando yt-dlp para obtener enlaces directos a los medios."
msgid "Permissions..."
msgstr "Permisos..."
@@ -1495,3 +1495,33 @@ 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."
msgid "Merge satellites by positions"
msgstr "Combinar satélites por posiciones"
msgid "Save satellite selection"
msgstr "Guardar selección de satélites"
msgid "Extract direct links"
msgstr "Extraer enlaces directos"
msgid "URL prefix:"
msgstr "Prefijo URL:"
msgid "Invalid prefix for the given URL!"
msgstr "¡Prefijo inválido para la URL dada!"
msgid "Alternate window title"
msgstr "Título alternativo de ventana"
msgid "Selected type:"
msgstr "Tipo elegido:"
msgid "Extension Manager"
msgstr "Gestor de extensiones"
msgid "Ver."
msgstr "Ver."
msgid "Installed"
msgstr "Instalado"

View File

@@ -1,12 +1,11 @@
# Copyright (C) 2018-2022 Dmitriy Yefremov
# Copyright (C) 2018-2024 Dmitriy Yefremov
# This file is distributed under the MIT license.
#
#
# Massimo Pissarello <mapi68@gmail.com>, 2022, 2023.
# SPDX-FileCopyrightText: 2022, 2023, 2024 Massimo Pissarello <mapi68@gmail.com>
msgid ""
msgstr ""
"Project-Id-Version: \n"
"PO-Revision-Date: 2023-04-13 02:15+0200\n"
"PO-Revision-Date: 2024-08-16 08:21+0200\n"
"Last-Translator: Massimo Pissarello <mapi68@gmail.com>\n"
"Language-Team: Italian <>\n"
"Language: it\n"
@@ -14,10 +13,10 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Lokalize 22.12.3\n"
"X-Generator: Lokalize 24.05.2\n"
msgid "translator-credits"
msgstr "Massimo Pissarello"
msgstr "Massimo Pissarello\nNicola Fanghella"
# Main
msgid "Service"
@@ -294,10 +293,10 @@ msgstr "Chiudi"
# Picons dialog
msgid "Load providers"
msgstr "Carica provider"
msgstr "Carica fornitori"
msgid "Providers"
msgstr "Provider"
msgstr "Fornitori"
msgid "Receive picons"
msgstr "Scarica picon"
@@ -336,16 +335,16 @@ msgid "Path to Enigma2 picons:"
msgstr "Persorso picon su Enigma2:"
msgid "Specify the correct position value for the provider!"
msgstr "Specifica il valore di posizione corretto per il provider!"
msgstr "Specifica il valore di posizione corretto per il fornitore!"
msgid "Converter between name formats"
msgstr "Convertitore tra formati di nome"
msgid "Receive picons for providers"
msgstr "Scarica picon per provider"
msgstr "Scarica picon per fornitore"
msgid "Load satellite providers."
msgstr "Carica provider satellitari"
msgstr "Carica fornitori satellitari"
msgid ""
"To automatically set the identifiers for picons,\n"
@@ -411,7 +410,7 @@ msgid "Reference"
msgstr "Riferimento"
msgid "Namespace"
msgstr "Spazio dei nomi"
msgstr "Namespace"
msgid "Flags:"
msgstr "Flag:"
@@ -512,8 +511,7 @@ msgstr "Non consentito in questo contesto!"
msgid "Please, download files from receiver or setup your path for read data!"
msgstr ""
"Per favore, scarica i file dal ricevitore o imposta il percorso da cui"
" leggere i dati!"
"Scarica i file dal ricevitore o imposta il percorso da cui leggere i dati!"
msgid "Reading data error!"
msgstr "Errore lettura dati!"
@@ -528,7 +526,7 @@ msgid "The text of marker is empty, please try again!"
msgstr "Il testo del marcatore è vuoto, riprova!"
msgid "Please, select only one item!"
msgstr "Per favore, seleziona solo un elemento!"
msgstr "Seleziona solo un elemento!"
msgid "No png file is selected!"
msgstr "Nessun file png selezionato!"
@@ -549,7 +547,7 @@ msgid "Done!"
msgstr "Fatto!"
msgid "Please, wait..."
msgstr "Attendere prego..."
msgstr "Attendi..."
msgid "Resizing..."
msgstr "Ridimensionamento..."
@@ -561,10 +559,10 @@ msgid "No satellite is selected!"
msgstr "Nessun satellite selezionato!"
msgid "Please, select only one satellite!"
msgstr "Per favore, seleziona solo un satellite!"
msgstr "Seleziona solo un satellite!"
msgid "Please check your parameters and try again."
msgstr "Per favore, controlla i tuoi parametri e riprova di nuovo."
msgstr "Controlla i parametri e riprova di nuovo."
msgid "No satellites.xml file is selected!"
msgstr "Nessun file satellites.xml selezionato!"
@@ -580,7 +578,7 @@ msgstr "VLC non trovato! Controlla che sia installato!"
# Search unavailable streams dialog
msgid "Please wait, streams testing in progress..."
msgstr "Attendere prego, test degli stream in corso..."
msgstr "Test degli stream in corso..."
msgid "Found"
msgstr "Trovato"
@@ -592,7 +590,7 @@ msgid "No changes required!"
msgstr "Non sono richiesti cambiamenti!"
msgid "This list does not contains IPTV streams!"
msgstr "L'elenco non contiene stream IPTV!"
msgstr "Questo elenco non contiene stream IPTV!"
msgid "New empty configuration"
msgstr "Nuova configurazione vuota"
@@ -728,7 +726,7 @@ msgid "XML file"
msgstr "File XML"
msgid "Use web source"
msgstr "Utilizza fonte web"
msgstr "Usa fonte web"
msgid "Url to *.xml.gz file:"
msgstr "Da URL a file *.xml.gz:"
@@ -780,7 +778,7 @@ msgstr ""
" bouquet!"
msgid "Use HTTP"
msgstr "Utilizza HTTP"
msgstr "Usa HTTP"
msgid "Close playback"
msgstr "Ferma riproduzione"
@@ -817,14 +815,14 @@ msgid "Language:"
msgstr "Lingua:"
msgid "Load the last open configuration at program startup"
msgstr "Carica l'ultima configurazione aperta all'avvio del programma"
msgstr "Carica ultima configurazione aperta all'avvio del programma"
msgid "Enable direct playback bar"
msgstr "Abilita barra di riproduzione diretta"
msgid "Enables direct sending and playback of media links on the receiver"
msgstr ""
"Consenti l'invio diretto e la riproduzione di collegamenti multimediali sul"
"Abilita l'invio e la riproduzione diretta di collegamenti multimediali sul"
" ricevitore"
msgid "Watch the channel in the program"
@@ -962,7 +960,7 @@ msgstr ""
" dell'elenco dei preferiti!"
msgid "Operates in standby mode or current active transponder!"
msgstr "Funziona in modalità standby o transponder attivo corrente!"
msgstr "Funziona in modalità standby o con il transponder attualmente attivo!"
msgid "No connection to the receiver!"
msgstr "Nessuna connessione con il ricevitore!"
@@ -1261,7 +1259,7 @@ msgstr ""
" picon!"
msgid "Streams detected:"
msgstr "Rilevati stream:"
msgstr "Stream rilevati:"
msgid "Download picons"
msgstr "Scarica picon"
@@ -1288,7 +1286,7 @@ msgid "Mark duplicates"
msgstr "Contrassegna duplicati"
msgid "Load only for selected bouquet"
msgstr "Scarica solo per i bouquet selezionati"
msgstr "Carica solo per i bouquet selezionati"
msgid "The task is canceled!"
msgstr "L'attività è stata annullata!"
@@ -1306,13 +1304,13 @@ msgid "Help"
msgstr "Aiuto"
msgid "HTTP API is not activated. Check your settings!"
msgstr "API HTTP non attivata. Controlla le tue impostazioni!"
msgstr "API HTTP non attivata. Controlla le impostazioni!"
msgid "Add picons"
msgstr "Aggiungi picon"
msgid "Logs"
msgstr "Log"
msgstr "Registri"
msgid "Title"
msgstr "Titolo"
@@ -1410,10 +1408,10 @@ msgstr "Tutti i bouquet"
msgid "Playback from the main list"
msgstr "Riproduci dall'elenco principale"
msgid "Enables URL parsing using youtube-dl to get direct links to media."
msgid "Enables URL parsing using yt-dlp to get direct links to media."
msgstr ""
"Abilita l'analisi degli URL utilizzando youtube-dl per ottenere collegamenti"
" diretti ai media."
"Abilita l'analisi degli URL utilizzando yt-dlp per ottenere collegamenti"
" diretti ai contenuti multimediali."
msgid "Permissions..."
msgstr "Permessi..."
@@ -1425,7 +1423,7 @@ msgid "EPG *.dat file:"
msgstr "File EPG *.dat:"
msgid "Use HTTP to reload data in the receiver"
msgstr "Utilizza HTTP per ricaricare i dati nel ricevitore"
msgstr "Usa HTTP per ricaricare i dati nel ricevitore"
msgid "Enable picons compression"
msgstr "Abilita compressione picon"
@@ -1459,7 +1457,7 @@ msgid "Region"
msgstr "Regione"
msgid "Provider"
msgstr "Provider"
msgstr "Fornitore"
msgid ""
"Enables upload as an archive if a large number of picon (> 1000) is"
@@ -1509,10 +1507,11 @@ msgid "Removed"
msgstr "Rimosso"
msgid "Enables overwriting existing main list services."
msgstr "Consente di sovrascrivere i servizi esistenti dell'elenco principale."
msgstr ""
"Abilita la sovrascrittura dei servizi esistenti dell'elenco principale."
msgid "Enables skipping services import from lamedb."
msgstr "Consente di saltare l'importazione dei servizi da lamedb."
msgstr "Abilita il salto dell'importazione dei servizi da lamedb."
msgid "Bouquets data only"
msgstr "Bouquet solo dati"
@@ -1550,3 +1549,70 @@ msgstr "Titolo alternativo della finestra"
msgid "Selected type:"
msgstr "Tipo selezionato:"
msgid "Extension Manager"
msgstr "Gestore estensioni"
msgid "Ver."
msgstr "Ver."
msgid "Installed"
msgstr "Installato"
msgid "Use common folder for picons"
msgstr "Usa la cartella comune per i picon"
msgid "Activates single folder use for several profiles."
msgstr "Attiva l'uso di una singola cartella per diversi profili."
msgid "Events"
msgstr "Eventi"
msgid "Markers"
msgstr "Marcatori"
msgid "IPTV only"
msgstr "Solo IPTV"
msgid "No"
msgstr "No"
msgid "Not found."
msgstr "Non trovato."
msgid "Current EPG cache contents."
msgstr "Contenuto attuale cache EPG."
msgid "Source error!"
msgstr "Errore fonte!"
msgid "The EPG source for the favorites list is not set!"
msgstr "La fonte EPG per l'elenco dei preferiti non è impostata!"
msgid "Add to EPG sources list"
msgstr "Aggiungi all'elenco delle fonti EPG"
msgid "Current bouquet"
msgstr "Bouquet attuale"
msgid "Single bouquet"
msgstr "Bouquet singolo"
msgid "Split by groups"
msgstr "Dividi per gruppi"
msgid "Create sub-bouquets"
msgstr "Crea sotto-bouquet"
msgid "Add image"
msgstr "Aggiungi immagine"
msgid "TV Format"
msgstr "Formato TV"
msgid "Use with Streamrelay"
msgstr "Usa con Streamrelay"
msgid "Remove use with Streamrelay"
msgstr "Rimuovi usa con Streamrelay"

View File

@@ -1346,8 +1346,8 @@ 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 "Enables URL parsing using yt-dlp to get direct links to media."
msgstr "Schakelt URL-parsing met behulp van yt-dlp in, om directe links naar media te krijgen."
msgid "Permissions..."
msgstr "Rechten..."
@@ -1455,3 +1455,33 @@ 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."
msgid "Merge satellites by positions"
msgstr "Satellieten samenvoegen op basis van positie"
msgid "Save satellite selection"
msgstr "Satellietselectie opslaan"
msgid "Extract direct links"
msgstr "Directe links uitpakken"
msgid "URL prefix:"
msgstr "Prefix URL:"
msgid "Invalid prefix for the given URL!"
msgstr "Ongeldige prefix voor de opgegeven URL!"
msgid "Alternate window title"
msgstr "Alternatieve venstertitel"
msgid "Selected type:"
msgstr "Geselecteerde type:"
msgid "Extension Manager"
msgstr "Uitbreidings Manager"
msgid "Ver."
msgstr "Ver."
msgid "Installed"
msgstr "Geïnstalleerd"

View File

@@ -4,16 +4,16 @@
#
msgid ""
msgstr ""
"Last-Translator: lareq <lareq@lareq.eu>\n"
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: acypaczom\n"
"Language-Team: \n"
"Language: pl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Language-Team: \n"
"X-Generator: Poedit 2.3\n"
"X-Generator: Poedit 3.0.1\n"
msgid "translator-credits"
msgstr ""
@@ -34,7 +34,7 @@ msgid "Picon"
msgstr "Pikon"
msgid "Freq"
msgstr "Freq"
msgstr "Częst"
msgid "Rate"
msgstr "Rate"
@@ -453,7 +453,7 @@ msgid "Preferences"
msgstr "Preferencje"
msgid "Profile:"
msgstr "Profile:"
msgstr "Profil:"
msgid "Timeout between commands in seconds"
msgstr "Limit czasu między poleceniami w sekundach"
@@ -1375,3 +1375,136 @@ msgstr "Wszystkie bukiety"
msgid "Playback from the main list"
msgstr "Odtwarzanie z listy głównej"
msgid "Enables URL parsing using yt-dlp to get direct links to media."
msgstr "Włącza parsowanie adresów URL przy użyciu yt-dlp w celu uzyskania bezpośrednich linków do multimediów."
msgid "Permissions..."
msgstr "Uprawnienia..."
msgid "Display EPG in bouquet list"
msgstr "Wyświetl EPG na liście bukietów"
msgid "EPG *.dat file:"
msgstr "Plik EPG *.dat:"
msgid "Use HTTP to reload data in the receiver"
msgstr "Użyj protokołu HTTP, aby ponownie załadować dane do odbiornika"
msgid "Enable picons compression"
msgstr "Włącz kompresję picon"
msgid "Update interval (sec):"
msgstr "Interwał aktualizacji (sec):"
msgid "Update:"
msgstr "Aktualizacja:"
msgid "Daily"
msgstr "Codziennie"
msgid "Assign reference"
msgstr "Przypisz referencję"
msgid "Specify hostname or IP address"
msgstr "Określ nazwę hosta lub adres IP"
msgid "Default selection"
msgstr "Domyślny wybór"
msgid "Don't change power state"
msgstr "Nie zmieniaj stanu zasilania"
msgid "Don't toggle standby mode when updating bouquets and services."
msgstr "Nie przełączaj w tryb czuwania podczas aktualizowania bukietów i usług."
msgid "Region"
msgstr "Region"
msgid "Provider"
msgstr "Dostawca"
msgid ""
"Enables upload as an archive if a large number of picon (> 1000) is selected.\n"
" Recommended only if you have external storage."
msgstr ""
"Umożliwia przesyłanie jako archiwum, jeśli wybrano dużą liczbę ikon (> 1000).\n"
" Zalecane tylko w przypadku posiadania pamięci zewnętrznej."
msgid "Clear \"New\" flag"
msgstr "Wyczyść znacznik „Nowy”."
msgid "Group by"
msgstr "Grupuj według"
msgid "Replace existing"
msgstr "Zastąp istniejące"
msgid "Already exists"
msgstr "Już istnieje"
msgid "Enable unlimited copy buffer"
msgstr "Włącz nieograniczony bufor kopiowania"
msgid "Enables unlimited copy buffer for the bouquets tab."
msgstr "Włącza nieograniczony bufor kopii dla zakładki bukiety."
msgid "Start time"
msgstr "Czas początku"
msgid "End time"
msgstr "Czas końca"
msgid "Enable extensions support"
msgstr "Włącz obsługę rozszerzeń"
msgid "After uploading the changes you may need to completely reboot the receiver!"
msgstr "Po załadowaniu zmian może być konieczne ponowne uruchomienie odbiornika!"
msgid "Remove duplicates"
msgstr "Usuń duplikaty"
msgid "Removed"
msgstr "Usunięty"
msgid "Enables overwriting existing main list services."
msgstr "zezwól na nadpisanie istniejących usług głównych serwisów ."
msgid "Enables skipping services import from lamedb."
msgstr "Umożliwia pomijanie importu usług z lamedb."
msgid "Bouquets data only"
msgstr "Tylko dane dotyczące bukietów"
msgid "Create Category bouquets"
msgstr "Utwórz bukiety kategorii"
msgid "Create Regional bouquets"
msgstr "Twórz regionalne bukiety"
msgid "Skip C-band"
msgstr "Pomiń pasmo C"
msgid "Automatically set the name selected in the bouquet list."
msgstr "Ustaw automatycznie wybraną nazwę na liście bukietów."
msgid "Merge satellites by positions"
msgstr "Połącz satelity według pozycji"
msgid "Save satellite selection"
msgstr "Zapisz wybór satelit"
msgid "Extract direct links"
msgstr "Wyodrębnij bezpośrednie linki"
msgid "URL prefix:"
msgstr "Prefiks adresu URL:"
msgid "Invalid prefix for the given URL!"
msgstr "Nieprawidłowy prefiks dla podanego adresu URL!"
msgid "Alternate window title"
msgstr "Alternatywny tytuł okna"
msgid "Selected type:"
msgstr "Wybrany typ:"

View File

@@ -1010,3 +1010,476 @@ msgstr "Captura de pantalla"
msgid "Video"
msgstr "Vidео"
msgid "The Neutrino has only experimental support. Not all features are supported!"
msgstr "Neutrino имеет только экспериментальную поддержку. Поддерживаются не все функции!"
msgid "Enable experimental features"
msgstr "Ativar funcionalidades experimentais"
msgid "Can't Playback!"
msgstr "Não consigo reproduzir!"
msgid "Enable Dark Mode"
msgstr "Ativar o modo escuro"
msgid "Extract..."
msgstr "Extrair..."
msgid "Unsupported format!"
msgstr "Formato não suportado!"
msgid "Combine with the current data?"
msgstr "Combinar com os dados actuais?"
msgid "Importing data done!"
msgstr "Importação de dados concluída!"
msgid "Current service"
msgstr "Serviço atual"
msgid "Open folder"
msgstr "Abrir pasta"
msgid "Open archive"
msgstr "Abrir ficheiro"
msgid "Import from Web"
msgstr "Importar da Web"
msgid "Control"
msgstr "Control"
msgid "Timers"
msgstr "Temporizadores"
msgid "Timer"
msgstr "Temporizador"
msgid "Add timer"
msgstr "Adicionar temporizador"
msgid "Hr."
msgstr "Hr."
msgid "Min."
msgstr "Min."
msgid "Power"
msgstr "Ligar"
msgid "Standby"
msgstr "Em espera"
msgid "Wake Up"
msgstr "Despertar"
msgid "Reboot"
msgstr "Reinicialização"
msgid "Restart GUI"
msgstr "Reiniciar a GUI"
msgid "Shutdown"
msgstr "Desativação"
msgid "Shut down"
msgstr "Desativar"
msgid "Do Nothing"
msgstr "Não fazer nada"
msgid "Auto"
msgstr "Auto"
msgid "Grab screenshot"
msgstr "Captura de ecrã"
msgid "Enabled:"
msgstr "Ativado:"
msgid "Name:"
msgstr "Nome:"
msgid "Description:"
msgstr "Descrição:"
msgid "Service:"
msgstr "Serviço:"
msgid "Service reference:"
msgstr "Referência do serviço:"
msgid "Event ID:"
msgstr "ID do evento:"
msgid "Begins:"
msgstr "Começa:"
msgid "Ends:"
msgstr "Termina:"
msgid "Repeated:"
msgstr "Repetido:"
msgid "Action:"
msgstr "Ação:"
msgid "After event:"
msgstr "Após o evento:"
msgid "Location:"
msgstr "Localização:"
msgid "Mo"
msgstr "Seg"
msgid "Tu"
msgstr "Ter"
msgid "We"
msgstr "Qua"
msgid "Th"
msgstr "Qui"
msgid "Fr"
msgstr "Sex"
msgid "Sa"
msgstr "Sáb"
msgid "Dom"
msgstr "Вс"
msgid "Set"
msgstr "Set"
msgid "Services update"
msgstr "Atualização de serviços"
msgid "Create folder"
msgstr "Criar pasta"
msgid "FTP client"
msgstr "Cliente FTP"
msgid "The file size is too large!"
msgstr "O ficheiro é demasiado grande!"
msgid "Connect"
msgstr "Ligar"
msgid "Disconnect"
msgstr "Desligar"
msgid "Size"
msgstr "Tamanho"
msgid "Date"
msgstr "Data"
msgid "Toggle display position"
msgstr "Alternar a posição de visualização"
msgid "Alternatives"
msgstr "Alternativas"
msgid "Add alternatives"
msgstr "Adicionar alternativas"
msgid "DreamOS only!"
msgstr "Apenas DreamOS!"
msgid "A similar service is already in this list!"
msgstr "Um serviço semelhante já consta desta lista!"
msgid "Play mode has been changed!\nRestart the program to apply the settings."
msgstr "O modo de reprodução foi alterado!\nReinicie o programa para aplicar as definições."
msgid "Set values for TID, NID and Namespace for correct naming of the picons!"
msgstr "Definir valores para TID, NID e Namespace para nomear corretamente os picons!"
msgid "Streams detected:"
msgstr "Fluxos detectados:"
msgid "Download picons"
msgstr "Descarregar picons"
msgid "Errors:"
msgstr "Erros:"
msgid "Use to play streams:"
msgstr "Utilizar para reproduzir fluxos:"
msgid "Font in the lists:"
msgstr "Fonte nas listas:"
msgid "Picons size in the lists:"
msgstr "Tamanho dos picões nas listas:"
msgid "Logo size in tooltips:"
msgstr "Tamanho do logótipo nas dicas de ferramenta:"
msgid "Save as"
msgstr "Salvar como"
msgid "Mark duplicates"
msgstr "Marcar duplicações"
msgid "Load only for selected bouquet"
msgstr "Carregar apenas para o bouquet selecionado"
msgid "The task is canceled!"
msgstr "A tarefa foi cancelada!"
msgid "Data loading in progress!"
msgstr "Carregamento de dados em curso!"
msgid "Recordings"
msgstr "Gravações"
msgid "Recordings:"
msgstr "Gravações:"
msgid "Help"
msgstr "Ajuda"
msgid "HTTP API is not activated. Check your settings!"
msgstr "A API HTTP não está activada. Verifique as suas definições!"
msgid "Add picons"
msgstr "Adicionar picons"
msgid "Logs"
msgstr "Registos"
msgid "Title"
msgstr "Título"
msgid "Time"
msgstr "Tempo"
msgid "Length"
msgstr "Duração"
msgid "Additional source"
msgstr "Fonte complementar"
msgid "Automatically set the name selected in the favorites list."
msgstr "Definir automaticamente o nome selecionado na lista de favoritos."
msgid "Playback"
msgstr "Reprodução"
msgid "Playback:"
msgstr "Reprodução:"
msgid "Audio"
msgstr "Áudio"
msgid "Audio Track"
msgstr "Pista áudio"
msgid "Subtitle"
msgstr "Legenda"
msgid "Subtitle Track"
msgstr "Faixa de legenda"
msgid "Aspect ratio"
msgstr "Rácio de aspeto"
msgid "This may change the settings of other profiles!"
msgstr "Isto pode alterar as definições de outros perfis!"
msgid "Drag the services to the desired picon or picon to the list of selected services."
msgstr "Arraste os serviços para o picon pretendido ou picon para a lista de serviços seleccionados."
msgid "Sets the profile folder as default to store picons, backups, etc."
msgstr "Define a pasta de perfil como predefinida para armazenar picons, cópias de segurança, etc."
msgid "New sub-bouquet"
msgstr "Novo sub-bouquet"
msgid "Mark not presented in Bouquets"
msgstr "Marca não apresentada em bouquets"
msgid "Not in Bouquets"
msgstr "Não em bouquets"
msgid "Do not show services present in Bouquets."
msgstr "Não mostrar serviços presentes em bouquets."
msgid "IPTV services only"
msgstr "Apenas serviços IPTV"
msgid "Display picons"
msgstr "Mostrar picons"
msgid "Alternate layout"
msgstr "Esquema alternativo"
msgid "Layout of elements has been changed!"
msgstr "O esquema dos elementos foi alterado!"
msgid "Restart the program to apply all changes."
msgstr "Reinicie o programa para aplicar todas as alterações."
msgid "New folder"
msgstr "Novo ficheiro"
msgid "Rename"
msgstr "Renomear"
msgid "Bookmarks"
msgstr "Marcadores"
msgid "Add bookmark"
msgstr "Adicionar marcador"
msgid "All bouquets"
msgstr "Todos os bouquets"
msgid "Playback from the main list"
msgstr "Reprodução da lista principal"
msgid "Enables URL parsing using yt-dlp to get direct links to media."
msgstr "Permite a análise de URLs usando yt-dlp para obter links diretos para mídia."
msgid "Permissions..."
msgstr "Permissões..."
msgid "Display EPG in bouquet list"
msgstr "Apresentar EPG na lista de bouquet"
msgid "EPG *.dat file:"
msgstr "Ficheiro EPG *.dat:"
msgid "Use HTTP to reload data in the receiver"
msgstr "Utilizar HTTP para recarregar dados no recetor"
msgid "Enable picons compression"
msgstr "Ativar a compressão de picons"
msgid "Update interval (sec):"
msgstr "Intervalo de atualização (seg):"
msgid "Update:"
msgstr "Atualização:"
msgid "Daily"
msgstr "Diário"
msgid "Assign reference"
msgstr "Atribuir referência"
msgid "Specify hostname or IP address"
msgstr "Especificar o nome do anfitrião ou o endereço IP"
msgid "Default selection"
msgstr "Seleção predefinida"
msgid "Don't change power state"
msgstr "Não alterar o estado de alimentação"
msgid "Don't toggle standby mode when updating bouquets and services."
msgstr "Não alterne o modo de espera ao atualizar bouquets e serviços."
msgid "Region"
msgstr "Região"
msgid "Provider"
msgstr "Provedor"
msgid "Enables upload as an archive if a large number of picon (> 1000) is selected.\n"
" Recommended only if you have external storage."
msgstr "Permite o carregamento como um arquivo se for selecionado um grande número de picons (> 1000).\n"
" Recomendado apenas se tiver armazenamento externo."
msgid "Clear \"New\" flag"
msgstr "Apagar a bandeira \"Novo\""
msgid "Group by"
msgstr "Agrupar por"
msgid "Replace existing"
msgstr "Substituir o existente"
msgid "Already exists"
msgstr "Já existe"
msgid "Enable unlimited copy buffer"
msgstr "Ativar o buffer de cópia ilimitado"
msgid "Enables unlimited copy buffer for the bouquets tab."
msgstr "Ativa a memória intermédia de cópia ilimitada para o separador bouquets."
msgid "Start time"
msgstr "Hora de início"
msgid "End time"
msgstr "Hora de fim"
msgid "Enable extensions support"
msgstr "Ativar o suporte de extensões"
msgid "After uploading the changes you may need to completely reboot the receiver!"
msgstr "Depois de carregar as alterações, poderá ser necessário reiniciar completamente o recetor!"
msgid "Remove duplicates"
msgstr "Eliminar duplicados"
msgid "Removed"
msgstr "Removido"
msgid "Enables overwriting existing main list services."
msgstr "Permite substituir os serviços da lista principal existentes."
msgid "Enables skipping services import from lamedb."
msgstr "Permite saltar a importação de serviços de lamedb."
msgid "Bouquets data only"
msgstr "Apenas dados de bouquets"
msgid "Create Category bouquets"
msgstr "Criar bouquets de categoria"
msgid "Create Regional bouquets"
msgstr "Criar bouquets regionais"
msgid "Skip C-band"
msgstr "Saltar a banda C"
msgid "Automatically set the name selected in the bouquet list."
msgstr "Definir automaticamente o nome selecionado na lista de bouquets."
msgid "Merge satellites by positions"
msgstr "Unir satélites por posições"
msgid "Save satellite selection"
msgstr "Guardar a seleção de satélite"
msgid "Extract direct links"
msgstr "Extrair ligações directas"
msgid "URL prefix:"
msgstr "Prefixo URL:"
msgid "Invalid prefix for the given URL!"
msgstr "Prefixo inválido para o URL indicado!"
msgid "Alternate window title"
msgstr "Título alternativo da janela"
msgid "Selected type:"
msgstr "Tipo selecionado:"
msgid "Extension Manager"
msgstr "Gestor de extensões"
msgid "Ver."
msgstr "Ver."
msgid "Installed"
msgstr "Instalado"

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2018-2023 Dmitriy Yefremov
# Copyright (C) 2018-2024 Dmitriy Yefremov
# This file is distributed under the MIT license.
#
#
@@ -1311,7 +1311,7 @@ msgid "Sets the profile folder as default to store picons, backups, etc."
msgstr "Устанавливает папку профиля по умолчанию для хранения пиконов, резервных копий и т. п."
msgid "New sub-bouquet"
msgstr "Создать суббукет"
msgstr "Создать подбукет"
msgid "Mark not presented in Bouquets"
msgstr "Отметить отсутствующие в букетах"
@@ -1355,8 +1355,8 @@ 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 "Enables URL parsing using yt-dlp to get direct links to media."
msgstr "Включает анализ URL-адресов с помощью yt-dlp для получения прямых ссылок на медиа."
msgid "Permissions..."
msgstr "Разрешения..."
@@ -1485,3 +1485,69 @@ msgstr "Альтернативный заголовок окна"
msgid "Selected type:"
msgstr "Выбран тип:"
msgid "Extension Manager"
msgstr "Менеджер расширений"
msgid "Ver."
msgstr "Вер."
msgid "Installed"
msgstr "Установлено"
msgid "Use common folder for picons"
msgstr "Использовать общую папку для пиконов"
msgid "Activates single folder use for several profiles."
msgstr "Активирует использование одной папки для нескольких профилей."
msgid "Events"
msgstr "События"
msgid "Markers"
msgstr "Маркеры"
msgid "IPTV only"
msgstr "Только IPTV"
msgid "No"
msgstr "Нет"
msgid "Not found."
msgstr "Не найдено."
msgid "Current EPG cache contents."
msgstr "Содержимое текущего EPG кэша."
msgid "Source error!"
msgstr "Ошибка источника!"
msgid "The EPG source for the favorites list is not set!"
msgstr "Не установлен источник EPG для списка избранного!"
msgid "Add to EPG sources list"
msgstr "Добавить в список источников EPG"
msgid "Current bouquet"
msgstr "Текущий букет"
msgid "Single bouquet"
msgstr "Одиночный букет"
msgid "Split by groups"
msgstr "Разбить по группам"
msgid "Create sub-bouquets"
msgstr "Создать подбукеты"
msgid "Add image"
msgstr "Добавить изображение"
msgid "TV Format"
msgstr "ТВ-формат"
msgid "Use with Streamrelay"
msgstr "Использовать Streamrelay"
msgid "Remove use with Streamrelay"
msgstr "Не использовать Streamrelay"

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: DemonEditor\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-16 15:59+0300\n"
"PO-Revision-Date: 2023-04-22 16:34+0300\n"
"PO-Revision-Date: 2024-09-29 22:18+0300\n"
"Last-Translator: audi06_19 <info@dreamosat-forum.com>\n"
"Language-Team: audi06_19 <info@dreamosat-forum.com>\n"
"Language: tr\n"
@@ -11,7 +11,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.2.2\n"
"X-Generator: Poedit 3.5\n"
msgid "translator-credits"
msgstr "audi06_19 <info@dreamosat-forum.com>"
@@ -1387,8 +1387,8 @@ msgstr "Tüm buketler"
msgid "Playback from the main list"
msgstr "Ana listeden oynatma"
msgid "Enables URL parsing using youtube-dl to get direct links to media."
msgstr "Medyaya doğrudan bağlantılar almak için youtube-dl kullanarak URL ayrıştırmayı etkinleştirir."
msgid "Enables URL parsing using yt-dlp to get direct links to media."
msgstr "Medyaya doğrudan bağlantılar almak için yt-dlp kullanarak URL ayrıştırmayı etkinleştirir."
msgid "Permissions..."
msgstr "İzinler..."
@@ -1519,3 +1519,69 @@ msgstr "Alternatif pencere başlığı"
msgid "Selected type:"
msgstr "Seçilen tip:"
msgid "Extension Manager"
msgstr "Eklenti Yöneticisi"
msgid "Ver."
msgstr "Ver."
msgid "Installed"
msgstr "Yüklenmiş"
msgid "Use common folder for picons"
msgstr "Piconlar için ortak klasörü kullan"
msgid "Activates single folder use for several profiles."
msgstr "Birden fazla profil için tek klasör kullanımını etkinleştirir."
msgid "Events"
msgstr "Olaylar"
msgid "Markers"
msgstr "İşaretleyiciler"
msgid "IPTV only"
msgstr "Yalnızca IPTV"
msgid "No"
msgstr "Hayır"
msgid "Not found."
msgstr "Bulunamadı."
msgid "Current EPG cache contents."
msgstr "Geçerli EPG önbellek içerikleri."
msgid "Source error!"
msgstr "Kaynak hatası!"
msgid "The EPG source for the favorites list is not set!"
msgstr "Favoriler listesi için EPG kaynağı ayarlanmamış!"
msgid "Add to EPG sources list"
msgstr "EPG kaynakları listesine ekle"
msgid "Current bouquet"
msgstr "Mevcut buket"
msgid "Single bouquet"
msgstr "Tek buket"
msgid "Split by groups"
msgstr "Gruplara göre böl"
msgid "Create sub-bouquets"
msgstr "Alt buketler oluştur"
msgid "Add image"
msgstr "Resim ekle"
msgid "TV Format"
msgstr "TV Formatı"
msgid "Use with Streamrelay"
msgstr "Streamrelay ile kullan"
msgid "Remove use with Streamrelay"
msgstr "Streamrelay ile kullanımı kaldır"