Compare commits

..

254 Commits

Author SHA1 Message Date
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
78 changed files with 13983 additions and 7397 deletions

View File

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

66
DemonEditor.spec Normal file
View File

@@ -0,0 +1,66 @@
import os
import datetime
import distutils.util
EXE_NAME = 'start.py'
DIR_PATH = os.getcwd()
COMPILING_PLATFORM = distutils.util.get_platform()
PATH_EXE = [os.path.join(DIR_PATH, EXE_NAME)]
STRIP = True
BUILD_DATE = datetime.datetime.now().strftime("%Y%m%d")
block_cipher = None
ui_files = [('app/ui/*.glade', 'ui'),
('app/ui/*.css', 'ui'),
('app/ui/*.ui', 'ui'),
('app/ui/lang*', 'share/locale'),
('app/ui/icons*', 'share/icons')
]
a = Analysis([EXE_NAME],
pathex=PATH_EXE,
binaries=None,
datas=ui_files,
hiddenimports=['fileinput', 'uuid'],
hookspath=[],
runtime_hooks=[],
excludes=['youtube_dl'],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher)
pyz = PYZ(a.pure,
a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
exclude_binaries=True,
name='DemonEditor',
debug=False,
strip=STRIP,
upx=True,
console=False)
coll = COLLECT(exe,
a.binaries,
a.zipfiles,
a.datas,
strip=STRIP,
upx=True,
name='DemonEditor')
app = BUNDLE(coll,
name='DemonEditor.app',
icon='icon.icns',
bundle_identifier=None,
info_plist={
'NSPrincipalClass': 'NSApplication',
'CFBundleName': 'DemonEditor',
'CFBundleDisplayName': 'DemonEditor',
'CFBundleGetInfoString': "Enigma2 channel and satellites editor",
'CFBundleShortVersionString': "1.0.0 Alpha-2 (Build: {})".format(BUILD_DATE),
'NSHumanReadableCopyright': u"Copyright © 2020, Dmitriy Yefremov",
'NSRequiresAquaSystemAppearance': 'false'
})

View File

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

101
README.md
View File

@@ -1,8 +1,7 @@
# <img src="app/ui/icons/hicolor/96x96/apps/demon-editor.png" width="32" /> DemonEditor # <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)
## Enigma2 channel and satellites list editor for GNU/Linux. ## Enigma2 channel and satellites list editor for macOS (experimental).
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc). ### The functionality and performance of this version may be different from the Linux version!
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: ### Main features of the program:
* Editing bouquets, channels, satellites. * Editing bouquets, channels, satellites.
@@ -15,52 +14,60 @@ Focused on the convenience of working in lists from the keyboard. The mouse is a
* Export of bouquets with IPTV services in m3u. * Export of bouquets with IPTV services in m3u.
* Assignment of EPGs from DVB or XML for IPTV services (only Enigma2, experimental). * 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 (should be installed [VLC](https://www.videolan.org/vlc/)). * Preview (playback) of IPTV or other streams directly from the bouquet list (should be installed [VLC](https://www.videolan.org/vlc/)).
#### Keyboard shortcuts:
### Keyboard shortcuts: * **&#8984; + X** - only in bouquet list.
* **Ctrl + X** - only in bouquet list. * **&#8984; + C** - only in services list.
* **Ctrl + C** - only in services list. Clipboard is **"rubber"**. There is an accumulation before the insertion!
Clipboard is **"rubber"**. There is an accumulation before the insertion! * **&#8984; + E** - edit.
* **Ctrl + Insert** - copies the selected channels from the main list to the the bouquet beginning * **&#8984; + R, F2** - rename.
or inserts (creates) a new bouquet. * **&#8984; + S, T** in Satellites edit tool for create satellite or transponder.
* **Ctrl + BackSpace** - copies the selected channels from the main list to the bouquet end. * **&#8984; + L** - parental lock.
* **Ctrl + E** - edit. * **&#8984; + H** - hide/skip.
* **Ctrl + R, F2** - rename. * **&#8984; + P** - start play IPTV or other stream in the bouquet list.
* **Ctrl + S, T** in Satellites edit tool for create satellite or transponder. * **&#8984; + Z** - switch(**zap**) the channel(works when the HTTP API is enabled, Enigma2 only).
* **Ctrl + L** - parental lock. * **&#8984; + W** - switch to the channel and watch in the program.
* **Ctrl + H** - hide/skip. * **&#8984; + Up/Down** - move selected items in the list.
* **Ctrl + P** - start play IPTV or other stream in the bouquet list. * **&#8984; + O** - (re)load user data from current dir.
* **Ctrl + Z** - switch(**zap**) the channel(works when the HTTP API is enabled, Enigma2 only). * **&#8984; + D** - load data from receiver.
* **Ctrl + W** - switch to the channel and watch in the program. * **&#8984; + U/B** - upload data/bouquets to receiver.
* **Space** - select/deselect. * **&#8984; + F** - show/hide search bar.
* **Left/Right** - remove selection. * **&#8679; + &#8984; + F** - show/hide filter bar.
* **Ctrl + Up, Down, PageUp, PageDown, Home, End** - move selected items in the list. * **Left/Right** - remove selection.
* **Ctrl + O** - (re)load user data from current dir.
* **Ctrl + D** - load data from receiver. For multiple mouse selection (including Drag and Drop), press and hold the **&#8984;** key!
* **Ctrl + U/B** upload data/bouquets to receiver.
* **Ctrl + F** - show/hide search bar.
* **Ctrl + Shift + F** - show/hide filter bar.
For multiple mouse selection (including Drag and Drop), press and hold the **Ctrl** key!
### Minimum requirements: ### Minimum requirements:
Python >= 3.5.2 and GTK+ >= 3.16 with PyGObject bindings, python3-requests. Python >= **3.5**, GTK+ >= **3.16**, pygobject3, adwaita-icon-theme, python3-requests.
### Installation:
### Launching: ```brew install python3 gtk+3 pygobject3 adwaita-icon-theme```
To start the program, in most cases it is enough to download the archive, unpack and run it by ```pip3 install requests```
double clicking on DemonEditor.desktop in the root directory, or launching from the console ### Optional:
with the command: ```./start.py``` ```brew install wget```
Extra folders can be deleted, excluding the *app* folder and root files like *DemonEditor.desktop* and *start.py*! ```pip3 install pillow, pyobjc```
### Launching:
To start the program, just download the archive, unpack and run it from the terminal with the command: ```./start.py```
### Building standalone application:
Install [PyInstaller](https://www.pyinstaller.org/) with the command from the terminal:
```pip3 install pyinstaller```
and in th root dir run command:
```pyinstaller DemonEditor.spec```
### Standalone package:
You can 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!
**The package may not contain all the latest changes. Not all features can be supported and tested!**
### Note: ### Note:
To create a simple **debian package**, you can use the *build-deb.sh.* THIS SOFTWARE COMES WITH ABSOLUTELY NO WARRANTY.
Users of **LTS** versions of [Ubuntu](https://ubuntu.com/) or those based on them can use [PPA](https://launchpad.net/~dmitriy-yefremov/+archive/ubuntu/demon-editor) repository. AUTHOR IS NOT LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY CONNECTION WITH THIS SOFTWARE.
The package may contain components distributed under the GPL [v3](http://www.gnu.org/licenses/gpl-3.0.html) or lower license.
The program is tested only with [openATV](https://www.opena.tv/) image and **Formuler F1** receiver in my favourite Linux distributions By downloading and using this package you agree to the terms of this [license](http://www.gnu.org/licenses/gpl-3.0.html) and the possible inconvenience associated with this!
(the latest versions of [Linux Mint](https://linuxmint.com/) 18.* and 19* MATE 64-bit)!
### Important: ### Important:
Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2! 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! 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! 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.

View File

@@ -6,28 +6,28 @@ from gi.repository import GLib
_LOG_FILE = "demon-editor.log" _LOG_FILE = "demon-editor.log"
_DATE_FORMAT = "%d-%m-%y %H:%M:%S" _DATE_FORMAT = "%d-%m-%y %H:%M:%S"
_LOGGER_NAME = "main_logger" _LOGGER_NAME = None
_USE_LOG = False
def init_logger(): def init_logger():
global _USE_LOG global _LOGGER_NAME
_USE_LOG = True _LOGGER_NAME = "main_logger"
logging.Logger(_LOGGER_NAME) logging.Logger(_LOGGER_NAME)
logging.basicConfig(level=logging.INFO, logging.basicConfig(level=logging.INFO,
format="%(asctime)s %(message)s", format="%(asctime)s %(message)s",
datefmt=_DATE_FORMAT, datefmt=_DATE_FORMAT,
handlers=[logging.FileHandler(_LOG_FILE), handlers=[logging.FileHandler(_LOG_FILE), logging.StreamHandler()])
logging.StreamHandler()])
log("Logging is enabled.", level=logging.INFO) log("Logging is enabled.", level=logging.INFO)
def get_logger(): def log(message, level=logging.ERROR, debug=False, fmt_message="{}"):
return logging.getLogger(_LOGGER_NAME) """ The main logging function. """
logger = logging.getLogger(_LOGGER_NAME)
if debug:
def log(message, level=logging.ERROR): from traceback import format_exc
get_logger().log(level, message) if _USE_LOG else print(message) logger.log(level, fmt_message.format(format_exc()))
else:
logger.log(level, message)
def run_idle(func): def run_idle(func):

View File

@@ -10,20 +10,20 @@ from http.client import RemoteDisconnected
from telnetlib import Telnet from telnetlib import Telnet
from urllib.error import HTTPError, URLError from urllib.error import HTTPError, URLError
from urllib.parse import urlencode from urllib.parse import urlencode
from urllib.request import urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, \ from urllib.request import (urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener,
build_opener, install_opener, Request install_opener, Request)
from app.commons import log, run_task from app.commons import log, run_task
from app.settings import SettingsType from app.settings import SettingsType
_BQ_FILES_LIST = ("tv", "radio", # enigma 2 BQ_FILES_LIST = ("tv", "radio", # enigma 2
"myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino "myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino
_DATA_FILES_LIST = ("lamedb", "lamedb5", "services.xml", "blacklist", "whitelist",) DATA_FILES_LIST = ("lamedb", "lamedb5", "blacklist", "whitelist",)
_SAT_XML_FILE = "satellites.xml" STC_XML_FILE = ("satellites.xml", "terrestrial.xml", "cables.xml")
_WEBTV_XML_FILE = "webtv.xml" WEB_TV_XML_FILE = ("webtv.xml",)
_PICONS_SUF = (".jpg", ".png") PICONS_SUF = (".jpg", ".png")
class DownloadType(Enum): class DownloadType(Enum):
@@ -51,6 +51,7 @@ class HttpRequestType(Enum):
PLAYER_PREV = "mediaplayercmd?command=previous" PLAYER_PREV = "mediaplayercmd?command=previous"
PLAYER_STOP = "mediaplayercmd?command=stop" PLAYER_STOP = "mediaplayercmd?command=stop"
PLAYER_REMOVE = "mediaplayerremove?file=" PLAYER_REMOVE = "mediaplayerremove?file="
GRUB = "grab?format=jpg&"
class TestException(Exception): class TestException(Exception):
@@ -61,60 +62,45 @@ class HttpApiException(Exception):
pass pass
def download_data(*, settings, download_type=DownloadType.ALL, callback=print): def download_data(*, settings, download_type=DownloadType.ALL, callback=print, files_filter=None):
with FTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp: with FTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
ftp.encoding = "utf-8" ftp.encoding = "utf-8"
callback("FTP OK.\n") callback("FTP OK.\n")
save_path = settings.data_local_path save_path = settings.data_local_path
os.makedirs(os.path.dirname(save_path), exist_ok=True) os.makedirs(os.path.dirname(save_path), exist_ok=True)
files = []
# bouquets # bouquets
if download_type is DownloadType.ALL or download_type is DownloadType.BOUQUETS: if download_type is DownloadType.ALL or download_type is DownloadType.BOUQUETS:
ftp.cwd(settings.services_path) ftp.cwd(settings.services_path)
ftp.dir(files.append) file_list = BQ_FILES_LIST + DATA_FILES_LIST if download_type is DownloadType.ALL else BQ_FILES_LIST
file_list = _BQ_FILES_LIST + _DATA_FILES_LIST if download_type is DownloadType.ALL else _BQ_FILES_LIST for file in filter(lambda f: f.endswith(file_list), ftp.nlst()):
for file in files: download_file(ftp, file, save_path, callback)
name = str(file).strip() # *.xml and webtv
if name.endswith(file_list): if download_type in (DownloadType.ALL, DownloadType.SATELLITES):
name = name.split()[-1] download_xml(ftp, save_path, settings.satellites_xml_path, STC_XML_FILE, callback)
download_file(ftp, name, save_path, callback) if download_type in (DownloadType.ALL, DownloadType.WEBTV):
# satellites.xml and webtv download_xml(ftp, save_path, settings.satellites_xml_path, WEB_TV_XML_FILE, callback)
if download_type in (DownloadType.ALL, DownloadType.SATELLITES, DownloadType.WEBTV):
ftp.cwd(settings.satellites_xml_path)
files.clear()
ftp.dir(files.append)
for file in files:
name = str(file).strip()
if download_type in (DownloadType.ALL, DownloadType.SATELLITES) and name.endswith(_SAT_XML_FILE):
download_file(ftp, _SAT_XML_FILE, save_path, callback)
if download_type in (DownloadType.ALL, DownloadType.WEBTV) and name.endswith(_WEBTV_XML_FILE):
download_file(ftp, _WEBTV_XML_FILE, save_path, callback)
if download_type is DownloadType.PICONS: if download_type is DownloadType.PICONS:
picons_path = settings.picons_local_path picons_path = settings.picons_local_path
os.makedirs(os.path.dirname(picons_path), exist_ok=True) os.makedirs(os.path.dirname(picons_path), exist_ok=True)
download_picons(ftp, settings.picons_path, picons_path, callback) download_picons(ftp, settings.picons_path, picons_path, callback, files_filter)
# epg.dat # epg.dat
if download_type is DownloadType.EPG: if download_type is DownloadType.EPG:
stb_path = settings.services_path stb_path = settings.services_path
epg_options = settings.epg_options epg_options = settings.epg_options
if epg_options: if epg_options:
stb_path = epg_options.epg_dat_stb_path or stb_path stb_path = epg_options.get("epg_dat_stb_path", stb_path)
save_path = epg_options.epg_dat_path or save_path save_path = epg_options.get("epg_dat_path", save_path)
ftp.cwd(stb_path) ftp.cwd(stb_path)
ftp.dir(files.append) for file in filter(lambda f: f.endswith("epg.dat"), ftp.nlst()):
for file in files: download_file(ftp, file, save_path, callback)
name = str(file).strip()
if name.endswith("epg.dat"):
name = name.split()[-1]
download_file(ftp, name, save_path, callback)
callback("\nDone.\n") callback("\nDone.\n")
def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False, def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False,
callback=print, done_callback=None, use_http=False): callback=print, done_callback=None, use_http=False, files_filter=None):
s_type = settings.setting_type s_type = settings.setting_type
data_path = settings.data_local_path data_path = settings.data_local_path
host = settings.host host = settings.host
@@ -133,6 +119,8 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
message = "All user data will be reloaded!" message = "All user data will be reloaded!"
elif download_type is DownloadType.SATELLITES: elif download_type is DownloadType.SATELLITES:
message = "Satellites.xml file will be updated!" message = "Satellites.xml file will be updated!"
elif download_type is DownloadType.PICONS:
message = "Picons will be updated!"
params = urlencode({"text": message, "type": 2, "timeout": 5}) params = urlencode({"text": message, "type": 2, "timeout": 5})
ht.send((url + "message?{}".format(params), "Sending info message... ")) ht.send((url + "message?{}".format(params), "Sending info message... "))
@@ -142,14 +130,15 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
ht.send((url + "powerstate?newstate=0", "Toggle Standby ")) ht.send((url + "powerstate?newstate=0", "Toggle Standby "))
time.sleep(2) time.sleep(2)
else: else:
# telnet if download_type is not DownloadType.PICONS:
tn = telnet(host=host, # telnet
user=settings.telnet_user, tn = telnet(host=host,
password=settings.telnet_password, user=settings.telnet_user,
timeout=settings.telnet_timeout) password=settings.telnet_password,
next(tn) timeout=settings.telnet_timeout)
# terminate enigma or neutrino next(tn)
tn.send("init 4") # terminate enigma or neutrino
tn.send("init 4")
with FTP(host=host, user=settings.user, passwd=settings.password) as ftp: with FTP(host=host, user=settings.user, passwd=settings.password) as ftp:
ftp.encoding = "utf-8" ftp.encoding = "utf-8"
@@ -158,26 +147,26 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
services_path = settings.services_path services_path = settings.services_path
if download_type is DownloadType.SATELLITES: if download_type is DownloadType.SATELLITES:
upload_xml(ftp, data_path, sat_xml_path, _SAT_XML_FILE, callback) upload_xml(ftp, data_path, sat_xml_path, STC_XML_FILE, callback)
if s_type is SettingsType.NEUTRINO_MP and download_type is DownloadType.WEBTV: if s_type is SettingsType.NEUTRINO_MP and download_type is DownloadType.WEBTV:
upload_xml(ftp, data_path, sat_xml_path, _WEBTV_XML_FILE, callback) upload_xml(ftp, data_path, sat_xml_path, WEB_TV_XML_FILE, callback)
if download_type is DownloadType.BOUQUETS: if download_type is DownloadType.BOUQUETS:
ftp.cwd(services_path) ftp.cwd(services_path)
upload_bouquets(ftp, data_path, remove_unused, callback) upload_bouquets(ftp, data_path, remove_unused, callback)
if download_type is DownloadType.ALL: if download_type is DownloadType.ALL:
upload_xml(ftp, data_path, sat_xml_path, _SAT_XML_FILE, callback) upload_xml(ftp, data_path, sat_xml_path, STC_XML_FILE, callback)
if s_type is SettingsType.NEUTRINO_MP: if s_type is SettingsType.NEUTRINO_MP:
upload_xml(ftp, data_path, sat_xml_path, _WEBTV_XML_FILE, callback) upload_xml(ftp, data_path, sat_xml_path, WEB_TV_XML_FILE, callback)
ftp.cwd(services_path) ftp.cwd(services_path)
upload_bouquets(ftp, data_path, remove_unused, callback) upload_bouquets(ftp, data_path, remove_unused, callback)
upload_files(ftp, data_path, _DATA_FILES_LIST, callback) upload_files(ftp, data_path, DATA_FILES_LIST, callback)
if download_type is DownloadType.PICONS: if download_type is DownloadType.PICONS:
upload_picons(ftp, settings.picons_local_path, settings.picons_path, callback) upload_picons(ftp, settings.picons_local_path, settings.picons_path, callback, files_filter)
if tn and not use_http: if tn and not use_http:
# resume enigma or restart neutrino # resume enigma or restart neutrino
@@ -201,36 +190,38 @@ def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False
def upload_bouquets(ftp, data_path, remove_unused, callback): def upload_bouquets(ftp, data_path, remove_unused, callback):
if remove_unused: if remove_unused:
remove_unused_bouquets(ftp, callback) remove_unused_bouquets(ftp, callback)
upload_files(ftp, data_path, _BQ_FILES_LIST, callback) upload_files(ftp, data_path, BQ_FILES_LIST, callback)
def upload_files(ftp, data_path, file_list, callback): def upload_files(ftp, data_path, file_list, callback):
for file_name in os.listdir(data_path): for file_name in os.listdir(data_path):
if file_name == _SAT_XML_FILE or file_name == _WEBTV_XML_FILE: if file_name in STC_XML_FILE or file_name in WEB_TV_XML_FILE:
continue continue
if file_name.endswith(file_list): if file_name.endswith(file_list):
send_file(file_name, data_path, ftp, callback) send_file(file_name, data_path, ftp, callback)
def remove_unused_bouquets(ftp, callback): def remove_unused_bouquets(ftp, callback):
files = [] for file in filter(lambda f: f.endswith(("tv", "radio", "bouquets.xml", "ubouquets.xml")), ftp.nlst()):
ftp.dir(files.append) callback("Deleting file: {}. Status: {}\n".format(file, ftp.delete(file)))
for file in files:
name = str(file).strip()
if name.endswith(("tv", "radio", "bouquets.xml", "ubouquets.xml")):
name = name.split()[-1]
callback("Deleting file: {}. Status: {}\n".format(name, ftp.delete(name)))
def upload_xml(ftp, data_path, xml_path, xml_file, callback): def upload_xml(ftp, data_path, xml_path, xml_files, callback):
""" Used for transfer satellites.xml or webtv.xml files """ """ Used for transfer *.xml files. """
ftp.cwd(xml_path) ftp.cwd(xml_path)
send_file(xml_file, data_path, ftp, callback) for xml_file in xml_files:
send_file(xml_file, data_path, ftp, callback)
def download_xml(ftp, data_path, xml_path, xml_files, callback):
""" Used for download *.xml files. """
ftp.cwd(xml_path)
list(map(lambda f: download_file(ftp, f, data_path, callback), (f for f in ftp.nlst() if f.endswith(xml_files))))
# ***************** Picons *******************# # ***************** Picons *******************#
def upload_picons(ftp, src, dest, callback): def upload_picons(ftp, src, dest, callback, files_filter=None):
try: try:
ftp.cwd(dest) ftp.cwd(dest)
except error_perm as e: except error_perm as e:
@@ -238,31 +229,22 @@ def upload_picons(ftp, src, dest, callback):
ftp.mkd(dest) # if not exist ftp.mkd(dest) # if not exist
ftp.cwd(dest) ftp.cwd(dest)
delete_picons(ftp, callback) for file_name in filter(picons_filter_function(files_filter), os.listdir(src)):
send_file(file_name, src, ftp, callback)
for file_name in os.listdir(src):
if file_name.endswith(_PICONS_SUF):
send_file(file_name, src, ftp, callback)
def download_picons(ftp, src, dest, callback): def download_picons(ftp, src, dest, callback, files_filter=None):
try: try:
ftp.cwd(src) ftp.cwd(src)
except error_perm as e: except error_perm as e:
callback(str(e)) callback(str(e))
return return
files = [] for file in filter(picons_filter_function(files_filter), ftp.nlst()):
ftp.dir(files.append) download_file(ftp, file, dest, callback)
for file in files:
name = str(file).strip()
if name.endswith(_PICONS_SUF):
name = name.split()[-1]
download_file(ftp, name, dest, callback)
def delete_picons(ftp, callback, dest=None): def delete_picons(ftp, callback, dest=None, files_filter=None):
if dest: if dest:
try: try:
ftp.cwd(dest) ftp.cwd(dest)
@@ -270,24 +252,23 @@ def delete_picons(ftp, callback, dest=None):
callback(str(e)) callback(str(e))
return return
files = [] for file in filter(picons_filter_function(files_filter), ftp.nlst()):
ftp.dir(files.append) callback("Delete file: {}. Status: {}\n".format(file, ftp.delete(file)))
for file in files:
name = str(file).strip()
if name.endswith(_PICONS_SUF):
name = name.split()[-1]
callback("Delete file: {}. Status: {}\n".format(name, ftp.delete(name)))
def remove_picons(*, settings, callback, done_callback=None): def remove_picons(*, settings, callback, done_callback=None, files_filter=None):
with FTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp: with FTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
ftp.encoding = "utf-8" ftp.encoding = "utf-8"
callback("FTP OK.\n") callback("FTP OK.\n")
delete_picons(ftp, callback, settings.picons_path) delete_picons(ftp, callback, settings.picons_path, files_filter)
if done_callback: if done_callback:
done_callback() done_callback()
def picons_filter_function(files_filter=None):
return lambda f: f in files_filter if files_filter else f.endswith(PICONS_SUF)
def download_file(ftp, name, save_path, callback): def download_file(ftp, name, save_path, callback):
with open(save_path + name, "wb") as f: with open(save_path + name, "wb") as f:
callback("Downloading file: {}. Status: {}\n".format(name, str(ftp.retrbinary("RETR " + name, f.write)))) callback("Downloading file: {}. Status: {}\n".format(name, str(ftp.retrbinary("RETR " + name, f.write))))
@@ -295,7 +276,12 @@ def download_file(ftp, name, save_path, callback):
def send_file(file_name, path, ftp, callback): def send_file(file_name, path, ftp, callback):
""" Opens the file in binary mode and transfers into receiver """ """ Opens the file in binary mode and transfers into receiver """
with open(path + file_name, "rb") as f: file_src = path + file_name
if not os.path.isfile(file_src):
log("Uploading file: '{}'. File not found. Skipping.".format(file_src))
return
with open(file_src, "rb") as f:
callback("Uploading file: {}. Status: {}\n".format(file_name, str(ftp.storbinary("STOR " + file_name, f)))) callback("Uploading file: {}. Status: {}\n".format(file_name, str(ftp.storbinary("STOR " + file_name, f))))
@@ -340,42 +326,65 @@ class HttpAPI:
__MAX_WORKERS = 4 __MAX_WORKERS = 4
def __init__(self, settings): def __init__(self, settings):
from concurrent.futures import ThreadPoolExecutor as PoolExecutor
self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS)
self._settings = settings self._settings = settings
self._shutdown = False self._shutdown = False
self._session_id = 0 self._session_id = 0
self._main_url = None
self._base_url = None self._base_url = None
self._data = None self._data = None
self._is_owif = True
self.init() self.init()
from concurrent.futures import ThreadPoolExecutor as PoolExecutor
self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS)
def send(self, req_type, ref, callback=print, ref_prefix=""): def send(self, req_type, ref, callback=print, ref_prefix=""):
if self._shutdown: if self._shutdown:
return return
url = self._base_url + req_type.value url = self._base_url + req_type.value
data = self._data
if req_type is HttpRequestType.ZAP or req_type is HttpRequestType.STREAM: if req_type is HttpRequestType.ZAP or req_type is HttpRequestType.STREAM:
url += urllib.parse.quote(ref) url += urllib.parse.quote(ref)
elif req_type is HttpRequestType.PLAY or req_type is HttpRequestType.PLAYER_REMOVE: elif req_type is HttpRequestType.PLAY or req_type is HttpRequestType.PLAYER_REMOVE:
url += "{}{}".format(ref_prefix, urllib.parse.quote(ref).replace("%3A", "%253A")) url += "{}{}".format(ref_prefix, urllib.parse.quote(ref).replace("%3A", "%253A"))
elif req_type is HttpRequestType.GRUB:
data = None # Must be disabled for token-based security.
url = "{}/{}{}".format(self._main_url, req_type.value, ref)
future = self._executor.submit(get_response, req_type, url, self._data) def done_callback(f):
future.add_done_callback(lambda f: callback(f.result())) callback(f.result())
future = self._executor.submit(get_response, req_type, url, data)
future.add_done_callback(done_callback)
@run_task @run_task
def init(self): def init(self):
user, password = self._settings.http_user, self._settings.http_password user, password = self._settings.http_user, self._settings.http_password
use_ssl = self._settings.http_use_ssl use_ssl = self._settings.http_use_ssl
url = "http{}://{}:{}".format("s" if use_ssl else "", self._settings.host, self._settings.http_port) self._main_url = "http{}://{}:{}".format("s" if use_ssl else "", self._settings.host, self._settings.http_port)
self._base_url = "{}/web/".format(url) self._base_url = "{}/web/".format(self._main_url)
init_auth(user, password, url, use_ssl) init_auth(user, password, self._main_url, use_ssl)
url = "{}/web/{}".format(url, HttpRequestType.TOKEN.value) url = "{}/web/{}".format(self._main_url, HttpRequestType.TOKEN.value)
s_id = get_session_id(user, password, url) s_id = get_session_id(user, password, url)
if s_id != "0": if s_id != "0":
self._data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8") self._data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
self.send(HttpRequestType.INFO, None, self.init_callback)
def init_callback(self, info):
if info:
version = info.get("e2webifversion", "").upper()
self._is_owif = "OWIF" in version
version_info = "Web Interface version: {}".format(version) if version else ""
log("HTTP API initialized... {}".format(version_info))
@property
def is_owif(self):
""" Returns true if the web interface is OpenWebif. """
return self._is_owif
@run_task @run_task
def close(self): def close(self):
self._shutdown = True self._shutdown = True
@@ -386,7 +395,9 @@ def get_response(req_type, url, data=None):
try: try:
with urlopen(Request(url, data=data), timeout=10) as f: with urlopen(Request(url, data=data), timeout=10) as f:
if req_type is HttpRequestType.STREAM or req_type is HttpRequestType.STREAM_CURRENT: if req_type is HttpRequestType.STREAM or req_type is HttpRequestType.STREAM_CURRENT:
return f.read().decode("utf-8") return {"m3u": f.read().decode("utf-8")}
elif req_type is HttpRequestType.GRUB:
return {"img_data": f.read()}
elif req_type is HttpRequestType.CURRENT: elif req_type is HttpRequestType.CURRENT:
for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2event"): 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 return {el.tag: el.text for el in el.iter()} # return first[current] event from the list

View File

@@ -33,9 +33,9 @@ def get_bouquets(path, s_type):
@run_task @run_task
def write_bouquets(path, bouquets, s_type): def write_bouquets(path, bouquets, s_type, force_bq_names=False):
if s_type is SettingsType.ENIGMA_2: if s_type is SettingsType.ENIGMA_2:
write_enigma_bouquets(path, bouquets) write_enigma_bouquets(path, bouquets, force_bq_names)
elif s_type is SettingsType.NEUTRINO_MP: elif s_type is SettingsType.NEUTRINO_MP:
write_neutrino_bouquets(path, bouquets) write_neutrino_bouquets(path, bouquets)

View File

@@ -13,6 +13,7 @@ class BqServiceType(Enum):
DEFAULT = "DEFAULT" DEFAULT = "DEFAULT"
IPTV = "IPTV" IPTV = "IPTV"
MARKER = "MARKER" # 64 MARKER = "MARKER" # 64
SPACE = "SPACE" # 832 [hidden marker]
Bouquet = namedtuple("Bouquet", ["name", "type", "services", "locked", "hidden"]) Bouquet = namedtuple("Bouquet", ["name", "type", "services", "locked", "hidden"])
@@ -135,7 +136,7 @@ TRANSMISSION_MODE = {"0": "2k", "1": "8k", "2": "Auto", "3": "4k", "4": "1k", "5
GUARD_INTERVAL = {"0": "1/32", "1": "1/16", "2": "1/8", "3": "1/4", "4": "Auto", "5": "1/128", "6": "19/128", GUARD_INTERVAL = {"0": "1/32", "1": "1/16", "2": "1/8", "3": "1/4", "4": "Auto", "5": "1/128", "6": "19/128",
"7": "19/256"} "7": "19/256"}
HIERARCHY = {"0": "None", "1": "1", "2": "2", "3": "4", "4": "Auto"} HIERARCHY = {"0": "None", "1": "1", "2": "2", "3": "4", "4": "Auto"}
T_FEC = {"0": "1/2", "1": "2/3", "2": "3/4", "3": "5/6", "4": "7/8", "5": "Auto", "6": "6/7", "7": "8/9"} T_FEC = {"0": "1/2", "1": "2/3", "2": "3/4", "3": "5/6", "4": "7/8", "5": "Auto", "6": "6/7", "7": "8/9"}
@@ -145,10 +146,8 @@ T_SYSTEM = {"0": "DVB-T", "1": "DVB-T2", "-1": "DVB-T/T2"}
C_MODULATION = {"0": "Auto", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM128", "5": "QAM256"} C_MODULATION = {"0": "Auto", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM128", "5": "QAM256"}
# CAS # CAS
CAS = {"C:2600": "BISS", "C:0b00": "Conax", "C:0b01": "Conax", "C:0b02": "Conax", "C:0baa": "Conax", "C:0602": "Irdeto", CAS = {"C:26": "BISS", "C:0B": "Conax", "C:06": "Irdeto", "C:18": "Nagravision", "C:05": "Viaccess", "C:01": "SECA",
"C:0604": "Irdeto", "C:0606": "Irdeto", "C:0608": "Irdeto", "C:0622": "Irdeto", "C:0626": "Irdeto", "C:0E": "PowerVu", "C:4A": "DRE-Crypt", "C:7B": "DRE-Crypt", "C:56": "Verimatrix", "C:09": "VideoGuard"}
"C:0664": "Irdeto", "C:0614": "Irdeto", "C:0692": "Irdeto", "C:1801": "Nagravision", "C:0500": "Viaccess",
"C:0E00": "PowerVu", "C:4ae0": "DRE-Crypt", "C:4ae1": "DRE-Crypt", "C:7be1": "DRE-Crypt"}
# 'on' attribute 0070(hex) = 112(int) = ONID(ONID-TID on www.lyngsat.com) # 'on' attribute 0070(hex) = 112(int) = ONID(ONID-TID on www.lyngsat.com)
PROVIDER = {112: "HTB+", 253: "Tricolor TV"} PROVIDER = {112: "HTB+", 253: "Tricolor TV"}

View File

@@ -1,4 +1,4 @@
""" Module for parsing bouquets """ """ Module for working with Enigma2 bouquets. """
import re import re
from collections import Counter from collections import Counter
@@ -15,41 +15,53 @@ def get_bouquets(path):
BqType.RADIO.value) BqType.RADIO.value)
def write_bouquets(path, bouquets): def write_bouquets(path, bouquets, force_bq_names=False):
""" Creating and writing bouquets files.
If "force_bq_names" then naming the files using the name of the bouquet.
Some images may have problems displaying the favorites list!
"""
srv_line = '#SERVICE 1:7:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n' srv_line = '#SERVICE 1:7:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
line = [] line = []
pattern = re.compile("[^\\w_()]+") pattern = re.compile("[^\\w_()]+")
current_marker = [0] m_index = [0]
s_index = [0]
for bqs in bouquets: for bqs in bouquets:
line.clear() line.clear()
line.append("#NAME {}\n".format(bqs.name)) line.append("#NAME {}\n".format(bqs.name))
for bq in bqs.bouquets: for index, bq in enumerate(bqs.bouquets):
bq_name = bq.name bq_name = bq.name
if bq_name == "Favourites (TV)" or bq_name == "Favourites (Radio)": if bq_name == "Favourites (TV)" or bq_name == "Favourites (Radio)":
bq_name = _DEFAULT_BOUQUET_NAME bq_name = _DEFAULT_BOUQUET_NAME
else: else:
bq_name = re.sub(pattern, "_", bq.name) bq_name = re.sub(pattern, "_", bq.name) if force_bq_names else "de{0:02d}".format(index)
line.append(srv_line.format(2 if bq.type == BqType.RADIO.value else 1, bq_name, bq.type)) line.append(srv_line.format(2 if bq.type == BqType.RADIO.value else 1, bq_name, bq.type))
write_bouquet(path + "userbouquet.{}.{}".format(bq_name, bq.type), bq.name, bq.services, current_marker) write_bouquet(path + "userbouquet.{}.{}".format(bq_name, bq.type), bq.name, bq.services, m_index, s_index)
with open(path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file: with open(path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file:
file.writelines(line) file.writelines(line)
def write_bouquet(path, name, services, current_marker): def write_bouquet(path, name, services, current_marker, current_space):
bouquet = ["#NAME {}\n".format(name)] bouquet = ["#NAME {}\n".format(name)]
marker = "#SERVICE 1:64:{:X}:0:0:0:0:0:0:0::{}\n" marker = "#SERVICE 1:64:{:X}:0:0:0:0:0:0:0::{}\n"
space = "#SERVICE 1:832:D:{}:0:0:0:0:0:0:\n"
for srv in services: for srv in services:
if srv.service_type == BqServiceType.IPTV.name: s_type = srv.service_type
if s_type == BqServiceType.IPTV.name:
bouquet.append("#SERVICE {}\n".format(srv.fav_id.strip())) bouquet.append("#SERVICE {}\n".format(srv.fav_id.strip()))
elif srv.service_type == BqServiceType.MARKER.name: elif s_type == BqServiceType.MARKER.name:
m_data = srv.fav_id.strip().split(":") m_data = srv.fav_id.strip().split(":")
m_data[2] = current_marker[0] m_data[2] = current_marker[0]
current_marker[0] += 1 current_marker[0] += 1
bouquet.append(marker.format(m_data[2], m_data[-1])) bouquet.append(marker.format(m_data[2], m_data[-1]))
elif s_type == BqServiceType.SPACE.name:
bouquet.append(space.format(current_space[0]))
current_space[0] += 1
else: else:
data = to_bouquet_id(srv) data = to_bouquet_id(srv)
if srv.service: if srv.service:
@@ -62,7 +74,7 @@ def write_bouquet(path, name, services, current_marker):
def to_bouquet_id(srv): def to_bouquet_id(srv):
""" Creates bouquet channel id """ """ Creates bouquet channel id. """
data_type = srv.data_id data_type = srv.data_id
if data_type and len(data_type) > 4: if data_type and len(data_type) > 4:
data_type = int(srv.data_id.split(":")[4]) data_type = int(srv.data_id.split(":")[4])
@@ -70,28 +82,39 @@ def to_bouquet_id(srv):
return "{}:0:{:X}:{}:0:0:0:".format(1, data_type, srv.fav_id) return "{}:0:{:X}:{}:0:0:0:".format(1, data_type, srv.fav_id)
def get_bouquet(path, name, bq_type): def get_bouquet(path, bq_name, bq_type):
""" Parsing services ids from bouquet file """ """ Parsing services ids from bouquet file. """
with open(path + "userbouquet.{}.{}".format(name, bq_type), encoding="utf-8", errors="replace") as file: with open(path + "userbouquet.{}.{}".format(bq_name, bq_type), encoding="utf-8", errors="replace") as file:
chs_list = file.read() chs_list = file.read()
services = [] services = []
srvs = list(filter(None, chs_list.split("\n#SERVICE"))) # filtering [''] srvs = list(filter(None, chs_list.split("\n#SERVICE"))) # filtering ['']
for ch in srvs[1:]: # May come across empty[wrong] files!
ch_data = ch.strip().split(":") if not srvs:
if ch_data[1] == "64": log("Bouquet file 'userbouquet.{}.{}' is empty or wrong!".format(bq_name, bq_type))
marker_data = ch.split("#DESCRIPTION", 1) return "{} [empty]".format(bq_name), services
services.append(BouquetService(marker_data[1].strip(), BqServiceType.MARKER, ch, ch_data[2]))
elif "http" in ch:
stream_data = ch.split("#DESCRIPTION", 1)
services.append(BouquetService(stream_data[-1].strip(":").strip(), BqServiceType.IPTV, ch, 0))
else:
fav_id = "{}:{}:{}:{}".format(ch_data[3], ch_data[4], ch_data[5], ch_data[6])
name = None
if len(ch_data) == 12:
name, desc = str(ch_data[-1]).split("\n#DESCRIPTION")
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id.upper(), 0))
return srvs[0].lstrip("#NAME").strip(), services bq_name = srvs.pop(0)
for num, srv in enumerate(srvs, start=1):
srv_data = srv.strip().split(":")
if srv_data[1] == "64":
m_data, sep, desc = srv.partition("#DESCRIPTION")
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.MARKER, srv, num))
elif srv_data[1] == "832":
m_data, sep, desc = srv.partition("#DESCRIPTION")
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.SPACE, srv, num))
elif "http" in srv or srv_data[0] == "8193":
stream_data, sep, desc = srv.partition("#DESCRIPTION")
desc = desc.lstrip(":").strip() if desc else srv_data[-1].strip()
services.append(BouquetService(desc, BqServiceType.IPTV, srv, num))
else:
fav_id = "{}:{}:{}:{}".format(srv_data[3], srv_data[4], srv_data[5], srv_data[6])
name = None
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))
return bq_name.lstrip("#NAME").strip(), services
def parse_bouquets(path, bq_name, bq_type): def parse_bouquets(path, bq_name, bq_type):
@@ -112,7 +135,7 @@ def parse_bouquets(path, bq_name, bq_type):
if name: if name:
b_name = name.group(1) b_name = name.group(1)
if b_name in b_names: if b_name in b_names:
raise ValueError("The list of bouquets contains duplicate [{}] names!".format(b_name)) log("The list of bouquets contains duplicate [{}] names!".format(b_name))
else: else:
b_names.add(b_name) b_names.add(b_name)

View File

@@ -1,7 +1,7 @@
""" Module for IPTV and streams support """ """ Module for IPTV and streams support """
import re import re
import urllib.request
from enum import Enum from enum import Enum
from urllib.parse import unquote, quote
from app.settings import SettingsType from app.settings import SettingsType
from app.ui.uicommons import IPTV_ICON from app.ui.uicommons import IPTV_ICON
@@ -18,6 +18,7 @@ class StreamType(Enum):
NONE_TS = "4097" NONE_TS = "4097"
NONE_REC_1 = "5001" NONE_REC_1 = "5001"
NONE_REC_2 = "5002" NONE_REC_2 = "5002"
E_SERVICE_URI = "8193"
def parse_m3u(path, s_type): def parse_m3u(path, s_type):
@@ -64,7 +65,7 @@ def export_to_m3u(path, bouquet, s_type):
lines.append("#EXTINF:-1,{}\n".format(s.name)) lines.append("#EXTINF:-1,{}\n".format(s.name))
if current_grp: if current_grp:
lines.append(current_grp) lines.append(current_grp)
lines.append("{}\n".format(urllib.request.unquote(data.strip()))) lines.append("{}\n".format(unquote(data.strip())))
elif s_type is BqServiceType.MARKER: elif s_type is BqServiceType.MARKER:
current_grp = "#EXTGRP:{}\n".format(s.name) current_grp = "#EXTGRP:{}\n".format(s.name)
@@ -75,9 +76,8 @@ def export_to_m3u(path, bouquet, s_type):
def get_fav_id(url, service_name, s_type): def get_fav_id(url, service_name, s_type):
""" Returns fav id depending on the profile. """ """ Returns fav id depending on the profile. """
if s_type is SettingsType.ENIGMA_2: if s_type is SettingsType.ENIGMA_2:
url = urllib.request.quote(url)
stream_type = StreamType.NONE_TS.value stream_type = StreamType.NONE_TS.value
return ENIGMA2_FAV_ID_FORMAT.format(stream_type, 1, 0, 0, 0, 0, url, service_name, service_name, None) return ENIGMA2_FAV_ID_FORMAT.format(stream_type, 1, 0, 0, 0, 0, quote(url), service_name, service_name, None)
elif s_type is SettingsType.NEUTRINO_MP: elif s_type is SettingsType.NEUTRINO_MP:
return NEUTRINO_FAV_ID_FORMAT.format(url, "", 0, None, None, None, None, "", "", 1) return NEUTRINO_FAV_ID_FORMAT.format(url, "", 0, None, None, None, None, "", "", 1)

View File

@@ -2,7 +2,9 @@ import copy
import json import json
import locale import locale
import os import os
import sys
from enum import Enum, IntEnum from enum import Enum, IntEnum
from functools import lru_cache
from pathlib import Path from pathlib import Path
from pprint import pformat from pprint import pformat
from textwrap import dedent from textwrap import dedent
@@ -12,6 +14,8 @@ CONFIG_PATH = HOME_PATH + "/.config/demon-editor/"
CONFIG_FILE = CONFIG_PATH + "config.json" CONFIG_FILE = CONFIG_PATH + "config.json"
DATA_PATH = HOME_PATH + "/DemonEditor/data/" DATA_PATH = HOME_PATH + "/DemonEditor/data/"
IS_DARWIN = sys.platform == "darwin"
class Defaults(Enum): class Defaults(Enum):
""" Default program settings """ """ Default program settings """
@@ -19,6 +23,7 @@ class Defaults(Enum):
BACKUP_BEFORE_DOWNLOADING = True BACKUP_BEFORE_DOWNLOADING = True
BACKUP_BEFORE_SAVE = True BACKUP_BEFORE_SAVE = True
V5_SUPPORT = False V5_SUPPORT = False
FORCE_BQ_NAMES = False
HTTP_API_SUPPORT = False HTTP_API_SUPPORT = False
ENABLE_YT_DL = False ENABLE_YT_DL = False
ENABLE_SEND_TO = False ENABLE_SEND_TO = False
@@ -26,6 +31,19 @@ class Defaults(Enum):
NEW_COLOR = "rgb(255,230,204)" NEW_COLOR = "rgb(255,230,204)"
EXTRA_COLOR = "rgb(179,230,204)" EXTRA_COLOR = "rgb(179,230,204)"
FAV_CLICK_MODE = 0 FAV_CLICK_MODE = 0
PLAY_STREAMS_MODE = 1 if IS_DARWIN else 0
PROFILE_FOLDER_DEFAULT = False
RECORDS_PATH = DATA_PATH + "records/"
ACTIVATE_TRANSCODING = False
ACTIVE_TRANSCODING_PRESET = "720p TV/device"
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") as config_file:
return json.load(config_file)
def get_default_settings(profile_name="default"): def get_default_settings(profile_name="default"):
@@ -43,14 +61,33 @@ def get_default_settings(profile_name="default"):
"use_colors": Defaults.USE_COLORS.value, "use_colors": Defaults.USE_COLORS.value,
"new_color": Defaults.NEW_COLOR.value, "new_color": Defaults.NEW_COLOR.value,
"extra_color": Defaults.EXTRA_COLOR.value, "extra_color": Defaults.EXTRA_COLOR.value,
"fav_click_mode": Defaults.FAV_CLICK_MODE.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 set_local_paths(settings, profile_name): def get_default_transcoding_presets():
settings["data_local_path"] = "{}{}/".format(settings["data_local_path"], profile_name) return {"720p TV/device": {"vcodec": "h264", "vb": "1500", "width": "1280", "height": "720", "acodec": "mp3",
settings["picons_local_path"] = "{}{}/".format(settings["picons_local_path"], profile_name) "ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"},
settings["backup_local_path"] = "{}{}/".format(settings["backup_local_path"], profile_name) "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") 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)
if use_profile_folder:
settings["picons_local_path"] = "{}{}/{}/".format(data_path, profile_name, "picons")
settings["backup_local_path"] = "{}{}/{}/".format(data_path, profile_name, "backup")
else:
settings["picons_local_path"] = "{}{}/{}/".format(data_path, "picons", profile_name)
settings["backup_local_path"] = "{}{}/{}/".format(data_path, "backup", profile_name)
class SettingsType(IntEnum): class SettingsType(IntEnum):
@@ -87,12 +124,26 @@ class SettingsException(Exception):
pass pass
class SettingsReadException(SettingsException):
pass
class PlayStreamsMode(IntEnum):
""" Behavior mode when opening streams. """
BUILT_IN = 0
VLC = 1
M3U = 2
class Settings: class Settings:
__INSTANCE = None __INSTANCE = None
__VERSION = 1 __VERSION = 1
def __init__(self, ext_settings=None): def __init__(self, ext_settings=None):
settings = ext_settings or get_settings() try:
settings = ext_settings or get_settings()
except PermissionError as e:
raise SettingsReadException(e)
if self.__VERSION > settings.get("version", 0): if self.__VERSION > settings.get("version", 0):
raise SettingsException("Outdated version of the settings format!") raise SettingsException("Outdated version of the settings format!")
@@ -126,7 +177,10 @@ class Settings:
def reset(self, force_write=False): def reset(self, force_write=False):
for k, v in self.setting_type.get_default_settings().items(): for k, v in self.setting_type.get_default_settings().items():
self._cp_settings[k] = v self._cp_settings[k] = v
set_local_paths(self._cp_settings, self._current_profile)
def_path = self.default_data_path
def_path += "enigma2/" if self.setting_type is SettingsType.ENIGMA_2 else "neutrino/"
set_local_paths(self._cp_settings, self._current_profile, def_path, self.profile_folder_is_default)
if force_write: if force_write:
self.save() self.save()
@@ -191,21 +245,7 @@ class Settings:
def setting_type(self, s_type): def setting_type(self, s_type):
self._cp_settings["setting_type"] = s_type.value self._cp_settings["setting_type"] = s_type.value
@property # ******* Network ******** #
def language(self):
return self._settings.get("language", locale.getlocale()[0] or "en_US")
@language.setter
def language(self, value):
self._settings["language"] = value
@property
def load_last_config(self):
return self._settings.get("load_last_config", False)
@load_last_config.setter
def load_last_config(self, value):
self._settings["load_last_config"] = value
@property @property
def host(self): def host(self):
@@ -335,14 +375,6 @@ class Settings:
def satellites_xml_path(self, value): def satellites_xml_path(self, value):
self._cp_settings["satellites_xml_path"] = value self._cp_settings["satellites_xml_path"] = value
@property
def data_local_path(self):
return self._cp_settings.get("data_local_path", self.get_default("data_local_path"))
@data_local_path.setter
def data_local_path(self, value):
self._cp_settings["data_local_path"] = value
@property @property
def picons_path(self): def picons_path(self):
return self._cp_settings.get("picons_path", self.get_default("picons_path")) return self._cp_settings.get("picons_path", self.get_default("picons_path"))
@@ -351,6 +383,32 @@ class Settings:
def picons_path(self, value): def picons_path(self, value):
self._cp_settings["picons_path"] = value self._cp_settings["picons_path"] = value
# ***** Local paths ***** #
@property
def profile_folder_is_default(self):
return self._settings.get("profile_folder_is_default", Defaults.PROFILE_FOLDER_DEFAULT.value)
@profile_folder_is_default.setter
def profile_folder_is_default(self, value):
self._settings["profile_folder_is_default"] = value
@property
def default_data_path(self):
return self._settings.get("default_data_path", DATA_PATH)
@default_data_path.setter
def default_data_path(self, value):
self._settings["default_data_path"] = value
@property
def data_local_path(self):
return self._cp_settings.get("data_local_path", self.get_default("data_local_path"))
@data_local_path.setter
def data_local_path(self, value):
self._cp_settings["data_local_path"] = value
@property @property
def picons_local_path(self): def picons_local_path(self):
return self._cp_settings.get("picons_local_path", self.get_default("picons_local_path")) return self._cp_settings.get("picons_local_path", self.get_default("picons_local_path"))
@@ -367,7 +425,60 @@ class Settings:
def backup_local_path(self, value): def backup_local_path(self, value):
self._cp_settings["backup_local_path"] = value self._cp_settings["backup_local_path"] = value
# ***** Program settings ***** @property
def records_path(self):
return self._settings.get("records_path", Defaults.RECORDS_PATH.value)
@records_path.setter
def records_path(self, value):
self._settings["records_path"] = value
# ******** Streaming ********* #
@property
def activate_transcoding(self):
return self._settings.get("activate_transcoding", Defaults.ACTIVATE_TRANSCODING.value)
@activate_transcoding.setter
def activate_transcoding(self, value):
self._settings["activate_transcoding"] = value
@property
def active_preset(self):
return self._settings.get("active_preset", Defaults.ACTIVE_TRANSCODING_PRESET.value)
@active_preset.setter
def active_preset(self, value):
self._settings["active_preset"] = value
@property
def transcoding_presets(self):
return self._settings.get("transcoding_presets", get_default_transcoding_presets())
@transcoding_presets.setter
def transcoding_presets(self, value):
self._settings["transcoding_presets"] = value
@property
def play_streams_mode(self):
return PlayStreamsMode(self._settings.get("play_streams_mode", Defaults.PLAY_STREAMS_MODE.value))
@play_streams_mode.setter
def play_streams_mode(self, value):
self._settings["play_streams_mode"] = value
# *********** EPG ************ #
@property
def epg_options(self):
""" Options used by the EPG dialog. """
return self._cp_settings.get("epg_options", None)
@epg_options.setter
def epg_options(self, value):
self._cp_settings["epg_options"] = value
# ***** Program settings ***** #
@property @property
def backup_before_save(self): def backup_before_save(self):
@@ -393,6 +504,14 @@ class Settings:
def v5_support(self, value): def v5_support(self, value):
self._settings["v5_support"] = value self._settings["v5_support"] = value
@property
def force_bq_names(self):
return self._settings.get("force_bq_names", Defaults.FORCE_BQ_NAMES.value)
@force_bq_names.setter
def force_bq_names(self, value):
self._settings["force_bq_names"] = value
@property @property
def http_api_support(self): def http_api_support(self):
return self._settings.get("http_api_support", Defaults.HTTP_API_SUPPORT.value) return self._settings.get("http_api_support", Defaults.HTTP_API_SUPPORT.value)
@@ -409,6 +528,14 @@ class Settings:
def enable_yt_dl(self, value): def enable_yt_dl(self, value):
self._settings["enable_yt_dl"] = value self._settings["enable_yt_dl"] = value
@property
def enable_yt_dl_update(self):
return self._settings.get("enable_yt_dl_update", Defaults.ENABLE_YT_DL.value)
@enable_yt_dl_update.setter
def enable_yt_dl_update(self, value):
self._settings["enable_yt_dl_update"] = value
@property @property
def enable_send_to(self): def enable_send_to(self):
return self._settings.get("enable_send_to", Defaults.ENABLE_SEND_TO.value) return self._settings.get("enable_send_to", Defaults.ENABLE_SEND_TO.value)
@@ -449,21 +576,126 @@ class Settings:
def fav_click_mode(self, value): def fav_click_mode(self, value):
self._settings["fav_click_mode"] = value self._settings["fav_click_mode"] = value
@property
def language(self):
return self._settings.get("language", locale.getlocale()[0] or "en_US")
def get_settings(): @language.setter
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True) # create dir if not exist def language(self, value):
os.makedirs(os.path.dirname(DATA_PATH), exist_ok=True) self._settings["language"] = value
if not os.path.isfile(CONFIG_FILE) or os.stat(CONFIG_FILE).st_size == 0: @property
write_settings(get_default_settings()) def load_last_config(self):
return self._settings.get("load_last_config", False)
with open(CONFIG_FILE, "r") as config_file: @load_last_config.setter
return json.load(config_file) def load_last_config(self, value):
self._settings["load_last_config"] = value
@property
def show_srv_hints(self):
""" Show short info as hints in the main services list. """
return self._settings.get("show_srv_hints", True)
def write_settings(config): @show_srv_hints.setter
with open(CONFIG_FILE, "w") as config_file: def show_srv_hints(self, value):
json.dump(config, config_file, indent=" ") self._settings["show_srv_hints"] = value
@property
def show_bq_hints(self):
""" Show detailed info as hints in the bouquet list. """
return self._settings.get("show_bq_hints", True)
@show_bq_hints.setter
def show_bq_hints(self, value):
self._settings["show_bq_hints"] = value
# *********** Appearance *********** #
@property
def dark_mode(self):
return self._settings.get("dark_mode", False)
@dark_mode.setter
def dark_mode(self, value):
self._settings["dark_mode"] = value
@property
def is_themes_support(self):
return self._settings.get("is_themes_support", False)
@is_themes_support.setter
def is_themes_support(self, value):
self._settings["is_themes_support"] = value
@property
def theme(self):
return self._settings.get("theme", "Default")
@theme.setter
def theme(self, value):
self._settings["theme"] = value
@property
@lru_cache(1)
def themes_path(self):
return "{}/.themes/".format(HOME_PATH)
@property
def icon_theme(self):
return self._settings.get("icon_theme", "Adwaita")
@icon_theme.setter
def icon_theme(self, value):
self._settings["icon_theme"] = value
@property
@lru_cache(1)
def icon_themes_path(self):
return "{}/.icons/".format(HOME_PATH)
@property
def is_darwin(self):
return IS_DARWIN
# *********** Download dialog *********** #
@property
def use_http(self):
return self._settings.get("use_http", True)
@use_http.setter
def use_http(self, value):
self._settings["use_http"] = value
@property
def remove_unused_bouquets(self):
return self._settings.get("remove_unused_bouquets", True)
@remove_unused_bouquets.setter
def remove_unused_bouquets(self, value):
self._settings["remove_unused_bouquets"] = value
# **************** Debug **************** #
@property
def debug_mode(self):
return self._settings.get("debug_mode", False)
@debug_mode.setter
def debug_mode(self, value):
self._settings["debug_mode"] = value
# **************** Experimental **************** #
@property
def is_enable_experimental(self):
""" Allows experimental functionality. """
return self._settings.get("enable_experimental", False)
@is_enable_experimental.setter
def is_enable_experimental(self, value):
self._settings["enable_experimental"] = value
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1,10 +1,17 @@
import os
import subprocess
import sys import sys
from datetime import datetime
from enum import Enum
from urllib.request import urlopen
from app.commons import run_task, log from app.commons import run_task, log, _DATE_FORMAT
from app.settings import PlayStreamsMode
class Player: class Player:
__VLC_INSTANCE = None __VLC_INSTANCE = None
__PLAY_STREAMS_MODE = PlayStreamsMode.BUILT_IN
def __init__(self, rewind_callback, position_callback, error_callback, playing_callback): def __init__(self, rewind_callback, position_callback, error_callback, playing_callback):
try: try:
@@ -44,6 +51,10 @@ class Player:
cls.__VLC_INSTANCE = Player(rewind_callback, position_callback, error_callback, playing_callback) cls.__VLC_INSTANCE = Player(rewind_callback, position_callback, error_callback, playing_callback)
return cls.__VLC_INSTANCE return cls.__VLC_INSTANCE
@staticmethod
def get_play_mode():
return Player.__PLAY_STREAMS_MODE
@run_task @run_task
def play(self, mrl=None): def play(self, mrl=None):
if mrl: if mrl:
@@ -103,5 +114,157 @@ class Player:
self._player.set_fullscreen(full) self._player.set_fullscreen(full)
class HttpPlayer:
""" Simple wrapper for VLC media player to interact over http. """
__VLC_INSTANCE = None
__PLAY_STREAMS_MODE = PlayStreamsMode.VLC
class Commands(Enum):
STATUS = "http://127.0.0.1:{}/requests/status.xml"
PLAY = "http://127.0.0.1:{}/requests/status.xml?command=in_play&input={}"
STOP = "http://127.0.0.1:{}/requests/status.xml?command=pl_stop"
CLEAR = "http://127.0.0.1:{}/requests/status.xml?command=pl_empty"
def __init__(self, exe, port, is_darwin):
from concurrent.futures import ThreadPoolExecutor as PoolExecutor
self._executor = PoolExecutor(max_workers=1)
self._cmd = [exe, "--no-stats", "--verbose=-1", "--extraintf", "http", "--http-port", port, "--quiet"]
if not is_darwin:
self._cmd.append("--one-instance")
self._p = None
self._state = None
self._port = port
@classmethod
def get_instance(cls, settings):
if not cls.__VLC_INSTANCE:
import shutil
is_darwin = settings.is_darwin
# TODO Add options[vlc_exe and port] to the settings!
exe = "/Applications/VLC.app/Contents/MacOS/VLC" if is_darwin else "/usr/bin/vlc"
if shutil.which(exe) is None:
raise ImportError
cls.__VLC_INSTANCE = HttpPlayer(exe=exe, port=str(9090), is_darwin=is_darwin)
return cls.__VLC_INSTANCE
@staticmethod
def get_play_mode():
return HttpPlayer.__PLAY_STREAMS_MODE
@run_task
def play(self, mrl=None):
if not self._p or self._p and self._p.poll() is not None:
self._p = subprocess.Popen(self._cmd + [mrl], preexec_fn=os.setsid)
self._p.communicate()
else:
self._executor.submit(self.open_command, self.Commands.CLEAR)
self._executor.submit(self.open_command, self.Commands.PLAY, mrl)
def open_command(self, command, url=None):
if command is self.Commands.PLAY:
url = self.Commands.PLAY.value.format(self._port, url)
else:
url = command.value.format(self._port)
try:
with urlopen(url, timeout=5) as f:
self._state = command
except Exception as e:
log("{}[open_command, {}] error: {}".format(__class__.__name__, command, e))
def stop(self):
if self._state is self.Commands.PLAY:
self._executor.submit(self.open_command, self.Commands.STOP)
def pause(self):
pass
def set_time(self, time):
pass
@run_task
def release(self):
if self._p and self._p.poll() is None:
import signal
# Good explanation here: https://stackoverflow.com/a/4791612
os.killpg(os.getpgid(self._p.pid), signal.SIGTERM)
def is_playing(self):
return self._state is self.Commands.PLAY
def set_full_screen(self, full):
pass
class Recorder:
__VLC_REC_INSTANCE = None
_CMD = "sout=#std{{access=file,mux=ts,dst={}.ts}}"
_TR_CMD = "sout=#transcode{{{}}}:file{{mux=mp4,dst={}.mp4}}"
def __init__(self, settings):
try:
from app.tools import vlc
from app.tools.vlc import EventType
except OSError as e:
log("{}: Load library error: {}".format(__class__.__name__, e))
raise ImportError
else:
self._settings = settings
self._is_record = False
args = "--quiet {}".format("" if sys.platform == "darwin" else "--no-xlib")
self._recorder = vlc.Instance(args).media_player_new()
@classmethod
def get_instance(cls, settings):
if not cls.__VLC_REC_INSTANCE:
cls.__VLC_REC_INSTANCE = Recorder(settings)
return cls.__VLC_REC_INSTANCE
@run_task
def record(self, url, name):
if self._recorder:
self._recorder.stop()
path = self._settings.records_path
os.makedirs(os.path.dirname(path), exist_ok=True)
d_now = datetime.now().strftime(_DATE_FORMAT)
path = "{}{}_{}".format(path, name.replace(" ", "_"), d_now.replace(" ", "_"))
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()
self._recorder.set_media(media)
self._is_record = True
self._recorder.play()
log("Record started {}".format(d_now))
@run_task
def stop(self):
self._recorder.stop()
self._is_record = False
log("Recording stopped.")
def is_record(self):
return self._is_record
@run_task
def release(self):
if self._recorder:
self._recorder.stop()
self._recorder.release()
self._is_record = False
log("Recording stopped. Releasing...")
def get_transcoding_cmd(self, path):
presets = self._settings.transcoding_presets
prs = presets.get(self._settings.active_preset)
return self._TR_CMD.format(",".join("{}={}".format(k, v) for k, v in prs.items()), path)
if __name__ == "__main__": if __name__ == "__main__":
pass pass

View File

@@ -125,10 +125,7 @@ class ProviderParser(HTMLParser):
_POSITION_PATTERN = re.compile("at\s\d+\..*(?:E|W)']") _POSITION_PATTERN = re.compile("at\s\d+\..*(?:E|W)']")
_ONID_TID_PATTERN = re.compile("^\d+-\d+.*") _ONID_TID_PATTERN = re.compile("^\d+-\d+.*")
_TRANSPONDER_FREQUENCY_PATTERN = re.compile("^\d+ [HVLR]+") _TRANSPONDER_FREQUENCY_PATTERN = re.compile("^\d+ [HVLR]+")
_DOMAIN = "http://www.lyngsat.com" _DOMAINS = {"/tvchannels/", "/radiochannels/", "/packages/"}
_TV_DOMAIN = _DOMAIN + "/tvchannels/"
_RADIO_DOMAIN = _DOMAIN + "/radiochannels/"
_PKG_DOMAIN = _DOMAIN + "/packages/"
def __init__(self, entities=False, separator=' '): def __init__(self, entities=False, separator=' '):
@@ -160,7 +157,7 @@ class ProviderParser(HTMLParser):
self._current_row.append(attrs[0][1]) self._current_row.append(attrs[0][1])
if tag == "a": if tag == "a":
url = attrs[0][1] url = attrs[0][1]
if url.startswith((self._PKG_DOMAIN, self._TV_DOMAIN, self._RADIO_DOMAIN)): if any(d in url for d in self._DOMAINS):
self._current_row.append(url) self._current_row.append(url)
if tag == "font" and len(attrs) == 1: if tag == "font" and len(attrs) == 1:
atr = attrs[0] atr = attrs[0]

View File

@@ -1,16 +1,21 @@
""" Module for working with YouTube service """ """ Module for working with YouTube service """
import gzip import gzip
import json import json
import os
import re import re
import urllib import shutil
import sys
from html.parser import HTMLParser from html.parser import HTMLParser
from json import JSONDecodeError from json import JSONDecodeError
from urllib.request import Request from urllib.error import URLError
from urllib.parse import unquote
from urllib.request import Request, urlopen, urlretrieve
from app.commons import log from app.commons import log
from app.ui.uicommons import show_notification
_YT_PATTERN = re.compile(r"https://www.youtube.com/.+(?:v=)([\w-]{11}).*") _YT_PATTERN = re.compile(r"https://www.youtube.com/.+(?:v=)([\w-]{11}).*")
_YT_LIST_PATTERN = re.compile(r"https://www.youtube.com/.+?(?:list=)([\w-]{23,})?.*") _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?.*") _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", _HEADERS = {"User-Agent": "Mozilla/5.0 (X11; Linux i586; rv:31.0) Gecko/20100101 Firefox/69.0",
"DNT": "1", "DNT": "1",
@@ -20,9 +25,35 @@ Quality = {137: "1080p", 136: "720p", 135: "480p", 134: "360p",
133: "240p", 160: "144p", 0: "0p", 18: "360p", 22: "720p"} 133: "240p", 160: "144p", 0: "0p", 18: "360p", 22: "720p"}
class YouTubeException(Exception):
pass
class YouTube: class YouTube:
""" Helper class for working with YouTube service. """ """ Helper class for working with YouTube service. """
_YT_INSTANCE = None
_VIDEO_INFO_LINK = "https://youtube.com/get_video_info?video_id={}&hl=en"
VIDEO_LINK = "https://www.youtube.com/watch?v={}"
def __init__(self, settings, callback):
self._settings = settings
self._yt_dl = None
self._callback = callback
if self._settings.enable_yt_dl:
try:
self._yt_dl = YouTubeDL.get_instance(self._settings, callback=self._callback)
except YouTubeException:
pass # NOP
@classmethod
def get_instance(cls, settings, callback=log):
if not cls._YT_INSTANCE:
cls._YT_INSTANCE = YouTube(settings, callback)
return cls._YT_INSTANCE
@staticmethod @staticmethod
def is_yt_video_link(url): def is_yt_video_link(url):
return re.match(_YT_VIDEO_PATTERN, url) return re.match(_YT_VIDEO_PATTERN, url)
@@ -41,17 +72,29 @@ class YouTube:
if yt: if yt:
return yt.group(1) return yt.group(1)
@staticmethod def get_yt_link(self, video_id, url=None, skip_errors=False):
def get_yt_link(video_id): """ Getting link to YouTube video by id or URL.
""" Getting link to YouTube video by id.
returns tuple from the video links dict and title Returns tuple from the video links dict and title.
""" """
req = Request("https://youtube.com/get_video_info?video_id={}&hl=en".format(video_id), headers=_HEADERS) if self._settings.enable_yt_dl and url:
if not self._yt_dl:
self._yt_dl = YouTubeDL.get_instance(self._settings, self._callback)
return self._yt_dl.get_yt_link(url, skip_errors)
with urllib.request.urlopen(req, timeout=2) as resp: return self.get_yt_link_by_id(video_id)
data = urllib.request.unquote(gzip.decompress(resp.read()).decode("utf-8")).split("&")
out = {k: v for k, sep, v in (str(d).partition("=") for d in map(urllib.request.unquote, data))} @staticmethod
def get_yt_link_by_id(video_id):
""" Getting link to YouTube video by id.
Returns tuple from the video links dict and title.
"""
req = Request(YouTube._VIDEO_INFO_LINK.format(video_id), headers=_HEADERS)
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) player_resp = out.get("player_response", None)
if player_resp: if player_resp:
@@ -67,7 +110,7 @@ class YouTube:
if fmts: if fmts:
urls = {Quality[i["itag"]]: i["url"] for i in urls = {Quality[i["itag"]]: i["url"] for i in
filter(lambda i: i.get("itag", -1) in Quality, fmts)} filter(lambda i: i.get("itag", -1) in Quality, fmts) if "url" in i}
if urls and title: if urls and title:
return urls, title.replace("+", " ") return urls, title.replace("+", " ")
@@ -76,7 +119,7 @@ class YouTube:
if stream_map: if stream_map:
s_map = {k: v for k, sep, v in (str(d).partition("=") for d in stream_map.split("&"))} 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 = s_map.get("url", None), out.get("title", None)
url, title = urllib.request.unquote(url) if url else "", title.replace("+", " ") if title else "" url, title = unquote(url) if url else "", title.replace("+", " ") if title else ""
if url and title: if url and title:
return {Quality[0]: url}, title.replace("+", " ") return {Quality[0]: url}, title.replace("+", " ")
@@ -121,7 +164,8 @@ class PlayListParser(HTMLParser):
ct = resp.get("contents", None) ct = resp.get("contents", None)
if ct: if ct:
for d in [(d["title"]["simpleText"], d["videoId"]) for d in flat("playlistVideoRenderer", ct)]: for d in [(d.get("title", {}).get("simpleText", ""),
d.get("videoId", "")) for d in flat("playlistVideoRenderer", ct)]:
self._playlist.append(d) self._playlist.append(d)
self._is_script = False self._is_script = False
@@ -144,13 +188,152 @@ class PlayListParser(HTMLParser):
""" """
request = Request("https://www.youtube.com/playlist?list={}&hl=en".format(play_list_id), headers=_HEADERS) request = Request("https://www.youtube.com/playlist?list={}&hl=en".format(play_list_id), headers=_HEADERS)
with urllib.request.urlopen(request, timeout=2) as resp: with urlopen(request, timeout=2) as resp:
data = gzip.decompress(resp.read()).decode("utf-8") data = gzip.decompress(resp.read()).decode("utf-8")
parser = PlayListParser() parser = PlayListParser()
parser.feed(data) parser.feed(data)
return parser.header, parser.playlist return parser.header, parser.playlist
class YouTubeDL:
""" Utility class [experimental] for working with youtube-dl.
[https://github.com/ytdl-org/youtube-dl]
"""
_DL_INSTANCE = None
_DownloadError = None
_LATEST_RELEASE_URL = "https://api.github.com/repos/ytdl-org/youtube-dl/releases/latest"
_OPTIONS = {"noplaylist": True, # Single video instead of a playlist [ignoring playlist in URL].
"quiet": True, # Do not print messages to stdout.
"simulate": True, # Do not download the video files.
"cookiefile": "cookies.txt"} # File name where cookies should be read from and dumped to.
def __init__(self, settings, callback):
self._path = settings.default_data_path + "tools/"
self._update = settings.enable_yt_dl_update
self._supported = {"22", "18"}
self._dl = None
self._callback = callback
self._download_exception = None
self._is_update_process = False
self.init()
@classmethod
def get_instance(cls, settings, callback=print):
if not cls._DL_INSTANCE:
cls._DL_INSTANCE = YouTubeDL(settings, callback)
return cls._DL_INSTANCE
def init(self):
if not os.path.isfile(self._path + "youtube_dl/version.py"):
self.get_latest_release()
if self._path not in sys.path:
sys.path.append(self._path)
self.init_dl()
def init_dl(self):
try:
import youtube_dl
except ModuleNotFoundError as e:
log("YouTubeDLHelper error: {}".format(str(e)))
raise YouTubeException(e)
except ImportError as e:
log("YouTubeDLHelper error: {}".format(str(e)))
else:
if self._update:
if hasattr(youtube_dl.version, "__version__"):
l_ver = self.get_last_release_id()
cur_ver = youtube_dl.version.__version__
if l_ver and youtube_dl.version.__version__ < l_ver:
msg = "youtube-dl has new release!\nCurrent: {}. Last: {}.".format(cur_ver, l_ver)
show_notification(msg)
log(msg)
self._callback(msg, False)
self.get_latest_release()
self._DownloadError = youtube_dl.utils.DownloadError
self._dl = youtube_dl.YoutubeDL(self._OPTIONS)
msg = "youtube-dl initialized..."
show_notification(msg)
log(msg)
@staticmethod
def get_last_release_id():
""" Getting last release id. """
url = "https://api.github.com/repos/ytdl-org/youtube-dl/releases/latest"
try:
with urlopen(url, timeout=10) as resp:
return json.loads(resp.read().decode("utf-8")).get("tag_name", "0")
except URLError as e:
log("YouTubeDLHelper error [get last release id]: {}".format(e))
def get_latest_release(self):
try:
self._is_update_process = True
log("Getting the last youtube-dl release...")
with urlopen(YouTubeDL._LATEST_RELEASE_URL, timeout=10) as resp:
r = json.loads(resp.read().decode("utf-8"))
zip_url = r.get("zipball_url", None)
if zip_url:
zip_file = self._path + "yt.zip"
os.makedirs(os.path.dirname(self._path), exist_ok=True)
f_name, headers = urlretrieve(zip_url, filename=zip_file)
import zipfile
with zipfile.ZipFile(f_name) as arch:
if os.path.isdir(self._path):
shutil.rmtree(self._path)
else:
os.makedirs(os.path.dirname(self._path), exist_ok=True)
for info in arch.infolist():
pref, sep, f = info.filename.partition("/youtube_dl/")
if sep:
arch.extract(info.filename)
shutil.move(info.filename, "{}{}{}".format(self._path, sep, f))
shutil.rmtree(pref)
msg = "Getting the last youtube-dl release is done!"
show_notification(msg)
log(msg)
self._callback(msg, False)
return True
except URLError as e:
log("YouTubeDLHelper error: {}".format(e))
raise YouTubeException(e)
finally:
self._is_update_process = False
def get_yt_link(self, url, skip_errors=False):
""" Returns tuple from the video links [dict] and title. """
if self._is_update_process:
self._callback("Update process. Please wait.", False)
return {}, ""
try:
info = self._dl.extract_info(url, download=False)
except URLError as e:
log(str(e))
raise YouTubeException(e)
except self._DownloadError as e:
log(str(e))
if not skip_errors:
raise YouTubeException(e)
else:
fmts = info.get("formats", None)
if fmts:
return {Quality.get(int(fm["format_id"])): fm.get("url", "") for fm in fmts if
fm.get("format_id", "") in self._supported}, info.get("title", "")
return {}, info.get("title", "")
def flat(key, d): def flat(key, d):
for k, v in d.items(): for k, v in d.items():
if k == key: if k == key:

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

@@ -0,0 +1,143 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<menu id="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="menu_bar">
<submenu>
<attribute name="label" translatable="yes">File</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">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">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>
</submenu>
<submenu>
<attribute name="label" translatable="yes">Edit</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>
<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>
<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>
<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>
</menu>
</interface>

View File

@@ -10,7 +10,7 @@ from app.commons import run_idle
from app.settings import SettingsType from app.settings import SettingsType
from app.ui.dialogs import show_dialog, DialogType from app.ui.dialogs import show_dialog, DialogType
from app.ui.main_helper import append_text_to_tview from app.ui.main_helper import append_text_to_tview
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK
class RestoreType(Enum): class RestoreType(Enum):
@@ -59,14 +59,13 @@ class BackupDialog:
def show(self): def show(self):
self._dialog_window.show() self._dialog_window.show()
@run_idle
def init_data(self): def init_data(self):
try: if os.path.isdir(self._backup_path):
files = os.listdir(self._backup_path) for file in filter(lambda x: x.endswith(".zip"), os.listdir(self._backup_path)):
except FileNotFoundError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
for file in filter(lambda x: x.endswith(".zip"), files):
self._model.append((file.rstrip(".zip"), False)) self._model.append((file.rstrip(".zip"), False))
else:
os.makedirs(os.path.dirname(self._backup_path), exist_ok=True)
def on_restore_bouquets(self, item): def on_restore_bouquets(self, item):
self.restore(RestoreType.BOUQUETS) self.restore(RestoreType.BOUQUETS)
@@ -129,6 +128,8 @@ class BackupDialog:
append_text_to_tview(name + "\n", self._text_view) append_text_to_tview(name + "\n", self._text_view)
except FileNotFoundError as e: except FileNotFoundError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR) self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
self._text_view.get_buffer().set_text("")
def restore(self, restore_type): def restore(self, restore_type):
model, paths = self._main_view.get_selection().get_selected_rows() model, paths = self._main_view.get_selection().get_selected_rows()
@@ -175,7 +176,7 @@ class BackupDialog:
if not KeyboardKey.value_exist(key_code): if not KeyboardKey.value_exist(key_code):
return return
key = KeyboardKey(key_code) key = KeyboardKey(key_code)
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK ctrl = event.state & MOD_MASK
if key is KeyboardKey.DELETE: if key is KeyboardKey.DELETE:
self.on_remove(view) self.on_remove(view)

View File

@@ -3,7 +3,7 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2018 Dmitriy Yefremov Copyright (c) 2018-2020 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -31,8 +31,14 @@ Author: Dmitriy Yefremov
<!-- interface-license-type mit --> <!-- interface-license-type mit -->
<!-- interface-name DemonEditor --> <!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. --> <!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
<!-- interface-copyright 2018 Dmitriy Yefremov --> <!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov --> <!-- interface-authors Dmitriy Yefremov -->
<object class="GtkImage" id="details_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">emblem-important-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkListStore" id="main_list_store"> <object class="GtkListStore" id="main_list_store">
<columns> <columns>
<!-- column-name date --> <!-- column-name date -->
@@ -41,131 +47,89 @@ Author: Dmitriy Yefremov
<column type="gboolean"/> <column type="gboolean"/>
</columns> </columns>
</object> </object>
<object class="GtkMenu" id="popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="restore_bouquets_popup_menu_item">
<property name="label" translatable="yes">Restore bouquets</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_restore_bouquets" swapped="no"/>
<accelerator key="r" signal="activate" modifiers="Primary"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="restore_all_popup_menu_item">
<property name="label" translatable="yes">Restore all</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_restore_all" swapped="no"/>
<accelerator key="e" signal="activate" modifiers="Primary"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="popup_menu_separator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="remove_popup_menu_item">
<property name="label">gtk-remove</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<signal name="activate" handler="on_remove" swapped="no"/>
<accelerator key="Delete" signal="activate"/>
</object>
</child>
</object>
<object class="GtkImage" id="remove_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">user-trash-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="restore_all_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-select-all-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="restore_bouquets_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-revert-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkWindow" id="dialog_window"> <object class="GtkWindow" id="dialog_window">
<property name="width_request">560</property> <property name="width_request">560</property>
<property name="height_request">320</property> <property name="height_request">320</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="title" translatable="yes">Backups</property>
<property name="modal">True</property> <property name="modal">True</property>
<property name="window_position">center-on-parent</property> <property name="window_position">center-on-parent</property>
<property name="destroy_with_parent">True</property> <property name="destroy_with_parent">True</property>
<property name="icon_name">document-revert</property> <property name="icon_name">document-revert</property>
<property name="gravity">center</property> <property name="gravity">center</property>
<signal name="check-resize" handler="on_resize" swapped="no"/> <signal name="check-resize" handler="on_resize" swapped="no"/>
<child type="titlebar"> <child>
<object class="GtkHeaderBar" id="header_bar"> <placeholder/>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Backups</property>
<property name="spacing">2</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkBox" id="header_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkButton" id="restore_bouquets_header_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Restore bouquets</property>
<signal name="clicked" handler="on_restore_bouquets" swapped="no"/>
<child>
<object class="GtkImage" id="restore_bouquets_header_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-revert</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="restore_all_header_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Restore all</property>
<signal name="clicked" handler="on_restore_all" swapped="no"/>
<child>
<object class="GtkImage" id="restore_all_header_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-select-all</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="remove_header_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Remove</property>
<signal name="clicked" handler="on_remove" swapped="no"/>
<child>
<object class="GtkImage" id="remove_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">user-trash</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkCheckButton" id="info_check_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">False</property>
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
<child>
<object class="GtkImage" id="info_check_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-dialog-info</property>
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child> </child>
<child> <child>
<object class="GtkBox" id="main_box"> <object class="GtkBox" id="main_box">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</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="orientation">vertical</property>
<property name="spacing">5</property>
<child> <child>
<object class="GtkPaned" id="main_paned"> <object class="GtkPaned" id="main_paned">
<property name="visible">True</property> <property name="visible">True</property>
@@ -249,6 +213,106 @@ Author: Dmitriy Yefremov
<property name="position">0</property> <property name="position">0</property>
</packing> </packing>
</child> </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">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="layout_style">expand</property>
<child>
<object class="GtkButton" id="restore_bouquets_header_button">
<property name="label" translatable="yes">Restore bouquets</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
<property name="image">restore_bouquets_image</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_restore_bouquets" 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="restore_all_header_button">
<property name="label" translatable="yes">Restore all</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
<property name="image">restore_all_image</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_restore_all" 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="remove_header_button">
<property name="label" translatable="yes">Remove</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="valign">center</property>
<property name="image">remove_image</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_remove" swapped="no"/>
<accelerator key="Delete" signal="clicked"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">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="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" modifiers="Primary"/>
</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> <child>
<object class="GtkInfoBar" id="info_bar"> <object class="GtkInfoBar" id="info_bar">
<property name="can_focus">False</property> <property name="can_focus">False</property>
@@ -299,57 +363,4 @@ Author: Dmitriy Yefremov
</object> </object>
</child> </child>
</object> </object>
<object class="GtkImage" id="restore_popup_menu_item_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-revert-to-saved</property>
</object>
<object class="GtkImage" id="restore_popup_menu_item_image2">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-select-all</property>
</object>
<object class="GtkMenu" id="popup_menu">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImageMenuItem" id="restore_bouquets_popup_menu_item">
<property name="label" translatable="yes">Restore bouquets</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">restore_popup_menu_item_image</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_restore_bouquets" swapped="no"/>
<accelerator key="r" signal="activate" modifiers="GDK_CONTROL_MASK"/>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="restore_all_popup_menu_item">
<property name="label" translatable="yes">Restore all</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="image">restore_popup_menu_item_image2</property>
<property name="use_stock">False</property>
<signal name="activate" handler="on_restore_all" swapped="no"/>
<accelerator key="e" signal="activate" modifiers="GDK_CONTROL_MASK"/>
</object>
</child>
<child>
<object class="GtkSeparatorMenuItem" id="popup_menu_separator">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
</child>
<child>
<object class="GtkImageMenuItem" id="remove_popup_menu_item">
<property name="label">gtk-remove</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<signal name="activate" handler="on_remove" swapped="no"/>
<accelerator key="Delete" signal="activate"/>
</object>
</child>
</object>
</interface> </interface>

40
app/ui/default_style.css Normal file
View File

@@ -0,0 +1,40 @@
* {
-GtkDialog-action-area-border: 5em;
}
entry {
min-height: 2em;
}
button {
min-height: 1.5em;
padding: 0.1em;
}
spinbutton {
min-height: 1.5em;
}
toolbutton {
padding: 0.1em;
}
spinner {
padding-left: 1em;
padding-right: 1em;
}
infobar {
min-height: 2em;
}
switch slider {
min-height: 1.5em;
min-width: 1.5em;
}
paned > separator {
background-repeat: no-repeat;
background-position: center;
background-size: 1px 24px;
}

View File

@@ -3,7 +3,7 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2018 Dmitriy Yefremov Copyright (c) 2018-2020 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -26,12 +26,12 @@ THE SOFTWARE.
Author: Dmitriy Yefremov Author: Dmitriy Yefremov
--> -->
<interface> <interface domain="demon-editor">
<requires lib="gtk+" version="3.16"/> <requires lib="gtk+" version="3.16"/>
<!-- interface-license-type mit --> <!-- interface-license-type mit -->
<!-- interface-name DemonEditor --> <!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. --> <!-- interface-description Enigma2 channel and satellites list editor for macOS. -->
<!-- interface-copyright 2018 Dmitriy Yefremov --> <!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov --> <!-- interface-authors Dmitriy Yefremov -->
<object class="GtkAboutDialog" id="about_dialog"> <object class="GtkAboutDialog" id="about_dialog">
<property name="can_focus">False</property> <property name="can_focus">False</property>
@@ -40,11 +40,12 @@ Author: Dmitriy Yefremov
<property name="icon_name">system-help</property> <property name="icon_name">system-help</property>
<property name="type_hint">normal</property> <property name="type_hint">normal</property>
<property name="program_name">DemonEditor</property> <property name="program_name">DemonEditor</property>
<property name="version">0.4.7 Pre-alpha</property> <property name="version">1.0.0 Alpha-2</property>
<property name="copyright">2018-2020 Dmitriy Yefremov <property name="copyright">2018-2020 Dmitriy Yefremov
</property> </property>
<property name="comments" translatable="yes">Enigma2 channel and satellites list editor for GNU/Linux</property> <property name="comments" translatable="yes">Enigma2 channel and satellites list editor for MacOS.
<property name="website">https://dyefremov.github.io/DemonEditor/</property> (Experimental)</property>
<property name="website">https://github.com/DYefremov/DemonEditor/tree/experimental-mac</property>
<property name="license" translatable="yes">Это приложение распространяется без каких-либо гарантий. <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 name="authors">Dmitriy Yefremov
@@ -65,7 +66,9 @@ Author: Dmitriy Yefremov
<child internal-child="action_area"> <child internal-child="action_area">
<object class="GtkButtonBox" id="aboutdialog_action_area"> <object class="GtkButtonBox" id="aboutdialog_action_area">
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="layout_style">end</property> <property name="margin_left">50</property>
<property name="margin_right">50</property>
<property name="layout_style">expand</property>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@@ -90,31 +93,58 @@ Author: Dmitriy Yefremov
<property name="skip_taskbar_hint">True</property> <property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property> <property name="skip_pager_hint">True</property>
<property name="gravity">center</property> <property name="gravity">center</property>
<child type="action"> <child type="titlebar">
<object class="GtkButton" id="input_dialog_cancel_button"> <placeholder/>
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
</object>
</child>
<child type="action">
<object class="GtkButton" id="input_dialog_ok_button">
<property name="label">gtk-ok</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
</object>
</child> </child>
<child internal-child="vbox"> <child internal-child="vbox">
<object class="GtkBox"> <object class="GtkBox">
<property name="can_focus">False</property> <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="orientation">vertical</property>
<property name="spacing">2</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="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>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child> <child>
<object class="GtkEntry" id="input_entry"> <object class="GtkEntry" id="input_entry">
<property name="visible">True</property> <property name="visible">True</property>
@@ -123,7 +153,7 @@ Author: Dmitriy Yefremov
<property name="margin_right">2</property> <property name="margin_right">2</property>
<property name="margin_top">2</property> <property name="margin_top">2</property>
<property name="margin_bottom">2</property> <property name="margin_bottom">2</property>
<property name="primary_icon_stock">gtk-edit</property> <property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_activatable">False</property> <property name="primary_icon_activatable">False</property>
<property name="secondary_icon_activatable">False</property> <property name="secondary_icon_activatable">False</property>
<property name="secondary_icon_sensitive">False</property> <property name="secondary_icon_sensitive">False</property>
@@ -131,7 +161,7 @@ Author: Dmitriy Yefremov
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">1</property> <property name="position">0</property>
</packing> </packing>
</child> </child>
</object> </object>
@@ -141,7 +171,7 @@ Author: Dmitriy Yefremov
<action-widget response="ok">input_dialog_ok_button</action-widget> <action-widget response="ok">input_dialog_ok_button</action-widget>
</action-widgets> </action-widgets>
</object> </object>
<object class="GtkDialog" id="wait_dialog"> <object class="GtkWindow" id="wait_dialog">
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="resizable">False</property> <property name="resizable">False</property>
<property name="modal">True</property> <property name="modal">True</property>
@@ -151,68 +181,50 @@ Author: Dmitriy Yefremov
<property name="skip_taskbar_hint">True</property> <property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property> <property name="skip_pager_hint">True</property>
<property name="decorated">False</property> <property name="decorated">False</property>
<child> <property name="gravity">center</property>
<child type="titlebar">
<placeholder/> <placeholder/>
</child> </child>
<child internal-child="vbox"> <child>
<object class="GtkBox" id="wait_dialog_vbox"> <object class="GtkBox" id="wait_dialog_box">
<property name="width_request">120</property> <property name="width_request">100</property>
<property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<child internal-child="action_area"> <child>
<object class="GtkButtonBox" id="dialog-action_area4"> <object class="GtkSpinner" id="spinner">
<property name="width_request">150</property>
<property name="height_request">45</property>
<property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="opacity">0</property> <property name="active">True</property>
<property name="layout_style">end</property>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">False</property> <property name="fill">True</property>
<property name="position">0</property> <property name="position">0</property>
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkBox" id="box4"> <object class="GtkLabel" id="wait_dialog_label">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="orientation">vertical</property> <property name="margin_left">10</property>
<child> <property name="margin_right">10</property>
<object class="GtkSpinner" id="spinner"> <property name="label" translatable="yes">Loading data...</property>
<property name="width_request">150</property>
<property name="height_request">45</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="active">True</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="wait_dialog_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Loading data...</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">1</property> <property name="position">1</property>
</packing> </packing>
</child> </child>
<style>
<class name="primary-toolbar"/>
</style>
</object> </object>
</child> </child>
<style>
<class name="app-notification"/>
</style>
</object> </object>
</interface> </interface>

View File

@@ -1,5 +1,5 @@
""" Common module for showing dialogs """ """ Common module for showing dialogs """
import locale import gettext
from enum import Enum from enum import Enum
from functools import lru_cache from functools import lru_cache
@@ -52,12 +52,17 @@ class WaitDialog:
builder, dialog = get_dialog_from_xml(DialogType.WAIT, transient) builder, dialog = get_dialog_from_xml(DialogType.WAIT, transient)
self._dialog = dialog self._dialog = dialog
self._dialog.set_transient_for(transient) self._dialog.set_transient_for(transient)
if text is not None: self._label = builder.get_object("wait_dialog_label")
builder.get_object("wait_dialog_label").set_text(text) self._default_text = text or self._label.get_text()
def show(self): def show(self, text=None):
self.set_text(text)
self._dialog.show() self._dialog.show()
@run_idle
def set_text(self, text):
self._label.set_text(get_message(text or self._default_text))
@run_idle @run_idle
def hide(self): def hide(self):
self._dialog.hide() self._dialog.hide()
@@ -76,15 +81,17 @@ def show_dialog(dialog_type: DialogType, transient, text=None, settings=None, ac
elif dialog_type is DialogType.INPUT: elif dialog_type is DialogType.INPUT:
return get_input_dialog(transient, text) return get_input_dialog(transient, text)
elif dialog_type is DialogType.QUESTION: elif dialog_type is DialogType.QUESTION:
return get_message_dialog(transient, DialogType.QUESTION, Gtk.ButtonsType.OK_CANCEL, text or "Are you sure?") action = action_type if action_type else Gtk.ButtonsType.OK_CANCEL
return get_message_dialog(transient, DialogType.QUESTION, action, text or "Are you sure?")
elif dialog_type is DialogType.ABOUT: elif dialog_type is DialogType.ABOUT:
return get_about_dialog(transient) return get_about_dialog(transient)
def get_chooser_dialog(transient, settings, pattern, name): def get_chooser_dialog(transient, settings, name, patterns):
file_filter = Gtk.FileFilter() file_filter = Gtk.FileFilter()
file_filter.add_pattern(pattern)
file_filter.set_name(name) file_filter.set_name(name)
for p in patterns:
file_filter.add_pattern(p)
return show_dialog(dialog_type=DialogType.CHOOSER, return show_dialog(dialog_type=DialogType.CHOOSER,
transient=transient, transient=transient,
@@ -94,17 +101,21 @@ def get_chooser_dialog(transient, settings, pattern, name):
def get_file_chooser_dialog(transient, text, settings, action_type, file_filter): def get_file_chooser_dialog(transient, text, settings, action_type, file_filter):
dialog = Gtk.FileChooserDialog(get_message(text) if text else "", transient, dialog = Gtk.FileChooserNative()
action_type if action_type is not None else Gtk.FileChooserAction.SELECT_FOLDER, dialog.set_title(get_message(text) if text else "")
(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK), dialog.set_transient_for(transient)
use_header_bar=IS_GNOME_SESSION) dialog.set_action(action_type if action_type is not None else Gtk.FileChooserAction.SELECT_FOLDER)
dialog.set_create_folders(False)
dialog.set_modal(True)
if file_filter is not None: if file_filter is not None:
dialog.add_filter(file_filter) dialog.add_filter(file_filter)
path = settings.data_local_path path = settings.data_local_path
dialog.set_current_folder(path) dialog.set_current_folder(path)
response = dialog.run() response = dialog.run()
if response == Gtk.ResponseType.OK:
if response == Gtk.ResponseType.ACCEPT:
if dialog.get_filename(): if dialog.get_filename():
path = dialog.get_filename() path = dialog.get_filename()
if action_type is not Gtk.FileChooserAction.OPEN: if action_type is not Gtk.FileChooserAction.OPEN:
@@ -163,7 +174,7 @@ def get_dialog_from_xml(dialog_type, transient, use_header=0, title=""):
def get_message(message): def get_message(message):
""" returns translated message """ """ returns translated message """
return locale.dgettext(TEXT_DOMAIN, message) return gettext.dgettext(TEXT_DOMAIN, message)
@lru_cache(maxsize=5) @lru_cache(maxsize=5)

501
app/ui/download_dialog.glade Normal file → Executable file
View File

@@ -3,7 +3,7 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2018 Dmitriy Yefremov Copyright (c) 2018-2020 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -30,12 +30,25 @@ Author: Dmitriy Yefremov
<requires lib="gtk+" version="3.16"/> <requires lib="gtk+" version="3.16"/>
<!-- interface-license-type mit --> <!-- interface-license-type mit -->
<!-- interface-name DemonEditor --> <!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. --> <!-- interface-description Enigma2 channel and satellites list editor for macOS. -->
<!-- interface-copyright 2018 Dmitriy Yefremov --> <!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
<!-- interface-authors 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"> <object class="GtkWindow" id="download_dialog_window">
<property name="width_request">500</property> <property name="width_request">500</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="title" translatable="yes">FTP-transfer</property>
<property name="resizable">False</property> <property name="resizable">False</property>
<property name="modal">True</property> <property name="modal">True</property>
<property name="window_position">center-on-parent</property> <property name="window_position">center-on-parent</property>
@@ -43,245 +56,30 @@ Author: Dmitriy Yefremov
<property name="skip_taskbar_hint">True</property> <property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property> <property name="skip_pager_hint">True</property>
<property name="gravity">center</property> <property name="gravity">center</property>
<child type="titlebar"> <child>
<object class="GtkHeaderBar" id="header_bar"> <placeholder/>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">FTP-transfer</property>
<property name="spacing">5</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkBox" id="header_left_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkButton" id="receive_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Receive</property>
<signal name="clicked" handler="on_receive" swapped="no"/>
<child>
<object class="GtkImage" id="receive_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-goto-bottom</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="send_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Send</property>
<signal name="clicked" handler="on_send" swapped="no"/>
<child>
<object class="GtkImage" id="send_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-goto-top</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="options_button">
<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"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-properties</property>
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child> </child>
<child> <child>
<object class="GtkBox" id="main_dialog_box"> <object class="GtkBox" id="main_dialog_box">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_left">1</property>
<property name="margin_right">1</property>
<property name="margin_bottom">1</property>
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<property name="spacing">2</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>
<property name="has_entry">True</property>
<signal name="changed" handler="on_profile_changed" swapped="no"/>
<child internal-child="entry">
<object class="GtkEntry">
<property name="can_focus">True</property>
<property name="has_tooltip">True</property>
<property name="editable">False</property>
<property name="has_frame">False</property>
<property name="max_width_chars">9</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">1</property>
</packing>
</child>
<child> <child>
<object class="GtkFrame" id="main_settings_box_frame"> <object class="GtkFrame" id="main_settings_box_frame">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_left">5</property> <property name="margin_left">10</property>
<property name="margin_right">5</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="label_xalign">0.019999999552965164</property>
<property name="shadow_type">in</property> <property name="shadow_type">in</property>
<child> <child>
<object class="GtkBox" id="main_settings_box"> <object class="GtkBox" id="main_settings_box">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_left">5</property> <property name="margin_left">10</property>
<property name="margin_right">5</property> <property name="margin_right">10</property>
<property name="margin_top">5</property> <property name="margin_top">5</property>
<property name="margin_bottom">5</property> <property name="margin_bottom">5</property>
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
@@ -367,6 +165,7 @@ Author: Dmitriy Yefremov
<property name="receives_default">False</property> <property name="receives_default">False</property>
<property name="active">True</property> <property name="active">True</property>
<property name="draw_indicator">True</property> <property name="draw_indicator">True</property>
<signal name="toggled" handler="on_remove_unused_bouquets_toggled" swapped="no"/>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@@ -397,6 +196,7 @@ Author: Dmitriy Yefremov
<property name="can_focus">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="tooltip_text" translatable="yes">Use http to reload data in the receiver.</property>
<property name="active">True</property> <property name="active">True</property>
<signal name="state-set" handler="on_use_http_state_set" swapped="no"/>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@@ -435,8 +235,8 @@ Author: Dmitriy Yefremov
<object class="GtkFrame" id="settings_frame"> <object class="GtkFrame" id="settings_frame">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_left">5</property> <property name="margin_left">10</property>
<property name="margin_right">5</property> <property name="margin_right">10</property>
<property name="margin_top">5</property> <property name="margin_top">5</property>
<property name="margin_bottom">5</property> <property name="margin_bottom">5</property>
<property name="label_xalign">0.019999999552965164</property> <property name="label_xalign">0.019999999552965164</property>
@@ -445,8 +245,9 @@ Author: Dmitriy Yefremov
<object class="GtkGrid" id="settings_grid"> <object class="GtkGrid" id="settings_grid">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_left">5</property> <property name="margin_left">10</property>
<property name="margin_right">5</property> <property name="margin_right">10</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property> <property name="margin_bottom">5</property>
<property name="row_spacing">2</property> <property name="row_spacing">2</property>
<property name="column_spacing">2</property> <property name="column_spacing">2</property>
@@ -614,6 +415,9 @@ Author: Dmitriy Yefremov
<property name="position">2</property> <property name="position">2</property>
</packing> </packing>
</child> </child>
<style>
<class name="group"/>
</style>
</object> </object>
</child> </child>
</object> </object>
@@ -623,6 +427,229 @@ Author: Dmitriy Yefremov
<property name="position">3</property> <property name="position">3</property>
</packing> </packing>
</child> </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>
<property name="valign">center</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> <child>
<object class="GtkExpander" id="expander"> <object class="GtkExpander" id="expander">
<property name="visible">True</property> <property name="visible">True</property>
@@ -659,7 +686,7 @@ Author: Dmitriy Yefremov
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">4</property> <property name="position">7</property>
</packing> </packing>
</child> </child>
<child> <child>
@@ -715,13 +742,17 @@ Author: Dmitriy Yefremov
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">5</property> <property name="position">8</property>
</packing> </packing>
</child> </child>
<child>
<placeholder/>
</child>
</object> </object>
</child> </child>
</object> </object>
<object class="GtkSizeGroup" id="settings_size_group">
<widgets>
<widget name="ftp_radio_button"/>
<widget name="http_radio_button"/>
<widget name="telnet_radio_button"/>
</widgets>
</object>
</interface> </interface>

View File

@@ -2,7 +2,7 @@ import os
from gi.repository import GLib from gi.repository import GLib
from app.commons import run_idle, run_task from app.commons import run_idle, run_task, log
from app.connections import download_data, DownloadType, upload_data from app.connections import download_data, DownloadType, upload_data
from app.settings import SettingsType from app.settings import SettingsType
from app.ui.backup import backup_data, restore_data from app.ui.backup import backup_data, restore_data
@@ -24,6 +24,8 @@ class DownloadDialog:
"on_settings_button": self.on_settings_button, "on_settings_button": self.on_settings_button,
"on_settings": self.on_settings, "on_settings": self.on_settings,
"on_profile_changed": self.on_profile_changed, "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} "on_info_bar_close": self.on_info_bar_close}
builder = Gtk.Builder() builder = Gtk.Builder()
@@ -64,7 +66,6 @@ class DownloadDialog:
self.update_profiles() self.update_profiles()
self.init_ui_settings() self.init_ui_settings()
@run_idle
def init_ui_settings(self): def init_ui_settings(self):
self._host_entry.set_text(self._settings.host) self._host_entry.set_text(self._settings.host)
self._data_path_entry.set_text(self._settings.data_local_path) self._data_path_entry.set_text(self._settings.data_local_path)
@@ -72,7 +73,8 @@ class DownloadDialog:
self._webtv_radio_button.set_visible(not is_enigma) self._webtv_radio_button.set_visible(not is_enigma)
self._http_radio_button.set_visible(is_enigma) self._http_radio_button.set_visible(is_enigma)
self._use_http_box.set_visible(is_enigma) self._use_http_box.set_visible(is_enigma)
self._use_http_switch.set_active(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): def update_profiles(self):
self._profile_combo_box.remove_all() self._profile_combo_box.remove_all()
@@ -143,13 +145,19 @@ class DownloadDialog:
self._s_type = self._settings.setting_type self._s_type = self._settings.setting_type
self.init_ui_settings() 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): def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False) self._info_bar.set_visible(False)
@run_task @run_task
def download(self, download, d_type): def download(self, download, d_type):
""" Download/upload data from/to receiver """ """ Download/upload data from/to receiver """
self._expander.set_expanded(True) GLib.idle_add(self._expander.set_expanded, True)
self.clear_output() self.clear_output()
backup, backup_src, data_path = self._settings.backup_before_downloading, None, None backup, backup_src, data_path = self._settings.backup_before_downloading, None, None
@@ -171,8 +179,9 @@ class DownloadDialog:
done_callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO), done_callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO),
use_http=self._use_http_switch.get_active()) use_http=self._use_http_switch.get_active())
except Exception as e: except Exception as e:
message = str(getattr(e, "message", str(e))) msg = "Downloading data error: {}"
self.show_info_message(message, Gtk.MessageType.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)): if all((download, backup, data_path)):
restore_data(backup_src, data_path) restore_data(backup_src, data_path)
else: else:

View File

@@ -3,7 +3,7 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2018-2019 Dmitriy Yefremov Copyright (c) 2018-2020 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -31,8 +31,20 @@ Author: Dmitriy Yefremov
<!-- interface-license-type mit --> <!-- interface-license-type mit -->
<!-- interface-name DemonEditor --> <!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. --> <!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
<!-- interface-copyright 2018-2019 Dmitriy Yefremov --> <!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov --> <!-- interface-authors Dmitriy Yefremov -->
<object class="GtkImage" id="apply_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-save-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="auto_config_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-select-all-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkListStore" id="bouquet_list_store"> <object class="GtkListStore" id="bouquet_list_store">
<columns> <columns>
<!-- column-name num --> <!-- column-name num -->
@@ -75,10 +87,16 @@ Author: Dmitriy Yefremov
<property name="image">copy_image</property> <property name="image">copy_image</property>
<property name="use_stock">False</property> <property name="use_stock">False</property>
<signal name="activate" handler="on_copy_ref" swapped="no"/> <signal name="activate" handler="on_copy_ref" swapped="no"/>
<accelerator key="c" signal="activate" modifiers="GDK_CONTROL_MASK"/> <accelerator key="c" signal="activate" modifiers="Primary"/>
</object> </object>
</child> </child>
</object> </object>
<object class="GtkImage" id="filter_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">edit-find-replace-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="insert_link_image"> <object class="GtkImage" id="insert_link_image">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
@@ -100,7 +118,7 @@ Author: Dmitriy Yefremov
<property name="image">insert_link_image</property> <property name="image">insert_link_image</property>
<property name="use_stock">False</property> <property name="use_stock">False</property>
<signal name="activate" handler="on_assign_ref" swapped="no"/> <signal name="activate" handler="on_assign_ref" swapped="no"/>
<accelerator key="v" signal="activate" modifiers="GDK_CONTROL_MASK"/> <accelerator key="v" signal="activate" modifiers="Primary"/>
</object> </object>
</child> </child>
<child> <child>
@@ -130,6 +148,12 @@ Author: Dmitriy Yefremov
</object> </object>
</child> </child>
</object> </object>
<object class="GtkImage" id="save_to_xml_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">document-save-as-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkListStore" id="services_list_store"> <object class="GtkListStore" id="services_list_store">
<columns> <columns>
<!-- column-name service --> <!-- column-name service -->
@@ -312,7 +336,7 @@ Author: Dmitriy Yefremov
<object class="GtkEntry" id="url_to_xml_entry"> <object class="GtkEntry" id="url_to_xml_entry">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="primary_icon_stock">gtk-connect</property> <property name="primary_icon_name">network-transmit-receive-symbolic</property>
<property name="primary_icon_activatable">False</property> <property name="primary_icon_activatable">False</property>
</object> </object>
<packing> <packing>
@@ -412,8 +436,8 @@ Author: Dmitriy Yefremov
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="text" translatable="yes">/data/epg/</property> <property name="text" translatable="yes">/data/epg/</property>
<property name="primary_icon_stock">gtk-edit</property> <property name="primary_icon_name">document-edit-symbolic</property>
<property name="secondary_icon_stock">gtk-directory</property> <property name="secondary_icon_name">folder-open-symbolic</property>
<property name="primary_icon_activatable">False</property> <property name="primary_icon_activatable">False</property>
<property name="secondary_icon_tooltip_text" translatable="yes">Select</property> <property name="secondary_icon_tooltip_text" translatable="yes">Select</property>
<signal name="icon-press" handler="on_field_icon_press" swapped="no"/> <signal name="icon-press" handler="on_field_icon_press" swapped="no"/>
@@ -442,7 +466,7 @@ Author: Dmitriy Yefremov
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="text" translatable="yes">/etc/enigma2/</property> <property name="text" translatable="yes">/etc/enigma2/</property>
<property name="primary_icon_stock">gtk-edit</property> <property name="primary_icon_name">document-edit-symbolic</property>
<property name="primary_icon_activatable">False</property> <property name="primary_icon_activatable">False</property>
</object> </object>
<packing> <packing>
@@ -559,58 +583,104 @@ Author: Dmitriy Yefremov
</object> </object>
</child> </child>
</object> </object>
<object class="GtkImage" id="update_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">emblem-synchronizing-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkWindow" id="epg_dialog_window"> <object class="GtkWindow" id="epg_dialog_window">
<property name="width_request">480</property> <property name="width_request">480</property>
<property name="height_request">320</property> <property name="height_request">320</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="title" translatable="yes">EPG</property>
<property name="modal">True</property> <property name="modal">True</property>
<property name="window_position">center-on-parent</property> <property name="window_position">center-on-parent</property>
<property name="default_width">480</property> <property name="default_width">480</property>
<property name="default_height">240</property> <property name="default_height">320</property>
<property name="destroy_with_parent">True</property> <property name="destroy_with_parent">True</property>
<property name="icon_name">gtk-index</property> <property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property> <property name="skip_pager_hint">True</property>
<property name="gravity">center</property> <property name="gravity">center</property>
<signal name="check-resize" handler="on_resize" swapped="no"/> <signal name="check-resize" handler="on_resize" swapped="no"/>
<signal name="delete-event" handler="on_close_dialog" swapped="no"/> <signal name="delete-event" handler="on_close_dialog" swapped="no"/>
<child type="titlebar"> <child>
<object class="GtkHeaderBar" id="header_bar"> <placeholder/>
</child>
<child>
<object class="GtkBox" id="main_box">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="title" translatable="yes">EPG</property> <property name="margin_left">1</property>
<property name="subtitle" translatable="yes">List configuration</property> <property name="margin_right">1</property>
<property name="spacing">2</property> <property name="margin_bottom">1</property>
<property name="show_close_button">True</property> <property name="orientation">vertical</property>
<child> <child>
<object class="GtkBox" id="left_header_box"> <object class="GtkBox" id="main_actions_box">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="spacing">2</property> <property name="margin_left">15</property>
<property name="margin_right">15</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="spacing">10</property>
<child> <child>
<object class="GtkButton" id="apply_button"> <object class="GtkButtonBox" id="left_action_box">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Apply</property>
<signal name="clicked" handler="on_apply" swapped="no"/>
<child>
<object class="GtkImage" id="apply_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-apply</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="GtkSeparator">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="layout_style">expand</property>
<child>
<object class="GtkButton" id="apply_button">
<property name="label" translatable="yes">Apply</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Apply</property>
<property name="image">apply_image</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_apply" 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="update_button">
<property name="label" translatable="yes">Update</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Update</property>
<property name="image">update_image</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_update" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkToggleButton" id="filter_button">
<property name="label" translatable="yes">Filter</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Filter</property>
<property name="image">filter_image</property>
<property name="always_show_image">True</property>
<signal name="toggled" handler="on_filter_toggled" swapped="no"/>
<accelerator key="f" signal="clicked" modifiers="GDK_SHIFT_MASK | GDK_CONTROL_MASK"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@@ -618,20 +688,11 @@ Author: Dmitriy Yefremov
<property name="position">1</property> <property name="position">1</property>
</packing> </packing>
</child> </child>
<child> <child type="center">
<object class="GtkButton" id="update_button"> <object class="GtkLabel" id="list_label">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">False</property>
<property name="receives_default">True</property> <property name="label" translatable="yes">List configuration</property>
<property name="tooltip_text" translatable="yes">Update</property>
<signal name="clicked" handler="on_update" swapped="no"/>
<child>
<object class="GtkImage" id="update_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-refresh</property>
</object>
</child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@@ -640,110 +701,106 @@ Author: Dmitriy Yefremov
</packing> </packing>
</child> </child>
<child> <child>
<object class="GtkToggleButton" id="filter_button"> <object class="GtkButtonBox" id="right_action_box">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">False</property>
<property name="receives_default">True</property> <property name="layout_style">expand</property>
<property name="tooltip_text" translatable="yes">Filter</property>
<signal name="toggled" handler="on_filter_toggled" swapped="no"/>
<child> <child>
<object class="GtkImage" id="filter_button_image"> <object class="GtkButton" id="auto_config_button">
<property name="label" translatable="yes">Auto</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">True</property>
<property name="stock">gtk-spell-check</property> <property name="receives_default">True</property>
</object> <property name="tooltip_text" translatable="yes">Auto configuration by service names.</property>
</child> <property name="image">auto_config_image</property>
</object> <property name="always_show_image">True</property>
<packing> <signal name="clicked" handler="on_auto_configuration" swapped="no"/>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
</child>
<child>
<object class="GtkBox" id="right_header_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkButton" id="auto_config_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Auto configuration by service names.</property>
<signal name="clicked" handler="on_auto_configuration" swapped="no"/>
<child>
<object class="GtkImage" id="auto_config_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-find-and-replace</property>
</object> </object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="save_to_xml_button">
<property name="label" translatable="yes">Save</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 list to xml.</property>
<property name="image">save_to_xml_image</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_save_to_xml" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkMenuButton" id="options_menu_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="direction">none</property>
<property name="popover">options_popover</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">2</property>
<child>
<object class="GtkImage" id="options_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Options</property>
<property name="icon_name">applications-system-symbolic</property>
<property name="icon_size">1</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Options</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child> </child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">0</property> <property name="position">0</property>
</packing> </packing>
</child> </child>
<child>
<object class="GtkButton" id="save_to_xml_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Save list to xml.</property>
<signal name="clicked" handler="on_save_to_xml" swapped="no"/>
<child>
<object class="GtkImage" id="save_to_xml_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-save-as</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="options_menu_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="direction">none</property>
<property name="popover">options_popover</property>
<child>
<object class="GtkImage" id="options_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes">Options</property>
<property name="stock">gtk-properties</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="pack_type">end</property> <property name="expand">False</property>
<property name="position">1</property> <property name="fill">True</property>
<property name="position">0</property>
</packing> </packing>
</child> </child>
</object>
</child>
<child>
<object class="GtkBox" id="main_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child> <child>
<object class="GtkSearchBar" id="filter_bar"> <object class="GtkSearchBar" id="filter_bar">
<property name="visible">True</property> <property name="visible">True</property>
@@ -752,7 +809,7 @@ Author: Dmitriy Yefremov
<object class="GtkSearchEntry" id="filter_entry"> <object class="GtkSearchEntry" id="filter_entry">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="primary_icon_name">tools-check-spelling</property> <property name="primary_icon_name">edit-find-replace-symbolic</property>
<property name="primary_icon_activatable">False</property> <property name="primary_icon_activatable">False</property>
<property name="primary_icon_sensitive">False</property> <property name="primary_icon_sensitive">False</property>
<signal name="search-changed" handler="on_filter_changed" swapped="no"/> <signal name="search-changed" handler="on_filter_changed" swapped="no"/>
@@ -861,13 +918,14 @@ Author: Dmitriy Yefremov
<property name="height_request">24</property> <property name="height_request">24</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_left">2</property> <property name="margin_left">5</property>
<property name="spacing">2</property> <property name="spacing">2</property>
<child> <child>
<object class="GtkImage"> <object class="GtkImage">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="stock">gtk-properties</property> <property name="icon_name">document-properties-symbolic</property>
<property name="icon_size">1</property>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@@ -889,6 +947,7 @@ Author: Dmitriy Yefremov
</child> </child>
<child> <child>
<object class="GtkBox" id="source_info_box"> <object class="GtkBox" id="source_info_box">
<property name="height_request">26</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="halign">center</property> <property name="halign">center</property>
@@ -1142,16 +1201,17 @@ Author: Dmitriy Yefremov
</child> </child>
<child> <child>
<object class="GtkBox" id="bouquet_info_bar_box"> <object class="GtkBox" id="bouquet_info_bar_box">
<property name="height_request">24</property> <property name="height_request">26</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_left">2</property> <property name="margin_left">5</property>
<property name="spacing">2</property> <property name="spacing">2</property>
<child> <child>
<object class="GtkImage"> <object class="GtkImage">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="stock">gtk-properties</property> <property name="icon_name">document-properties-symbolic</property>
<property name="icon_size">1</property>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@@ -1188,7 +1248,9 @@ Author: Dmitriy Yefremov
<object class="GtkImage"> <object class="GtkImage">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_left">2</property>
<property name="stock">gtk-index</property> <property name="stock">gtk-index</property>
<property name="icon_size">1</property>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>

View File

@@ -15,7 +15,7 @@ from app.eparser.ecommons import BouquetService, BqServiceType
from app.tools.epg import EPG, ChannelsParser from app.tools.epg import EPG, ChannelsParser
from app.ui.dialogs import get_message, show_dialog, DialogType from app.ui.dialogs import get_message, show_dialog, DialogType
from .main_helper import on_popup_menu, update_entry_data from .main_helper import on_popup_menu, update_entry_data
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, Column, EPG_ICON, KeyboardKey from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, Column, EPG_ICON, KeyboardKey, MOD_MASK
class RefsSource(Enum): class RefsSource(Enum):
@@ -279,7 +279,7 @@ class EpgDialog:
if not KeyboardKey.value_exist(key_code): if not KeyboardKey.value_exist(key_code):
return return
key = KeyboardKey(key_code) key = KeyboardKey(key_code)
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK ctrl = event.state & MOD_MASK
if ctrl and key is KeyboardKey.C: if ctrl and key is KeyboardKey.C:
self.on_copy_ref() self.on_copy_ref()
@@ -486,7 +486,7 @@ class EpgDialog:
epg_dat_path = self._settings.data_local_path + "epg/" epg_dat_path = self._settings.data_local_path + "epg/"
self._epg_dat_path_entry.set_text(epg_dat_path) self._epg_dat_path_entry.set_text(epg_dat_path)
default_epg_data_stb_path = "/etc/enigma2" default_epg_data_stb_path = "/etc/enigma2"
epg_options = self._settings.get("epg_options") epg_options = self._settings.epg_options
if epg_options: if epg_options:
self._refs_source = RefsSource.XML if epg_options.get("xml_source", False) else RefsSource.SERVICES 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._xml_radiobutton.set_active(self._refs_source is RefsSource.XML)
@@ -506,15 +506,14 @@ class EpgDialog:
os.makedirs(os.path.dirname(self._epg_dat_path_entry.get_text()), exist_ok=True) os.makedirs(os.path.dirname(self._epg_dat_path_entry.get_text()), exist_ok=True)
def on_options_save(self, item=None): def on_options_save(self, item=None):
epg_options = {"xml_source": self._xml_radiobutton.get_active(), self._settings.epg_options = {"xml_source": self._xml_radiobutton.get_active(),
"use_web_source": self._use_web_source_switch.get_active(), "use_web_source": self._use_web_source_switch.get_active(),
"local_path_to_xml": self._xml_chooser_button.get_filename(), "local_path_to_xml": self._xml_chooser_button.get_filename(),
"url_to_xml": self._url_to_xml_entry.get_text(), "url_to_xml": self._url_to_xml_entry.get_text(),
"enable_filtering": self._enable_filtering_switch.get_active(), "enable_filtering": self._enable_filtering_switch.get_active(),
"epg_dat_path": self._epg_dat_path_entry.get_text(), "epg_dat_path": self._epg_dat_path_entry.get_text(),
"epg_dat_stb_path": self._epg_dat_stb_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()} "epg_data_update_on_start": self._update_on_start_switch.get_active()}
self._settings.add("epg_options", epg_options)
def on_resize(self, window): def on_resize(self, window):
if self._settings: if self._settings:

View File

@@ -3,7 +3,7 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2018-2019 Dmitriy Yefremov Copyright (c) 2018-2020 Dmitriy Yefremov
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -26,13 +26,25 @@ THE SOFTWARE.
Author: Dmitriy Yefremov Author: Dmitriy Yefremov
--> -->
<interface> <interface domain="demon-editor">
<requires lib="gtk+" version="3.16"/> <requires lib="gtk+" version="3.16"/>
<!-- interface-css-provider-path style.css -->
<!-- interface-license-type mit --> <!-- interface-license-type mit -->
<!-- interface-name DemonEditor --> <!-- interface-name DemonEditor -->
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. --> <!-- interface-description Enigma2 channel and satellites list editor for macOS. -->
<!-- interface-copyright 2018-2019 Dmitriy Yefremov --> <!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
<!-- interface-authors 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"> <object class="GtkListStore" id="main_list_store">
<columns> <columns>
<!-- column-name name --> <!-- column-name name -->
@@ -82,6 +94,7 @@ Author: Dmitriy Yefremov
</object> </object>
<object class="GtkWindow" id="dialog_window"> <object class="GtkWindow" id="dialog_window">
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="title" translatable="yes">Import</property>
<property name="modal">True</property> <property name="modal">True</property>
<property name="window_position">center-on-parent</property> <property name="window_position">center-on-parent</property>
<property name="default_width">480</property> <property name="default_width">480</property>
@@ -90,51 +103,8 @@ Author: Dmitriy Yefremov
<property name="type_hint">dialog</property> <property name="type_hint">dialog</property>
<property name="gravity">center</property> <property name="gravity">center</property>
<signal name="check-resize" handler="on_resize" swapped="no"/> <signal name="check-resize" handler="on_resize" swapped="no"/>
<child type="titlebar"> <child>
<object class="GtkHeaderBar" id="header_bar"> <placeholder/>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Import</property>
<property name="subtitle" translatable="yes">Bouquets and services</property>
<property name="spacing">2</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkButton" id="import_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Import</property>
<signal name="clicked" handler="on_import" swapped="no"/>
<child>
<object class="GtkImage" id="import_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-revert-to-saved</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkCheckButton" id="info_check_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="draw_indicator">False</property>
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
<child>
<object class="GtkImage" id="info_check_button_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-dialog-info</property>
</object>
</child>
</object>
<packing>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child> </child>
<child> <child>
<object class="GtkBox" id="main_box"> <object class="GtkBox" id="main_box">
@@ -159,8 +129,8 @@ Author: Dmitriy Yefremov
<object class="GtkLabel"> <object class="GtkLabel">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_top">2</property> <property name="margin_top">5</property>
<property name="margin_bottom">2</property> <property name="margin_bottom">5</property>
<property name="label" translatable="yes">Bouquets</property> <property name="label" translatable="yes">Bouquets</property>
</object> </object>
<packing> <packing>
@@ -258,8 +228,8 @@ Author: Dmitriy Yefremov
<object class="GtkLabel"> <object class="GtkLabel">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_top">2</property> <property name="margin_top">5</property>
<property name="margin_bottom">2</property> <property name="margin_bottom">5</property>
<property name="label" translatable="yes">Bouquet details</property> <property name="label" translatable="yes">Bouquet details</property>
</object> </object>
<packing> <packing>
@@ -335,6 +305,62 @@ Author: Dmitriy Yefremov
<property name="position">0</property> <property name="position">0</property>
</packing> </packing>
</child> </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> <child>
<object class="GtkInfoBar" id="info_bar"> <object class="GtkInfoBar" id="info_bar">
<property name="can_focus">False</property> <property name="can_focus">False</property>
@@ -380,7 +406,7 @@ Author: Dmitriy Yefremov
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
<property name="fill">True</property> <property name="fill">True</property>
<property name="position">1</property> <property name="position">2</property>
</packing> </packing>
</child> </child>
</object> </object>

View File

@@ -12,7 +12,7 @@ from app.ui.main_helper import on_popup_menu
from .uicommons import Gtk, UI_RESOURCES_PATH, KeyboardKey, Column from .uicommons import Gtk, UI_RESOURCES_PATH, KeyboardKey, Column
def import_bouquet(transient, model, path, settings, services, appender): def import_bouquet(transient, model, path, settings, services, appender, file_path=None):
""" Import of single bouquet """ """ Import of single bouquet """
itr = model.get_iter(path) itr = model.get_iter(path)
bq_type = BqType(model.get(itr, Column.BQ_TYPE)[0]) bq_type = BqType(model.get(itr, Column.BQ_TYPE)[0])
@@ -21,7 +21,7 @@ def import_bouquet(transient, model, path, settings, services, appender):
if profile is SettingsType.ENIGMA_2: if profile is SettingsType.ENIGMA_2:
pattern = ".{}".format(bq_type.value) pattern = ".{}".format(bq_type.value)
f_pattern = "userbouquet.*{}".format(pattern) f_pattern = "*" + pattern if settings.is_darwin else "userbouquet.*{}".format(pattern)
elif profile is SettingsType.NEUTRINO_MP: elif profile is SettingsType.NEUTRINO_MP:
pattern = "webtv.xml" if bq_type is BqType.WEBTV else "bouquets.xml" pattern = "webtv.xml" if bq_type is BqType.WEBTV else "bouquets.xml"
f_pattern = "bouquets.xml" f_pattern = "bouquets.xml"
@@ -30,15 +30,19 @@ def import_bouquet(transient, model, path, settings, services, appender):
elif bq_type is BqType.WEBTV: elif bq_type is BqType.WEBTV:
f_pattern = "webtv.xml" f_pattern = "webtv.xml"
file_path = get_chooser_dialog(transient, settings, f_pattern, "bouquet files") file_path = file_path or get_chooser_dialog(transient, settings, "bouquet files", (f_pattern,))
if file_path == Gtk.ResponseType.CANCEL: if file_path == Gtk.ResponseType.CANCEL:
return 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!") show_dialog(DialogType.ERROR, transient, text="No bouquet file is selected!")
return return
if profile is SettingsType.ENIGMA_2: if profile is SettingsType.ENIGMA_2:
if settings.is_darwin and file_path.rfind("userbouquet.") < 0:
show_dialog(DialogType.ERROR, transient, text="No bouquet file is selected!")
return
bq = get_enigma2_bouquet(file_path) bq = get_enigma2_bouquet(file_path)
imported = list(filter(lambda x: x.data in services or x.type is BqServiceType.IPTV, bq.services)) imported = list(filter(lambda x: x.data in services or x.type is BqServiceType.IPTV, bq.services))

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@ import concurrent.futures
import re import re
import urllib import urllib
from urllib.error import HTTPError from urllib.error import HTTPError
from urllib.parse import urlparse from urllib.parse import urlparse, unquote, quote
from urllib.request import Request, urlopen from urllib.request import Request, urlopen
from gi.repository import GLib from gi.repository import GLib
@@ -11,11 +11,11 @@ from app.commons import run_idle, run_task
from app.eparser.ecommons import BqServiceType, Service 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 from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT, StreamType, ENIGMA2_FAV_ID_FORMAT, get_fav_id, MARKER_FORMAT
from app.settings import SettingsType from app.settings import SettingsType
from app.tools.yt import YouTube, PlayListParser from app.tools.yt import PlayListParser, YouTubeException, YouTube
from .dialogs import Action, show_dialog, DialogType, get_dialogs_string, get_message from .dialogs import Action, show_dialog, DialogType, get_dialogs_string, get_message
from .main_helper import get_base_model, get_iptv_url, on_popup_menu from .main_helper import get_base_model, get_iptv_url, on_popup_menu
from .uicommons import Gtk, Gdk, TEXT_DOMAIN, UI_RESOURCES_PATH, IPTV_ICON, Column, IS_GNOME_SESSION, KeyboardKey, \ from .uicommons import (Gtk, Gdk, TEXT_DOMAIN, UI_RESOURCES_PATH, IPTV_ICON, Column, IS_GNOME_SESSION, KeyboardKey,
get_yt_icon get_yt_icon)
_DIGIT_ENTRY_NAME = "digit-entry" _DIGIT_ENTRY_NAME = "digit-entry"
_ENIGMA2_REFERENCE = "{}:0:{}:{:X}:{:X}:{:X}:{:X}:0:0:0" _ENIGMA2_REFERENCE = "{}:0:{}:{:X}:{:X}:{:X}:{:X}:0:0:0"
@@ -38,12 +38,14 @@ def get_stream_type(box):
return StreamType.NONE_TS.value return StreamType.NONE_TS.value
elif active == 2: elif active == 2:
return StreamType.NONE_REC_1.value return StreamType.NONE_REC_1.value
return StreamType.NONE_REC_2.value elif active == 3:
return StreamType.NONE_REC_2.value
return StreamType.E_SERVICE_URI.value
class IptvDialog: class IptvDialog:
def __init__(self, transient, view, services, bouquet, profile=SettingsType.ENIGMA_2, action=Action.ADD): def __init__(self, transient, view, services, bouquet, settings, action=Action.ADD):
handlers = {"on_response": self.on_response, handlers = {"on_response": self.on_response,
"on_entry_changed": self.on_entry_changed, "on_entry_changed": self.on_entry_changed,
"on_url_changed": self.on_url_changed, "on_url_changed": self.on_url_changed,
@@ -52,18 +54,20 @@ class IptvDialog:
"on_yt_quality_changed": self.on_yt_quality_changed, "on_yt_quality_changed": self.on_yt_quality_changed,
"on_info_bar_close": self.on_info_bar_close} "on_info_bar_close": self.on_info_bar_close}
self._action = action
self._s_type = settings.setting_type
self._settings = settings
self._bouquet = bouquet
self._services = services
self._yt_links = None
self._yt_dl = None
builder = Gtk.Builder() builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN) builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION), builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("iptv_dialog", "stream_type_liststore", "yt_quality_liststore")) ("iptv_dialog", "stream_type_liststore", "yt_quality_liststore"))
builder.connect_signals(handlers) builder.connect_signals(handlers)
self._action = action
self._profile = profile
self._bouquet = bouquet
self._services = services
self._yt_links = None
self._dialog = builder.get_object("iptv_dialog") self._dialog = builder.get_object("iptv_dialog")
self._dialog.set_transient_for(transient) self._dialog.set_transient_for(transient)
self._name_entry = builder.get_object("name_entry") self._name_entry = builder.get_object("name_entry")
@@ -91,7 +95,7 @@ class IptvDialog:
for el in self._digit_elems: for el in self._digit_elems:
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider, el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER) Gtk.STYLE_PROVIDER_PRIORITY_USER)
if profile is SettingsType.NEUTRINO_MP: if self._s_type is SettingsType.NEUTRINO_MP:
builder.get_object("iptv_dialog_ts_data_frame").set_visible(False) builder.get_object("iptv_dialog_ts_data_frame").set_visible(False)
builder.get_object("iptv_type_label").set_visible(False) builder.get_object("iptv_type_label").set_visible(False)
builder.get_object("reference_entry").set_visible(False) builder.get_object("reference_entry").set_visible(False)
@@ -104,8 +108,8 @@ class IptvDialog:
if self._action is Action.ADD: if self._action is Action.ADD:
self._save_button.set_visible(False) self._save_button.set_visible(False)
self._add_button.set_visible(True) self._add_button.set_visible(True)
if self._profile is SettingsType.ENIGMA_2: if self._s_type is SettingsType.ENIGMA_2:
self._update_reference_entry() self.update_reference_entry()
self._stream_type_combobox.set_active(1) self._stream_type_combobox.set_active(1)
elif self._action is Action.EDIT: elif self._action is Action.EDIT:
self._current_srv = get_base_model(self._model)[self._paths][:] self._current_srv = get_base_model(self._model)[self._paths][:]
@@ -115,7 +119,7 @@ class IptvDialog:
self._dialog.run() self._dialog.run()
def on_response(self, dialog, response): def on_response(self, dialog, response):
if response == Gtk.ResponseType.CANCEL: if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
self._dialog.destroy() self._dialog.destroy()
def on_save(self, item): def on_save(self, item):
@@ -126,16 +130,16 @@ class IptvDialog:
self.show_info_message(get_message("Error. Verify the data!"), Gtk.MessageType.ERROR) self.show_info_message(get_message("Error. Verify the data!"), Gtk.MessageType.ERROR)
return return
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL: if show_dialog(DialogType.QUESTION, self._dialog) in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return return
self.save_enigma2_data() if self._profile is SettingsType.ENIGMA_2 else self.save_neutrino_data() self.save_enigma2_data() if self._s_type is SettingsType.ENIGMA_2 else self.save_neutrino_data()
self._dialog.destroy() self._dialog.destroy()
def init_data(self, srv): def init_data(self, srv):
name, fav_id = srv[2], srv[7] name, fav_id = srv[2], srv[7]
self._name_entry.set_text(name) self._name_entry.set_text(name)
self.init_enigma2_data(fav_id) if self._profile is SettingsType.ENIGMA_2 else self.init_neutrino_data(fav_id) 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): def init_enigma2_data(self, fav_id):
data, sep, desc = fav_id.partition("#DESCRIPTION") data, sep, desc = fav_id.partition("#DESCRIPTION")
@@ -155,6 +159,8 @@ class IptvDialog:
self._stream_type_combobox.set_active(2) self._stream_type_combobox.set_active(2)
elif stream_type is StreamType.NONE_REC_2: elif stream_type is StreamType.NONE_REC_2:
self._stream_type_combobox.set_active(3) self._stream_type_combobox.set_active(3)
elif stream_type is StreamType.E_SERVICE_URI:
self._stream_type_combobox.set_active(4)
except ValueError: except ValueError:
self.show_info_message("Unknown stream type {}".format(s_type), Gtk.MessageType.ERROR) self.show_info_message("Unknown stream type {}".format(s_type), Gtk.MessageType.ERROR)
@@ -163,16 +169,17 @@ class IptvDialog:
self._tr_id_entry.set_text(str(int(data[4], 16))) self._tr_id_entry.set_text(str(int(data[4], 16)))
self._net_id_entry.set_text(str(int(data[5], 16))) self._net_id_entry.set_text(str(int(data[5], 16)))
self._namespace_entry.set_text(str(int(data[6], 16))) self._namespace_entry.set_text(str(int(data[6], 16)))
self._url_entry.set_text(urllib.request.unquote(data[10].strip())) self._url_entry.set_text(unquote(data[10].strip()))
self._update_reference_entry() self.update_reference_entry()
def init_neutrino_data(self, fav_id): def init_neutrino_data(self, fav_id):
data = fav_id.split("::") data = fav_id.split("::")
self._url_entry.set_text(data[0]) self._url_entry.set_text(data[0])
self._description_entry.set_text(data[1]) self._description_entry.set_text(data[1])
def _update_reference_entry(self): def update_reference_entry(self):
if self._profile is SettingsType.ENIGMA_2: 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._reference_entry.set_text(_ENIGMA2_REFERENCE.format(self.get_type(),
self._srv_type_entry.get_text(), self._srv_type_entry.get_text(),
int(self._sid_entry.get_text()), int(self._sid_entry.get_text()),
@@ -188,12 +195,13 @@ class IptvDialog:
entry.set_name(_DIGIT_ENTRY_NAME) entry.set_name(_DIGIT_ENTRY_NAME)
else: else:
entry.set_name("GtkEntry") entry.set_name("GtkEntry")
self._update_reference_entry() self.update_reference_entry()
def on_url_changed(self, entry): def on_url_changed(self, entry):
url_str = entry.get_text() url_str = entry.get_text()
url = urlparse(url_str) url = urlparse(url_str)
entry.set_name("GtkEntry" if all([url.scheme, url.netloc, url.path]) else _DIGIT_ENTRY_NAME) cond = all([url.scheme, url.netloc, url.path]) or self.get_type() == StreamType.E_SERVICE_URI.value
entry.set_name("GtkEntry" if cond else _DIGIT_ENTRY_NAME)
yt_id = YouTube.get_yt_id(url_str) yt_id = YouTube.get_yt_id(url_str)
if yt_id: if yt_id:
@@ -211,10 +219,21 @@ class IptvDialog:
def set_yt_url(self, entry, video_id): def set_yt_url(self, entry, video_id):
try: try:
links, title = YouTube.get_yt_link(video_id) if not self._yt_dl:
def callback(message, error=True):
msg_type = Gtk.MessageType.ERROR if error else Gtk.MessageType.INFO
self.show_info_message(message, msg_type)
self._yt_dl = YouTube.get_instance(self._settings, callback=callback)
yield True
links, title = self._yt_dl.get_yt_link(video_id, entry.get_text())
yield True
except urllib.error.URLError as e: except urllib.error.URLError as e:
self.show_info_message(get_message("Getting link error:") + (str(e)), Gtk.MessageType.ERROR) self.show_info_message(get_message("Getting link error:") + (str(e)), Gtk.MessageType.ERROR)
return return
except YouTubeException as e:
self.show_info_message((str(e)), Gtk.MessageType.ERROR)
return
else: else:
if self._action is Action.ADD: if self._action is Action.ADD:
self._name_entry.set_text(title) self._name_entry.set_text(title)
@@ -232,7 +251,9 @@ class IptvDialog:
yield True yield True
def on_stream_type_changed(self, item): def on_stream_type_changed(self, item):
self._update_reference_entry() if self.get_type() == StreamType.E_SERVICE_URI.value:
self.show_info_message("DreamOS only!", Gtk.MessageType.WARNING)
self.update_reference_entry()
def on_yt_quality_changed(self, box): def on_yt_quality_changed(self, box):
model = box.get_model() model = box.get_model()
@@ -248,7 +269,7 @@ class IptvDialog:
int(self._tr_id_entry.get_text()), int(self._tr_id_entry.get_text()),
int(self._net_id_entry.get_text()), int(self._net_id_entry.get_text()),
int(self._namespace_entry.get_text()), int(self._namespace_entry.get_text()),
urllib.request.quote(self._url_entry.get_text()), quote(self._url_entry.get_text()),
name, name) name, name)
self.update_bouquet_data(name, fav_id) self.update_bouquet_data(name, fav_id)
@@ -291,7 +312,7 @@ class IptvDialog:
class SearchUnavailableDialog: class SearchUnavailableDialog:
def __init__(self, transient, model, fav_bouquet, iptv_rows, profile): def __init__(self, transient, model, fav_bouquet, iptv_rows, s_type):
handlers = {"on_response": self.on_response} handlers = {"on_response": self.on_response}
builder = Gtk.Builder() builder = Gtk.Builder()
@@ -305,7 +326,7 @@ class SearchUnavailableDialog:
self._counter_label = builder.get_object("streams_rows_counter_label") self._counter_label = builder.get_object("streams_rows_counter_label")
self._level_bar = builder.get_object("unavailable_streams_level_bar") self._level_bar = builder.get_object("unavailable_streams_level_bar")
self._bouquet = fav_bouquet self._bouquet = fav_bouquet
self._profile = profile self._s_type = s_type
self._iptv_rows = iptv_rows self._iptv_rows = iptv_rows
self._counter = -1 self._counter = -1
self._max_rows = len(self._iptv_rows) self._max_rows = len(self._iptv_rows)
@@ -333,7 +354,7 @@ class SearchUnavailableDialog:
if not self._download_task: if not self._download_task:
return return
try: try:
req = Request(get_iptv_url(row, self._profile)) req = Request(get_iptv_url(row, self._s_type))
self.update_bar() self.update_bar()
urlopen(req, timeout=2) urlopen(req, timeout=2)
except HTTPError as e: except HTTPError as e:
@@ -375,7 +396,7 @@ class SearchUnavailableDialog:
class IptvListConfigurationDialog: class IptvListConfigurationDialog:
def __init__(self, transient, services, iptv_rows, bouquet, fav_model, profile): def __init__(self, transient, services, iptv_rows, bouquet, fav_model, s_type):
handlers = {"on_apply": self.on_apply, handlers = {"on_apply": self.on_apply,
"on_response": self.on_response, "on_response": self.on_response,
"on_stream_type_default_togged": self.on_stream_type_default_togged, "on_stream_type_default_togged": self.on_stream_type_default_togged,
@@ -389,18 +410,18 @@ class IptvListConfigurationDialog:
"on_entry_changed": self.on_entry_changed, "on_entry_changed": self.on_entry_changed,
"on_info_bar_close": self.on_info_bar_close} "on_info_bar_close": self.on_info_bar_close}
self._rows = iptv_rows
self._services = services
self._bouquet = bouquet
self._fav_model = fav_model
self._s_type = s_type
builder = Gtk.Builder() builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN) builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION), builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("iptv_list_configuration_dialog", "stream_type_liststore")) ("iptv_list_configuration_dialog", "stream_type_liststore"))
builder.connect_signals(handlers) builder.connect_signals(handlers)
self._rows = iptv_rows
self._services = services
self._bouquet = bouquet
self._fav_model = fav_model
self._profile = profile
self._dialog = builder.get_object("iptv_list_configuration_dialog") self._dialog = builder.get_object("iptv_list_configuration_dialog")
self._dialog.set_transient_for(transient) self._dialog.set_transient_for(transient)
self._info_bar = builder.get_object("list_configuration_info_bar") self._info_bar = builder.get_object("list_configuration_info_bar")
@@ -487,7 +508,7 @@ class IptvListConfigurationDialog:
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!") show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return return
if self._profile is SettingsType.ENIGMA_2: if self._s_type is SettingsType.ENIGMA_2:
reset = self._reset_to_default_switch.get_active() reset = self._reset_to_default_switch.get_active()
type_default = self._type_check_button.get_active() type_default = self._type_check_button.get_active()
tid_default = self._tid_check_button.get_active() tid_default = self._tid_check_button.get_active()
@@ -541,7 +562,7 @@ class IptvListConfigurationDialog:
class YtListImportDialog: class YtListImportDialog:
def __init__(self, transient, profile, appender): def __init__(self, transient, settings, appender):
handlers = {"on_import": self.on_import, handlers = {"on_import": self.on_import,
"on_receive": self.on_receive, "on_receive": self.on_receive,
"on_yt_url_entry_changed": self.on_url_entry_changed, "on_yt_url_entry_changed": self.on_url_entry_changed,
@@ -553,11 +574,20 @@ class YtListImportDialog:
"on_key_press": self.on_key_press, "on_key_press": self.on_key_press,
"on_close": self.on_close} "on_close": self.on_close}
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 = Gtk.Builder() builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN) builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION), builder.add_objects_from_string(get_dialogs_string(_UI_PATH).format(use_header=IS_GNOME_SESSION),
("yt_import_dialog_window", "yt_liststore", "yt_quality_liststore", ("yt_import_dialog_window", "yt_liststore", "yt_quality_liststore",
"yt_popup_menu", "remove_selection_image")) "yt_popup_menu", "remove_selection_image", "yt_receive_image",
"yt_import_image"))
builder.connect_signals(handlers) builder.connect_signals(handlers)
self._dialog = builder.get_object("yt_import_dialog_window") self._dialog = builder.get_object("yt_import_dialog_window")
@@ -583,12 +613,6 @@ class YtListImportDialog:
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider, self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER) Gtk.STYLE_PROVIDER_PRIORITY_USER)
self.appender = appender
self._profile = profile
self._download_task = False
self._yt_list_id = None
self._yt_list_title = None
def show(self): def show(self):
self._dialog.show() self._dialog.show()
@@ -602,7 +626,11 @@ class YtListImportDialog:
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
done_links = {} done_links = {}
rows = list(filter(lambda r: r[2], self._model)) rows = list(filter(lambda r: r[2], self._model))
futures = {executor.submit(YouTube.get_yt_link, r[1]): r for r in rows} if not self._yt:
self._yt = YouTube.get_instance(self._settings)
futures = {executor.submit(self._yt.get_yt_link, r[1], YouTube.VIDEO_LINK.format(r[1]),
True): r for r in rows}
size = len(futures) size = len(futures)
counter = 0 counter = 0
@@ -614,6 +642,8 @@ class YtListImportDialog:
done_links[futures[future]] = future.result() done_links[futures[future]] = future.result()
counter += 1 counter += 1
self.update_progress_bar(counter / size) self.update_progress_bar(counter / size)
except YouTubeException as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
except Exception as e: except Exception as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR) self.show_info_message(str(e), Gtk.MessageType.ERROR)
else: else:
@@ -647,8 +677,8 @@ class YtListImportDialog:
self.update_active_elements(True) self.update_active_elements(True)
def update_links(self, links): def update_links(self, links):
for l in links: for link in links:
yield self._model.append((l[0], l[1], True, None)) yield self._model.append((link[0], link[1], True, None))
size = len(self._model) size = len(self._model)
self._yt_count_label.set_text(str(size)) self._yt_count_label.set_text(str(size))
@@ -668,11 +698,11 @@ class YtListImportDialog:
act = self._quality_model.get_value(self._quality_box.get_active_iter(), 0) act = self._quality_model.get_value(self._quality_box.get_active_iter(), 0)
for link in links: for link in links:
lnk, title = link lnk, title = link or (None, None)
if not lnk: if not lnk:
continue continue
ln = lnk.get(act) if act in lnk else lnk[sorted(lnk, key=lambda x: int(x.rstrip("p")), reverse=True)[0]] 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._profile) 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, *aggr[0:3], BqServiceType.IPTV.name, *aggr, None, fav_id, None)
srvs.append(srv) srvs.append(srv)
self.appender(srvs) self.appender(srvs)

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
""" This is helper module for ui """ """ Helper module for the ui. """
import os import os
import shutil import shutil
import urllib.request from urllib.parse import unquote
from gi.repository import GdkPixbuf, GLib from gi.repository import GdkPixbuf, GLib
@@ -16,23 +16,28 @@ from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON,
# ***************** Markers *******************# # ***************** Markers *******************#
def insert_marker(view, bouquets, selected_bouquet, services, parent_window): def insert_marker(view, bouquets, selected_bouquet, services, parent_window, m_type=BqServiceType.MARKER):
"""" Inserts marker into bouquet services list. """ """" Inserts marker into bouquet services list. """
response = show_dialog(DialogType.INPUT, parent_window) fav_id, text = "1:832:D:0:0:0:0:0:0:0:\n", None
if response == Gtk.ResponseType.CANCEL:
return
if not response.strip(): if m_type is BqServiceType.MARKER:
show_dialog(DialogType.ERROR, parent_window, "The text of marker is empty, please try again!") response = show_dialog(DialogType.INPUT, parent_window)
return if response == Gtk.ResponseType.CANCEL:
return
fav_id = "1:64:0:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n".format(response, response) if not response.strip():
s_type = BqServiceType.MARKER.name show_dialog(DialogType.ERROR, parent_window, "The text of marker is empty, please try again!")
return
fav_id = "1:64:0:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n".format(response, response)
text = response
s_type = m_type.name
model, paths = view.get_selection().get_selected_rows() model, paths = view.get_selection().get_selected_rows()
marker = (None, None, response, None, None, s_type, None, fav_id, None, None, None) 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) 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) bouquets[selected_bouquet].insert(model.get_path(itr)[0], fav_id)
services[fav_id] = Service(None, None, None, response, 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] * 9, 0, fav_id, None)
# ***************** Movement *******************# # ***************** Movement *******************#
@@ -208,6 +213,7 @@ def set_flags(flag, services_view, fav_view, services, blacklist):
if not paths: if not paths:
return return
paths = get_base_paths(paths, model)
model = get_base_model(model) model = get_base_model(model)
if flag is Flag.HIDE: if flag is Flag.HIDE:
@@ -236,13 +242,14 @@ def set_lock(blacklist, services, model, paths, target, services_model):
locked = has_locked_hide(model, paths, col_num) locked = has_locked_hide(model, paths, col_num)
ids = [] ids = []
skip_type = {BqServiceType.MARKER.name, BqServiceType.SPACE.name}
for path in paths: for path in paths:
itr = model.get_iter(path) itr = model.get_iter(path)
fav_id = model.get_value(itr, Column.SRV_FAV_ID if target is ViewTarget.SERVICES else Column.FAV_ID) fav_id = model.get_value(itr, Column.SRV_FAV_ID if target is ViewTarget.SERVICES else Column.FAV_ID)
srv = services.get(fav_id, None) srv = services.get(fav_id, None)
if srv: if srv and srv.service_type not in skip_type:
bq_id = to_bouquet_id(srv) bq_id = srv.data_id if srv.service_type == BqServiceType.IPTV.name else to_bouquet_id(srv)
if not bq_id: if not bq_id:
continue continue
blacklist.discard(bq_id) if locked else blacklist.add(bq_id) blacklist.discard(bq_id) if locked else blacklist.add(bq_id)
@@ -362,59 +369,75 @@ def append_picons(picons, model):
GLib.idle_add(lambda: next(app, False), priority=GLib.PRIORITY_LOW) GLib.idle_add(lambda: next(app, False), priority=GLib.PRIORITY_LOW)
def assign_picon(target, srv_view, fav_view, transient, picons, settings, services): def assign_picons(target, srv_view, fav_view, transient, picons, settings, services, src_path=None, dst_path=None):
""" Assigning picons and returns picons files list. """
view = srv_view if target is ViewTarget.SERVICES else fav_view view = srv_view if target is ViewTarget.SERVICES else fav_view
model, paths = view.get_selection().get_selected_rows() model, paths = view.get_selection().get_selected_rows()
if not is_only_one_item_selected(paths, transient): picons_files = []
return
response = get_chooser_dialog(transient, settings, "*.png", "png files") if not src_path:
if response == Gtk.ResponseType.CANCEL: src_path = get_chooser_dialog(transient, settings, "*.png files", ("*.png",))
return if src_path == Gtk.ResponseType.CANCEL:
return picons_files
if not str(response).endswith(".png"): 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!") show_dialog(DialogType.ERROR, transient, text="No png file is selected!")
return return picons_files
picon_pos = Column.SRV_PICON p_pos = Column.SRV_PICON
model = get_base_model(model) col_num = Column.SRV_FAV_ID if target is ViewTarget.SERVICES else Column.FAV_ID
itr = model.get_iter(paths) itrs = [model.get_iter(p) for p in paths]
fav_id = model.get_value(itr, Column.SRV_FAV_ID if target is ViewTarget.SERVICES else Column.FAV_ID)
picon_id = services.get(fav_id)[Column.SRV_PICON_ID]
if picon_id: if target is ViewTarget.SERVICES:
if os.path.isfile(response): f_model = model.get_model()
picons_path = settings.picons_local_path itrs = [f_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr)) for itr in itrs]
model = get_base_model(model)
for itr in itrs:
fav_id = model.get_value(itr, col_num)
picon_id = services.get(fav_id)[Column.SRV_PICON_ID]
if picon_id:
picons_path = dst_path or settings.picons_local_path
os.makedirs(os.path.dirname(picons_path), exist_ok=True) os.makedirs(os.path.dirname(picons_path), exist_ok=True)
picon_file = picons_path + picon_id picon_file = picons_path + picon_id
shutil.copy(response, picon_file) shutil.copy(src_path, picon_file)
picons_files.append(picon_file)
picon = get_picon_pixbuf(picon_file) picon = get_picon_pixbuf(picon_file)
picons[picon_id] = picon picons[picon_id] = picon
model.set_value(itr, picon_pos, picon) model.set_value(itr, p_pos, picon)
if target is ViewTarget.SERVICES: if target is ViewTarget.SERVICES:
set_picon(fav_id, fav_view.get_model(), picon, Column.FAV_ID, picon_pos) set_picon(fav_id, fav_view.get_model(), picon, Column.FAV_ID, p_pos)
else: else:
set_picon(fav_id, get_base_model(srv_view.get_model()), picon, Column.SRV_FAV_ID, picon_pos) set_picon(fav_id, get_base_model(srv_view.get_model()), picon, Column.SRV_FAV_ID, p_pos)
return picons_files
def set_picon(fav_id, model, picon, fav_id_pos, picon_pos): def set_picon(fav_id, model, picon, fav_id_pos, picon_pos):
for row in model: for row in model:
if row[fav_id_pos] == fav_id: if row[fav_id_pos] == fav_id:
row[picon_pos] = picon row[picon_pos] = picon
break return True
return True
def remove_picon(target, srv_view, fav_view, picons, settings): def remove_picon(target, srv_view, fav_view, picons, settings):
view = srv_view if target is ViewTarget.SERVICES else fav_view view = srv_view if target is ViewTarget.SERVICES else fav_view
model, paths = view.get_selection().get_selected_rows() model, paths = view.get_selection().get_selected_rows()
model = get_base_model(model)
fav_ids = [] fav_ids = []
picon_ids = [] picon_ids = []
picon_pos = Column.SRV_PICON # picon position is equal for services and fav picon_pos = Column.SRV_PICON # picon position is equal for services and fav
for path in paths: itrs = [model.get_iter(p) for p in paths]
itr = model.get_iter(path)
if target is ViewTarget.SERVICES:
f_model = model.get_model()
itrs = [f_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr)) for itr in itrs]
model = get_base_model(model)
for itr in itrs:
model.set_value(itr, picon_pos, None) model.set_value(itr, picon_pos, None)
if target is ViewTarget.SERVICES: if target is ViewTarget.SERVICES:
fav_ids.append(model.get_value(itr, Column.SRV_FAV_ID)) fav_ids.append(model.get_value(itr, Column.SRV_FAV_ID))
@@ -426,8 +449,10 @@ def remove_picon(target, srv_view, fav_view, picons, settings):
else: else:
fav_ids.append(fav_id) fav_ids.append(fav_id)
fav_id_column = Column.FAV_ID if target is ViewTarget.SERVICES else Column.SRV_FAV_ID
def remove(md, path, it): def remove(md, path, it):
if md.get_value(it, Column.FAV_ID if target is ViewTarget.SERVICES else Column.SRV_FAV_ID) in fav_ids: if md.get_value(it, fav_id_column) in fav_ids:
md.set_value(it, picon_pos, None) md.set_value(it, picon_pos, None)
if target is ViewTarget.FAV: if target is ViewTarget.FAV:
picon_ids.append(md.get_value(it, Column.SRV_PICON_ID)) picon_ids.append(md.get_value(it, Column.SRV_PICON_ID))
@@ -488,9 +513,9 @@ def is_only_one_item_selected(paths, transient):
return True return True
def get_picon_pixbuf(path): def get_picon_pixbuf(path, size=32):
try: try:
return GdkPixbuf.Pixbuf.new_from_file_at_scale(filename=path, width=32, height=32, preserve_aspect_ratio=True) return GdkPixbuf.Pixbuf.new_from_file_at_scale(path, width=size, height=size, preserve_aspect_ratio=True)
except GLib.GError as e: except GLib.GError as e:
pass pass
@@ -567,12 +592,28 @@ def update_entry_data(entry, dialog, settings):
def get_base_model(model): def get_base_model(model):
""" Returns base tree model if has wrappers ("TreeModelSort" and "TreeModelFilter") """ """ Returns base tree model if has wrappers [TreeModelSort, TreeModelFilter]. """
if type(model) is Gtk.TreeModelSort: if type(model) is Gtk.TreeModelSort:
return model.get_model().get_model() return model.get_model().get_model()
return model return model
def get_base_itrs(itrs, model):
""" Returns base iters from wrapper models. """
if type(model) is Gtk.TreeModelSort:
filter_model = model.get_model()
return [filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr)) for itr in itrs]
return itrs
def get_base_paths(paths, model):
""" Returns base paths from wrapper models. """
if type(model) is Gtk.TreeModelSort:
filter_model = model.get_model()
return [filter_model.convert_path_to_child_path(model.convert_path_to_child_path(p)) for p in paths]
return paths
def get_model_data(view): def get_model_data(view):
""" Returns model name and base model from the given view """ """ Returns model name and base model from the given view """
model = get_base_model(view.get_model()) model = get_base_model(view.get_model())
@@ -595,7 +636,7 @@ def get_iptv_url(row, s_type):
data = list(filter(lambda x: "http" in x, data)) data = list(filter(lambda x: "http" in x, data))
if data: if data:
url = data[0] url = data[0]
return urllib.request.unquote(url) if s_type is SettingsType.ENIGMA_2 else url return unquote(url) if s_type is SettingsType.ENIGMA_2 else url
def on_popup_menu(menu, event): def on_popup_menu(menu, event):

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,410 +0,0 @@
import os
import re
import shutil
import subprocess
import tempfile
from gi.repository import GLib, GdkPixbuf
from app.commons import run_idle, run_task
from app.connections import upload_data, DownloadType, download_data, remove_picons
from app.settings import SettingsType
from app.tools.picons import PiconsParser, parse_providers, Provider, convert_to
from app.tools.satellites import SatellitesParser, SatelliteSource
from .dialogs import show_dialog, DialogType, get_message
from .main_helper import update_entry_data, append_text_to_tview, scroll_to, on_popup_menu
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, TV_ICON
class PiconsDialog:
def __init__(self, transient, settings, picon_ids, sat_positions):
self._picon_ids = picon_ids
self._sat_positions = sat_positions
self._TMP_DIR = tempfile.gettempdir() + "/"
self._BASE_URL = "www.lyngsat.com/packages/"
self._PATTERN = re.compile(r"^https://www\.lyngsat\.com/[\w-]+\.html$")
self._POS_PATTERN = re.compile(r"^\d+\.\d+[EW]?$")
self._current_process = None
self._terminate = False
handlers = {"on_receive": self.on_receive,
"on_load_providers": self.on_load_providers,
"on_cancel": self.on_cancel,
"on_close": self.on_close,
"on_send": self.on_send,
"on_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_position_edited": self.on_position_edited,
"on_notebook_switch_page": self.on_notebook_switch_page,
"on_convert": self.on_convert,
"on_satellites_view_realize": self.on_satellites_view_realize,
"on_satellite_selection": self.on_satellite_selection,
"on_select_all": self.on_select_all,
"on_unselect_all": self.on_unselect_all,
"on_popup_menu": on_popup_menu}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_from_file(UI_RESOURCES_PATH + "picons_dialog.glade")
builder.connect_signals(handlers)
self._dialog = builder.get_object("picons_dialog")
self._dialog.set_transient_for(transient)
self._providers_tree_view = builder.get_object("providers_tree_view")
self._satellites_tree_view = builder.get_object("satellites_tree_view")
self._expander = builder.get_object("expander")
self._text_view = builder.get_object("text_view")
self._info_bar = builder.get_object("info_bar")
self._ip_entry = builder.get_object("ip_entry")
self._picons_entry = builder.get_object("picons_entry")
self._url_entry = builder.get_object("url_entry")
self._picons_dir_entry = builder.get_object("picons_dir_entry")
self._info_bar = builder.get_object("info_bar")
self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label")
self._load_providers_button = builder.get_object("load_providers_button")
self._receive_button = builder.get_object("receive_button")
self._convert_button = builder.get_object("convert_button")
self._enigma2_path_button = builder.get_object("enigma2_path_button")
self._save_to_button = builder.get_object("save_to_button")
self._send_button = builder.get_object("send_button")
self._cancel_button = builder.get_object("cancel_button")
self._enigma2_radio_button = builder.get_object("enigma2_radio_button")
self._neutrino_mp_radio_button = builder.get_object("neutrino_mp_radio_button")
self._resize_no_radio_button = builder.get_object("resize_no_radio_button")
self._resize_220_132_radio_button = builder.get_object("resize_220_132_radio_button")
self._resize_100_60_radio_button = builder.get_object("resize_100_60_radio_button")
self._satellite_label = builder.get_object("satellite_label")
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._cancel_button.bind_property("visible", self._header_download_box, "visible", 4)
self._convert_button.bind_property("visible", self._header_download_box, "visible", 4)
# style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
self._settings = settings
self._s_type = settings.setting_type
self._ip_entry.set_text(self._settings.host)
self._picons_entry.set_text(self._settings.picons_path)
self._picons_path = self._settings.picons_local_path
self._picons_dir_entry.set_text(self._picons_path)
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"
"first load the required services list into the main application window.")
self.show_info_message(message, Gtk.MessageType.WARNING)
self._satellite_label.show()
def show(self):
self._dialog.run()
def on_satellites_view_realize(self, view):
self.get_satellites(view)
@run_task
def get_satellites(self, view):
sats = SatellitesParser().get_satellites_list(SatelliteSource.LYNGSAT)
if not sats:
self.show_info_message("Getting satellites list error!", Gtk.MessageType.ERROR)
gen = self.append_satellites(view.get_model(), sats)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def append_satellites(self, model, sats):
try:
for sat in sats:
pos = sat[1]
name, pos = "{} ({})".format(sat[0], pos), "{}{}".format("-" if pos[-1] == "W" else "", pos[:-1])
if not self._terminate and model:
if pos in self._sat_positions:
yield model.append((name, sat[3], pos))
finally:
self._satellite_label.show()
def on_satellite_selection(self, view, path, column):
model = view.get_model()
self._url_entry.set_text(model.get(model.get_iter(path), 1)[0])
@run_idle
def on_load_providers(self, item):
self._expander.set_expanded(True)
self.on_info_bar_close()
self._cancel_button.show()
url = self._url_entry.get_text()
try:
self._current_process = subprocess.Popen(["wget", "-pkP", self._TMP_DIR, url],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
except FileNotFoundError as e:
self._cancel_button.hide()
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
GLib.io_add_watch(self._current_process.stderr, GLib.IO_IN, self.write_to_buffer)
model = self._providers_tree_view.get_model()
model.clear()
self.append_providers(url, model)
@run_task
def append_providers(self, url, model):
self._current_process.wait()
try:
self._terminate = False
providers = parse_providers(self._TMP_DIR + url[url.find("w"):])
except FileNotFoundError:
pass # NOP
else:
if providers:
for p in providers:
if self._terminate:
return
model.append((self.get_pixbuf(p[0]) if p[0] else TV_ICON, *p[1:]))
self.update_receive_button_state()
finally:
GLib.idle_add(self._cancel_button.hide)
self._terminate = False
def get_pixbuf(self, img_url):
return GdkPixbuf.Pixbuf.new_from_file_at_scale(filename=self._TMP_DIR + "www.lyngsat.com/" + img_url,
width=48, height=48, preserve_aspect_ratio=True)
def on_receive(self, item):
self._cancel_button.show()
self.start_download()
@run_task
def start_download(self):
if self._current_process.poll() is None:
self.show_dialog("The task is already running!", DialogType.ERROR)
return
self._terminate = False
self._expander.set_expanded(True)
providers = self.get_selected_providers()
for prv in providers:
if not self._POS_PATTERN.match(prv[2]):
self.show_info_message(
get_message("Specify the correct position value for the provider!"), Gtk.MessageType.ERROR)
scroll_to(prv.path, self._providers_tree_view)
return
try:
for prv in providers:
if self._terminate:
return
self.process_provider(Provider(*prv))
if self._resize_no_radio_button.get_active():
self.resize(self._picons_path)
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
finally:
GLib.idle_add(self._cancel_button.hide)
self._terminate = False
def process_provider(self, prv):
url = prv.url
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
self._current_process = subprocess.Popen(["wget", "-pkP", self._TMP_DIR, url],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
GLib.io_add_watch(self._current_process.stderr, GLib.IO_IN, self.write_to_buffer)
self._current_process.wait()
path = self._TMP_DIR + (url[url.find("//") + 2:] if prv.single else self._BASE_URL + url[url.rfind("/") + 1:])
PiconsParser.parse(path, self._picons_path, self._TMP_DIR, prv, self._picon_ids, self.get_picons_format())
def write_to_buffer(self, fd, condition):
if condition == GLib.IO_IN:
char = fd.read(1)
self.append_output(char)
return True
return False
@run_idle
def append_output(self, char):
append_text_to_tview(char, self._text_view)
def resize(self, path):
self.show_info_message(get_message("Resizing..."), Gtk.MessageType.INFO)
command = "mogrify -resize {}! *.png".format(
"220x132" if self._resize_220_132_radio_button.get_active() else "100x60").split()
try:
self._current_process = subprocess.Popen(command, universal_newlines=True, cwd=path)
self._current_process.wait()
except FileNotFoundError as e:
self.show_info_message("Conversion error. " + str(e), Gtk.MessageType.ERROR)
def on_cancel(self, item=None):
if self.is_task_running() and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return True
self.terminate_task()
@run_task
def terminate_task(self):
self._terminate = True
if self._current_process:
self._current_process.terminate()
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.save_window_size(window)
self.clean_data()
GLib.idle_add(self._dialog.destroy)
def save_window_size(self, window):
t, _ = self._text_view.get_allocated_size()
b, _ = self._info_bar.get_allocated_size()
size = window.get_size()
self._settings.add("picons_downloader_window_size", (size.width, size.height - t.height - b.height))
@run_task
def clean_data(self):
path = self._TMP_DIR + "www.lyngsat.com"
if os.path.exists(path):
shutil.rmtree(path)
def on_send(self, item):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
self.run_func(lambda: upload_data(settings=self._settings,
download_type=DownloadType.PICONS,
callback=self.append_output,
done_callback=lambda: self.show_info_message(get_message("Done!"),
Gtk.MessageType.INFO)))
def on_download(self, item):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
self.run_func(lambda: download_data(settings=self._settings,
download_type=DownloadType.PICONS,
callback=self.append_output))
def on_remove(self, item):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
self.run_func(lambda: remove_picons(settings=self._settings,
callback=self.append_output,
done_callback=lambda: self.show_info_message(get_message("Done!"),
Gtk.MessageType.INFO)))
@run_task
def run_func(self, func):
try:
GLib.idle_add(self._expander.set_expanded, True)
GLib.idle_add(self._header_download_box.set_sensitive, False)
func()
except OSError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
GLib.idle_add(self._header_download_box.set_sensitive, True)
def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False)
@run_idle
def show_info_message(self, text, message_type):
self._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(text)
def on_picons_dir_open(self, entry, icon, event_button):
update_entry_data(entry, self._dialog, settings=self._settings)
@run_idle
def on_selected_toggled(self, toggle, path):
model = self._providers_tree_view.get_model()
model.set_value(model.get_iter(path), 7, not toggle.get_active())
self.update_receive_button_state()
def on_select_all(self, view):
self.update_selection(view, True)
def on_unselect_all(self, view):
self.update_selection(view, False)
def update_selection(self, view, select):
view.get_model().foreach(lambda mod, path, itr: mod.set_value(itr, 7, select))
self.update_receive_button_state()
def on_url_changed(self, entry):
suit = self._PATTERN.search(entry.get_text())
entry.set_name("GtkEntry" if suit else "digit-entry")
self._load_providers_button.set_sensitive(suit if suit else False)
def on_position_edited(self, render, path, value):
model = self._providers_tree_view.get_model()
model.set_value(model.get_iter(path), 2, value)
@run_idle
def on_notebook_switch_page(self, nb, box, tab_num):
self._convert_button.set_visible(tab_num)
@run_idle
def on_convert(self, item):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
picons_path = self._enigma2_path_button.get_filename()
save_path = self._save_to_button.get_filename()
if not picons_path or not save_path:
show_dialog(DialogType.ERROR, transient=self._dialog, text="Select paths!")
return
self._expander.set_expanded(True)
convert_to(src_path=picons_path,
dest_path=save_path,
s_type=SettingsType.ENIGMA_2,
callback=self.append_output,
done_callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO))
@run_idle
def update_receive_button_state(self):
try:
self._receive_button.set_sensitive(len(self.get_selected_providers()) > 0)
except TypeError:
pass # NOP
def get_selected_providers(self):
""" returns selected providers """
return [r for r in self._providers_tree_view.get_model() if r[7]]
@run_idle
def show_dialog(self, message, dialog_type):
show_dialog(dialog_type, self._dialog, message)
def get_picons_format(self):
picon_format = SettingsType.ENIGMA_2
if self._neutrino_mp_radio_button.get_active():
picon_format = SettingsType.NEUTRINO_MP
return picon_format
def is_task_running(self):
return self._current_process and self._current_process.poll() is None
if __name__ == "__main__":
pass

1835
app/ui/picons_manager.glade Normal file

File diff suppressed because it is too large Load Diff

844
app/ui/picons_manager.py Normal file
View File

@@ -0,0 +1,844 @@
import os
import re
import shutil
import subprocess
import tempfile
from pathlib import Path
from urllib.parse import urlparse, unquote
from gi.repository import GLib, GdkPixbuf
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
from app.tools.picons import PiconsParser, parse_providers, Provider, convert_to
from app.tools.satellites import SatellitesParser, SatelliteSource
from .dialogs import show_dialog, DialogType, get_message
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, GTK_PATH, KeyboardKey
class PiconsDialog:
def __init__(self, transient, settings, picon_ids, sat_positions, app):
self._picon_ids = picon_ids
self._sat_positions = sat_positions
self._app = app
self._TMP_DIR = tempfile.gettempdir() + "/"
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]?$")
self._current_process = None
self._terminate = False
self._filter_binding = None
self._services = None
self._current_picon_info = None
handlers = {"on_receive": self.on_receive,
"on_load_providers": self.on_load_providers,
"on_cancel": self.on_cancel,
"on_close": self.on_close,
"on_send": self.on_send,
"on_download": self.on_download,
"on_remove": self.on_remove,
"on_info_bar_close": self.on_info_bar_close,
"on_picons_dir_open": self.on_picons_dir_open,
"on_selected_toggled": self.on_selected_toggled,
"on_url_changed": self.on_url_changed,
"on_picons_filter_changed": self.on_picons_filter_changed,
"on_position_edited": self.on_position_edited,
"on_visible_page": self.on_visible_page,
"on_convert": self.on_convert,
"on_picons_src_changed": self.on_picons_src_changed,
"on_picons_dest_changed": self.on_picons_dest_changed,
"on_picons_view_drag_data_get": self.on_picons_view_drag_data_get,
"on_picons_src_view_drag_drop": self.on_picons_src_view_drag_drop,
"on_picons_src_view_drag_data_received": self.on_picons_src_view_drag_data_received,
"on_picons_src_view_drag_end": self.on_picons_src_view_drag_end,
"on_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_satellites_view_realize": self.on_satellites_view_realize,
"on_satellite_selection": self.on_satellite_selection,
"on_select_all": self.on_select_all,
"on_unselect_all": self.on_unselect_all,
"on_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_tree_view_key_press": self.on_tree_view_key_press,
"on_popup_menu": on_popup_menu}
builder = Gtk.Builder()
builder.add_from_file(UI_RESOURCES_PATH + "picons_manager.glade")
builder.connect_signals(handlers)
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")
self._satellites_view = builder.get_object("satellites_view")
self._picons_src_filter_model = builder.get_object("picons_src_filter_model")
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._ip_entry = builder.get_object("ip_entry")
self._picons_entry = builder.get_object("picons_entry")
self._url_entry = builder.get_object("url_entry")
self._picons_dir_entry = builder.get_object("picons_dir_entry")
self._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._load_providers_button = builder.get_object("load_providers_button")
self._receive_button = builder.get_object("receive_button")
self._convert_button = builder.get_object("convert_button")
self._enigma2_path_button = builder.get_object("enigma2_path_button")
self._save_to_button = builder.get_object("save_to_button")
self._send_button = builder.get_object("send_button")
self._download_button = builder.get_object("download_button")
self._remove_button = builder.get_object("remove_button")
self._cancel_button = builder.get_object("cancel_button")
self._enigma2_radio_button = builder.get_object("enigma2_radio_button")
self._neutrino_mp_radio_button = builder.get_object("neutrino_mp_radio_button")
self._resize_no_radio_button = builder.get_object("resize_no_radio_button")
self._resize_220_132_radio_button = builder.get_object("resize_220_132_radio_button")
self._resize_100_60_radio_button = builder.get_object("resize_100_60_radio_button")
self._satellite_label = builder.get_object("satellite_label")
self._explorer_action_box = builder.get_object("explorer_action_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._cancel_button.bind_property("visible", builder.get_object("receive_button"), "visible", 4)
self._cancel_button.bind_property("visible", self._load_providers_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()
# Style
self._style_provider = Gtk.CssProvider()
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
self._settings = settings
self._s_type = settings.setting_type
self._ip_entry.set_text(self._settings.host)
self._picons_entry.set_text(self._settings.picons_path)
self._picons_dir_entry.set_text(self._settings.picons_local_path)
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"
"first load the required services list into the main application window.")
self.show_info_message(message, Gtk.MessageType.WARNING)
self._satellite_label.show()
def show(self):
self._dialog.show()
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)
def on_picons_src_changed(self, button):
self.update_picons_data(self._picons_src_view, button)
def on_picons_dest_changed(self, button):
self.update_picon_info()
self.update_picons_data(self._picons_dest_view, button)
def update_picons_data(self, view, button):
path = button.get_filename()
if not path or not os.path.exists(path):
return
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, button):
p_model = view.get_model()
if not p_model:
button.set_sensitive(True)
return
model = get_base_model(p_model)
view.set_model(None)
factor = self._app.DEL_FACTOR
for index, itr in enumerate([row.iter for row in model]):
model.remove(itr)
if index % factor == 0:
yield True
for file in os.listdir(path):
if self._terminate:
return
p_path = "{}/{}".format(path, file)
p = self.get_pixbuf_at_scale(p_path, 72, 48, True)
if p:
yield model.append((p, file, p_path))
view.set_model(p_model)
button.set_sensitive(True)
yield 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())
f_path = str(path.resolve())
if not f_path:
return
model = get_base_model(view.get_model())
if path.is_file():
p = self.get_pixbuf_at_scale(f_path, 72, 48, True)
if p:
model.append((p, path.name, f_path))
elif path.is_dir():
self._explorer_src_path_button.select_filename(f_path)
def get_pixbuf_at_scale(self, path, width, height, p_ratio):
try:
return GdkPixbuf.Pixbuf.new_from_file_at_scale(path, width, height, p_ratio)
except GLib.GError:
pass
# ***************** Drag-and-drop ********************* #
def init_drag_and_drop(self):
self._picons_src_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
self._picons_src_view.drag_source_add_uri_targets()
self._picons_dest_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
self._picons_dest_view.drag_source_add_uri_targets()
self._picons_src_view.enable_model_drag_dest([], Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE)
self._picons_src_view.drag_dest_add_text_targets()
self._picon_info_image.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
self._picon_info_image.drag_dest_add_uri_targets()
self._send_button.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
self._send_button.drag_dest_add_uri_targets()
self._download_button.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
self._download_button.drag_dest_add_uri_targets()
self._remove_button.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.COPY)
self._remove_button.drag_dest_add_uri_targets()
def on_picons_view_drag_data_get(self, view, drag_context, data, info, time):
model, path = view.get_selection().get_selected_rows()
if path:
p_uri = Path(model[path][-1]).as_uri()
dest_uri = Path(self._explorer_dest_path_button.get_filename()).as_uri()
data.set_uris(["{}::::{}".format(p_uri, dest_uri)])
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_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:
return
if txt.startswith("file://"):
self.update_picons_from_file(view, txt)
return
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)
if not path:
return
model = view.get_model()
if src == self._app.FAV_MODEL_NAME:
target_view = self._app.fav_view
c_id = Column.FAV_ID
else:
target_view = self._app.services_view
c_id = Column.SRV_FAV_ID
t_mod = target_view.get_model()
dest_path = self._explorer_dest_path_button.get_filename() + "/"
self.update_picons_dest_view(self._app.on_assign_picon(target_view, model[path][-1], dest_path))
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
def update_picons_dest_view(self, picons):
""" Update destination view on adding/changing picons. """
if picons:
dest_model = get_base_model(self._picons_dest_view.get_model())
paths = {r[1]: r.iter for r in dest_model}
for p_path in picons:
p = self.get_pixbuf_at_scale(p_path, 72, 48, True)
if p:
p_name = Path(p_path).name
itr = paths.get(p_name, None)
if itr:
dest_model.set_value(itr, 0, p)
else:
itr = dest_model.append((p, p_name, p_path))
scroll_to(dest_model.get_path(itr), self._picons_dest_view)
@run_idle
def show_assign_info(self, fav_ids):
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)
self.append_output("Picon assignment for the service:\n{}\n{}\n".format(info, " * " * 30))
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):
if not self._current_picon_info:
self.show_info_message("No selected item!", Gtk.MessageType.ERROR)
return
uris = data.get_uris()
if uris:
name, fav_id = self._current_picon_info
src, sep, dst = uris[0].partition("::::")
src = urlparse(unquote(src)).path
dst = "{}/{}".format(urlparse(unquote(dst)).path, name)
if src != dst:
shutil.copy(src, dst)
for row in get_base_model(self._picons_dest_view.get_model()):
if name == row[1]:
row[0] = self.get_pixbuf_at_scale(row[-1], 72, 48, True)
img.set_from_pixbuf(self.get_pixbuf_at_scale(row[-1], 100, 60, True))
gen = self.update_picon_in_lists(dst, fav_id)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def on_send_button_drag_data_received(self, button, drag_context, x, y, data, info, time):
path = self.get_path_from_uris(data)
if path and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.OK:
self.on_send(files_filter={path.name}, path=path.parent)
def on_download_button_drag_data_received(self, button, drag_context, x, y, data, info, time):
path = self.get_path_from_uris(data)
if path:
self.on_download(files_filter={path.name})
def on_remove_button_drag_data_received(self, button, drag_context, x, y, data, info, time):
path = self.get_path_from_uris(data)
if path:
self.on_remove(files_filter={path.name})
def get_path_from_uris(self, data):
uris = data.get_uris()
if uris:
src, sep, dst = uris[0].partition("::::")
return Path(urlparse(unquote(src)).path).resolve()
def update_picon_in_lists(self, dst, fav_id):
picon = get_picon_pixbuf(dst)
p_pos = Column.SRV_PICON
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)
# ******************** Download/Upload/Remove ************************* #
def on_selective_send(self, view):
path = self.get_selected_path(view)
if path and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.OK:
self.on_send(files_filter={path.name}, path=path.parent)
def on_selective_download(self, view):
path = self.get_selected_path(view)
if path:
self.on_download(files_filter={path.name})
def on_selective_remove(self, view):
path = self.get_selected_path(view)
if path:
self.on_remove(files_filter={path.name})
def on_local_remove(self, view):
model, paths = view.get_selection().get_selected_rows()
if paths and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.OK:
itr = model.get_iter(paths.pop())
p_path = Path(model.get_value(itr, 2)).resolve()
if p_path.is_file():
p_path.unlink()
base_model = get_base_model(model)
filter_model = model.get_model()
itr = filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr))
base_model.remove(itr)
def on_send(self, item=None, files_filter=None, path=None):
dest_path = path or self.check_dest_path()
if not dest_path:
return
settings = Settings(self._settings.settings)
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, item=None, files_filter=None, path=None):
path = path or self.check_dest_path()
if not path:
return
settings = Settings(self._settings.settings)
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._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))
def get_selected_path(self, view):
model, paths = view.get_selection().get_selected_rows()
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_satellites_view_realize(self, view):
self.get_satellites(view)
@run_task
def get_satellites(self, view):
sats = SatellitesParser().get_satellites_list(SatelliteSource.LYNGSAT)
if not sats:
self.show_info_message("Getting satellites list error!", Gtk.MessageType.ERROR)
gen = self.append_satellites(view.get_model(), sats)
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
def append_satellites(self, model, sats):
try:
for sat in sats:
pos = sat[1]
name, pos = "{} ({})".format(sat[0], pos), "{}{}".format("-" if pos[-1] == "W" else "", pos[:-1])
if not self._terminate and model:
if pos in self._sat_positions:
yield model.append((name, sat[3], pos))
finally:
self._satellite_label.show()
def on_satellite_selection(self, view, path, column):
model = view.get_model()
self._url_entry.set_text(model.get(model.get_iter(path), 1)[0])
@run_idle
def on_load_providers(self, item):
self._expander.set_expanded(True)
self.on_info_bar_close()
self._cancel_button.show()
url = self._url_entry.get_text()
try:
exe = "{}wget".format("./" if GTK_PATH else "")
self._current_process = subprocess.Popen([exe, "-pkP", self._TMP_DIR, url],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
except FileNotFoundError as e:
self._cancel_button.hide()
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
GLib.io_add_watch(self._current_process.stderr, GLib.IO_IN, self.write_to_buffer)
model = self._providers_view.get_model()
model.clear()
self.append_providers(url, model)
@run_task
def append_providers(self, url, model):
self._current_process.wait()
try:
self._terminate = False
providers = parse_providers(self._TMP_DIR + url[url.find("w"):])
except FileNotFoundError:
pass # NOP
else:
if providers:
for p in providers:
if self._terminate:
return
model.append((self.get_pixbuf(p[0]) if p[0] else TV_ICON, *p[1:]))
self.update_receive_button_state()
finally:
GLib.idle_add(self._cancel_button.hide)
self._terminate = False
def get_pixbuf(self, img_url):
return GdkPixbuf.Pixbuf.new_from_file_at_scale(filename=self._TMP_DIR + "www.lyngsat.com/" + img_url,
width=48, height=48, preserve_aspect_ratio=True)
def on_receive(self, item):
self._cancel_button.show()
self.start_download()
@run_task
def start_download(self):
if self._current_process.poll() is None:
self.show_dialog("The task is already running!", DialogType.ERROR)
return
self._terminate = False
self._expander.set_expanded(True)
providers = self.get_selected_providers()
for prv in providers:
if not self._POS_PATTERN.match(prv[2]):
self.show_info_message(
get_message("Specify the correct position value for the provider!"), Gtk.MessageType.ERROR)
scroll_to(prv.path, self._providers_view)
return
try:
for prv in providers:
if self._terminate:
return
self.process_provider(Provider(*prv))
if not self._resize_no_radio_button.get_active():
self.resize(self._picons_dir_entry.get_text())
else:
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
finally:
GLib.idle_add(self._cancel_button.hide)
self._terminate = False
def process_provider(self, prv):
url = prv.url
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
exe = "{}wget".format("./" if GTK_PATH else "")
self._current_process = subprocess.Popen([exe, "-pkP", self._TMP_DIR, url],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
GLib.io_add_watch(self._current_process.stderr, GLib.IO_IN, self.write_to_buffer)
self._current_process.wait()
path = self._TMP_DIR + (url[url.find("//") + 2:] if prv.single else self._BASE_URL + url[url.rfind("/") + 1:])
PiconsParser.parse(path, self._picons_dir_entry.get_text(),
self._TMP_DIR, prv, self._picon_ids, self.get_picons_format())
def write_to_buffer(self, fd, condition):
if condition == GLib.IO_IN:
char = fd.read(1)
self.append_output(char)
return char
return False
@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)
try:
from pathlib import Path
from PIL import Image
except ImportError as e:
self.show_info_message("{} {}".format(get_message("Conversion error."), e), Gtk.MessageType.ERROR)
else:
res = (220, 132) if self._resize_220_132_radio_button.get_active() else (100, 60)
for img_file in Path(path).glob("*.png"):
img = Image.open(img_file)
img = img.resize(res, Image.ANTIALIAS)
img.save(img_file, "PNG", optimize=True)
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
def on_cancel(self, item=None):
if self.is_task_running() and show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return True
self.terminate_task()
@run_task
def terminate_task(self):
self._terminate = True
if self._current_process:
self._current_process.terminate()
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.save_window_size(window)
self.clean_data()
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 clean_data(self):
path = self._TMP_DIR + "www.lyngsat.com"
if os.path.exists(path):
shutil.rmtree(path)
@run_task
def run_func(self, func, update=False):
try:
GLib.idle_add(self._expander.set_expanded, True)
GLib.idle_add(self._explorer_action_box.set_sensitive, False)
func()
except OSError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
finally:
GLib.idle_add(self._explorer_action_box.set_sensitive, True)
if update:
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._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type)
self._message_label.set_text(get_message(text))
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):
model = self._providers_view.get_model()
model.set_value(model.get_iter(path), 7, not toggle.get_active())
self.update_receive_button_state()
def on_select_all(self, view):
self.update_selection(view, True)
def on_unselect_all(self, view):
self.update_selection(view, False)
def update_selection(self, view, select):
view.get_model().foreach(lambda mod, path, itr: mod.set_value(itr, 7, select))
self.update_receive_button_state()
# *********************** Filter **************************** #
def on_filter_toggled(self, button):
active = button.get_active()
self._filter_bar.set_search_mode(active)
if not active:
self._picons_filter_entry.set_text("")
def on_fiter_srcs_toggled(self, filter_model):
""" Activates re-filtering for model when filter check-button has toggled. """
GLib.idle_add(filter_model.refilter, priority=GLib.PRIORITY_LOW)
def on_filter_services_switch(self, button, state):
""" Activates or deactivates filtering in the main list of services. """
if state:
self._filter_binding = self._picons_filter_entry.bind_property("text", self._app.filter_entry, "text")
self._app.filter_entry.set_text(self._picons_filter_entry.get_text())
else:
if self._filter_binding:
self._filter_binding.unbind()
self._app.filter_entry.set_text("")
@run_with_delay(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)
def picons_src_filter_function(self, model, itr, data):
return self.filter_function(itr, model, self._src_filter_button.get_active())
def picons_dst_filter_function(self, model, itr, data):
return self.filter_function(itr, model, self._dst_filter_button.get_active())
def filter_function(self, itr, model, active):
""" Main filtering function. """
if any((not active, model is None, model == "None")):
return True
t = model.get_value(itr, 1)
if not t:
return True
txt = self._picons_filter_entry.get_text().upper()
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_toggle_button.get_active():
model, path = view.get_selection().get_selected_rows()
if not path:
return
row = model[path][:]
name, path = row[1], row[-1]
srv = self._services.get(row[1], None)
self.update_picon_info(name, path, srv)
def update_picon_info(self, name=None, path=None, srv=None):
self._picon_info_image.set_from_pixbuf(self.get_pixbuf_at_scale(path, 100, 60, True) if path else None)
self._picon_info_label.set_text(self.get_service_info(srv))
self._current_picon_info = (name, srv.fav_id) if srv else None
def get_service_info(self, srv):
""" Returns short info about the service. """
if not srv:
return ""
if srv.service_type == "IPTV":
return self._app.get_hint_for_srv_list(srv)
header, ref = self._app.get_hint_header_info(srv)
return "{} {}: {}\n{}: {} {}: {}\n{}".format(header.rstrip(), get_message("Package"), srv.package,
get_message("System"), srv.system, get_message("Freq"), srv.freq,
ref)
def on_tree_view_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_local_remove(view)
def on_url_changed(self, entry):
suit = self._PATTERN.search(entry.get_text())
entry.set_name("GtkEntry" if suit else "digit-entry")
self._load_providers_button.set_sensitive(suit if suit else False)
def on_position_edited(self, render, path, value):
model = self._providers_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")
is_explorer = name == "explorer"
self._explorer_action_box.set_visible(is_explorer)
if is_explorer:
self.on_picons_dest_changed(self._explorer_dest_path_button)
@run_idle
def on_convert(self, item):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
picons_path = self._enigma2_path_button.get_filename()
save_path = self._save_to_button.get_filename()
if not picons_path or not save_path:
show_dialog(DialogType.ERROR, transient=self._dialog, text="Select paths!")
return
self._expander.set_expanded(True)
convert_to(src_path=picons_path,
dest_path=save_path,
s_type=SettingsType.ENIGMA_2,
callback=self.append_output,
done_callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO))
@run_idle
def update_receive_button_state(self):
try:
self._receive_button.set_sensitive(len(self.get_selected_providers()) > 0)
except TypeError:
pass # NOP
def get_selected_providers(self):
""" returns selected providers """
return [r for r in self._providers_view.get_model() if r[7]]
@run_idle
def show_dialog(self, message, dialog_type):
show_dialog(dialog_type, self._dialog, message)
def get_picons_format(self):
picon_format = SettingsType.ENIGMA_2
if self._neutrino_mp_radio_button.get_active():
picon_format = SettingsType.NEUTRINO_MP
return picon_format
def is_task_running(self):
return self._current_process and self._current_process.poll() is None
if __name__ == "__main__":
pass

File diff suppressed because it is too large Load Diff

View File

@@ -9,10 +9,10 @@ from app.commons import run_idle, run_task
from app.eparser import get_satellites, write_satellites, Satellite, Transponder from app.eparser import get_satellites, write_satellites, Satellite, Transponder
from app.eparser.ecommons import PLS_MODE, get_key_by_value from app.eparser.ecommons import PLS_MODE, get_key_by_value
from app.tools.satellites import SatellitesParser, SatelliteSource from app.tools.satellites import SatellitesParser, SatelliteSource
from .search import SearchProvider from .dialogs import show_dialog, DialogType, get_dialogs_string, get_chooser_dialog
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, MOVE_KEYS, KeyboardKey, IS_GNOME_SESSION
from .dialogs import show_dialog, DialogType, get_dialogs_string
from .main_helper import move_items, scroll_to, append_text_to_tview, get_base_model, on_popup_menu 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, TEXT_DOMAIN, MOVE_KEYS, KeyboardKey, IS_GNOME_SESSION, MOD_MASK
_UI_PATH = UI_RESOURCES_PATH + "satellites_dialog.glade" _UI_PATH = UI_RESOURCES_PATH + "satellites_dialog.glade"
@@ -48,7 +48,8 @@ class SatellitesDialog:
builder.set_translation_domain(TEXT_DOMAIN) builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_string(get_dialogs_string(_UI_PATH), builder.add_objects_from_string(get_dialogs_string(_UI_PATH),
("satellites_editor_window", "satellites_tree_store", "popup_menu", ("satellites_editor_window", "satellites_tree_store", "popup_menu",
"left_header_menu", "popup_menu_add_image", "popup_menu_add_image_2")) "left_header_menu", "popup_menu_add_image", "popup_menu_add_image_2",
"sat_editor_save_image", "sat_editor_update_image"))
builder.connect_signals(handlers) builder.connect_signals(handlers)
self._window = builder.get_object("satellites_editor_window") self._window = builder.get_object("satellites_editor_window")
@@ -84,27 +85,17 @@ class SatellitesDialog:
@run_idle @run_idle
def on_open(self, model): def on_open(self, model):
response = self.get_file_dialog_response(Gtk.FileChooserAction.OPEN) response = get_chooser_dialog(self._window, self._settings, "satellites.xml", ("*.xml",))
if response == Gtk.ResponseType.CANCEL: if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return return
if not str(response).endswith("satellites.xml"): if not str(response).endswith("satellites.xml"):
show_dialog(DialogType.ERROR, self._window, text="No satellites.xml file is selected!") show_dialog(DialogType.ERROR, self._window, text="No satellites.xml file is selected!")
return return
self._data_path = response self._data_path = response
self.load_satellites_list(model) self.load_satellites_list(model)
def get_file_dialog_response(self, action: Gtk.FileChooserAction):
file_filter = Gtk.FileFilter()
file_filter.add_pattern("satellites.xml")
file_filter.set_name("satellites.xml")
response = show_dialog(dialog_type=DialogType.CHOOSER,
transient=self._window,
settings=self._settings,
action_type=action,
file_filter=file_filter)
return response
@staticmethod @staticmethod
def on_row_activated(view, path, column): def on_row_activated(view, path, column):
if view.row_expanded(path): if view.row_expanded(path):
@@ -124,7 +115,7 @@ class SatellitesDialog:
if not KeyboardKey.value_exist(key_code): if not KeyboardKey.value_exist(key_code):
return return
key = KeyboardKey(key_code) key = KeyboardKey(key_code)
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK ctrl = event.state & MOD_MASK
if key is KeyboardKey.DELETE: if key is KeyboardKey.DELETE:
self.on_remove(view) self.on_remove(view)
@@ -466,7 +457,8 @@ class SatellitesUpdateDialog:
("satellites_update_window", "update_source_store", "update_sat_list_store", ("satellites_update_window", "update_source_store", "update_sat_list_store",
"update_sat_list_model_filter", "update_sat_list_model_sort", "side_store", "update_sat_list_model_filter", "update_sat_list_model_sort", "side_store",
"pos_adjustment", "pos_adjustment2", "satellites_update_popup_menu", "pos_adjustment", "pos_adjustment2", "satellites_update_popup_menu",
"remove_selection_image")) "remove_selection_image", "sat_update_cancel_image", "sat_receive_image",
"sat_update_filter_image", "sat_update_search_image", "sat_update_image"))
builder.connect_signals(handlers) builder.connect_signals(handlers)
self._window = builder.get_object("satellites_update_window") self._window = builder.get_object("satellites_update_window")

View File

@@ -31,6 +31,8 @@ class SearchProvider:
if self._max_indexes > 0: if self._max_indexes > 0:
self.on_search_down() self.on_search_down()
self.update_navigation_buttons()
def scroll_to(self, index): def scroll_to(self, index):
view, path = self._paths[index] view, path = self._paths[index]
view.scroll_to_cell(path, None) view.scroll_to_cell(path, None)

View File

@@ -43,6 +43,19 @@
</row> </row>
</data> </data>
</object> </object>
<object class="GtkComboBox" id="rate_lp_combo_box">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="model">fec_list_store</property>
<property name="id_column">0</property>
<child>
<object class="GtkCellRendererText" id="rate_lp_cellrenderertext"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<object class="GtkListStore" id="invertion_list_store"> <object class="GtkListStore" id="invertion_list_store">
<columns> <columns>
<!-- column-name invertion --> <!-- column-name invertion -->
@@ -217,19 +230,6 @@
</row> </row>
</data> </data>
</object> </object>
<object class="GtkComboBox" id="rate_lp_combo_box">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">False</property>
<property name="model">fec_list_store</property>
<property name="id_column">0</property>
<child>
<object class="GtkCellRendererText" id="rate_lp_cellrenderertext"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<object class="GtkDialog" id="service_details_dialog"> <object class="GtkDialog" id="service_details_dialog">
<property name="use-header-bar">{use_header}</property> <property name="use-header-bar">{use_header}</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
@@ -246,40 +246,6 @@
<child type="titlebar"> <child type="titlebar">
<placeholder/> <placeholder/>
</child> </child>
<child type="action">
<object class="GtkButton" id="cancel_button">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Cancel</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
</object>
</child>
<child type="action">
<object class="GtkButton" id="apply_button">
<property name="label">gtk-save</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="use_stock">True</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_save" swapped="no"/>
</object>
</child>
<child type="action">
<object class="GtkButton" id="create_button">
<property name="label">gtk-new</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="use_stock">True</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_create_new" swapped="no"/>
</object>
</child>
<child internal-child="vbox"> <child internal-child="vbox">
<object class="GtkBox" id="dialog_vbox"> <object class="GtkBox" id="dialog_vbox">
<property name="can_focus">False</property> <property name="can_focus">False</property>
@@ -290,6 +256,54 @@
<child internal-child="action_area"> <child internal-child="action_area">
<object class="GtkButtonBox"> <object class="GtkButtonBox">
<property name="can_focus">False</property> <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>
</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> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@@ -335,7 +349,7 @@
<object class="GtkEntry" id="name_entry"> <object class="GtkEntry" id="name_entry">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="primary_icon_stock">gtk-edit</property> <property name="primary_icon_name">document-edit-symbolic</property>
</object> </object>
<packing> <packing>
<property name="left_attach">0</property> <property name="left_attach">0</property>
@@ -357,7 +371,7 @@
<object class="GtkEntry" id="package_entry"> <object class="GtkEntry" id="package_entry">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="primary_icon_stock">gtk-edit</property> <property name="primary_icon_name">document-edit-symbolic</property>
</object> </object>
<packing> <packing>
<property name="left_attach">1</property> <property name="left_attach">1</property>
@@ -381,7 +395,7 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="width_chars">10</property> <property name="width_chars">10</property>
<property name="max_width_chars">10</property> <property name="max_width_chars">10</property>
<property name="primary_icon_stock">gtk-edit</property> <property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/> <signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
<signal name="key-release-event" handler="update_reference" swapped="no"/> <signal name="key-release-event" handler="update_reference" swapped="no"/>
</object> </object>
@@ -426,7 +440,7 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="width_chars">7</property> <property name="width_chars">7</property>
<property name="max_width_chars">7</property> <property name="max_width_chars">7</property>
<property name="primary_icon_stock">gtk-edit</property> <property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/> <signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
<signal name="changed" handler="update_reference" swapped="no"/> <signal name="changed" handler="update_reference" swapped="no"/>
</object> </object>
@@ -864,7 +878,7 @@
<property name="tooltip_text">C:0000,C:a1b2,etc.</property> <property name="tooltip_text">C:0000,C:a1b2,etc.</property>
<property name="width_chars">15</property> <property name="width_chars">15</property>
<property name="max_width_chars">26</property> <property name="max_width_chars">26</property>
<property name="primary_icon_stock">gtk-edit</property> <property name="primary_icon_name">document-edit-symbolic</property>
<property name="placeholder_text" translatable="yes">C:0000,C:a1b2,etc.</property> <property name="placeholder_text" translatable="yes">C:0000,C:a1b2,etc.</property>
<signal name="changed" handler="on_cas_entry_changed" swapped="no"/> <signal name="changed" handler="on_cas_entry_changed" swapped="no"/>
</object> </object>
@@ -967,7 +981,7 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="width_chars">12</property> <property name="width_chars">12</property>
<property name="max_width_chars">12</property> <property name="max_width_chars">12</property>
<property name="primary_icon_stock">gtk-edit</property> <property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/> <signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
</object> </object>
<packing> <packing>
@@ -993,7 +1007,7 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="width_chars">12</property> <property name="width_chars">12</property>
<property name="max_width_chars">12</property> <property name="max_width_chars">12</property>
<property name="primary_icon_stock">gtk-edit</property> <property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/> <signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
</object> </object>
<packing> <packing>
@@ -1072,7 +1086,7 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="width_chars">12</property> <property name="width_chars">12</property>
<property name="max_width_chars">12</property> <property name="max_width_chars">12</property>
<property name="primary_icon_stock">gtk-edit</property> <property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/> <signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
<signal name="key-release-event" handler="update_reference" swapped="no"/> <signal name="key-release-event" handler="update_reference" swapped="no"/>
</object> </object>
@@ -1125,7 +1139,7 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="width_chars">8</property> <property name="width_chars">8</property>
<property name="max_width_chars">10</property> <property name="max_width_chars">10</property>
<property name="primary_icon_stock">gtk-edit</property> <property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/> <signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
<signal name="key-release-event" handler="update_reference" swapped="no"/> <signal name="key-release-event" handler="update_reference" swapped="no"/>
</object> </object>
@@ -1152,7 +1166,7 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="width_chars">8</property> <property name="width_chars">8</property>
<property name="max_width_chars">10</property> <property name="max_width_chars">10</property>
<property name="primary_icon_stock">gtk-edit</property> <property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/> <signal name="changed" handler="on_non_empty_entry_changed" swapped="no"/>
<signal name="key-release-event" handler="update_reference" swapped="no"/> <signal name="key-release-event" handler="update_reference" swapped="no"/>
</object> </object>
@@ -1317,7 +1331,7 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="width_chars">8</property> <property name="width_chars">8</property>
<property name="max_width_chars">10</property> <property name="max_width_chars">10</property>
<property name="primary_icon_stock">gtk-edit</property> <property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_digit_entry_changed" swapped="no"/> <signal name="changed" handler="on_digit_entry_changed" swapped="no"/>
</object> </object>
<packing> <packing>
@@ -1343,7 +1357,7 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="width_chars">8</property> <property name="width_chars">8</property>
<property name="max_width_chars">10</property> <property name="max_width_chars">10</property>
<property name="primary_icon_stock">gtk-edit</property> <property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_digit_entry_changed" swapped="no"/> <signal name="changed" handler="on_digit_entry_changed" swapped="no"/>
</object> </object>
<packing> <packing>
@@ -1369,7 +1383,7 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="width_chars">8</property> <property name="width_chars">8</property>
<property name="max_width_chars">10</property> <property name="max_width_chars">10</property>
<property name="primary_icon_stock">gtk-edit</property> <property name="primary_icon_name">document-edit-symbolic</property>
<signal name="changed" handler="on_digit_entry_changed" swapped="no"/> <signal name="changed" handler="on_digit_entry_changed" swapped="no"/>
</object> </object>
<packing> <packing>
@@ -1498,7 +1512,7 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_left">10</property> <property name="margin_left">10</property>
<property name="stock">gtk-edit</property> <property name="icon_name">document-edit-symbolic</property>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@@ -1562,26 +1576,6 @@
<child> <child>
<placeholder/> <placeholder/>
</child> </child>
<child type="action">
<object class="GtkButton" id="tr_services_no_button">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
</object>
</child>
<child type="action">
<object class="GtkButton" id="tr_services_ok_button">
<property name="label">gtk-ok</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
</object>
</child>
<child internal-child="vbox"> <child internal-child="vbox">
<object class="GtkBox" id="tr_services_dialog_vbox"> <object class="GtkBox" id="tr_services_dialog_vbox">
<property name="can_focus">False</property> <property name="can_focus">False</property>
@@ -1590,6 +1584,35 @@
<child internal-child="action_area"> <child internal-child="action_area">
<object class="GtkButtonBox"> <object class="GtkButtonBox">
<property name="can_focus">False</property> <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> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@@ -1616,7 +1639,7 @@
<property name="margin_right">20</property> <property name="margin_right">20</property>
<property name="margin_top">5</property> <property name="margin_top">5</property>
<property name="margin_bottom">5</property> <property name="margin_bottom">5</property>
<property name="label" translatable="yes">Changes will be applied to all services of this transponder! <property name="label" translatable="yes">Changes will be applied to all services of this transponder!
Continue?</property> Continue?</property>
<property name="justify">center</property> <property name="justify">center</property>
<property name="lines">2</property> <property name="lines">2</property>

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
import os import os
import re
from enum import Enum from enum import Enum
from pathlib import Path
from app.commons import run_task, run_idle from app.commons import run_task, run_idle, log
from app.connections import test_telnet, test_ftp, TestException, test_http, HttpApiException from app.connections import test_telnet, test_ftp, TestException, test_http, HttpApiException
from app.settings import SettingsType, Settings from app.settings import SettingsType, Settings, PlayStreamsMode
from app.ui.dialogs import show_dialog, DialogType from app.ui.dialogs import show_dialog, DialogType, get_message, get_chooser_dialog
from .main_helper import update_entry_data, scroll_to from .main_helper import update_entry_data, scroll_to, get_picon_pixbuf
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON
@@ -21,6 +21,8 @@ class Property(Enum):
class SettingsDialog: class SettingsDialog:
_DIGIT_ENTRY_NAME = "digit-entry"
_DIGIT_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
def __init__(self, transient, settings: Settings): def __init__(self, transient, settings: Settings):
handlers = {"on_field_icon_press": self.on_field_icon_press, handlers = {"on_field_icon_press": self.on_field_icon_press,
@@ -31,10 +33,13 @@ class SettingsDialog:
"on_apply_profile_settings": self.on_apply_profile_settings, "on_apply_profile_settings": self.on_apply_profile_settings,
"on_connection_test": self.on_connection_test, "on_connection_test": self.on_connection_test,
"on_info_bar_close": self.on_info_bar_close, "on_info_bar_close": self.on_info_bar_close,
"on_set_color_switch_state": self.on_set_color_switch_state, "on_set_color_switch": self.on_set_color_switch,
"on_http_mode_switch_state": self.on_http_mode_switch_state, "on_force_bq_name": self.on_force_bq_name,
"on_yt_dl_switch_state": self.on_yt_dl_switch_state, "on_http_mode_switch": self.on_http_mode_switch,
"on_send_to_switch_state": self.on_send_to_switch_state, "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_add": self.on_profile_add,
"on_profile_edit": self.on_profile_edit, "on_profile_edit": self.on_profile_edit,
"on_profile_remove": self.on_profile_remove, "on_profile_remove": self.on_profile_remove,
@@ -48,7 +53,23 @@ class SettingsDialog:
"on_network_settings_visible": self.on_network_settings_visible, "on_network_settings_visible": self.on_network_settings_visible,
"on_http_use_ssl_toggled": self.on_http_use_ssl_toggled, "on_http_use_ssl_toggled": self.on_http_use_ssl_toggled,
"on_click_mode_togged": self.on_click_mode_togged, "on_click_mode_togged": self.on_click_mode_togged,
"on_view_popup_menu": self.on_view_popup_menu} "on_play_mode_changed": self.on_play_mode_changed,
"on_transcoding_preset_changed": self.on_transcoding_preset_changed,
"on_apply_presets": self.on_apply_presets,
"on_digit_entry_changed": self.on_digit_entry_changed,
"on_view_popup_menu": self.on_view_popup_menu,
"on_theme_changed": self.on_theme_changed,
"on_theme_add": self.on_theme_add,
"on_theme_remove": self.on_theme_remove,
"on_icon_theme_changed": self.on_icon_theme_changed,
"on_icon_theme_add": self.on_icon_theme_add,
"on_icon_theme_remove": self.on_icon_theme_remove}
# Settings
self._ext_settings = settings
self._settings = Settings(settings.settings)
self._profiles = self._settings.profiles
self._s_type = self._settings.setting_type
builder = Gtk.Builder() builder = Gtk.Builder()
builder.add_from_file(UI_RESOURCES_PATH + "settings_dialog.glade") builder.add_from_file(UI_RESOURCES_PATH + "settings_dialog.glade")
@@ -80,6 +101,9 @@ class SettingsDialog:
self._picons_field = builder.get_object("picons_field") self._picons_field = builder.get_object("picons_field")
self._picons_dir_field = builder.get_object("picons_dir_field") self._picons_dir_field = builder.get_object("picons_dir_field")
self._backup_dir_field = builder.get_object("backup_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")
# Info bar # Info bar
self._info_bar = builder.get_object("info_bar") self._info_bar = builder.get_object("info_bar")
self._message_label = builder.get_object("info_bar_message_label") self._message_label = builder.get_object("info_bar_message_label")
@@ -88,19 +112,42 @@ class SettingsDialog:
self._enigma_radio_button = builder.get_object("enigma_radio_button") self._enigma_radio_button = builder.get_object("enigma_radio_button")
self._neutrino_radio_button = builder.get_object("neutrino_radio_button") self._neutrino_radio_button = builder.get_object("neutrino_radio_button")
self._support_ver5_switch = builder.get_object("support_ver5_switch") 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")
self._presets_combo_box = builder.get_object("presets_combo_box")
self._video_bitrate_field = builder.get_object("video_bitrate_field")
self._video_width_field = builder.get_object("video_width_field")
self._video_height_field = builder.get_object("video_height_field")
self._audio_bitrate_field = builder.get_object("audio_bitrate_field")
self._audio_channels_combo_box = builder.get_object("audio_channels_combo_box")
self._audio_sample_rate_combo_box = builder.get_object("audio_sample_rate_combo_box")
self._audio_codec_combo_box = builder.get_object("audio_codec_combo_box")
self._transcoding_switch.bind_property("active", builder.get_object("record_box"), "sensitive")
self._edit_preset_switch.bind_property("active", self._apply_presets_button, "sensitive")
self._edit_preset_switch.bind_property("active", builder.get_object("video_options_frame"), "sensitive")
self._edit_preset_switch.bind_property("active", builder.get_object("audio_options_frame"), "sensitive")
self._play_in_built_radio_button = builder.get_object("play_in_built_radio_button")
self._play_in_vlc_radio_button = builder.get_object("play_in_vlc_radio_button")
self._get_m3u_radio_button = builder.get_object("get_m3u_radio_button")
# Program # Program
self._before_save_switch = builder.get_object("before_save_switch") self._before_save_switch = builder.get_object("before_save_switch")
self._before_downloading_switch = builder.get_object("before_downloading_switch") self._before_downloading_switch = builder.get_object("before_downloading_switch")
self._program_frame = builder.get_object("program_frame") self._enable_experimental_box = builder.get_object("enable_experimental_box")
self._extra_support_grid = builder.get_object("extra_support_grid")
self._colors_grid = builder.get_object("colors_grid") self._colors_grid = builder.get_object("colors_grid")
self._set_color_switch = builder.get_object("set_color_switch") self._set_color_switch = builder.get_object("set_color_switch")
self._new_color_button = builder.get_object("new_color_button") self._new_color_button = builder.get_object("new_color_button")
self._extra_color_button = builder.get_object("extra_color_button") self._extra_color_button = builder.get_object("extra_color_button")
self._load_on_startup_switch = builder.get_object("load_on_startup_switch") self._load_on_startup_switch = builder.get_object("load_on_startup_switch")
# HTTP API 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")
# Extra
self._support_http_api_switch = builder.get_object("support_http_api_switch") self._support_http_api_switch = builder.get_object("support_http_api_switch")
self._enable_y_dl_switch = builder.get_object("enable_y_dl_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._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_disabled_button = builder.get_object("click_mode_disabled_button")
self._click_mode_stream_button = builder.get_object("click_mode_stream_button") self._click_mode_stream_button = builder.get_object("click_mode_stream_button")
@@ -109,38 +156,57 @@ class SettingsDialog:
self._click_mode_zap_and_play_button = builder.get_object("click_mode_zap_and_play_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_play_button, "sensitive")
self._click_mode_zap_button.bind_property("sensitive", self._click_mode_zap_and_play_button, "sensitive") self._click_mode_zap_button.bind_property("sensitive", self._click_mode_zap_and_play_button, "sensitive")
self._click_mode_zap_button.bind_property("sensitive", self._enable_send_to_switch, "sensitive") # EXPERIMENTAL
self._enable_send_to_switch.bind_property("sensitive", builder.get_object("enable_send_to_label"), "sensitive") self._enable_exp_switch = builder.get_object("enable_experimental_switch")
self._extra_support_grid.bind_property("sensitive", builder.get_object("v5_support_grid"), "sensitive") 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")
# Profiles # Profiles
self._profile_view = builder.get_object("profile_tree_view") self._profile_view = builder.get_object("profile_tree_view")
self._profile_add_button = builder.get_object("profile_add_button") self._profile_add_button = builder.get_object("profile_add_button")
self._profile_remove_button = builder.get_object("profile_remove_button") self._profile_remove_button = builder.get_object("profile_remove_button")
self._apply_profile_button = builder.get_object("apply_profile_button") self._apply_profile_button = builder.get_object("apply_profile_button")
self._apply_profile_button.bind_property("visible", builder.get_object("header_separator"), "visible")
self._apply_profile_button.bind_property("visible", builder.get_object("reset_button"), "visible") self._apply_profile_button.bind_property("visible", builder.get_object("reset_button"), "visible")
# Language # Style
self._lang_combo_box = builder.get_object("lang_combo_box") self._style_provider = Gtk.CssProvider()
# Settings self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._ext_settings = settings self._digit_elems = (self._port_field, self._http_port_field, self._telnet_port_field, self._video_width_field,
self._settings = Settings(settings.settings) self._video_bitrate_field, self._video_height_field, self._audio_bitrate_field)
self._profiles = self._settings.profiles for el in self._digit_elems:
self._s_type = self._settings.setting_type el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
self.set_settings() Gtk.STYLE_PROVIDER_PRIORITY_USER)
self.init_ui_elements(self._s_type) self.init_ui_elements(self._s_type)
self.init_profiles() self.init_profiles()
if self._settings.is_darwin:
# Appearance
self._appearance_box = builder.get_object("appearance_box")
self._appearance_box.set_visible(True)
self._theme_thumbnail_image = builder.get_object("theme_thumbnail_image")
self._theme_combo_box = builder.get_object("theme_combo_box")
self._icon_theme_combo_box = builder.get_object("icon_theme_combo_box")
self._dark_mode_switch = builder.get_object("dark_mode_switch")
self._themes_support_switch = builder.get_object("themes_support_switch")
self._themes_support_switch.bind_property("active", builder.get_object("gtk_theme_frame"), "sensitive")
self._themes_support_switch.bind_property("active", builder.get_object("icon_theme_frame"), "sensitive")
self.init_appearance()
@run_idle @run_idle
def init_ui_elements(self, s_type): def init_ui_elements(self, s_type):
is_enigma_profile = s_type is SettingsType.ENIGMA_2 is_enigma_profile = s_type is SettingsType.ENIGMA_2
self._neutrino_radio_button.set_active(s_type is SettingsType.NEUTRINO_MP) self._neutrino_radio_button.set_active(s_type is SettingsType.NEUTRINO_MP)
self.update_header_bar() self.update_title()
self._settings_stack.get_child_by_name(Property.HTTP.value).set_visible(is_enigma_profile) self._settings_stack.get_child_by_name(Property.HTTP.value).set_visible(is_enigma_profile)
self._program_frame.set_sensitive(is_enigma_profile)
self._extra_support_grid.set_sensitive(is_enigma_profile)
http_active = self._support_http_api_switch.get_active() http_active = self._support_http_api_switch.get_active()
self._click_mode_zap_button.set_sensitive(is_enigma_profile and http_active) self._click_mode_zap_button.set_sensitive(is_enigma_profile and http_active)
self._lang_combo_box.set_active_id(self._settings.language) 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( 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) "The Neutrino has only experimental support. Not all features are supported!", Gtk.MessageType.WARNING)
@@ -152,14 +218,15 @@ class SettingsDialog:
model.append((p, icon)) model.append((p, icon))
if icon: if icon:
scroll_to(ind, self._profile_view) scroll_to(ind, self._profile_view)
self.on_profile_selected(self._profile_view, False)
self._profile_remove_button.set_sensitive(len(self._profile_view.get_model()) > 1) self._profile_remove_button.set_sensitive(len(self._profile_view.get_model()) > 1)
def update_header_bar(self): def update_title(self):
label, sep, st = self._header_bar.get_subtitle().partition(":") title = "{} [{}]"
if self._s_type is SettingsType.ENIGMA_2: if self._s_type is SettingsType.ENIGMA_2:
self._header_bar.set_subtitle("{}: {}".format(label, self._enigma_radio_button.get_label())) self._dialog.set_title(title.format(get_message("Options"), self._enigma_radio_button.get_label()))
elif self._s_type is SettingsType.NEUTRINO_MP: elif self._s_type is SettingsType.NEUTRINO_MP:
self._header_bar.set_subtitle("{}: {}".format(label, self._neutrino_radio_button.get_label())) self._dialog.set_title(title.format(get_message("Options"), self._neutrino_radio_button.get_label()))
def show(self): def show(self):
self._dialog.run() self._dialog.run()
@@ -207,15 +274,27 @@ class SettingsDialog:
self._data_dir_field.set_text(self._settings.data_local_path) self._data_dir_field.set_text(self._settings.data_local_path)
self._picons_dir_field.set_text(self._settings.picons_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._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_save_switch.set_active(self._settings.backup_before_save)
self._before_downloading_switch.set_active(self._settings.backup_before_downloading) self._before_downloading_switch.set_active(self._settings.backup_before_downloading)
self.set_fav_click_mode(self._settings.fav_click_mode) self.set_fav_click_mode(self._settings.fav_click_mode)
self.set_play_stream_mode(self._settings.play_streams_mode)
self._load_on_startup_switch.set_active(self._settings.load_last_config) 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)
self._default_data_paths_switch.set_active(self._settings.profile_folder_is_default)
self._transcoding_switch.set_active(self._settings.activate_transcoding)
self._presets_combo_box.set_active_id(self._settings.active_preset)
self.on_transcoding_preset_changed(self._presets_combo_box)
if self._s_type is SettingsType.ENIGMA_2: 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._support_ver5_switch.set_active(self._settings.v5_support)
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._support_http_api_switch.set_active(self._settings.http_api_support)
self._enable_y_dl_switch.set_active(self._settings.enable_yt_dl) 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) self._enable_send_to_switch.set_active(self._settings.enable_send_to)
self._set_color_switch.set_active(self._settings.use_colors) self._set_color_switch.set_active(self._settings.use_colors)
new_rgb = Gdk.RGBA() new_rgb = Gdk.RGBA()
@@ -230,7 +309,11 @@ class SettingsDialog:
else: else:
self._neutrino_radio_button.activate() self._neutrino_radio_button.activate()
def on_apply_profile_settings(self, item): def on_apply_profile_settings(self, item=None):
if not self.is_data_correct(self._digit_elems):
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
self._s_type = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP self._s_type = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP
self._settings.setting_type = self._s_type self._settings.setting_type = self._s_type
self._settings.host = self._host_field.get_text() self._settings.host = self._host_field.get_text()
@@ -254,23 +337,41 @@ class SettingsDialog:
self._settings.backup_local_path = self._backup_dir_field.get_text() self._settings.backup_local_path = self._backup_dir_field.get_text()
def apply_settings(self, item=None): def apply_settings(self, item=None):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL: if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.OK:
return return
self.on_apply_profile_settings()
self._ext_settings.profiles = self._settings.profiles self._ext_settings.profiles = self._settings.profiles
self._ext_settings.backup_before_save = self._before_save_switch.get_active() 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.backup_before_downloading = self._before_downloading_switch.get_active()
self._ext_settings.fav_click_mode = self.get_fav_click_mode() 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.language = self._lang_combo_box.get_active_id() 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.load_last_config = self._load_on_startup_switch.get_active()
self._ext_settings.show_bq_hints = self._bouquet_hints_switch.get_active()
self._ext_settings.show_srv_hints = self._services_hints_switch.get_active()
self._ext_settings.profile_folder_is_default = self._default_data_paths_switch.get_active()
self._ext_settings.default_data_path = self._default_data_dir_field.get_text()
self._ext_settings.records_path = self._record_data_dir_field.get_text()
self._ext_settings.activate_transcoding = self._transcoding_switch.get_active()
self._ext_settings.active_preset = self._presets_combo_box.get_active_id()
if self._ext_settings.is_darwin:
self._ext_settings.dark_mode = self._dark_mode_switch.get_active()
self._ext_settings.is_themes_support = self._themes_support_switch.get_active()
self._ext_settings.theme = self._theme_combo_box.get_active_id()
self._ext_settings.icon_theme = self._icon_theme_combo_box.get_active_id()
if self._s_type is SettingsType.ENIGMA_2: if self._s_type is SettingsType.ENIGMA_2:
self._ext_settings.is_enable_experimental = self._enable_exp_switch.get_active()
self._ext_settings.use_colors = self._set_color_switch.get_active() self._ext_settings.use_colors = self._set_color_switch.get_active()
self._ext_settings.new_color = self._new_color_button.get_rgba().to_string() 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.extra_color = self._extra_color_button.get_rgba().to_string()
self._ext_settings.v5_support = self._support_ver5_switch.get_active() self._ext_settings.v5_support = self._support_ver5_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.http_api_support = self._support_http_api_switch.get_active()
self._ext_settings.enable_yt_dl = self._enable_y_dl_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.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.default_profile = list(filter(lambda r: r[1], self._profile_view.get_model()))[0][0]
@@ -328,7 +429,7 @@ class SettingsDialog:
def show_info_message(self, text, message_type): def show_info_message(self, text, message_type):
self._info_bar.set_visible(True) self._info_bar.set_visible(True)
self._info_bar.set_message_type(message_type) self._info_bar.set_message_type(message_type)
self._message_label.set_text(text) self._message_label.set_text(get_message(text))
@run_idle @run_idle
def show_spinner(self, show): def show_spinner(self, show):
@@ -338,21 +439,40 @@ class SettingsDialog:
def on_info_bar_close(self, bar=None, resp=None): def on_info_bar_close(self, bar=None, resp=None):
self._info_bar.set_visible(False) self._info_bar.set_visible(False)
def on_set_color_switch_state(self, switch, state): def on_set_color_switch(self, switch, state):
self._colors_grid.set_sensitive(state) self._colors_grid.set_sensitive(state)
def on_http_mode_switch_state(self, switch, state): def on_http_mode_switch(self, switch, state):
self._click_mode_zap_button.set_sensitive(state) self._click_mode_zap_button.set_sensitive(state)
if any((self._click_mode_play_button.get_active(), if any((self._click_mode_play_button.get_active(),
self._click_mode_zap_button.get_active(), self._click_mode_zap_button.get_active(),
self._click_mode_zap_and_play_button.get_active())): self._click_mode_zap_and_play_button.get_active())):
self._click_mode_disabled_button.set_active(True) self._click_mode_disabled_button.set_active(True)
def on_yt_dl_switch_state(self, switch, state): def on_experimental_switch(self, switch, state):
if not state:
self._support_ver5_switch.set_active(state)
self._enable_send_to_switch.set_active(state)
self._enable_yt_dl_switch.set_active(state)
def on_force_bq_name(self, switch, state):
if self._main_stack.get_visible_child_name() != "extra":
return
if state:
msg = "Some images may have problems displaying the favorites list!"
self.show_info_message(msg, Gtk.MessageType.WARNING)
else:
self.on_info_bar_close()
def on_yt_dl_switch(self, switch, state):
self.show_info_message("Not implemented yet!", Gtk.MessageType.WARNING) self.show_info_message("Not implemented yet!", Gtk.MessageType.WARNING)
def on_send_to_switch_state(self, switch, state): def on_default_path_mode_switch(self, switch, state):
self.show_info_message("Not implemented yet!", Gtk.MessageType.WARNING) 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): def on_profile_add(self, item):
model = self._profile_view.get_model() model = self._profile_view.get_model()
@@ -365,11 +485,7 @@ class SettingsDialog:
self._profiles[name] = self._s_type.get_default_settings() self._profiles[name] = self._s_type.get_default_settings()
model.append((name, None)) model.append((name, None))
scroll_to(len(model) - 1, self._profile_view) scroll_to(len(model) - 1, self._profile_view)
self.on_profile_selected(self._profile_view) self.on_profile_selected(self._profile_view, False)
p = name + "/"
self._settings.data_local_path += p
self._settings.picons_local_path += p
self._settings.backup_local_path += p
self.on_reset() self.on_reset()
def on_profile_edit(self, item=None): def on_profile_edit(self, item=None):
@@ -392,29 +508,29 @@ class SettingsDialog:
def on_profile_edited(self, render, path, new_value): def on_profile_edited(self, render, path, new_value):
row = self._profile_view.get_model()[path] row = self._profile_view.get_model()[path]
p_name = row[0] old_name = row[0]
if p_name == new_value: if old_name == new_value:
return return
if new_value in self._profiles: if new_value in self._profiles:
show_dialog(DialogType.ERROR, self._dialog, "A profile with that name exists!") show_dialog(DialogType.ERROR, self._dialog, "A profile with that name exists!")
return return
p_name = self._profiles.pop(p_name, None) p_settings = self._profiles.pop(old_name, None)
if p_name: if p_settings:
row[0] = new_value row[0] = new_value
self._profiles[new_value] = p_name self._profiles[new_value] = p_settings
self.update_local_paths(new_value) self.update_local_paths(new_value, old_name)
self.on_profile_selected(self._profile_view) self.on_profile_selected(self._profile_view, False)
def update_local_paths(self, p_name, force_rename=False): def update_local_paths(self, p_name, old_name, force_rename=False):
data_path = self._settings.data_local_path data_path = self._settings.data_local_path
picons_path = self._settings.picons_local_path picons_path = self._settings.picons_local_path
backup_path = self._settings.backup_local_path backup_path = self._settings.backup_local_path
self._settings.data_local_path = "{}/{}/".format(Path(data_path).parent, p_name) self._settings.data_local_path = p_name.join(data_path.rsplit(old_name, 1))
self._settings.picons_local_path = "{}/{}/".format(Path(picons_path).parent, p_name) self._settings.picons_local_path = p_name.join(picons_path.rsplit(old_name, 1))
self._settings.backup_local_path = "{}/{}/".format(Path(backup_path).parent, p_name) self._settings.backup_local_path = p_name.join(backup_path.rsplit(old_name, 1))
if force_rename: if force_rename:
try: try:
@@ -427,7 +543,10 @@ class SettingsDialog:
except OSError as e: except OSError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR) self.show_info_message(str(e), Gtk.MessageType.ERROR)
def on_profile_selected(self, view): def on_profile_selected(self, view, force=True):
if force:
self.on_apply_profile_settings()
model, paths = self._profile_view.get_selection().get_selected_rows() model, paths = self._profile_view.get_selection().get_selected_rows()
if paths: if paths:
profile = model.get_value(model.get_iter(paths), 0) profile = model.get_value(model.get_iter(paths), 0)
@@ -450,7 +569,9 @@ class SettingsDialog:
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING) self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
def on_main_settings_visible(self, stack, param): def on_main_settings_visible(self, stack, param):
self._apply_profile_button.set_visible(stack.get_visible_child_name() == "profiles") name = stack.get_visible_child_name()
self._apply_profile_button.set_visible(name == "profiles")
self._apply_presets_button.set_visible(name == "streaming")
def on_network_settings_visible(self, stack, param): def on_network_settings_visible(self, stack, param):
self._http_use_ssl_check_button.set_visible(Property(stack.get_visible_child_name()) is Property.HTTP) self._http_use_ssl_check_button.set_visible(Property(stack.get_visible_child_name()) is Property.HTTP)
@@ -493,10 +614,190 @@ class SettingsDialog:
return FavClickMode.DISABLED return FavClickMode.DISABLED
def on_play_mode_changed(self, button):
if self._main_stack.get_visible_child_name() != "streaming":
return
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_sensitive(not self._settings.is_darwin)
self._play_in_built_radio_button.set_active(mode is PlayStreamsMode.BUILT_IN)
self._play_in_vlc_radio_button.set_active(mode is PlayStreamsMode.VLC)
self._get_m3u_radio_button.set_active(mode is PlayStreamsMode.M3U)
def get_play_stream_mode(self):
if self._play_in_built_radio_button.get_active():
return PlayStreamsMode.BUILT_IN
if self._play_in_vlc_radio_button.get_active():
return PlayStreamsMode.VLC
if self._get_m3u_radio_button.get_active():
return PlayStreamsMode.M3U
return self._settings.play_streams_mode
def on_transcoding_preset_changed(self, button):
presets = self._settings.transcoding_presets
prs = presets.get(button.get_active_id())
self._video_bitrate_field.set_text(prs.get("vb", "0"))
self._video_width_field.set_text(prs.get("width", "0"))
self._video_height_field.set_text(prs.get("height", "0"))
self._audio_bitrate_field.set_text(prs.get("ab", "0"))
self._audio_channels_combo_box.set_active_id(prs.get("channels", "2"))
self._audio_sample_rate_combo_box.set_active_id(prs.get("samplerate", "44100"))
self._audio_codec_combo_box.set_active_id(prs.get("acodec", "mp3"))
def on_apply_presets(self, item):
if not self.is_data_correct(self._digit_elems):
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
return
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
return
presets = self._settings.transcoding_presets
prs = presets.get(self._presets_combo_box.get_active_id())
prs["vb"] = self._video_bitrate_field.get_text()
prs["width"] = self._video_width_field.get_text()
prs["height"] = self._video_height_field.get_text()
prs["ab"] = self._audio_bitrate_field.get_text()
prs["channels"] = self._audio_channels_combo_box.get_active_id()
prs["samplerate"] = self._audio_sample_rate_combo_box.get_active_id()
prs["acodec"] = self._audio_codec_combo_box.get_active_id()
self._ext_settings.transcoding_presets = presets
self._edit_preset_switch.set_active(False)
def on_digit_entry_changed(self, entry):
if self._DIGIT_PATTERN.search(entry.get_text()):
entry.set_name(self._DIGIT_ENTRY_NAME)
else:
entry.set_name("GtkEntry")
def is_data_correct(self, elems):
return not any(elem.get_name() == self._DIGIT_ENTRY_NAME for elem in elems)
def on_view_popup_menu(self, menu, event): def on_view_popup_menu(self, menu, event):
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY: 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) menu.popup(None, None, None, None, event.button, event.time)
def on_theme_changed(self, button):
if self._main_stack.get_visible_child_name() != "appearance":
return
self.set_theme_thumbnail_image(button.get_active_id())
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
@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)
self._theme_thumbnail_image.set_from_pixbuf(get_picon_pixbuf(img_path, 96))
def on_theme_add(self, button):
self.add_theme(self._ext_settings.themes_path, self._theme_combo_box)
def on_theme_remove(self, button):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.OK:
Gtk.Settings().get_default().set_property("gtk-theme-name", "")
self.remove_theme(self._theme_combo_box, self._ext_settings.themes_path)
def on_icon_theme_changed(self, button, state=False):
if self._main_stack.get_visible_child_name() != "appearance":
return
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
def on_icon_theme_add(self, button):
self.add_theme(self._ext_settings.icon_themes_path, self._icon_theme_combo_box)
def on_icon_theme_remove(self, button):
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.OK:
Gtk.Settings().get_default().set_property("gtk-icon-theme-name", "")
self.remove_theme(self._icon_theme_combo_box, self._ext_settings.icon_themes_path)
@run_idle
def add_theme(self, path, button):
response = get_chooser_dialog(self._dialog, self._settings, "Themes Archive [*.xz, *.zip]", ("*.xz", "*.zip"))
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
return
self._appearance_box.set_sensitive(False)
self.unpack_theme(response, path, button)
@run_task
def unpack_theme(self, src, dst, button):
try:
os.makedirs(os.path.dirname(dst), exist_ok=True)
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.")
finally:
self.update_theme_button(button, dst)
self._appearance_box.set_sensitive(True)
@run_idle
def update_theme_button(self, button, dst):
exist = set(os.listdir(dst))
current = {r[0] for r in button.get_model()}
added = exist - current
if added:
theme = added.pop()
if theme not in current:
button.append(theme, theme)
button.set_active_id(theme)
self.show_info_message("Done!", Gtk.MessageType.INFO)
@run_idle
def remove_theme(self, button, path):
theme = button.get_active_id()
if not theme:
self.show_info_message("No selected item!", Gtk.MessageType.ERROR)
return
from shutil import rmtree
try:
rmtree(path + theme, ignore_errors=True)
except OSError as e:
self.show_info_message(str(e), Gtk.MessageType.ERROR)
else:
self.theme_button_remove_active(button)
@run_idle
def theme_button_remove_active(self, button):
button.remove(button.get_active())
button.set_active(0)
@run_idle
def init_appearance(self):
self._dark_mode_switch.set_active(self._ext_settings.dark_mode)
t_support = self._ext_settings.is_themes_support
self._themes_support_switch.set_active(t_support)
if t_support:
# GTK
try:
for t in os.listdir(self._ext_settings.themes_path):
self._theme_combo_box.append(t, t)
self._theme_combo_box.set_active_id(self._ext_settings.theme)
self.set_theme_thumbnail_image(self._ext_settings.theme)
except FileNotFoundError:
pass
except PermissionError as e:
log("{}".format(e))
# Icons
try:
for t in os.listdir(self._ext_settings.icon_themes_path):
self._icon_theme_combo_box.append(t, t)
self._icon_theme_combo_box.set_active_id(self._ext_settings.icon_theme)
except FileNotFoundError:
pass
except PermissionError as e:
log("{}".format(e))
if __name__ == "__main__": if __name__ == "__main__":
pass pass

View File

@@ -1,8 +1,32 @@
#digit-entry { #digit-entry {
border-color: Red; border-color: Red;
border-width: 0.15em;
} }
#status-bar-button { #status-bar-button {
padding: 1px; margin: 0.1em;
margin: 1px;
} }
#textview-large {
font-size: 14px;
}
.group {}
.group :first-child {
padding-left: 0.5em;
padding-right: 0.5em;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.group :last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.group :not(:first-child):not(:last-child) {
border-radius: 0;
border-left-width: 0;
border-right-width: 0;
}

250
app/ui/telnet.glade Executable file
View File

@@ -0,0 +1,250 @@
<?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-license-type mit -->
<!-- interface-name DemonEditor -->
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
<!-- interface-authors Dmitriy Yefremov -->
<object class="GtkTextTagTable" id="tag_table">
<child type="tag">
<object class="GtkTextTag" id="end_tag">
<property name="font">Normal</property>
<property name="editable">False</property>
</object>
</child>
</object>
<object class="GtkTextBuffer" id="text_buffer">
<property name="tag_table">tag_table</property>
</object>
<object class="GtkWindow" id="dialog_window">
<property name="can_focus">False</property>
<property name="title" translatable="yes">DemonEditor [Telnet client]</property>
<property name="destroy_with_parent">True</property>
<property name="icon_name">terminal</property>
<property name="skip_taskbar_hint">True</property>
<property name="skip_pager_hint">True</property>
<signal name="delete-event" handler="on_close" swapped="no"/>
<child>
<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="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkScrolledWindow" id="telnet_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTextView" id="text_view">
<property name="name">textview-large</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="wrap_mode">char</property>
<property name="left_margin">5</property>
<property name="right_margin">5</property>
<property name="buffer">text_buffer</property>
<property name="overwrite">True</property>
<property name="input_hints">GTK_INPUT_HINT_WORD_COMPLETION | GTK_INPUT_HINT_NONE</property>
<property name="monospace">True</property>
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
<signal name="realize" handler="on_text_view_realize" swapped="no"/>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">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>
</object>
</interface>

256
app/ui/telnet.py Executable file
View File

@@ -0,0 +1,256 @@
import re
import selectors
import socket
from collections import deque
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
class ExtTelnet(Telnet):
def __init__(self, output_callback, **kwargs):
super().__init__(**kwargs)
self._output_callback = output_callback
def interact(self):
"""Interaction function, emulates a very dumb telnet client."""
with selectors.DefaultSelector() as selector:
selector.register(self, selectors.EVENT_READ)
while True:
for key, events in selector.select():
if key.fileobj is self:
try:
text = self.read_very_eager()
except EOFError as e:
msg = "\n*** Connection closed by remote host ***\n"
self._output_callback(msg)
log(msg)
raise e
else:
if text:
self._output_callback(text)
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, 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_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)
provider = Gtk.CssProvider()
provider.load_from_path(UI_RESOURCES_PATH + "style.css")
builder.get_object("main_box").get_style_context().add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
window_size = settings.get("telnet_dialog_window_size")
if window_size:
self._dialog_window.resize(*window_size)
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()
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)
GLib.idle_add(self.on_info_bar_close)
user, password = self._settings.telnet_user, self._settings.telnet_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: ")
self._tn.write(user.encode("utf-8") + b"\n")
if password != "":
self._tn.read_until(b"Password: ")
self._tn.write(password.encode("utf-8") + b"\n")
self._tn.interact()
except (OSError, EOFError, socket.timeout, ConnectionRefusedError) as e:
log("{}: {}".format(self.__class__.__name__, e))
self.show_info_message(str(e), Gtk.MessageType.ERROR)
finally:
GLib.idle_add(self._connect_button.set_visible, True)
@run_task
def on_disconnect(self, item=None):
if self._tn:
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("")
if command and self._tn:
self._tn.write(command.encode("ascii") + b"\r")
def on_clear(self, item=None):
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 """
if event.keyval == Gdk.KEY_Return:
self.do_command()
return True
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:
if self._tn and self._tn.sock:
self._tn.write(b"\x03") # interrupt
# last commands navigation
if key is KeyboardKey.UP:
self.delete_last_command()
if self._commands:
cmd = self._commands.pop()
self._commands.appendleft(cmd)
self._buf.insert_at_cursor(cmd, -1)
return True
elif key is KeyboardKey.DOWN:
self.delete_last_command()
if self._commands:
cmd = self._commands.popleft()
self._commands.append(cmd)
self._buf.insert_at_cursor(cmd, -1)
return True
def delete_last_command(self):
end = self._buf.get_end_iter()
if end.ends_tag(self._end_tag):
return
if end.backward_to_tag_toggle(self._end_tag):
self._buf.delete(self._buf.get_end_iter(), end)
def do_command(self):
count = self._buf.get_line_count()
begin = self._buf.get_iter_at_line(count)
end = self._buf.get_end_iter()
command = []
while end.backward_to_tag_toggle(self._end_tag):
command.append(self._buf.get_text(end, begin, False))
break
else: # if buf is empty
command.append(self._buf.get_text(begin, end, False))
# to preventing duplication of the command in the buf
self._buf.delete(end, begin)
if command and self._tn.sock:
cmd = command[0]
if cmd in self._NOT_SUPPORTED:
self.show_info_message("'{}' is not supported by this client.".format(cmd), Gtk.MessageType.ERROR)
else:
self._tn.write(cmd.encode("ascii") + b"\r")
self._commands.append(cmd)
@run_idle
def append_output(self, txt):
t = txt.decode("ascii", errors="ignore")
ap = re.search(self._APP_MODE_PATTERN, t)
if ap:
on, of = ap.group(1), ap.group(2)
if on:
self._app_mode = True
elif of:
self._app_mode = False
self.on_clear()
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()
count = self._buf.get_line_count()
new_lines = t.split("\r\n")
ext_lines = self._buf.get_text(start, end, True).split("\r\n")
if count < len(new_lines):
self._buf.set_text(re.sub(self._ERASING_PATTERN, "", t))
else:
for i, line in enumerate(new_lines):
if line:
ext_lines[i] = re.sub(self._ERASING_PATTERN, "", line)
self._buf.set_text("\r\n".join(ext_lines))
else:
self._buf.insert_at_cursor(t, -1)
insert = self._buf.get_insert()
self._text_view.scroll_to_mark(insert, 0.0, True, 0.0, 1.0)
self._buf.apply_tag(self._end_tag, self._buf.get_start_iter(), self._buf.get_end_iter())
if __name__ == "__main__":
pass

View File

@@ -5,6 +5,7 @@ import gi
from gi.repository import GLib from gi.repository import GLib
from app.commons import log from app.commons import log
from app.settings import IS_DARWIN
from app.connections import HttpRequestType from app.connections import HttpRequestType
from app.tools.yt import YouTube from app.tools.yt import YouTube
from app.ui.iptv import get_yt_icon from app.ui.iptv import get_yt_icon
@@ -12,13 +13,13 @@ from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH
class LinksTransmitter: 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. It used for direct playback of media links by the enigma2 media player.
""" """
__STREAM_PREFIX = "4097:0:1:0:0:0:0:0:0:0:" __STREAM_PREFIX = "4097:0:1:0:0:0:0:0:0:0:"
def __init__(self, http_api, app_window): def __init__(self, http_api, app_window, settings):
handlers = {"on_popup_menu": self.on_popup_menu, handlers = {"on_popup_menu": self.on_popup_menu,
"on_status_icon_activate": self.on_status_icon_activate, "on_status_icon_activate": self.on_status_icon_activate,
"on_url_changed": self.on_url_changed, "on_url_changed": self.on_url_changed,
@@ -45,21 +46,25 @@ class LinksTransmitter:
self._restore_menu_item = builder.get_object("restore_menu_item") self._restore_menu_item = builder.get_object("restore_menu_item")
self._status_active = None self._status_active = None
self._status_passive = None self._status_passive = None
self._yt = YouTube.get_instance(settings)
try: if IS_DARWIN:
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") self._tray = builder.get_object("status_icon")
else: else:
self._is_status_icon = False try:
self._status_active = AppIndicator3.IndicatorStatus.ACTIVE gi.require_version("AppIndicator3", "0.1")
self._status_passive = AppIndicator3.IndicatorStatus.PASSIVE 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 category = AppIndicator3.IndicatorCategory.APPLICATION_STATUS
path = Path(UI_RESOURCES_PATH + "/icons/hicolor/scalable/apps/demon-editor.svg").resolve() path = Path(UI_RESOURCES_PATH + "/icons/hicolor/scalable/apps/demon-editor.svg")
path = str(path) if path.is_file() else "demon-editor" path = str(path.resolve()) if path.is_file() else "demon-editor"
self._tray = AppIndicator3.Indicator.new("DemonEditor", path, category) self._tray = AppIndicator3.Indicator.new("DemonEditor", path, category)
self._tray.set_status(self._status_active) self._tray.set_status(self._status_active)
self._tray.set_secondary_activate_target(builder.get_object("show_menu_item")) self._tray.set_secondary_activate_target(builder.get_object("show_menu_item"))
@@ -113,7 +118,7 @@ class LinksTransmitter:
if yt_id: if yt_id:
self._url_entry.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, get_yt_icon("youtube", 32)) self._url_entry.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, get_yt_icon("youtube", 32))
links, title = YouTube.get_yt_link(yt_id) links, title = self._yt.get_yt_link(yt_id, url)
yield True yield True
if links: if links:
url = links[sorted(links, key=lambda x: int(x.rstrip("p")), reverse=True)[0]] url = links[sorted(links, key=lambda x: int(x.rstrip("p")), reverse=True)[0]]

View File

@@ -1,49 +1,93 @@
import locale
import os import os
from enum import Enum, IntEnum from enum import Enum, IntEnum
from functools import lru_cache from functools import lru_cache
from app.settings import Settings, SettingsException, IS_DARWIN
import gi import gi
from app.settings import Settings, SettingsException
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
gi.require_version("Gdk", "3.0") gi.require_version("Gdk", "3.0")
from gi.repository import Gtk, Gdk from gi.repository import Gtk, Gdk, GLib
# path to *.glade files
UI_RESOURCES_PATH = "app/ui/" if os.path.exists("app/ui/") else "/usr/share/demoneditor/app/ui/"
# Setting mod mask for keyboard depending on platform
MOD_MASK = Gdk.ModifierType.MOD2_MASK if IS_DARWIN else Gdk.ModifierType.CONTROL_MASK
# Path to *.glade files
UI_RESOURCES_PATH = "app/ui/" if os.path.exists("app/ui/") else "ui/"
LANG_PATH = UI_RESOURCES_PATH + "lang"
GTK_PATH = os.environ.get("GTK_PATH", None)
NOTIFY_IS_INIT = False
IS_GNOME_SESSION = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID"))) IS_GNOME_SESSION = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
# translation # Translation.
TEXT_DOMAIN = "demon-editor" TEXT_DOMAIN = "demon-editor"
try: try:
settings = Settings.get_instance() settings = Settings.get_instance()
except SettingsException: except SettingsException:
pass pass
else: else:
os.environ["LANGUAGE"] = settings.language os.environ["LANGUAGE"] = settings.language
if UI_RESOURCES_PATH == "app/ui/": st = Gtk.Settings().get_default()
locale.bindtextdomain(TEXT_DOMAIN, UI_RESOURCES_PATH + "lang") 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:
style_provider = Gtk.CssProvider()
s_path = "{}default_style.css".format(GTK_PATH + "/" + UI_RESOURCES_PATH if GTK_PATH else UI_RESOURCES_PATH)
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_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:
import locale
locale.bindtextdomain(TEXT_DOMAIN, LANG_PATH)
# Init notify
try:
gi.require_version("Notify", "0.7")
from gi.repository import Notify
except ImportError:
pass
else:
NOTIFY_IS_INIT = Notify.init("DemonEditor")
theme = Gtk.IconTheme.get_default() theme = Gtk.IconTheme.get_default()
theme.append_search_path(UI_RESOURCES_PATH + "icons") theme.append_search_path(GTK_PATH + "/share/icons" if GTK_PATH else UI_RESOURCES_PATH + "icons")
_IMAGE_MISSING = theme.load_icon("image-missing", 16, 0) if theme.lookup_icon("image-missing", 16, 0) else None
CODED_ICON = theme.load_icon("emblem-readonly", 16, 0) if theme.lookup_icon( def get_theme_icon(icon_theme, name, size):
"emblem-readonly", 16, 0) else _IMAGE_MISSING try:
LOCKED_ICON = theme.load_icon("changes-prevent-symbolic", 16, 0) if theme.lookup_icon( return icon_theme.load_icon(name, size, 0)
"system-lock-screen", 16, 0) else _IMAGE_MISSING except GLib.Error:
HIDE_ICON = theme.load_icon("go-jump", 16, 0) if theme.lookup_icon("go-jump", 16, 0) else _IMAGE_MISSING pass
TV_ICON = theme.load_icon("tv-symbolic", 16, 0) if theme.lookup_icon("tv-symbolic", 16, 0) else _IMAGE_MISSING
IPTV_ICON = theme.load_icon("emblem-shared", 16, 0) if theme.lookup_icon("emblem-shared", 16, 0) else None
EPG_ICON = theme.load_icon("gtk-index", 16, 0) if theme.lookup_icon("gtk-index", 16, 0) else None _IMAGE_MISSING = get_theme_icon(theme, "image-missing", 16)
DEFAULT_ICON = theme.load_icon("emblem-default", 16, 0) if theme.lookup_icon("emblem-default", 16, 0) else None 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) @lru_cache(maxsize=1)
def get_yt_icon(icon_name, size=24): def get_yt_icon(icon_name, size=24):
""" Getting YouTube icon. If the icon is not found in the icon themes, the "Info" icon is returned by default! """ """ Getting YouTube icon.
If the icon is not found in the icon themes, the "Info" icon is returned by default!
"""
default_theme = Gtk.IconTheme.get_default() default_theme = Gtk.IconTheme.get_default()
if default_theme.has_icon(icon_name): if default_theme.has_icon(icon_name):
return default_theme.load_icon(icon_name, size, 0) return default_theme.load_icon(icon_name, size, 0)
@@ -52,41 +96,61 @@ def get_yt_icon(icon_name, size=24):
import glob import glob
for theme_name in map(os.path.basename, filter(os.path.isdir, glob.glob("/usr/share/icons/*"))): for theme_name in map(os.path.basename, filter(os.path.isdir, glob.glob("/usr/share/icons/*"))):
n_theme.set_custom_theme(theme_name) theme.set_custom_theme(theme_name)
if n_theme.has_icon(icon_name): if n_theme.has_icon(icon_name):
return n_theme.load_icon(icon_name, size, 0) return n_theme.load_icon(icon_name, size, 0)
return default_theme.load_icon("info", 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):
""" Shows notification.
@param message: text to display
@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()
class KeyboardKey(Enum): class KeyboardKey(Enum):
""" The raw(hardware) codes of the keyboard keys. """ """ The raw(hardware) codes of the keyboard keys. """
E = 26 F = 3 if IS_DARWIN else 41
R = 27 E = 14 if IS_DARWIN else 26
T = 28 R = 15 if IS_DARWIN else 27
P = 33 T = 17 if IS_DARWIN else 28
S = 39 P = 35 if IS_DARWIN else 33
F = 41 S = 1 if IS_DARWIN else 39
X = 53 H = 4 if IS_DARWIN else 43
C = 54 L = 37 if IS_DARWIN else 46
V = 55 X = 7 if IS_DARWIN else 53
W = 25 C = 8 if IS_DARWIN else 54
Z = 52 V = 9 if IS_DARWIN else 55
INSERT = 118 W = 13 if IS_DARWIN else 25
HOME = 110 Z = 6 if IS_DARWIN else 52
END = 115 INSERT = -1 if IS_DARWIN else 118
UP = 111 HOME = -1 if IS_DARWIN else 110
DOWN = 116 END = -1 if IS_DARWIN else 115
PAGE_UP = 112 UP = 126 if IS_DARWIN else 111
PAGE_DOWN = 117 DOWN = 125 if IS_DARWIN else 116
LEFT = 113 PAGE_UP = -1 if IS_DARWIN else 112
RIGHT = 114 PAGE_DOWN = -1 if IS_DARWIN else 117
F2 = 68 LEFT = 123 if IS_DARWIN else 113
SPACE = 65 RIGHT = 123 if IS_DARWIN else 114
DELETE = 119 F2 = 120 if IS_DARWIN else 68
BACK_SPACE = 22 SPACE = 49 if IS_DARWIN else 65
CTRL_L = 37 DELETE = 51 if IS_DARWIN else 119
CTRL_R = 105 BACK_SPACE = 76 if IS_DARWIN else 22
CTRL_L = 55 if IS_DARWIN else 37
CTRL_R = 55 if IS_DARWIN else 105
# Laptop codes # Laptop codes
HOME_KP = 79 HOME_KP = 79
END_KP = 87 END_KP = 87

View File

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

View File

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

View File

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

View File

@@ -1,26 +0,0 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Contact: Dmitriy Yefremov <dmitry.v.yefremov@gmail.com>
Source: https://github.com/DYefremov/DemonEditor
Files: *
MIT License
Copyright (c) 2018-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.

View File

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

View File

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

View File

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

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env python3
from app.ui.main_app_window import start_app
start_app()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,634 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg1541"
version="1.1"
viewBox="0 0 16.743339 16.72816"
height="63.224556"
width="63.281971"
sodipodi:docname="demon-editor.svg"
inkscape:version="0.92.4 (unknown)">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="716"
id="namedview127"
showgrid="true"
fit-margin-left="0"
inkscape:zoom="6.1714295"
inkscape:cx="40.088627"
inkscape:cy="31.742631"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1541"
fit-margin-top="0"
fit-margin-right="0"
fit-margin-bottom="0">
<inkscape:grid
type="xygrid"
id="grid128"
originx="-10.604603"
originy="-1.1727724" />
</sodipodi:namedview>
<title
id="title1088">DeamonEditor Icons</title>
<defs
id="defs1535">
<linearGradient
id="linearGradient2198">
<stop
id="stop2194"
style="stop-color:#ffb320;stop-opacity:1"
offset="0" />
<stop
id="stop2196"
style="stop-color:#e7ff00;stop-opacity:1"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient2192">
<stop
offset="0"
style="stop-color:#ffb320;stop-opacity:1"
id="stop2188" />
<stop
offset="1"
style="stop-color:#b3c54c;stop-opacity:1"
id="stop2190" />
</linearGradient>
<linearGradient
id="linearGradient3700-8">
<stop
offset="0"
style="stop-color:#2e4f84;stop-opacity:1"
id="stop2183" />
<stop
offset="1"
style="stop-color:#4c77c5;stop-opacity:1"
id="stop2185" />
</linearGradient>
<linearGradient
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
spreadMethod="pad"
id="linearGradient1844">
<stop
style="stop-opacity:1;stop-color:#29282b"
offset="0"
id="stop1826" />
<stop
id="stop1828"
offset="0.13293758"
style="stop-color:#b5bdcf;stop-opacity:1" />
<stop
style="stop-opacity:1;stop-color:#92979f"
offset="0.21261224"
id="stop1832" />
<stop
style="stop-opacity:1;stop-color:#737881"
offset="0.29780689"
id="stop1834" />
<stop
style="stop-opacity:1;stop-color:#70757e"
offset="0.29780689"
id="stop1836" />
<stop
style="stop-opacity:1;stop-color:#e4e6e8"
offset="0.45395693"
id="stop1838" />
<stop
style="stop-opacity:1;stop-color:#696c76"
offset="0.71871042"
id="stop1840" />
<stop
style="stop-opacity:1;stop-color:#29282b"
offset="1"
id="stop1842" />
</linearGradient>
<linearGradient
id="linearGradient1754"
spreadMethod="pad"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
gradientUnits="userSpaceOnUse"
y2="0"
x2="1"
y1="0"
x1="0">
<stop
id="stop1736"
offset="0"
style="stop-opacity:1;stop-color:#29282b" />
<stop
style="stop-color:#b5bdcf;stop-opacity:1"
offset="0.02582242"
id="stop1738" />
<stop
id="stop1740"
offset="0.14669065"
style="stop-opacity:1;stop-color:#868c95" />
<stop
id="stop1742"
offset="0.21261224"
style="stop-opacity:1;stop-color:#92979f" />
<stop
id="stop1744"
offset="0.29780689"
style="stop-opacity:1;stop-color:#737881" />
<stop
id="stop1746"
offset="0.29780689"
style="stop-opacity:1;stop-color:#70757e" />
<stop
id="stop1748"
offset="0.45395693"
style="stop-opacity:1;stop-color:#e4e6e8" />
<stop
id="stop1750"
offset="0.71871042"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1752"
offset="1"
style="stop-opacity:1;stop-color:#1d191a" />
</linearGradient>
<linearGradient
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
spreadMethod="pad"
id="linearGradient1606">
<stop
style="stop-opacity:1;stop-color:#29282b"
offset="0"
id="stop1590" />
<stop
id="stop1608"
offset="0.03065561"
style="stop-color:#b5bdcf;stop-opacity:1" />
<stop
style="stop-opacity:1;stop-color:#868c95"
offset="0.1125849"
id="stop1592" />
<stop
style="stop-opacity:1;stop-color:#92979f"
offset="0.13955873"
id="stop1594" />
<stop
style="stop-opacity:1;stop-color:#737881"
offset="0.1915853"
id="stop1596" />
<stop
style="stop-opacity:1;stop-color:#70757e"
offset="0.25400829"
id="stop1598" />
<stop
style="stop-opacity:1;stop-color:#e4e6e8"
offset="0.45395693"
id="stop1600" />
<stop
style="stop-opacity:1;stop-color:#ffffff"
offset="0.71871042"
id="stop1602" />
<stop
style="stop-opacity:1;stop-color:#1d191a"
offset="1"
id="stop1604" />
</linearGradient>
<linearGradient
id="linearGradient886-0">
<stop
style="stop-color:#535353;stop-opacity:1"
offset="0"
id="stop888" />
<stop
style="stop-color:#f0f0f0;stop-opacity:1"
offset="1"
id="stop890" />
</linearGradient>
<linearGradient
id="linearGradient1473">
<stop
id="stop1469"
offset="0"
style="stop-color:#ffffff;stop-opacity:0" />
<stop
id="stop1471"
offset="1"
style="stop-color:#ffffff;stop-opacity:0.96088022" />
</linearGradient>
<linearGradient
id="linearGradient2447-35">
<stop
id="stop2449"
offset="0"
style="stop-color:#00c62e;stop-opacity:1" />
<stop
id="stop2451"
offset="1"
style="stop-color:#136100;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient3795-1">
<stop
id="stop3797-1"
offset="0"
style="stop-color:#803400;stop-opacity:1" />
<stop
id="stop3799-3"
offset="1"
style="stop-color:#c87137;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient2447-3">
<stop
style="stop-color:#c60300;stop-opacity:1"
offset="0"
id="stop2443" />
<stop
style="stop-color:#c40e00;stop-opacity:1"
offset="1"
id="stop2445" />
</linearGradient>
<linearGradient
id="linearGradient2458">
<stop
id="stop2454"
offset="0"
style="stop-color:#c60300;stop-opacity:1" />
<stop
id="stop2456"
offset="1"
style="stop-color:#ee6000;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient3519">
<stop
id="stop3521"
offset="0"
style="stop-color:#1d2120;stop-opacity:1" />
<stop
id="stop3523"
offset="1"
style="stop-color:#545d5d;stop-opacity:1" />
</linearGradient>
<linearGradient
id="linearGradient1446">
<stop
id="stop1442"
offset="0"
style="stop-color:#ffffff;stop-opacity:1;" />
<stop
id="stop1444"
offset="1"
style="stop-color:#ffffff;stop-opacity:0;" />
</linearGradient>
<linearGradient
id="linearGradient1547"
spreadMethod="pad"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
gradientUnits="userSpaceOnUse"
y2="0"
x2="1"
y1="0"
x1="0">
<stop
id="stop1529"
offset="0"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1531"
offset="0.0263736"
style="stop-opacity:1;stop-color:#29282b" />
<stop
id="stop1533"
offset="0.263736"
style="stop-opacity:1;stop-color:#868c95" />
<stop
id="stop1535"
offset="0.395604"
style="stop-opacity:1;stop-color:#92979f" />
<stop
id="stop1537"
offset="0.39560401"
style="stop-opacity:1;stop-color:#737881" />
<stop
id="stop1539"
offset="0.42333773"
style="stop-opacity:1;stop-color:#70757e" />
<stop
id="stop1541"
offset="0.56268591"
style="stop-opacity:1;stop-color:#e4e6e8" />
<stop
id="stop1543"
offset="0.62400264"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1545"
offset="1"
style="stop-opacity:1;stop-color:#1d191a" />
</linearGradient>
<linearGradient
id="linearGradient1527"
spreadMethod="pad"
gradientTransform="matrix(13.526835,20.525875,20.525875,-13.526835,19.18986,1021.0543)"
gradientUnits="userSpaceOnUse"
y2="0"
x2="1"
y1="0"
x1="0">
<stop
id="stop1509"
offset="0"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1511"
offset="0.0527472"
style="stop-opacity:1;stop-color:#29282b" />
<stop
id="stop1513"
offset="0.142147"
style="stop-opacity:1;stop-color:#868c95" />
<stop
id="stop1515"
offset="0.19700864"
style="stop-opacity:1;stop-color:#92979f" />
<stop
id="stop1517"
offset="0.25031137"
style="stop-opacity:1;stop-color:#737881" />
<stop
id="stop1519"
offset="0.3710371"
style="stop-opacity:1;stop-color:#70757e" />
<stop
id="stop1521"
offset="0.53961843"
style="stop-opacity:1;stop-color:#e4e6e8" />
<stop
id="stop1523"
offset="0.76283824"
style="stop-opacity:1;stop-color:#ffffff" />
<stop
id="stop1525"
offset="1"
style="stop-opacity:1;stop-color:#1d191a" />
</linearGradient>
<linearGradient
gradientTransform="matrix(0.26458333,0,0,0.26458333,-5.8254634,-78.732754)"
y2="1179.7145"
x2="66.791626"
y1="1188.7661"
x1="99.044022"
gradientUnits="userSpaceOnUse"
id="linearGradient1485"
xlink:href="#linearGradient1473" />
<linearGradient
gradientTransform="matrix(0.26458333,0,0,0.26458333,-2.2733744,-70.526334)"
gradientUnits="userSpaceOnUse"
y2="1155.8046"
x2="67.311417"
y1="1161.6112"
x1="77.19442"
id="linearGradient1448"
xlink:href="#linearGradient1446" />
<linearGradient
gradientTransform="matrix(0.26458333,0,0,0.26458333,-18.198747,-79.859424)"
gradientUnits="userSpaceOnUse"
y2="1186.8096"
x2="146.16808"
y1="1186.8096"
x1="131.86871"
id="linearGradient2180"
xlink:href="#linearGradient886-0" />
<linearGradient
gradientTransform="matrix(0.20863982,0,0,0.20863982,-0.63935937,-13.031239)"
gradientUnits="userSpaceOnUse"
y2="1184.73"
x2="101.19952"
y1="1184.73"
x1="83.066002"
id="linearGradient2160"
xlink:href="#linearGradient886-0" />
<linearGradient
gradientTransform="matrix(0.26458333,0,0,0.26458333,-6.2370714,-102.12414)"
gradientUnits="userSpaceOnUse"
y2="1267.1335"
x2="117.99127"
y1="1267.1335"
x1="75.853806"
id="linearGradient2131"
xlink:href="#linearGradient886-0" />
<linearGradient
id="linearGradient3700-8-3">
<stop
id="stop3702-1"
style="stop-color:#2e4f84;stop-opacity:1"
offset="0" />
<stop
id="stop3704-8"
style="stop-color:#4c77c5;stop-opacity:1"
offset="1" />
</linearGradient>
<defs
id="defs4922">
<filter
style="color-interpolation-filters:sRGB"
id="Adobe_OpacityMaskFilter"
filterUnits="userSpaceOnUse"
x="3.7850001"
y="4.6750002"
width="5.8829999"
height="73.013">
<feColorMatrix
type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"
id="feColorMatrix4925" />
</filter>
</defs>
<mask
maskUnits="userSpaceOnUse"
x="3.785"
y="4.675"
width="5.883"
height="73.013"
id="SVGID_2_">
<g
style="filter:url(#Adobe_OpacityMaskFilter)"
id="g4928">
<linearGradient
id="SVGID_3_"
gradientUnits="userSpaceOnUse"
x1="3.7852001"
y1="41.181198"
x2="9.6680002"
y2="41.181198">
<stop
offset="0"
style="stop-color:#FFFFFF"
id="stop4931" />
<stop
offset="0.0029"
style="stop-color:#FAFBFB"
id="stop4933" />
<stop
offset="0.0756"
style="stop-color:#BBBDBF"
id="stop4935" />
<stop
offset="0.1438"
style="stop-color:#898B8E"
id="stop4937" />
<stop
offset="0.2053"
style="stop-color:#646567"
id="stop4939" />
<stop
offset="0.259"
style="stop-color:#444446"
id="stop4941" />
<stop
offset="0.3028"
style="stop-color:#1D1C1D"
id="stop4943" />
<stop
offset="0.3313"
style="stop-color:#000000"
id="stop4945" />
</linearGradient>
<rect
style="fill:url(#SVGID_3_)"
x="3.7850001"
y="4.6750002"
width="5.8829999"
height="73.013"
id="rect4947" />
</g>
</mask>
<filter
style="color-interpolation-filters:sRGB"
id="filter1268"
filterUnits="userSpaceOnUse"
x="3.7850001"
y="4.6750002"
width="5.8829999"
height="73.013">
<feColorMatrix
type="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"
id="feColorMatrix1266" />
</filter>
<linearGradient
gradientUnits="userSpaceOnUse"
y2="203.22046"
x2="23.551136"
y1="203.22046"
x1="13.487289"
id="linearGradient1160"
xlink:href="#linearGradient3519" />
<clipPath
id="clipPath1890"
clipPathUnits="userSpaceOnUse">
<circle
style="opacity:1;fill:#2d2d2d;fill-opacity:1;stroke:#434242;stroke-width:0.0575568;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle1892"
cx="78.548424"
cy="-31.019459"
r="6.4721422"
transform="scale(1,-1)" />
</clipPath>
<linearGradient
gradientTransform="translate(-0.24389927,14.877856)"
y2="27.314217"
x2="84.864914"
y1="27.314217"
x1="72.164909"
gradientUnits="userSpaceOnUse"
id="linearGradient2134"
xlink:href="#linearGradient3700-8-3" />
</defs>
<metadata
id="metadata1538">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>DeamonEditor Icons</dc:title>
<dc:publisher>
<cc:Agent>
<dc:title>mfgeg</dc:title>
</cc:Agent>
</dc:publisher>
<dc:date>7.1.2020</dc:date>
</cc:Work>
</rdf:RDF>
</metadata>
<g
transform="matrix(1.1690805,0,0,1.1690805,-14.929261,-167.45253)"
id="layer1">
<g
transform="translate(0.86526724,-82.691658)"
id="g1351">
<path
d="m 13.715358,227.65981 h 2.97616 v 12.57332 h -2.97616 z m 5.79826,-1.73376 -0.327076,0.42227 c -0.179879,0.23226 -0.586303,0.67932 -0.903223,0.99412 -0.480677,0.47726 -0.640897,0.57235 -0.967777,0.57235 -0.223557,0 -0.380895,-0.0584 -0.624024,-0.25067 v 3.18307 c 0.172884,-0.0214 0.359647,-0.0333 0.575071,-0.0352 0.68403,-0.006 0.91815,0.0416 1.436333,0.29581 1.14586,0.56274 1.762492,1.5589 1.768251,2.8566 0.0094,2.10836 -1.870896,3.55161 -3.779655,3.16529 v 3.10345 c 4.345352,0.0758 4.093104,-2.37537 6.573781,-2.25837 1.486139,0.0748 1.333421,0.16555 1.796225,-1.064 l 0.259834,-0.6913 -0.803704,-0.66493 c -0.960855,-0.79478 -1.303983,-1.29079 -1.205013,-1.74132 0.06631,-0.30189 1.20818,-1.50093 1.779011,-1.86778 0.240181,-0.1543 0.244255,-0.17878 0.09575,-0.59661 -0.08522,-0.23979 -0.259522,-0.65746 -0.386789,-0.92744 l -0.23132,-0.49115 h -1.412666 c -1.868582,0 -1.802386,0.068 -1.805368,-1.82472 l -0.0021,-1.43582 -0.917745,-0.37171 z"
style="opacity:1;fill:#000000;fill-opacity:0.5372549;stroke:none;stroke-width:0.18460207;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.5012225"
id="path2133"
inkscape:connector-curvature="0" />
<path
id="path2106"
style="opacity:1;fill:url(#linearGradient2131);fill-opacity:1;stroke:none;stroke-width:0.17733108;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.5012225"
d="m 13.832581,227.93117 h 2.858936 v 12.07807 h -2.858936 z m 5.569881,-1.6655 -0.314194,0.40566 c -0.172793,0.22309 -0.563209,0.65257 -0.867647,0.95496 -0.46174,0.45847 -0.615654,0.5498 -0.929658,0.5498 -0.214752,0 -0.365893,-0.056 -0.599446,-0.24079 v 3.05768 c 0.166077,-0.0206 0.345483,-0.0319 0.55242,-0.0338 0.657089,-0.006 0.881987,0.0399 1.379764,0.28416 1.100724,0.54057 1.693073,1.49747 1.698606,2.74408 0.009,2.02535 -1.797211,3.41174 -3.63079,3.04061 v 2.98122 c 4.174205,0.0728 3.931888,-2.28179 6.314859,-2.1694 1.427604,0.0718 1.2809,0.15902 1.725477,-1.02213 l 0.249597,-0.66408 -0.772046,-0.63871 c -0.923009,-0.76345 -1.252622,-1.23996 -1.157552,-1.67277 0.0637,-0.29001 1.160592,-1.44177 1.708939,-1.79419 0.230721,-0.14825 0.234635,-0.17174 0.09198,-0.57312 -0.08187,-0.23032 -0.249296,-0.63156 -0.371555,-0.89088 l -0.222207,-0.4718 h -1.357022 c -1.794983,0 -1.731396,0.0653 -1.734262,-1.75284 l -0.0021,-1.3793 -0.881603,-0.35705 z"
inkscape:connector-curvature="0" />
<path
id="path2151"
d="m 17.266983,230.95755 c -0.215637,0.003 -0.402408,0.014 -0.575465,0.0354 v 6.28854 c 1.910653,0.38671 3.792716,-1.05815 3.783336,-3.16865 -0.0058,-1.29897 -0.623064,-2.29597 -1.770059,-2.85927 -0.518699,-0.25451 -0.753103,-0.30234 -1.437812,-0.29607 z m 0.482341,1.17002 c 0.433747,-0.004 0.582202,0.0265 0.910784,0.18761 0.726595,0.35682 1.117604,0.98848 1.121256,1.81134 0.0059,1.33694 -1.186343,2.25211 -2.396695,2.00715 v -3.98359 c 0.109628,-0.0135 0.228054,-0.0214 0.364655,-0.0225 z"
style="opacity:1;fill:url(#linearGradient2160);fill-opacity:1;stroke:none;stroke-width:0.18478528;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.5012225"
inkscape:connector-curvature="0" />
<path
style="opacity:1;fill:none;fill-opacity:1;stroke:url(#linearGradient2180);stroke-width:0.18478528;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.5012225"
d="m 17.266981,230.95753 c -0.215636,0.003 -0.402408,0.014 -0.575464,0.0354 v 6.28853 c 1.910652,0.38672 3.792715,-1.05815 3.783336,-3.16865 -0.0057,-1.29897 -0.623065,-2.29597 -1.77006,-2.85927 -0.518697,-0.2545 -0.753103,-0.30234 -1.437812,-0.29607 z m 0.482343,1.17001 c 0.433747,-0.004 0.5822,0.0265 0.910783,0.18762 0.726594,0.35681 1.117605,0.98848 1.121257,1.81133 0.006,1.33694 -1.186344,2.25211 -2.396697,2.00716 v -3.98359 c 0.109628,-0.0135 0.228055,-0.0214 0.364657,-0.0225 z"
id="path2170"
inkscape:connector-curvature="0" />
<path
id="path1422"
d="m 17.266983,230.95755 c -0.215637,0.003 -0.402408,0.014 -0.575464,0.0354 v 6.28854 c 1.910652,0.38671 3.792715,-1.05815 3.783335,-3.16865 -0.0057,-1.29897 -0.623064,-2.29597 -1.770059,-2.85927 -0.518699,-0.25451 -0.753103,-0.30234 -1.437812,-0.29607 z m 0.482342,1.17002 c 0.433748,-0.004 0.582201,0.0265 0.910784,0.18761 0.726594,0.35682 1.117605,0.98848 1.121257,1.81134 0.006,1.33694 -1.186344,2.25211 -2.396697,2.00715 v -3.98359 c 0.109628,-0.0135 0.228054,-0.0214 0.364656,-0.0225 z"
style="opacity:1;fill:none;fill-opacity:1;stroke:url(#linearGradient1448);stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:connector-curvature="0" />
<path
id="path1454"
d="m 19.402462,226.26149 -0.314194,0.40566 c -0.172793,0.22309 -0.563209,0.65259 -0.867644,0.95499 -0.461741,0.45847 -0.615657,0.54983 -0.929661,0.54983 -0.214752,0 -0.365893,-0.056 -0.599446,-0.2408 v -0.004 h -2.858741 v 12.0783 h 2.858741 c 4.174205,0.0728 3.931888,-2.28177 6.314861,-2.16937 1.427604,0.0718 1.280898,0.15899 1.725475,-1.02217 l 0.249597,-0.66402 -0.772046,-0.63873 c -0.923009,-0.76346 -1.252622,-1.23997 -1.157552,-1.67278 0.0637,-0.29001 1.160595,-1.44176 1.708939,-1.79419 0.230721,-0.14825 0.234635,-0.17171 0.09198,-0.57309 -0.08187,-0.23032 -0.249296,-0.63158 -0.371555,-0.8909 l -0.222207,-0.47181 h -1.357025 c -1.794983,0 -1.731393,0.0653 -1.734259,-1.75286 l -0.0021,-1.37925 -0.8816,-0.35708 z"
style="opacity:1;fill:none;fill-opacity:1;stroke:url(#linearGradient1485);stroke-width:0.1;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:connector-curvature="0" />
</g>
<g
transform="translate(63.49728,-55.136805)"
id="g1174" />
<g
transform="translate(50.48327,11.624037)"
id="g1812" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 22 KiB

BIN
icon.icns Normal file

Binary file not shown.

View File

@@ -10,14 +10,9 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Language-Team: \n"
"X-Generator: Poedit 2.0.6\n"
msgid "translator-credits" msgid "translator-credits"
msgstr "Charly" msgstr "Charly\nDmitriy Yefremov"
# Main # Main
msgid "Service" msgid "Service"
@@ -676,11 +671,11 @@ msgstr "Play Stream"
msgid "Disabled" msgid "Disabled"
msgstr "Ausgeschaltet" msgstr "Ausgeschaltet"
msgid "Enable ver. 5 support (experimental)" msgid "Enable lamedb ver. 5 support"
msgstr "Lamedb ver. 5 Unterstützung aktivieren (experimentell)" msgstr "Lamedb ver. 5 Unterstützung aktivieren"
msgid "Enable HTTP API (experimental)" msgid "Enable HTTP API"
msgstr "HTTP-API aktivieren (experimentell)" msgstr "HTTP-API aktivieren"
msgid "Switch(zap) the channel(Ctrl + Z)" msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Umschalten des Kanals (Strg + Z)" msgstr "Umschalten des Kanals (Strg + Z)"
@@ -800,8 +795,8 @@ msgstr "Sprache:"
msgid "Load the last open configuration at program startup" msgid "Load the last open configuration at program startup"
msgstr "Laden der zuletzt geöffneten Konfiguration beim Programmstart" msgstr "Laden der zuletzt geöffneten Konfiguration beim Programmstart"
msgid "Enable direct playback bar (experimental)" msgid "Enable direct playback bar"
msgstr "Aktivieren der direkten Wiedergabeleiste (experimentell)" msgstr "Aktivieren der direkten Wiedergabeleiste"
msgid "Enables direct sending and playback of media links on the receiver" msgid "Enables direct sending and playback of media links on the receiver"
msgstr "Ermöglicht das direkte Senden und Abspielen von Medienlinks auf dem Box" msgstr "Ermöglicht das direkte Senden und Abspielen von Medienlinks auf dem Box"
@@ -820,3 +815,226 @@ msgstr "Hinzugefügte Links in der Wiedergabeliste entfernen"
msgid "A bouquet with that name exists!" msgid "A bouquet with that name exists!"
msgstr "Bouquet mit diesem Namen existiert!" msgstr "Bouquet mit diesem Namen existiert!"
msgid "Details"
msgstr "Details"
msgid "Profile"
msgstr "Profil"
msgid "Reset"
msgstr "Reset"
msgid "File"
msgstr "Ablage"
msgid "Picons manager"
msgstr "Picons-Manager"
msgid "Explorer"
msgstr ""
msgid "Satellite url:"
msgstr "Satellit URL:"
msgid "Cut"
msgstr "Ausschneiden"
msgid "Paste"
msgstr "Einfügen"
msgid "To the top"
msgstr "Zum Anfang"
msgid "To the end"
msgstr "Zum Ende"
msgid "View"
msgstr "Darstellung"
msgid "Lock"
msgstr "Sperre"
msgid "Parent lock"
msgstr "Elternsperre"
msgid "Hide/Skip"
msgstr "Ausblenden/Überspringen"
msgid "IPTV tools"
msgstr "IPTV-Tools"
msgid "Make profile folder as default for the additional data"
msgstr "Profilordner als Standard für die zusätzlichen Daten festlegen"
msgid "Default data path:"
msgstr "Standard-Datenpfad:"
msgid "Streams record path:"
msgstr "Streams Aufnahmepfad:"
msgid "Record"
msgstr "Aufnahme"
msgid "Record:"
msgstr "Aufnahme:"
msgid "Record to disk:"
msgstr "Aufnahme auf Festplatte:"
msgid "Streaming"
msgstr "Streaming"
msgid "Activate transcoding"
msgstr "Aktivieren der Transkodierung"
msgid "Presets:"
msgstr "Voreinstellungen:"
msgid "Video options:"
msgstr "Video-Optionen:"
msgid "Audio options:"
msgstr "Audio-Optionen:"
msgid "Bitrate (kb/s):"
msgstr "Bitrate (kb/s):"
msgid "Codec:"
msgstr "Codec:"
msgid "Width (px):"
msgstr "Breite (px):"
msgid "Height (px):"
msgstr "Höhe (px):"
msgid "Channels:"
msgstr "Kanälen:"
msgid "Sample rate (Hz):"
msgstr "Samplerate (Hz):"
msgid "Play streams mode:"
msgstr "Streams Abspielen-Modus:"
msgid "Built-in player"
msgstr "Integrierter Player"
msgid "VLC media player"
msgstr "VLC Media Player"
msgid "Only get m3u file"
msgstr "Nur m3u-Datei erhalten"
msgid "Save and restart the program to apply the settings."
msgstr "Speicher und starte das Programm neu, um die Einstellungen zu übernehmen."
msgid "Some images may have problems displaying the favorites list!"
msgstr "Einige Images können Probleme mit der Anzeige der Favoritenliste haben!"
msgid "Operates in standby mode or current active transponder!"
msgstr "Arbeitet im Standby-Modus oder auf dem aktuell aktiven Transponder!"
msgid "No connection to the receiver!"
msgstr "Keine Verbindung zum Box!"
msgid "Signal level"
msgstr "Signalpegel"
msgid "Receiver info"
msgstr "Box-Info"
msgid "A profile with that name exists!"
msgstr "Ein Profil mit diesem Namen existiert!"
msgid "Show short info as hints in the main services list"
msgstr "Kurzinfos als Tooltips in der Hauptliste anzeigen"
msgid "Show detailed info as hints in the bouquet list"
msgstr "Detaillierteinfos als Tooltips in der Bouquetliste anzeigen"
msgid "Enable alternate bouquet file naming"
msgstr "Aktivieren der Alternativerbenennung für Bouquet-Dateien"
msgid "Allows you to name bouquet files using their names."
msgstr "Ermöglicht Bouquet-Dateien mit ihren Namen zu benennen."
msgid "Appearance"
msgstr "Aussehen"
msgid "Enable Themes support"
msgstr "Unterstützung von Themen aktivieren"
msgid "Gtk3 Theme:"
msgstr "Gtk3-Theme:"
msgid "Icon Theme:"
msgstr "Icon-Theme:"
msgid "Gtk3 Themes and Icons:"
msgstr "Gtk3 Themes and Icons:"
msgid "Deleting data..."
msgstr "Daten löschen..."
msgid "Download from the receiver"
msgstr "Downloaden vom Receiver"
msgid "Remove all picons from the receiver"
msgstr "Alle Picons aus dem Receiver entfernen"
msgid "Service reference"
msgstr "Kanalreferenz"
msgid "Enable support for"
msgstr "Unterstützung aktivieren für"
msgid "Auto-check for updates"
msgstr "Automatische Prüfung auf Updates"
msgid "Filter services"
msgstr "Dienste filtern"
msgid "Filter services in the main list."
msgstr "Dienste in der Hauptliste filtern."
msgid "Destination:"
msgstr "Ziel:"
msgid "EXPERIMENTAL!"
msgstr "EXPERIMENTELL!"
msgid "Sorting data..."
msgstr "Daten sortieren..."
msgid "There are unsaved changes.\n\n\t Save them now?"
msgstr "Es gibt ungespeicherte Änderungen.\n\n\t Möchtest du jetzt speichern?"
msgid "Are you sure you want to change the order\n\t of services in this bouquet?"
msgstr "Bist du sicher, dass du die Reihenfolge der Dienstleistungen\n\t in diesem Bouquet ändern willst?"
msgid "Remove from the receiver"
msgstr "Aus dem Receiver entfernen"
msgid "Screenshot"
msgstr "Screenshot"
msgid "Video"
msgstr "Video"
msgid "The Neutrino has only experimental support. Not all features are supported!"
msgstr "Die Neutrino hat nur experimentelle Unterstützung. Nicht alle Funktionen werden unterstützt!"
msgid "Enable experimental features"
msgstr "Experimentelle Funktionen aktivieren"
msgid "Can't Playback!"
msgstr "Kann nicht abgespielt werden!"
msgid "Enable Dark Mode"
msgstr "Dunkelmodus aktivieren"

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2018-2019 Frank Neirynck # Copyright (C) 2018-2020 Frank Neirynck
# This file is distributed under the MIT license. # This file is distributed under the MIT license.
# #
# Frank Neirynck <frank@insink.be>, 2018-2019. # Frank Neirynck <frank@insink.be>, 2018-2019.
@@ -486,7 +486,7 @@ msgid "STB file paths:"
msgstr "Rutas de ficheros del receptor:" msgstr "Rutas de ficheros del receptor:"
msgid "Local file paths:" msgid "Local file paths:"
msgstr "Rutas de ficheros local:" msgstr "Rutas de ficheros locales:"
# Dialogs messages # Dialogs messages
msgid "Error. No bouquet is selected!" msgid "Error. No bouquet is selected!"
@@ -620,7 +620,7 @@ msgid "Before downloading from the receiver"
msgstr "Antes de recibir del receptor" msgstr "Antes de recibir del receptor"
msgid "Set background color for the services" msgid "Set background color for the services"
msgstr "Determinar color de fondo de los servicios" msgstr "Fijar color de fondo de los servicios"
msgid "Marked as new:" msgid "Marked as new:"
msgstr "Marcado como nuevo:" msgstr "Marcado como nuevo:"
@@ -666,7 +666,7 @@ msgid "Test connection"
msgstr "Probar conexión" msgstr "Probar conexión"
msgid "Double click on the service in the bouquet list:" msgid "Double click on the service in the bouquet list:"
msgstr "Haga doble clic en el servicio en la lista de bouquet:" msgstr "Al hacer doble clic en el servicio en la lista de bouquet:"
msgid "Zap" msgid "Zap"
msgstr "Zapear" msgstr "Zapear"
@@ -677,11 +677,11 @@ msgstr "Reproducir flujo"
msgid "Disabled" msgid "Disabled"
msgstr "Desactivado" msgstr "Desactivado"
msgid "Enable ver. 5 support (experimental)" msgid "Enable lamedb ver. 5 support"
msgstr "Soporte para ver. 5 (experimental)" msgstr "Soporte para lamedb ver. 5"
msgid "Enable HTTP API (experimental)" msgid "Enable HTTP API"
msgstr "Habilitar API HTTP (experimental)" msgstr "Habilitar API HTTP"
msgid "Switch(zap) the channel(Ctrl + Z)" msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Poner el canal (Ctrl + Z)" msgstr "Poner el canal (Ctrl + Z)"
@@ -771,10 +771,257 @@ msgid "Import YouTube playlist"
msgstr "Importar lista de reproducción de YouTube" msgstr "Importar lista de reproducción de YouTube"
msgid "Found a link to the YouTube resource!\nTry to get a direct link to the video?" msgid "Found a link to the YouTube resource!\nTry to get a direct link to the video?"
msgstr "¡Encontrado enlace al recurso de YouTube!\n¿Intentar obtener un enlace directo al vídeo?" msgstr "¡Se ha encontrado enlace al recurso de YouTube!\n¿Intentar obtener un enlace directo al vídeo?"
msgid "Playlist import" msgid "Playlist import"
msgstr "Importar lista de reproducción" msgstr "Importar lista de reproducción"
msgid "Getting link error:" msgid "Getting link error:"
msgstr "Error en el enlace:" msgstr "Error en el enlace:"
msgid "Extra"
msgstr "Extra"
msgid "Apply profile settings"
msgstr "Aplicar ajustes de perfil"
msgid "Settings type:"
msgstr "Tipo de ajustes:"
msgid "Set default"
msgstr "Por defecto"
msgid "Language:"
msgstr "Idioma:"
msgid "Load the last open configuration at program startup"
msgstr "Cargar la última configuración abierta al iniciar el programa"
msgid "Enable direct playback bar"
msgstr "Habilitar la barra de reproducción directa"
msgid "Enables direct sending and playback of media links on the receiver"
msgstr "Habilita el envío directo y la reproducción de enlaces de medios en el receptor"
msgid "Watch the channel in the program"
msgstr "Ver el canal en el programa"
msgid "Zap and Play"
msgstr "Zapear y reproducir"
msgid "Drag or paste the link here"
msgstr "Soltar o pegar en enlace aquí"
msgid "Remove added links in the playlist"
msgstr "Quitar los enlaces añadidos en la lista de reproducción"
msgid "A bouquet with that name exists!"
msgstr "¡Ya existe un bouquet con ese nombre!"
msgid "Details"
msgstr "Detalles"
msgid "Profile"
msgstr "Perfil"
msgid "Reset"
msgstr "Reset"
msgid "File"
msgstr "Archivo"
msgid "Picons manager"
msgstr "Picons manager"
msgid "Explorer"
msgstr "Explorador"
msgid "Satellite url:"
msgstr "Url Satelite:"
msgid "Cut"
msgstr "Cortar"
msgid "Paste"
msgstr "Pegar"
msgid "To the top"
msgstr "Ir arriba"
msgid "To the end"
msgstr "Al final"
msgid "View"
msgstr "Vista"
msgid "Lock"
msgstr "Bloqueo"
msgid "Parent lock"
msgstr "Bloqueo parental"
msgid "Hide/Skip"
msgstr "Escoder/Saltar"
msgid "IPTV tools"
msgstr "Intrumentos IPTV"
msgid "Make profile folder as default for the additional data"
msgstr "Has folder de perfil estandar para datos adicionales"
msgid "Default data path:"
msgstr "Ruta estandar de datos:"
msgid "Streams record path:"
msgstr "Ruta de gravacion de stream:"
msgid "Record"
msgstr "Gravar"
msgid "Record:"
msgstr "Gravar:"
msgid "Record to disk:"
msgstr "Gravar en disco:"
msgid "Streaming"
msgstr "Streameando"
msgid "Activate transcoding"
msgstr "Activer transcodificacion"
msgid "Presets:"
msgstr "Presets:"
msgid "Video options:"
msgstr "Opciones Video:"
msgid "Audio options:"
msgstr "Opciones Audio:"
msgid "Bitrate (kb/s):"
msgstr "Bitrate (kb/s):"
msgid "Codec:"
msgstr "Codec:"
msgid "Width (px):"
msgstr "Ancho (px):"
msgid "Height (px):"
msgstr "Alto (px):"
msgid "Channels:"
msgstr "Canales:"
msgid "Sample rate (Hz):"
msgstr "Sample rate (Гц):"
msgid "Play streams mode:"
msgstr "Tocar en modo streams:"
msgid "Built-in player"
msgstr "Reproductor interno"
msgid "VLC media player"
msgstr "Reproductor VLC"
msgid "Only get m3u file"
msgstr "Solo bajar archivo *.m3u"
msgid "Save and restart the program to apply the settings."
msgstr "Guarde y reinicie el programa para aplicar la configuración."
msgid "Some images may have problems displaying the favorites list!"
msgstr "Algunas imágenes pueden tener problemas para mostrar la lista de favoritos!"
msgid "Operates in standby mode or current active transponder!"
msgstr "Funciona en modo de espera o transpondedor activo actual!"
msgid "No connection to the receiver!"
msgstr "Sin conexión al receptor!"
msgid "Signal level"
msgstr "Nivel de señal"
msgid "Receiver info"
msgstr "Informacion sobre receptor"
msgid "A profile with that name exists!"
msgstr "Existe un perfil con ese nombre!"
msgid "Show short info as hints in the main services list"
msgstr "Mostrar información breve como sugerencias en la lista de servicios principal"
msgid "Show detailed info as hints in the bouquet list"
msgstr "Mostrar información detallada como pistas en la lista de bouquet"
msgid "Enable alternate bouquet file naming"
msgstr "Habilitar nombres alternativos de archivos de bouquet"
msgid "Allows you to name bouquet files using their names."
msgstr "Le permite nombrar archivos de bouquet usando sus nombres."
msgid "Appearance"
msgstr "Apariencia"
msgid "Enable Themes support"
msgstr "Habilitar compatibilidad con temas"
msgid "Gtk3 Theme:"
msgstr "Тема Gtk3:"
msgid "Icon Theme:"
msgstr "Тема Icono:"
msgid "Gtk3 Themes and Icons:"
msgstr "Tema Gtk3 e Iconos:"
msgid "Deleting data..."
msgstr "Borrando datos ..."
msgid "Download from the receiver"
msgstr "Descargar desde el receptor"
msgid "Remove all picons from the receiver"
msgstr "Eliminar todos los picons del receptor"
msgid "Service reference"
msgstr "Referencia de servicio"
msgid "Enable support for"
msgstr "Habilitar soporte para"
msgid "Auto-check for updates"
msgstr "Verificación automática de actualizaciones"
msgid "Filter services"
msgstr "Filtrar servicios"
msgid "Filter services in the main list."
msgstr "Filtrar servicios en la lista principal."
msgid "Destination:"
msgstr "Destino:"
msgid "EXPERIMENTAL!"
msgstr "EXPERIMENTAL!"
msgid "Sorting data..."
msgstr "Ordenando datos..."
msgid "There are unsaved changes.\n\n\t Save them now?"
msgstr "Hay cambios sin guardar.\n\n\t ¿Guardarlos ahora?"
msgid "Are you sure you want to change the order\n\t of services in this bouquet?"
msgstr "¿Está seguro de que desea cambiar el orden\n\t de servicios en este bouquet?"
msgid "Remove from the receiver"
msgstr "Retirar del receptor"
msgid "Screenshot"
msgstr "Captura de pantalla"
msgid "Video"
msgstr "Vidео"

View File

@@ -1,7 +1,7 @@
# Copyright (C) 2018-2019 Frank Neirynck # Copyright (C) 2018-2020 Frank Neirynck
# This file is distributed under the MIT license. # This file is distributed under the MIT license.
# #
# Frank Neirynck <frank@insink.be>, 2018-2019. # Frank Neirynck <frank@insink.be>, 2018-2020.
# #
msgid "" msgid ""
msgstr "" msgstr ""
@@ -10,11 +10,6 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Language-Team: \n"
"X-Generator: Poedit 2.2.1\n"
msgid "translator-credits" msgid "translator-credits"
msgstr "Frank Neirynck <frank@insink.be>" msgstr "Frank Neirynck <frank@insink.be>"
@@ -676,11 +671,11 @@ msgstr "Speel stream af"
msgid "Disabled" msgid "Disabled"
msgstr "Uitgeschakeld" msgstr "Uitgeschakeld"
msgid "Enable ver. 5 support (experimental)" msgid "Enable lamedb ver. 5 support"
msgstr "Ondersteuning voor ver. 5 inschakelen (experimenteel)" msgstr "Ondersteuning voor lamedb ver. 5 inschakelen"
msgid "Enable HTTP API (experimental)" msgid "Enable HTTP API"
msgstr "HTTP API inschakelen (experimenteel)" msgstr "HTTP API inschakelen"
msgid "Switch(zap) the channel(Ctrl + Z)" msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Schakelaar (ZAP) naar het kanaal (CTRL + Z)" msgstr "Schakelaar (ZAP) naar het kanaal (CTRL + Z)"
@@ -776,4 +771,245 @@ msgid "Playlist import"
msgstr "Importeer playlist" msgstr "Importeer playlist"
msgid "Getting link error:" msgid "Getting link error:"
msgstr "Volgende Link error gekregen:" msgstr "Volgende Link error gekregen:"
msgid "Extra"
msgstr "Extra"
msgid "Apply profile settings"
msgstr "Pas profiel settings toe"
msgid "Settings type:"
msgstr "Settings type:"
msgid "Set default"
msgstr "Stel standaard in"
msgid "Language:"
msgstr "Taal:"
msgid "Load the last open configuration at program startup"
msgstr "Laad de laatst geopende configuratie op bij opstart programma"
msgid "Enable direct playback bar"
msgstr "Laat onmiddelijk playback bar toe"
msgid "Enables direct sending and playback of media links on the receiver"
msgstr "Laat rechtstreeks versturen van and playback en media links op de ontvanger toe"
msgid "Watch the channel in the program"
msgstr "Bekijk het kanaal in het programma"
msgid "Zap and Play"
msgstr "Zap en speel af"
msgid "Drag or paste the link here"
msgstr "Sleep of plak link naar hier"
msgid "Remove added links in the playlist"
msgstr "Verwijder toegevoegde links uit playlist"
msgid "A bouquet with that name exists!"
msgstr "Er bestaat al een boeket met deze naam!"
msgid "Details"
msgstr "Details"
msgid "Profile"
msgstr "Profiel"
msgid "Reset"
msgstr "Reset"
msgid "File"
msgstr "File"
msgid "Picons manager"
msgstr "Picons manager"
msgid "Explorer"
msgstr "Explorer"
msgid "Satellite url:"
msgstr "Satelliet url:"
msgid "Cut"
msgstr "Knip"
msgid "Paste"
msgstr "Plak"
msgid "To the top"
msgstr "Naar de top"
msgid "To the end"
msgstr "Naar het einde"
msgid "View"
msgstr "View"
msgid "Lock"
msgstr "Slot"
msgid "Parent lock"
msgstr "Ouderlijk Slot"
msgid "Hide/Skip"
msgstr "Verberg/Spring"
msgid "IPTV tools"
msgstr "IPTV instrumenten"
msgid "Make profile folder as default for the additional data"
msgstr "Maak profiel map standaard voor addionele data"
msgid "Default data path:"
msgstr "Standaard data pad:"
msgid "Streams record path:"
msgstr "Opnamepad streams:"
msgid "Record"
msgstr "Opnemen"
msgid "Record:"
msgstr "Opnemen:"
msgid "Record to disk:"
msgstr "Opnemen op schijf:"
msgid "Streaming"
msgstr "Streaming"
msgid "Activate transcoding"
msgstr "Activeer transcodering"
msgid "Presets:"
msgstr "Onstellingen:"
msgid "Video options:"
msgstr "Video opties:"
msgid "Audio options:"
msgstr "Audio opties:"
msgid "Bitrate (kb/s):"
msgstr "Bitrate (kb/s):"
msgid "Codec:"
msgstr "Codec:"
msgid "Width (px):"
msgstr "Breedte (px):"
msgid "Height (px):"
msgstr "Hoogte (px):"
msgid "Channels:"
msgstr "Kanalen:"
msgid "Sample rate (Hz):"
msgstr "Sample rate (Hz):"
msgid "Play streams mode:"
msgstr "Speel in streams mode:"
msgid "Built-in player"
msgstr "Ingebouwde speler"
msgid "VLC media player"
msgstr "VLC Media speler"
msgid "Only get m3u file"
msgstr "Enkel de m3u file ophalen"
msgid "Save and restart the program to apply the settings."
msgstr "Het programma opslaan en herstarten om settings toe te passen."
msgid "Some images may have problems displaying the favorites list!"
msgstr "Sommige afbeeldingen kunnen problemen opleveren bij vertonen in de favorietenlijst!"
msgid "Operates in standby mode or current active transponder!"
msgstr "Werkt in standby mode of op dehuiduge actieve transponder!"
msgid "No connection to the receiver!"
msgstr "Geen verbinding met de ontvanger!"
msgid "Signal level"
msgstr "Signaal niveau"
msgid "Receiver info"
msgstr "Informatie over de ontvanger"
msgid "A profile with that name exists!"
msgstr "Er bestaat al een profiel met die naam!"
msgid "Show short info as hints in the main services list"
msgstr "Toon korte info als hints in de hoofd servicelijst"
msgid "Show detailed info as hints in the bouquet list"
msgstr "Toon gedetalleerde info als hints in de boeket lijst"
msgid "Enable alternate bouquet file naming"
msgstr "Laat alternatieve boeket benamingen toe"
msgid "Allows you to name bouquet files using their names."
msgstr "Laat toe om boeket te noemen naar bestandsbenaming."
msgid "Appearance"
msgstr "Uitzicht"
msgid "Enable Themes support"
msgstr "Laat themaondersteuning toe"
msgid "Gtk3 Theme:"
msgstr "Gtk3 Thema:"
msgid "Icon Theme:"
msgstr "Icoon Thema:"
msgid "Gtk3 Themes and Icons:"
msgstr "Gtk3 en Icoon Themas:"
msgid "Deleting data..."
msgstr "Wist data ..."
msgid "Download from the receiver"
msgstr "Download van de ontvanger"
msgid "Service reference"
msgstr "Service referentie"
msgid "Auto-check for updates"
msgstr "Auto-check voor updates"
msgid "Filter services"
msgstr "Filter diensten"
msgid "Filter services in the main list."
msgstr "Filter diensten in de hoofdlijst."
msgid "Destination:"
msgstr "Doel:"
msgid "EXPERIMENTAL!"
msgstr "EXPERIMENTEEL!"
msgid "Sorting data..."
msgstr "Data ordenen..."
msgid "There are unsaved changes.\n\n\t Save them now?"
msgstr "Er zijn niet-bwaarde wijzigingen.\n\n\t Nu opslaan?"
msgid "Are you sure you want to change the order\n\t of services in this bouquet?"
msgstr "Ben je zeker dat je de volgorde\n\t van de diensten in dit boeket wil wijzigen?"
msgid "Remove from the receiver"
msgstr "Verwijder van de ontvanger"
msgid "Screenshot"
msgstr "Schermafbeelding"
msgid "Video"
msgstr "Vidео"

299
po/pl/demon-editor.po Normal file → Executable file
View File

@@ -1,7 +1,6 @@
# Copyright (C) 2018-2019 Dmitriy Yefremov # Copyright (C) 2018-2019 Dmitriy Yefremov
# This file is distributed under the MIT license. # This file is distributed under the MIT license.
# #
#
msgid "" msgid ""
msgstr "" msgstr ""
"Last-Translator: wwns\n" "Last-Translator: wwns\n"
@@ -9,16 +8,20 @@ msgstr ""
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Language-Team: \n"
"X-Generator: Poedit 2.3\n"
msgid "translator-credits" msgid "translator-credits"
msgstr "wwns" msgstr "wwns"
# Main # Main
msgid "File"
msgstr "Plik"
msgid "View"
msgstr "Widok"
msgid "Lock"
msgstr "Zablokuj"
msgid "Service" msgid "Service"
msgstr "Serwis" msgstr "Serwis"
@@ -86,7 +89,7 @@ msgid "Hide"
msgstr "Ukryj" msgstr "Ukryj"
msgid "Hide/Skip On/Off Ctrl + H" msgid "Hide/Skip On/Off Ctrl + H"
msgstr "Ukryj/Pomiń Wł./Wył Ctrl + H" msgstr "Ukryj/Pomiń wł./wył Ctrl + H"
msgid "Add IPTV or stream service" msgid "Add IPTV or stream service"
msgstr "Dodaj strumień IPTV" msgstr "Dodaj strumień IPTV"
@@ -149,7 +152,7 @@ msgid "Open"
msgstr "Otwórz" msgstr "Otwórz"
msgid "Parent lock On/Off Ctrl + L" msgid "Parent lock On/Off Ctrl + L"
msgstr "Blokada rodzicielska On/Off Ctrl + L" msgstr "Blokada rodzicielska wł./wył Ctrl + L"
msgid "Picons" msgid "Picons"
msgstr "Pikony" msgstr "Pikony"
@@ -175,9 +178,6 @@ msgstr "Zapisz"
msgid "Search" msgid "Search"
msgstr "Szukaj" msgstr "Szukaj"
msgid "Services"
msgstr "Kanały"
msgid "Services filter" msgid "Services filter"
msgstr "Filtr kanałów" msgstr "Filtr kanałów"
@@ -212,10 +212,10 @@ msgid "Host:"
msgstr "Host:" msgstr "Host:"
msgid "Loading data..." msgid "Loading data..."
msgstr "Ładowanie danych…." msgstr "Ładowanie danych…"
msgid "Receive" msgid "Receive"
msgstr "Pobieranie" msgstr "Pobierz"
msgid "Receive files from receiver" msgid "Receive files from receiver"
msgstr "Pobieranie plików z odbiornika" msgstr "Pobieranie plików z odbiornika"
@@ -251,7 +251,19 @@ msgid "User bouquet files:"
msgstr "Pliki bukietu użytkownika:" msgstr "Pliki bukietu użytkownika:"
msgid "Extra:" msgid "Extra:"
msgstr "Ekstra:" msgstr "Dodatkowe"
msgid "IPTV tools"
msgstr "Narzędzia IPTV"
msgid "Picons manager"
msgstr "Menedżer pikonów"
msgid "Hide/Skip"
msgstr "Ukryj/Pomiń"
msgid "Parent lock"
msgstr "Blokada rodzicielska"
# Filter bar # Filter bar
msgid "Only free" msgid "Only free"
@@ -264,9 +276,6 @@ msgid "All types"
msgstr "Wszystkie typy" msgstr "Wszystkie typy"
# Streams player # Streams player
msgid "Play"
msgstr "Odtwarzaj"
msgid "Stop playback" msgid "Stop playback"
msgstr "Zatrzymaj odtwarzanie" msgstr "Zatrzymaj odtwarzanie"
@@ -533,10 +542,10 @@ msgid "Done!"
msgstr "Zrobione!" msgstr "Zrobione!"
msgid "Please, wait..." msgid "Please, wait..."
msgstr "Proszę czekać ..." msgstr "Proszę czekać"
msgid "Resizing..." msgid "Resizing..."
msgstr "Zmiana rozmiaru..." msgstr "Zmiana rozmiaru"
msgid "Select paths!" msgid "Select paths!"
msgstr "Wybierz ścieżki!" msgstr "Wybierz ścieżki!"
@@ -564,7 +573,7 @@ msgstr "Nie znaleziono VLC. Sprawdź, czy jest zainstalowany!"
# Search unavailable streams dialog # Search unavailable streams dialog
msgid "Please wait, streams testing in progress..." msgid "Please wait, streams testing in progress..."
msgstr "Proszę czekać, trwa testowanie strumieni..." msgstr "Proszę czekać, trwa testowanie strumieni"
msgid "Found" msgid "Found"
msgstr "Znaleziono" msgstr "Znaleziono"
@@ -587,15 +596,9 @@ msgstr "Brak danych do zapisania!"
msgid "Network" msgid "Network"
msgstr "Sieć" msgstr "Sieć"
msgid "Paths"
msgstr "Ścieżki"
msgid "Program" msgid "Program"
msgstr "Program" msgstr "Program"
msgid "Backup:"
msgstr "Kopia:"
msgid "Backup" msgid "Backup"
msgstr "Kopia" msgstr "Kopia"
@@ -611,21 +614,6 @@ msgstr "Przywróć bukiety"
msgid "Restore all" msgid "Restore all"
msgstr "Przywrócić wszystko" msgstr "Przywrócić wszystko"
msgid "Before saving"
msgstr "Przed zapisaniem"
msgid "Before downloading from the receiver"
msgstr "Przed pobraniem z odbiornika"
msgid "Set background color for the services"
msgstr "Ustaw kolor tła dla usług"
msgid "Marked as new:"
msgstr "Oznacz jako nowy:"
msgid "With an extra name in the bouquet:"
msgstr "Z dodatkową nazwą w bukiecie:"
msgid "Select" msgid "Select"
msgstr "Wybierz" msgstr "Wybierz"
@@ -666,21 +654,6 @@ msgstr "Testuj połączenie"
msgid "Double click on the service in the bouquet list:" msgid "Double click on the service in the bouquet list:"
msgstr "Kliknij dwukrotnie usługę na liście bukietów:" msgstr "Kliknij dwukrotnie usługę na liście bukietów:"
msgid "Zap"
msgstr "Przełącz"
msgid "Play stream"
msgstr "Odtwórz strumień"
msgid "Disabled"
msgstr "Wyłączone"
msgid "Enable ver. 5 support (experimental)"
msgstr "Włącz wer. 5 wsparcie (eksperymentalne)"
msgid "Enable HTTP API (experimental)"
msgstr "Włącz API HTTP (eksperymentalne)"
msgid "Switch(zap) the channel(Ctrl + Z)" msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Przełącz(zap) kanał(Ctrl + Z)" msgstr "Przełącz(zap) kanał(Ctrl + Z)"
@@ -780,3 +753,217 @@ msgstr "Import listy odtwarzania"
msgid "Getting link error:" msgid "Getting link error:"
msgstr "Błąd pobierania łącza:" msgstr "Błąd pobierania łącza:"
msgid "Apply profile settings"
msgstr "Zastosuj ustawienia profilu"
msgid "Settings type:"
msgstr "Ustawienia dla:"
msgid "Set default"
msgstr "Uataw domyślnie"
msgid "Enable direct playback bar"
msgstr "Włącz pasek bezpośredniego odtwarzania"
msgid "Enables direct sending and playback of media links on the receiver"
msgstr "Umożliwia bezpośrednie wysyłanie i odtwarzanie łączy multimedialnych w odbiorniku"
msgid "Watch the channel in the program"
msgstr "Obejrzyj kanał w programie"
msgid "Receiver info"
msgstr "Informacje o odbiorniku"
msgid "Operates in standby mode or current active transponder!"
msgstr "Działa w trybie gotowości lub aktualnie jest aktywny transponder!"
msgid "Download picons from the receiver"
msgstr "Pobierz pikony z odbiornika"
msgid "Remove picons from the receiver"
msgstr "Usuń pikony z odbiornika"
msgid "Use http to reload data in the receiver."
msgstr "Użyj http aby ponownie załadować dane do odbiornika."
msgid "Drag or paste the link here"
msgstr "Przeciągnij lub wklej tutaj link"
msgid "Remove added links in the playlist"
msgstr "Usuń dodane linki z listy odtwarzania"
msgid "A bouquet with that name exists!"
msgstr "Istnieje bukiet o tej nazwie!"
msgid "Channels:"
msgstr "Kanały:"
msgid "Remove all picons from the receiver"
msgstr "Usuń wszystkie pikony z odbiornika"
msgid "Download from the receiver"
msgstr "Pobierz z odbiornika"
msgid "The Neutrino has only experimental support. Not all features are supported!"
msgstr "Neutrino ma jedynie wsparcie eksperymentalne. Nie wszystkie funkcje są obsługiwane!"
# Appearance
msgid "Appearance"
msgstr "Wygląd"
msgid "Enable Themes support"
msgstr "Włącz obsługę motywów"
msgid "EXPERIMENTAL!"
msgstr "EKSPERYMENTALNE!"
msgid "Gtk3 Theme:"
msgstr "Gtk3 motyw:"
msgid "Icon Theme:"
msgstr "Motyw ikon:"
msgid "Gtk3 Themes and Icons:"
msgstr "Gtk3 motywy i ikony:"
msgid "Save and restart the program to apply the settings."
msgstr "Zapisz i uruchom ponownie program, aby zastosować ustawienia."
# Extra
msgid "Extra"
msgstr "Ekstra"
msgid "Enable lamedb ver. 5 support"
msgstr "Włącz lamedb wer. 5 wsparcie"
msgid "Enable alternate bouquet file naming"
msgstr "Włącz alternatywne nazewnictwo plików bukietów"
msgid "Some images may have problems displaying the favorites list!"
msgstr "Niektóre obrazy mogą mieć problemy z wyświetlaniem listy ulubionych!"
msgid "Allows you to name bouquet files using their names."
msgstr "Pozwala nazwać pliki bukietów przy użyciu ich nazw."
msgid "Enable HTTP API"
msgstr "Włącz API HTTP"
msgid "Enable send to receiver"
msgstr "Włącz wysyłanie do odbiornika"
msgid "Zap"
msgstr "Przełącz"
msgid "Play"
msgstr "Odtwarzaj"
msgid "Zap and Play"
msgstr "Przełącz i Odtwórz"
msgid "Play stream"
msgstr "Odtwórz strumień"
msgid "Disabled"
msgstr "Wyłączone"
msgid "Language:"
msgstr "Język:"
msgid "Load the last open configuration at program startup"
msgstr "Załaduj ostatnią otwartą konfigurację podczas uruchamiania programu"
msgid "Show short info as hints in the main services list"
msgstr "Pokaż krótkie informacje jako wskazówki na głównej liście usług"
msgid "Show detailed info as hints in the bouquet list"
msgstr "Pokaż szczegółowe informacje jako wskazówki na liście bukietów"
msgid "Set background color for the services"
msgstr "Ustaw kolor tła dla usług"
msgid "Marked as new:"
msgstr "Oznacz jako nowy:"
msgid "With an extra name in the bouquet:"
msgstr "Z dodatkową nazwą w bukiecie:"
msgid "Backup:"
msgstr "Kopia:"
msgid "Before saving"
msgstr "Przed zapisaniem"
msgid "Before downloading from the receiver"
msgstr "Przed pobraniem z odbiornika"
#Streaming
msgid "Streaming"
msgstr "Transmisja"
msgid "Record to disk:"
msgstr "Nagrywanie na dysk:"
msgid "Activate transcoding"
msgstr "Aktywuj transkodowanie"
msgid "Presets:"
msgstr "Ustawienia wstępne:"
msgid "720p TV/device"
msgstr "Dla urządzeń z obsługą rozdzielczości 720p"
msgid "1080p TV/device"
msgstr "Dla urządzeń z obsługą rozdzielczości 1080p"
msgid "Video options:"
msgstr "Opcje wideo:"
msgid "Width (px):"
msgstr "Szerokość (px):"
msgid "Height (px):"
msgstr "Wysokość (px):"
msgid "Codec:"
msgstr "Kodek:"
msgid "Audio options:"
msgstr "Opcje audio:"
msgid "Services"
msgstr "Kanały"
msgid "Sample rate (Hz):"
msgstr "Częstotliwość próbkowania (Hz):"
msgid "Play streams mode:"
msgstr "Odtwarzaj tryb strumieni:"
msgid "Bulit-in player"
msgstr "Wbudowany odtwarzacz"
msgid "VLC media player"
msgstr "Odtwarzacz multimedialny VLC"
msgid "Only get m3u file"
msgstr "Tylko pobierz plik m3u"
# Paths
msgid "Paths"
msgstr "Ścieżki"
msgid "Make profile folder as default for the additional data"
msgstr "Ustaw folder profilu jako domyślny dla dodatkowych danych"
msgid "Sets the profile folder as default to store picons, backups, etc."
msgstr "Ustawia folder profilu jako domyślny do przechowywania pikonów, kopii zapasowych itp."
msgid "Default data path:"
msgstr "Domyślna ścieżka danych:"
msgid "Record"
msgstr "Nagrania"
msgid "Streams record path:"
msgstr "Ścieżka zapisu nagrań:"

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2018-2019 Frank Neirynck # Copyright (C) 2018-2020 Frank Neirynck
# This file is distributed under the MIT license. # This file is distributed under the MIT license.
# #
#Frank Neirynck <frank@insink.be>, 2018-2019. #Frank Neirynck <frank@insink.be>, 2018-2019.
@@ -663,11 +663,11 @@ msgstr "Play stream"
msgid "Disabled" msgid "Disabled"
msgstr "Desativado" msgstr "Desativado"
msgid "Enable ver. 5 support (experimental)" msgid "Enable lamedb ver. 5 support"
msgstr "Ativar ver. 5 suporte (experimental)" msgstr "Ativar lamedb ver. 5 suporte"
msgid "Enable HTTP API (experimental)" msgid "Enable HTTP API"
msgstr "Ativar HTTP API (experimental)" msgstr "Ativar HTTP API"
msgid "Switch(zap) the channel(Ctrl + Z)" msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Mudar(zap) o canal(Ctrl + Z)" msgstr "Mudar(zap) o canal(Ctrl + Z)"
@@ -763,4 +763,250 @@ msgid "Playlist import"
msgstr "Importação de lista de reprodução" msgstr "Importação de lista de reprodução"
msgid "Getting link error:" msgid "Getting link error:"
msgstr "Obtendo erro de link:" msgstr "Obtendo erro de link:"
msgid "Extra"
msgstr "Extra"
msgid "Apply profile settings"
msgstr "Aplicar ajustes de perfil"
msgid "Settings type:"
msgstr "Tipo de ajustes:"
msgid "Set default"
msgstr "Por defecto"
msgid "Language:"
msgstr "Idioma:"
msgid "Load the last open configuration at program startup"
msgstr "Cargar la última configuración abierta al iniciar el programa"
msgid "Enable direct playback bar"
msgstr "Habilitar la barra de reproducción directa"
msgid "Enables direct sending and playback of media links on the receiver"
msgstr "Habilita el envío directo y la reproducción de enlaces de medios en el receptor"
msgid "Watch the channel in the program"
msgstr "Ver el canal en el programa"
msgid "Zap and Play"
msgstr "Zapear y reproducir"
msgid "Drag or paste the link here"
msgstr "Soltar o pegar en enlace aquí"
msgid "Remove added links in the playlist"
msgstr "Quitar los enlaces añadidos en la lista de reproducción"
msgid "A bouquet with that name exists!"
msgstr "¡Ya existe un bouquet con ese nombre!"
msgid "Details"
msgstr "Detalles"
msgid "Profile"
msgstr "Perfil"
msgid "Reset"
msgstr "Reset"
msgid "File"
msgstr "Archivo"
msgid "Picons manager"
msgstr "Picons manager"
msgid "Explorer"
msgstr "Explorador"
msgid "Satellite url:"
msgstr "Url Satelite:"
msgid "Cut"
msgstr "Cortar"
msgid "Paste"
msgstr "Pegar"
msgid "To the top"
msgstr "Ir arriba"
msgid "To the end"
msgstr "Al final"
msgid "View"
msgstr "Vista"
msgid "Lock"
msgstr "Bloqueo"
msgid "Parent lock"
msgstr "Bloqueo parental"
msgid "Hide/Skip"
msgstr "Escoder/Saltar"
msgid "IPTV tools"
msgstr "Intrumentos IPTV"
msgid "Make profile folder as default for the additional data"
msgstr "Has folder de perfil estandar para datos adicionales"
msgid "Default data path:"
msgstr "Ruta estandar de datos:"
msgid "Streams record path:"
msgstr "Ruta de gravacion de stream:"
msgid "Record"
msgstr "Gravar"
msgid "Record:"
msgstr "Gravar:"
msgid "Record to disk:"
msgstr "Gravar en disco:"
msgid "Streaming"
msgstr "Streameando"
msgid "Activate transcoding"
msgstr "Activer transcodificacion"
msgid "Presets:"
msgstr "Presets:"
msgid "Video options:"
msgstr "Opciones Video:"
msgid "Audio options:"
msgstr "Opciones Audio:"
msgid "Bitrate (kb/s):"
msgstr "Bitrate (kb/s):"
msgid "Codec:"
msgstr "Codec:"
msgid "Width (px):"
msgstr "Ancho (px):"
msgid "Height (px):"
msgstr "Alto (px):"
msgid "Channels:"
msgstr "Canales:"
msgid "Sample rate (Hz):"
msgstr "Sample rate (Гц):"
msgid "Play streams mode:"
msgstr "Tocar en modo streams:"
msgid "Built-in player"
msgstr "Reproductor interno"
msgid "VLC media player"
msgstr "Reproductor VLC"
msgid "Only get m3u file"
msgstr "Solo bajar archivo *.m3u"
msgid "Save and restart the program to apply the settings."
msgstr "Guarde y reinicie el programa para aplicar la configuración."
msgid "Some images may have problems displaying the favorites list!"
msgstr "Algunas imágenes pueden tener problemas para mostrar la lista de favoritos!"
msgid "Operates in standby mode or current active transponder!"
msgstr "Funciona en modo de espera o transpondedor activo actual!"
msgid "No connection to the receiver!"
msgstr "Sin conexión al receptor!"
msgid "Signal level"
msgstr "Nivel de señal"
msgid "Receiver info"
msgstr "Informacion sobre receptor"
msgid "A profile with that name exists!"
msgstr "Existe un perfil con ese nombre!"
msgid "Show short info as hints in the main services list"
msgstr "Mostrar información breve como sugerencias en la lista de servicios principal"
msgid "Show detailed info as hints in the bouquet list"
msgstr "Mostrar información detallada como pistas en la lista de bouquet"
msgid "Enable alternate bouquet file naming"
msgstr "Habilitar nombres alternativos de archivos de bouquet"
msgid "Allows you to name bouquet files using their names."
msgstr "Le permite nombrar archivos de bouquet usando sus nombres."
msgid "Appearance"
msgstr "Apariencia"
msgid "Enable Themes support"
msgstr "Habilitar compatibilidad con temas"
msgid "Gtk3 Theme:"
msgstr "Тема Gtk3:"
msgid "Icon Theme:"
msgstr "Тема Icono:"
msgid "Gtk3 Themes and Icons:"
msgstr "Tema Gtk3 e Iconos:"
msgid "Deleting data..."
msgstr "Borrando datos ..."
msgid "Download from the receiver"
msgstr "Descargar desde el receptor"
msgid "Remove all picons from the receiver"
msgstr "Eliminar todos los picons del receptor"
msgid "Service reference"
msgstr "Referencia de servicio"
msgid "Enable support for"
msgstr "Habilitar soporte para"
msgid "Auto-check for updates"
msgstr "Verificación automática de actualizaciones"
msgid "Filter services"
msgstr "Filtrar servicios"
msgid "Filter services in the main list."
msgstr "Filtrar servicios en la lista principal."
msgid "Destination:"
msgstr "Destino:"
msgid "EXPERIMENTAL!"
msgstr "EXPERIMENTAL!"
msgid "Sorting data..."
msgstr "Ordenando datos..."
msgid "There are unsaved changes.\n\n\t Save them now?"
msgstr "Hay cambios sin guardar.\n\n\t ¿Guardarlos ahora?"
msgid "Are you sure you want to change the order\n\t of services in this bouquet?"
msgstr "¿Está seguro de que desea cambiar el orden\n\t de servicios en este bouquet?"
msgid "Remove from the receiver"
msgstr "Retirar del receptor"
msgid "Screenshot"
msgstr "Captura de pantalla"
msgid "Video"
msgstr "Vidео"

View File

@@ -1,4 +1,4 @@
# Copyright (C) 2018-2019 Dmitriy Yefremov # Copyright (C) 2018-2020 Dmitriy Yefremov
# This file is distributed under the MIT license. # This file is distributed under the MIT license.
# #
# #
@@ -662,11 +662,11 @@ msgstr "Воспр. потока"
msgid "Disabled" msgid "Disabled"
msgstr "Выкл." msgstr "Выкл."
msgid "Enable ver. 5 support (experimental)" msgid "Enable lamedb ver. 5 support"
msgstr "Включить поддержку вер. 5 (экспериментально)" msgstr "Включить поддержку lamedb вер. 5"
msgid "Enable HTTP API (experimental)" msgid "Enable HTTP API"
msgstr "Включить HTTP API (экспериментально)" msgstr "Включить HTTP API"
msgid "Switch(zap) the channel(Ctrl + Z)" msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Переключить канал(Ctrl + Z)" msgstr "Переключить канал(Ctrl + Z)"
@@ -782,8 +782,8 @@ msgstr "Язык:"
msgid "Load the last open configuration at program startup" msgid "Load the last open configuration at program startup"
msgstr "Загружать последнюю открытую конфигурацию при запуске программы" msgstr "Загружать последнюю открытую конфигурацию при запуске программы"
msgid "Enable direct playback bar (experimental)" msgid "Enable direct playback bar"
msgstr "Включить панель прямого воспроизведения (экспериментально)" msgstr "Включить панель прямого воспроизведения"
msgid "Enables direct sending and playback of media links on the receiver" msgid "Enables direct sending and playback of media links on the receiver"
msgstr "Включает прямую отправку и воспроизведение медиа-ссылок на ресивере" msgstr "Включает прямую отправку и воспроизведение медиа-ссылок на ресивере"
@@ -801,4 +801,224 @@ msgid "Remove added links in the playlist"
msgstr "Удалить добавленные ссылки из плейлиста" msgstr "Удалить добавленные ссылки из плейлиста"
msgid "A bouquet with that name exists!" msgid "A bouquet with that name exists!"
msgstr "Букет с таким именем существует!" msgstr "Букет с таким именем существует!"
msgid "Details"
msgstr "Подробно"
msgid "Profile"
msgstr "Профиль"
msgid "Reset"
msgstr "Сброс"
msgid "File"
msgstr "Файл"
msgid "Picons manager"
msgstr "Менеджер пиконов"
msgid "Explorer"
msgstr "Проводник"
msgid "Satellite url:"
msgstr "URL cпутника:"
msgid "Cut"
msgstr "Вырезать"
msgid "Paste"
msgstr "Вставить"
msgid "To the top"
msgstr "В начало"
msgid "To the end"
msgstr "В конец"
msgid "View"
msgstr "Вид"
msgid "Lock"
msgstr "Замок"
msgid "Parent lock"
msgstr "Родительский замок"
msgid "Hide/Skip"
msgstr "Скрыть/Пропустить"
msgid "IPTV tools"
msgstr "Инструменты IPTV"
msgid "Make profile folder as default for the additional data"
msgstr "Установить папку профиля по умолчанию для доп. данных"
msgid "Default data path:"
msgstr "Путь к данным по умолчанию:"
msgid "Streams record path:"
msgstr "Путь к записям потоков:"
msgid "Record"
msgstr "Запись"
msgid "Record:"
msgstr "Запись:"
msgid "Record to disk:"
msgstr "Запись на диск:"
msgid "Streaming"
msgstr "Потоки"
msgid "Activate transcoding"
msgstr "Активировать перекодировку"
msgid "Presets:"
msgstr "Предустановки:"
msgid "Video options:"
msgstr "Опции видео:"
msgid "Audio options:"
msgstr "Опции аудио:"
msgid "Bitrate (kb/s):"
msgstr "Битрейт (kb/s):"
msgid "Codec:"
msgstr "Кодек:"
msgid "Width (px):"
msgstr "Ширина (px):"
msgid "Height (px):"
msgstr "Высота (px):"
msgid "Channels:"
msgstr "Каналы:"
msgid "Sample rate (Hz):"
msgstr "Частота дискр. (Гц):"
msgid "Play streams mode:"
msgstr "Режим воспроизведения потоков:"
msgid "Built-in player"
msgstr "Встроенный плеер"
msgid "VLC media player"
msgstr "VLC медиаплеер"
msgid "Only get m3u file"
msgstr "Получить файл *.m3u"
msgid "Save and restart the program to apply the settings."
msgstr "Сохраните и перезапустите программу, чтобы применить настройки."
msgid "Some images may have problems displaying the favorites list!"
msgstr "Некоторые образы могут иметь проблемы с отображением списка избранного!"
msgid "Operates in standby mode or current active transponder!"
msgstr "Работает в режиме ожидания или текущем активном транспондере!"
msgid "No connection to the receiver!"
msgstr "Нет соединение с ресивером!"
msgid "Signal level"
msgstr "Уровень сигнала"
msgid "Receiver info"
msgstr "Информация о ресивере"
msgid "A profile with that name exists!"
msgstr "Профиль с таким именем существует!"
msgid "Show short info as hints in the main services list"
msgstr "Показывать краткую информацию в виде подсказок в основном списке услуг"
msgid "Show detailed info as hints in the bouquet list"
msgstr "Показывать подробную информацию в виде подсказок в списке букетов"
msgid "Enable alternate bouquet file naming"
msgstr "Включить альтернативное именование файлов букета"
msgid "Allows you to name bouquet files using their names."
msgstr "Позволяет называть файлы букетов, используя их имена."
msgid "Appearance"
msgstr "Внешний вид"
msgid "Enable Themes support"
msgstr "Включить поддержку тем"
msgid "Gtk3 Theme:"
msgstr "Тема Gtk3:"
msgid "Icon Theme:"
msgstr "Тема значков:"
msgid "Gtk3 Themes and Icons:"
msgstr "Gtk3 темы и иконки:"
msgid "Deleting data..."
msgstr "Удаление данных ..."
msgid "Download from the receiver"
msgstr "Загрузить с ресивера"
msgid "Remove all picons from the receiver"
msgstr "Удалить все пиконы с ресивера"
msgid "Service reference"
msgstr "Сервисная ссылка"
msgid "Enable support for"
msgstr "Включить поддержку"
msgid "Auto-check for updates"
msgstr "Автопроверка обновлений"
msgid "Filter services"
msgstr "Фильтровать сервисы"
msgid "Filter services in the main list."
msgstr "Фильтровать сервисы в основном списке."
msgid "Destination:"
msgstr "Назначение:"
msgid "EXPERIMENTAL!"
msgstr "ЭКСПЕРИМЕНТАЛЬНО!"
msgid "Sorting data..."
msgstr "Сортировка данных..."
msgid "There are unsaved changes.\n\n\t Save them now?"
msgstr "Имеются несохранённые изменения.\n\n\t Сохранить их сейчас?"
msgid "Are you sure you want to change the order\n\t of services in this bouquet?"
msgstr "Вы уверены, что хотите изменить порядок\n\t сервисов в этом букете?"
msgid "Remove from the receiver"
msgstr "Удалить с ресивера"
msgid "Screenshot"
msgstr "Скриншот"
msgid "Video"
msgstr "Видео"
msgid "The Neutrino has only experimental support. Not all features are supported!"
msgstr "Neutrino имеет только экспериментальную поддержку. Поддерживаются не все функции!"
msgid "Enable experimental features"
msgstr "Включить экспериментальные функции"
msgid "Can't Playback!"
msgstr "Не удается воспроизвести!"
msgid "Enable Dark Mode"
msgstr "Включить темный режим"

990
po/tr/demon-editor.po Normal file
View File

@@ -0,0 +1,990 @@
msgid ""
msgstr ""
"Project-Id-Version: DemonEditor\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-04-16 15:59+0300\n"
"PO-Revision-Date: 2020-06-08 21:53+0300\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Last-Translator: audi06_19 <info@dreamosat-forum.com>\n"
"Language-Team: \n"
"X-Generator: Poedit 2.2.1\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Language: tr\n"
msgid "translator-credits"
msgstr "audi06_19 <info@dreamosat-forum.com>"
# Main
msgid "Service"
msgstr "Hizmet"
msgid "Package"
msgstr "Paket"
msgid "Type"
msgstr "Tür"
msgid "Picon"
msgstr "Picon"
msgid "Freq"
msgstr "Frekans"
msgid "Rate"
msgstr "Oran"
msgid "Pol"
msgstr "Pol"
msgid "System"
msgstr "Sistem"
msgid "Pos"
msgstr "Pos"
msgid "Num"
msgstr "Num"
msgid "Current IP:"
msgstr "Geçerli IP:"
msgid "Assign"
msgstr "Ata"
msgid "Bouquet details"
msgstr "Buket detayları"
msgid "Bouquets"
msgstr "Buketler"
msgid "Copy"
msgstr "Kopya"
msgid "Copy reference"
msgstr "Referansı kopyala"
msgid "Download"
msgstr "İndir"
msgid "Edit"
msgstr "Düzelt"
msgid "Edit mаrker text"
msgstr "Marker metnini Düzenle"
msgid "FTP-transfer"
msgstr "FTP aktarımı"
msgid "Global search"
msgstr "Global arama"
msgid "Hide"
msgstr "Gizle"
msgid "Hide/Skip On/Off Ctrl + H"
msgstr "Gizle/Atla Açık/Kapalı Ctrl + H"
msgid "Add IPTV or stream service"
msgstr "IPTV veya akış hizmeti ekle"
msgid "Import m3u"
msgstr "M3u aktar"
msgid "Import m3u file"
msgstr "İçe aktar M3U"
msgid "List configuration"
msgstr "Liste yapılandırması"
msgid "Rename for this bouquet"
msgstr "Bu buketi yeniden adlandır"
msgid "Set default name"
msgstr "Varsayılan adı ayarla"
msgid "Insert marker"
msgstr "İşaretçi ekle"
msgid "Locate in services"
msgstr "Hizmetlerde bulun"
msgid "Locked"
msgstr "Kilitli"
msgid "Move"
msgstr "Taçı"
msgid "New"
msgstr "Yeni"
msgid "New bouquet"
msgstr "Yeni buket"
msgid "Create bouquet"
msgstr "Buket oluştur"
msgid "For current satellite"
msgstr "Mevcut uydu için"
msgid "For current package"
msgstr "Mevcut paket için"
msgid "For current type"
msgstr "Mevcut tip için"
msgid "For each satellite"
msgstr "Her uydu için"
msgid "For each package"
msgstr "Her paket için"
msgid "For each type"
msgstr "Her tip için"
msgid "Open"
msgstr "Aç"
msgid "Parent lock On/Off Ctrl + L"
msgstr "Ebeveyn kilidi Açık/Kapalı Ctrl + L"
msgid "Picons"
msgstr "Piconlar"
msgid "Picons downloader"
msgstr "Piconları Güncelle/indir"
msgid "Satellites downloader"
msgstr "Satellites Güncelle/indir"
msgid "Remove"
msgstr "Kaldır"
msgid "Remove all unavailable"
msgstr "Tüm mevcut olmayanları kaldır"
msgid "Satellites editor"
msgstr "Uydular editörü"
msgid "Save"
msgstr "Kaydet"
msgid "Search"
msgstr "Arama"
msgid "Services"
msgstr "Hizmetler"
msgid "Services filter"
msgstr "Hizmet filtresi"
msgid "Settings"
msgstr "Ayarlar"
msgid "Up"
msgstr "Yukarı"
msgid "Down"
msgstr "Aşağı"
msgid "Active profile:"
msgstr "Etkin profil:"
msgid "All"
msgstr "Tümü"
msgid "Are you sure?"
msgstr "Emin misin?"
msgid "Current data path:"
msgstr "Mevcut veri yolu:"
msgid "Data:"
msgstr "Veri:"
msgid "Enigma2 channel and satellites list editor for GNU/Linux"
msgstr "GNU/Linux için Enigma2 kanalı ve uydu listesi editörü"
msgid "Host:"
msgstr "Ana bilgisayar:"
msgid "Loading data..."
msgstr "Veriler yükleniyor ..."
msgid "Receive"
msgstr "Cihazdan Al"
msgid "Receive files from receiver"
msgstr "Alıcıdan dosya al"
msgid "Receiver IP:"
msgstr "Alıcı IP'si:"
msgid "Remove unused bouquets"
msgstr "Kullanılmayan buketleri kaldır"
msgid "Reset profile"
msgstr "Profili sıfırla"
msgid "Satellites"
msgstr "Uydular"
msgid "Satellites.xml file:"
msgstr "Satellites.xml dosyası:"
msgid "Selected"
msgstr "Seçildi"
msgid "Send"
msgstr "Cihaza Gönder"
msgid "Send files to receiver"
msgstr "Alıcıya dosya gönder"
msgid "Services and Bouquets files:"
msgstr "Hizmetler ve Buketler dosyaları:"
msgid "User bouquet files:"
msgstr "Kullanıcı buketi dosyaları:"
msgid "Extra:"
msgstr "Ekstra:"
# Filter bar
msgid "Only free"
msgstr "Sadece ücretsiz"
msgid "All positions"
msgstr "Tüm pozisyonlar"
msgid "All types"
msgstr "Tüm türler"
# Streams player
msgid "Play"
msgstr "Oynat"
msgid "Stop playback"
msgstr "Oynatmayı durdur"
msgid "Previous stream in the list"
msgstr "Listedeki önceki akış"
msgid "Next stream in the list"
msgstr "Listedeki sonraki akış"
msgid "Toggle in fullscreen"
msgstr "Tam ekranda geçiş yap"
msgid "Close"
msgstr "Kapat"
# Picons dialog
msgid "Load providers"
msgstr "Yük sağlayıcılar"
msgid "Providers"
msgstr "Yayıncılar"
msgid "Receive picons"
msgstr "Piconları al"
msgid "Picons name format:"
msgstr "Piconların adı biçimi:"
msgid "Resize:"
msgstr "Yeniden boyutlandır:"
msgid "Current picons path:"
msgstr "Geçerli picon yolları:"
msgid "Receiver picons path:"
msgstr "Alıcı picon yolu:"
msgid "Picons download tool"
msgstr "Picon indirme aracı"
msgid "Transfer to receiver"
msgstr "Alıcıya aktar"
msgid "Downloader"
msgstr "İndirici"
msgid "Converter"
msgstr "Dönüştürücü"
msgid "Convert"
msgstr "Dönüştür"
msgid "Path to save:"
msgstr "Kaydetme yolu:"
msgid "Path to Enigma2 picons:"
msgstr "Enigma2 piconların yolu:"
msgid "Specify the correct position value for the provider!"
msgstr "Sağlayıcı için doğru pozisyon değerini belirtin!"
msgid "Converter between name formats"
msgstr "İsim formatları arasında dönüştürücü"
msgid "Receive picons for providers"
msgstr "Sağlayıcılar için picon alma"
msgid "Load satellite providers."
msgstr "Uydu sağlayıcılarını yükle."
msgid ""
"To automatically set the identifiers for picons,\n"
"first load the required services list into the main application window."
msgstr ""
"Picon tanımlayıcılarını otomatik olarak ayarlamak için \n"
"önce gerekli uygulama listesini ana uygulama penceresine yükleyin."
# Satellites editor
msgid "Satellites edit tool"
msgstr "Uydular düzenleme aracı"
msgid "Add"
msgstr "Ekle"
msgid "Satellite"
msgstr "Uydu Ekle"
msgid "Transponder"
msgstr "Transponder Ekle"
msgid "Satellite properties:"
msgstr "Uydu özellikleri:"
msgid "Transponder properties:"
msgstr "Transponder özellikleri:"
msgid "Name"
msgstr "Ad"
msgid "Position"
msgstr "Konum"
# Satellites update dialog
msgid "Satellites update"
msgstr "Uydular güncelleme"
msgid "Remove selection"
msgstr "Seçimi kaldır"
# Service details dialog
msgid "Service data:"
msgstr "Servis verileri:"
msgid "Transponder data:"
msgstr "Transponder verileri:"
msgid "Service data"
msgstr "Servis verileri"
msgid "Transponder details"
msgstr "Transponder detayları"
msgid ""
"Changes will be applied to all services of this transponder!\n"
"Continue?"
msgstr ""
"Değişiklikler bu transponderin tüm servislerine uygulanacak!\n"
"Devam edilsinmi?"
msgid "Reference"
msgstr "Referans"
msgid "Namespace"
msgstr "Ad alanı"
msgid "Flags:"
msgstr "Bayraklar:"
msgid "Delays (ms):"
msgstr "Gecikmeler (ms):"
msgid "Bitstream"
msgstr "Bit akımı"
msgid "Description"
msgstr "Açıklama"
msgid "Source:"
msgstr "Kaynak:"
msgid "Cancel"
msgstr "İptal"
msgid "Update"
msgstr "Güncelle"
msgid "Filter"
msgstr "Filtre"
msgid "Find"
msgstr "Bul"
# IPTV dialog
msgid "Stream data"
msgstr "Veri akışı"
# IPTV list configuration dialog
msgid "Starting values"
msgstr "Başlangıç değerleri"
msgid "Reset to default"
msgstr "Varsayılana sıfırla"
msgid "IPTV streams list configuration"
msgstr "IPTV akış listesi yapılandırması"
# Settings dialog
msgid "Preferences"
msgstr "Tercihler"
msgid "Profile:"
msgstr "Profil:"
msgid "Timeout between commands in seconds"
msgstr "Komutlar arasında saniye cinsinden zaman aşımı"
msgid "Timeout:"
msgstr "Zaman aşımı:"
msgid "Login:"
msgstr "Giriş:"
msgid "Options"
msgstr "Seçenekler"
msgid "Password:"
msgstr "Parola:"
msgid "Picons:"
msgstr "Piconlar:"
msgid "Port:"
msgstr "Port:"
msgid "Data path:"
msgstr "Veri yolu:"
msgid "Picons path:"
msgstr "Picon yolu:"
msgid "Network settings:"
msgstr "Ağ ayarları:"
msgid "STB file paths:"
msgstr "STB dosya yolları:"
msgid "Local file paths:"
msgstr "Yerel dosya yolları:"
# Dialogs messages
msgid "Error. No bouquet is selected!"
msgstr "Hata. Buket seçilmedi!"
msgid "This item is not allowed to be removed!"
msgstr "Bu öğenin kaldırılmasına izin verilmiyor!"
msgid "This item is not allowed to edit!"
msgstr "Bu öğenin düzenlenmesine izin verilmiyor!"
msgid "Not allowed in this context!"
msgstr "Bu bağlamda izin verilmiyor!"
msgid "Please, download files from receiver or setup your path for read data!"
msgstr "Lütfen, alıcıdan dosya indirin veya veri okumak için yolunuzu ayarlayın!"
msgid "Reading data error!"
msgstr "Veri hatası okunuyor!"
msgid "No m3u file is selected!"
msgstr "Hiçbir m3u dosyası seçilmedi!"
msgid "Not implemented yet!"
msgstr "Henüz uygulanmadı!"
msgid "The text of marker is empty, please try again!"
msgstr "İşaretçi metni boş, lütfen tekrar deneyin!"
msgid "Please, select only one item!"
msgstr "Lütfen sadece bir ürün seçiniz!"
msgid "No png file is selected!"
msgstr "Hiçbir png dosyası seçilmedi!"
msgid "No reference is present!"
msgstr "Referans yok!"
msgid "No selected item!"
msgstr "Seçili öğe yok!"
msgid "The task is already running!"
msgstr "Görev zaten çalışıyor!"
msgid "Done!"
msgstr "Bitti!"
msgid "Please, wait..."
msgstr "Lütfen bekleyin ..."
msgid "Resizing..."
msgstr "Yeniden boyutlandırılıyor ..."
msgid "Select paths!"
msgstr "Yolları seç!"
msgid "No satellite is selected!"
msgstr "Uydu seçilmedi!"
msgid "Please, select only one satellite!"
msgstr "Lütfen sadece bir uydu seçin!"
msgid "Please check your parameters and try again."
msgstr "Lütfen parametrelerinizi kontrol edip tekrar deneyin."
msgid "No satellites.xml file is selected!"
msgstr "Hiçbir satellites.xml dosyası seçilmedi!"
msgid "Error. Verify the data!"
msgstr "Hata. Verileri doğrulayın!"
msgid "Operation not allowed in this context!"
msgstr "Bu bağlamda işleme izin verilmiyor!"
msgid "No VLC is found. Check that it is installed!"
msgstr "VLC bulunamadı. Yüklü olduğundan emin olun!"
# Search unavailable streams dialog
msgid "Please wait, streams testing in progress..."
msgstr "Lütfen bekleyin, akış testi devam ediyor ..."
msgid "Found"
msgstr "Bulundu"
msgid "unavailable streams."
msgstr "mevcut olmayan akışlar."
msgid "No changes required!"
msgstr "Hiçbir değişiklik gerekli!"
msgid "This list does not contains IPTV streams!"
msgstr "Bu liste IPTV akışı içermiyor!"
msgid "New empty configuration"
msgstr "Yeni boş yapılandırma"
msgid "No data to save!"
msgstr "Kaydedilecek veri yok!"
msgid "Network"
msgstr "Ağ"
msgid "Paths"
msgstr "Yollar"
msgid "Program"
msgstr "Program"
msgid "Backup:"
msgstr "Yedekleme:"
msgid "Backup"
msgstr "Yedekleme"
msgid "Backups"
msgstr "Yedeklemeler"
msgid "Backup path:"
msgstr "Yedekleme yolu:"
msgid "Restore bouquets"
msgstr "Buketleri geri yükle"
msgid "Restore all"
msgstr "Tümünü geri yükle"
msgid "Before saving"
msgstr "Kaydetmeden önce"
msgid "Before downloading from the receiver"
msgstr "Alıcıdan indirmeden önce"
msgid "Set background color for the services"
msgstr "Hizmetler için arka plan rengini ayarla"
msgid "Marked as new:"
msgstr "Yeni olarak işaretlendi:"
msgid "With an extra name in the bouquet:"
msgstr "Bukette fazladan bir isim ile:"
msgid "Select"
msgstr "Seç"
msgid "About"
msgstr "Hakkında"
msgid "Exit"
msgstr "Çıkış"
msgid "Tools"
msgstr "Araçlar"
# Import
msgid "Import"
msgstr "İçe aktar"
msgid "Bouquet"
msgstr "Buket"
msgid "Bouquets and services"
msgstr "Buketler ve hizmetler"
msgid "The main list does not contain services for this bouquet!"
msgstr "Ana liste bu buket için hizmet içermiyor!"
msgid "No bouquet file is selected!"
msgstr "Hiçbir buket dosyası seçilmedi!"
msgid "Remove all unused"
msgstr "Kullanılmayanların tümünü kaldır"
msgid "Test"
msgstr "Test"
msgid "Test connection"
msgstr "Bağlantıyı test et"
msgid "Double click on the service in the bouquet list:"
msgstr "Buket listesindeki hizmete çift tıklayın:"
msgid "Zap"
msgstr "Zap"
msgid "Play stream"
msgstr "Akışı oynat"
msgid "Disabled"
msgstr "Devre dışı"
msgid "Enable lamedb ver. 5 support"
msgstr "Sürüm 5 desteğini etkinleştir"
msgid "Enable HTTP API"
msgstr "HTTP API'sini etkinleştir"
msgid "Switch(zap) the channel(Ctrl + Z)"
msgstr "Kanalı değiştir (zap) (Ctrl + Z)"
msgid "Switch the channel and watch in the program(Ctrl + W)"
msgstr "Kanal değiştirme ve programda izleme (Ctrl + W)"
msgid "Play IPTV or other stream in the program(Ctrl + P)"
msgstr "Programdaki IPTV veya diğer akışları oynat (Ctrl + P)"
msgid "Export to m3u"
msgstr "Dışa aktar M3U"
msgid "EPG configuration"
msgstr "EPG yapılandırması"
msgid "Apply"
msgstr "Uygula"
msgid "EPG source"
msgstr "EPG kaynağı"
msgid "Service names source:"
msgstr "Hizmet adları kaynağı:"
msgid "Main service list"
msgstr "Ana hizmet listesi"
msgid "XML file"
msgstr "XML dosyası"
msgid "Use web source"
msgstr "Web kaynağını kullan"
msgid "Url to *.xml.gz file:"
msgstr "URL. * .xml.gz dosyasına:"
msgid "Enable filtering"
msgstr "Filtrelemeyi etkinleştir"
msgid "Filter by presence in the epg.dat file."
msgstr "Epg.dat dosyasındaki varlığına göre filtreleyin."
msgid "Paths to the epg.dat file:"
msgstr "Epg.dat dosyasının yolları:"
msgid "Local path:"
msgstr "Yerel yol:"
msgid "STB path:"
msgstr "STB yolu:"
msgid "Update on start"
msgstr "Başlangıçta güncelleme"
msgid "Auto configuration by service names."
msgstr "Hizmet adlarına göre otomatik yapılandırma."
msgid "Save list to xml."
msgstr "Listeyi xml'ye kaydet."
msgid "Download XML file error."
msgstr "XML dosyası indir hatası."
msgid "Unsupported file type:"
msgstr "Desteklenmeyen dosya türü:"
msgid "Unpacking data error."
msgstr "Veri paketi açılırken hata oluştu."
msgid "XML parsing error:"
msgstr "XML ayrıştırma hatası:"
msgid "Count of successfully configured services:"
msgstr "Başarılı bir şekilde yapılandırılmış hizmetlerin sayısı:"
msgid "Current epg.dat file does not contains references for the services of this bouquet!"
msgstr "Şu anki epg.dat dosyası bu buketin hizmetleri için referans içermiyor!"
msgid "Use HTTP"
msgstr "HTTP kullan"
msgid "Close playback"
msgstr "Oynatmayı kapat"
msgid "Import YouTube playlist"
msgstr "YouTube oynatma listesini içe aktar"
msgid ""
"Found a link to the YouTube resource!\n"
"Try to get a direct link to the video?"
msgstr ""
"YouTube kaynağına bir bağlantı bulundu!\n"
"Videoya doğrudan bağlantı almaya çalışılsın mı?"
msgid "Playlist import"
msgstr "Oynatma listesi içe aktarma"
msgid "Getting link error:"
msgstr "Bağlantı hatası alınıyor:"
msgid "Extra"
msgstr "Ekstra"
msgid "Apply profile settings"
msgstr "Profil ayarlarını uygula"
msgid "Settings type:"
msgstr "Ayarlar türü:"
msgid "Set default"
msgstr "Varsayılanı ayarla"
msgid "Language:"
msgstr "Dil:"
msgid "Load the last open configuration at program startup"
msgstr "Program açılışında son açık yapılandırmayı yükle"
msgid "Enable direct playback bar"
msgstr "Doğrudan oynatma çubuğunu etkinleştir"
msgid "Enables direct sending and playback of media links on the receiver"
msgstr "Alıcıdaki medya bağlantılarının doğrudan gönderilmesini ve oynatılmasını sağlar"
msgid "Watch the channel in the program"
msgstr "Programdaki kanalı izle"
msgid "Zap and Play"
msgstr "Zap ve Oyna"
msgid "Drag or paste the link here"
msgstr "Bağlantıyı buraya sürükleyin veya yapıştırın"
msgid "Remove added links in the playlist"
msgstr "Oynatma listesine eklenen bağlantıları kaldır"
msgid "A bouquet with that name exists!"
msgstr "Bu isimli bir buket var!"
msgid "Details"
msgstr "Detaylar"
msgid "Profile"
msgstr "Profil"
msgid "Reset"
msgstr "Yeniden Başlat"
msgid "File"
msgstr "Dosya"
msgid "Picons manager"
msgstr "Picon yöneticisi"
msgid "Explorer"
msgstr "Explorer"
msgid "Satellite url:"
msgstr "Uydu url:"
msgid "Cut"
msgstr "Kes"
msgid "Paste"
msgstr "Yapıştır"
msgid "To the top"
msgstr "Başlangıçta"
msgid "To the end"
msgstr "Sonunda"
msgid "View"
msgstr "Görünüm"
msgid "Lock"
msgstr "Kilit"
msgid "Parent lock"
msgstr "Ebeveyn kilidi"
msgid "Hide/Skip"
msgstr "Gizle/Atla"
msgid "IPTV tools"
msgstr "IPTV araçları"
msgid "Make profile folder as default for the additional data"
msgstr "Ek veriler için profil klasörünü varsayılan yap"
msgid "Default data path:"
msgstr "Varsayılan veri yolu:"
msgid "Streams record path:"
msgstr "Akış kayıt yolu:"
msgid "Record"
msgstr "Kayıt"
msgid "Record:"
msgstr "Kayıt:"
msgid "Record to disk:"
msgstr "Diske kaydet:"
msgid "Streaming"
msgstr "Yayın Akışı"
msgid "Activate transcoding"
msgstr "Kod dönüştürmeyi etkinleştir"
msgid "Presets:"
msgstr "Ön ayarlar:"
msgid "Video options:"
msgstr "Video seçenekleri:"
msgid "Audio options:"
msgstr "Ses seçenekleri:"
msgid "Bitrate (kb/s):"
msgstr "Bit hızı (kb/s):"
msgid "Codec:"
msgstr "Codec:"
msgid "Width (px):"
msgstr "Genişlik (px):"
msgid "Height (px):"
msgstr "Yükseklik (px):"
msgid "Channels:"
msgstr "Kanallar:"
msgid "Sample rate (Hz):"
msgstr "Örnekleme hızı (Hz):"
msgid "Play streams mode:"
msgstr "Akışları oynatma modu:"
msgid "Built-in player"
msgstr "Dahili oynatıcı"
msgid "VLC media player"
msgstr "VLC media player"
msgid "Only get m3u file"
msgstr "Sadece m3u dosyası al"
msgid "Save and restart the program to apply the settings."
msgstr "Ayarları uygulamak için programı kaydedin ve yeniden başlatın."
msgid "Some images may have problems displaying the favorites list!"
msgstr "Bazı resimler sık kullanılanlar listesini görüntülerken sorun yaşayabilir!"
msgid "Operates in standby mode or current active transponder!"
msgstr "Bekleme modunda veya geçerli aktif transponderde çalışır!"
msgid "No connection to the receiver!"
msgstr "Alıcıya bağlantı yok!"
msgid "Signal level"
msgstr "Sinyal seviyesi"
msgid "Receiver info"
msgstr "Alıcı bilgisi"
msgid "A profile with that name exists!"
msgstr "Bu ada sahip bir profil var!"
msgid "Show short info as hints in the main services list"
msgstr "Kısa bilgileri ana hizmetler listesinde ipuçları olarak göster"
msgid "Show detailed info as hints in the bouquet list"
msgstr "Ayrıntılı bilgileri buket listesinde ipuçları olarak göster"
msgid "Enable alternate bouquet file naming"
msgstr "Alternatif buket dosyası adlandırmasını etkinleştir"
msgid "Allows you to name bouquet files using their names."
msgstr "Buket dosyalarını isimlerini kullanarak isimlendirmenizi sağlar."
msgid "Appearance"
msgstr "Görünüm"
msgid "Enable Themes support"
msgstr "Temalar desteğini etkinleştir"
msgid "Gtk3 Theme:"
msgstr "Gtk3 Teması:"
msgid "Icon Theme:"
msgstr "Simge Teması:"
msgid "Gtk3 Themes and Icons:"
msgstr "Gtk3 Tema ve Simgeler:"
msgid "Deleting data..."
msgstr "Veriler siliniyor..."
msgid "Download from the receiver"
msgstr "Alıcıdan indir"
msgid "Remove all picons from the receiver"
msgstr "Alıcıdaki tüm piconları kaldırın"
msgid "Service reference"
msgstr "Servis referansı"

View File

@@ -1,29 +1,19 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
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)
try:
from Cocoa import NSBundle
except ImportError as e:
print(e)
else:
ns_bundle = NSBundle.mainBundle()
if ns_bundle:
ns_bundle = ns_bundle.localizedInfoDictionary() or ns_bundle.infoDictionary()
if ns_bundle:
ns_bundle["CFBundleName"] = "DemonEditor"
if __name__ == "__main__": if __name__ == "__main__":
from multiprocessing import set_start_method
from app.ui.main_app_window import start_app from app.ui.main_app_window import start_app
update_icon() set_start_method("fork") # For compatibility [Python > 3.7]
start_app() start_app()