Compare commits

..

447 Commits

Author SHA1 Message Date
DYefremov
4af353099e update of *.spec file 2021-05-25 19:55:24 +03:00
DYefremov
7f0cdbfdd4 minor fix 2021-05-25 15:18:29 +03:00
DYefremov
ba9289e051 minor fix for vlc init 2021-05-25 14:15:23 +03:00
DYefremov
64bdc87b90 xml translation fix 2021-05-24 16:12:59 +03:00
DYefremov
a350ef1372 Russian, Belarusian and German translations update 2021-05-24 16:03:16 +03:00
DYefremov
876fe8b330 added support for editing extra pid's for services 2021-05-20 15:01:57 +03:00
DYefremov
3ed5e3b59e minor fix for picons downloader gui 2021-05-18 23:51:46 +03:00
DYefremov
21458a5ba8 picons extraction fix 2021-05-18 18:20:31 +03:00
DYefremov
124f9e2ba4 small refactoring for iptv list dialog 2021-05-18 16:23:58 +03:00
DYefremov
f6f0a8bb81 prevent data loading when ftp client is active 2021-05-17 14:36:06 +03:00
DYefremov
94235a87ef minor fixes 2021-05-17 09:24:27 +03:00
DYefremov
41a72d2ff8 changes for m3u import dialog buttons 2021-05-17 00:08:54 +03:00
DYefremov
7db3429384 added error catching for logo loading 2021-05-16 12:50:54 +03:00
DYefremov
d4a24caf17 minor changes for picon downloader 2021-05-15 17:34:03 +03:00
DYefremov
1919a38766 added support for saving single bouquets 2021-05-15 17:33:55 +03:00
DYefremov
3bff527660 added picons loading only for the selected bouquet 2021-05-15 17:33:42 +03:00
DYefremov
2314b66c5a added basic support for downloading picons from picon.cz 2021-05-15 17:33:28 +03:00
DYefremov
33e1b876b5 bump version 2021-05-04 11:30:57 +03:00
DYefremov
91f2784388 fixed getting youtube-dl 2021-05-02 22:47:25 +03:00
DYefremov
cf6fc09a20 fix dnd 2021-04-30 10:38:25 +03:00
DYefremov
e3df33e50f archives extraction fix 2021-04-28 20:49:30 +03:00
DYefremov
cece51e069 README update 2021-04-28 18:08:07 +03:00
DYefremov
cb6fbb5107 builder creation refactoring 2021-04-28 14:58:16 +03:00
DYefremov
8329149b1f code adaptation 2021-04-28 10:37:35 +03:00
DYefremov
137ee213dc Merge branch 'development-mac' into experimental-win
# Conflicts:
#	app/tools/media.py
#	app/ui/app_menu_bar.ui
#	app/ui/main_app_window.py
#	app/ui/main_window.glade
#	app/ui/settings_dialog.py
2021-04-26 10:57:45 +03:00
DYefremov
7cb1787de7 added auto setting of the dark mode 2021-04-24 00:02:26 +03:00
DYefremov
b4bca084de changed layout for ftp client 2021-04-23 23:37:10 +03:00
DYefremov
3990ee6572 fixed getting sats for lyngsat 2021-04-19 20:02:39 +03:00
DYefremov
46e8be54dd added mark for duplicates in fav list 2021-04-19 13:06:36 +03:00
DYefremov
4d35f71ddc preventing size save of the maximized window 2021-04-19 12:30:13 +03:00
DYefremov
38ff00bfb3 changed order of the toolbar items 2021-04-14 16:19:04 +03:00
DYefremov
d8f9dfe50e fix picons downloading from the web 2021-04-12 14:21:33 +03:00
DYefremov
b6f3d888cb enabled lamedb5 support for import feature 2021-04-12 14:21:26 +03:00
DYefremov
043a0371d2 fixed loading of services from the web 2021-04-12 14:21:18 +03:00
DYefremov
e613f9f55e combining of the search and filtering panels 2021-04-12 14:20:26 +03:00
DYefremov
2806d95972 added auto-hide mouse cursor in playback mode 2021-04-11 19:22:14 +03:00
DYefremov
a34798f215 some gui corrections 2021-04-07 23:24:52 +03:00
DYefremov
d6738826d3 playback adaptation 2021-04-07 12:41:33 +03:00
DYefremov
a437ec6030 copy de *.mo file 2021-04-06 23:06:45 +03:00
Thomas Schmidt
9cc8605994 Update german translation 2021-04-06 23:06:38 +03:00
DYefremov
a972ee353f minor fixes 2021-04-06 23:06:10 +03:00
DYefremov
c8d38161ae copy de *.mo file 2021-04-04 10:19:36 +03:00
Thomas Schmidt
3758c738fe Update german translation (#47)
* Update german translation

* Update demon-editor.po

Co-authored-by: Dmitriy Yefremov <dmitry.v.yefremov@gmail.com>
2021-04-04 10:19:25 +03:00
DYefremov
be2d64e480 sid value fix for some picons 2021-04-03 22:16:02 +03:00
DYefremov
76e732c8a0 fixed width for provider name 2021-04-03 22:15:58 +03:00
DYefremov
b18c4e254e minor fixes 2021-04-03 22:15:16 +03:00
DYefremov
1ad7781de7 sid value fix for some picons 2021-04-03 15:22:52 +03:00
DYefremov
2325c0e541 fixed width for provider name 2021-04-03 13:21:50 +03:00
DYefremov
e81e13a5c0 minor fixes 2021-04-03 00:08:56 +03:00
DYefremov
5b12777223 Merge branch 'development-mac' into experimental-win
# Conflicts:
#	DemonEditor.spec
#	app/tools/media.py
#	app/ui/dialogs.glade
#	app/ui/main_app_window.py
#	app/ui/main_window.glade
#	app/ui/satellites_dialog.glade
#	app/ui/settings_dialog.glade
#	app/ui/settings_dialog.py
#	app/ui/uicommons.py
2021-04-02 21:51:05 +03:00
DYefremov
e0c953ee05 fixed picons download from the web 2021-04-01 23:00:18 +03:00
DYefremov
3dc4caf65d added support for ATSC services 2021-04-01 22:50:49 +03:00
DYefremov
8308b715dd bump version 2021-04-01 22:49:31 +03:00
DYefremov
3a8831f0f9 minor style fix for buttons 2021-03-27 17:43:35 +03:00
DYefremov
a020a23211 Russian, Belarusian and German translations update 2021-03-27 16:16:54 +03:00
DYefremov
a67c81235c added multiple choice of pos and type to filter feature 2021-03-27 16:15:44 +03:00
DYefremov
3ef587841e added base support for mpv 2021-03-23 22:20:49 +03:00
DYefremov
55a21fbc18 some corrections for playback mode 2021-03-16 00:26:28 +03:00
DYefremov
b60a9a69b6 minor fix for epg 2021-03-14 20:15:34 +03:00
DYefremov
8b517f6f88 added logo for the tooltip in the picon explorer 2021-03-14 13:21:32 +03:00
DYefremov
81dd12a038 minor icon fix 2021-03-13 10:39:42 +03:00
DYefremov
17de78f169 fixed picons download for providers 2021-03-12 16:47:11 +03:00
DYefremov
3411f32868 added display bouquet list in playback mode 2021-03-12 16:46:47 +03:00
DYefremov
5f669f4480 added new appearance options [list font, picons size] 2021-03-12 11:47:51 +03:00
DYefremov
f56e4b616a reworking of built-in player [GStreamer support] 2021-03-12 00:19:26 +03:00
DYefremov
653ef1422f some minor fixes 2021-03-06 14:32:33 +03:00
DYefremov
9d5e07af1f bump version 2021-03-02 15:18:13 +03:00
DYefremov
399c1ff01b copy tr *.mo file 2021-03-02 15:15:42 +03:00
audi06_19
ad8e6975b1 Turkish translations update (#45) 2021-03-02 15:15:26 +03:00
DYefremov
5f79b27daa streams playback improvements 2021-03-01 13:07:58 +03:00
DYefremov
3b23ddc1a7 setting encoding for file opening 2021-02-28 21:22:22 +03:00
DYefremov
a0e3566bec enabled language selection 2021-02-28 17:05:28 +03:00
DYefremov
1cada0408f appending providers fix 2021-02-26 13:11:19 +03:00
DYefremov
e1a5b8e39d reworking of built-in player [GStreamer support] 2021-02-26 12:36:19 +03:00
DYefremov
a46c6ae816 keyboard codes adaptation 2021-02-26 08:50:36 +03:00
DYefremov
33137ee879 basic adaptation of the code and gui 2021-02-25 11:45:29 +03:00
DYefremov
51181057b1 adaptation to Python 3.4 2021-02-23 11:10:06 +03:00
DYefremov
ca1e823bf1 improved satellites import [added KingOfSat support] 2021-02-21 08:44:33 +03:00
DYefremov
7fb2d9ac4a minor fix 2021-02-21 08:24:12 +03:00
DYefremov
f5a02ddf1d added info message after m3u import finished 2021-02-14 14:44:12 +03:00
DYefremov
1266f8e04b added custom sort function for position column 2021-02-14 14:39:27 +03:00
DYefremov
2dcee99981 preventing tooltips for an inactive window 2021-02-13 12:31:58 +03:00
DYefremov
7053628e56 fixed import of satellites from the web 2021-02-12 10:30:18 +03:00
DYefremov
b8e1f0e7fd refactoring of picons downloading 2021-02-10 23:46:34 +03:00
DYefremov
954f1c514a alt service timer delete/edit fix 2021-02-09 20:23:57 +03:00
DYefremov
60e1f6c467 Russian, Belarusian and German translations update 2021-02-08 22:31:43 +03:00
DYefremov
986f10c640 minor fix 2021-02-08 22:29:37 +03:00
DYefremov
4c95972381 streams play mode refactoring 2021-02-08 22:28:01 +03:00
DYefremov
052dd3efbe improved built-in player [added windowed mode] 2021-02-08 22:23:28 +03:00
DYefremov
4e867b6f22 added picons downloading to * .m3u import 2021-02-08 22:02:55 +03:00
DYefremov
c11278041e telnet login fix 2021-02-05 11:12:46 +03:00
DYefremov
b89df3d65d services web import fix 2021-02-03 19:11:54 +03:00
DYefremov
d252c69628 improved *.m3u import 2021-02-03 19:11:32 +03:00
DYefremov
6785e46745 added saving of bouquet file names 2021-02-03 10:26:05 +03:00
DYefremov
bbffeaa30e added epg display from the alternatives list 2021-01-21 19:16:22 +03:00
DYefremov
ce11723d34 improved service details editing [neutrino] 2021-01-21 19:10:08 +03:00
DYefremov
2cdefdca42 adaptation to the new format 2021-01-21 19:06:10 +03:00
DYefremov
52b2bb28b4 changed format for freq, sr and pol columns 2021-01-21 19:05:58 +03:00
DYefremov
672586e227 lamedb parsing refactoring 2021-01-21 19:05:41 +03:00
DYefremov
eaff4eec6c changed names for new config 2021-01-21 19:05:32 +03:00
DYefremov
f068696aad fix first bouquet name 2021-01-21 19:05:13 +03:00
DYefremov
f8eddd8710 fix drag icon in filter mode 2021-01-15 09:40:35 +03:00
DYefremov
a5206c89ef copyright update 2021-01-15 09:40:22 +03:00
DYefremov
6555c3c882 bump version 2021-01-15 09:38:30 +03:00
DYefremov
155ed02f11 Russian, Belarusian and German translations update 2021-01-12 11:56:02 +03:00
DYefremov
a1ce729ce2 changed alternatives naming 2021-01-12 11:24:35 +03:00
DYefremov
33ffccf57a added 8739 stream type 2021-01-11 15:32:55 +03:00
DYefremov
b9881fc345 naming alternatives fix 2021-01-10 22:47:06 +03:00
DYefremov
35ce913ab0 added moving alternatives in the list 2021-01-10 22:32:52 +03:00
DYefremov
29e1cb10a3 added editing of alternatives 2021-01-09 00:27:37 +03:00
DYefremov
558843c728 set extra tools visible by default 2021-01-08 13:17:16 +03:00
DYefremov
c3534052ae removed bq position option 2021-01-07 11:10:12 +03:00
DYefremov
1113fec26e added separate column for picon to fav list 2021-01-07 10:51:41 +03:00
DYefremov
2f55fb4e64 added basic support for alternatives 2021-01-06 01:54:10 +03:00
DYefremov
412a66e5e5 added switching position of fav list on the fly 2021-01-06 01:29:51 +03:00
DYefremov
676bc14f73 some streams detection fix 2021-01-02 18:10:58 +03:00
DYefremov
ec6ebb2a0e redesigned network settings 2020-12-30 23:38:42 +03:00
DYefremov
f8710a4bf0 bump version 2020-12-30 22:42:08 +03:00
DYefremov
b48f638495 _config.yml update 2020-12-28 00:19:35 +03:00
DYefremov
fd0559d76e README update 2020-12-28 00:19:03 +03:00
DYefremov
6c6948ce23 added new order to alternate layout 2020-12-25 09:38:11 +03:00
DYefremov
573d755e31 added display option for bouquet details list 2020-12-24 23:31:37 +03:00
DYefremov
912083f203 README update 2020-12-23 09:39:08 +03:00
DYefremov
4df0553333 Russian, Belarusian and German translations update 2020-12-23 09:38:27 +03:00
DYefremov
f0d535ba4e yt fix 2020-12-23 09:38:12 +03:00
DYefremov
88ef5563cf ftp client improvements 2020-12-22 14:31:38 +03:00
DYefremov
6431f2ccd8 minor fix 2020-12-22 14:31:25 +03:00
DYefremov
ca9b4a780d storing app window size on close 2020-12-19 12:38:40 +03:00
DYefremov
f74eead20b added alternate layout support 2020-12-18 22:18:13 +03:00
DYefremov
8d4d90fd9f added alternate layout option 2020-12-18 22:16:03 +03:00
DYefremov
4269d16d31 added ftp client to main window 2020-12-17 14:00:14 +03:00
DYefremov
9cf3e97bd3 added ftp client base class 2020-12-17 10:27:01 +03:00
DYefremov
3b85d35b62 added keyboard key 2020-12-17 10:25:30 +03:00
DYefremov
6bddd89206 ftp refactoring [func extension] 2020-12-14 15:12:28 +03:00
DYefremov
e16f2cba82 allowed to add dir during path config 2020-12-14 15:11:44 +03:00
DYefremov
59748aa9ba added encoding detection for *.m3u import 2020-12-07 09:32:00 +03:00
DYefremov
caf4925409 data load rework (#37 fix [can't decode byte]) 2020-12-05 14:13:29 +03:00
DYefremov
2ab540ccfa bump version 2020-12-05 14:12:04 +03:00
DYefremov
4b762802da added playlist extraction via youtube-dl 2020-12-05 14:10:09 +03:00
DYefremov
41a6e54e90 upd. README 2020-12-02 01:06:50 +03:00
DYefremov
7db02f2a9e added types to the service parser 2020-11-28 14:25:08 +03:00
DYefremov
fd40fd8d72 changes for popup menu actions 2020-11-27 21:27:23 +03:00
DYefremov
0b4313e4cf minor changes in control panel gui 2020-11-27 21:25:00 +03:00
DYefremov
a74628ed5c added sorting reset when loading data 2020-11-26 13:54:00 +03:00
DYefremov
443f6bf252 translations correction for app description 2020-11-26 13:53:49 +03:00
DYefremov
bb243ce281 added timeouts for telnet login 2020-11-26 13:53:20 +03:00
DYefremov
44049c380e config.yml update 2020-11-26 13:53:03 +03:00
DYefremov
281fe2a8f4 Russian, Belarusian and German translations update 2020-11-26 13:52:21 +03:00
DYefremov
39cc0ad8b3 some changes in the control panel 2020-11-26 13:51:46 +03:00
DYefremov
a625dc9f8b style correction 2020-11-26 13:51:29 +03:00
DYefremov
53f69b8f67 Russian, Belarusian and German translations update 2020-11-26 13:51:13 +03:00
DYefremov
94dfda0fa2 added basic support for timers via http api 2020-11-26 13:40:36 +03:00
DYefremov
cfe3f4c707 added styles 2020-11-26 13:39:36 +03:00
DYefremov
d18734910d small http api refactoring 2020-11-26 13:36:45 +03:00
DYefremov
d843633043 added epg display in control panel 2020-11-26 13:28:16 +03:00
DYefremov
b513d7a9b0 added prototype of simple control panel (#38) 2020-11-26 13:09:55 +03:00
DYefremov
92b2f840f8 fix update of sat positions after web import 2020-11-25 22:18:22 +03:00
DYefremov
9e4c8f388c added transponder view popup menu 2020-11-25 22:17:04 +03:00
DYefremov
664c52cfe1 minor compatibility fix 2020-11-25 22:15:27 +03:00
DYefremov
cc96cdd8fd added web import for services (#16) 2020-11-25 22:09:13 +03:00
DYefremov
15cca3f5f7 small decoupling for lamedb parsing 2020-11-25 21:27:22 +03:00
DYefremov
0ec2570043 added remote control requests 2020-11-25 21:26:51 +03:00
DYefremov
97cb26cd60 bump version 2020-11-25 21:26:36 +03:00
DYefremov
1da3eacc8c minor fix for the picon parser 2020-10-10 15:21:47 +03:00
DYefremov
cb6f185032 minor fix for yt 2020-10-06 11:27:09 +03:00
DYefremov
a8918bcf1f Russian, Belarusian and German translations update 2020-10-05 22:17:20 +03:00
DYefremov
c358197080 data loading refactoring 2020-10-05 22:16:54 +03:00
DYefremov
474fc3ec58 adaptation of dnd 2020-10-05 22:15:01 +03:00
DYefremov
c17bad215f changed getting of the drag icon 2020-09-30 20:58:30 +03:00
DYefremov
7891aca6e2 upd README 2020-09-28 13:48:04 +03:00
DYefremov
608de65897 Russian, Belarusian and German translations update 2020-09-28 13:46:08 +03:00
DYefremov
cbed3f7cca reworked and improved dnd for lists 2020-09-28 13:45:01 +03:00
DYefremov
08c1dca06d added support for loading and importing data via dnd 2020-09-28 13:30:12 +03:00
DYefremov
1edbd7d771 version update 2020-09-28 13:25:10 +03:00
DYefremov
0c3f6870dd added support for opening archives 2020-09-28 13:22:19 +03:00
DYefremov
f877872059 minor rework of the chooser dialog 2020-09-28 12:59:38 +03:00
DYefremov
335dfc005a displaying sid value in uppercase for tooltips(#34) 2020-09-12 22:52:18 +03:00
DYefremov
46450cf9b6 Display the sid value in tooltips in hex and dec format(#34). 2020-09-12 22:52:07 +03:00
Víctor Pont
9ed82ea129 Spanish translation update (#36) 2020-09-11 16:38:01 +03:00
DYefremov
555699c2a1 renaming bouquet fix [losing custom names](#33) 2020-09-08 12:45:32 +03:00
DYefremov
83b810286a upd README 2020-09-06 13:30:08 +03:00
DYefremov
61a56f1989 copy pl *.mo file 2020-09-02 23:04:08 +03:00
Wieslaw Weglowski
50ce4a688a Polish translation corrections (#31) 2020-09-02 22:53:47 +03:00
DYefremov
871b428b19 Russian, Belarusian and German translations update 2020-09-02 22:53:22 +03:00
DYefremov
3cd864cd84 version update 2020-08-31 22:27:45 +03:00
DYefremov
78c6a3c9fa minor fix for picon assignment 2020-08-31 22:27:20 +03:00
DYefremov
4c0904cf6c added Belarusian translation 2020-08-31 12:24:33 +03:00
DYefremov
7aa688df15 fix playback from the start screen 2020-08-31 12:20:37 +03:00
DYefremov
c91d58e0cf minor changes in sat dialogs 2020-08-27 23:21:19 +03:00
DYefremov
d071bb5d85 telnet password visibility fix 2020-08-24 22:44:30 +03:00
DYefremov
8cee77357c Polish translation update 2020-08-24 22:22:25 +03:00
DYefremov
20f53dee33 minor optimization 2020-08-24 22:22:09 +03:00
Wieslaw Weglowski
c454a33b3c Update demon-editor.po (#30) 2020-08-24 22:21:56 +03:00
DYefremov
5642b8871c upd README 2020-08-19 21:24:34 +03:00
DYefremov
e7480ec622 rework of the picons resizing 2020-08-19 21:20:07 +03:00
DYefremov
ecce001ce4 German and Russian translation update 2020-08-15 16:55:55 +03:00
DYefremov
7bae895458 minor yt fix 2020-08-15 16:55:40 +03:00
DYefremov
5b3bd48746 output fix for picons downloader 2020-08-09 00:14:58 +03:00
DYefremov
4769a814bd update *.spec 2020-08-09 00:05:46 +03:00
DYefremov
b08d4ed7d7 added dark mode support 2020-08-09 00:05:04 +03:00
DYefremov
233eb6bc53 added dark mode option 2020-08-08 14:47:57 +03:00
DYefremov
8b3d24c006 small changes of settings dialog appearance 2020-08-07 15:11:32 +03:00
DYefremov
48184c1fd9 minor fixes 2020-08-07 11:37:59 +03:00
DYefremov
46d91b93bc skipping enigma2 stop during picons upload 2020-08-06 21:19:47 +03:00
DYefremov
69989e784d minor correction of translations 2020-08-04 12:52:01 +03:00
DYefremov
ce1c978222 version update 2020-08-03 22:50:22 +03:00
DYefremov
5bcac35deb added option for experimental features 2020-08-03 22:23:44 +03:00
DYefremov
3ec5d264a0 Spanish, Portuguese and Dutch translation update 2020-07-27 21:29:07 +03:00
DYefremov
a2882b6589 upd README 2020-07-25 23:34:22 +03:00
DYefremov
31780bbf56 rm dist 2020-07-25 23:13:17 +03:00
DYefremov
286f1ffc3f version update 2020-07-25 13:43:46 +03:00
DYefremov
3bf97e5e0d minor change in dialogs appearance 2020-07-25 13:34:57 +03:00
DYefremov
c7c411c72b update ref fix 2020-07-24 11:00:20 +03:00
DYefremov
6b8a83511a slight appearance change of dialogs 2020-07-24 03:39:14 +03:00
DYefremov
f8e259293a German and Russian translation update 2020-07-22 17:33:36 +03:00
DYefremov
7adbf6b8a9 added keyboard[del] support 2020-07-22 17:32:57 +03:00
DYefremov
b68535e88a loading providers fix 2020-07-22 17:31:10 +03:00
DYefremov
b98ca359df fix display of cas 2020-07-22 17:30:36 +03:00
DYefremov
cbfd1486e1 added lock support for iptv 2020-07-22 17:30:03 +03:00
DYefremov
ad185f1efa German translation update 2020-07-22 17:29:33 +03:00
DYefremov
cea4ed1a66 Russian translation update 2020-07-22 17:28:38 +03:00
DYefremov
853d054a68 added audio codec option 2020-07-22 17:25:01 +03:00
DYefremov
8a1cead2f7 added notifications 2020-07-21 14:30:59 +03:00
DYefremov
37e0a8fdac dist update 2020-07-13 21:44:25 +03:00
DYefremov
29c66142ee minor fix 2020-07-13 21:00:26 +03:00
DYefremov
4fd2a2a600 bouquet import fix 2020-07-12 19:18:11 +03:00
DYefremov
6b360d48c4 added bouquets import via dnd 2020-07-12 16:44:40 +03:00
DYefremov
3a307b277c minor gui fix 2020-07-12 16:33:13 +03:00
DYefremov
9e685058a2 added debug mode option 2020-07-12 16:32:56 +03:00
DYefremov
3f07b09bb5 added sorting of bouquet services 2020-07-12 16:28:18 +03:00
DYefremov
1dca45f18f dist update 2020-07-04 14:02:52 +03:00
DYefremov
8b5ebc132d added download dialog options 2020-07-04 13:41:46 +03:00
DYefremov
b076db23bb auto save profile settings 2020-07-02 17:32:51 +03:00
DYefremov
41d479e18f dist update 2020-06-30 09:07:30 +03:00
DYefremov
cf540e5c9a picons explorer gui changes 2020-06-29 15:58:43 +03:00
DYefremov
3c7c8ebd83 fix lock/hide in filter mode 2020-06-22 19:46:36 +03:00
DYefremov
9b0c173eb8 skip of marker counting 2020-06-22 11:11:21 +03:00
DYefremov
208ce53c48 update *.spec 2020-06-21 10:27:15 +03:00
DYefremov
bb6679eddf fix getting path from uri 2020-06-21 00:52:50 +03:00
DYefremov
57f5e40439 changed filter entry icon 2020-06-15 21:39:13 +03:00
DYefremov
dad02e8e5c changed dnd for picons 2020-06-15 21:32:47 +03:00
DYefremov
844dab10a0 changed callback for screenshots 2020-06-15 16:57:39 +03:00
DYefremov
c1f5fd8006 small yt refactoring 2020-06-13 21:01:14 +03:00
DYefremov
86b974b632 get fav id fix 2020-06-13 19:14:31 +03:00
DYefremov
cf7e3a1b1b added space item to fav elements 2020-06-13 19:12:23 +03:00
DYefremov
bb07eb0a8a small dnd fix 2020-06-12 00:39:46 +03:00
DYefremov
f6de7d0fce version update 2020-06-11 11:48:45 +03:00
DYefremov
647b528899 added frame for info bar 2020-06-11 10:47:27 +03:00
DYefremov
7ed64c76ba added paned style 2020-06-11 10:27:23 +03:00
DYefremov
bcfdb09169 small http api init fix 2020-06-10 18:34:42 +03:00
DYefremov
c0c2ddef34 added basic youtube-dl support 2020-06-10 18:02:47 +03:00
DYefremov
b02eb37f1c corrections after merge 2020-06-10 18:01:23 +03:00
DYefremov
9b9f1d5492 added eserviceuri (8193) stream type 2020-06-10 11:56:01 +03:00
DYefremov
caba789e02 small rework of screenshot mode 2020-06-10 11:55:49 +03:00
DYefremov
5b1bffc078 impl local removing for picons 2020-06-10 11:55:41 +03:00
DYefremov
7e35a081a0 logging extension on data downloading 2020-06-10 11:55:32 +03:00
DYefremov
ccbc7a4315 added dnd for selective download/send 2020-06-10 11:55:21 +03:00
DYefremov
7f3f900725 added selective download/send of picons 2020-06-10 11:53:08 +03:00
DYefremov
921b936db0 added update picons dest view 2020-06-10 11:52:57 +03:00
DYefremov
bc6d372ade improved functionality of the picons explorer 2020-06-10 11:51:51 +03:00
DYefremov
3c28d12579 added youtube-dl options 2020-06-10 11:45:10 +03:00
DYefremov
e9544cc77f added app settings read exception 2020-06-10 11:42:26 +03:00
DYefremov
bd047e5f72 added services filtering from picons manager 2020-06-10 11:42:05 +03:00
DYefremov
adf7262ed6 adding picons to src via dnd 2020-06-10 11:41:23 +03:00
DYefremov
74a1ffea3a added basic screenshots support 2020-06-10 11:40:54 +03:00
DYefremov
6e78a539c3 modified cas values 2020-06-10 11:36:46 +03:00
DYefremov
c3ce3fc82e added space [hidden marker] support 2020-06-10 11:36:32 +03:00
DYefremov
8c61720423 version update 2020-06-10 11:32:54 +03:00
DYefremov
25e0e6939a copy tr *.mo file 2020-06-08 23:55:39 +03:00
audi06_19
e3232e48cf Turkish translation correction (#26) 2020-06-08 23:55:26 +03:00
DYefremov
aaa610852b dist update 2020-06-04 12:47:19 +03:00
DYefremov
04e9179025 added accelerator for input dialog 2020-06-04 11:46:21 +03:00
DYefremov
bce5636eaa added check for unsaved changes 2020-06-04 11:41:38 +03:00
DYefremov
0e10631931 rm unavailable iptv fix 2020-06-03 11:30:26 +03:00
DYefremov
77a3edead2 zap mode fix 2020-06-03 00:07:19 +03:00
DYefremov
8a8b249e14 small bq parsing changes (prevent #12) 2020-06-02 23:58:39 +03:00
DYefremov
4025f0933d copy pl *.mo file 2020-06-02 10:03:31 +03:00
DYefremov
bba4054bff Polish translation correction 2020-06-02 09:36:37 +03:00
Wieslaw Weglowski
e322d36023 Polish translation update(#23) 2020-06-02 09:07:31 +03:00
DYefremov
bb5afb0206 dist update 2020-05-23 01:36:35 +03:00
DYefremov
115f3960a7 drag on icon fix
(cherry picked from commit 0e50f1952d)
2020-05-23 01:22:09 +03:00
DYefremov
6d37da072e German translation update 2020-05-19 14:32:31 +03:00
DYefremov
99c3b1d194 Russian translation update 2020-05-19 14:32:17 +03:00
DYefremov
43afaf77b8 minor log changes 2020-05-19 12:01:11 +03:00
DYefremov
38aabb1b94 Dutch translation update 2020-05-18 16:31:24 +03:00
DYefremov
ef501f1557 small dnd fix 2020-05-18 12:40:21 +03:00
DYefremov
4679f9379c changed dnd for bouquets list 2020-05-17 15:28:44 +03:00
DYefremov
b2ea39f8a6 minor fixes 2020-05-17 14:55:18 +03:00
DYefremov
638be67425 dist update 2020-05-12 21:15:31 +03:00
DYefremov
9ca5a597d5 scaling picons on loading 2020-05-12 21:14:00 +03:00
DYefremov
d95ba7336f copy tr *.mo file 2020-05-12 17:26:12 +03:00
audi06
c78b18ddb7 Turkish translation update (#22) 2020-05-12 17:24:45 +03:00
DYefremov
92984c5fa6 start update 2020-05-12 15:19:47 +03:00
DYefremov
ca65f64a4f changed some dialogs elements 2020-05-12 14:13:01 +03:00
DYefremov
78dcccbd51 minor optimization and fix 2020-05-10 21:39:48 +03:00
DYefremov
f984d10c82 German translation update 2020-05-10 18:50:22 +03:00
DYefremov
c4ea451f52 small fix to prevent (#12) 2020-05-10 18:50:11 +03:00
DYefremov
36ec6d5079 added picons filter by service name 2020-05-10 18:49:59 +03:00
DYefremov
91706c722f Russian translation update 2020-05-10 15:13:06 +03:00
DYefremov
4ef8c4d186 fix to prevent #12 2020-05-08 17:15:34 +03:00
DYefremov
f9e92b28d0 dist update 2020-05-07 15:10:48 +03:00
DYefremov
832bab91a4 reworked settings dialog 2020-05-07 14:19:46 +03:00
DYefremov
951c99338f added skip upload if file not found 2020-05-05 00:04:05 +03:00
DYefremov
ee91eb9413 fix use colors 2020-05-04 22:33:05 +03:00
DYefremov
912c38825b redesigned appearance for most dialogs 2020-05-04 19:36:52 +03:00
DYefremov
de4d012784 fix to prevent (#21) 2020-05-04 19:20:51 +03:00
DYefremov
351ce81e94 minor fixes for yt 2020-05-03 02:05:36 +03:00
DYefremov
3a0f096a6c changed data dir creation 2020-05-03 01:42:28 +03:00
DYefremov
29088ec19e dist update 2020-04-30 14:12:58 +03:00
DYefremov
4c144951f0 reworking of download dialog 2020-04-30 14:09:01 +03:00
DYefremov
dae6ad765a added accelerators and tooltips 2020-04-30 13:55:40 +03:00
DYefremov
b934407d7e added download/upload of [terrestrial, cable].xml 2020-04-30 13:53:15 +03:00
DYefremov
3fb5b82cc6 slight optimization of loading/deleting data 2020-04-30 13:47:24 +03:00
DYefremov
ba3ad9a9ef setting text for wait dialog 2020-04-30 13:45:09 +03:00
DYefremov
7a4620a374 path resolve fix 2020-04-30 13:42:02 +03:00
DYefremov
174634ecbc reworking of picons dialog 2020-04-30 13:36:01 +03:00
DYefremov
73ae57d07b extracting themes with tar 2020-04-23 18:43:22 +03:00
DYefremov
055a700586 small refactoring of base icons init 2020-04-23 15:43:48 +03:00
DYefremov
04203240a7 minor fixes for filter and search 2020-04-23 10:33:56 +03:00
DYefremov
a433e01b65 added group style 2020-04-22 10:02:47 +03:00
DYefremov
8f591a8b9a small refactoring of chooser dialog 2020-04-21 14:45:34 +03:00
DYefremov
dcc217b0de upd README 2020-04-20 20:50:10 +03:00
DYefremov
d06334b0af added picons assignment by drag on icon 2020-04-20 13:55:38 +03:00
DYefremov
6957a960ca added hints support for the main list 2020-04-20 13:51:05 +03:00
DYefremov
9fe328b54e minor refactoring 2020-04-20 13:46:42 +03:00
DYefremov
b3dc9b72c9 version update 2020-04-16 21:42:07 +03:00
DYefremov
b6a4d46227 upd README 2020-04-16 21:41:05 +03:00
audi06
53776bdf62 added Turkish selection (#20) 2020-04-16 21:10:28 +03:00
DYefremov
ba9ba4129f copy tr *.mo file 2020-04-16 21:10:13 +03:00
audi06
a2411ba86e added Turkish translation (#19)
(cherry picked from commit 8d96f02e2e)
2020-04-16 21:10:01 +03:00
DYefremov
a6d8573999 added bouquet file naming option 2020-04-16 17:29:04 +03:00
DYefremov
7510d42fb9 minor fixes 2020-04-13 20:08:22 +03:00
DYefremov
036e666c9b styles decoupling 2020-04-13 20:07:57 +03:00
DYefremov
c9c962e129 added appearance settings 2020-04-13 13:54:54 +03:00
DYefremov
ea71af9462 dist update 2020-04-10 23:22:57 +03:00
DYefremov
0a5b51de6e style changes for some ui elements 2020-04-10 23:09:17 +03:00
DYefremov
8cb413ec92 added basic hints support 2020-04-08 18:40:27 +03:00
DYefremov
5dfb702484 added option for hints 2020-04-08 18:37:10 +03:00
DYefremov
0cab4e1238 epg options fix 2020-04-02 16:53:03 +03:00
DYefremov
85f5c37f28 changed some player args 2020-03-28 20:20:49 +03:00
DYefremov
3df6d7bba0 translations update for dist 2020-03-28 19:56:24 +03:00
DYefremov
e45c56f4cc changed toolbar elements position 2020-03-28 19:31:13 +03:00
DYefremov
7d03631924 basic implementation of the play mode 2020-03-28 18:45:05 +03:00
DYefremov
7b9ec6a4b1 small cleaning 2020-03-28 18:39:13 +03:00
DYefremov
d640210ab0 copy *.mo file 2020-03-24 15:38:35 +03:00
wwns
f7e8283355 Polish translation update (#18) 2020-03-24 15:38:10 +03:00
DYefremov
f93c81de19 wrap m3u data 2020-03-24 15:38:06 +03:00
DYefremov
e1804755d2 added play streams mode options 2020-03-24 15:38:01 +03:00
DYefremov
1cf56639c1 copy .mo file 2020-03-21 16:47:50 +03:00
Víctor Pont
943b4c540f Spanish translation update and corrections (#17) 2020-03-21 16:47:43 +03:00
DYefremov
4602c51c01 added simple telnet client 2020-03-17 14:10:49 +03:00
DYefremov
a84cc7727f minor fix 2020-03-14 09:09:11 +03:00
DYefremov
250e03af5d changed record button update 2020-03-13 14:37:21 +03:00
DYefremov
2c5f8eb0ed toolbar elements changes 2020-03-11 16:47:10 +03:00
DYefremov
6f4ff4c97d added transcoding options 2020-03-11 16:05:13 +03:00
DYefremov
ee29659739 added record of current service 2020-03-11 16:04:41 +03:00
DYefremov
8a1496a84c added new paths settings 2020-03-11 15:57:41 +03:00
DYefremov
23c3035162 fix service status info 2020-03-11 15:56:57 +03:00
DYefremov
a506356547 version update 2020-03-11 15:55:59 +03:00
DYefremov
0c284fb0d9 added picons multiple assignment 2020-03-11 15:50:23 +03:00
DYefremov
b437385325 German translation update
(cherry picked from commit 3d627b57a4)
2020-02-28 00:53:33 +03:00
DYefremov
c60bba5535 upd README 2020-02-24 12:54:29 +03:00
DYefremov
1c2d0ab9ea small fix
(cherry picked from commit 7444db7e21)
2020-02-24 12:51:56 +03:00
DYefremov
f35f7fbc8a update dist 2020-02-21 00:14:10 +03:00
DYefremov
42aaad291f Russian translation update
(cherry picked from commit 7554f40c6a)
2020-02-20 14:16:09 +03:00
DYefremov
9c8c617393 gui changes for send to 2020-02-20 12:11:41 +03:00
DYefremov
98fc963fa1 fix getting sats 2020-02-19 12:03:27 +03:00
DYefremov
fbb5cd0352 added appindicator support 2020-02-19 10:15:01 +03:00
DYefremov
5abe3de3b6 toolbar changes 2020-02-18 00:35:17 +03:00
DYefremov
0b3f26ab84 .mo file update 2020-02-17 09:21:40 +03:00
wwns
2666146b5e Polish translation update (#11)
Polish translation update.
2020-02-17 08:50:32 +03:00
DYefremov
be90b518c9 update of .mo file 2020-02-14 22:55:36 +03:00
wwns
adeae58488 Polish translation update (#10) 2020-02-14 22:20:07 +03:00
DYefremov
b204f042ee update dist 2020-02-12 22:29:50 +03:00
DYefremov
79d0e9d256 added icons path to .spec file 2020-02-12 21:45:14 +03:00
DYefremov
4dcfde8b53 revert of get yt icon 2020-02-12 21:16:47 +03:00
DYefremov
42f687020b moved get yt icon 2020-02-12 20:19:09 +03:00
DYefremov
14bf79dbf9 toolbar changes 2020-02-12 17:35:44 +03:00
DYefremov
f660beef16 update of data path 2020-02-12 13:53:18 +03:00
DYefremov
99d17b36c3 added Polish selection 2020-02-12 12:40:27 +03:00
wwns
3113fadcca added a Polish translation (#5)
* added a Polish translation

* added a Polish translation

* name change
2020-02-11 21:43:53 +03:00
DYefremov
67a394359d fix bq deletion 2020-02-10 19:28:59 +03:00
DYefremov
1acb7fdd81 changing profile on data download 2020-02-10 17:01:48 +03:00
DYefremov
dced81581c fix profile edit 2020-02-10 14:52:30 +03:00
DYefremov
6a52988f1a update dist 2020-02-01 18:55:31 +03:00
DYefremov
4a1d714604 added basic support for keyboard shortcuts 2020-02-01 17:45:36 +03:00
DYefremov
2e12e1ec87 auto rename bouquets with duplicate names 2020-02-01 09:34:45 +03:00
DYefremov
4c6336e75f added checking for bouquet names duplicate 2020-01-29 14:58:32 +03:00
DYefremov
b7d0ba7f4b added controls to the transmitter 2020-01-28 16:37:53 +03:00
DYefremov
405e07bbc4 added player requests 2020-01-28 16:36:08 +03:00
DYefremov
6ded67147b update dist 2020-01-24 20:47:24 +03:00
DYefremov
4a0e2acd9c minor gui changes 2020-01-24 01:06:08 +03:00
DYefremov
5876f70884 upd. README 2020-01-23 19:18:49 +03:00
DYefremov
3cb4f1095d fix picons downloading from bundle 2020-01-23 19:17:54 +03:00
DYefremov
af46c2fb1d minor fixes for player elems 2020-01-23 16:13:09 +03:00
DYefremov
cbcdf19be6 added remove and download to picons 2020-01-23 00:48:57 +03:00
DYefremov
f326a9c723 some corrections 2020-01-19 20:07:59 +03:00
DYefremov
53888a45dc Merge branch 'development' into experimental-mac
# Conflicts:
#	DemonEditor.desktop
#	README.md
#	app/connections.py
#	app/ui/main_app_window.py
#	app/ui/main_window.glade
#	app/ui/uicommons.py
#	build-deb.sh
#	deb/DEBIAN/control
#	deb/DEBIAN/copyright
#	deb/usr/bin/demon-editor
#	deb/usr/share/applications/DemonEditor.desktop
#	start.py
2020-01-18 23:08:45 +03:00
DYefremov
c2d3cb7673 some changes for translation 2019-12-28 23:21:19 +03:00
DYefremov
9d4b507559 Merge remote-tracking branch 'origin/experimental-mac' into experimental-mac 2019-12-21 13:57:37 -08:00
DYefremov
2993fcd7f7 update dist 2019-12-18 03:50:17 -08:00
DYefremov
5bea9887db minor clean 2019-12-18 08:56:41 +03:00
DYefremov
dd0edfc811 minor clean 2019-12-17 12:56:07 +03:00
DYefremov
5bf6500809 added exception 2019-12-17 12:03:15 +03:00
DYefremov
d05da3f44c minor player fix 2019-12-16 15:49:24 +03:00
DYefremov
b251ce8b69 fixes after merge 2019-12-16 10:47:18 +03:00
DYefremov
df36860239 Merge branch 'development' into experimental-mac
# Conflicts:
#	app/properties.py
#	app/tools/media.py
#	app/ui/dialogs.py
#	app/ui/main_app_window.py
#	app/ui/transmitter.py
#	deb/usr/share/locale/es/LC_MESSAGES/demon-editor.mo
2019-12-16 10:36:19 +03:00
DYefremov
6f27040164 upd README 2019-12-14 13:22:22 +03:00
DYefremov
4d488dd224 update dist 2019-12-08 02:38:28 -08:00
DYefremov
c728a59e92 update dist 2019-12-08 02:32:40 -08:00
DYefremov
98341064d3 fix set nso 2019-12-08 01:09:44 -08:00
DYefremov
fc00b25fd2 revert media player view 2019-12-08 01:20:06 +03:00
DYefremov
638f33ac5a update vlc 2019-12-07 21:07:51 +03:00
DYefremov
88167912b3 fix status image show 2019-12-06 15:20:14 +03:00
DYefremov
450d7f4c72 added dist 2019-12-06 08:50:26 +03:00
DYefremov
24729c064c base impl of send to 2019-12-05 10:52:04 -08:00
DYefremov
df3c2a3938 added build support 2019-12-05 10:47:49 -08:00
DYefremov
44bf8b96ff fix set icon after marge 2019-11-26 18:46:57 +03:00
DYefremov
6b86db4aa4 Merge branch 'development' into experimental-mac
# Conflicts:
#	app/connections.py
#	app/ui/main_app_window.py
#	app/ui/main_window.glade
#	deb/DEBIAN/control
2019-11-26 18:41:07 +03:00
DYefremov
bf9ad139e5 added style 2019-11-01 00:24:13 +03:00
DYefremov
20fc199d02 moved toolbar to the top 2019-10-28 11:59:58 +03:00
DYefremov
e0a22f72fc changed setting service info 2019-10-28 11:44:30 +03:00
DYefremov
8cadc47da5 added current service status info 2019-10-28 11:44:12 +03:00
DYefremov
499ca31992 added http-api prototype for vlc 2019-10-27 00:39:32 +03:00
DYefremov
e198f0b1e6 removed separators from toolbar 2019-10-26 15:19:39 +03:00
DYefremov
a43ac0de02 added bottom toolbar 2019-10-26 15:01:23 +03:00
DYefremov
b14e6fac16 added edit and view to the app bar menu 2019-10-25 19:17:31 +03:00
DYefremov
2dcc9a85b5 updated app bar menu 2019-10-23 12:38:23 +03:00
DYefremov
7ce9ba0db2 added prototype of playing current service 2019-10-23 12:24:57 +03:00
DYefremov
fc56b047a1 added prototype of playing current service 2019-10-23 12:20:20 +03:00
DYefremov
78dc62d46e added german translation 2019-10-23 12:19:31 +03:00
DYefremov
20e7ee3478 removing deb 2019-10-20 23:58:10 +03:00
DYefremov
ffa144367f slight refactoring of stream play 2019-10-20 23:48:35 +03:00
DYefremov
237d09a711 updated version 2019-10-20 23:48:28 +03:00
DYefremov
272cbdeb2f changed yt links icon to default 2019-10-19 09:31:51 +03:00
DYefremov
f9191a7465 minor gui changes 2019-10-18 21:07:48 +03:00
DYefremov
f22abe1d87 test nso 2019-10-17 08:11:04 -07:00
DYefremov
b94c08284a new gui test prototype for the player 2019-10-17 15:02:31 +03:00
DYefremov
e303f25f99 updated app bar menu 2019-10-14 22:09:14 +03:00
DYefremov
b80dcb7d74 fix save 2019-10-13 18:53:33 +03:00
DYefremov
63c5df0ef6 Improved functionality of the app menu bar 2019-10-13 14:07:46 +03:00
DYefremov
c69888a72d Merge remote-tracking branch 'origin/experimental-mac' into experimental-mac 2019-10-12 23:32:16 +03:00
DYefremov
07606077e5 added app menu bar prototype 2019-10-12 23:32:01 +03:00
DYefremov
e1f63bfed7 updating of dutch, spanish and portuguese 2019-10-12 23:23:48 +03:00
DYefremov
c0865beb3c Merge branch 'development' into experimental-mac 2019-10-11 15:54:53 +03:00
DYefremov
05eff28b75 upd. README 2019-10-11 14:34:08 +03:00
DYefremov
6d2150b731 changes to start on macos 2019-10-10 14:19:10 -07:00
133 changed files with 21155 additions and 39871 deletions

View File

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

View File

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

106
README.md
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,35 +1,6 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2022 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" Module for working with Enigma2 bouquets. """
import re
from collections import Counter
from enum import Enum
from pathlib import Path
from app.commons import log
@@ -38,11 +9,10 @@ from app.eparser.ecommons import BqServiceType, BouquetService, Bouquets, Bouque
_TV_FILE = "bouquets.tv"
_RADIO_FILE = "bouquets.radio"
_DEFAULT_BOUQUET_NAME = "favourites"
_MARKER_PREFIX = "[MARKER!] "
class BouquetsWriter:
""" Class for creating and writing bouquet files.
""" Class for creating and writing bouquet files..
If "force_bq_names" then naming the files using the name of the bouquet.
Some images may have problems displaying the favorites list!
@@ -60,54 +30,42 @@ class BouquetsWriter:
self._marker_index = 1
self._space_index = 0
self._alt_names = set()
self._NAME_PATTERN = re.compile("[^\\w_()]+")
def write(self):
line = []
pattern = re.compile("[^\\w_()]+")
for bqs in self._bouquets:
line.clear()
line.append(f"#NAME {bqs.name}\n")
line.append("#NAME {}\n".format(bqs.name))
bq_file_names = {b.file for b in bqs.bouquets}
count = 1
m_count = 0
for bq in bqs.bouquets:
bq_name = bq.file
if not bq_name:
if self._force_bq_names:
bq_name = re.sub(self._NAME_PATTERN, "_", bq.name)
bq_name = re.sub(pattern, "_", bq.name)
else:
bq_name = f"de{count:02d}"
bq_name = "de{0:02d}".format(count)
while bq_name in bq_file_names:
count += 1
bq_name = f"de{count:02d}"
bq_name = "de{0:02d}".format(count)
bq_file_names.add(bq_name)
bq_type = BqType(bq.type)
if bq_type is BqType.MARKER:
m_data = bq.file.split(":") if bq.file else None
b_name = m_data[-1].strip() if m_data else bq.name.lstrip(_MARKER_PREFIX)
line.append(self._MARKER.format(m_count, b_name))
m_count += 1
else:
if bq_type is BqType.BOUQUET:
bq_name = re.sub(self._NAME_PATTERN, "_", bq.name)
self.write_sub_bouquet(self._path, bq_name, bq, bqs.type)
else:
self.write_bouquet(f"{self._path}userbouquet.{bq_name}.{bqs.type}", bq.name, bq.services)
line.append(self._SERVICE.format(2 if bqs.type == BqType.RADIO.value else 1, bq_name, bqs.type))
line.append(self._SERVICE.format(2 if bq.type == BqType.RADIO.value else 1, bq_name, bq.type))
self.write_bouquet(self._path + "userbouquet.{}.{}".format(bq_name, bq.type), bq.name, bq.services)
with open(f"{self._path}bouquets.{bqs.type}", "w", encoding="utf-8", newline="\n") as file:
with open(self._path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file:
file.writelines(line)
def write_bouquet(self, path, name, services):
""" Writes single bouquet file. """
bouquet = [f"#NAME {name}\n"]
bouquet = ["#NAME {}\n".format(name)]
for srv in services:
s_type = srv.service_type
if s_type == BqServiceType.IPTV.name:
bouquet.append(f"#SERVICE {srv.fav_id.strip()}\n")
bouquet.append("#SERVICE {}\n".format(srv.fav_id.strip()))
elif s_type == BqServiceType.MARKER.name:
m_data = srv.fav_id.strip().split(":")
m_data[2] = self._marker_index
@@ -121,56 +79,30 @@ class BouquetsWriter:
if services:
p = Path(path)
alt_name = srv.data_id
f_name = f"alternatives.{alt_name}{p.suffix}"
f_name = "alternatives.{}{}".format(alt_name, p.suffix)
if self._force_bq_names:
alt_name = re.sub(self._ALT_PAT, "_", srv.service).lower()
f_name = f"alternatives.{alt_name}{p.suffix}"
f_name = "alternatives.{}{}".format(alt_name, p.suffix)
alt_path = "{}/{}".format(p.parent, f_name)
bouquet.append(self._ALT.format(f_name))
self.write_bouquet(f"{p.parent}/{f_name}", srv.service, services)
self.write_bouquet(alt_path, 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("#SERVICE {}:{}\n#DESCRIPTION {}\n".format(data, srv.service, srv.service))
else:
bouquet.append(f"#SERVICE {data}\n")
bouquet.append("#SERVICE {}\n".format(data))
with open(path, "w", encoding="utf-8", newline="\n") as file:
with open(path, "w", encoding="utf-8") as file:
file.writelines(bouquet)
def write_sub_bouquet(self, path, file_name, bq, bq_type):
bouquet = [f"#NAME {bq.name}\n"]
sb_type = 2 if bq_type == BqType.RADIO.value else 1
for sb in bq.services:
bq_name = f"subbouquet.{re.sub(self._NAME_PATTERN, '_', sb.name)}.{sb.type}"
self.write_bouquet(f"{path}{bq_name}", sb.name, sb.services)
bouquet.append(f"#SERVICE 1:7:{sb_type}:0:0:0:0:0:0:0:FROM BOUQUET \"{bq_name}\" ORDER BY bouquet\n")
with open(f"{self._path}userbouquet.{file_name}.{bq_type}", "w", encoding="utf-8", newline="\n") as file:
file.writelines(bouquet)
class ServiceType(Enum):
SERVICE = "0"
BOUQUET = "7" # Sub bouquet.
MARKER = "64"
SPACE = "832" # Hidden marker.
ALT = "134" # Alternatives.
UDP = "256"
@classmethod
def _missing_(cls, value):
log("Error. No matching service type [{} {}] was found.".format(cls.__name__, value))
return cls.SERVICE
class BouquetsReader:
""" Class for reading and parsing bouquets. """
_ALT_PAT = re.compile(r".*alternatives\.+(.*)\.([tv|radio]+).*")
_BQ_PAT = re.compile(r".*\s+\W(.*bouquet)\.+(.*)\.+[tv|radio].*")
_SUB_BQ_PAT = re.compile(r".*subbouquet\.+(.*)\.([tv|radio]+).*")
_ALT_PAT = re.compile(".*alternatives\\.+(.*)\\.([tv|radio]+).*")
_BQ_PAT = re.compile(".*userbouquet\\.+(.*)\\.+[tv|radio].*")
_STREAM_TYPES = {"4097", "5001", "5002", "8193", "8739"}
__slots__ = ["_path"]
@@ -184,96 +116,77 @@ class BouquetsReader:
def parse_bouquets(self, bq_name, bq_type):
with open(self._path + bq_name, encoding="utf-8", errors="replace") as file:
line = file.readline()
_, _, bqs_name = line.partition("#NAME")
if not bqs_name:
log(f"No bouquets name found in '{bq_name}'")
bqs_name = "Bouquets (TV)" if bq_type == BqType.TV.value else "Bouquets (Radio)"
bouquets = Bouquets(bqs_name.strip(), bq_type, [])
lines = file.readlines()
bouquets = None
nm_sep = "#NAME"
b_names = set()
real_b_names = Counter()
for line in file.readlines():
if "#SERVICE" in line:
for line in lines:
if nm_sep in line:
_, _, name = line.partition(nm_sep)
bouquets = Bouquets(name.strip(), bq_type, [])
if bouquets and "#SERVICE" in line:
name = re.match(self._BQ_PAT, line)
if name:
prefix, b_name = name.group(1), name.group(2)
b_name = name.group(1)
if b_name in b_names:
log(f"The list of bouquets contains duplicate [{b_name}] names!")
log("The list of bouquets contains duplicate [{}] names!".format(b_name))
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, b_name, bq_type)
if rb_name in real_b_names:
log(f"Bouquet file '{prefix}.{b_name}.{bq_type}' has duplicate name: {rb_name}")
log("Bouquet file 'userbouquet.{}.{}' has duplicate name: {}".format(b_name, bq_type,
rb_name))
real_b_names[rb_name] += 1
rb_name = f"{rb_name} {real_b_names[rb_name]}"
rb_name = "{} {}".format(rb_name, real_b_names[rb_name])
else:
real_b_names[rb_name] = 0
bouquets[2].append(Bouquet(rb_name, bq_type, services, None, None, b_name))
else:
s_data = line.split(":")
if len(s_data) == 12 and s_data[1] == ServiceType.MARKER.value:
b_name = f"{_MARKER_PREFIX}{s_data[-1].strip()}"
bouquets[2].append(Bouquet(b_name, BqType.MARKER.value, [], None, None, line.strip()))
else:
log(f"Unsupported or invalid data format: [{line}].")
else:
log(f"Unsupported or invalid line format: [{line}].")
raise ValueError("No bouquet name found for: {}".format(line))
return bouquets
@staticmethod
def get_bouquet(path, bq_name, bq_type, prefix="userbouquet"):
""" Parsing services ids from bouquet file. """
with open(f"{path}{prefix}.{bq_name}.{bq_type}", encoding="utf-8", errors="replace") as file:
with open(path + "{}.{}.{}".format(prefix, bq_name, bq_type), encoding="utf-8", errors="replace") as file:
chs_list = file.read()
services = []
srvs = list(filter(None, chs_list.split("\n#SERVICE"))) # filtering ['']
# May come across empty[wrong] files!
if not srvs:
log(f"Bouquet file 'userbouquet.{bq_name}.{bq_type}' is empty or wrong!")
return f"{bq_name} [empty]", services
log("Bouquet file 'userbouquet.{}.{}' is empty or wrong!".format(bq_name, bq_type))
return "{} [empty]".format(bq_name), services
bq_name = srvs.pop(0)
for num, srv in enumerate(srvs, start=1):
srv_data = srv.strip().split(":")
data_len = len(srv_data)
if data_len < 10:
log(f"The bouquet [{bq_name}] service [{num}] has the wrong data format: [{srv}]")
continue
s_type = ServiceType(srv_data[1])
if s_type is ServiceType.MARKER:
s_type = srv_data[1]
if s_type == "64":
m_data, sep, desc = srv.partition("#DESCRIPTION")
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.MARKER, srv, num))
elif s_type is ServiceType.SPACE:
elif s_type == "832":
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:
elif s_type == "134":
alt = re.match(BouquetsReader._ALT_PAT, srv)
if alt:
alt_name, alt_type = alt.group(1), alt.group(2)
alt_bq_name, alt_srvs = BouquetsReader.get_bouquet(path, alt_name, alt_type, "alternatives")
services.append(BouquetService(alt_bq_name, BqServiceType.ALT, alt_name, tuple(alt_srvs)))
elif s_type is ServiceType.BOUQUET:
sub = re.match(BouquetsReader._SUB_BQ_PAT, srv)
if sub:
sub_name, sub_type = sub.group(1), sub.group(2)
sub_bq_name, sub_srvs = BouquetsReader.get_bouquet(path, sub_name, sub_type, "subbouquet")
bq = Bouquet(sub_bq_name, sub_type, tuple(sub_srvs), None, None, sub_name)
services.append(BouquetService(sub_bq_name, BqServiceType.BOUQUET, bq, num))
elif srv_data[0].strip() in BouquetsReader._STREAM_TYPES or srv_data[10].startswith(("http", "rtsp")):
stream_data, sep, desc = srv.partition("#DESCRIPTION")
desc = desc.lstrip(":").strip() if desc else srv_data[-1].strip()
services.append(BouquetService(desc, BqServiceType.IPTV, srv, num))
else:
fav_id = f"{srv_data[3]}:{srv_data[4]}:{srv_data[5]}:{srv_data[6]}"
fav_id = "{}:{}:{}:{}".format(srv_data[3], srv_data[4], srv_data[5], srv_data[6])
name = None
if data_len == 12:
if len(srv_data) == 12:
name, sep, desc = str(srv_data[-1]).partition("\n#DESCRIPTION")
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id.upper(), num))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

171
app/ui/app_menu_bar.ui Normal file
View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,144 +1,244 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2023 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" Receiver control module via HTTP API. """
import os
import re
from datetime import datetime
from enum import Enum
from urllib.parse import quote
from gi.repository import GLib
from .dialogs import get_builder, get_message
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH
from app.settings import IS_WIN
from .dialogs import get_builder
from .dialogs import show_dialog, DialogType, get_message
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column
from ..commons import run_task, run_with_delay, log, run_idle
from ..connections import HttpAPI
from ..settings import IS_DARWIN, IS_LINUX, IS_WIN
from ..eparser.ecommons import BqServiceType
class ControlTool(Gtk.Box):
class ControlBox(Gtk.HBox):
_TIME_STR = "%Y-%m-%d %H:%M"
def __init__(self, app, settings, *args, **kwargs):
class Tool(Enum):
""" The currently displayed tool. """
REMOTE = "control"
EPG = "epg"
TIMERS = "timers"
TIMER = "timer"
class EpgRow(Gtk.ListBoxRow):
def __init__(self, event: dict, **properties):
super().__init__(**properties)
self._event_data = event
h_box = Gtk.HBox()
h_box.set_orientation(Gtk.Orientation.VERTICAL)
self._title = event.get("e2eventtitle", "")
title_label = Gtk.Label(self._title)
self._desc = event.get("e2eventdescription", "")
description = Gtk.Label()
description.set_markup("<i>{}</i>".format(self._desc))
description.set_line_wrap(True)
description.set_max_width_chars(25)
start = int(event.get("e2eventstart", "0"))
start_time = datetime.fromtimestamp(start)
end_time = datetime.fromtimestamp(start + int(event.get("e2eventduration", "0")))
time_label = Gtk.Label()
time_label.set_margin_top(5)
self._time_header = "{} - {}".format(start_time.strftime("%A, %H:%M"), end_time.strftime("%H:%M"))
time_label.set_markup("<b>{}</b>".format(self._time_header))
h_box.add(time_label)
h_box.add(title_label)
h_box.add(description)
sep = Gtk.Separator()
sep.set_margin_top(5)
h_box.add(sep)
h_box.set_spacing(5)
self.add(h_box)
self.show_all()
@property
def event_data(self):
return self._event_data or {}
@property
def title(self):
return self._title or ""
@property
def desc(self):
return self._desc or ""
@property
def time_header(self):
return self._time_header or ""
class TimerRow(Gtk.ListBoxRow):
_UI_PATH = UI_RESOURCES_PATH + "timer_row.glade"
def __init__(self, timer, **properties):
super().__init__(**properties)
self._timer = timer
builder = get_builder(self._UI_PATH, None, use_str=True)
row_box = builder.get_object("timer_row_box")
name_label = builder.get_object("timer_name_label")
description_label = builder.get_object("timer_description_label")
service_name_label = builder.get_object("timer_service_name_label")
time_label = builder.get_object("timer_time_label")
name_label.set_text(timer.get("e2name", "") or "")
description_label.set_text(timer.get("e2description", "") or "")
service_name_label.set_text(timer.get("e2servicename", "") or "")
# Time
start_time = datetime.fromtimestamp(int(timer.get("e2timebegin", "0")))
end_time = datetime.fromtimestamp(int(timer.get("e2timeend", "0")))
time_label.set_text("{} - {}".format(start_time.strftime("%A, %H:%M"), end_time.strftime("%H:%M")))
self.add(row_box)
self.show()
@property
def timer(self):
return self._timer
class TimerAction(Enum):
ADD = 0
EVENT = 1
CHANGE = 2
def __init__(self, app, http_api, settings, *args, **kwargs):
super().__init__(*args, **kwargs)
self._http_api = http_api
self._settings = settings
self._update_epg = False
self._app = app
self._app.connect("layout-changed", self.on_layout_changed)
self._pix = None
self._last_tool = self.Tool.REMOTE
self._timer_action = self.TimerAction.ADD
self._current_timer = {}
handlers = {"on_volume_changed": self.on_volume_changed,
"on_screenshot_draw": self.on_screenshot_draw,
"on_network_toggled": self.on_network_toggled,
"on_network_view_query_tooltip": self.on_network_view_query_tooltip}
handlers = {"on_visible_tool": self.on_visible_tool,
"on_volume_changed": self.on_volume_changed,
"on_epg_press": self.on_epg_press,
"on_epg_filter_changed": self.on_epg_filter_changed,
"on_timers_press": self.on_timers_press,
"on_timers_drag_data_received": self.on_timers_drag_data_received}
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers)
self.pack_start(builder.get_object("control_box"), True, True, 0)
self._remote_box = builder.get_object("remote_box")
self._screenshot_area = builder.get_object("screenshot_area")
self.add(builder.get_object("main_box_frame"))
self._stack = builder.get_object("stack")
self._screenshot_image = builder.get_object("screenshot_image")
self._screenshot_button_box = builder.get_object("screenshot_button_box")
self._screenshot_check_button = builder.get_object("screenshot_check_button")
self._screenshot_check_button.bind_property("active", self._screenshot_area, "visible")
self._screenshot_check_button.bind_property("active", self._screenshot_image, "visible")
self._snr_value_label = builder.get_object("snr_value_label")
self._ber_value_label = builder.get_object("ber_value_label")
self._agc_value_label = builder.get_object("agc_value_label")
self._snr_level_bar = builder.get_object("snr_level_bar")
self._ber_level_bar = builder.get_object("ber_level_bar")
self._agc_level_bar = builder.get_object("agc_level_bar")
self._volume_button = builder.get_object("volume_button")
self._header_box = builder.get_object("control_header_box")
self._screenshot_button_box = builder.get_object("screenshot_button_box")
# Network.
self._network_button = builder.get_object("control_network_button")
self._network_model = builder.get_object("network_model")
self._epg_list_box = builder.get_object("epg_list_box")
self._epg_list_box.set_filter_func(self.epg_filter_function)
self._epg_filter_entry = builder.get_object("epg_filter_entry")
self._timers_list_box = builder.get_object("timers_list_box")
self._app._control_revealer.bind_property("visible", self, "visible")
# Timers
self._timer_remove_button = builder.get_object("timer_remove_button")
self._timer_remove_button.bind_property("visible", builder.get_object("timer_edit_button"), "visible")
# Timer
self._timer_name_entry = builder.get_object("timer_name_entry")
self._timer_desc_entry = builder.get_object("timer_desc_entry")
self._timer_service_entry = builder.get_object("timer_service_entry")
self._timer_service_ref_entry = builder.get_object("timer_service_ref_entry")
self._timer_event_id_entry = builder.get_object("timer_event_id_entry")
self._timer_begins_entry = builder.get_object("timer_begins_entry")
self._timer_ends_entry = builder.get_object("timer_ends_entry")
self._timer_begins_calendar = builder.get_object("timer_begins_calendar")
self._timer_begins_hr_button = builder.get_object("timer_begins_hr_button")
self._timer_begins_min_button = builder.get_object("timer_begins_min_button")
self._timer_ends_calendar = builder.get_object("timer_ends_calendar")
self._timer_ends_hr_button = builder.get_object("timer_ends_hr_button")
self._timer_ends_min_button = builder.get_object("timer_ends_min_button")
self._timer_enabled_switch = builder.get_object("timer_enabled_switch")
self._timer_action_combo_box = builder.get_object("timer_action_combo_box")
self._timer_after_combo_box = builder.get_object("timer_after_combo_box")
self._timer_mo_check_button = builder.get_object("timer_mo_check_button")
self._timer_tu_check_button = builder.get_object("timer_tu_check_button")
self._timer_we_check_button = builder.get_object("timer_we_check_button")
self._timer_th_check_button = builder.get_object("timer_th_check_button")
self._timer_fr_check_button = builder.get_object("timer_fr_check_button")
self._timer_sa_check_button = builder.get_object("timer_sa_check_button")
self._timer_su_check_button = builder.get_object("timer_su_check_button")
self._timer_location_switch = builder.get_object("timer_location_switch")
self._timer_location_entry = builder.get_object("timer_location_entry")
self._timer_location_switch.bind_property("active", self._timer_location_entry, "sensitive")
# Disable DnD for timer entries.
self._timer_name_entry.drag_dest_unset()
self._timer_desc_entry.drag_dest_unset()
self._timer_service_entry.drag_dest_unset()
# DnD initialization for the timer list.
self._timers_list_box.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
self._timers_list_box.drag_dest_add_text_targets()
self.init_actions(app)
if settings.alternate_layout:
self.on_layout_changed(app, True)
self.connect("hide", self.on_hide)
self.show()
def init_actions(self, app):
# Remote controller actions.
app.set_action("on_one", lambda a, v: self.on_remote_action(HttpAPI.Remote.ONE))
app.set_action("on_two", lambda a, v: self.on_remote_action(HttpAPI.Remote.TWO))
app.set_action("on_three", lambda a, v: self.on_remote_action(HttpAPI.Remote.THREE))
app.set_action("on_four", lambda a, v: self.on_remote_action(HttpAPI.Remote.FOUR))
app.set_action("on_five", lambda a, v: self.on_remote_action(HttpAPI.Remote.FIVE))
app.set_action("on_six", lambda a, v: self.on_remote_action(HttpAPI.Remote.SIX))
app.set_action("on_seven", lambda a, v: self.on_remote_action(HttpAPI.Remote.SEVEN))
app.set_action("on_eight", lambda a, v: self.on_remote_action(HttpAPI.Remote.EIGHT))
app.set_action("on_nine", lambda a, v: self.on_remote_action(HttpAPI.Remote.NINE))
app.set_action("on_zero", lambda a, v: self.on_remote_action(HttpAPI.Remote.ZERO))
# Remote controller actions
app.set_action("on_up", lambda a, v: self.on_remote_action(HttpAPI.Remote.UP))
app.set_action("on_down", lambda a, v: self.on_remote_action(HttpAPI.Remote.DOWN))
app.set_action("on_left", lambda a, v: self.on_remote_action(HttpAPI.Remote.LEFT))
app.set_action("on_right", lambda a, v: self.on_remote_action(HttpAPI.Remote.RIGHT))
app.set_action("on_next", lambda a, v: self.on_remote_action(HttpAPI.Remote.NEXT))
app.set_action("on_back", lambda a, v: self.on_remote_action(HttpAPI.Remote.BACK))
app.set_action("on_info", lambda a, v: self.on_remote_action(HttpAPI.Remote.INFO))
app.set_action("on_ok", lambda a, v: self.on_remote_action(HttpAPI.Remote.OK))
app.set_action("on_menu", lambda a, v: self.on_remote_action(HttpAPI.Remote.MENU))
app.set_action("on_exit", lambda a, v: self.on_remote_action(HttpAPI.Remote.EXIT))
app.set_action("on_epg", lambda a, v: self.on_remote_action(HttpAPI.Remote.EPG))
app.set_action("on_ch_up", lambda a, v: self.on_remote_action(HttpAPI.Remote.CH_UP))
app.set_action("on_ch_down", lambda a, v: self.on_remote_action(HttpAPI.Remote.CH_DOWN))
app.set_action("on_red", lambda a, v: self.on_remote_action(HttpAPI.Remote.RED))
app.set_action("on_green", lambda a, v: self.on_remote_action(HttpAPI.Remote.GREEN))
app.set_action("on_yellow", lambda a, v: self.on_remote_action(HttpAPI.Remote.YELLOW))
app.set_action("on_blue", lambda a, v: self.on_remote_action(HttpAPI.Remote.BLUE))
app.set_action("on_audio", lambda a, v: self.on_remote_action(HttpAPI.Remote.AUDIO))
app.set_action("on_tv", lambda a, v: self.on_remote_action(HttpAPI.Remote.TV))
app.set_action("on_radio", lambda a, v: self.on_remote_action(HttpAPI.Remote.RADIO))
app.set_action("on_fav", lambda a, v: self.on_remote_action(HttpAPI.Remote.FAV))
# Playback.
app.set_action("on_prev_media", lambda a, v: self.on_player_action(HttpAPI.Request.PLAYER_PREV))
app.set_action("on_play_media", lambda a, v: self.on_player_action(HttpAPI.Request.PLAYER_PLAY))
app.set_action("on_stop_media", lambda a, v: self.on_player_action(HttpAPI.Request.PLAYER_STOP))
app.set_action("on_next_media", lambda a, v: self.on_player_action(HttpAPI.Request.PLAYER_NEXT))
# Power.
# Power
app.set_action("on_standby", lambda a, v: self.on_power_action(HttpAPI.Power.STANDBY))
app.set_action("on_wake_up", lambda a, v: self.on_power_action(HttpAPI.Power.WAKEUP))
app.set_action("on_reboot", lambda a, v: self.on_power_action(HttpAPI.Power.REBOOT))
app.set_action("on_restart_gui", lambda a, v: self.on_power_action(HttpAPI.Power.RESTART_GUI))
app.set_action("on_shutdown", lambda a, v: self.on_power_action(HttpAPI.Power.DEEP_STANDBY))
# Screenshots.
# Screenshots
app.set_action("on_screenshot_all", self.on_screenshot_all)
app.set_action("on_screenshot_video", self.on_screenshot_video)
app.set_action("on_screenshot_osd", self.on_screenshot_osd)
# Timers
app.set_action("on_timer_add", self.on_timer_add)
app.set_action("on_timer_add_from_event", self.on_timer_add_from_event)
app.set_action("on_timer_remove", self.on_timer_remove)
app.set_action("on_timer_edit", self.on_timer_edit)
app.set_action("on_timer_save", self.on_timer_save)
app.set_action("on_timer_cancel", self.on_timer_cancel)
app.set_action("on_timer_begins_set", self.on_timer_begins_set)
app.set_action("on_timer_ends_set", self.on_timer_ends_set)
def on_layout_changed(self, app, alt_layout):
children = self._remote_box.get_children()
self._remote_box.reorder_child(children[0], len(children) - 1)
self._remote_box.reorder_child(children[-1], 0)
pack_type = Gtk.PackType.END if alt_layout else Gtk.PackType.START
self._header_box.set_child_packing(self._network_button, False, False, 0, pack_type)
pack_type = Gtk.PackType.START if alt_layout else Gtk.PackType.END
self._header_box.set_child_packing(self._screenshot_button_box, False, False, 0, pack_type)
@property
def update_epg(self):
return self._update_epg
def on_visible_tool(self, stack, param):
tool = self.Tool(stack.get_visible_child_name())
self._update_epg = tool is self.Tool.EPG
if tool is self.Tool.TIMERS:
self.update_timer_list()
if tool is not self.Tool.TIMER:
self._last_tool = tool
def on_hide(self, item):
self._update_epg = False
# ***************** Remote controller ********************* #
@@ -149,17 +249,14 @@ class ControlTool(Gtk.Box):
self._remote_revealer.set_reveal_child(state)
if state:
self._app.send_http_request(HttpAPI.Request.VOL, "state", self.update_volume)
self._http_api.send(HttpAPI.Request.VOL, "state", self.update_volume)
def on_remote_action(self, action):
self._app.send_http_request(HttpAPI.Request.REMOTE, action, self.on_response)
def on_player_action(self, action):
self._app.send_http_request(action, "", self.on_response)
self._http_api.send(HttpAPI.Request.REMOTE, action, self.on_response)
@run_with_delay(0.5)
def on_volume_changed(self, button, value):
self._app.send_http_request(HttpAPI.Request.VOL, f"{value:.0f}", self.on_response)
self._http_api.send(HttpAPI.Request.VOL, "{:.0f}".format(value), self.on_response)
def update_volume(self, vol):
if "error_code" in vol:
@@ -171,9 +268,9 @@ class ControlTool(Gtk.Box):
if "error_code" in resp:
return
if self._screenshot_check_button.get_active() and self._app.http_api:
ref = "mode=all" if self._app.http_api.is_owif else "d="
self._app.send_http_request(HttpAPI.Request.GRUB, ref, self.update_screenshot)
if self._screenshot_check_button.get_active():
ref = "mode=all" if self._http_api.is_owif else "d="
self._http_api.send(HttpAPI.Request.GRUB, ref, self.update_screenshot)
@run_task
def update_screenshot(self, data):
@@ -184,43 +281,29 @@ class ControlTool(Gtk.Box):
if data:
from gi.repository import GdkPixbuf
allocation = self._screenshot_area.get_parent().get_allocation()
loader = GdkPixbuf.PixbufLoader.new_with_type("jpeg")
loader.set_size(allocation.width, allocation.height)
loader.set_size(280, 165)
try:
loader.write(data)
pix = loader.get_pixbuf()
except GLib.Error:
pass # NOP
else:
self._pix = pix
self._screenshot_area.queue_draw() # Redrawing the area!
GLib.idle_add(self._screenshot_image.set_from_pixbuf, pix)
finally:
loader.close()
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()
def on_screenshot_all(self, action, value=None):
if self._app.http_api:
self._app.send_http_request(HttpAPI.Request.GRUB, "mode=all" if self._app.http_api.is_owif else "d=",
self.on_screenshot)
self._http_api.send(HttpAPI.Request.GRUB, "mode=all" if self._http_api.is_owif else "d=",
self.on_screenshot)
def on_screenshot_video(self, action, value=None):
if self._app.http_api:
self._app.send_http_request(HttpAPI.Request.GRUB, "mode=video" if self._app.http_api.is_owif else "v=",
self.on_screenshot)
self._http_api.send(HttpAPI.Request.GRUB, "mode=video" if self._http_api.is_owif else "v=",
self.on_screenshot)
def on_screenshot_osd(self, action, value=None):
if self._app.http_api:
self._app.send_http_request(HttpAPI.Request.GRUB, "mode=osd" if self._app.http_api.is_owif else "o=",
self.on_screenshot)
self._http_api.send(HttpAPI.Request.GRUB, "mode=osd" if self._http_api.is_owif else "o=",
self.on_screenshot)
@run_task
def on_screenshot(self, data):
@@ -229,115 +312,370 @@ class ControlTool(Gtk.Box):
img = data.get("img_data", None)
if img:
GLib.idle_add(self._screenshot_button_box.set_sensitive, not IS_LINUX)
path = os.path.expanduser("~/Desktop") if not IS_LINUX else None
GLib.idle_add(self._screenshot_button_box.set_sensitive, IS_WIN)
path = os.path.expanduser("~/Desktop") if IS_WIN else None
try:
import tempfile
import subprocess
with tempfile.NamedTemporaryFile(mode="wb", suffix=".jpg", dir=path, delete=IS_LINUX) as tf:
with tempfile.NamedTemporaryFile(mode="wb", suffix=".jpg", dir=path, delete=not IS_WIN) as tf:
tf.write(img)
if IS_LINUX:
cmd = ["xdg-open", tf.name]
elif IS_DARWIN:
cmd = ["open", tf.name]
else:
cmd = [tf.name]
if not IS_WIN:
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
# File must be closed.
if IS_WIN:
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=IS_WIN).communicate()
f_name = tf.name
subprocess.Popen([f_name], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate()
finally:
GLib.idle_add(self._screenshot_button_box.set_sensitive, True)
def on_power_action(self, action):
self._app.send_http_request(HttpAPI.Request.POWER, action, lambda resp: log("Power status changed..."))
self._http_api.send(HttpAPI.Request.POWER, action, lambda resp: log("Power status changed..."))
def update_signal(self, sig):
snr = sig.get("e2snr", "0 %").strip() if sig else "0 %"
snr_db = sig.get("e2snrdb", "0 dB").strip() if sig else "0 dB"
acg = sig.get("e2acg", "0 %").strip() if sig else "0 %"
ber = (sig.get("e2ber", None) or "").strip() if sig else ""
# Labels.
self._snr_value_label.set_text(f"{snr_db} ({snr})")
self._agc_value_label.set_text(acg)
self._ber_value_label.set_text(ber)
# Level bars.
self._snr_level_bar.set_value(int(snr.strip("%N/A") or 0))
self._agc_level_bar.set_value(int(acg.rstrip("%N/A") or 0))
self._ber_level_bar.set_value(int(ber.rstrip("N/A") or 0))
self._snr_value_label.set_text(sig.get("e2snrdb", "0 dB").strip())
self._ber_value_label.set_text(str(sig.get("e2ber", None) or "0").strip())
self._agc_value_label.set_text(sig.get("e2acg", "0 %").strip())
# ***************** Network explorer ********************** #
# ************************ EPG **************************** #
def on_network_toggled(self, button):
self._network_model.clear()
if button.get_active():
self.update_network()
@run_task
def update_network(self):
pattern = re.compile(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})')
ips = [match for match in re.findall(pattern, os.popen("arp -a").read())]
for ip in ips:
if not self._network_button.get_active():
break
url = f"http://{ip}/web/{HttpAPI.Request.INFO.value}"
try:
resp = HttpAPI.get_response(HttpAPI.Request.INFO, url, timeout=5)
except OSError as e:
log(f"{ip} {e}")
else:
if resp.get("e2distroversion", None):
log(f"Receiver found. Model: {resp.get('e2model', 'N/A')} [{ip} ]")
self.append_box_data(resp)
def on_service_changed(self, ref):
self._app._wait_dialog.show()
self._http_api.send(HttpAPI.Request.EPG, quote(ref), self.update_epg_data)
@run_idle
def append_box_data(self, data):
ip = data.get('e2lanip', 'N/A')
itr = self._network_model.append((data.get("e2model", "N/A"), ip, None, data, None))
GLib.timeout_add_seconds(3, self.check_power_state, itr, priority=GLib.PRIORITY_LOW)
def update_epg_data(self, epg):
list(map(self._epg_list_box.remove, (r for r in self._epg_list_box)))
list(map(lambda e: self._epg_list_box.add(self.EpgRow(e)), epg.get("event_list", [])))
self._app._wait_dialog.hide()
def on_network_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
result = view.get_dest_row_at_pos(x, y)
if not result:
return False
def on_epg_press(self, list_box, event):
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(list_box) > 0:
row = list_box.get_selected_row()
if row:
self.set_timer_from_event_data(row.event_data)
path, pos = result
model = view.get_model()
data = model[path][3]
def on_epg_filter_changed(self, entry):
self._epg_list_box.invalidate_filter()
dist = data.get("e2distroversion", "N/A")
img = data.get("e2imageversion", "N/A")
txt = f"Distro version: {dist}\nImage version: {img}"
tooltip.set_text(txt)
view.set_tooltip_row(tooltip, path)
return True
def epg_filter_function(self, row):
txt = self._epg_filter_entry.get_text().upper()
return any((not txt, txt in row.time_header.upper(), txt in row.title.upper(), txt in row.desc.upper()))
def check_power_state(self, itr):
active = self._network_button.get_active()
if not active:
return False
def on_timer_add_from_event(self, action, value=None):
rows = self._epg_list_box.get_selected_rows()
if not rows:
self._app.show_error_dialog("No selected item!")
return
data = self._network_model.get_value(itr, 3)
url = f"http://{data.get('e2lanip', 'N/A')}/web/powerstate"
self.update_power_state(itr, url)
return active
refs = []
for row in rows:
event = row.event_data
ref = "timeraddbyeventid?sRef={}&eventid={}&justplay=0".format(event.get("e2eventservicereference", ""),
event.get("e2eventid", ""))
refs.append(ref)
@run_task
def update_power_state(self, itr, url):
try:
resp = HttpAPI.get_response(HttpAPI.Request.POWER, url, timeout=2)
except OSError as e:
log(e)
gen = self.write_timers_list(refs)
GLib.idle_add(lambda: next(gen, False))
def write_timers_list(self, refs):
self._app._wait_dialog.show()
tasks = list(refs)
for ref in refs:
self._http_api.send(HttpAPI.Request.TIMER, ref, lambda x: tasks.pop())
yield True
while tasks:
yield True
self._stack.set_visible_child_name(self.Tool.TIMERS.value)
# *********************** Timers *************************** #
def on_timers_press(self, list_box, event):
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(list_box) > 0:
self.on_timer_edit()
def update_timer_list(self):
self._app._wait_dialog.show()
self._http_api.send(HttpAPI.Request.TIMER_LIST, "", self.update_timers_data)
@run_idle
def update_timers_data(self, timers):
list(map(self._timers_list_box.remove, (r for r in self._timers_list_box)))
list(map(lambda t: self._timers_list_box.add(self.TimerRow(t)), timers.get("timer_list", [])))
self._timer_remove_button.set_visible(len(self._timers_list_box))
self._app._wait_dialog.hide()
def on_timer_add(self, action=None, value=None):
self._timer_action = self.TimerAction.ADD
date = datetime.now()
self.set_begins_date(date)
self.set_ends_date(date)
self._timer_event_id_entry.set_text("")
self._timer_location_switch.set_active(False)
self.set_repetition_flags(0)
self._stack.set_visible_child_name(self.Tool.TIMER.value)
def on_timer_remove(self, action, value=None):
rows = self._timers_list_box.get_selected_rows()
if not rows or show_dialog(DialogType.QUESTION, self._app._main_window) != Gtk.ResponseType.OK:
return
refs = {}
for row in rows:
timer = row.timer
ref = "timerdelete?sRef={}&begin={}&end={}".format(quote(timer.get("e2servicereference", "")),
timer.get("e2timebegin", ""),
timer.get("e2timeend", ""))
refs[ref] = row
self._app._wait_dialog.show("Deleting data...")
gen = self.remove_timers(refs)
GLib.idle_add(lambda: next(gen, False))
def remove_timers(self, refs):
tasks = list(refs)
removed = set()
for ref in refs:
yield from self.remove_timer(ref, removed, tasks)
while tasks:
yield True
list(map(self._timers_list_box.remove, (refs[ref] for ref in refs if ref in removed)))
self._app._wait_dialog.hide()
self._timer_remove_button.set_visible(len(self._timers_list_box))
yield True
def remove_timer(self, ref, removed, tasks=None):
def callback(resp):
if resp.get("e2state", "") == "True":
log(resp.get("e2statetext", ""))
removed.add(ref)
else:
log(resp.get("e2statetext", None) or "Timer deletion error.")
if tasks:
tasks.pop()
self._http_api.send(HttpAPI.Request.TIMER, ref, callback)
yield True
def on_timer_edit(self, action=None, value=None):
row = self._timers_list_box.get_selected_row()
if row:
self._timer_action = self.TimerAction.CHANGE
timer = row.timer
self._current_timer = timer
self._timer_name_entry.set_text(timer.get("e2name", ""))
self._timer_desc_entry.set_text(timer.get("e2description", "") or "")
self._timer_service_entry.set_text(timer.get("e2servicename", "") or "")
self._timer_service_ref_entry.set_text(timer.get("e2servicereference", ""))
self._timer_event_id_entry.set_text(timer.get("e2eit", ""))
self._timer_enabled_switch.set_active((timer.get("e2disabled", "0") == "0"))
self._timer_action_combo_box.set_active_id(timer.get("e2justplay", "0"))
self._timer_after_combo_box.set_active_id(timer.get("e2afterevent", "0"))
self.set_time_data(int(timer.get("e2timebegin", "0")), int(timer.get("e2timeend", "0")))
location = timer.get("e2location", "")
self._timer_location_entry.set_text("" if location == "None" else location)
# Days
self.set_repetition_flags(int(timer.get("e2repeated", "0")))
self._stack.set_visible_child_name(self.Tool.TIMER.value)
def on_timer_save(self, action, value=None):
args = []
t_data = self.get_timer_data()
s_ref = quote(t_data.get("sRef", ""))
if self._timer_action is self.TimerAction.EVENT:
args.append("timeraddbyeventid?sRef={}".format(s_ref))
args.append("eventid={}".format(t_data.get("eit", "0")))
args.append("justplay={}".format(t_data.get("justplay", "")))
args.append("tags={}".format(""))
else:
state = get_message("On" if resp.get("e2instandby", "N/A").strip() == "false" else "Standby")
GLib.idle_add(self._network_model.set_value, itr, 2, state)
if self._timer_action is self.TimerAction.ADD:
args.append("timeradd?sRef={}".format(s_ref))
args.append("deleteOldOnSave={}".format(0))
elif self._timer_action is self.TimerAction.CHANGE:
args.append("timerchange?sRef={}".format(s_ref))
args.append("channelOld={}".format(s_ref))
args.append("beginOld={}".format(self._current_timer.get("e2timebegin", "0")))
args.append("endOld={}".format(self._current_timer.get("e2timeend", "0")))
args.append("deleteOldOnSave={}".format(1))
args.append("begin={}".format(t_data.get("begin", "")))
args.append("end={}".format(t_data.get("end", "")))
args.append("name={}".format(quote(t_data.get("name", ""))))
args.append("description={}".format(quote(t_data.get("description", ""))))
args.append("tags={}".format(""))
args.append("eit={}".format("0"))
args.append("disabled={}".format(t_data.get("disabled", "1")))
args.append("justplay={}".format(t_data.get("justplay", "1")))
args.append("afterevent={}".format(t_data.get("afterevent", "0")))
args.append("repeated={}".format(self.get_repetition_flags()))
if __name__ == "__main__":
pass
if self._timer_location_switch.get_active():
args.append("dirname={}".format(self._timer_location_entry.get_text()))
self._http_api.send(HttpAPI.Request.TIMER, "&".join(args), self.timer_add_edit_callback)
@run_idle
def timer_add_edit_callback(self, resp):
if "error_code" in resp:
msg = "Error getting timer status.\n{}".format(resp.get("error_code"))
self._app.show_error_dialog(msg)
log(msg)
return
state = resp.get("e2state", None)
if state == "False":
msg = resp.get("e2statetext", "")
self._app.show_error_dialog(msg)
log(msg)
if state == "True":
log(resp.get("e2statetext", ""))
self._stack.set_visible_child_name(self._last_tool.value)
else:
log("Error getting timer status. No response!")
def on_timer_cancel(self, action, value=None):
self._stack.set_visible_child_name(self._last_tool.value)
def on_timer_begins_set(self, action, value=None):
self.set_begins_date(self.get_begins_date())
def on_timer_ends_set(self, action, value=None):
self.set_ends_date(self.get_ends_date())
def get_begins_date(self):
date = self._timer_begins_calendar.get_date()
return datetime(year=date.year, month=date.month + 1, day=date.day,
hour=int(self._timer_begins_hr_button.get_value()),
minute=int(self._timer_begins_min_button.get_value()))
def set_begins_date(self, date):
hour = date.hour
minute = date.minute
self._timer_begins_hr_button.set_value(hour)
self._timer_begins_min_button.set_value(minute)
self._timer_begins_calendar.select_day(date.day)
self._timer_begins_calendar.select_month(date.month - 1, date.year)
self._timer_begins_entry.set_text("{}-{}-{} {}:{:02d}".format(date.year, date.month, date.day, hour, minute))
def get_ends_date(self):
date = self._timer_ends_calendar.get_date()
return datetime(year=date.year, month=date.month + 1, day=date.day,
hour=int(self._timer_ends_hr_button.get_value()),
minute=int(self._timer_ends_min_button.get_value()))
def set_ends_date(self, date):
hour = date.hour
minute = date.minute
self._timer_ends_hr_button.set_value(hour)
self._timer_ends_min_button.set_value(minute)
self._timer_ends_calendar.select_day(date.day)
self._timer_ends_calendar.select_month(date.month - 1, date.year)
self._timer_ends_entry.set_text("{}-{}-{} {}:{:02d}".format(date.year, date.month, date.day, hour, minute))
def set_timer_from_event_data(self, timer):
self._stack.set_visible_child_name(self.Tool.TIMER.value)
self._timer_action = self.TimerAction.EVENT
self._timer_name_entry.set_text(timer.get("e2eventtitle", ""))
self._timer_desc_entry.set_text(timer.get("e2eventdescription", ""))
self._timer_service_entry.set_text(timer.get("e2eventservicename", ""))
self._timer_service_ref_entry.set_text(timer.get("e2eventservicereference", ""))
self._timer_event_id_entry.set_text(timer.get("e2eventid", ""))
self._timer_action_combo_box.set_active_id("1")
self._timer_after_combo_box.set_active_id("3")
start_time = int(timer.get("e2eventstart", "0"))
self.set_time_data(start_time, start_time + int(timer.get("e2eventduration", "0")))
def set_time_data(self, start_time, end_time):
""" Sets values for time widgets. """
ev_time_start = datetime.fromtimestamp(start_time) or datetime.now()
ev_time_end = datetime.fromtimestamp(end_time) or datetime.now()
self._timer_begins_entry.set_text(ev_time_start.strftime(self._TIME_STR))
self._timer_ends_entry.set_text(ev_time_end.strftime(self._TIME_STR))
self._timer_begins_calendar.select_day(ev_time_start.day)
self._timer_begins_calendar.select_month(ev_time_start.month - 1, ev_time_start.year)
self._timer_ends_calendar.select_day(ev_time_end.day)
self._timer_ends_calendar.select_month(ev_time_end.month - 1, ev_time_end.year)
self._timer_begins_hr_button.set_value(ev_time_start.hour)
self._timer_begins_min_button.set_value(ev_time_start.minute)
self._timer_ends_hr_button.set_value(ev_time_end.hour)
self._timer_ends_min_button.set_value(ev_time_end.minute)
def get_timer_data(self):
""" Returns timer data as a dict. """
return {"sRef": self._timer_service_ref_entry.get_text(),
"begin": int(datetime.strptime(self._timer_begins_entry.get_text(), self._TIME_STR).timestamp()),
"end": int(datetime.strptime(self._timer_ends_entry.get_text(), self._TIME_STR).timestamp()),
"name": self._timer_name_entry.get_text(),
"description": self._timer_desc_entry.get_text(),
"dirname": "",
"eit": self._timer_event_id_entry.get_text(),
"disabled": int(not self._timer_enabled_switch.get_active()),
"justplay": self._timer_action_combo_box.get_active_id(),
"afterevent": self._timer_after_combo_box.get_active_id(),
"repeated": self.get_repetition_flags()}
def get_repetition_flags(self):
""" Returns flags for repetition. """
day_flags = 0
for i, box in enumerate((self._timer_mo_check_button,
self._timer_tu_check_button,
self._timer_we_check_button,
self._timer_th_check_button,
self._timer_fr_check_button,
self._timer_sa_check_button,
self._timer_su_check_button)):
if box.get_active():
day_flags = day_flags | (1 << i)
return day_flags
def set_repetition_flags(self, flags):
for i, box in enumerate((self._timer_mo_check_button,
self._timer_tu_check_button,
self._timer_we_check_button,
self._timer_th_check_button,
self._timer_fr_check_button,
self._timer_sa_check_button,
self._timer_su_check_button)):
box.set_active(flags & 1 == 1)
flags = flags >> 1
# ***************** Drag-and-drop ********************* #
def on_timers_drag_data_received(self, box, context, x, y, data, info, time):
txt = data.get_text()
if txt:
itr_str, sep, source = txt.partition(self._app.DRAG_SEP)
if not source:
return
itrs = itr_str.split(",")
if len(itrs) > 1:
self._app.show_error_dialog("Please, select only one item!")
return
fav_id = None
if source == self._app.FAV_MODEL_NAME:
model = self._app.fav_view.get_model()
fav_id = model.get_value(model.get_iter_from_string(itrs[0]), Column.FAV_ID)
elif source == self._app.SERVICE_MODEL_NAME:
model = self._app.services_view.get_model()
fav_id = model.get_value(model.get_iter_from_string(itrs[0]), Column.SRV_FAV_ID)
service = self._app.current_services.get(fav_id, None)
if service:
if service.service_type == BqServiceType.ALT.name:
msg = "Alternative service.\n\n {}".format(get_message("Not implemented yet!"))
show_dialog(DialogType.ERROR, transient=self._app._main_window, text=msg)
context.finish(False, False, time)
return
self._timer_name_entry.set_text(service.service)
self._timer_service_entry.set_text(service.service)
self._timer_service_ref_entry.set_text(service.picon_id.rstrip(".png").replace("_", ":"))
self.on_timer_add()
context.finish(True, False, time)

View File

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

View File

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

553
app/ui/download_dialog.glade Executable file
View File

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

171
app/ui/download_dialog.py Normal file
View File

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

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,995 +0,0 @@
# -*- coding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2018-2023 Dmitriy Yefremov
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# Author: Dmitriy Yefremov
#
""" Module for working with EPG. """
import gzip
import locale
import os
import re
import shutil
import urllib.request
from datetime import datetime
from enum import Enum
from urllib.error import HTTPError, URLError
from urllib.parse import quote
from gi.repository import GLib
from app.commons import run_idle, run_task, run_with_delay
from app.connections import download_data, DownloadType, HttpAPI
from app.eparser.ecommons import BouquetService, BqServiceType
from app.settings import SEP, EpgSource, IS_DARWIN
from app.tools.epg import EPG, ChannelsParser, EpgEvent, XmlTvReader
from app.ui.dialogs import get_message, show_dialog, DialogType, get_builder
from app.ui.tasks import BGTaskWidget
from app.ui.timers import TimerTool
from ..main_helper import on_popup_menu, update_entry_data, scroll_to, update_toggle_model, update_filter_sat_positions
from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column, EPG_ICON, KeyboardKey, IS_GNOME_SESSION, Page, HeaderBar
class RefsSource(Enum):
SERVICES = 0
XML = 1
class EpgCache(dict):
def __init__(self, app):
super().__init__()
self._current_bq = None
self._reader = None
self._canceled = False
self._settings = app.app_settings
self._src = self._settings.epg_source
self._app = app
self._app.connect("bouquet-changed", self.on_bouquet_changed)
self._app.connect("profile-changed", self.on_profile_changed)
self._app.connect("task-canceled", self.on_xml_load_cancel)
self.init()
@run_with_delay(5)
def init(self):
if self._src is EpgSource.XML:
url = self._settings.epg_xml_source
gz_file = f"{self._settings.profile_data_path}epg{os.sep}epg.gz"
self._reader = XmlTvReader(gz_file, url)
@run_with_delay(2)
def process_data():
t = BGTaskWidget(self._app, "Processing XMLTV data...", self._reader.parse, )
self._app.emit("add-background-task", t)
if os.path.isfile(gz_file):
# Difference calculation between the current time and file modification.
dif = datetime.now() - datetime.fromtimestamp(os.path.getmtime(gz_file))
# We will update daily. -> Temporarily!!!
if dif.days > 0 and not self._canceled:
task = BGTaskWidget(self._app, "Downloading EPG...", self._reader.download, process_data, )
self._app.emit("add-background-task", task)
else:
process_data()
else:
if not self._canceled:
task = BGTaskWidget(self._app, "Downloading EPG...", self._reader.download, process_data, )
self._app.emit("add-background-task", task)
elif self._src is EpgSource.DAT:
self._reader = EPG.DatReader(f"{self._settings.profile_data_path}epg{os.sep}epg.dat")
self._reader.download()
GLib.timeout_add_seconds(self._settings.epg_update_interval, self.update_epg_data, priority=GLib.PRIORITY_LOW)
def on_bouquet_changed(self, app, bq):
self._current_bq = bq
def on_profile_changed(self, app, p):
self.clear()
def on_xml_load_cancel(self, app, widget):
self._canceled = True
def update_epg_data(self):
if self._src is EpgSource.HTTP:
api = self._app.http_api
bq = self._app.current_bouquet_files.get(self._current_bq, None)
if bq and api:
req = quote(f'FROM BOUQUET "userbouquet.{bq}.{self._current_bq.split(":")[-1]}"')
api.send(HttpAPI.Request.EPG_NOW, f'1:7:1:0:0:0:0:0:0:0:{req}', self.update_http_data)
elif self._src is EpgSource.XML:
self.update_xml_data()
return self._app.display_epg
def update_http_data(self, epg):
for e in (EpgTool.get_event(e, False) for e in epg.get("event_list", []) if e.get("e2eventid", "").isdigit()):
self[e.event_data.get("e2eventservicename", "")] = e
@run_task
def update_xml_data(self):
services = self._app.current_services
names = {services[s].service for s in self._app.current_bouquets.get(self._current_bq, [])}
for name, e in self._reader.get_current_events(names).items():
self[name] = e
def get_current_event(self, service_name):
return self.get(service_name, EpgEvent())
class EpgSettingsPopover(Gtk.Popover):
def __init__(self, app, **kwarg):
super().__init__(**kwarg)
self._app = app
self._app.connect("profile-changed", self.on_profile_changed)
handlers = {"on_apply": self.on_apply,
"on_close": lambda b: self.popdown()}
builder = get_builder(f"{UI_RESOURCES_PATH}epg{SEP}settings.glade", handlers)
self.add(builder.get_object("main_box"))
self._http_src_button = builder.get_object("http_src_button")
self._xml_src_button = builder.get_object("xml_src_button")
self._dat_src_button = builder.get_object("dat_src_button")
self._interval_button = builder.get_object("interval_button")
self._url_entry = builder.get_object("url_entry")
self._dat_path_box = builder.get_object("dat_path_box")
self.init()
def init(self):
settings = self._app.app_settings
src = settings.epg_source
if src is EpgSource.HTTP:
self._http_src_button.set_active(True)
elif src is EpgSource.XML:
self._xml_src_button.set_active(True)
else:
self._dat_src_button.set_active(True)
self._interval_button.set_value(settings.epg_update_interval)
self._url_entry.set_text(settings.epg_xml_source)
self._dat_path_box.set_active_id(settings.epg_dat_path)
def on_apply(self, button):
settings = self._app.app_settings
if self._http_src_button.get_active():
settings.epg_source = EpgSource.HTTP
elif self._xml_src_button.get_active():
settings.epg_source = EpgSource.XML
else:
settings.epg_source = EpgSource.DAT
settings.epg_update_interval = self._interval_button.get_value()
settings.epg_xml_source = self._url_entry.get_text()
settings.epg_dat_path = self._dat_path_box.get_active_id()
self.popdown()
self._app.change_action_state("display_epg", GLib.Variant.new_boolean(True))
def on_profile_changed(self, app, p):
self.init()
class EpgTool(Gtk.Box):
def __init__(self, app, *args, **kwargs):
super().__init__(*args, **kwargs)
self._current_bq = None
self._app = app
self._app.connect("fav-changed", self.on_service_changed)
self._app.connect("profile-changed", self.on_profile_changed)
self._app.connect("bouquet-changed", self.on_bouquet_changed)
self._app.connect("filter-toggled", self.on_filter_toggled)
handlers = {"on_epg_press": self.on_epg_press,
"on_timer_add": self.on_timer_add,
"on_epg_filter_changed": self.on_epg_filter_changed,
"on_epg_filter_toggled": self.on_epg_filter_toggled,
"on_view_query_tooltip": self.on_view_query_tooltip,
"on_multi_epg_toggled": self.on_multi_epg_toggled}
builder = get_builder(f"{UI_RESOURCES_PATH}epg{SEP}tab.glade", handlers)
self._view = builder.get_object("epg_view")
self._model = builder.get_object("epg_model")
self._filter_model = builder.get_object("epg_filter_model")
self._filter_model.set_visible_func(self.epg_filter_function)
self._filter_button = builder.get_object("epg_filter_button")
self._filter_entry = builder.get_object("epg_filter_entry")
self._multi_epg_button = builder.get_object("multi_epg_button")
self._event_count_label = builder.get_object("event_count_label")
self.pack_start(builder.get_object("epg_frame"), True, True, 0)
# Custom data functions.
renderer = builder.get_object("epg_start_renderer")
column = builder.get_object("epg_start_column")
column.set_cell_data_func(renderer, self.start_data_func)
renderer = builder.get_object("epg_end_renderer")
column = builder.get_object("epg_end_column")
column.set_cell_data_func(renderer, self.end_data_func)
renderer = builder.get_object("epg_length_renderer")
column = builder.get_object("epg_length_column")
column.set_cell_data_func(renderer, self.duration_data_func)
# Time formats.
self._time_fmt = "%a %x - %H:%M"
self._duration_fmt = f"%-Hh %Mm"
self.show()
def on_timer_add(self, action=None, value=None):
model, paths = self._view.get_selection().get_selected_rows()
p_count = len(paths)
if p_count == 1:
dialog = TimerTool.TimerDialog(self._app.app_window, TimerTool.TimerAction.EVENT, model[paths][-1])
response = dialog.run()
if response == Gtk.ResponseType.OK:
gen = self.write_timers_list([dialog.get_request()])
GLib.idle_add(lambda: next(gen, False))
dialog.destroy()
elif p_count > 1:
if show_dialog(DialogType.QUESTION, self._app.app_window,
"Add timers for selected events?") != Gtk.ResponseType.OK:
return True
self.add_timers_list((model[p][-1] for p in paths))
else:
self._app.show_error_message("No selected item!")
def add_timers_list(self, paths):
ref_str = "timeraddbyeventid?sRef={}&eventid={}&justplay=0"
refs = [ref_str.format(quote(ev.get("e2eventservicereference", "")), ev.get("e2eventid", "")) for ev in paths]
gen = self.write_timers_list(refs)
GLib.idle_add(lambda: next(gen, False))
def write_timers_list(self, refs):
self._app.wait_dialog.show()
tasks = list(refs)
for ref in refs:
self._app.send_http_request(HttpAPI.Request.TIMER, ref, lambda x: tasks.pop())
yield True
while tasks:
yield True
self._app.emit("change-page", Page.TIMERS.value)
def on_epg_press(self, view, event):
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(view.get_model()) > 0:
self.on_timer_add()
def on_service_changed(self, app, ref):
if app.page is Page.EPG:
if self._multi_epg_button.get_active():
ref += ":"
path = next((r.path for r in self._model if r[-1].get("e2eventservicereference", None) == ref), None)
scroll_to(path, self._view) if path else None
else:
self._app.wait_dialog.show()
self._app.send_http_request(HttpAPI.Request.EPG, quote(ref), self.update_epg_data)
def on_profile_changed(self, app, prf):
self.update_epg_data()
@run_idle
def update_epg_data(self, epg=None):
self._event_count_label.set_text("0")
self._model.clear()
if epg:
list(map(self._model.append, (self.get_event(e) for e in epg.get("event_list", [])
if e.get("e2eventid", "").isdigit())))
self._event_count_label.set_text(str(len(self._model)))
self._app.wait_dialog.hide()
@staticmethod
def get_event(event, show_day=True):
s_name = event.get("e2eventservicename", "")
title = event.get("e2eventtitle", "") or ""
desc = event.get("e2eventdescription", "") or ""
desc = desc.strip()
start, duration = int(event.get("e2eventstart", "0")), int(event.get("e2eventduration", "0"))
return EpgEvent(s_name, title, start, start + duration, duration, desc, event)
def start_data_func(self, column, renderer, model, itr, data):
value = datetime.fromtimestamp(model.get_value(itr, Column.EPG_START))
renderer.set_property("text", value.strftime(self._time_fmt))
def end_data_func(self, column, renderer, model, itr, data):
value = datetime.fromtimestamp(model.get_value(itr, Column.EPG_END))
renderer.set_property("text", value.strftime(self._time_fmt))
def duration_data_func(self, column, renderer, model, itr, data):
value = datetime.utcfromtimestamp(model.get_value(itr, Column.EPG_LENGTH))
renderer.set_property("text", value.strftime(self._duration_fmt))
def on_epg_filter_changed(self, entry):
self._filter_model.refilter()
def on_epg_filter_toggled(self, button):
if not button.get_active():
self._filter_entry.set_text("")
def epg_filter_function(self, model, itr, data):
txt = self._filter_entry.get_text().upper()
return next((s for s in model.get(itr,
Column.EPG_SERVICE,
Column.EPG_TITLE,
Column.EPG_DESC) if txt in s.upper()), False)
def on_filter_toggled(self, app, value):
if self._app.page is Page.EPG:
active = not self._filter_button.get_active()
self._filter_button.set_active(active)
if active:
self._filter_entry.grab_focus()
def on_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
dst = view.get_dest_row_at_pos(x, y)
if not dst:
return False
path, pos = dst
model = view.get_model()
data = model[path][-1]
desc = data.get("e2eventdescription", "") or ""
ext_desc = data.get("e2eventdescriptionextended", "") or ""
tooltip.set_text(ext_desc if ext_desc else desc)
view.set_tooltip_row(tooltip, path)
return True
def on_multi_epg_toggled(self, button):
self._model.clear()
self._event_count_label.set_text("0")
if button.get_active():
self.get_multi_epg()
def on_bouquet_changed(self, app, bq):
self._current_bq = bq
if app.page is Page.EPG and self._multi_epg_button.get_active():
self.get_multi_epg()
def get_multi_epg(self):
if not self._current_bq:
return
self._app.wait_dialog.show()
bq = self._app.current_bouquet_files.get(self._current_bq, None)
api = self._app.http_api
if bq and api:
tm = datetime.now().timestamp()
req = quote(f'FROM BOUQUET "userbouquet.{bq}.{self._current_bq.split(":")[-1]}"&time={tm}')
api.send(HttpAPI.Request.EPG_MULTI, f'1:7:1:0:0:0:0:0:0:0:{req}', self.update_epg_data, timeout=15)
class EpgDialog:
def __init__(self, app, bouquet_name):
handlers = {"on_close_dialog": self.on_close_dialog,
"on_apply": self.on_apply,
"on_update": self.on_update,
"on_save_to_xml": self.on_save_to_xml,
"on_auto_configuration": self.on_auto_configuration,
"on_filter_toggled": self.on_filter_toggled,
"on_filter_satellite_toggled": self.on_filter_satellite_toggled,
"on_filter_changed": self.on_filter_changed,
"on_info_bar_close": self.on_info_bar_close,
"on_popup_menu": on_popup_menu,
"on_bouquet_popup_menu": self.on_bouquet_popup_menu,
"on_copy_ref": self.on_copy_ref,
"on_assign_ref": self.on_assign_ref,
"on_reset": self.on_reset,
"on_list_reset": self.on_list_reset,
"on_drag_begin": self.on_drag_begin,
"on_drag_data_get": self.on_drag_data_get,
"on_drag_data_received": self.on_drag_data_received,
"on_resize": self.on_resize,
"on_names_source_changed": self.on_names_source_changed,
"on_options_save": self.on_options_save,
"on_use_web_source_switch": self.on_use_web_source_switch,
"on_enable_filtering_switch": self.on_enable_filtering_switch,
"on_update_on_start_switch": self.on_update_on_start_switch,
"on_field_icon_press": self.on_field_icon_press,
"on_key_press": self.on_key_press,
"on_bq_cursor_changed": self.on_bq_cursor_changed}
self._app = app
self._ex_services = self._app.current_services
self._ex_fav_model = self._app.fav_view.get_model()
self._settings = self._app.app_settings
self._bouquet_name = bouquet_name
self._current_ref = []
self._enable_dat_filter = False
self._use_web_source = False
self._update_epg_data_on_start = False
self._refs_source = RefsSource.SERVICES
self._download_xml_is_active = False
self._sat_positions = None
builder = get_builder(f"{UI_RESOURCES_PATH}epg{SEP}dialog.glade", handlers)
self._dialog = builder.get_object("epg_dialog_window")
self._dialog.set_transient_for(self._app.app_window)
self._source_view = builder.get_object("source_view")
self._bouquet_view = builder.get_object("bouquet_view")
self._bouquet_model = builder.get_object("bouquet_list_store")
self._services_model = builder.get_object("services_list_store")
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label")
self._assign_ref_popup_item = builder.get_object("bouquet_assign_ref_popup_item")
self._left_action_box = builder.get_object("left_action_box")
self._xml_download_progress_bar = builder.get_object("xml_download_progress_bar")
self._src_load_spinner = builder.get_object("src_load_spinner")
# Filter
self._filter_bar = builder.get_object("filter_bar")
self._filter_entry = builder.get_object("filter_entry")
self._filter_auto_switch = builder.get_object("filter_auto_switch")
self._services_filter_model = builder.get_object("services_filter_model")
self._services_filter_model.set_visible_func(self.services_filter_function)
self._sat_pos_filter_model = builder.get_object("sat_pos_filter_model")
# Info
self._source_count_label = builder.get_object("source_count_label")
self._source_info_label = builder.get_object("source_info_label")
self._bouquet_count_label = builder.get_object("bouquet_count_label")
self._bouquet_epg_count_label = builder.get_object("bouquet_epg_count_label")
# Options
self._xml_radiobutton = builder.get_object("xml_radiobutton")
self._xml_chooser_button = builder.get_object("xml_chooser_button")
self._names_source_box = builder.get_object("names_source_box")
self._web_source_box = builder.get_object("web_source_box")
self._use_web_source_switch = builder.get_object("use_web_source_switch")
self._url_to_xml_entry = builder.get_object("url_to_xml_entry")
self._enable_filtering_switch = builder.get_object("enable_filtering_switch")
self._epg_dat_path_entry = builder.get_object("epg_dat_path_entry")
self._epg_dat_stb_path_entry = builder.get_object("epg_dat_stb_path_entry")
self._update_on_start_switch = builder.get_object("update_on_start_switch")
self._epg_dat_source_box = builder.get_object("epg_dat_source_box")
if IS_GNOME_SESSION or IS_DARWIN:
header_bar = HeaderBar(title="EPG", subtitle=get_message("List configuration"))
self._dialog.set_titlebar(header_bar)
builder.get_object("left_action_box").reparent(header_bar)
right_box = builder.get_object("right_action_box")
builder.get_object("main_actions_box").remove(right_box)
header_bar.pack_end(right_box)
builder.get_object("toolbar_box").set_visible(False)
self._app.connect("epg-dat-downloaded", self.on_epg_dat_downloaded)
# Setting the last size of the dialog window
window_size = self._settings.get("epg_tool_window_size")
if window_size:
self._dialog.resize(*window_size)
self.init_drag_and_drop()
self.on_update()
def show(self):
self._dialog.show()
def on_close_dialog(self, window, event):
self._download_xml_is_active = False
@run_idle
def on_apply(self, item):
if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.OK:
return
p = re.compile(r"\d+")
updated = {}
for i, row in enumerate(self._bouquet_model):
if row[Column.FAV_LOCKED]:
fav_id = self._ex_fav_model[row.path][Column.FAV_ID]
srv = self._ex_services.pop(fav_id, None)
if srv:
new_fav_id, picon_id = row[Column.FAV_ID], row[Column.FAV_POS]
if picon_id:
picon_id = re.sub(p, re.search(p, srv.picon_id).group(), picon_id, count=1)
else:
picon_id = srv.picon_id
new = srv._replace(fav_id=new_fav_id, data_id=new_fav_id.strip(), picon_id=picon_id)
self._ex_services[new_fav_id] = new
updated[fav_id] = (srv, new)
if updated:
self._app.emit("iptv-service-edited", updated)
self._dialog.destroy()
@run_idle
def on_update(self, item=None):
self.clear_data()
self.init_options()
if self._update_epg_data_on_start:
self.download_epg_from_stb()
else:
gen = self.init_data()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def clear_data(self):
self._services_model.clear()
self._bouquet_model.clear()
self._source_info_label.set_text("")
self._bouquet_epg_count_label.set_text("")
self.on_info_bar_close()
def init_data(self):
gen = self.init_bouquet_data()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
refs = None
if self._enable_dat_filter:
try:
epg_reader = EPG.DatReader(f"{self._epg_dat_path_entry.get_text()}epg.dat")
epg_reader.read()
refs = epg_reader.get_refs()
except (OSError, ValueError) as e:
self.show_info_message(f"Read data error: {e}", Gtk.MessageType.ERROR)
return
yield True
self._src_load_spinner.start()
if self._refs_source is RefsSource.SERVICES:
yield from self.init_lamedb_source(refs)
elif self._refs_source is RefsSource.XML:
xml_gen = self.init_xml_source(refs)
try:
yield from xml_gen
except ValueError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
self.show_info_message("Unknown names source!", Gtk.MessageType.ERROR)
self._src_load_spinner.stop()
yield True
def init_bouquet_data(self):
for r in self._ex_fav_model:
row = [*r[:]]
yield self._bouquet_model.append(row)
self._bouquet_count_label.set_text(str(len(self._bouquet_model)))
yield True
def init_lamedb_source(self, refs):
srvs = {k[:k.rfind(":")]: v for k, v in self._ex_services.items()}
s_types = (BqServiceType.MARKER.value, BqServiceType.IPTV.value)
filtered = filter(None, [srvs.get(ref) for ref in refs]) if refs else filter(
lambda s: s.service_type not in s_types, self._ex_services.values())
factor = self._app.DEL_FACTOR / 4
for index, srv in enumerate(filtered):
self._services_model.append((srv.service, srv.pos, srv.fav_id, srv.picon_id))
if index % factor == 0:
yield True
self.update_source_count_info()
yield True
def init_xml_source(self, refs):
path = self._epg_dat_path_entry.get_text() if self._use_web_source else self._xml_chooser_button.get_filename()
if not path:
self.show_info_message("The path to the xml file is not set!", Gtk.MessageType.ERROR)
return
if self._use_web_source:
# Downloading gzipped xml file that contains services names with references from the web.
self._download_xml_is_active = True
self.update_active_header_elements(False)
url = self._url_to_xml_entry.get_text()
try:
with urllib.request.urlopen(url, timeout=2) as fp:
headers = fp.info()
content_type = headers.get("Content-Type", "")
if content_type != "application/gzip":
self._download_xml_is_active = False
raise ValueError("{} {} {}".format(get_message("Download XML file error."),
get_message("Unsupported file type:"),
content_type))
file_name = os.path.basename(url)
data_path = self._epg_dat_path_entry.get_text()
with open(data_path + file_name, "wb") as tfp:
bs = 1024 * 8
size = -1
read = 0
b_num = 0
if "content-length" in headers:
size = int(headers["Content-Length"])
while self._download_xml_is_active:
block = fp.read(bs)
if not block:
break
read += len(block)
tfp.write(block)
b_num += 1
self.update_download_progress(b_num * bs / size)
yield True
path = tfp.name.rstrip(".gz")
except (HTTPError, URLError) as e:
raise ValueError(f"{get_message('Download XML file error.')} {e}")
else:
try:
with open(path, "wb") as f_out:
with gzip.open(tfp.name, "rb") as f:
shutil.copyfileobj(f, f_out)
os.remove(tfp.name)
except Exception as e:
raise ValueError(f"{get_message('Unpacking data error.')} {e}")
finally:
self._download_xml_is_active = False
self.update_active_header_elements(True)
try:
s_refs, info = ChannelsParser.get_refs_from_xml(path)
yield True
except Exception as e:
raise ValueError(f"{get_message('XML parsing error:')} {e}")
else:
if refs:
s_refs = filter(lambda x: x.num in refs, s_refs)
factor = self._app.DEL_FACTOR / 4
for index, srv in enumerate(s_refs):
self._services_model.append((srv.name, " ", srv.data, ""))
if index % factor == 0:
yield True
self.update_source_info(info)
self.update_source_count_info()
yield True
def on_key_press(self, view, event):
""" Handling keystrokes """
key_code = event.hardware_keycode
if not KeyboardKey.value_exist(key_code):
return
key = KeyboardKey(key_code)
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
if ctrl and key is KeyboardKey.C:
self.on_copy_ref()
elif ctrl and key is KeyboardKey.V:
self.on_assign_ref()
def on_bq_cursor_changed(self, view):
if self._filter_bar.get_visible() and self._filter_auto_switch.get_active():
path, column = view.get_cursor()
model = view.get_model()
if path:
self._filter_entry.set_text(model[path][Column.FAV_SERVICE] or "")
@run_idle
def on_save_to_xml(self, item):
response = show_dialog(DialogType.CHOOSER, self._dialog, settings=self._settings)
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
services = []
iptv_types = (BqServiceType.IPTV.value, BqServiceType.MARKER.value)
for r in self._bouquet_model:
srv_type = r[Column.FAV_TYPE]
if srv_type in iptv_types:
srv = BouquetService(name=r[Column.FAV_SERVICE],
type=BqServiceType(srv_type),
data=r[Column.FAV_ID],
num=r[Column.FAV_NUM])
services.append(srv)
ChannelsParser.write_refs_to_xml("{}{}.xml".format(response, self._bouquet_name), services)
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
@run_idle
def on_auto_configuration(self, item):
""" Simple mapping of services by name. """
use_cyrillic = locale.getdefaultlocale()[0] in ("ru_RU", "be_BY", "uk_UA", "sr_RS")
tr = None
if use_cyrillic:
# may be not entirely correct
symbols = (u"АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯІÏҐЎЈЂЉЊЋЏTB",
u"ABVGDEEJZIJKLMNOPRSTUFHZCSS_Y_EUAIEGUEDLNCJTV")
tr = {ord(k): ord(v) for k, v in zip(*symbols)}
source = {}
for row in self._source_view.get_model():
name = re.sub("\\W+", "", str(row[0])).upper()
name = name.translate(tr) if use_cyrillic else name
source[name] = row
success_count = 0
not_founded = {}
for r in self._bouquet_model:
if r[Column.FAV_TYPE] != BqServiceType.IPTV.value:
continue
name = re.sub("\\W+", "", str(r[Column.FAV_SERVICE])).upper()
if use_cyrillic:
name = name.translate(tr)
ref = source.get(name, None) # Not [pop], because the list may contain duplicates or similar names!
if ref:
self.assign_data(r, ref, True)
success_count += 1
else:
not_founded[name] = r
# Additional attempt to search in the remaining elements
for n in not_founded:
for k in source:
if k.startswith(n):
self.assign_data(not_founded[n], source[k], True)
success_count += 1
break
self.update_epg_count()
self.show_info_message("{} {} {}".format(get_message("Done!"),
get_message("Count of successfully configured services:"),
success_count), Gtk.MessageType.INFO)
def assign_refs(self, model, paths, data):
[self.assign_data(model[p], data) for p in paths]
self.update_epg_count()
def assign_data(self, row, data, show_error=False):
if row[Column.FAV_TYPE] != BqServiceType.IPTV.value:
if not show_error:
self.show_info_message(get_message("Not allowed in this context!"), Gtk.MessageType.ERROR)
return
fav_id = row[Column.FAV_ID]
fav_id_data = fav_id.split(":")
fav_id_data[3:7] = data[-2].split(":")
if data[-1]:
row[Column.FAV_POS] = data[-1]
p_data = data[-1].split("_")
if p_data:
fav_id_data[2] = p_data[2]
new_fav_id = ":".join(fav_id_data)
row[Column.FAV_ID] = new_fav_id
row[Column.FAV_LOCKED] = EPG_ICON
pos = f"({data[1] if self._refs_source is RefsSource.SERVICES else 'XML'})"
src = f"{get_message('EPG source')}: {(GLib.markup_escape_text(data[0] or ''))} {pos}"
row[Column.FAV_TOOLTIP] = f"{get_message('Service reference')}: {':'.join(fav_id_data[:10])}\n{src}"
def on_filter_toggled(self, button):
self._filter_bar.set_visible(button.get_active())
if button.get_active():
self._sat_positions = {r[1] for r in self._services_model}
update_filter_sat_positions(self._sat_pos_filter_model, self._sat_positions)
else:
self._sat_positions = None
self._filter_entry.set_text("") if self._filter_entry.get_text() else self.on_filter_changed()
def on_filter_satellite_toggled(self, toggle, path):
update_toggle_model(self._sat_pos_filter_model, path, toggle)
self._sat_positions.clear()
self._sat_positions.update({r[0] for r in self._sat_pos_filter_model if r[1]})
self.on_filter_changed()
@run_with_delay(2)
def on_filter_changed(self, entry=None):
self._services_filter_model.refilter()
def services_filter_function(self, model, itr, data):
txt = self._filter_entry.get_text().upper()
pos = model.get_value(itr, 1)
pos = self._sat_positions is None or self._xml_radiobutton.get_active() or pos in self._sat_positions
return model is None or model == "None" or (txt in model.get_value(itr, 0).upper() and pos)
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
def on_copy_ref(self, item=None):
model, paths = self._source_view.get_selection().get_selected_rows()
self._current_ref.clear()
if paths:
self._current_ref.append(model[paths][:])
def on_assign_ref(self, item=None):
if self._current_ref:
model, paths = self._bouquet_view.get_selection().get_selected_rows()
self.assign_refs(model, paths, self._current_ref.pop())
@run_idle
def on_reset(self, item):
model, paths = self._bouquet_view.get_selection().get_selected_rows()
if paths:
row = self._bouquet_model[paths]
self.reset_row_data(row)
self.update_epg_count()
@run_idle
def on_list_reset(self, item):
list(map(self.reset_row_data, self._bouquet_model))
self.update_epg_count()
def reset_row_data(self, row):
row[Column.FAV_LOCKED], row[Column.FAV_TOOLTIP], row[Column.FAV_POS] = None, None, None
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(text)
@run_idle
def update_source_info(self, info):
lines = info.split("\n")
self._source_info_label.set_text(lines[0] if lines else "")
self._source_view.set_tooltip_text(info)
@run_idle
def update_source_count_info(self):
source_count = len(self._services_model)
self._source_count_label.set_text(str(source_count))
if self._enable_dat_filter and source_count == 0:
msg = get_message("Current epg.dat file does not contains references for the services of this bouquet!")
self.show_info_message(msg, Gtk.MessageType.WARNING)
@run_idle
def update_epg_count(self):
count = len(list((filter(None, [r[Column.FAV_LOCKED] for r in self._bouquet_model]))))
self._bouquet_epg_count_label.set_text(str(count))
@run_idle
def update_active_header_elements(self, state):
self._left_action_box.set_sensitive(state)
self._xml_download_progress_bar.set_visible(not state)
self._source_info_label.set_text("" if state else "Downloading XML:")
@run_idle
def update_download_progress(self, value):
self._xml_download_progress_bar.set_fraction(value)
def on_bouquet_popup_menu(self, menu, event):
self._assign_ref_popup_item.set_sensitive(self._current_ref)
on_popup_menu(menu, event)
# ***************** Drag-and-drop *********************#
def init_drag_and_drop(self):
""" Enable drag-and-drop. """
target = []
self._source_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, target, Gdk.DragAction.COPY)
self._source_view.drag_source_add_text_targets()
self._bouquet_view.enable_model_drag_dest(target, Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE)
self._bouquet_view.drag_dest_add_text_targets()
def on_drag_begin(self, view, context):
""" Selects a row under the cursor in the view at the dragging beginning. """
selection = view.get_selection()
if selection.count_selected_rows() > 1:
view.do_toggle_cursor_row(view)
def on_drag_data_get(self, view, drag_context, data, info, time):
model, paths = view.get_selection().get_selected_rows()
if paths:
s_data = model[paths][:]
if all(s_data[:-1]):
data.set_text("::::".join(s_data), -1)
else:
self.show_info_message(get_message("Source error!"), Gtk.MessageType.ERROR)
def on_drag_data_received(self, view, drag_context, x, y, data, info, time):
path, pos = view.get_dest_row_at_pos(x, y)
model = view.get_model()
data = data.get_text()
if data:
data = data.split("::::")
self.assign_refs(model, path, data)
return False
# ***************** Options *********************#
def init_options(self):
epg_dat_path = "{}epg{}".format(self._settings.profile_data_path, SEP)
self._epg_dat_path_entry.set_text(epg_dat_path)
default_epg_data_stb_path = "/etc/enigma2"
epg_options = self._settings.epg_options
if epg_options:
self._refs_source = RefsSource.XML if epg_options.get("xml_source", False) else RefsSource.SERVICES
self._xml_radiobutton.set_active(self._refs_source is RefsSource.XML)
self._use_web_source = epg_options.get("use_web_source", False)
self._use_web_source_switch.set_active(self._use_web_source)
self._url_to_xml_entry.set_text(epg_options.get("url_to_xml", ""))
self._enable_dat_filter = epg_options.get("enable_filtering", False)
self._enable_filtering_switch.set_active(self._enable_dat_filter)
epg_dat_path = epg_options.get("epg_dat_path", epg_dat_path)
self._epg_dat_path_entry.set_text(epg_dat_path)
self._epg_dat_stb_path_entry.set_text(epg_options.get("epg_dat_stb_path", default_epg_data_stb_path))
self._update_epg_data_on_start = epg_options.get("epg_data_update_on_start", False)
self._update_on_start_switch.set_active(self._update_epg_data_on_start)
local_xml_path = epg_options.get("local_path_to_xml", None)
if local_xml_path:
self._xml_chooser_button.set_filename(local_xml_path)
os.makedirs(os.path.dirname(self._epg_dat_path_entry.get_text()), exist_ok=True)
def on_options_save(self, item=None):
self._settings.epg_options = {"xml_source": self._xml_radiobutton.get_active(),
"use_web_source": self._use_web_source_switch.get_active(),
"local_path_to_xml": self._xml_chooser_button.get_filename(),
"url_to_xml": self._url_to_xml_entry.get_text(),
"enable_filtering": self._enable_filtering_switch.get_active(),
"epg_dat_path": self._epg_dat_path_entry.get_text(),
"epg_dat_stb_path": self._epg_dat_stb_path_entry.get_text(),
"epg_data_update_on_start": self._update_on_start_switch.get_active()}
def on_resize(self, window):
if self._settings:
self._settings.add("epg_tool_window_size", window.get_size())
def on_names_source_changed(self, button):
self._refs_source = RefsSource.XML if button.get_active() else RefsSource.SERVICES
self._names_source_box.set_sensitive(button.get_active())
def on_enable_filtering_switch(self, switch, state):
self._epg_dat_source_box.set_sensitive(state)
self._update_on_start_switch.set_active(False if not state else self._update_epg_data_on_start)
def on_update_on_start_switch(self, switch, state):
pass
def on_use_web_source_switch(self, switch, state):
self._web_source_box.set_sensitive(state)
self._xml_chooser_button.set_sensitive(not state)
def on_field_icon_press(self, entry, icon, event_button):
update_entry_data(entry, self._dialog, self._settings)
# ***************** Downloads *********************#
def on_epg_dat_downloaded(self, app, value):
gen = self.init_data()
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
@run_task
def download_epg_from_stb(self):
""" Download the epg.dat file via ftp from the receiver. """
try:
download_data(settings=self._settings, download_type=DownloadType.EPG, callback=print)
except Exception as e:
GLib.idle_add(self.show_info_message, f"Download epg.dat file error: {e}", Gtk.MessageType.ERROR)
else:
GLib.idle_add(self._app.emit, "epg-dat-downloaded", None)
if __name__ == "__main__":
pass

View File

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

View File

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

1344
app/ui/epg_dialog.glade Normal file

File diff suppressed because it is too large Load Diff

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

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

File diff suppressed because it is too large Load Diff

View File

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

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

3548
app/ui/main_window.glade Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

955
app/ui/satellites_dialog.py Normal file
View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

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

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

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

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

141
app/ui/timer_row.glade Normal file
View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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