mirror of
https://github.com/DYefremov/DemonEditor.git
synced 2026-05-08 11:07:30 +02:00
Compare commits
1193 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e7a116db7 | ||
|
|
285014480f | ||
|
|
279c255ad0 | ||
|
|
90a3053192 | ||
|
|
19c6a5bef9 | ||
|
|
945ee13058 | ||
|
|
c65b6c540c | ||
|
|
62091dfa96 | ||
|
|
6ca06fd2cd | ||
|
|
4cab05fc09 | ||
|
|
81e714ebab | ||
|
|
147430d4f3 | ||
|
|
f8f209d288 | ||
|
|
bd0e08e90b | ||
|
|
57020423d7 | ||
|
|
1fd3e45dd3 | ||
|
|
ac1725b3ef | ||
|
|
39a592fd4d | ||
|
|
e40e0f2458 | ||
|
|
d5889cd96c | ||
|
|
77a8bfe2c6 | ||
|
|
470d2d843b | ||
|
|
a908845b4e | ||
|
|
8fee5033a4 | ||
|
|
a9b1f8b26c | ||
|
|
8fa306a9d1 | ||
|
|
383ea2b9b3 | ||
|
|
a40ba2ff68 | ||
|
|
421d9b1c96 | ||
|
|
7357939241 | ||
|
|
08ef7bc451 | ||
|
|
8b255ec824 | ||
|
|
c81084015d | ||
|
|
66c8e9e916 | ||
|
|
4b93ae6950 | ||
|
|
e2cafef113 | ||
|
|
c35be2aa24 | ||
|
|
852404bae6 | ||
|
|
f6d2765137 | ||
|
|
182c7a9cc7 | ||
|
|
271ea97040 | ||
|
|
364fb68743 | ||
|
|
85a9d5e67e | ||
|
|
50c0a0cf37 | ||
|
|
25fd6df967 | ||
|
|
6106e86d18 | ||
|
|
1c5f7fab11 | ||
|
|
024f90d23f | ||
|
|
ee3041174c | ||
|
|
6f28aae40c | ||
|
|
1b2de795a2 | ||
|
|
8d485a9993 | ||
|
|
1721567731 | ||
|
|
fd4325961c | ||
|
|
294d32c705 | ||
|
|
aa961030ce | ||
|
|
bc4c6746c9 | ||
|
|
61282b0cc8 | ||
|
|
07e855f99d | ||
|
|
0d0f19122b | ||
|
|
4789688efd | ||
|
|
b13c2c3be0 | ||
|
|
d72189abc4 | ||
|
|
35d194100b | ||
|
|
aa0b97b9ae | ||
|
|
5f54452ee2 | ||
|
|
580e8ca82c | ||
|
|
4ba2fb1a04 | ||
|
|
b4612c26cb | ||
|
|
9a8b1e871d | ||
|
|
3a53a95f86 | ||
|
|
24a94cfe9a | ||
|
|
0ca08e3a1d | ||
|
|
db4e9d2696 | ||
|
|
3be9b374c8 | ||
|
|
0b9fd37ee9 | ||
|
|
be90d9694a | ||
|
|
a5144e8e34 | ||
|
|
04e0a25956 | ||
|
|
dc24c899af | ||
|
|
a55495fd7c | ||
|
|
ed3aea42f5 | ||
|
|
a6904360f9 | ||
|
|
1e42d693cc | ||
|
|
5e64605be6 | ||
|
|
63c55ea2ed | ||
|
|
c7f85b027d | ||
|
|
b24910a9a5 | ||
|
|
1bdb4f123f | ||
|
|
8f8d7633b8 | ||
|
|
c997724300 | ||
|
|
727a3fa8a2 | ||
|
|
4009f5c2a2 | ||
|
|
ca5d648032 | ||
|
|
938fe297c5 | ||
|
|
db29b78fd7 | ||
|
|
e599ea04c7 | ||
|
|
50a2f66fc3 | ||
|
|
34056b1006 | ||
|
|
6188fecda9 | ||
|
|
eb41b9629e | ||
|
|
7694754919 | ||
|
|
11f240a81f | ||
|
|
1b75034317 | ||
|
|
5b6900cae7 | ||
|
|
eeeca881e8 | ||
|
|
ed16fb0195 | ||
|
|
833b386356 | ||
|
|
83424124d3 | ||
|
|
d77aa68a39 | ||
|
|
94266e13b8 | ||
|
|
8db6fb1b0b | ||
|
|
e07c5d4bf7 | ||
|
|
6f3090a7e1 | ||
|
|
b73c1d1118 | ||
|
|
df127c05f3 | ||
|
|
9b579af528 | ||
|
|
8290e723c9 | ||
|
|
b12c29be84 | ||
|
|
1780fbadbd | ||
|
|
4fe4e92442 | ||
|
|
bfad5cf9ac | ||
|
|
5d285f61c0 | ||
|
|
f61d9a1f61 | ||
|
|
8d115677d1 | ||
|
|
f7c6cd6908 | ||
|
|
2d2a90542c | ||
|
|
535c9c9102 | ||
|
|
866e18762d | ||
|
|
aef5027d23 | ||
|
|
3a142eca4a | ||
|
|
606bad7716 | ||
|
|
fa07f8bf85 | ||
|
|
92280162c6 | ||
|
|
5d285e88d8 | ||
|
|
0355714e92 | ||
|
|
b06e877a0c | ||
|
|
9b479b051d | ||
|
|
b953ee8762 | ||
|
|
0e7d6bec69 | ||
|
|
cb9824d404 | ||
|
|
c87adb256f | ||
|
|
899d05a186 | ||
|
|
bd4f86e91e | ||
|
|
42fb365b45 | ||
|
|
67d6ea861e | ||
|
|
d887a61636 | ||
|
|
9d9efb7577 | ||
|
|
b6d331a311 | ||
|
|
1060e169a1 | ||
|
|
562c1a5955 | ||
|
|
3c4dec323f | ||
|
|
3bafe08030 | ||
|
|
722f8df813 | ||
|
|
8f6984dbaf | ||
|
|
bf6e9617ec | ||
|
|
ec27c32d35 | ||
|
|
bfa3b1aa66 | ||
|
|
a1e32abd07 | ||
|
|
f93370293b | ||
|
|
79a2a034eb | ||
|
|
791c073d1a | ||
|
|
de5ec53a18 | ||
|
|
156ac7d364 | ||
|
|
5680423f14 | ||
|
|
fa256c5a0b | ||
|
|
d9a5d9a972 | ||
|
|
4e59bdf38e | ||
|
|
7edb03836a | ||
|
|
0ea0c889d4 | ||
|
|
e494a34bc4 | ||
|
|
4a57234293 | ||
|
|
aca4875ee6 | ||
|
|
a289b9fb53 | ||
|
|
a735b3db13 | ||
|
|
e57e8f9dd7 | ||
|
|
300e0a38c3 | ||
|
|
5eb6f8d63f | ||
|
|
3ec4817665 | ||
|
|
55bb7b2f45 | ||
|
|
a3cf5edd5e | ||
|
|
4f8f6cfcc9 | ||
|
|
ef97cd365c | ||
|
|
4b27e81f5e | ||
|
|
408753f124 | ||
|
|
c3bd3cd2bb | ||
|
|
a15666204c | ||
|
|
1e5c47c3cc | ||
|
|
bc94bb3505 | ||
|
|
6cfb72b1d7 | ||
|
|
3c5144134c | ||
|
|
4fd2939d2e | ||
|
|
68b5fcfd39 | ||
|
|
9fcc1e5f90 | ||
|
|
92b028dc16 | ||
|
|
665de22549 | ||
|
|
5036ecee37 | ||
|
|
e755e9eb6e | ||
|
|
305323726f | ||
|
|
7b3ae01253 | ||
|
|
08fb55ea54 | ||
|
|
f1ee406818 | ||
|
|
8aa9d0797f | ||
|
|
8d61663d3e | ||
|
|
3a9ca26619 | ||
|
|
228466d523 | ||
|
|
e9b3b3f374 | ||
|
|
05b1619d1e | ||
|
|
9a2a2b49f6 | ||
|
|
a034d0476d | ||
|
|
0cf2f070b0 | ||
|
|
dd70dffa1c | ||
|
|
5902b207e1 | ||
|
|
b23319ba52 | ||
|
|
cb8ec264cc | ||
|
|
9fc07308ab | ||
|
|
5f0f1e4e64 | ||
|
|
3fe78e0292 | ||
|
|
0ae9123d98 | ||
|
|
7da3c0fd94 | ||
|
|
b584126bff | ||
|
|
fb173449c6 | ||
|
|
64de141807 | ||
|
|
cab20f744f | ||
|
|
9d44b8002c | ||
|
|
d4389e8b0a | ||
|
|
57696a2460 | ||
|
|
8383214e15 | ||
|
|
56f1c75a41 | ||
|
|
f7be210b85 | ||
|
|
7e0b694a4c | ||
|
|
9a24cae626 | ||
|
|
6e459f80bd | ||
|
|
c55645e5db | ||
|
|
d6796bc5a5 | ||
|
|
4ac04e5401 | ||
|
|
3c72f0cc3c | ||
|
|
4b940b7135 | ||
|
|
0aa9eaa401 | ||
|
|
de8445d55a | ||
|
|
8153c5e6d6 | ||
|
|
51fd013e0f | ||
|
|
872f3f0f81 | ||
|
|
95aa8aaed6 | ||
|
|
b18207b376 | ||
|
|
8f840ed3f5 | ||
|
|
6841090cc0 | ||
|
|
4e700ca1e8 | ||
|
|
3312576416 | ||
|
|
b047153591 | ||
|
|
b15cae8d79 | ||
|
|
c5e8e19941 | ||
|
|
50d8e3365e | ||
|
|
42fd490a33 | ||
|
|
ef84a45ebb | ||
|
|
d924643436 | ||
|
|
a7bde2e25f | ||
|
|
f64d9d31ab | ||
|
|
ec4ebfa24a | ||
|
|
03ba1af356 | ||
|
|
d231edd0bb | ||
|
|
97fef050b7 | ||
|
|
61fe46da8a | ||
|
|
dc7dc9a087 | ||
|
|
e27c34df4e | ||
|
|
a3b6d37fd3 | ||
|
|
d01af855f9 | ||
|
|
5353af94ac | ||
|
|
2656d0b3a9 | ||
|
|
a99c1e00b9 | ||
|
|
73a1f2ccef | ||
|
|
a4892efe61 | ||
|
|
5ded562e12 | ||
|
|
d2508218f7 | ||
|
|
1e10df6f03 | ||
|
|
06887d9440 | ||
|
|
3e9776e8e0 | ||
|
|
6b2c4dc79f | ||
|
|
726abc4965 | ||
|
|
6450cf1b17 | ||
|
|
2aaa196acf | ||
|
|
4d35364a30 | ||
|
|
f1c5a57abd | ||
|
|
dada57141f | ||
|
|
d45d1d42c2 | ||
|
|
9ee757aa19 | ||
|
|
17de98991d | ||
|
|
2862f22243 | ||
|
|
9c2ab5c3e6 | ||
|
|
4d6fbfce6c | ||
|
|
5c0d1d27f5 | ||
|
|
1c26887e8f | ||
|
|
2d67fbfe44 | ||
|
|
f502bce2aa | ||
|
|
8c442280ec | ||
|
|
3b79677a9b | ||
|
|
33529ca010 | ||
|
|
eae14a472f | ||
|
|
f85bbceceb | ||
|
|
f7cfc5a9a0 | ||
|
|
6b57c8f617 | ||
|
|
1006251490 | ||
|
|
e871492006 | ||
|
|
353fb9cc53 | ||
|
|
497964822e | ||
|
|
a067821d46 | ||
|
|
ab52964a20 | ||
|
|
344ac43996 | ||
|
|
0e07b8d492 | ||
|
|
7580a448c8 | ||
|
|
4886d2dfcf | ||
|
|
2dfc571a64 | ||
|
|
7922f368b5 | ||
|
|
d87c878d4d | ||
|
|
564adf1c61 | ||
|
|
9057dc1f81 | ||
|
|
108b3a72b9 | ||
|
|
0090cc6262 | ||
|
|
acc946eb30 | ||
|
|
b1dffe622d | ||
|
|
ab008aeb9c | ||
|
|
c4b6e009d5 | ||
|
|
7164c6debd | ||
|
|
fb06e4364f | ||
|
|
7ff8212fa6 | ||
|
|
9822129307 | ||
|
|
876aaeb4fe | ||
|
|
b8140c4df0 | ||
|
|
a364ff22bf | ||
|
|
bde2abf2e4 | ||
|
|
42644e37c6 | ||
|
|
9016143bb9 | ||
|
|
45cc506dd0 | ||
|
|
c6a841de76 | ||
|
|
d95ec0af17 | ||
|
|
77c159fc1a | ||
|
|
95508c3015 | ||
|
|
1f6e6b9812 | ||
|
|
20697a08f2 | ||
|
|
95c7213988 | ||
|
|
a988ea6e68 | ||
|
|
061e42a36e | ||
|
|
3a5473ca03 | ||
|
|
54a5be9c3d | ||
|
|
858c64d531 | ||
|
|
f05802841d | ||
|
|
e4af45b0e0 | ||
|
|
0cad69cb00 | ||
|
|
071fffb303 | ||
|
|
95d51d37e9 | ||
|
|
f1935e4a56 | ||
|
|
96aeb05dd1 | ||
|
|
af1c3c3ca6 | ||
|
|
2ba37f1506 | ||
|
|
8581de5a10 | ||
|
|
155f3b254a | ||
|
|
13e6c858d2 | ||
|
|
7fac48c44d | ||
|
|
9aa2ec191e | ||
|
|
13e12165eb | ||
|
|
ae37feebaa | ||
|
|
573bcba32f | ||
|
|
f4e5c56d39 | ||
|
|
4dc0dffc1f | ||
|
|
ceb329700d | ||
|
|
8cca2da48d | ||
|
|
e9bab4ebb7 | ||
|
|
3e7750e4ef | ||
|
|
184c1cbee5 | ||
|
|
c9e8625ea5 | ||
|
|
fd72615a63 | ||
|
|
f11a40e04d | ||
|
|
9522267dee | ||
|
|
f538d609b9 | ||
|
|
3edc699277 | ||
|
|
19562c7281 | ||
|
|
5a104c1778 | ||
|
|
09afc5b76a | ||
|
|
6129c5ac23 | ||
|
|
a552d13933 | ||
|
|
c4fa3b1eee | ||
|
|
795c708a61 | ||
|
|
92721c65e4 | ||
|
|
b8470f7dfa | ||
|
|
fd02ddedb7 | ||
|
|
f3beec141c | ||
|
|
8910b6d68b | ||
|
|
2a4c4576cc | ||
|
|
4e8fd693a6 | ||
|
|
7184642e68 | ||
|
|
10f4a461bd | ||
|
|
bec7f35b17 | ||
|
|
2a6dc3f167 | ||
|
|
e9e36c4a9c | ||
|
|
0c49096733 | ||
|
|
e2aa21060b | ||
|
|
b15691207b | ||
|
|
ab3ca134a7 | ||
|
|
7f839f3fa0 | ||
|
|
c3e8eac4b9 | ||
|
|
04f808843d | ||
|
|
eea4a68993 | ||
|
|
bfba5b5237 | ||
|
|
e203a38966 | ||
|
|
a3b5609138 | ||
|
|
b3c131b753 | ||
|
|
a8ea5ad974 | ||
|
|
831184af2e | ||
|
|
1f51766dea | ||
|
|
f8f1536213 | ||
|
|
0accfbd3d1 | ||
|
|
ee462b24f7 | ||
|
|
17ee189db8 | ||
|
|
8389293b4b | ||
|
|
99e0c79b6c | ||
|
|
02cdbc4e56 | ||
|
|
d57f0490d2 | ||
|
|
9680347180 | ||
|
|
7fbdc32f91 | ||
|
|
a69f54435d | ||
|
|
0d47433f80 | ||
|
|
d16a8e44f6 | ||
|
|
053a834d6d | ||
|
|
62c1ef852c | ||
|
|
b5234c55e8 | ||
|
|
472ebba8e9 | ||
|
|
d427cf66b0 | ||
|
|
9fe3d8077f | ||
|
|
2f12ef7bdd | ||
|
|
b6ad661e39 | ||
|
|
6355e0d75a | ||
|
|
29016056c2 | ||
|
|
23fe71e5cc | ||
|
|
80e4edd084 | ||
|
|
3d0bb6ad3c | ||
|
|
a8937d0698 | ||
|
|
b97997b7a0 | ||
|
|
0003c6c5d5 | ||
|
|
13b9d64bd0 | ||
|
|
c68511b223 | ||
|
|
c6ef61222e | ||
|
|
8309353784 | ||
|
|
3f65975ac2 | ||
|
|
4f6443e6e3 | ||
|
|
d517b3b9d6 | ||
|
|
faf228fa6f | ||
|
|
eaf5e39458 | ||
|
|
8a2539d57b | ||
|
|
8c827b126f | ||
|
|
db1bfb0fb9 | ||
|
|
8ba7751b97 | ||
|
|
65b58c9d08 | ||
|
|
72aed5ff6e | ||
|
|
ad07469c35 | ||
|
|
6f0de03b22 | ||
|
|
1cec96d2b5 | ||
|
|
a9935dd0a7 | ||
|
|
65dfd6c1c4 | ||
|
|
65c24a324a | ||
|
|
78b0cc1517 | ||
|
|
d66d4e6402 | ||
|
|
7b11822664 | ||
|
|
8c5f27cc8a | ||
|
|
b51c8a9aed | ||
|
|
2c4302f57d | ||
|
|
bd0ac077f9 | ||
|
|
105b907392 | ||
|
|
b4fb684af4 | ||
|
|
20a1bac22e | ||
|
|
9a9229f67c | ||
|
|
a941c96c61 | ||
|
|
05a6e36589 | ||
|
|
50a5cf6fc3 | ||
|
|
6ef844157e | ||
|
|
72583ba879 | ||
|
|
e7d96b0cbb | ||
|
|
a7458494a3 | ||
|
|
747c1a9722 | ||
|
|
dac2fe17a6 | ||
|
|
fb8cf6c882 | ||
|
|
bcf69231a8 | ||
|
|
5352d87b82 | ||
|
|
c978f5abab | ||
|
|
636442bcd3 | ||
|
|
9e74f0f525 | ||
|
|
b7028ae27d | ||
|
|
4f3b05ede5 | ||
|
|
e6e6d3510d | ||
|
|
1d1b4acdca | ||
|
|
0e50f1952d | ||
|
|
8686e15446 | ||
|
|
5fee00a150 | ||
|
|
0ec9873940 | ||
|
|
0600737319 | ||
|
|
cf51a5bfd8 | ||
|
|
533d8fae25 | ||
|
|
16167b1b13 | ||
|
|
38f6c06292 | ||
|
|
4170864a74 | ||
|
|
3171540bf2 | ||
|
|
0ed4f6d7f9 | ||
|
|
637becaa59 | ||
|
|
fd5a0d23a8 | ||
|
|
22626ea03d | ||
|
|
2e34568f74 | ||
|
|
f42b2f3c75 | ||
|
|
f88b36cee7 | ||
|
|
4421f767e1 | ||
|
|
898b32ca5f | ||
|
|
7c2840c570 | ||
|
|
1c9c58d48a | ||
|
|
45a1c79808 | ||
|
|
19fbc753c5 | ||
|
|
c8c424750b | ||
|
|
3d769a5e18 | ||
|
|
1b7d2f15b0 | ||
|
|
58cf299097 | ||
|
|
af8fe227e0 | ||
|
|
3160ec2455 | ||
|
|
04fd8b7182 | ||
|
|
ee90d11557 | ||
|
|
b312816804 | ||
|
|
39647cb811 | ||
|
|
e64cd66977 | ||
|
|
754c586fb1 | ||
|
|
d5a2b22819 | ||
|
|
b20bcce5fa | ||
|
|
aba82c7120 | ||
|
|
e0beeef2a3 | ||
|
|
c8a9d3f4a0 | ||
|
|
9f7c713712 | ||
|
|
f89196041b | ||
|
|
efa2d94239 | ||
|
|
f53f483dce | ||
|
|
ebcf0a90b5 | ||
|
|
2311b046e7 | ||
|
|
9544b6028f | ||
|
|
e04144b10f | ||
|
|
93f68a7fe2 | ||
|
|
947ea21ed1 | ||
|
|
d20d40c19b | ||
|
|
bb349a3fe9 | ||
|
|
bb3ecf975b | ||
|
|
bbc693e5e6 | ||
|
|
cb29cf0155 | ||
|
|
0c8592fb0d | ||
|
|
66f067340d | ||
|
|
b0ec8e5483 | ||
|
|
aa4b31edfc | ||
|
|
3d627b57a4 | ||
|
|
6b1bec500c | ||
|
|
7444db7e21 | ||
|
|
8be92a9c7e | ||
|
|
7554f40c6a | ||
|
|
17ab321e44 | ||
|
|
ac345d4ef3 | ||
|
|
e2a56a316d | ||
|
|
9b79bf2b81 | ||
|
|
ee2a9bda90 | ||
|
|
da0c5fa8a6 | ||
|
|
e202ec6abe | ||
|
|
e87be79f42 | ||
|
|
6372ac474c | ||
|
|
c6b0f70c8e | ||
|
|
6a921ad394 | ||
|
|
4c8743517f | ||
|
|
74ec0fe956 | ||
|
|
041f717a01 | ||
|
|
67dbdb19d7 | ||
|
|
de49179dd2 | ||
|
|
2723d255fe | ||
|
|
4515b2538b | ||
|
|
88e3a22cf0 | ||
|
|
7ac63b81c0 | ||
|
|
234611b686 | ||
|
|
fdb2691430 | ||
|
|
d81700c30c | ||
|
|
e91c4c33a5 | ||
|
|
40bf54e94f | ||
|
|
4a50c36ab4 | ||
|
|
ea305dadf1 | ||
|
|
136fd118cb | ||
|
|
7df7e0b630 | ||
|
|
0b4e923037 | ||
|
|
c2d2361de9 | ||
|
|
cc15594338 | ||
|
|
0f64234312 | ||
|
|
a1098750da | ||
|
|
47c80b2b29 | ||
|
|
dfbf019c64 | ||
|
|
9082d5c96e | ||
|
|
2e3ec1c99d | ||
|
|
024d48b464 | ||
|
|
0571a29b66 | ||
|
|
1d62e2660b | ||
|
|
bcbe2b3f46 | ||
|
|
d0638f7158 | ||
|
|
614c87cbf3 | ||
|
|
5aec42548e | ||
|
|
3859c84c0e | ||
|
|
77bb4a7fef | ||
|
|
a8b4047239 | ||
|
|
a4800ace14 | ||
|
|
f26f806147 | ||
|
|
98d3c04a08 | ||
|
|
24311827bf | ||
|
|
17a3ec4fef | ||
|
|
30927b2546 | ||
|
|
84a4cef5b5 | ||
|
|
5fe2559789 | ||
|
|
1e35a69539 | ||
|
|
16c58907f4 | ||
|
|
783e78dc14 | ||
|
|
c7e1f05955 | ||
|
|
e11f68e3bd | ||
|
|
27f60bcea2 | ||
|
|
0a16265aa2 | ||
|
|
49d8c0ef92 | ||
|
|
998a6eb118 | ||
|
|
f7d75c2404 | ||
|
|
9396ec197d | ||
|
|
9ad9de4821 | ||
|
|
408a4cfa32 | ||
|
|
fe3fb1fefe | ||
|
|
95b5c7aa74 | ||
|
|
d2e80c6d44 | ||
|
|
57f157ef9b | ||
|
|
f62f104c8d | ||
|
|
8d6b1303dc | ||
|
|
de770169fa | ||
|
|
7b2e467111 | ||
|
|
1c1dff2497 | ||
|
|
309f960e1e | ||
|
|
dadb73280c | ||
|
|
81260211a4 | ||
|
|
d4d1dd397d | ||
|
|
fe3e1ef30a | ||
|
|
1f46eae6be | ||
|
|
a398b0c6e9 | ||
|
|
272b6af3ad | ||
|
|
d976e02cf6 | ||
|
|
fb6951896f | ||
|
|
9c62a49968 | ||
|
|
99bb508912 | ||
|
|
e382a51f81 | ||
|
|
fdd30b2ac9 | ||
|
|
f2b99f9eea | ||
|
|
dea5723bb7 | ||
|
|
236a7a15d0 | ||
|
|
cfe281116a | ||
|
|
984e8ca088 | ||
|
|
62eae7f029 | ||
|
|
204962b531 | ||
|
|
767c06a0ff | ||
|
|
d4ec28e9cd | ||
|
|
8fee65cabb | ||
|
|
95069bbf24 | ||
|
|
60f106bc2a | ||
|
|
4becdf1d6e | ||
|
|
f2f027c6f4 | ||
|
|
edeab12b50 | ||
|
|
896aa7f66e | ||
|
|
6b6220a3ac | ||
|
|
3fa46c6c6c | ||
|
|
a0b322b188 | ||
|
|
3550f58603 | ||
|
|
729c85be77 | ||
|
|
f8aee1b807 | ||
|
|
75d93f6a19 | ||
|
|
4581cc7d4f | ||
|
|
2f8ea069e1 | ||
|
|
8afcec6b7e | ||
|
|
b5a9321c5c | ||
|
|
7ee781c39b | ||
|
|
48f3c1a4d6 | ||
|
|
717bac6446 | ||
|
|
fe1323f8cf | ||
|
|
0f30d74edc | ||
|
|
0686c91a5d | ||
|
|
a84090cda7 | ||
|
|
291b3aa289 | ||
|
|
dd92ffc9b1 | ||
|
|
97a8f793c3 | ||
|
|
3ad2e3d6b6 | ||
|
|
7d6763ffb5 | ||
|
|
6582be7a0d | ||
|
|
1e45621bd8 | ||
|
|
61bcb85bbc | ||
|
|
9eee9ac424 | ||
|
|
3f720afedc | ||
|
|
cd19c5fd9c | ||
|
|
61ca2f3e8b | ||
|
|
75fc7adc88 | ||
|
|
3678a9d29d | ||
|
|
a5927dd2b6 | ||
|
|
34e0ed4748 | ||
|
|
d7a214b445 | ||
|
|
e194827af7 | ||
|
|
e9e53da5cc | ||
|
|
822497317d | ||
|
|
c2047bd7b5 | ||
|
|
3636da60d6 | ||
|
|
2eebd55b77 | ||
|
|
12f76f8e28 | ||
|
|
28e6cca919 | ||
|
|
9b53538da6 | ||
|
|
994541bad5 | ||
|
|
3cbb16febe | ||
|
|
2b61fa07b9 | ||
|
|
406f4bd0f0 | ||
|
|
1ec6b817e9 | ||
|
|
7c55692c99 | ||
|
|
3aa29a788d | ||
|
|
55b0dccc80 | ||
|
|
edb97cbf8c | ||
|
|
7620f03e2b | ||
|
|
cced856297 | ||
|
|
3bcfd66971 | ||
|
|
e7e7c667e9 | ||
|
|
6de0bc4201 | ||
|
|
878520b7f9 | ||
|
|
63ac413982 | ||
|
|
171c58c546 | ||
|
|
6758ae3d16 | ||
|
|
329513d2a7 | ||
|
|
be195e9001 | ||
|
|
635a3fb966 | ||
|
|
281f7a28f3 | ||
|
|
507f5817c2 | ||
|
|
d3822474ba | ||
|
|
e1ce9f3006 | ||
|
|
c2b0768857 | ||
|
|
283d85ef8e | ||
|
|
f5656d8d5f | ||
|
|
5dd5a09bfc | ||
|
|
1b5f3372b4 | ||
|
|
974e964f42 | ||
|
|
8cb6ed02d2 | ||
|
|
8bb3b780d1 | ||
|
|
3000c8830c | ||
|
|
ac550e016d | ||
|
|
7420751806 | ||
|
|
f35889e8e4 | ||
|
|
857b252f4c | ||
|
|
572584a14f | ||
|
|
7e4ac3e69c | ||
|
|
0d73ffa79d | ||
|
|
5e2f1ddb84 | ||
|
|
6c4040901f | ||
|
|
103e09b900 | ||
|
|
8ddc517ab7 | ||
|
|
b26d982db4 | ||
|
|
3733bc395b | ||
|
|
26bfbafc0e | ||
|
|
84d1a18111 | ||
|
|
1cdacd5276 | ||
|
|
354715558c | ||
|
|
bca1613bff | ||
|
|
36b533b890 | ||
|
|
2eabccc1a9 | ||
|
|
75cd78277e | ||
|
|
5181b732ed | ||
|
|
513c0e8d3d | ||
|
|
f932feb305 | ||
|
|
6a2fda5ec0 | ||
|
|
86c30dd2c1 | ||
|
|
0ed41c473d | ||
|
|
5078a854d2 | ||
|
|
353bf04924 | ||
|
|
474ff8e303 | ||
|
|
5834bd4a0b | ||
|
|
81f31e5d8d | ||
|
|
267f645c16 | ||
|
|
8c10d7d6a5 | ||
|
|
bbdb47ee7a | ||
|
|
7f6856e6aa | ||
|
|
3439a3ad0a | ||
|
|
ee2e2ac49d | ||
|
|
a745167fb7 | ||
|
|
8551bc2459 | ||
|
|
38ceb6f65d | ||
|
|
33fe78911a | ||
|
|
3552415054 | ||
|
|
a2ec6f1e1f | ||
|
|
5f90d07853 | ||
|
|
d88548eece | ||
|
|
9ead5b3918 | ||
|
|
1a376e6922 | ||
|
|
eb29f2e1b2 | ||
|
|
bfaab7b2fb | ||
|
|
3e6a7f8a42 | ||
|
|
aff34d7627 | ||
|
|
8ef0a451ff | ||
|
|
8678f02fd6 | ||
|
|
a0a64606cd | ||
|
|
6574296278 | ||
|
|
d8414f56ee | ||
|
|
bb9499392b | ||
|
|
069ea9348f | ||
|
|
8c932c8913 | ||
|
|
562501dda8 | ||
|
|
79583869e4 | ||
|
|
5c9abcee21 | ||
|
|
e8377ed174 | ||
|
|
16f8df0238 | ||
|
|
dab772e25c | ||
|
|
37791a5537 | ||
|
|
eb55cc76be | ||
|
|
f728a01963 | ||
|
|
8aae503e35 | ||
|
|
fe499d6c94 | ||
|
|
cd2c820324 | ||
|
|
b4c5af4c04 | ||
|
|
f239957ca2 | ||
|
|
3b835e6f34 | ||
|
|
4ab3d2d0e1 | ||
|
|
3781686213 | ||
|
|
0247aeed52 | ||
|
|
3d34539c16 | ||
|
|
0a06b36f60 | ||
|
|
ea1d536c6c | ||
|
|
5087266693 | ||
|
|
3e617e329c | ||
|
|
1ab0b3d3f0 | ||
|
|
cb3f2d71d1 | ||
|
|
62bcb64640 | ||
|
|
300ea3b6d9 | ||
|
|
3ae1901757 | ||
|
|
2b63a59c91 | ||
|
|
ec69bae2a6 | ||
|
|
c07e0b606b | ||
|
|
61745df2a7 | ||
|
|
146c59e0db | ||
|
|
c7c0a4055b | ||
|
|
6c7d39889d | ||
|
|
9d9626d065 | ||
|
|
9bf7a10bf1 | ||
|
|
9659514feb | ||
|
|
f39ddd4315 | ||
|
|
90bd9e0211 | ||
|
|
48f419fec0 | ||
|
|
b993eba2df | ||
|
|
dbab024778 | ||
|
|
7999fc6893 | ||
|
|
b2eecf6cd9 | ||
|
|
919e1c66ba | ||
|
|
3fc5a3fd68 | ||
|
|
345ebc0983 | ||
|
|
8e14e78847 | ||
|
|
856b09bb4b | ||
|
|
c303cd3683 | ||
|
|
097bb82af2 | ||
|
|
e780448be5 | ||
|
|
732076e60e | ||
|
|
6bcbb48993 | ||
|
|
2b80704063 | ||
|
|
215010e2a4 | ||
|
|
d9a035d399 | ||
|
|
f9008c62d0 | ||
|
|
6c28ce29a9 | ||
|
|
6ac88317cc | ||
|
|
332b2342cd | ||
|
|
3703ab4427 | ||
|
|
a1586944b7 | ||
|
|
22f93c0b89 | ||
|
|
803b26ea02 | ||
|
|
d86a566668 | ||
|
|
a1011c7516 | ||
|
|
40df14ace8 | ||
|
|
96f9e4cca8 | ||
|
|
cd1a3c5df2 | ||
|
|
9f9db9257e | ||
|
|
f748fae549 | ||
|
|
c303162c09 | ||
|
|
6d4434c652 | ||
|
|
a92b7526eb | ||
|
|
c33f95e0b5 | ||
|
|
7bfeef9d73 | ||
|
|
506207f2e5 | ||
|
|
f9ef03b827 | ||
|
|
b5dabf65c7 | ||
|
|
3fb3d04161 | ||
|
|
d76e379542 | ||
|
|
d046bd7c28 | ||
|
|
bc2ec7fff1 | ||
|
|
e3c3f20da7 | ||
|
|
2254164f40 | ||
|
|
2ae4ac6383 | ||
|
|
80cd4c89b0 | ||
|
|
3df6fd7d0e | ||
|
|
f3243c9e40 | ||
|
|
528df9602b | ||
|
|
cd83539855 | ||
|
|
ecc17fa4b2 | ||
|
|
4c555bb7f4 | ||
|
|
e2592bb87a | ||
|
|
631564b22c | ||
|
|
a8bc81fb13 | ||
|
|
fecb77090d | ||
|
|
692495283c | ||
|
|
5ee6615d6f | ||
|
|
4e34057d16 | ||
|
|
f16b47ebf1 | ||
|
|
3ac16cb7af | ||
|
|
2b3c357657 | ||
|
|
5658a3cfc3 | ||
|
|
06c3cb6c42 | ||
|
|
b60bb6b3b1 | ||
|
|
b3648a2dae | ||
|
|
04efdab63a | ||
|
|
966dd13c23 | ||
|
|
2f6450f2b4 | ||
|
|
0db2080e2b | ||
|
|
4210c44ee9 | ||
|
|
9d7fef36c2 | ||
|
|
303d4d6107 | ||
|
|
9ac847ce19 | ||
|
|
d2c7c297b1 | ||
|
|
995def4be8 | ||
|
|
0f08ba67e7 | ||
|
|
499c1aa104 | ||
|
|
348b0743b4 | ||
|
|
769445fafe | ||
|
|
61af6e50ce | ||
|
|
4fd7295762 | ||
|
|
b0ad01da6c | ||
|
|
4320adc46d | ||
|
|
e2ec359327 | ||
|
|
dc773339ab | ||
|
|
e5f8ebbf37 | ||
|
|
1489b3ba4f | ||
|
|
e871e88f46 | ||
|
|
ea9ce5dfaf | ||
|
|
a1dada9a55 | ||
|
|
b137a790a0 | ||
|
|
9ef776d8ab | ||
|
|
e5b726cbe6 | ||
|
|
fcfac6c20b | ||
|
|
e7a4b06945 | ||
|
|
3fe84f82f9 | ||
|
|
3eee824160 | ||
|
|
bfcab961d5 | ||
|
|
c3f4390d11 | ||
|
|
dce3f0104d | ||
|
|
0c4c0da17f | ||
|
|
1dec2c8fc1 | ||
|
|
6eb112eb38 | ||
|
|
8307f153cd | ||
|
|
4796a558bb | ||
|
|
61d257f801 | ||
|
|
af74e7c32c | ||
|
|
bf474ee8d0 | ||
|
|
26e6c40a1b | ||
|
|
5723f29b60 | ||
|
|
bc65e1b446 | ||
|
|
1842bec2aa | ||
|
|
9cc33b9a33 | ||
|
|
c8fefae571 | ||
|
|
6d6616425c | ||
|
|
cb4ed7ebd1 | ||
|
|
a51929068a | ||
|
|
dff2071fa3 | ||
|
|
fdd2d61a28 | ||
|
|
1532b213e4 | ||
|
|
d2b76b08e1 | ||
|
|
af40443730 | ||
|
|
62d9e21433 | ||
|
|
ddfd3db7fa | ||
|
|
a50a7c426e | ||
|
|
e93760b2ac | ||
|
|
4a80da7515 | ||
|
|
962db5f736 | ||
|
|
1c0ca0dbeb | ||
|
|
40faa4029d | ||
|
|
86351c61ae | ||
|
|
1b56899636 | ||
|
|
b92a7f1bdb | ||
|
|
7b1db69867 | ||
|
|
8ae34fa6e6 | ||
|
|
a507f9d401 | ||
|
|
ac724fc36f | ||
|
|
90564859b2 | ||
|
|
562a5e5c6d | ||
|
|
49605b87f9 | ||
|
|
27bdac7b4f | ||
|
|
b92c02fd63 | ||
|
|
aec3874e0b | ||
|
|
08a31e9aa0 | ||
|
|
8850ff910c | ||
|
|
5f79f5b523 | ||
|
|
62bf6eadac | ||
|
|
c7575c4646 | ||
|
|
57ae0c8d53 | ||
|
|
c37838d2c0 | ||
|
|
fa9949d562 | ||
|
|
bf54187f28 | ||
|
|
696bce8201 | ||
|
|
5d01bd5479 | ||
|
|
403426ba75 | ||
|
|
43a884159b | ||
|
|
f925aa5642 | ||
|
|
8bdbe45a57 | ||
|
|
b2a24974a3 | ||
|
|
9dd6633c6a | ||
|
|
1dc5461f90 | ||
|
|
15418d234d | ||
|
|
0ac439cc84 | ||
|
|
b4eda74c6d | ||
|
|
35d598f1f4 | ||
|
|
d2272e5715 | ||
|
|
252c2245f7 | ||
|
|
93fd3cd2c3 | ||
|
|
0e3d5df4bf | ||
|
|
e476c26bb5 | ||
|
|
fb0996f94e | ||
|
|
1da72666d7 | ||
|
|
f790f7d0b5 | ||
|
|
76a0f43485 | ||
|
|
0dcbf98d1f | ||
|
|
bc0eb37775 | ||
|
|
d494702257 | ||
|
|
db60217474 | ||
|
|
a399660a15 | ||
|
|
2eeb53537a | ||
|
|
ab5f98a2b6 | ||
|
|
6d92ed667f | ||
|
|
ff123b579f | ||
|
|
f9a66f8f75 | ||
|
|
32f33815c2 | ||
|
|
ea9ea98e1a | ||
|
|
9c32d24a20 | ||
|
|
25f483d760 | ||
|
|
d9e471eaec | ||
|
|
e4f8a075f4 | ||
|
|
0a3dc8f79d | ||
|
|
ad69df0b63 | ||
|
|
2a3a9e124b | ||
|
|
8afd1e8a80 | ||
|
|
ee8cc5b139 | ||
|
|
bed490f491 | ||
|
|
620ff4bd60 | ||
|
|
99ecb0f22e | ||
|
|
31603bfd41 | ||
|
|
abd803a58c | ||
|
|
f84e77cbce | ||
|
|
170c8ffc55 | ||
|
|
bf3ba96fb9 | ||
|
|
b3e057a5a3 | ||
|
|
78c07f3934 | ||
|
|
249a49aff5 | ||
|
|
03c291a61e | ||
|
|
97d9ce8b68 | ||
|
|
1a55df6674 | ||
|
|
6ac10c1380 | ||
|
|
13270b6152 | ||
|
|
b2c0359017 | ||
|
|
8a6dd1da93 | ||
|
|
2e1410ca36 | ||
|
|
3d96181450 | ||
|
|
fe749ca594 | ||
|
|
1b6cd58112 | ||
|
|
8d405d223a | ||
|
|
81ad19043a | ||
|
|
34db58f8e0 | ||
|
|
890163af4a | ||
|
|
c4e8a6646d | ||
|
|
639c8511bf | ||
|
|
5e082fc5d7 | ||
|
|
7f393ff9ba | ||
|
|
b37aac0cd9 | ||
|
|
d857c4b786 | ||
|
|
6ffd1d7926 | ||
|
|
415ad79c80 | ||
|
|
da4fef7f6b | ||
|
|
76c034435d | ||
|
|
f9239f0642 | ||
|
|
7f096df998 | ||
|
|
3f0738d874 | ||
|
|
b310b640b4 | ||
|
|
18caa58336 | ||
|
|
03e5909c23 | ||
|
|
d9071632d2 | ||
|
|
694269113a | ||
|
|
78f347a505 | ||
|
|
eb9be7b190 | ||
|
|
952aeb4d22 | ||
|
|
8a865513b3 | ||
|
|
30e38dde3f | ||
|
|
5f68eb0f1a | ||
|
|
ce92134a00 | ||
|
|
2c80d13170 | ||
|
|
a49d6490c5 | ||
|
|
dc76a7801e | ||
|
|
0b0d3ded8c | ||
|
|
c05dd026fb | ||
|
|
dca94271b0 | ||
|
|
8d7aa8736e | ||
|
|
1d693670f4 | ||
|
|
52f50cdaf5 | ||
|
|
d2ac5d5ac4 | ||
|
|
fe579358f6 | ||
|
|
db942ee10b | ||
|
|
b4648a6efd | ||
|
|
647f468feb | ||
|
|
ef608df76b | ||
|
|
8e32373a99 | ||
|
|
7f817944fa | ||
|
|
7752da92b1 | ||
|
|
24023d438d | ||
|
|
9e0d8840f3 | ||
|
|
d762f097d0 | ||
|
|
336aa47177 | ||
|
|
1eeccd654a | ||
|
|
e37abef359 | ||
|
|
c120f42ee1 | ||
|
|
1531548e51 | ||
|
|
72ebdceb6e | ||
|
|
791fa2b5f6 | ||
|
|
47df44c202 | ||
|
|
d7635370ba | ||
|
|
1d577750c0 | ||
|
|
b56685edb1 | ||
|
|
f7f230f40e | ||
|
|
88e19e2fd1 | ||
|
|
ae6f0e1ae2 | ||
|
|
c3e880890e | ||
|
|
a7edb6d0f6 | ||
|
|
03e18401cc | ||
|
|
1d6b8c2558 | ||
|
|
ccd111cd94 | ||
|
|
e8f3b5df8a | ||
|
|
c4c9c73809 | ||
|
|
a4514ebb2b | ||
|
|
7b44df9afd | ||
|
|
3a018e9654 | ||
|
|
9d4e571d89 | ||
|
|
f62184c96c | ||
|
|
320183554c | ||
|
|
a525816eca | ||
|
|
0e11a223ad | ||
|
|
6e4b992a79 | ||
|
|
1164d38e5c | ||
|
|
4288d62a53 | ||
|
|
b17bd13fb5 | ||
|
|
7124fd6a92 | ||
|
|
81f354207d | ||
|
|
15cb611764 | ||
|
|
6115433aba | ||
|
|
0c5b9165ef | ||
|
|
c432646f30 | ||
|
|
f70913832c | ||
|
|
25ee7f3538 | ||
|
|
5a76601ae6 | ||
|
|
4367fe6ead | ||
|
|
242642a7ed | ||
|
|
074fc960e5 | ||
|
|
e26d08ca8e | ||
|
|
8c433680a9 | ||
|
|
547046bddb | ||
|
|
12983bb1a6 | ||
|
|
5dc20232ef | ||
|
|
0040ecee32 | ||
|
|
e8f30b667d | ||
|
|
99a9f081fa | ||
|
|
51605ae680 | ||
|
|
ca06400071 | ||
|
|
588df32b2f | ||
|
|
ab7f560b4f | ||
|
|
ffce103eae | ||
|
|
90418f0e28 | ||
|
|
0daaf6d1e5 | ||
|
|
47f26b0f4c | ||
|
|
e67ce41667 | ||
|
|
0cff24486a | ||
|
|
850ba0d96a | ||
|
|
303c9a0267 | ||
|
|
0f95165088 | ||
|
|
105cf9c90c | ||
|
|
cb40a8d0de | ||
|
|
f19ab37bc8 | ||
|
|
d716bd6a86 | ||
|
|
5b13b22823 | ||
|
|
49076fe477 | ||
|
|
50a517b6f1 | ||
|
|
184c3b18ba | ||
|
|
2f8dcaf47b | ||
|
|
dbe18b345f | ||
|
|
5d68ec8176 | ||
|
|
83e58f9375 | ||
|
|
fe199d78a4 | ||
|
|
d5f7acb019 | ||
|
|
a141c34ee7 | ||
|
|
25c9189e1a |
@@ -1,11 +1,16 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=DemonEditor
|
||||
Comment=Channels and satellites list editor for Enigma2
|
||||
Comment=Channel and satellite list editor for Enigma2
|
||||
Comment[ru]=Редактор списка каналов и спутников для Enigma2
|
||||
Icon=accessories-text-editor
|
||||
Comment[be]=Рэдактар спіса каналаў і спадарожнікаў для Enigma2
|
||||
Comment[de]=Programm- und Satellitenlisten-Editor für Enigma2
|
||||
Comment[it]=Editor di liste canali e satelliti per Enigma2
|
||||
Comment[tr]=Enigma2 için kanal ve uydu listesi editörü
|
||||
Comment[es]=Editor de listas de canales y satélites para Enigma2
|
||||
Icon=demon-editor
|
||||
Exec=bash -c 'cd $(dirname %k) && ./start.py'
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Utility;Application;
|
||||
StartupNotify=true
|
||||
StartupNotify=false
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
132
README.md
132
README.md
@@ -1,32 +1,118 @@
|
||||
# DemonEditor
|
||||
# <img src="app/ui/icons/hicolor/96x96/apps/demon-editor.png" width="32" /> DemonEditor
|
||||
[](LICENSE) 
|
||||
### Enigma2 channel and satellite list editor for GNU/Linux.
|
||||
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
|
||||
|
||||
Enigma2 channel and satellites list editor for GNU/Linux.
|
||||
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
|
||||
Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc)
|
||||
## Main features of the program
|
||||
* Editing bouquets, channels, satellites.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141680963-9b8eb6cc-c712-46b2-aefe-19769e21a7d5.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141680963-9b8eb6cc-c712-46b2-aefe-19769e21a7d5.png)
|
||||
* Import function.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141681059-68bc1b55-6fab-436c-aa73-ef24e2e5113b.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681059-68bc1b55-6fab-436c-aa73-ef24e2e5113b.png)
|
||||
* Backup function.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141681104-ed9b5d35-25de-426f-b9bb-2a6e4db022bb.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681104-ed9b5d35-25de-426f-b9bb-2a6e4db022bb.png)
|
||||
* Support of picons.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141681115-957c63a3-4113-422d-bb27-2d96b1463cd1.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681115-957c63a3-4113-422d-bb27-2d96b1463cd1.png)
|
||||
* Importing services, downloading picons and updating satellites from the Web.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141681075-28f18ea5-e456-4e84-bf64-1b7d9a95324d.png" width="262"/>](https://user-images.githubusercontent.com/7511379/141681075-28f18ea5-e456-4e84-bf64-1b7d9a95324d.png)
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141681040-b1ad190a-6bc2-4741-bb42-1fb219a0fcab.png" width="250"/>](https://user-images.githubusercontent.com/7511379/141681040-b1ad190a-6bc2-4741-bb42-1fb219a0fcab.png)
|
||||
* Extended support of IPTV.
|
||||
* Import to bouquet(Neutrino WEBTV) from m3u.
|
||||
* Export of bouquets with IPTV services in m3u.
|
||||
* Assignment of EPG from DVB or XML for IPTV services (Enigma2 only).
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141681187-fae4e784-c9e0-43df-b499-4d38e83d6560.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681187-fae4e784-c9e0-43df-b499-4d38e83d6560.png)
|
||||
* Playback of IPTV or other streams directly from the bouquet list.
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141681129-98f78cdc-9a98-46ef-b738-618a327634d4.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681129-98f78cdc-9a98-46ef-b738-618a327634d4.png)
|
||||
* Control panel (via HTTP API).
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141684475-4511ea4f-b152-42d5-b9c8-f3e1e9a160d0.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141684475-4511ea4f-b152-42d5-b9c8-f3e1e9a160d0.png)
|
||||
* Ability to view EPG and manage timers (via HTTP API).
|
||||
* Simple FTP client (experimental).
|
||||
[<img src="https://user-images.githubusercontent.com/7511379/141681165-5679c331-72e7-4044-b365-dcdb30b1433c.png" width="480"/>](https://user-images.githubusercontent.com/7511379/141681165-5679c331-72e7-4044-b365-dcdb30b1433c.png)
|
||||
|
||||
#### Keyboard shortcuts
|
||||
* **Ctrl + X** - only in bouquet list.
|
||||
* **Ctrl + C** - only in services list.
|
||||
Clipboard is **"rubber"**. There is an accumulation before the insertion!
|
||||
* **Ctrl + Insert** - copies the selected channels from the main list to the bouquet
|
||||
beginning or inserts (creates) a new bouquet.
|
||||
* **Ctrl + BackSpace** - copies the selected channels from the main list to the bouquet end.
|
||||
* **Ctrl + E** - edit.
|
||||
* **Ctrl + R, F2** - rename.
|
||||
* **Ctrl + Alt + R** - rename for bouquet.
|
||||
* **Ctrl + S, T** in Satellites edit tool for create satellite or transponder.
|
||||
* **Ctrl + L** - parental lock.
|
||||
* **Ctrl + H** - hide/skip.
|
||||
* **Ctrl + P** - start play IPTV or other stream in the bouquet list.
|
||||
* **Ctrl + Z** - switch(**zap**) the channel(works when the HTTP API is enabled, Enigma2 only).
|
||||
* **Ctrl + W** - switch to the channel and watch in the program.
|
||||
* **Space** - select/deselect.
|
||||
* **Left/Right** - remove selection.
|
||||
* **Ctrl + Up, Down, PageUp, PageDown, Home, End**- move selected items in the list.
|
||||
* **Ctrl + O** - (re)load user data from current dir.
|
||||
* **Ctrl + D** - load data from receiver.
|
||||
* **Ctrl + U/B** - upload data/bouquets to receiver.
|
||||
* **Ctrl + I** - extra info, details.
|
||||
* **Ctrl + F** - show search bar.
|
||||
* **Ctrl + Shift + F** - show/hide filter bar.
|
||||
* **Ctrl + T** - show/hide built-in Telnet client.
|
||||
* **Ctrl + Shift + L** - show/hide logging panel.
|
||||
|
||||
For **multiple** selection with the mouse, press and hold the **Ctrl** key!
|
||||
|
||||
Keyboard shortcuts:
|
||||
Ctrl + X, C, V, Up, Down, PageUp, PageDown, S, T, E, L, H, Space; Insert, Delete, F2.
|
||||
Insert - copies the selected channels from the main list to the bouquet or inserts (creates) a new bouquet.
|
||||
Ctrl + X - only in bouquet list. Ctrl + C - only in services list.
|
||||
Clipboard is "rubber". There is an accumulation before the insertion!
|
||||
Ctrl + E, F2 - edit/rename.
|
||||
Ctrl + S, T, E in Satellites edit tool for create and edit satellite or transponder.
|
||||
Ctrl + L - parental lock.
|
||||
Ctrl + H - hide/skip.
|
||||
Left/Right - remove selection.
|
||||
## Minimum requirements
|
||||
*Python >= 3.6, GTK+ >= 3.22, python3-gi, python3-gi-cairo, python3-requests.*
|
||||
|
||||
Multiple selections in lists only with Space key (as in file managers)!
|
||||
***Optional:** python3-pil, python3-chardet.*
|
||||
## Installation and Launch
|
||||
* ### Linux
|
||||
To start the program, in most cases it is enough to download the [archive](https://github.com/DYefremov/DemonEditor/archive/master.zip), unpack
|
||||
and run it by double clicking on DemonEditor.desktop in the root directory,
|
||||
or launching from the console with the command:```./start.py```
|
||||
Extra folders can be deleted, excluding the *app* folder and root files like *DemonEditor.desktop* and *start.py*!
|
||||
|
||||
Extra:
|
||||
Ability to import IPTV into bouquet from m3u files(Enigma2 only)!
|
||||
Tool for downloading picons from lyngsat.com.
|
||||
To create a simple **debian package**, you can use the *build-deb.sh.* You can also download a ready-made *.deb package from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
|
||||
Users of **LTS** versions of [Ubuntu](https://ubuntu.com/) or distributions based on them can use [PPA](https://launchpad.net/~dmitriy-yefremov/+archive/ubuntu/demon-editor) repository.
|
||||
A ready-made [package](https://aur.archlinux.org/packages/demoneditor-bin) is also available for [Arch Linux](https://archlinux.org/) users in the [AUR](https://aur.archlinux.org/) repository.
|
||||
* ### macOS
|
||||
**This program can be run on macOS.**
|
||||
To run the program on macOS, you need to install [brew](https://brew.sh/).
|
||||
Then install the required components via terminal:
|
||||
```brew install python3 gtk+3 pygobject3 adwaita-icon-theme```
|
||||
```pip3 install requests, pillow```
|
||||
|
||||
Tests only in image based on OpenPLi or last BPanther(neutrino) images with GM 990 Spark Reloaded receiver
|
||||
in my preferred linux distro (Last Linux Mint 18.* - MATE 64-bit)!
|
||||
Launch is similar to Linux.
|
||||
|
||||
Minimum requirements: Python >= 3.5.2 and GTK+ 3 with PyGObject bindings.
|
||||
You can also download the ready-made package as a ***.dmg** file from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
|
||||
Recommended copy the package to the **Application** directory.
|
||||
Perhaps in the security settings it will be necessary to allow the launch of this application!
|
||||
|
||||
Terrestrial and cable channels at the moment are not supported!
|
||||
* ### MS Windows
|
||||
**Windows users can also run this program.**
|
||||
One way is to use the [MSYS2](https://www.msys2.org/) platform. You can use [this](https://github.com/DYefremov/DemonEditor/blob/master/build/BUILD_WIN.md) quick guide.
|
||||
In addition, you can download a ready-made build (**64-bit**) from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
|
||||
|
||||
Note. To create a simple debian package, you can use the build-deb.sh
|
||||
**All builds may contain components distributed under the GPL [v3](http://www.gnu.org/licenses/gpl-3.0.html) or lower license.
|
||||
By downloading and using this packages you agree to the terms of this [license](http://www.gnu.org/licenses/gpl-3.0.html) and the possible inconvenience associated with this!**
|
||||
|
||||
THIS SOFTWARE COMES WITH ABSOLUTELY NO WARRANTY.
|
||||
AUTHOR IS NOT LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY CONNECTION WITH THIS SOFTWARE.
|
||||
|
||||
## Important
|
||||
The program is tested only with [openATV](https://www.opena.tv/) image and **Formuler F1** receiver in [Linux Mint](https://linuxmint.com/) (MATE 64-bit) distribution!
|
||||
Support for DVB-T/T2 and DVB-C channels for Neutrino is not fully implemented and has an experimental status.
|
||||
|
||||
Main supported *lamedb* format is version **4**. Versions **3** and **5** has only **experimental** support! For version **3** is only read mode available. When saving, version **4** format is used instead.
|
||||
|
||||
When using the multiple import feature, from *lamedb* will be taken data **only for channels that are in the selected bouquets!**
|
||||
If you need full set of the data, including *[satellites, terrestrial, cables].xml* (current files will be overwritten),
|
||||
just load your data via *"File/Open"* and press *"Save"*. When importing separate bouquet files, only those services
|
||||
(excluding IPTV) that are in the **current open lamedb** (main list of services) will be imported.
|
||||
|
||||
**The built-in Telnet client does not support ANSI escape sequences!**
|
||||
|
||||
For streams playback, this app supports [VLC](https://www.videolan.org/vlc/), [MPV](https://mpv.io/) and [GStreamer](https://gstreamer.freedesktop.org/). Depending on your distro, you may need to install additional packages and libraries.
|
||||
#### Command line arguments:
|
||||
* **-l** - write logs to file.
|
||||
* **-d on/off** - turn on/off debug mode. Allows to display more information in the logs.
|
||||
|
||||
## License
|
||||
Licensed under the [MIT](LICENSE) license.
|
||||
|
||||
4
_config.yml
Normal file
4
_config.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
theme: jekyll-theme-slate
|
||||
title: DemonEditor
|
||||
description: Enigma2 channel and satellite list editor.
|
||||
show_downloads: false
|
||||
@@ -1,25 +1,33 @@
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from functools import wraps
|
||||
from threading import Thread
|
||||
from threading import Thread, Timer
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
_LOG_FILE = "demon-editor.log"
|
||||
_DATE_FORMAT = "%d-%m-%y %H:%M:%S"
|
||||
_LOGGER_NAME = "main_logger"
|
||||
logging.Logger(_LOGGER_NAME)
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
filename=_LOG_FILE,
|
||||
format="%(asctime)s %(message)s",
|
||||
datefmt=_DATE_FORMAT)
|
||||
|
||||
LOGGER_NAME = "main_logger"
|
||||
|
||||
|
||||
def get_logger():
|
||||
return logging.getLogger(_LOGGER_NAME)
|
||||
def init_logger():
|
||||
logging.Logger(LOGGER_NAME)
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format="%(asctime)s %(message)s",
|
||||
datefmt=_DATE_FORMAT,
|
||||
handlers=[logging.FileHandler(_LOG_FILE), logging.StreamHandler()])
|
||||
log("Logging is enabled.", level=logging.INFO)
|
||||
|
||||
|
||||
def log(message, level=logging.ERROR):
|
||||
get_logger().log(level, message)
|
||||
def log(message, level=logging.ERROR, debug=False, fmt_message="{}"):
|
||||
""" The main logging function. """
|
||||
logger = logging.getLogger(LOGGER_NAME)
|
||||
if debug:
|
||||
from traceback import format_exc
|
||||
logger.log(level, fmt_message.format(format_exc()))
|
||||
else:
|
||||
logger.log(level, message)
|
||||
|
||||
|
||||
def run_idle(func):
|
||||
@@ -43,5 +51,44 @@ def run_task(func):
|
||||
return wrapper
|
||||
|
||||
|
||||
def run_with_delay(timeout=5):
|
||||
""" Starts the function with a delay.
|
||||
|
||||
If the previous timer still works, it will canceled!
|
||||
"""
|
||||
|
||||
def run_with(func):
|
||||
timer = None
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
nonlocal timer
|
||||
if timer and timer.is_alive():
|
||||
timer.cancel()
|
||||
|
||||
def run():
|
||||
GLib.idle_add(func, *args, **kwargs, priority=GLib.PRIORITY_LOW)
|
||||
|
||||
timer = Timer(interval=timeout, function=run)
|
||||
timer.start()
|
||||
|
||||
return wrapper
|
||||
|
||||
return run_with
|
||||
|
||||
|
||||
class DefaultDict(defaultdict):
|
||||
""" Extended to support functions with params as default factory. """
|
||||
|
||||
def __missing__(self, key):
|
||||
if self.default_factory:
|
||||
value = self[key] = self.default_factory(key)
|
||||
return value
|
||||
return super().__missing__(key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self[key]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
854
app/connections.py
Normal file
854
app/connections.py
Normal file
@@ -0,0 +1,854 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import time
|
||||
import urllib
|
||||
import xml.etree.ElementTree as ETree
|
||||
from enum import Enum
|
||||
from ftplib import FTP, CRLF, Error, error_perm
|
||||
from http.client import RemoteDisconnected
|
||||
from telnetlib import Telnet
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import urlencode, quote
|
||||
from urllib.request import (urlopen, HTTPPasswordMgrWithDefaultRealm, HTTPBasicAuthHandler, build_opener,
|
||||
install_opener, Request)
|
||||
|
||||
from app.commons import log, run_task
|
||||
from app.settings import SettingsType
|
||||
|
||||
BQ_FILES_LIST = ("tv", "radio", # enigma 2
|
||||
"services.xml", "myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino
|
||||
|
||||
DATA_FILES_LIST = ("lamedb", "lamedb5", "blacklist", "whitelist",)
|
||||
|
||||
STC_XML_FILE = ("satellites.xml", "terrestrial.xml", "cables.xml")
|
||||
WEB_TV_XML_FILE = ("webtv.xml",)
|
||||
PICONS_SUF = (".jpg", ".png")
|
||||
|
||||
|
||||
class DownloadType(Enum):
|
||||
ALL = 0
|
||||
BOUQUETS = 1
|
||||
SATELLITES = 2
|
||||
PICONS = 3
|
||||
WEBTV = 4
|
||||
EPG = 5
|
||||
|
||||
|
||||
class TestException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class HttpApiException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UtfFTP(FTP):
|
||||
""" FTP class wrapper. """
|
||||
|
||||
def retrlines(self, cmd, callback=None):
|
||||
""" Small modification of the original method.
|
||||
|
||||
It is used to retrieve data in line mode and skip errors related
|
||||
to reading file names in encoding other than UTF-8 or Latin-1.
|
||||
Decode errors are ignored [UnicodeDecodeError, etc].
|
||||
"""
|
||||
if callback is None:
|
||||
callback = log
|
||||
self.sendcmd("TYPE A")
|
||||
with self.transfercmd(cmd) as conn, conn.makefile("r", encoding=self.encoding, errors="ignore") as fp:
|
||||
while 1:
|
||||
line = fp.readline(self.maxline + 1)
|
||||
if len(line) > self.maxline:
|
||||
msg = "UtfFTP [retrlines] error: got more than {} bytes".format(self.maxline)
|
||||
log(msg)
|
||||
raise Error(msg)
|
||||
if self.debugging > 2:
|
||||
log('UtfFTP [retrlines] *retr* {}'.format(repr(line)))
|
||||
if not line:
|
||||
break
|
||||
if line[-2:] == CRLF:
|
||||
line = line[:-2]
|
||||
elif line[-1:] == "\n":
|
||||
line = line[:-1]
|
||||
callback(line)
|
||||
return self.voidresp()
|
||||
|
||||
# ***************** Download ******************* #
|
||||
|
||||
def download_files(self, save_path, file_list, callback=None):
|
||||
""" Downloads files from the receiver via FTP. """
|
||||
for file in filter(lambda s: s.endswith(file_list), self.nlst()):
|
||||
self.download_file(file, save_path, callback)
|
||||
|
||||
def download_file(self, name, save_path, callback=None):
|
||||
with open(save_path + name, "wb") as f:
|
||||
msg = "Downloading file: {}. Status: {}\n"
|
||||
try:
|
||||
resp = str(self.retrbinary("RETR " + name, f.write))
|
||||
except error_perm as e:
|
||||
resp = str(e)
|
||||
msg = msg.format(name, e)
|
||||
log(msg.rstrip())
|
||||
else:
|
||||
msg = msg.format(name, resp)
|
||||
|
||||
callback(msg) if callback else log(msg.rstrip())
|
||||
|
||||
return resp
|
||||
|
||||
def download_dir(self, path, save_path, callback=None):
|
||||
""" Downloads directory from FTP with all contents.
|
||||
|
||||
Creates a leaf directory and all intermediate ones. This is recursive.
|
||||
"""
|
||||
os.makedirs(os.path.join(save_path, path), exist_ok=True)
|
||||
|
||||
files = []
|
||||
self.dir(path, files.append)
|
||||
for f in files:
|
||||
f_data = self.get_file_data(f)
|
||||
f_path = f_data[8]
|
||||
|
||||
if f_data[0][0] == "d":
|
||||
try:
|
||||
os.makedirs(os.path.join(save_path, f_path), exist_ok=True)
|
||||
except OSError as e:
|
||||
msg = "Download dir error: {}".format(e).rstrip()
|
||||
log(msg)
|
||||
return "500 " + msg
|
||||
else:
|
||||
self.download_dir(f_path, save_path, callback)
|
||||
else:
|
||||
try:
|
||||
self.download_file(f_path, save_path, callback)
|
||||
except OSError as e:
|
||||
log("Download dir error: {}".format(e).rstrip())
|
||||
|
||||
resp = "226 Transfer complete."
|
||||
msg = "Copy directory {}. Status: {}".format(path, resp)
|
||||
log(msg)
|
||||
|
||||
if callback:
|
||||
callback(msg)
|
||||
|
||||
return resp
|
||||
|
||||
def download_xml(self, data_path, xml_path, xml_files, callback):
|
||||
""" Used for download *.xml files. """
|
||||
self.cwd(xml_path)
|
||||
self.download_files(data_path, xml_files, callback)
|
||||
|
||||
def download_picons(self, src, dest, callback, files_filter=None):
|
||||
try:
|
||||
self.cwd(src)
|
||||
except error_perm as e:
|
||||
callback(str(e))
|
||||
return
|
||||
|
||||
for file in filter(picons_filter_function(files_filter), self.nlst()):
|
||||
self.download_file(file, dest, callback)
|
||||
|
||||
# ***************** Uploading ******************* #
|
||||
|
||||
def upload_bouquets(self, data_path, remove_unused, callback):
|
||||
if remove_unused:
|
||||
self.remove_unused_bouquets(callback)
|
||||
self.upload_files(data_path, BQ_FILES_LIST, callback)
|
||||
|
||||
def upload_files(self, data_path, file_list, callback):
|
||||
for file_name in os.listdir(data_path):
|
||||
if file_name in STC_XML_FILE or file_name in WEB_TV_XML_FILE:
|
||||
continue
|
||||
if file_name.endswith(file_list):
|
||||
self.send_file(file_name, data_path, callback)
|
||||
|
||||
def upload_xml(self, data_path, xml_path, xml_files, callback):
|
||||
""" Used for transfer *.xml files. """
|
||||
self.cwd(xml_path)
|
||||
for xml_file in xml_files:
|
||||
self.send_file(xml_file, data_path, callback)
|
||||
|
||||
def upload_picons(self, src, dest, callback, files_filter=None):
|
||||
try:
|
||||
self.cwd(dest)
|
||||
except error_perm as e:
|
||||
if str(e).startswith("550"):
|
||||
self.mkd(dest) # if not exist
|
||||
self.cwd(dest)
|
||||
|
||||
for file_name in filter(picons_filter_function(files_filter), os.listdir(src)):
|
||||
self.send_file(file_name, src, callback)
|
||||
|
||||
def remove_unused_bouquets(self, callback):
|
||||
bq_files = ("userbouquet.", "subbouquet.", "bouquets.xml", "ubouquets.xml")
|
||||
|
||||
for file in filter(lambda f: f.startswith(bq_files), self.nlst()):
|
||||
self.delete_file(file, callback)
|
||||
|
||||
def send_file(self, file_name, path, callback=None):
|
||||
""" Opens the file in binary mode and transfers into receiver """
|
||||
file_src = path + file_name
|
||||
resp = "500"
|
||||
if not os.path.isfile(file_src):
|
||||
log("Uploading file: '{}'. File not found. Skipping.".format(file_src))
|
||||
return resp + " File not found."
|
||||
|
||||
with open(file_src, "rb") as f:
|
||||
msg = "Uploading file: {}. Status: {}\n"
|
||||
try:
|
||||
resp = str(self.storbinary("STOR " + file_name, f))
|
||||
except Error as e:
|
||||
resp = str(e)
|
||||
msg = msg.format(file_name, resp)
|
||||
log(msg)
|
||||
else:
|
||||
msg = msg.format(file_name, resp)
|
||||
|
||||
if callback:
|
||||
callback(msg)
|
||||
|
||||
return resp
|
||||
|
||||
def upload_dir(self, path, callback=None):
|
||||
""" Uploads directory to FTP with all contents.
|
||||
|
||||
Creates a leaf directory and all intermediate ones. This is recursive.
|
||||
"""
|
||||
resp = "200"
|
||||
msg = "Uploading directory: {}. Status: {}"
|
||||
try:
|
||||
files = os.listdir(path)
|
||||
except OSError as e:
|
||||
log(e)
|
||||
else:
|
||||
os.chdir(path)
|
||||
for f in files:
|
||||
file = r"{}{}".format(path, f)
|
||||
if os.path.isfile(file):
|
||||
self.send_file(f, path, callback)
|
||||
elif os.path.isdir(file):
|
||||
try:
|
||||
self.mkd(f)
|
||||
except Error:
|
||||
pass # NOP
|
||||
|
||||
try:
|
||||
self.cwd(f)
|
||||
except Error as e:
|
||||
resp = str(e)
|
||||
log(msg.format(f, resp))
|
||||
else:
|
||||
self.upload_dir(file + "/")
|
||||
|
||||
self.cwd("..")
|
||||
os.chdir("..")
|
||||
|
||||
if callback:
|
||||
callback(msg.format(path, resp))
|
||||
|
||||
return resp
|
||||
|
||||
# ****************** Deletion ******************** #
|
||||
|
||||
def delete_picons(self, callback, dest=None, files_filter=None):
|
||||
if dest:
|
||||
try:
|
||||
self.cwd(dest)
|
||||
except Error as e:
|
||||
callback(str(e))
|
||||
return
|
||||
|
||||
for file in filter(picons_filter_function(files_filter), self.nlst()):
|
||||
self.delete_file(file, callback)
|
||||
|
||||
def delete_file(self, file, callback=log):
|
||||
msg = "Deleting file: {}. Status: {}\n"
|
||||
try:
|
||||
resp = self.delete(file)
|
||||
except Error as e:
|
||||
resp = str(e)
|
||||
msg = msg.format(file, resp)
|
||||
log(msg)
|
||||
else:
|
||||
msg = msg.format(file, resp)
|
||||
|
||||
if callback:
|
||||
callback(msg)
|
||||
|
||||
return resp
|
||||
|
||||
def delete_dir(self, path, callback=None):
|
||||
files = []
|
||||
self.dir(path, files.append)
|
||||
for f in files:
|
||||
f_data = self.get_file_data(f)
|
||||
f_path = f"{path}/{f_data[8]}"
|
||||
|
||||
if f_data[0][0] == "d":
|
||||
self.delete_dir(f_path, callback)
|
||||
else:
|
||||
self.delete_file(f_path, callback)
|
||||
|
||||
msg = "Remove directory {}. Status: {}\n"
|
||||
try:
|
||||
resp = self.rmd(path)
|
||||
except Error as e:
|
||||
msg = msg.format(path, e)
|
||||
log(msg)
|
||||
return "500"
|
||||
else:
|
||||
msg = msg.format(path, resp)
|
||||
log(msg.rstrip())
|
||||
|
||||
if callback:
|
||||
callback(msg)
|
||||
|
||||
return resp
|
||||
|
||||
def rename_file(self, from_name, to_name, callback=None):
|
||||
msg = "File rename: {}. Status: {}\n"
|
||||
try:
|
||||
resp = self.rename(from_name, to_name)
|
||||
except Error as e:
|
||||
resp = str(e)
|
||||
msg = msg.format(from_name, resp)
|
||||
log(msg)
|
||||
else:
|
||||
msg = msg.format(from_name, resp)
|
||||
|
||||
if callback:
|
||||
callback(msg)
|
||||
|
||||
return resp
|
||||
|
||||
@staticmethod
|
||||
def get_file_data(file):
|
||||
""" Returns a prepared list of file data from a file string. """
|
||||
f_data = file.split()
|
||||
# Ignoring space in file name.
|
||||
f_data = f_data[0:9]
|
||||
f_data[8] = file[file.index(f_data[8]):]
|
||||
return f_data
|
||||
|
||||
|
||||
def download_data(*, settings, download_type=DownloadType.ALL, callback=log, files_filter=None):
|
||||
with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
|
||||
ftp.encoding = "utf-8"
|
||||
callback("FTP OK.\n")
|
||||
save_path = settings.profile_data_path
|
||||
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||
# bouquets
|
||||
if download_type is DownloadType.ALL or download_type is DownloadType.BOUQUETS:
|
||||
ftp.cwd(settings.services_path)
|
||||
file_list = BQ_FILES_LIST + DATA_FILES_LIST if download_type is DownloadType.ALL else BQ_FILES_LIST
|
||||
ftp.download_files(save_path, file_list, callback)
|
||||
# *.xml and webtv
|
||||
if download_type in (DownloadType.ALL, DownloadType.SATELLITES):
|
||||
ftp.download_xml(save_path, settings.satellites_xml_path, STC_XML_FILE, callback)
|
||||
if download_type in (DownloadType.ALL, DownloadType.WEBTV):
|
||||
ftp.download_xml(save_path, settings.satellites_xml_path, WEB_TV_XML_FILE, callback)
|
||||
|
||||
if download_type is DownloadType.PICONS:
|
||||
picons_path = settings.profile_picons_path
|
||||
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
|
||||
ftp.download_picons(settings.picons_path, picons_path, callback, files_filter)
|
||||
# epg.dat
|
||||
if download_type is DownloadType.EPG:
|
||||
stb_path = settings.services_path
|
||||
epg_options = settings.epg_options
|
||||
if epg_options:
|
||||
stb_path = epg_options.get("epg_dat_stb_path", stb_path)
|
||||
save_path = epg_options.get("epg_dat_path", save_path)
|
||||
|
||||
ftp.cwd(stb_path)
|
||||
ftp.download_files(save_path, "epg.dat", callback)
|
||||
|
||||
callback("\nDone.\n")
|
||||
|
||||
|
||||
def upload_data(*, settings, download_type=DownloadType.ALL, remove_unused=False,
|
||||
callback=log, done_callback=None, use_http=False, files_filter=None):
|
||||
s_type = settings.setting_type
|
||||
data_path = settings.profile_data_path
|
||||
host, port, use_ssl = settings.host, settings.http_port, settings.http_use_ssl
|
||||
base_url = f"http{'s' if use_ssl else ''}://{host}:{port}"
|
||||
base = "web" if s_type is SettingsType.ENIGMA_2 else "control"
|
||||
url = f"{base_url}/{base}/"
|
||||
tn, ht = None, None # telnet, http
|
||||
|
||||
try:
|
||||
if use_http:
|
||||
ht = http(settings.user, settings.password, base_url, callback, use_ssl, s_type)
|
||||
next(ht)
|
||||
message = ""
|
||||
if download_type is DownloadType.BOUQUETS:
|
||||
message = "User bouquets will be updated!"
|
||||
elif download_type is DownloadType.ALL:
|
||||
message = "All user data will be reloaded!"
|
||||
elif download_type is DownloadType.SATELLITES:
|
||||
message = "Satellites.xml file will be updated!"
|
||||
elif download_type is DownloadType.PICONS:
|
||||
message = "Picons will be updated!"
|
||||
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
params = urlencode({"text": message, "type": 2, "timeout": 5})
|
||||
else:
|
||||
params = urlencode({"nmsg": message, "timeout": 5}, quote_via=quote)
|
||||
|
||||
ht.send((f"{url}message?{params}", "Sending info message... "))
|
||||
|
||||
if s_type is SettingsType.ENIGMA_2 and download_type is DownloadType.ALL:
|
||||
time.sleep(5)
|
||||
ht.send((f"{url}powerstate?newstate=0", "Toggle Standby "))
|
||||
time.sleep(2)
|
||||
else:
|
||||
if download_type is not DownloadType.PICONS:
|
||||
# Telnet
|
||||
tn = telnet(host=host,
|
||||
user=settings.user,
|
||||
password=settings.password,
|
||||
timeout=settings.telnet_timeout)
|
||||
next(tn)
|
||||
# Terminate Enigma2 or Neutrino.
|
||||
callback("Telnet initialization ...\n")
|
||||
tn.send("init 4")
|
||||
callback("Stopping GUI...\n")
|
||||
|
||||
with UtfFTP(host=host, user=settings.user, passwd=settings.password) as ftp:
|
||||
ftp.encoding = "utf-8"
|
||||
callback("FTP OK.\n")
|
||||
sat_xml_path = settings.satellites_xml_path
|
||||
services_path = settings.services_path
|
||||
|
||||
if download_type is DownloadType.SATELLITES:
|
||||
ftp.upload_xml(data_path, sat_xml_path, STC_XML_FILE, callback)
|
||||
|
||||
if s_type is SettingsType.NEUTRINO_MP and download_type is DownloadType.WEBTV:
|
||||
ftp.upload_xml(data_path, sat_xml_path, WEB_TV_XML_FILE, callback)
|
||||
|
||||
if download_type is DownloadType.BOUQUETS:
|
||||
ftp.cwd(services_path)
|
||||
ftp.upload_bouquets(data_path, remove_unused, callback)
|
||||
|
||||
if download_type is DownloadType.ALL:
|
||||
ftp.upload_xml(data_path, sat_xml_path, STC_XML_FILE, callback)
|
||||
if s_type is SettingsType.NEUTRINO_MP:
|
||||
ftp.upload_xml(data_path, sat_xml_path, WEB_TV_XML_FILE, callback)
|
||||
|
||||
ftp.cwd(services_path)
|
||||
ftp.upload_bouquets(data_path, remove_unused, callback)
|
||||
ftp.upload_files(data_path, DATA_FILES_LIST, callback)
|
||||
|
||||
if download_type is DownloadType.PICONS:
|
||||
ftp.upload_picons(settings.profile_picons_path, settings.picons_path, callback, files_filter)
|
||||
|
||||
if tn and not use_http:
|
||||
# Resume Enigma2 or restart Neutrino.
|
||||
tn.send("init 3" if s_type is SettingsType.ENIGMA_2 else "init 6")
|
||||
callback("Starting...\n" if s_type is SettingsType.ENIGMA_2 else "Rebooting...\n")
|
||||
elif ht and use_http:
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
if download_type is DownloadType.BOUQUETS:
|
||||
ht.send((f"{url}servicelistreload?mode=2", "Reloading Userbouquets."))
|
||||
elif download_type is DownloadType.ALL:
|
||||
ht.send((f"{url}servicelistreload?mode=0", "Reloading lamedb and Userbouquets."))
|
||||
ht.send((f"{url}powerstate?newstate=4", "Wakeup from Standby."))
|
||||
else:
|
||||
ht.send((f"{url}reloadchannels", "Reloading channels..."))
|
||||
|
||||
if done_callback is not None:
|
||||
done_callback()
|
||||
finally:
|
||||
if tn:
|
||||
tn.close()
|
||||
if ht:
|
||||
ht.close()
|
||||
|
||||
|
||||
# ***************** Picons *******************#
|
||||
|
||||
def remove_picons(*, settings, callback, done_callback=None, files_filter=None):
|
||||
with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
|
||||
ftp.encoding = "utf-8"
|
||||
callback("FTP OK.\n")
|
||||
ftp.delete_picons(callback, settings.picons_path, files_filter)
|
||||
if 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 http(user, password, url, callback, use_ssl=False, s_type=SettingsType.ENIGMA_2):
|
||||
HttpAPI.init_auth(user, password, url, use_ssl)
|
||||
data = HttpAPI.get_post_data(url, password, url) if s_type is SettingsType.ENIGMA_2 else None
|
||||
|
||||
while True:
|
||||
url, message = yield
|
||||
resp = HttpAPI.get_response(HttpAPI.Request.TEST, url, data, s_type)
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
resp = resp.get("e2statetext", None)
|
||||
|
||||
callback(f"HTTP: {message} {'Successful.' if resp and message else ''}\n")
|
||||
|
||||
|
||||
def telnet(host, port=23, user="", password="", timeout=5):
|
||||
try:
|
||||
tn = Telnet(host=host, port=port, timeout=timeout)
|
||||
except socket.timeout:
|
||||
log("telnet error: socket timeout")
|
||||
else:
|
||||
time.sleep(1)
|
||||
command = yield
|
||||
if user != "":
|
||||
tn.read_until(b"login: ", timeout)
|
||||
tn.write(user.encode("utf-8") + b"\n")
|
||||
time.sleep(timeout)
|
||||
if password != "":
|
||||
tn.read_until(b"Password: ", timeout)
|
||||
tn.write(password.encode("utf-8") + b"\n")
|
||||
time.sleep(timeout)
|
||||
tn.write("{}\r\n".format(command).encode("utf-8"))
|
||||
time.sleep(timeout)
|
||||
command = yield
|
||||
time.sleep(timeout)
|
||||
tn.write("{}\r\n".format(command).encode("utf-8"))
|
||||
time.sleep(timeout)
|
||||
yield
|
||||
|
||||
|
||||
# ***************** HTTP API *******************#
|
||||
|
||||
class HttpAPI:
|
||||
__MAX_WORKERS = 4
|
||||
|
||||
class Request(str, Enum):
|
||||
ZAP = "zap?sRef="
|
||||
INFO = "about"
|
||||
SIGNAL = "signal"
|
||||
STREAM = "stream.m3u?ref="
|
||||
STREAM_TS = "ts.m3u?file="
|
||||
STREAM_CURRENT = "streamcurrent.m3u"
|
||||
CURRENT = "getcurrent"
|
||||
TEST = None
|
||||
TOKEN = "session"
|
||||
# Player
|
||||
PLAY = "mediaplayerplay?file="
|
||||
PLAYER_LIST = "mediaplayerlist?path=playlist"
|
||||
PLAYER_PLAY = "mediaplayercmd?command=play"
|
||||
PLAYER_NEXT = "mediaplayercmd?command=next"
|
||||
PLAYER_PREV = "mediaplayercmd?command=previous"
|
||||
PLAYER_STOP = "mediaplayercmd?command=stop"
|
||||
PLAYER_REMOVE = "mediaplayerremove?file="
|
||||
# Remote control
|
||||
POWER = "powerstate?newstate="
|
||||
REMOTE = "remotecontrol?command="
|
||||
VOL = "vol?set=set"
|
||||
# EPG
|
||||
EPG = "epgservice?sRef="
|
||||
# Timer
|
||||
TIMER = ""
|
||||
TIMER_LIST = "timerlist"
|
||||
# Recordings
|
||||
RECORDINGS = "movielist?dirname="
|
||||
REC_DIRS = "getlocations"
|
||||
REC_CURRENT = "getcurrlocation"
|
||||
# Screenshot
|
||||
GRUB = "grab?format=jpg&"
|
||||
# Neutrino requests.
|
||||
N_INFO = "info"
|
||||
N_ZAP = "zapto"
|
||||
N_STREAM = "build_playlist?id="
|
||||
|
||||
class Remote(str, Enum):
|
||||
""" Args for HttpRequestType [REMOTE] class. """
|
||||
UP = "103"
|
||||
LEFT = "105"
|
||||
RIGHT = "106"
|
||||
DOWN = "108"
|
||||
MENU = "139"
|
||||
EXIT = "174"
|
||||
OK = "352"
|
||||
INFO = "358"
|
||||
RED = "398"
|
||||
GREEN = "399"
|
||||
YELLOW = "400"
|
||||
BLUE = "401"
|
||||
BACK = "412"
|
||||
|
||||
class Power(str, Enum):
|
||||
""" Args for HttpRequestType [POWER] class. """
|
||||
TOGGLE_STANDBY = "0"
|
||||
DEEP_STANDBY = "1"
|
||||
REBOOT = "2"
|
||||
RESTART_GUI = "3"
|
||||
WAKEUP = "4"
|
||||
STANDBY = "5"
|
||||
|
||||
PARAM_REQUESTS = {Request.REMOTE,
|
||||
Request.POWER,
|
||||
Request.VOL,
|
||||
Request.EPG,
|
||||
Request.TIMER,
|
||||
Request.RECORDINGS,
|
||||
Request.N_ZAP}
|
||||
|
||||
STREAM_REQUESTS = {Request.STREAM,
|
||||
Request.STREAM_CURRENT,
|
||||
Request.STREAM_TS,
|
||||
Request.N_STREAM}
|
||||
|
||||
def __init__(self, settings):
|
||||
from concurrent.futures import ThreadPoolExecutor as PoolExecutor
|
||||
self._executor = PoolExecutor(max_workers=self.__MAX_WORKERS)
|
||||
|
||||
self._settings = settings
|
||||
self._shutdown = False
|
||||
self._session_id = 0
|
||||
self._main_url = None
|
||||
self._base_url = None
|
||||
self._data = None
|
||||
self._is_owif = True
|
||||
self._s_type = SettingsType.ENIGMA_2
|
||||
self.init()
|
||||
|
||||
def send(self, req_type, ref, callback=print, ref_prefix=""):
|
||||
if self._shutdown:
|
||||
return
|
||||
|
||||
url = self._base_url + req_type
|
||||
data = self._data
|
||||
|
||||
if req_type is self.Request.ZAP or req_type in self.STREAM_REQUESTS:
|
||||
url += quote(ref)
|
||||
elif req_type is self.Request.PLAY or req_type is self.Request.PLAYER_REMOVE:
|
||||
url = f"{url}{ref_prefix}{quote(ref).replace('%3A', '%253A')}"
|
||||
elif req_type is self.Request.GRUB:
|
||||
data = None # Must be disabled for token-based security.
|
||||
url = f"{self._main_url}/{req_type}{ref}"
|
||||
elif req_type in self.PARAM_REQUESTS:
|
||||
url += ref
|
||||
|
||||
def done_callback(f):
|
||||
callback(f.result())
|
||||
|
||||
future = self._executor.submit(self.get_response, req_type, url, data, self._s_type)
|
||||
future.add_done_callback(done_callback)
|
||||
|
||||
@run_task
|
||||
def init(self):
|
||||
self._s_type = self._settings.setting_type
|
||||
user, password, use_ssl = self._settings.user, self._settings.password, self._settings.http_use_ssl
|
||||
self._main_url = f"http{'s' if use_ssl else ''}://{self._settings.host}:{self._settings.http_port}"
|
||||
self._base_url = f"{self._main_url}/{'web' if self._s_type is SettingsType.ENIGMA_2 else 'control'}/"
|
||||
self.init_auth(user, password, self._main_url, use_ssl)
|
||||
|
||||
self._data = None
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
s_id = self.get_session_id(user, password, f"{self._main_url}/web/{self.Request.TOKEN}")
|
||||
if s_id != "0":
|
||||
self._data = urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
|
||||
|
||||
self.send(self.Request.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
|
||||
def close(self):
|
||||
self._shutdown = True
|
||||
self._executor.shutdown()
|
||||
|
||||
@staticmethod
|
||||
def get_response(req_type, url, data=None, s_type=SettingsType.ENIGMA_2):
|
||||
try:
|
||||
with urlopen(Request(url, data=data), timeout=10) as f:
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
return HttpAPI.get_e2_response_data(req_type, f)
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
return HttpAPI.get_neutrino_response_data(req_type, f)
|
||||
else:
|
||||
return f.read().decode("utf-8")
|
||||
except HTTPError as e:
|
||||
if req_type is HttpAPI.Request.TEST:
|
||||
raise e
|
||||
return {"error_code": e.code}
|
||||
except (URLError, RemoteDisconnected, ConnectionResetError) as e:
|
||||
if req_type is HttpAPI.Request.TEST:
|
||||
raise e
|
||||
except ETree.ParseError as e:
|
||||
log("Parsing response error: {}".format(e))
|
||||
|
||||
return {"error_code": -1}
|
||||
|
||||
@staticmethod
|
||||
def get_e2_response_data(req_type, f):
|
||||
if req_type in HttpAPI.STREAM_REQUESTS:
|
||||
return {"m3u": f.read().decode("utf-8")}
|
||||
elif req_type is HttpAPI.Request.GRUB:
|
||||
return {"img_data": f.read()}
|
||||
elif req_type is HttpAPI.Request.CURRENT:
|
||||
for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2event"):
|
||||
return {el.tag: el.text for el in el.iter()} # return first[current] event from the list
|
||||
elif req_type is HttpAPI.Request.PLAYER_LIST:
|
||||
return [{el.tag: el.text for el in el.iter()} for el in
|
||||
ETree.fromstring(f.read().decode("utf-8")).iter("e2file")]
|
||||
elif req_type is HttpAPI.Request.EPG:
|
||||
return {"event_list": [{el.tag: el.text for el in el.iter()} for el in
|
||||
ETree.fromstring(f.read().decode("utf-8")).iter("e2event")]}
|
||||
elif req_type is HttpAPI.Request.TIMER_LIST:
|
||||
return {"timer_list": [{el.tag: el.text for el in el.iter()} for el in
|
||||
ETree.fromstring(f.read().decode("utf-8")).iter("e2timer")]}
|
||||
elif req_type is HttpAPI.Request.REC_DIRS:
|
||||
return {"rec_dirs": [el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2location")]}
|
||||
elif req_type is HttpAPI.Request.RECORDINGS:
|
||||
return {"recordings": [{el.tag: el.text for el in el.iter()} for el in
|
||||
ETree.fromstring(f.read().decode("utf-8")).iter("e2movie")]}
|
||||
else:
|
||||
return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()}
|
||||
|
||||
@staticmethod
|
||||
def get_neutrino_response_data(req_type, f):
|
||||
if req_type is HttpAPI.Request.N_INFO:
|
||||
return {"info": f.read().decode("utf-8").strip()}
|
||||
elif req_type is HttpAPI.Request.N_STREAM:
|
||||
return {"m3u": f.read().decode("utf-8")}
|
||||
return {"data": f.read().decode("utf-8")}
|
||||
|
||||
@staticmethod
|
||||
def init_auth(user, password, url, use_ssl=False):
|
||||
""" Init authentication """
|
||||
pass_mgr = HTTPPasswordMgrWithDefaultRealm()
|
||||
pass_mgr.add_password(None, url, user, password)
|
||||
auth_handler = HTTPBasicAuthHandler(pass_mgr)
|
||||
|
||||
if use_ssl:
|
||||
import ssl
|
||||
from urllib.request import HTTPSHandler
|
||||
|
||||
opener = build_opener(auth_handler, HTTPSHandler(context=ssl._create_unverified_context()))
|
||||
else:
|
||||
opener = build_opener(auth_handler)
|
||||
|
||||
install_opener(opener)
|
||||
|
||||
@staticmethod
|
||||
def get_session_id(user, password, url):
|
||||
data = urllib.parse.urlencode(dict(user=user, password=password)).encode("utf-8")
|
||||
return HttpAPI.get_response(HttpAPI.Request.TOKEN, url, data=data).get("e2sessionid", "0")
|
||||
|
||||
@staticmethod
|
||||
def get_post_data(base_url, password, user):
|
||||
s_id = HttpAPI.get_session_id(user, password, "{}/web/{}".format(base_url, HttpAPI.Request.TOKEN))
|
||||
data = None
|
||||
if s_id != "0":
|
||||
data = urllib.parse.urlencode({"user": user, "password": password, "sessionid": s_id}).encode("utf-8")
|
||||
return data
|
||||
|
||||
|
||||
# ***************** Connections testing *******************#
|
||||
|
||||
def test_ftp(host, port, user, password, timeout=5):
|
||||
try:
|
||||
with FTP(host=host, user=user, passwd=password, timeout=timeout) as ftp:
|
||||
return ftp.getwelcome()
|
||||
except (error_perm, ConnectionRefusedError, OSError) as e:
|
||||
raise TestException(e)
|
||||
|
||||
|
||||
def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message=False, s_type=SettingsType.ENIGMA_2):
|
||||
t_msg = "Connection test!"
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
params = urlencode({"text": t_msg, "type": 2, "timeout": timeout})
|
||||
params = "statusinfo" if skip_message else f"message?{params}"
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
params = urlencode({"nmsg": t_msg, "timeout": 5}, quote_via=quote)
|
||||
params = "info" if skip_message else f"message?{params}"
|
||||
else:
|
||||
raise TestException("This type of settings is not supported!")
|
||||
|
||||
base_url = f"http{'s' if use_ssl else ''}://{host}:{port}"
|
||||
base = "web" if s_type is SettingsType.ENIGMA_2 else "control"
|
||||
url = f"{base_url}/{base}/{params}"
|
||||
# Authentication
|
||||
HttpAPI.init_auth(user, password, base_url, use_ssl)
|
||||
data = HttpAPI.get_post_data(base_url, password, user) if s_type is SettingsType.ENIGMA_2 else None
|
||||
|
||||
try:
|
||||
resp = HttpAPI.get_response(HttpAPI.Request.TEST, url, data, s_type)
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
return resp.get("e2statetext", "")
|
||||
return resp
|
||||
except (RemoteDisconnected, URLError, HTTPError) as e:
|
||||
raise TestException(e)
|
||||
|
||||
|
||||
def test_telnet(host, port, user, password, timeout=5):
|
||||
try:
|
||||
gen = telnet_test(host, port, user, password, timeout)
|
||||
res = next(gen)
|
||||
msg = str(res, encoding="utf8").strip()
|
||||
log(msg)
|
||||
next(gen)
|
||||
if re.search("password", msg, re.IGNORECASE):
|
||||
raise TestException(msg)
|
||||
return msg
|
||||
except (socket.timeout, OSError) as e:
|
||||
raise TestException(e)
|
||||
|
||||
|
||||
def telnet_test(host, port, user, password, timeout):
|
||||
tn = Telnet(host=host, port=port, timeout=timeout)
|
||||
time.sleep(1)
|
||||
tn.read_until(b"login: ", timeout=2)
|
||||
tn.write(user.encode("utf-8") + b"\r")
|
||||
time.sleep(timeout)
|
||||
tn.read_until(b"Password: ", timeout=2)
|
||||
tn.write(password.encode("utf-8") + b"\r")
|
||||
time.sleep(timeout)
|
||||
yield tn.read_very_eager()
|
||||
tn.close()
|
||||
yield
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@@ -1,8 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
from app.commons import run_task
|
||||
from app.properties import Profile
|
||||
from .ecommons import Service, Satellite, Transponder, Bouquet, Bouquets
|
||||
from app.settings import SettingsType
|
||||
from .ecommons import Service, Satellite, Transponder, Bouquet, Bouquets, is_transponder_valid
|
||||
from .enigma.blacklist import get_blacklist, write_blacklist
|
||||
from .enigma.bouquets import get_bouquets as get_enigma_bouquets, write_bouquets as write_enigma_bouquets, to_bouquet_id
|
||||
from .enigma.bouquets import to_bouquet_id, BouquetsWriter, BouquetsReader
|
||||
from .enigma.lamedb import get_services as get_enigma_services, write_services as write_enigma_services
|
||||
from .iptv import parse_m3u
|
||||
from .neutrino.bouquets import get_bouquets as get_neutrino_bouquets, write_bouquets as write_neutrino_bouquets
|
||||
@@ -10,33 +37,42 @@ from .neutrino.services import get_services as get_neutrino_services, write_serv
|
||||
from .satxml import get_satellites, write_satellites
|
||||
|
||||
|
||||
def get_services(data_path, profile):
|
||||
if profile is Profile.ENIGMA_2:
|
||||
return get_enigma_services(data_path)
|
||||
elif profile is Profile.NEUTRINO_MP:
|
||||
def get_services(data_path, s_type, format_version):
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
return get_enigma_services(data_path, format_version)
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
return get_neutrino_services(data_path)
|
||||
|
||||
|
||||
@run_task
|
||||
def write_services(path, channels, profile):
|
||||
if profile is Profile.ENIGMA_2:
|
||||
write_enigma_services(path, channels)
|
||||
elif profile is Profile.NEUTRINO_MP:
|
||||
def write_services(path, channels, s_type, format_version):
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
write_enigma_services(path, channels, format_version)
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
write_neutrino_services(path, channels)
|
||||
|
||||
|
||||
def get_bouquets(path, profile):
|
||||
if profile is Profile.ENIGMA_2:
|
||||
return get_enigma_bouquets(path)
|
||||
elif profile is Profile.NEUTRINO_MP:
|
||||
def get_bouquets(path, s_type):
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
return BouquetsReader(path).get()
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
return get_neutrino_bouquets(path)
|
||||
|
||||
|
||||
def write_bouquet(path, bq, s_type):
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
writer = BouquetsWriter(path, None)
|
||||
writer.write_bouquet(f"{path}userbouquet.{bq.name}.{bq.type}", bq.name, bq.services)
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
from .neutrino.bouquets import write_bouquet
|
||||
write_bouquet(path, bq)
|
||||
|
||||
|
||||
@run_task
|
||||
def write_bouquets(path, bouquets, profile):
|
||||
if profile is Profile.ENIGMA_2:
|
||||
write_enigma_bouquets(path, bouquets)
|
||||
elif profile is Profile.NEUTRINO_MP:
|
||||
def write_bouquets(path, bouquets, s_type, force_bq_names=False):
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
BouquetsWriter(path, bouquets, force_bq_names).write()
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
write_neutrino_bouquets(path, bouquets)
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,39 @@
|
||||
""" Common elements module """
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Common elements module. """
|
||||
from collections import namedtuple
|
||||
from enum import Enum
|
||||
|
||||
Service = namedtuple("Service", ["flags_cas", "transponder_type", "coded", "service", "locked", "hide",
|
||||
"package", "service_type", "ssid", "freq", "rate", "pol", "fec",
|
||||
from app.commons import log
|
||||
|
||||
Service = namedtuple("Service", ["flags_cas", "transponder_type", "coded", "service", "locked", "hide", "package",
|
||||
"service_type", "picon", "picon_id", "ssid", "freq", "rate", "pol", "fec",
|
||||
"system", "pos", "data_id", "fav_id", "transponder"])
|
||||
|
||||
|
||||
@@ -13,9 +43,17 @@ class BqServiceType(Enum):
|
||||
DEFAULT = "DEFAULT"
|
||||
IPTV = "IPTV"
|
||||
MARKER = "MARKER" # 64
|
||||
SPACE = "SPACE" # 832 [hidden marker]
|
||||
ALT = "ALT" # Service with alternatives
|
||||
BOUQUET = "BOUQUET" # Sub bouquet.
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.DEFAULT
|
||||
|
||||
|
||||
Bouquet = namedtuple("Bouquet", ["name", "type", "services", "locked", "hidden"])
|
||||
Bouquet = namedtuple("Bouquet", ["name", "type", "services", "locked", "hidden", "file"])
|
||||
Bouquet.__new__.__defaults__ = (None, BqServiceType.DEFAULT, [], None, None, None) # For Python3 < 3.7
|
||||
Bouquets = namedtuple("Bouquets", ["name", "type", "bouquets"])
|
||||
BouquetService = namedtuple("BouquetService", ["name", "type", "data", "num"])
|
||||
|
||||
@@ -23,19 +61,39 @@ BouquetService = namedtuple("BouquetService", ["name", "type", "data", "num"])
|
||||
|
||||
Satellite = namedtuple("Satellite", ["name", "flags", "position", "transponders"])
|
||||
|
||||
Transponder = namedtuple("Transponder", ["frequency", "symbol_rate", "polarization", "fec_inner",
|
||||
"system", "modulation", "pls_mode", "pls_code", "is_id"])
|
||||
Transponder = namedtuple("Transponder", ["frequency", "symbol_rate", "polarization", "fec_inner", "system",
|
||||
"modulation", "pls_mode", "pls_code", "is_id", "t2mi_plp_id"])
|
||||
|
||||
|
||||
class Type(Enum):
|
||||
""" Types of DVB transponders """
|
||||
class TrType(Enum):
|
||||
""" Transponders type """
|
||||
Satellite = "s"
|
||||
Terestrial = "t"
|
||||
Terrestrial = "t"
|
||||
Cable = "c"
|
||||
ATSC = "a"
|
||||
|
||||
|
||||
class FLAG(Enum):
|
||||
""" Service flags """
|
||||
class BqType(Enum):
|
||||
""" Bouquet type. """
|
||||
BOUQUET = "bouquet"
|
||||
TV = "tv"
|
||||
RADIO = "radio"
|
||||
WEBTV = "webtv"
|
||||
MARKER = "marker"
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.TV
|
||||
|
||||
|
||||
class Flag(Enum):
|
||||
""" Service flags
|
||||
|
||||
K - last bit (1)
|
||||
H - second from end (10)
|
||||
P - third (100)
|
||||
N - sixth (100000)
|
||||
"""
|
||||
KEEP = 1 # Do not automatically update the services parameters.
|
||||
HIDE = 2
|
||||
PIDS = 4 # Always use the cached instead of current pids.
|
||||
@@ -43,9 +101,77 @@ class FLAG(Enum):
|
||||
NEW = 40 # Marked as new at the last scan
|
||||
|
||||
@staticmethod
|
||||
def hide_values():
|
||||
return 2, 3, 6, 7, 10, 42, 43, 46, 47
|
||||
def is_hide(value: int):
|
||||
return value & 1 << 1
|
||||
|
||||
@staticmethod
|
||||
def is_keep(value: int):
|
||||
return value & 1 << 0
|
||||
|
||||
@staticmethod
|
||||
def is_pids(value: int):
|
||||
return value & 1 << 2
|
||||
|
||||
@staticmethod
|
||||
def is_new(value: int):
|
||||
return value & 1 << 5
|
||||
|
||||
@staticmethod
|
||||
def parse(value: str) -> int:
|
||||
""" Returns an int representation of the flag value.
|
||||
|
||||
The flag value is usually represented by the number [int],
|
||||
but can also be appear in hex format.
|
||||
"""
|
||||
if len(value) < 3:
|
||||
return 0
|
||||
|
||||
value = value[2:]
|
||||
if value.isdigit():
|
||||
return int(value)
|
||||
return int(value, 16)
|
||||
|
||||
|
||||
class Pids(Enum):
|
||||
VIDEO = "c:00"
|
||||
AUDIO = "c:01"
|
||||
TELETEXT = "c:02"
|
||||
PCR = "c:03"
|
||||
AC3 = "c:04"
|
||||
VIDEO_TYPE = "c:05"
|
||||
AUDIO_CHANNEL = "c:06"
|
||||
BIT_STREAM_DELAY = "c:07" # in ms
|
||||
PCM_DELAY = "c:08" # in ms
|
||||
SUBTITLE = "c:09"
|
||||
|
||||
|
||||
class Inversion(Enum):
|
||||
Off = "0"
|
||||
On = "1"
|
||||
Auto = "2"
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.Auto
|
||||
|
||||
|
||||
class Pilot(Enum):
|
||||
Off = "0"
|
||||
On = "1"
|
||||
Auto = "2"
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.Auto
|
||||
|
||||
|
||||
class SystemCable(Enum):
|
||||
""" System of cable service """
|
||||
ANNEX_A = "0"
|
||||
ANNEX_C = "1"
|
||||
|
||||
|
||||
ROLL_OFF = {"0": "35%", "1": "25%", "2": "20%", "3": "Auto"}
|
||||
|
||||
POLARIZATION = {"0": "H", "1": "V", "2": "L", "3": "R"}
|
||||
|
||||
@@ -56,18 +182,83 @@ FEC = {"0": "Auto", "1": "1/2", "2": "2/3", "3": "3/4", "4": "5/6", "5": "7/8",
|
||||
"17": "4/5", "18": "9/10", "19": "1/2", "20": "2/3", "21": "3/4", "22": "5/6", "23": "7/8", "24": "8/9",
|
||||
"25": "3/5", "26": "4/5", "27": "9/10", "28": "Auto"}
|
||||
|
||||
FEC_DEFAULT = {"0": "Auto", "1": "1/2", "2": "2/3", "3": "3/4", "4": "5/6", "5": "7/8", "6": "8/9", "7": "3/5",
|
||||
"8": "4/5", "9": "9/10", "10": "6/7", "15": "None"}
|
||||
|
||||
SYSTEM = {"0": "DVB-S", "1": "DVB-S2"}
|
||||
|
||||
MODULATION = {"0": "Auto", "1": "QPSK", "2": "8PSK", "3": "16APSK", "5": "32APSK"}
|
||||
MODULATION = {"0": "Auto", "1": "QPSK", "2": "8PSK", "4": "16APSK", "5": "32APSK"}
|
||||
|
||||
SERVICE_TYPE = {"-2": "Unknown", "1": "TV", "2": "Radio", "3": "Data",
|
||||
"10": "Radio", "12": "Data", "22": "TV", "25": "TV (HD)",
|
||||
"136": "Data", "139": "Data"}
|
||||
SERVICE_TYPE = {"-2": "Data", "1": "TV", "2": "Radio", "3": "Data", "10": "Radio", "22": "TV (H264)",
|
||||
"25": "TV (HD)", "31": "TV (UHD)"}
|
||||
|
||||
CAS = {"C:2600": "BISS", "C:0b00": "Conax", "C:0b01": "Conax", "C:0b02": "Conax", "C:0baa": "Conax", "C:0602": "Irdeto",
|
||||
"C:0604": "Irdeto", "C:0606": "Irdeto", "C:0608": "Irdeto", "C:0622": "Irdeto", "C:0626": "Irdeto",
|
||||
"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"}
|
||||
# Terrestrial
|
||||
BANDWIDTH = {"0": "8MHz", "1": "7MHz", "2": "6MHz", "3": "Auto", "4": "5MHz", "5": "1/712MHz", "6": "10MHz"}
|
||||
|
||||
T_MODULATION = {"0": "QPSK", "1": "QAM16", "2": "QAM64", "3": "Auto", "4": "QAM256"}
|
||||
|
||||
TRANSMISSION_MODE = {"0": "2k", "1": "8k", "2": "Auto", "3": "4k", "4": "1k", "5": "16k", "6": "32k"}
|
||||
|
||||
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"}
|
||||
|
||||
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_SYSTEM = {"0": "DVB-T", "1": "DVB-T2", "-1": "DVB-T/T2"}
|
||||
|
||||
# Cable
|
||||
C_MODULATION = {"0": "Auto", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM128", "5": "QAM256"}
|
||||
|
||||
# ATSC
|
||||
A_MODULATION = {"0": "Auto", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM128", "5": "QAM256", "6": "8VSB",
|
||||
"7": "16VSB"}
|
||||
|
||||
# CAS
|
||||
CAS = {"C:26": "BISS", "C:0B": "Conax", "C:06": "Irdeto", "C:18": "Nagravision", "C:05": "Viaccess", "C:01": "SECA",
|
||||
"C:0E": "PowerVu", "C:4A": "DRE-Crypt", "C:7B": "DRE-Crypt", "C:56": "Verimatrix", "C:09": "VideoGuard"}
|
||||
|
||||
# 'on' attribute 0070(hex) = 112(int) = ONID(ONID-TID on www.lyngsat.com)
|
||||
PROVIDER = {112: "HTB+", 253: "Tricolor TV"}
|
||||
|
||||
|
||||
# ************* subsidiary functions ****************
|
||||
|
||||
def get_key_by_value(dc: dict, value):
|
||||
""" Returns key from dict by value """
|
||||
for k, v in dc.items():
|
||||
if v == value:
|
||||
return k
|
||||
|
||||
|
||||
def get_value_by_name(en, name):
|
||||
""" Returns value by name from enums """
|
||||
for n in en:
|
||||
if n.name == name:
|
||||
return n.value
|
||||
|
||||
|
||||
def is_transponder_valid(tr: Transponder):
|
||||
""" Checks transponder validity. """
|
||||
try:
|
||||
int(tr.frequency)
|
||||
int(tr.symbol_rate)
|
||||
tr.pls_mode is None or int(tr.pls_mode)
|
||||
tr.pls_code is None or int(tr.pls_code)
|
||||
tr.is_id is None or int(tr.is_id)
|
||||
tr.t2mi_plp_id is None or int(tr.t2mi_plp_id)
|
||||
except (TypeError, ValueError) as e:
|
||||
log(f"Transponder validation error: {e}\n{tr}")
|
||||
return False
|
||||
|
||||
if tr.polarization not in POLARIZATION.values():
|
||||
return False
|
||||
if tr.fec_inner not in FEC.values():
|
||||
return False
|
||||
if tr.system not in SYSTEM.values():
|
||||
return False
|
||||
if tr.modulation not in MODULATION.values():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,3 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" This module used for parsing blacklist file
|
||||
|
||||
Parent Lock/Unlock
|
||||
@@ -9,13 +37,14 @@ __FILE_NAME = "blacklist"
|
||||
|
||||
def get_blacklist(path):
|
||||
with suppress(FileNotFoundError):
|
||||
with open(path + __FILE_NAME, "r") as file:
|
||||
with open(path + __FILE_NAME, "r", encoding="utf-8") as file:
|
||||
# filter empty values and "\n"
|
||||
return {*list(filter(None, (x.strip() for x in file.readlines())))}
|
||||
return {}
|
||||
|
||||
|
||||
def write_blacklist(path, channels):
|
||||
with open(path + __FILE_NAME, "w") as file:
|
||||
with open(path + __FILE_NAME, "w", encoding="utf-8") as file:
|
||||
if channels:
|
||||
file.writelines("\n".join(channels))
|
||||
|
||||
|
||||
@@ -1,97 +1,292 @@
|
||||
""" Module for parsing bouquets """
|
||||
from app.eparser.ecommons import BqServiceType, BouquetService, Bouquets, Bouquet
|
||||
|
||||
_TV_ROOT_FILE_NAME = "bouquets.tv"
|
||||
_RADIO_ROOT_FILE_NAME = "bouquets.radio"
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
def get_bouquets(path):
|
||||
return parse_bouquets(path, "bouquets.tv", "tv"), parse_bouquets(path, "bouquets.radio", "radio")
|
||||
""" Module for working with Enigma2 bouquets. """
|
||||
import re
|
||||
from collections import Counter
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
from app.commons import log
|
||||
from app.eparser.ecommons import BqServiceType, BouquetService, Bouquets, Bouquet, BqType
|
||||
|
||||
_TV_FILE = "bouquets.tv"
|
||||
_RADIO_FILE = "bouquets.radio"
|
||||
_DEFAULT_BOUQUET_NAME = "favourites"
|
||||
_MARKER_PREFIX = "[MARKER!] "
|
||||
|
||||
|
||||
def write_bouquets(path, bouquets):
|
||||
srv_line = '#SERVICE 1:7:1:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
|
||||
line = []
|
||||
class BouquetsWriter:
|
||||
""" Class for creating and writing bouquet files.
|
||||
|
||||
for bqs in bouquets:
|
||||
line.clear()
|
||||
line.append("#NAME {}\n".format(bqs.name))
|
||||
If "force_bq_names" then naming the files using the name of the bouquet.
|
||||
Some images may have problems displaying the favorites list!
|
||||
"""
|
||||
_SERVICE = '#SERVICE 1:7:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
|
||||
_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"
|
||||
_ALT = '#SERVICE 1:134:1:0:0:0:0:0:0:0:FROM BOUQUET "{}" ORDER BY bouquet\n'
|
||||
_ALT_PAT = r"[<>:\"/\\|?*\-\s]"
|
||||
|
||||
for bq in bqs.bouquets:
|
||||
line.append(srv_line.format(bq.name, bq.type))
|
||||
write_bouquet(path, bq.name, bq.type, bq.services)
|
||||
def __init__(self, path, bouquets, force_bq_names=False):
|
||||
self._path = path
|
||||
self._bouquets = bouquets
|
||||
self._force_bq_names = force_bq_names
|
||||
self._marker_index = 1
|
||||
self._space_index = 0
|
||||
self._alt_names = set()
|
||||
self._NAME_PATTERN = re.compile("[^\\w_()]+")
|
||||
|
||||
with open(path + "bouquets.{}".format(bqs.type), "w") as file:
|
||||
file.writelines(line)
|
||||
def write(self):
|
||||
line = []
|
||||
|
||||
for bqs in self._bouquets:
|
||||
line.clear()
|
||||
line.append(f"#NAME {bqs.name}\n")
|
||||
bq_file_names = {b.file for b in bqs.bouquets}
|
||||
count = 1
|
||||
m_count = 0
|
||||
|
||||
def write_bouquet(path, name, bq_type, channels):
|
||||
bouquet = ["#NAME {}\n".format(name)]
|
||||
for bq in bqs.bouquets:
|
||||
bq_name = bq.file
|
||||
if not bq_name:
|
||||
if self._force_bq_names:
|
||||
bq_name = re.sub(self._NAME_PATTERN, "_", bq.name)
|
||||
else:
|
||||
bq_name = f"de{count:02d}"
|
||||
while bq_name in bq_file_names:
|
||||
count += 1
|
||||
bq_name = f"de{count:02d}"
|
||||
bq_file_names.add(bq_name)
|
||||
|
||||
for ch in channels:
|
||||
if not ch: # if was duplicate
|
||||
continue
|
||||
bq_type = BqType(bq.type)
|
||||
if bq_type is BqType.MARKER:
|
||||
m_data = bq.file.split(":") if bq.file else None
|
||||
b_name = m_data[-1].strip() if m_data else bq.name.lstrip(_MARKER_PREFIX)
|
||||
line.append(self._MARKER.format(m_count, b_name))
|
||||
m_count += 1
|
||||
else:
|
||||
if bq_type is BqType.BOUQUET:
|
||||
bq_name = re.sub(self._NAME_PATTERN, "_", bq.name)
|
||||
self.write_sub_bouquet(self._path, bq_name, bq, bqs.type)
|
||||
else:
|
||||
self.write_bouquet(f"{self._path}userbouquet.{bq_name}.{bqs.type}", bq.name, bq.services)
|
||||
line.append(self._SERVICE.format(2 if bqs.type == BqType.RADIO.value else 1, bq_name, bqs.type))
|
||||
|
||||
if ch.service_type == BqServiceType.IPTV.name or ch.service_type == BqServiceType.MARKER.name:
|
||||
bouquet.append("#SERVICE {}\n".format(ch.fav_id.strip()))
|
||||
else:
|
||||
bouquet.append("#SERVICE {}\n".format(to_bouquet_id(ch)))
|
||||
with open(f"{self._path}bouquets.{bqs.type}", "w", encoding="utf-8", newline="\n") as file:
|
||||
file.writelines(line)
|
||||
|
||||
with open(path + "userbouquet.{}.{}".format(name, bq_type), "w") as file:
|
||||
file.writelines(bouquet)
|
||||
def write_bouquet(self, path, name, services):
|
||||
""" Writes single bouquet file. """
|
||||
bouquet = [f"#NAME {name}\n"]
|
||||
for srv in services:
|
||||
s_type = srv.service_type
|
||||
if s_type == BqServiceType.IPTV.name:
|
||||
bouquet.append(f"#SERVICE {srv.fav_id.strip()}\n")
|
||||
elif s_type == BqServiceType.MARKER.name:
|
||||
m_data = srv.fav_id.strip().split(":")
|
||||
m_data[2] = self._marker_index
|
||||
self._marker_index += 1
|
||||
bouquet.append(self._MARKER.format(m_data[2], m_data[-1]))
|
||||
elif s_type == BqServiceType.SPACE.name:
|
||||
bouquet.append(self._SPACE.format(self._space_index))
|
||||
self._space_index += 1
|
||||
elif s_type == BqServiceType.ALT.name:
|
||||
services = srv.transponder
|
||||
if services:
|
||||
p = Path(path)
|
||||
alt_name = srv.data_id
|
||||
f_name = f"alternatives.{alt_name}{p.suffix}"
|
||||
|
||||
if self._force_bq_names:
|
||||
alt_name = re.sub(self._ALT_PAT, "_", srv.service).lower()
|
||||
f_name = f"alternatives.{alt_name}{p.suffix}"
|
||||
|
||||
def to_bouquet_id(ch):
|
||||
""" Creates bouquet channel id """
|
||||
data_type = ch.data_id
|
||||
if data_type:
|
||||
data_type = int(ch.data_id.split(":")[-2])
|
||||
if data_type == 22:
|
||||
data_type = 16
|
||||
elif data_type == 25:
|
||||
data_type = 19
|
||||
service = "{}:0:{}:{}:0:0:0:".format(1, data_type, ch.fav_id)
|
||||
|
||||
return service
|
||||
|
||||
|
||||
def get_bouquet(path, name, bq_type):
|
||||
""" Parsing services ids from bouquet file """
|
||||
with open(path + "userbouquet.{}.{}".format(name, bq_type)) as file:
|
||||
chs_list = file.read()
|
||||
services = []
|
||||
for ch in list(filter(lambda x: len(x) > 1, chs_list.split("#SERVICE")[1:])): # filtering ['']
|
||||
ch_data = ch.strip().split(":")
|
||||
if ch_data[1] == "64":
|
||||
services.append(BouquetService(ch_data[-1].split("\n")[0], BqServiceType.MARKER, ch, ch_data[2]))
|
||||
elif "http" in ch:
|
||||
services.append(BouquetService(ch_data[-1].split("\n")[0], BqServiceType.IPTV, ch, 0))
|
||||
bouquet.append(self._ALT.format(f_name))
|
||||
self.write_bouquet(f"{p.parent}/{f_name}", srv.service, services)
|
||||
else:
|
||||
services.append(BouquetService(None, BqServiceType.DEFAULT,
|
||||
"{}:{}:{}:{}".format(ch_data[3], ch_data[4], ch_data[5], ch_data[6]), 0))
|
||||
data = to_bouquet_id(srv)
|
||||
if srv.service:
|
||||
bouquet.append(f"#SERVICE {data}:{srv.service}\n#DESCRIPTION {srv.service}\n")
|
||||
else:
|
||||
bouquet.append(f"#SERVICE {data}\n")
|
||||
|
||||
return services
|
||||
with open(path, "w", encoding="utf-8", newline="\n") as file:
|
||||
file.writelines(bouquet)
|
||||
|
||||
def write_sub_bouquet(self, path, file_name, bq, bq_type):
|
||||
bouquet = [f"#NAME {bq.name}\n"]
|
||||
sb_type = 2 if bq_type == BqType.RADIO.value else 1
|
||||
|
||||
for sb in bq.services:
|
||||
bq_name = f"subbouquet.{re.sub(self._NAME_PATTERN, '_', sb.name)}.{sb.type}"
|
||||
self.write_bouquet(f"{path}{bq_name}", sb.name, sb.services)
|
||||
bouquet.append(f"#SERVICE 1:7:{sb_type}:0:0:0:0:0:0:0:FROM BOUQUET \"{bq_name}\" ORDER BY bouquet\n")
|
||||
|
||||
with open(f"{self._path}userbouquet.{file_name}.{bq_type}", "w", encoding="utf-8", newline="\n") as file:
|
||||
file.writelines(bouquet)
|
||||
|
||||
|
||||
def parse_bouquets(path, bq_name, bq_type):
|
||||
with open(path + bq_name) as file:
|
||||
lines = file.readlines()
|
||||
bouquets = None
|
||||
nm_sep = "#NAME"
|
||||
class ServiceType(Enum):
|
||||
SERVICE = "0"
|
||||
BOUQUET = "7" # Sub bouquet.
|
||||
MARKER = "64"
|
||||
SPACE = "832" # Hidden marker.
|
||||
ALT = "134" # Alternatives.
|
||||
UDP = "256"
|
||||
|
||||
for line in lines:
|
||||
if nm_sep in line:
|
||||
_, _, name = line.partition(nm_sep)
|
||||
bouquets = Bouquets(name.strip(), bq_type, [])
|
||||
if bouquets and "#SERVICE" in line:
|
||||
name = line.split(".")[1]
|
||||
bouquets[2].append(Bouquet(name=name,
|
||||
type=bq_type,
|
||||
services=get_bouquet(path, name, bq_type),
|
||||
locked=None,
|
||||
hidden=None))
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
log("Error. No matching service type [{} {}] was found.".format(cls.__name__, value))
|
||||
return cls.SERVICE
|
||||
|
||||
return bouquets
|
||||
|
||||
class BouquetsReader:
|
||||
""" Class for reading and parsing bouquets. """
|
||||
_ALT_PAT = re.compile(".*alternatives\\.+(.*)\\.([tv|radio]+).*")
|
||||
_BQ_PAT = re.compile(".*userbouquet\\.+(.*)\\.+[tv|radio].*")
|
||||
_SUB_BQ_PAT = re.compile(".*subbouquet\\.+(.*)\\.([tv|radio]+).*")
|
||||
_STREAM_TYPES = {"4097", "5001", "5002", "8193", "8739"}
|
||||
|
||||
__slots__ = ["_path"]
|
||||
|
||||
def __init__(self, path):
|
||||
self._path = path
|
||||
|
||||
def get(self):
|
||||
""" Returns a tuple of TV and Radio bouquets. """
|
||||
return self.parse_bouquets(_TV_FILE, BqType.TV.value), self.parse_bouquets(_RADIO_FILE, BqType.RADIO.value)
|
||||
|
||||
def parse_bouquets(self, bq_name, bq_type):
|
||||
with open(self._path + bq_name, encoding="utf-8", errors="replace") as file:
|
||||
line = file.readline()
|
||||
_, _, bqs_name = line.partition("#NAME")
|
||||
if not bqs_name:
|
||||
log(f"No bouquets name found in '{bq_name}'")
|
||||
bqs_name = "Bouquets (TV)" if bq_type == BqType.TV.value else "Bouquets (Radio)"
|
||||
bouquets = Bouquets(bqs_name.strip(), bq_type, [])
|
||||
|
||||
b_names = set()
|
||||
real_b_names = Counter()
|
||||
|
||||
for line in file.readlines():
|
||||
if "#SERVICE" in line:
|
||||
name = re.match(self._BQ_PAT, line)
|
||||
if name:
|
||||
b_name = name.group(1)
|
||||
if b_name in b_names:
|
||||
log(f"The list of bouquets contains duplicate [{b_name}] names!")
|
||||
else:
|
||||
b_names.add(b_name)
|
||||
|
||||
rb_name, services = self.get_bouquet(self._path, b_name, bq_type)
|
||||
if rb_name in real_b_names:
|
||||
log(f"Bouquet file 'userbouquet.{b_name}.{bq_type}' has duplicate name: {rb_name}")
|
||||
real_b_names[rb_name] += 1
|
||||
rb_name = f"{rb_name} {real_b_names[rb_name]}"
|
||||
else:
|
||||
real_b_names[rb_name] = 0
|
||||
|
||||
bouquets[2].append(Bouquet(rb_name, bq_type, services, None, None, b_name))
|
||||
else:
|
||||
s_data = line.split(":")
|
||||
if len(s_data) == 12 and s_data[1] == ServiceType.MARKER.value:
|
||||
b_name = f"{_MARKER_PREFIX}{s_data[-1].strip()}"
|
||||
bouquets[2].append(Bouquet(b_name, BqType.MARKER.value, [], None, None, line.strip()))
|
||||
else:
|
||||
log(f"Unsupported or invalid data format: [{line}].")
|
||||
else:
|
||||
log(f"Unsupported or invalid line format: [{line}].")
|
||||
|
||||
return bouquets
|
||||
|
||||
@staticmethod
|
||||
def get_bouquet(path, bq_name, bq_type, prefix="userbouquet"):
|
||||
""" Parsing services ids from bouquet file. """
|
||||
with open(f"{path}{prefix}.{bq_name}.{bq_type}", encoding="utf-8", errors="replace") as file:
|
||||
chs_list = file.read()
|
||||
services = []
|
||||
srvs = list(filter(None, chs_list.split("\n#SERVICE"))) # filtering ['']
|
||||
# May come across empty[wrong] files!
|
||||
if not srvs:
|
||||
log(f"Bouquet file 'userbouquet.{bq_name}.{bq_type}' is empty or wrong!")
|
||||
return f"{bq_name} [empty]", services
|
||||
|
||||
bq_name = srvs.pop(0)
|
||||
|
||||
for num, srv in enumerate(srvs, start=1):
|
||||
srv_data = srv.strip().split(":")
|
||||
data_len = len(srv_data)
|
||||
if data_len < 10:
|
||||
log(f"The bouquet [{bq_name}] service [{num}] has the wrong data format: [{srv}]")
|
||||
continue
|
||||
|
||||
s_type = ServiceType(srv_data[1])
|
||||
if s_type is ServiceType.MARKER:
|
||||
m_data, sep, desc = srv.partition("#DESCRIPTION")
|
||||
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.MARKER, srv, num))
|
||||
elif s_type is ServiceType.SPACE:
|
||||
m_data, sep, desc = srv.partition("#DESCRIPTION")
|
||||
services.append(BouquetService(desc.strip() if desc else "", BqServiceType.SPACE, srv, num))
|
||||
elif s_type is ServiceType.ALT:
|
||||
alt = re.match(BouquetsReader._ALT_PAT, srv)
|
||||
if alt:
|
||||
alt_name, alt_type = alt.group(1), alt.group(2)
|
||||
alt_bq_name, alt_srvs = BouquetsReader.get_bouquet(path, alt_name, alt_type, "alternatives")
|
||||
services.append(BouquetService(alt_bq_name, BqServiceType.ALT, alt_name, tuple(alt_srvs)))
|
||||
elif s_type is ServiceType.BOUQUET:
|
||||
sub = re.match(BouquetsReader._SUB_BQ_PAT, srv)
|
||||
if sub:
|
||||
sub_name, sub_type = sub.group(1), sub.group(2)
|
||||
sub_bq_name, sub_srvs = BouquetsReader.get_bouquet(path, sub_name, sub_type, "subbouquet")
|
||||
bq = Bouquet(sub_bq_name, sub_type, tuple(sub_srvs), None, None, sub_name)
|
||||
services.append(BouquetService(sub_bq_name, BqServiceType.BOUQUET, bq, num))
|
||||
elif srv_data[0].strip() in BouquetsReader._STREAM_TYPES or srv_data[10].startswith(("http", "rtsp")):
|
||||
stream_data, sep, desc = srv.partition("#DESCRIPTION")
|
||||
desc = desc.lstrip(":").strip() if desc else srv_data[-1].strip()
|
||||
services.append(BouquetService(desc, BqServiceType.IPTV, srv, num))
|
||||
else:
|
||||
fav_id = f"{srv_data[3]}:{srv_data[4]}:{srv_data[5]}:{srv_data[6]}"
|
||||
name = None
|
||||
if data_len == 12:
|
||||
name, sep, desc = str(srv_data[-1]).partition("\n#DESCRIPTION")
|
||||
services.append(BouquetService(name, BqServiceType.DEFAULT, fav_id.upper(), num))
|
||||
|
||||
return bq_name.lstrip("#NAME").strip(), services
|
||||
|
||||
|
||||
def to_bouquet_id(srv):
|
||||
""" Creates bouquet channel id. """
|
||||
data_type = srv.data_id
|
||||
if data_type and len(data_type) > 4:
|
||||
data_type = int(srv.data_id.split(":")[4])
|
||||
|
||||
return "{}:0:{:X}:{}:0:0:0:".format(1, data_type, srv.fav_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,141 +1,342 @@
|
||||
""" This module used for parsing lamedb file
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" This module used for parsing and write lamedb file """
|
||||
import re
|
||||
|
||||
Currently implemented only for satellite channels!!!
|
||||
Description of format taken from here: http://www.satsupreme.com/showthread.php/194074-Lamedb-format-explained
|
||||
"""
|
||||
from app.commons import log
|
||||
from app.ui import CODED_ICON, LOCKED_ICON, HIDE_ICON
|
||||
from app.ui.uicommons import CODED_ICON, LOCKED_ICON, HIDE_ICON
|
||||
from .blacklist import get_blacklist
|
||||
from ..ecommons import Service, POLARIZATION, SYSTEM, FEC, SERVICE_TYPE, FLAG
|
||||
from ..ecommons import Service, POLARIZATION, FEC, SERVICE_TYPE, Flag, T_FEC, TrType, FEC_DEFAULT, T_SYSTEM
|
||||
|
||||
_HEADER = "eDVB services /4/"
|
||||
_HEADER = "eDVB services /{}/"
|
||||
_SEP = ":" # separator
|
||||
_FILE_NAME = "lamedb"
|
||||
_END_LINE = "# File was created in DemonEditor.\n# ....Enjoy watching!....\n"
|
||||
|
||||
|
||||
def get_services(path):
|
||||
return parse(path)
|
||||
def get_services(path, format_version):
|
||||
return LameDbReader(path, format_version).parse()
|
||||
|
||||
|
||||
def write_services(path, services):
|
||||
lines = [_HEADER, "\ntransponders\n"]
|
||||
tr_lines = []
|
||||
services_lines = ["end\nservices\n"]
|
||||
tr_set = set()
|
||||
|
||||
for srv in services:
|
||||
data_id = str(srv.data_id).split(_SEP)
|
||||
tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3])
|
||||
if tr_id not in tr_set:
|
||||
transponder = "{}\n\t{}\n/\n".format(tr_id, srv.transponder)
|
||||
tr_lines.append(transponder)
|
||||
tr_set.add(tr_id)
|
||||
# Services
|
||||
services_lines.append("{}\n{}\n{}\n".format(srv.data_id, srv.service, srv.flags_cas))
|
||||
|
||||
tr_lines.sort()
|
||||
lines.extend(tr_lines)
|
||||
lines.extend(services_lines)
|
||||
lines.append("end\nFile was created in DemonEditor.\n....Enjoy watching!....\n")
|
||||
|
||||
with open(path + _FILE_NAME, "w") as file:
|
||||
file.writelines(lines)
|
||||
def write_services(path, services, format_version=4):
|
||||
LameDbWriter(path, services, format_version).write()
|
||||
|
||||
|
||||
def parse(path):
|
||||
""" Parsing lamedb """
|
||||
with open(path + _FILE_NAME, "r", encoding="utf-8", errors="replace") as file:
|
||||
try:
|
||||
data = str(file.read())
|
||||
except UnicodeDecodeError as e:
|
||||
log("lamedb parse error: " + str(e))
|
||||
else:
|
||||
transponders, sep, services = data.partition("transponders") # 1 step
|
||||
if not transponders.endswith("/4/\n"):
|
||||
msg = "lamedb parsing error: unsupported format.\n Only version 4 is supported!"
|
||||
log(msg)
|
||||
raise SyntaxError(msg)
|
||||
transponders, sep, services = services.partition("services") # 2 step
|
||||
services, sep, _ = services.partition("end") # 3 step
|
||||
class LameDbReader:
|
||||
""" Lamedb parser class.
|
||||
|
||||
return parse_services(services.split("\n"), transponders.split("/"), path)
|
||||
Reads and parses the Enigma2 lamedb[5] file.
|
||||
Supports versions 3, 4 and 5.
|
||||
"""
|
||||
__slots__ = ["_path", "_fmt"]
|
||||
|
||||
def __init__(self, path, fmt=4):
|
||||
self._path = path
|
||||
self._fmt = fmt
|
||||
|
||||
def parse(self):
|
||||
""" Parsing lamedb. """
|
||||
if self._fmt == 4:
|
||||
return self.parse_v4()
|
||||
elif self._fmt == 5:
|
||||
return self.parse_v5()
|
||||
raise SyntaxError("Unsupported version of the format.")
|
||||
|
||||
def parse_v3(self, services, transponders):
|
||||
""" Parsing version 3. """
|
||||
for t in transponders:
|
||||
tr = transponders[t].lower()
|
||||
tr_type = tr[0:1]
|
||||
if tr_type == "c":
|
||||
tr += ":0:0:0"
|
||||
elif tr_type == "t" or tr_type == "a":
|
||||
tr += ":0:0"
|
||||
else:
|
||||
tr_data = tr.split(_SEP)
|
||||
len_data = len(tr_data)
|
||||
if len_data == 6:
|
||||
tr_data.append("0")
|
||||
elif len_data == 9:
|
||||
tr_data.insert(6, "0")
|
||||
tr_data.append("0")
|
||||
tr_data.append("2")
|
||||
|
||||
tr = _SEP.join(tr_data)
|
||||
|
||||
transponders[t] = tr
|
||||
|
||||
return self.parse_services(services, transponders)
|
||||
|
||||
def parse_v4(self):
|
||||
""" Parsing version 4. """
|
||||
with open(self._path + _FILE_NAME, "r", encoding="utf-8", errors="replace") as file:
|
||||
try:
|
||||
data = str(file.read())
|
||||
except UnicodeDecodeError as e:
|
||||
log(f"lamedb parse error: {e}")
|
||||
else:
|
||||
return self.get_services_list(data)
|
||||
|
||||
def parse_v5(self):
|
||||
""" Parsing version 5. """
|
||||
with open(self._path + "lamedb5", "r", encoding="utf-8", errors="replace") as file:
|
||||
lns = file.readlines()
|
||||
|
||||
if lns and not lns[0].endswith("/5/\n"):
|
||||
raise SyntaxError("lamedb ver.5 parsing error: unsupported format.")
|
||||
|
||||
trs, srvs = {}, [""]
|
||||
for line in lns:
|
||||
if line.startswith("s:"):
|
||||
srv_data = line.strip("s:").split(",", 2)
|
||||
srv_data[1] = srv_data[1].strip("\"\n")
|
||||
data_len = len(srv_data)
|
||||
if data_len == 3:
|
||||
srv_data[2] = srv_data[2].strip()
|
||||
elif data_len == 2:
|
||||
srv_data.append("p:")
|
||||
srvs.extend(srv_data)
|
||||
elif line.startswith("t:"):
|
||||
data = line.split(",")
|
||||
len_data = len(data)
|
||||
if len_data > 1:
|
||||
tr, srv = data[0].strip("t:"), data[1].strip().replace(":", " ", 1)
|
||||
trs[tr] = srv
|
||||
else:
|
||||
log(f"Error while parsing transponder data [ver. 5] for line: {line}")
|
||||
|
||||
return self.parse_services(srvs, trs)
|
||||
|
||||
def parse_services(self, services, transponders):
|
||||
""" Parsing services. """
|
||||
services_list = []
|
||||
blacklist = get_blacklist(self._path) if self._path else {}
|
||||
srvs = self.split(services, 3)
|
||||
if srvs[0][0] == "": # Remove first empty element.
|
||||
srvs.remove(srvs[0])
|
||||
|
||||
for srv in srvs:
|
||||
data_id = str(srv[0]).lower() # Lower is for lamedb ver.3.
|
||||
data = data_id.split(_SEP)
|
||||
sp = "0"
|
||||
tid = data[2]
|
||||
nid = data[3]
|
||||
# For lamedb ver.3
|
||||
is_v3 = False
|
||||
if len(tid) < 4:
|
||||
is_v3 = True
|
||||
tid = f"{tid:0>4}"
|
||||
data[2] = tid
|
||||
if len(nid) < 4:
|
||||
is_v3 = True
|
||||
nid = f"{nid:0>4}"
|
||||
data[3] = nid
|
||||
if is_v3:
|
||||
data[0] = f"{data[0]:0>4}"
|
||||
data_id = _SEP.join(data)
|
||||
|
||||
srv_type = int(data[4])
|
||||
transponder_id = f"{data[1]}:{tid}:{nid}"
|
||||
transponder = transponders.get(transponder_id, None)
|
||||
# The tid and nid values can be 0.
|
||||
tid = tid.lstrip(sp).upper() or "0"
|
||||
nid = nid.lstrip(sp).upper() or "0"
|
||||
ssid = str(data[0]).lstrip(sp).upper()
|
||||
onid = str(data[1]).lstrip(sp).upper()
|
||||
# For comparison in bouquets. Needed in upper case!!!
|
||||
fav_id = f"{ssid}:{tid}:{nid}:{onid}"
|
||||
picon_id = f"1_0_{srv_type:X}_{ssid}_{tid}_{nid}_{onid}_0_0_0.png"
|
||||
s_id = f"1:0:{srv_type:X}:{ssid}:{tid}:{nid}:{onid}:0:0:0:"
|
||||
|
||||
all_flags = srv[2].split(",")
|
||||
coded = CODED_ICON if list(filter(lambda x: x.startswith("C:"), all_flags)) else None
|
||||
flags = list(filter(lambda x: x.startswith("f:"), all_flags))
|
||||
hide = HIDE_ICON if flags and Flag.is_hide(Flag.parse(flags[0])) else None
|
||||
locked = LOCKED_ICON if s_id in blacklist else None
|
||||
|
||||
package = list(filter(lambda x: x.startswith("p:"), all_flags))
|
||||
package = package[0][2:] if package else ""
|
||||
|
||||
if transponder is not None:
|
||||
tr_type, sp, tr = str(transponder).partition(" ")
|
||||
tr_type = TrType(tr_type)
|
||||
tr = tr.split(_SEP)
|
||||
service_type = SERVICE_TYPE.get(data[4], SERVICE_TYPE["-2"])
|
||||
# Removing all non-printable symbols!
|
||||
srv_name = "".join(c for c in srv[1] if c.isprintable())
|
||||
freq = tr[0]
|
||||
rate = tr[1]
|
||||
pol = None
|
||||
fec = None
|
||||
system = None
|
||||
pos = None
|
||||
|
||||
if tr_type is TrType.Satellite:
|
||||
pol = POLARIZATION.get(tr[2], None)
|
||||
fec = FEC.get(tr[3], None)
|
||||
system = "DVB-S2" if len(tr) > 7 else "DVB-S"
|
||||
pos = tr[4]
|
||||
if tr_type is TrType.Terrestrial:
|
||||
system = T_SYSTEM.get(tr[10] if len(tr) > 10 else "0", None)
|
||||
pos = "T"
|
||||
fec = T_FEC.get(tr[3], None)
|
||||
elif tr_type is TrType.Cable:
|
||||
system = "DVB-C"
|
||||
pos = "C"
|
||||
fec = FEC_DEFAULT.get(tr[4])
|
||||
elif tr_type is TrType.ATSC:
|
||||
system = "ATSC"
|
||||
pos = "T"
|
||||
fec = FEC_DEFAULT.get("0")
|
||||
|
||||
# Formatting displayed values.
|
||||
try:
|
||||
freq = f"{int(freq) // 1000}"
|
||||
rate = f"{int(rate) // 1000}"
|
||||
if tr_type is TrType.Satellite:
|
||||
pos = int(pos)
|
||||
pos = f"{abs(pos / 10):0.1f}{'W' if pos < 0 else 'E'}"
|
||||
except ValueError as e:
|
||||
log(f"Parse error [parse_services]: {e}")
|
||||
|
||||
s = Service(srv[2], tr_type.value, coded, srv_name, locked, hide, package, service_type, None,
|
||||
picon_id, data[0], freq, rate, pol, fec, system, pos, data_id, fav_id, transponder)
|
||||
|
||||
services_list.append(s)
|
||||
return services_list
|
||||
|
||||
def get_services_list(self, data):
|
||||
""" Returns a list of services from a string data representation. """
|
||||
transponders, sep, services = data.partition("transponders") # 1 step
|
||||
pattern = re.compile("/[34]/$")
|
||||
match = re.search(pattern, transponders)
|
||||
if not match:
|
||||
msg = "lamedb parsing error: unsupported format."
|
||||
log(msg)
|
||||
raise SyntaxError(msg)
|
||||
|
||||
transponders, sep, services = services.partition("services") # 2 step
|
||||
services, sep, _ = services.partition("\nend") # 3 step
|
||||
|
||||
if match.group() == "/3/":
|
||||
return self.parse_v3(services.split("\n"), self.parse_transponders(transponders.split("/")))
|
||||
|
||||
return self.parse_services(services.split("\n"), self.parse_transponders(transponders.split("/")))
|
||||
|
||||
@staticmethod
|
||||
def get_services_lines(services):
|
||||
""" Returns a list of strings from services for lamedb [v.4]. """
|
||||
lines = [_HEADER.format(4), "\ntransponders\n"]
|
||||
tr_lines = []
|
||||
services_lines = ["end\nservices\n"]
|
||||
tr_set = set()
|
||||
for srv in services:
|
||||
data_id = str(srv.data_id).split(_SEP)
|
||||
tr_id = f"{data_id[1]}:{data_id[2]}:{data_id[3]}"
|
||||
if tr_id not in tr_set:
|
||||
tr_lines.append(f"{tr_id}\n\t{srv.transponder}\n/\n")
|
||||
tr_set.add(tr_id)
|
||||
# Services
|
||||
services_lines.append(f"{srv.data_id}\n{srv.service}\n{srv.flags_cas}\n")
|
||||
|
||||
tr_lines.sort()
|
||||
lines.extend(tr_lines)
|
||||
lines.extend(services_lines)
|
||||
lines.append(f"end\n{_END_LINE}")
|
||||
|
||||
return lines
|
||||
|
||||
def parse_transponders(self, arg):
|
||||
""" Parsing transponders. """
|
||||
transponders = {}
|
||||
for ar in arg:
|
||||
tr = ar.replace("\n", "").split("\t")
|
||||
if len(tr) == 2:
|
||||
transponders[tr[0]] = tr[1]
|
||||
|
||||
return transponders
|
||||
|
||||
def split(self, itr, size):
|
||||
""" Divide the iterable. """
|
||||
srv = []
|
||||
tmp = []
|
||||
for i, line in enumerate(itr):
|
||||
tmp.append(line)
|
||||
if i % size == 0:
|
||||
srv.append(tuple(tmp))
|
||||
tmp.clear()
|
||||
|
||||
return srv
|
||||
|
||||
|
||||
def parse_transponders(arg):
|
||||
""" Parsing transponders """
|
||||
transponders = {}
|
||||
for ar in arg:
|
||||
tr = ar.replace("\n", "").split("\t")
|
||||
if len(tr) == 2:
|
||||
transponders[tr[0]] = tr[1]
|
||||
class LameDbWriter:
|
||||
""" Writes the Enigma2 lamedb[5] file.
|
||||
|
||||
return transponders
|
||||
Version 4 will be used instead of version 3!
|
||||
"""
|
||||
__slots__ = ["_path", "_fmt", "_services"]
|
||||
|
||||
def __init__(self, path, services, fmt=4):
|
||||
self._path = path
|
||||
self._fmt = fmt
|
||||
self._services = services
|
||||
|
||||
def parse_services(services, transponders, path):
|
||||
""" Parsing channels """
|
||||
channels = []
|
||||
transponders = parse_transponders(transponders)
|
||||
blacklist = str(get_blacklist(path))
|
||||
def write(self):
|
||||
if self._fmt == 4:
|
||||
# Writing lamedb file ver.4
|
||||
with open(self._path + _FILE_NAME, "w", encoding="utf-8", newline="\n") as file:
|
||||
file.writelines(LameDbReader.get_services_lines(self._services))
|
||||
elif self._fmt == 5:
|
||||
self.write_to_lamedb5()
|
||||
|
||||
srv = split(services, 3)
|
||||
if srv[0][0] == "": # remove first empty element
|
||||
srv.remove(srv[0])
|
||||
def write_to_lamedb5(self):
|
||||
""" Writing lamedb5 file. """
|
||||
lines = [_HEADER.format(5) + "\n"]
|
||||
services_lines = []
|
||||
tr_set = set()
|
||||
|
||||
for ch in srv:
|
||||
data = str(ch[0]).split(_SEP)
|
||||
sp = "0"
|
||||
# For comparison in bouquets. Needed in upper case!!!
|
||||
fav_id = "{}:{}:{}:{}".format(str(data[0]).lstrip(sp), str(data[2]).lstrip(sp),
|
||||
str(data[3]).lstrip(sp), str(data[1]).lstrip(sp)).upper()
|
||||
all_flags = ch[2].split(",")
|
||||
coded = CODED_ICON if list(filter(lambda x: x.startswith("C:"), all_flags)) else None
|
||||
flags = list(filter(lambda x: x.startswith("f:"), all_flags))
|
||||
hide = HIDE_ICON if flags and int(flags[0][2:]) in FLAG.hide_values() else None
|
||||
locked = LOCKED_ICON if fav_id in blacklist else None
|
||||
for srv in self._services:
|
||||
data_id = str(srv.data_id).split(_SEP)
|
||||
tr_id = f"{data_id[1]}:{data_id[2]}:{data_id[3]}"
|
||||
tr_set.add(f"t:{tr_id},{srv.transponder.replace(' ', ':', 1)}\n")
|
||||
# Removing empty packages
|
||||
flags = list(filter(lambda x: x != "p:", srv.flags_cas.split(",")))
|
||||
flags = ",".join(flags)
|
||||
flags = "," + flags if flags else ""
|
||||
services_lines.append(f"s:{srv.data_id},\"{srv.service}\"{flags}\n")
|
||||
|
||||
package = list(filter(lambda x: x.startswith("p:"), all_flags))
|
||||
package = package[0][2:] if package else None
|
||||
lines.extend(sorted(tr_set))
|
||||
lines.extend(services_lines)
|
||||
lines.append(_END_LINE)
|
||||
|
||||
transponder_id = "{}:{}:{}".format(data[1], data[2], data[3])
|
||||
transponder = transponders.get(transponder_id, None)
|
||||
|
||||
if transponder is not None:
|
||||
tr_type, sp, tr = str(transponder).partition(" ")
|
||||
tr = tr.split(_SEP)
|
||||
service_type = SERVICE_TYPE.get(data[4], SERVICE_TYPE["-2"])
|
||||
channels.append(Service(flags_cas=ch[2],
|
||||
transponder_type=tr_type,
|
||||
coded=coded,
|
||||
service=ch[1],
|
||||
locked=locked,
|
||||
hide=hide,
|
||||
package=package,
|
||||
service_type=service_type,
|
||||
ssid=data[0],
|
||||
freq=tr[0],
|
||||
rate=tr[1],
|
||||
pol=POLARIZATION[tr[2]],
|
||||
fec=FEC[tr[3]],
|
||||
system=SYSTEM[tr[6]],
|
||||
pos="{}.{}".format(tr[4][:-1], tr[4][-1:]),
|
||||
data_id=ch[0],
|
||||
fav_id=fav_id,
|
||||
transponder=transponder))
|
||||
return channels
|
||||
|
||||
|
||||
def split(itr, size):
|
||||
""" Divide the iterable. """
|
||||
srv = []
|
||||
tmp = []
|
||||
for i, line in enumerate(itr):
|
||||
tmp.append(line)
|
||||
if i % size == 0:
|
||||
srv.append(tuple(tmp))
|
||||
tmp.clear()
|
||||
|
||||
return srv
|
||||
with open(self._path + "lamedb5", "w", encoding="utf-8", newline="\n") as file:
|
||||
file.writelines(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,23 +1,172 @@
|
||||
from .ecommons import BqServiceType, Service
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
def parse_m3u(path):
|
||||
with open(path) as file:
|
||||
aggr = [None] * 8
|
||||
channels = []
|
||||
count = 0
|
||||
""" Module for IPTV and streams support """
|
||||
import re
|
||||
from enum import Enum
|
||||
from urllib.parse import unquote, quote
|
||||
|
||||
from app.commons import log
|
||||
from app.eparser.ecommons import BqServiceType, Service
|
||||
from app.settings import SettingsType
|
||||
from app.ui.uicommons import IPTV_ICON
|
||||
|
||||
# url, description, urlkey, account, usrname, psw, s_type, iconsrc, iconsrc_b, group
|
||||
NEUTRINO_FAV_ID_FORMAT = "{}::{}::{}::{}::{}::{}::{}::{}::{}::{}"
|
||||
ENIGMA2_FAV_ID_FORMAT = " {}:{}:{}:{:X}:{:X}:{:X}:{:X}:0:0:0:{}:{}\n#DESCRIPTION: {}\n"
|
||||
MARKER_FORMAT = " 1:64:{}:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n"
|
||||
PICON_FORMAT = "{}_{}_{:X}_{:X}_{:X}_{:X}_{:X}_0_0_0.png"
|
||||
|
||||
|
||||
class StreamType(Enum):
|
||||
DVB_TS = "1"
|
||||
NONE_TS = "4097"
|
||||
NONE_REC_1 = "5001"
|
||||
NONE_REC_2 = "5002"
|
||||
E_SERVICE_URI = "8193"
|
||||
E_SERVICE_HLS = "8739"
|
||||
UNKNOWN = "0"
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.UNKNOWN
|
||||
|
||||
|
||||
def parse_m3u(path, s_type, detect_encoding=True, params=None):
|
||||
with open(path, "rb") as file:
|
||||
data = file.read()
|
||||
encoding = "utf-8"
|
||||
|
||||
if detect_encoding:
|
||||
try:
|
||||
import chardet
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
else:
|
||||
enc = chardet.detect(data)
|
||||
encoding = enc.get("encoding", "utf-8")
|
||||
|
||||
aggr = [None] * 10
|
||||
s_aggr = aggr[: -3]
|
||||
services = []
|
||||
groups = set()
|
||||
marker_counter = 1
|
||||
sid_counter = 1
|
||||
name = None
|
||||
for line in file.readlines():
|
||||
if line.startswith("#EXTINF"):
|
||||
name = line[1 + line.index(","):].strip()
|
||||
count += 1
|
||||
elif count == 1:
|
||||
count = 0
|
||||
fav_id = " 1:0:1:0:0:0:0:0:0:0:{}:{}\n#DESCRIPTION: {}\n".format(
|
||||
line.strip().replace(":", "%3a"), name, name, None)
|
||||
channels.append(Service(*aggr[0:3], name, *aggr[0:3], BqServiceType.IPTV.name, *aggr, fav_id, None))
|
||||
picon = None
|
||||
p_id = "1_0_1_0_0_0_0_0_0_0.png"
|
||||
st = BqServiceType.IPTV.name
|
||||
params = params or [0, 0, 0, 0]
|
||||
m_name = BqServiceType.MARKER.name
|
||||
|
||||
return channels
|
||||
for line in str(data, encoding=encoding, errors="ignore").splitlines():
|
||||
if line.startswith("#EXTINF"):
|
||||
line, sep, name = line.rpartition(",")
|
||||
|
||||
data = re.split('"', line)
|
||||
size = len(data)
|
||||
if size < 3:
|
||||
continue
|
||||
d = {data[i].lower().strip(" ="): data[i + 1] for i in range(0, len(data) - 1, 2)}
|
||||
picon = d.get("tvg-logo", None)
|
||||
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
grp_name = d.get("group-title", None)
|
||||
if grp_name not in groups:
|
||||
groups.add(grp_name)
|
||||
fav_id = MARKER_FORMAT.format(marker_counter, grp_name, grp_name)
|
||||
marker_counter += 1
|
||||
mr = Service(None, None, None, grp_name, *aggr[0:3], m_name, *aggr, fav_id, None)
|
||||
services.append(mr)
|
||||
elif line.startswith("#EXTGRP") and s_type is SettingsType.ENIGMA_2:
|
||||
grp_name = line.strip("#EXTGRP:").strip()
|
||||
if grp_name not in groups:
|
||||
groups.add(grp_name)
|
||||
fav_id = MARKER_FORMAT.format(marker_counter, grp_name, grp_name)
|
||||
marker_counter += 1
|
||||
mr = Service(None, None, None, grp_name, *aggr[0:3], m_name, *aggr, fav_id, None)
|
||||
services.append(mr)
|
||||
elif not line.startswith("#"):
|
||||
url = line.strip()
|
||||
params[0] = sid_counter
|
||||
sid_counter += 1
|
||||
fav_id = get_fav_id(url, name, s_type, params)
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
p_id = get_picon_id(params)
|
||||
|
||||
if all((name, url, fav_id)):
|
||||
srv = Service(None, None, IPTV_ICON, name, *aggr[0:3], st, picon, p_id, *s_aggr, url, fav_id, None)
|
||||
services.append(srv)
|
||||
else:
|
||||
log(f"*.m3u* parse error ['{path}']: name[{name}], url[{url}], fav id[{fav_id}]")
|
||||
|
||||
return services
|
||||
|
||||
|
||||
def export_to_m3u(path, bouquet, s_type, url=None):
|
||||
pattern = re.compile(".*:(http.*):.*") if s_type is SettingsType.ENIGMA_2 else re.compile("(http.*?)::::.*")
|
||||
lines = ["#EXTM3U\n"]
|
||||
current_grp = None
|
||||
|
||||
for s in bouquet.services:
|
||||
s_type = s.type
|
||||
if s_type is BqServiceType.IPTV:
|
||||
res = re.match(pattern, s.data)
|
||||
if not res:
|
||||
continue
|
||||
lines.append(f"#EXTINF:-1,{s.name}\n")
|
||||
lines.append(current_grp) if current_grp else None
|
||||
lines.append(f"{unquote(res.group(1).strip())}\n")
|
||||
elif s_type is BqServiceType.MARKER:
|
||||
current_grp = f"#EXTGRP:{s.name}\n"
|
||||
elif s_type is BqServiceType.DEFAULT and url:
|
||||
lines.append(f"#EXTINF:-1,{s.name}\n")
|
||||
lines.append(current_grp) if current_grp else None
|
||||
lines.append(f"{url}{s.data}\n")
|
||||
|
||||
with open(f"{path}{bouquet.name}.m3u", "w", encoding="utf-8") as file:
|
||||
file.writelines(lines)
|
||||
|
||||
|
||||
def get_fav_id(url, name, settings_type, params=None, st_type=None, s_id=0, srv_type=1):
|
||||
""" Returns fav id depending on the profile. """
|
||||
if settings_type is SettingsType.ENIGMA_2:
|
||||
st_type = st_type or StreamType.NONE_TS.value
|
||||
params = params or (0, 0, 0, 0)
|
||||
return ENIGMA2_FAV_ID_FORMAT.format(st_type, s_id, srv_type, *params, quote(url), name, name, None)
|
||||
elif settings_type is SettingsType.NEUTRINO_MP:
|
||||
return NEUTRINO_FAV_ID_FORMAT.format(url, "", 0, None, None, None, None, "", "", 1)
|
||||
|
||||
|
||||
def get_picon_id(params=None, st_type=None, s_id=0, srv_type=1):
|
||||
st_type = st_type or StreamType.NONE_TS.value
|
||||
params = params or (0, 0, 0, 0)
|
||||
return PICON_FORMAT.format(st_type, s_id, srv_type, *params)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
SP = "_:::_"
|
||||
KSP = "_::_"
|
||||
API_VER = "4"
|
||||
|
||||
|
||||
def get_attributes(data):
|
||||
return {el[0]: el[1] for el in (e.split(KSP) for e in data.split(SP))}
|
||||
|
||||
|
||||
def get_xml_attributes(attr):
|
||||
attrs = attr.attributes
|
||||
return {t: attrs[t].value for t in attrs.keys()}
|
||||
|
||||
@@ -1,23 +1,50 @@
|
||||
import os
|
||||
from contextlib import suppress
|
||||
from enum import Enum
|
||||
from xml.dom.minidom import parse, Document
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
from app.ui import LOCKED_ICON, HIDE_ICON
|
||||
from ..ecommons import Bouquets, Bouquet, BouquetService, BqServiceType, PROVIDER
|
||||
|
||||
import os
|
||||
|
||||
from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT
|
||||
from app.eparser.neutrino import KSP, SP, get_xml_attributes, get_attributes, API_VER
|
||||
from app.eparser.neutrino.nxml import XmlHandler, NeutrinoDocument
|
||||
from app.ui.uicommons import LOCKED_ICON, HIDE_ICON
|
||||
from ..ecommons import Bouquets, Bouquet, BouquetService, BqServiceType, PROVIDER, BqType
|
||||
|
||||
_FILE = "bouquets.xml"
|
||||
_U_FILE = "ubouquets.xml"
|
||||
_W_FILE = "webtv.xml"
|
||||
|
||||
|
||||
class BqType(Enum):
|
||||
BOUQUET = "bouquet"
|
||||
TV = "tv"
|
||||
_COMMENT = " File was created in DemonEditor. Enjoy watching! "
|
||||
|
||||
|
||||
def get_bouquets(path):
|
||||
return (parse_bouquets(path + _FILE, "Providers", BqType.BOUQUET.value),
|
||||
parse_bouquets(path + _U_FILE, "FAV", BqType.TV.value))
|
||||
parse_bouquets(path + _U_FILE, "FAV", BqType.TV.value),
|
||||
parse_webtv(path + _W_FILE, "WEBTV", BqType.WEBTV.value))
|
||||
|
||||
|
||||
def parse_bouquets(file, name, bq_type):
|
||||
@@ -25,66 +52,105 @@ def parse_bouquets(file, name, bq_type):
|
||||
if not os.path.exists(file):
|
||||
return bouquets
|
||||
|
||||
dom = parse(file)
|
||||
dom = XmlHandler.parse(file)
|
||||
|
||||
for elem in dom.getElementsByTagName("Bouquet"):
|
||||
if elem.hasAttributes():
|
||||
bq_name = elem.attributes["name"].value
|
||||
hidden = elem.attributes.get("hidden")
|
||||
hidden = hidden.value if hidden else hidden
|
||||
locked = elem.attributes.get("locked")
|
||||
locked = locked.value if locked else locked
|
||||
# epg = elem.attributes["epg"].value
|
||||
bq_attrs = get_xml_attributes(elem)
|
||||
bq_name = bq_attrs.get("name", "")
|
||||
hidden = bq_attrs.get("hidden", "0")
|
||||
locked = bq_attrs.get("locked", "0")
|
||||
services = []
|
||||
for srv_elem in elem.getElementsByTagName("S"):
|
||||
if srv_elem.hasAttributes():
|
||||
ssid = srv_elem.attributes["i"].value
|
||||
on = srv_elem.attributes["on"].value
|
||||
tr_id = srv_elem.attributes["t"].value
|
||||
s_attrs = get_xml_attributes(srv_elem)
|
||||
ssid = s_attrs.get("i", "0")
|
||||
on = s_attrs.get("on", "0")
|
||||
tr_id = s_attrs.get("t", "0")
|
||||
fav_id = "{}:{}:{}".format(tr_id, on, ssid)
|
||||
services.append(BouquetService(None, BqServiceType.DEFAULT, fav_id, 0))
|
||||
bouquets[2].append(Bouquet(name=bq_name,
|
||||
type=bq_type,
|
||||
services=services,
|
||||
locked=LOCKED_ICON if locked == "1" else None,
|
||||
hidden=HIDE_ICON if hidden == "1" else None))
|
||||
hidden=HIDE_ICON if hidden == "1" else None,
|
||||
file=SP.join("{}{}{}".format(k, KSP, v) for k, v in bq_attrs.items())))
|
||||
|
||||
if BqType(bq_type) is BqType.BOUQUET:
|
||||
for bq in bouquets.bouquets:
|
||||
if bq.services:
|
||||
name = bq.name
|
||||
name = name[name.index("]") + 1:]
|
||||
key = int(bq.services[0].data.split(":")[1], 16)
|
||||
if key not in PROVIDER:
|
||||
pos, sep, name = bq.name.partition("]")
|
||||
PROVIDER[key] = name
|
||||
|
||||
return bouquets
|
||||
|
||||
|
||||
def write_bouquets(path, bouquets):
|
||||
if len(bouquets) < 2:
|
||||
for f in path + _FILE, path + _U_FILE:
|
||||
with suppress(FileNotFoundError):
|
||||
os.remove(f)
|
||||
def parse_webtv(path, name, bq_type):
|
||||
bouquets = Bouquets(name=name, type=bq_type, bouquets=[])
|
||||
if not os.path.exists(path):
|
||||
return bouquets
|
||||
|
||||
dom = XmlHandler.parse(path)
|
||||
services = []
|
||||
for elem in dom.getElementsByTagName("webtv"):
|
||||
if elem.hasAttributes():
|
||||
web_attrs = get_xml_attributes(elem)
|
||||
title = web_attrs.get("title", "")
|
||||
url = web_attrs.get("url", "")
|
||||
description = web_attrs.get("description", "")
|
||||
urlkey = web_attrs.get("urlkey", None)
|
||||
account = web_attrs.get("account", None)
|
||||
usrname = web_attrs.get("usrname", None)
|
||||
psw = web_attrs.get("psw", None)
|
||||
s_type = web_attrs.get("type", None)
|
||||
iconsrc = web_attrs.get("iconsrc", None)
|
||||
iconsrc_b = web_attrs.get("iconsrc_b", None)
|
||||
group = web_attrs.get("group", None)
|
||||
fav_id = NEUTRINO_FAV_ID_FORMAT.format(url, description, urlkey, account, usrname, psw, s_type, iconsrc,
|
||||
iconsrc_b, group)
|
||||
services.append(BouquetService(name=title, type=BqServiceType.IPTV, data=fav_id, num=0))
|
||||
|
||||
bouquet = Bouquet(name="default", type=bq_type, services=services, locked=None, hidden=None, file=None)
|
||||
bouquets[2].append(bouquet)
|
||||
|
||||
return bouquets
|
||||
|
||||
|
||||
def write_bouquets(path, bouquets):
|
||||
for bq in bouquets:
|
||||
bq_type = BqType(bq.type)
|
||||
write_bouquet(path + (_FILE if bq_type is BqType.BOUQUET else _U_FILE), bq)
|
||||
if bq_type is BqType.WEBTV:
|
||||
write_webtv(path + _W_FILE, bq)
|
||||
else:
|
||||
write_bouquet(path + (_FILE if bq_type is BqType.BOUQUET else _U_FILE), bq)
|
||||
|
||||
|
||||
def write_bouquet(file, bouquet):
|
||||
doc = Document()
|
||||
doc = NeutrinoDocument()
|
||||
root = doc.createElement("zapit")
|
||||
root.setAttribute("api", API_VER)
|
||||
doc.appendChild(root)
|
||||
comment = doc.createComment(" File was created in DemonEditor. Enjoy watching! ")
|
||||
comment = doc.createComment(_COMMENT)
|
||||
doc.appendChild(comment)
|
||||
|
||||
for bq in bouquet.bouquets:
|
||||
attrs = get_attributes(bq.file) if bq.file else {}
|
||||
attrs["name"] = bq.name
|
||||
if bq.hidden:
|
||||
attrs["hidden"] = "1"
|
||||
else:
|
||||
attrs.pop("hidden", None)
|
||||
if bq.locked:
|
||||
attrs["locked"] = "1"
|
||||
else:
|
||||
attrs.pop("locked", None)
|
||||
|
||||
bq_elem = doc.createElement("Bouquet")
|
||||
bq_elem.setAttribute("name", bq.name)
|
||||
bq_elem.setAttribute("hidden", "1" if bq.hidden else "0")
|
||||
bq_elem.setAttribute("locked", "1" if bq.locked else "0")
|
||||
bq_elem.setAttribute("epg", "0")
|
||||
for k, v in attrs.items():
|
||||
bq_elem.setAttribute(k, v)
|
||||
|
||||
root.appendChild(bq_elem)
|
||||
|
||||
for srv in bq.services:
|
||||
@@ -94,12 +160,49 @@ def write_bouquet(file, bouquet):
|
||||
srv_elem.setAttribute("n", srv.service)
|
||||
srv_elem.setAttribute("t", tr_id)
|
||||
srv_elem.setAttribute("on", on)
|
||||
srv_elem.setAttribute("s", srv.pos.replace(".", ""))
|
||||
srv_elem.setAttribute("frq", srv.freq[:-3])
|
||||
srv_elem.setAttribute("l", "0") # temporary !!!
|
||||
srv_elem.setAttribute("frq", srv.freq)
|
||||
srv_elem.setAttribute("s", get_attributes(srv.flags_cas).get("position", "0"))
|
||||
bq_elem.appendChild(srv_elem)
|
||||
|
||||
doc.writexml(open(file, "w"), addindent=" ", newl="\n", encoding="UTF-8")
|
||||
doc.write_xml(file)
|
||||
|
||||
|
||||
def write_webtv(file, bouquet):
|
||||
doc = NeutrinoDocument()
|
||||
root = doc.createElement("webtvs")
|
||||
doc.appendChild(root)
|
||||
comment = doc.createComment(_COMMENT)
|
||||
doc.appendChild(comment)
|
||||
|
||||
for bq in bouquet.bouquets:
|
||||
for srv in bq.services:
|
||||
url, description, urlkey, account, usrname, psw, s_type, iconsrc, iconsrc_b, group = srv.fav_id.split("::")
|
||||
srv_elem = doc.createElement("webtv")
|
||||
srv_elem.setAttribute("title", srv.service)
|
||||
srv_elem.setAttribute("url", url)
|
||||
|
||||
if description != "None":
|
||||
srv_elem.setAttribute("description", description)
|
||||
if urlkey != "None":
|
||||
srv_elem.setAttribute("urlkey", urlkey)
|
||||
if account != "None":
|
||||
srv_elem.setAttribute("account", account)
|
||||
if usrname != "None":
|
||||
srv_elem.setAttribute("usrname", usrname)
|
||||
if psw != "None":
|
||||
srv_elem.setAttribute("psw", psw)
|
||||
if s_type != "None":
|
||||
srv_elem.setAttribute("type", s_type)
|
||||
if iconsrc != "None":
|
||||
srv_elem.setAttribute("iconsrc", iconsrc)
|
||||
if iconsrc_b != "None":
|
||||
srv_elem.setAttribute("iconsrc_b", iconsrc_b)
|
||||
if group != "None":
|
||||
srv_elem.setAttribute("group", group)
|
||||
|
||||
root.appendChild(srv_elem)
|
||||
|
||||
doc.write_xml(file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
114
app/eparser/neutrino/nxml.py
Normal file
114
app/eparser/neutrino/nxml.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Additional module for working with Neutrino xml files. """
|
||||
import re
|
||||
from xml.dom.minidom import parseString, Document, Element, Node
|
||||
from xml.parsers.expat import ExpatError
|
||||
|
||||
from app.commons import log
|
||||
|
||||
|
||||
class XmlHandler:
|
||||
""" Utility class for handling Neutrino xml files. """
|
||||
__slots__ = ()
|
||||
|
||||
ERROR_MESSAGE = "The file [{}] is not formatted correctly or contains invalid characters! Cause: {}"
|
||||
|
||||
@staticmethod
|
||||
def parse(path):
|
||||
""" Parses a file into the DOM by filename. """
|
||||
try:
|
||||
return parseString(open(path, "r", encoding="utf-8", errors="ignore").read())
|
||||
except ExpatError as e:
|
||||
# Some neutrino configuration files may contain text data with invalid character ['&'].
|
||||
# https://www.w3.org/TR/xml/#syntax
|
||||
# Apparently there is an error in Neutrino itself and the document is not initially formed correctly.
|
||||
log(XmlHandler.ERROR_MESSAGE.format(path, e))
|
||||
|
||||
return XmlHandler.preprocess(path)
|
||||
|
||||
@staticmethod
|
||||
def preprocess(path):
|
||||
""" Pre-processing xml [for '&' symbol] for correct parsing. """
|
||||
with open(path, "r", encoding="utf-8", errors="ignore") as f:
|
||||
pat = re.compile("&([^;\\W]*([^;\\w]|$))")
|
||||
log("Processing the file '{}'...".format(path))
|
||||
try:
|
||||
dom = parseString(re.sub(pat, "&", f.read()))
|
||||
except ExpatError as e:
|
||||
msg = XmlHandler.ERROR_MESSAGE.format(path, e)
|
||||
log(msg)
|
||||
raise ValueError(e)
|
||||
else:
|
||||
log("Done!")
|
||||
return dom
|
||||
|
||||
|
||||
class NeutrinoDocument(Document):
|
||||
|
||||
def createElement(self, tag_name):
|
||||
e = NElement(tag_name)
|
||||
e.ownerDocument = self
|
||||
return e
|
||||
|
||||
def write_xml(self, path):
|
||||
self.writexml(open(path, "w", encoding="utf-8"), addindent=" ", newl="\n", encoding="UTF-8")
|
||||
|
||||
|
||||
class NElement(Element):
|
||||
|
||||
def writexml(self, writer, indent="", add_indent="", new_line=""):
|
||||
""" Overridden specifically for neutrino for more correct [' -> optional] xml attrs generation. """
|
||||
writer.write(indent + "<" + self.tagName)
|
||||
attrs = self._get_attributes()
|
||||
|
||||
for a_name in attrs.keys():
|
||||
writer.write(" %s=\"" % a_name)
|
||||
self.write_data(writer, attrs[a_name].value)
|
||||
writer.write("\"")
|
||||
if self.childNodes:
|
||||
writer.write(">")
|
||||
if len(self.childNodes) == 1 and self.childNodes[0].nodeType in (Node.TEXT_NODE, Node.CDATA_SECTION_NODE):
|
||||
self.childNodes[0].writexml(writer, '', '', '')
|
||||
else:
|
||||
writer.write(new_line)
|
||||
for node in self.childNodes:
|
||||
node.writexml(writer, indent + add_indent, add_indent, new_line)
|
||||
writer.write(indent)
|
||||
writer.write("</%s>%s" % (self.tagName, new_line))
|
||||
else:
|
||||
writer.write("/>%s" % new_line)
|
||||
|
||||
@staticmethod
|
||||
def write_data(writer, data):
|
||||
""" Writes data chars to writer."""
|
||||
if data:
|
||||
data = data.replace("&", "&").replace("<", "<").replace("\"", """).replace(">", ">")
|
||||
data = data.replace("'", "'")
|
||||
writer.write(data)
|
||||
@@ -1,167 +1,227 @@
|
||||
from xml.dom.minidom import parse, Document
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
from ..ecommons import Service, POLARIZATION, FEC, SYSTEM, SERVICE_TYPE, PROVIDER
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from app.commons import log
|
||||
from app.eparser.ecommons import (Service, POLARIZATION, FEC, SYSTEM, SERVICE_TYPE, PROVIDER, T_SYSTEM, TrType,
|
||||
SystemCable)
|
||||
from app.eparser.neutrino import get_xml_attributes, SP, KSP, get_attributes, API_VER
|
||||
from app.eparser.neutrino.nxml import XmlHandler, NeutrinoDocument
|
||||
|
||||
_FILE = "services.xml"
|
||||
_TR_ATTR_NAMES = ("id", "on", "frq", "inv", "sr", "fec", "pol", "mod", "sys") # transponder attributes
|
||||
_SRV_ATTR_NAMES = ("t", "s", "num", "f", "v", "a", "p", "pmt", "tx", "vt") # service attributes
|
||||
|
||||
|
||||
def write_services(path, services):
|
||||
doc = Document()
|
||||
root = doc.createElement("zapit")
|
||||
root.setAttribute("api", "4")
|
||||
doc.appendChild(root)
|
||||
comment = doc.createComment(" File was created in DemonEditor. Enjoy watching! ")
|
||||
doc.appendChild(comment)
|
||||
|
||||
sats = {}
|
||||
for srv in services:
|
||||
flag = srv[0]
|
||||
if flag in sats:
|
||||
sats.get(flag).append(srv)
|
||||
else:
|
||||
srv_list = [srv]
|
||||
sats[flag] = srv_list
|
||||
|
||||
for sat in sats:
|
||||
tr_atr = sat.split(":")
|
||||
sat_elem = doc.createElement("sat")
|
||||
sat_elem.setAttribute("name", tr_atr[0])
|
||||
sat_elem.setAttribute("position", tr_atr[1].replace(".", ""))
|
||||
sat_elem.setAttribute("diseqc", tr_atr[2])
|
||||
sat_elem.setAttribute("uncommited", tr_atr[3])
|
||||
root.appendChild(sat_elem)
|
||||
|
||||
transponers = {}
|
||||
for srv in sats.get(sat):
|
||||
flag = srv[-1]
|
||||
if flag in transponers:
|
||||
transponers.get(flag).append(srv)
|
||||
else:
|
||||
srv_list = [srv]
|
||||
transponers[flag] = srv_list
|
||||
|
||||
for tr in transponers:
|
||||
tr_elem = doc.createElement("TS")
|
||||
tr_atr = tr.split(":")
|
||||
for i, value in enumerate(tr_atr):
|
||||
if value == "None":
|
||||
continue
|
||||
tr_elem.setAttribute(_TR_ATTR_NAMES[i], value)
|
||||
sat_elem.appendChild(tr_elem)
|
||||
|
||||
for srv in transponers.get(tr):
|
||||
srv_elem = doc.createElement("S")
|
||||
srv_elem.setAttribute("i", srv[8])
|
||||
srv_elem.setAttribute("n", srv[3])
|
||||
|
||||
srv_attrs = srv.data_id.split(":")
|
||||
api = srv_attrs.pop(0)
|
||||
|
||||
if api == "3":
|
||||
root.setAttribute("api", "3") # !!!
|
||||
for i, value in enumerate(srv_attrs):
|
||||
if value == "None":
|
||||
continue
|
||||
srv_elem.setAttribute(_SRV_ATTR_NAMES[i], value)
|
||||
|
||||
tr_elem.appendChild(srv_elem)
|
||||
|
||||
doc.writexml(open(path + _FILE, "w"), addindent=" ", newl="\n", encoding="UTF-8")
|
||||
doc.unlink()
|
||||
NeutrinoServiceWriter(path, services).write()
|
||||
|
||||
|
||||
def get_services(path):
|
||||
return parse_services(path)
|
||||
return NeutrinoServicesReader(path).get_services()
|
||||
|
||||
|
||||
def parse_services(path):
|
||||
""" Parsing services from xml"""
|
||||
dom = parse(path + _FILE)
|
||||
services = []
|
||||
class NeutrinoServiceWriter:
|
||||
|
||||
for root in dom.getElementsByTagName("zapit"):
|
||||
api = root.attributes["api"].value
|
||||
def __init__(self, path, services):
|
||||
self._path = path + _FILE
|
||||
self._services = services
|
||||
|
||||
for elem in root.getElementsByTagName("sat"):
|
||||
if elem.hasAttributes():
|
||||
sat_name = elem.attributes["name"].value
|
||||
sat_pos = elem.attributes["position"].value
|
||||
sat_pos = "{}.{}".format(sat_pos[:-1], sat_pos[-1:])
|
||||
diseqc = elem.attributes.get("diseqc")
|
||||
diseqc = diseqc.value if diseqc else diseqc
|
||||
uncommited = elem.attributes.get("uncommited")
|
||||
uncommited = uncommited.value if uncommited else uncommited
|
||||
sat = "{}:{}:{}:{}".format(sat_name, sat_pos, diseqc, uncommited)
|
||||
self._api = API_VER
|
||||
self._doc = NeutrinoDocument()
|
||||
self._root = self._doc.createElement("zapit")
|
||||
self._root.setAttribute("api", self._api)
|
||||
self._doc.appendChild(self._root)
|
||||
self._doc.appendChild(self._doc.createComment(" File was created in DemonEditor. Enjoy watching! "))
|
||||
|
||||
for tr_elem in elem.getElementsByTagName("TS"):
|
||||
if tr_elem.hasAttributes():
|
||||
parse_transponder(api, sat, sat_pos, services, tr_elem)
|
||||
def write(self):
|
||||
srvs = defaultdict(list)
|
||||
for s in self._services:
|
||||
srvs[s.transponder_type].append(s)
|
||||
self.append_services(srvs.get(TrType.Satellite.value), "sat")
|
||||
self.append_services(srvs.get(TrType.Terrestrial.value), "terrestrial")
|
||||
self.append_services(srvs.get(TrType.Cable.value), "cable")
|
||||
|
||||
return services
|
||||
self._doc.write_xml(self._path)
|
||||
self._doc.unlink()
|
||||
|
||||
def append_services(self, services, s_type):
|
||||
if not services:
|
||||
return
|
||||
|
||||
sats = defaultdict(list)
|
||||
for srv in services:
|
||||
sats[srv[0]].append(srv)
|
||||
|
||||
for sat in sats:
|
||||
sat_elem = self._doc.createElement(s_type)
|
||||
attrs = get_attributes(sat)
|
||||
for k, v in attrs.items():
|
||||
sat_elem.setAttribute(k, v)
|
||||
|
||||
self._root.appendChild(sat_elem)
|
||||
|
||||
transponders = defaultdict(list)
|
||||
for srv in sats.get(sat):
|
||||
transponders[srv[-1]].append(srv)
|
||||
|
||||
for tr in transponders:
|
||||
tr_elem = self._doc.createElement("TS")
|
||||
for k, v in get_attributes(tr).items():
|
||||
tr_elem.setAttribute(k, v)
|
||||
sat_elem.appendChild(tr_elem)
|
||||
|
||||
for srv in transponders.get(tr):
|
||||
srv_elem = self._doc.createElement("S")
|
||||
s_attrs = get_attributes(srv.data_id)
|
||||
api = s_attrs.pop("api", self._api)
|
||||
if api != self._api:
|
||||
self._root.setAttribute("api", api)
|
||||
|
||||
for k, v in s_attrs.items():
|
||||
srv_elem.setAttribute(k, v)
|
||||
|
||||
tr_elem.appendChild(srv_elem)
|
||||
|
||||
|
||||
def parse_transponder(api, sat, sat_pos, services, tr_elem):
|
||||
tr_id = tr_elem.attributes["id"].value
|
||||
on = tr_elem.attributes["on"].value
|
||||
freq = tr_elem.attributes["frq"].value
|
||||
rate = tr_elem.attributes["sr"].value
|
||||
inv = tr_elem.attributes["inv"].value
|
||||
fec = tr_elem.attributes["fec"].value
|
||||
pol = tr_elem.attributes["pol"].value
|
||||
mod = tr_elem.attributes.get("mod")
|
||||
mod = mod.value if mod else mod
|
||||
sys = tr_elem.attributes.get("sys")
|
||||
sys = sys.value if sys else sys
|
||||
class NeutrinoServicesReader:
|
||||
|
||||
tr = "{}:{}:{}:{}:{}:{}:{}:{}:{}".format(tr_id, on, freq, inv, rate, fec, pol, mod, sys)
|
||||
def __init__(self, path):
|
||||
self._path = path + _FILE
|
||||
self._attrs = None
|
||||
self._tr = None
|
||||
self._api = "4"
|
||||
self._services = []
|
||||
|
||||
tr_id = tr_id.lstrip("0")
|
||||
on = on.lstrip("0")
|
||||
def get_services(self):
|
||||
dom = XmlHandler.parse(self._path)
|
||||
|
||||
for srv_elem in tr_elem.getElementsByTagName("S"):
|
||||
if srv_elem.hasAttributes():
|
||||
ssid = srv_elem.attributes["i"].value
|
||||
name = srv_elem.attributes["n"].value
|
||||
srv_type = srv_elem.attributes["t"].value
|
||||
sys = srv_elem.attributes["s"].value
|
||||
num = srv_elem.attributes.get("num")
|
||||
num = num.value if num else num
|
||||
f = srv_elem.attributes.get("f")
|
||||
f = f.value if f else f
|
||||
v, a, p, pmt, tx, vt = [None] * 6
|
||||
# For v3 is possible so: '<S i="0001" n="name" t="1" s="0" num="770" f="4"/>' (equals v4 api)
|
||||
if api == "3" and len(srv_elem.attributes) > 6:
|
||||
v = srv_elem.attributes["v"].value
|
||||
a = srv_elem.attributes["a"].value
|
||||
p = srv_elem.attributes["p"].value
|
||||
pmt = srv_elem.attributes["pmt"].value
|
||||
tx = srv_elem.attributes["tx"].value
|
||||
vt = srv_elem.attributes["vt"].value
|
||||
for root in dom.getElementsByTagName("zapit"):
|
||||
if root.hasAttributes():
|
||||
api = root.attributes["api"]
|
||||
self._api = api.value if api else self._api
|
||||
|
||||
data_id = "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}".format(api, srv_type, sys, num, f, v, a, p, pmt, tx, vt)
|
||||
fav_id = "{}:{}:{}".format(tr_id, on, ssid.lstrip("0"))
|
||||
for elem in root.getElementsByTagName("sat"):
|
||||
if elem.hasAttributes():
|
||||
sat_attrs = get_xml_attributes(elem)
|
||||
sat_pos = 0
|
||||
try:
|
||||
sat_pos = int(sat_attrs.get("position", "0"))
|
||||
sat_pos = "{:0.1f}{}".format(abs(sat_pos / 10), "W" if sat_pos < 0 else "E")
|
||||
except ValueError as e:
|
||||
log("Neutrino parsing error [parse sat position]: {}".format(e))
|
||||
sat = SP.join("{}{}{}".format(k, KSP, v) for k, v in sat_attrs.items())
|
||||
for tr_elem in elem.getElementsByTagName("TS"):
|
||||
if tr_elem.hasAttributes():
|
||||
self.parse_sat_transponder(sat, sat_pos, tr_elem)
|
||||
|
||||
srv = Service(flags_cas=sat,
|
||||
transponder_type=None,
|
||||
coded=None,
|
||||
service=name,
|
||||
locked=None,
|
||||
hide=None,
|
||||
package=PROVIDER.get(int(on, 16)),
|
||||
service_type=SERVICE_TYPE.get(str(int(srv_type, 16))),
|
||||
ssid=ssid,
|
||||
freq=freq,
|
||||
rate=rate,
|
||||
pol=POLARIZATION.get(pol),
|
||||
fec=FEC.get(fec),
|
||||
system=SYSTEM.get(sys),
|
||||
pos=sat_pos,
|
||||
data_id=data_id,
|
||||
fav_id=fav_id,
|
||||
transponder=tr)
|
||||
services.append(srv)
|
||||
# Terrestrial DVB-T[2].
|
||||
for elem in root.getElementsByTagName("terrestrial"):
|
||||
if elem.hasAttributes():
|
||||
terr_attrs = get_xml_attributes(elem)
|
||||
terr = SP.join("{}{}{}".format(k, KSP, v) for k, v in terr_attrs.items())
|
||||
|
||||
for tr_elem in elem.getElementsByTagName("TS"):
|
||||
if tr_elem.hasAttributes():
|
||||
self.parse_ct_transponder(terr, tr_elem, TrType.Terrestrial)
|
||||
|
||||
# Cable.
|
||||
for elem in root.getElementsByTagName("cable"):
|
||||
if elem.hasAttributes():
|
||||
cable_attrs = get_xml_attributes(elem)
|
||||
cable = SP.join("{}{}{}".format(k, KSP, v) for k, v in cable_attrs.items())
|
||||
|
||||
for tr_elem in elem.getElementsByTagName("TS"):
|
||||
if tr_elem.hasAttributes():
|
||||
self.parse_ct_transponder(cable, tr_elem, TrType.Cable)
|
||||
|
||||
return self._services
|
||||
|
||||
def parse_sat_transponder(self, sat, sat_pos, tr_elem):
|
||||
tr_attr = get_xml_attributes(tr_elem)
|
||||
tr = SP.join("{}{}{}".format(k, KSP, v) for k, v in tr_attr.items())
|
||||
tr_id = tr_attr.get("id", "0").lstrip("0")
|
||||
on = tr_attr.get("on", "0")
|
||||
freq = tr_attr.get("frq", "0")
|
||||
rate = tr_attr.get("sr", "0")
|
||||
fec = tr_attr.get("fec", "0")
|
||||
|
||||
pol = POLARIZATION.get(tr_attr.get("pol", "0"))
|
||||
# Formatting displayed values.
|
||||
try:
|
||||
freq = "{}".format(int(freq) // 1000)
|
||||
rate = "{}".format(int(rate) // 1000)
|
||||
except ValueError as e:
|
||||
log("Neutrino parsing error [parse_transponder]: {}".format(e))
|
||||
|
||||
for srv_elem in tr_elem.getElementsByTagName("S"):
|
||||
if srv_elem.hasAttributes():
|
||||
at = get_xml_attributes(srv_elem)
|
||||
at["api"] = self._api
|
||||
ssid, name, s_type, sys = at.get("i", "0"), at.get("n", ""), at.get("t", "3"), at.get("s", "0")
|
||||
data_id = SP.join("{}{}{}".format(k, KSP, v) for k, v in at.items())
|
||||
fav_id = "{}:{}:{}".format(tr_id, on.lstrip("0"), ssid.lstrip("0"))
|
||||
picon_id = "{}{}{}.png".format(tr_id, on, ssid)
|
||||
prv = PROVIDER.get(int(on, 16), "")
|
||||
st = SERVICE_TYPE.get(str(int(s_type, 16)), SERVICE_TYPE.get("-2"))
|
||||
|
||||
srv = Service(sat, TrType.Satellite.value, None, name, None, None, prv, st, None, picon_id, ssid, freq,
|
||||
rate, pol, FEC.get(fec), SYSTEM.get(sys), sat_pos, data_id, fav_id, tr)
|
||||
self._services.append(srv)
|
||||
|
||||
def parse_ct_transponder(self, terr, tr_elem, tr_type):
|
||||
attrs = get_xml_attributes(tr_elem)
|
||||
tr = SP.join("{}{}{}".format(k, KSP, v) for k, v in attrs.items())
|
||||
tr_id, on, freq = attrs.get("id", "0").lstrip("0"), attrs.get("on", "0"), attrs.get("frq", "0")
|
||||
|
||||
for srv_elem in tr_elem.getElementsByTagName("S"):
|
||||
if srv_elem.hasAttributes():
|
||||
s_at = get_xml_attributes(srv_elem)
|
||||
s_at["api"] = self._api
|
||||
ssid, name, s_type, sys = s_at.get("i", "0"), s_at.get("n", ""), s_at.get("t", "3"), s_at.get("s", "0")
|
||||
data_id = SP.join("{}{}{}".format(k, KSP, v) for k, v in s_at.items())
|
||||
fav_id = "{}:{}:{}".format(tr_id, on.lstrip("0"), ssid.lstrip("0"))
|
||||
picon_id = "{}{}{}.png".format(tr_id, on, ssid)
|
||||
prv = PROVIDER.get(int(on, 16), "")
|
||||
st = SERVICE_TYPE.get(str(int(s_type, 16)), SERVICE_TYPE.get("-2"))
|
||||
|
||||
if tr_type is TrType.Terrestrial:
|
||||
sys = T_SYSTEM.get(sys)
|
||||
pos = "T"
|
||||
elif tr_type is TrType.Cable:
|
||||
sys = SystemCable(sys).name
|
||||
pos = "C"
|
||||
else:
|
||||
log("Parse transponder error: Not supported type [{}]".format(tr_type))
|
||||
break
|
||||
|
||||
srv = Service(terr, tr_type.value, None, name, None, None, prv, st, None, picon_id, ssid,
|
||||
freq, "0", None, None, sys, pos, data_id, fav_id, tr)
|
||||
self._services.append(srv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,10 +1,39 @@
|
||||
""" Module foe parsing Satellites.xml
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Module for parsing satellites.xml file.
|
||||
|
||||
For more info see __COMMENT
|
||||
"""
|
||||
from xml.dom.minidom import parse, Document
|
||||
|
||||
from .ecommons import POLARIZATION, FEC, SYSTEM, MODULATION, PLS_MODE, Transponder, Satellite
|
||||
from app.commons import log
|
||||
from .ecommons import POLARIZATION, FEC, SYSTEM, MODULATION, Transponder, Satellite, get_key_by_value
|
||||
|
||||
__COMMENT = (" File was created in DemonEditor\n\n"
|
||||
"usable flags are\n"
|
||||
@@ -18,7 +47,7 @@ __COMMENT = (" File was created in DemonEditor\n\n"
|
||||
"polarization: 0 - Horizontal, 1 - Vertical, 2 - Left Circular, 3 - Right Circular\n"
|
||||
"fec_inner: 0 - Auto, 1 - 1/2, 2 - 2/3, 3 - 3/4, 4 - 5/6, 5 - 7/8, 6 - 8/9, 7 - 3/5,\n"
|
||||
"8 - 4/5, 9 - 9/10, 15 - None\n"
|
||||
"modulation: 0 - Auto, 1 - QPSK, 2 - 8PSK, 3 - 16APSK, 5 - 32APSK\n"
|
||||
"modulation: 0 - Auto, 1 - QPSK, 2 - 8PSK, 4 - 16APSK, 5 - 32APSK\n"
|
||||
"rolloff: 0 - 0.35, 1 - 0.25, 2 - 0.20, 3 - Auto\n"
|
||||
"pilot: 0 - Off, 1 - On, 2 - Auto\n"
|
||||
"inversion: 0 = Off, 1 = On, 2 = Auto (default)\n"
|
||||
@@ -52,15 +81,17 @@ def write_satellites(satellites, data_path):
|
||||
transponder_child.setAttribute("frequency", tr.frequency)
|
||||
transponder_child.setAttribute("symbol_rate", tr.symbol_rate)
|
||||
transponder_child.setAttribute("polarization", get_key_by_value(POLARIZATION, tr.polarization))
|
||||
transponder_child.setAttribute("fec_inner", get_key_by_value(FEC, tr.fec_inner))
|
||||
transponder_child.setAttribute("system", get_key_by_value(SYSTEM, tr.system))
|
||||
transponder_child.setAttribute("modulation", get_key_by_value(MODULATION, tr.modulation))
|
||||
transponder_child.setAttribute("fec_inner", get_key_by_value(FEC, tr.fec_inner) or "0")
|
||||
transponder_child.setAttribute("system", get_key_by_value(SYSTEM, tr.system) or "0")
|
||||
transponder_child.setAttribute("modulation", get_key_by_value(MODULATION, tr.modulation) or "0")
|
||||
if tr.pls_mode:
|
||||
transponder_child.setAttribute("pls_mode", get_key_by_value(PLS_MODE, tr.pls_mode))
|
||||
transponder_child.setAttribute("pls_mode", tr.pls_mode)
|
||||
if tr.pls_code:
|
||||
transponder_child.setAttribute("pls_code", tr.pls_code)
|
||||
if tr.is_id:
|
||||
transponder_child.setAttribute("is_id", tr.is_id)
|
||||
if tr.t2mi_plp_id:
|
||||
transponder_child.setAttribute("t2mi_plp_id", tr.t2mi_plp_id)
|
||||
sat_child.appendChild(transponder_child)
|
||||
root.appendChild(sat_child)
|
||||
doc.writexml(open(data_path, "w"),
|
||||
@@ -71,35 +102,42 @@ def write_satellites(satellites, data_path):
|
||||
doc.unlink()
|
||||
|
||||
|
||||
def parse_transponders(elem):
|
||||
def parse_transponders(elem, sat_name):
|
||||
""" Parsing satellite transponders """
|
||||
transponders = []
|
||||
for el in elem.getElementsByTagName("transponder"):
|
||||
if el.hasAttributes():
|
||||
atr = el.attributes
|
||||
tr = Transponder(atr["frequency"].value,
|
||||
atr["symbol_rate"].value,
|
||||
POLARIZATION[atr["polarization"].value],
|
||||
FEC[atr["fec_inner"].value],
|
||||
SYSTEM[atr["system"].value],
|
||||
MODULATION[atr["modulation"].value],
|
||||
PLS_MODE[atr["pls_mode"].value] if "pls_mode" in atr else None,
|
||||
atr["pls_code"].value if "pls_code" in atr else None,
|
||||
atr["is_id"].value if "is_id" in atr else None)
|
||||
transponders.append(tr)
|
||||
try:
|
||||
tr = Transponder(atr["frequency"].value,
|
||||
atr["symbol_rate"].value,
|
||||
POLARIZATION[atr["polarization"].value],
|
||||
FEC[atr["fec_inner"].value],
|
||||
SYSTEM[atr["system"].value],
|
||||
MODULATION[atr["modulation"].value],
|
||||
atr["pls_mode"].value if "pls_mode" in atr else None,
|
||||
atr["pls_code"].value if "pls_code" in atr else None,
|
||||
atr["is_id"].value if "is_id" in atr else None,
|
||||
atr["t2mi_plp_id"].value if "t2mi_plp_id" in atr else None)
|
||||
except Exception as e:
|
||||
message = f"Error: can't parse transponder for '{sat_name}' satellite! {repr(e)}"
|
||||
log(message)
|
||||
else:
|
||||
transponders.append(tr)
|
||||
return transponders
|
||||
|
||||
|
||||
def parse_sat(elem):
|
||||
""" Parsing satellite """
|
||||
return Satellite(elem.attributes["name"].value,
|
||||
""" Parsing satellite. """
|
||||
sat_name = elem.attributes["name"].value
|
||||
return Satellite(sat_name,
|
||||
elem.attributes["flags"].value,
|
||||
elem.attributes["position"].value,
|
||||
parse_transponders(elem))
|
||||
parse_transponders(elem, sat_name))
|
||||
|
||||
|
||||
def parse_satellites(path):
|
||||
""" Parsing satellites from xml"""
|
||||
""" Parsing satellites from xml. """
|
||||
dom = parse(path)
|
||||
satellites = []
|
||||
|
||||
@@ -110,11 +148,5 @@ def parse_satellites(path):
|
||||
return satellites
|
||||
|
||||
|
||||
def get_key_by_value(dictionary, value):
|
||||
for k, v in dictionary.items():
|
||||
if v == value:
|
||||
return k
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
157
app/ftp.py
157
app/ftp.py
@@ -1,157 +0,0 @@
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
from enum import Enum
|
||||
from ftplib import FTP, error_perm
|
||||
from telnetlib import Telnet
|
||||
|
||||
from app.commons import log
|
||||
from app.properties import Profile
|
||||
|
||||
__DATA_FILES_LIST = ("tv", "radio", "lamedb", "blacklist", "whitelist", # enigma 2
|
||||
"services.xml", "myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino
|
||||
|
||||
|
||||
class DownloadDataType(Enum):
|
||||
ALL = 0
|
||||
BOUQUETS = 1
|
||||
SATELLITES = 2
|
||||
PICONS = 3
|
||||
|
||||
|
||||
def download_data(*, properties, download_type=DownloadDataType.ALL, callback=None):
|
||||
with FTP(host=properties["host"]) as ftp:
|
||||
ftp.login(user=properties["user"], passwd=properties["password"])
|
||||
save_path = properties["data_dir_path"]
|
||||
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||
files = []
|
||||
# bouquets section
|
||||
if download_type is DownloadDataType.ALL or download_type is DownloadDataType.BOUQUETS:
|
||||
ftp.cwd(properties["services_path"])
|
||||
ftp.dir(files.append)
|
||||
|
||||
for file in files:
|
||||
name = str(file).strip()
|
||||
if name.endswith(__DATA_FILES_LIST):
|
||||
name = name.split()[-1]
|
||||
with open(save_path + name, "wb") as f:
|
||||
ftp.retrbinary("RETR " + name, f.write)
|
||||
# satellites.xml section
|
||||
if download_type is DownloadDataType.ALL or download_type is DownloadDataType.SATELLITES:
|
||||
ftp.cwd(properties["satellites_xml_path"])
|
||||
files.clear()
|
||||
ftp.dir(files.append)
|
||||
|
||||
for file in files:
|
||||
name = str(file).strip()
|
||||
xml_file = "satellites.xml"
|
||||
if name.endswith(xml_file):
|
||||
with open(save_path + xml_file, 'wb') as f:
|
||||
ftp.retrbinary("RETR " + xml_file, f.write)
|
||||
|
||||
if callback is not None:
|
||||
callback()
|
||||
|
||||
|
||||
def upload_data(*, properties, download_type=DownloadDataType.ALL, remove_unused=False, profile=Profile.ENIGMA_2,
|
||||
callback=None):
|
||||
data_path = properties["data_dir_path"]
|
||||
host = properties["host"]
|
||||
# telnet
|
||||
tn = telnet(host=host, user=properties.get("telnet_user", "root"), password=properties.get("telnet_password", ""),
|
||||
timeout=properties.get("telnet_timeout", 5))
|
||||
next(tn)
|
||||
# terminate enigma or enigma
|
||||
tn.send("init 4")
|
||||
|
||||
with FTP(host=host) as ftp:
|
||||
ftp.login(user=properties["user"], passwd=properties["password"])
|
||||
|
||||
if download_type is DownloadDataType.ALL or download_type is DownloadDataType.SATELLITES:
|
||||
ftp.cwd(properties["satellites_xml_path"])
|
||||
file_name = "satellites.xml"
|
||||
send = send_file(file_name, data_path, ftp)
|
||||
if download_type == DownloadDataType.SATELLITES:
|
||||
return send
|
||||
|
||||
if download_type is DownloadDataType.ALL or download_type is DownloadDataType.BOUQUETS:
|
||||
ftp.cwd(properties["services_path"])
|
||||
if remove_unused:
|
||||
files = []
|
||||
ftp.dir(files.append)
|
||||
for file in files:
|
||||
name = str(file).strip()
|
||||
if name.endswith(__DATA_FILES_LIST):
|
||||
name = name.split()[-1]
|
||||
ftp.delete(name)
|
||||
|
||||
for file_name in os.listdir(data_path):
|
||||
if file_name == "satellites.xml":
|
||||
continue
|
||||
if file_name.endswith(__DATA_FILES_LIST):
|
||||
send_file(file_name, data_path, ftp)
|
||||
|
||||
if download_type is DownloadDataType.PICONS:
|
||||
picons_dir_path = properties.get("picons_dir_path")
|
||||
picons_path = properties.get("picons_path")
|
||||
try:
|
||||
ftp.cwd(picons_path)
|
||||
except error_perm as e:
|
||||
if str(e).startswith("550"):
|
||||
ftp.mkd(picons_path) # if not exist
|
||||
ftp.cwd(picons_path)
|
||||
|
||||
files = []
|
||||
ftp.dir(files.append)
|
||||
picons_suf = (".jpg", ".png")
|
||||
|
||||
for file in files:
|
||||
name = str(file).strip()
|
||||
if name.endswith(picons_suf):
|
||||
name = name.split()[-1]
|
||||
ftp.delete(name)
|
||||
for file_name in os.listdir(picons_dir_path):
|
||||
if file_name.endswith(picons_suf):
|
||||
send_file(file_name, picons_dir_path, ftp)
|
||||
|
||||
# resume enigma or restart neutrino
|
||||
tn.send("init 3" if profile is Profile.ENIGMA_2 else "init 6")
|
||||
|
||||
if callback is not None:
|
||||
callback()
|
||||
|
||||
|
||||
def send_file(file_name, path, ftp):
|
||||
""" Opens the file in binary mode and transfers into receiver """
|
||||
with open(path + file_name, "rb") as f:
|
||||
return ftp.storbinary("STOR " + file_name, f)
|
||||
|
||||
|
||||
def telnet(host, port=23, user="", password="", timeout=5):
|
||||
try:
|
||||
tn = Telnet(host=host, port=port, timeout=timeout)
|
||||
except socket.timeout:
|
||||
log("telnet error: socket timeout")
|
||||
else:
|
||||
time.sleep(1)
|
||||
command = yield
|
||||
if user != "":
|
||||
tn.read_until(b"login: ")
|
||||
tn.write(user.encode("utf-8") + b"\n")
|
||||
time.sleep(timeout)
|
||||
if password != "":
|
||||
tn.read_until(b"Password: ")
|
||||
tn.write(password.encode("utf-8") + b"\n")
|
||||
time.sleep(timeout)
|
||||
tn.write("{}\r\n".format(command).encode("utf-8"))
|
||||
time.sleep(timeout)
|
||||
command = yield
|
||||
time.sleep(timeout)
|
||||
tn.write("{}\r\n".format(command).encode("utf-8"))
|
||||
time.sleep(timeout)
|
||||
tn.close()
|
||||
yield
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@@ -1,169 +0,0 @@
|
||||
import os
|
||||
import shutil
|
||||
from collections import namedtuple
|
||||
from html.parser import HTMLParser
|
||||
|
||||
from app.commons import log
|
||||
from app.properties import Profile
|
||||
|
||||
Provider = namedtuple("Provider", ["logo", "name", "url", "on_id", "selected"])
|
||||
Picon = namedtuple("Picon", ["ref", "ssid", "v_pid"])
|
||||
|
||||
|
||||
class PiconsParser(HTMLParser):
|
||||
""" Parser for package html page. (https://www.lyngsat.com/packages/*provider-name*.html) """
|
||||
|
||||
def __init__(self, entities=False, separator=' '):
|
||||
|
||||
HTMLParser.__init__(self)
|
||||
|
||||
self._parse_html_entities = entities
|
||||
self._separator = separator
|
||||
self._is_td = False
|
||||
self._is_th = False
|
||||
self._current_row = []
|
||||
self._current_cell = []
|
||||
self.picons = []
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == 'td':
|
||||
self._is_td = True
|
||||
if tag == 'th':
|
||||
self._is_th = True
|
||||
if tag == "img":
|
||||
self._current_row.append(attrs[0][1])
|
||||
|
||||
def handle_data(self, data):
|
||||
""" Save content to a cell """
|
||||
if self._is_td or self._is_th:
|
||||
self._current_cell.append(data.strip())
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == 'td':
|
||||
self._is_td = False
|
||||
elif tag == 'th':
|
||||
self._is_th = False
|
||||
|
||||
if tag in ('td', 'th'):
|
||||
final_cell = self._separator.join(self._current_cell).strip()
|
||||
self._current_row.append(final_cell)
|
||||
self._current_cell = []
|
||||
elif tag == 'tr':
|
||||
row = self._current_row
|
||||
ln = len(row)
|
||||
if 9 < ln < 13:
|
||||
url = None
|
||||
if row[0].startswith("../logo/"):
|
||||
url = row[0]
|
||||
elif row[1].startswith("../logo/"):
|
||||
url = row[1]
|
||||
|
||||
ssid = row[-4]
|
||||
if url and len(ssid) > 2:
|
||||
self.picons.append(Picon(url, ssid, row[-3]))
|
||||
|
||||
self._current_row = []
|
||||
|
||||
def error(self, message):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def parse(open_path, picons_path, tmp_path, on_id, profile=Profile.ENIGMA_2):
|
||||
with open(open_path, encoding="utf-8", errors="replace") as f:
|
||||
parser = PiconsParser()
|
||||
parser.reset()
|
||||
parser.feed(f.read())
|
||||
picons = parser.picons
|
||||
if picons:
|
||||
os.makedirs(picons_path, exist_ok=True)
|
||||
for p in picons:
|
||||
try:
|
||||
picon_file_name = picons_path + PiconsParser.format(p.ssid, on_id, p.v_pid, profile)
|
||||
shutil.copyfile(tmp_path + "www.lyngsat.com/" + p.ref.lstrip("."), picon_file_name)
|
||||
except (TypeError, ValueError) as e:
|
||||
log("Picons format parse error: {} {} {}".format(p.ref, p.ssid, p.v_pid) + "\n" + str(e))
|
||||
print(e)
|
||||
|
||||
@staticmethod
|
||||
def format(ssid, on_id, v_pid, profile: Profile):
|
||||
tr_id = int(ssid[:-2] if len(ssid) < 4 else ssid[:2])
|
||||
if profile is Profile.ENIGMA_2:
|
||||
return "1_0_{}_{:X}_{:X}_{:X}_1680000_0_0_0.png".format(1 if v_pid else 2, int(ssid), tr_id, int(on_id))
|
||||
elif profile is Profile.NEUTRINO_MP:
|
||||
return "{:x}{:04x}{:04x}.png".format(tr_id, int(on_id), int(ssid))
|
||||
else:
|
||||
return "{}.png".format(ssid)
|
||||
|
||||
|
||||
class ProviderParser(HTMLParser):
|
||||
""" Parser for satellite html page. (https://www.lyngsat.com/*sat-name*.html) """
|
||||
|
||||
def __init__(self, entities=False, separator=' '):
|
||||
|
||||
HTMLParser.__init__(self)
|
||||
|
||||
self._ON_ID_BLACK_LIST = ("65535", "?", "0", "1")
|
||||
self._parse_html_entities = entities
|
||||
self._separator = separator
|
||||
self._is_td = False
|
||||
self._is_th = False
|
||||
self._is_provider = False
|
||||
self._current_row = []
|
||||
self._current_cell = []
|
||||
self.rows = []
|
||||
self._ids = set()
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == 'td':
|
||||
self._is_td = True
|
||||
if tag == 'tr':
|
||||
self._is_th = True
|
||||
if tag == "img":
|
||||
if attrs[0][1].startswith("logo/"):
|
||||
self._current_row.append(attrs[0][1])
|
||||
if tag == "a":
|
||||
if "https://www.lyngsat.com/packages/" in attrs[0][1]:
|
||||
self._current_row.append(attrs[0][1])
|
||||
|
||||
def handle_data(self, data):
|
||||
""" Save content to a cell """
|
||||
if self._is_td or self._is_th:
|
||||
self._current_cell.append(data.strip())
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == 'td':
|
||||
self._is_td = False
|
||||
elif tag == 'tr':
|
||||
self._is_th = False
|
||||
|
||||
if tag in ('td', 'th'):
|
||||
final_cell = self._separator.join(self._current_cell).strip()
|
||||
self._current_row.append(final_cell)
|
||||
self._current_cell = []
|
||||
elif tag == 'tr':
|
||||
row = self._current_row
|
||||
if len(row) == 12:
|
||||
on_id, sep, tid = str(row[-2]).partition("-")
|
||||
if tid and on_id not in self._ON_ID_BLACK_LIST and on_id not in self._ids:
|
||||
row[-2] = on_id
|
||||
self.rows.append(row)
|
||||
self._ids.add(on_id)
|
||||
self._current_row = []
|
||||
|
||||
def error(self, message):
|
||||
pass
|
||||
|
||||
|
||||
def parse_providers(open_path):
|
||||
with open(open_path, encoding="utf-8", errors="replace") as f:
|
||||
parser = ProviderParser()
|
||||
parser.reset()
|
||||
parser.feed(f.read())
|
||||
rows = parser.rows
|
||||
|
||||
if rows:
|
||||
return [Provider(logo=r[2], name=r[5], url=r[6], on_id=r[-2], selected=True) for r in rows]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@@ -1,67 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
CONFIG_PATH = str(Path.home()) + "/.config/demon-editor/"
|
||||
CONFIG_FILE = CONFIG_PATH + "config.json"
|
||||
DATA_PATH = "data/"
|
||||
|
||||
|
||||
class Profile(Enum):
|
||||
""" Profiles for settings """
|
||||
ENIGMA_2 = "0"
|
||||
NEUTRINO_MP = "1"
|
||||
|
||||
|
||||
def get_config():
|
||||
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True) # create dir if not exist
|
||||
os.makedirs(os.path.dirname(DATA_PATH), exist_ok=True)
|
||||
|
||||
if not os.path.isfile(CONFIG_FILE) or os.stat(CONFIG_FILE).st_size == 0:
|
||||
reset_config()
|
||||
|
||||
with open(CONFIG_FILE, "r") as config_file:
|
||||
return json.load(config_file)
|
||||
|
||||
|
||||
def reset_config():
|
||||
with open(CONFIG_FILE, "w") as default_config_file:
|
||||
json.dump(get_default_settings(), default_config_file)
|
||||
|
||||
|
||||
def write_config(config):
|
||||
assert isinstance(config, dict)
|
||||
with open(CONFIG_FILE, "w") as config_file:
|
||||
json.dump(config, config_file)
|
||||
|
||||
|
||||
def get_default_settings():
|
||||
return {
|
||||
Profile.ENIGMA_2.value: {
|
||||
"host": "127.0.0.1", "port": "21",
|
||||
"user": "root", "password": "root",
|
||||
"telnet_user": "", "telnet_password": "",
|
||||
"telnet_port": "21", "telnet_timeout": 5,
|
||||
"services_path": "/etc/enigma2/",
|
||||
"user_bouquet_path": "/etc/enigma2/",
|
||||
"satellites_xml_path": "/etc/tuxbox/",
|
||||
"picons_path": "/usr/share/enigma2/picon",
|
||||
"data_dir_path": DATA_PATH + "enigma2/",
|
||||
"picons_dir_path": DATA_PATH + "enigma2/picons/"},
|
||||
Profile.NEUTRINO_MP.value: {
|
||||
"host": "127.0.0.1", "port": "21",
|
||||
"user": "root", "password": "root",
|
||||
"telnet_user": "root", "telnet_password": "",
|
||||
"telnet_port": "21", "telnet_timeout": 1,
|
||||
"services_path": "/var/tuxbox/config/zapit/",
|
||||
"user_bouquet_path": "/var/tuxbox/config/zapit/",
|
||||
"satellites_xml_path": "/var/tuxbox/config/",
|
||||
"picons_path": "/usr/share/tuxbox/neutrino/icons/logo/",
|
||||
"data_dir_path": DATA_PATH + "neutrino/",
|
||||
"picons_dir_path": DATA_PATH + "neutrino/picons/"},
|
||||
"profile": Profile.ENIGMA_2.value}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
855
app/settings.py
Normal file
855
app/settings.py
Normal file
@@ -0,0 +1,855 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import copy
|
||||
import json
|
||||
import locale
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum, IntEnum
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from textwrap import dedent
|
||||
|
||||
SEP = os.sep
|
||||
HOME_PATH = str(Path.home())
|
||||
CONFIG_PATH = HOME_PATH + "{}.config{}demon-editor{}".format(SEP, SEP, SEP)
|
||||
CONFIG_FILE = CONFIG_PATH + "config.json"
|
||||
DATA_PATH = HOME_PATH + "{}DemonEditor{}".format(SEP, SEP)
|
||||
GTK_PATH = os.environ.get("GTK_PATH", None)
|
||||
|
||||
IS_DARWIN = sys.platform == "darwin"
|
||||
IS_WIN = sys.platform == "win32"
|
||||
IS_LINUX = sys.platform == "linux"
|
||||
|
||||
|
||||
class Defaults(Enum):
|
||||
""" Default program settings """
|
||||
USER = "root"
|
||||
PASSWORD = ""
|
||||
HOST = "127.0.0.1"
|
||||
FTP_PORT = "21"
|
||||
HTTP_PORT = "80"
|
||||
TELNET_PORT = "23"
|
||||
HTTP_USE_SSL = False
|
||||
# Enigma2.
|
||||
BOX_SERVICES_PATH = "/etc/enigma2/"
|
||||
BOX_SATELLITE_PATH = "/etc/tuxbox/"
|
||||
BOX_PICON_PATH = "/usr/share/enigma2/picon/"
|
||||
BOX_PICON_PATHS = ("/usr/share/enigma2/picon/",
|
||||
"/media/hdd/picon/",
|
||||
"/media/usb/picon/",
|
||||
"/media/mmc/picon/",
|
||||
"/media/cf/picon/")
|
||||
# Neutrino.
|
||||
NEUTRINO_BOX_SERVICES_PATH = "/var/tuxbox/config/zapit/"
|
||||
NEUTRINO_BOX_SATELLITE_PATH = "/var/tuxbox/config/"
|
||||
NEUTRINO_BOX_PICON_PATH = "/usr/share/tuxbox/neutrino/icons/logo/"
|
||||
NEUTRINO_BOX_PICON_PATHS = ("/usr/share/tuxbox/neutrino/icons/logo/",)
|
||||
# Paths.
|
||||
BACKUP_PATH = "{}backup{}".format(DATA_PATH, SEP)
|
||||
PICON_PATH = "{}picons{}".format(DATA_PATH, SEP)
|
||||
|
||||
DEFAULT_PROFILE = "default"
|
||||
BACKUP_BEFORE_DOWNLOADING = True
|
||||
BACKUP_BEFORE_SAVE = True
|
||||
V5_SUPPORT = False
|
||||
FORCE_BQ_NAMES = False
|
||||
HTTP_API_SUPPORT = True
|
||||
ENABLE_YT_DL = False
|
||||
ENABLE_SEND_TO = False
|
||||
USE_COLORS = True
|
||||
NEW_COLOR = "rgb(255,230,204)"
|
||||
EXTRA_COLOR = "rgb(179,230,204)"
|
||||
TOOLTIP_LOGO_SIZE = 96
|
||||
LIST_PICON_SIZE = 32
|
||||
FAV_CLICK_MODE = 0
|
||||
PLAY_STREAMS_MODE = 1 if IS_DARWIN else 0
|
||||
STREAM_LIB = "mpv" if IS_WIN else "vlc"
|
||||
MAIN_LIST_PLAYBACK = False
|
||||
PROFILE_FOLDER_DEFAULT = False
|
||||
RECORDS_PATH = DATA_PATH + "records{}".format(SEP)
|
||||
ACTIVATE_TRANSCODING = False
|
||||
ACTIVE_TRANSCODING_PRESET = "720p TV{}device".format(SEP)
|
||||
|
||||
|
||||
class SettingsType(IntEnum):
|
||||
""" Profiles for settings """
|
||||
ENIGMA_2 = 0
|
||||
NEUTRINO_MP = 1
|
||||
|
||||
def get_default_settings(self):
|
||||
""" Returns default settings for current type. """
|
||||
if self is self.ENIGMA_2:
|
||||
srv_path = Defaults.BOX_SERVICES_PATH.value
|
||||
sat_path = Defaults.BOX_SATELLITE_PATH.value
|
||||
picons_path = Defaults.BOX_PICON_PATH.value
|
||||
http_timeout = 5
|
||||
telnet_timeout = 5
|
||||
else:
|
||||
srv_path = Defaults.NEUTRINO_BOX_SERVICES_PATH.value
|
||||
sat_path = Defaults.NEUTRINO_BOX_SATELLITE_PATH.value
|
||||
picons_path = Defaults.NEUTRINO_BOX_PICON_PATH.value
|
||||
http_timeout = 2
|
||||
telnet_timeout = 1
|
||||
|
||||
return {"setting_type": self.value,
|
||||
"host": Defaults.HOST.value,
|
||||
"port": Defaults.FTP_PORT.value,
|
||||
"timeout": 5,
|
||||
"user": Defaults.USER.value,
|
||||
"password": Defaults.PASSWORD.value,
|
||||
"http_port": Defaults.HTTP_PORT.value,
|
||||
"http_timeout": http_timeout,
|
||||
"http_use_ssl": Defaults.HTTP_USE_SSL.value,
|
||||
"telnet_port": Defaults.TELNET_PORT.value,
|
||||
"telnet_timeout": telnet_timeout,
|
||||
"services_path": srv_path,
|
||||
"user_bouquet_path": srv_path,
|
||||
"satellites_xml_path": sat_path,
|
||||
"picons_path": picons_path}
|
||||
|
||||
|
||||
class SettingsException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SettingsReadException(SettingsException):
|
||||
pass
|
||||
|
||||
|
||||
class PlayStreamsMode(IntEnum):
|
||||
""" Behavior mode when opening streams. """
|
||||
BUILT_IN = 0
|
||||
WINDOW = 1
|
||||
M3U = 2
|
||||
|
||||
|
||||
class Settings:
|
||||
__INSTANCE = None
|
||||
__VERSION = 2
|
||||
|
||||
def __init__(self, ext_settings=None):
|
||||
try:
|
||||
settings = ext_settings or self.get_settings()
|
||||
except PermissionError as e:
|
||||
raise SettingsReadException(e)
|
||||
|
||||
if self.__VERSION > settings.get("version", 0):
|
||||
raise SettingsException("Outdated version of the settings format!")
|
||||
|
||||
self._settings = settings
|
||||
self._current_profile = self._settings.get("default_profile", "default")
|
||||
self._profiles = self._settings.get("profiles", {"default": SettingsType.ENIGMA_2.get_default_settings()})
|
||||
self._cp_settings = self._profiles.get(self._current_profile, None) # Current profile settings
|
||||
if not self._cp_settings:
|
||||
raise SettingsException("Error reading settings [current profile].")
|
||||
|
||||
def __str__(self):
|
||||
return dedent(""" Current profile: {}
|
||||
Current profile options:
|
||||
{}
|
||||
Full config:
|
||||
{}
|
||||
""").format(self._current_profile,
|
||||
pformat(self._cp_settings),
|
||||
pformat(self._settings))
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
if not cls.__INSTANCE:
|
||||
cls.__INSTANCE = Settings()
|
||||
return cls.__INSTANCE
|
||||
|
||||
def save(self):
|
||||
self.write_settings(self._settings)
|
||||
|
||||
def reset(self, force_write=False):
|
||||
for k, v in self.setting_type.get_default_settings().items():
|
||||
self._cp_settings[k] = v
|
||||
|
||||
if force_write:
|
||||
self.save()
|
||||
|
||||
@staticmethod
|
||||
def reset_to_default():
|
||||
Settings.write_settings(Settings.get_default_settings())
|
||||
|
||||
def get_default(self, p_name):
|
||||
""" Returns default value for current settings type """
|
||||
return self.setting_type.get_default_settings().get(p_name)
|
||||
|
||||
def add(self, name, value):
|
||||
""" Adds extra options """
|
||||
self._settings[name] = value
|
||||
|
||||
def get(self, name, default=None):
|
||||
""" Returns extra options or None """
|
||||
return self._settings.get(name, default)
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
""" Returns copy of the current settings! """
|
||||
return copy.deepcopy(self._settings)
|
||||
|
||||
@settings.setter
|
||||
def settings(self, value):
|
||||
""" Sets copy of the settings! """
|
||||
self._settings = copy.deepcopy(value)
|
||||
|
||||
@property
|
||||
def current_profile(self):
|
||||
return self._current_profile
|
||||
|
||||
@current_profile.setter
|
||||
def current_profile(self, value):
|
||||
self._current_profile = value
|
||||
self._cp_settings = self._profiles.get(self._current_profile)
|
||||
|
||||
@property
|
||||
def default_profile(self):
|
||||
return self._settings.get("default_profile", "default")
|
||||
|
||||
@default_profile.setter
|
||||
def default_profile(self, value):
|
||||
self._settings["default_profile"] = value
|
||||
|
||||
@property
|
||||
def current_profile_settings(self):
|
||||
return self._cp_settings
|
||||
|
||||
@property
|
||||
def profiles(self):
|
||||
return self._profiles
|
||||
|
||||
@profiles.setter
|
||||
def profiles(self, ps):
|
||||
self._profiles = ps
|
||||
self._settings["profiles"] = self._profiles
|
||||
|
||||
@property
|
||||
def setting_type(self):
|
||||
return SettingsType(self._cp_settings.get("setting_type", SettingsType.ENIGMA_2.value))
|
||||
|
||||
@setting_type.setter
|
||||
def setting_type(self, s_type):
|
||||
self._cp_settings["setting_type"] = s_type.value
|
||||
|
||||
# ******* Network ******** #
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
return self._cp_settings.get("host", self.get_default("host"))
|
||||
|
||||
@host.setter
|
||||
def host(self, value):
|
||||
self._cp_settings["host"] = value
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
return self._cp_settings.get("port", self.get_default("port"))
|
||||
|
||||
@port.setter
|
||||
def port(self, value):
|
||||
self._cp_settings["port"] = value
|
||||
|
||||
@property
|
||||
def user(self):
|
||||
return self._cp_settings.get("user", self.get_default("user"))
|
||||
|
||||
@user.setter
|
||||
def user(self, value):
|
||||
self._cp_settings["user"] = value
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
return self._cp_settings.get("password", self.get_default("password"))
|
||||
|
||||
@password.setter
|
||||
def password(self, value):
|
||||
self._cp_settings["password"] = value
|
||||
|
||||
@property
|
||||
def http_port(self):
|
||||
return self._cp_settings.get("http_port", self.get_default("http_port"))
|
||||
|
||||
@http_port.setter
|
||||
def http_port(self, value):
|
||||
self._cp_settings["http_port"] = value
|
||||
|
||||
@property
|
||||
def http_timeout(self):
|
||||
return self._cp_settings.get("http_timeout", self.get_default("http_timeout"))
|
||||
|
||||
@http_timeout.setter
|
||||
def http_timeout(self, value):
|
||||
self._cp_settings["http_timeout"] = value
|
||||
|
||||
@property
|
||||
def http_use_ssl(self):
|
||||
return self._cp_settings.get("http_use_ssl", self.get_default("http_use_ssl"))
|
||||
|
||||
@http_use_ssl.setter
|
||||
def http_use_ssl(self, value):
|
||||
self._cp_settings["http_use_ssl"] = value
|
||||
|
||||
@property
|
||||
def telnet_port(self):
|
||||
return self._cp_settings.get("telnet_port", self.get_default("telnet_port"))
|
||||
|
||||
@telnet_port.setter
|
||||
def telnet_port(self, value):
|
||||
self._cp_settings["telnet_port"] = value
|
||||
|
||||
@property
|
||||
def telnet_timeout(self):
|
||||
return self._cp_settings.get("telnet_timeout", self.get_default("telnet_timeout"))
|
||||
|
||||
@telnet_timeout.setter
|
||||
def telnet_timeout(self, value):
|
||||
self._cp_settings["telnet_timeout"] = value
|
||||
|
||||
@property
|
||||
def services_path(self):
|
||||
return self._cp_settings.get("services_path", self.get_default("services_path"))
|
||||
|
||||
@services_path.setter
|
||||
def services_path(self, value):
|
||||
self._cp_settings["services_path"] = value
|
||||
|
||||
@property
|
||||
def user_bouquet_path(self):
|
||||
return self._cp_settings.get("user_bouquet_path", self.get_default("user_bouquet_path"))
|
||||
|
||||
@user_bouquet_path.setter
|
||||
def user_bouquet_path(self, value):
|
||||
self._cp_settings["user_bouquet_path"] = value
|
||||
|
||||
@property
|
||||
def satellites_xml_path(self):
|
||||
return self._cp_settings.get("satellites_xml_path", self.get_default("satellites_xml_path"))
|
||||
|
||||
@satellites_xml_path.setter
|
||||
def satellites_xml_path(self, value):
|
||||
self._cp_settings["satellites_xml_path"] = value
|
||||
|
||||
@property
|
||||
def picons_path(self):
|
||||
return self._cp_settings.get("picons_path", self.get_default("picons_path"))
|
||||
|
||||
@picons_path.setter
|
||||
def picons_path(self, value):
|
||||
self._cp_settings["picons_path"] = value
|
||||
|
||||
@property
|
||||
def picons_paths(self):
|
||||
if self.setting_type is SettingsType.NEUTRINO_MP:
|
||||
return self._settings.get("neutrino_picon_paths", Defaults.NEUTRINO_BOX_PICON_PATHS.value)
|
||||
else:
|
||||
return self._settings.get("picon_paths", Defaults.BOX_PICON_PATHS.value)
|
||||
|
||||
@picons_paths.setter
|
||||
def picons_paths(self, value):
|
||||
if self.setting_type is SettingsType.NEUTRINO_MP:
|
||||
self._settings["neutrino_picon_paths"] = value
|
||||
else:
|
||||
self._settings["picon_paths"] = value
|
||||
|
||||
# ***** Local paths ***** #
|
||||
|
||||
@property
|
||||
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 default_backup_path(self):
|
||||
return self._settings.get("default_backup_path", Defaults.BACKUP_PATH.value)
|
||||
|
||||
@default_backup_path.setter
|
||||
def default_backup_path(self, value):
|
||||
self._settings["default_backup_path"] = value
|
||||
|
||||
@property
|
||||
def default_picon_path(self):
|
||||
return self._settings.get("default_picon_path", Defaults.PICON_PATH.value)
|
||||
|
||||
@default_picon_path.setter
|
||||
def default_picon_path(self, value):
|
||||
self._settings["default_picon_path"] = value
|
||||
|
||||
@property
|
||||
def profile_data_path(self):
|
||||
return f"{self.default_data_path}data{SEP}{self._current_profile}{SEP}"
|
||||
|
||||
@profile_data_path.setter
|
||||
def profile_data_path(self, value):
|
||||
self._cp_settings["profile_data_path"] = value
|
||||
|
||||
@property
|
||||
def profile_picons_path(self):
|
||||
if self.profile_folder_is_default:
|
||||
return f"{self.profile_data_path}picons{SEP}"
|
||||
return f"{self.default_picon_path}{self._current_profile}{SEP}"
|
||||
|
||||
@profile_picons_path.setter
|
||||
def profile_picons_path(self, value):
|
||||
self._cp_settings["profile_picons_path"] = value
|
||||
|
||||
@property
|
||||
def profile_backup_path(self):
|
||||
if self.profile_folder_is_default:
|
||||
return f"{self.profile_data_path}backup{SEP}"
|
||||
return f"{self.default_backup_path}{self._current_profile}{SEP}"
|
||||
|
||||
@profile_backup_path.setter
|
||||
def profile_backup_path(self, value):
|
||||
self._cp_settings["profile_backup_path"] = value
|
||||
|
||||
@property
|
||||
def 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", self.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
|
||||
|
||||
@property
|
||||
def stream_lib(self):
|
||||
return self._settings.get("stream_lib", Defaults.STREAM_LIB.value)
|
||||
|
||||
@stream_lib.setter
|
||||
def stream_lib(self, value):
|
||||
self._settings["stream_lib"] = value
|
||||
|
||||
@property
|
||||
def fav_click_mode(self):
|
||||
return self._settings.get("fav_click_mode", Defaults.FAV_CLICK_MODE.value)
|
||||
|
||||
@fav_click_mode.setter
|
||||
def fav_click_mode(self, value):
|
||||
self._settings["fav_click_mode"] = value
|
||||
|
||||
@property
|
||||
def main_list_playback(self):
|
||||
return self._settings.get("main_list_playback", Defaults.MAIN_LIST_PLAYBACK.value)
|
||||
|
||||
@main_list_playback.setter
|
||||
def main_list_playback(self, value):
|
||||
self._settings["main_list_playback"] = value
|
||||
|
||||
# *********** EPG ************ #
|
||||
|
||||
@property
|
||||
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
|
||||
|
||||
# *********** FTP ************ #
|
||||
|
||||
@property
|
||||
def ftp_bookmarks(self):
|
||||
return self._cp_settings.get("ftp_bookmarks", [])
|
||||
|
||||
@ftp_bookmarks.setter
|
||||
def ftp_bookmarks(self, value):
|
||||
self._cp_settings["ftp_bookmarks"] = value
|
||||
|
||||
# ***** Program settings ***** #
|
||||
|
||||
@property
|
||||
def backup_before_save(self):
|
||||
return self._settings.get("backup_before_save", Defaults.BACKUP_BEFORE_SAVE.value)
|
||||
|
||||
@backup_before_save.setter
|
||||
def backup_before_save(self, value):
|
||||
self._settings["backup_before_save"] = value
|
||||
|
||||
@property
|
||||
def backup_before_downloading(self):
|
||||
return self._settings.get("backup_before_downloading", Defaults.BACKUP_BEFORE_DOWNLOADING.value)
|
||||
|
||||
@backup_before_downloading.setter
|
||||
def backup_before_downloading(self, value):
|
||||
self._settings["backup_before_downloading"] = value
|
||||
|
||||
@property
|
||||
def v5_support(self):
|
||||
return self._settings.get("v5_support", Defaults.V5_SUPPORT.value)
|
||||
|
||||
@v5_support.setter
|
||||
def v5_support(self, 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
|
||||
def http_api_support(self):
|
||||
return self._settings.get("http_api_support", Defaults.HTTP_API_SUPPORT.value)
|
||||
|
||||
@http_api_support.setter
|
||||
def http_api_support(self, value):
|
||||
self._settings["http_api_support"] = value
|
||||
|
||||
@property
|
||||
def enable_yt_dl(self):
|
||||
return self._settings.get("enable_yt_dl", Defaults.ENABLE_YT_DL.value)
|
||||
|
||||
@enable_yt_dl.setter
|
||||
def enable_yt_dl(self, 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
|
||||
def enable_send_to(self):
|
||||
return self._settings.get("enable_send_to", Defaults.ENABLE_SEND_TO.value)
|
||||
|
||||
@enable_send_to.setter
|
||||
def enable_send_to(self, value):
|
||||
self._settings["enable_send_to"] = value
|
||||
|
||||
@property
|
||||
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
|
||||
def show_srv_hints(self):
|
||||
""" Show short info as hints in the main services list. """
|
||||
return self._settings.get("show_srv_hints", True)
|
||||
|
||||
@show_srv_hints.setter
|
||||
def show_srv_hints(self, value):
|
||||
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 list_font(self):
|
||||
return self._settings.get("list_font", "")
|
||||
|
||||
@list_font.setter
|
||||
def list_font(self, value):
|
||||
self._settings["list_font"] = value
|
||||
|
||||
@property
|
||||
def list_picon_size(self):
|
||||
return self._settings.get("list_picon_size", Defaults.LIST_PICON_SIZE.value)
|
||||
|
||||
@list_picon_size.setter
|
||||
def list_picon_size(self, value):
|
||||
self._settings["list_picon_size"] = value
|
||||
|
||||
@property
|
||||
def tooltip_logo_size(self):
|
||||
return self._settings.get("tooltip_logo_size", Defaults.TOOLTIP_LOGO_SIZE.value)
|
||||
|
||||
@tooltip_logo_size.setter
|
||||
def tooltip_logo_size(self, value):
|
||||
self._settings["tooltip_logo_size"] = value
|
||||
|
||||
@property
|
||||
def use_colors(self):
|
||||
return self._settings.get("use_colors", Defaults.USE_COLORS.value)
|
||||
|
||||
@use_colors.setter
|
||||
def use_colors(self, value):
|
||||
self._settings["use_colors"] = value
|
||||
|
||||
@property
|
||||
def new_color(self):
|
||||
return self._settings.get("new_color", Defaults.NEW_COLOR.value)
|
||||
|
||||
@new_color.setter
|
||||
def new_color(self, value):
|
||||
self._settings["new_color"] = value
|
||||
|
||||
@property
|
||||
def extra_color(self):
|
||||
return self._settings.get("extra_color", Defaults.EXTRA_COLOR.value)
|
||||
|
||||
@extra_color.setter
|
||||
def extra_color(self, value):
|
||||
self._settings["extra_color"] = value
|
||||
|
||||
@property
|
||||
def dark_mode(self):
|
||||
if IS_DARWIN:
|
||||
import subprocess
|
||||
|
||||
cmd = ["defaults", "read", "-g", "AppleInterfaceStyle"]
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
|
||||
return "Dark" in str(p[0])
|
||||
|
||||
return self._settings.get("dark_mode", False)
|
||||
|
||||
@dark_mode.setter
|
||||
def dark_mode(self, value):
|
||||
self._settings["dark_mode"] = value
|
||||
|
||||
@property
|
||||
def display_picons(self):
|
||||
return self._settings.get("display_picons", True)
|
||||
|
||||
@display_picons.setter
|
||||
def display_picons(self, value):
|
||||
self._settings["display_picons"] = value
|
||||
|
||||
@property
|
||||
def alternate_layout(self):
|
||||
return self._settings.get("alternate_layout", IS_DARWIN)
|
||||
|
||||
@alternate_layout.setter
|
||||
def alternate_layout(self, value):
|
||||
self._settings["alternate_layout"] = value
|
||||
|
||||
@property
|
||||
def bq_details_first(self):
|
||||
return self._settings.get("bq_details_first", False)
|
||||
|
||||
@bq_details_first.setter
|
||||
def bq_details_first(self, value):
|
||||
self._settings["bq_details_first"] = 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 f"{HOME_PATH}{SEP}.themes{SEP}"
|
||||
|
||||
@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 f"{HOME_PATH}{SEP}.icons{SEP}"
|
||||
|
||||
@property
|
||||
def is_darwin(self):
|
||||
return IS_DARWIN
|
||||
|
||||
@property
|
||||
def force_external_themes(self):
|
||||
return self._settings.get("force_external_themes", False)
|
||||
|
||||
@force_external_themes.setter
|
||||
def force_external_themes(self, value):
|
||||
self._settings["force_external_themes"] = value
|
||||
|
||||
# *********** 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
|
||||
|
||||
# **************** Get-Set settings **************** #
|
||||
|
||||
@staticmethod
|
||||
def get_settings():
|
||||
if not os.path.isfile(CONFIG_FILE) or os.stat(CONFIG_FILE).st_size == 0:
|
||||
Settings.write_settings(Settings.get_default_settings())
|
||||
|
||||
with open(CONFIG_FILE, "r", encoding="utf-8") as config_file:
|
||||
try:
|
||||
return json.load(config_file)
|
||||
except ValueError as e:
|
||||
raise SettingsReadException(e)
|
||||
|
||||
@staticmethod
|
||||
def get_default_settings(profile_name="default"):
|
||||
def_settings = SettingsType.ENIGMA_2.get_default_settings()
|
||||
|
||||
return {
|
||||
"version": Settings.__VERSION,
|
||||
"default_profile": Defaults.DEFAULT_PROFILE.value,
|
||||
"profiles": {profile_name: def_settings},
|
||||
"v5_support": Defaults.V5_SUPPORT.value,
|
||||
"http_api_support": Defaults.HTTP_API_SUPPORT.value,
|
||||
"enable_yt_dl": Defaults.ENABLE_YT_DL.value,
|
||||
"enable_send_to": Defaults.ENABLE_SEND_TO.value,
|
||||
"use_colors": Defaults.USE_COLORS.value,
|
||||
"new_color": Defaults.NEW_COLOR.value,
|
||||
"extra_color": Defaults.EXTRA_COLOR.value,
|
||||
"fav_click_mode": Defaults.FAV_CLICK_MODE.value,
|
||||
"profile_folder_is_default": Defaults.PROFILE_FOLDER_DEFAULT.value,
|
||||
"records_path": Defaults.RECORDS_PATH.value
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_default_transcoding_presets():
|
||||
return {"720p TV/device": {"vcodec": "h264", "vb": "1500", "width": "1280", "height": "720", "acodec": "mp3",
|
||||
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"},
|
||||
"1080p TV/device": {"vcodec": "h264", "vb": "3500", "width": "1920", "height": "1080", "acodec": "mp3",
|
||||
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"}}
|
||||
|
||||
@staticmethod
|
||||
def write_settings(config):
|
||||
os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True)
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as config_file:
|
||||
json.dump(config, config_file, indent=" ")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
144
app/tools/epg.py
Normal file
144
app/tools/epg.py
Normal file
@@ -0,0 +1,144 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Module for working with epg.dat file. """
|
||||
import struct
|
||||
from datetime import datetime
|
||||
from xml.dom.minidom import parse, Node, Document
|
||||
|
||||
from app.eparser.ecommons import BqServiceType, BouquetService
|
||||
|
||||
|
||||
class EPG:
|
||||
|
||||
@staticmethod
|
||||
def get_epg_refs(path):
|
||||
""" The read algorithm was taken from the eEPGCache::load() function from this source:
|
||||
https://github.com/OpenPLi/enigma2/blob/develop/lib/dvb/epgcache.cpp#L955
|
||||
"""
|
||||
refs = set()
|
||||
|
||||
with open(path, mode="rb") as f:
|
||||
crc = struct.unpack("<I", f.read(4))[0]
|
||||
if crc != int(0x98765432):
|
||||
raise ValueError("Epg file has incorrect byte order!")
|
||||
|
||||
header = f.read(13).decode()
|
||||
if header == "ENIGMA_EPG_V7":
|
||||
epg_ver = 7
|
||||
elif header == "ENIGMA_EPG_V8":
|
||||
epg_ver = 8
|
||||
else:
|
||||
raise ValueError("Unsupported format of epd.dat file!")
|
||||
|
||||
channels_count = struct.unpack("<I", f.read(4))[0]
|
||||
_len_read_size = 3 if epg_ver == 8 else 2
|
||||
_type_read_str = f"<{'H' if epg_ver == 8 else 'B'}B"
|
||||
|
||||
for i in range(channels_count):
|
||||
sid, nid, tsid, events_size = struct.unpack("<IIII", f.read(16))
|
||||
service_id = f"{sid:X}:{tsid:X}:{nid:X}"
|
||||
|
||||
for j in range(events_size):
|
||||
_type, _len = struct.unpack(_type_read_str, f.read(_len_read_size))
|
||||
f.read(10)
|
||||
n_crc = (_len - 10) // 4
|
||||
if n_crc > 0:
|
||||
[f.read(4) for n in range(n_crc)]
|
||||
|
||||
refs.add(service_id)
|
||||
|
||||
return refs
|
||||
|
||||
|
||||
class ChannelsParser:
|
||||
_COMMENT = "File was created in DemonEditor"
|
||||
|
||||
@staticmethod
|
||||
def get_refs_from_xml(path):
|
||||
""" Returns tuple from references and description. """
|
||||
refs = []
|
||||
dom = parse(path)
|
||||
description = "".join(n.data + "\n" for n in dom.childNodes if n.nodeType == Node.COMMENT_NODE)
|
||||
|
||||
for elem in dom.getElementsByTagName("channels"):
|
||||
c_count = 0
|
||||
comment_count = 0
|
||||
current_data = ""
|
||||
|
||||
if elem.hasChildNodes():
|
||||
for n in elem.childNodes:
|
||||
if n.nodeType == Node.COMMENT_NODE:
|
||||
c_count += 1
|
||||
comment_count += 1
|
||||
txt = n.data.strip()
|
||||
if comment_count:
|
||||
comment_count -= 1
|
||||
else:
|
||||
ref_data = current_data.split(":")
|
||||
refs.append(BouquetService(name=txt,
|
||||
type=BqServiceType.DEFAULT,
|
||||
data="{}:{}:{}:{}".format(*ref_data[3:7]).upper(),
|
||||
num="{}:{}:{}".format(*ref_data[3:6]).upper()))
|
||||
|
||||
if n.hasChildNodes():
|
||||
for s_node in n.childNodes:
|
||||
if s_node.nodeType == Node.TEXT_NODE:
|
||||
comment_count -= 1
|
||||
current_data = s_node.data
|
||||
return refs, description
|
||||
|
||||
@staticmethod
|
||||
def write_refs_to_xml(path, services):
|
||||
header = '<?xml version="1.0" encoding="utf-8"?>\n<!-- {} -->\n<!-- {} -->\n<channels>\n'.format(
|
||||
"Created in DemonEditor.", datetime.now().strftime("%d.%m.%Y %H:%M:%S"))
|
||||
doc = Document()
|
||||
lines = [header]
|
||||
|
||||
for srv in services:
|
||||
srv_type = srv.type
|
||||
if srv_type is BqServiceType.IPTV:
|
||||
channel_child = doc.createElement("channel")
|
||||
channel_child.setAttribute("id", srv.name)
|
||||
data = srv.data.strip().split(":")
|
||||
channel_child.appendChild(doc.createTextNode(":".join(data[:10])))
|
||||
comment = doc.createComment(srv.name)
|
||||
lines.append(f"{channel_child.toxml()} {comment.toxml()}\n")
|
||||
elif srv_type is BqServiceType.MARKER:
|
||||
comment = doc.createComment(srv.name)
|
||||
lines.append(f"{comment.toxml()}\n")
|
||||
|
||||
lines.append("</channels>")
|
||||
doc.unlink()
|
||||
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.writelines(lines)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
567
app/tools/media.py
Normal file
567
app/tools/media.py
Normal file
@@ -0,0 +1,567 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
from gi.repository import Gdk, Gtk, GObject
|
||||
|
||||
from app.commons import run_task, log, _DATE_FORMAT, run_with_delay
|
||||
from app.settings import IS_DARWIN, IS_LINUX, IS_WIN
|
||||
|
||||
|
||||
class Player(Gtk.DrawingArea):
|
||||
""" Base player class. Also used as a factory. """
|
||||
|
||||
def __init__(self, mode, widget, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
GObject.signal_new("error", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("message", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("position", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("played", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("audio-track", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("subtitle-track", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
|
||||
self.connect("draw", self.on_draw)
|
||||
self.connect("motion-notify-event", self.on_mouse_motion)
|
||||
self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.POINTER_MOTION_MASK)
|
||||
widget.add(self)
|
||||
|
||||
parent = widget.get_parent()
|
||||
parent.connect("play", self.on_play)
|
||||
parent.connect("stop", self.on_stop)
|
||||
self.show()
|
||||
|
||||
def get_play_mode(self):
|
||||
pass
|
||||
|
||||
def play(self, mrl=None):
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
pass
|
||||
|
||||
def pause(self):
|
||||
pass
|
||||
|
||||
def set_time(self, time):
|
||||
pass
|
||||
|
||||
def release(self):
|
||||
pass
|
||||
|
||||
def is_playing(self):
|
||||
pass
|
||||
|
||||
def set_audio_track(self, track):
|
||||
pass
|
||||
|
||||
def get_audio_track(self):
|
||||
pass
|
||||
|
||||
def set_subtitle_track(self, track):
|
||||
pass
|
||||
|
||||
def set_aspect_ratio(self, ratio):
|
||||
pass
|
||||
|
||||
def get_instance(self, mode, widget):
|
||||
pass
|
||||
|
||||
def on_play(self, widget, url):
|
||||
self.play(url)
|
||||
|
||||
def on_stop(self, widget, state):
|
||||
self.stop()
|
||||
|
||||
def on_release(self, widget, state):
|
||||
self.release()
|
||||
|
||||
def get_window_handle(self):
|
||||
""" Returns the identifier [pointer] for the window.
|
||||
|
||||
Based on gtkvlc.py[get_window_pointer] example from here:
|
||||
https://github.com/oaubert/python-vlc/tree/master/examples
|
||||
"""
|
||||
if IS_LINUX:
|
||||
return self.get_window().get_xid()
|
||||
else:
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
libgdk = ctypes.CDLL("libgdk-3.0.dylib" if IS_DARWIN else "libgdk-3-0.dll")
|
||||
except OSError as e:
|
||||
log(f"{__class__.__name__}: Load library error: {e}")
|
||||
else:
|
||||
# https://gitlab.gnome.org/GNOME/pygobject/-/issues/112
|
||||
ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p
|
||||
ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object]
|
||||
gpointer = ctypes.pythonapi.PyCapsule_GetPointer(self.get_window().__gpointer__, None)
|
||||
get_pointer = libgdk.gdk_quartz_window_get_nsview if IS_DARWIN else libgdk.gdk_win32_window_get_handle
|
||||
get_pointer.restype = ctypes.c_void_p
|
||||
get_pointer.argtypes = [ctypes.c_void_p]
|
||||
|
||||
return get_pointer(gpointer)
|
||||
|
||||
def on_draw(self, widget, cr):
|
||||
""" Used for black background drawing in the player drawing area. """
|
||||
cr.set_source_rgb(0, 0, 0)
|
||||
cr.paint()
|
||||
|
||||
def on_mouse_motion(self, widget, event):
|
||||
display = widget.get_display()
|
||||
window = widget.get_window()
|
||||
cursor = Gdk.Cursor.new_from_name(display, "default")
|
||||
window.set_cursor(cursor)
|
||||
|
||||
self.hide_mouse_cursor(window, display)
|
||||
|
||||
@run_with_delay(3)
|
||||
def hide_mouse_cursor(self, window, display):
|
||||
cursor = Gdk.Cursor.new_for_display(display, Gdk.CursorType.BLANK_CURSOR)
|
||||
window.set_cursor(cursor)
|
||||
|
||||
@staticmethod
|
||||
def make(name, mode, widget):
|
||||
""" Factory method. We will not use a separate factory to return a specific implementation.
|
||||
|
||||
@param name: implementation name.
|
||||
@param mode: current player mode [Built-in or windowed].
|
||||
@param widget: parent of video widget.
|
||||
|
||||
Throws a NameError if there is no implementation for the given name.
|
||||
"""
|
||||
if name == "mpv":
|
||||
return MpvPlayer.get_instance(mode, widget)
|
||||
elif name == "gst":
|
||||
return GstPlayer.get_instance(mode, widget)
|
||||
elif name == "vlc":
|
||||
return VlcPlayer.get_instance(mode, widget)
|
||||
else:
|
||||
raise NameError(f"There is no such [{name}] implementation.")
|
||||
|
||||
|
||||
class MpvPlayer(Player):
|
||||
""" Simple wrapper for MPV media player.
|
||||
|
||||
Uses python-mvp [https://github.com/jaseg/python-mpv].
|
||||
"""
|
||||
__INSTANCE = None
|
||||
|
||||
def __init__(self, mode, widget):
|
||||
super().__init__(mode, widget)
|
||||
try:
|
||||
from app.tools import mpv
|
||||
|
||||
self._player = mpv.MPV(wid=str(self.get_window_handle()),
|
||||
input_default_bindings=False,
|
||||
input_cursor=False,
|
||||
cursor_autohide="no")
|
||||
except OSError as e:
|
||||
log(f"{__class__.__name__}: Load library error: {e}")
|
||||
raise ImportError("No libmpv is found. Check that it is installed!")
|
||||
else:
|
||||
self._mode = mode
|
||||
self._is_playing = False
|
||||
|
||||
@self._player.event_callback(mpv.MpvEventID.FILE_LOADED)
|
||||
def on_open(event):
|
||||
log("Starting playback...")
|
||||
self.emit("played", 0)
|
||||
|
||||
t_list = self._player._get_property("track-list")
|
||||
if t_list:
|
||||
# Audio tracks.
|
||||
a_tracks = filter(lambda t: t.get("type", "") == "audio", t_list)
|
||||
self.emit("audio-track", ((t.get("id", 1), t.get("lang", "Unknown")) for t in a_tracks))
|
||||
# Subtitle.
|
||||
sub_tracks = [(0, "no")]
|
||||
tracks = filter(lambda t: t.get("type", "") == "sub", t_list)
|
||||
[sub_tracks.append((t.get("id", 1), t.get("lang", "Unknown"))) for t in tracks]
|
||||
self.emit("subtitle-track", sub_tracks)
|
||||
|
||||
@self._player.event_callback(mpv.MpvEventID.END_FILE)
|
||||
def on_end(event):
|
||||
event = event.get("event", {})
|
||||
if event.get("reason", mpv.MpvEventEndFile.ERROR) == mpv.MpvEventEndFile.ERROR:
|
||||
log(f"Stream playback error: {event.get('error', mpv.ErrorCode.GENERIC)}")
|
||||
self.emit("error", "Can't Playback!")
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, mode, widget):
|
||||
if not cls.__INSTANCE:
|
||||
cls.__INSTANCE = MpvPlayer(mode, widget)
|
||||
return cls.__INSTANCE
|
||||
|
||||
def get_play_mode(self):
|
||||
return self._mode
|
||||
|
||||
def play(self, mrl=None):
|
||||
if not mrl:
|
||||
return
|
||||
|
||||
self._player.play(mrl)
|
||||
self._is_playing = True
|
||||
|
||||
def stop(self):
|
||||
self._player.stop()
|
||||
self._is_playing = True
|
||||
|
||||
def pause(self):
|
||||
pass
|
||||
|
||||
def set_time(self, time):
|
||||
pass
|
||||
|
||||
@run_task
|
||||
def release(self):
|
||||
self._player.terminate()
|
||||
self.__INSTANCE = None
|
||||
|
||||
def is_playing(self):
|
||||
return self._is_playing
|
||||
|
||||
def set_audio_track(self, track):
|
||||
self._player._set_property("aid", track)
|
||||
|
||||
def set_subtitle_track(self, track):
|
||||
self._player._set_property("sub", track)
|
||||
|
||||
def set_aspect_ratio(self, ratio):
|
||||
self._player._set_property("aspect", ratio or "-1.0")
|
||||
|
||||
|
||||
class GstPlayer(Player):
|
||||
""" Simple wrapper for GStreamer playbin. """
|
||||
|
||||
__INSTANCE = None
|
||||
|
||||
def __init__(self, mode, widget):
|
||||
super().__init__(mode, widget)
|
||||
try:
|
||||
import gi
|
||||
|
||||
gi.require_version("Gst", "1.0")
|
||||
gi.require_version("GstVideo", "1.0")
|
||||
from gi.repository import Gst, GstVideo
|
||||
# Initialization of GStreamer.
|
||||
Gst.init(sys.argv)
|
||||
except (OSError, ValueError) as e:
|
||||
log(f"{__class__.__name__}: Load library error: {e}")
|
||||
raise ImportError("No GStreamer is found. Check that it is installed!")
|
||||
else:
|
||||
self.STATE = Gst.State
|
||||
self.STAT_RETURN = Gst.StateChangeReturn
|
||||
|
||||
self._mode = mode
|
||||
self._is_playing = False
|
||||
self._player = Gst.ElementFactory.make("playbin", "player")
|
||||
self._player.set_window_handle(self.get_window_handle())
|
||||
|
||||
bus = self._player.get_bus()
|
||||
bus.add_signal_watch()
|
||||
bus.connect("message::error", self.on_error)
|
||||
bus.connect("message::state-changed", self.on_state_changed)
|
||||
bus.connect("message::eos", self.on_eos)
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, mode, widget):
|
||||
if not cls.__INSTANCE:
|
||||
cls.__INSTANCE = GstPlayer(mode, widget)
|
||||
return cls.__INSTANCE
|
||||
|
||||
def get_play_mode(self):
|
||||
return self._mode
|
||||
|
||||
def play(self, mrl=None):
|
||||
self._player.set_state(self.STATE.READY)
|
||||
if not mrl:
|
||||
return
|
||||
|
||||
self._player.set_property("uri", mrl)
|
||||
|
||||
log(f"Setting the URL for playback: {mrl}")
|
||||
ret = self._player.set_state(self.STATE.PLAYING)
|
||||
|
||||
if ret == self.STAT_RETURN.FAILURE:
|
||||
msg = f"ERROR: Unable to set the 'PLAYING' state for '{mrl}'."
|
||||
log(msg)
|
||||
self.emit("error", msg)
|
||||
else:
|
||||
self.emit("played", 0)
|
||||
self._is_playing = True
|
||||
|
||||
def stop(self):
|
||||
log("Stop playback...")
|
||||
self._player.set_state(self.STATE.READY)
|
||||
self._is_playing = False
|
||||
|
||||
def pause(self):
|
||||
self._player.set_state(self.STATE.PAUSED)
|
||||
|
||||
def set_time(self, time):
|
||||
pass
|
||||
|
||||
@run_task
|
||||
def release(self):
|
||||
self._is_playing = False
|
||||
self._player.set_state(self.STATE.NULL)
|
||||
self.__INSTANCE = None
|
||||
|
||||
def set_mrl(self, mrl):
|
||||
self._player.set_property("uri", mrl)
|
||||
|
||||
def is_playing(self):
|
||||
return self._is_playing
|
||||
|
||||
def on_error(self, bus, msg):
|
||||
err, dbg = msg.parse_error()
|
||||
log(err)
|
||||
self.emit("error", "Can't Playback!")
|
||||
|
||||
def on_state_changed(self, bus, msg):
|
||||
if not msg.src == self._player:
|
||||
# Not from the player.
|
||||
return
|
||||
|
||||
old_state, new_state, pending = msg.parse_state_changed()
|
||||
if new_state is self.STATE.PLAYING:
|
||||
log("Starting playback...")
|
||||
self.emit("played", 0)
|
||||
self.get_stream_info()
|
||||
|
||||
def on_eos(self, bus, msg):
|
||||
""" Called when an end-of-stream message appears. """
|
||||
self._player.set_state(self.STATE.READY)
|
||||
self._is_playing = False
|
||||
|
||||
def get_stream_info(self):
|
||||
log("Getting stream info...")
|
||||
nr_video = self._player.get_property("n-video")
|
||||
for i in range(nr_video):
|
||||
# Retrieve the stream's video tags.
|
||||
tags = self._player.emit("get-video-tags", i)
|
||||
if tags:
|
||||
_, cod = tags.get_string("video-codec")
|
||||
log(f"Video codec: {cod or 'unknown'}")
|
||||
|
||||
nr_audio = self._player.get_property("n-audio")
|
||||
for i in range(nr_audio):
|
||||
# Retrieve the stream's video tags.
|
||||
tags = self._player.emit("get-audio-tags", i)
|
||||
if tags:
|
||||
_, cod = tags.get_string("audio-codec")
|
||||
log(f"Audio codec: {cod or 'unknown'}")
|
||||
|
||||
|
||||
class VlcPlayer(Player):
|
||||
""" Simple wrapper for VLC media player.
|
||||
|
||||
Uses python-vlc [https://github.com/oaubert/python-vlc].
|
||||
"""
|
||||
|
||||
__VLC_INSTANCE = None
|
||||
|
||||
def __init__(self, mode, widget):
|
||||
super().__init__(mode, widget)
|
||||
try:
|
||||
if IS_WIN:
|
||||
os.add_dll_directory(r"C:\Program Files\VideoLAN\VLC")
|
||||
|
||||
from app.tools import vlc
|
||||
from app.tools.vlc import EventType
|
||||
|
||||
args = f"--quiet {'' if IS_DARWIN else '--no-xlib'}"
|
||||
self._player = vlc.Instance(args).media_player_new()
|
||||
vlc.libvlc_video_set_key_input(self._player, False)
|
||||
vlc.libvlc_video_set_mouse_input(self._player, False)
|
||||
except (OSError, AttributeError, NameError) as e:
|
||||
log(f"{__class__.__name__}: Load library error: {e}")
|
||||
raise ImportError("No VLC is found. Check that it is installed!")
|
||||
else:
|
||||
self._mode = mode
|
||||
self._is_playing = False
|
||||
|
||||
ev_mgr = self._player.event_manager()
|
||||
ev_mgr.event_attach(EventType.MediaPlayerVout, self.on_playback_start)
|
||||
ev_mgr.event_attach(EventType.MediaPlayerTimeChanged,
|
||||
lambda et: self.emit("position", self._player.get_time()))
|
||||
ev_mgr.event_attach(EventType.MediaPlayerEncounteredError, lambda et: self.emit("error", "Can't Playback!"))
|
||||
|
||||
self.init_video_widget(widget)
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls, mode, widget):
|
||||
if not cls.__VLC_INSTANCE:
|
||||
cls.__VLC_INSTANCE = VlcPlayer(mode, widget)
|
||||
return cls.__VLC_INSTANCE
|
||||
|
||||
def get_play_mode(self):
|
||||
return self._mode
|
||||
|
||||
def play(self, mrl=None):
|
||||
if mrl:
|
||||
self._player.set_mrl(mrl)
|
||||
self._player.play()
|
||||
self._is_playing = True
|
||||
|
||||
def stop(self):
|
||||
if self._is_playing:
|
||||
self._player.stop()
|
||||
self._is_playing = False
|
||||
|
||||
def pause(self):
|
||||
self._player.pause()
|
||||
|
||||
def set_time(self, time):
|
||||
self._player.set_time(time)
|
||||
|
||||
@run_task
|
||||
def release(self):
|
||||
if self._player:
|
||||
self._is_playing = False
|
||||
self._player.stop()
|
||||
self._player.release()
|
||||
self.__VLC_INSTANCE = None
|
||||
|
||||
def set_mrl(self, mrl):
|
||||
self._player.set_mrl(mrl)
|
||||
|
||||
def is_playing(self):
|
||||
return self._is_playing
|
||||
|
||||
def set_audio_track(self, track):
|
||||
self._player.audio_set_track(track)
|
||||
|
||||
def get_audio_track(self):
|
||||
return self._player.audio_get_track()
|
||||
|
||||
def set_subtitle_track(self, track):
|
||||
self._player.video_set_spu(track)
|
||||
|
||||
def set_aspect_ratio(self, ratio):
|
||||
self._player.video_set_aspect_ratio(ratio)
|
||||
|
||||
def on_playback_start(self, event):
|
||||
self.emit("played", self._player.get_media().get_duration())
|
||||
# Audio tracks.
|
||||
a_desc = self._player.audio_get_track_description()
|
||||
self.emit("audio-track", [(t[0], t[1].decode(encoding="utf-8", errors="ignore")) for t in a_desc])
|
||||
# Subtitle.
|
||||
s_desc = self._player.video_get_spu_description()
|
||||
self.emit("subtitle-track", [(s[0], s[1].decode(encoding="utf-8", errors="ignore")) for s in s_desc])
|
||||
|
||||
def init_video_widget(self, widget):
|
||||
if IS_LINUX:
|
||||
self._player.set_xwindow(self.get_window_handle())
|
||||
elif IS_DARWIN:
|
||||
self._player.set_nsobject(self.get_window_handle())
|
||||
else:
|
||||
self._player.set_hwnd(self.get_window_handle())
|
||||
|
||||
|
||||
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:
|
||||
if IS_WIN:
|
||||
os.add_dll_directory(r"C:\Program Files\VideoLAN\VLC")
|
||||
|
||||
from app.tools import vlc
|
||||
from app.tools.vlc import EventType
|
||||
except OSError as e:
|
||||
log(f"{__class__.__name__}: Load library error: {e}")
|
||||
raise ImportError
|
||||
else:
|
||||
self._settings = settings
|
||||
self._is_record = False
|
||||
args = f"--quiet {'' if IS_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)
|
||||
d_now = d_now.replace(" ", "_").replace(":", "-") if IS_WIN else d_now.replace(" ", "_")
|
||||
path = f"{path}{name.replace(' ', '_')}_{d_now}"
|
||||
cmd = self.get_transcoding_cmd(path) if self._settings.activate_transcoding else self._CMD.format(path)
|
||||
media = self._recorder.get_instance().media_new(url, cmd)
|
||||
media.get_mrl()
|
||||
|
||||
self._recorder.set_media(media)
|
||||
self._is_record = True
|
||||
self._recorder.play()
|
||||
log(f"Record started {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(f"{k}={v}" for k, v in prs.items()), path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
1941
app/tools/mpv.py
Normal file
1941
app/tools/mpv.py
Normal file
File diff suppressed because it is too large
Load Diff
534
app/tools/picons.py
Normal file
534
app/tools/picons.py
Normal file
@@ -0,0 +1,534 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from collections import namedtuple
|
||||
from html.parser import HTMLParser
|
||||
|
||||
import requests
|
||||
|
||||
from app.commons import run_task, log
|
||||
from app.settings import SettingsType, IS_LINUX, IS_WIN, IS_DARWIN, GTK_PATH
|
||||
from .satellites import _HEADERS
|
||||
|
||||
_ENIGMA2_PICON_KEY = "{:X}:{:X}:{}"
|
||||
_NEUTRINO_PICON_KEY = "{:x}{:04x}{:04x}.png"
|
||||
|
||||
Provider = namedtuple("Provider", ["logo", "name", "pos", "url", "on_id", "ssid", "single", "selected"])
|
||||
Picon = namedtuple("Picon", ["ref", "ssid"])
|
||||
|
||||
|
||||
class PiconsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PiconsCzDownloader:
|
||||
""" The main class for loading picons from the https://picon.cz/ source (by Chocholoušek). """
|
||||
|
||||
_PERM_URL = "https://picon.cz/download/7337"
|
||||
_BASE_URL = "https://picon.cz/download/"
|
||||
_BASE_LOGO_URL = "https://picon.cz/picon/0/"
|
||||
_HEADER = {"User-Agent": "DemonEditor/2.2.3", "Referer": ""}
|
||||
_LINK_PATTERN = re.compile(r"((.*)-\d+x\d+)-(.*)_by_chocholousek.7z$")
|
||||
_FILE_PATTERN = re.compile(b"\\s+(1_.*\\.png).*")
|
||||
|
||||
def __init__(self, picon_ids=set(), appender=log):
|
||||
self._perm_links = {}
|
||||
self._providers = {}
|
||||
self._provider_logos = {}
|
||||
self._picon_ids = picon_ids
|
||||
self._appender = appender
|
||||
|
||||
def init(self):
|
||||
""" Initializes dict with values: download_id -> perm link and provider data. """
|
||||
if self._perm_links:
|
||||
return
|
||||
|
||||
self._HEADER["Referer"] = self._PERM_URL
|
||||
|
||||
with requests.get(url=self._PERM_URL, headers=self._HEADER, stream=True) as request:
|
||||
if request.reason == "OK":
|
||||
logo_map = self.get_logos_map()
|
||||
name_map = self.get_name_map()
|
||||
|
||||
for line in request.iter_lines():
|
||||
data = line.decode(encoding="utf-8", errors="ignore").split(maxsplit=1)
|
||||
if len(data) != 2:
|
||||
continue
|
||||
|
||||
l_id, perm_link = data
|
||||
self._perm_links[str(l_id)] = str(perm_link)
|
||||
data = re.match(self._LINK_PATTERN, perm_link)
|
||||
if data:
|
||||
sat_pos = data.group(3)
|
||||
# Logo url.
|
||||
logo = logo_map.get(data.group(2), None)
|
||||
l_name = name_map.get(sat_pos, None) or sat_pos.replace(".", "")
|
||||
logo_url = f"{self._BASE_LOGO_URL}{logo}/{l_name}.png" if logo else None
|
||||
|
||||
prv = Provider(None, data.group(1), sat_pos, self._BASE_URL + l_id, l_id, logo_url, None, False)
|
||||
if sat_pos in self._providers:
|
||||
self._providers[sat_pos].append(prv)
|
||||
else:
|
||||
self._providers[sat_pos] = [prv]
|
||||
else:
|
||||
log(f"{self.__class__.__name__} [get permalinks] error: {request.reason}")
|
||||
raise PiconsError(request.reason)
|
||||
|
||||
@property
|
||||
def providers(self):
|
||||
return self._providers
|
||||
|
||||
def get_sat_providers(self, url):
|
||||
return self._providers.get(url, [])
|
||||
|
||||
def download(self, provider, picons_path, picon_ids=None):
|
||||
self._HEADER["Referer"] = provider.url
|
||||
with requests.get(url=provider.url, headers=self._HEADER, stream=True) as request:
|
||||
if request.reason == "OK":
|
||||
dest = f"{picons_path}{provider.on_id}.7z"
|
||||
self._appender(f"Downloading: {provider.url}\n")
|
||||
with open(dest, mode="bw") as f:
|
||||
for data in request.iter_content(chunk_size=1024):
|
||||
f.write(data)
|
||||
self._appender(f"Extracting: {provider.on_id}\n")
|
||||
self.extract(dest, picons_path, picon_ids)
|
||||
else:
|
||||
log(f"{self.__class__.__name__} [download] error: {request.reason}")
|
||||
|
||||
def extract(self, src, dest, picon_ids=None):
|
||||
""" Extracts 7z archives. """
|
||||
# TODO: think about https://github.com/miurahr/py7zr
|
||||
exe = "7z"
|
||||
if IS_DARWIN and GTK_PATH:
|
||||
exe = "./7zr"
|
||||
|
||||
if IS_LINUX and not os.path.isfile(f"/usr/bin/{exe}"):
|
||||
raise PiconsError("7-zip [7z] archiver not found!")
|
||||
|
||||
if IS_WIN:
|
||||
exe = f"{exe}.exe" if GTK_PATH else f"C:{os.sep}Program Files{os.sep}7-Zip{os.sep}{exe}.exe"
|
||||
if not os.path.isfile(exe):
|
||||
raise PiconsError("7-Zip executable not found!")
|
||||
|
||||
cmd = [exe, "l", src]
|
||||
try:
|
||||
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
|
||||
if err:
|
||||
log(f"{self.__class__.__name__} [extract] error: {err}")
|
||||
raise PiconsError(err)
|
||||
except OSError as e:
|
||||
log(f"{self.__class__.__name__} [extract] error: {e}")
|
||||
raise PiconsError(e)
|
||||
|
||||
is_filter = bool(picon_ids)
|
||||
ids = picon_ids or self._picon_ids
|
||||
to_extract = []
|
||||
|
||||
for o in re.finditer(self._FILE_PATTERN, out):
|
||||
p_id = o.group(1).decode("utf-8", errors="ignore")
|
||||
if p_id in ids:
|
||||
to_extract.append(p_id)
|
||||
|
||||
if is_filter and not to_extract:
|
||||
if os.path.isfile(src):
|
||||
os.remove(src)
|
||||
raise PiconsError("No matching picons found!")
|
||||
|
||||
cmd = [exe, "e", src, "-o{}".format(dest), "-y", "-r"]
|
||||
cmd.extend(to_extract)
|
||||
try:
|
||||
out, err = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
|
||||
if err:
|
||||
log(f"{self.__class__.__name__} [extract] error: {err}")
|
||||
raise PiconsError(err)
|
||||
else:
|
||||
if os.path.isfile(src):
|
||||
os.remove(src)
|
||||
except OSError as e:
|
||||
log(e)
|
||||
raise PiconsError(e)
|
||||
|
||||
def get_logo_data(self, url):
|
||||
""" Returns the logo data if present. """
|
||||
return self._provider_logos.get(url, None)
|
||||
|
||||
def get_provider_logo(self, url):
|
||||
""" Retrieves package logo. """
|
||||
# Getting package logo.
|
||||
logo = self._provider_logos.get(url, None)
|
||||
if logo:
|
||||
return logo
|
||||
|
||||
try:
|
||||
with requests.get(url=url, stream=True) as logo_request:
|
||||
if logo_request.reason == "OK":
|
||||
data = logo_request.content
|
||||
self._provider_logos[url] = data
|
||||
return data
|
||||
else:
|
||||
log(f"Downloading package logo error: {logo_request.reason}")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
log(f"{self.__class__.__name__} error [get provider logo]: {e}")
|
||||
|
||||
def get_logos_map(self):
|
||||
return {"piconblack": "b50",
|
||||
"picontransparent": "t50",
|
||||
"piconwhite": "w50",
|
||||
"piconmirrorglass": "mr100",
|
||||
"piconnoName": "n100",
|
||||
"piconsrhd": "srhd100",
|
||||
"piconfreezeframe": "ff220",
|
||||
"piconfreezewhite": "fw100",
|
||||
"piconpoolrainbow": "r100",
|
||||
"piconsimpleblack": "s220",
|
||||
"piconjustblack": "jb220",
|
||||
"picondirtypaper": "dp220",
|
||||
"picongray": "g400",
|
||||
"piconmonochrom": "m220",
|
||||
"picontransparentwhite": "tw100",
|
||||
"picontransparentdark": "td220",
|
||||
"piconoled": "o96",
|
||||
"piconblack80": "b50",
|
||||
"piconblack3d": "b50",
|
||||
"piconwin11": "win11220"
|
||||
}
|
||||
|
||||
def get_name_map(self):
|
||||
return {"antiksat": "ANTIK",
|
||||
"digiczsk": "DIGI",
|
||||
"DTTitaly": "picon_trs-it",
|
||||
"dvbtCZSK": "picon_trs",
|
||||
"PolandDTT": "picon_trs-pl",
|
||||
"freeSAT": "UPC DIRECT",
|
||||
"orangesat": "ORANGE TV",
|
||||
"skylink": "M7 GROUP",
|
||||
}
|
||||
|
||||
|
||||
class PiconsParser(HTMLParser):
|
||||
""" Parser for package html page. (https://www.lyngsat.com/packages/*provider-name*.html) """
|
||||
_BASE_URL = "https://www.lyngsat.com"
|
||||
|
||||
def __init__(self, entities=False, separator=' ', single=None):
|
||||
|
||||
HTMLParser.__init__(self)
|
||||
|
||||
self._parse_html_entities = entities
|
||||
self._separator = separator
|
||||
self._single = single
|
||||
self._is_td = False
|
||||
self._is_th = False
|
||||
self._current_row = []
|
||||
self._current_cell = []
|
||||
self.picons = []
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == "td":
|
||||
self._is_td = True
|
||||
if tag == "th":
|
||||
self._is_th = True
|
||||
if tag == "img":
|
||||
self._current_row.append(attrs[0][1])
|
||||
|
||||
def handle_data(self, data):
|
||||
""" Save content to a cell """
|
||||
if self._is_td or self._is_th:
|
||||
self._current_cell.append(data.strip())
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == "td":
|
||||
self._is_td = False
|
||||
elif tag == "th":
|
||||
self._is_th = False
|
||||
|
||||
if tag in ("td", "th"):
|
||||
final_cell = self._separator.join(self._current_cell).strip()
|
||||
self._current_row.append(final_cell)
|
||||
self._current_cell = []
|
||||
elif tag == "tr":
|
||||
row = self._current_row
|
||||
ln = len(row)
|
||||
|
||||
if self._single and ln == 4 and row[0].startswith("/logo/"):
|
||||
self.picons.append(Picon(row[0].strip(), "0"))
|
||||
else:
|
||||
if ln > 8:
|
||||
url = None
|
||||
if row[2].startswith("/logo/"):
|
||||
url = row[2]
|
||||
|
||||
if url and row[0].isdigit():
|
||||
self.picons.append(Picon(url, row[0]))
|
||||
|
||||
self._current_row = []
|
||||
|
||||
def error(self, message):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def parse(provider, picons_path, picon_ids, s_type=SettingsType.ENIGMA_2):
|
||||
""" Returns tuple(url, picon file name) list. """
|
||||
req = requests.get(provider.url, timeout=5)
|
||||
if req.status_code == 200:
|
||||
logo_data = req.text
|
||||
else:
|
||||
log("Provider picons downloading error: {} {}".format(provider.url, req.reason))
|
||||
return
|
||||
|
||||
on_id, pos, ssid, single = provider.on_id, provider.pos, provider.ssid, provider.single
|
||||
neg_pos = pos.endswith("W")
|
||||
pos = int("".join(c for c in pos if c.isdigit()))
|
||||
# For negative (West) positions 3600 - numeric position value!!!
|
||||
if neg_pos:
|
||||
pos = 3600 - pos
|
||||
|
||||
parser = PiconsParser(single=provider.single)
|
||||
parser.reset()
|
||||
parser.feed(logo_data)
|
||||
picons = parser.picons
|
||||
picons_data = []
|
||||
|
||||
if picons:
|
||||
for p in picons:
|
||||
try:
|
||||
if single:
|
||||
on_id, freq = on_id.strip().split("::")
|
||||
namespace = "{:X}{:X}".format(int(pos), int(freq))
|
||||
else:
|
||||
namespace = "{:X}0000".format(int(pos))
|
||||
|
||||
if single and not ssid.isdigit():
|
||||
ssid = "".join(c for c in ssid if c.isdigit()) or "0"
|
||||
name = PiconsParser.format(ssid if single else p.ssid, on_id, namespace, picon_ids, s_type)
|
||||
p_name = picons_path + (name if name else os.path.basename(p.ref))
|
||||
picons_data.append(("{}{}".format(PiconsParser._BASE_URL, p.ref), p_name))
|
||||
except (TypeError, ValueError) as e:
|
||||
msg = "Picons format parse error: {}".format(p) + "\n" + str(e)
|
||||
log(msg)
|
||||
|
||||
return picons_data
|
||||
|
||||
@staticmethod
|
||||
def format(ssid, on_id, namespace, picon_ids, s_type):
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
return picon_ids.get(_ENIGMA2_PICON_KEY.format(int(ssid), int(on_id), namespace), None)
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
tr_id = int(ssid[:-2] if len(ssid) < 4 else ssid[:2])
|
||||
return _NEUTRINO_PICON_KEY.format(tr_id, int(on_id), int(ssid))
|
||||
else:
|
||||
return "{}.png".format(ssid)
|
||||
|
||||
|
||||
class ProviderParser(HTMLParser):
|
||||
""" Parser for satellite html page. (https://www.lyngsat.com/*sat-name*.html) """
|
||||
|
||||
_POSITION_PATTERN = re.compile("at\s\d+\..*(?:E|W)']")
|
||||
_ONID_TID_PATTERN = re.compile("^\d+-\d+.*")
|
||||
_TRANSPONDER_FREQUENCY_PATTERN = re.compile("^\d+ [HVLR]+")
|
||||
_DOMAINS = {"/tvchannels/", "/radiochannels/", "/packages/", "/logo/"}
|
||||
_BASE_URL = "https://www.lyngsat.com"
|
||||
|
||||
def __init__(self, entities=False, separator=' '):
|
||||
|
||||
HTMLParser.__init__(self)
|
||||
self.convert_charrefs = False
|
||||
|
||||
self._parse_html_entities = entities
|
||||
self._separator = separator
|
||||
self._is_td = False
|
||||
self._is_th = False
|
||||
self._is_onid_tid = False
|
||||
self._is_provider = False
|
||||
self._current_row = []
|
||||
self._current_cell = []
|
||||
self.rows = []
|
||||
self._ids = set()
|
||||
self._prv_names = set()
|
||||
self._positon = None
|
||||
self._on_id = None
|
||||
self._freq = None
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == 'td':
|
||||
self._is_td = True
|
||||
if tag == 'tr':
|
||||
self._is_th = True
|
||||
if tag == "img":
|
||||
if attrs[0][1].startswith("/logo/"):
|
||||
self._current_row.append(attrs[0][1])
|
||||
if tag == "a":
|
||||
url = attrs[0][1]
|
||||
if any(d in url for d in self._DOMAINS):
|
||||
self._current_row.append(url)
|
||||
|
||||
def handle_data(self, data):
|
||||
""" Save content to a cell """
|
||||
if self._is_td or self._is_th:
|
||||
self._current_cell.append(data.strip())
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == 'td':
|
||||
self._is_td = False
|
||||
elif tag == 'tr':
|
||||
self._is_th = False
|
||||
|
||||
if tag in ('td', 'th'):
|
||||
final_cell = self._separator.join(self._current_cell).strip()
|
||||
self._current_row.append(final_cell)
|
||||
self._current_cell = []
|
||||
elif tag == 'tr':
|
||||
row = self._current_row
|
||||
# Satellite position
|
||||
if not self._positon:
|
||||
pos = re.findall(self._POSITION_PATTERN, str(row))
|
||||
if pos:
|
||||
self._positon = "".join(c for c in str(pos) if c.isdigit() or c in ".EW")
|
||||
|
||||
len_row = len(row)
|
||||
if len_row > 2:
|
||||
m = self._TRANSPONDER_FREQUENCY_PATTERN.match(row[0])
|
||||
if m:
|
||||
self._freq = m.group().split()[0]
|
||||
|
||||
if len_row > 12:
|
||||
# Providers
|
||||
name = row[5]
|
||||
self._prv_names.add(name)
|
||||
m = self._ONID_TID_PATTERN.match(str(row[-5]))
|
||||
if m:
|
||||
on_id, tid = m.group().split("-")
|
||||
if on_id not in self._ids:
|
||||
self._on_id = on_id
|
||||
row[-2] = on_id
|
||||
self._ids.add(on_id)
|
||||
row[0] = self._positon
|
||||
if name + on_id not in self._prv_names:
|
||||
self._prv_names.add(name + on_id)
|
||||
logo_data = None
|
||||
if row[2].startswith("/logo/"):
|
||||
req = requests.get(self._BASE_URL + row[2], timeout=5)
|
||||
if req.status_code == 200:
|
||||
logo_data = req.content
|
||||
else:
|
||||
log("Downloading provider logo error: {}".format(req.reason))
|
||||
self.rows.append(Provider(logo=logo_data, name=name, pos=self._positon, url=row[6], on_id=on_id,
|
||||
ssid=None, single=False, selected=True))
|
||||
elif 6 < len_row < 12:
|
||||
# Single services
|
||||
name, url, ssid = None, None, None
|
||||
if row[0].startswith("http"):
|
||||
name, url, ssid = row[1], row[0], row[0]
|
||||
elif row[1].startswith("http"):
|
||||
name, url, ssid = row[2], row[1], row[0]
|
||||
|
||||
if name and url:
|
||||
on_id = "{}::{}".format(self._on_id if self._on_id else "1", self._freq)
|
||||
self.rows.append(Provider(logo=None, name=name, pos=self._positon, url=url, on_id=on_id,
|
||||
ssid=ssid, single=True, selected=False))
|
||||
|
||||
self._current_row = []
|
||||
|
||||
def error(self, message):
|
||||
pass
|
||||
|
||||
def reset(self):
|
||||
super().reset()
|
||||
|
||||
|
||||
def parse_providers(url):
|
||||
""" Returns a list of providers sorted by logo [single channels after providers]. """
|
||||
parser = ProviderParser()
|
||||
|
||||
request = requests.get(url=url, headers=_HEADERS)
|
||||
if request.status_code == 200:
|
||||
parser.feed(request.text)
|
||||
else:
|
||||
log("Parse providers error [{}]: {}".format(url, request.reason))
|
||||
|
||||
def srt(p):
|
||||
if p.logo is None:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
providers = parser.rows
|
||||
providers.sort(key=srt)
|
||||
|
||||
return providers
|
||||
|
||||
|
||||
def download_picon(src_url, dest_path, callback):
|
||||
""" Downloads and saves the picon to file. """
|
||||
err_msg = "Picon download error: {} [{}]"
|
||||
timeout = (3, 5) # connect and read timeouts
|
||||
|
||||
if callback:
|
||||
callback("Downloading: {}.\n".format(os.path.basename(dest_path)))
|
||||
|
||||
req = requests.get(src_url, timeout=timeout, stream=True)
|
||||
if req.status_code != 200:
|
||||
err_msg = err_msg.format(src_url, req.reason)
|
||||
log(err_msg)
|
||||
if callback:
|
||||
callback(err_msg + "\n")
|
||||
else:
|
||||
try:
|
||||
with open(dest_path, "wb") as f:
|
||||
for chunk in req:
|
||||
f.write(chunk)
|
||||
except OSError as e:
|
||||
err_msg = "Saving picon [{}] error: {}".format(dest_path, e)
|
||||
log(err_msg)
|
||||
if callback:
|
||||
callback(err_msg + "\n")
|
||||
|
||||
|
||||
@run_task
|
||||
def convert_to(src_path, dest_path, s_type, callback, done_callback):
|
||||
""" Converts names format of picons.
|
||||
|
||||
Copies resulting files from src to dest and writes state to callback.
|
||||
"""
|
||||
pattern = "/*_0_0_0.png" if s_type is SettingsType.ENIGMA_2 else "/*.png"
|
||||
for file in glob.glob(src_path + pattern):
|
||||
base_name = os.path.basename(file)
|
||||
pic_data = base_name.rstrip(".png").split("_")
|
||||
dest_file = _NEUTRINO_PICON_KEY.format(int(pic_data[4], 16), int(pic_data[5], 16), int(pic_data[3], 16))
|
||||
dest = "{}/{}".format(dest_path, dest_file)
|
||||
callback('Converting "{}" to "{}"\n'.format(base_name, dest_file))
|
||||
shutil.copyfile(file, dest)
|
||||
|
||||
done_callback()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
761
app/tools/satellites.py
Normal file
761
app/tools/satellites.py
Normal file
@@ -0,0 +1,761 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Module for downloading satellites, transponders and services from the Web.
|
||||
|
||||
Sources: www.flysat.com, www.lyngsat.com, www.kingofsat.net.
|
||||
Replaces or updates the current satellites.xml file.
|
||||
"""
|
||||
import re
|
||||
from enum import Enum
|
||||
from html.parser import HTMLParser
|
||||
|
||||
import requests
|
||||
|
||||
from app.commons import log
|
||||
from app.eparser import Satellite, Transponder, is_transponder_valid
|
||||
from app.eparser.ecommons import (PLS_MODE, get_key_by_value, FEC, SYSTEM, POLARIZATION, MODULATION, SERVICE_TYPE,
|
||||
Service, CAS)
|
||||
|
||||
_HEADERS = {"User-Agent": "Mozilla/5.0 (Linux x86_64; rv:92.0) Gecko/20100101 Firefox/92.0"}
|
||||
_TIMEOUT = 10
|
||||
|
||||
|
||||
class SatelliteSource(Enum):
|
||||
FLYSAT = ("https://www.flysat.com/en/satellitelist",)
|
||||
LYNGSAT = ("https://www.lyngsat.com/asia.html", "https://www.lyngsat.com/europe.html",
|
||||
"https://www.lyngsat.com/atlantic.html", "https://www.lyngsat.com/america.html")
|
||||
KINGOFSAT = ("https://en.kingofsat.net/satellites.php",)
|
||||
|
||||
@staticmethod
|
||||
def get_sources(src):
|
||||
return src.value
|
||||
|
||||
|
||||
class Cell:
|
||||
""" Cell representation for table parsers. """
|
||||
__slots__ = ["_text", "_url", "_img"]
|
||||
|
||||
def __init__(self, text=None, link=None, img=None):
|
||||
self._text = text
|
||||
self._url = link
|
||||
self._img = img
|
||||
|
||||
def __repr__(self):
|
||||
return f"Cell({self._text}, {self._url}, {self._img})"
|
||||
|
||||
def __str__(self):
|
||||
return f"<Cell(text={self._text}, link={self._url}, img={self._img})>"
|
||||
|
||||
def __iter__(self):
|
||||
return (x for x in (self._text, self._url, self._img))
|
||||
|
||||
def __len__(self):
|
||||
return 3
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return self._text
|
||||
|
||||
@text.setter
|
||||
def text(self, value):
|
||||
self._text = value
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return self._url
|
||||
|
||||
@url.setter
|
||||
def url(self, value):
|
||||
self._url = value
|
||||
|
||||
@property
|
||||
def img(self):
|
||||
return self._img
|
||||
|
||||
@img.setter
|
||||
def img(self, value):
|
||||
self._img = value
|
||||
|
||||
|
||||
class SatellitesParser(HTMLParser):
|
||||
""" Parser for satellite html page. """
|
||||
|
||||
POS_PAT = re.compile(r".*?(\d+\.\d°?[EW]).*")
|
||||
|
||||
def __init__(self, source=SatelliteSource.FLYSAT, entities=False, separator=' '):
|
||||
|
||||
HTMLParser.__init__(self)
|
||||
|
||||
self._parse_html_entities = entities
|
||||
self._separator = separator
|
||||
self._is_td = False
|
||||
self._is_th = False
|
||||
self._is_provider = False
|
||||
self._current_row = []
|
||||
self._current_cell = []
|
||||
self._rows = []
|
||||
self._source = source
|
||||
self.pls_modes = {v: k for k, v in PLS_MODE.items()}
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == "td":
|
||||
self._is_td = True
|
||||
if tag == "tr":
|
||||
self._is_th = True
|
||||
if tag == "a":
|
||||
for atr in attrs:
|
||||
if atr[0] == "href":
|
||||
self._current_row.append(atr[1])
|
||||
|
||||
def handle_data(self, data):
|
||||
""" Save content to a cell """
|
||||
if self._is_td or self._is_th:
|
||||
self._current_cell.append(data.strip())
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == "td":
|
||||
self._is_td = False
|
||||
elif tag == "tr":
|
||||
self._is_th = False
|
||||
|
||||
if tag in ("td", "th"):
|
||||
final_cell = self._separator.join(self._current_cell).strip()
|
||||
self._current_row.append(final_cell)
|
||||
self._current_cell = []
|
||||
elif tag == "tr":
|
||||
row = self._current_row
|
||||
self._rows.append(row)
|
||||
self._current_row = []
|
||||
|
||||
def error(self, message):
|
||||
pass
|
||||
|
||||
def get_satellites_list(self, source):
|
||||
""" Getting complete list of satellites. """
|
||||
self.reset()
|
||||
self._rows.clear()
|
||||
self._source = source
|
||||
|
||||
for src in SatelliteSource.get_sources(self._source):
|
||||
try:
|
||||
resp = requests.get(url=src, headers=_HEADERS, timeout=_TIMEOUT)
|
||||
except requests.exceptions.RequestException as e:
|
||||
log(f"Getting satellite list error: {repr(e)}")
|
||||
else:
|
||||
reason = resp.reason
|
||||
if reason == "OK":
|
||||
self.feed(resp.text)
|
||||
else:
|
||||
log(f"Getting satellite list error: {reason} -> {resp.url}")
|
||||
|
||||
if self._rows:
|
||||
if self._source is SatelliteSource.FLYSAT:
|
||||
return self.get_satellites_for_fly_sat()
|
||||
elif self._source is SatelliteSource.LYNGSAT:
|
||||
return self.get_satellites_for_lyng_sat()
|
||||
elif source is SatelliteSource.KINGOFSAT:
|
||||
return self.get_satellites_for_king_of_sat()
|
||||
|
||||
return []
|
||||
|
||||
def get_satellite(self, sat):
|
||||
pos = sat[1]
|
||||
return Satellite(name=f"{pos} {sat[0]}", flags="0",
|
||||
position=self.get_position(pos.replace(".", "")),
|
||||
transponders=self.get_transponders(sat[3]))
|
||||
|
||||
def get_satellites_for_fly_sat(self):
|
||||
sat_pat = re.compile(r"https://.*/satellite/.+")
|
||||
pos_pat = re.compile(r"https://.*/satellite/position/.+")
|
||||
names = []
|
||||
pos = ""
|
||||
pos_url = ""
|
||||
satellites = []
|
||||
|
||||
def normalize_pos(p):
|
||||
return f"{float(p[:-1])}{p[-1]}" if "." not in p else p
|
||||
|
||||
for row in filter(lambda x: len(x) > 6, self._rows):
|
||||
if re.match(sat_pat, row[1]):
|
||||
row.pop(0)
|
||||
|
||||
if re.match(sat_pat, row[0]) and row[-2]: # r[-2] -> skip EMPTY satellites!
|
||||
if re.match(pos_pat, row[0]):
|
||||
names.clear()
|
||||
pos_url = row[0]
|
||||
name = row[3]
|
||||
pos = normalize_pos(self.parse_position(row[-4]))
|
||||
names.append(name)
|
||||
satellites.append((name, pos, row[-2], row[2], False))
|
||||
|
||||
if len(row) == 7:
|
||||
single_pos = normalize_pos(self.parse_position(row[-4]))
|
||||
name = row[1]
|
||||
if pos == single_pos:
|
||||
names.append(name)
|
||||
else:
|
||||
# Uniting satellites in position.
|
||||
if len(names) > 1:
|
||||
satellites.append(("/".join(names), pos, None, pos_url, False))
|
||||
names.clear()
|
||||
satellites.append((name, single_pos, row[-2], row[0], False))
|
||||
|
||||
return satellites
|
||||
|
||||
def get_satellites_for_lyng_sat(self):
|
||||
base_url = "https://www.lyngsat.com/"
|
||||
sats = []
|
||||
cur_pos = "0"
|
||||
for row in filter(lambda x: 3 < len(x) < 8, self._rows):
|
||||
if not row[0]:
|
||||
row = row[1:]
|
||||
|
||||
pos = self.parse_position(row[1])
|
||||
if not self.POS_PAT.match(pos):
|
||||
if len(row) == 4 and row[0].endswith(".html"):
|
||||
sats.append((row[1], cur_pos, row[-2], base_url + row[0], False))
|
||||
continue
|
||||
|
||||
sats.append((row[-3], pos, row[-2], base_url + row[0], False))
|
||||
cur_pos = pos
|
||||
return sats
|
||||
|
||||
def get_satellites_for_king_of_sat(self):
|
||||
def get_sat(r):
|
||||
return r[3], self.parse_position(r[1]), None, r[2], False
|
||||
|
||||
return list(map(get_sat, filter(lambda x: len(x) == 17, self._rows)))
|
||||
|
||||
@staticmethod
|
||||
def parse_position(pos_str):
|
||||
return "".join(c for c in pos_str if c.isdigit() or c.isalpha() or c == ".")
|
||||
|
||||
@staticmethod
|
||||
def get_position(pos):
|
||||
return f"{'-' if pos[-1] == 'W' else ''}{pos[:-1]}"
|
||||
|
||||
def get_transponders(self, sat_url):
|
||||
""" Getting transponders(sorted by frequency). """
|
||||
self._rows.clear()
|
||||
trs = []
|
||||
|
||||
if self._source is SatelliteSource.KINGOFSAT:
|
||||
sat_url = "https://en.kingofsat.net/" + sat_url
|
||||
|
||||
try:
|
||||
request = requests.get(url=sat_url, headers=_HEADERS, timeout=_TIMEOUT)
|
||||
except requests.exceptions.RequestException as e:
|
||||
log(f"Getting transponders error: {e}")
|
||||
else:
|
||||
if request.status_code == 200:
|
||||
self.feed(request.text)
|
||||
if self._source is SatelliteSource.FLYSAT:
|
||||
self.get_transponders_for_fly_sat(trs)
|
||||
elif self._source is SatelliteSource.LYNGSAT:
|
||||
self.get_transponders_for_lyng_sat(trs)
|
||||
elif self._source is SatelliteSource.KINGOFSAT:
|
||||
self.get_transponders_for_king_of_sat(trs)
|
||||
else:
|
||||
log(f"SatellitesParser [get transponders] error: {sat_url} {request.reason}")
|
||||
|
||||
return sorted(trs, key=lambda x: int(x.frequency))
|
||||
|
||||
def get_transponders_for_fly_sat(self, trs):
|
||||
""" Parsing transponders for FlySat. """
|
||||
frq_pol_pattern = re.compile(r"(\d{4,5})+\s+([RLHV]).*(DVB-S[2]?)/(.+PSK)?.*")
|
||||
pls_pattern = re.compile(r".*PLS\s+(Root|Gold|Combo)+\s(\d+)?")
|
||||
is_id_pattern = re.compile(r"Stream\s(\d+)")
|
||||
sr_fec_pattern = re.compile(r"(\d{4,5})+\s+(\d+/\d+).*")
|
||||
n_trs = []
|
||||
|
||||
if self._rows:
|
||||
is_ids = []
|
||||
for r in self._rows:
|
||||
row_len = len(r)
|
||||
if row_len == 1:
|
||||
is_ids.extend(re.findall(is_id_pattern, r[0]))
|
||||
continue
|
||||
if row_len < 12:
|
||||
continue
|
||||
|
||||
freq = re.findall(frq_pol_pattern, r[2])
|
||||
if not freq:
|
||||
continue
|
||||
freq, pol, sys, mod = freq[0]
|
||||
|
||||
sr_fec = re.match(sr_fec_pattern, r[3])
|
||||
if not sr_fec:
|
||||
continue
|
||||
sr, fec = sr_fec.group(1), sr_fec.group(2)
|
||||
|
||||
pls = re.match(pls_pattern, r[2])
|
||||
pls_code = None
|
||||
pls_mode = None
|
||||
if pls:
|
||||
pls_mode = self.pls_modes.get(pls.group(1), None)
|
||||
pls_code = pls.group(2)
|
||||
|
||||
if is_ids:
|
||||
tr = trs.pop()
|
||||
for index, is_id in enumerate(is_ids):
|
||||
tr = tr._replace(is_id=is_id)
|
||||
if is_transponder_valid(tr):
|
||||
n_trs.append(tr)
|
||||
|
||||
tr = Transponder(f"{freq}000", f"{sr}000", pol, fec, sys, mod, pls_mode, pls_code, None, None)
|
||||
if is_transponder_valid(tr):
|
||||
trs.append(tr)
|
||||
|
||||
is_ids.clear()
|
||||
trs.extend(n_trs)
|
||||
|
||||
def get_transponders_for_lyng_sat(self, trs):
|
||||
""" Parsing transponders for LyngSat. """
|
||||
frq_pol_pattern = re.compile(r"(\d{4,5})\s+([RLHV]).*")
|
||||
sr_fec_pattern = re.compile((r"(DVB-S[2]?)\s+(.+PSK)?.*?(\d+)\s+(\d/\d)\s?"
|
||||
r"(?:T2-MI\s+PLP\s+(\d+))?.*"
|
||||
r"?(?:PLS\s+(Root|Gold|Combo)\s+(\d+))?"
|
||||
r"(?:.*Stream\s+(\d+))?.*"))
|
||||
|
||||
for row in filter(lambda x: len(x) > 8, self._rows):
|
||||
for freq in row[1], row[2], row[3]:
|
||||
res = re.match(frq_pol_pattern, freq)
|
||||
if res:
|
||||
break
|
||||
if not res:
|
||||
continue
|
||||
|
||||
freq, pol = res.group(1), res.group(2)
|
||||
res = re.search(sr_fec_pattern, row[3])
|
||||
if not res:
|
||||
continue
|
||||
|
||||
sys, mod, sr, fec = res.group(1), res.group(2), res.group(3), res.group(4)
|
||||
mod = mod.strip() if mod else "Auto"
|
||||
plp, pls_mode, pls_code, is_id = res.group(5), res.group(6), res.group(7), res.group(8)
|
||||
pls_mode = self.pls_modes.get(pls_mode, None)
|
||||
|
||||
if plp is not None:
|
||||
log(f"Detected T2-MI transponder! [{freq} {sr} {pol}] ")
|
||||
|
||||
tr = Transponder(f"{freq}000", f"{sr}000", pol, fec, sys, mod, pls_mode, pls_code, is_id, None)
|
||||
if is_transponder_valid(tr):
|
||||
trs.append(tr)
|
||||
|
||||
def get_transponders_for_king_of_sat(self, trs):
|
||||
""" Getting transponders for KingOfSat source.
|
||||
|
||||
Since the *.ini file contains incomplete information, it is not used.
|
||||
"""
|
||||
sys_pat = re.compile(r"(DVB-S[2]?)\s?(?:T2-MI,\s+PLP\s+(\d+))?.*?(?:PLS:\s+(Root|Gold|Combo)\+(\d+))?")
|
||||
mod_pat = re.compile(r"(.*PSK).*?(?:.*Stream\s+(\d+))?.*")
|
||||
sr_fec_pattern = re.compile(r"(\d{4,5})+\s+(\d+/\d+).*")
|
||||
|
||||
for row in filter(lambda r: len(r) == 16 and self.POS_PAT.match(r[0]), self._rows):
|
||||
freq, pol = row[2].replace(".", "0"), row[3]
|
||||
if not freq.isdigit() or pol not in "VHLR":
|
||||
continue
|
||||
|
||||
res = re.match(sys_pat, row[8])
|
||||
if not res:
|
||||
continue
|
||||
sys, t2_mi, pls_id, pls_code = res.group(1), res.group(2), res.group(3), res.group(4)
|
||||
pls_id = self.pls_modes.get(pls_id, None)
|
||||
|
||||
res = re.match(mod_pat, row[9])
|
||||
if not res:
|
||||
continue
|
||||
mod, is_id = res.group(1), res.group(2)
|
||||
|
||||
res = re.match(sr_fec_pattern, row[10])
|
||||
if not res:
|
||||
continue
|
||||
sr, fec = res.group(1), res.group(2)
|
||||
|
||||
if t2_mi:
|
||||
log(f"Detected T2-MI transponder! [{freq} {sr} {pol}] ")
|
||||
|
||||
tr = Transponder(freq, f"{sr}000", pol, fec, sys, mod, pls_id, pls_code, is_id, None)
|
||||
if is_transponder_valid(tr):
|
||||
trs.append(tr)
|
||||
|
||||
|
||||
class ServicesParser(HTMLParser):
|
||||
""" Services parser for LYNGSAT source. """
|
||||
|
||||
def __init__(self, source=SatelliteSource.LYNGSAT, entities=False, separator=' '):
|
||||
|
||||
HTMLParser.__init__(self)
|
||||
|
||||
self._S_TYPES = {"": "2", "MPEG-2 SD": "1", "MPEG-2/SD": "1", "SD": "1", "MPEG-4 SD": "22", "MPEG-4/SD": "22",
|
||||
"MPEG-4": "22", "HEVC SD": "22", "MPEG-4/HD": "25", "MPEG-4 HD": "25", "MPEG-4 HD 1080": "25",
|
||||
"MPEG-4 HD 720": "25", "HEVC HD": "25", "HEVC/HD": "25", "HEVC": "31", "HEVC/UHD": "31",
|
||||
"HEVC UHD": "31", "HEVC UHD 4K": "31", "3": "Data"}
|
||||
|
||||
self._TR = "s {}000:{}000:{}:{}:{}:{}:{}:{}"
|
||||
self._S2_TR = "{}:{}:{}:{}"
|
||||
|
||||
self._POS_PAT = re.compile(r".*?(\d+\.\d°[EW]).*")
|
||||
# LyngSat.
|
||||
self._TR_PAT = re.compile((r".*?(\d+)\.?\d?\s+([RLHV]).*(DVB-S[2]?)/?(.*PSK)?\s"
|
||||
r"?(T2-MI)?\s?(PLS\s+Multistream)?\s?"
|
||||
r"SR-FEC:\s(\d+)-(\d/\d)\s+.*ONID-TID:\s+(\d+)-(\d+).*"))
|
||||
|
||||
self._MULTI_PAT = re.compile(r"PLS\s+(Root|Gold|Combo)+\s(\d+)?\s+(?:Stream\s(\d+))")
|
||||
# KingOfSat.
|
||||
self._KING_TR_PAT = re.compile((r"(DVB-S[2]?)\s?(?:T2-MI,\s+PLP\s+(\d+))?.*"
|
||||
r"?(?:PLS:\s+(Root|Gold|Combo)\+(\d+))?"
|
||||
r"\s+(.*PSK).*?(?:.*Stream\s+(\d+))?.*"))
|
||||
|
||||
self._parse_html_entities = entities
|
||||
self._separator = separator
|
||||
self._is_td = False
|
||||
self._is_th = False
|
||||
self._is_mux_div = False
|
||||
self._current_row = []
|
||||
self._current_cell_text = []
|
||||
self._current_cell = Cell()
|
||||
self._rows = []
|
||||
self._source = source
|
||||
self._t_url = ""
|
||||
self._use_short_names = True
|
||||
self._pls_modes = {v: k for k, v in PLS_MODE.items()}
|
||||
|
||||
@property
|
||||
def source(self):
|
||||
return self._source
|
||||
|
||||
@source.setter
|
||||
def source(self, value):
|
||||
self._source = value
|
||||
self.reset()
|
||||
|
||||
@property
|
||||
def use_short_names(self):
|
||||
return self._use_short_names
|
||||
|
||||
@use_short_names.setter
|
||||
def use_short_names(self, value):
|
||||
self._use_short_names = value
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == "td":
|
||||
self._is_td = True
|
||||
elif tag == "tr":
|
||||
self._is_th = True
|
||||
elif tag == "a" and not self._current_cell.url:
|
||||
if attrs:
|
||||
for a in attrs:
|
||||
if a[0] == "href":
|
||||
self._current_cell.url = a[1]
|
||||
|
||||
if self._source is SatelliteSource.KINGOFSAT and self._use_short_names:
|
||||
if a[0] != "title":
|
||||
continue
|
||||
txt = a[1]
|
||||
sep = "Id: "
|
||||
if txt and txt.startswith(sep):
|
||||
# Saving the 'short' name.
|
||||
_, sep, name = txt.partition(sep)
|
||||
self._current_cell.text = name
|
||||
elif tag == "img":
|
||||
img_link = attrs[0][1]
|
||||
if self._source is SatelliteSource.LYNGSAT:
|
||||
if img_link.startswith("/logo/"):
|
||||
self._current_cell.img = img_link
|
||||
elif self._source is SatelliteSource.KINGOFSAT:
|
||||
self._current_cell.img = img_link
|
||||
elif tag == "div" and self._source is SatelliteSource.LYNGSAT:
|
||||
self._is_mux_div = bool(list(filter(lambda at: at[-1] == "mux-header", attrs)))
|
||||
|
||||
def handle_data(self, data):
|
||||
""" Save content to a cell """
|
||||
if self._is_td or self._is_th:
|
||||
self._current_cell_text.append(data.strip())
|
||||
|
||||
if self._is_mux_div:
|
||||
self._current_cell.url = data.strip()
|
||||
self._is_mux_div = False
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == "td":
|
||||
self._is_td = False
|
||||
elif tag == "tr":
|
||||
self._is_th = False
|
||||
|
||||
if tag in ("td", "th"):
|
||||
if not self._current_cell.text:
|
||||
txt = self._separator.join(self._current_cell_text).strip()
|
||||
self._current_cell.text = txt
|
||||
self._current_row.append(self._current_cell)
|
||||
self._current_cell_text = []
|
||||
self._current_cell = Cell()
|
||||
elif tag == "tr":
|
||||
row = self._current_row
|
||||
self._rows.append(row)
|
||||
self._current_row = []
|
||||
|
||||
def error(self, message):
|
||||
log(f"ServicesParser error: {message}")
|
||||
|
||||
def init_data(self, url):
|
||||
""" Initializes data for the given URL. """
|
||||
if self._source not in (SatelliteSource.LYNGSAT, SatelliteSource.KINGOFSAT):
|
||||
raise ValueError(f"Unsupported source: {self._source.name}!")
|
||||
|
||||
self._rows.clear()
|
||||
try:
|
||||
request = requests.get(url=url, headers=_HEADERS, timeout=_TIMEOUT)
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise ValueError(e)
|
||||
else:
|
||||
reason = request.reason
|
||||
if reason == "OK":
|
||||
self.feed(request.text)
|
||||
else:
|
||||
raise ValueError(reason)
|
||||
|
||||
def get_transponders_links(self, sat_url):
|
||||
""" Returns transponder links. """
|
||||
try:
|
||||
if self._source is SatelliteSource.KINGOFSAT:
|
||||
sat_url = "https://en.kingofsat.net/" + sat_url
|
||||
self.init_data(sat_url)
|
||||
except ValueError as e:
|
||||
log(e)
|
||||
else:
|
||||
if self._source is SatelliteSource.LYNGSAT:
|
||||
url = "https://www.lyngsat.com/muxes/"
|
||||
return [row[0] for row in
|
||||
filter(lambda x: x and len(x) > 8 and x[0].url and x[0].url.startswith(url), self._rows)]
|
||||
elif self._source is SatelliteSource.KINGOFSAT:
|
||||
trs = []
|
||||
for r in self._rows:
|
||||
if len(r) == 13 and SatellitesParser.POS_PAT.match(r[0].text):
|
||||
t_cell = r[4]
|
||||
if t_cell.url and t_cell.url.startswith("tp.php?tp="):
|
||||
t_cell.url = f"https://en.kingofsat.net/{t_cell.url}"
|
||||
t_cell.text = f"{r[2].text} {r[3].text} {r[6].text} {r[8].text}"
|
||||
trs.append(t_cell)
|
||||
return trs
|
||||
return []
|
||||
|
||||
def get_transponder_services(self, tr_url, sat_position=None, use_pids=False):
|
||||
""" Returns services for given transponder.
|
||||
|
||||
@param tr_url: transponder URL.
|
||||
@param sat_position: custom satellite position. Sometimes required to adjust the namespace.
|
||||
@param use_pids: if possible use additional pids [video, audio].
|
||||
"""
|
||||
try:
|
||||
self._t_url = tr_url
|
||||
self.init_data(tr_url)
|
||||
except ValueError as e:
|
||||
log(e)
|
||||
return []
|
||||
else:
|
||||
if self._source is SatelliteSource.LYNGSAT:
|
||||
return self.get_lyngsat_services(sat_position, use_pids)
|
||||
elif self._source is SatelliteSource.KINGOFSAT:
|
||||
return self.get_kingofsat_services(sat_position, use_pids)
|
||||
return []
|
||||
|
||||
def get_lyngsat_services(self, sat_position=None, use_pids=False):
|
||||
services = []
|
||||
pos, freq, sr, fec, pol, nsp, tid, nid = sat_position or 0, 0, 0, 0, 0, 0, 0, 0
|
||||
sys = "DVB-S"
|
||||
pos_found = False
|
||||
tr = None
|
||||
# Multi-stream.
|
||||
multi_tr = None
|
||||
multi = False
|
||||
# Transponder.
|
||||
for r in filter(lambda x: x and 6 < len(x) < 9, self._rows):
|
||||
if not pos_found:
|
||||
pos_tr = re.match(self._POS_PAT, r[0].text)
|
||||
if not pos_tr:
|
||||
continue
|
||||
|
||||
if not sat_position:
|
||||
pos = self.get_position(pos_tr.group(1))
|
||||
pos_found = True
|
||||
|
||||
if pos_found:
|
||||
text = " ".join(c.text for c in r[1:])
|
||||
td = re.match(self._TR_PAT, text)
|
||||
if td:
|
||||
freq, pol = int(td.group(1)), get_key_by_value(POLARIZATION, td.group(2))
|
||||
sys, mod, sr, _fec, = td.group(3), td.group(4), td.group(7), td.group(8)
|
||||
nid, tid = td.group(9), td.group(10)
|
||||
sys, mod, fec, nsp, s2_flags, roll_off, pilot, inv = self.get_transponder_data(pos, _fec, sys, mod)
|
||||
nid, tid = int(nid), int(tid)
|
||||
|
||||
if td.group(5):
|
||||
log(f"Detected T2-MI transponder! [{freq} {sr} {pol}]")
|
||||
|
||||
if td.group(6):
|
||||
log(f"Detected multi-stream transponder! [{freq} {sr} {pol}]")
|
||||
multi = True
|
||||
|
||||
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
|
||||
|
||||
if not tr:
|
||||
er = f"Transponder [{self._t_url}] not found or its type [T2-MI, etc] not supported yet."
|
||||
log(f"ServicesParser error [get transponder services]: {er}")
|
||||
return services
|
||||
|
||||
# Services.
|
||||
for r in filter(None, self._rows):
|
||||
if multi and r[0].url:
|
||||
res = re.match(self._MULTI_PAT, r[0].url)
|
||||
if res:
|
||||
pls_mode, is_code, is_id = self._pls_modes.get(res.group(1), None), res.group(2), res.group(3)
|
||||
multi_tr = f"{tr}:{is_id}:{is_code}:{pls_mode}" if all((pls_mode, is_code, is_id)) else None
|
||||
tid = int(is_id) if multi_tr else tid
|
||||
|
||||
if len(r) == 12 and r[0].text.isdigit():
|
||||
sid, name, s_type, v_pid, a_pid, cas, pkg = r[0].text, r[2].text, r[4].text, r[
|
||||
5].text.strip(), r[6].text.split(), r[9].text, r[10].text.strip()
|
||||
try:
|
||||
s_type = self._S_TYPES.get(s_type, "3") # 3 = Data
|
||||
_s_type = SERVICE_TYPE.get(s_type, SERVICE_TYPE.get("3")) # str repr
|
||||
flags, sid, fav_id, picon_id, data_id = self.get_service_data(s_type, pkg, sid, tid, nid, nsp,
|
||||
v_pid, a_pid, cas, use_pids)
|
||||
services.append(Service(flags, "s", None, name, None, None, pkg, _s_type, r[1].img, picon_id,
|
||||
sid, freq, sr, pol, fec, sys, pos, data_id, fav_id, multi_tr or tr))
|
||||
except ValueError as e:
|
||||
log(f"ServicesParser error [get transponder services]: {e}")
|
||||
|
||||
return services
|
||||
|
||||
def get_kingofsat_services(self, sat_position=None, use_pids=False):
|
||||
services = []
|
||||
# Transponder
|
||||
tr = list(filter(lambda r: len(r) == 13 and r[4].url and r[4].url.startswith("tp.php?tp="), self._rows))
|
||||
if not tr:
|
||||
log(f"ServicesParser error [get transponder services]: Transponder [{self._t_url}] not found!")
|
||||
return services
|
||||
|
||||
tr, multi_tr, tid, nid, nsp = None, None, None, None, None
|
||||
freq, sr, pol, fec, sys, pos = None, None, None, None, None, None
|
||||
|
||||
for r in filter(lambda x: len(x) > 12, self._rows):
|
||||
r_size = len(r)
|
||||
if r_size == 13 and r[4].url and r[4].url.startswith("tp.php?tp="):
|
||||
res = re.match(self._KING_TR_PAT, f"{r[6].text} {r[7].text}")
|
||||
if not res:
|
||||
continue
|
||||
|
||||
sys, mod = res.group(1), res.group(5)
|
||||
s_pos, freq, pol, sr_fec = r[0].text, r[2].text, r[3].text, r[8].text
|
||||
nid, tid = r[10].text, r[11].text
|
||||
|
||||
pos = sat_position
|
||||
if not sat_position:
|
||||
pos_tr = re.match(self._POS_PAT, s_pos)
|
||||
if pos_tr:
|
||||
pos = self.get_position(pos_tr.group(1))
|
||||
|
||||
sr, fec = sr_fec.split()
|
||||
pol = get_key_by_value(POLARIZATION, pol)
|
||||
sys, mod, fec, nsp, s2_flags, roll_off, pilot, inv = self.get_transponder_data(pos, fec, sys, mod)
|
||||
freq, nid, tid = int(float(freq)), int(nid), int(tid)
|
||||
tr = self._TR.format(freq, sr, pol, fec, pos, inv, sys, s2_flags)
|
||||
|
||||
pls_mode, is_code, is_id = self._pls_modes.get(res.group(3), None), res.group(4), res.group(6)
|
||||
multi_tr = f"{tr}:{is_id}:{is_code}:{pls_mode}" if all((pls_mode, is_code, is_id)) else None
|
||||
tid = int(is_id) if multi_tr else tid
|
||||
|
||||
if res.group(2):
|
||||
log(f"Detected T2-MI transponder! [{freq} {sr}]")
|
||||
|
||||
if multi_tr:
|
||||
log(f"Detected multi-stream transponder! [{freq} {sr}]")
|
||||
|
||||
if tr and r_size == 14 and not r[1].text and r[7].text and r[7].text.isdigit():
|
||||
if r[1].img == "/radio.gif":
|
||||
s_type = ""
|
||||
elif r[8].img == "/hd.gif":
|
||||
s_type = "HEVC HD"
|
||||
elif r[1].img == "/data.gif":
|
||||
s_type = "Data"
|
||||
else:
|
||||
s_type = "SD"
|
||||
|
||||
s_type = self._S_TYPES.get(s_type, "3")
|
||||
_s_type = SERVICE_TYPE.get(s_type, SERVICE_TYPE.get("3"))
|
||||
|
||||
name, pkg, cas, sid, v_pid, a_pid = r[2].text, r[5].text, r[6].text, r[7].text, None, None
|
||||
flags, sid, fav_id, picon_id, data_id = self.get_service_data(s_type, pkg, sid, tid, nid, nsp,
|
||||
v_pid, a_pid, cas, use_pids)
|
||||
services.append(Service(flags, "s", None, name, None, None, pkg, _s_type, None, picon_id,
|
||||
sid, str(freq), sr, pol, fec, sys, pos, data_id, fav_id, multi_tr or tr))
|
||||
|
||||
return services
|
||||
|
||||
def get_transponder_data(self, pos, fec, sys, mod):
|
||||
""" Returns converted transponder data. """
|
||||
sys = get_key_by_value(SYSTEM, sys)
|
||||
mod = get_key_by_value(MODULATION, mod)
|
||||
fec = get_key_by_value(FEC, fec)
|
||||
# For negative (West) positions: 3600 - numeric position value!!!
|
||||
namespace = f"{3600 - pos if pos < 0 else pos:04x}0000"
|
||||
tr_flag = 1
|
||||
roll_off = 0 # 35% DVB-S2/DVB-S (default)
|
||||
pilot = 2 # Auto
|
||||
s2_flags = "" if sys == "DVB-S" else self._S2_TR.format(tr_flag, mod or 0, roll_off, pilot)
|
||||
inv = 2 # Default
|
||||
|
||||
return sys, mod, fec, namespace, s2_flags, roll_off, pilot, inv
|
||||
|
||||
@staticmethod
|
||||
def get_service_data(s_type, pkg, sid, tid, nid, namespace, v_pid, a_pid, cas, use_pids=False):
|
||||
sid = int(sid)
|
||||
data_id = f"{sid:04x}:{namespace}:{tid:04x}:{nid:04x}:{s_type}:0:0"
|
||||
fav_id = f"{sid}:{tid}:{nid}:{namespace}"
|
||||
picon_id = f"1_0_{int(s_type):X}_{sid}_{tid}_{nid}_{namespace}_0_0_0.png"
|
||||
# Flags.
|
||||
flags = f"p:{pkg}"
|
||||
cas = ",".join(get_key_by_value(CAS, c) or "C:0000" for c in cas.split()) if cas else None
|
||||
if use_pids:
|
||||
v_pid = f"c:00{int(v_pid):04x}" if v_pid else None
|
||||
a_pid = ",".join([f"c:01{int(p):04x}" for p in a_pid]) if a_pid else None
|
||||
flags = ",".join(filter(None, (flags, v_pid, a_pid, cas)))
|
||||
else:
|
||||
flags = ",".join(filter(None, (flags, cas)))
|
||||
|
||||
return flags, sid, fav_id, picon_id, data_id
|
||||
|
||||
@staticmethod
|
||||
def get_position(pos):
|
||||
return int(SatellitesParser.get_position("".join(c for c in pos if c.isdigit() or c.isalpha())))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
8729
app/tools/vlc.py
Normal file
8729
app/tools/vlc.py
Normal file
File diff suppressed because it is too large
Load Diff
472
app/tools/yt.py
Normal file
472
app/tools/yt.py
Normal file
@@ -0,0 +1,472 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Module for working with YouTube service. """
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from html.parser import HTMLParser
|
||||
from json import JSONDecodeError
|
||||
from urllib import parse
|
||||
from urllib.error import URLError
|
||||
from urllib.request import Request, urlopen, urlretrieve
|
||||
|
||||
from app.commons import log
|
||||
from app.settings import SEP
|
||||
from app.ui.uicommons import show_notification
|
||||
|
||||
_TIMEOUT = 5
|
||||
_HEADERS = {"User-Agent": "Mozilla/5.0 (Linux x86_64; rv:92.0) Gecko/20100101 Firefox/92.0",
|
||||
"DNT": "1",
|
||||
"Accept-Encoding": "gzip, deflate"}
|
||||
_YT_PATTERN = re.compile(r"https://www.youtube.com/.+(?:v=)([\w-]{11}).*")
|
||||
_YT_LIST_PATTERN = re.compile(r"https://www.youtube.com/.+?(?:list=)([\w-]{18,})?.*")
|
||||
_YT_VIDEO_PATTERN = re.compile(r"https://r\d+---sn-[\w]{10}-[\w]{3,5}.googlevideo.com/videoplayback?.*")
|
||||
|
||||
Quality = {137: "1080p", 136: "720p", 135: "480p", 134: "360p",
|
||||
133: "240p", 160: "144p", 0: "0p", 18: "360p", 22: "720p"}
|
||||
|
||||
|
||||
class YouTubeException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class YouTube:
|
||||
""" 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
|
||||
def is_yt_video_link(url):
|
||||
return re.match(_YT_VIDEO_PATTERN, url)
|
||||
|
||||
@staticmethod
|
||||
def get_yt_id(url):
|
||||
""" Returns video id or None """
|
||||
yt = re.search(_YT_PATTERN, url)
|
||||
if yt:
|
||||
return yt.group(1)
|
||||
|
||||
@staticmethod
|
||||
def get_yt_list_id(url):
|
||||
""" Returns playlist id or None """
|
||||
yt = re.search(_YT_LIST_PATTERN, url)
|
||||
if yt:
|
||||
return yt.group(1)
|
||||
|
||||
def get_yt_link(self, video_id, url=None, skip_errors=False):
|
||||
""" Getting link to YouTube video by id or URL.
|
||||
|
||||
Returns tuple from the video links dict and title.
|
||||
"""
|
||||
if self._settings.enable_yt_dl and url:
|
||||
if not self._yt_dl:
|
||||
self._yt_dl = YouTubeDL.get_instance(self._settings, self._callback)
|
||||
if not self._yt_dl:
|
||||
raise YouTubeException("youtube-dl initialization error.")
|
||||
return self._yt_dl.get_yt_link(url, skip_errors)
|
||||
|
||||
return self.get_yt_link_by_id(video_id)
|
||||
|
||||
@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.
|
||||
"""
|
||||
info = InnerTube().player(video_id)
|
||||
det = info.get("videoDetails", None)
|
||||
title = det.get("title", None) if det else None
|
||||
streaming_data = info.get("streamingData", None)
|
||||
fmts = streaming_data.get("formats", None) if streaming_data else None
|
||||
|
||||
if fmts:
|
||||
links = {Quality[i["itag"]]: i["url"] for i in filter(
|
||||
lambda i: i.get("itag", -1) in Quality, fmts) if "url" in i}
|
||||
|
||||
if links and title:
|
||||
return links, title.replace("+", " ")
|
||||
|
||||
cause = None
|
||||
status = info.get("playabilityStatus", None)
|
||||
if status:
|
||||
cause = f"[{status.get('status', '')}] {status.get('reason', '')}"
|
||||
|
||||
log(f"{__class__.__name__}: Getting link to video with id '{video_id}' filed! Cause: {cause}")
|
||||
|
||||
return None, cause
|
||||
|
||||
def get_yt_playlist(self, list_id, url=None):
|
||||
""" Returns tuple from the playlist header and list of tuples (title, video id). """
|
||||
if self._settings.enable_yt_dl and url:
|
||||
try:
|
||||
if not self._yt_dl:
|
||||
raise YouTubeException("youtube-dl is not initialized!")
|
||||
|
||||
self._yt_dl.update_options({"noplaylist": False, "extract_flat": True})
|
||||
info = self._yt_dl.get_info(url, skip_errors=False)
|
||||
if "url" in info:
|
||||
info = self._yt_dl.get_info(info.get("url"), skip_errors=False)
|
||||
|
||||
return info.get("title", ""), [(e.get("title", ""), e.get("id", "")) for e in info.get("entries", [])]
|
||||
finally:
|
||||
# Restoring default options
|
||||
if self._yt_dl:
|
||||
self._yt_dl.update_options({"noplaylist": True, "extract_flat": False})
|
||||
|
||||
return PlayListParser.get_yt_playlist(list_id)
|
||||
|
||||
|
||||
class InnerTube:
|
||||
""" Object for interacting with the innertube API.
|
||||
|
||||
Based on InnerTube class from pytube [https://github.com/pytube/pytube] project!
|
||||
"""
|
||||
_BASE_URI = "https://www.youtube.com/youtubei/v1"
|
||||
|
||||
_DEFAULT_CLIENTS = {
|
||||
"ANDROID": {
|
||||
"context": {"client": {"clientName": "ANDROID", "clientVersion": "16.20"}},
|
||||
"api_key": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
|
||||
},
|
||||
"ANDROID_EMBED": {
|
||||
"context": {"client": {"clientName": "ANDROID", "clientVersion": "16.20", "clientScreen": "EMBED"}},
|
||||
"api_key": "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, client="ANDROID"):
|
||||
""" Initialize an InnerTube object.
|
||||
|
||||
@param client: Client to use for the object. Default to web because it returns the most playback types.
|
||||
"""
|
||||
self.context = self._DEFAULT_CLIENTS[client]["context"]
|
||||
self.api_key = self._DEFAULT_CLIENTS[client]["api_key"]
|
||||
|
||||
@property
|
||||
def base_data(self):
|
||||
"""Return the base json data to transmit to the innertube API."""
|
||||
return {"context": self.context}
|
||||
|
||||
@property
|
||||
def base_params(self):
|
||||
"""Return the base query parameters to transmit to the innertube API."""
|
||||
return {"key": self.api_key, "contentCheckOk": True, "racyCheckOk": True}
|
||||
|
||||
def player(self, video_id):
|
||||
""" Make a request to the player endpoint. Returns raw player info results. """
|
||||
endpoint = f"{self._BASE_URI}/player"
|
||||
query = {"videoId": video_id}
|
||||
query.update(self.base_params)
|
||||
return self._call_api(endpoint, query, self.base_data) or {}
|
||||
|
||||
@staticmethod
|
||||
def _call_api(endpoint, query, data):
|
||||
""" Make a request to a given endpoint with the provided query parameters and data."""
|
||||
headers = {"Content-Type": "application/json", }
|
||||
response = InnerTube._execute(f"{endpoint}?{parse.urlencode(query)}", "POST", headers=headers, data=data)
|
||||
|
||||
try:
|
||||
resp = json.loads(response.read())
|
||||
except JSONDecodeError as e:
|
||||
log(f"{__class__.__name__}: Parsing response error: {e}")
|
||||
else:
|
||||
return resp
|
||||
|
||||
@staticmethod
|
||||
def _execute(url, method=None, headers=None, data=None, timeout=_TIMEOUT):
|
||||
base_headers = {"User-Agent": "Mozilla/5.0", "accept-language": "en-US,en"}
|
||||
if headers:
|
||||
base_headers.update(headers)
|
||||
if data:
|
||||
# Encoding data for request.
|
||||
if not isinstance(data, bytes):
|
||||
data = bytes(json.dumps(data), encoding="utf-8")
|
||||
return urlopen(Request(url, headers=base_headers, method=method, data=data), timeout=timeout)
|
||||
|
||||
|
||||
class PlayListParser(HTMLParser):
|
||||
""" Very simple parser to handle YouTube playlist pages. """
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._is_header = False
|
||||
self._header = ""
|
||||
self._playlist = []
|
||||
self._is_script = False
|
||||
self._scr_start = ('var ytInitialData = ', 'window["ytInitialData"] = ')
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == "script":
|
||||
self._is_script = True
|
||||
|
||||
def handle_data(self, data):
|
||||
if self._is_script:
|
||||
data = data.lstrip()
|
||||
if data.startswith(self._scr_start):
|
||||
data = data.split(";")[0]
|
||||
for s in self._scr_start:
|
||||
data = data.lstrip(s)
|
||||
|
||||
try:
|
||||
resp = json.loads(data)
|
||||
except JSONDecodeError as e:
|
||||
log(f"{__class__.__name__}: Parsing data error: {e}")
|
||||
else:
|
||||
sb = resp.get("sidebar", None)
|
||||
if sb:
|
||||
for t in [t["runs"][0] for t in flat("title", sb) if "runs" in t]:
|
||||
txt = t.get("text", None)
|
||||
if txt:
|
||||
self._header = txt
|
||||
break
|
||||
|
||||
ct = resp.get("contents", None)
|
||||
if ct:
|
||||
for d in [(d.get("title", {}).get("runs", [{}])[0].get("text", ""),
|
||||
d.get("videoId", "")) for d in flat("playlistVideoRenderer", ct)]:
|
||||
self._playlist.append(d)
|
||||
self._is_script = False
|
||||
|
||||
def error(self, message):
|
||||
log(f"{__class__.__name__} Parsing error: {message}")
|
||||
|
||||
@property
|
||||
def header(self):
|
||||
return self._header
|
||||
|
||||
@property
|
||||
def playlist(self):
|
||||
return self._playlist
|
||||
|
||||
@staticmethod
|
||||
def get_yt_playlist(play_list_id):
|
||||
""" Getting YouTube playlist by id.
|
||||
|
||||
returns tuple from the playlist header and list of tuples (title, video id)
|
||||
"""
|
||||
request = Request(f"https://www.youtube.com/playlist?list={play_list_id}&hl=en", headers=_HEADERS)
|
||||
|
||||
with urlopen(request, timeout=_TIMEOUT) as resp:
|
||||
data = gzip.decompress(resp.read()).decode("utf-8")
|
||||
parser = PlayListParser()
|
||||
parser.feed(data)
|
||||
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].
|
||||
"extract_flat": False, # Do not resolve URLs, return the immediate result.
|
||||
"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 = f"{settings.default_data_path}tools{SEP}"
|
||||
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(f"{self._path}youtube_dl{SEP}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(f"YouTubeDLHelper error: {e}")
|
||||
raise YouTubeException(e)
|
||||
except ImportError as e:
|
||||
log(f"YouTubeDLHelper error: {e}")
|
||||
else:
|
||||
if self._path not in youtube_dl.__file__:
|
||||
msg = "Another version of youtube-dl was found on your system!"
|
||||
log(msg)
|
||||
raise YouTubeException(msg)
|
||||
|
||||
if self._update:
|
||||
if hasattr(youtube_dl.version, "__version__"):
|
||||
l_ver = self.get_last_release_id()
|
||||
cur_ver = youtube_dl.version.__version__
|
||||
if l_ver and youtube_dl.version.__version__ < l_ver:
|
||||
msg = f"youtube-dl has new release!\nCurrent: {cur_ver}. Last: {l_ver}."
|
||||
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(f"YouTubeDLHelper error [get last release id]: {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:
|
||||
if os.path.isdir(self._path):
|
||||
shutil.rmtree(self._path)
|
||||
|
||||
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:
|
||||
for info in arch.infolist():
|
||||
pref, sep, f = info.filename.partition("/youtube_dl/")
|
||||
if sep:
|
||||
arch.extract(info.filename)
|
||||
shutil.move(info.filename, f"{self._path}{sep}{f}")
|
||||
shutil.rmtree(pref)
|
||||
msg = "Getting the last youtube-dl release is done!"
|
||||
show_notification(msg)
|
||||
log(msg)
|
||||
self._callback(msg, False)
|
||||
|
||||
if os.path.isfile(zip_file):
|
||||
os.remove(zip_file)
|
||||
return True
|
||||
except URLError as e:
|
||||
log(f"YouTubeDLHelper error: {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 {}, ""
|
||||
|
||||
info = self.get_info(url, skip_errors)
|
||||
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 get_info(self, url, skip_errors=False):
|
||||
try:
|
||||
return self._dl.extract_info(url, download=False)
|
||||
except URLError as e:
|
||||
log(f"YouTubeDLHelper error [get info]: {e}")
|
||||
raise YouTubeException(e)
|
||||
except self._DownloadError as e:
|
||||
log(f"YouTubeDLHelper error [get info]: {e}")
|
||||
if not skip_errors:
|
||||
raise YouTubeException(e)
|
||||
|
||||
def update_options(self, options):
|
||||
self._dl.params.update(options)
|
||||
|
||||
@property
|
||||
def options(self):
|
||||
return self._dl.params
|
||||
|
||||
|
||||
def flat(key, d):
|
||||
for k, v in d.items():
|
||||
if k == key:
|
||||
yield v
|
||||
elif isinstance(v, dict):
|
||||
yield from flat(key, v)
|
||||
elif isinstance(v, list):
|
||||
for el in v:
|
||||
if isinstance(el, dict):
|
||||
yield from flat(key, el)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@@ -1,20 +1 @@
|
||||
import gi
|
||||
import os
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
# path to *.glade files
|
||||
UI_RESOURCES_PATH = "app/ui/" if os.path.exists("app/ui/") else "/usr/share/demoneditor/app/ui/"
|
||||
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
_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(
|
||||
"emblem-readonly", 16, 0) else _IMAGE_MISSING
|
||||
LOCKED_ICON = theme.load_icon("system-lock-screen", 16, 0) if theme.lookup_icon(
|
||||
"system-lock-screen", 16, 0) else _IMAGE_MISSING
|
||||
HIDE_ICON = theme.load_icon("go-jump", 16, 0) if theme.lookup_icon("go-jump", 16, 0) else _IMAGE_MISSING
|
||||
TV_ICON = theme.load_icon("tv-symbolic", 16, 0) if theme.lookup_icon("tv-symbolic", 16, 0) else _IMAGE_MISSING
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
481
app/ui/app_menu.ui
Normal file
481
app/ui/app_menu.ui
Normal file
@@ -0,0 +1,481 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<menu id="menu_bar">
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Playback</attribute>
|
||||
<attribute name="action">app.hide_media_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Close</attribute>
|
||||
<attribute name="action">app.on_playback_close</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">File</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Import</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Bouquet</attribute>
|
||||
<attribute name="action">app.on_import_bouquet</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Bouquets and services</attribute>
|
||||
<attribute name="action">app.on_import_bouquets</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Import from Web</attribute>
|
||||
<attribute name="action">app.on_import_from_web</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">New empty configuration</attribute>
|
||||
<attribute name="action">app.on_new_configuration</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Open</attribute>
|
||||
<attribute name="action">app.on_data_open</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Extract...</attribute>
|
||||
<attribute name="action">app.on_archive_open</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Save</attribute>
|
||||
<attribute name="action">app.on_data_save</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Save as</attribute>
|
||||
<attribute name="action">app.on_data_save_as</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">FTP-transfer</attribute>
|
||||
<attribute name="action">app.on_download</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Settings</attribute>
|
||||
<attribute name="action">app.on_settings</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Exit</attribute>
|
||||
<attribute name="action">app.on_close_app</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Edit</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Lock</attribute>
|
||||
<attribute name="action">app.on_locked</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Hide</attribute>
|
||||
<attribute name="action">app.on_hide</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu id="view_menu">
|
||||
<attribute name="label" translatable="yes">View</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Bouquets</attribute>
|
||||
<attribute name="action">app.show_bouquets</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Satellites</attribute>
|
||||
<attribute name="action">app.show_satellites</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Picons</attribute>
|
||||
<attribute name="action">app.show_picons</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">EPG</attribute>
|
||||
<attribute name="action">app.show_epg</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Timers</attribute>
|
||||
<attribute name="action">app.show_timers</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Recordings</attribute>
|
||||
<attribute name="action">app.show_recordings</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">FTP</attribute>
|
||||
<attribute name="action">app.show_ftp</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Control</attribute>
|
||||
<attribute name="action">app.show_control</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Display picons</attribute>
|
||||
<attribute name="action">app.display_picons</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Alternate layout</attribute>
|
||||
<attribute name="action">app.set_alternate_layout</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu id="tools_menu">
|
||||
<attribute name="label" translatable="yes">Tools</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Backups</attribute>
|
||||
<attribute name="action">app.on_backup_tool_show</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Telnet</attribute>
|
||||
<attribute name="action">app.on_telnet_show</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Logs</attribute>
|
||||
<attribute name="action">app.on_logs_show</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">FTP client</attribute>
|
||||
<attribute name="action">app.show_ftp_menu</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Close</attribute>
|
||||
<attribute name="action">app.on_ftp_client_close</attribute>
|
||||
</item>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Help</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">About</attribute>
|
||||
<attribute name="action">app.on_about_app</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
</menu>
|
||||
<menu id="mac_app_menu">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">About</attribute>
|
||||
<attribute name="action">app.on_about_app</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Settings</attribute>
|
||||
<attribute name="action">app.on_settings</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Exit</attribute>
|
||||
<attribute name="action">app.on_close_app</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
<menu id="mac_menu_bar">
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Playback</attribute>
|
||||
<attribute name="action">app.hide_media_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Play</attribute>
|
||||
<attribute name="action">app.on_play</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Stop</attribute>
|
||||
<attribute name="action">app.on_stop</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Close</attribute>
|
||||
<attribute name="action">app.on_playback_close</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">File</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Import</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Bouquet</attribute>
|
||||
<attribute name="action">app.on_import_bouquet</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Bouquets and services</attribute>
|
||||
<attribute name="action">app.on_import_bouquets</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Import from Web</attribute>
|
||||
<attribute name="action">app.on_import_from_web</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">New empty configuration</attribute>
|
||||
<attribute name="action">app.on_new_configuration</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Open</attribute>
|
||||
<attribute name="action">app.on_data_open</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Extract...</attribute>
|
||||
<attribute name="action">app.on_archive_open</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Save</attribute>
|
||||
<attribute name="action">app.on_data_save</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Save as</attribute>
|
||||
<attribute name="action">app.on_data_save_as</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<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>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Lock</attribute>
|
||||
<attribute name="action">app.on_locked</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Hide</attribute>
|
||||
<attribute name="action">app.on_hide</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">View</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Bouquets</attribute>
|
||||
<attribute name="action">app.show_bouquets</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Satellites</attribute>
|
||||
<attribute name="action">app.show_satellites</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Picons</attribute>
|
||||
<attribute name="action">app.show_picons</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">EPG</attribute>
|
||||
<attribute name="action">app.show_epg</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Timers</attribute>
|
||||
<attribute name="action">app.show_timers</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Recordings</attribute>
|
||||
<attribute name="action">app.show_recordings</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">FTP</attribute>
|
||||
<attribute name="action">app.show_ftp</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Control</attribute>
|
||||
<attribute name="action">app.show_control</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Display picons</attribute>
|
||||
<attribute name="action">app.display_picons</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Alternate layout</attribute>
|
||||
<attribute name="action">app.set_alternate_layout</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Tools</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Backups</attribute>
|
||||
<attribute name="action">app.on_backup_tool_show</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Telnet</attribute>
|
||||
<attribute name="action">app.on_telnet_show</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Logs</attribute>
|
||||
<attribute name="action">app.on_logs_show</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">FTP client</attribute>
|
||||
<attribute name="action">app.show_ftp_menu</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Close</attribute>
|
||||
<attribute name="action">app.on_ftp_client_close</attribute>
|
||||
</item>
|
||||
</submenu>
|
||||
</menu>
|
||||
<menu id="iptv_menu">
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Add IPTV or stream service</attribute>
|
||||
<attribute name="action">app.on_iptv</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Import YouTube playlist</attribute>
|
||||
<attribute name="action">app.on_import_yt_list</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Import m3u</attribute>
|
||||
<attribute name="action">app.on_import_m3u</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Export to m3u</attribute>
|
||||
<attribute name="action">app.on_export_iptv_to_m3u</attribute>
|
||||
</item>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">EPG configuration</attribute>
|
||||
<attribute name="action">app.on_epg_list_configuration</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">List configuration</attribute>
|
||||
<attribute name="action">app.on_iptv_list_configuration</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Remove all unavailable</attribute>
|
||||
<attribute name="action">app.on_remove_all_unavailable</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
<menu id="audio_menu">
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Audio</attribute>
|
||||
<attribute name="action">app.hide_media_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<submenu id="audio_track_menu">
|
||||
<attribute name="label" translatable="yes">Audio Track</attribute>
|
||||
</submenu>
|
||||
</section>
|
||||
</submenu>
|
||||
</menu>
|
||||
<menu id="video_menu">
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Video</attribute>
|
||||
<attribute name="action">app.hide_media_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<submenu id="aspect_ratio_menu">
|
||||
<attribute name="label" translatable="yes">Aspect ratio</attribute>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Default</attribute>
|
||||
<attribute name="action">app.on_set_aspect_ratio</attribute>
|
||||
<attribute name="target"/>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">16:9</attribute>
|
||||
<attribute name="action">app.on_set_aspect_ratio</attribute>
|
||||
<attribute name="target">16:9</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">4:3</attribute>
|
||||
<attribute name="action">app.on_set_aspect_ratio</attribute>
|
||||
<attribute name="target">4:3</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">1:1</attribute>
|
||||
<attribute name="action">app.on_set_aspect_ratio</attribute>
|
||||
<attribute name="target">1:1</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">16:10</attribute>
|
||||
<attribute name="action">app.on_set_aspect_ratio</attribute>
|
||||
<attribute name="target">16:10</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">5:4</attribute>
|
||||
<attribute name="action">app.on_set_aspect_ratio</attribute>
|
||||
<attribute name="target">5:4</attribute>
|
||||
</item>
|
||||
</submenu>
|
||||
</section>
|
||||
</submenu>
|
||||
</menu>
|
||||
<menu id="subtitle_menu">
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Subtitle</attribute>
|
||||
<attribute name="action">app.hide_media_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<submenu id="subtitle_track_menu">
|
||||
<attribute name="label" translatable="yes">Subtitle Track</attribute>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Default</attribute>
|
||||
<attribute name="action">app.on_set_subtitle_track</attribute>
|
||||
<attribute name="target"/>
|
||||
</item>
|
||||
</submenu>
|
||||
</section>
|
||||
</submenu>
|
||||
</menu>
|
||||
</interface>
|
||||
264
app/ui/backup.py
Normal file
264
app/ui/backup.py
Normal file
@@ -0,0 +1,264 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
from app.commons import run_idle
|
||||
from app.settings import SettingsType, SEP
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_builder
|
||||
from app.ui.main_helper import append_text_to_tview
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, IS_GNOME_SESSION
|
||||
|
||||
|
||||
class RestoreType(Enum):
|
||||
BOUQUETS = 0
|
||||
ALL = 1
|
||||
|
||||
|
||||
class BackupDialog:
|
||||
def __init__(self, transient, settings, callback):
|
||||
handlers = {"on_restore_bouquets": self.on_restore_bouquets,
|
||||
"on_restore_all": self.on_restore_all,
|
||||
"on_remove": self.on_remove,
|
||||
"on_view_popup_menu": self.on_view_popup_menu,
|
||||
"on_info_button_toggled": self.on_info_button_toggled,
|
||||
"on_info_bar_close": self.on_info_bar_close,
|
||||
"on_cursor_changed": self.on_cursor_changed,
|
||||
"on_resize": self.on_resize,
|
||||
"on_key_release": self.on_key_release}
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "backup_dialog.glade", handlers)
|
||||
|
||||
self._settings = settings
|
||||
self._s_type = settings.setting_type
|
||||
self._data_path = self._settings.profile_data_path
|
||||
self._backup_path = self._settings.profile_backup_path or "{}backup{}".format(self._data_path, os.sep)
|
||||
self._open_data_callback = callback
|
||||
self._dialog_window = builder.get_object("dialog_window")
|
||||
self._dialog_window.set_transient_for(transient)
|
||||
self._model = builder.get_object("main_list_store")
|
||||
self._main_view = builder.get_object("main_view")
|
||||
self._text_view = builder.get_object("text_view")
|
||||
self._text_view_scrolled_window = builder.get_object("text_view_scrolled_window")
|
||||
self._info_check_button = builder.get_object("info_check_button")
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("message_label")
|
||||
|
||||
if IS_GNOME_SESSION:
|
||||
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True)
|
||||
self._dialog_window.set_titlebar(header_bar)
|
||||
|
||||
button_box = builder.get_object("main_button_box")
|
||||
button_box.set_margin_top(0)
|
||||
button_box.set_margin_bottom(0)
|
||||
button_box.set_margin_left(0)
|
||||
button_box.reparent(header_bar)
|
||||
|
||||
ch_button = builder.get_object("info_check_button")
|
||||
ch_button.set_margin_right(0)
|
||||
h_bar = builder.get_object("header_bar")
|
||||
h_bar.remove(ch_button)
|
||||
h_bar.set_visible(False)
|
||||
header_bar.pack_end(ch_button)
|
||||
|
||||
# Setting the last size of the dialog window if it was saved
|
||||
window_size = self._settings.get("backup_tool_window_size")
|
||||
if window_size:
|
||||
self._dialog_window.resize(*window_size)
|
||||
|
||||
self.init_data()
|
||||
|
||||
def show(self):
|
||||
self._dialog_window.show()
|
||||
|
||||
@run_idle
|
||||
def init_data(self):
|
||||
if os.path.isdir(self._backup_path):
|
||||
for file in filter(lambda x: x.endswith(".zip"), os.listdir(self._backup_path)):
|
||||
self._model.append((file.rstrip(".zip"), False))
|
||||
else:
|
||||
os.makedirs(os.path.dirname(self._backup_path), exist_ok=True)
|
||||
|
||||
def on_restore_bouquets(self, item):
|
||||
self.restore(RestoreType.BOUQUETS)
|
||||
|
||||
def on_restore_all(self, item):
|
||||
self.restore(RestoreType.ALL)
|
||||
|
||||
def on_remove(self, item):
|
||||
model, paths = self._main_view.get_selection().get_selected_rows()
|
||||
if not paths:
|
||||
show_dialog(DialogType.ERROR, self._dialog_window, "No selected item!")
|
||||
return
|
||||
|
||||
if show_dialog(DialogType.QUESTION, self._dialog_window) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
itrs_to_delete = []
|
||||
try:
|
||||
for itr in map(model.get_iter, paths):
|
||||
file_name = model.get_value(itr, 0)
|
||||
os.remove("{}{}{}".format(self._backup_path, file_name, ".zip"))
|
||||
itrs_to_delete.append(itr)
|
||||
except FileNotFoundError as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
else:
|
||||
list(map(model.remove, itrs_to_delete))
|
||||
|
||||
def on_view_popup_menu(self, menu, event):
|
||||
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
|
||||
menu.popup(None, None, None, None, event.button, event.time)
|
||||
|
||||
def on_info_button_toggled(self, button):
|
||||
active = button.get_active()
|
||||
self._text_view_scrolled_window.set_visible(active)
|
||||
if active:
|
||||
self.on_cursor_changed(self._main_view)
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(True)
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._message_label.set_text(text)
|
||||
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
self._info_bar.set_visible(False)
|
||||
|
||||
def on_cursor_changed(self, view):
|
||||
if not self._info_check_button.get_active():
|
||||
return
|
||||
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if paths:
|
||||
try:
|
||||
file_name = self._backup_path + model.get_value(model.get_iter(paths[0]), 0) + ".zip"
|
||||
created = time.ctime(os.path.getctime(file_name))
|
||||
self._text_view.get_buffer().set_text(
|
||||
"Created: {}\n********** Files: **********\n".format(created))
|
||||
with zipfile.ZipFile(file_name) as zip_file:
|
||||
for name in zip_file.namelist():
|
||||
append_text_to_tview(name + "\n", self._text_view)
|
||||
except FileNotFoundError as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
else:
|
||||
self._text_view.get_buffer().set_text("")
|
||||
|
||||
def restore(self, restore_type):
|
||||
model, paths = self._main_view.get_selection().get_selected_rows()
|
||||
if not paths:
|
||||
show_dialog(DialogType.ERROR, self._dialog_window, "No selected item!")
|
||||
return
|
||||
|
||||
if len(paths) > 1:
|
||||
show_dialog(DialogType.ERROR, self._dialog_window, "Please, select only one item!")
|
||||
return
|
||||
|
||||
if show_dialog(DialogType.QUESTION, self._dialog_window) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
file_name = model.get_value(model.get_iter(paths[0]), 0)
|
||||
full_file_name = self._backup_path + file_name + ".zip"
|
||||
|
||||
try:
|
||||
if restore_type is RestoreType.ALL:
|
||||
clear_data_path(self._data_path)
|
||||
shutil.unpack_archive(full_file_name, self._data_path)
|
||||
elif restore_type is RestoreType.BOUQUETS:
|
||||
tmp_dir = tempfile.gettempdir() + SEP + file_name
|
||||
cond = (".tv", ".radio") if self._s_type is SettingsType.ENIGMA_2 else "bouquets.xml"
|
||||
shutil.unpack_archive(full_file_name, tmp_dir)
|
||||
for file in filter(lambda f: f.endswith(cond), os.listdir(self._data_path)):
|
||||
os.remove(os.path.join(self._data_path, file))
|
||||
for file in filter(lambda f: f.endswith(cond), os.listdir(tmp_dir)):
|
||||
shutil.move(os.path.join(tmp_dir, file), self._data_path + file)
|
||||
shutil.rmtree(tmp_dir)
|
||||
except FileNotFoundError as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
else:
|
||||
self.show_info_message("Done!", Gtk.MessageType.INFO)
|
||||
self._open_data_callback(self._data_path)
|
||||
|
||||
def on_resize(self, window):
|
||||
if self._settings:
|
||||
self._settings.add("backup_tool_window_size", window.get_size())
|
||||
|
||||
def on_key_release(self, view, event):
|
||||
""" Handling keystrokes """
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
return
|
||||
key = KeyboardKey(key_code)
|
||||
ctrl = event.state & MOD_MASK
|
||||
|
||||
if key is KeyboardKey.DELETE:
|
||||
self.on_remove(view)
|
||||
elif ctrl and key is KeyboardKey.E:
|
||||
self.restore(RestoreType.ALL)
|
||||
elif ctrl and key is KeyboardKey.R:
|
||||
self.restore(RestoreType.BOUQUETS)
|
||||
|
||||
|
||||
def backup_data(path, backup_path, move=True):
|
||||
""" Creating data backup from a folder at the specified path
|
||||
|
||||
Returns full path to the compressed file.
|
||||
"""
|
||||
backup_path = "{}{}{}".format(backup_path, datetime.now().strftime("%Y-%m-%d_%H-%M-%S"), SEP)
|
||||
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
# backup files in data dir(skipping dirs and satellites.xml)
|
||||
for file in filter(lambda f: f != "satellites.xml" and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
|
||||
src, dst = os.path.join(path, file), backup_path + file
|
||||
shutil.move(src, dst) if move else shutil.copy(src, dst)
|
||||
# compressing to zip and delete remaining files
|
||||
zip_file = shutil.make_archive(backup_path, "zip", backup_path)
|
||||
shutil.rmtree(backup_path)
|
||||
|
||||
return zip_file
|
||||
|
||||
|
||||
def restore_data(src, dst):
|
||||
""" Unpacks backup data. """
|
||||
clear_data_path(dst)
|
||||
shutil.unpack_archive(src, dst)
|
||||
|
||||
|
||||
def clear_data_path(path):
|
||||
""" Clearing data at the specified path excluding satellites.xml file """
|
||||
for file in filter(lambda f: f != "satellites.xml" and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
|
||||
os.remove(os.path.join(path, file))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
368
app/ui/backup_dialog.glade
Normal file
368
app/ui/backup_dialog.glade
Normal file
@@ -0,0 +1,368 @@
|
||||
<?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>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkImage" id="details_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">emblem-important-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkListStore" id="main_list_store">
|
||||
<columns>
|
||||
<!-- column-name date -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name selected -->
|
||||
<column type="gboolean"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkMenu" id="popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="restore_bouquets_popup_menu_item">
|
||||
<property name="label" translatable="yes">Restore bouquets</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_restore_bouquets" swapped="no"/>
|
||||
<accelerator key="r" signal="activate" modifiers="Primary"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="restore_all_popup_menu_item">
|
||||
<property name="label" translatable="yes">Restore all</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_restore_all" swapped="no"/>
|
||||
<accelerator key="e" signal="activate" modifiers="Primary"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="popup_menu_separator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="remove_popup_menu_item">
|
||||
<property name="label">gtk-remove</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="activate" handler="on_remove" swapped="no"/>
|
||||
<accelerator key="Delete" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkImage" id="remove_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">user-trash-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="restore_all_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-select-all-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="restore_bouquets_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">document-revert-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkWindow" id="dialog_window">
|
||||
<property name="width_request">560</property>
|
||||
<property name="height_request">320</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">Backups</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window_position">center-on-parent</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="icon_name">document-revert</property>
|
||||
<signal name="check-resize" handler="on_resize" swapped="no"/>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="header_bar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="main_button_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_left">15</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="margin_bottom">10</property>
|
||||
<property name="layout_style">expand</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="restore_bouquets_header_button">
|
||||
<property name="label" translatable="yes">Restore bouquets</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="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">False</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">False</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">False</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_right">15</property>
|
||||
<property name="image">details_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
|
||||
<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>
|
||||
<style>
|
||||
<class name="primary-toolbar"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkPaned" id="main_paned">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="wide_handle">True</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="main_view_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="main_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="model">main_list_store</property>
|
||||
<property name="headers_visible">False</property>
|
||||
<property name="search_column">0</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<property name="activate_on_single_click">True</property>
|
||||
<signal name="button-press-event" handler="on_view_popup_menu" object="popup_menu" swapped="no"/>
|
||||
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
|
||||
<signal name="key-release-event" handler="on_key_release" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="main_view_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="backup_date_column">
|
||||
<property name="title" translatable="yes">Backup</property>
|
||||
<property name="clickable">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="reorderable">True</property>
|
||||
<property name="sort_column_id">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="date_render">
|
||||
<property name="xpad">10</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="text_view_scrolled_window">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="text_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="pixels_above_lines">5</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="left_margin">10</property>
|
||||
<property name="right_margin">10</property>
|
||||
<property name="indent">10</property>
|
||||
<property name="cursor_visible">False</property>
|
||||
<property name="accepts_tab">False</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<signal name="response" handler="on_info_bar_close" swapped="no"/>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout_style">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">16</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="message_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="label" translatable="yes">message</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
3425
app/ui/control.glade
Normal file
3425
app/ui/control.glade
Normal file
File diff suppressed because it is too large
Load Diff
1037
app/ui/control.py
Normal file
1037
app/ui/control.py
Normal file
File diff suppressed because it is too large
Load Diff
1281
app/ui/dialogs.glade
1281
app/ui/dialogs.glade
File diff suppressed because it is too large
Load Diff
@@ -1,62 +1,257 @@
|
||||
""" Common module for showing dialogs """
|
||||
from enum import Enum
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
from . import Gtk, UI_RESOURCES_PATH
|
||||
|
||||
""" Common module for showing dialogs """
|
||||
import gettext
|
||||
import xml.etree.ElementTree as ET
|
||||
from enum import Enum
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
|
||||
from app.commons import run_idle
|
||||
from app.settings import SEP, IS_WIN
|
||||
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN, IS_GNOME_SESSION
|
||||
|
||||
|
||||
class Dialog(Enum):
|
||||
MESSAGE = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<object class="GtkMessageDialog" id="message_dialog">
|
||||
<property name="use-header-bar">{use_header}</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="width_request">250</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<property name="skip_taskbar_hint">True</property>
|
||||
<property name="skip_pager_hint">True</property>
|
||||
<property name="message_type">{message_type}</property>
|
||||
<property name="buttons">{buttons_type}</property>
|
||||
</object>
|
||||
</interface>
|
||||
"""
|
||||
|
||||
|
||||
class Action(Enum):
|
||||
EDIT = 0
|
||||
ADD = 1
|
||||
|
||||
|
||||
class DialogType(Enum):
|
||||
INPUT = "input_dialog"
|
||||
MESSAGE = ""
|
||||
CHOOSER = "path_chooser_dialog"
|
||||
ERROR = "error_dialog"
|
||||
QUESTION = "question_dialog"
|
||||
ABOUT = "about_dialog"
|
||||
INPUT = "input"
|
||||
CHOOSER = "chooser"
|
||||
ERROR = "error"
|
||||
QUESTION = "question"
|
||||
INFO = "info"
|
||||
ABOUT = "about"
|
||||
WAIT = "wait"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
def show_dialog(dialog_type: DialogType, transient, text=None, options=None, action_type=None, file_filter=None):
|
||||
""" Shows dialogs by name """
|
||||
class WaitDialog:
|
||||
def __init__(self, transient, text=None):
|
||||
builder, dialog = get_dialog_from_xml(DialogType.WAIT, transient)
|
||||
self._dialog = dialog
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._label = builder.get_object("wait_dialog_label")
|
||||
self._default_text = text or self._label.get_text()
|
||||
|
||||
def show(self, text=None):
|
||||
self.set_text(text)
|
||||
self._dialog.show()
|
||||
|
||||
@run_idle
|
||||
def set_text(self, text):
|
||||
self._label.set_text(get_message(text or self._default_text))
|
||||
|
||||
@run_idle
|
||||
def hide(self):
|
||||
self._dialog.hide()
|
||||
|
||||
@run_idle
|
||||
def destroy(self):
|
||||
self._dialog.destroy()
|
||||
|
||||
|
||||
def show_dialog(dialog_type, transient, text=None, settings=None, action_type=None, file_filter=None, buttons=None,
|
||||
title=None, create_dir=False):
|
||||
""" Shows dialogs by name. """
|
||||
if dialog_type in (DialogType.INFO, DialogType.ERROR):
|
||||
return get_message_dialog(transient, dialog_type, Gtk.ButtonsType.OK, text)
|
||||
elif dialog_type is DialogType.CHOOSER and settings:
|
||||
return get_file_chooser_dialog(transient, text, settings, action_type, file_filter, buttons, title, create_dir)
|
||||
elif dialog_type is DialogType.INPUT:
|
||||
return get_input_dialog(transient, text)
|
||||
elif dialog_type is DialogType.QUESTION:
|
||||
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:
|
||||
return get_about_dialog(transient)
|
||||
|
||||
|
||||
def get_chooser_dialog(transient, settings, name, patterns, title=None, file_filter=None):
|
||||
if not file_filter:
|
||||
file_filter = Gtk.FileFilter()
|
||||
file_filter.set_name(name)
|
||||
for p in patterns:
|
||||
file_filter.add_pattern(p)
|
||||
|
||||
return show_dialog(dialog_type=DialogType.CHOOSER,
|
||||
transient=transient,
|
||||
settings=settings,
|
||||
action_type=Gtk.FileChooserAction.OPEN,
|
||||
file_filter=file_filter,
|
||||
title=title)
|
||||
|
||||
|
||||
def get_file_chooser_dialog(transient, text, settings, action_type, file_filter, buttons=None, title=None, dirs=False):
|
||||
action_type = Gtk.FileChooserAction.SELECT_FOLDER if action_type is None else action_type
|
||||
dialog = Gtk.FileChooserNative.new(get_message(title) if title else "", transient, action_type)
|
||||
dialog.set_create_folders(dirs)
|
||||
dialog.set_modal(True)
|
||||
|
||||
if file_filter is not None:
|
||||
dialog.add_filter(file_filter)
|
||||
|
||||
dialog.set_current_folder(settings.profile_data_path)
|
||||
response = dialog.run()
|
||||
|
||||
if response == Gtk.ResponseType.ACCEPT:
|
||||
path = Path(dialog.get_filename() or dialog.get_current_folder())
|
||||
if path.is_dir():
|
||||
response = "{}{}".format(path.resolve(), SEP)
|
||||
elif path.is_file():
|
||||
response = str(path.resolve())
|
||||
dialog.destroy()
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def get_input_dialog(transient, text):
|
||||
builder, dialog = get_dialog_from_xml(DialogType.INPUT, transient, use_header=IS_GNOME_SESSION)
|
||||
entry = builder.get_object("input_entry")
|
||||
entry.set_text(text if text else "")
|
||||
response = dialog.run()
|
||||
txt = entry.get_text()
|
||||
dialog.destroy()
|
||||
|
||||
return txt if response == Gtk.ResponseType.OK else Gtk.ResponseType.CANCEL
|
||||
|
||||
|
||||
def get_message_dialog(transient, message_type, buttons_type, text):
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "dialogs.glade")
|
||||
dialog = builder.get_object(dialog_type.value)
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
dialog_str = Dialog.MESSAGE.value.format(use_header=0, message_type=message_type, buttons_type=int(buttons_type))
|
||||
builder.add_from_string(dialog_str)
|
||||
dialog = builder.get_object("message_dialog")
|
||||
dialog.set_transient_for(transient)
|
||||
|
||||
if dialog_type is DialogType.CHOOSER and options:
|
||||
if action_type is not None:
|
||||
dialog.set_action(action_type)
|
||||
if file_filter is not None:
|
||||
dialog.add_filter(file_filter)
|
||||
|
||||
path = options.get("data_dir_path")
|
||||
dialog.set_current_folder(path)
|
||||
|
||||
response = dialog.run()
|
||||
if response == -12: # -12 for fix assertion 'gtk_widget_get_can_default (widget)' failed
|
||||
if dialog.get_filename():
|
||||
path = dialog.get_filename()
|
||||
if action_type is not Gtk.FileChooserAction.OPEN:
|
||||
path = path + "/"
|
||||
|
||||
response = path
|
||||
dialog.destroy()
|
||||
|
||||
return response
|
||||
|
||||
if dialog_type is DialogType.INPUT:
|
||||
entry = builder.get_object("input_entry")
|
||||
entry.set_text(text if text else "")
|
||||
response = dialog.run()
|
||||
txt = entry.get_text()
|
||||
dialog.destroy()
|
||||
|
||||
return txt if response == Gtk.ResponseType.OK else Gtk.ResponseType.CANCEL
|
||||
|
||||
if text:
|
||||
dialog.set_markup(text)
|
||||
dialog.set_markup(get_message(text))
|
||||
response = dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def get_about_dialog(transient):
|
||||
builder, dialog = get_dialog_from_xml(DialogType.ABOUT, transient)
|
||||
dialog.set_transient_for(transient)
|
||||
response = dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def get_dialog_from_xml(dialog_type, transient, use_header=0, title=""):
|
||||
dialog_name = dialog_type.value + "_dialog"
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
dialog_str = get_dialogs_string(UI_RESOURCES_PATH + "dialogs.glade").format(use_header=use_header, title=title)
|
||||
builder.add_objects_from_string(dialog_str, (dialog_name,))
|
||||
dialog = builder.get_object(dialog_name)
|
||||
dialog.set_transient_for(transient)
|
||||
|
||||
return builder, dialog
|
||||
|
||||
|
||||
def get_message(message):
|
||||
""" returns translated message """
|
||||
return gettext.dgettext(TEXT_DOMAIN, message)
|
||||
|
||||
|
||||
@lru_cache(maxsize=5)
|
||||
def get_dialogs_string(path, tag="property"):
|
||||
if IS_WIN:
|
||||
return translate_xml(path, tag)
|
||||
else:
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return "".join(f)
|
||||
|
||||
|
||||
def get_builder(path, handlers=None, use_str=False, objects=None, tag="property"):
|
||||
""" Creates and returns a Gtk.Builder instance. """
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
|
||||
if use_str:
|
||||
if objects:
|
||||
builder.add_objects_from_string(get_dialogs_string(path, tag).format(use_header=IS_GNOME_SESSION), objects)
|
||||
else:
|
||||
builder.add_from_string(get_dialogs_string(path, tag).format(use_header=IS_GNOME_SESSION))
|
||||
else:
|
||||
if objects:
|
||||
builder.add_objects_from_string(get_dialogs_string(path, tag), objects)
|
||||
else:
|
||||
builder.add_from_string(get_dialogs_string(path, tag))
|
||||
|
||||
builder.connect_signals(handlers or {})
|
||||
|
||||
return builder
|
||||
|
||||
|
||||
def translate_xml(path, tag="property"):
|
||||
""" Used to translate GUI from * .glade files in MS Windows.
|
||||
|
||||
More info: https://gitlab.gnome.org/GNOME/gtk/-/issues/569
|
||||
"""
|
||||
et = ET.parse(path)
|
||||
root = et.getroot()
|
||||
for e in root.iter():
|
||||
if e.tag == tag and e.attrib.get("translatable", None) == "yes":
|
||||
e.text = get_message(e.text)
|
||||
elif e.tag == "item" and e.attrib.get("translatable", None) == "yes":
|
||||
e.text = get_message(e.text)
|
||||
|
||||
return ET.tostring(root, encoding="unicode", method="xml")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
573
app/ui/download_dialog.glade
Normal file
573
app/ui/download_dialog.glade
Normal file
@@ -0,0 +1,573 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor. -->
|
||||
<!-- interface-copyright 2018 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkImage" id="download_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">network-receive-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="send_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">network-transmit-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkWindow" id="download_dialog_window">
|
||||
<property name="width_request">550</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">FTP-transfer</property>
|
||||
<property name="resizable">False</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window_position">center-on-parent</property>
|
||||
<property name="icon_name">mail-send-receive-symbolic</property>
|
||||
<property name="skip_taskbar_hint">True</property>
|
||||
<property name="skip_pager_hint">True</property>
|
||||
<child type="titlebar">
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_dialog_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="header_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="profile_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Profile:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="profile_combo_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="active">0</property>
|
||||
<property name="has_frame">False</property>
|
||||
<signal name="changed" handler="on_profile_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="options_button">
|
||||
<property name="label" translatable="yes">Options</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Options</property>
|
||||
<signal name="clicked" handler="on_settings" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="selection_data_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label10">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="all_radio_button">
|
||||
<property name="label" translatable="yes">All</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">satellites_radio_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="bouquets_radio_button">
|
||||
<property name="label" translatable="yes">Bouquets</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">satellites_radio_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="satellites_radio_button">
|
||||
<property name="label" translatable="yes">Satellites</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">all_radio_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="webtv_radio_button">
|
||||
<property name="label" translatable="yes">WebTV</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">all_radio_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="primary-toolbar"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="main_settings_box_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="label_xalign">0.019999999552965164</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_settings_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="margin_bottom">10</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkGrid" id="main_settings_bo">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="row_spacing">5</property>
|
||||
<property name="column_spacing">10</property>
|
||||
<property name="column_homogeneous">True</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ip_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Receiver IP:</property>
|
||||
<property name="xalign">0.10000000149011612</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="host_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="max_width_chars">10</property>
|
||||
<property name="text">127.0.0.1</property>
|
||||
<property name="caps_lock_warning">False</property>
|
||||
<property name="primary_icon_name">network-transmit-receive-symbolic</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="data_path_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Current data path:</property>
|
||||
<property name="xalign">0.10000000149011612</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="data_path_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="text">data/</property>
|
||||
<property name="caps_lock_warning">False</property>
|
||||
<property name="primary_icon_name">folder-open-symbolic</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="extra_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<child>
|
||||
<object class="GtkCheckButton" id="remove_unused_check_button">
|
||||
<property name="label" translatable="yes">Remove unused bouquets</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<signal name="toggled" handler="on_remove_unused_bouquets_toggled" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="use_http_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="use_http_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Use HTTP</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="use_http_switch">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Use http to reload data in the receiver.</property>
|
||||
<property name="active">True</property>
|
||||
<signal name="state-set" handler="on_use_http_state_set" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label_item">
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="button_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_left">20</property>
|
||||
<property name="margin_right">20</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="margin_bottom">10</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="layout_style">expand</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="receive_button">
|
||||
<property name="label" translatable="yes">Receive</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Receive</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="image">download_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_receive" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="send_button">
|
||||
<property name="label" translatable="yes">Send</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Send</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="image">send_image</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_send" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="log_bar_frame">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="label_xalign">0</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="log_bar">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="baseline_position">bottom</property>
|
||||
<property name="message_type">other</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox" id="log_bar_button_box">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="layout_style">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox" id="log_bar_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrolled_window">
|
||||
<property name="height_request">100</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="text_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="left_margin">5</property>
|
||||
<property name="right_margin">5</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label_item">
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<signal name="response" handler="on_info_bar_close" swapped="no"/>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="layout_style">expand</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">16</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="info_bar_message_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Info</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">8</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
@@ -1,88 +1,186 @@
|
||||
from app.commons import run_idle, run_task
|
||||
from app.ftp import download_data, DownloadDataType, upload_data
|
||||
from app.properties import Profile
|
||||
from . import Gtk, UI_RESOURCES_PATH
|
||||
from .dialogs import show_dialog, DialogType
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
def show_download_dialog(transient, options, open_data, profile=Profile.ENIGMA_2):
|
||||
dialog = DownloadDialog(transient, options, open_data, profile)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
import os
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import run_idle, run_task, log
|
||||
from app.connections import download_data, DownloadType, upload_data
|
||||
from app.settings import SettingsType
|
||||
from app.ui.backup import backup_data, restore_data
|
||||
from app.ui.main_helper import append_text_to_tview
|
||||
from app.ui.settings_dialog import SettingsDialog
|
||||
from .dialogs import show_dialog, DialogType, get_message, get_builder
|
||||
from .uicommons import Gtk, UI_RESOURCES_PATH
|
||||
|
||||
|
||||
class DownloadDialog:
|
||||
def __init__(self, transient, properties, open_data, profile):
|
||||
self._properties = properties
|
||||
self._open_data = open_data
|
||||
self._profile = profile
|
||||
def __init__(self, transient, settings, open_data_callback, update_settings_callback):
|
||||
self._s_type = settings.setting_type
|
||||
self._settings = settings
|
||||
self._open_data_callback = open_data_callback
|
||||
self._update_settings_callback = update_settings_callback
|
||||
|
||||
handlers = {"on_receive": self.on_receive,
|
||||
"on_send": self.on_send,
|
||||
"on_settings": self.on_settings,
|
||||
"on_profile_changed": self.on_profile_changed,
|
||||
"on_use_http_state_set": self.on_use_http_state_set,
|
||||
"on_remove_unused_bouquets_toggled": self.on_remove_unused_bouquets_toggled,
|
||||
"on_info_bar_close": self.on_info_bar_close}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.add_objects_from_file(UI_RESOURCES_PATH + "dialogs.glade", ("download_dialog",))
|
||||
builder.connect_signals(handlers)
|
||||
builder = get_builder(UI_RESOURCES_PATH + "download_dialog.glade", handlers)
|
||||
|
||||
self._dialog = builder.get_object("download_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("info_bar_message_label")
|
||||
self._host_entry = builder.get_object("host_entry").set_text(properties["host"])
|
||||
self._data_path_entry = builder.get_object("data_path_entry").set_text(properties["data_dir_path"])
|
||||
self._dialog_window = builder.get_object("download_dialog_window")
|
||||
self._dialog_window.set_transient_for(transient)
|
||||
self._host_entry = builder.get_object("host_entry")
|
||||
self._data_path_entry = builder.get_object("data_path_entry")
|
||||
self._remove_unused_check_button = builder.get_object("remove_unused_check_button")
|
||||
self._all_radio_button = builder.get_object("all_radio_button")
|
||||
self._bouquets_radio_button = builder.get_object("bouquets_radio_button")
|
||||
self._satellites_radio_button = builder.get_object("satellites_radio_button")
|
||||
# self._dialog.get_content_area().set_border_width(0)
|
||||
self._webtv_radio_button = builder.get_object("webtv_radio_button")
|
||||
self._use_http_switch = builder.get_object("use_http_switch")
|
||||
self._http_radio_button = builder.get_object("http_radio_button")
|
||||
self._profile_combo_box = builder.get_object("profile_combo_box")
|
||||
# Info.
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("info_bar_message_label")
|
||||
self._text_view = builder.get_object("text_view")
|
||||
self._log_bar = builder.get_object("log_bar")
|
||||
self._log_bar.bind_property("visible", builder.get_object("log_bar_frame"), "visible")
|
||||
self._log_bar.connect("response", lambda b, r: b.set_visible(False))
|
||||
|
||||
self.init_settings()
|
||||
|
||||
def show(self):
|
||||
self._dialog_window.show()
|
||||
|
||||
def init_settings(self):
|
||||
self.update_profiles()
|
||||
self.init_ui_settings()
|
||||
|
||||
def init_ui_settings(self):
|
||||
self._host_entry.set_text(self._settings.host)
|
||||
self._data_path_entry.set_text(self._settings.profile_data_path)
|
||||
is_enigma = self._s_type is SettingsType.ENIGMA_2
|
||||
self._webtv_radio_button.set_visible(not is_enigma)
|
||||
self._use_http_switch.set_active(self._settings.use_http)
|
||||
self._remove_unused_check_button.set_active(self._settings.remove_unused_bouquets)
|
||||
|
||||
def update_profiles(self):
|
||||
self._profile_combo_box.remove_all()
|
||||
for p in self._settings.profiles:
|
||||
self._profile_combo_box.append(p, p)
|
||||
self._profile_combo_box.set_active_id(self._settings.current_profile)
|
||||
|
||||
@run_idle
|
||||
def on_receive(self, item):
|
||||
self.download(True, d_type=self.get_download_type())
|
||||
self.download(True, self.get_download_type())
|
||||
|
||||
@run_idle
|
||||
def on_send(self, item):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.CANCEL:
|
||||
self.download(d_type=self.get_download_type())
|
||||
if show_dialog(DialogType.QUESTION, self._dialog_window) != Gtk.ResponseType.CANCEL:
|
||||
self.download(False, self.get_download_type())
|
||||
|
||||
def get_download_type(self):
|
||||
download_type = DownloadDataType.ALL
|
||||
download_type = DownloadType.ALL
|
||||
if self._bouquets_radio_button.get_active():
|
||||
download_type = DownloadDataType.BOUQUETS
|
||||
download_type = DownloadType.BOUQUETS
|
||||
elif self._satellites_radio_button.get_active():
|
||||
download_type = DownloadDataType.SATELLITES
|
||||
download_type = DownloadType.SATELLITES
|
||||
elif self._webtv_radio_button.get_active():
|
||||
download_type = DownloadType.WEBTV
|
||||
return download_type
|
||||
|
||||
def run(self):
|
||||
return self._dialog.run()
|
||||
|
||||
def destroy(self):
|
||||
self._dialog.destroy()
|
||||
self._dialog_window.destroy()
|
||||
|
||||
def on_settings(self, item):
|
||||
dialog = SettingsDialog(self._dialog_window, self._settings)
|
||||
dialog.show()
|
||||
if dialog.is_updated():
|
||||
self._s_type = self._settings.setting_type
|
||||
self.update_profiles()
|
||||
gen = self._update_settings_callback()
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def on_profile_changed(self, box):
|
||||
active = box.get_active_text()
|
||||
if active in self._settings.profiles:
|
||||
self._settings.current_profile = active
|
||||
self._profile_combo_box.set_active_id(active)
|
||||
self._s_type = self._settings.setting_type
|
||||
self.init_ui_settings()
|
||||
|
||||
def on_use_http_state_set(self, button, state):
|
||||
self._settings.use_http = state
|
||||
|
||||
def on_remove_unused_bouquets_toggled(self, button):
|
||||
self._settings.remove_unused_bouquets = button.get_active()
|
||||
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
self._info_bar.set_visible(False)
|
||||
|
||||
@run_idle
|
||||
@run_task
|
||||
def download(self, download=False, d_type=DownloadDataType.ALL):
|
||||
def download(self, download, d_type):
|
||||
""" Download/upload data from/to receiver """
|
||||
GLib.idle_add(self._log_bar.set_visible, True)
|
||||
self.clear_output()
|
||||
backup, backup_src, data_path = self._settings.backup_before_downloading, None, None
|
||||
|
||||
try:
|
||||
if download:
|
||||
download_data(properties=self._properties, download_type=d_type)
|
||||
if backup and d_type is not DownloadType.SATELLITES:
|
||||
data_path = self._settings.profile_data_path or self._data_path_entry.get_text()
|
||||
os.makedirs(os.path.dirname(data_path), exist_ok=True)
|
||||
backup_path = self._settings.profile_backup_path or self._settings.default_backup_path
|
||||
backup_src = backup_data(data_path, backup_path, d_type is DownloadType.ALL)
|
||||
|
||||
download_data(settings=self._settings, download_type=d_type, callback=self.append_output)
|
||||
else:
|
||||
self.show_info_message("Please, wait...", Gtk.MessageType.INFO)
|
||||
upload_data(properties=self._properties,
|
||||
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
|
||||
upload_data(settings=self._settings,
|
||||
download_type=d_type,
|
||||
remove_unused=self._remove_unused_check_button.get_active(),
|
||||
profile=self._profile,
|
||||
callback=lambda: self.show_info_message("Done!", Gtk.MessageType.INFO))
|
||||
callback=self.append_output,
|
||||
done_callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO),
|
||||
use_http=self._use_http_switch.get_active())
|
||||
except Exception as e:
|
||||
message = str(getattr(e, "message", str(e)))
|
||||
self.show_info_message(message, Gtk.MessageType.ERROR)
|
||||
msg = "Downloading data error: {}"
|
||||
log(msg.format(e), debug=self._settings.debug_mode, fmt_message=msg)
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
if all((download, backup, data_path)):
|
||||
restore_data(backup_src, data_path)
|
||||
else:
|
||||
if download and d_type is not DownloadDataType.SATELLITES:
|
||||
self._open_data()
|
||||
if download and d_type is not DownloadType.SATELLITES:
|
||||
GLib.idle_add(self._open_data_callback)
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
@@ -90,6 +188,14 @@ class DownloadDialog:
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._message_label.set_text(text)
|
||||
|
||||
@run_idle
|
||||
def append_output(self, text):
|
||||
append_text_to_tview(text, self._text_view)
|
||||
|
||||
@run_idle
|
||||
def clear_output(self):
|
||||
self._text_view.get_buffer().set_text("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
1440
app/ui/epg.glade
Normal file
1440
app/ui/epg.glade
Normal file
File diff suppressed because it is too large
Load Diff
624
app/ui/epg.py
Normal file
624
app/ui/epg.py
Normal file
@@ -0,0 +1,624 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import gzip
|
||||
import locale
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import urllib.request
|
||||
from enum import Enum
|
||||
from urllib.error import HTTPError, URLError
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import run_idle, run_task, run_with_delay
|
||||
from app.connections import download_data, DownloadType
|
||||
from app.eparser.ecommons import BouquetService, BqServiceType
|
||||
from app.settings import SEP
|
||||
from app.tools.epg import EPG, ChannelsParser
|
||||
from app.ui.dialogs import get_message, show_dialog, DialogType, get_builder
|
||||
from .main_helper import on_popup_menu, update_entry_data
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, Column, EPG_ICON, KeyboardKey, IS_GNOME_SESSION
|
||||
|
||||
|
||||
class RefsSource(Enum):
|
||||
SERVICES = 0
|
||||
XML = 1
|
||||
|
||||
|
||||
class EpgDialog:
|
||||
|
||||
def __init__(self, app, bouquet, bouquet_name):
|
||||
|
||||
handlers = {"on_close_dialog": self.on_close_dialog,
|
||||
"on_apply": self.on_apply,
|
||||
"on_update": self.on_update,
|
||||
"on_save_to_xml": self.on_save_to_xml,
|
||||
"on_auto_configuration": self.on_auto_configuration,
|
||||
"on_filter_toggled": self.on_filter_toggled,
|
||||
"on_filter_changed": self.on_filter_changed,
|
||||
"on_info_bar_close": self.on_info_bar_close,
|
||||
"on_popup_menu": on_popup_menu,
|
||||
"on_bouquet_popup_menu": self.on_bouquet_popup_menu,
|
||||
"on_copy_ref": self.on_copy_ref,
|
||||
"on_assign_ref": self.on_assign_ref,
|
||||
"on_reset": self.on_reset,
|
||||
"on_list_reset": self.on_list_reset,
|
||||
"on_drag_begin": self.on_drag_begin,
|
||||
"on_drag_data_get": self.on_drag_data_get,
|
||||
"on_drag_data_received": self.on_drag_data_received,
|
||||
"on_resize": self.on_resize,
|
||||
"on_names_source_changed": self.on_names_source_changed,
|
||||
"on_options_save": self.on_options_save,
|
||||
"on_use_web_source_switch": self.on_use_web_source_switch,
|
||||
"on_enable_filtering_switch": self.on_enable_filtering_switch,
|
||||
"on_update_on_start_switch": self.on_update_on_start_switch,
|
||||
"on_field_icon_press": self.on_field_icon_press,
|
||||
"on_key_press": self.on_key_press,
|
||||
"on_bq_cursor_changed": self.on_bq_cursor_changed}
|
||||
|
||||
self._app = app
|
||||
self._services = {}
|
||||
self._ex_services = self._app.current_services
|
||||
self._ex_fav_model = self._app.fav_view.get_model()
|
||||
self._settings = self._app.app_settings
|
||||
self._bouquet = bouquet
|
||||
self._bouquet_name = bouquet_name
|
||||
self._current_ref = []
|
||||
self._enable_dat_filter = False
|
||||
self._use_web_source = False
|
||||
self._update_epg_data_on_start = False
|
||||
self._refs_source = RefsSource.SERVICES
|
||||
self._download_xml_is_active = False
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "epg.glade", handlers)
|
||||
|
||||
self._dialog = builder.get_object("epg_dialog_window")
|
||||
self._dialog.set_transient_for(self._app.app_window)
|
||||
self._source_view = builder.get_object("source_view")
|
||||
self._bouquet_view = builder.get_object("bouquet_view")
|
||||
self._bouquet_model = builder.get_object("bouquet_list_store")
|
||||
self._services_model = builder.get_object("services_list_store")
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("info_bar_message_label")
|
||||
self._assign_ref_popup_item = builder.get_object("bouquet_assign_ref_popup_item")
|
||||
self._left_action_box = builder.get_object("left_action_box")
|
||||
self._xml_download_progress_bar = builder.get_object("xml_download_progress_bar")
|
||||
# Filter
|
||||
self._filter_bar = builder.get_object("filter_bar")
|
||||
self._filter_entry = builder.get_object("filter_entry")
|
||||
self._filter_auto_switch = builder.get_object("filter_auto_switch")
|
||||
self._services_filter_model = builder.get_object("services_filter_model")
|
||||
self._services_filter_model.set_visible_func(self.services_filter_function)
|
||||
# Info
|
||||
self._source_count_label = builder.get_object("source_count_label")
|
||||
self._source_info_label = builder.get_object("source_info_label")
|
||||
self._bouquet_count_label = builder.get_object("bouquet_count_label")
|
||||
self._bouquet_epg_count_label = builder.get_object("bouquet_epg_count_label")
|
||||
# Options
|
||||
self._xml_radiobutton = builder.get_object("xml_radiobutton")
|
||||
self._xml_chooser_button = builder.get_object("xml_chooser_button")
|
||||
self._names_source_box = builder.get_object("names_source_box")
|
||||
self._web_source_box = builder.get_object("web_source_box")
|
||||
self._use_web_source_switch = builder.get_object("use_web_source_switch")
|
||||
self._url_to_xml_entry = builder.get_object("url_to_xml_entry")
|
||||
self._enable_filtering_switch = builder.get_object("enable_filtering_switch")
|
||||
self._epg_dat_path_entry = builder.get_object("epg_dat_path_entry")
|
||||
self._epg_dat_stb_path_entry = builder.get_object("epg_dat_stb_path_entry")
|
||||
self._update_on_start_switch = builder.get_object("update_on_start_switch")
|
||||
self._epg_dat_source_box = builder.get_object("epg_dat_source_box")
|
||||
|
||||
if IS_GNOME_SESSION:
|
||||
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True, title="EPG",
|
||||
subtitle=get_message("List configuration"))
|
||||
self._dialog.set_titlebar(header_bar)
|
||||
builder.get_object("left_action_box").reparent(header_bar)
|
||||
right_box = builder.get_object("right_action_box")
|
||||
builder.get_object("main_actions_box").remove(right_box)
|
||||
header_bar.pack_end(right_box)
|
||||
builder.get_object("toolbar_box").set_visible(False)
|
||||
|
||||
self._app.connect("epg-dat-downloaded", self.on_epg_dat_downloaded)
|
||||
|
||||
# Setting the last size of the dialog window
|
||||
window_size = self._settings.get("epg_tool_window_size")
|
||||
if window_size:
|
||||
self._dialog.resize(*window_size)
|
||||
|
||||
self.init_drag_and_drop()
|
||||
self.on_update()
|
||||
|
||||
def show(self):
|
||||
self._dialog.show()
|
||||
|
||||
def on_close_dialog(self, window, event):
|
||||
self._download_xml_is_active = False
|
||||
|
||||
@run_idle
|
||||
def on_apply(self, item):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
self._bouquet.clear()
|
||||
list(map(self._bouquet.append, [r[Column.FAV_ID] for r in self._bouquet_model]))
|
||||
for index, row in enumerate(self._ex_fav_model):
|
||||
fav_id = self._bouquet[index]
|
||||
row[Column.FAV_ID] = fav_id
|
||||
if row[Column.FAV_TYPE] == BqServiceType.IPTV.name:
|
||||
old_fav_id = self._services[fav_id]
|
||||
srv = self._ex_services.pop(old_fav_id, None)
|
||||
if srv:
|
||||
self._ex_services[fav_id] = srv._replace(fav_id=fav_id)
|
||||
self._dialog.destroy()
|
||||
|
||||
@run_idle
|
||||
def on_update(self, item=None):
|
||||
self.clear_data()
|
||||
self.init_options()
|
||||
if self._update_epg_data_on_start:
|
||||
self.download_epg_from_stb()
|
||||
else:
|
||||
gen = self.init_data()
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def clear_data(self):
|
||||
self._services_model.clear()
|
||||
self._bouquet_model.clear()
|
||||
self._services.clear()
|
||||
self._source_info_label.set_text("")
|
||||
self._bouquet_epg_count_label.set_text("")
|
||||
self.on_info_bar_close()
|
||||
|
||||
def init_data(self):
|
||||
gen = self.init_bouquet_data()
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
refs = None
|
||||
if self._enable_dat_filter:
|
||||
try:
|
||||
refs = EPG.get_epg_refs(self._epg_dat_path_entry.get_text() + "epg.dat")
|
||||
except (OSError, ValueError) as e:
|
||||
self.show_info_message(f"Read data error: {e}", Gtk.MessageType.ERROR)
|
||||
return
|
||||
yield True
|
||||
|
||||
if self._refs_source is RefsSource.SERVICES:
|
||||
yield from self.init_lamedb_source(refs)
|
||||
elif self._refs_source is RefsSource.XML:
|
||||
xml_gen = self.init_xml_source(refs)
|
||||
try:
|
||||
yield from xml_gen
|
||||
except ValueError as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
else:
|
||||
self.show_info_message("Unknown names source!", Gtk.MessageType.ERROR)
|
||||
yield True
|
||||
|
||||
def init_bouquet_data(self):
|
||||
for r in self._ex_fav_model:
|
||||
row = [*r[:]]
|
||||
fav_id = r[Column.FAV_ID]
|
||||
self._services[fav_id] = self._ex_services[fav_id].fav_id
|
||||
yield self._bouquet_model.append(row)
|
||||
self._bouquet_count_label.set_text(str(len(self._bouquet_model)))
|
||||
yield True
|
||||
|
||||
def init_lamedb_source(self, refs):
|
||||
srvs = {k[:k.rfind(":")]: v for k, v in self._ex_services.items()}
|
||||
s_types = (BqServiceType.MARKER.value, BqServiceType.IPTV.value)
|
||||
filtered = filter(None, [srvs.get(ref) for ref in refs]) if refs else filter(
|
||||
lambda s: s.service_type not in s_types, self._ex_services.values())
|
||||
|
||||
factor = self._app.DEL_FACTOR / 4
|
||||
for index, srv in enumerate(filtered):
|
||||
self._services_model.append((srv.service, srv.pos, srv.fav_id))
|
||||
if index % factor == 0:
|
||||
yield True
|
||||
|
||||
self.update_source_count_info()
|
||||
yield True
|
||||
|
||||
def init_xml_source(self, refs):
|
||||
path = self._epg_dat_path_entry.get_text() if self._use_web_source else self._xml_chooser_button.get_filename()
|
||||
if not path:
|
||||
self.show_info_message("The path to the xml file is not set!", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
if self._use_web_source:
|
||||
# Downloading gzipped xml file that contains services names with references from the web.
|
||||
self._download_xml_is_active = True
|
||||
self.update_active_header_elements(False)
|
||||
url = self._url_to_xml_entry.get_text()
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=2) as fp:
|
||||
headers = fp.info()
|
||||
content_type = headers.get("Content-Type", "")
|
||||
|
||||
if content_type != "application/gzip":
|
||||
self._download_xml_is_active = False
|
||||
raise ValueError("{} {} {}".format(get_message("Download XML file error."),
|
||||
get_message("Unsupported file type:"),
|
||||
content_type))
|
||||
|
||||
file_name = os.path.basename(url)
|
||||
data_path = self._epg_dat_path_entry.get_text()
|
||||
|
||||
with open(data_path + file_name, "wb") as tfp:
|
||||
bs = 1024 * 8
|
||||
size = -1
|
||||
read = 0
|
||||
b_num = 0
|
||||
if "content-length" in headers:
|
||||
size = int(headers["Content-Length"])
|
||||
|
||||
while self._download_xml_is_active:
|
||||
block = fp.read(bs)
|
||||
if not block:
|
||||
break
|
||||
read += len(block)
|
||||
tfp.write(block)
|
||||
b_num += 1
|
||||
self.update_download_progress(b_num * bs / size)
|
||||
yield True
|
||||
|
||||
path = tfp.name.rstrip(".gz")
|
||||
except (HTTPError, URLError) as e:
|
||||
raise ValueError(f"{get_message('Download XML file error.')} {e}")
|
||||
else:
|
||||
try:
|
||||
with open(path, "wb") as f_out:
|
||||
with gzip.open(tfp.name, "rb") as f:
|
||||
shutil.copyfileobj(f, f_out)
|
||||
os.remove(tfp.name)
|
||||
except Exception as e:
|
||||
raise ValueError(f"{get_message('Unpacking data error.')} {e}")
|
||||
finally:
|
||||
self._download_xml_is_active = False
|
||||
self.update_active_header_elements(True)
|
||||
|
||||
try:
|
||||
s_refs, info = ChannelsParser.get_refs_from_xml(path)
|
||||
yield True
|
||||
except Exception as e:
|
||||
raise ValueError(f"{get_message('XML parsing error:')} {e}")
|
||||
else:
|
||||
if refs:
|
||||
s_refs = filter(lambda x: x.num in refs, s_refs)
|
||||
|
||||
factor = self._app.DEL_FACTOR / 4
|
||||
for index, srv in enumerate(s_refs):
|
||||
self._services_model.append((srv.name, " ", srv.data))
|
||||
if index % factor == 0:
|
||||
yield True
|
||||
|
||||
self.update_source_info(info)
|
||||
self.update_source_count_info()
|
||||
yield True
|
||||
|
||||
def on_key_press(self, view, event):
|
||||
""" Handling keystrokes """
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
return
|
||||
key = KeyboardKey(key_code)
|
||||
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
|
||||
|
||||
if ctrl and key is KeyboardKey.C:
|
||||
self.on_copy_ref()
|
||||
elif ctrl and key is KeyboardKey.V:
|
||||
self.on_assign_ref()
|
||||
|
||||
def on_bq_cursor_changed(self, view):
|
||||
if self._filter_bar.get_visible() and self._filter_auto_switch.get_active():
|
||||
path, column = view.get_cursor()
|
||||
model = view.get_model()
|
||||
if path:
|
||||
self._filter_entry.set_text(model[path][Column.FAV_SERVICE] or "")
|
||||
|
||||
@run_idle
|
||||
def on_save_to_xml(self, item):
|
||||
response = show_dialog(DialogType.CHOOSER, self._dialog, settings=self._settings)
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
|
||||
services = []
|
||||
iptv_types = (BqServiceType.IPTV.value, BqServiceType.MARKER.value)
|
||||
for r in self._bouquet_model:
|
||||
srv_type = r[Column.FAV_TYPE]
|
||||
if srv_type in iptv_types:
|
||||
srv = BouquetService(name=r[Column.FAV_SERVICE],
|
||||
type=BqServiceType(srv_type),
|
||||
data=r[Column.FAV_ID],
|
||||
num=r[Column.FAV_NUM])
|
||||
services.append(srv)
|
||||
|
||||
ChannelsParser.write_refs_to_xml("{}{}.xml".format(response, self._bouquet_name), services)
|
||||
self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO)
|
||||
|
||||
@run_idle
|
||||
def on_auto_configuration(self, item):
|
||||
""" Simple mapping of services by name. """
|
||||
use_cyrillic = locale.getdefaultlocale()[0] in ("ru_RU", "be_BY", "uk_UA", "sr_RS")
|
||||
tr = None
|
||||
if use_cyrillic:
|
||||
# may be not entirely correct
|
||||
symbols = (u"АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯІÏҐЎЈЂЉЊЋЏTB",
|
||||
u"ABVGDEEJZIJKLMNOPRSTUFHZCSS_Y_EUAIEGUEDLNCJTV")
|
||||
tr = {ord(k): ord(v) for k, v in zip(*symbols)}
|
||||
|
||||
source = {}
|
||||
for row in self._services_model:
|
||||
name = re.sub("\\W+", "", str(row[0])).upper()
|
||||
name = name.translate(tr) if use_cyrillic else name
|
||||
source[name] = row
|
||||
|
||||
success_count = 0
|
||||
not_founded = {}
|
||||
|
||||
for r in self._bouquet_model:
|
||||
if r[Column.FAV_TYPE] != BqServiceType.IPTV.value:
|
||||
continue
|
||||
name = re.sub("\\W+", "", str(r[Column.FAV_SERVICE])).upper()
|
||||
if use_cyrillic:
|
||||
name = name.translate(tr)
|
||||
ref = source.get(name, None) # Not [pop], because the list may contain duplicates or similar names!
|
||||
if ref:
|
||||
self.assign_data(r, ref, True)
|
||||
success_count += 1
|
||||
else:
|
||||
not_founded[name] = r
|
||||
# Additional attempt to search in the remaining elements
|
||||
for n in not_founded:
|
||||
for k in source:
|
||||
if k.startswith(n):
|
||||
self.assign_data(not_founded[n], source[k], True)
|
||||
success_count += 1
|
||||
break
|
||||
|
||||
self.update_epg_count()
|
||||
self.show_info_message("{} {} {}".format(get_message("Done!"),
|
||||
get_message("Count of successfully configured services:"),
|
||||
success_count), Gtk.MessageType.INFO)
|
||||
|
||||
def assign_data(self, row, data, show_error=False):
|
||||
if row[Column.FAV_TYPE] != BqServiceType.IPTV.value:
|
||||
if not show_error:
|
||||
self.show_info_message(get_message("Not allowed in this context!"), Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
fav_id = row[Column.FAV_ID]
|
||||
fav_id_data = fav_id.split(":")
|
||||
fav_id_data[3:7] = data[-1].split(":")
|
||||
new_fav_id = ":".join(fav_id_data)
|
||||
service = self._services.pop(fav_id, None)
|
||||
if service:
|
||||
self._services[new_fav_id] = service
|
||||
row[Column.FAV_ID] = new_fav_id
|
||||
row[Column.FAV_LOCKED] = EPG_ICON
|
||||
pos = f"({data[1] if self._refs_source is RefsSource.SERVICES else 'XML'})"
|
||||
src = f"{get_message('EPG source')}: {(GLib.markup_escape_text(data[0] or ''))} {pos}"
|
||||
row[Column.FAV_TOOLTIP] = f"{get_message('Service reference')}: {':'.join(fav_id_data[:10])}\n{src}"
|
||||
|
||||
def on_filter_toggled(self, button):
|
||||
self._filter_bar.set_visible(button.get_active())
|
||||
if not button.get_active():
|
||||
self._filter_entry.set_text("")
|
||||
|
||||
@run_with_delay(1)
|
||||
def on_filter_changed(self, entry):
|
||||
self._services_filter_model.refilter()
|
||||
|
||||
def services_filter_function(self, model, itr, data):
|
||||
txt = self._filter_entry.get_text().upper()
|
||||
return model is None or model == "None" or txt in model.get_value(itr, 0).upper()
|
||||
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
self._info_bar.set_visible(False)
|
||||
|
||||
def on_copy_ref(self, item=None):
|
||||
model, paths = self._source_view.get_selection().get_selected_rows()
|
||||
self._current_ref.clear()
|
||||
if paths:
|
||||
self._current_ref.append(model[paths][:])
|
||||
|
||||
def on_assign_ref(self, item=None):
|
||||
if self._current_ref:
|
||||
model, paths = self._bouquet_view.get_selection().get_selected_rows()
|
||||
self.assign_data(model[paths], self._current_ref.pop())
|
||||
self.update_epg_count()
|
||||
|
||||
@run_idle
|
||||
def on_reset(self, item):
|
||||
model, paths = self._bouquet_view.get_selection().get_selected_rows()
|
||||
if paths:
|
||||
row = self._bouquet_model[paths]
|
||||
self.reset_row_data(row)
|
||||
self.update_epg_count()
|
||||
|
||||
@run_idle
|
||||
def on_list_reset(self, item):
|
||||
list(map(self.reset_row_data, self._bouquet_model))
|
||||
self.update_epg_count()
|
||||
|
||||
def reset_row_data(self, row):
|
||||
default_fav_id = self._services.pop(row[Column.FAV_ID], None)
|
||||
if default_fav_id:
|
||||
self._services[default_fav_id] = default_fav_id
|
||||
row[Column.FAV_ID], row[Column.FAV_LOCKED], row[Column.FAV_TOOLTIP] = default_fav_id, None, None
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(True)
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._message_label.set_text(text)
|
||||
|
||||
@run_idle
|
||||
def update_source_info(self, info):
|
||||
lines = info.split("\n")
|
||||
self._source_info_label.set_text(lines[0] if lines else "")
|
||||
self._source_view.set_tooltip_text(info)
|
||||
|
||||
@run_idle
|
||||
def update_source_count_info(self):
|
||||
source_count = len(self._services_model)
|
||||
self._source_count_label.set_text(str(source_count))
|
||||
if self._enable_dat_filter and source_count == 0:
|
||||
msg = get_message("Current epg.dat file does not contains references for the services of this bouquet!")
|
||||
self.show_info_message(msg, Gtk.MessageType.WARNING)
|
||||
|
||||
@run_idle
|
||||
def update_epg_count(self):
|
||||
count = len(list((filter(None, [r[Column.FAV_LOCKED] for r in self._bouquet_model]))))
|
||||
self._bouquet_epg_count_label.set_text(str(count))
|
||||
|
||||
@run_idle
|
||||
def update_active_header_elements(self, state):
|
||||
self._left_action_box.set_sensitive(state)
|
||||
self._xml_download_progress_bar.set_visible(not state)
|
||||
self._source_info_label.set_text("" if state else "Downloading XML:")
|
||||
|
||||
@run_idle
|
||||
def update_download_progress(self, value):
|
||||
self._xml_download_progress_bar.set_fraction(value)
|
||||
|
||||
def on_bouquet_popup_menu(self, menu, event):
|
||||
self._assign_ref_popup_item.set_sensitive(self._current_ref)
|
||||
on_popup_menu(menu, event)
|
||||
|
||||
# ***************** Drag-and-drop *********************#
|
||||
|
||||
def init_drag_and_drop(self):
|
||||
""" Enable drag-and-drop. """
|
||||
target = []
|
||||
self._source_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, target, Gdk.DragAction.COPY)
|
||||
self._source_view.drag_source_add_text_targets()
|
||||
self._bouquet_view.enable_model_drag_dest(target, Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE)
|
||||
self._bouquet_view.drag_dest_add_text_targets()
|
||||
|
||||
def on_drag_begin(self, view, context):
|
||||
""" Selects a row under the cursor in the view at the dragging beginning. """
|
||||
selection = view.get_selection()
|
||||
if selection.count_selected_rows() > 1:
|
||||
view.do_toggle_cursor_row(view)
|
||||
|
||||
def on_drag_data_get(self, view, drag_context, data, info, time):
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if paths:
|
||||
s_data = model[paths][:]
|
||||
if all(s_data):
|
||||
data.set_text("::::".join(s_data), -1)
|
||||
else:
|
||||
self.show_info_message(get_message("Source error!"), Gtk.MessageType.ERROR)
|
||||
|
||||
def on_drag_data_received(self, view, drag_context, x, y, data, info, time):
|
||||
path, pos = view.get_dest_row_at_pos(x, y)
|
||||
model = view.get_model()
|
||||
data = data.get_text()
|
||||
if data:
|
||||
self.assign_data(model[path], data.split("::::"))
|
||||
self.update_epg_count()
|
||||
return False
|
||||
|
||||
# ***************** Options *********************#
|
||||
|
||||
def init_options(self):
|
||||
epg_dat_path = "{}epg{}".format(self._settings.profile_data_path, SEP)
|
||||
self._epg_dat_path_entry.set_text(epg_dat_path)
|
||||
default_epg_data_stb_path = "/etc/enigma2"
|
||||
epg_options = self._settings.epg_options
|
||||
if epg_options:
|
||||
self._refs_source = RefsSource.XML if epg_options.get("xml_source", False) else RefsSource.SERVICES
|
||||
self._xml_radiobutton.set_active(self._refs_source is RefsSource.XML)
|
||||
self._use_web_source = epg_options.get("use_web_source", False)
|
||||
self._use_web_source_switch.set_active(self._use_web_source)
|
||||
self._url_to_xml_entry.set_text(epg_options.get("url_to_xml", ""))
|
||||
self._enable_dat_filter = epg_options.get("enable_filtering", False)
|
||||
self._enable_filtering_switch.set_active(self._enable_dat_filter)
|
||||
epg_dat_path = epg_options.get("epg_dat_path", epg_dat_path)
|
||||
self._epg_dat_path_entry.set_text(epg_dat_path)
|
||||
self._epg_dat_stb_path_entry.set_text(epg_options.get("epg_dat_stb_path", default_epg_data_stb_path))
|
||||
self._update_epg_data_on_start = epg_options.get("epg_data_update_on_start", False)
|
||||
self._update_on_start_switch.set_active(self._update_epg_data_on_start)
|
||||
local_xml_path = epg_options.get("local_path_to_xml", None)
|
||||
if local_xml_path:
|
||||
self._xml_chooser_button.set_filename(local_xml_path)
|
||||
os.makedirs(os.path.dirname(self._epg_dat_path_entry.get_text()), exist_ok=True)
|
||||
|
||||
def on_options_save(self, item=None):
|
||||
self._settings.epg_options = {"xml_source": self._xml_radiobutton.get_active(),
|
||||
"use_web_source": self._use_web_source_switch.get_active(),
|
||||
"local_path_to_xml": self._xml_chooser_button.get_filename(),
|
||||
"url_to_xml": self._url_to_xml_entry.get_text(),
|
||||
"enable_filtering": self._enable_filtering_switch.get_active(),
|
||||
"epg_dat_path": self._epg_dat_path_entry.get_text(),
|
||||
"epg_dat_stb_path": self._epg_dat_stb_path_entry.get_text(),
|
||||
"epg_data_update_on_start": self._update_on_start_switch.get_active()}
|
||||
|
||||
def on_resize(self, window):
|
||||
if self._settings:
|
||||
self._settings.add("epg_tool_window_size", window.get_size())
|
||||
|
||||
def on_names_source_changed(self, button):
|
||||
self._refs_source = RefsSource.XML if button.get_active() else RefsSource.SERVICES
|
||||
self._names_source_box.set_sensitive(button.get_active())
|
||||
|
||||
def on_enable_filtering_switch(self, switch, state):
|
||||
self._epg_dat_source_box.set_sensitive(state)
|
||||
self._update_on_start_switch.set_active(False if not state else self._update_epg_data_on_start)
|
||||
|
||||
def on_update_on_start_switch(self, switch, state):
|
||||
pass
|
||||
|
||||
def on_use_web_source_switch(self, switch, state):
|
||||
self._web_source_box.set_sensitive(state)
|
||||
self._xml_chooser_button.set_sensitive(not state)
|
||||
|
||||
def on_field_icon_press(self, entry, icon, event_button):
|
||||
update_entry_data(entry, self._dialog, self._settings)
|
||||
|
||||
# ***************** Downloads *********************#
|
||||
|
||||
def on_epg_dat_downloaded(self, app, value):
|
||||
gen = self.init_data()
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
@run_task
|
||||
def download_epg_from_stb(self):
|
||||
""" Download the epg.dat file via ftp from the receiver. """
|
||||
try:
|
||||
download_data(settings=self._settings, download_type=DownloadType.EPG, callback=print)
|
||||
except Exception as e:
|
||||
GLib.idle_add(self.show_info_message, f"Download epg.dat file error: {e}", Gtk.MessageType.ERROR)
|
||||
else:
|
||||
GLib.idle_add(self._app.emit, "epg-dat-downloaded", None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
994
app/ui/ftp.glade
Normal file
994
app/ui/ftp.glade
Normal file
@@ -0,0 +1,994 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellite list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkMenu" id="bookmark_popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="bookmark_remove_menu_item">
|
||||
<property name="label">gtk-remove</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="activate" handler="on_bookmark_remove" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkListStore" id="bookmarks_list_store">
|
||||
<columns>
|
||||
<!-- column-name path -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkImage" id="file_create_folder_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">folder-new</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="file_download_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">network-transmit</property>
|
||||
</object>
|
||||
<object class="GtkListStore" id="file_list_store">
|
||||
<columns>
|
||||
<!-- column-name icon -->
|
||||
<column type="GdkPixbuf"/>
|
||||
<!-- column-name name -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name size -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name date -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name type -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name extra -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkImage" id="ftp_create_folder_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">folder-new</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="ftp_download_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">network-receive</property>
|
||||
</object>
|
||||
<object class="GtkListStore" id="ftp_list_store">
|
||||
<columns>
|
||||
<!-- column-name icon -->
|
||||
<column type="GdkPixbuf"/>
|
||||
<!-- column-name name -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name size -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name date -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name attr -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name extra -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkBox" id="main_ftp_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">2</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkPaned" id="paned">
|
||||
<property name="width_request">320</property>
|
||||
<property name="height_request">240</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="position_set">True</property>
|
||||
<property name="wide_handle">True</property>
|
||||
<signal name="size-allocate" handler="on_paned_size_allocate" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkFrame" id="ftp_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0.49000000953674316</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="ftp_bpx">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="ftp_button_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="connect_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Connect</property>
|
||||
<signal name="clicked" handler="on_connect" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="connect_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-connect</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="disconnect_button">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Disconnect</property>
|
||||
<signal name="clicked" handler="on_disconnect" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="disconnect_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-disconnect</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="bookmark_button_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="layout_style">expand</property>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="bookmarks_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Bookmarks</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="bookmark_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">user-bookmarks-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="add_ftp_bookmark_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Add bookmark</property>
|
||||
<signal name="clicked" handler="on_bookmark_add" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="ftp_remove_button_image1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">bookmark-new-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="ftp_actions_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="ftp_add_folder_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">New folder</property>
|
||||
<signal name="clicked" handler="on_ftp_create_folder" object="ftp_name_column_renderer" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="ftp_add_folder_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">folder-new-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="ftp_edit_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Edit</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_ftp_edit" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="ftp_edit_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">document-edit-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="ftp_remove_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<signal name="clicked" handler="on_ftp_remove" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="ftp_remove_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">user-trash-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="ftp_data_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="bookmarks_box">
|
||||
<property name="width_request">150</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="bookmarks_view_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="bookmarks_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">bookmarks_list_store</property>
|
||||
<property name="headers_visible">False</property>
|
||||
<property name="search_column">0</property>
|
||||
<property name="tooltip_column">0</property>
|
||||
<property name="activate_on_single_click">True</property>
|
||||
<signal name="button-press-event" handler="on_view_popup_menu" object="bookmark_popup_menu" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
|
||||
<signal name="row-activated" handler="on_bookmark_activated" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="bookmarks_selection"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="bookmark_name_column">
|
||||
<property name="title" translatable="yes">Name</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="bookmark_name_renderer">
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="ftp_view_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="min_content_height">100</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="ftp_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">ftp_list_store</property>
|
||||
<property name="search_column">1</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<signal name="button-press-event" handler="on_view_popup_menu" object="ftp_popup_menu" swapped="no"/>
|
||||
<signal name="button-press-event" handler="on_view_press" swapped="no"/>
|
||||
<signal name="button-release-event" handler="on_view_release" swapped="no"/>
|
||||
<signal name="drag-begin" handler="on_view_drag_begin" after="yes" swapped="no"/>
|
||||
<signal name="drag-data-get" handler="on_ftp_drag_data_get" swapped="no"/>
|
||||
<signal name="drag-data-received" handler="on_ftp_drag_data_received" swapped="no"/>
|
||||
<signal name="drag-end" handler="on_view_drag_end" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
|
||||
<signal name="row-activated" handler="on_ftp_row_activated" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="ftp_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_name_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="min_width">100</property>
|
||||
<property name="title" translatable="yes">Name</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">1</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererPixbuf" id="ftp_icon_column_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_name_column_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
<property name="ellipsize">end</property>
|
||||
<signal name="edited" handler="on_ftp_renamed" swapped="no"/>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_size_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">75</property>
|
||||
<property name="title" translatable="yes">Size</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">2</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_size_column_renderer">
|
||||
<property name="xalign">0.94999998807907104</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_date_column">
|
||||
<property name="min_width">75</property>
|
||||
<property name="title" translatable="yes">Date</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">3</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_date_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_attr_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">85</property>
|
||||
<property name="title" translatable="yes">Attr.</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">4</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_attr_column_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">4</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="ftp_extra_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="title" translatable="yes">Extra</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_extra_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">5</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="ftp_status_bar_box">
|
||||
<property name="height_request">24</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="ftp_info_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">document-properties</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ftp_info_label">
|
||||
<property name="height_request">16</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="ftp_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label">FTP</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="semibold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="pc_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0.49000000953674316</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="file_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="pc_header_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="pc_add_folder_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">New folder</property>
|
||||
<signal name="clicked" handler="on_file_create_folder" object="file_name_column_renderer" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="pc_add_folder_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">folder-new-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="pc_remove_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<signal name="clicked" handler="on_file_remove" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="pc_remove_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">user-trash-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="file_view_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="min_content_height">100</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="file_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">file_list_store</property>
|
||||
<property name="search_column">1</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<signal name="button-press-event" handler="on_view_popup_menu" object="file_popup_menu" swapped="no"/>
|
||||
<signal name="button-press-event" handler="on_view_press" swapped="no"/>
|
||||
<signal name="button-release-event" handler="on_view_release" swapped="no"/>
|
||||
<signal name="drag-begin" handler="on_view_drag_begin" after="yes" swapped="no"/>
|
||||
<signal name="drag-data-get" handler="on_file_drag_data_get" swapped="no"/>
|
||||
<signal name="drag-data-received" handler="on_file_drag_data_received" swapped="no"/>
|
||||
<signal name="drag-end" handler="on_view_drag_end" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
|
||||
<signal name="row-activated" handler="on_file_row_activated" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="file_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_name_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="min_width">100</property>
|
||||
<property name="title" translatable="yes">Name</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">1</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererPixbuf" id="file_icon_column_renderer">
|
||||
<property name="xalign">0.20000000298023224</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_name_column_renderer">
|
||||
<property name="ellipsize">end</property>
|
||||
<signal name="edited" handler="on_file_renamed" swapped="no"/>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_size_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">75</property>
|
||||
<property name="title" translatable="yes">Size</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">2</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_size_column_renderer">
|
||||
<property name="xalign">0.94999998807907104</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_date_column">
|
||||
<property name="min_width">75</property>
|
||||
<property name="title" translatable="yes">Date</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">3</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_date_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_type_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">50</property>
|
||||
<property name="title" translatable="yes">Path</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_path_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">4</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="file_extra_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="title" translatable="yes">Extra</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="file_extra_column_renderer"/>
|
||||
<attributes>
|
||||
<attribute name="text">5</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="file_status_bar_box">
|
||||
<property name="height_request">24</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="pc_info_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="pc_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">PC</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="semibold"/>
|
||||
</attributes>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkImage" id="remove_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">list-remove</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="remove_image_2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">list-remove</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="rename_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-find-replace</property>
|
||||
</object>
|
||||
<object class="GtkMenu" id="ftp_popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="ftp_download_menu_item">
|
||||
<property name="label" translatable="yes">Receive</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">ftp_download_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_ftp_copy" swapped="no"/>
|
||||
<accelerator key="F5" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="ftp_create_folder_menu_item">
|
||||
<property name="label" translatable="yes">Create folder</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">ftp_create_folder_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_ftp_create_folder" object="ftp_name_column_renderer" swapped="no"/>
|
||||
<accelerator key="F7" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="ftp_edit_menu_item">
|
||||
<property name="label">gtk-edit</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="activate" handler="on_ftp_edit" swapped="no"/>
|
||||
<accelerator key="F4" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="ftp_rename_menu_item">
|
||||
<property name="label" translatable="yes">Rename</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="image">rename_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_ftp_rename" object="ftp_name_column_renderer" swapped="no"/>
|
||||
<accelerator key="r" signal="activate" modifiers="Primary"/>
|
||||
<accelerator key="F2" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="ftp_remove_menu_item">
|
||||
<property name="label" translatable="yes">Remove</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">remove_image_2</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_ftp_remove" swapped="no"/>
|
||||
<accelerator key="Delete" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkImage" id="rename_image_2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-find-replace</property>
|
||||
</object>
|
||||
<object class="GtkMenu" id="file_popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="file_download_menu_item">
|
||||
<property name="label" translatable="yes">Send</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">file_download_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_file_copy" swapped="no"/>
|
||||
<accelerator key="F5" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="file_create_folder_menu_item">
|
||||
<property name="label" translatable="yes">Create folder</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">file_create_folder_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_file_create_folder" object="file_name_column_renderer" swapped="no"/>
|
||||
<accelerator key="F7" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="file_rename_menu_item">
|
||||
<property name="label" translatable="yes">Rename</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">rename_image_2</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_file_rename" object="file_name_column_renderer" swapped="no"/>
|
||||
<accelerator key="r" signal="activate" modifiers="Primary"/>
|
||||
<accelerator key="F2" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="file_remove_menu_item">
|
||||
<property name="label" translatable="yes">Remove</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">remove_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_file_remove" swapped="no"/>
|
||||
<accelerator key="Delete" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
825
app/ui/ftp.py
Normal file
825
app/ui/ftp.py
Normal file
@@ -0,0 +1,825 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Simple FTP client module. """
|
||||
import subprocess
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from enum import IntEnum
|
||||
from ftplib import all_errors
|
||||
from io import TextIOWrapper, BytesIO
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
from urllib.parse import urlparse, unquote
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import log, run_task, run_idle
|
||||
from app.connections import UtfFTP
|
||||
from app.settings import IS_LINUX, IS_DARWIN, IS_WIN, SEP
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_builder
|
||||
from app.ui.main_helper import on_popup_menu
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, IS_GNOME_SESSION
|
||||
|
||||
File = namedtuple("File", ["icon", "name", "size", "date", "attr", "extra"])
|
||||
|
||||
|
||||
class FtpClientBox(Gtk.HBox):
|
||||
""" Simple FTP client base class. """
|
||||
ROOT = ".."
|
||||
FOLDER = "<Folder>"
|
||||
LINK = "<Link>"
|
||||
MAX_SIZE = 10485760 # 10 MB file limit
|
||||
URI_SEP = "::::"
|
||||
|
||||
class Column(IntEnum):
|
||||
ICON = 0
|
||||
NAME = 1
|
||||
SIZE = 2
|
||||
DATE = 3
|
||||
ATTR = 4
|
||||
EXTRA = 5
|
||||
|
||||
class TextEditDialog(Gtk.Dialog):
|
||||
""" Simple text edit dialog. """
|
||||
|
||||
def __init__(self, path, use_header_bar=0, *args, **kwargs):
|
||||
super().__init__(title=f"DemonEditor [{path}]", use_header_bar=use_header_bar, *args, **kwargs)
|
||||
|
||||
self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK, )
|
||||
|
||||
content_box = self.get_content_area()
|
||||
self._search_entry = Gtk.SearchEntry(visible=True, primary_icon_name="system-search-symbolic")
|
||||
self._search_entry.connect("search-changed", self.on_search_changed)
|
||||
|
||||
if use_header_bar:
|
||||
bar = self.get_header_bar()
|
||||
bar.pack_start(self._search_entry)
|
||||
bar.set_title("DemonEditor")
|
||||
bar.set_subtitle(path)
|
||||
else:
|
||||
search_bar = Gtk.SearchBar(visible=True)
|
||||
search_bar.add(self._search_entry)
|
||||
search_bar.set_search_mode(True)
|
||||
content_box.pack_start(search_bar, False, False, 0)
|
||||
|
||||
scrolled_window = Gtk.ScrolledWindow(hexpand=True, vexpand=True,
|
||||
min_content_width=720,
|
||||
min_content_height=320)
|
||||
content_box.pack_start(scrolled_window, True, True, 0)
|
||||
|
||||
try:
|
||||
import gi
|
||||
|
||||
gi.require_version("GtkSource", "3.0")
|
||||
from gi.repository import GtkSource
|
||||
except (ImportError, ValueError) as e:
|
||||
self._text_view = Gtk.TextView()
|
||||
self._buf = self._text_view.get_buffer()
|
||||
log(e)
|
||||
else:
|
||||
self._text_view = GtkSource.View(show_line_numbers=True, show_line_marks=True)
|
||||
self._buf = self._text_view.get_buffer()
|
||||
self._buf.set_highlight_syntax(True)
|
||||
self._buf.set_highlight_matching_brackets(True)
|
||||
lang_manager = GtkSource.LanguageManager.new()
|
||||
self._buf.set_language(lang_manager.guess_language(path))
|
||||
# Style
|
||||
self._buf.set_style_scheme(GtkSource.StyleSchemeManager().get_default().get_scheme("tango"))
|
||||
|
||||
self._tag_found = self._buf.create_tag("found", background="yellow")
|
||||
scrolled_window.add(self._text_view)
|
||||
|
||||
self.show_all()
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return self._buf.get_text(self._buf.get_start_iter(), self._buf.get_end_iter(), include_hidden_chars=True)
|
||||
|
||||
@text.setter
|
||||
def text(self, value):
|
||||
self._buf.set_text(value)
|
||||
|
||||
def on_search_changed(self, entry):
|
||||
self._buf.remove_tag(self._tag_found, self._buf.get_start_iter(), self._buf.get_end_iter())
|
||||
cursor_mark = self._buf.get_insert()
|
||||
start = self._buf.get_iter_at_mark(cursor_mark)
|
||||
if start.get_offset() == self._buf.get_char_count():
|
||||
start = self._buf.get_start_iter()
|
||||
|
||||
self.search_and_mark(entry.get_text(), start)
|
||||
|
||||
def search_and_mark(self, text, start, first=True):
|
||||
end = self._buf.get_end_iter()
|
||||
match = start.forward_search(text, 0, end)
|
||||
|
||||
if match is not None:
|
||||
match_start, match_end = match
|
||||
self._buf.apply_tag(self._tag_found, match_start, match_end)
|
||||
if first:
|
||||
self._text_view.scroll_to_iter(match_start, 0.0, False, 0.0, 0.0)
|
||||
GLib.idle_add(self.search_and_mark, text, match_end, False)
|
||||
|
||||
def __init__(self, app, settings, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_spacing(2)
|
||||
self.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
|
||||
self._app = app
|
||||
self._settings = settings
|
||||
self._ftp = None
|
||||
self._select_enabled = True
|
||||
|
||||
handlers = {"on_connect": self.on_connect,
|
||||
"on_disconnect": self.on_disconnect,
|
||||
"on_ftp_row_activated": self.on_ftp_row_activated,
|
||||
"on_file_row_activated": self.on_file_row_activated,
|
||||
"on_bookmark_activated": self.on_bookmark_activated,
|
||||
"on_ftp_edit": self.on_ftp_edit,
|
||||
"on_ftp_rename": self.on_ftp_rename,
|
||||
"on_ftp_renamed": self.on_ftp_renamed,
|
||||
"on_ftp_copy": self.on_ftp_copy,
|
||||
"on_file_rename": self.on_file_rename,
|
||||
"on_file_renamed": self.on_file_renamed,
|
||||
"on_file_copy": self.on_file_copy,
|
||||
"on_file_remove": self.on_file_remove,
|
||||
"on_ftp_remove": self.on_ftp_file_remove,
|
||||
"on_bookmark_remove": self.on_bookmark_remove,
|
||||
"on_file_create_folder": self.on_file_create_folder,
|
||||
"on_ftp_create_folder": self.on_ftp_create_folder,
|
||||
"on_view_drag_begin": self.on_view_drag_begin,
|
||||
"on_ftp_drag_data_get": self.on_ftp_drag_data_get,
|
||||
"on_ftp_drag_data_received": self.on_ftp_drag_data_received,
|
||||
"on_file_drag_data_get": self.on_file_drag_data_get,
|
||||
"on_file_drag_data_received": self.on_file_drag_data_received,
|
||||
"on_view_drag_end": self.on_view_drag_end,
|
||||
"on_bookmark_add": self.on_bookmark_add,
|
||||
"on_view_popup_menu": on_popup_menu,
|
||||
"on_view_key_press": self.on_view_key_press,
|
||||
"on_view_press": self.on_view_press,
|
||||
"on_view_release": self.on_view_release,
|
||||
"on_paned_size_allocate": self.on_paned_size_allocate}
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "ftp.glade", handlers)
|
||||
|
||||
self.add(builder.get_object("main_ftp_box"))
|
||||
self._ftp_info_label = builder.get_object("ftp_info_label")
|
||||
self._ftp_view = builder.get_object("ftp_view")
|
||||
self._ftp_model = builder.get_object("ftp_list_store")
|
||||
self._ftp_name_renderer = builder.get_object("ftp_name_column_renderer")
|
||||
self._file_view = builder.get_object("file_view")
|
||||
self._file_model = builder.get_object("file_list_store")
|
||||
self._file_name_renderer = builder.get_object("file_name_column_renderer")
|
||||
self._bookmark_view = builder.get_object("bookmarks_view")
|
||||
self._bookmark_model = builder.get_object("bookmarks_list_store")
|
||||
# Buttons
|
||||
self._connect_button = builder.get_object("connect_button")
|
||||
disconnect_button = builder.get_object("disconnect_button")
|
||||
disconnect_button.bind_property("visible", builder.get_object("ftp_actions_box"), "sensitive")
|
||||
disconnect_button.bind_property("visible", builder.get_object("ftp_create_folder_menu_item"), "sensitive")
|
||||
disconnect_button.bind_property("visible", builder.get_object("ftp_edit_menu_item"), "sensitive")
|
||||
disconnect_button.bind_property("visible", builder.get_object("ftp_rename_menu_item"), "sensitive")
|
||||
disconnect_button.bind_property("visible", builder.get_object("ftp_remove_menu_item"), "sensitive")
|
||||
disconnect_button.bind_property("visible", builder.get_object("add_ftp_bookmark_button"), "sensitive")
|
||||
self._connect_button.bind_property("visible", builder.get_object("disconnect_button"), "visible", 4)
|
||||
self._bookmarks_button = builder.get_object("bookmarks_button")
|
||||
self._bookmarks_button.bind_property("active", builder.get_object("bookmarks_box"), "visible")
|
||||
# Force Ctrl
|
||||
self._ftp_view.connect("key-press-event", self._app.force_ctrl)
|
||||
self._file_view.connect("key-press-event", self._app.force_ctrl)
|
||||
# Icons
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
folder_icon = "folder-symbolic" if settings.is_darwin else "folder"
|
||||
self._folder_icon = theme.load_icon(folder_icon, 16, 0) if theme.lookup_icon(folder_icon, 16, 0) else None
|
||||
self._link_icon = theme.load_icon("emblem-symbolic-link", 16, 0) if theme.lookup_icon("emblem-symbolic-link",
|
||||
16, 0) else None
|
||||
# Initialization
|
||||
self.init_drag_and_drop()
|
||||
self.init_ftp()
|
||||
self.init_file_data()
|
||||
self.show()
|
||||
|
||||
@run_task
|
||||
def init_ftp(self):
|
||||
self.init_bookmarks()
|
||||
GLib.idle_add(self._ftp_model.clear)
|
||||
try:
|
||||
if self._ftp:
|
||||
self._ftp.close()
|
||||
|
||||
self._ftp = UtfFTP(host=self._settings.host, user=self._settings.user, passwd=self._settings.password)
|
||||
self._ftp.encoding = "utf-8"
|
||||
self.update_ftp_info(self._ftp.getwelcome())
|
||||
except all_errors as e:
|
||||
self.update_ftp_info(str(e))
|
||||
self.on_disconnect()
|
||||
else:
|
||||
self.init_ftp_data()
|
||||
|
||||
@run_task
|
||||
def init_ftp_data(self, path=None):
|
||||
if not self._ftp:
|
||||
return
|
||||
|
||||
if path:
|
||||
try:
|
||||
self._ftp.cwd(path)
|
||||
except all_errors as e:
|
||||
self.update_ftp_info(str(e))
|
||||
|
||||
files = []
|
||||
try:
|
||||
self._ftp.dir(files.append)
|
||||
except all_errors as e:
|
||||
log(e)
|
||||
self.update_ftp_info(str(e))
|
||||
self.on_disconnect()
|
||||
else:
|
||||
self.append_ftp_data(files)
|
||||
GLib.idle_add(self._connect_button.set_visible, False)
|
||||
|
||||
@run_task
|
||||
def init_file_data(self, path=None):
|
||||
self.append_file_data(Path(path if path else self._settings.profile_data_path))
|
||||
|
||||
@run_idle
|
||||
def append_file_data(self, path: Path):
|
||||
self._file_model.clear()
|
||||
self._file_model.append(File(None, self.ROOT, None, None, str(path), "0"))
|
||||
|
||||
try:
|
||||
dirs = [p for p in path.iterdir()]
|
||||
except OSError as e:
|
||||
log(e)
|
||||
else:
|
||||
for p in dirs:
|
||||
is_dir = p.is_dir()
|
||||
st = p.stat()
|
||||
size = str(st.st_size)
|
||||
date = datetime.fromtimestamp(st.st_mtime).strftime("%d-%m-%y %H:%M")
|
||||
icon = None
|
||||
if is_dir:
|
||||
r_size = self.FOLDER
|
||||
icon = self._folder_icon
|
||||
elif p.is_symlink():
|
||||
r_size = self.LINK
|
||||
icon = self._link_icon
|
||||
else:
|
||||
r_size = self.get_size_from_bytes(size)
|
||||
|
||||
self._file_model.append(File(icon, p.name, r_size, date, str(p.resolve()), size))
|
||||
|
||||
@run_idle
|
||||
def append_ftp_data(self, files):
|
||||
self._ftp_model.clear()
|
||||
self._ftp_model.append(File(None, self.ROOT, None, None, self._ftp.pwd(), "0"))
|
||||
|
||||
for f in files:
|
||||
f_data = self._ftp.get_file_data(f)
|
||||
f_type = f_data[0][0]
|
||||
is_dir = f_type == "d"
|
||||
is_link = f_type == "l"
|
||||
size = f_data[4]
|
||||
|
||||
icon = None
|
||||
if is_dir:
|
||||
r_size = self.FOLDER
|
||||
icon = self._folder_icon
|
||||
elif is_link:
|
||||
r_size = self.LINK
|
||||
icon = self._link_icon
|
||||
else:
|
||||
r_size = self.get_size_from_bytes(size)
|
||||
|
||||
date = f"{f_data[5]}, {f_data[6]} {f_data[7]}"
|
||||
self._ftp_model.append(File(icon, f_data[8], r_size, date, f_data[0], size))
|
||||
|
||||
def on_connect(self, item=None):
|
||||
self.init_ftp()
|
||||
|
||||
def on_disconnect(self, item=None):
|
||||
if self._ftp:
|
||||
self._ftp.close()
|
||||
self._connect_button.set_visible(True)
|
||||
GLib.idle_add(self._ftp_model.clear)
|
||||
|
||||
def on_ftp_row_activated(self, view, path, column):
|
||||
row = self._ftp_model[path][:]
|
||||
f_path = row[self.Column.NAME]
|
||||
size = row[self.Column.SIZE]
|
||||
|
||||
if size == self.FOLDER or f_path == self.ROOT:
|
||||
self.init_ftp_data(f_path)
|
||||
elif size == self.LINK:
|
||||
name, sep, f_path = f_path.partition("->")
|
||||
if f_path:
|
||||
self.init_ftp_data(f_path.strip())
|
||||
else:
|
||||
b_size = row[self.Column.EXTRA]
|
||||
if b_size.isdigit() and int(b_size) > self.MAX_SIZE:
|
||||
self._app.show_error_message("The file size is too large!")
|
||||
else:
|
||||
self.open_ftp_file(f_path)
|
||||
|
||||
def on_file_row_activated(self, view, path, column):
|
||||
row = self._file_model[path][:]
|
||||
path = Path(row[self.Column.ATTR])
|
||||
if row[self.Column.SIZE] == self.FOLDER:
|
||||
self.init_file_data(path)
|
||||
elif row[self.Column.NAME] == self.ROOT:
|
||||
self.init_file_data(path.parent)
|
||||
else:
|
||||
self.open_file(row[self.Column.ATTR])
|
||||
|
||||
@run_task
|
||||
def open_file(self, path):
|
||||
GLib.idle_add(self._file_view.set_sensitive, False)
|
||||
try:
|
||||
cmd = [self.get_open_file_cmd(), path]
|
||||
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=IS_WIN).communicate()
|
||||
finally:
|
||||
GLib.idle_add(self._file_view.set_sensitive, True)
|
||||
|
||||
@run_task
|
||||
def open_ftp_file(self, f_path):
|
||||
GLib.idle_add(self._ftp_view.set_sensitive, False)
|
||||
|
||||
try:
|
||||
import tempfile
|
||||
import os
|
||||
path = os.path.expanduser("~/Desktop") if not IS_LINUX else None
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="wb", dir=path, delete=IS_LINUX) as tf:
|
||||
msg = "Downloading file: {}. Status: {}"
|
||||
try:
|
||||
status = self._ftp.retrbinary("RETR " + f_path, tf.write)
|
||||
self.update_ftp_info(msg.format(f_path, status))
|
||||
except all_errors as e:
|
||||
self.update_ftp_info(msg.format(f_path, e))
|
||||
|
||||
tf.flush()
|
||||
|
||||
cmd = [self.get_open_file_cmd(), tf.name]
|
||||
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=IS_WIN).communicate()
|
||||
finally:
|
||||
GLib.idle_add(self._ftp_view.set_sensitive, True)
|
||||
|
||||
@staticmethod
|
||||
def get_open_file_cmd():
|
||||
if IS_DARWIN:
|
||||
return "open"
|
||||
elif IS_WIN:
|
||||
return "start"
|
||||
return "xdg-open"
|
||||
|
||||
@run_task
|
||||
def on_ftp_edit(self, item=None):
|
||||
path = self.get_ftp_edit_path()
|
||||
if path:
|
||||
row = self._ftp_model[path]
|
||||
f_path = row[self.Column.NAME]
|
||||
size = row[self.Column.SIZE]
|
||||
|
||||
if size == self.FOLDER or f_path == self.ROOT:
|
||||
self._app.show_error_message("Not allowed in this context!")
|
||||
else:
|
||||
b_size = row[self.Column.EXTRA]
|
||||
if b_size.isdigit() and int(b_size) > self.MAX_SIZE / 5:
|
||||
self._app.show_error_message("The file size is too large!")
|
||||
else:
|
||||
msg = "Retrieving file: {}. Status: {}"
|
||||
io = BytesIO()
|
||||
try:
|
||||
status = self._ftp.retrbinary("RETR " + f_path, io.write)
|
||||
self.update_ftp_info(msg.format(f_path, status))
|
||||
except all_errors as e:
|
||||
self.update_ftp_info(msg.format(f_path, e))
|
||||
else:
|
||||
io.seek(0)
|
||||
self.show_edit_dialog(f_path, TextIOWrapper(io, errors="ignore").read())
|
||||
|
||||
def on_ftp_edited(self, f_path, txt_data):
|
||||
buf = BytesIO()
|
||||
buf.write(txt_data.encode())
|
||||
buf.seek(0)
|
||||
|
||||
msg = "Uploading file: {}. Status: {}"
|
||||
try:
|
||||
status = self._ftp.storbinary(f"STOR {f_path}", buf)
|
||||
self.update_ftp_info(msg.format(f_path, status))
|
||||
except all_errors as e:
|
||||
self.update_ftp_info(msg.format(f_path, e))
|
||||
|
||||
@run_idle
|
||||
def show_edit_dialog(self, f_path, data):
|
||||
dialog = self.TextEditDialog(f_path, IS_GNOME_SESSION)
|
||||
dialog.text = data
|
||||
ok = Gtk.ResponseType.OK
|
||||
if dialog.run() == ok and show_dialog(DialogType.QUESTION, self._app.app_window) == ok:
|
||||
self.on_ftp_edited(f_path, dialog.text)
|
||||
dialog.destroy()
|
||||
|
||||
def on_ftp_rename(self, renderer):
|
||||
path = self.get_ftp_edit_path()
|
||||
if path:
|
||||
renderer.set_property("editable", True)
|
||||
self._ftp_view.set_cursor(path, self._ftp_view.get_column(0), True)
|
||||
|
||||
def get_ftp_edit_path(self):
|
||||
model, paths = self._ftp_view.get_selection().get_selected_rows()
|
||||
if not paths:
|
||||
return
|
||||
|
||||
if len(paths) > 1:
|
||||
self._app.show_error_message("Please, select only one item!")
|
||||
return
|
||||
return paths
|
||||
|
||||
def on_ftp_renamed(self, renderer, path, new_value):
|
||||
renderer.set_property("editable", False)
|
||||
row = self._ftp_model[path]
|
||||
old_name = row[self.Column.NAME]
|
||||
if old_name == new_value:
|
||||
return
|
||||
|
||||
resp = self._ftp.rename_file(old_name, new_value)
|
||||
self.update_ftp_info(f"{old_name} Status: {resp}")
|
||||
if resp[0] == "2":
|
||||
row[self.Column.NAME] = new_value
|
||||
|
||||
def on_file_rename(self, renderer):
|
||||
model, paths = self._file_view.get_selection().get_selected_rows()
|
||||
if len(paths) > 1:
|
||||
self._app.show_error_message("Please, select only one item!")
|
||||
return
|
||||
|
||||
renderer.set_property("editable", True)
|
||||
self._file_view.set_cursor(paths, self._file_view.get_column(0), True)
|
||||
|
||||
def on_file_renamed(self, renderer, path, new_value):
|
||||
renderer.set_property("editable", False)
|
||||
row = self._file_model[path]
|
||||
old_name = row[self.Column.NAME]
|
||||
if old_name == new_value:
|
||||
return
|
||||
|
||||
path = Path(row[self.Column.ATTR])
|
||||
if path.exists():
|
||||
try:
|
||||
new_path = path.rename(f"{path.parent}/{new_value}")
|
||||
except ValueError as e:
|
||||
log(e)
|
||||
self._app.show_error_message(str(e))
|
||||
else:
|
||||
if new_path.name == new_value:
|
||||
row[self.Column.NAME] = new_value
|
||||
row[self.Column.ATTR] = str(new_path.resolve())
|
||||
|
||||
def on_file_copy(self, item=None):
|
||||
uris = self.get_file_uris()
|
||||
self.copy_to_ftp(uris) if uris else None
|
||||
|
||||
def on_ftp_copy(self, item=None):
|
||||
uris = self.get_ftp_uris()
|
||||
self.copy_to_pc(uris) if uris else None
|
||||
|
||||
def on_file_remove(self, item=None):
|
||||
if show_dialog(DialogType.QUESTION, self._app.app_window) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
model, paths = self._file_view.get_selection().get_selected_rows()
|
||||
to_delete = []
|
||||
|
||||
for path in filter(lambda p: model[p][self.Column.NAME] != self.ROOT, paths):
|
||||
f_path = Path(model[path][self.Column.ATTR])
|
||||
try:
|
||||
rmtree(f_path, ignore_errors=True) if f_path.is_dir() else f_path.unlink()
|
||||
except OSError as e:
|
||||
log(e)
|
||||
else:
|
||||
to_delete.append(model.get_iter(path))
|
||||
|
||||
list(map(model.remove, to_delete))
|
||||
|
||||
def on_ftp_file_remove(self, item=None):
|
||||
if show_dialog(DialogType.QUESTION, self._app.app_window) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
model, paths = self._ftp_view.get_selection().get_selected_rows()
|
||||
to_delete = []
|
||||
|
||||
for path in filter(lambda p: model[p][self.Column.NAME] != self.ROOT, paths):
|
||||
row = model[path][:]
|
||||
name = row[self.Column.NAME]
|
||||
if row[self.Column.SIZE] == self.FOLDER:
|
||||
resp = self._ftp.delete_dir(name, self.update_ftp_info)
|
||||
else:
|
||||
resp = self._ftp.delete_file(name, self.update_ftp_info)
|
||||
|
||||
if resp[0] == "2":
|
||||
to_delete.append(model.get_iter(path))
|
||||
|
||||
list(map(model.remove, to_delete))
|
||||
|
||||
def on_file_create_folder(self, renderer):
|
||||
itr = self._file_model.get_iter_first()
|
||||
if not itr:
|
||||
return
|
||||
|
||||
name = self.get_new_folder_name(self._file_model)
|
||||
cur_path = self._file_model.get_value(itr, self.Column.ATTR)
|
||||
path = Path(f"{cur_path}/{name}")
|
||||
|
||||
try:
|
||||
path.mkdir()
|
||||
except OSError as e:
|
||||
log(e)
|
||||
self._app.show_error_message(str(e))
|
||||
else:
|
||||
itr = self._file_model.append(File(self._folder_icon, path.name, self.FOLDER, "", str(path.resolve()), "0"))
|
||||
renderer.set_property("editable", True)
|
||||
self._file_view.set_cursor(self._file_model.get_path(itr), self._file_view.get_column(0), True)
|
||||
|
||||
def on_ftp_create_folder(self, renderer):
|
||||
itr = self._ftp_model.get_iter_first()
|
||||
if not itr:
|
||||
return
|
||||
|
||||
cur_path = self._ftp_model.get_value(itr, self.Column.ATTR)
|
||||
name = self.get_new_folder_name(self._ftp_model)
|
||||
|
||||
try:
|
||||
folder = f"{cur_path}/{name}"
|
||||
resp = self._ftp.mkd(folder)
|
||||
except all_errors as e:
|
||||
self.update_ftp_info(str(e))
|
||||
log(e)
|
||||
else:
|
||||
if resp == f"{cur_path}/{name}":
|
||||
itr = self._ftp_model.append(File(self._folder_icon, name, self.FOLDER, "", "drwxr-xr-x", "0"))
|
||||
renderer.set_property("editable", True)
|
||||
self._ftp_view.set_cursor(self._ftp_model.get_path(itr), self._ftp_view.get_column(0), True)
|
||||
|
||||
def get_new_folder_name(self, model):
|
||||
""" Returns the default name for the newly created folder. """
|
||||
name = "new folder"
|
||||
names = {r[self.Column.NAME] for r in model}
|
||||
count = 0
|
||||
while name in names:
|
||||
count += 1
|
||||
name = f"{name}{count}"
|
||||
return name
|
||||
|
||||
# ***************** Drag-and-drop ********************* #
|
||||
|
||||
def init_drag_and_drop(self):
|
||||
self._ftp_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
|
||||
self._ftp_view.enable_model_drag_dest([], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
|
||||
self._ftp_view.drag_source_add_uri_targets()
|
||||
self._ftp_view.drag_dest_add_uri_targets()
|
||||
|
||||
self._file_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, [], Gdk.DragAction.COPY)
|
||||
self._file_view.enable_model_drag_dest([], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
|
||||
self._file_view.drag_source_add_uri_targets()
|
||||
self._file_view.drag_dest_add_uri_targets()
|
||||
|
||||
self._ftp_view.get_selection().set_select_function(lambda *args: self._select_enabled)
|
||||
self._file_view.get_selection().set_select_function(lambda *args: self._select_enabled)
|
||||
|
||||
def on_view_drag_begin(self, view, context):
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if len(paths) < 1:
|
||||
return
|
||||
|
||||
pix = self._app.get_drag_icon_pixbuf(model, paths, self.Column.NAME, self.Column.SIZE)
|
||||
Gtk.drag_set_icon_pixbuf(context, pix, 0, 0)
|
||||
return True
|
||||
|
||||
def on_ftp_drag_data_get(self, view, context, data, info, time):
|
||||
uris = self.get_ftp_uris()
|
||||
data.set_uris(uris) if uris else None
|
||||
|
||||
def get_ftp_uris(self):
|
||||
""" Returns the selected paths in FTP view as a list containing uris string or None. """
|
||||
model, paths = self._ftp_view.get_selection().get_selected_rows()
|
||||
if len(paths) > 0:
|
||||
sep = self.URI_SEP if self._settings.is_darwin else "\n"
|
||||
uris = []
|
||||
for r in [model[p][:] for p in paths]:
|
||||
if r[self.Column.SIZE] != self.LINK and r[self.Column.NAME] != self.ROOT:
|
||||
path = Path(f"/{r[self.Column.NAME]}:{r[self.Column.ATTR]}")
|
||||
uris.append(str(path.resolve()) if IS_WIN else path.as_uri())
|
||||
return [sep.join(uris)]
|
||||
|
||||
def on_ftp_drag_data_received(self, view, context, x, y, data: Gtk.SelectionData, info, time):
|
||||
if not self._ftp:
|
||||
return
|
||||
|
||||
self.copy_to_ftp(data.get_uris())
|
||||
Gtk.drag_finish(context, True, False, time)
|
||||
return True
|
||||
|
||||
@run_task
|
||||
def copy_to_ftp(self, uris):
|
||||
resp = "2"
|
||||
try:
|
||||
GLib.idle_add(self._app.wait_dialog.show)
|
||||
|
||||
if len(uris) == 1:
|
||||
uris = uris[0].split(self.URI_SEP if self._settings.is_darwin else "\n")
|
||||
|
||||
for uri in uris:
|
||||
uri = urlparse(unquote(uri)).path
|
||||
if IS_WIN:
|
||||
uri = uri.lstrip("/")
|
||||
|
||||
path = Path(uri)
|
||||
if path.is_dir():
|
||||
try:
|
||||
self._ftp.mkd(path.name)
|
||||
except all_errors as e:
|
||||
pass # NOP
|
||||
self._ftp.cwd(path.name)
|
||||
resp = self._ftp.upload_dir(str(path.resolve()) + SEP, self.update_ftp_info)
|
||||
else:
|
||||
resp = self._ftp.send_file(path.name, str(path.parent) + SEP, callback=self.update_ftp_info)
|
||||
finally:
|
||||
GLib.idle_add(self._app.wait_dialog.hide)
|
||||
if resp and resp[0] == "2":
|
||||
itr = self._ftp_model.get_iter_first()
|
||||
if itr:
|
||||
self.init_ftp_data(self._ftp_model.get_value(itr, self.Column.ATTR))
|
||||
|
||||
def on_file_drag_data_get(self, view, context, data, info, time):
|
||||
uris = self.get_file_uris()
|
||||
data.set_uris(uris) if uris else None
|
||||
|
||||
def get_file_uris(self):
|
||||
""" Returns the selected paths in the file view as a list containing uris string or None. """
|
||||
model, paths = self._file_view.get_selection().get_selected_rows()
|
||||
if len(paths) > 0:
|
||||
sep = self.URI_SEP if self._settings.is_darwin else "\n"
|
||||
return [sep.join([Path(model[p][self.Column.ATTR]).as_uri() for p in paths])]
|
||||
|
||||
def on_file_drag_data_received(self, view, context, x, y, data, info, time):
|
||||
self.copy_to_pc(data.get_uris())
|
||||
Gtk.drag_finish(context, True, False, time)
|
||||
return True
|
||||
|
||||
@run_task
|
||||
def copy_to_pc(self, uris):
|
||||
cur_path = self._file_model.get_value(self._file_model.get_iter_first(), self.Column.ATTR) + "/"
|
||||
try:
|
||||
GLib.idle_add(self._app.wait_dialog.show)
|
||||
|
||||
if len(uris) == 1:
|
||||
uris = uris[0].split(self.URI_SEP if self._settings.is_darwin else "\n")
|
||||
|
||||
for uri in uris:
|
||||
name, sep, attr = unquote(Path(uri).name).partition(":")
|
||||
if not attr:
|
||||
return True
|
||||
|
||||
if attr[0] == "d":
|
||||
self._ftp.download_dir(name, cur_path, self.update_ftp_info)
|
||||
else:
|
||||
self._ftp.download_file(name, cur_path, self.update_ftp_info)
|
||||
except OSError as e:
|
||||
log(e)
|
||||
finally:
|
||||
GLib.idle_add(self._app.wait_dialog.hide)
|
||||
self.init_file_data(cur_path)
|
||||
|
||||
def on_view_drag_end(self, view, context):
|
||||
self._select_enabled = True
|
||||
view.get_selection().unselect_all()
|
||||
|
||||
@run_idle
|
||||
def update_ftp_info(self, message):
|
||||
message = message.strip()
|
||||
self._ftp_info_label.set_text(message)
|
||||
self._ftp_info_label.set_tooltip_text(message)
|
||||
|
||||
# **************** Bookmarks ***************** #
|
||||
|
||||
@run_idle
|
||||
def init_bookmarks(self):
|
||||
self._bookmark_model.clear()
|
||||
list(map(lambda b: self._bookmark_model.append((b,)), self._settings.ftp_bookmarks))
|
||||
|
||||
def on_bookmark_activated(self, view, path, column):
|
||||
self.init_ftp_data(self._bookmark_model[path][0])
|
||||
|
||||
def on_bookmark_add(self, item=None):
|
||||
self._bookmarks_button.set_active(True)
|
||||
self._bookmark_model.append((self._ftp_model.get_value(self._ftp_model.get_iter_first(), 4),))
|
||||
self._settings.ftp_bookmarks = [r[0] for r in self._bookmark_model]
|
||||
|
||||
def on_bookmark_remove(self, item=None):
|
||||
model, paths = self._bookmark_view.get_selection().get_selected_rows()
|
||||
if paths and show_dialog(DialogType.QUESTION, self._app.app_window) == Gtk.ResponseType.OK:
|
||||
list(map(lambda p: model.remove(model.get_iter(p)), paths))
|
||||
self._settings.ftp_bookmarks = [r[0] for r in self._bookmark_model]
|
||||
|
||||
def on_view_key_press(self, view, event):
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
return
|
||||
|
||||
key = KeyboardKey(key_code)
|
||||
ctrl = event.state & MOD_MASK
|
||||
|
||||
if key is KeyboardKey.F7:
|
||||
if self._ftp_view.is_focus():
|
||||
self.on_ftp_create_folder(self._ftp_name_renderer)
|
||||
elif self._file_view.is_focus():
|
||||
self.on_file_create_folder(self._file_name_renderer)
|
||||
elif key is KeyboardKey.F2 or ctrl and KeyboardKey.R:
|
||||
if self._ftp_view.is_focus():
|
||||
self.on_ftp_rename(self._ftp_name_renderer)
|
||||
elif self._file_view.is_focus():
|
||||
self.on_file_rename(self._file_name_renderer)
|
||||
elif key is KeyboardKey.F4:
|
||||
if self._ftp_view.is_focus():
|
||||
self.on_ftp_edit()
|
||||
elif key is KeyboardKey.F5:
|
||||
if self._ftp_view.is_focus():
|
||||
self.on_ftp_copy()
|
||||
elif self._file_view.is_focus():
|
||||
self.on_file_copy()
|
||||
elif key is KeyboardKey.DELETE:
|
||||
if self._ftp_view.is_focus():
|
||||
self.on_ftp_file_remove()
|
||||
elif self._file_view.is_focus():
|
||||
self.on_file_remove()
|
||||
elif self._bookmark_view.is_focus():
|
||||
self.on_bookmark_remove()
|
||||
elif key is KeyboardKey.RETURN:
|
||||
path, column = view.get_cursor()
|
||||
if path:
|
||||
view.emit("row-activated", path, column)
|
||||
|
||||
def on_view_press(self, view, event):
|
||||
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_PRIMARY:
|
||||
target = view.get_path_at_pos(event.x, event.y)
|
||||
mask = not (event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK))
|
||||
if target and mask and view.get_selection().path_is_selected(target[0]):
|
||||
self._select_enabled = False
|
||||
|
||||
def on_view_release(self, view, event):
|
||||
""" Handles a mouse click (release) to view. """
|
||||
# Enable selection.
|
||||
self._select_enabled = True
|
||||
|
||||
@staticmethod
|
||||
def on_paned_size_allocate(paned, allocation):
|
||||
""" Sets default homogeneous sizes. """
|
||||
paned.set_position(0.5 * allocation.width)
|
||||
|
||||
@staticmethod
|
||||
def get_size_from_bytes(size):
|
||||
""" Simple convert function from bytes to other units like K, M or G. """
|
||||
try:
|
||||
b = float(size)
|
||||
except ValueError:
|
||||
return size
|
||||
else:
|
||||
kb, mb, gb = 1024.0, 1048576.0, 1073741824.0
|
||||
|
||||
if b < kb:
|
||||
return str(b)
|
||||
elif kb <= b < mb:
|
||||
return f"{b / kb:.1f} K"
|
||||
elif mb <= b < gb:
|
||||
return f"{b / mb:.1f} M"
|
||||
elif gb <= b:
|
||||
return f"{b / gb:.1f} G"
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
||||
BIN
app/ui/icons/hicolor/96x96/apps/demon-editor.png
Normal file
BIN
app/ui/icons/hicolor/96x96/apps/demon-editor.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
634
app/ui/icons/hicolor/scalable/apps/demon-editor.svg
Normal file
634
app/ui/icons/hicolor/scalable/apps/demon-editor.svg
Normal file
@@ -0,0 +1,634 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 22 KiB |
441
app/ui/imports.glade
Normal file
441
app/ui/imports.glade
Normal file
@@ -0,0 +1,441 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2020 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellite list editor. -->
|
||||
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkImage" id="details_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">emblem-important-symbolic</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="import_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">document-revert-symbolic-rtl</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkListStore" id="main_list_store">
|
||||
<columns>
|
||||
<!-- column-name name -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name type -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name selected -->
|
||||
<column type="gboolean"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkImage" id="remove_selection_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-undo</property>
|
||||
</object>
|
||||
<object class="GtkMenu" id="popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="select_all_popup_item">
|
||||
<property name="label">gtk-select-all</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="activate" handler="on_select_all" object="main_view" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="unselect_all_popup_item">
|
||||
<property name="label" translatable="yes">Remove selection</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">remove_selection_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_unselect_all" object="main_view" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkListStore" id="services_list_store">
|
||||
<columns>
|
||||
<!-- column-name name -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name type -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkWindow" id="dialog_window">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">Import</property>
|
||||
<property name="modal">True</property>
|
||||
<property name="window_position">center-on-parent</property>
|
||||
<property name="default_width">480</property>
|
||||
<property name="default_height">320</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<signal name="check-resize" handler="on_resize" swapped="no"/>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_box">
|
||||
<property name="width_request">480</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="toolbar_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="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">10</property>
|
||||
<property name="margin_bottom">10</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>
|
||||
<accelerator key="i" signal="clicked" modifiers="Primary"/>
|
||||
</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">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="primary-toolbar"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkPaned" id="main_paned">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="wide_handle">True</property>
|
||||
<child>
|
||||
<object class="GtkFrame" id="bouquets_box_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0.49000000953674316</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="bouquets_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="bouquets_screlled_window">
|
||||
<property name="width_request">200</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="main_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">main_list_store</property>
|
||||
<property name="headers_clickable">False</property>
|
||||
<property name="search_column">0</property>
|
||||
<signal name="button-press-event" handler="on_popup_menu" object="popup_menu" swapped="no"/>
|
||||
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_key_press" swapped="no"/>
|
||||
<signal name="select-all" handler="on_select_all" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="bouquet_name_column">
|
||||
<property name="title" translatable="yes">Name</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="bq_name_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="bouquet_type_column">
|
||||
<property name="title" translatable="yes">Type</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="bq_type_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="bouquet_selected_column">
|
||||
<property name="title" translatable="yes">Selected</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererToggle" id="bq_selected_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
<signal name="toggled" handler="on_selected_toggled" swapped="no"/>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="active">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="label" translatable="yes">Bouquets</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="services_box_frame">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0.49000000953674316</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="services_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="services_view_scrolled_window">
|
||||
<property name="width_request">150</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="services_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">services_list_store</property>
|
||||
<property name="headers_clickable">False</property>
|
||||
<property name="search_column">0</property>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="service_name_column">
|
||||
<property name="title" translatable="yes">Service</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="info_name_renderer">
|
||||
<property name="xalign">0.02</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="service_type_column">
|
||||
<property name="title" translatable="yes">Type</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="info_type_renderer">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="label" translatable="yes">Bouquet details</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<signal name="response" handler="on_info_bar_close" swapped="no"/>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout_style">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">16</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="message_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">message</property>
|
||||
<property name="wrap">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
271
app/ui/imports.py
Normal file
271
app/ui/imports.py
Normal file
@@ -0,0 +1,271 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
|
||||
from app.commons import run_idle, log
|
||||
from app.eparser import get_bouquets, get_services, BouquetsReader
|
||||
from app.eparser.ecommons import BqType, BqServiceType, Bouquet
|
||||
from app.eparser.neutrino.bouquets import parse_webtv, parse_bouquets as get_neutrino_bouquets
|
||||
from app.settings import SettingsType, IS_DARWIN, SEP
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_chooser_dialog, get_message, get_builder
|
||||
from app.ui.main_helper import on_popup_menu
|
||||
from .uicommons import Gtk, UI_RESOURCES_PATH, KeyboardKey, Column
|
||||
|
||||
|
||||
def import_bouquet(transient, model, path, settings, services, appender, file_path=None):
|
||||
""" Import of single bouquet """
|
||||
itr = model.get_iter(path)
|
||||
bq_type = BqType(model.get(itr, Column.BQ_TYPE)[0])
|
||||
pattern, f_pattern = None, None
|
||||
profile = settings.setting_type
|
||||
|
||||
if profile is SettingsType.ENIGMA_2:
|
||||
pattern = f".{bq_type.value}"
|
||||
f_pattern = f"{'' if IS_DARWIN else 'userbouquet.'}*{pattern}"
|
||||
elif profile is SettingsType.NEUTRINO_MP:
|
||||
pattern = "webtv.xml" if bq_type is BqType.WEBTV else "bouquets.xml"
|
||||
f_pattern = "bouquets.xml"
|
||||
if bq_type is BqType.TV:
|
||||
f_pattern = "ubouquets.xml"
|
||||
elif bq_type is BqType.WEBTV:
|
||||
f_pattern = "webtv.xml"
|
||||
|
||||
file_path = file_path or get_chooser_dialog(transient, settings, "bouquet files", (f_pattern,))
|
||||
if file_path == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
if not str(file_path).endswith(pattern):
|
||||
show_dialog(DialogType.ERROR, transient, text="No bouquet file is selected!")
|
||||
return
|
||||
|
||||
if profile is SettingsType.ENIGMA_2:
|
||||
if IS_DARWIN and file_path.rfind("userbouquet.") < 0:
|
||||
show_dialog(DialogType.ERROR, transient, text="Not allowed in this context!")
|
||||
return
|
||||
|
||||
bq = get_enigma2_bouquet(file_path)
|
||||
imported = list(filter(lambda x: x.data in services or x.type is BqServiceType.IPTV, bq.services))
|
||||
|
||||
if len(imported) == 0:
|
||||
show_dialog(DialogType.ERROR, transient, text="The main list does not contain services for this bouquet!")
|
||||
return
|
||||
|
||||
if model.iter_n_children(itr):
|
||||
appender(bq, itr)
|
||||
else:
|
||||
p_itr = model.iter_parent(itr)
|
||||
appender(bq, p_itr) if p_itr else appender(bq, itr)
|
||||
elif profile is SettingsType.NEUTRINO_MP:
|
||||
if bq_type is BqType.WEBTV:
|
||||
bqs = parse_webtv(file_path, "WEBTV", bq_type.value)
|
||||
else:
|
||||
bqs = get_neutrino_bouquets(file_path, "", bq_type.value)
|
||||
file_path = f"{Path(file_path).parent}{SEP}"
|
||||
ImportDialog(transient, file_path, settings, services.keys(), lambda b, s: appender(b), (bqs,)).show()
|
||||
|
||||
|
||||
def get_enigma2_bouquet(path):
|
||||
path, sep, f_name = path.rpartition("userbouquet.")
|
||||
name, sep, suf = f_name.rpartition(".")
|
||||
bq = BouquetsReader.get_bouquet(path, name, suf)
|
||||
bouquet = Bouquet(name=bq[0], type=BqType(suf).value, services=bq[1], locked=None, hidden=None)
|
||||
return bouquet
|
||||
|
||||
|
||||
class ImportDialog:
|
||||
def __init__(self, transient, path, settings, service_ids, appender, bouquets=None):
|
||||
handlers = {"on_import": self.on_import,
|
||||
"on_cursor_changed": self.on_cursor_changed,
|
||||
"on_selected_toggled": self.on_selected_toggled,
|
||||
"on_info_bar_close": self.on_info_bar_close,
|
||||
"on_select_all": self.on_select_all,
|
||||
"on_unselect_all": self.on_unselect_all,
|
||||
"on_popup_menu": on_popup_menu,
|
||||
"on_resize": self.on_resize,
|
||||
"on_key_press": self.on_key_press}
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "imports.glade", handlers)
|
||||
|
||||
self._bq_services = {}
|
||||
self._services = {}
|
||||
self._service_ids = service_ids
|
||||
self._append = appender
|
||||
self._profile = settings.setting_type
|
||||
self._settings = settings
|
||||
self._bouquets = bouquets
|
||||
|
||||
self._dialog_window = builder.get_object("dialog_window")
|
||||
self._dialog_window.set_transient_for(transient)
|
||||
self._main_model = builder.get_object("main_list_store")
|
||||
self._main_view = builder.get_object("main_view")
|
||||
self._services_view = builder.get_object("services_view")
|
||||
self._services_model = builder.get_object("services_list_store")
|
||||
self._info_check_button = builder.get_object("info_check_button")
|
||||
self._info_check_button.bind_property("active", builder.get_object("services_box_frame"), "visible")
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("message_label")
|
||||
window_size = self._settings.get("import_dialog_window_size")
|
||||
if window_size:
|
||||
self._dialog_window.resize(*window_size)
|
||||
|
||||
self.init_data(path)
|
||||
|
||||
def show(self):
|
||||
self._dialog_window.show()
|
||||
|
||||
@run_idle
|
||||
def init_data(self, path):
|
||||
self._main_model.clear()
|
||||
self._services_model.clear()
|
||||
try:
|
||||
if not self._bouquets:
|
||||
log("Import [init data]: getting bouquets...")
|
||||
self._bouquets = get_bouquets(path, self._profile)
|
||||
for bqs in self._bouquets:
|
||||
for bq in bqs.bouquets:
|
||||
self._main_model.append((bq.name, bq.type, True))
|
||||
self._bq_services[(bq.name, bq.type)] = bq.services
|
||||
|
||||
if self._profile is SettingsType.ENIGMA_2:
|
||||
services = get_services(path, self._profile, 5 if self._settings.v5_support else 4)
|
||||
elif self._profile is SettingsType.NEUTRINO_MP:
|
||||
services = get_services(path, self._profile, 0)
|
||||
else:
|
||||
self.show_info_message("Setting format not supported!", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
for srv in services:
|
||||
self._services[srv.fav_id] = srv
|
||||
except FileNotFoundError as e:
|
||||
log("Import error [init data]: {}".format(e))
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
|
||||
def on_import(self, item):
|
||||
if not any(r[-1] for r in self._main_model):
|
||||
self.show_info_message(get_message("No selected item!"), Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
if not self._bouquets or show_dialog(DialogType.QUESTION, self._dialog_window) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
self.import_data()
|
||||
|
||||
@run_idle
|
||||
def import_data(self):
|
||||
""" Importing data into models. """
|
||||
if not self._bouquets:
|
||||
return
|
||||
|
||||
log("Importing data...")
|
||||
services = set()
|
||||
to_delete = set()
|
||||
for row in self._main_model:
|
||||
bq = (row[0], row[1])
|
||||
if row[-1]:
|
||||
for bq_srv in self._bq_services.get(bq, []):
|
||||
srv = self._services.get(bq_srv.data, None)
|
||||
if srv:
|
||||
services.add(srv)
|
||||
else:
|
||||
to_delete.add(bq)
|
||||
bqs_to_delete = []
|
||||
for bqs in self._bouquets:
|
||||
for bq in bqs.bouquets:
|
||||
if (bq.name, bq.type) in to_delete:
|
||||
bqs_to_delete.append(bq)
|
||||
for bqs in self._bouquets:
|
||||
bq = bqs.bouquets
|
||||
for b in bqs_to_delete:
|
||||
with suppress(ValueError):
|
||||
bq.remove(b)
|
||||
self._append(self._bouquets, list(filter(lambda s: s.fav_id not in self._service_ids, services)))
|
||||
self._dialog_window.destroy()
|
||||
|
||||
@run_idle
|
||||
def on_cursor_changed(self, view):
|
||||
if not self._info_check_button.get_active():
|
||||
return
|
||||
|
||||
self._services_model.clear()
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if not paths:
|
||||
return
|
||||
|
||||
bq_services = self._bq_services.get(model.get(model.get_iter(paths[0]), 0, 1))
|
||||
for bq_srv in bq_services:
|
||||
if bq_srv.type is BqServiceType.DEFAULT:
|
||||
srv = self._services.get(bq_srv.data, None)
|
||||
if srv:
|
||||
self._services_model.append((srv.service, srv.service_type))
|
||||
else:
|
||||
self._services_model.append((bq_srv.name, bq_srv.type.value))
|
||||
|
||||
def on_selected_toggled(self, toggle, path):
|
||||
self._main_model.set_value(self._main_model.get_iter(path), 2, not toggle.get_active())
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(True)
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._message_label.set_text(text)
|
||||
|
||||
@run_idle
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
self._info_bar.set_visible(False)
|
||||
|
||||
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, 2, select))
|
||||
|
||||
def on_resize(self, window):
|
||||
if self._settings:
|
||||
self._settings.add("import_dialog_window_size", window.get_size())
|
||||
|
||||
def on_key_press(self, view, event):
|
||||
""" Handling keystrokes """
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
return
|
||||
key = KeyboardKey(key_code)
|
||||
|
||||
if key is KeyboardKey.SPACE:
|
||||
path, column = view.get_cursor()
|
||||
itr = self._main_model.get_iter(path)
|
||||
selected = self._main_model.get_value(itr, 2)
|
||||
self._main_model.set_value(itr, 2, not selected)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
1758
app/ui/iptv.glade
Normal file
1758
app/ui/iptv.glade
Normal file
File diff suppressed because it is too large
Load Diff
1060
app/ui/iptv.py
Normal file
1060
app/ui/iptv.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
app/ui/lang/be/LC_MESSAGES/demon-editor.mo
Normal file
BIN
app/ui/lang/be/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
BIN
app/ui/lang/de/LC_MESSAGES/demon-editor.mo
Normal file
BIN
app/ui/lang/de/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
BIN
app/ui/lang/es/LC_MESSAGES/demon-editor.mo
Normal file
BIN
app/ui/lang/es/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
BIN
app/ui/lang/it/LC_MESSAGES/demon-editor.mo
Normal file
BIN
app/ui/lang/it/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
BIN
app/ui/lang/nl/LC_MESSAGES/demon-editor.mo
Normal file
BIN
app/ui/lang/nl/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
BIN
app/ui/lang/pl/LC_MESSAGES/demon-editor.mo
Normal file
BIN
app/ui/lang/pl/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
BIN
app/ui/lang/pt/LC_MESSAGES/demon-editor.mo
Normal file
BIN
app/ui/lang/pt/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
BIN
app/ui/lang/ru/LC_MESSAGES/demon-editor.mo
Normal file
BIN
app/ui/lang/ru/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
BIN
app/ui/lang/tr/LC_MESSAGES/demon-editor.mo
Normal file
BIN
app/ui/lang/tr/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
BIN
app/ui/lang/zh_CN/LC_MESSAGES/demon-editor.mo
Normal file
BIN
app/ui/lang/zh_CN/LC_MESSAGES/demon-editor.mo
Normal file
Binary file not shown.
125
app/ui/logs.glade
Normal file
125
app/ui/logs.glade
Normal file
@@ -0,0 +1,125 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.18"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellite list editor. -->
|
||||
<!-- interface-copyright 2018-2021 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkFrame" id="log_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0.49000000953674316</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="header_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">2</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="clear_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Clear</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_clear" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="clear_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-clear</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="log_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="left_margin">5</property>
|
||||
<property name="right_margin">5</property>
|
||||
<property name="top_margin">5</property>
|
||||
<property name="bottom_margin">5</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="log_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Logs</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
66
app/ui/logs.py
Normal file
66
app/ui/logs.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import logging
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import LOGGER_NAME
|
||||
from app.ui.dialogs import get_builder
|
||||
from app.ui.main_helper import append_text_to_tview
|
||||
from app.ui.uicommons import Gtk, UI_RESOURCES_PATH
|
||||
|
||||
|
||||
class LogsClient(Gtk.Box):
|
||||
""" Logger GUI client. """
|
||||
|
||||
class LogHandler(logging.Handler):
|
||||
def __init__(self, view):
|
||||
logging.Handler.__init__(self)
|
||||
self._view = view
|
||||
|
||||
def handle(self, rec):
|
||||
GLib.idle_add(append_text_to_tview, f"{rec.msg}\n", self._view)
|
||||
|
||||
def __init__(self, app, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._app = app
|
||||
|
||||
handlers = {"on_clear": self.on_clear}
|
||||
builder = get_builder(UI_RESOURCES_PATH + "logs.glade", handlers)
|
||||
|
||||
self._log_view = builder.get_object("log_view")
|
||||
self.pack_start(builder.get_object("log_frame"), True, True, 0)
|
||||
|
||||
logger = logging.getLogger(LOGGER_NAME)
|
||||
logger.addHandler(LogsClient.LogHandler(self._log_view))
|
||||
|
||||
self.show()
|
||||
|
||||
def on_clear(self, button):
|
||||
GLib.idle_add(self._log_view.get_buffer().set_text, "")
|
||||
75
app/ui/mac_style.css
Normal file
75
app/ui/mac_style.css
Normal file
@@ -0,0 +1,75 @@
|
||||
* {
|
||||
background-clip: padding-box;
|
||||
-GtkScrolledWindow-scrollbar-spacing: 0;
|
||||
-GtkToolItemGroup-expander-size: 11;
|
||||
-GtkWidget-text-handle-width: 20;
|
||||
-GtkWidget-text-handle-height: 20;
|
||||
-GtkDialog-button-spacing: 12;
|
||||
-GtkDialog-action-area-border: 6;
|
||||
}
|
||||
|
||||
entry {
|
||||
min-height: 2.0em;
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
entry > image {
|
||||
padding-left: 0.3em;
|
||||
padding-right: 0.3em;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 1.2em;
|
||||
min-width: 1.5em;
|
||||
padding-top: 0.3em;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
button:active, button:checked {
|
||||
color: @theme_selected_fg_color;
|
||||
background-image: linear-gradient(@theme_selected_bg_color, @theme_selected_bg_color);
|
||||
}
|
||||
|
||||
combobox {
|
||||
min-height: 2.2em;
|
||||
}
|
||||
|
||||
spinbutton {
|
||||
min-height: 1.5em;
|
||||
}
|
||||
|
||||
toolbutton {
|
||||
padding: 0.1em;
|
||||
}
|
||||
|
||||
spinner {
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
infobar {
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
revealer > box > button {
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
switch slider {
|
||||
min-height: 1.5em;
|
||||
min-width: 1.5em;
|
||||
}
|
||||
|
||||
popover .view {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.font > box {
|
||||
min-height: 1.5em;
|
||||
padding-top: 0.1em;
|
||||
padding-bottom: 0.1em;
|
||||
}
|
||||
|
||||
.dialog-action-area button {
|
||||
margin-bottom: 0.6em;
|
||||
}
|
||||
4892
app/ui/main.glade
Normal file
4892
app/ui/main.glade
Normal file
File diff suppressed because it is too large
Load Diff
4226
app/ui/main.py
Normal file
4226
app/ui/main.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,856 +0,0 @@
|
||||
import os
|
||||
from contextlib import suppress
|
||||
from functools import lru_cache
|
||||
|
||||
from app.commons import run_idle, log
|
||||
from app.eparser import get_blacklist, write_blacklist, parse_m3u
|
||||
from app.eparser import get_services, get_bouquets, write_bouquets, write_services, Bouquets, Bouquet, Service
|
||||
from app.eparser.ecommons import CAS, FLAG
|
||||
from app.eparser.enigma.bouquets import BqServiceType
|
||||
from app.properties import get_config, write_config, Profile
|
||||
from . import Gtk, Gdk, UI_RESOURCES_PATH, LOCKED_ICON, HIDE_ICON
|
||||
from .dialogs import show_dialog, DialogType
|
||||
from .download_dialog import show_download_dialog
|
||||
from .main_helper import edit_marker, insert_marker, move_items, edit, ViewTarget, set_flags, locate_in_services, \
|
||||
scroll_to, get_base_model
|
||||
from .picons_dialog import PiconsDialog
|
||||
from .satellites_dialog import show_satellites_dialog
|
||||
from .settings_dialog import show_settings_dialog
|
||||
|
||||
|
||||
class MainAppWindow:
|
||||
_SERVICE_LIST_NAME = "services_list_store"
|
||||
_FAV_LIST_NAME = "fav_list_store"
|
||||
_BOUQUETS_LIST_NAME = "bouquets_tree_store"
|
||||
# dynamically active elements depending on the selected view
|
||||
_SERVICE_ELEMENTS = ("copy_tool_button", "to_fav_tool_button", "copy_menu_item", "services_to_fav_move_popup_item",
|
||||
"services_edit_popup_item", "services_copy_popup_item", "filter_entry")
|
||||
|
||||
_BOUQUET_ELEMENTS = ("edit_tool_button", "new_tool_button",
|
||||
"bouquets_new_popup_item", "bouquets_edit_popup_item")
|
||||
|
||||
_COMMONS_ELEMENTS = ("edit_tool_button", "remove_tool_button", "delete_menu_item", "services_remove_popup_item",
|
||||
"bouquets_remove_popup_item", "fav_remove_popup_item", "up_tool_button", "down_tool_button")
|
||||
|
||||
_FAV_ELEMENTS = ("cut_tool_button", "paste_tool_button", "cut_menu_item",
|
||||
"paste_menu_item", "fav_cut_popup_item", "fav_paste_popup_item", "import_m3u_tool_button",
|
||||
"fav_import_m3u_popup_item", "fav_insert_marker_popup_item", "fav_edit_popup_item",
|
||||
"fav_locate_popup_item")
|
||||
|
||||
_FAV_ONLY_ELEMENTS = ("import_m3u_tool_button", "fav_import_m3u_popup_item", "fav_insert_marker_popup_item",
|
||||
"fav_edit_marker_popup_item")
|
||||
|
||||
_LOCK_HIDE_ELEMENTS = ("locked_tool_button", "hide_tool_button")
|
||||
|
||||
__DYNAMIC_ELEMENTS = ("up_tool_button", "down_tool_button", "cut_tool_button", "copy_tool_button",
|
||||
"paste_tool_button", "to_fav_tool_button", "new_tool_button", "remove_tool_button",
|
||||
"cut_menu_item", "copy_menu_item", "paste_menu_item", "delete_menu_item", "edit_tool_button",
|
||||
"services_to_fav_move_popup_item", "services_edit_popup_item", "locked_tool_button",
|
||||
"services_remove_popup_item", "fav_cut_popup_item", "fav_paste_popup_item",
|
||||
"bouquets_new_popup_item", "bouquets_edit_popup_item", "services_remove_popup_item",
|
||||
"bouquets_remove_popup_item", "fav_remove_popup_item", "hide_tool_button",
|
||||
"import_m3u_tool_button", "fav_import_m3u_popup_item", "fav_insert_marker_popup_item",
|
||||
"fav_edit_marker_popup_item", "fav_edit_popup_item", "fav_locate_popup_item",
|
||||
"services_copy_popup_item", "filter_entry")
|
||||
|
||||
def __init__(self):
|
||||
handlers = {"on_close_main_window": self.on_quit,
|
||||
"on_resize": self.on_resize,
|
||||
"on_about_app": self.on_about_app,
|
||||
"on_preferences": self.on_preferences,
|
||||
"on_download": self.on_download,
|
||||
"on_data_open": self.on_data_open,
|
||||
"on_data_save": self.on_data_save,
|
||||
"on_tree_view_key_release": self.on_tree_view_key_release,
|
||||
"on_bouquets_selection": self.on_bouquets_selection,
|
||||
"on_satellite_editor_show": self.on_satellite_editor_show,
|
||||
"on_services_selection": self.on_services_selection,
|
||||
"on_fav_selection": self.on_fav_selection,
|
||||
"on_up": self.on_up,
|
||||
"on_down": self.on_down,
|
||||
"on_cut": self.on_cut,
|
||||
"on_copy": self.on_copy,
|
||||
"on_paste": self.on_paste,
|
||||
"on_edit": self.on_edit,
|
||||
"on_delete": self.on_delete,
|
||||
"on_new_bouquet": self.on_new_bouquet,
|
||||
"on_bouquets_edit": self.on_bouquets_edit,
|
||||
"on_tool_edit": self.on_tool_edit,
|
||||
"on_to_fav_move": self.on_to_fav_move,
|
||||
"on_services_tree_view_drag_data_get": self.on_services_tree_view_drag_data_get,
|
||||
"on_fav_tree_view_drag_data_get": self.on_fav_tree_view_drag_data_get,
|
||||
"on_fav_tree_view_drag_data_received": self.on_fav_tree_view_drag_data_received,
|
||||
"on_view_popup_menu": self.on_view_popup_menu,
|
||||
"on_view_focus": self.on_view_focus,
|
||||
"on_hide": self.on_hide,
|
||||
"on_locked": self.on_locked,
|
||||
"on_model_changed": self.on_model_changed,
|
||||
"on_import_m3u": self.on_import_m3u,
|
||||
"on_insert_marker": self.on_insert_marker,
|
||||
"on_edit_marker": self.on_edit_marker,
|
||||
"on_fav_popup": self.on_fav_popup,
|
||||
"on_locate_in_services": self.on_locate_in_services,
|
||||
"on_picons_loader_show": self.on_picons_loader_show,
|
||||
"on_filter_changed": self.on_filter_changed}
|
||||
|
||||
self.__options = get_config()
|
||||
self.__profile = self.__options.get("profile")
|
||||
os.makedirs(os.path.dirname(self.__options.get(self.__profile).get("data_dir_path")), exist_ok=True)
|
||||
# Used for copy/paste. When adding the previous data will not be deleted.
|
||||
# Clearing only after the insertion!
|
||||
self.__rows_buffer = []
|
||||
self.__services = {}
|
||||
self.__bouquets = {}
|
||||
self.__blacklist = set()
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "main_window.glade")
|
||||
builder.connect_signals(handlers)
|
||||
self.__main_window = builder.get_object("main_window")
|
||||
main_window_size = self.__options.get("window_size", None)
|
||||
# Setting the last size of the window if it was saved
|
||||
if main_window_size:
|
||||
self.__main_window.resize(*main_window_size)
|
||||
self.__services_view = builder.get_object("services_tree_view")
|
||||
self.__fav_view = builder.get_object("fav_tree_view")
|
||||
self.__bouquets_view = builder.get_object("bouquets_tree_view")
|
||||
self.__fav_model = builder.get_object("fav_list_store")
|
||||
self.__services_model = builder.get_object("services_list_store")
|
||||
self.__bouquets_model = builder.get_object("bouquets_tree_store")
|
||||
self.__status_bar = builder.get_object("status_bar")
|
||||
self.__profile_label = builder.get_object("profile_label")
|
||||
self.__status_bar.push(0, "Current IP: " + self.__options.get(self.__profile).get("host"))
|
||||
self.__profile_label.set_text("Enigma2 v.4" if Profile(self.__profile) is Profile.ENIGMA_2 else "Neutrino-MP")
|
||||
# dynamically active elements depending on the selected view
|
||||
self.__tool_elements = {k: builder.get_object(k) for k in self.__DYNAMIC_ELEMENTS}
|
||||
self.__cas_label = builder.get_object("cas_label")
|
||||
self.__fav_count_label = builder.get_object("fav_count_label")
|
||||
self.__bouquets_count_label = builder.get_object("bouquets_count_label")
|
||||
self.__tv_count_label = builder.get_object("tv_count_label")
|
||||
self.__radio_count_label = builder.get_object("radio_count_label")
|
||||
self.__data_count_label = builder.get_object("data_count_label")
|
||||
self.__fav_edit_marker_popup_item = builder.get_object("fav_edit_marker_popup_item")
|
||||
# Filter
|
||||
self.__services_model_filter = builder.get_object("services_model_filter")
|
||||
self.__services_model_filter.set_visible_func(self.services_filter_function)
|
||||
self.__filter_entry = builder.get_object("filter_entry")
|
||||
self.init_drag_and_drop() # drag and drop
|
||||
# Force ctrl press event for view. Multiple selections in lists only with Space key(as in file managers)!!!
|
||||
self.__services_view.connect("key-press-event", self.force_ctrl)
|
||||
self.__fav_view.connect("key-press-event", self.force_ctrl)
|
||||
self.__main_window.show()
|
||||
|
||||
def init_drag_and_drop(self):
|
||||
""" Enable drag and drop """
|
||||
target = []
|
||||
self.__services_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, target, Gdk.DragAction.COPY)
|
||||
self.__fav_view.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, target,
|
||||
Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE)
|
||||
self.__fav_view.enable_model_drag_dest(target, Gdk.DragAction.DEFAULT | Gdk.DragAction.MOVE)
|
||||
self.__fav_view.drag_dest_set_target_list(None)
|
||||
self.__fav_view.drag_source_set_target_list(None)
|
||||
self.__fav_view.drag_dest_add_text_targets()
|
||||
self.__fav_view.drag_source_add_text_targets()
|
||||
self.__services_view.drag_source_set_target_list(None)
|
||||
self.__services_view.drag_source_add_text_targets()
|
||||
|
||||
def force_ctrl(self, view, event):
|
||||
""" Function for force ctrl press event for view """
|
||||
event.state |= Gdk.ModifierType.CONTROL_MASK
|
||||
|
||||
def on_quit(self, *args):
|
||||
""" Called before app quit """
|
||||
write_config(self.__options) # storing current config
|
||||
Gtk.main_quit()
|
||||
|
||||
def on_resize(self, window):
|
||||
""" Stores new size properties for app window after resize """
|
||||
self.__options["window_size"] = window.get_size()
|
||||
|
||||
def on_up(self, item):
|
||||
self.move_items(Gdk.KEY_Up)
|
||||
|
||||
def on_down(self, item):
|
||||
self.move_items(Gdk.KEY_Down)
|
||||
|
||||
def on_about_app(self, item):
|
||||
show_dialog(DialogType.ABOUT, self.__main_window)
|
||||
|
||||
def move_items(self, key):
|
||||
""" Move items in fav or bouquets tree view """
|
||||
if self.__services_view.is_focus():
|
||||
return
|
||||
elif self.__fav_view.is_focus():
|
||||
move_items(key, self.__fav_view)
|
||||
elif self.__bouquets_view and key not in (Gdk.KEY_Page_Up, Gdk.KEY_Page_Down):
|
||||
move_items(key, self.__bouquets_view)
|
||||
|
||||
def on_cut(self, view):
|
||||
for row in tuple(self.on_delete(view)):
|
||||
self.__rows_buffer.append(row)
|
||||
|
||||
def on_copy(self, view):
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
itrs = [model.get_iter(path) for path in paths]
|
||||
rows = [(0, *model.get(in_itr, 2, 3, 4, 5, 7, 14, 16)) for in_itr in itrs]
|
||||
self.__rows_buffer.extend(rows)
|
||||
|
||||
def on_paste(self, view):
|
||||
selection = view.get_selection()
|
||||
dest_index = 0
|
||||
bq_selected = self.is_bouquet_selected()
|
||||
|
||||
if not bq_selected:
|
||||
show_dialog(DialogType.ERROR, self.__main_window, "Error. No bouquet is selected!")
|
||||
return
|
||||
|
||||
fav_bouquet = self.__bouquets[bq_selected]
|
||||
model, paths = selection.get_selected_rows()
|
||||
|
||||
if paths:
|
||||
dest_index = int(paths[0][0])
|
||||
|
||||
for row in self.__rows_buffer:
|
||||
dest_index += 1
|
||||
model.insert(dest_index, row)
|
||||
fav_bouquet.insert(dest_index, row[-1])
|
||||
|
||||
if model.get_name() == self._FAV_LIST_NAME:
|
||||
self.update_fav_num_column(model)
|
||||
|
||||
self.__rows_buffer.clear()
|
||||
self.on_view_focus(view, None)
|
||||
|
||||
def on_edit(self, view):
|
||||
model = get_base_model(view.get_model())
|
||||
name = model.get_name()
|
||||
if name == self._BOUQUETS_LIST_NAME:
|
||||
self.on_bouquets_edit(view)
|
||||
# edit(view, self.__main_window, ViewTarget.BOUQUET)
|
||||
elif name == self._FAV_LIST_NAME:
|
||||
edit(view, self.__main_window, ViewTarget.FAV, service_view=self.__services_view, channels=self.__services)
|
||||
elif name == self._SERVICE_LIST_NAME:
|
||||
edit(view, self.__main_window, ViewTarget.SERVICES, fav_view=self.__fav_view, channels=self.__services)
|
||||
|
||||
def on_delete(self, item):
|
||||
""" Delete selected items from views
|
||||
|
||||
returns deleted rows list!
|
||||
"""
|
||||
for view in [self.__services_view, self.__fav_view, self.__bouquets_view]:
|
||||
if view.is_focus():
|
||||
selection = view.get_selection()
|
||||
model, paths = selection.get_selected_rows()
|
||||
model_name = get_base_model(model).get_name()
|
||||
itrs = [model.get_iter(path) for path in paths]
|
||||
rows = [model[in_itr][:] for in_itr in itrs]
|
||||
bq_selected = self.is_bouquet_selected()
|
||||
fav_bouquet = None
|
||||
|
||||
if bq_selected:
|
||||
fav_bouquet = self.__bouquets.get(bq_selected, None)
|
||||
|
||||
if model_name == self._FAV_LIST_NAME:
|
||||
self.remove_favs(fav_bouquet, itrs, model)
|
||||
elif model_name == self._BOUQUETS_LIST_NAME:
|
||||
self.delete_bouquets(itrs, model, bq_selected)
|
||||
elif model_name == self._SERVICE_LIST_NAME:
|
||||
self.delete_services(bq_selected, itrs, model, rows)
|
||||
|
||||
self.on_view_focus(view, None)
|
||||
|
||||
return rows
|
||||
|
||||
def remove_favs(self, fav_bouquet, itrs, model):
|
||||
""" Deleting bouquet services """
|
||||
if fav_bouquet:
|
||||
for itr in itrs:
|
||||
del fav_bouquet[int(model.get_path(itr)[0])]
|
||||
self.__fav_model.remove(itr)
|
||||
self.update_fav_num_column(model)
|
||||
|
||||
def delete_services(self, bq_selected, itrs, model, rows):
|
||||
""" Deleting services """
|
||||
srv_itrs = [self.__services_model_filter.convert_iter_to_child_iter(
|
||||
model.convert_iter_to_child_iter(itr)) for itr in itrs]
|
||||
for s_itr in srv_itrs:
|
||||
self.__services_model.remove(s_itr)
|
||||
|
||||
for row in rows:
|
||||
# There are channels with the same parameters except for the name.
|
||||
# None because it can have duplicates! Need fix
|
||||
fav_id = row[-2]
|
||||
for bq in self.__bouquets:
|
||||
services = self.__bouquets[bq]
|
||||
if services:
|
||||
with suppress(ValueError):
|
||||
services.remove(fav_id)
|
||||
self.__services.pop(fav_id, None)
|
||||
self.__fav_model.clear()
|
||||
|
||||
if bq_selected:
|
||||
self.update_bouquet_channels(self.__fav_model, None, bq_selected)
|
||||
|
||||
def delete_bouquets(self, itrs, model, bouquet):
|
||||
""" Deleting bouquets """
|
||||
for itr in itrs:
|
||||
if len(model.get_path(itr)) < 2:
|
||||
show_dialog(DialogType.ERROR, self.__main_window, "This item is not allowed to be removed!")
|
||||
return
|
||||
else:
|
||||
self.__bouquets.pop(bouquet)
|
||||
self.__fav_model.clear()
|
||||
self.__bouquets_model.remove(itr)
|
||||
|
||||
def get_bouquet_file_name(self, bouquet):
|
||||
bouquet_file_name = "{}userbouquet.{}.{}".format(self.__options.get(self.__profile).get("data_dir_path"),
|
||||
*bouquet.split(":"))
|
||||
return bouquet_file_name
|
||||
|
||||
def on_new_bouquet(self, view):
|
||||
""" Creates a new item in the bouquets tree """
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
|
||||
if paths:
|
||||
itr = model.get_iter(paths[0])
|
||||
bq_type = model.get_value(itr, 3)
|
||||
|
||||
bq_name = "bouquet"
|
||||
count = 0
|
||||
key = "{}:{}".format(bq_name, bq_type)
|
||||
# Generating name of new bouquet
|
||||
while key in self.__bouquets:
|
||||
count += 1
|
||||
bq_name = "bouquet{}".format(count)
|
||||
key = "{}:{}".format(bq_name, bq_type)
|
||||
|
||||
response = show_dialog(DialogType.INPUT, self.__main_window, bq_name)
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
bq = response, None, None, bq_type
|
||||
key = "{}:{}".format(response, bq_type)
|
||||
|
||||
if model.iter_n_children(itr): # parent
|
||||
ch_itr = model.insert(itr, 0, bq)
|
||||
scroll_to(model.get_path(ch_itr), view, paths)
|
||||
else:
|
||||
p_itr = model.iter_parent(itr)
|
||||
it = model.insert(p_itr, int(model.get_path(itr)[1]) + 1, bq) if p_itr else model.append(itr, bq)
|
||||
scroll_to(model.get_path(it), view, paths)
|
||||
self.__bouquets[key] = []
|
||||
|
||||
def on_tool_edit(self, item):
|
||||
""" Edit tool bar button """
|
||||
if self.__services_view.is_focus():
|
||||
self.on_edit(self.__services_view)
|
||||
elif self.__fav_view.is_focus():
|
||||
self.on_edit(self.__fav_view)
|
||||
elif self.__bouquets_view.is_focus():
|
||||
self.on_edit(self.__bouquets_view)
|
||||
|
||||
def on_bouquets_edit(self, view):
|
||||
""" Rename bouquets """
|
||||
bq_selected = self.is_bouquet_selected()
|
||||
if not bq_selected:
|
||||
show_dialog(DialogType.ERROR, self.__main_window, "This item is not allowed to edit!")
|
||||
return
|
||||
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
|
||||
if paths:
|
||||
itr = model.get_iter(paths[0])
|
||||
bq_name, bq_type = model.get(itr, 0, 3)
|
||||
response = show_dialog(DialogType.INPUT, self.__main_window, bq_name)
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
model.set_value(itr, 0, response)
|
||||
self.__bouquets["{}:{}".format(response, bq_type)] = self.__bouquets.pop("{}:{}".format(bq_name, bq_type))
|
||||
|
||||
def on_to_fav_move(self, view):
|
||||
""" Move items from app to fav list """
|
||||
selection = self.get_selection(view)
|
||||
|
||||
if selection:
|
||||
self.receive_selection(view=self.__fav_view, drop_info=None, data=selection)
|
||||
|
||||
def get_selection(self, view):
|
||||
""" Creates a string from the iterators of the selected rows """
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if model.get_model(): # needs think about it !
|
||||
model = model.get_model().get_model()
|
||||
|
||||
if len(paths) > 0:
|
||||
itrs = [model.get_iter(path) for path in paths]
|
||||
return "{}:{}".format(",".join([model.get_string_from_iter(itr) for itr in itrs]), model.get_name())
|
||||
|
||||
def receive_selection(self, *, view, drop_info, data):
|
||||
""" Update fav view after data received """
|
||||
bq_selected = self.is_bouquet_selected()
|
||||
|
||||
if not bq_selected:
|
||||
show_dialog(DialogType.ERROR, self.__main_window, "Error. No bouquet is selected!")
|
||||
return
|
||||
|
||||
model = get_base_model(view.get_model())
|
||||
dest_index = 0
|
||||
|
||||
if drop_info:
|
||||
path, position = drop_info
|
||||
dest_iter = model.get_iter(path)
|
||||
if dest_iter:
|
||||
dest_index = model.get_value(dest_iter, 0)
|
||||
|
||||
itr_str, sep, source = data.partition(":")
|
||||
itrs = itr_str.split(",")
|
||||
|
||||
try:
|
||||
fav_bouquet = self.__bouquets[bq_selected]
|
||||
|
||||
if source == self._SERVICE_LIST_NAME:
|
||||
ext_model = self.__services_view.get_model()
|
||||
ext_itrs = [ext_model.get_iter_from_string(itr) for itr in itrs]
|
||||
ext_rows = [ext_model[ext_itr][:] for ext_itr in ext_itrs]
|
||||
dest_index -= 1
|
||||
for ext_row in ext_rows:
|
||||
dest_index += 1
|
||||
fav_id = ext_row[-2]
|
||||
channel = self.__services[fav_id]
|
||||
model.insert(dest_index, (0, channel.coded, channel.service, channel.locked, channel.hide,
|
||||
channel.service_type, channel.pos, channel.fav_id))
|
||||
fav_bouquet.insert(dest_index, channel.fav_id)
|
||||
elif source == self._FAV_LIST_NAME:
|
||||
in_itrs = [model.get_iter_from_string(itr) for itr in itrs]
|
||||
in_rows = [model[in_itr][:] for in_itr in in_itrs]
|
||||
for row in in_rows:
|
||||
model.insert(dest_index, row)
|
||||
fav_bouquet.insert(dest_index, row[4])
|
||||
for in_itr in in_itrs:
|
||||
del fav_bouquet[int(model.get_path(in_itr)[0])]
|
||||
model.remove(in_itr)
|
||||
self.update_fav_num_column(model)
|
||||
except ValueError as e:
|
||||
self.__status_bar.push(1, getattr(e, "message", repr(e)))
|
||||
|
||||
def update_fav_num_column(self, model):
|
||||
""" Iterate through model and updates values for Num column """
|
||||
model.foreach(lambda store, pth, itr: store.set_value(itr, 0, int(pth[0]) + 1)) # iter , column, value
|
||||
|
||||
def update_bouquet_list(self):
|
||||
""" Update bouquet after move items """
|
||||
bq_selected = self.is_bouquet_selected()
|
||||
if bq_selected:
|
||||
fav_bouquet = self.__bouquets[bq_selected]
|
||||
fav_bouquet.clear()
|
||||
for row in self.__fav_model:
|
||||
fav_bouquet.append(row[7])
|
||||
|
||||
def on_services_tree_view_drag_data_get(self, view, drag_context, data, info, time):
|
||||
""" DnD """
|
||||
data.set_text(self.get_selection(view), -1)
|
||||
|
||||
def on_fav_tree_view_drag_data_get(self, view, drag_context, data, info, time):
|
||||
""" DnD """
|
||||
data.set_text(self.get_selection(view), -1)
|
||||
|
||||
def on_fav_tree_view_drag_data_received(self, view, drag_context, x, y, data, info, time):
|
||||
""" DnD """
|
||||
self.receive_selection(view=view, drop_info=view.get_dest_row_at_pos(x, y), data=data.get_text())
|
||||
|
||||
def on_view_popup_menu(self, menu, event):
|
||||
""" Shows popup menu for any view """
|
||||
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
|
||||
menu.popup(None, None, None, None, event.button, event.time)
|
||||
|
||||
@run_idle
|
||||
def on_satellite_editor_show(self, model):
|
||||
""" Shows satellites editor dialog """
|
||||
show_satellites_dialog(self.__main_window, self.__options.get(self.__profile))
|
||||
|
||||
def on_data_open(self, model):
|
||||
response = show_dialog(DialogType.CHOOSER, self.__main_window, options=self.__options.get(self.__profile))
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
self.open_data(response)
|
||||
|
||||
@run_idle
|
||||
def open_data(self, data_path=None):
|
||||
""" Opening data and fill views. """
|
||||
self.clear_current_data()
|
||||
|
||||
data_path = self.__options.get(self.__profile).get("data_dir_path") if data_path is None else data_path
|
||||
try:
|
||||
self.append_blacklist(data_path)
|
||||
self.append_bouquets(data_path)
|
||||
self.append_services(data_path)
|
||||
self.update_services_counts(len(self.__services_model))
|
||||
except FileNotFoundError as e:
|
||||
show_dialog(DialogType.ERROR, self.__main_window, getattr(e, "message", str(e)) +
|
||||
"\n\nPlease, download files from receiver or setup your path for read data!")
|
||||
except SyntaxError as e:
|
||||
show_dialog(DialogType.ERROR, self.__main_window, str(e))
|
||||
|
||||
def append_blacklist(self, data_path):
|
||||
black_list = get_blacklist(data_path)
|
||||
if black_list:
|
||||
self.__blacklist.update(black_list)
|
||||
|
||||
def append_bouquets(self, data_path):
|
||||
for bouquet in get_bouquets(data_path, Profile(self.__profile)):
|
||||
parent = self.__bouquets_model.append(None, [bouquet.name, None, None, bouquet.type])
|
||||
for bt in bouquet.bouquets:
|
||||
name, bt_type, locked, hidden = bt.name, bt.type, bt.locked, bt.hidden
|
||||
self.__bouquets_model.append(parent, [name, locked, hidden, bt_type])
|
||||
services = []
|
||||
agr = [None] * 7
|
||||
for srv in bt.services:
|
||||
fav_id = srv.data
|
||||
# IPTV and MARKER services
|
||||
s_type = srv.type
|
||||
if s_type is BqServiceType.MARKER or s_type is BqServiceType.IPTV:
|
||||
self.__services[fav_id] = Service(*agr[0:3], srv.name, *agr[0:3],
|
||||
s_type.name, *agr, srv.num, fav_id, None)
|
||||
services.append(fav_id)
|
||||
self.__bouquets["{}:{}".format(name, bt_type)] = services
|
||||
|
||||
def append_services(self, data_path):
|
||||
try:
|
||||
services = get_services(data_path, Profile(self.__profile))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
log("Append services error: " + str(e))
|
||||
show_dialog(DialogType.ERROR, self.__main_window, "Error opening data!")
|
||||
else:
|
||||
if services:
|
||||
for srv in services:
|
||||
# adding channels to dict with fav_id as keys
|
||||
self.__services[srv.fav_id] = srv
|
||||
self.__services_model.append(srv)
|
||||
|
||||
def clear_current_data(self):
|
||||
""" Clearing current data from lists """
|
||||
self.__bouquets_model.clear()
|
||||
self.__fav_model.clear()
|
||||
self.__services_model.clear()
|
||||
self.__blacklist.clear()
|
||||
self.__services.clear()
|
||||
self.__rows_buffer.clear()
|
||||
self.__bouquets.clear()
|
||||
|
||||
def on_data_save(self, *args):
|
||||
if show_dialog(DialogType.QUESTION, self.__main_window) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
path = self.__options.get(self.__profile).get("data_dir_path")
|
||||
# deleting files in data dir(skipping dirs) :)
|
||||
list(map(os.unlink, (os.path.join(path, f) for f in filter(
|
||||
lambda f: f != "satellites.xml" and os.path.isfile(os.path.join(path, f)), os.listdir(path)))))
|
||||
|
||||
bouquets = []
|
||||
services_model = self.__services_view.get_model()
|
||||
|
||||
def parse_bouquets(model, b_path, itr):
|
||||
bqs = None
|
||||
if model.iter_has_child(itr):
|
||||
bqs = []
|
||||
num_of_children = model.iter_n_children(itr)
|
||||
for num in range(num_of_children):
|
||||
bq_itr = model.iter_nth_child(itr, num)
|
||||
bq_name, locked, hidden, bq_type = model.get(bq_itr, 0, 1, 2, 3)
|
||||
favs = self.__bouquets["{}:{}".format(bq_name, bq_type)]
|
||||
bq = Bouquet(bq_name, bq_type, [self.__services.get(f_id, None) for f_id in favs], locked, hidden)
|
||||
bqs.append(bq)
|
||||
if len(b_path) == 1:
|
||||
bouquets.append(Bouquets(*model.get(itr, 0, 3), bqs if bqs else []))
|
||||
|
||||
profile = Profile(self.__profile)
|
||||
# Getting bouquets
|
||||
self.__bouquets_view.get_model().foreach(parse_bouquets)
|
||||
write_bouquets(path, bouquets, profile)
|
||||
# Getting services
|
||||
services = [Service(*row[:]) for row in services_model]
|
||||
write_services(path, services, profile)
|
||||
# removing bouquet files
|
||||
if profile is profile.ENIGMA_2:
|
||||
# blacklist
|
||||
write_blacklist(path, self.__blacklist)
|
||||
|
||||
def on_services_selection(self, model, path, column):
|
||||
self.delete_selection(self.__fav_view)
|
||||
self.update_service_bar(model, path)
|
||||
|
||||
def update_service_bar(self, model, path):
|
||||
def_val = "Unknown"
|
||||
cas = model.get_value(model.get_iter(path), 0)
|
||||
if not cas:
|
||||
return
|
||||
cas_values = list(filter(lambda val: val.startswith("C:"), cas.split(",")))
|
||||
self.__cas_label.set_text(",".join(map(str, sorted(set(CAS.get(val, def_val) for val in cas_values)))))
|
||||
|
||||
def on_fav_selection(self, model, path, column):
|
||||
self.delete_selection(self.__services_view)
|
||||
|
||||
def on_bouquets_selection(self, model, path, column):
|
||||
self.__fav_model.clear()
|
||||
|
||||
if self.__bouquets_view.row_expanded(path):
|
||||
self.__bouquets_view.collapse_row(path)
|
||||
else:
|
||||
self.__bouquets_view.expand_row(path, column)
|
||||
|
||||
if len(path) > 1:
|
||||
self.delete_selection(self.__services_view)
|
||||
self.update_bouquet_channels(model, path)
|
||||
|
||||
def update_bouquet_channels(self, model, path, bq_key=None):
|
||||
""" Updates list of bouquet channels """
|
||||
tree_iter = None
|
||||
if path:
|
||||
tree_iter = model.get_iter(path)
|
||||
|
||||
key = bq_key if bq_key else "{}:{}".format(*model.get(tree_iter, 0, 3))
|
||||
services = self.__bouquets[key]
|
||||
|
||||
for num, ch_id in enumerate(services):
|
||||
channel = self.__services.get(ch_id, None)
|
||||
if channel:
|
||||
self.__fav_model.append((num + 1, channel.coded, channel.service, channel.locked,
|
||||
channel.hide, channel.service_type, channel.pos, channel.fav_id))
|
||||
|
||||
def is_bouquet_selected(self):
|
||||
""" Checks whether the bouquet is selected
|
||||
|
||||
returns 'name:type' of selected bouquet or False
|
||||
"""
|
||||
model, path = self.__bouquets_view.get_selection().get_selected_rows()
|
||||
|
||||
if not path or len(path[0]) < 2:
|
||||
return False
|
||||
|
||||
return "{}:{}".format(*model.get(model.get_iter(path), 0, 3))
|
||||
|
||||
@run_idle
|
||||
def delete_selection(self, view, *args):
|
||||
""" Used for clear selection on given view(s) """
|
||||
for v in [view, *args]:
|
||||
v.get_selection().unselect_all()
|
||||
|
||||
@run_idle
|
||||
def on_preferences(self, item):
|
||||
response = show_settings_dialog(self.__main_window, self.__options)
|
||||
if response != Gtk.ResponseType.CANCEL:
|
||||
profile = self.__options.get("profile")
|
||||
if profile != self.__profile:
|
||||
self.__status_bar.push(0, "Current IP: " + self.__options.get(profile).get("host"))
|
||||
self.__profile_label.set_text("Enigma 2 v.4" if Profile(profile) is Profile.ENIGMA_2 else "Neutrino-MP")
|
||||
self.__profile = profile
|
||||
self.clear_current_data()
|
||||
self.update_services_counts()
|
||||
|
||||
def on_tree_view_key_release(self, view, event):
|
||||
""" Handling keystrokes """
|
||||
key = event.keyval
|
||||
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
|
||||
alt = event.state & Gdk.ModifierType.MOD1_MASK
|
||||
model = get_base_model(view.get_model())
|
||||
model_name = model.get_name()
|
||||
|
||||
if key == Gdk.KEY_Delete:
|
||||
self.on_delete(view)
|
||||
elif ctrl and key in (Gdk.KEY_Up, Gdk.KEY_Page_Up, Gdk.KEY_KP_Page_Up): # KEY_KP_Page_Up for laptop!
|
||||
self.move_items(key)
|
||||
elif ctrl and key in (Gdk.KEY_Down, Gdk.KEY_Page_Down, Gdk.KEY_KP_Page_Down):
|
||||
self.move_items(key)
|
||||
elif model_name == self._FAV_LIST_NAME and key == Gdk.KEY_Control_L or key == Gdk.KEY_Control_R:
|
||||
self.update_fav_num_column(model)
|
||||
self.update_bouquet_list()
|
||||
elif key == Gdk.KEY_Insert:
|
||||
# Move items from app to fav list
|
||||
if model_name == self._SERVICE_LIST_NAME:
|
||||
self.on_to_fav_move(view)
|
||||
elif model_name == self._BOUQUETS_LIST_NAME:
|
||||
self.on_new_bouquet(view)
|
||||
elif ctrl and (key == Gdk.KEY_c or key == Gdk.KEY_C) and model_name == self._SERVICE_LIST_NAME:
|
||||
self.on_copy(view)
|
||||
elif ctrl and key == Gdk.KEY_x or key == Gdk.KEY_X:
|
||||
if model_name == self._FAV_LIST_NAME:
|
||||
self.on_cut(view)
|
||||
elif ctrl and key == Gdk.KEY_v or key == Gdk.KEY_V:
|
||||
self.on_paste(view)
|
||||
elif ctrl and key == Gdk.KEY_s or key == Gdk.KEY_S:
|
||||
self.on_data_save()
|
||||
elif ctrl and key == Gdk.KEY_l or key == Gdk.KEY_L:
|
||||
self.on_locked(None)
|
||||
elif ctrl and key == Gdk.KEY_h or key == Gdk.KEY_H:
|
||||
self.on_hide(None)
|
||||
elif ctrl and key == Gdk.KEY_E or key == Gdk.KEY_e or key == Gdk.KEY_F2:
|
||||
self.on_edit(view)
|
||||
elif key == Gdk.KEY_space and model_name == self._FAV_LIST_NAME:
|
||||
pass
|
||||
elif key == Gdk.KEY_Left or key == Gdk.KEY_Right:
|
||||
view.do_unselect_all(view)
|
||||
|
||||
def on_download(self, item):
|
||||
show_download_dialog(transient=self.__main_window,
|
||||
options=self.__options.get(self.__profile),
|
||||
open_data=self.open_data,
|
||||
profile=Profile(self.__profile))
|
||||
|
||||
@run_idle
|
||||
def on_view_focus(self, view, focus_event):
|
||||
profile = Profile(self.__profile)
|
||||
model = get_base_model(view.get_model())
|
||||
model_name = model.get_name()
|
||||
not_empty = len(model) > 0 # if > 0 model has items
|
||||
|
||||
if model_name == self._BOUQUETS_LIST_NAME:
|
||||
for elem in self.__tool_elements:
|
||||
self.__tool_elements[elem].set_sensitive(False)
|
||||
for elem in self._BOUQUET_ELEMENTS:
|
||||
self.__tool_elements[elem].set_sensitive(not_empty)
|
||||
if profile is Profile.NEUTRINO_MP:
|
||||
for elem in self._LOCK_HIDE_ELEMENTS:
|
||||
self.__tool_elements[elem].set_sensitive(not_empty)
|
||||
else:
|
||||
is_service = model_name == self._SERVICE_LIST_NAME
|
||||
for elem in self._FAV_ELEMENTS:
|
||||
if elem in ("paste_tool_button", "paste_menu_item", "fav_paste_popup_item"):
|
||||
self.__tool_elements[elem].set_sensitive(not is_service and self.__rows_buffer)
|
||||
elif elem in self._FAV_ONLY_ELEMENTS:
|
||||
if profile is Profile.ENIGMA_2:
|
||||
self.__tool_elements[elem].set_sensitive(self.is_bouquet_selected() and not is_service)
|
||||
else:
|
||||
self.__tool_elements[elem].set_sensitive(not_empty and not is_service)
|
||||
for elem in self._SERVICE_ELEMENTS:
|
||||
self.__tool_elements[elem].set_sensitive(not_empty and is_service)
|
||||
for elem in self._BOUQUET_ELEMENTS:
|
||||
self.__tool_elements[elem].set_sensitive(False)
|
||||
for elem in self._LOCK_HIDE_ELEMENTS:
|
||||
self.__tool_elements[elem].set_sensitive(not_empty and profile is Profile.ENIGMA_2)
|
||||
|
||||
for elem in self._COMMONS_ELEMENTS:
|
||||
self.__tool_elements[elem].set_sensitive(not_empty)
|
||||
|
||||
def on_hide(self, item):
|
||||
self.set_service_flags(FLAG.HIDE)
|
||||
|
||||
def on_locked(self, item):
|
||||
self.set_service_flags(FLAG.LOCK)
|
||||
|
||||
def set_service_flags(self, flag):
|
||||
profile = Profile(self.__profile)
|
||||
bq_selected = self.is_bouquet_selected()
|
||||
if profile is Profile.ENIGMA_2:
|
||||
if set_flags(flag, self.__services_view, self.__fav_view, self.__services, self.__blacklist):
|
||||
if bq_selected:
|
||||
self.__fav_model.clear()
|
||||
self.update_bouquet_channels(self.__fav_model, None, bq_selected)
|
||||
elif profile is Profile.NEUTRINO_MP:
|
||||
if bq_selected:
|
||||
model, path = self.__bouquets_view.get_selection().get_selected()
|
||||
value = model.get_value(path, 1 if flag is FLAG.LOCK else 2)
|
||||
value = None if value else LOCKED_ICON if flag is FLAG.LOCK else HIDE_ICON
|
||||
model.set_value(path, 1 if flag is FLAG.LOCK else 2, value)
|
||||
|
||||
@run_idle
|
||||
def on_model_changed(self, model, path, itr=None):
|
||||
model_name = model.get_name()
|
||||
|
||||
if model_name == self._FAV_LIST_NAME:
|
||||
self.__fav_count_label.set_text(str(len(model)))
|
||||
elif model_name == self._SERVICE_LIST_NAME:
|
||||
self.update_services_counts(len(model))
|
||||
elif model_name == self._BOUQUETS_LIST_NAME:
|
||||
self.__bouquets_count_label.set_text(str(len(self.__bouquets.keys())))
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def update_services_counts(self, size=0):
|
||||
""" Updates counters for services. May be temporary! """
|
||||
tv_count = 0
|
||||
radio_count = 0
|
||||
data_count = 0
|
||||
|
||||
for ch in self.__services.values():
|
||||
ch_type = ch.service_type
|
||||
if ch_type in ("TV", "TV (HD)"):
|
||||
tv_count += 1
|
||||
elif ch_type == "Radio":
|
||||
radio_count += 1
|
||||
elif ch_type == "Data":
|
||||
data_count += 1
|
||||
|
||||
self.__tv_count_label.set_text(str(tv_count))
|
||||
self.__radio_count_label.set_text(str(radio_count))
|
||||
self.__data_count_label.set_text(str(data_count))
|
||||
|
||||
def on_import_m3u(self, item):
|
||||
""" Imports iptv from m3u files. """
|
||||
file_filter = Gtk.FileFilter()
|
||||
file_filter.add_pattern("*.m3u")
|
||||
file_filter.set_name("m3u files")
|
||||
response = show_dialog(dialog_type=DialogType.CHOOSER,
|
||||
transient=self.__main_window,
|
||||
options=self.__options.get(self.__profile),
|
||||
action_type=Gtk.FileChooserAction.OPEN,
|
||||
file_filter=file_filter)
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
if not str(response).endswith("m3u"):
|
||||
show_dialog(DialogType.ERROR, self.__main_window, text="No m3u file is selected!")
|
||||
return
|
||||
|
||||
channels = parse_m3u(response)
|
||||
bq_selected = self.is_bouquet_selected()
|
||||
if channels and bq_selected:
|
||||
bq_services = self.__bouquets.get(bq_selected)
|
||||
self.__fav_model.clear()
|
||||
for ch in channels:
|
||||
self.__services[ch.fav_id] = ch
|
||||
bq_services.append(ch.fav_id)
|
||||
self.update_bouquet_channels(self.__fav_model, None, bq_selected)
|
||||
|
||||
def on_insert_marker(self, view):
|
||||
""" Inserts marker into bouquet services list. """
|
||||
insert_marker(view, self.__bouquets, self.is_bouquet_selected(), self.__services, self.__main_window)
|
||||
self.update_fav_num_column(self.__fav_model)
|
||||
|
||||
def on_edit_marker(self, view):
|
||||
edit_marker(view, self.__bouquets, self.is_bouquet_selected(), self.__services, self.__main_window)
|
||||
|
||||
@run_idle
|
||||
def on_fav_popup(self, view, event):
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
self.__fav_edit_marker_popup_item.set_sensitive(
|
||||
len(paths) == 1 and model.get_value(model.get_iter(paths[0]), 5) == BqServiceType.MARKER.name)
|
||||
|
||||
def on_locate_in_services(self, view):
|
||||
locate_in_services(view, self.__services_view, self.__main_window)
|
||||
|
||||
def on_picons_loader_show(self, item):
|
||||
dialog = PiconsDialog(self.__main_window, self.__options.get(self.__profile), Profile(self.__profile))
|
||||
dialog.show()
|
||||
|
||||
@run_idle
|
||||
def on_filter_changed(self, entry):
|
||||
self.__services_model_filter.refilter()
|
||||
|
||||
def services_filter_function(self, model, iter, data):
|
||||
if self.__services_model_filter is None or self.__services_model_filter == "None":
|
||||
return True
|
||||
else:
|
||||
return self.__filter_entry.get_text() in str(model.get(iter, 3, 6, 7, 8, 9, 10, 11, 12, 13, 14))
|
||||
|
||||
|
||||
def start_app():
|
||||
MainAppWindow()
|
||||
Gtk.main()
|
||||
|
||||
|
||||
def close_app():
|
||||
Gtk.main_quit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@@ -1,148 +1,243 @@
|
||||
""" This is helper module for ui """
|
||||
from enum import Enum
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Helper module for the GUI. """
|
||||
|
||||
__all__ = ("insert_marker", "move_items", "rename", "ViewTarget", "set_flags", "locate_in_services",
|
||||
"scroll_to", "get_base_model", "copy_picon_reference", "assign_picons", "remove_picon",
|
||||
"is_only_one_item_selected", "gen_bouquets", "BqGenType", "get_selection",
|
||||
"get_model_data", "remove_all_unused_picons", "get_picon_pixbuf", "get_base_itrs", "get_iptv_url",
|
||||
"update_entry_data", "append_text_to_tview", "on_popup_menu")
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from urllib.parse import unquote
|
||||
|
||||
from gi.repository import GdkPixbuf, GLib
|
||||
|
||||
from app.eparser import Service
|
||||
from app.eparser.ecommons import FLAG
|
||||
from app.eparser.ecommons import Flag, BouquetService, Bouquet, BqType
|
||||
from app.eparser.enigma.bouquets import BqServiceType, to_bouquet_id
|
||||
from . import Gtk, Gdk, HIDE_ICON, LOCKED_ICON
|
||||
from .dialogs import show_dialog, DialogType
|
||||
|
||||
|
||||
class ViewTarget(Enum):
|
||||
""" Used for set target view """
|
||||
BOUQUET = 0
|
||||
FAV = 1
|
||||
SERVICES = 2
|
||||
from app.settings import SettingsType, SEP, IS_WIN, IS_DARWIN, IS_LINUX
|
||||
from .dialogs import show_dialog, DialogType, get_message
|
||||
from .uicommons import ViewTarget, BqGenType, Gtk, Gdk, HIDE_ICON, LOCKED_ICON, KeyboardKey, Column
|
||||
|
||||
|
||||
# ***************** Markers *******************#
|
||||
|
||||
def insert_marker(view, bouquets, selected_bouquet, channels, parent_window):
|
||||
def insert_marker(view, bouquets, selected_bouquet, services, parent_window, m_type=BqServiceType.MARKER):
|
||||
"""" Inserts marker into bouquet services list. """
|
||||
response = show_dialog(DialogType.INPUT, parent_window)
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
fav_id, text = "1:832:D:0:0:0:0:0:0:0:\n", None
|
||||
|
||||
if not response.strip():
|
||||
show_dialog(DialogType.ERROR, parent_window, "The text of marker is empty, please try again!")
|
||||
return
|
||||
if m_type is BqServiceType.MARKER:
|
||||
response = show_dialog(DialogType.INPUT, parent_window)
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
# Searching for max num value in all marker services (if empty default = 0)
|
||||
max_num = max(map(lambda num: int(num.data_id, 16),
|
||||
filter(lambda ch: ch.service_type == BqServiceType.MARKER.name, channels.values())), default=0)
|
||||
max_num = '{:x}'.format(max_num + 1)
|
||||
fav_id = "1:64:{}:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n".format(max_num, response, response)
|
||||
s_type = BqServiceType.MARKER.name
|
||||
if not response.strip():
|
||||
show_dialog(DialogType.ERROR, parent_window, "The text of marker is empty, please try again!")
|
||||
return
|
||||
|
||||
fav_id = f"1:64:0:0:0:0:0:0:0:0::{response}\n#DESCRIPTION {response}\n"
|
||||
text = response
|
||||
|
||||
s_type = m_type.name
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
itr = model.insert_before(model.get_iter(paths[0]), (None, None, response, None, None, s_type, None, fav_id))
|
||||
channels[fav_id] = Service(None, None, None, response, None, None, None, s_type, *[None] * 7, max_num, fav_id, 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)
|
||||
bouquets[selected_bouquet].insert(model.get_path(itr)[0], fav_id)
|
||||
|
||||
|
||||
def edit_marker(view, bouquets, selected_bouquet, channels, parent_window):
|
||||
""" Edits marker text """
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
itr = model.get_iter(paths[0])
|
||||
name, fav_id = model.get(itr, 2, 7)
|
||||
response = show_dialog(DialogType.INPUT, parent_window, text=name)
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
bq_services = bouquets[selected_bouquet]
|
||||
index = bq_services.index(fav_id)
|
||||
old_ch = channels.pop(fav_id, None)
|
||||
new_fav_id = "{}::{}\n#DESCRIPTION {}\n".format(fav_id.split("::")[0], response, response)
|
||||
model.set(itr, {2: response, 7: new_fav_id})
|
||||
channels[new_fav_id] = Service(*old_ch[0:3], response, *old_ch[4:15], old_ch.data_id, new_fav_id, None)
|
||||
bq_services.pop(index)
|
||||
bq_services.insert(index, new_fav_id)
|
||||
services[fav_id] = Service(None, None, None, text, None, None, None, s_type, *[None] * 9, 0, fav_id, None)
|
||||
|
||||
|
||||
# ***************** Movement *******************#
|
||||
|
||||
def move_items(key, view):
|
||||
""" Move items in tree view """
|
||||
def move_items(key, view: Gtk.TreeView):
|
||||
""" Move items in the tree view """
|
||||
selection = view.get_selection()
|
||||
model, paths = selection.get_selected_rows()
|
||||
if not paths:
|
||||
return
|
||||
|
||||
if paths:
|
||||
# for correct down move!
|
||||
if key in (Gdk.KEY_Down, Gdk.KEY_Page_Down, Gdk.KEY_KP_Page_Down):
|
||||
paths = reversed(paths)
|
||||
is_tree_store = type(model) is Gtk.TreeStore
|
||||
mod_length = len(model)
|
||||
if not is_tree_store and mod_length == len(paths):
|
||||
return
|
||||
|
||||
for path in paths:
|
||||
itr = model.get_iter(path)
|
||||
if key == Gdk.KEY_Down:
|
||||
next_itr = model.iter_next(itr)
|
||||
if next_itr:
|
||||
model.move_after(itr, next_itr)
|
||||
elif key == Gdk.KEY_Up:
|
||||
prev_itr = model.iter_previous(itr)
|
||||
if prev_itr:
|
||||
model.move_before(itr, prev_itr)
|
||||
elif key == Gdk.KEY_Page_Up or key == Gdk.KEY_KP_Page_Up:
|
||||
up_itr = model.get_iter(view.get_cursor()[0])
|
||||
if up_itr:
|
||||
model.move_before(itr, up_itr)
|
||||
elif key == Gdk.KEY_Page_Down or key == Gdk.KEY_KP_Page_Down:
|
||||
down_itr = model.get_iter(view.get_cursor()[0])
|
||||
if down_itr:
|
||||
model.move_after(itr, down_itr)
|
||||
cursor_path = view.get_cursor()[0]
|
||||
max_path = Gtk.TreePath.new_from_indices((mod_length,))
|
||||
min_path = Gtk.TreePath.new_from_indices((0,))
|
||||
|
||||
if is_tree_store:
|
||||
is_tree_store = False
|
||||
parent_paths = list(filter(lambda p: p.get_depth() == 1, paths))
|
||||
if parent_paths:
|
||||
paths = parent_paths
|
||||
min_path = model.get_path(model.get_iter_first())
|
||||
view.collapse_all()
|
||||
if mod_length == len(paths):
|
||||
return
|
||||
else:
|
||||
if not is_some_level(paths):
|
||||
return
|
||||
parent_itr = model.iter_parent(model.get_iter(paths[0]))
|
||||
parent_index = model.get_path(parent_itr)
|
||||
children_num = model.iter_n_children(parent_itr)
|
||||
if key in (KeyboardKey.PAGE_DOWN, KeyboardKey.END, KeyboardKey.END_KP, KeyboardKey.PAGE_DOWN_KP):
|
||||
children_num -= 1
|
||||
min_path = Gtk.TreePath.new_from_string(f"{parent_index}:{0}")
|
||||
max_path = Gtk.TreePath.new_from_string(f"{parent_index}:{children_num}")
|
||||
is_tree_store = True
|
||||
|
||||
if key is KeyboardKey.UP:
|
||||
top_path = Gtk.TreePath(paths[0])
|
||||
set_cursor(top_path, paths, selection, view)
|
||||
top_path.prev()
|
||||
move_up(top_path, model, paths)
|
||||
elif key is KeyboardKey.DOWN:
|
||||
down_path = Gtk.TreePath(paths[-1])
|
||||
set_cursor(down_path, paths, selection, view)
|
||||
down_path.next()
|
||||
if down_path < max_path:
|
||||
move_down(down_path, model, paths)
|
||||
else:
|
||||
max_path.prev()
|
||||
move_down(max_path, model, paths)
|
||||
elif key in (KeyboardKey.PAGE_UP, KeyboardKey.HOME, KeyboardKey.PAGE_UP_KP, KeyboardKey.HOME_KP):
|
||||
move_up(min_path if is_tree_store else cursor_path, model, paths)
|
||||
elif key in (KeyboardKey.PAGE_DOWN, KeyboardKey.END, KeyboardKey.END_KP, KeyboardKey.PAGE_DOWN_KP):
|
||||
move_down(max_path if is_tree_store else cursor_path, model, paths)
|
||||
|
||||
|
||||
# ***************** Edit *******************#
|
||||
def move_up(top_path, model, paths):
|
||||
top_iter = model.get_iter(top_path)
|
||||
for path in paths:
|
||||
itr = model.get_iter(path)
|
||||
model.move_before(itr, top_iter)
|
||||
top_path.next()
|
||||
top_iter = model.get_iter(top_path)
|
||||
|
||||
def edit(view, parent_window, target, fav_view=None, service_view=None, channels=None):
|
||||
|
||||
def move_down(down_path, model, paths):
|
||||
top_iter = model.get_iter(down_path)
|
||||
for path in reversed(paths):
|
||||
itr = model.get_iter(path)
|
||||
model.move_after(itr, top_iter)
|
||||
down_path.prev()
|
||||
top_iter = model.get_iter(down_path)
|
||||
|
||||
|
||||
def is_some_level(paths):
|
||||
for i in range(1, len(paths)):
|
||||
prev = paths[i - 1]
|
||||
current = paths[i]
|
||||
if len(prev) != len(current) or (len(prev) == 2 and len(current) == 2 and prev[0] != current[0]):
|
||||
return
|
||||
return True
|
||||
|
||||
|
||||
def set_cursor(dest_path, paths, selection, view):
|
||||
view.set_cursor(dest_path, view.get_column(0), False)
|
||||
for p in paths:
|
||||
selection.select_path(p)
|
||||
|
||||
|
||||
# ***************** Rename *******************#
|
||||
|
||||
def rename(view, parent_window, target, fav_view=None, service_view=None, services=None):
|
||||
selection = get_selection(view, parent_window)
|
||||
if not selection:
|
||||
return
|
||||
|
||||
model, paths = selection
|
||||
itr = model.get_iter(paths)
|
||||
f_id, srv_name, srv_type = None, None, None
|
||||
|
||||
if target is ViewTarget.SERVICES:
|
||||
name, fav_id = model.get(itr, Column.SRV_SERVICE, Column.SRV_FAV_ID)
|
||||
f_id = fav_id
|
||||
response = show_dialog(DialogType.INPUT, parent_window, name)
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
srv_name = response
|
||||
model.set_value(itr, Column.SRV_SERVICE, response)
|
||||
if fav_view is not None:
|
||||
for row in fav_view.get_model():
|
||||
if row[Column.FAV_ID] == fav_id:
|
||||
row[Column.FAV_SERVICE] = response
|
||||
break
|
||||
elif target is ViewTarget.FAV:
|
||||
name, srv_type, fav_id = model.get(itr, Column.FAV_SERVICE, Column.FAV_TYPE, Column.FAV_ID)
|
||||
f_id = fav_id
|
||||
response = show_dialog(DialogType.INPUT, parent_window, name)
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
srv_name = response
|
||||
if not model.get_value(itr, Column.FAV_BACKGROUND):
|
||||
model.set_value(itr, Column.FAV_SERVICE, response)
|
||||
|
||||
if service_view is not None:
|
||||
for row in get_base_model(service_view.get_model()):
|
||||
if row[Column.SRV_FAV_ID] == fav_id:
|
||||
row[Column.SRV_SERVICE] = response
|
||||
break
|
||||
|
||||
old_srv = services.get(f_id, None)
|
||||
if old_srv:
|
||||
if srv_type == BqServiceType.IPTV.name or srv_type == BqServiceType.MARKER.name:
|
||||
l, sep, r = f_id.partition("#DESCRIPTION")
|
||||
old_name = old_srv.service.strip()
|
||||
new_name = srv_name.strip()
|
||||
new_fav_id = "".join((new_name.join(l.rsplit(old_name, 1)), sep, new_name.join(r.rsplit(old_name, 1))))
|
||||
services[f_id] = old_srv._replace(service=srv_name, fav_id=new_fav_id)
|
||||
else:
|
||||
services[f_id] = old_srv._replace(service=srv_name)
|
||||
|
||||
|
||||
def get_selection(view, parent):
|
||||
""" Returns (model, paths) if possible """
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
model = get_base_model(model)
|
||||
|
||||
if not paths:
|
||||
return
|
||||
elif len(paths) > 1:
|
||||
show_dialog(DialogType.ERROR, parent_window, "Please, select only one item!")
|
||||
show_dialog(DialogType.ERROR, parent, "Please, select only one item!")
|
||||
return
|
||||
|
||||
itr = model.get_iter(paths)
|
||||
f_id = None
|
||||
channel_name = None
|
||||
|
||||
if target is ViewTarget.SERVICES:
|
||||
name, fav_id = model.get(itr, 3, 16)
|
||||
f_id = fav_id
|
||||
response = show_dialog(DialogType.INPUT, parent_window, name)
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
channel_name = response
|
||||
model.set_value(itr, 3, response)
|
||||
if fav_view is not None:
|
||||
for row in fav_view.get_model():
|
||||
if row[7] == fav_id:
|
||||
row[2] = response
|
||||
break
|
||||
elif target is ViewTarget.FAV:
|
||||
name, fav_id = model.get(itr, 2, 7)
|
||||
f_id = fav_id
|
||||
response = show_dialog(DialogType.INPUT, parent_window, name)
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
channel_name = response
|
||||
model.set_value(itr, 2, response)
|
||||
|
||||
if service_view is not None:
|
||||
for row in service_view.get_model():
|
||||
if row[16] == fav_id:
|
||||
row[3] = response
|
||||
break
|
||||
|
||||
old_ch = channels.get(f_id, None)
|
||||
if old_ch:
|
||||
channels[f_id] = Service(*old_ch[0:3], channel_name, *old_ch[4:])
|
||||
return model, paths
|
||||
|
||||
|
||||
# ***************** Flags *******************#
|
||||
|
||||
def set_flags(flag, services_view, fav_view, channels, blacklist):
|
||||
def set_flags(flag, services_view, fav_view, services, blacklist):
|
||||
""" Updates flags for services. Returns True if any was changed. """
|
||||
target = ViewTarget.SERVICES if services_view.is_focus() else ViewTarget.FAV if fav_view.is_focus() else None
|
||||
if not target:
|
||||
@@ -158,47 +253,64 @@ def set_flags(flag, services_view, fav_view, channels, blacklist):
|
||||
if not paths:
|
||||
return
|
||||
|
||||
paths = get_base_paths(paths, model)
|
||||
model = get_base_model(model)
|
||||
|
||||
if flag is FLAG.HIDE:
|
||||
if flag is Flag.HIDE:
|
||||
if target is ViewTarget.SERVICES:
|
||||
set_hide(channels, model, paths)
|
||||
set_hide(services, model, paths)
|
||||
else:
|
||||
fav_ids = [model.get_value(model.get_iter(path), 7) for path in paths]
|
||||
fav_ids = [model.get_value(model.get_iter(path), Column.FAV_ID) for path in paths]
|
||||
srv_model = get_base_model(services_view.get_model())
|
||||
srv_paths = [row.path for row in srv_model if row[16] in fav_ids]
|
||||
set_hide(channels, srv_model, srv_paths)
|
||||
elif flag is FLAG.LOCK:
|
||||
set_lock(blacklist, channels, model, paths, target, services_model=get_base_model(services_view.get_model()))
|
||||
srv_paths = [row.path for row in srv_model if row[Column.SRV_FAV_ID] in fav_ids]
|
||||
set_hide(services, srv_model, srv_paths)
|
||||
elif flag is Flag.LOCK:
|
||||
set_lock(blacklist, services, model, paths, target, services_model=get_base_model(services_view.get_model()))
|
||||
|
||||
return True
|
||||
update_fav_model(fav_view, services)
|
||||
|
||||
|
||||
def set_lock(blacklist, channels, model, paths, target, services_model):
|
||||
col_num = 4 if target is ViewTarget.SERVICES else 3
|
||||
def update_fav_model(fav_view, services):
|
||||
for row in get_base_model(fav_view.get_model()):
|
||||
srv = services.get(row[Column.FAV_ID], None)
|
||||
if srv:
|
||||
row[Column.FAV_LOCKED], row[Column.FAV_HIDE] = srv.locked, srv.hide
|
||||
|
||||
|
||||
def set_lock(blacklist, services, model, paths, target, services_model):
|
||||
col_num = Column.SRV_LOCKED if target is ViewTarget.SERVICES else Column.FAV_LOCKED
|
||||
locked = has_locked_hide(model, paths, col_num)
|
||||
|
||||
ids = []
|
||||
skip_type = {BqServiceType.MARKER.name, BqServiceType.SPACE.name, BqServiceType.ALT.name}
|
||||
|
||||
for path in paths:
|
||||
itr = model.get_iter(path)
|
||||
fav_id = model.get_value(itr, 16 if target is ViewTarget.SERVICES else 7)
|
||||
channel = channels.get(fav_id, None)
|
||||
if channel:
|
||||
bq_id = to_bouquet_id(channel)
|
||||
fav_id = model.get_value(itr, Column.SRV_FAV_ID if target is ViewTarget.SERVICES else Column.FAV_ID)
|
||||
srv = services.get(fav_id, None)
|
||||
if srv and srv.service_type not in skip_type:
|
||||
bq_id = srv.data_id if srv.service_type == BqServiceType.IPTV.name else to_bouquet_id(srv)
|
||||
if not bq_id:
|
||||
continue
|
||||
blacklist.discard(bq_id) if locked else blacklist.add(bq_id)
|
||||
model.set_value(itr, col_num, None if locked else LOCKED_ICON)
|
||||
channels[fav_id] = Service(*channel[:4], None if locked else LOCKED_ICON, *channel[5:])
|
||||
services[fav_id] = srv._replace(locked=None if locked else LOCKED_ICON)
|
||||
ids.append(fav_id)
|
||||
|
||||
if target is ViewTarget.FAV and ids:
|
||||
for ch in services_model:
|
||||
if ch[16] in ids:
|
||||
ch[4] = None if locked else LOCKED_ICON
|
||||
gen = update_services_model(ids, locked, services_model)
|
||||
GLib.idle_add(lambda: next(gen, False))
|
||||
|
||||
|
||||
def set_hide(channels, model, paths):
|
||||
col_num = 5
|
||||
def update_services_model(ids, locked, services_model):
|
||||
for srv in services_model:
|
||||
if srv[Column.SRV_FAV_ID] in ids:
|
||||
srv[Column.SRV_LOCKED] = None if locked else LOCKED_ICON
|
||||
yield True
|
||||
|
||||
|
||||
def set_hide(services, model, paths):
|
||||
col_num = Column.SRV_HIDE
|
||||
hide = has_locked_hide(model, paths, col_num)
|
||||
|
||||
for path in paths:
|
||||
@@ -215,28 +327,28 @@ def set_hide(channels, model, paths):
|
||||
value = int(flag[2:]) if flag else 0
|
||||
|
||||
if not hide:
|
||||
if value in FLAG.hide_values():
|
||||
if Flag.is_hide(value):
|
||||
continue # skip if already hidden
|
||||
value += FLAG.HIDE.value
|
||||
value += Flag.HIDE.value
|
||||
else:
|
||||
if value not in FLAG.hide_values():
|
||||
if not Flag.is_hide(value):
|
||||
continue # skip if already allowed to show
|
||||
value -= FLAG.HIDE.value
|
||||
value -= Flag.HIDE.value
|
||||
|
||||
if value == 0 and index is not None:
|
||||
del flags[index]
|
||||
else:
|
||||
value = "f:{}".format(value) if value > 10 else "f:0{}".format(value)
|
||||
value = "f:{:02d}".format(value)
|
||||
if index is not None:
|
||||
flags[index] = value
|
||||
else:
|
||||
flags.append(value)
|
||||
|
||||
model.set_value(itr, 0, (",".join(reversed(sorted(flags)))))
|
||||
fav_id = model.get_value(itr, 16)
|
||||
channel = channels.get(fav_id, None)
|
||||
if channel:
|
||||
channels[fav_id] = Service(*channel[:5], None if hide else HIDE_ICON, *channel[6:])
|
||||
fav_id = model.get_value(itr, Column.SRV_FAV_ID)
|
||||
srv = services.get(fav_id, None)
|
||||
if srv:
|
||||
services[fav_id] = srv._replace(hide=None if hide else HIDE_ICON)
|
||||
|
||||
|
||||
def has_locked_hide(model, paths, col_num):
|
||||
@@ -248,7 +360,7 @@ def has_locked_hide(model, paths, col_num):
|
||||
|
||||
# ***************** Location *******************#
|
||||
|
||||
def locate_in_services(fav_view, services_view, parent_window):
|
||||
def locate_in_services(fav_view, services_view, column, parent_window):
|
||||
""" Locating and scrolling to the service """
|
||||
model, paths = fav_view.get_selection().get_selected_rows()
|
||||
|
||||
@@ -258,9 +370,9 @@ def locate_in_services(fav_view, services_view, parent_window):
|
||||
show_dialog(DialogType.ERROR, parent_window, "Please, select only one item!")
|
||||
return
|
||||
|
||||
fav_id = model.get_value(model.get_iter(paths[0]), 7)
|
||||
fav_id = model.get_value(model.get_iter(paths[0]), Column.FAV_ID)
|
||||
for index, row in enumerate(services_view.get_model()):
|
||||
if row[16] == fav_id:
|
||||
if row[column] == fav_id:
|
||||
scroll_to(index, services_view)
|
||||
break
|
||||
|
||||
@@ -275,11 +387,261 @@ def scroll_to(index, view, paths=None):
|
||||
selection.select_path(index)
|
||||
|
||||
|
||||
# ***************** Picons ********************* #
|
||||
|
||||
def get_picon_dialog(transient, title, button_text, multiple=True):
|
||||
""" Returns a copy dialog with a preview of images [picons -> *.png]. """
|
||||
dialog = Gtk.FileChooserNative.new(title, transient, Gtk.FileChooserAction.OPEN, button_text)
|
||||
dialog.set_select_multiple(multiple)
|
||||
dialog.set_modal(True)
|
||||
# Filter.
|
||||
file_filter = Gtk.FileFilter()
|
||||
file_filter.set_name("*.png")
|
||||
file_filter.add_pattern("*.png")
|
||||
file_filter.add_mime_type("image/png") if IS_DARWIN else None
|
||||
dialog.add_filter(file_filter)
|
||||
|
||||
if IS_LINUX:
|
||||
preview_image = Gtk.Image(margin_right=10)
|
||||
dialog.set_preview_widget(preview_image)
|
||||
|
||||
def update_preview_widget(dlg):
|
||||
path = dialog.get_preview_filename()
|
||||
if not path:
|
||||
return
|
||||
|
||||
pix = get_picon_pixbuf(path, 220)
|
||||
preview_image.set_from_pixbuf(pix)
|
||||
dlg.set_preview_widget_active(bool(pix))
|
||||
|
||||
dialog.connect("update-preview", update_preview_widget)
|
||||
|
||||
return dialog
|
||||
|
||||
|
||||
def assign_picons(target, srv_view, fav_view, transient, picons, settings, services, src_path=None, dst_path=None):
|
||||
""" Assigning picons and returns picons files list. """
|
||||
view = srv_view if target is ViewTarget.SERVICES else fav_view
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
picons_files = []
|
||||
|
||||
if not src_path:
|
||||
dialog = get_picon_dialog(transient, get_message("Picon selection"), get_message("Open"), False)
|
||||
if dialog.run() in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT) or not dialog.get_filenames():
|
||||
return picons_files
|
||||
|
||||
src_path = dialog.get_filenames()[0]
|
||||
|
||||
if IS_WIN:
|
||||
src_path = src_path.lstrip("/")
|
||||
dst_path = dst_path.lstrip("/") if dst_path else dst_path
|
||||
|
||||
if not str(src_path).endswith(".png") or not os.path.isfile(src_path):
|
||||
show_dialog(DialogType.ERROR, transient, text="No png file is selected!")
|
||||
return picons_files
|
||||
|
||||
p_pos = Column.SRV_PICON
|
||||
col_num = Column.SRV_FAV_ID if target is ViewTarget.SERVICES else Column.FAV_ID
|
||||
itrs = [model.get_iter(p) for p in paths]
|
||||
|
||||
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:
|
||||
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.profile_picons_path
|
||||
os.makedirs(os.path.dirname(picons_path), exist_ok=True)
|
||||
picon_file = picons_path + picon_id
|
||||
try:
|
||||
shutil.copy(src_path, picon_file)
|
||||
except shutil.SameFileError:
|
||||
pass # NOP
|
||||
else:
|
||||
picons_files.append(picon_file)
|
||||
picon = get_picon_pixbuf(picon_file, settings.list_picon_size)
|
||||
picons[picon_id] = picon
|
||||
model.set_value(itr, p_pos, picon)
|
||||
if target is ViewTarget.SERVICES:
|
||||
set_picon(fav_id, fav_view.get_model(), picon, Column.FAV_ID, p_pos)
|
||||
else:
|
||||
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):
|
||||
for row in model:
|
||||
if row[fav_id_pos] == fav_id:
|
||||
row[picon_pos] = picon
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
def remove_picon(target, srv_view, fav_view, picons, settings):
|
||||
view = srv_view if target is ViewTarget.SERVICES else fav_view
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
|
||||
fav_ids = []
|
||||
picon_ids = []
|
||||
picon_pos = Column.SRV_PICON # picon position is equal for services and fav
|
||||
|
||||
itrs = [model.get_iter(p) for p in paths]
|
||||
|
||||
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)
|
||||
if target is ViewTarget.SERVICES:
|
||||
fav_ids.append(model.get_value(itr, Column.SRV_FAV_ID))
|
||||
picon_ids.append(model.get_value(itr, Column.SRV_PICON_ID))
|
||||
else:
|
||||
srv_type, fav_id = model.get(itr, Column.FAV_TYPE, Column.FAV_ID)
|
||||
if srv_type == BqServiceType.IPTV.name:
|
||||
picon_ids.append("{}_{}_{}_{}_{}_{}_{}_{}_{}_{}.png".format(*fav_id.split(":")[0:10]).strip())
|
||||
else:
|
||||
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):
|
||||
if md.get_value(it, fav_id_column) in fav_ids:
|
||||
md.set_value(it, picon_pos, None)
|
||||
if target is ViewTarget.FAV:
|
||||
picon_ids.append(md.get_value(it, Column.SRV_PICON_ID))
|
||||
|
||||
fav_view.get_model().foreach(remove) if target is ViewTarget.SERVICES else get_base_model(
|
||||
srv_view.get_model()).foreach(remove)
|
||||
|
||||
remove_picons(settings, picon_ids, picons)
|
||||
|
||||
|
||||
def copy_picon_reference(target, view, services, clipboard, transient):
|
||||
""" Copying picon id to clipboard """
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if not is_only_one_item_selected(paths, transient):
|
||||
return
|
||||
|
||||
if target is ViewTarget.SERVICES:
|
||||
picon_id = model.get_value(model.get_iter(paths), Column.SRV_PICON_ID)
|
||||
if picon_id:
|
||||
clipboard.set_text(picon_id.rstrip(".png"), -1)
|
||||
else:
|
||||
show_dialog(DialogType.ERROR, transient, "No reference is present!")
|
||||
elif target is ViewTarget.FAV:
|
||||
fav_id = model.get_value(model.get_iter(paths), Column.FAV_ID)
|
||||
srv = services.get(fav_id, None)
|
||||
if srv and srv.picon_id:
|
||||
clipboard.set_text(srv.picon_id.rstrip(".png"), -1)
|
||||
else:
|
||||
show_dialog(DialogType.ERROR, transient, "No reference is present!")
|
||||
|
||||
|
||||
def remove_all_unused_picons(settings, services):
|
||||
""" Removes picons from profile picons folder if there are no services for these picons. """
|
||||
ids = {s.picon_id for s in services}
|
||||
for p in Path(settings.profile_picons_path).glob("*.png"):
|
||||
if p.name not in ids and p.is_file():
|
||||
p.unlink()
|
||||
|
||||
|
||||
def remove_picons(settings, picon_ids, picons):
|
||||
pions_path = settings.profile_picons_path
|
||||
backup_path = f"{settings.profile_backup_path}picons{SEP}"
|
||||
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
|
||||
for p_id in picon_ids:
|
||||
picons[p_id] = None
|
||||
src = pions_path + p_id
|
||||
if os.path.isfile(src):
|
||||
shutil.move(src, backup_path + p_id)
|
||||
|
||||
|
||||
def is_only_one_item_selected(paths, transient):
|
||||
if len(paths) > 1:
|
||||
show_dialog(DialogType.ERROR, transient, "Please, select only one item!")
|
||||
return False
|
||||
|
||||
if not paths:
|
||||
show_dialog(DialogType.ERROR, transient, "No selected item!")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_picon_pixbuf(path, size=32):
|
||||
try:
|
||||
return GdkPixbuf.Pixbuf.new_from_file_at_scale(path, width=size, height=size, preserve_aspect_ratio=True)
|
||||
except GLib.GError as e:
|
||||
pass
|
||||
|
||||
|
||||
# ***************** Bouquets ********************* #
|
||||
|
||||
def gen_bouquets(view, bq_view, transient, gen_type, s_type, callback):
|
||||
""" Auto-generate and append list of bouquets. """
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
single_types = (BqGenType.SAT, BqGenType.PACKAGE, BqGenType.TYPE)
|
||||
if gen_type in single_types:
|
||||
if not is_only_one_item_selected(paths, transient):
|
||||
return
|
||||
|
||||
fav_id_index = Column.SRV_FAV_ID
|
||||
index = Column.SRV_TYPE
|
||||
if gen_type in (BqGenType.PACKAGE, BqGenType.EACH_PACKAGE):
|
||||
index = Column.SRV_PACKAGE
|
||||
elif gen_type in (BqGenType.SAT, BqGenType.EACH_SAT):
|
||||
index = Column.SRV_POS
|
||||
|
||||
# Splitting services [caching] by column value.
|
||||
s_data = defaultdict(list)
|
||||
for row in model:
|
||||
s_data[row[index]].append(BouquetService(None, BqServiceType.DEFAULT, row[fav_id_index], 0))
|
||||
|
||||
bq_type = BqType.BOUQUET.value if s_type is SettingsType.NEUTRINO_MP else BqType.TV.value
|
||||
bq_index = 0 if s_type is SettingsType.ENIGMA_2 else 1
|
||||
bq_root_iter = bq_view.get_model().get_iter(bq_index)
|
||||
srv = Service(*model[paths][:Column.SRV_TOOLTIP])
|
||||
cond = srv.package if gen_type is BqGenType.PACKAGE else srv.pos if gen_type is BqGenType.SAT else srv.service_type
|
||||
bq_view.expand_row(Gtk.TreePath(bq_index), 0)
|
||||
|
||||
bq_names = get_bouquets_names(bq_view.get_model())
|
||||
|
||||
if gen_type in single_types:
|
||||
if cond in bq_names:
|
||||
show_dialog(DialogType.ERROR, transient, "A bouquet with that name exists!")
|
||||
else:
|
||||
callback(Bouquet(cond, bq_type, s_data.get(cond)), bq_root_iter)
|
||||
else:
|
||||
# We add a bouquet only if the given name is missing [keys - names]!
|
||||
for name in sorted(s_data.keys() - bq_names):
|
||||
callback(Bouquet(name, BqType.TV.value, s_data.get(name)), bq_root_iter)
|
||||
|
||||
|
||||
def get_bouquets_names(model):
|
||||
""" Returns all current bouquets names """
|
||||
bouquets_names = set()
|
||||
for row in model:
|
||||
itr = row.iter
|
||||
if model.iter_has_child(itr):
|
||||
num_of_children = model.iter_n_children(itr)
|
||||
for num in range(num_of_children):
|
||||
child_itr = model.iter_nth_child(itr, num)
|
||||
bouquets_names.add(model[child_itr][0])
|
||||
return bouquets_names
|
||||
|
||||
|
||||
# ***************** Others *********************#
|
||||
|
||||
def update_entry_data(entry, dialog, options):
|
||||
""" Updates value in text entry from chooser dialog """
|
||||
response = show_dialog(dialog_type=DialogType.CHOOSER, transient=dialog, options=options)
|
||||
def update_entry_data(entry, dialog, settings):
|
||||
""" Updates value in text entry from chooser dialog. """
|
||||
response = show_dialog(dialog_type=DialogType.CHOOSER, transient=dialog, settings=settings, create_dir=True)
|
||||
if response not in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
entry.set_text(response)
|
||||
return response
|
||||
@@ -287,11 +649,58 @@ def update_entry_data(entry, dialog, options):
|
||||
|
||||
|
||||
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:
|
||||
return model.get_model().get_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):
|
||||
""" Returns model name and base model from the given view """
|
||||
model = get_base_model(view.get_model())
|
||||
model_name = model.get_name() if model else ""
|
||||
return model_name, model
|
||||
|
||||
|
||||
def append_text_to_tview(char, view):
|
||||
""" Appending text and scrolling to a given line in the text view. """
|
||||
buf = view.get_buffer()
|
||||
buf.insert_at_cursor(char)
|
||||
insert = buf.get_insert()
|
||||
view.scroll_to_mark(insert, 0.0, True, 0.0, 1.0)
|
||||
|
||||
|
||||
def get_iptv_url(row, s_type, column=Column.FAV_ID):
|
||||
""" Returns url from iptv type row """
|
||||
data = row[column].split(":" if s_type is SettingsType.ENIGMA_2 else "::")
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
data = list(filter(lambda x: "http" in x, data))
|
||||
if data:
|
||||
url = data[0]
|
||||
return unquote(url) if s_type is SettingsType.ENIGMA_2 else url
|
||||
|
||||
|
||||
def on_popup_menu(menu, event):
|
||||
""" Shows popup menu for the view """
|
||||
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
|
||||
menu.popup(None, None, None, None, event.button, event.time)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1941
app/ui/picons.glade
Normal file
1941
app/ui/picons.glade
Normal file
File diff suppressed because it is too large
Load Diff
1083
app/ui/picons.py
Normal file
1083
app/ui/picons.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,692 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.18.3 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.12"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<object class="GtkListStore" id="providers_list_store">
|
||||
<columns>
|
||||
<!-- column-name logo -->
|
||||
<column type="GdkPixbuf"/>
|
||||
<!-- column-name name -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name url -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name on_id -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name selected -->
|
||||
<column type="gboolean"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkDialog" id="picons_dialog">
|
||||
<property name="width_request">480</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">Picons download tool</property>
|
||||
<property name="resizable">False</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="icon_name">emblem-photos</property>
|
||||
<property name="type_hint">dialog</property>
|
||||
<child internal-child="vbox">
|
||||
<object class="GtkBox" id="picons_dialog_vbox">
|
||||
<property name="width_request">320</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">2</property>
|
||||
<property name="margin_right">2</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox" id="dialog_action_area">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">2</property>
|
||||
<property name="margin_right">2</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="layout_style">spread</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="label">gtk-close</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>
|
||||
<signal name="clicked" handler="on_close" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkGrid" id="grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="column_spacing">2</property>
|
||||
<property name="column_homogeneous">True</property>
|
||||
<child>
|
||||
<object class="GtkEntry" id="ip_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">network-transmit-receive-symbolic</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="picons_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ip_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Receiver IP:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="res_picons_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Receiver picons path:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="picons_dir_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Current picons path:</property>
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="picons_dir_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="secondary_icon_name">folder-open-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<signal name="icon-press" handler="on_picons_dir_open" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkGrid" id="format_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="column_spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="name_format_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Picons name format:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="box2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_right">5</property>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="enigma2_radio_button">
|
||||
<property name="label" translatable="yes">Enigma2 (default)</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">neutrino_mp_radio_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="neutrino_mp_radio_button">
|
||||
<property name="label" translatable="yes">Neutrino-MP</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">enigma2_radio_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="resize_format_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Resize: </property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="resize_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="resize_no_radio_button">
|
||||
<property name="label" translatable="yes">No(default)</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">resize_100_60_radio_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="resize_220_132_radio_button">
|
||||
<property name="label" translatable="yes">220x132</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">resize_100_60_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="resize_100_60_radio_button">
|
||||
<property name="label" translatable="yes">100x60</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="xalign">0</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw_indicator">True</property>
|
||||
<property name="group">resize_no_radio_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparator" id="separator3">
|
||||
<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="orientation">vertical</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="primary-toolbar"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparator" id="separator1">
|
||||
<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">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="url_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Satellite url (www.lyngsat.com):</property>
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="url_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">network-workgroup-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="placeholder_text" translatable="yes">https://www.lyngsat.com/*satellite*.html</property>
|
||||
<property name="input_purpose">url</property>
|
||||
<signal name="changed" handler="on_url_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="providers_scrolled_window">
|
||||
<property name="height_request">150</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">out</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="providers_tree_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="has_focus">True</property>
|
||||
<property name="model">providers_list_store</property>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="treeview_selection"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="provider_column">
|
||||
<property name="spacing">15</property>
|
||||
<property name="title" translatable="yes">Providers</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererPixbuf" id="logo_cellrendererpixbuf"/>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="name_cellrenderertext"/>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="url_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="title" translatable="yes">Url</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="cellrenderertext1"/>
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="on_id_column">
|
||||
<property name="visible">False</property>
|
||||
<property name="title" translatable="yes">ONID</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="cellrenderertext2"/>
|
||||
<attributes>
|
||||
<attribute name="text">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="selected_column">
|
||||
<property name="title" translatable="yes">Selected</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererToggle" id="cellrenderer_toggle">
|
||||
<signal name="toggled" handler="on_selected_toggled" swapped="no"/>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="active">4</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">7</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparator" id="separator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="padding">2</property>
|
||||
<property name="position">10</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolbar" id="toolbar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="show_arrow">False</property>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="cancel_tool_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Cancel</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-cancel</property>
|
||||
<signal name="clicked" handler="on_cancel" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="homogeneous">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorToolItem" id="separatortoolitem2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="receive_tool_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Receive picons for providers</property>
|
||||
<property name="is_important">True</property>
|
||||
<property name="label" translatable="yes">Receive picons</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="icon_name">go-bottom</property>
|
||||
<signal name="clicked" handler="on_receive" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="homogeneous">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="load_providers_tool_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Load satellite providers.</property>
|
||||
<property name="is_important">True</property>
|
||||
<property name="label" translatable="yes">Load providers</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="icon_name">network-server-symbolic</property>
|
||||
<signal name="clicked" handler="on_load_providers" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorToolItem" id="separatortoolitem1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="send_tool_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Transfer to receiver</property>
|
||||
<property name="label" translatable="yes">Send</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="icon_name">go-top</property>
|
||||
<signal name="clicked" handler="on_send" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="homogeneous">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="primary-toolbar"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">11</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkExpander" id="expander">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="resize_toplevel">True</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrolled_window">
|
||||
<property name="height_request">150</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="min_content_width">240</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="text_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="wrap_mode">word-char</property>
|
||||
<property name="overwrite">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="expander_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Info</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">12</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="app_paintable">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<signal name="response" handler="on_info_bar_close" swapped="no"/>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox" id="infobar-action_area1">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<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" id="infobar-content_area1">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">16</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="info_bar_message_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Info</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</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">13</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparator" id="separator2">
|
||||
<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">14</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
@@ -1,239 +0,0 @@
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from gi.repository import GLib, GdkPixbuf
|
||||
|
||||
from app.commons import run_idle, run_task
|
||||
from app.ftp import upload_data, DownloadDataType
|
||||
from app.picons.picons import PiconsParser, parse_providers, Provider
|
||||
from app.properties import Profile
|
||||
from . import Gtk, Gdk, UI_RESOURCES_PATH
|
||||
from .dialogs import show_dialog, DialogType
|
||||
from .main_helper import update_entry_data
|
||||
|
||||
|
||||
class PiconsDialog:
|
||||
def __init__(self, transient, options, profile=Profile.ENIGMA_2):
|
||||
self._TMP_DIR = tempfile.gettempdir() + "/"
|
||||
self._BASE_URL = "www.lyngsat.com/packages/"
|
||||
self._PATTERN = re.compile("^https://www\.lyngsat\.com/[\w-]+\.html$")
|
||||
self._current_process = None
|
||||
self._picons_path = options.get("picons_dir_path", "")
|
||||
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_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}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.add_objects_from_file(UI_RESOURCES_PATH + "picons_dialog.glade",
|
||||
("picons_dialog", "receive_image", "providers_list_store"))
|
||||
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._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_tool_button = builder.get_object("load_providers_tool_button")
|
||||
self._receive_tool_button = builder.get_object("receive_tool_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")
|
||||
# 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._properties = options
|
||||
self._profile = profile
|
||||
self._ip_entry.set_text(options.get("host", ""))
|
||||
self._picons_entry.set_text(options.get("picons_path", ""))
|
||||
self._picons_path = options.get("picons_dir_path", "")
|
||||
self._picons_dir_entry.set_text(self._picons_path)
|
||||
|
||||
def show(self):
|
||||
self._dialog.run()
|
||||
self._dialog.destroy()
|
||||
|
||||
@run_idle
|
||||
def on_load_providers(self, item):
|
||||
self._expander.set_expanded(True)
|
||||
url = self._url_entry.get_text()
|
||||
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.append_providers(url)
|
||||
|
||||
@run_task
|
||||
def append_providers(self, url):
|
||||
model = self._providers_tree_view.get_model()
|
||||
model.clear()
|
||||
self._current_process.wait()
|
||||
providers = parse_providers(self._TMP_DIR + url[url.find("w"):])
|
||||
if providers:
|
||||
for p in providers:
|
||||
logo = self.get_pixbuf(p[0])
|
||||
model.append((logo, p.name, p.url, p.on_id, p.selected))
|
||||
self.update_receive_button_state()
|
||||
|
||||
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)
|
||||
|
||||
@run_idle
|
||||
def on_receive(self, item):
|
||||
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)
|
||||
|
||||
for prv in self.get_selected_providers():
|
||||
if self._terminate:
|
||||
break
|
||||
self.process_provider(Provider(*prv))
|
||||
|
||||
def process_provider(self, provider):
|
||||
url = provider.url
|
||||
self.show_info_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 + self._BASE_URL + url[url.rfind("/") + 1:]
|
||||
PiconsParser.parse(path, self._picons_path, self._TMP_DIR, provider.on_id, self.get_picons_format())
|
||||
self.resize(self._picons_path)
|
||||
self.show_info_message("Done", Gtk.MessageType.INFO)
|
||||
|
||||
def write_to_buffer(self, fd, condition):
|
||||
if condition == GLib.IO_IN:
|
||||
char = fd.read(1)
|
||||
self.append_output(char)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@run_idle
|
||||
def append_output(self, char):
|
||||
buf = self._text_view.get_buffer()
|
||||
buf.insert_at_cursor(char)
|
||||
self.scroll_to_end(buf)
|
||||
|
||||
def scroll_to_end(self, buf):
|
||||
insert = buf.get_insert()
|
||||
self._text_view.scroll_to_mark(insert, 0.0, True, 0.0, 1.0)
|
||||
|
||||
def resize(self, path):
|
||||
if self._resize_no_radio_button.get_active():
|
||||
return
|
||||
|
||||
self.show_info_message("Resizing...", Gtk.MessageType.INFO)
|
||||
command = "mogrify -resize {}! *.png".format(
|
||||
"320x240" if self._resize_220_132_radio_button.get_active() else "100x60").split()
|
||||
self._current_process = subprocess.Popen(command, universal_newlines=True, cwd=path)
|
||||
self._current_process.wait()
|
||||
|
||||
@run_task
|
||||
def on_cancel(self, item):
|
||||
if self._current_process:
|
||||
self._terminate = True
|
||||
self._current_process.terminate()
|
||||
time.sleep(1)
|
||||
|
||||
@run_idle
|
||||
def on_close(self, item):
|
||||
self.on_cancel(item)
|
||||
self._dialog.destroy()
|
||||
|
||||
def on_send(self, item):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
self.show_info_message("Please, wait...", Gtk.MessageType.INFO)
|
||||
self.upload_picons()
|
||||
|
||||
@run_task
|
||||
def upload_picons(self):
|
||||
if self._current_process is not None and self._current_process.poll() is None:
|
||||
self.show_dialog("The task is already running!", DialogType.ERROR)
|
||||
return
|
||||
|
||||
upload_data(properties=self._properties,
|
||||
download_type=DownloadDataType.PICONS,
|
||||
profile=self._profile,
|
||||
callback=lambda: self.show_info_message("Done!", Gtk.MessageType.INFO))
|
||||
|
||||
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, options={"data_dir_path": self._picons_path})
|
||||
|
||||
@run_idle
|
||||
def on_selected_toggled(self, toggle, path):
|
||||
model = self._providers_tree_view.get_model()
|
||||
model.set_value(model.get_iter(path), 4, not toggle.get_active())
|
||||
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_tool_button.set_sensitive(suit if suit else False)
|
||||
|
||||
@run_idle
|
||||
def update_receive_button_state(self):
|
||||
self._receive_tool_button.set_sensitive(len(self.get_selected_providers()) > 0)
|
||||
|
||||
def get_selected_providers(self):
|
||||
""" returns selected providers """
|
||||
return [r for r in self._providers_tree_view.get_model() if r[4]]
|
||||
|
||||
@run_idle
|
||||
def show_dialog(self, message, dialog_type):
|
||||
show_dialog(dialog_type, self._dialog, message)
|
||||
|
||||
def get_picons_format(self):
|
||||
picon_format = Profile.ENIGMA_2
|
||||
|
||||
if self._neutrino_mp_radio_button.get_active():
|
||||
picon_format = Profile.NEUTRINO_MP
|
||||
|
||||
return picon_format
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
242
app/ui/playback.glade
Normal file
242
app/ui/playback.glade
Normal file
@@ -0,0 +1,242 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<object class="GtkEventBox" id="event_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<signal name="button-press-event" handler="on_press" swapped="no"/>
|
||||
<signal name="realize" handler="on_realize" swapped="no"/>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkToolbar" id="tool_bar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="prev_button">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Previous stream in the list</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-media-previous</property>
|
||||
<signal name="clicked" handler="on_previous" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="play_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Play</property>
|
||||
<property name="action_name">app.on_play</property>
|
||||
<property name="stock_id">gtk-media-play</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="stop_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Stop playback</property>
|
||||
<property name="action_name">app.on_stop</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-media-stop</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="next_button">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Next stream in the list</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-media-next</property>
|
||||
<signal name="clicked" handler="on_next" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolItem" id="player_item">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="rewind_box">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="current_time_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScale" id="scale">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="restrict_to_fill_level">False</property>
|
||||
<property name="fill_level">0</property>
|
||||
<property name="draw_value">False</property>
|
||||
<property name="has_origin">False</property>
|
||||
<signal name="change-value" handler="on_rewind" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="full_time_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="homogeneous">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolItem" id="extras_item">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="extras_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="audio_menu_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="relief">none</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="audio_menu_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Audio Track</property>
|
||||
<property name="icon_name">audio-volume-high</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="video_menu_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="relief">none</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="video_menu_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Aspect ratio</property>
|
||||
<property name="icon_name">view-restore</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="subtitle_menu_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="relief">none</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="subtitle_menu_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Subtitle Track</property>
|
||||
<property name="icon_name">format-text-underline</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="full_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Toggle in fullscreen</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-fullscreen</property>
|
||||
<signal name="clicked" handler="on_full_screen" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToolButton" id="close_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Close playback</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-close</property>
|
||||
<signal name="clicked" handler="on_close" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="homogeneous">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
454
app/ui/playback.py
Normal file
454
app/ui/playback.py
Normal file
@@ -0,0 +1,454 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Additional module for playback. """
|
||||
from functools import lru_cache
|
||||
|
||||
from gi.repository import GLib, GObject, Gio
|
||||
|
||||
from app.commons import run_idle, run_with_delay
|
||||
from app.connections import HttpAPI
|
||||
from app.eparser.ecommons import BqServiceType
|
||||
from app.settings import PlayStreamsMode, IS_DARWIN, SettingsType
|
||||
from app.tools.media import Player
|
||||
from app.ui.dialogs import get_builder, get_message
|
||||
from app.ui.main_helper import get_iptv_url
|
||||
from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, Column, IS_GNOME_SESSION, Page
|
||||
|
||||
|
||||
class PlayerBox(Gtk.Box):
|
||||
|
||||
def __init__(self, app, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# Signals.
|
||||
GObject.signal_new("playback-full-screen", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("playback-close", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("play", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
GObject.signal_new("stop", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
|
||||
self._app = app
|
||||
self._app.connect("fav-clicked", self.on_fav_clicked)
|
||||
self._app.connect("srv-clicked", self.on_srv_clicked)
|
||||
self._app.connect("iptv-clicked", self.on_iptv_clicked)
|
||||
self._app.connect("page-changed", self.on_page_changed)
|
||||
self._app.connect("play-current", self.on_play_current)
|
||||
self._app.connect("play-recording", self.on_play_recording)
|
||||
self._fav_view = app.fav_view
|
||||
self._player = None
|
||||
self._current_mrl = None
|
||||
self._full_screen = False
|
||||
self._playback_window = None
|
||||
self._audio_track_menu = None
|
||||
self._subtitle_track_menu = None
|
||||
self._play_mode = self._app.app_settings.play_streams_mode
|
||||
|
||||
handlers = {"on_realize": self.on_realize,
|
||||
"on_press": self.on_press,
|
||||
"on_next": self.on_next,
|
||||
"on_previous": self.on_previous,
|
||||
"on_rewind": self.on_rewind,
|
||||
"on_full_screen": self.on_full_screen,
|
||||
"on_close": self.on_close}
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "playback.glade", handlers)
|
||||
self.set_spacing(5)
|
||||
self.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
self._event_box = builder.get_object("event_box")
|
||||
self.pack_start(self._event_box, True, True, 0)
|
||||
if not IS_DARWIN:
|
||||
self.pack_end(builder.get_object("tool_bar"), False, True, 0)
|
||||
self._scale = builder.get_object("scale")
|
||||
self._full_time_label = builder.get_object("full_time_label")
|
||||
self._current_time_label = builder.get_object("current_time_label")
|
||||
self._rewind_box = builder.get_object("rewind_box")
|
||||
self._tool_bar = builder.get_object("tool_bar")
|
||||
self._prev_button = builder.get_object("prev_button")
|
||||
self._next_button = builder.get_object("next_button")
|
||||
self._audio_menu_button = builder.get_object("audio_menu_button")
|
||||
self._video_menu_button = builder.get_object("video_menu_button")
|
||||
self._subtitle_menu_button = builder.get_object("subtitle_menu_button")
|
||||
self._fav_view.bind_property("sensitive", self._prev_button, "sensitive")
|
||||
self._fav_view.bind_property("sensitive", self._next_button, "sensitive")
|
||||
|
||||
self.connect("delete-event", self.on_delete)
|
||||
self.connect("show", self.set_player_area_size)
|
||||
|
||||
def on_fav_clicked(self, app, mode):
|
||||
if mode is not FavClickMode.STREAM and not self._app.http_api:
|
||||
return
|
||||
|
||||
self._fav_view.set_sensitive(False)
|
||||
if mode is FavClickMode.STREAM:
|
||||
self.on_play_stream()
|
||||
elif mode is FavClickMode.ZAP_PLAY:
|
||||
self._app.on_zap(self.on_watch)
|
||||
elif mode is FavClickMode.PLAY:
|
||||
self.on_play_service()
|
||||
|
||||
def on_srv_clicked(self, app, mode):
|
||||
if not self._app.http_api:
|
||||
return
|
||||
|
||||
view = self._app.services_view
|
||||
path, column = view.get_cursor()
|
||||
if path:
|
||||
srv = self._app.current_services.get(view.get_model()[path][Column.SRV_FAV_ID], None)
|
||||
if not srv or not srv.picon_id:
|
||||
return
|
||||
|
||||
ref = self._app.get_service_ref_data(srv)
|
||||
s_type = self._app.app_settings.setting_type
|
||||
error_msg = "No connection to the receiver!"
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
def zap(rq):
|
||||
self.on_watch() if rq and rq.get("e2state", False) else self.on_error(None, error_msg)
|
||||
|
||||
self._app.http_api.send(HttpAPI.Request.ZAP, ref, zap)
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
def zap(rq):
|
||||
self.on_watch() if rq and rq.get("data", None) == "ok" else self.on_error(None, error_msg)
|
||||
|
||||
self._app.http_api.send(HttpAPI.Request.N_ZAP, f"?{ref}", zap)
|
||||
|
||||
def on_iptv_clicked(self, app, mode):
|
||||
if not self._app.http_api:
|
||||
return
|
||||
|
||||
view = self._app.iptv_services_view
|
||||
path, column = view.get_cursor()
|
||||
if path:
|
||||
row = view.get_model()[path][:]
|
||||
url = get_iptv_url(row, self._app.app_settings.setting_type, Column.IPTV_FAV_ID)
|
||||
self.play(url, row[Column.IPTV_SERVICE]) if url else self.on_error(None, "No reference is present!")
|
||||
|
||||
def on_play_current(self, app, url):
|
||||
self.on_watch()
|
||||
|
||||
def on_play_recording(self, app, url):
|
||||
self.play(url)
|
||||
|
||||
def on_page_changed(self, app, page):
|
||||
self.on_close()
|
||||
self.set_visible(False)
|
||||
|
||||
def on_realize(self, box):
|
||||
if not self._player:
|
||||
settings = self._app.app_settings
|
||||
try:
|
||||
self._player = Player.make(settings.stream_lib, settings.play_streams_mode, self._event_box)
|
||||
except (ImportError, NameError) as e:
|
||||
self._app.show_error_message(str(e))
|
||||
return True
|
||||
else:
|
||||
self.init_playback_elements()
|
||||
self.emit("play", self._current_mrl)
|
||||
finally:
|
||||
if settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
|
||||
self.set_player_area_size(box)
|
||||
|
||||
def init_playback_elements(self):
|
||||
self._player.connect("error", self.on_error)
|
||||
self._player.connect("played", self.on_played)
|
||||
self._player.connect("audio-track", self.on_audio_track_changed)
|
||||
self._player.connect("subtitle-track", self.on_subtitle_track_changed)
|
||||
self._app.app_window.connect("key-press-event", self.on_key_press)
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "app_menu.ui")
|
||||
self._audio_track_menu = builder.get_object("audio_track_menu")
|
||||
self._subtitle_track_menu = builder.get_object("subtitle_track_menu")
|
||||
audio_menu = builder.get_object("audio_menu")
|
||||
video_menu = builder.get_object("video_menu")
|
||||
subtitle_menu = builder.get_object("subtitle_menu")
|
||||
|
||||
if not IS_GNOME_SESSION:
|
||||
menu_bar = self._app.get_menubar()
|
||||
menu_bar.insert_section(1, None, audio_menu)
|
||||
menu_bar.insert_section(2, None, video_menu)
|
||||
menu_bar.insert_section(3, None, subtitle_menu)
|
||||
|
||||
if not IS_DARWIN:
|
||||
self._player.connect("position", self.on_time_changed)
|
||||
self._audio_menu_button.set_menu_model(self._audio_track_menu)
|
||||
self._video_menu_button.set_menu_model(builder.get_object("aspect_ratio_menu"))
|
||||
self._subtitle_menu_button.set_menu_model(self._subtitle_track_menu)
|
||||
# Actions.
|
||||
self._app.set_action("on_play", self.on_play)
|
||||
self._app.set_action("on_stop", self.on_stop)
|
||||
audio_track_action = Gio.SimpleAction.new_stateful("on_set_audio_track", GLib.VariantType.new("i"),
|
||||
GLib.Variant("i", 0))
|
||||
audio_track_action.connect("activate", self.on_set_audio_track)
|
||||
self._app.add_action(audio_track_action)
|
||||
aspect_action = Gio.SimpleAction.new_stateful("on_set_aspect_ratio", GLib.VariantType.new("s"),
|
||||
GLib.Variant("s", ""))
|
||||
aspect_action.connect("activate", self.on_set_aspect_ratio)
|
||||
self._app.add_action(aspect_action)
|
||||
subtitle_track_action = Gio.SimpleAction.new_stateful("on_set_subtitle_track", GLib.VariantType.new("i"),
|
||||
GLib.Variant("i", -1))
|
||||
subtitle_track_action.connect("activate", self.on_set_subtitle_track)
|
||||
self._app.add_action(subtitle_track_action)
|
||||
|
||||
def on_play(self, action=None, value=None):
|
||||
self.emit("play", None)
|
||||
|
||||
def on_stop(self, action=None, value=None):
|
||||
self.emit("stop", None)
|
||||
|
||||
def on_next(self, button):
|
||||
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, 1):
|
||||
self.set_player_action()
|
||||
|
||||
def on_previous(self, button):
|
||||
if self._fav_view.do_move_cursor(self._fav_view, Gtk.MovementStep.DISPLAY_LINES, -1):
|
||||
self.set_player_action()
|
||||
|
||||
def on_rewind(self, scale, scroll_type, value):
|
||||
self._player.set_time(int(value))
|
||||
|
||||
def on_full_screen(self, item=None):
|
||||
self._full_screen = not self._full_screen
|
||||
if self._play_mode is PlayStreamsMode.BUILT_IN:
|
||||
self._tool_bar.set_visible(not self._full_screen)
|
||||
self.emit("playback-full-screen", not self._full_screen)
|
||||
elif self._playback_window:
|
||||
if not IS_DARWIN:
|
||||
self._tool_bar.set_visible(not self._full_screen)
|
||||
self._playback_window.fullscreen() if self._full_screen else self._playback_window.unfullscreen()
|
||||
|
||||
def on_close(self, action=None, value=None):
|
||||
if self._playback_window:
|
||||
self._app.app_settings.add("playback_window_size", self._playback_window.get_size())
|
||||
self._playback_window.hide()
|
||||
|
||||
self.on_stop()
|
||||
self.hide()
|
||||
self.emit("playback-close", None)
|
||||
|
||||
return True
|
||||
|
||||
@run_with_delay(1)
|
||||
def on_audio_track_changed(self, player, tracks):
|
||||
self._audio_track_menu.remove_all()
|
||||
for t in tracks:
|
||||
item = Gio.MenuItem.new(t[1], None)
|
||||
item.set_action_and_target_value("app.on_set_audio_track", GLib.Variant("i", t[0]))
|
||||
self._audio_track_menu.append_item(item)
|
||||
|
||||
@run_with_delay(1)
|
||||
def on_subtitle_track_changed(self, player, tracks):
|
||||
self._subtitle_track_menu.remove_all()
|
||||
for t in tracks:
|
||||
item = Gio.MenuItem.new(t[1], None)
|
||||
item.set_action_and_target_value("app.on_set_subtitle_track", GLib.Variant("i", t[0]))
|
||||
self._subtitle_track_menu.append_item(item)
|
||||
|
||||
def on_set_audio_track(self, action, value):
|
||||
action.set_state(value)
|
||||
self._player.set_audio_track(value.get_int32())
|
||||
|
||||
def on_set_aspect_ratio(self, action, value):
|
||||
action.set_state(value)
|
||||
self._player.set_aspect_ratio(value.get_string())
|
||||
|
||||
def on_set_subtitle_track(self, action, value):
|
||||
action.set_state(value)
|
||||
self._player.set_subtitle_track(value.get_int32())
|
||||
|
||||
def on_press(self, area, event):
|
||||
if event.button == Gdk.BUTTON_PRIMARY:
|
||||
if event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
|
||||
self.on_full_screen()
|
||||
|
||||
def on_key_press(self, widget, event):
|
||||
if self._player and self.get_visible():
|
||||
key = event.keyval
|
||||
if any((key == Gdk.KEY_F11, key == Gdk.KEY_f, self._full_screen and key == Gdk.KEY_Escape)):
|
||||
self.on_full_screen()
|
||||
|
||||
def on_delete(self, box):
|
||||
if self._player:
|
||||
self._player.release()
|
||||
|
||||
@run_with_delay(1)
|
||||
def set_player_action(self):
|
||||
click_mode = self._app.app_settings.fav_click_mode
|
||||
self._fav_view.set_sensitive(False)
|
||||
if click_mode is FavClickMode.PLAY:
|
||||
self.on_play_service()
|
||||
elif click_mode is FavClickMode.ZAP_PLAY:
|
||||
self._app.on_zap(self.on_watch)
|
||||
elif click_mode is FavClickMode.STREAM:
|
||||
self.on_play_stream()
|
||||
|
||||
def update_buttons(self):
|
||||
if self._player:
|
||||
path, column = self._fav_view.get_cursor()
|
||||
current_index = path[0]
|
||||
self._player_prev_button.set_sensitive(current_index != 0)
|
||||
self._player_next_button.set_sensitive(len(self._fav_model) != current_index + 1)
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def on_duration_changed(self, duration):
|
||||
self._scale.set_value(0)
|
||||
self._scale.get_adjustment().set_upper(duration)
|
||||
GLib.idle_add(self._rewind_box.set_visible, duration > 0, priority=GLib.PRIORITY_LOW)
|
||||
GLib.idle_add(self._current_time_label.set_text, "0", priority=GLib.PRIORITY_LOW)
|
||||
GLib.idle_add(self._full_time_label.set_text, self.get_time_str(duration),
|
||||
priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def on_time_changed(self, widget, t):
|
||||
if not self._full_screen and self._rewind_box.get_visible():
|
||||
GLib.idle_add(self._current_time_label.set_text, self.get_time_str(t),
|
||||
priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def get_time_str(self, duration):
|
||||
""" Returns a string representation of time from duration in milliseconds """
|
||||
m, s = divmod(duration // 1000, 60)
|
||||
h, m = divmod(m, 60)
|
||||
return f"{str(h) + ':' if h else ''}{m:02d}:{s:02d}"
|
||||
|
||||
def set_player_area_size(self, widget):
|
||||
w, h = self._app.app_window.get_size()
|
||||
widget.set_size_request(w * 0.6, -1)
|
||||
|
||||
@run_idle
|
||||
def show_playback_window(self, title=None):
|
||||
width, height = 480, 240
|
||||
size = self._app.app_settings.get("playback_window_size")
|
||||
if size:
|
||||
width, height = size
|
||||
|
||||
if self._playback_window:
|
||||
self._playback_window.show()
|
||||
self._playback_window.set_title(title or self.get_playback_title())
|
||||
else:
|
||||
self._playback_window = Gtk.Window(title=title or self.get_playback_title(),
|
||||
window_position=Gtk.WindowPosition.CENTER,
|
||||
icon_name="demon-editor")
|
||||
|
||||
self._playback_window.connect("delete-event", self.on_close)
|
||||
self._playback_window.connect("key-press-event", self.on_key_press)
|
||||
self._playback_window.bind_property("visible", self._event_box, "visible")
|
||||
|
||||
if not IS_DARWIN:
|
||||
self._prev_button.set_visible(False)
|
||||
self._next_button.set_visible(False)
|
||||
|
||||
self.reparent(self._playback_window)
|
||||
self._playback_window.set_application(self._app)
|
||||
|
||||
self.show()
|
||||
self._playback_window.resize(width, height)
|
||||
self._playback_window.show()
|
||||
|
||||
def get_playback_title(self):
|
||||
if self._app.page is not Page.RECORDINGS:
|
||||
path, column = self._fav_view.get_cursor()
|
||||
if path:
|
||||
return f"DemonEditor [{self._app.fav_view.get_model()[path][:][Column.FAV_SERVICE]}]"
|
||||
else:
|
||||
return f"DemonEditor [{get_message('Recordings')}]"
|
||||
return f"DemonEditor [{get_message('Playback')}]"
|
||||
|
||||
def on_play_stream(self):
|
||||
path, column = self._fav_view.get_cursor()
|
||||
if path:
|
||||
row = self._fav_view.get_model()[path][:]
|
||||
if row[Column.FAV_TYPE] != BqServiceType.IPTV.name:
|
||||
self.on_error(None, "Not allowed in this context!")
|
||||
return
|
||||
|
||||
url = get_iptv_url(row, self._app.app_settings.setting_type)
|
||||
self.play(url) if url else self.on_error(None, "No reference is present!")
|
||||
|
||||
def on_play_service(self, item=None):
|
||||
path, column = self._fav_view.get_cursor()
|
||||
if not path or not self._app.http_api:
|
||||
return
|
||||
|
||||
ref = self._app.get_service_ref(path)
|
||||
if not ref:
|
||||
return
|
||||
|
||||
if self._player and self._player.is_playing():
|
||||
self.emit("stop", None)
|
||||
|
||||
s_type = self._app.app_settings.setting_type
|
||||
req = HttpAPI.Request.STREAM if s_type is SettingsType.ENIGMA_2 else HttpAPI.Request.N_STREAM
|
||||
self._app.http_api.send(req, ref, self.watch)
|
||||
|
||||
def on_watch(self, item=None):
|
||||
""" Switch to the channel and watch in the player. """
|
||||
s_type = self._app.app_settings.setting_type
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
self._app.http_api.send(HttpAPI.Request.STREAM_CURRENT, "", self.watch)
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
self._app.http_api.send(HttpAPI.Request.N_ZAP, "",
|
||||
lambda rf: self._app.http_api.send(HttpAPI.Request.N_STREAM, rf.get("data", ""),
|
||||
self.watch))
|
||||
|
||||
def watch(self, data):
|
||||
url = self._app.get_url_from_m3u(data)
|
||||
GLib.timeout_add_seconds(1, self.play, url) if url else self.on_error(None, "Can't Playback!")
|
||||
|
||||
def play(self, url, title=None):
|
||||
if self._play_mode is PlayStreamsMode.M3U:
|
||||
self._app.save_stream_to_m3u(url)
|
||||
return
|
||||
|
||||
if self._play_mode is not self._app.app_settings.play_streams_mode:
|
||||
self.on_error(None, "Play mode has been changed!\nRestart the program to apply the settings.")
|
||||
return
|
||||
|
||||
if self._play_mode is PlayStreamsMode.BUILT_IN:
|
||||
self.show()
|
||||
elif self._play_mode is PlayStreamsMode.WINDOW:
|
||||
self.show_playback_window(title)
|
||||
|
||||
if self._player:
|
||||
self.emit("play", url)
|
||||
else:
|
||||
self._current_mrl = url
|
||||
|
||||
@run_idle
|
||||
def on_played(self, player, duration):
|
||||
self._fav_view.set_sensitive(True)
|
||||
if not IS_DARWIN:
|
||||
self.on_duration_changed(duration)
|
||||
|
||||
@run_idle
|
||||
def on_error(self, player, msg):
|
||||
self._app.show_error_message(msg)
|
||||
self._fav_view.set_sensitive(True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
2452
app/ui/satellites.glade
Normal file
2452
app/ui/satellites.glade
Normal file
File diff suppressed because it is too large
Load Diff
946
app/ui/satellites.py
Normal file
946
app/ui/satellites.py
Normal file
@@ -0,0 +1,946 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import concurrent.futures
|
||||
import re
|
||||
import time
|
||||
from math import fabs
|
||||
from pyexpat import ExpatError
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import run_idle, run_task, log
|
||||
from app.eparser import get_satellites, write_satellites, Satellite, Transponder
|
||||
from app.eparser.ecommons import PLS_MODE, get_key_by_value
|
||||
from app.tools.satellites import SatellitesParser, SatelliteSource, ServicesParser
|
||||
from .dialogs import show_dialog, DialogType, get_chooser_dialog, get_message, get_builder
|
||||
from .main_helper import move_items, append_text_to_tview, get_base_model, on_popup_menu
|
||||
from .search import SearchProvider
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, MOVE_KEYS, KeyboardKey, MOD_MASK
|
||||
|
||||
_UI_PATH = UI_RESOURCES_PATH + "satellites.glade"
|
||||
|
||||
|
||||
class SatellitesTool(Gtk.Box):
|
||||
_aggr = [None for x in range(9)] # aggregate
|
||||
|
||||
def __init__(self, app, settings, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._app = app
|
||||
self._settings = settings
|
||||
self._current_sat_path = None
|
||||
|
||||
handlers = {"on_remove": self.on_remove,
|
||||
"on_update": self.on_update,
|
||||
"on_up": self.on_up,
|
||||
"on_down": self.on_down,
|
||||
"on_button_press": self.on_button_press,
|
||||
"on_satellite_add": self.on_satellite_add,
|
||||
"on_transponder_add": self.on_transponder_add,
|
||||
"on_edit": self.on_edit,
|
||||
"on_key_release": self.on_key_release,
|
||||
"on_satellite_selection": self.on_satellite_selection}
|
||||
|
||||
builder = get_builder(_UI_PATH, handlers, use_str=True,
|
||||
objects=("satellite_editor_box", "satellite_view_model", "transponder_view_model",
|
||||
"satellite_popup_menu", "transponder_popup_menu", "left_header_menu",
|
||||
"popup_menu_add_image", "popup_menu_add_image_2"))
|
||||
|
||||
self._satellite_view = builder.get_object("satellite_view")
|
||||
self._transponder_view = builder.get_object("transponder_view")
|
||||
builder.get_object("sat_pos_column").set_cell_data_func(builder.get_object("sat_pos_renderer"),
|
||||
self.sat_pos_func)
|
||||
|
||||
self._stores = {3: builder.get_object("pol_store"),
|
||||
4: builder.get_object("fec_store"),
|
||||
5: builder.get_object("system_store"),
|
||||
6: builder.get_object("mod_store")}
|
||||
|
||||
self.pack_start(builder.get_object("satellite_editor_box"), True, True, 0)
|
||||
self._app.connect("profile-changed", lambda a, m: self.load_satellites_list())
|
||||
self.show()
|
||||
self.load_satellites_list()
|
||||
|
||||
def load_satellites_list(self, path=None):
|
||||
gen = self.on_satellites_list_load(path)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
@run_idle
|
||||
def on_open(self):
|
||||
response = get_chooser_dialog(self._app.app_window, self._settings, "satellites.xml", ("*.xml",))
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
|
||||
if not str(response).endswith("satellites.xml"):
|
||||
self._app.show_error_message("No satellites.xml file is selected!")
|
||||
return
|
||||
|
||||
self.load_satellites_list(response)
|
||||
|
||||
def on_satellite_selection(self, view):
|
||||
model = self._transponder_view.get_model()
|
||||
model.clear()
|
||||
|
||||
self._current_sat_path, column = view.get_cursor()
|
||||
if self._current_sat_path:
|
||||
list(map(model.append, view.get_model()[self._current_sat_path][-1]))
|
||||
|
||||
def on_up(self, item):
|
||||
move_items(KeyboardKey.UP, self._satellite_view)
|
||||
|
||||
def on_down(self, item):
|
||||
move_items(KeyboardKey.DOWN, self._satellite_view)
|
||||
|
||||
def on_button_press(self, menu, event):
|
||||
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS:
|
||||
self.on_edit(self._satellite_view if self._satellite_view.is_focus() else self._transponder_view)
|
||||
else:
|
||||
on_popup_menu(menu, event)
|
||||
|
||||
def on_key_release(self, view, event):
|
||||
""" Handling keystrokes """
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
return
|
||||
key = KeyboardKey(key_code)
|
||||
ctrl = event.state & MOD_MASK
|
||||
|
||||
if key is KeyboardKey.DELETE:
|
||||
self.on_remove(view)
|
||||
elif key is KeyboardKey.INSERT:
|
||||
pass
|
||||
elif ctrl and key is KeyboardKey.E:
|
||||
self.on_edit(view)
|
||||
elif ctrl and key is KeyboardKey.S:
|
||||
self.on_satellite()
|
||||
elif ctrl and key is KeyboardKey.T:
|
||||
self.on_transponder()
|
||||
elif ctrl and key in MOVE_KEYS:
|
||||
move_items(key, self._satellite_view)
|
||||
elif key is KeyboardKey.LEFT or key is KeyboardKey.RIGHT:
|
||||
view.do_unselect_all(view)
|
||||
|
||||
def on_satellites_list_load(self, path=None):
|
||||
""" Load satellites data into model """
|
||||
model = self._satellite_view.get_model()
|
||||
model.clear()
|
||||
|
||||
try:
|
||||
path = path or self._settings.profile_data_path + "satellites.xml"
|
||||
satellites = get_satellites(path)
|
||||
yield True
|
||||
except FileNotFoundError as e:
|
||||
msg = get_message("Please, download files from receiver or setup your path for read data!")
|
||||
self._app.show_error_message(f"{e}\n{msg}")
|
||||
except ExpatError as e:
|
||||
msg = f"The file [{path}] is not formatted correctly or contains invalid characters! Cause: {e}"
|
||||
self._app.show_error_message(msg)
|
||||
else:
|
||||
for sat in satellites:
|
||||
yield model.append(sat)
|
||||
|
||||
def on_add(self, view):
|
||||
""" Common adding """
|
||||
self.on_edit(view, force=True)
|
||||
|
||||
def on_satellite_add(self, item):
|
||||
self.on_satellite()
|
||||
|
||||
def on_transponder_add(self, item):
|
||||
self.on_transponder()
|
||||
|
||||
def on_edit(self, view, force=False):
|
||||
""" Common edit """
|
||||
paths = self.check_selection(view, "Please, select only one item!")
|
||||
if not paths:
|
||||
return
|
||||
|
||||
model = view.get_model()
|
||||
row = model[paths][:]
|
||||
itr = model.get_iter(paths)
|
||||
|
||||
if view is self._satellite_view:
|
||||
self.on_satellite(None if force else Satellite(*row), itr)
|
||||
elif view is self._transponder_view:
|
||||
self.on_transponder(None if force else Transponder(*row), itr)
|
||||
|
||||
def on_satellite(self, satellite=None, edited_itr=None):
|
||||
""" Create or edit satellite"""
|
||||
sat_dialog = SatelliteDialog(self._app.get_active_window(), satellite)
|
||||
sat = sat_dialog.run()
|
||||
sat_dialog.destroy()
|
||||
|
||||
if sat:
|
||||
model, paths = self._satellite_view.get_selection().get_selected_rows()
|
||||
if satellite and edited_itr:
|
||||
model.set(edited_itr, {i: v for i, v in enumerate(sat)})
|
||||
else:
|
||||
if len(model):
|
||||
index = paths[0].get_indices()[0] + 1
|
||||
model.insert(index, sat)
|
||||
else:
|
||||
model.append(sat)
|
||||
|
||||
def on_transponder(self, transponder=None, edited_itr=None):
|
||||
""" Create or edit transponder """
|
||||
|
||||
paths = self.check_selection(self._satellite_view, "Please, select only one satellite!")
|
||||
if paths is None:
|
||||
return
|
||||
elif len(paths) == 0:
|
||||
self._app.show_error_message("No satellite is selected!")
|
||||
return
|
||||
|
||||
dialog = TransponderDialog(self._app.get_active_window(), transponder)
|
||||
tr = dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
if tr:
|
||||
sat_model = self._satellite_view.get_model()
|
||||
transponders = sat_model[paths][-1]
|
||||
tr_model, tr_paths = self._transponder_view.get_selection().get_selected_rows()
|
||||
|
||||
if transponder and edited_itr:
|
||||
tr_model.set(edited_itr, {i: v for i, v in enumerate(tr)})
|
||||
transponders[tr_model.get_path(edited_itr).get_indices()[0]] = tr
|
||||
else:
|
||||
index = paths[0].get_indices()[0] + 1
|
||||
tr_model.insert(index, tr)
|
||||
transponders.insert(index, tr)
|
||||
|
||||
def check_selection(self, view, message):
|
||||
""" Checks if any row is selected. Shows error dialog if selected more than one.
|
||||
|
||||
Returns selected path or None.
|
||||
"""
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if len(paths) > 1:
|
||||
self._app.show_error_message(message)
|
||||
return
|
||||
|
||||
return paths
|
||||
|
||||
def on_remove(self, view):
|
||||
""" Removes selected satellites and transponders. """
|
||||
selection = view.get_selection()
|
||||
model, paths = selection.get_selected_rows()
|
||||
|
||||
if view is self._satellite_view:
|
||||
list(map(model.remove, [model.get_iter(path) for path in paths]))
|
||||
elif view is self._transponder_view:
|
||||
if self._current_sat_path:
|
||||
trs = self._satellite_view.get_model()[self._current_sat_path][-1]
|
||||
list(map(trs.pop, sorted(map(lambda p: p.get_indices()[0], paths), reverse=True)))
|
||||
list(map(model.remove, [model.get_iter(path) for path in paths]))
|
||||
else:
|
||||
self._app.show_error_message("No satellite is selected!")
|
||||
|
||||
def sat_pos_func(self, column, renderer, model, itr, data):
|
||||
""" Converts and sets the satellite position value to a readable format. """
|
||||
pos = int(model.get_value(itr, 2))
|
||||
renderer.set_property("text", f"{abs(pos / 10):0.1f}{'W' if pos < 0 else 'E'}")
|
||||
|
||||
@run_idle
|
||||
def on_save(self):
|
||||
if show_dialog(DialogType.QUESTION, self._app.app_window) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
write_satellites((Satellite(*r) for r in self._satellite_view.get_model()),
|
||||
self._settings.profile_data_path + "satellites.xml")
|
||||
|
||||
def on_save_as(self):
|
||||
show_dialog(DialogType.ERROR, transient=self._app.app_window, text="Not implemented yet!")
|
||||
|
||||
@run_idle
|
||||
def on_update(self, item):
|
||||
SatellitesUpdateDialog(self._app.get_active_window(), self._settings, self._satellite_view.get_model()).show()
|
||||
|
||||
|
||||
# ***************** Transponder dialog *******************#
|
||||
|
||||
class TransponderDialog:
|
||||
""" Shows dialog for adding or edit transponder """
|
||||
|
||||
def __init__(self, transient, transponder: Transponder = None):
|
||||
|
||||
handlers = {"on_entry_changed": self.on_entry_changed}
|
||||
objects = ("transponder_dialog", "pol_store", "fec_store", "mod_store", "system_store", "pls_mode_store")
|
||||
builder = get_builder(_UI_PATH, handlers, use_str=True, objects=objects)
|
||||
|
||||
self._dialog = builder.get_object("transponder_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._freq_entry = builder.get_object("freq_entry")
|
||||
self._rate_entry = builder.get_object("rate_entry")
|
||||
self._pol_box = builder.get_object("pol_box")
|
||||
self._fec_box = builder.get_object("fec_box")
|
||||
self._sys_box = builder.get_object("sys_box")
|
||||
self._mod_box = builder.get_object("mod_box")
|
||||
self._pls_mode_box = builder.get_object("pls_mode_box")
|
||||
self._pls_code_entry = builder.get_object("pls_code_entry")
|
||||
self._is_id_entry = builder.get_object("is_id_entry")
|
||||
self._t2mi_plp_id_entry = builder.get_object("t2mi_plp_id_entry")
|
||||
# pattern for frequency and rate entries (only digits)
|
||||
self._pattern = re.compile(r"\D")
|
||||
# style
|
||||
self._style_provider = Gtk.CssProvider()
|
||||
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
self._freq_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
self._rate_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
if transponder:
|
||||
self.init_transponder(transponder)
|
||||
|
||||
def run(self):
|
||||
while self._dialog.run() != Gtk.ResponseType.CANCEL:
|
||||
tr = self.to_transponder()
|
||||
if self.is_accept(tr):
|
||||
return tr
|
||||
show_dialog(DialogType.ERROR, self._dialog, "Please check your parameters and try again.")
|
||||
|
||||
def destroy(self):
|
||||
self._dialog.destroy()
|
||||
|
||||
def init_transponder(self, transponder):
|
||||
self._freq_entry.set_text(transponder.frequency)
|
||||
self._rate_entry.set_text(transponder.symbol_rate)
|
||||
self._pol_box.set_active_id(transponder.polarization)
|
||||
self._fec_box.set_active_id(transponder.fec_inner)
|
||||
self._sys_box.set_active_id(transponder.system)
|
||||
self._mod_box.set_active_id(transponder.modulation)
|
||||
self._pls_mode_box.set_active_id(PLS_MODE.get(transponder.pls_mode, None))
|
||||
self._is_id_entry.set_text(transponder.is_id if transponder.is_id else "")
|
||||
self._pls_code_entry.set_text(transponder.pls_code if transponder.pls_code else "")
|
||||
self._t2mi_plp_id_entry.set_text(transponder.t2mi_plp_id if transponder.t2mi_plp_id else "")
|
||||
|
||||
def to_transponder(self):
|
||||
return Transponder(frequency=self._freq_entry.get_text(),
|
||||
symbol_rate=self._rate_entry.get_text(),
|
||||
polarization=self._pol_box.get_active_id(),
|
||||
fec_inner=self._fec_box.get_active_id(),
|
||||
system=self._sys_box.get_active_id(),
|
||||
modulation=self._mod_box.get_active_id(),
|
||||
pls_mode=get_key_by_value(PLS_MODE, self._pls_mode_box.get_active_id()),
|
||||
pls_code=self._pls_code_entry.get_text(),
|
||||
is_id=self._is_id_entry.get_text(),
|
||||
t2mi_plp_id=self._t2mi_plp_id_entry.get_text())
|
||||
|
||||
def on_entry_changed(self, entry):
|
||||
entry.set_name("digit-entry" if self._pattern.search(entry.get_text()) else "GtkEntry")
|
||||
|
||||
def is_accept(self, tr):
|
||||
if self._pattern.search(tr.frequency) or not tr.frequency:
|
||||
return False
|
||||
elif self._pattern.search(tr.symbol_rate) or not tr.symbol_rate:
|
||||
return False
|
||||
elif None in (tr.polarization, tr.fec_inner, tr.system, tr.modulation):
|
||||
return False
|
||||
elif self._pattern.search(tr.pls_code) or self._pattern.search(tr.is_id):
|
||||
return False
|
||||
elif self._pattern.search(tr.t2mi_plp_id):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# ***************** Satellite dialog *******************#
|
||||
|
||||
class SatelliteDialog:
|
||||
""" Shows dialog for adding or edit satellite """
|
||||
|
||||
def __init__(self, transient, satellite=None):
|
||||
builder = get_builder(_UI_PATH, use_str=True, objects=("satellite_dialog", "side_store", "pos_adjustment"))
|
||||
|
||||
self._dialog = builder.get_object("satellite_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._sat_name = builder.get_object("sat_name_entry")
|
||||
self._sat_position = builder.get_object("sat_position_button")
|
||||
self._side = builder.get_object("side_box")
|
||||
self._transponders = satellite.transponders if satellite else []
|
||||
|
||||
if satellite:
|
||||
self._sat_name.set_text(satellite.name)
|
||||
pos = satellite.position
|
||||
pos = float(f"{pos[:-1]}.{pos[-1:]}")
|
||||
self._sat_position.set_value(fabs(pos))
|
||||
self._side.set_active(0 if pos >= 0 else 1) # E or W
|
||||
|
||||
def run(self):
|
||||
if self._dialog.run() == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
return self.to_satellite()
|
||||
|
||||
def destroy(self):
|
||||
self._dialog.destroy()
|
||||
|
||||
def to_satellite(self):
|
||||
name = self._sat_name.get_text()
|
||||
pos = round(self._sat_position.get_value(), 1)
|
||||
side = self._side.get_active()
|
||||
pos = "{}{}{}".format("-" if side == 1 else "", *str(pos).split("."))
|
||||
|
||||
return Satellite(name=name, flags="0", position=pos, transponders=self._transponders)
|
||||
|
||||
|
||||
# ********************** Update dialogs ************************ #
|
||||
|
||||
class UpdateDialog:
|
||||
""" Base dialog for update satellites, transponders and services from the web."""
|
||||
|
||||
def __init__(self, transient, settings, title=None):
|
||||
handlers = {"on_update_satellites_list": self.on_update_satellites_list,
|
||||
"on_receive_data": self.on_receive_data,
|
||||
"on_cancel_receive": self.on_cancel_receive,
|
||||
"on_satellite_toggled": self.on_satellite_toggled,
|
||||
"on_satellite_changed": self.on_satellite_changed,
|
||||
"on_transponder_toggled": self.on_transponder_toggled,
|
||||
"on_info_bar_close": self.on_info_bar_close,
|
||||
"on_filter_toggled": self.on_filter_toggled,
|
||||
"on_find_toggled": self.on_find_toggled,
|
||||
"on_popup_menu": on_popup_menu,
|
||||
"on_select_all": self.on_select_all,
|
||||
"on_unselect_all": self.on_unselect_all,
|
||||
"on_filter": self.on_filter,
|
||||
"on_quit": self.on_quit}
|
||||
|
||||
self._settings = settings
|
||||
self._download_task = False
|
||||
self._parser = None
|
||||
self._size_name = f"{'_'.join(re.findall('[A-Z][^A-Z]*', self.__class__.__name__))}_window_size".lower()
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "satellites.glade", handlers,
|
||||
objects=("satellites_update_window", "update_source_store", "update_sat_list_store",
|
||||
"update_sat_list_model_filter", "update_sat_list_model_sort", "side_store",
|
||||
"pos_adjustment", "pos_adjustment2", "satellites_update_popup_menu",
|
||||
"remove_selection_image", "sat_update_cancel_image", "sat_receive_image",
|
||||
"sat_update_image", "update_transponder_store", "update_service_store"))
|
||||
|
||||
self._window = builder.get_object("satellites_update_window")
|
||||
self._window.set_transient_for(transient)
|
||||
if title:
|
||||
self._window.set_title(title)
|
||||
|
||||
self._transponder_paned = builder.get_object("sat_update_tr_paned")
|
||||
self._sat_view = builder.get_object("sat_update_tree_view")
|
||||
self._transponder_view = builder.get_object("sat_update_tr_view")
|
||||
self._service_view = builder.get_object("sat_update_srv_view")
|
||||
self._source_box = builder.get_object("source_combo_box")
|
||||
self._text_view = builder.get_object("text_view")
|
||||
self._receive_button = builder.get_object("receive_data_button")
|
||||
self._sat_update_info_bar = builder.get_object("sat_update_info_bar")
|
||||
self._info_bar_message_label = builder.get_object("info_bar_message_label")
|
||||
self._satellites_count_label = builder.get_object("satellites_count_label")
|
||||
self._transponders_count_label = builder.get_object("transponders_count_label")
|
||||
self._services_count_label = builder.get_object("services_count_label")
|
||||
self._receive_button.bind_property("visible", builder.get_object("cancel_data_button"), "visible", 4)
|
||||
update_button = builder.get_object("sat_update_button")
|
||||
self._sat_view.bind_property("sensitive", update_button, "sensitive")
|
||||
self._sat_view.bind_property("sensitive", self._source_box, "sensitive")
|
||||
self._sat_view.bind_property("sensitive", self._source_box, "sensitive")
|
||||
self._sat_view.bind_property("sensitive", self._receive_button, "sensitive")
|
||||
self._receive_button.bind_property("visible", update_button, "visible")
|
||||
# Filter
|
||||
self._filter_bar = builder.get_object("sat_update_filter_bar")
|
||||
self._from_pos_button = builder.get_object("from_pos_button")
|
||||
self._to_pos_button = builder.get_object("to_pos_button")
|
||||
self._filter_from_combo_box = builder.get_object("filter_from_combo_box")
|
||||
self._filter_to_combo_box = builder.get_object("filter_to_combo_box")
|
||||
self._filter_model = builder.get_object("update_sat_list_model_filter")
|
||||
self._filter_model.set_visible_func(self.filter_function)
|
||||
self._filter_positions = (0, 0)
|
||||
self._filter_bar.bind_property("search-mode-enabled", self._filter_bar, "visible")
|
||||
# Log.
|
||||
self._log_frame = builder.get_object("log_frame")
|
||||
builder.get_object("log_info_bar").connect("response", lambda b, r: self._log_frame.set_visible(False))
|
||||
# Search.
|
||||
self._search_bar = builder.get_object("sat_update_search_bar")
|
||||
self._search_bar.bind_property("search-mode-enabled", self._search_bar, "visible")
|
||||
search_provider = SearchProvider(self._sat_view,
|
||||
builder.get_object("sat_update_search_entry"),
|
||||
builder.get_object("sat_update_search_down_button"),
|
||||
builder.get_object("sat_update_search_up_button"))
|
||||
builder.get_object("sat_update_find_button").connect("toggled", search_provider.on_search_toggled)
|
||||
|
||||
window_size = self._settings.get(self._size_name)
|
||||
if window_size:
|
||||
self._window.resize(*window_size)
|
||||
|
||||
def show(self):
|
||||
self._window.show()
|
||||
|
||||
@property
|
||||
def is_download(self):
|
||||
return self._download_task
|
||||
|
||||
@is_download.setter
|
||||
def is_download(self, value):
|
||||
self._download_task = value
|
||||
self._receive_button.set_visible(not value)
|
||||
|
||||
@run_idle
|
||||
def on_update_satellites_list(self, item=None):
|
||||
if self.is_download:
|
||||
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
|
||||
return
|
||||
|
||||
self.clear_data()
|
||||
|
||||
self.is_download = True
|
||||
self._sat_view.set_sensitive(False)
|
||||
src = self._source_box.get_active()
|
||||
if not self._parser:
|
||||
self._parser = SatellitesParser()
|
||||
|
||||
self.get_sat_list(src, self.append_satellites)
|
||||
|
||||
def clear_data(self):
|
||||
get_base_model(self._sat_view.get_model()).clear()
|
||||
self._transponder_view.get_model().clear()
|
||||
self._service_view.get_model().clear()
|
||||
self._satellites_count_label.set_text("0")
|
||||
self._transponders_count_label.set_text("0")
|
||||
self._services_count_label.set_text("0")
|
||||
|
||||
@run_task
|
||||
def get_sat_list(self, src, callback):
|
||||
sat_src = SatelliteSource.LYNGSAT
|
||||
if src == 1:
|
||||
sat_src = SatelliteSource.KINGOFSAT
|
||||
elif src == 2:
|
||||
sat_src = SatelliteSource.FLYSAT
|
||||
|
||||
sats = self._parser.get_satellites_list(sat_src)
|
||||
callback(sats)
|
||||
self.is_download = False
|
||||
|
||||
@run_idle
|
||||
def append_satellites(self, sats):
|
||||
model = get_base_model(self._sat_view.get_model())
|
||||
for sat in sats:
|
||||
model.append(sat)
|
||||
|
||||
self._sat_view.set_sensitive(True)
|
||||
self._satellites_count_label.set_text(str(len(model)))
|
||||
|
||||
@run_idle
|
||||
def on_receive_data(self, item):
|
||||
if self.is_download:
|
||||
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
|
||||
return
|
||||
|
||||
@run_idle
|
||||
def update_log_visibility(self):
|
||||
self._log_frame.set_visible(True)
|
||||
self._text_view.get_buffer().set_text("", 0)
|
||||
|
||||
def append_output(self):
|
||||
@run_idle
|
||||
def append(t):
|
||||
append_text_to_tview(t, self._text_view)
|
||||
|
||||
while True:
|
||||
text = yield
|
||||
append(text)
|
||||
|
||||
def on_cancel_receive(self, item=None):
|
||||
self._download_task = False
|
||||
|
||||
def on_satellite_changed(self, box):
|
||||
self.on_update_satellites_list()
|
||||
|
||||
def on_satellite_toggled(self, toggle, path):
|
||||
model = self._sat_view.get_model()
|
||||
self.update_state(model, path, not toggle.get_active())
|
||||
self.update_receive_button_state(self._filter_model)
|
||||
|
||||
def on_transponder_toggled(self, toggle, path):
|
||||
model = self._transponder_view.get_model()
|
||||
model.set_value(model.get_iter(path), 2, not toggle.get_active())
|
||||
|
||||
@run_idle
|
||||
def update_receive_button_state(self, model):
|
||||
self._receive_button.set_sensitive((any(r[4] for r in model)))
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._sat_update_info_bar.set_visible(True)
|
||||
self._sat_update_info_bar.set_message_type(message_type)
|
||||
self._info_bar_message_label.set_text(text)
|
||||
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
self._sat_update_info_bar.set_visible(False)
|
||||
|
||||
def on_find_toggled(self, button: Gtk.ToggleToolButton):
|
||||
self._search_bar.set_search_mode(button.get_active())
|
||||
|
||||
def on_filter_toggled(self, button: Gtk.ToggleToolButton):
|
||||
self._filter_bar.set_search_mode(button.get_active())
|
||||
|
||||
@run_idle
|
||||
def on_filter(self, item):
|
||||
self._filter_positions = self.get_positions()
|
||||
self._filter_model.refilter()
|
||||
|
||||
def filter_function(self, model, itr, data):
|
||||
if self._filter_model is None or self._filter_model == "None":
|
||||
return True
|
||||
|
||||
from_pos, to_pos = self._filter_positions
|
||||
if from_pos == 0 and to_pos == 0:
|
||||
return True
|
||||
|
||||
if from_pos > to_pos:
|
||||
from_pos, to_pos = to_pos, from_pos
|
||||
|
||||
return from_pos <= float(self._parser.get_position(model.get(itr, 1)[0])) <= to_pos
|
||||
|
||||
def get_positions(self):
|
||||
from_pos = round(self._from_pos_button.get_value(), 1) * (-1 if self._filter_from_combo_box.get_active() else 1)
|
||||
to_pos = round(self._to_pos_button.get_value(), 1) * (-1 if self._filter_to_combo_box.get_active() else 1)
|
||||
return from_pos, to_pos
|
||||
|
||||
def on_select_all(self, view):
|
||||
self.update_selection(view, True)
|
||||
|
||||
def on_unselect_all(self, view):
|
||||
self.update_selection(view, False)
|
||||
|
||||
def update_selection(self, view, select):
|
||||
model = view.get_model()
|
||||
view.get_model().foreach(lambda mod, path, itr: self.update_state(model, path, select))
|
||||
self.update_receive_button_state(self._filter_model)
|
||||
|
||||
def update_state(self, model, path, select):
|
||||
""" Updates checkbox state by given path in the list """
|
||||
itr = self._filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(model.get_iter(path)))
|
||||
self._filter_model.get_model().set_value(itr, 4, select)
|
||||
|
||||
def on_quit(self, window, event):
|
||||
self._settings.add(self._size_name, window.get_size())
|
||||
self.is_download = False
|
||||
|
||||
|
||||
class SatellitesUpdateDialog(UpdateDialog):
|
||||
""" Dialog for update satellites from the web. """
|
||||
|
||||
def __init__(self, transient, settings, main_model):
|
||||
super().__init__(transient=transient, settings=settings)
|
||||
|
||||
self._main_model = main_model
|
||||
self._source_box.connect("changed", self.on_update_satellites_list)
|
||||
|
||||
@run_idle
|
||||
def on_receive_data(self, item):
|
||||
if self.is_download:
|
||||
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
|
||||
return
|
||||
|
||||
self.receive_satellites()
|
||||
|
||||
@run_task
|
||||
def receive_satellites(self):
|
||||
self.is_download = True
|
||||
self.update_log_visibility()
|
||||
model = self._sat_view.get_model()
|
||||
start = time.time()
|
||||
|
||||
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
|
||||
text = "Processing: {}\n"
|
||||
sats = []
|
||||
appender = self.append_output()
|
||||
next(appender)
|
||||
futures = {executor.submit(self._parser.get_satellite, sat[:-1]): sat for sat in [r for r in model if r[4]]}
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
if not self.is_download:
|
||||
self.is_download = True
|
||||
executor.shutdown()
|
||||
appender.send("\nCanceled\n")
|
||||
appender.close()
|
||||
self.is_download = False
|
||||
return
|
||||
data = future.result()
|
||||
appender.send(text.format(data[0]))
|
||||
sats.append(data)
|
||||
|
||||
appender.send("-" * 75 + "\n")
|
||||
sat_count = len(sats)
|
||||
|
||||
sats = {s[0]: s for s in sats} # key = name, v = satellite
|
||||
|
||||
for row in self._main_model:
|
||||
pos = row[0]
|
||||
if pos in sats:
|
||||
sat = sats.pop(pos)
|
||||
appender.send(f"Updating satellite: {row[0]}\n")
|
||||
GLib.idle_add(self._main_model.set, row.iter, {i: v for i, v in enumerate(sat)})
|
||||
|
||||
for p, s in sats.items():
|
||||
appender.send(f"Adding satellite: {s.name}\n")
|
||||
self.append_satellite(s)
|
||||
|
||||
appender.send("-" * 75 + "\n")
|
||||
appender.send(f"Consumed: {time.time() - start:0.0f}s, {sat_count} satellites received.\n")
|
||||
appender.close()
|
||||
self.is_download = False
|
||||
|
||||
@run_idle
|
||||
def append_satellite(self, sat):
|
||||
self._main_model.append(sat)
|
||||
|
||||
|
||||
class ServicesUpdateDialog(UpdateDialog):
|
||||
""" Dialog for updating services from the web. """
|
||||
|
||||
def __init__(self, transient, settings, callback):
|
||||
super().__init__(transient=transient, settings=settings, title="Services update")
|
||||
|
||||
self._callback = callback
|
||||
self._satellite_paths = {}
|
||||
self._transponders = {}
|
||||
self._services = {}
|
||||
self._selected_transponders = set()
|
||||
self._services_parser = ServicesParser(source=SatelliteSource.LYNGSAT)
|
||||
# Transponder view popup menu
|
||||
tr_popup_menu = Gtk.Menu()
|
||||
select_all_item = Gtk.ImageMenuItem.new_from_stock("gtk-select-all")
|
||||
select_all_item.connect("activate", lambda w: self.update_transponder_selection(True))
|
||||
tr_popup_menu.append(select_all_item)
|
||||
remove_selection_item = Gtk.ImageMenuItem.new_from_stock("gtk-undo")
|
||||
remove_selection_item.set_label(get_message("Remove selection"))
|
||||
remove_selection_item.connect("activate", lambda w: self.update_transponder_selection(False))
|
||||
tr_popup_menu.append(remove_selection_item)
|
||||
tr_popup_menu.show_all()
|
||||
|
||||
self._sat_view.connect("row-activated", self.on_activate_satellite)
|
||||
self._transponder_view.connect("row-activated", self.on_activate_transponder)
|
||||
self._transponder_view.connect("button-press-event", lambda w, e: on_popup_menu(tr_popup_menu, e))
|
||||
self._transponder_view.connect("select_all", lambda w: self.update_transponder_selection(True))
|
||||
|
||||
self._transponder_paned.set_visible(True)
|
||||
self._source_box.connect("changed", self.on_update_satellites_list)
|
||||
|
||||
@run_idle
|
||||
def on_receive_data(self, item):
|
||||
if self.is_download:
|
||||
show_dialog(DialogType.ERROR, self._window, "The task is already running!")
|
||||
return
|
||||
|
||||
self.receive_services()
|
||||
|
||||
@run_task
|
||||
def receive_services(self):
|
||||
self.is_download = True
|
||||
self.update_log_visibility()
|
||||
model = self._sat_view.get_model()
|
||||
appender = self.append_output()
|
||||
next(appender)
|
||||
|
||||
start = time.time()
|
||||
non_cached_sats = []
|
||||
sat_names = {}
|
||||
t_names = {}
|
||||
t_urls = set()
|
||||
services = []
|
||||
|
||||
for r in (r for r in model if r[-1]):
|
||||
if not self.is_download:
|
||||
appender.send("\nCanceled\n")
|
||||
return
|
||||
|
||||
sat, url = r[0], r[3]
|
||||
trs = self._transponders.get(url, None)
|
||||
if trs:
|
||||
for t in filter(lambda tp: tp.url in self._selected_transponders, trs):
|
||||
t_urls.add(t.url)
|
||||
t_names[t.url] = t.text
|
||||
else:
|
||||
non_cached_sats.append(url)
|
||||
sat_names[url] = sat
|
||||
|
||||
if non_cached_sats:
|
||||
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
|
||||
futures = {executor.submit(self._services_parser.get_transponders_links, u): u for u in non_cached_sats}
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
if not self.is_download:
|
||||
appender.send("\nCanceled.\n")
|
||||
self.is_download = False
|
||||
return
|
||||
|
||||
appender.send(f"Getting transponders for: {sat_names.get(futures[future])}.\n")
|
||||
for t in future.result():
|
||||
t_urls.add(t.url)
|
||||
t_names[t.url] = t.text
|
||||
|
||||
appender.send("-" * 75 + "\n")
|
||||
appender.send(f"{len(t_urls)} transponders received.\n\n")
|
||||
|
||||
non_cached_ts = []
|
||||
for tr in t_urls:
|
||||
srvs = self._services.get(tr)
|
||||
services.extend(srvs) if srvs else non_cached_ts.append(tr)
|
||||
|
||||
if non_cached_ts:
|
||||
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
|
||||
futures = {executor.submit(self._services_parser.get_transponder_services, u): u for u in non_cached_ts}
|
||||
for future in concurrent.futures.as_completed(futures):
|
||||
if not self.is_download:
|
||||
appender.send("\nCanceled.\n")
|
||||
self.is_download = False
|
||||
return
|
||||
|
||||
appender.send(f"Getting services for: {t_names.get(futures[future], '')}.\n")
|
||||
try:
|
||||
list(map(services.append, future.result()))
|
||||
except ValueError as e:
|
||||
log(f"Getting services error: {e} [{t_names.get(futures[future])}]")
|
||||
|
||||
appender.send("-" * 75 + "\n")
|
||||
appender.send(f"Consumed: {time.time() - start:0.0f}s, {len(services)} services received.")
|
||||
|
||||
try:
|
||||
from app.eparser.enigma.lamedb import LameDbReader
|
||||
# Used for double check!
|
||||
reader = LameDbReader(path=None)
|
||||
srvs = reader.get_services_list("".join(reader.get_services_lines(services)))
|
||||
except ValueError as e:
|
||||
log(f"ServicesUpdateDialog [on receive data] error: {e}")
|
||||
else:
|
||||
self._callback(srvs)
|
||||
|
||||
self.is_download = False
|
||||
|
||||
@run_task
|
||||
def get_sat_list(self, src, callback):
|
||||
sat_src = SatelliteSource.LYNGSAT
|
||||
if src == 1:
|
||||
sat_src = SatelliteSource.KINGOFSAT
|
||||
|
||||
self._services_parser.source = sat_src
|
||||
sats = self._parser.get_satellites_list(sat_src)
|
||||
callback(sats)
|
||||
self.is_download = False
|
||||
|
||||
def on_satellite_toggled(self, toggle, path):
|
||||
model = self._sat_view.get_model()
|
||||
self.update_state(model, path, not toggle.get_active())
|
||||
self.update_receive_button_state(self._filter_model)
|
||||
|
||||
url = model.get_value(model.get_iter(path), 3)
|
||||
selected = toggle.get_active()
|
||||
transponders = self._transponders.get(url, None)
|
||||
|
||||
if transponders:
|
||||
for t in transponders:
|
||||
self._selected_transponders.add(t.url) if selected else self._selected_transponders.discard(t.url)
|
||||
|
||||
def on_transponder_toggled(self, toggle, path):
|
||||
model = self._transponder_view.get_model()
|
||||
itr = model.get_iter(path)
|
||||
active = not toggle.get_active()
|
||||
url = self.update_transponder_state(itr, model, active)
|
||||
|
||||
s_path = self._satellite_paths.get(url, None)
|
||||
if s_path:
|
||||
self.update_sat_state(model, s_path, active)
|
||||
|
||||
def update_sat_state(self, model, path, active):
|
||||
sat_model = self._sat_view.get_model()
|
||||
if active:
|
||||
self.update_state(sat_model, path, active)
|
||||
else:
|
||||
self.update_state(sat_model, path, any((r[-1] for r in model)))
|
||||
self.update_receive_button_state(self._filter_model)
|
||||
|
||||
def update_transponder_state(self, itr, model, active):
|
||||
model.set_value(itr, 2, active)
|
||||
url = model.get_value(itr, 1)
|
||||
self._selected_transponders.add(url) if active else self._selected_transponders.discard(url)
|
||||
return url
|
||||
|
||||
@run_task
|
||||
def on_activate_satellite(self, view, path, column):
|
||||
GLib.idle_add(self._transponder_view.get_model().clear)
|
||||
GLib.idle_add(self._service_view.get_model().clear)
|
||||
|
||||
model = view.get_model()
|
||||
itr = model.get_iter(path)
|
||||
url, selected = model.get_value(itr, 3), model.get_value(itr, 4)
|
||||
transponders = self._transponders.get(url, None)
|
||||
if transponders is None:
|
||||
GLib.idle_add(view.set_sensitive, False)
|
||||
transponders = self._services_parser.get_transponders_links(url)
|
||||
self._transponders[url] = transponders
|
||||
|
||||
for t in transponders:
|
||||
t_url = t.url
|
||||
self._satellite_paths[t_url] = path
|
||||
self._selected_transponders.add(t_url) if selected else self._selected_transponders.discard(t_url)
|
||||
|
||||
self.append_transponders(self._transponder_view.get_model(), transponders)
|
||||
|
||||
@run_idle
|
||||
def append_transponders(self, model, trs_list):
|
||||
model.clear()
|
||||
list(map(model.append, [(t.text, t.url, t.url in self._selected_transponders) for t in trs_list]))
|
||||
self._sat_view.set_sensitive(True)
|
||||
self._transponders_count_label.set_text(str(len(model)))
|
||||
|
||||
@run_task
|
||||
def on_activate_transponder(self, view, path, column):
|
||||
url = view.get_model()[path][1]
|
||||
services = self._services.get(url, None)
|
||||
if services is None:
|
||||
GLib.idle_add(view.set_sensitive, False)
|
||||
services = self._services_parser.get_transponder_services(url)
|
||||
self._services[url] = services
|
||||
|
||||
self.append_services(self._service_view.get_model(), services)
|
||||
|
||||
@run_idle
|
||||
def append_services(self, model, srv_list):
|
||||
model.clear()
|
||||
for s in srv_list:
|
||||
model.append((None, s.service, s.package, s.service_type, str(s.ssid), None))
|
||||
|
||||
self._transponder_view.set_sensitive(True)
|
||||
self._services_count_label.set_text(str(len(model)))
|
||||
|
||||
def update_transponder_selection(self, select):
|
||||
m = self._transponder_view.get_model()
|
||||
if not len(m):
|
||||
return
|
||||
|
||||
s_path = self._satellite_paths.get({self.update_transponder_state(r.iter, m, select) for r in m}.pop(), None)
|
||||
if s_path:
|
||||
self.update_sat_state(m, s_path, select)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,426 +0,0 @@
|
||||
import re
|
||||
from math import fabs
|
||||
|
||||
from app.commons import run_idle
|
||||
from app.eparser import get_satellites, write_satellites, Satellite, Transponder
|
||||
from . import Gtk, Gdk, UI_RESOURCES_PATH
|
||||
from .dialogs import show_dialog, DialogType
|
||||
from .main_helper import move_items, scroll_to
|
||||
|
||||
|
||||
def show_satellites_dialog(transient, options):
|
||||
dialog = SatellitesDialog(transient, options)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
|
||||
class SatellitesDialog:
|
||||
__slots__ = ["_dialog", "_data_path", "_stores", "_options", "_sat_view"]
|
||||
|
||||
_aggr = [None for x in range(9)] # aggregate
|
||||
|
||||
def __init__(self, transient, options):
|
||||
self._data_path = options.get("data_dir_path") + "satellites.xml"
|
||||
self._options = options
|
||||
|
||||
handlers = {"on_open": self.on_open,
|
||||
"on_remove": self.on_remove,
|
||||
"on_save": self.on_save,
|
||||
"on_up": self.on_up,
|
||||
"on_down": self.on_down,
|
||||
"on_popup_menu": self.on_popup_menu,
|
||||
"on_satellite_add": self.on_satellite_add,
|
||||
"on_transponder_add": self.on_transponder_add,
|
||||
"on_edit": self.on_edit,
|
||||
"on_key_release": self.on_key_release,
|
||||
"on_row_activated": self.on_row_activated,
|
||||
"on_resize": self.on_resize,
|
||||
"on_quit": self.on_quit}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
|
||||
("satellites_editor_dialog", "satellites_tree_store",
|
||||
"popup_menu", "add_popup_menu", "add_menu_icon"))
|
||||
builder.connect_signals(handlers)
|
||||
# Adding custom image for add_menu_tool_button
|
||||
add_menu_tool_button = builder.get_object("add_menu_tool_button")
|
||||
add_menu_tool_button.set_image(builder.get_object("add_menu_icon"))
|
||||
|
||||
self._dialog = builder.get_object("satellites_editor_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._dialog.get_content_area().set_border_width(0) # The width of the border around the app dialog area!
|
||||
self._sat_view = builder.get_object("satellites_editor_tree_view")
|
||||
# Setting the last size of the dialog window if it was saved
|
||||
window_size = self._options.get("sat_editor_window_size", None)
|
||||
if window_size:
|
||||
self._dialog.resize(*window_size)
|
||||
|
||||
self._stores = {3: builder.get_object("pol_store"),
|
||||
4: builder.get_object("fec_store"),
|
||||
5: builder.get_object("system_store"),
|
||||
6: builder.get_object("mod_store")}
|
||||
self.on_satellites_list_load(self._sat_view.get_model())
|
||||
|
||||
def run(self):
|
||||
self._dialog.run()
|
||||
|
||||
def destroy(self):
|
||||
self._dialog.destroy()
|
||||
|
||||
def on_resize(self, window):
|
||||
""" Stores new size properties for dialog window after resize """
|
||||
if self._options:
|
||||
self._options["sat_editor_window_size"] = window.get_size()
|
||||
|
||||
def on_quit(self, item):
|
||||
self.destroy()
|
||||
|
||||
def on_open(self, model):
|
||||
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._dialog,
|
||||
options=self._options,
|
||||
action_type=Gtk.FileChooserAction.OPEN,
|
||||
file_filter=file_filter)
|
||||
if response == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
if not str(response).endswith("satellites.xml"):
|
||||
show_dialog(DialogType.ERROR, self._dialog, text="No satellites.xml file is selected!")
|
||||
return
|
||||
self._data_path = response
|
||||
self.on_satellites_list_load(model)
|
||||
|
||||
@staticmethod
|
||||
def on_row_activated(view, path, column):
|
||||
if view.row_expanded(path):
|
||||
view.collapse_row(path)
|
||||
else:
|
||||
view.expand_row(path, column)
|
||||
|
||||
def on_up(self, item):
|
||||
move_items(Gdk.KEY_Up, self._sat_view)
|
||||
|
||||
def on_down(self, item):
|
||||
move_items(Gdk.KEY_Down, self._sat_view)
|
||||
|
||||
def on_key_release(self, view, event):
|
||||
""" Handling keystrokes """
|
||||
key = event.keyval
|
||||
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
|
||||
|
||||
if key == Gdk.KEY_Delete:
|
||||
self.on_remove(view)
|
||||
elif key == Gdk.KEY_Insert:
|
||||
pass
|
||||
elif ctrl and key == Gdk.KEY_E or key == Gdk.KEY_e:
|
||||
self.on_edit(view)
|
||||
elif ctrl and key == Gdk.KEY_s or key == Gdk.KEY_S:
|
||||
self.on_satellite()
|
||||
elif ctrl and key == Gdk.KEY_t or key == Gdk.KEY_T:
|
||||
self.on_transponder()
|
||||
elif key == Gdk.KEY_space:
|
||||
pass
|
||||
elif ctrl and key in (Gdk.KEY_Up, Gdk.KEY_Page_Up, Gdk.KEY_KP_Page_Up): # KEY_KP_Page_Up for laptop!
|
||||
move_items(key, self._sat_view)
|
||||
elif ctrl and key in (Gdk.KEY_Down, Gdk.KEY_Page_Down, Gdk.KEY_KP_Page_Down):
|
||||
move_items(key, self._sat_view)
|
||||
|
||||
@run_idle
|
||||
def on_satellites_list_load(self, model):
|
||||
""" Load satellites data into model """
|
||||
try:
|
||||
satellites = get_satellites(self._data_path)
|
||||
except FileNotFoundError as e:
|
||||
show_dialog(DialogType.ERROR, self._dialog, getattr(e, "message", str(e)) +
|
||||
"\n\nPlease, download files from receiver or setup your path for read data!")
|
||||
else:
|
||||
model.clear()
|
||||
self.append_data(model, satellites)
|
||||
|
||||
@run_idle
|
||||
def append_data(self, model, satellites):
|
||||
for name, flags, pos, transponders in satellites:
|
||||
parent = model.append(None, [name, *self._aggr, flags, pos])
|
||||
for transponder in transponders:
|
||||
model.append(parent, ["Transponder:", *transponder, None, None])
|
||||
|
||||
def on_add(self, view):
|
||||
""" Common adding """
|
||||
self.on_edit(view, force=True)
|
||||
|
||||
def on_satellite_add(self, item):
|
||||
self.on_satellite(None)
|
||||
|
||||
def on_transponder_add(self, item):
|
||||
self.on_transponder(None)
|
||||
|
||||
def on_edit(self, view, force=False):
|
||||
""" Common edit """
|
||||
paths = self.check_selection(view, "Please, select only one item!")
|
||||
if not paths:
|
||||
return
|
||||
|
||||
model = view.get_model()
|
||||
itr = model.get_iter(paths[0])
|
||||
row = model.get(itr, *[x for x in range(view.get_n_columns())])
|
||||
|
||||
if row[-1]: # satellite
|
||||
self.on_satellite(None if force else Satellite(row[0], None, row[-1], None), itr)
|
||||
else:
|
||||
self.on_transponder(None if force else Transponder(*row[1:-2]), itr)
|
||||
|
||||
def on_satellite(self, satellite=None, edited_itr=None):
|
||||
""" Create or edit satellite"""
|
||||
sat_dialog = SatelliteDialog(self._dialog, satellite)
|
||||
sat = sat_dialog.run()
|
||||
sat_dialog.destroy()
|
||||
|
||||
if sat:
|
||||
view = self._sat_view
|
||||
model = view.get_model()
|
||||
if satellite and edited_itr:
|
||||
model.set(edited_itr, {0: sat.name, 10: sat.flags, 11: sat.position})
|
||||
else:
|
||||
index = self.get_sat_position_index(sat.position, model)
|
||||
model.insert(None, index, [sat.name, *self._aggr, sat.flags, sat.position])
|
||||
scroll_to(index, view)
|
||||
|
||||
def on_transponder(self, transponder=None, edited_itr=None):
|
||||
""" Create or edit transponder """
|
||||
|
||||
paths = self.check_selection(self._sat_view, "Please, select only one satellite!")
|
||||
if paths is None:
|
||||
return
|
||||
elif len(paths) == 0:
|
||||
show_dialog(DialogType.ERROR, self._dialog, "No satellite is selected!")
|
||||
return
|
||||
|
||||
dialog = TransponderDialog(self._dialog, transponder)
|
||||
tr = dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
if tr:
|
||||
view = self._sat_view
|
||||
model = view.get_model()
|
||||
if transponder and edited_itr:
|
||||
model.set(edited_itr, {1: tr.frequency, 2: tr.symbol_rate, 3: tr.polarization,
|
||||
4: tr.fec_inner, 5: tr.system, 6: tr.modulation,
|
||||
7: tr.pls_mode, 8: tr.pls_code, 9: tr.is_id})
|
||||
else:
|
||||
row = ["Transponder:", *tr, None, None]
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
itr = model.get_iter(paths[0])
|
||||
view.expand_row(paths[0], 0)
|
||||
# Get parent iter if selected transponder
|
||||
parent_itr = model.iter_parent(itr)
|
||||
if parent_itr:
|
||||
itr = parent_itr
|
||||
freq = int(tr.frequency if tr.frequency else 0)
|
||||
tr_itr = model.iter_children(itr)
|
||||
# Inserting according to frequency value.
|
||||
while tr_itr:
|
||||
cur_freq = int(model.get_value(tr_itr, 1))
|
||||
if freq <= cur_freq:
|
||||
path = model.get_path(tr_itr)
|
||||
index = path.get_indices()[1]
|
||||
model.insert(model.iter_parent(tr_itr), index, row)
|
||||
scroll_to(path, view)
|
||||
break
|
||||
else:
|
||||
tr_itr = model.iter_next(tr_itr)
|
||||
else:
|
||||
itr = model.append(itr, row)
|
||||
scroll_to(model.get_path(itr), view)
|
||||
|
||||
def get_sat_position_index(self, pos, model):
|
||||
""" Search and returns index after given position """
|
||||
pos = int(pos)
|
||||
row = next(filter(lambda r: int(r[-1]) >= pos, model), None)
|
||||
|
||||
return row.path[0] if row else len(model)
|
||||
|
||||
def check_selection(self, view, message):
|
||||
""" Checks if any row is selected. Shows error dialog if selected more than one.
|
||||
|
||||
returns selected path or None
|
||||
"""
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
paths_count = len(paths)
|
||||
|
||||
if paths_count > 1:
|
||||
show_dialog(DialogType.ERROR, self._dialog, message)
|
||||
return
|
||||
|
||||
return paths
|
||||
|
||||
@staticmethod
|
||||
def on_remove(view):
|
||||
selection = view.get_selection()
|
||||
model, paths = selection.get_selected_rows()
|
||||
itrs = [model.get_iter(path) for path in paths]
|
||||
|
||||
for itr in itrs:
|
||||
model.remove(itr)
|
||||
|
||||
def on_save(self, view):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
model = view.get_model()
|
||||
satellites = []
|
||||
model.foreach(self.parse_data, satellites)
|
||||
write_satellites(satellites, self._data_path)
|
||||
|
||||
@staticmethod
|
||||
def parse_data(model, path, itr, sats):
|
||||
if model.iter_has_child(itr):
|
||||
num_of_children = model.iter_n_children(itr)
|
||||
transponders = []
|
||||
num_columns = model.get_n_columns()
|
||||
|
||||
for num in range(num_of_children):
|
||||
transponder_itr = model.iter_nth_child(itr, num)
|
||||
transponder = model.get(transponder_itr, *[item for item in range(num_columns)])
|
||||
transponders.append(Transponder(*transponder[1:-2]))
|
||||
|
||||
sat = model.get(itr, *[item for item in range(num_columns)])
|
||||
satellite = Satellite(sat[0], sat[-2], sat[-1], transponders)
|
||||
sats.append(satellite)
|
||||
|
||||
@staticmethod
|
||||
def on_popup_menu(menu, event):
|
||||
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
|
||||
menu.popup(None, None, None, None, event.button, event.time)
|
||||
|
||||
|
||||
class TransponderDialog:
|
||||
""" Shows dialog for adding or edit transponder """
|
||||
|
||||
def __init__(self, transient, transponder: Transponder = None):
|
||||
|
||||
handlers = {"on_entry_changed": self.on_entry_changed}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
|
||||
("transponder_dialog",
|
||||
"pol_store", "fec_store",
|
||||
"mod_store", "system_store",
|
||||
"pls_mode_store"))
|
||||
builder.connect_signals(handlers)
|
||||
|
||||
self._dialog = builder.get_object("transponder_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._freq_entry = builder.get_object("freq_entry")
|
||||
self._rate_entry = builder.get_object("rate_entry")
|
||||
self._pol_box = builder.get_object("pol_box")
|
||||
self._fec_box = builder.get_object("fec_box")
|
||||
self._sys_box = builder.get_object("sys_box")
|
||||
self._mod_box = builder.get_object("mod_box")
|
||||
self._pls_mode_box = builder.get_object("pls_mode_box")
|
||||
self._pls_code_entry = builder.get_object("pls_code_entry")
|
||||
self._is_id_entry = builder.get_object("is_id_entry")
|
||||
# pattern for frequency and rate entries (only digits)
|
||||
self._pattern = re.compile("\D")
|
||||
# style
|
||||
self._style_provider = Gtk.CssProvider()
|
||||
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
self._freq_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
self._rate_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
if transponder:
|
||||
self.init_transponder(transponder)
|
||||
|
||||
def run(self):
|
||||
while self._dialog.run() != Gtk.ResponseType.CANCEL:
|
||||
tr = self.to_transponder()
|
||||
if self.is_accept(tr):
|
||||
return tr
|
||||
show_dialog(DialogType.ERROR, self._dialog, "Please check your parameters and try again.")
|
||||
|
||||
def destroy(self):
|
||||
self._dialog.destroy()
|
||||
|
||||
def init_transponder(self, transponder):
|
||||
self._freq_entry.set_text(transponder.frequency)
|
||||
self._rate_entry.set_text(transponder.symbol_rate)
|
||||
self._pol_box.set_active_id(transponder.polarization)
|
||||
self._fec_box.set_active_id(transponder.fec_inner)
|
||||
self._sys_box.set_active_id(transponder.system)
|
||||
self._mod_box.set_active_id(transponder.modulation)
|
||||
self._pls_mode_box.set_active_id(transponder.pls_mode)
|
||||
self._is_id_entry.set_text(transponder.is_id if transponder.is_id else "")
|
||||
self._pls_code_entry.set_text(transponder.pls_code if transponder.pls_code else "")
|
||||
|
||||
def to_transponder(self):
|
||||
return Transponder(frequency=self._freq_entry.get_text(),
|
||||
symbol_rate=self._rate_entry.get_text(),
|
||||
polarization=self._pol_box.get_active_id(),
|
||||
fec_inner=self._fec_box.get_active_id(),
|
||||
system=self._sys_box.get_active_id(),
|
||||
modulation=self._mod_box.get_active_id(),
|
||||
pls_mode=self._pls_mode_box.get_active_id(),
|
||||
pls_code=self._pls_code_entry.get_text(),
|
||||
is_id=self._is_id_entry.get_text())
|
||||
|
||||
def on_entry_changed(self, entry):
|
||||
entry.set_name("digit-entry" if self._pattern.search(entry.get_text()) else "GtkEntry")
|
||||
|
||||
def is_accept(self, tr):
|
||||
if self._pattern.search(tr.frequency) or not tr.frequency:
|
||||
return False
|
||||
elif self._pattern.search(tr.symbol_rate) or not tr.symbol_rate:
|
||||
return False
|
||||
elif None in (tr.polarization, tr.fec_inner, tr.system, tr.modulation):
|
||||
return False
|
||||
elif self._pattern.search(tr.pls_code) or self._pattern.search(tr.is_id):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class SatelliteDialog:
|
||||
""" Shows dialog for adding or edit satellite """
|
||||
|
||||
def __init__(self, transient, satellite: Satellite = None):
|
||||
builder = Gtk.Builder()
|
||||
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
|
||||
("satellite_dialog", "side_store", "pos_adjustment"))
|
||||
|
||||
self._dialog = builder.get_object("satellite_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._sat_name = builder.get_object("sat_name_entry")
|
||||
self._sat_position = builder.get_object("sat_position_button")
|
||||
self._side = builder.get_object("side_box")
|
||||
|
||||
if satellite:
|
||||
self._sat_name.set_text(satellite.name[0:satellite.name.find("(")].strip())
|
||||
pos = satellite.position
|
||||
pos = float("{}.{}".format(pos[:-1], pos[-1:]))
|
||||
self._sat_position.set_value(fabs(pos))
|
||||
self._side.set_active(0 if pos >= 0 else 1) # E or W
|
||||
|
||||
def run(self):
|
||||
if self._dialog.run() == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
return self.to_satellite()
|
||||
|
||||
def destroy(self):
|
||||
self._dialog.destroy()
|
||||
|
||||
def to_satellite(self):
|
||||
name = self._sat_name.get_text()
|
||||
pos = round(self._sat_position.get_value(), 1)
|
||||
side = self._side.get_active()
|
||||
name = "{} ({}{})".format(name, pos, self._side.get_active_id())
|
||||
pos = "{}{}{}".format("-" if side == 1 else "", *str(pos).split("."))
|
||||
|
||||
return Satellite(name=name, flags="0", position=pos, transponders=None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
73
app/ui/search.py
Normal file
73
app/ui/search.py
Normal file
@@ -0,0 +1,73 @@
|
||||
""" This is helper module for search features """
|
||||
from app.commons import run_with_delay
|
||||
|
||||
|
||||
class SearchProvider:
|
||||
def __init__(self, view, entry, down_button, up_button, columns=None):
|
||||
self._paths = []
|
||||
self._current_index = -1
|
||||
self._max_indexes = 0
|
||||
self._view = view
|
||||
self._entry = entry
|
||||
self._up_button = up_button
|
||||
self._down_button = down_button
|
||||
self._columns = columns
|
||||
|
||||
entry.connect("changed", self.on_search)
|
||||
self._down_button.connect("clicked", self.on_search_down)
|
||||
self._up_button.connect("clicked", self.on_search_up)
|
||||
|
||||
def search(self, text):
|
||||
self._current_index = -1
|
||||
self._paths.clear()
|
||||
model = self._view.get_model()
|
||||
selection = self._view.get_selection()
|
||||
if not selection:
|
||||
return
|
||||
|
||||
selection.unselect_all()
|
||||
if not text:
|
||||
return
|
||||
|
||||
text = text.upper()
|
||||
for r in model:
|
||||
data = [r[i] for i in self._columns] if self._columns else r[:]
|
||||
if next((s for s in data if text in str(s).upper()), False):
|
||||
path = r.path
|
||||
selection.select_path(r.path)
|
||||
self._paths.append(path)
|
||||
|
||||
self._max_indexes = len(self._paths) - 1
|
||||
if self._max_indexes > 0:
|
||||
self.on_search_down()
|
||||
|
||||
self.update_navigation_buttons()
|
||||
|
||||
def scroll_to(self, index):
|
||||
self._view.scroll_to_cell(self._paths[index], None)
|
||||
self.update_navigation_buttons()
|
||||
|
||||
def on_search_down(self, button=None):
|
||||
if self._current_index < self._max_indexes:
|
||||
self._current_index += 1
|
||||
self.scroll_to(self._current_index)
|
||||
|
||||
def on_search_up(self, button=None):
|
||||
if self._current_index > -1:
|
||||
self._current_index -= 1
|
||||
self.scroll_to(self._current_index)
|
||||
|
||||
def update_navigation_buttons(self):
|
||||
self._up_button.set_sensitive(self._current_index > 0)
|
||||
self._down_button.set_sensitive(self._current_index < self._max_indexes)
|
||||
|
||||
@run_with_delay(1)
|
||||
def on_search(self, entry):
|
||||
self.search(entry.get_text())
|
||||
|
||||
def on_search_toggled(self, action, value=None):
|
||||
self._entry.grab_focus() if action.get_active() else self._entry.set_text("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
1877
app/ui/service_details_dialog.glade
Normal file
1877
app/ui/service_details_dialog.glade
Normal file
File diff suppressed because it is too large
Load Diff
957
app/ui/service_details_dialog.py
Normal file
957
app/ui/service_details_dialog.py
Normal file
@@ -0,0 +1,957 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from app.commons import run_idle, log
|
||||
from app.eparser import Service
|
||||
from app.eparser.ecommons import (MODULATION, Inversion, ROLL_OFF, Pilot, Flag, Pids, POLARIZATION, get_key_by_value,
|
||||
get_value_by_name, FEC_DEFAULT, PLS_MODE, SERVICE_TYPE, T_MODULATION, C_MODULATION,
|
||||
TrType, SystemCable, T_SYSTEM, BANDWIDTH, TRANSMISSION_MODE, GUARD_INTERVAL, T_FEC,
|
||||
HIERARCHY, A_MODULATION)
|
||||
from app.eparser.neutrino import get_attributes, SP, KSP
|
||||
from app.settings import SettingsType
|
||||
from .dialogs import show_dialog, DialogType, Action, get_builder
|
||||
from .main_helper import get_base_model
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, CODED_ICON, Column
|
||||
|
||||
_UI_PATH = UI_RESOURCES_PATH + "service_details_dialog.glade"
|
||||
|
||||
|
||||
class ServiceDetailsDialog:
|
||||
_ENIGMA2_DATA_ID = "{:04x}:{:08x}:{:04x}:{:04x}:{}:{}"
|
||||
|
||||
_ENIGMA2_FAV_ID = "{:X}:{:X}:{:X}:{:X}"
|
||||
|
||||
_ENIGMA2_TRANSPONDER_DATA = "{} {}:{}:{}:{}:{}:{}:{}"
|
||||
|
||||
_NEUTRINO_FAV_ID = "{:x}:{:x}:{:x}"
|
||||
|
||||
_DIGIT_ENTRY_ELEMENTS = ("bitstream_entry", "pcm_entry", "video_pid_entry", "pcr_pid_entry", "srv_type_entry",
|
||||
"ac3_pid_entry", "ac3plus_pid_entry", "acc_pid_entry", "he_acc_pid_entry",
|
||||
"teletext_pid_entry", "pls_code_entry", "stream_id_entry", "tr_flag_entry",
|
||||
"audio_pid_entry")
|
||||
_NOT_EMPTY_DIGIT_ELEMENTS = ("sid_entry", "freq_entry", "rate_entry", "transponder_id_entry", "network_id_entry",
|
||||
"namespace_entry", "srv_type_entry")
|
||||
|
||||
_DIGIT_ENTRY_NAME = "digit-entry"
|
||||
|
||||
def __init__(self, app, new_color, action=Action.EDIT):
|
||||
handlers = {"on_system_changed": self.on_system_changed,
|
||||
"on_save": self.on_save,
|
||||
"on_create_new": self.on_create_new,
|
||||
"on_tr_edit_toggled": self.on_tr_edit_toggled,
|
||||
"update_reference": self.update_reference,
|
||||
"on_cas_entry_changed": self.on_cas_entry_changed,
|
||||
"on_extra_pids_entry_changed": self.on_extra_pids_entry_changed,
|
||||
"on_digit_entry_changed": self.on_digit_entry_changed,
|
||||
"on_non_empty_entry_changed": self.on_non_empty_entry_changed,
|
||||
"on_cancel": lambda item: self._dialog.destroy()}
|
||||
|
||||
builder = get_builder(_UI_PATH, handlers, use_str=True)
|
||||
self._builder = builder
|
||||
settings = app.app_settings
|
||||
|
||||
self._dialog = builder.get_object("service_details_dialog")
|
||||
self._dialog.set_transient_for(app.app_window)
|
||||
self._s_type = settings.setting_type
|
||||
self._tr_type = TrType.Satellite
|
||||
self._picons_path = settings.profile_picons_path
|
||||
self._services_view = app.services_view
|
||||
self._fav_view = app.fav_view
|
||||
self._action = action
|
||||
self._old_service = None
|
||||
self._services = app.current_services
|
||||
self._bouquets = app.current_bouquets
|
||||
self._new_color = new_color
|
||||
self._transponder_services_iters = None
|
||||
self._current_model = None
|
||||
self._current_itr = None
|
||||
# Patterns
|
||||
self._DIGIT_PATTERN = re.compile("\\D")
|
||||
self._NON_EMPTY_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
|
||||
self._CAID_PATTERN = re.compile("(?:^[\\s]*$)|(C:[0-9a-fA-F]{1,4})(,C:[0-9a-fA-F]{1,4})*")
|
||||
self._PIDS_PATTERN = re.compile("(?:^[\\s]*$)|(c:[0-9]{2}[0-9a-fA-F]{4})(,c:[0-9]{2}[0-9a-fA-F]{4})*")
|
||||
# Buttons
|
||||
self._apply_button = builder.get_object("apply_button")
|
||||
self._create_button = builder.get_object("create_button")
|
||||
# style
|
||||
self._style_provider = Gtk.CssProvider()
|
||||
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
# initialization only digit elements
|
||||
self._digit_elements = {k: builder.get_object(k) for k in self._DIGIT_ENTRY_ELEMENTS}
|
||||
for elem in self._digit_elements.values():
|
||||
elem.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
# initialization of non empty elements
|
||||
self._non_empty_elements = {k: builder.get_object(k) for k in self._NOT_EMPTY_DIGIT_ELEMENTS}
|
||||
for elem in self._non_empty_elements.values():
|
||||
elem.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
self._sid_entry = self._non_empty_elements.get("sid_entry")
|
||||
self._bitstream_entry = self._digit_elements.get("bitstream_entry")
|
||||
self._pcm_entry = self._digit_elements.get("pcm_entry")
|
||||
self._video_pid_entry = self._digit_elements.get("video_pid_entry")
|
||||
self._pcr_pid_entry = self._digit_elements.get("pcr_pid_entry")
|
||||
self._audio_pid_entry = self._digit_elements.get("audio_pid_entry")
|
||||
self._ac3_pid_entry = self._digit_elements.get("ac3_pid_entry")
|
||||
self._ac3plus_pid_entry = self._digit_elements.get("ac3plus_pid_entry")
|
||||
self._acc_pid_entry = self._digit_elements.get("acc_pid_entry")
|
||||
self._he_acc_pid_entry = self._digit_elements.get("he_acc_pid_entry")
|
||||
self._teletext_pid_entry = self._digit_elements.get("teletext_pid_entry")
|
||||
self._transponder_id_entry = self._non_empty_elements.get("transponder_id_entry")
|
||||
self._network_id_entry = self._non_empty_elements.get("network_id_entry")
|
||||
self._freq_entry = self._non_empty_elements.get("freq_entry")
|
||||
self._rate_entry = self._non_empty_elements.get("rate_entry")
|
||||
self._pls_code_entry = self._digit_elements.get("pls_code_entry")
|
||||
self._stream_id_entry = self._digit_elements.get("stream_id_entry")
|
||||
self._tr_flag_entry = self._digit_elements.get("tr_flag_entry")
|
||||
self._namespace_entry = self._non_empty_elements.get("namespace_entry")
|
||||
self._extra_pids_entry = builder.get_object("extra_pids_entry")
|
||||
# Service elements
|
||||
self._name_entry = builder.get_object("name_entry")
|
||||
self._package_entry = builder.get_object("package_entry")
|
||||
self._srv_type_entry = self._non_empty_elements.get("srv_type_entry")
|
||||
self._service_type_combo_box = builder.get_object("service_type_combo_box")
|
||||
self._cas_entry = builder.get_object("cas_entry")
|
||||
self._reference_entry = builder.get_object("reference_entry")
|
||||
self._keep_check_button = builder.get_object("keep_check_button")
|
||||
self._hide_check_button = builder.get_object("hide_check_button")
|
||||
self._use_pids_check_button = builder.get_object("use_pids_check_button")
|
||||
self._new_check_button = builder.get_object("new_check_button")
|
||||
self._pids_grid = builder.get_object("pids_grid")
|
||||
# Transponder elements
|
||||
self._sat_pos_button = builder.get_object("sat_pos_button")
|
||||
self._pos_side_box = builder.get_object("pos_side_box")
|
||||
self._pol_combo_box = builder.get_object("pol_combo_box")
|
||||
self._fec_combo_box = builder.get_object("fec_combo_box")
|
||||
self._rate_lp_combo_box = builder.get_object("rate_lp_combo_box")
|
||||
self._sys_combo_box = builder.get_object("sys_combo_box")
|
||||
self._mod_combo_box = builder.get_object("mod_combo_box")
|
||||
self._invertion_combo_box = builder.get_object("invertion_combo_box")
|
||||
self._rolloff_combo_box = builder.get_object("rolloff_combo_box")
|
||||
self._pilot_combo_box = builder.get_object("pilot_combo_box")
|
||||
self._pls_mode_combo_box = builder.get_object("pls_mode_combo_box")
|
||||
self._tr_edit_switch = builder.get_object("tr_edit_switch")
|
||||
self._tr_extra_expander = builder.get_object("tr_extra_expander")
|
||||
|
||||
self._DVB_S2_ELEMENTS = (self._mod_combo_box, self._rolloff_combo_box, self._pilot_combo_box,
|
||||
self._pls_mode_combo_box, self._pls_code_entry, self._stream_id_entry)
|
||||
self._TRANSPONDER_ELEMENTS = (self._sat_pos_button, self._pol_combo_box, self._invertion_combo_box,
|
||||
self._sys_combo_box, self._freq_entry, self._transponder_id_entry,
|
||||
self._network_id_entry, self._namespace_entry, self._fec_combo_box,
|
||||
self._rate_entry, self._rate_lp_combo_box, self._pos_side_box)
|
||||
|
||||
if self._action is Action.EDIT:
|
||||
self.update_data_elements()
|
||||
elif self._action is Action.ADD:
|
||||
self.init_default_data_elements()
|
||||
|
||||
def show(self):
|
||||
self._dialog.show()
|
||||
|
||||
@run_idle
|
||||
def init_default_data_elements(self):
|
||||
self._apply_button.set_visible(False)
|
||||
self._create_button.set_visible(True)
|
||||
self._tr_edit_switch.set_sensitive(False)
|
||||
self.on_tr_edit_toggled(self._tr_edit_switch.set_active(True), True)
|
||||
for elem in self._non_empty_elements.values():
|
||||
elem.set_text(" ")
|
||||
elem.set_text("")
|
||||
self._new_check_button.set_active(True)
|
||||
self._tr_extra_expander.activate()
|
||||
self._service_type_combo_box.set_active(0)
|
||||
self._pol_combo_box.set_active(0)
|
||||
self._fec_combo_box.set_active(0)
|
||||
self._sys_combo_box.set_active(0)
|
||||
self._invertion_combo_box.set_active(2)
|
||||
|
||||
def update_data_elements(self):
|
||||
model, paths = self._services_view.get_selection().get_selected_rows()
|
||||
# Unpacking to search for an iterator for the base model
|
||||
filter_model = model.get_model()
|
||||
self._current_model = get_base_model(model)
|
||||
itr = None
|
||||
if not paths:
|
||||
# If editing from bouquet list and services list in the filter mode
|
||||
fav_model, paths = self._fav_view.get_selection().get_selected_rows()
|
||||
fav_id = fav_model[paths][7]
|
||||
for row in self._current_model:
|
||||
if row[-2] == fav_id:
|
||||
itr = row.iter
|
||||
break
|
||||
else:
|
||||
itr = model.get_iter(paths)
|
||||
itr = filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr))
|
||||
|
||||
if not itr:
|
||||
return
|
||||
|
||||
srv = Service(*self._current_model[itr][: Column.SRV_TOOLTIP])
|
||||
self._old_service = srv
|
||||
self._current_itr = itr
|
||||
# Service
|
||||
self._name_entry.set_text(srv.service)
|
||||
self._package_entry.set_text(srv.package)
|
||||
self._sid_entry.set_text(str(int(srv.ssid, 16)))
|
||||
# Transponder
|
||||
self._tr_type = TrType(srv.transponder_type)
|
||||
self._freq_entry.set_text(srv.freq)
|
||||
self._rate_entry.set_text(srv.rate)
|
||||
self.select_active_text(self._pol_combo_box, srv.pol)
|
||||
self.select_active_text(self._fec_combo_box, srv.fec)
|
||||
self.select_active_text(self._sys_combo_box, srv.system)
|
||||
if self._tr_type is TrType.Terrestrial:
|
||||
self.update_ui_for_terrestrial()
|
||||
elif self._tr_type is TrType.Cable:
|
||||
self.update_ui_for_cable()
|
||||
elif self._tr_type is TrType.ATSC:
|
||||
self.update_ui_for_atsc()
|
||||
else:
|
||||
self.set_sat_positions(srv.pos)
|
||||
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
self.init_enigma2_service_data(srv)
|
||||
self.init_enigma2_transponder_data(srv)
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
self.init_neutrino_data(srv)
|
||||
self.init_neutrino_ui_elements()
|
||||
|
||||
# ***************** Init Enigma2 data *********************#
|
||||
|
||||
@run_idle
|
||||
def init_enigma2_service_data(self, srv):
|
||||
""" Service data initialisation """
|
||||
flags = srv.flags_cas
|
||||
if flags:
|
||||
flags = flags.split(",")
|
||||
self.init_enigma2_flags(flags)
|
||||
self.init_enigma2_pids(flags)
|
||||
self.init_enigma2_cas(flags)
|
||||
|
||||
def init_enigma2_flags(self, flags):
|
||||
f_flags = list(filter(lambda x: x.startswith("f:"), flags))
|
||||
if f_flags:
|
||||
value = Flag.parse(f_flags[0])
|
||||
self._keep_check_button.set_active(Flag.is_keep(value))
|
||||
self._hide_check_button.set_active(Flag.is_hide(value))
|
||||
self._use_pids_check_button.set_active(Flag.is_pids(value))
|
||||
self._new_check_button.set_active(Flag.is_new(value))
|
||||
|
||||
def init_enigma2_cas(self, flags):
|
||||
cas = list(filter(lambda x: x.startswith("C:"), flags))
|
||||
if cas:
|
||||
self._cas_entry.set_text(",".join(cas))
|
||||
|
||||
def init_enigma2_pids(self, flags):
|
||||
pids = list(filter(lambda x: x.startswith("c:"), flags))
|
||||
if pids:
|
||||
extra_pids = []
|
||||
for pid in pids:
|
||||
if pid.startswith(Pids.VIDEO.value):
|
||||
self._video_pid_entry.set_text(str(int(pid[4:], 16)))
|
||||
elif pid.startswith(Pids.AUDIO.value):
|
||||
self._audio_pid_entry.set_text(str(int(pid[4:], 16)))
|
||||
elif pid.startswith(Pids.TELETEXT.value):
|
||||
self._teletext_pid_entry.set_text(str(int(pid[4:], 16)))
|
||||
elif pid.startswith(Pids.PCR.value):
|
||||
self._pcr_pid_entry.set_text(str(int(pid[4:], 16)))
|
||||
elif pid.startswith(Pids.AC3.value):
|
||||
self._ac3_pid_entry.set_text(str(int(pid[4:], 16)))
|
||||
elif pid.startswith(Pids.VIDEO_TYPE.value):
|
||||
extra_pids.append(pid)
|
||||
elif pid.startswith(Pids.AUDIO_CHANNEL.value):
|
||||
extra_pids.append(pid)
|
||||
elif pid.startswith(Pids.BIT_STREAM_DELAY.value):
|
||||
self._bitstream_entry.set_text(str(int(pid[4:], 16)))
|
||||
elif pid.startswith(Pids.PCM_DELAY.value):
|
||||
self._pcm_entry.set_text(str(int(pid[4:], 16)))
|
||||
elif pid.startswith(Pids.SUBTITLE.value):
|
||||
extra_pids.append(pid)
|
||||
else:
|
||||
extra_pids.append(pid)
|
||||
|
||||
self._extra_pids_entry.set_text(",".join(extra_pids))
|
||||
|
||||
def init_enigma2_transponder_data(self, srv):
|
||||
""" Transponder data initialisation """
|
||||
data = srv.data_id.split(":")
|
||||
tr_data = srv.transponder.split(":")
|
||||
tr_type = TrType(srv.transponder_type)
|
||||
|
||||
self._namespace_entry.set_text(str(int(data[1], 16)))
|
||||
self._transponder_id_entry.set_text(str(int(data[2], 16)))
|
||||
self._network_id_entry.set_text(str(int(data[3], 16)))
|
||||
|
||||
if tr_type is TrType.Satellite:
|
||||
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[5]).name)
|
||||
if srv.system == "DVB-S2":
|
||||
self.select_active_text(self._mod_combo_box, MODULATION.get(tr_data[8]))
|
||||
self.select_active_text(self._rolloff_combo_box, ROLL_OFF.get(tr_data[9]))
|
||||
self.select_active_text(self._pilot_combo_box, Pilot(tr_data[10]).name)
|
||||
self._tr_flag_entry.set_text(tr_data[7])
|
||||
if len(tr_data) > 12:
|
||||
self._stream_id_entry.set_text(tr_data[11])
|
||||
self._pls_code_entry.set_text(tr_data[12])
|
||||
self.select_active_text(self._pls_mode_combo_box, PLS_MODE.get(tr_data[13]))
|
||||
elif tr_type is TrType.Cable:
|
||||
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[2]).name)
|
||||
self.select_active_text(self._mod_combo_box, C_MODULATION.get(tr_data[3]))
|
||||
self.select_active_text(self._fec_combo_box, FEC_DEFAULT.get(tr_data[4]))
|
||||
self.select_active_text(self._sys_combo_box, SystemCable(tr_data[5]).name)
|
||||
elif tr_type is TrType.Terrestrial:
|
||||
self.select_active_text(self._fec_combo_box, T_FEC.get(tr_data[2]))
|
||||
self.select_active_text(self._rate_lp_combo_box, T_FEC.get(tr_data[3]))
|
||||
# Pol -> Bandwidth
|
||||
self.select_active_text(self._pol_combo_box, BANDWIDTH.get(tr_data[1]))
|
||||
self.select_active_text(self._mod_combo_box, T_MODULATION.get(tr_data[4]))
|
||||
# Transmission Mode -> Roll off
|
||||
self.select_active_text(self._rolloff_combo_box, TRANSMISSION_MODE.get(tr_data[5]))
|
||||
# GuardInterval -> Pilot
|
||||
self.select_active_text(self._pilot_combo_box, GUARD_INTERVAL.get(tr_data[6]))
|
||||
# Hierarchy -> Pls Mode
|
||||
self.select_active_text(self._pls_mode_combo_box, HIERARCHY.get(tr_data[7]))
|
||||
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[8]).name)
|
||||
self.select_active_text(self._sys_combo_box, T_SYSTEM.get(tr_data[9]))
|
||||
elif tr_type is TrType.ATSC:
|
||||
self._sys_combo_box.set_active(0)
|
||||
self.select_active_text(self._mod_combo_box, A_MODULATION.get(tr_data[2]))
|
||||
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[1]).name)
|
||||
|
||||
# Should be called last to properly initialize the reference
|
||||
self._srv_type_entry.set_text(data[4])
|
||||
|
||||
# ***************** Init Neutrino data *********************#
|
||||
|
||||
def init_neutrino_data(self, srv):
|
||||
if self._tr_type is not TrType.Satellite:
|
||||
return
|
||||
tr_data = get_attributes(srv.transponder)
|
||||
self._transponder_id_entry.set_text(str(int(tr_data.get("id", "0"), 16)))
|
||||
self._network_id_entry.set_text(str(int(tr_data.get("on", "0"), 16)))
|
||||
self.select_active_text(self._invertion_combo_box, Inversion(tr_data.get("inv", "2")).name)
|
||||
self.select_active_text(self._service_type_combo_box, srv.service_type)
|
||||
self.update_reference_entry()
|
||||
|
||||
def init_neutrino_ui_elements(self):
|
||||
self._builder.get_object("flags_box").set_visible(False)
|
||||
self._builder.get_object("pids_grid").set_visible(False)
|
||||
tr_grid = self._builder.get_object("tr_grid")
|
||||
tr_grid.remove_column(7)
|
||||
tr_grid.set_margin_bottom(5)
|
||||
self._builder.get_object("tr_extra_expander").set_visible(False)
|
||||
self._builder.get_object("srv_separator").set_visible(False)
|
||||
self._package_entry.set_sensitive(False)
|
||||
|
||||
# ***************** Init Sat positions *********************#
|
||||
|
||||
def set_sat_positions(self, sat_pos):
|
||||
""" Sat positions initialisation """
|
||||
self._sat_pos_button.set_value(float(sat_pos[:-1]))
|
||||
self._pos_side_box.set_active_id(sat_pos[-1:])
|
||||
|
||||
def on_system_changed(self, box):
|
||||
if not self._tr_edit_switch.get_active():
|
||||
return
|
||||
active = box.get_active()
|
||||
self.update_dvb_s2_elements(active)
|
||||
|
||||
def update_dvb_s2_elements(self, active):
|
||||
for elem in self._DVB_S2_ELEMENTS:
|
||||
elem.set_sensitive(active)
|
||||
self._pls_code_entry.set_name("GtkEntry")
|
||||
self._stream_id_entry.set_name("GtkEntry")
|
||||
|
||||
if active:
|
||||
if not self._mod_combo_box.get_active_id():
|
||||
self._mod_combo_box.set_active_id(MODULATION["2"])
|
||||
if not self._rolloff_combo_box.get_active_id():
|
||||
self._rolloff_combo_box.set_active_id(ROLL_OFF["0"])
|
||||
if not self._pilot_combo_box.get_active_id():
|
||||
self._pilot_combo_box.set_active_id(Pilot.Auto.name)
|
||||
if not self._pls_mode_combo_box.get_active_id():
|
||||
self._pls_mode_combo_box.set_active_id(PLS_MODE["0"])
|
||||
|
||||
# ***************** Save data *********************#
|
||||
|
||||
def on_save(self, item):
|
||||
self.save_data()
|
||||
|
||||
def on_create_new(self, item):
|
||||
self.save_data()
|
||||
|
||||
def save_data(self):
|
||||
if self._s_type is SettingsType.NEUTRINO_MP and self._tr_type is not TrType.Satellite:
|
||||
show_dialog(DialogType.ERROR, transient=self._dialog, text="Not implemented yet!")
|
||||
return
|
||||
|
||||
if not self.is_data_correct():
|
||||
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
|
||||
return
|
||||
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
if self.on_edit() if self._action is Action.EDIT else self.on_new():
|
||||
self._dialog.destroy()
|
||||
|
||||
def on_new(self):
|
||||
""" Create new service. """
|
||||
service = self.get_service(*self.get_srv_data(), self.get_satellite_transponder_data())
|
||||
show_dialog(DialogType.ERROR, transient=self._dialog, text="Not implemented yet!")
|
||||
return True
|
||||
|
||||
def on_edit(self):
|
||||
""" Edit current service. """
|
||||
fav_id, data_id = self.get_srv_data()
|
||||
# Transponder
|
||||
transponder = self._old_service.transponder
|
||||
if self._tr_edit_switch.get_active():
|
||||
try:
|
||||
if self._tr_type is TrType.Satellite:
|
||||
transponder = self.get_satellite_transponder_data()
|
||||
elif self._tr_type is TrType.Terrestrial:
|
||||
transponder = self.get_terrestrial_transponder_data()
|
||||
elif self._tr_type is TrType.Cable:
|
||||
transponder = self.get_cable_transponder_data()
|
||||
elif self._tr_type is TrType.ATSC:
|
||||
transponder = self.get_atsc_transponder_data()
|
||||
except Exception as e:
|
||||
log("Edit service error: {}".format(e))
|
||||
show_dialog(DialogType.ERROR, transient=self._dialog, text="Error getting transponder parameters!")
|
||||
else:
|
||||
if self._transponder_services_iters:
|
||||
self.update_transponder_services(transponder, self.get_sat_position())
|
||||
# Service
|
||||
service = self.get_service(fav_id, data_id, transponder)
|
||||
old_fav_id = self._old_service.fav_id
|
||||
if old_fav_id != fav_id:
|
||||
if fav_id in self._services:
|
||||
msg = "{}\n\n\t{}".format("A similar service is already in this list!", "Are you sure?")
|
||||
if show_dialog(DialogType.QUESTION, transient=self._dialog, text=msg) != Gtk.ResponseType.OK:
|
||||
return False
|
||||
self.update_bouquets(fav_id, old_fav_id)
|
||||
|
||||
self._services[fav_id] = service
|
||||
|
||||
if self._old_service.picon_id != service.picon_id:
|
||||
self.update_picon_name(self._old_service.picon_id, service.picon_id)
|
||||
|
||||
flags = service.flags_cas
|
||||
extra_data = {Column.SRV_TOOLTIP: None, Column.SRV_BACKGROUND: None}
|
||||
if self._s_type is SettingsType.ENIGMA_2 and flags:
|
||||
f_flags = list(filter(lambda x: x.startswith("f:"), flags.split(",")))
|
||||
if f_flags and Flag.is_new(Flag.parse(f_flags[0])):
|
||||
extra_data[Column.SRV_BACKGROUND] = self._new_color
|
||||
|
||||
self._current_model.set(self._current_itr, extra_data)
|
||||
self._current_model.set(self._current_itr, {i: v for i, v in enumerate(service)})
|
||||
self.update_fav_view(self._old_service, service)
|
||||
self._old_service = service
|
||||
return True
|
||||
|
||||
def update_bouquets(self, fav_id, old_fav_id):
|
||||
self._services.pop(old_fav_id, None)
|
||||
for bq in self._bouquets.values():
|
||||
indexes = []
|
||||
for i, f_id in enumerate(bq):
|
||||
if old_fav_id == f_id:
|
||||
indexes.append(i)
|
||||
for i in indexes:
|
||||
bq[i] = fav_id
|
||||
|
||||
@run_idle
|
||||
def update_fav_view(self, old_service, new_service):
|
||||
model = self._fav_view.get_model()
|
||||
for row in filter(lambda r: old_service.fav_id == r[7], model):
|
||||
itr = row.iter
|
||||
if not model.get_value(itr, Column.FAV_BACKGROUND):
|
||||
model.set_value(itr, Column.FAV_SERVICE, new_service.service)
|
||||
|
||||
model.set(itr, {Column.FAV_CODED: new_service.coded,
|
||||
Column.FAV_LOCKED: new_service.locked,
|
||||
Column.FAV_HIDE: new_service.hide,
|
||||
Column.FAV_TYPE: new_service.service_type,
|
||||
Column.FAV_POS: new_service.pos,
|
||||
Column.FAV_ID: new_service.fav_id,
|
||||
Column.FAV_PICON: new_service.picon})
|
||||
|
||||
def update_picon_name(self, old_name, new_name):
|
||||
if not os.path.isdir(self._picons_path):
|
||||
return
|
||||
|
||||
for file_name in os.listdir(self._picons_path):
|
||||
if file_name == old_name:
|
||||
old_file = os.path.join(self._picons_path, old_name)
|
||||
new_file = os.path.join(self._picons_path, new_name)
|
||||
os.rename(old_file, new_file)
|
||||
break
|
||||
|
||||
# ***************** Service ********************* #
|
||||
|
||||
def get_service(self, fav_id, data_id, transponder):
|
||||
freq, rate, pol, fec, system, pos = self.get_transponder_values()
|
||||
return Service(flags_cas=self.get_flags(),
|
||||
transponder_type=self._old_service.transponder_type,
|
||||
coded=CODED_ICON if self._cas_entry.get_text() else None,
|
||||
service=self._name_entry.get_text(),
|
||||
locked=self._old_service.locked,
|
||||
hide=HIDE_ICON if self._hide_check_button.get_active() else None,
|
||||
package=self._package_entry.get_text(),
|
||||
service_type=SERVICE_TYPE.get(self._srv_type_entry.get_text(), SERVICE_TYPE["3"]),
|
||||
picon=self._old_service.picon,
|
||||
picon_id=self._reference_entry.get_text().replace(":", "_") + ".png",
|
||||
ssid="{:04x}".format(int(self._sid_entry.get_text())),
|
||||
freq=freq,
|
||||
rate=rate,
|
||||
pol=pol,
|
||||
fec=fec,
|
||||
system=system,
|
||||
pos=pos,
|
||||
data_id=data_id,
|
||||
fav_id=fav_id,
|
||||
transponder=transponder)
|
||||
|
||||
def get_flags(self):
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
return self.get_enigma2_flags()
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
flags = get_attributes(self._old_service.flags_cas)
|
||||
flags["position"] = self.get_sat_position()
|
||||
return SP.join("{}{}{}".format(k, KSP, v) for k, v in flags.items())
|
||||
|
||||
def get_enigma2_flags(self):
|
||||
flags = ["p:{}".format(self._package_entry.get_text())]
|
||||
# cas
|
||||
cas = self._cas_entry.get_text()
|
||||
if cas:
|
||||
flags.append(cas)
|
||||
# pids
|
||||
video_pid = self._video_pid_entry.get_text()
|
||||
if video_pid:
|
||||
flags.append("{}{:04x}".format(Pids.VIDEO.value, int(video_pid)))
|
||||
audio_pid = self._audio_pid_entry.get_text()
|
||||
if audio_pid:
|
||||
flags.append("{}{:04x}".format(Pids.AUDIO.value, int(audio_pid)))
|
||||
teletext_pid = self._teletext_pid_entry.get_text()
|
||||
if teletext_pid:
|
||||
flags.append("{}{:04x}".format(Pids.TELETEXT.value, int(teletext_pid)))
|
||||
pcr_pid = self._pcr_pid_entry.get_text()
|
||||
if pcr_pid:
|
||||
flags.append("{}{:04x}".format(Pids.PCR.value, int(pcr_pid)))
|
||||
ac3_pid = self._ac3_pid_entry.get_text()
|
||||
if ac3_pid:
|
||||
flags.append("{}{:04x}".format(Pids.AC3.value, int(ac3_pid)))
|
||||
bitstream_pid = self._bitstream_entry.get_text()
|
||||
if bitstream_pid:
|
||||
flags.append("{}{:04x}".format(Pids.BIT_STREAM_DELAY.value, int(bitstream_pid)))
|
||||
pcm_pid = self._pcm_entry.get_text()
|
||||
if pcm_pid:
|
||||
flags.append("{}{:04x}".format(Pids.PCM_DELAY.value, int(pcm_pid)))
|
||||
extra_pids = self._extra_pids_entry.get_text()
|
||||
if extra_pids:
|
||||
flags.append(extra_pids)
|
||||
# flags
|
||||
f_flags = Flag.KEEP.value if self._keep_check_button.get_active() else 0
|
||||
f_flags = f_flags + Flag.HIDE.value if self._hide_check_button.get_active() else f_flags
|
||||
f_flags = f_flags + Flag.PIDS.value if self._use_pids_check_button.get_active() else f_flags
|
||||
f_flags = f_flags + Flag.NEW.value if self._new_check_button.get_active() else f_flags
|
||||
if f_flags:
|
||||
flags.append("f:{:02d}".format(f_flags))
|
||||
|
||||
return ",".join(flags)
|
||||
|
||||
def get_srv_data(self):
|
||||
ssid = int(self._sid_entry.get_text())
|
||||
net_id, tr_id = int(self._network_id_entry.get_text()), int(self._transponder_id_entry.get_text())
|
||||
service_type = self._srv_type_entry.get_text()
|
||||
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
namespace = int(self._namespace_entry.get_text())
|
||||
data_id = self._ENIGMA2_DATA_ID.format(ssid, namespace, tr_id, net_id, service_type, 0)
|
||||
fav_id = self._ENIGMA2_FAV_ID.format(ssid, tr_id, net_id, namespace)
|
||||
return fav_id, data_id
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
data = get_attributes(self._old_service.data_id)
|
||||
data["n"] = self._name_entry.get_text()
|
||||
data["t"] = "{:x}".format(int(service_type))
|
||||
data["i"] = "{:04x}".format(ssid)
|
||||
fav_id = self._NEUTRINO_FAV_ID.format(tr_id, net_id, ssid)
|
||||
return fav_id, SP.join("{}{}{}".format(k, KSP, v) for k, v in data.items())
|
||||
|
||||
# ***************** Transponder ********************* #
|
||||
|
||||
def get_transponder_values(self):
|
||||
freq = self._freq_entry.get_text()
|
||||
fec = self._fec_combo_box.get_active_id()
|
||||
system = self._sys_combo_box.get_active_id()
|
||||
o_srv = self._old_service
|
||||
|
||||
if self._tr_type is TrType.Satellite or self._s_type is SettingsType.NEUTRINO_MP:
|
||||
freq = self._freq_entry.get_text()
|
||||
rate = self._rate_entry.get_text()
|
||||
pol = self._pol_combo_box.get_active_id()
|
||||
pos = "{}{}".format(round(self._sat_pos_button.get_value(), 1), self._pos_side_box.get_active_id())
|
||||
return freq, rate, pol, fec, system, pos
|
||||
elif self._tr_type in (TrType.Terrestrial, TrType.ATSC):
|
||||
return freq, o_srv.rate, o_srv.pol, fec, system, o_srv.pos
|
||||
elif self._tr_type is TrType.Cable:
|
||||
return freq, self._rate_entry.get_text(), o_srv.pol, fec, o_srv.system, o_srv.pos
|
||||
|
||||
def get_satellite_transponder_data(self):
|
||||
sys = self._sys_combo_box.get_active_id()
|
||||
freq = "{}000".format(self._freq_entry.get_text())
|
||||
rate = "{}000".format(self._rate_entry.get_text())
|
||||
pol = self.get_value_from_combobox_id(self._pol_combo_box, POLARIZATION)
|
||||
fec = self.get_value_from_combobox_id(self._fec_combo_box, FEC_DEFAULT)
|
||||
sat_pos = self.get_sat_position()
|
||||
|
||||
inv = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
|
||||
srv_sys = "0" # !!!
|
||||
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
dvb_s_tr = self._ENIGMA2_TRANSPONDER_DATA.format("s", freq, rate, pol, fec, sat_pos, inv, srv_sys)
|
||||
if sys == "DVB-S":
|
||||
return dvb_s_tr
|
||||
if sys == "DVB-S2":
|
||||
flag = self._tr_flag_entry.get_text()
|
||||
mod = self.get_value_from_combobox_id(self._mod_combo_box, MODULATION)
|
||||
roll_off = self.get_value_from_combobox_id(self._rolloff_combo_box, ROLL_OFF)
|
||||
pilot = get_value_by_name(Pilot, self._pilot_combo_box.get_active_id())
|
||||
pls_mode = self.get_value_from_combobox_id(self._pls_mode_combo_box, PLS_MODE)
|
||||
pls_code = self._pls_code_entry.get_text()
|
||||
st_id = self._stream_id_entry.get_text()
|
||||
pls = ":{}:{}:{}".format(st_id, pls_code, pls_mode) if pls_mode and pls_code and st_id else ""
|
||||
|
||||
return "{}:{}:{}:{}:{}{}".format(dvb_s_tr, flag, mod, roll_off, pilot, pls)
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
tr_data = get_attributes(self._old_service.transponder)
|
||||
tr_data["frq"] = freq
|
||||
tr_data["sr"] = rate
|
||||
tr_data["pol"] = pol
|
||||
tr_data["fec"] = fec
|
||||
tr_data["on"] = "{:04x}".format(int(self._network_id_entry.get_text()))
|
||||
tr_data["id"] = "{:04x}".format(int(self._transponder_id_entry.get_text()))
|
||||
tr_data["inv"] = inv
|
||||
|
||||
return SP.join("{}{}{}".format(k, KSP, v) for k, v in tr_data.items())
|
||||
|
||||
def get_sat_position(self):
|
||||
sat_pos = self._sat_pos_button.get_value() * (-1 if self._pos_side_box.get_active_id() == "W" else 1)
|
||||
sat_pos = str(round(sat_pos, 1)).replace(".", "")
|
||||
return sat_pos
|
||||
|
||||
def get_terrestrial_transponder_data(self):
|
||||
tr_data = re.split("\s|:", self._old_service.transponder)
|
||||
# frequency, bandwidth, code rate HP, code rate LP, modulation, transmission mode, guard interval, hierarchy,
|
||||
# inversion, system, plp_id
|
||||
# Bandwidth -> Pol, Rate HP -> FEC, TransmissionMode -> Roll off, GuardInterval -> Pilot, Hierarchy -> Pls Mode
|
||||
tr_data[1] = "{}000".format(self._freq_entry.get_text())
|
||||
tr_data[2] = self.get_value_from_combobox_id(self._pol_combo_box, BANDWIDTH)
|
||||
tr_data[3] = self.get_value_from_combobox_id(self._fec_combo_box, T_FEC)
|
||||
tr_data[4] = self.get_value_from_combobox_id(self._rate_lp_combo_box, T_FEC)
|
||||
tr_data[5] = self.get_value_from_combobox_id(self._mod_combo_box, T_MODULATION)
|
||||
tr_data[6] = self.get_value_from_combobox_id(self._rolloff_combo_box, TRANSMISSION_MODE)
|
||||
tr_data[7] = self.get_value_from_combobox_id(self._pilot_combo_box, GUARD_INTERVAL)
|
||||
tr_data[8] = self.get_value_from_combobox_id(self._pls_mode_combo_box, HIERARCHY)
|
||||
tr_data[9] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
|
||||
tr_data[10] = self.get_value_from_combobox_id(self._sys_combo_box, T_SYSTEM)
|
||||
|
||||
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
|
||||
|
||||
def get_cable_transponder_data(self):
|
||||
tr_data = re.split("\s|:", self._old_service.transponder)
|
||||
# frequency, symbol_rate, modulation, inversion, fec_inner, system;
|
||||
tr_data[1] = "{}000".format(self._freq_entry.get_text())
|
||||
tr_data[2] = "{}000".format(self._rate_entry.get_text())
|
||||
tr_data[3] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
|
||||
tr_data[4] = self.get_value_from_combobox_id(self._mod_combo_box, C_MODULATION)
|
||||
tr_data[5] = self.get_value_from_combobox_id(self._fec_combo_box, FEC_DEFAULT)
|
||||
tr_data[6] = get_value_by_name(SystemCable, self._sys_combo_box.get_active_id())
|
||||
|
||||
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
|
||||
|
||||
def get_atsc_transponder_data(self):
|
||||
tr_data = re.split("\s|:", self._old_service.transponder)
|
||||
# frequency, inversion, modulation, system
|
||||
tr_data[1] = "{}000".format(self._freq_entry.get_text())
|
||||
tr_data[2] = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
|
||||
tr_data[3] = self.get_value_from_combobox_id(self._mod_combo_box, A_MODULATION)
|
||||
|
||||
return "{} {}".format(tr_data[0], ":".join(tr_data[1:]))
|
||||
|
||||
def update_transponder_services(self, transponder, sat_pos):
|
||||
for itr in self._transponder_services_iters:
|
||||
srv = self._current_model[itr][:]
|
||||
srv[Column.SRV_FREQ], srv[Column.SRV_RATE], srv[Column.SRV_POL], srv[Column.SRV_FEC], srv[
|
||||
Column.SRV_SYSTEM], srv[Column.SRV_POS] = self.get_transponder_values()
|
||||
srv[Column.SRV_TRANSPONDER] = transponder
|
||||
|
||||
fav_id = srv[Column.SRV_FAV_ID]
|
||||
old_srv = self._services.pop(fav_id, None)
|
||||
if not old_srv:
|
||||
log("Update transponder services error: No service found for ID {}".format(srv[Column.SRV_FAV_ID]))
|
||||
continue
|
||||
|
||||
if self._s_type is SettingsType.NEUTRINO_MP:
|
||||
flags = get_attributes(srv[Column.SRV_CAS_FLAGS])
|
||||
flags["position"] = sat_pos
|
||||
srv[Column.SRV_CAS_FLAGS] = SP.join("{}{}{}".format(k, KSP, v) for k, v in flags.items())
|
||||
|
||||
self._services[fav_id] = Service(*srv[:Column.SRV_TOOLTIP])
|
||||
self._current_model.set_row(itr, srv)
|
||||
|
||||
# ***************** Others *********************#
|
||||
|
||||
def select_active_text(self, box, text):
|
||||
model = box.get_model()
|
||||
for index, row in enumerate(model):
|
||||
if row[0] == text:
|
||||
box.set_active(index)
|
||||
break
|
||||
|
||||
def on_digit_entry_changed(self, entry):
|
||||
entry.set_name(self._DIGIT_ENTRY_NAME if self._DIGIT_PATTERN.search(entry.get_text()) else "GtkEntry")
|
||||
|
||||
def on_non_empty_entry_changed(self, entry):
|
||||
entry.set_name(self._DIGIT_ENTRY_NAME if self._NON_EMPTY_PATTERN.search(entry.get_text()) else "GtkEntry")
|
||||
|
||||
def on_cas_entry_changed(self, entry):
|
||||
entry.set_name("GtkEntry" if self._CAID_PATTERN.fullmatch(entry.get_text()) else self._DIGIT_ENTRY_NAME)
|
||||
|
||||
def on_extra_pids_entry_changed(self, entry):
|
||||
entry.set_name("GtkEntry" if self._PIDS_PATTERN.fullmatch(entry.get_text()) else self._DIGIT_ENTRY_NAME)
|
||||
|
||||
def get_value_from_combobox_id(self, box: Gtk.ComboBox, dc: dict):
|
||||
cb_id = box.get_active_id()
|
||||
return get_key_by_value(dc, cb_id)
|
||||
|
||||
@run_idle
|
||||
def on_tr_edit_toggled(self, switch, active):
|
||||
if active and self._action is Action.EDIT:
|
||||
self._transponder_services_iters = []
|
||||
response = TransponderServicesDialog(self._dialog,
|
||||
self._services_view,
|
||||
self._old_service.transponder,
|
||||
self._transponder_services_iters).show()
|
||||
if response == Gtk.ResponseType.CANCEL or response == Gtk.ResponseType.DELETE_EVENT:
|
||||
switch.set_active(False)
|
||||
self._transponder_services_iters = None
|
||||
return
|
||||
|
||||
self.update_dvb_s2_elements(active and (self._sys_combo_box.get_active_id() == "DVB-S2"
|
||||
or self._old_service.transponder_type in "tca"))
|
||||
|
||||
for elem in self._TRANSPONDER_ELEMENTS:
|
||||
elem.set_sensitive(active)
|
||||
|
||||
def is_data_correct(self):
|
||||
for elem in self._digit_elements.values():
|
||||
if elem.get_name() == self._DIGIT_ENTRY_NAME:
|
||||
return False
|
||||
for elem in self._non_empty_elements.values():
|
||||
if elem.get_name() == self._DIGIT_ENTRY_NAME:
|
||||
return False
|
||||
if self._cas_entry.get_name() == self._DIGIT_ENTRY_NAME:
|
||||
return False
|
||||
if self._extra_pids_entry.get_name() == self._DIGIT_ENTRY_NAME:
|
||||
return False
|
||||
return True
|
||||
|
||||
def update_reference(self, entry, event=None):
|
||||
if not self.is_data_correct() or (event is None and self._s_type is SettingsType.NEUTRINO_MP):
|
||||
return
|
||||
self.update_reference_entry()
|
||||
|
||||
def update_reference_entry(self):
|
||||
srv_type = int(self._srv_type_entry.get_text())
|
||||
ssid = int(self._sid_entry.get_text())
|
||||
tid = int(self._transponder_id_entry.get_text())
|
||||
nid = int(self._network_id_entry.get_text())
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
on_id = int(self._namespace_entry.get_text())
|
||||
ref = "1:0:{:X}:{:X}:{:X}:{:X}:{:X}:0:0:0".format(srv_type, ssid, tid, nid, on_id)
|
||||
self._reference_entry.set_text(ref)
|
||||
else:
|
||||
self._reference_entry.set_text("{:x}{:04x}{:04x}".format(tid, nid, ssid))
|
||||
|
||||
def update_ui_for_terrestrial(self):
|
||||
tr_grid = self.get_transponder_grid_for_non_satellite()
|
||||
tr_grid.remove_column(1)
|
||||
tr_grid.insert_column(1)
|
||||
extra_tr_grid = self._builder.get_object("extra_transponder_grid")
|
||||
for i in range(4):
|
||||
extra_tr_grid.remove_column(6)
|
||||
# Bandwidth -> Pol
|
||||
pol_label = self._builder.get_object("pol_label")
|
||||
pol_label.set_text("Bandwidth")
|
||||
tr_grid.attach(pol_label, 1, 0, 1, 1)
|
||||
tr_grid.attach(self._pol_combo_box, 1, 1, 1, 1)
|
||||
# Rate HP -> FEC
|
||||
self._builder.get_object("fec_label").set_text("Rate HP")
|
||||
# Rate LP
|
||||
tr_grid.insert_column(3)
|
||||
rate_lp_label = self._builder.get_object("pls_code_label")
|
||||
rate_lp_label.set_text("Rate LP")
|
||||
tr_grid.attach(rate_lp_label, 3, 0, 1, 1)
|
||||
tr_grid.attach(self._rate_lp_combo_box, 3, 1, 1, 1)
|
||||
# Modulation
|
||||
tr_grid.insert_column(4)
|
||||
extra_tr_grid.remove_column(1)
|
||||
tr_grid.attach(self._builder.get_object("mod_label"), 4, 0, 1, 1)
|
||||
tr_grid.attach(self._mod_combo_box, 4, 1, 1, 1)
|
||||
# TransmissionMode -> Roll off
|
||||
rolloff_label = self._builder.get_object("rolloff_label")
|
||||
rolloff_label.set_text("T mode")
|
||||
# GuardInterval -> Pilot
|
||||
pilot_label = self._builder.get_object("pilot_label")
|
||||
pilot_label.set_text("Guard Interval")
|
||||
# Hierarchy -> Pls Mode
|
||||
pls_mode_label = self._builder.get_object("pls_mode_label")
|
||||
pls_mode_label.set_text("Hierarchy")
|
||||
# Models
|
||||
fec_model, modulation_model, sys_model = self.get_models_for_non_satellite()
|
||||
pol_model = self._pol_combo_box.get_model()
|
||||
roll_off_model = self._rolloff_combo_box.get_model()
|
||||
pilot_model = self._pilot_combo_box.get_model()
|
||||
pls_model = self._pls_mode_combo_box.get_model()
|
||||
# Models clearing
|
||||
for m in pol_model, roll_off_model, pilot_model, pls_model:
|
||||
m.clear()
|
||||
|
||||
self.init_terrestrial_models((pol_model, modulation_model, roll_off_model, pilot_model, pls_model, sys_model),
|
||||
(BANDWIDTH, T_MODULATION, TRANSMISSION_MODE, GUARD_INTERVAL, HIERARCHY, T_SYSTEM))
|
||||
|
||||
# Removing the latest FEC elements from the model
|
||||
for itr in [fec_model.get_iter(Gtk.TreePath.new_from_string(str(i))) for i in range(7, 11)]:
|
||||
fec_model.remove(itr)
|
||||
# Extra
|
||||
self._namespace_entry.set_max_width_chars(15)
|
||||
self._sys_combo_box.set_hexpand(False)
|
||||
|
||||
def init_terrestrial_models(self, models, properties):
|
||||
for index, model in enumerate(models):
|
||||
for v in properties[index].values():
|
||||
model.append((v,))
|
||||
|
||||
def update_ui_for_cable(self):
|
||||
tr_grid = self.get_transponder_grid_for_non_satellite()
|
||||
tr_box = self._builder.get_object("tr_box")
|
||||
# Models
|
||||
fec_model, modulation_model, system_model = self.get_models_for_non_satellite()
|
||||
|
||||
extra_tr_grid = self._builder.get_object("extra_transponder_grid")
|
||||
for child in extra_tr_grid.get_children():
|
||||
extra_tr_grid.remove(child)
|
||||
tr_grid.remove(extra_tr_grid)
|
||||
|
||||
tr_grid.insert_column(3)
|
||||
tr_grid.insert_column(4)
|
||||
tr_grid.insert_column(5)
|
||||
# Modulation
|
||||
tr_grid.attach(self._builder.get_object("mod_label"), 3, 0, 1, 1)
|
||||
tr_grid.attach(self._mod_combo_box, 3, 1, 1, 1)
|
||||
for v in C_MODULATION.values():
|
||||
modulation_model.append((v,))
|
||||
# Inversion
|
||||
tr_grid.attach(self._builder.get_object("inversion_label"), 4, 0, 1, 1)
|
||||
tr_grid.attach(self._invertion_combo_box, 4, 1, 1, 1)
|
||||
# System
|
||||
tr_grid.attach(self._builder.get_object("system_label"), 5, 0, 1, 1)
|
||||
tr_grid.attach(self._sys_combo_box, 5, 1, 1, 1)
|
||||
system_model.append((SystemCable.ANNEX_A.name,))
|
||||
system_model.append((SystemCable.ANNEX_C.name,))
|
||||
# FEC
|
||||
fec_model.append(("None",))
|
||||
# Extra
|
||||
tr_box.remove(self._tr_extra_expander)
|
||||
tr_grid.set_margin_bottom(5)
|
||||
self._freq_entry.set_width_chars(10)
|
||||
self._freq_entry.set_max_width_chars(10)
|
||||
self._rate_entry.set_width_chars(10)
|
||||
self._rate_entry.set_max_width_chars(10)
|
||||
self._transponder_id_entry.set_max_width_chars(8)
|
||||
self._network_id_entry.set_max_width_chars(8)
|
||||
|
||||
def update_ui_for_atsc(self):
|
||||
self.update_ui_for_cable()
|
||||
tr_grid = self._builder.get_object("tr_grid")
|
||||
tr_grid.remove_column(1)
|
||||
tr_grid.remove_column(1)
|
||||
# Init models
|
||||
fec_model, modulation_model, system_model = self.get_models_for_non_satellite()
|
||||
system_model.append((TrType.ATSC.name,))
|
||||
[modulation_model.append((v,)) for k, v in A_MODULATION.items()]
|
||||
# Extra
|
||||
self._namespace_entry.set_max_width_chars(25)
|
||||
|
||||
def get_transponder_grid_for_non_satellite(self):
|
||||
self._pids_grid.set_visible(False)
|
||||
tr_grid = self._builder.get_object("tr_grid")
|
||||
tr_grid.remove_column(0)
|
||||
tr_grid.remove_column(2)
|
||||
return tr_grid
|
||||
|
||||
def get_models_for_non_satellite(self):
|
||||
fec_model = self._fec_combo_box.get_model()
|
||||
modulation_model = self._mod_combo_box.get_model()
|
||||
modulation_model.clear()
|
||||
system_model = self._sys_combo_box.get_model()
|
||||
system_model.clear()
|
||||
return fec_model, modulation_model, system_model
|
||||
|
||||
|
||||
class TransponderServicesDialog:
|
||||
def __init__(self, transient, services_view, transponder, tr_iters):
|
||||
builder = get_builder(_UI_PATH, use_str=True, objects=("tr_services_dialog", "transponder_services_liststore"))
|
||||
self._dialog = builder.get_object("tr_services_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._srv_model = builder.get_object("transponder_services_liststore")
|
||||
self.append_services(services_view, transponder, tr_iters)
|
||||
builder.get_object("srv_list_dialog_info_bar").connect("response", lambda bar, resp: bar.hide())
|
||||
|
||||
def append_services(self, view, transponder, tr_iters):
|
||||
model = view.get_model()
|
||||
filter_model = model.get_model()
|
||||
for row in model:
|
||||
if row[Column.SRV_TRANSPONDER] == transponder:
|
||||
self._srv_model.append((row[Column.SRV_SERVICE], row[Column.SRV_PACKAGE], row[Column.SRV_TYPE],
|
||||
row[Column.SRV_SSID], row[Column.SRV_FREQ], row[Column.SRV_POS]))
|
||||
itr = model.get_iter(row.path)
|
||||
tr_iters.append(filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr)))
|
||||
|
||||
def show(self):
|
||||
response = self._dialog.run()
|
||||
self._dialog.destroy()
|
||||
return response
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
3538
app/ui/settings_dialog.glade
Normal file
3538
app/ui/settings_dialog.glade
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,118 +1,815 @@
|
||||
from app.properties import write_config, Profile, get_default_settings
|
||||
from . import Gtk, UI_RESOURCES_PATH
|
||||
from .main_helper import update_entry_data
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
def show_settings_dialog(transient, options):
|
||||
return SettingsDialog(transient, options).show()
|
||||
import os
|
||||
import re
|
||||
|
||||
from app.commons import run_task, run_idle, log
|
||||
from app.connections import test_telnet, test_ftp, TestException, test_http, HttpApiException
|
||||
from app.settings import SettingsType, Settings, PlayStreamsMode, IS_LINUX, SEP, IS_WIN
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_message, get_chooser_dialog, get_builder
|
||||
from .main_helper import update_entry_data, scroll_to, get_picon_pixbuf
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON, APP_FONT, IS_GNOME_SESSION
|
||||
|
||||
|
||||
class SettingsDialog:
|
||||
def __init__(self, transient, options):
|
||||
handlers = {"on_data_dir_field_icon_press": self.on_data_dir_field_icon_press,
|
||||
"on_picons_dir_field_icon_press": self.on_picons_dir_field_icon_press,
|
||||
"on_profile_changed": self.on_profile_changed,
|
||||
"on_reset": self.on_reset,
|
||||
"apply_settings": self.apply_settings}
|
||||
_DIGIT_ENTRY_NAME = "digit-entry"
|
||||
_DIGIT_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.add_objects_from_file(UI_RESOURCES_PATH + "dialogs.glade",
|
||||
("settings_dialog", "telnet_timeout_adjustment"))
|
||||
builder.connect_signals(handlers)
|
||||
def __init__(self, transient, settings: Settings):
|
||||
handlers = {"on_field_icon_press": self.on_field_icon_press,
|
||||
"on_settings_type_changed": self.on_settings_type_changed,
|
||||
"on_reset": self.on_reset,
|
||||
"on_response": self.on_response,
|
||||
"on_connection_test": self.on_connection_test,
|
||||
"on_info_bar_close": self.on_info_bar_close,
|
||||
"on_set_color_switch": self.on_set_color_switch,
|
||||
"on_force_bq_name": self.on_force_bq_name,
|
||||
"on_http_mode_switch": self.on_http_mode_switch,
|
||||
"on_experimental_switch": self.on_experimental_switch,
|
||||
"on_yt_dl_switch": self.on_yt_dl_switch,
|
||||
"on_default_path_mode_switch": self.on_default_path_mode_switch,
|
||||
"on_profile_add": self.on_profile_add,
|
||||
"on_profile_edit": self.on_profile_edit,
|
||||
"on_profile_remove": self.on_profile_remove,
|
||||
"on_profile_deleted": self.on_profile_deleted,
|
||||
"on_profile_inserted": self.on_profile_inserted,
|
||||
"on_profile_edited": self.on_profile_edited,
|
||||
"on_profile_selected": self.on_profile_selected,
|
||||
"on_profile_set_default": self.on_profile_set_default,
|
||||
"on_add_picon_path": self.on_add_picon_path,
|
||||
"on_remove_picon_path": self.on_remove_picon_path,
|
||||
"on_lang_changed": self.on_lang_changed,
|
||||
"on_main_settings_visible": self.on_main_settings_visible,
|
||||
"on_http_use_ssl_toggled": self.on_http_use_ssl_toggled,
|
||||
"on_click_mode_togged": self.on_click_mode_togged,
|
||||
"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_list_font_reset": self.on_list_font_reset,
|
||||
"on_theme_changed": self.on_theme_changed,
|
||||
"on_theme_add": self.on_theme_add,
|
||||
"on_theme_remove": self.on_theme_remove,
|
||||
"on_appearance_changed": self.on_appearance_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
|
||||
self._updated = False
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "settings_dialog.glade", handlers)
|
||||
|
||||
self._dialog = builder.get_object("settings_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._dialog.set_border_width(0)
|
||||
self._dialog.set_margin_left(0)
|
||||
self._main_stack = builder.get_object("main_stack")
|
||||
# Network.
|
||||
self._host_field = builder.get_object("host_field")
|
||||
self._port_field = builder.get_object("port_field")
|
||||
self._login_field = builder.get_object("login_field")
|
||||
self._password_field = builder.get_object("password_field")
|
||||
self._telnet_login_field = builder.get_object("telnet_login_field")
|
||||
self._telnet_password_field = builder.get_object("telnet_password_field")
|
||||
self._http_port_field = builder.get_object("http_port_field")
|
||||
self._http_use_ssl_check_button = builder.get_object("http_use_ssl_check_button")
|
||||
self._telnet_port_field = builder.get_object("telnet_port_field")
|
||||
self._telnet_timeout_spin_button = builder.get_object("telnet_timeout_spin_button")
|
||||
self._reset_button = builder.get_object("reset_button")
|
||||
# Test.
|
||||
self._ftp_radio_button = builder.get_object("ftp_radio_button")
|
||||
self._http_radio_button = builder.get_object("http_radio_button")
|
||||
# Network paths.
|
||||
self._services_field = builder.get_object("services_field")
|
||||
self._user_bouquet_field = builder.get_object("user_bouquet_field")
|
||||
self._satellites_xml_field = builder.get_object("satellites_xml_field")
|
||||
self._data_dir_field = builder.get_object("data_dir_field")
|
||||
self._picons_field = builder.get_object("picons_field")
|
||||
self._picons_dir_field = builder.get_object("picons_dir_field")
|
||||
self._picons_paths_box = builder.get_object("picons_paths_box")
|
||||
self._remove_picon_path_button = builder.get_object("remove_picon_path_button")
|
||||
# Paths.
|
||||
self._picons_path_field = builder.get_object("picons_path_field")
|
||||
self._data_path_field = builder.get_object("data_path_field")
|
||||
self._backup_path_field = builder.get_object("backup_path_field")
|
||||
self._record_data_path_field = builder.get_object("record_data_path_field")
|
||||
self._default_data_paths_switch = builder.get_object("default_data_paths_switch")
|
||||
self._default_data_paths_switch.bind_property("active", self._backup_path_field, "sensitive", 4)
|
||||
self._default_data_paths_switch.bind_property("active", self._picons_path_field, "sensitive", 4)
|
||||
# Info bar.
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("info_bar_message_label")
|
||||
self._test_spinner = builder.get_object("test_spinner")
|
||||
# Settings type.
|
||||
self._enigma_radio_button = builder.get_object("enigma_radio_button")
|
||||
self._neutrino_radio_button = builder.get_object("neutrino_radio_button")
|
||||
self._support_ver5_switch = builder.get_object("support_ver5_switch")
|
||||
self._force_bq_name_switch = builder.get_object("force_bq_name_switch")
|
||||
# Streaming.
|
||||
self._apply_presets_button = builder.get_object("apply_presets_button")
|
||||
self._transcoding_switch = builder.get_object("transcoding_switch")
|
||||
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_grid"), "sensitive")
|
||||
self._edit_preset_switch.bind_property("active", builder.get_object("audio_options_grid"), "sensitive")
|
||||
self._play_streams_combo_box = builder.get_object("play_streams_combo_box")
|
||||
self._stream_lib_combo_box = builder.get_object("stream_lib_combo_box")
|
||||
self._double_click_combo_box = builder.get_object("double_click_combo_box")
|
||||
self._allow_main_list_playback_switch = builder.get_object("allow_main_list_playback_switch")
|
||||
# Program.
|
||||
self._before_save_switch = builder.get_object("before_save_switch")
|
||||
self._before_downloading_switch = builder.get_object("before_downloading_switch")
|
||||
self._load_on_startup_switch = builder.get_object("load_on_startup_switch")
|
||||
self._bouquet_hints_switch = builder.get_object("bouquet_hints_switch")
|
||||
self._services_hints_switch = builder.get_object("services_hints_switch")
|
||||
self._lang_combo_box = builder.get_object("lang_combo_box")
|
||||
# Appearance.
|
||||
self._list_font_button = builder.get_object("list_font_button")
|
||||
self._picons_size_button = builder.get_object("picons_size_button")
|
||||
self._tooltip_logo_size_button = builder.get_object("tooltip_logo_size_button")
|
||||
self._colors_grid = builder.get_object("colors_grid")
|
||||
self._set_color_switch = builder.get_object("set_color_switch")
|
||||
self._new_color_button = builder.get_object("new_color_button")
|
||||
self._extra_color_button = builder.get_object("extra_color_button")
|
||||
# Extra.
|
||||
self._support_http_api_switch = builder.get_object("support_http_api_switch")
|
||||
self._enable_yt_dl_switch = builder.get_object("enable_yt_dl_switch")
|
||||
self._enable_update_yt_dl_switch = builder.get_object("enable_update_yt_dl_switch")
|
||||
self._enable_send_to_switch = builder.get_object("enable_send_to_switch")
|
||||
# EXPERIMENTAL.
|
||||
self._enable_exp_switch = builder.get_object("enable_experimental_switch")
|
||||
self._enable_exp_switch.bind_property("active", builder.get_object("yt_dl_box"), "sensitive")
|
||||
self._enable_yt_dl_switch.bind_property("active", builder.get_object("yt_dl_update_box"), "sensitive")
|
||||
self._enable_exp_switch.bind_property("active", builder.get_object("v5_support_box"), "sensitive")
|
||||
self._enable_exp_switch.bind_property("active", builder.get_object("enable_direct_playback_box"), "sensitive")
|
||||
# Enigma2 only.
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("bq_naming_grid"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("program_frame"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("experimental_box"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("allow_double_click_box"), "sensitive")
|
||||
# Profiles.
|
||||
self._profile_view = builder.get_object("profile_tree_view")
|
||||
self._profile_add_button = builder.get_object("profile_add_button")
|
||||
self._profile_remove_button = builder.get_object("profile_remove_button")
|
||||
# Network.
|
||||
# Separated due to a bug with response (presumably in the builder) in ubuntu 18.04 and derivatives.
|
||||
builder.get_object("network_settings_frame").add(builder.get_object("network_box"))
|
||||
# Style.
|
||||
self._style_provider = Gtk.CssProvider()
|
||||
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
self._digit_elems = (self._port_field, self._http_port_field, self._telnet_port_field, self._video_width_field,
|
||||
self._video_bitrate_field, self._video_height_field, self._audio_bitrate_field)
|
||||
for el in self._digit_elems:
|
||||
el.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
self._options = options
|
||||
self._active_profile = options.get("profile")
|
||||
self.set_settings()
|
||||
self._neutrino_radio_button.set_active(Profile(self._active_profile) is Profile.NEUTRINO_MP)
|
||||
if IS_GNOME_SESSION:
|
||||
switcher = builder.get_object("main_stack_switcher")
|
||||
switcher.set_margin_top(0)
|
||||
switcher.set_margin_bottom(0)
|
||||
builder.get_object("main_box").remove(switcher)
|
||||
header_bar = Gtk.HeaderBar(visible=True, show_close_button=True)
|
||||
header_bar.set_custom_title(switcher)
|
||||
self._dialog.set_titlebar(header_bar)
|
||||
|
||||
self.init_ui_elements()
|
||||
self.init_profiles()
|
||||
|
||||
if not IS_LINUX:
|
||||
# Themes.
|
||||
builder.get_object("style_frame").set_visible(IS_WIN)
|
||||
builder.get_object("themes_support_frame").set_visible(True)
|
||||
self._layout_switch = builder.get_object("layout_switch")
|
||||
self._layout_switch.set_active(self._ext_settings.alternate_layout)
|
||||
self._force_ext_themes_switch = builder.get_object("force_ext_themes_switch")
|
||||
self._force_ext_themes_switch.set_active(self._settings.force_external_themes)
|
||||
self._theme_frame = builder.get_object("theme_frame")
|
||||
self._theme_frame.set_visible(True)
|
||||
self._theme_thumbnail_image = builder.get_object("theme_thumbnail_image")
|
||||
self._theme_combo_box = builder.get_object("theme_combo_box")
|
||||
self._icon_theme_combo_box = builder.get_object("icon_theme_combo_box")
|
||||
self._dark_mode_switch = builder.get_object("dark_mode_switch")
|
||||
self._dark_mode_switch.set_active(self._ext_settings.dark_mode)
|
||||
self._themes_support_switch = builder.get_object("themes_support_switch")
|
||||
self._themes_support_switch.bind_property("active", self._theme_frame, "sensitive")
|
||||
self.init_themes()
|
||||
|
||||
def init_ui_elements(self):
|
||||
is_enigma_profile = self._s_type is SettingsType.ENIGMA_2
|
||||
self._neutrino_radio_button.set_active(self._s_type is SettingsType.NEUTRINO_MP)
|
||||
self.update_picon_paths()
|
||||
self.update_title()
|
||||
self._lang_combo_box.set_active_id(self._ext_settings.language)
|
||||
self.on_info_bar_close() if is_enigma_profile else self.show_info_message(
|
||||
"The Neutrino has only experimental support. Not all features are supported!", Gtk.MessageType.WARNING)
|
||||
|
||||
def init_profiles(self):
|
||||
p_def = self._settings.default_profile
|
||||
model = self._profile_view.get_model()
|
||||
for ind, p in enumerate(self._profiles):
|
||||
icon = DEFAULT_ICON if p == p_def else None
|
||||
model.append((p, icon))
|
||||
if icon:
|
||||
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)
|
||||
|
||||
def update_title(self):
|
||||
title = "{} [{}]"
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
self._dialog.set_title(title.format(get_message("Options"), self._enigma_radio_button.get_label()))
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
self._dialog.set_title(title.format(get_message("Options"), self._neutrino_radio_button.get_label()))
|
||||
|
||||
def update_picon_paths(self):
|
||||
model = self._picons_paths_box.get_model()
|
||||
model.clear()
|
||||
list(map(lambda p: model.append((p, p)), self._settings.picons_paths))
|
||||
if self._settings.picons_path in self._settings.picons_paths:
|
||||
self._picons_paths_box.set_active_id(self._settings.picons_path)
|
||||
else:
|
||||
self._picons_paths_box.set_active(0)
|
||||
|
||||
def show(self):
|
||||
response = self._dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
self.apply_settings()
|
||||
write_config(self._options)
|
||||
self._dialog.destroy()
|
||||
return self._dialog.run()
|
||||
|
||||
return response
|
||||
def is_updated(self):
|
||||
return self._updated
|
||||
|
||||
def on_data_dir_field_icon_press(self, entry, icon, event_button):
|
||||
update_entry_data(entry, self._dialog, self._options.get(self._options.get("profile")))
|
||||
def on_response(self, dialog, resp):
|
||||
if resp == Gtk.ResponseType.ACCEPT:
|
||||
self._updated = self.on_save_settings()
|
||||
dialog.destroy()
|
||||
|
||||
def on_picons_dir_field_icon_press(self, entry, icon, event_button):
|
||||
update_entry_data(entry, self._dialog, self._options.get(self._options.get("profile")))
|
||||
def on_field_icon_press(self, entry, icon, event_button):
|
||||
update_entry_data(entry, self._dialog, self._settings)
|
||||
|
||||
def on_profile_changed(self, item):
|
||||
self.set_profile(Profile.ENIGMA_2 if self._enigma_radio_button.get_active() else Profile.NEUTRINO_MP)
|
||||
def on_settings_type_changed(self, item):
|
||||
s_type = SettingsType.ENIGMA_2 if self._enigma_radio_button.get_active() else SettingsType.NEUTRINO_MP
|
||||
if s_type is not self._s_type:
|
||||
self._settings.setting_type = s_type
|
||||
self._s_type = s_type
|
||||
self.on_reset()
|
||||
self.init_ui_elements()
|
||||
|
||||
def set_profile(self, profile):
|
||||
self._active_profile = profile.value
|
||||
self.set_settings()
|
||||
|
||||
def on_reset(self, item):
|
||||
def_settings = get_default_settings()
|
||||
for key in def_settings:
|
||||
current = self._options.get(key)
|
||||
if type(current) is str:
|
||||
continue
|
||||
default = def_settings.get(key)
|
||||
for k in default:
|
||||
current[k] = default.get(k)
|
||||
def on_reset(self, item=None):
|
||||
self._settings.reset()
|
||||
self.set_settings()
|
||||
|
||||
def set_settings(self):
|
||||
options = self._options.get(self._active_profile)
|
||||
self._host_field.set_text(options.get("host", ""))
|
||||
self._port_field.set_text(options.get("port", ""))
|
||||
self._login_field.set_text(options.get("user", ""))
|
||||
self._password_field.set_text(options.get("password", ""))
|
||||
self._telnet_login_field.set_text(options.get("telnet_user", ""))
|
||||
self._telnet_password_field.set_text(options.get("telnet_password", ""))
|
||||
self._telnet_port_field.set_text(options.get("telnet_port", ""))
|
||||
self._telnet_timeout_spin_button.set_value(options.get("telnet_timeout", 5))
|
||||
self._services_field.set_text(options.get("services_path", ""))
|
||||
self._user_bouquet_field.set_text(options.get("user_bouquet_path", ""))
|
||||
self._satellites_xml_field.set_text(options.get("satellites_xml_path", ""))
|
||||
self._picons_field.set_text(options.get("picons_path", ""))
|
||||
self._data_dir_field.set_text(options.get("data_dir_path", ""))
|
||||
self._picons_dir_field.set_text(options.get("picons_dir_path", ""))
|
||||
self._s_type = self._settings.setting_type
|
||||
self._host_field.set_text(self._settings.host)
|
||||
self._port_field.set_text(self._settings.port)
|
||||
self._login_field.set_text(self._settings.user)
|
||||
self._password_field.set_text(self._settings.password)
|
||||
self._http_port_field.set_text(self._settings.http_port)
|
||||
self._http_use_ssl_check_button.set_active(self._settings.http_use_ssl)
|
||||
self._telnet_port_field.set_text(self._settings.telnet_port)
|
||||
self._telnet_timeout_spin_button.set_value(self._settings.telnet_timeout)
|
||||
self._services_field.set_text(self._settings.services_path)
|
||||
self._user_bouquet_field.set_text(self._settings.user_bouquet_path)
|
||||
self._satellites_xml_field.set_text(self._settings.satellites_xml_path)
|
||||
self._picons_paths_box.set_active_id(self._settings.picons_path)
|
||||
self._data_path_field.set_text(self._settings.default_data_path)
|
||||
self._picons_path_field.set_text(self._settings.default_picon_path)
|
||||
self._backup_path_field.set_text(self._settings.default_backup_path)
|
||||
self._record_data_path_field.set_text(self._settings.records_path)
|
||||
self._before_save_switch.set_active(self._settings.backup_before_save)
|
||||
self._before_downloading_switch.set_active(self._settings.backup_before_downloading)
|
||||
self._play_streams_combo_box.set_active_id(str(self._settings.play_streams_mode.value))
|
||||
self._stream_lib_combo_box.set_active_id(self._settings.stream_lib)
|
||||
self._double_click_combo_box.set_active_id(str(self._settings.fav_click_mode))
|
||||
self._allow_main_list_playback_switch.set_active(self._settings.main_list_playback)
|
||||
self._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)
|
||||
self._picons_size_button.set_active_id(str(self._settings.list_picon_size))
|
||||
self._tooltip_logo_size_button.set_active_id(str(self._settings.tooltip_logo_size))
|
||||
self._list_font_button.set_font(self._settings.list_font)
|
||||
self._support_http_api_switch.set_active(self._settings.http_api_support)
|
||||
|
||||
def apply_settings(self, item=None):
|
||||
profile = Profile.ENIGMA_2.value if self._enigma_radio_button.get_active() else Profile.NEUTRINO_MP.value
|
||||
self._active_profile = profile
|
||||
self._options["profile"] = profile
|
||||
options = self._options.get(self._active_profile)
|
||||
options["host"] = self._host_field.get_text()
|
||||
options["port"] = self._port_field.get_text()
|
||||
options["user"] = self._login_field.get_text()
|
||||
options["password"] = self._password_field.get_text()
|
||||
options["telnet_user"] = self._telnet_login_field.get_text()
|
||||
options["telnet_password"] = self._telnet_password_field.get_text()
|
||||
options["telnet_port"] = self._telnet_port_field.get_text()
|
||||
options["telnet_timeout"] = int(self._telnet_timeout_spin_button.get_value())
|
||||
options["services_path"] = self._services_field.get_text()
|
||||
options["user_bouquet_path"] = self._user_bouquet_field.get_text()
|
||||
options["satellites_xml_path"] = self._satellites_xml_field.get_text()
|
||||
options["picons_path"] = self._picons_field.get_text()
|
||||
options["data_dir_path"] = self._data_dir_field.get_text()
|
||||
options["picons_dir_path"] = self._picons_dir_field.get_text()
|
||||
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._force_bq_name_switch.set_active(self._settings.force_bq_names)
|
||||
self._enable_yt_dl_switch.set_active(self._settings.enable_yt_dl)
|
||||
self._enable_update_yt_dl_switch.set_active(self._settings.enable_yt_dl_update)
|
||||
self._enable_send_to_switch.set_active(self._settings.enable_send_to)
|
||||
self._set_color_switch.set_active(self._settings.use_colors)
|
||||
new_rgb = Gdk.RGBA()
|
||||
new_rgb.parse(self._settings.new_color)
|
||||
extra_rgb = Gdk.RGBA()
|
||||
extra_rgb.parse(self._settings.extra_color)
|
||||
self._new_color_button.set_rgba(new_rgb)
|
||||
self._extra_color_button.set_rgba(extra_rgb)
|
||||
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
self._enigma_radio_button.activate()
|
||||
else:
|
||||
self._neutrino_radio_button.activate()
|
||||
|
||||
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._settings.setting_type = self._s_type
|
||||
self._settings.host = self._host_field.get_text()
|
||||
self._settings.port = self._port_field.get_text()
|
||||
self._settings.user = self._login_field.get_text()
|
||||
self._settings.password = self._password_field.get_text()
|
||||
self._settings.http_port = self._http_port_field.get_text()
|
||||
self._settings.http_use_ssl = self._http_use_ssl_check_button.get_active()
|
||||
self._settings.telnet_port = self._telnet_port_field.get_text()
|
||||
self._settings.telnet_timeout = int(self._telnet_timeout_spin_button.get_value())
|
||||
self._settings.services_path = self._services_field.get_text()
|
||||
self._settings.user_bouquet_path = self._user_bouquet_field.get_text()
|
||||
self._settings.satellites_xml_path = self._satellites_xml_field.get_text()
|
||||
self._settings.picons_path = self._picons_paths_box.get_active_id()
|
||||
|
||||
def on_save_settings(self, item=None):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.OK:
|
||||
return False
|
||||
|
||||
self.on_apply_profile_settings()
|
||||
self._ext_settings.profiles = self._settings.profiles
|
||||
self._ext_settings.backup_before_save = self._before_save_switch.get_active()
|
||||
self._ext_settings.backup_before_downloading = self._before_downloading_switch.get_active()
|
||||
self._ext_settings.play_streams_mode = PlayStreamsMode(int(self._play_streams_combo_box.get_active_id()))
|
||||
self._ext_settings.stream_lib = self._stream_lib_combo_box.get_active_id()
|
||||
self._ext_settings.fav_click_mode = int(self._double_click_combo_box.get_active_id())
|
||||
self._ext_settings.main_list_playback = self._allow_main_list_playback_switch.get_active()
|
||||
self._ext_settings.language = self._lang_combo_box.get_active_id()
|
||||
self._ext_settings.load_last_config = self._load_on_startup_switch.get_active()
|
||||
self._ext_settings.show_bq_hints = self._bouquet_hints_switch.get_active()
|
||||
self._ext_settings.show_srv_hints = self._services_hints_switch.get_active()
|
||||
self._ext_settings.profile_folder_is_default = self._default_data_paths_switch.get_active()
|
||||
self._ext_settings.default_data_path = self._data_path_field.get_text()
|
||||
self._ext_settings.default_backup_path = self._backup_path_field.get_text()
|
||||
self._ext_settings.default_picon_path = self._picons_path_field.get_text()
|
||||
self._ext_settings.records_path = self._record_data_path_field.get_text()
|
||||
self._ext_settings.activate_transcoding = self._transcoding_switch.get_active()
|
||||
self._ext_settings.active_preset = self._presets_combo_box.get_active_id()
|
||||
self._ext_settings.list_picon_size = int(self._picons_size_button.get_active_id())
|
||||
self._ext_settings.tooltip_logo_size = int(self._tooltip_logo_size_button.get_active_id())
|
||||
self._ext_settings.list_font = self._list_font_button.get_font()
|
||||
self._ext_settings.http_api_support = self._support_http_api_switch.get_active()
|
||||
|
||||
if not IS_LINUX:
|
||||
self._ext_settings.dark_mode = self._dark_mode_switch.get_active()
|
||||
self._ext_settings.alternate_layout = self._layout_switch.get_active()
|
||||
self._ext_settings.is_themes_support = self._themes_support_switch.get_active()
|
||||
self._ext_settings.theme = self._theme_combo_box.get_active_id()
|
||||
self._ext_settings.icon_theme = self._icon_theme_combo_box.get_active_id()
|
||||
self._ext_settings.force_external_themes = self._force_ext_themes_switch.get_active()
|
||||
|
||||
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.new_color = self._new_color_button.get_rgba().to_string()
|
||||
self._ext_settings.extra_color = self._extra_color_button.get_rgba().to_string()
|
||||
self._ext_settings.v5_support = self._support_ver5_switch.get_active()
|
||||
self._ext_settings.force_bq_names = self._force_bq_name_switch.get_active()
|
||||
self._ext_settings.enable_yt_dl = self._enable_yt_dl_switch.get_active()
|
||||
self._ext_settings.enable_yt_dl_update = self._enable_update_yt_dl_switch.get_active()
|
||||
self._ext_settings.enable_send_to = self._enable_send_to_switch.get_active()
|
||||
|
||||
self._ext_settings.default_profile = list(filter(lambda r: r[1], self._profile_view.get_model()))[0][0]
|
||||
self._ext_settings.save()
|
||||
|
||||
return True
|
||||
|
||||
@run_task
|
||||
def on_connection_test(self, item):
|
||||
if self._test_spinner.get_state() is Gtk.StateType.ACTIVE:
|
||||
return
|
||||
self.show_spinner(True)
|
||||
if self._ftp_radio_button.get_active():
|
||||
self.test_ftp()
|
||||
elif self._http_radio_button.get_active():
|
||||
self.test_http()
|
||||
else:
|
||||
self.test_telnet()
|
||||
|
||||
def test_http(self):
|
||||
user, password = self._login_field.get_text(), self._password_field.get_text()
|
||||
host, port = self._host_field.get_text(), self._http_port_field.get_text()
|
||||
use_ssl = self._http_use_ssl_check_button.get_active()
|
||||
try:
|
||||
self.show_info_message(test_http(host, port, user, password, use_ssl=use_ssl, s_type=self._s_type),
|
||||
Gtk.MessageType.INFO)
|
||||
except TestException as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
except HttpApiException as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.WARNING)
|
||||
finally:
|
||||
self.show_spinner(False)
|
||||
|
||||
def test_telnet(self):
|
||||
timeout = int(self._telnet_timeout_spin_button.get_value())
|
||||
host, port = self._host_field.get_text(), self._telnet_port_field.get_text()
|
||||
user, password = self._login_field.get_text(), self._password_field.get_text()
|
||||
try:
|
||||
self.show_info_message(test_telnet(host, port, user, password, timeout), Gtk.MessageType.INFO)
|
||||
self.show_spinner(False)
|
||||
except TestException as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
self.show_spinner(False)
|
||||
|
||||
def test_ftp(self):
|
||||
host, port = self._host_field.get_text(), self._port_field.get_text()
|
||||
user, password = self._login_field.get_text(), self._password_field.get_text()
|
||||
try:
|
||||
self.show_info_message(f"OK. {test_ftp(host, port, user, password)}", Gtk.MessageType.INFO)
|
||||
except TestException as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
finally:
|
||||
self.show_spinner(False)
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
self._info_bar.set_visible(False)
|
||||
self._info_bar.set_message_type(message_type)
|
||||
self._message_label.set_text(get_message(text))
|
||||
self._info_bar.set_visible(True)
|
||||
|
||||
@run_idle
|
||||
def show_spinner(self, show):
|
||||
self._test_spinner.start() if show else self._test_spinner.stop()
|
||||
self._test_spinner.set_state(Gtk.StateType.ACTIVE if show else Gtk.StateType.NORMAL)
|
||||
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
self._info_bar.set_visible(False)
|
||||
|
||||
def on_set_color_switch(self, switch, state):
|
||||
self._colors_grid.set_sensitive(state)
|
||||
|
||||
def on_http_mode_switch(self, switch, state):
|
||||
if self._main_stack.get_visible_child_name() == "program" and not state:
|
||||
self.show_info_message("May affect some features availability! ", Gtk.MessageType.WARNING)
|
||||
|
||||
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)
|
||||
|
||||
def on_default_path_mode_switch(self, switch, state):
|
||||
self._settings.profile_folder_is_default = state
|
||||
|
||||
def on_profile_add(self, item):
|
||||
model = self._profile_view.get_model()
|
||||
count = 0
|
||||
name = "profile"
|
||||
while name in self._profiles:
|
||||
count += 1
|
||||
name = f"profile{count}"
|
||||
|
||||
self._profiles[name] = self._s_type.get_default_settings()
|
||||
model.append((name, None))
|
||||
scroll_to(len(model) - 1, self._profile_view)
|
||||
self.on_profile_selected(self._profile_view, False)
|
||||
self.on_reset()
|
||||
|
||||
def on_profile_edit(self, item=None):
|
||||
model, paths = self._profile_view.get_selection().get_selected_rows()
|
||||
self._profile_view.set_cursor(paths, self._profile_view.get_column(0), True)
|
||||
|
||||
def on_profile_remove(self, item):
|
||||
model, paths = self._profile_view.get_selection().get_selected_rows()
|
||||
if paths:
|
||||
row = model[paths]
|
||||
is_default = row[1]
|
||||
self._profiles.pop(row[0], None)
|
||||
del model[paths]
|
||||
|
||||
if is_default:
|
||||
model.set_value(model.get_iter_first(), 1, DEFAULT_ICON)
|
||||
|
||||
def on_profile_deleted(self, model, paths):
|
||||
self._profile_remove_button.set_sensitive(len(model) > 1)
|
||||
|
||||
def on_profile_edited(self, render, path, new_value):
|
||||
row = self._profile_view.get_model()[path]
|
||||
old_name = row[0]
|
||||
if old_name == new_value:
|
||||
return
|
||||
|
||||
if new_value in self._profiles:
|
||||
show_dialog(DialogType.ERROR, self._dialog, "A profile with that name exists!")
|
||||
return
|
||||
|
||||
p_settings = self._profiles.pop(old_name, None)
|
||||
if p_settings:
|
||||
row[0] = new_value
|
||||
self._profiles[new_value] = p_settings
|
||||
self.on_profile_selected(self._profile_view, False)
|
||||
|
||||
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()
|
||||
if paths:
|
||||
profile = model.get_value(model.get_iter(paths), 0)
|
||||
self._settings.current_profile = profile
|
||||
self.set_settings()
|
||||
|
||||
def on_profile_set_default(self, item):
|
||||
model, paths = self._profile_view.get_selection().get_selected_rows()
|
||||
if paths:
|
||||
itr = model.get_iter(paths)
|
||||
model.foreach(lambda m, p, i: model.set_value(i, 1, None))
|
||||
model.set_value(itr, 1, DEFAULT_ICON)
|
||||
self._settings.default_profile = model.get_value(itr, 0)
|
||||
|
||||
def on_profile_inserted(self, model, path, itr):
|
||||
self._profile_remove_button.set_sensitive(len(model) > 1)
|
||||
|
||||
def on_add_picon_path(self, button):
|
||||
response = show_dialog(DialogType.INPUT, self._dialog, self._settings.picons_path)
|
||||
if response is Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
if response in self._settings.picons_paths:
|
||||
self.show_info_message("This path already exists!", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
path = response if response.endswith(SEP) else response + SEP
|
||||
model = self._picons_paths_box.get_model()
|
||||
model.append((path, path))
|
||||
self._picons_paths_box.set_active_id(path)
|
||||
self._ext_settings.picons_paths = tuple(r[0] for r in model)
|
||||
|
||||
def on_remove_picon_path(self, button):
|
||||
msg = f"{get_message('This may change the settings of other profiles!')}\n\n\t\t{get_message('Are you sure?')}"
|
||||
if show_dialog(DialogType.QUESTION, self._dialog, msg) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
model = self._picons_paths_box.get_model()
|
||||
active = self._picons_paths_box.get_active_iter()
|
||||
if active:
|
||||
model.remove(active)
|
||||
|
||||
self._picons_paths_box.set_active(0)
|
||||
self._remove_picon_path_button.set_sensitive(len(model) > 1)
|
||||
self._ext_settings.picons_paths = tuple(r[0] for r in model)
|
||||
|
||||
def on_lang_changed(self, box):
|
||||
if box.get_active_id() != self._settings.language:
|
||||
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
|
||||
|
||||
def on_main_settings_visible(self, stack, param):
|
||||
name = stack.get_visible_child_name()
|
||||
self._apply_presets_button.set_visible(name == "streaming")
|
||||
self._reset_button.set_visible(name == "profiles")
|
||||
|
||||
def on_http_use_ssl_toggled(self, button):
|
||||
active = button.get_active()
|
||||
self._settings.http_use_ssl = active
|
||||
port = "443" if active else "80"
|
||||
self._http_port_field.set_text(port)
|
||||
self._settings.http_port = port
|
||||
|
||||
def on_click_mode_togged(self, button):
|
||||
if self._main_stack.get_visible_child_name() != "streaming":
|
||||
return
|
||||
|
||||
mode = FavClickMode(int(self._double_click_combo_box.get_active_id()))
|
||||
if mode is FavClickMode.PLAY:
|
||||
self.show_info_message("Operates in standby mode or current active transponder!", Gtk.MessageType.WARNING)
|
||||
elif mode is FavClickMode.STREAM:
|
||||
self.show_info_message("Playback IPTV streams only!", Gtk.MessageType.WARNING)
|
||||
elif mode is FavClickMode.DISABLED:
|
||||
self._allow_main_list_playback_switch.set_active(False)
|
||||
else:
|
||||
self.on_info_bar_close()
|
||||
|
||||
self._allow_main_list_playback_switch.set_sensitive(mode is not FavClickMode.DISABLED)
|
||||
|
||||
def on_play_mode_changed(self, button):
|
||||
if self._main_stack.get_visible_child_name() != "streaming":
|
||||
return
|
||||
|
||||
self.show_info_message("Save and restart the program to apply the settings.", Gtk.MessageType.WARNING)
|
||||
|
||||
def on_transcoding_preset_changed(self, button):
|
||||
presets = self._settings.transcoding_presets
|
||||
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):
|
||||
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
|
||||
menu.popup(None, None, None, None, event.button, event.time)
|
||||
|
||||
def on_list_font_reset(self, button):
|
||||
self._list_font_button.set_font(APP_FONT)
|
||||
|
||||
# ******************* Themes *********************** #
|
||||
|
||||
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, SEP, SEP)
|
||||
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_appearance_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._theme_frame.set_sensitive(False)
|
||||
self.unpack_theme(response, path, button)
|
||||
|
||||
@run_task
|
||||
def unpack_theme(self, src, dst, button):
|
||||
try:
|
||||
from shutil import unpack_archive
|
||||
|
||||
log(f"Unpacking '{src}' started...")
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
unpack_archive(src, dst)
|
||||
log("Unpacking end.")
|
||||
except (ValueError, OSError) as e:
|
||||
msg = f"Unpacking error: {e}"
|
||||
log(msg)
|
||||
self.show_info_message(msg, Gtk.MessageType.ERROR)
|
||||
finally:
|
||||
self.update_theme_button(button, dst)
|
||||
|
||||
@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)
|
||||
self._theme_frame.set_sensitive(True)
|
||||
|
||||
@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_themes(self):
|
||||
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__":
|
||||
|
||||
@@ -1,3 +1,89 @@
|
||||
* {
|
||||
-GtkDialog-action-area-border: 6;
|
||||
}
|
||||
|
||||
#digit-entry {
|
||||
border-color: Red;
|
||||
}
|
||||
border-color: Red;
|
||||
}
|
||||
|
||||
#status-bar-button {
|
||||
padding-top: 1px;
|
||||
padding-bottom: 1px;
|
||||
padding-left: 3px;
|
||||
padding-right: 3px;
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
#stack-switch-button {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
paned > separator {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
paned.horizontal > separator {
|
||||
background-size: 2px 24px;
|
||||
}
|
||||
|
||||
paned.vertical > separator {
|
||||
background-size: 24px 2px;
|
||||
}
|
||||
|
||||
.red-button {
|
||||
background-image: none;
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.green-button {
|
||||
background-image: none;
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
.yellow-button {
|
||||
background-image: none;
|
||||
background-color: yellow;
|
||||
}
|
||||
|
||||
.blue-button {
|
||||
background-image: none;
|
||||
background-color: blue;
|
||||
}
|
||||
|
||||
.time-entry {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.group {}
|
||||
|
||||
.group :first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.group :last-child {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left-width: 0;
|
||||
}
|
||||
|
||||
.group :not(:first-child):not(:last-child) {
|
||||
border-radius: 0;
|
||||
border-left-width: 0;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
||||
.stack-switcher > button > label {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.stack-switcher > button.text-button {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
177
app/ui/telnet.glade
Normal file
177
app/ui/telnet.glade
Normal file
@@ -0,0 +1,177 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2020 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.18"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-copyright 2018-2021 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="GtkFrame" id="telnet_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0.49000000953674316</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="telnet_main_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="commands_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">2</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="connect_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Connect</property>
|
||||
<signal name="clicked" handler="on_connect" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="connect_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-connect</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="disconnect_button">
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Disconnect</property>
|
||||
<signal name="clicked" handler="on_disconnect" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="disconnect_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-disconnect</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="clear_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Clear</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_clear" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="clear_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-clear</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="telnet_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="text_view">
|
||||
<property name="name">textview-large</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="wrap_mode">char</property>
|
||||
<property name="left_margin">5</property>
|
||||
<property name="right_margin">5</property>
|
||||
<property name="buffer">text_buffer</property>
|
||||
<property name="overwrite">True</property>
|
||||
<property name="input_hints">GTK_INPUT_HINT_WORD_COMPLETION | GTK_INPUT_HINT_NONE</property>
|
||||
<property name="monospace">True</property>
|
||||
<signal name="key-press-event" handler="on_view_key_press" swapped="no"/>
|
||||
<signal name="realize" handler="on_text_view_realize" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Telnet</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
252
app/ui/telnet.py
Normal file
252
app/ui/telnet.py
Normal file
@@ -0,0 +1,252 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import re
|
||||
import selectors
|
||||
import socket
|
||||
from collections import deque
|
||||
from telnetlib import Telnet
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import run_task, run_idle, log
|
||||
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 TelnetClient(Gtk.Box):
|
||||
""" Very simple telnet client. """
|
||||
_COLOR_PATTERN = re.compile("\x1b.*?m") # Color info
|
||||
_ERASING_PATTERN = re.compile("\x1b.*?K") # Erase to right
|
||||
_APP_MODE_PATTERN = re.compile("\x1b.*?(1h)|(1l)") # h - on, l - off
|
||||
_ALL_PATTERN = re.compile(r'(\x1b\[|\x9b)[0-?]*[@-~]')
|
||||
_NOT_SUPPORTED = {"mc", "mcedit", "vi", "nano"}
|
||||
|
||||
def __init__(self, app, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._app = app
|
||||
self._app.connect("profile-changed", self.on_profile_changed)
|
||||
|
||||
self._tn = None
|
||||
self._app_mode = False
|
||||
self._commands = deque(maxlen=10)
|
||||
|
||||
self._handlers = {"on_clear": self.on_clear,
|
||||
"on_text_view_realize": self.on_text_view_realize,
|
||||
"on_view_key_press": self.on_view_key_press,
|
||||
"on_connect": self.on_connect,
|
||||
"on_disconnect": self.on_disconnect}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "telnet.glade")
|
||||
builder.connect_signals(self._handlers)
|
||||
|
||||
self._text_view = builder.get_object("text_view")
|
||||
self._buf = builder.get_object("text_buffer")
|
||||
self._end_tag = builder.get_object("end_tag")
|
||||
self._connect_button = builder.get_object("connect_button")
|
||||
self._connect_button.bind_property("visible", builder.get_object("disconnect_button"), "visible", 4)
|
||||
|
||||
main_frame = builder.get_object("telnet_frame")
|
||||
provider = Gtk.CssProvider()
|
||||
provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
main_frame.get_style_context().add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
self.pack_start(main_frame, True, True, 0)
|
||||
self.show()
|
||||
|
||||
def on_profile_changed(self, app, data):
|
||||
self.on_clear()
|
||||
self.on_disconnect()
|
||||
self.on_connect()
|
||||
|
||||
def on_text_view_realize(self, view):
|
||||
self.on_connect()
|
||||
|
||||
@run_task
|
||||
def on_connect(self, item=None):
|
||||
try:
|
||||
GLib.idle_add(self._connect_button.set_visible, False)
|
||||
settings = self._app.app_settings
|
||||
user, password, timeout = settings.user, settings.password, settings.telnet_timeout
|
||||
self._tn = ExtTelnet(self.append_output, host=settings.host, port=settings.telnet_port, timeout=timeout)
|
||||
|
||||
if user != "":
|
||||
self._tn.read_until(b"login: ")
|
||||
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(f"{self.__class__.__name__}: {e}")
|
||||
self._app.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
finally:
|
||||
GLib.idle_add(self._connect_button.set_visible, True)
|
||||
|
||||
@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_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._app.show_info_message(f"'{cmd}' is not supported by this client.", Gtk.MessageType.ERROR)
|
||||
else:
|
||||
self._tn.write(cmd.encode("ascii") + b"\r")
|
||||
self._commands.append(cmd)
|
||||
|
||||
@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
|
||||
233
app/ui/transmitter.glade
Normal file
233
app/ui/transmitter.glade
Normal file
@@ -0,0 +1,233 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2020 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2020 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkWindow" id="main_window">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="resizable">False</property>
|
||||
<property name="window_position">mouse</property>
|
||||
<property name="destroy_with_parent">True</property>
|
||||
<property name="skip_taskbar_hint">True</property>
|
||||
<property name="skip_pager_hint">True</property>
|
||||
<property name="decorated">False</property>
|
||||
<property name="gravity">center</property>
|
||||
<property name="has_resize_grip">True</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="tool_bar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">1</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="previous_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Previous stream in the list</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_left">1</property>
|
||||
<property name="margin_top">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<signal name="clicked" handler="on_previous" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="previous_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-media-previous</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="next_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Next stream in the list</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_top">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<signal name="clicked" handler="on_next" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="next_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-media-next</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="GtkEntry" id="url_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Drag or paste the link here</property>
|
||||
<property name="margin_left">2</property>
|
||||
<property name="margin_right">2</property>
|
||||
<property name="margin_top">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<property name="primary_icon_stock">gtk-paste</property>
|
||||
<signal name="activate" handler="on_url_activate" swapped="no"/>
|
||||
<signal name="changed" handler="on_url_changed" swapped="no"/>
|
||||
<signal name="drag-data-received" handler="on_drag_data_received" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="play_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Play</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_top">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<signal name="clicked" handler="on_play" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="play_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-media-play</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="stop_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Stop playback</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_top">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<signal name="clicked" handler="on_stop" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="stop_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-media-stop</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="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">Remove added links in the playlist</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_right">1</property>
|
||||
<property name="margin_top">1</property>
|
||||
<property name="margin_bottom">1</property>
|
||||
<signal name="clicked" handler="on_clear" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="clear_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-clear</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="primary-toolbar"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkImage" id="show_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">view-restore</property>
|
||||
</object>
|
||||
<object class="GtkMenu" id="staus_popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="show_menu_item">
|
||||
<property name="label" translatable="yes">Show/Hide</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="image">show_image</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_status_icon_activate" object="main_window" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkStatusIcon" id="status_icon">
|
||||
<property name="icon_name">demon-editor</property>
|
||||
<property name="has_tooltip">True</property>
|
||||
<signal name="activate" handler="on_status_icon_activate" object="main_window" swapped="no"/>
|
||||
<signal name="popup-menu" handler="on_popup_menu" object="staus_popup_menu" swapped="no"/>
|
||||
</object>
|
||||
</interface>
|
||||
174
app/ui/transmitter.py
Normal file
174
app/ui/transmitter.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import gi
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import log
|
||||
from app.connections import HttpAPI
|
||||
from app.tools.yt import YouTube
|
||||
from app.ui.dialogs import get_builder
|
||||
from app.ui.iptv import get_yt_icon
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH
|
||||
|
||||
|
||||
class LinksTransmitter:
|
||||
""" The main class for the "send to" function.
|
||||
|
||||
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:"
|
||||
|
||||
def __init__(self, http_api, app_window, settings):
|
||||
handlers = {"on_popup_menu": self.on_popup_menu,
|
||||
"on_status_icon_activate": self.on_status_icon_activate,
|
||||
"on_url_changed": self.on_url_changed,
|
||||
"on_url_activate": self.on_url_activate,
|
||||
"on_drag_data_received": self.on_drag_data_received,
|
||||
"on_previous": self.on_previous,
|
||||
"on_next": self.on_next,
|
||||
"on_stop": self.on_stop,
|
||||
"on_clear": self.on_clear,
|
||||
"on_play": self.on_play}
|
||||
|
||||
self._http_api = http_api
|
||||
self._app_window = app_window
|
||||
self._is_status_icon = True
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "transmitter.glade", handlers)
|
||||
|
||||
self._main_window = builder.get_object("main_window")
|
||||
self._url_entry = builder.get_object("url_entry")
|
||||
self._tool_bar = builder.get_object("tool_bar")
|
||||
self._popup_menu = builder.get_object("staus_popup_menu")
|
||||
self._restore_menu_item = builder.get_object("restore_menu_item")
|
||||
self._status_active = None
|
||||
self._status_passive = None
|
||||
self._yt = YouTube.get_instance(settings)
|
||||
|
||||
try:
|
||||
gi.require_version("AppIndicator3", "0.1")
|
||||
from gi.repository import AppIndicator3
|
||||
except (ImportError, ValueError) as e:
|
||||
log("{}: Load library error: {}".format(__class__.__name__, e))
|
||||
self._tray = builder.get_object("status_icon")
|
||||
else:
|
||||
self._is_status_icon = False
|
||||
self._status_active = AppIndicator3.IndicatorStatus.ACTIVE
|
||||
self._status_passive = AppIndicator3.IndicatorStatus.PASSIVE
|
||||
|
||||
category = AppIndicator3.IndicatorCategory.APPLICATION_STATUS
|
||||
path = Path(UI_RESOURCES_PATH + "/icons/hicolor/scalable/apps/demon-editor.svg")
|
||||
path = str(path.resolve()) if path.is_file() else "demon-editor"
|
||||
self._tray = AppIndicator3.Indicator.new("DemonEditor", path, category)
|
||||
self._tray.set_status(self._status_active)
|
||||
self._tray.set_secondary_activate_target(builder.get_object("show_menu_item"))
|
||||
self._tray.set_menu(self._popup_menu)
|
||||
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
|
||||
self._url_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
def show(self, show):
|
||||
if self._is_status_icon:
|
||||
self._tray.set_visible(show)
|
||||
elif self._status_active:
|
||||
self._tray.set_status(self._status_active if show else self._status_passive)
|
||||
if not show:
|
||||
self.hide()
|
||||
|
||||
def hide(self):
|
||||
self._main_window.hide()
|
||||
|
||||
def on_popup_menu(self, menu, button, time):
|
||||
menu.popup(None, None, None, None, button, time)
|
||||
|
||||
def on_status_icon_activate(self, window):
|
||||
visible = window.get_visible()
|
||||
window.hide() if visible else window.show()
|
||||
self._app_window.present() if visible else self._app_window.iconify()
|
||||
|
||||
def on_url_changed(self, entry):
|
||||
entry.set_name("GtkEntry" if self.is_url(entry.get_text()) else "digit-entry")
|
||||
|
||||
def on_url_activate(self, entry):
|
||||
gen = self.activate_url(entry.get_text())
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def on_drag_data_received(self, entry, drag_context, x, y, data, info, time):
|
||||
url = data.get_text()
|
||||
GLib.idle_add(entry.set_text, url)
|
||||
gen = self.activate_url(url)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def activate_url(self, url):
|
||||
self._url_entry.set_name("GtkEntry")
|
||||
self._url_entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None)
|
||||
|
||||
if self.is_url(url):
|
||||
self._tool_bar.set_sensitive(False)
|
||||
yt_id = YouTube.get_yt_id(url)
|
||||
yield True
|
||||
|
||||
if yt_id:
|
||||
self._url_entry.set_icon_from_pixbuf(Gtk.EntryIconPosition.SECONDARY, get_yt_icon("youtube", 32))
|
||||
links, title = self._yt.get_yt_link(yt_id, url)
|
||||
yield True
|
||||
if links:
|
||||
url = links[sorted(links, key=lambda x: int(x.rstrip("p")), reverse=True)[0]]
|
||||
else:
|
||||
self.on_done(links)
|
||||
return
|
||||
else:
|
||||
self._url_entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, None)
|
||||
|
||||
self._http_api.send(HttpAPI.Request.PLAY, url, self.on_done, self.__STREAM_PREFIX)
|
||||
yield True
|
||||
|
||||
def on_done(self, res):
|
||||
""" Play callback """
|
||||
res = res.get("e2state", None) if res else res
|
||||
self._url_entry.set_name("GtkEntry" if res else "digit-entry")
|
||||
GLib.idle_add(self._tool_bar.set_sensitive, True)
|
||||
|
||||
def on_previous(self, item):
|
||||
self._http_api.send(HttpAPI.Request.PLAYER_PREV, None, self.on_done)
|
||||
|
||||
def on_next(self, item):
|
||||
self._http_api.send(HttpAPI.Request.PLAYER_NEXT, None, self.on_done)
|
||||
|
||||
def on_play(self, item):
|
||||
self._http_api.send(HttpAPI.Request.PLAYER_PLAY, None, self.on_done)
|
||||
|
||||
def on_stop(self, item):
|
||||
self._http_api.send(HttpAPI.Request.PLAYER_STOP, None, self.on_done)
|
||||
|
||||
def on_clear(self, item):
|
||||
""" Remove added links in the playlist. """
|
||||
GLib.idle_add(self._tool_bar.set_sensitive, False)
|
||||
self._http_api.send(HttpAPI.Request.PLAYER_LIST, None, self.clear_playlist)
|
||||
|
||||
def clear_playlist(self, res):
|
||||
GLib.idle_add(self._tool_bar.set_sensitive, not res)
|
||||
if "error_code" in res:
|
||||
log("Error clearing playlist. There may be no http connection.")
|
||||
self.on_done(res)
|
||||
return
|
||||
|
||||
for ref in res:
|
||||
GLib.idle_add(self._tool_bar.set_sensitive, False)
|
||||
self._http_api.send(HttpAPI.Request.PLAYER_REMOVE,
|
||||
ref.get("e2servicereference", ""),
|
||||
self.on_done,
|
||||
self.__STREAM_PREFIX)
|
||||
|
||||
@staticmethod
|
||||
def is_url(text):
|
||||
""" Simple url checking. """
|
||||
result = urlparse(text)
|
||||
return result.scheme and result.netloc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
411
app/ui/uicommons.py
Normal file
411
app/ui/uicommons.py
Normal file
@@ -0,0 +1,411 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2021 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import locale
|
||||
import os
|
||||
from enum import Enum, IntEnum
|
||||
from functools import lru_cache
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("Gdk", "3.0")
|
||||
from gi.repository import Gtk, Gdk, GLib
|
||||
|
||||
from app.settings import Settings, SettingsException, IS_DARWIN, GTK_PATH, IS_LINUX
|
||||
|
||||
# Setting mod mask for keyboard depending on platform
|
||||
MOD_MASK = Gdk.ModifierType.MOD2_MASK if IS_DARWIN else Gdk.ModifierType.CONTROL_MASK
|
||||
# Paths.
|
||||
BASE_PATH = "app/ui/"
|
||||
EX_PATH = "/usr/share/demoneditor/app/ui/" if IS_LINUX else "ui/"
|
||||
# Path to *.glade files.
|
||||
UI_RESOURCES_PATH = BASE_PATH if os.path.exists(BASE_PATH) else EX_PATH
|
||||
# Translation.
|
||||
LANG_PATH = UI_RESOURCES_PATH + "lang"
|
||||
TEXT_DOMAIN = "demon-editor"
|
||||
|
||||
NOTIFY_IS_INIT = False
|
||||
APP_FONT = None
|
||||
IS_GNOME_SESSION = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
|
||||
|
||||
try:
|
||||
settings = Settings.get_instance()
|
||||
except SettingsException:
|
||||
pass
|
||||
else:
|
||||
os.environ["LANGUAGE"] = settings.language
|
||||
st = Gtk.Settings().get_default()
|
||||
APP_FONT = st.get_property("gtk-font-name")
|
||||
st.set_property("gtk-application-prefer-dark-theme", settings.dark_mode)
|
||||
|
||||
if settings.is_themes_support:
|
||||
st.set_property("gtk-theme-name", settings.theme)
|
||||
st.set_property("gtk-icon-theme-name", settings.icon_theme)
|
||||
else:
|
||||
if not IS_LINUX:
|
||||
if IS_DARWIN:
|
||||
s_path = f"{GTK_PATH + '/' + UI_RESOURCES_PATH if GTK_PATH else UI_RESOURCES_PATH}mac_style.css"
|
||||
else:
|
||||
s_path = f"{UI_RESOURCES_PATH}win_style.css"
|
||||
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_path(s_path)
|
||||
Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), style_provider,
|
||||
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||||
|
||||
if IS_LINUX:
|
||||
if UI_RESOURCES_PATH == BASE_PATH:
|
||||
locale.bindtextdomain(TEXT_DOMAIN, LANG_PATH)
|
||||
# Init notify
|
||||
try:
|
||||
gi.require_version("Notify", "0.7")
|
||||
from gi.repository import Notify
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
NOTIFY_IS_INIT = Notify.init("DemonEditor")
|
||||
elif IS_DARWIN:
|
||||
import gettext
|
||||
|
||||
if GTK_PATH:
|
||||
LANG_PATH = GTK_PATH + "/share/locale"
|
||||
gettext.bindtextdomain(TEXT_DOMAIN, LANG_PATH)
|
||||
# For launching from the bundle.
|
||||
if os.getcwd() == "/" and GTK_PATH:
|
||||
os.chdir(GTK_PATH)
|
||||
else:
|
||||
locale.setlocale(locale.LC_NUMERIC, "C")
|
||||
|
||||
# Icons.
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
theme.append_search_path(UI_RESOURCES_PATH + "icons")
|
||||
|
||||
|
||||
def get_icon(name, size, default=None):
|
||||
try:
|
||||
return theme.load_icon(name, size, 0) if theme.lookup_icon(name, size, 0) else default
|
||||
except GLib.Error:
|
||||
return default
|
||||
|
||||
|
||||
_IMAGE_MISSING = get_icon("image-missing", 16)
|
||||
CODED_ICON = get_icon("emblem-readonly", 16, _IMAGE_MISSING)
|
||||
LOCKED_ICON = get_icon("changes-prevent-symbolic", 16, _IMAGE_MISSING)
|
||||
HIDE_ICON = get_icon("go-jump", 16, _IMAGE_MISSING)
|
||||
TV_ICON = get_icon("tv-symbolic", 16, _IMAGE_MISSING)
|
||||
IPTV_ICON = get_icon("emblem-shared", 16, _IMAGE_MISSING)
|
||||
EPG_ICON = get_icon("gtk-index", 16, _IMAGE_MISSING)
|
||||
DEFAULT_ICON = get_icon("emblem-default", 16, _IMAGE_MISSING)
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
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!
|
||||
"""
|
||||
default_theme = Gtk.IconTheme.get_default()
|
||||
if default_theme.has_icon(icon_name):
|
||||
return default_theme.load_icon(icon_name, size, 0)
|
||||
|
||||
n_theme = Gtk.IconTheme.new()
|
||||
import glob
|
||||
|
||||
for theme_name in map(os.path.basename, filter(os.path.isdir, glob.glob("/usr/share/icons/*"))):
|
||||
n_theme.set_custom_theme(theme_name)
|
||||
if n_theme.has_icon(icon_name):
|
||||
return n_theme.load_icon(icon_name, size, 0)
|
||||
|
||||
return default_theme.load_icon("emblem-important-symbolic", size, 0)
|
||||
|
||||
|
||||
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 Page(Enum):
|
||||
""" Main stack widget page. """
|
||||
INFO = "info"
|
||||
SERVICES = "services"
|
||||
SATELLITE = "satellite"
|
||||
PICONS = "picons"
|
||||
EPG = "epg"
|
||||
TIMERS = "timers"
|
||||
RECORDINGS = "recordings"
|
||||
FTP = "ftp"
|
||||
CONTROL = "control"
|
||||
|
||||
|
||||
class FavClickMode(IntEnum):
|
||||
""" Double click mode on the service in the bouquet(FAV) list. """
|
||||
DISABLED = 0
|
||||
STREAM = 1
|
||||
PLAY = 2
|
||||
ZAP = 3
|
||||
ZAP_PLAY = 4
|
||||
|
||||
|
||||
class ViewTarget(Enum):
|
||||
""" Used for set target view. """
|
||||
BOUQUET = 0
|
||||
FAV = 1
|
||||
SERVICES = 2
|
||||
IPTV = 3
|
||||
|
||||
|
||||
class BqGenType(Enum):
|
||||
""" Bouquet generation type. """
|
||||
SAT = 0
|
||||
EACH_SAT = 1
|
||||
PACKAGE = 2
|
||||
EACH_PACKAGE = 3
|
||||
TYPE = 4
|
||||
EACH_TYPE = 5
|
||||
|
||||
|
||||
class Column(IntEnum):
|
||||
""" Column nums in the views """
|
||||
# Main view
|
||||
SRV_CAS_FLAGS = 0
|
||||
SRV_STANDARD = 1
|
||||
SRV_CODED = 2
|
||||
SRV_SERVICE = 3
|
||||
SRV_LOCKED = 4
|
||||
SRV_HIDE = 5
|
||||
SRV_PACKAGE = 6
|
||||
SRV_TYPE = 7
|
||||
SRV_PICON = 8
|
||||
SRV_PICON_ID = 9
|
||||
SRV_SSID = 10
|
||||
SRV_FREQ = 11
|
||||
SRV_RATE = 12
|
||||
SRV_POL = 13
|
||||
SRV_FEC = 14
|
||||
SRV_SYSTEM = 15
|
||||
SRV_POS = 16
|
||||
SRV_DATA_ID = 17
|
||||
SRV_FAV_ID = 18
|
||||
SRV_TRANSPONDER = 19
|
||||
SRV_TOOLTIP = 20
|
||||
SRV_BACKGROUND = 21
|
||||
# FAV view
|
||||
FAV_NUM = 0
|
||||
FAV_CODED = 1
|
||||
FAV_SERVICE = 2
|
||||
FAV_LOCKED = 3
|
||||
FAV_HIDE = 4
|
||||
FAV_TYPE = 5
|
||||
FAV_POS = 6
|
||||
FAV_ID = 7
|
||||
FAV_PICON = 8
|
||||
FAV_TOOLTIP = 9
|
||||
FAV_BACKGROUND = 10
|
||||
# Bouquets view
|
||||
BQ_NAME = 0
|
||||
BQ_LOCKED = 1
|
||||
BQ_HIDDEN = 2
|
||||
BQ_TYPE = 3
|
||||
# Alternatives view
|
||||
ALT_NUM = 0
|
||||
ALT_PICON = 1
|
||||
ALT_SERVICE = 2
|
||||
ALT_TYPE = 3
|
||||
ALT_POS = 4
|
||||
ALT_FAV_ID = 5
|
||||
ALT_ID = 6
|
||||
ALT_ITER = 7
|
||||
# Recordings view
|
||||
REC_SERVICE = 0
|
||||
REC_TITLE = 1
|
||||
REC_TIME = 2
|
||||
REC_LEN = 3
|
||||
REC_FILE = 4
|
||||
REC_DESC = 5
|
||||
# IPTV view
|
||||
IPTV_SERVICE = 0
|
||||
IPTV_TYPE = 1
|
||||
IPTV_PICON = 2
|
||||
IPTV_REF = 3
|
||||
IPTV_FAV_ID = 4
|
||||
IPTV_PICON_ID = 5
|
||||
|
||||
def __index__(self):
|
||||
""" Overridden to get the index in slices directly """
|
||||
return self.value
|
||||
|
||||
|
||||
# *************** Keyboard keys *************** #
|
||||
|
||||
class BaseKeyboardKey(Enum):
|
||||
@classmethod
|
||||
def value_exist(cls, value):
|
||||
return value in (val.value for val in cls.__members__.values())
|
||||
|
||||
|
||||
if IS_LINUX:
|
||||
class KeyboardKey(BaseKeyboardKey):
|
||||
""" The raw(hardware) codes [Linux] of the keyboard keys. """
|
||||
E = 26
|
||||
R = 27
|
||||
T = 28
|
||||
P = 33
|
||||
S = 39
|
||||
F = 41
|
||||
X = 53
|
||||
C = 54
|
||||
V = 55
|
||||
W = 25
|
||||
Z = 52
|
||||
INSERT = 118
|
||||
HOME = 110
|
||||
END = 115
|
||||
UP = 111
|
||||
DOWN = 116
|
||||
PAGE_UP = 112
|
||||
PAGE_DOWN = 117
|
||||
LEFT = 113
|
||||
RIGHT = 114
|
||||
F2 = 68
|
||||
F4 = 70
|
||||
F5 = 71
|
||||
F7 = 73
|
||||
SPACE = 65
|
||||
DELETE = 119
|
||||
BACK_SPACE = 22
|
||||
RETURN = 36
|
||||
CTRL_L = 37
|
||||
CTRL_R = 105
|
||||
# Laptop codes
|
||||
HOME_KP = 79
|
||||
END_KP = 87
|
||||
PAGE_UP_KP = 81
|
||||
PAGE_DOWN_KP = 89
|
||||
|
||||
elif IS_DARWIN:
|
||||
class KeyboardKey(BaseKeyboardKey):
|
||||
""" The raw(hardware) codes [macOS] of the keyboard keys. """
|
||||
F = 3
|
||||
E = 14
|
||||
R = 15
|
||||
T = 17
|
||||
P = 35
|
||||
S = 1
|
||||
H = 4
|
||||
L = 37
|
||||
X = 7
|
||||
C = 8
|
||||
V = 9
|
||||
W = 13
|
||||
Z = 6
|
||||
INSERT = -1
|
||||
HOME = -1
|
||||
END = -1
|
||||
UP = 126
|
||||
DOWN = 125
|
||||
PAGE_UP = -1
|
||||
PAGE_DOWN = -1
|
||||
LEFT = 123
|
||||
RIGHT = 123
|
||||
F2 = 120
|
||||
F4 = 118
|
||||
F5 = 96
|
||||
F7 = 98
|
||||
SPACE = 49
|
||||
DELETE = 51
|
||||
BACK_SPACE = 76
|
||||
RETURN = 36
|
||||
CTRL_L = 55
|
||||
CTRL_R = 55
|
||||
# Laptop codes.
|
||||
HOME_KP = -1
|
||||
END_KP = -1
|
||||
PAGE_UP_KP = -1
|
||||
PAGE_DOWN_KP = -1
|
||||
|
||||
else:
|
||||
class KeyboardKey(BaseKeyboardKey):
|
||||
""" The raw(hardware) codes [Windows] of the keyboard keys. """
|
||||
E = 69
|
||||
R = 82
|
||||
T = 84
|
||||
P = 80
|
||||
S = 83
|
||||
F = 70
|
||||
X = 88
|
||||
C = 67
|
||||
V = 86
|
||||
W = 87
|
||||
Z = 90
|
||||
INSERT = 45
|
||||
HOME = 36
|
||||
END = 35
|
||||
UP = 38
|
||||
DOWN = 40
|
||||
PAGE_UP = 33
|
||||
PAGE_DOWN = 34
|
||||
LEFT = 37
|
||||
RIGHT = 39
|
||||
F2 = 113
|
||||
F4 = 115
|
||||
F5 = 116
|
||||
F7 = 118
|
||||
SPACE = 32
|
||||
DELETE = 46
|
||||
BACK_SPACE = 8
|
||||
RETURN = 13
|
||||
CTRL_L = 17
|
||||
CTRL_R = 163
|
||||
# Laptop codes.
|
||||
HOME_KP = -1
|
||||
END_KP = -1
|
||||
PAGE_UP_KP = -1
|
||||
PAGE_DOWN_KP = -1
|
||||
|
||||
# Keys for move in lists. KEY_KP_(NAME) for laptop!
|
||||
MOVE_KEYS = {KeyboardKey.UP, KeyboardKey.PAGE_UP,
|
||||
KeyboardKey.DOWN, KeyboardKey.PAGE_DOWN,
|
||||
KeyboardKey.HOME, KeyboardKey.END,
|
||||
KeyboardKey.HOME_KP, KeyboardKey.END_KP,
|
||||
KeyboardKey.PAGE_UP_KP, KeyboardKey.PAGE_DOWN_KP}
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
20
app/ui/win_style.css
Normal file
20
app/ui/win_style.css
Normal file
@@ -0,0 +1,20 @@
|
||||
* {
|
||||
-GtkDialog-action-area-border: 12;
|
||||
}
|
||||
|
||||
switch {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
spinbutton entry {
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
button > image {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
grid > button {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
7
build/BUILDING.md
Normal file
7
build/BUILDING.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## Building DemonEditor
|
||||
This directory contains build scripts and additional files for various platforms and distributions.
|
||||
|
||||
### Supported platforms
|
||||
* GNU/Linux
|
||||
* macOS
|
||||
* MS Windows
|
||||
40
build/BUILD_WIN.md
Normal file
40
build/BUILD_WIN.md
Normal file
@@ -0,0 +1,40 @@
|
||||
## Launch
|
||||
The best way to run this program from source is using of [MSYS2](https://www.msys2.org/) platform.
|
||||
1. Download and install the platform as described [here](https://www.msys2.org/) up to point 4.
|
||||
2. Launch **mingw64** shell.
|
||||

|
||||
3. Run first `pacman -Suy` After that, you may need to restart the terminal and re-run the update command.
|
||||
4. Install minimal required packages:
|
||||
`pacman -S mingw-w64-x86_64-gtk3 mingw-w64-x86_64-python3 mingw-w64-x86_64-python3-gobject mingw-w64-x86_64-python3-pip mingw-w64-x86_64-python3-requests`
|
||||
Optional: `pacman -S mingw-w64-x86_64-python3-pillow`
|
||||
To support streams playback, install the following packages (the list may not be complete):
|
||||
For [MPV](https://mpv.io/) `pacman -S mingw-w64-x86_64-mpv`,
|
||||
For [GStreamer](https://gstreamer.freedesktop.org/) `pacman -S mingw-w64-x86_64-gst-libav mingw-w64-x86_64-gst-plugins-bad mingw-w64-x86_64-gst-plugins-base mingw-w64-x86_64-gst-plugins-good mingw-w64-x86_64-gstreamer`
|
||||
5. Download and unzip the archive with sources from preferred branch (e.g. [master](https://github.com/DYefremov/DemonEditor/archive/refs/heads/master.zip)) in to folder where MSYS2 is installed. E.g: `c:\msys64\home\username\`
|
||||
6. Run mingw64 shell. Go to the folder where the program was unpacked. E.g: `cd DemonEditor/`
|
||||
And run: `./start.py`
|
||||
|
||||
## Building a package
|
||||
To build a standalone package, we can use [PyInstaller](https://pyinstaller.readthedocs.io/en/stable/).
|
||||
1. Launch mingw64 shell.
|
||||
2. Install PyInstaller via pip: `pip3 install pyinstaller`
|
||||
3. Go to the folder where the program was unpacked. E.g: `c:\msys64\home\username\DemonEditor\`
|
||||
4. Сopy and replace the files from the /build/win/ folder to the root .
|
||||
5. Go to the folder with the program in the running terminal: `cd DemonEditor/`
|
||||
6. Give the following command: `pyinstaller.exe DemonEditor.spec`
|
||||
7. Wait until the operation end. In the dist folder you will find a ready-made build.
|
||||
|
||||
### Appearance
|
||||
To change the look we can use third party [Gtk3 themes and Icon sets](https://www.gnome-look.org).
|
||||
To set the default theme:
|
||||
1. Сreate a folder "`\etc\gtk-3.0\`" in the root of the finished build folder.
|
||||
2. Create a _settings.ini_ file in this folder with the following content:
|
||||
```
|
||||
[Settings]
|
||||
gtk-icon-theme-name = Adwaita
|
||||
gtk-theme-name = Windows-10
|
||||
```
|
||||
In this case, we are using the default icon theme "Adwaita" and the [third party theme](https://github.com/B00merang-Project/Windows-10) "Windows-10".
|
||||
Themes and icon sets should be located in the `share\themes` and `share\icons` folders respectively.
|
||||
To fine-tune the default theme you use, you can use the _win_style.css_ file in the `ui` folder.
|
||||
You can find more info about changing the appearance of Gtk applications on the Web yourself.
|
||||
@@ -0,0 +1,49 @@
|
||||
diff -Nru demon-editor-2.0-development-orig/DemonEditor.desktop demon-editor-2.0-development/DemonEditor.desktop
|
||||
--- demon-editor-2.0-development-orig/DemonEditor.desktop 2021-10-14 21:32:56.000000000 +0300
|
||||
+++ demon-editor-2.0-development/DemonEditor.desktop 2021-09-29 13:19:24.000000000 +0300
|
||||
@@ -6,8 +6,8 @@
|
||||
Comment[be]=Рэдактар спіса каналаў і спадарожнікаў для Enigma2
|
||||
Comment[de]=Programm- und Satellitenlisten-Editor für Enigma2
|
||||
Icon=demon-editor
|
||||
-Exec=bash -c 'cd $(dirname %k) && ./start.py'
|
||||
+Exec=demon-editor
|
||||
Terminal=false
|
||||
Type=Application
|
||||
-Categories=Utility;Application;
|
||||
+Categories=Utility;
|
||||
StartupNotify=false
|
||||
diff -Nru demon-editor-2.0-development-orig/start.py demon-editor-2.0-development/start.py
|
||||
--- demon-editor-2.0-development-orig/start.py 2021-10-14 21:32:56.000000000 +0300
|
||||
+++ demon-editor-2.0-development/start.py 2021-09-29 13:19:24.000000000 +0300
|
||||
@@ -1,29 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
-import os
|
||||
+from app.ui.main import start_app
|
||||
|
||||
-
|
||||
-def update_icon():
|
||||
- need_update = False
|
||||
- icon_name = "DemonEditor.desktop"
|
||||
-
|
||||
- with open(icon_name, "r") as f:
|
||||
- lines = f.readlines()
|
||||
- for i, line in enumerate(lines):
|
||||
- if line.startswith("Icon="):
|
||||
- icon_path = line.lstrip("Icon=")
|
||||
- current_path = "{}/app/ui/icons/hicolor/96x96/apps/demon-editor.png".format(os.getcwd())
|
||||
- if icon_path != current_path:
|
||||
- need_update = True
|
||||
- lines[i] = "Icon={}\n".format(current_path)
|
||||
- break
|
||||
-
|
||||
- if need_update:
|
||||
- with open(icon_name, "w") as f:
|
||||
- f.writelines(lines)
|
||||
-
|
||||
-
|
||||
-if __name__ == "__main__":
|
||||
- from app.ui.main import start_app
|
||||
-
|
||||
- update_icon()
|
||||
- start_app()
|
||||
+start_app()
|
||||
86
build/linux/ALTLinux spec/demon-editor.spec
Normal file
86
build/linux/ALTLinux spec/demon-editor.spec
Normal file
@@ -0,0 +1,86 @@
|
||||
Name: demon-editor
|
||||
Version: 2.0
|
||||
Release: slava0
|
||||
BuildArch: noarch
|
||||
Summary: Enigma2 channel and satellite list editor
|
||||
Url: https://github.com/DYefremov/DemonEditor
|
||||
License: MIT
|
||||
Group: Other
|
||||
Source: %name-%version-development.tar.gz
|
||||
Patch0: %name-%version-development-startfix.patch
|
||||
AutoReq: no
|
||||
Requires: python3 python3-module-requests python3-module-pygobject3 python3-module-chardet libmpv1
|
||||
BuildRequires: python3-dev python3-module-mpl_toolkits
|
||||
|
||||
%description
|
||||
Enigma2 channel and satellites list editor for GNU/Linux.
|
||||
|
||||
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
|
||||
Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc).
|
||||
|
||||
Main features of the program:
|
||||
Editing bouquets, channels, satellites.
|
||||
Import function.
|
||||
Backup function.
|
||||
Support of picons.
|
||||
Importing services, downloading picons and updating satellites from the Web.
|
||||
Extended support of IPTV.
|
||||
Import to bouquet(Neutrino WEBTV) from m3u.
|
||||
Export of bouquets with IPTV services in m3u.
|
||||
Assignment of EPGs from DVB or XML for IPTV services (only Enigma2, experimental).
|
||||
Playback of IPTV or other streams directly from the bouquet list.
|
||||
Control panel with the ability to view EPG and manage timers (via HTTP API, experimental).
|
||||
Simple FTP client (experimental).
|
||||
|
||||
%prep
|
||||
%setup -n %name-%version-development
|
||||
%patch0 -p1
|
||||
|
||||
%install
|
||||
%__install -d %buildroot%_datadir/demoneditor/app
|
||||
%__install -m644 app/*py %buildroot%_datadir/demoneditor/app
|
||||
%__install -d %buildroot%_datadir/demoneditor/app/eparser
|
||||
%__install -m644 app/eparser/*py %buildroot%_datadir/demoneditor/app/eparser
|
||||
%__install -d %buildroot%_datadir/demoneditor/app/eparser/enigma
|
||||
%__install -m644 app/eparser/enigma/*py %buildroot%_datadir/demoneditor/app/eparser/enigma
|
||||
%__install -d %buildroot%_datadir/demoneditor/app/eparser/neutrino
|
||||
%__install -m644 app/eparser/neutrino/*py %buildroot%_datadir/demoneditor/app/eparser/neutrino
|
||||
%__install -d %buildroot%_datadir/demoneditor/app/tools
|
||||
%__install -m644 app/tools/*py %buildroot%_datadir/demoneditor/app/tools
|
||||
%__install -d %buildroot%_datadir/demoneditor/app/ui
|
||||
%__install -m644 app/ui/*py %buildroot%_datadir/demoneditor/app/ui
|
||||
%__install -m644 app/ui/*glade %buildroot%_datadir/demoneditor/app/ui
|
||||
%__install -m644 app/ui/*css %buildroot%_datadir/demoneditor/app/ui
|
||||
%__install -m644 app/ui/*ui %buildroot%_datadir/demoneditor/app/ui
|
||||
%__install -m755 start.py %buildroot%_datadir/demoneditor
|
||||
|
||||
%__install -d %buildroot%_iconsdir/hicolor/96x96/apps
|
||||
%__install -d %buildroot%_iconsdir/hicolor/scalable/apps
|
||||
%__install -m644 app/ui/icons/hicolor/96x96/apps/%name.* %buildroot%_iconsdir/hicolor/96x96/apps
|
||||
%__install -m644 app/ui/icons/hicolor/scalable/apps%name.* -d %buildroot%_iconsdir/hicolor/scalable/apps
|
||||
|
||||
%__install -d %buildroot%_datadir/locale
|
||||
cp -r app/ui/lang/* %buildroot%_datadir/locale
|
||||
|
||||
%__install -d %buildroot%_bindir
|
||||
echo "#!/bin/bash
|
||||
python3 %_datadir/demoneditor/start.py $1" > %buildroot%_bindir/%name
|
||||
chmod 755 %buildroot%_bindir/%name
|
||||
|
||||
%__install -d %buildroot%_desktopdir
|
||||
%__install -m644 DemonEditor.desktop %buildroot%_desktopdir/DemonEditor.desktop
|
||||
|
||||
%find_lang %name
|
||||
|
||||
%files -f %name.lang
|
||||
%doc deb/DEBIAN/README.source
|
||||
%_bindir/%name
|
||||
%_datadir/demoneditor
|
||||
%_iconsdir/*/*/*/%name.*
|
||||
%_desktopdir/DemonEditor.desktop
|
||||
|
||||
%changelog
|
||||
* Wed Sep 29 2021 Viacheslav Dikonov <sdiconov@mail.ru> 1.0.10-slava0
|
||||
- ALTLinux package
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
#!/bin/env bash
|
||||
VER="0.2.2_Pre-alpha"
|
||||
#!/bin/bash
|
||||
VER="2.2.3_Beta"
|
||||
B_PATH="dist/DemonEditor"
|
||||
DEB_PATH="$B_PATH/usr/share/demoneditor"
|
||||
|
||||
mkdir -p $B_PATH
|
||||
cp -TRv deb $B_PATH
|
||||
cp -Rv app $DEB_PATH
|
||||
cp -Rv start.py $DEB_PATH
|
||||
rsync --exclude=app/ui/lang --exclude=app/ui/icons --exclude=__pycache__ -arv ../../app $DEB_PATH
|
||||
|
||||
cd dist
|
||||
fakeroot dpkg-deb --build DemonEditor
|
||||
62
build/linux/deb/DEBIAN/README.source
Normal file
62
build/linux/deb/DEBIAN/README.source
Normal file
@@ -0,0 +1,62 @@
|
||||
demon-editor for Debian
|
||||
----------------------
|
||||
DemonEditor
|
||||
Enigma2 channel and satellite list editor for GNU/Linux.
|
||||
|
||||
Experimental support of Neutrino-MP or others on the same basis (BPanther, etc).
|
||||
Focused on the convenience of working in lists from the keyboard. The mouse is also fully supported (Drag and Drop etc).
|
||||
|
||||
Main features of the program:
|
||||
Editing bouquets, channels, satellites.
|
||||
Import function.
|
||||
Backup function.
|
||||
Support of picons.
|
||||
Importing services, downloading picons and updating satellites from the Web.
|
||||
Extended support of IPTV.
|
||||
Import to bouquet(Neutrino WEBTV) from m3u.
|
||||
Export of bouquets with IPTV services in m3u.
|
||||
Assignment of EPGs from DVB or XML for IPTV services (only Enigma2, experimental).
|
||||
Playback of IPTV or other streams directly from the bouquet list.
|
||||
Control panel with the ability to view EPG and manage timers (via HTTP API, experimental).
|
||||
Simple FTP client (experimental).
|
||||
|
||||
Keyboard shortcuts:
|
||||
Ctrl + Insert - copies the selected channels from the main list to the the bouquet beginning or inserts (creates) a new bouquet.
|
||||
Ctrl + BackSpace - copies the selected channels from the main list to the bouquet end.
|
||||
Ctrl + X - only in bouquet list. Ctrl + C - only in services list.
|
||||
Clipboard is "rubber". There is an accumulation before the insertion!
|
||||
Ctrl + E - edit.
|
||||
Ctrl + R, F2 - rename.
|
||||
Ctrl + S, T in Satellites edit tool for create satellite or transponder.
|
||||
Ctrl + L - parental lock.
|
||||
Ctrl + H - hide/skip.
|
||||
Ctrl + P - start play IPTV or other stream in the bouquet list.
|
||||
Ctrl + Z - switch (zap) the channel (works when the HTTP API is enabled, Enigma2 only).
|
||||
Ctrl + W - switch to the channel and watch in the program.
|
||||
Space - select/deselect.
|
||||
Left/Right - remove selection.
|
||||
Ctrl + Up, Down, PageUp, PageDown, Home, End - move selected items in the list.
|
||||
Ctrl + O - (re)load user data from current dir.
|
||||
Ctrl + D - load data from receiver.
|
||||
Ctrl + U/B upload data/bouquets to receiver.
|
||||
Ctrl + F - show/hide search bar.
|
||||
Ctrl + Shift + F - show/hide filter bar.
|
||||
|
||||
For multiple selection with the mouse, press and hold the Ctrl key!
|
||||
|
||||
Minimum requirements:
|
||||
Python >= 3.6, GTK+ >= 3.22, python3-gi, python3-gi-cairo, python3-requests.
|
||||
|
||||
Important:
|
||||
Terrestrial(DVB-T/T2) and cable(DVB-C) channels are only supported for Enigma2!
|
||||
Main supported *lamedb* format is version **4**. Versions **3** and **5** has only **experimental** support!
|
||||
For version **3** is only read mode available. When saving, version **4** format is used instead!
|
||||
|
||||
When using the multiple import feature, from *lamedb* will be taken data **only for channels that are in the
|
||||
selected bouquets!** If you need full set of the data, including *[satellites, terrestrial, cables].xml* (current files will be overwritten),
|
||||
just load your data via *"File/Open"* and press *"Save"*. When importing separate bouquet files, only those services
|
||||
(excluding IPTV) that are in the **current open lamedb** (main list of services) will be imported.
|
||||
|
||||
For streams playback, this app supports VLC, MPV and GStreamer.
|
||||
Depending on your distro, you may need to install additional packages and libraries.
|
||||
|
||||
25
build/linux/deb/DEBIAN/control
Normal file
25
build/linux/deb/DEBIAN/control
Normal file
@@ -0,0 +1,25 @@
|
||||
Package: demon-editor
|
||||
Version: 2.2.3-Beta
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Architecture: all
|
||||
Essential: no
|
||||
Depends: python3 (>= 3.6),
|
||||
python3-requests,
|
||||
python3-gi,
|
||||
python3-gi-cairo,
|
||||
gir1.2-notify-0.7,
|
||||
p7zip-full
|
||||
Recommends: libmpv1,
|
||||
python3-chardet,
|
||||
libgtksourceview (>= 3.0)
|
||||
Maintainer: Dmitriy Yefremov <dmitry.v.yefremov@gmail.com>
|
||||
Homepage: https://dyefremov.github.io/DemonEditor
|
||||
Description: Enigma2 channel and satellite list editor
|
||||
Editing bouquets, channels, satellites, importing services,
|
||||
downloading picons and updating satellites from the Web,
|
||||
extended support of IPTV, assignment of EPG from DVB or
|
||||
XML for IPTV services, playback of IPTV or other streams
|
||||
directly from the bouquet list, control panel (via HTTP API),
|
||||
ability to view EPG and manage timers (via HTTP API),
|
||||
simple FTP client (experimental).
|
||||
@@ -5,7 +5,7 @@ Source: https://github.com/DYefremov/DemonEditor
|
||||
Files: *
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
1
build/linux/deb/DEBIAN/demon-editor-docs.docs
Normal file
1
build/linux/deb/DEBIAN/demon-editor-docs.docs
Normal file
@@ -0,0 +1 @@
|
||||
README.source
|
||||
2
build/linux/deb/usr/bin/demon-editor
Executable file
2
build/linux/deb/usr/bin/demon-editor
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/bin/bash
|
||||
python3 /usr/share/demoneditor/start.py $1
|
||||
16
build/linux/deb/usr/share/applications/DemonEditor.desktop
Executable file
16
build/linux/deb/usr/share/applications/DemonEditor.desktop
Executable file
@@ -0,0 +1,16 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=DemonEditor
|
||||
Comment=Channel and satellite list editor for Enigma2
|
||||
Comment[ru]=Редактор списка каналов и спутников для Enigma2
|
||||
Comment[be]=Рэдактар спіса каналаў і спадарожнікаў для Enigma2
|
||||
Comment[de]=Programm- und Satellitenlisten-Editor für Enigma2
|
||||
Comment[it]=Editor di liste canali e satelliti per Enigma2
|
||||
Comment[tr]=Enigma2 için kanal ve uydu listesi editörü
|
||||
Comment[es]=Editor de listas de canales y satélites para Enigma2
|
||||
Icon=demon-editor
|
||||
Exec=/usr/bin/demon-editor
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Utility;Application;
|
||||
StartupNotify=false
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user