mirror of
https://github.com/DYefremov/DemonEditor.git
synced 2026-05-08 14:37:18 +02:00
Compare commits
1341 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cf19e5413 | ||
|
|
cd4a814838 | ||
|
|
d7c49f50f2 | ||
|
|
b61b8e16fa | ||
|
|
56d2a3e991 | ||
|
|
b65ea9c0d3 | ||
|
|
a62ee8f378 | ||
|
|
0db8ee6d47 | ||
|
|
ed41b01f63 | ||
|
|
c2eaecb8b8 | ||
|
|
f5313f2c40 | ||
|
|
c6a0b80fdd | ||
|
|
7813aeb059 | ||
|
|
3a0e5c09a1 | ||
|
|
ab6a44dc3f | ||
|
|
caefb4587d | ||
|
|
93ff78d7ce | ||
|
|
1d583ecd99 | ||
|
|
d4914ac451 | ||
|
|
6207b6a10d | ||
|
|
1eb8fe621d | ||
|
|
79b41b1661 | ||
|
|
7a36ba8148 | ||
|
|
08e970fc96 | ||
|
|
9681fcbc79 | ||
|
|
7b002b208f | ||
|
|
95a1732f01 | ||
|
|
57e4fdff7f | ||
|
|
89c456993f | ||
|
|
2f3fc31023 | ||
|
|
5c18e49cf7 | ||
|
|
64530bcb85 | ||
|
|
4d472609b4 | ||
|
|
640b995ab8 | ||
|
|
42980a988f | ||
|
|
64c5f28957 | ||
|
|
a32bf230cf | ||
|
|
b8cac728a8 | ||
|
|
079f07cfd2 | ||
|
|
9a5884cc9a | ||
|
|
115237a10f | ||
|
|
994bd0ee1c | ||
|
|
27e5b373a3 | ||
|
|
43c05b1739 | ||
|
|
bd96c286e9 | ||
|
|
1bded41eab | ||
|
|
5f0f51679c | ||
|
|
380bb3150b | ||
|
|
d1a7a486a2 | ||
|
|
1be167bec3 | ||
|
|
dd3e88589c | ||
|
|
c5a2df6d7d | ||
|
|
c9fc3803c7 | ||
|
|
6afd518cfc | ||
|
|
02a51c9b56 | ||
|
|
c96cfa0e1b | ||
|
|
08bc4ff4c4 | ||
|
|
ae2b78e990 | ||
|
|
88be9fe49c | ||
|
|
9dae9b7219 | ||
|
|
f296a6c90b | ||
|
|
0486776d83 | ||
|
|
3e6146d825 | ||
|
|
9b97341e70 | ||
|
|
41714136e6 | ||
|
|
f781cbb9f6 | ||
|
|
177be7679b | ||
|
|
a65914a48c | ||
|
|
e3ffc2e24b | ||
|
|
79415c69c5 | ||
|
|
54c4e02cee | ||
|
|
f580c5d83c | ||
|
|
e0cbdb2f8d | ||
|
|
839855c076 | ||
|
|
87db39590b | ||
|
|
ebe58903e5 | ||
|
|
ff8d4e5321 | ||
|
|
907415a2c9 | ||
|
|
f9afffbdb3 | ||
|
|
326856b1e3 | ||
|
|
865a326fe9 | ||
|
|
dd2661c6c9 | ||
|
|
623e5a17f5 | ||
|
|
18c1fa736b | ||
|
|
0e6142c751 | ||
|
|
c274c265c6 | ||
|
|
777c09c9b8 | ||
|
|
12a68a4dbb | ||
|
|
eab869d4d5 | ||
|
|
aec76eec45 | ||
|
|
bdab316ba7 | ||
|
|
771ecb696f | ||
|
|
8993fbed5d | ||
|
|
f3cad81a7d | ||
|
|
c1cf343f69 | ||
|
|
f998f66a35 | ||
|
|
9eb4cdc574 | ||
|
|
20120e0db4 | ||
|
|
3446bb225c | ||
|
|
191975bd14 | ||
|
|
f4be52a202 | ||
|
|
ba2272cf13 | ||
|
|
7bd3fcd9a6 | ||
|
|
e54719ca2c | ||
|
|
0c1c44c866 | ||
|
|
06b82251ef | ||
|
|
fb929ec723 | ||
|
|
fd1c1bfd6e | ||
|
|
c48a08b239 | ||
|
|
b647b0a338 | ||
|
|
9b608eeb74 | ||
|
|
07e55b3f1e | ||
|
|
7e639f5637 | ||
|
|
5570d47cae | ||
|
|
5c49c0d123 | ||
|
|
ee6dd511b5 | ||
|
|
1f847233b3 | ||
|
|
835e1af8e4 | ||
|
|
50e0d8b66a | ||
|
|
fb0789664a | ||
|
|
5dd39492f2 | ||
|
|
33be9f21a2 | ||
|
|
7acc9ae74f | ||
|
|
d492022232 | ||
|
|
b034995130 | ||
|
|
f037b3554d | ||
|
|
7f1f27da57 | ||
|
|
d7f3afecb0 | ||
|
|
f309005c52 | ||
|
|
25661816e7 | ||
|
|
a2652cef4b | ||
|
|
adbc9ad322 | ||
|
|
2dc8611294 | ||
|
|
72bfd21056 | ||
|
|
65ef018f81 | ||
|
|
1236c5ebc9 | ||
|
|
f0011ebcf2 | ||
|
|
392e94e7ba | ||
|
|
c6de18271d | ||
|
|
71a65242c1 | ||
|
|
4efc956870 | ||
|
|
a605fdd545 | ||
|
|
285c1cae69 | ||
|
|
2e937a42a3 | ||
|
|
e208cf4656 | ||
|
|
3db82e3e18 | ||
|
|
38e9a85694 | ||
|
|
438e9c10d4 | ||
|
|
920fa01159 | ||
|
|
d2787364cd | ||
|
|
839c0fae23 | ||
|
|
f87548e12e | ||
|
|
463702c371 | ||
|
|
0b84a81439 | ||
|
|
6b68740961 | ||
|
|
92aa2400f6 | ||
|
|
2cf4e5b756 | ||
|
|
6cfa68e219 | ||
|
|
138aa54b44 | ||
|
|
3e1a3d1595 | ||
|
|
b833458c45 | ||
|
|
d1e88be1cc | ||
|
|
72e128aeb9 | ||
|
|
457b4e4645 | ||
|
|
076243b0ac | ||
|
|
e5ff185791 | ||
|
|
2b6b0dd827 | ||
|
|
a11fdd683e | ||
|
|
636bc5c52f | ||
|
|
90d64d46c7 | ||
|
|
115f77108c | ||
|
|
2082f8e973 | ||
|
|
7e586bf0a6 | ||
|
|
c5c4823534 | ||
|
|
6235519cf9 | ||
|
|
7d3a9f768c | ||
|
|
37bea3a93c | ||
|
|
33a22fdca7 | ||
|
|
d54e97b1b8 | ||
|
|
12c2a449ea | ||
|
|
7164f54773 | ||
|
|
84adeb994e | ||
|
|
69c3b4a6c1 | ||
|
|
cbec74c2a4 | ||
|
|
8d856bc989 | ||
|
|
a1ec7600da | ||
|
|
2fd71c3645 | ||
|
|
a0f6b1f651 | ||
|
|
671c2204bf | ||
|
|
d2a0419c06 | ||
|
|
fb48395d1e | ||
|
|
44fb760241 | ||
|
|
428a240416 | ||
|
|
8c58b9395b | ||
|
|
021b2b08cf | ||
|
|
0d68e43212 | ||
|
|
751e633a51 | ||
|
|
9ff96f2e1d | ||
|
|
3e85fa0149 | ||
|
|
ab5620f9d1 | ||
|
|
bceddc199b | ||
|
|
4f26855ec3 | ||
|
|
15b8483107 | ||
|
|
f4dac57d06 | ||
|
|
71f7b3a570 | ||
|
|
265fb59f0b | ||
|
|
5b090672d9 | ||
|
|
aa8f2a8df0 | ||
|
|
6782da5f83 | ||
|
|
26d7b22c3a | ||
|
|
69ec1f8359 | ||
|
|
c5aac859b0 | ||
|
|
8d0b241ca3 | ||
|
|
8943eab99d | ||
|
|
a5a4f267cc | ||
|
|
a69127f0cc | ||
|
|
858b2ae2d6 | ||
|
|
13a08c98de | ||
|
|
272e3786dc | ||
|
|
3b510e6935 | ||
|
|
f3a6d2bd9c | ||
|
|
ca20852bfe | ||
|
|
5e1bd8e1c9 | ||
|
|
0b87f4f143 | ||
|
|
00cbe43aa7 | ||
|
|
deb161a153 | ||
|
|
2a2611abde | ||
|
|
dbcfb71224 | ||
|
|
2441d3726b | ||
|
|
681b43b164 | ||
|
|
31025777a3 | ||
|
|
33e39d2f25 | ||
|
|
14e200f262 | ||
|
|
3cef75e765 | ||
|
|
a973f8e636 | ||
|
|
c6bea94ff5 | ||
|
|
736655542c | ||
|
|
073521de75 | ||
|
|
46748d3fc4 | ||
|
|
f2e571185d | ||
|
|
c2fd116252 | ||
|
|
c1c5e866ad | ||
|
|
d4a2e78a09 | ||
|
|
0f68d5b292 | ||
|
|
c3d9159822 | ||
|
|
b5a508ef54 | ||
|
|
14459b8e7e | ||
|
|
a9998b9d17 | ||
|
|
6d05a6ec20 | ||
|
|
b821fd54be | ||
|
|
c3bc3a1160 | ||
|
|
748b41e31c | ||
|
|
a4cbe00e96 | ||
|
|
9f0ad72d42 | ||
|
|
673a8547ff | ||
|
|
f6058dafb9 | ||
|
|
0140fb4eb4 | ||
|
|
97f04999c4 | ||
|
|
e879c8db18 | ||
|
|
d41ceb6bbc | ||
|
|
8e82d8562a | ||
|
|
4cba9a6754 | ||
|
|
d568a9429d | ||
|
|
8b39fedaed | ||
|
|
c3351b43cc | ||
|
|
58156dd4c1 | ||
|
|
d09c14518e | ||
|
|
a8739be31d | ||
|
|
67a75e5ffa | ||
|
|
7d31a050fe | ||
|
|
5f4cee759f | ||
|
|
f7ed1736c5 | ||
|
|
4bdbb511ee | ||
|
|
e49d32f931 | ||
|
|
42feb8d5f6 | ||
|
|
e408f88afd | ||
|
|
eaf0434e19 | ||
|
|
079cf6a482 | ||
|
|
d3ae2187c8 | ||
|
|
eac0cc47a9 | ||
|
|
1972374505 | ||
|
|
d8626e63cc | ||
|
|
528f59d990 | ||
|
|
28802957fc | ||
|
|
b763d9785d | ||
|
|
601a81beb9 | ||
|
|
ace38433a1 | ||
|
|
536b23a845 | ||
|
|
d71a1d5dac | ||
|
|
4a92084c75 | ||
|
|
e7b8412c11 | ||
|
|
ef53de1796 | ||
|
|
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 | ||
|
|
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 |
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
*.pyc
|
||||
*.pyo
|
||||
*__pycache__
|
||||
.idea
|
||||
@@ -1,11 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=DemonEditor
|
||||
Comment=Channels and satellites list editor for Enigma2
|
||||
Comment[ru]=Редактор списка каналов и спутников для Enigma2
|
||||
Icon=accessories-text-editor
|
||||
Exec=bash -c 'cd $(dirname %k) && ./start.py'
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Utility;Application;
|
||||
StartupNotify=true
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Dmitriy Yefremov
|
||||
Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
133
README.md
133
README.md
@@ -1,36 +1,119 @@
|
||||
# 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)
|
||||
### Keyboard shortcuts:
|
||||
**Ctrl + X, C, V, Up, Down, PageUp, PageDown, Home, End, S, T, E, L, H, Space; Insert, Delete, F2, Enter, P.**
|
||||
* **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!
|
||||
## 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)
|
||||
|
||||
**To increase program functionality you can use [extensions](https://github.com/DYefremov/demoneditor-extensions).**
|
||||
|
||||
#### Keyboard shortcuts
|
||||
* **Ctrl + X** - only in bouquet list.
|
||||
* **Ctrl + C** - only in services list.
|
||||
* **Ctrl + Insert** - copies the selected channels from the main list to the bouquet
|
||||
beginning or inserts (creates) a new bouquet.
|
||||
* **Ctrl + BackSpace** - copies the selected channels from the main list to the bouquet end.
|
||||
* **Ctrl + E** - edit.
|
||||
* **Ctrl + R, F2** - rename.
|
||||
* **Ctrl + R, F2** - rename.
|
||||
* **Ctrl + Alt + R** - rename for bouquet.
|
||||
* **Ctrl + S, T** in Satellites edit tool for create satellite or transponder.
|
||||
* **Ctrl + L** - parental lock.
|
||||
* **Ctrl + H** - hide/skip.
|
||||
* **P** - enable/disable preview mode for IPTV in the bouquet list.
|
||||
* **Enter** - start play IPTV or other stream in the bouquet list.
|
||||
* **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.
|
||||
### Extra:
|
||||
* Multiple selections in lists only with Space key (as in file managers).
|
||||
* Ability to import IPTV into bouquet (Neutrino WEBTV) from m3u files.
|
||||
* Ability to download picons and update satellites (transponders) from web.
|
||||
* Preview (playing) IPTV or other streams directly from the bouquet list(should be installed VLC).
|
||||
### Minimum requirements:
|
||||
Python >= 3.5.2 and GTK+ 3 with PyGObject bindings.
|
||||
#### Note.
|
||||
To create a simple debian package, you can use the build-deb.sh
|
||||
* **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!
|
||||
|
||||
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)!
|
||||
## Minimum requirements
|
||||
*Python >= 3.6, GTK+ >= 3.22, python3-gi, python3-gi-cairo, python3-requests.*
|
||||
|
||||
**Terrestrial and cable channels at the moment are not supported!**
|
||||
***Optional:** python3-pil, python3-chardet.*
|
||||
## Installation and Launch
|
||||
* ### Linux
|
||||
To start the program, in most cases it is enough to download the [archive](https://github.com/DYefremov/DemonEditor/archive/master.zip), unpack
|
||||
and run it by double clicking on DemonEditor.desktop in the root directory,
|
||||
or launching from the console with the command:```./start.py```
|
||||
Extra folders can be deleted, excluding the *app* folder and root files like *DemonEditor.desktop* and *start.py*!
|
||||
|
||||
To create a simple **debian package**, you can use the *build-deb.sh.* You can also download a ready-made *.deb package from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
|
||||
Users of **LTS** versions of [Ubuntu](https://ubuntu.com/) or distributions based on them can use [PPA](https://launchpad.net/~dmitriy-yefremov/+archive/ubuntu/demon-editor) repository.
|
||||
A ready-made [package](https://aur.archlinux.org/packages/demoneditor-bin) is also available for [Arch Linux](https://archlinux.org/) users in the [AUR](https://aur.archlinux.org/) repository.
|
||||
* ### macOS
|
||||
**This program can be run on macOS.**
|
||||
To run the program on macOS, you need to install [brew](https://brew.sh/).
|
||||
Then install the required components via terminal:
|
||||
```brew install python3 gtk+3 pygobject3 adwaita-icon-theme```
|
||||
```pip3 install requests, pillow```
|
||||
|
||||
Launch is similar to Linux.
|
||||
|
||||
You can also download the ready-made package as a ***.dmg** file from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
|
||||
Recommended copy the package to the **Application** directory.
|
||||
Perhaps in the security settings it will be necessary to allow the launch of this application!
|
||||
|
||||
* ### MS Windows
|
||||
**Windows users can also run this program.**
|
||||
One way is to use the [MSYS2](https://www.msys2.org/) platform. You can use [this](https://github.com/DYefremov/DemonEditor/blob/master/build/BUILD_WIN.md) quick guide.
|
||||
In addition, you can download a ready-made build (**64-bit**) from the [releases](https://github.com/DYefremov/DemonEditor/releases) page.
|
||||
|
||||
**All builds may contain components distributed under the GPL [v3](http://www.gnu.org/licenses/gpl-3.0.html) or lower license.
|
||||
By downloading and using this packages you agree to the terms of this [license](http://www.gnu.org/licenses/gpl-3.0.html) and the possible inconvenience associated with this!**
|
||||
|
||||
THIS SOFTWARE COMES WITH ABSOLUTELY NO WARRANTY.
|
||||
AUTHOR IS NOT LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY CONNECTION WITH THIS SOFTWARE.
|
||||
|
||||
## 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)
|
||||
LOG_DATE_FORMAT = "%d-%m-%y %H:%M:%S"
|
||||
LOGGER_NAME = "main_logger"
|
||||
LOG_FORMAT = "%(asctime)s %(message)s"
|
||||
|
||||
|
||||
def get_logger():
|
||||
return logging.getLogger(_LOGGER_NAME)
|
||||
def init_logger():
|
||||
logging.Logger(LOGGER_NAME)
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format=LOG_FORMAT,
|
||||
datefmt=LOG_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,63 @@ 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
|
||||
|
||||
|
||||
def get_size_from_bytes(size):
|
||||
""" Simple convert function from bytes to other units like K, M or G. """
|
||||
try:
|
||||
b = float(size)
|
||||
except ValueError:
|
||||
return size
|
||||
else:
|
||||
kb, mb, gb = 1024.0, 1048576.0, 1073741824.0
|
||||
|
||||
if b < kb:
|
||||
return str(b)
|
||||
elif kb <= b < mb:
|
||||
return f"{b / kb:.1f} K"
|
||||
elif mb <= b < gb:
|
||||
return f"{b / mb:.1f} M"
|
||||
elif gb <= b:
|
||||
return f"{b / gb:.1f} G"
|
||||
|
||||
|
||||
class DefaultDict(defaultdict):
|
||||
""" Extended to support functions with params as default factory. """
|
||||
|
||||
def __missing__(self, key):
|
||||
if self.default_factory:
|
||||
value = self[key] = self.default_factory(key)
|
||||
return value
|
||||
return super().__missing__(key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self[key]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
947
app/connections.py
Normal file
947
app/connections.py
Normal file
@@ -0,0 +1,947 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import time
|
||||
import urllib
|
||||
import xml.etree.ElementTree as ETree
|
||||
from enum import Enum
|
||||
from ftplib import FTP, CRLF, Error, all_errors
|
||||
from http.client import RemoteDisconnected
|
||||
from pathlib import Path
|
||||
from 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", # Enigma2.
|
||||
"services.xml", "myservices.xml", "bouquets.xml", "ubouquets.xml") # Neutrino.
|
||||
|
||||
DATA_FILES_LIST = ("lamedb", "lamedb5", "blacklist", "whitelist",)
|
||||
|
||||
STC_XML_FILE = ("satellites.xml", "terrestrial.xml", "cables.xml")
|
||||
WEB_TV_XML_FILE = ("webtv.xml",)
|
||||
PICONS_SUF = (".jpg", ".png")
|
||||
PICONS_MAX_NUM = 1000 # Maximum picon number for sending without compression.
|
||||
|
||||
|
||||
class DownloadType(Enum):
|
||||
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 = f"UtfFTP [retrlines] error: got more than {self.maxline} bytes"
|
||||
log(msg)
|
||||
raise Error(msg)
|
||||
if self.debugging > 2:
|
||||
log(f"UtfFTP [retrlines] *retr* {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:
|
||||
resp = self.download_binary(name, f)
|
||||
msg = f"Downloading file: {name}. Status: {resp}"
|
||||
callback(msg) if callback else log(msg.rstrip())
|
||||
|
||||
return resp
|
||||
|
||||
def download_binary(self, src, fo):
|
||||
try:
|
||||
resp = str(self.retrbinary(f"RETR {src}", fo.write))
|
||||
except all_errors as e:
|
||||
resp = str(e)
|
||||
log(f"Error. {e}")
|
||||
|
||||
return resp
|
||||
|
||||
def download_dir(self, path, save_path, callback=None):
|
||||
""" Downloads directory from FTP with all contents.
|
||||
|
||||
Creates a leaf directory and all intermediate ones. This is recursive.
|
||||
"""
|
||||
dir_path = os.path.join(save_path, path, "")
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
current_path = self.pwd()
|
||||
|
||||
files = []
|
||||
self.dir(path, files.append)
|
||||
|
||||
try:
|
||||
self.cwd(path)
|
||||
except all_errors as e:
|
||||
msg = f"Download dir error: {e}".rstrip()
|
||||
log(msg)
|
||||
return f"500 {msg}"
|
||||
|
||||
for f in files:
|
||||
f_data = self.get_file_data(f)
|
||||
f_path = f_data[8]
|
||||
|
||||
if f_data[0][0] == "d":
|
||||
self.download_dir(f_path, dir_path, callback)
|
||||
else:
|
||||
try:
|
||||
self.download_file(f_path, dir_path, callback)
|
||||
except OSError as e:
|
||||
log(f"Download dir error: {e}".rstrip())
|
||||
|
||||
self.cwd(current_path)
|
||||
resp = "226 Transfer complete."
|
||||
msg = f"Copying directory: {path}. Status: {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 all_errors 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 all_errors 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: {}"
|
||||
try:
|
||||
resp = str(self.storbinary("STOR " + file_name, f))
|
||||
except all_errors as e:
|
||||
resp = str(e)
|
||||
msg = msg.format(file_name, resp)
|
||||
log(msg)
|
||||
else:
|
||||
msg = msg.format(file_name, resp)
|
||||
if callback:
|
||||
callback(msg)
|
||||
|
||||
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 all_errors:
|
||||
pass # NOP
|
||||
|
||||
try:
|
||||
self.cwd(f)
|
||||
except all_errors 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 all_errors 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: {}"
|
||||
try:
|
||||
resp = self.delete(file)
|
||||
except all_errors 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: {}"
|
||||
try:
|
||||
resp = self.rmd(path)
|
||||
except all_errors 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: {}"
|
||||
try:
|
||||
resp = self.rename(from_name, to_name)
|
||||
except all_errors 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.")
|
||||
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:
|
||||
ftp.cwd(settings.epg_dat_path)
|
||||
ftp.download_files(f"{settings.profile_data_path}epg{os.sep}", "epg.dat", callback)
|
||||
|
||||
callback("*** Done. ***")
|
||||
|
||||
|
||||
def upload_data(*, settings, download_type=DownloadType.ALL, callback=log, done_callback=None,
|
||||
files_filter=None, ext_host=None):
|
||||
s_type = settings.setting_type
|
||||
use_http = s_type is SettingsType.ENIGMA_2 and settings.use_http
|
||||
data_path = settings.profile_data_path
|
||||
host, port, use_ssl = ext_host or settings.host, settings.http_port, settings.http_use_ssl
|
||||
user, password = settings.user, settings.password
|
||||
base_url = f"http{'s' if use_ssl else ''}://{host}:{port}"
|
||||
base = "web" if s_type is SettingsType.ENIGMA_2 else "control"
|
||||
url = f"{base_url}/{base}/"
|
||||
tn, ht = None, None # Telnet, HTTP.
|
||||
|
||||
try:
|
||||
use_http = use_http and test_http(host, port, user, password, use_ssl=use_ssl, skip_message=True, s_type=s_type)
|
||||
except TestException:
|
||||
log("HTTP test failed.")
|
||||
use_http = False
|
||||
|
||||
try:
|
||||
if use_http:
|
||||
ht = http(user, password, base_url, callback, use_ssl, s_type)
|
||||
next(ht)
|
||||
message = get_upload_info_message(download_type)
|
||||
|
||||
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)
|
||||
if not settings.keep_power_mode:
|
||||
ht.send((f"{url}powerstate?newstate=0", "Toggle Standby "))
|
||||
time.sleep(2)
|
||||
else:
|
||||
if download_type is not DownloadType.PICONS:
|
||||
# Telnet
|
||||
tn = telnet(host=host, user=user, password=password, timeout=settings.telnet_timeout)
|
||||
next(tn)
|
||||
# Terminate Enigma2 or Neutrino.
|
||||
callback("Telnet initialization ...")
|
||||
tn.send("init 4")
|
||||
callback("Stopping GUI...")
|
||||
|
||||
with UtfFTP(host=host, user=user, passwd=password) as ftp:
|
||||
ftp.encoding = "utf-8"
|
||||
callback("FTP OK.")
|
||||
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, settings.remove_unused_bouquets, 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, settings.remove_unused_bouquets, callback)
|
||||
ftp.upload_files(data_path, DATA_FILES_LIST, callback)
|
||||
|
||||
if download_type is DownloadType.PICONS:
|
||||
p_src, p_dst = settings.profile_picons_path, settings.picons_path
|
||||
compress = all((settings.compress_picons, files_filter, len(files_filter) > PICONS_MAX_NUM))
|
||||
if compress:
|
||||
from zipfile import ZipFile
|
||||
|
||||
z_name = "picons.zip"
|
||||
zip_file = f"{p_src}{z_name}"
|
||||
p_dst = Path(p_dst).parent.as_posix()
|
||||
|
||||
if files_filter and z_name in files_filter:
|
||||
files_filter.remove(z_name)
|
||||
|
||||
if os.path.isfile(zip_file):
|
||||
try:
|
||||
os.unlink(zip_file)
|
||||
except OSError:
|
||||
pass # NOP
|
||||
|
||||
log("Compressing picons...")
|
||||
with ZipFile(zip_file, "w") as zf:
|
||||
list(map(lambda p: zf.write(os.path.join(p_src, p), arcname=p), files_filter))
|
||||
|
||||
files_filter = {z_name}
|
||||
|
||||
log("Uploading...")
|
||||
ftp.upload_picons(p_src, p_dst, callback, files_filter)
|
||||
|
||||
if compress:
|
||||
if not tn:
|
||||
callback("Telnet initialization...")
|
||||
tn = telnet(host=host, user=user, password=password, timeout=settings.telnet_timeout)
|
||||
next(tn)
|
||||
|
||||
callback("Extracting...")
|
||||
cmd = f"mkdir -p {settings.picons_path} && unzip -o -q {p_dst}/{z_name} -d {settings.picons_path}"
|
||||
tn.send(cmd)
|
||||
ftp.delete_file(z_name)
|
||||
|
||||
try:
|
||||
os.unlink(zip_file)
|
||||
except OSError:
|
||||
pass # NOP
|
||||
|
||||
if all((tn, download_type is not DownloadType.PICONS, not use_http)):
|
||||
# Resume Enigma2 or restart Neutrino.
|
||||
tn.send("init 3" if s_type is SettingsType.ENIGMA_2 else "init 6")
|
||||
callback("Starting..." if s_type is SettingsType.ENIGMA_2 else "Rebooting...")
|
||||
elif ht and use_http:
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
if download_type is DownloadType.BOUQUETS:
|
||||
ht.send((f"{url}servicelistreload?mode=2", "Reloading Userbouquets."))
|
||||
elif download_type is DownloadType.ALL:
|
||||
ht.send((f"{url}servicelistreload?mode=0", "Reloading lamedb and Userbouquets."))
|
||||
if not settings.keep_power_mode:
|
||||
ht.send((f"{url}powerstate?newstate=4", "Wakeup from Standby."))
|
||||
else:
|
||||
ht.send((f"{url}reloadchannels", "Reloading channels..."))
|
||||
|
||||
if done_callback is not None:
|
||||
done_callback()
|
||||
finally:
|
||||
if tn:
|
||||
tn.close()
|
||||
if ht:
|
||||
ht.close()
|
||||
|
||||
|
||||
def get_upload_info_message(download_type):
|
||||
if download_type is DownloadType.BOUQUETS:
|
||||
return "User bouquets will be updated!"
|
||||
elif download_type is DownloadType.ALL:
|
||||
return "All user data will be reloaded!"
|
||||
elif download_type is DownloadType.SATELLITES:
|
||||
return "Satellites.xml file will be updated!"
|
||||
elif download_type is DownloadType.PICONS:
|
||||
return "Picons will be updated!"
|
||||
return ""
|
||||
|
||||
|
||||
# ***************** Picons *******************#
|
||||
|
||||
def remove_picons(*, settings, callback=log, done_callback=None, files_filter=None):
|
||||
with UtfFTP(host=settings.host, user=settings.user, passwd=settings.password) as ftp:
|
||||
ftp.encoding = "utf-8"
|
||||
callback("FTP OK.")
|
||||
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 ''}")
|
||||
|
||||
|
||||
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)
|
||||
|
||||
command = f"{command}\r\n".encode("utf-8")
|
||||
tn.write(command)
|
||||
|
||||
msg = tn.read_until(command, timeout)
|
||||
while msg.endswith(command) or not msg:
|
||||
time.sleep(timeout)
|
||||
msg = tn.read_until(command, timeout)
|
||||
|
||||
command = yield
|
||||
time.sleep(timeout)
|
||||
tn.write(f"{command}\r\n".encode("utf-8"))
|
||||
time.sleep(timeout)
|
||||
yield
|
||||
|
||||
|
||||
# ***************** HTTP API ******************* #
|
||||
|
||||
class HttpAPI:
|
||||
_MAX_WORKERS = 4
|
||||
_TIMEOUT = 10
|
||||
|
||||
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="
|
||||
EPG_NOW = "epgnow?bRef="
|
||||
EPG_MULTI = "epgmulti?bRef="
|
||||
# Timer
|
||||
TIMER = ""
|
||||
TIMER_LIST = "timerlist"
|
||||
# Recordings
|
||||
RECORDINGS = "movielist?dirname="
|
||||
REC_DIRS = "getlocations"
|
||||
REC_CURRENT = "getcurrlocation"
|
||||
# Screenshot
|
||||
GRUB = "grab?format=jpg&"
|
||||
# Neutrino requests.
|
||||
N_INFO = "info"
|
||||
N_ZAP = "zapto"
|
||||
N_STREAM = "build_playlist?id="
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
class Remote(str, Enum):
|
||||
""" Args for HttpRequestType [REMOTE] class. """
|
||||
ONE = "2"
|
||||
TWO = "3"
|
||||
THREE = "4"
|
||||
FOUR = "5"
|
||||
FIVE = "6"
|
||||
SIX = "7"
|
||||
SEVEN = "8"
|
||||
EIGHT = "9"
|
||||
NINE = "10"
|
||||
ZERO = "11"
|
||||
UP = "103"
|
||||
LEFT = "105"
|
||||
RIGHT = "106"
|
||||
DOWN = "108"
|
||||
MENU = "139"
|
||||
EXIT = "174"
|
||||
OK = "352"
|
||||
INFO = "358"
|
||||
EPG = "365"
|
||||
TV = "377"
|
||||
RADIO = "385"
|
||||
AUDIO = "392"
|
||||
FAV = "393"
|
||||
RED = "398"
|
||||
GREEN = "399"
|
||||
YELLOW = "400"
|
||||
BLUE = "401"
|
||||
CH_UP = "402"
|
||||
CH_DOWN = "403"
|
||||
NEXT = "407"
|
||||
BACK = "412"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
class Power(str, Enum):
|
||||
""" Args for HttpRequestType [POWER] class. """
|
||||
TOGGLE_STANDBY = "0"
|
||||
DEEP_STANDBY = "1"
|
||||
REBOOT = "2"
|
||||
RESTART_GUI = "3"
|
||||
WAKEUP = "4"
|
||||
STANDBY = "5"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
PARAM_REQUESTS = {Request.REMOTE,
|
||||
Request.POWER,
|
||||
Request.VOL,
|
||||
Request.EPG,
|
||||
Request.EPG_NOW,
|
||||
Request.EPG_MULTI,
|
||||
Request.TIMER,
|
||||
Request.RECORDINGS,
|
||||
Request.N_ZAP}
|
||||
|
||||
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="", timeout=_TIMEOUT):
|
||||
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, timeout)
|
||||
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, timeout=_TIMEOUT):
|
||||
try:
|
||||
with urlopen(Request(url, data=data), timeout=timeout) as f:
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
return HttpAPI.get_e2_response_data(req_type, f)
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
return HttpAPI.get_neutrino_response_data(req_type, f)
|
||||
else:
|
||||
return f.read().decode("utf-8")
|
||||
except HTTPError as e:
|
||||
if req_type is HttpAPI.Request.TEST:
|
||||
raise e
|
||||
return {"error_code": e.code}
|
||||
except OSError as e:
|
||||
if req_type is HttpAPI.Request.TEST:
|
||||
raise e
|
||||
except ETree.ParseError as e:
|
||||
log("Parsing response error: {}".format(e))
|
||||
|
||||
return {"error_code": -1}
|
||||
|
||||
@staticmethod
|
||||
def get_e2_response_data(req_type, f):
|
||||
if req_type in HttpAPI.STREAM_REQUESTS:
|
||||
return {"m3u": f.read().decode("utf-8")}
|
||||
elif req_type is HttpAPI.Request.GRUB:
|
||||
return {"img_data": f.read()}
|
||||
elif req_type is HttpAPI.Request.CURRENT:
|
||||
for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2event"):
|
||||
return {el.tag: el.text for el in el.iter()} # return first[current] event from the list
|
||||
elif req_type is HttpAPI.Request.PLAYER_LIST:
|
||||
return [{el.tag: el.text for el in el.iter()} for el in
|
||||
ETree.fromstring(f.read().decode("utf-8")).iter("e2file")]
|
||||
elif req_type in (HttpAPI.Request.EPG, HttpAPI.Request.EPG_NOW, HttpAPI.Request.EPG_MULTI):
|
||||
return {"event_list": [{el.tag: el.text for el in el.iter()} for el in
|
||||
ETree.fromstring(f.read().decode("utf-8")).iter("e2event")]}
|
||||
elif req_type is HttpAPI.Request.TIMER_LIST:
|
||||
return {"timer_list": [{el.tag: el.text for el in el.iter()} for el in
|
||||
ETree.fromstring(f.read().decode("utf-8")).iter("e2timer")]}
|
||||
elif req_type is HttpAPI.Request.REC_DIRS:
|
||||
return {"rec_dirs": [el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter("e2location")]}
|
||||
elif req_type is HttpAPI.Request.RECORDINGS:
|
||||
return {"recordings": [{el.tag: el.text for el in el.iter()} for el in
|
||||
ETree.fromstring(f.read().decode("utf-8")).iter("e2movie")]}
|
||||
else:
|
||||
return {el.tag: el.text for el in ETree.fromstring(f.read().decode("utf-8")).iter()}
|
||||
|
||||
@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 all_errors as e:
|
||||
raise TestException(e)
|
||||
|
||||
|
||||
def test_http(host, port, user, password, timeout=5, use_ssl=False, skip_message=False, s_type=SettingsType.ENIGMA_2):
|
||||
t_msg = "Connection test!"
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
params = urlencode({"text": t_msg, "type": 2, "timeout": timeout})
|
||||
params = "deviceinfo" if skip_message else f"message?{params}"
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
params = urlencode({"nmsg": t_msg, "timeout": 5}, quote_via=quote)
|
||||
params = "info" if skip_message else f"message?{params}"
|
||||
else:
|
||||
raise TestException("This type of settings is not supported!")
|
||||
|
||||
base_url = f"http{'s' if use_ssl else ''}://{host}:{port}"
|
||||
base = "web" if s_type is SettingsType.ENIGMA_2 else "control"
|
||||
url = f"{base_url}/{base}/{params}"
|
||||
# Authentication
|
||||
HttpAPI.init_auth(user, password, base_url, use_ssl)
|
||||
data = HttpAPI.get_post_data(base_url, password, user) if s_type is SettingsType.ENIGMA_2 else None
|
||||
|
||||
try:
|
||||
log("Testing HTTP connection...")
|
||||
resp = HttpAPI.get_response(HttpAPI.Request.TEST, url, data, s_type)
|
||||
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
return resp.get("e2enigmaversion", "")
|
||||
return resp
|
||||
except (RemoteDisconnected, URLError, HTTPError) as e:
|
||||
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-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
from app.commons import run_task
|
||||
from app.properties import Profile
|
||||
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,41 @@ 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, format_version):
|
||||
if profile is Profile.ENIGMA_2:
|
||||
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 profile is Profile.NEUTRINO_MP:
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
return get_neutrino_services(data_path)
|
||||
|
||||
|
||||
@run_task
|
||||
def write_services(path, channels, profile, format_version):
|
||||
if profile is Profile.ENIGMA_2:
|
||||
def write_services(path, channels, s_type, format_version):
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
write_enigma_services(path, channels, format_version)
|
||||
elif profile is Profile.NEUTRINO_MP:
|
||||
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)
|
||||
|
||||
|
||||
@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_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)
|
||||
|
||||
|
||||
def write_bouquets(path, bouquets, s_type, force_bq_names=False, blacklist=None):
|
||||
if s_type is SettingsType.ENIGMA_2:
|
||||
BouquetsWriter(path, bouquets, force_bq_names, blacklist).write()
|
||||
elif s_type is SettingsType.NEUTRINO_MP:
|
||||
write_neutrino_bouquets(path, bouquets)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,37 @@
|
||||
""" 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
|
||||
|
||||
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,33 +43,53 @@ 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"])
|
||||
|
||||
# ***************** Satellites *******************#
|
||||
# *************** *.xml [Satellites, Terrestrial, Cable] ***************** #
|
||||
|
||||
Satellite = namedtuple("Satellite", ["name", "flags", "position", "transponders"])
|
||||
Terrestrial = namedtuple("Terrestrial", ["name", "flags", "countrycode", "transponders"])
|
||||
Cable = namedtuple("Cable", ["name", "flags", "satfeed", "countrycode", "transponders"])
|
||||
|
||||
Transponder = namedtuple("Transponder", ["frequency", "symbol_rate", "polarization", "fec_inner",
|
||||
"system", "modulation", "pls_mode", "pls_code", "is_id"])
|
||||
Transponder = namedtuple("Transponder", ["frequency", "symbol_rate", "polarization", "fec_inner", "system",
|
||||
"modulation", "pls_mode", "pls_code", "is_id", "t2mi_plp_id"])
|
||||
TerTransponder = namedtuple("TerTransponder", ["centre_frequency", "system", "bandwidth", "constellation",
|
||||
"code_rate_hp", "code_rate_lp", "guard_interval", "transmission_mode",
|
||||
"hierarchy_information", "inversion", "plp_id"])
|
||||
CableTransponder = namedtuple("CableTransponder", ["frequency", "symbol_rate", "fec_inner", "modulation"])
|
||||
|
||||
|
||||
class TrType(Enum):
|
||||
""" Transponders type """
|
||||
Satellite = "s"
|
||||
Terestrial = "t"
|
||||
Terrestrial = "t"
|
||||
Cable = "c"
|
||||
ATSC = "a"
|
||||
|
||||
|
||||
class BqType(Enum):
|
||||
""" Bouquet type"""
|
||||
""" Bouquet type. """
|
||||
BOUQUET = "bouquet"
|
||||
TV = "tv"
|
||||
RADIO = "radio"
|
||||
WEBTV = "webtv"
|
||||
MARKER = "marker"
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.TV
|
||||
|
||||
|
||||
class Flag(Enum):
|
||||
@@ -72,6 +122,21 @@ class Flag(Enum):
|
||||
def is_new(value: int):
|
||||
return value & 1 << 5
|
||||
|
||||
@staticmethod
|
||||
def parse(value: str) -> int:
|
||||
""" Returns an int representation of the flag value.
|
||||
|
||||
The flag value is usually represented by the number [int],
|
||||
but can also be appear in hex format.
|
||||
"""
|
||||
if len(value) < 3:
|
||||
return 0
|
||||
|
||||
value = value[2:]
|
||||
if value.isdigit():
|
||||
return int(value)
|
||||
return int(value, 16)
|
||||
|
||||
|
||||
class Pids(Enum):
|
||||
VIDEO = "c:00"
|
||||
@@ -91,12 +156,26 @@ class Inversion(Enum):
|
||||
On = "1"
|
||||
Auto = "2"
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.Auto
|
||||
|
||||
|
||||
class Pilot(Enum):
|
||||
Off = "0"
|
||||
On = "1"
|
||||
Auto = "2"
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.Auto
|
||||
|
||||
|
||||
class SystemCable(Enum):
|
||||
""" System of cable service """
|
||||
ANNEX_A = "0"
|
||||
ANNEX_C = "1"
|
||||
|
||||
|
||||
ROLL_OFF = {"0": "35%", "1": "25%", "2": "20%", "3": "Auto"}
|
||||
|
||||
@@ -110,19 +189,43 @@ FEC = {"0": "Auto", "1": "1/2", "2": "2/3", "3": "3/4", "4": "5/6", "5": "7/8",
|
||||
"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"}
|
||||
"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": "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"}
|
||||
|
||||
CONSTELLATION = {"0": "QPSK", "1": "16-QAM", "2": "64-QAM", "3": "Auto"}
|
||||
|
||||
T_MODULATION = {"0": "QPSK", "1": "QAM16", "2": "QAM64", "3": "Auto", "4": "QAM256"}
|
||||
|
||||
TRANSMISSION_MODE = {"0": "2k", "1": "8k", "2": "Auto", "3": "4k", "4": "1k", "5": "16k", "6": "32k"}
|
||||
|
||||
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": "QPSK", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM128", "5": "QAM256", "6": "Auto"}
|
||||
|
||||
# ATSC
|
||||
A_MODULATION = {"0": "Auto", "1": "QAM16", "2": "QAM32", "3": "QAM64", "4": "QAM128", "5": "QAM256", "6": "8VSB",
|
||||
"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"}
|
||||
@@ -145,23 +248,25 @@ def get_value_by_name(en, name):
|
||||
|
||||
|
||||
def is_transponder_valid(tr: Transponder):
|
||||
""" Checks transponder validity """
|
||||
""" Checks transponder validity. """
|
||||
try:
|
||||
int(tr.frequency)
|
||||
int(tr.symbol_rate)
|
||||
tr.pls_mode is None or int(tr.pls_mode)
|
||||
tr.pls_code is None or int(tr.pls_code)
|
||||
tr.is_id is None or int(tr.is_id)
|
||||
except TypeError:
|
||||
tr.t2mi_plp_id is None or int(tr.t2mi_plp_id)
|
||||
except (TypeError, ValueError) as e:
|
||||
log(f"Transponder validation error: {e}\n{tr}")
|
||||
return False
|
||||
|
||||
if tr.polarization not in POLARIZATION.values():
|
||||
if tr.polarization not in POLARIZATION:
|
||||
return False
|
||||
if tr.fec_inner not in FEC.values():
|
||||
if tr.fec_inner not in FEC:
|
||||
return False
|
||||
if tr.system not in SYSTEM.values():
|
||||
if tr.system not in SYSTEM:
|
||||
return False
|
||||
if tr.modulation not in MODULATION.values():
|
||||
if tr.modulation not in MODULATION:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -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,94 +1,310 @@
|
||||
""" Module for parsing bouquets """
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Module for working with 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_ROOT_FILE_NAME = "bouquets.tv"
|
||||
_RADIO_ROOT_FILE_NAME = "bouquets.radio"
|
||||
_TV_FILE = "bouquets.tv"
|
||||
_RADIO_FILE = "bouquets.radio"
|
||||
_DEFAULT_BOUQUET_NAME = "favourites"
|
||||
_MARKER_PREFIX = "[MARKER!] "
|
||||
|
||||
|
||||
def get_bouquets(path):
|
||||
return parse_bouquets(path, "bouquets.tv", BqType.TV.value), parse_bouquets(path, "bouquets.radio",
|
||||
BqType.RADIO.value)
|
||||
class BouquetsWriter:
|
||||
""" Class for creating and writing bouquet files.
|
||||
|
||||
If "force_bq_names" then naming the files using the name of the bouquet.
|
||||
Some images may have problems displaying the favorites list!
|
||||
"""
|
||||
_SERVICE = '#SERVICE 1:{}:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet\n'
|
||||
_MARKER = "#SERVICE 1:64:{:X}:0:0:0:0:0:0:0::{}\n"
|
||||
_SPACE = "#SERVICE 1:832:D:{}:0:0:0:0:0:0:\n"
|
||||
_LOCKED = '1:{}:{}:0:0:0:0:0:0:0:FROM BOUQUET "userbouquet.{}.{}" ORDER BY bouquet'
|
||||
_ALT = '#SERVICE 1:134:1:0:0:0:0:0:0:0:FROM BOUQUET "{}" ORDER BY bouquet\n'
|
||||
_ALT_PAT = r"[<>:\"/\\|?*\-\s]"
|
||||
|
||||
def 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 = []
|
||||
def __init__(self, path, bouquets, force_bq_names=False, blacklist=None):
|
||||
self._path = path
|
||||
self._bouquets = bouquets
|
||||
self._force_bq_names = force_bq_names
|
||||
self._black_list = set() if blacklist is None else blacklist
|
||||
|
||||
for bqs in bouquets:
|
||||
line.clear()
|
||||
line.append("#NAME {}\n".format(bqs.name))
|
||||
self._marker_index = 1
|
||||
self._space_index = 0
|
||||
self._alt_names = set()
|
||||
self._NAME_PATTERN = re.compile("[^\\w_()]+")
|
||||
|
||||
for bq in bqs.bouquets:
|
||||
line.append(srv_line.format(bq.name.replace(" ", "_"), bq.type))
|
||||
write_bouquet(path, bq.name, bq.type, bq.services)
|
||||
def write(self):
|
||||
line = []
|
||||
|
||||
with open(path + "bouquets.{}".format(bqs.type), "w", encoding="utf-8") as file:
|
||||
file.writelines(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
|
||||
|
||||
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)
|
||||
|
||||
def write_bouquet(path, name, bq_type, channels):
|
||||
bouquet = ["#NAME {}\n".format(name)]
|
||||
bq_type = BqType(bq.type)
|
||||
if bq_type is BqType.MARKER:
|
||||
m_data = bq.file.split(":") if bq.file else None
|
||||
b_name = m_data[-1].strip() if m_data else bq.name.lstrip(_MARKER_PREFIX)
|
||||
line.append(self._MARKER.format(m_count, b_name))
|
||||
m_count += 1
|
||||
else:
|
||||
if bq_type is BqType.BOUQUET:
|
||||
bq_name = re.sub(self._NAME_PATTERN, "_", bq.name)
|
||||
self.write_sub_bouquet(self._path, bq_name, bq, bqs.type)
|
||||
else:
|
||||
self.write_bouquet(f"{self._path}userbouquet.{bq_name}.{bqs.type}", bq.name, bq.services)
|
||||
bq_type = 2 if bqs.type == BqType.RADIO.value else 1
|
||||
# Parental lock.
|
||||
locked = self._LOCKED.format(ServiceType.SERVICE, bq_type, bq_name, bqs.type)
|
||||
self._black_list.add(locked) if bq.locked else self._black_list.discard(locked)
|
||||
# Hiding.
|
||||
s_type = ServiceType.HIDDEN if bq.hidden else ServiceType.BOUQUET
|
||||
line.append(self._SERVICE.format(s_type, bq_type, bq_name, bqs.type))
|
||||
|
||||
for ch in channels:
|
||||
if not ch: # if was duplicate
|
||||
continue
|
||||
with open(f"{self._path}bouquets.{bqs.type}", "w", encoding="utf-8", newline="\n") as file:
|
||||
file.writelines(line)
|
||||
|
||||
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)))
|
||||
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}"
|
||||
|
||||
with open(path + "userbouquet.{}.{}".format(name.replace(" ", "_"), bq_type), "w", encoding="utf-8") as file:
|
||||
file.writelines(bouquet)
|
||||
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 and len(data_type) > 4:
|
||||
data_type = int(ch.data_id.split(":")[4])
|
||||
|
||||
return "{}:0:{:X}:{}:0:0:0:".format(1, data_type, ch.fav_id)
|
||||
|
||||
|
||||
def get_bouquet(path, name, bq_type):
|
||||
""" Parsing services ids from bouquet file """
|
||||
with open(path + "userbouquet.{}.{}".format(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 ['']
|
||||
for ch in srvs[1:]:
|
||||
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:
|
||||
fav_id = "{}:{}:{}:{}".format(ch_data[3], ch_data[4], ch_data[5], ch_data[6])
|
||||
services.append(BouquetService(None, BqServiceType.DEFAULT, fav_id, 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 srvs[0].strip("#NAME").strip(), 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, encoding="utf-8", errors="replace") as file:
|
||||
lines = file.readlines()
|
||||
bouquets = None
|
||||
nm_sep = "#NAME"
|
||||
class ServiceType(Enum):
|
||||
SERVICE = "0"
|
||||
BOUQUET = "7" # Sub bouquet.
|
||||
MARKER = "64"
|
||||
SPACE = "832"
|
||||
ALT = "134" # Alternatives.
|
||||
UDP = "256"
|
||||
HIDDEN = "519" # Skip, hide.
|
||||
|
||||
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:
|
||||
b_name, services = get_bouquet(path, line.split(".")[1], bq_type)
|
||||
bouquets[2].append(Bouquet(name=b_name,
|
||||
type=bq_type,
|
||||
services=services,
|
||||
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
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class BouquetsReader:
|
||||
""" Class for reading and parsing bouquets. """
|
||||
_ALT_PAT = re.compile(r".*alternatives\.+(.*)\.([tv|radio]+).*")
|
||||
_BQ_PAT = re.compile(r".*\s+\W(.*bouquet)\.+(.*)\.+[tv|radio].*")
|
||||
_SUB_BQ_PAT = re.compile(r".*subbouquet\.+(.*)\.([tv|radio]+).*")
|
||||
_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)
|
||||
s_data = line.split(":")
|
||||
s_type = ServiceType(s_data[1])
|
||||
if name:
|
||||
prefix, b_name = name.group(1), name.group(2)
|
||||
if b_name in b_names:
|
||||
log(f"The list of bouquets contains duplicate [{b_name}] names!")
|
||||
else:
|
||||
b_names.add(b_name)
|
||||
|
||||
rb_name, services = self.get_bouquet(self._path, b_name, bq_type, prefix)
|
||||
if rb_name in real_b_names:
|
||||
log(f"Bouquet file '{prefix}.{b_name}.{bq_type}' has duplicate name: {rb_name}")
|
||||
real_b_names[rb_name] += 1
|
||||
rb_name = f"{rb_name} {real_b_names[rb_name]}"
|
||||
else:
|
||||
real_b_names[rb_name] = 0
|
||||
# Locked, hidden.
|
||||
s_data[:2] = "10"
|
||||
locked = ":".join(s_data).rstrip()
|
||||
hidden = s_type is ServiceType.HIDDEN
|
||||
|
||||
bouquets[2].append(Bouquet(rb_name, bq_type, services, locked, hidden, b_name))
|
||||
else:
|
||||
if len(s_data) == 12 and s_type is ServiceType.MARKER:
|
||||
b_name = f"{_MARKER_PREFIX}{s_data[-1].strip()}"
|
||||
bouquets[2].append(Bouquet(b_name, BqType.MARKER.value, [], None, None, line.strip()))
|
||||
else:
|
||||
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,11 +1,39 @@
|
||||
""" This module used for parsing and write 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!!!
|
||||
"""
|
||||
from app.commons import log
|
||||
from app.eparser.satxml import get_pos_str
|
||||
from app.ui.uicommons import CODED_ICON, LOCKED_ICON, HIDE_ICON
|
||||
from .blacklist import get_blacklist
|
||||
from ..ecommons import Service, POLARIZATION, FEC, SERVICE_TYPE, Flag
|
||||
from ..ecommons import Service, POLARIZATION, FEC, SERVICE_TYPE, Flag, T_FEC, TrType, FEC_DEFAULT, T_SYSTEM
|
||||
|
||||
_HEADER = "eDVB services /{}/"
|
||||
_SEP = ":" # separator
|
||||
@@ -14,194 +42,301 @@ _END_LINE = "# File was created in DemonEditor.\n# ....Enjoy watching!....\n"
|
||||
|
||||
|
||||
def get_services(path, format_version):
|
||||
return parse(path, format_version)
|
||||
return LameDbReader(path, format_version).parse()
|
||||
|
||||
|
||||
def write_services(path, services, format_version=4):
|
||||
if format_version == 4:
|
||||
write_to_lamedb(path, services)
|
||||
elif format_version == 5:
|
||||
write_to_lamedb5(path, services)
|
||||
LameDbWriter(path, services, format_version).write()
|
||||
|
||||
|
||||
def write_to_lamedb(path, services):
|
||||
""" Writing lamedb file ver.4 """
|
||||
lines = [_HEADER.format(4), "\ntransponders\n"]
|
||||
tr_lines = []
|
||||
services_lines = ["end\nservices\n"]
|
||||
tr_set = set()
|
||||
class LameDbReader:
|
||||
""" Lamedb parser class.
|
||||
|
||||
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))
|
||||
Reads and parses the Enigma2 lamedb[5] file.
|
||||
Supports versions 3, 4 and 5.
|
||||
"""
|
||||
__slots__ = ["_path", "_fmt"]
|
||||
|
||||
tr_lines.sort()
|
||||
lines.extend(tr_lines)
|
||||
lines.extend(services_lines)
|
||||
lines.append("end\n" + _END_LINE)
|
||||
with open(path + _FILE_NAME, "w") as file:
|
||||
file.writelines(lines)
|
||||
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 = get_pos_str(int(pos))
|
||||
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 write_to_lamedb5(path, services):
|
||||
""" Writing lamedb5 file """
|
||||
lines = [_HEADER.format(5) + "\n"]
|
||||
services_lines = []
|
||||
tr_set = set()
|
||||
class LameDbWriter:
|
||||
""" Writes the Enigma2 lamedb[5] file.
|
||||
|
||||
for srv in services:
|
||||
data_id = str(srv.data_id).split(_SEP)
|
||||
tr_id = "{}:{}:{}".format(data_id[1], data_id[2], data_id[3])
|
||||
tr_set.add("t:{},{}\n".format(tr_id, srv.transponder.replace(" ", ":", 1)))
|
||||
services_lines.append("s:{},\"{}\",{}\n".format(srv.data_id, srv.service, srv.flags_cas))
|
||||
Version 4 will be used instead of version 3!
|
||||
"""
|
||||
__slots__ = ["_path", "_fmt", "_services"]
|
||||
|
||||
lines.extend(sorted(tr_set))
|
||||
lines.extend(services_lines)
|
||||
lines.append(_END_LINE)
|
||||
def __init__(self, path, services, fmt=4):
|
||||
self._path = path
|
||||
self._fmt = fmt
|
||||
self._services = services
|
||||
|
||||
with open(path + "lamedb5", "w") as file:
|
||||
file.writelines(lines)
|
||||
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()
|
||||
|
||||
def write_to_lamedb5(self):
|
||||
""" Writing lamedb5 file. """
|
||||
lines = [_HEADER.format(5) + "\n"]
|
||||
services_lines = []
|
||||
tr_set = set()
|
||||
|
||||
def parse(path, version=4):
|
||||
""" Parsing lamedb """
|
||||
if version == 4:
|
||||
return parse_v4(path)
|
||||
elif version == 5:
|
||||
return parse_v5(path)
|
||||
raise SyntaxError("Unsupported version of the format.")
|
||||
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")
|
||||
|
||||
lines.extend(sorted(tr_set))
|
||||
lines.extend(services_lines)
|
||||
lines.append(_END_LINE)
|
||||
|
||||
def parse_v4(path):
|
||||
""" Parsing version 4 """
|
||||
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("\nend") # 3 step
|
||||
|
||||
return parse_services(services.split("\n"), parse_transponders(transponders.split("/")), path)
|
||||
|
||||
|
||||
def parse_v5(path):
|
||||
""" Parsing version 5 """
|
||||
with open(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 v.5 parsing error: unsupported format.")
|
||||
|
||||
trs, srvs = {}, [""]
|
||||
for l in lns:
|
||||
if l.startswith("s:"):
|
||||
srv_data = l.strip("s:").split(",", 2)
|
||||
srv_data[1], srv_data[2] = srv_data[1].strip("\""), srv_data[2].strip()
|
||||
srvs.extend(srv_data)
|
||||
elif l.startswith("t:"):
|
||||
tr, srv = l.split(",")
|
||||
trs[tr.strip("t:")] = srv.strip().replace(":", " ", 1)
|
||||
|
||||
return parse_services(srvs, trs, path)
|
||||
|
||||
|
||||
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]
|
||||
|
||||
return transponders
|
||||
|
||||
|
||||
def parse_services(services, transponders, path):
|
||||
""" Parsing channels """
|
||||
channels = []
|
||||
blacklist = str(get_blacklist(path))
|
||||
srv = split(services, 3)
|
||||
if srv[0][0] == "": # remove first empty element
|
||||
srv.remove(srv[0])
|
||||
|
||||
for ch in srv:
|
||||
data = str(ch[0]).split(_SEP)
|
||||
sp = "0"
|
||||
tid = data[2]
|
||||
nid = data[3]
|
||||
transponder_id = "{}:{}:{}".format(data[1], tid, nid)
|
||||
transponder = transponders.get(transponder_id, None)
|
||||
|
||||
tid = tid.lstrip(sp).upper()
|
||||
nid = nid.lstrip(sp).upper()
|
||||
ssid = str(data[0]).lstrip(sp).upper()
|
||||
onid = str(data[1]).lstrip(sp).upper()
|
||||
# For comparison in bouquets. Needed in upper case!!!
|
||||
fav_id = "{}:{}:{}:{}".format(ssid, tid, nid, onid)
|
||||
picon_id = "1_0_{}_{}_{}_{}_{}_0_0_0.png".format(1, ssid, tid, nid, onid)
|
||||
|
||||
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 Flag.is_hide(int(flags[0][2:])) else None
|
||||
locked = LOCKED_ICON if fav_id in blacklist else None
|
||||
|
||||
package = list(filter(lambda x: x.startswith("p:"), all_flags))
|
||||
package = package[0][2:] if package else 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"])
|
||||
# removing all non printable symbols!
|
||||
srv_name = "".join(c for c in ch[1] if c.isprintable())
|
||||
channels.append(Service(flags_cas=ch[2],
|
||||
transponder_type=tr_type,
|
||||
coded=coded,
|
||||
service=srv_name,
|
||||
locked=locked,
|
||||
hide=hide,
|
||||
package=package,
|
||||
service_type=service_type,
|
||||
picon=None,
|
||||
picon_id=picon_id,
|
||||
ssid=data[0],
|
||||
freq=tr[0],
|
||||
rate=tr[1],
|
||||
pol=POLARIZATION[tr[2]],
|
||||
fec=FEC[tr[3]],
|
||||
system="DVB-S2" if len(tr) > 7 else "DVB-S",
|
||||
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,42 +1,173 @@
|
||||
""" Module for IPTV and streams support """
|
||||
from enum import Enum
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
from app.properties import Profile
|
||||
|
||||
""" 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
|
||||
from .ecommons import BqServiceType, Service
|
||||
|
||||
# url, description, urlkey, account, usrname, psw, s_type, iconsrc, iconsrc_b, group
|
||||
NEUTRINO_FAV_ID_FORMAT = "{}::{}::{}::{}::{}::{}::{}::{}::{}::{}"
|
||||
ENIGMA2_FAV_ID_FORMAT = " {}:0:{}:{:X}:{:X}:{:X}:{:X}:0:0:0:{}:{}\n#DESCRIPTION: {}\n"
|
||||
ENIGMA2_FAV_ID_FORMAT = " {}:{}:{:X}:{:X}:{:X}:{:X}:{:X}:0:0:0:{}:{}\n#DESCRIPTION {}\n"
|
||||
MARKER_FORMAT = " 1:64:{}:0:0:0:0:0:0:0::{}\n#DESCRIPTION {}\n"
|
||||
PICON_FORMAT = "{}_{}_{:X}_{:X}_{:X}_{:X}_{:X}_0_0_0.png"
|
||||
|
||||
|
||||
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, profile):
|
||||
with open(path) as file:
|
||||
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
|
||||
channels = []
|
||||
count = 0
|
||||
s_aggr = aggr[: -3]
|
||||
services = []
|
||||
groups = set()
|
||||
marker_counter = 1
|
||||
sid_counter = 1
|
||||
name = None
|
||||
fav_id = None
|
||||
for line in file.readlines():
|
||||
if line.startswith("#EXTINF"):
|
||||
name = line[1 + line.index(","):].strip()
|
||||
count += 1
|
||||
elif count == 1:
|
||||
count = 0
|
||||
if profile is Profile.ENIGMA_2:
|
||||
fav_id = ENIGMA2_FAV_ID_FORMAT.format(StreamType.DVB_TS.value, 1, 0, 0, 0, 0,
|
||||
line.strip().replace(":", "%3a"), name, name, None)
|
||||
elif profile is Profile.NEUTRINO_MP:
|
||||
fav_id = NEUTRINO_FAV_ID_FORMAT.format(line.strip(), "", 0, None, None, None, None, "", "", 1)
|
||||
srv = Service(None, None, IPTV_ICON, name, *aggr[0:3], BqServiceType.IPTV.name, *aggr, fav_id, None)
|
||||
channels.append(srv)
|
||||
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, force_quote=True):
|
||||
""" Returns fav id depending on the settings type. """
|
||||
if settings_type is SettingsType.ENIGMA_2:
|
||||
st_type = st_type or StreamType.NONE_TS.value
|
||||
params = params or (0, 0, 0, 0)
|
||||
url = quote(url) if force_quote else url
|
||||
return ENIGMA2_FAV_ID_FORMAT.format(st_type, s_id, srv_type, *params, url, name, name, None)
|
||||
elif settings_type is SettingsType.NEUTRINO_MP:
|
||||
return NEUTRINO_FAV_ID_FORMAT.format(url, "", 0, None, None, None, None, "", "", 1)
|
||||
|
||||
|
||||
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,8 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
from xml.dom.minidom import parse, Document
|
||||
|
||||
from app.eparser.iptv import NEUTRINO_FAV_ID_FORMAT
|
||||
from app.ui.uicommons import LOCKED_ICON, HIDE_ICON
|
||||
from app.eparser.neutrino import KSP, SP, get_xml_attributes, get_attributes, API_VER
|
||||
from app.eparser.neutrino.nxml import XmlHandler, NeutrinoDocument
|
||||
from ..ecommons import Bouquets, Bouquet, BouquetService, BqServiceType, PROVIDER, BqType
|
||||
|
||||
_FILE = "bouquets.xml"
|
||||
@@ -23,37 +51,36 @@ 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))
|
||||
locked=locked == "1",
|
||||
hidden=hidden == "1",
|
||||
file=SP.join("{}{}{}".format(k, KSP, v) for k, v in bq_attrs.items())))
|
||||
|
||||
if BqType(bq_type) is BqType.BOUQUET:
|
||||
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
|
||||
@@ -64,38 +91,27 @@ def parse_webtv(path, name, bq_type):
|
||||
if not os.path.exists(path):
|
||||
return bouquets
|
||||
|
||||
dom = parse(path)
|
||||
dom = XmlHandler.parse(path)
|
||||
services = []
|
||||
for elem in dom.getElementsByTagName("webtv"):
|
||||
if elem.hasAttributes():
|
||||
title = elem.attributes["title"].value
|
||||
url = elem.attributes["url"].value
|
||||
description = elem.attributes.get("description")
|
||||
description = description.value if description else description
|
||||
urlkey = elem.attributes.get("urlkey", None)
|
||||
urlkey = urlkey.value if urlkey else urlkey
|
||||
account = elem.attributes.get("account", None)
|
||||
account = account.value if account else account
|
||||
usrname = elem.attributes.get("usrname", None)
|
||||
usrname = usrname.value if usrname else usrname
|
||||
psw = elem.attributes.get("psw", None)
|
||||
psw = psw.value if psw else psw
|
||||
s_type = elem.attributes.get("type", None)
|
||||
s_type = s_type.value if s_type else s_type
|
||||
iconsrc = elem.attributes.get("iconsrc", None)
|
||||
iconsrc = iconsrc.value if iconsrc else iconsrc
|
||||
iconsrc_b = elem.attributes.get("iconsrc_b", None)
|
||||
iconsrc_b = iconsrc_b.value if iconsrc_b else iconsrc_b
|
||||
group = elem.attributes.get("group", None)
|
||||
group = group.value if group else group
|
||||
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)
|
||||
srv = BouquetService(name=title,
|
||||
type=BqServiceType.IPTV,
|
||||
data=fav_id,
|
||||
num=0)
|
||||
services.append(srv)
|
||||
bouquet = Bouquet(name="default", type=bq_type, services=services, locked=None, hidden=None)
|
||||
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
|
||||
@@ -111,18 +127,29 @@ def write_bouquets(path, bouquets):
|
||||
|
||||
|
||||
def write_bouquet(file, bouquet):
|
||||
doc = Document()
|
||||
doc = NeutrinoDocument()
|
||||
root = doc.createElement("zapit")
|
||||
root.setAttribute("api", API_VER)
|
||||
doc.appendChild(root)
|
||||
comment = doc.createComment(_COMMENT)
|
||||
doc.appendChild(comment)
|
||||
|
||||
for bq in bouquet.bouquets:
|
||||
attrs = get_attributes(bq.file) if bq.file else {}
|
||||
attrs["name"] = bq.name
|
||||
if bq.hidden:
|
||||
attrs["hidden"] = "1"
|
||||
else:
|
||||
attrs.pop("hidden", None)
|
||||
if bq.locked:
|
||||
attrs["locked"] = "1"
|
||||
else:
|
||||
attrs.pop("locked", None)
|
||||
|
||||
bq_elem = doc.createElement("Bouquet")
|
||||
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:
|
||||
@@ -132,16 +159,15 @@ 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 = Document()
|
||||
doc = NeutrinoDocument()
|
||||
root = doc.createElement("webtvs")
|
||||
doc.appendChild(root)
|
||||
comment = doc.createComment(_COMMENT)
|
||||
@@ -175,7 +201,7 @@ def write_webtv(file, bouquet):
|
||||
|
||||
root.appendChild(srv_elem)
|
||||
|
||||
doc.writexml(open(file, "w"), addindent=" ", newl="\n", encoding="UTF-8")
|
||||
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,168 +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.ssid)
|
||||
srv_elem.setAttribute("n", srv.service)
|
||||
|
||||
srv_attrs = srv.data_id.split(":")
|
||||
api = srv_attrs.pop(0)
|
||||
|
||||
if api == "3":
|
||||
root.setAttribute("api", "3") # !!!
|
||||
for i, value in enumerate(srv_attrs):
|
||||
if value == "None":
|
||||
continue
|
||||
srv_elem.setAttribute(_SRV_ATTR_NAMES[i], value)
|
||||
|
||||
tr_elem.appendChild(srv_elem)
|
||||
|
||||
doc.writexml(open(path + _FILE, "w"), addindent=" ", newl="\n", encoding="UTF-8")
|
||||
doc.unlink()
|
||||
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)
|
||||
tr_id = tr_id.lstrip("0")
|
||||
def __init__(self, path):
|
||||
self._path = path + _FILE
|
||||
self._attrs = None
|
||||
self._tr = None
|
||||
self._api = "4"
|
||||
self._services = []
|
||||
|
||||
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
|
||||
def get_services(self):
|
||||
dom = XmlHandler.parse(self._path)
|
||||
|
||||
data_id = "{}:{}:{}:{}:{}:{}:{}:{}:{}:{}:{}".format(api, srv_type, sys, num, f, v, a, p, pmt, tx, vt)
|
||||
fav_id = "{}:{}:{}".format(tr_id, on.lstrip("0"), ssid.lstrip("0"))
|
||||
picon_id = "{}{}{}.png".format(tr_id, on, ssid)
|
||||
for root in dom.getElementsByTagName("zapit"):
|
||||
if root.hasAttributes():
|
||||
api = root.attributes["api"]
|
||||
self._api = api.value if api else self._api
|
||||
|
||||
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))),
|
||||
picon=None,
|
||||
picon_id=picon_id,
|
||||
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)
|
||||
for elem in root.getElementsByTagName("sat"):
|
||||
if elem.hasAttributes():
|
||||
sat_attrs = get_xml_attributes(elem)
|
||||
sat_pos = 0
|
||||
try:
|
||||
sat_pos = int(sat_attrs.get("position", "0"))
|
||||
sat_pos = "{:0.1f}{}".format(abs(sat_pos / 10), "W" if sat_pos < 0 else "E")
|
||||
except ValueError as e:
|
||||
log("Neutrino parsing error [parse sat position]: {}".format(e))
|
||||
sat = SP.join("{}{}{}".format(k, KSP, v) for k, v in sat_attrs.items())
|
||||
for tr_elem in elem.getElementsByTagName("TS"):
|
||||
if tr_elem.hasAttributes():
|
||||
self.parse_sat_transponder(sat, sat_pos, tr_elem)
|
||||
|
||||
# Terrestrial DVB-T[2].
|
||||
for elem in root.getElementsByTagName("terrestrial"):
|
||||
if elem.hasAttributes():
|
||||
terr_attrs = get_xml_attributes(elem)
|
||||
terr = SP.join("{}{}{}".format(k, KSP, v) for k, v in terr_attrs.items())
|
||||
|
||||
for tr_elem in elem.getElementsByTagName("TS"):
|
||||
if tr_elem.hasAttributes():
|
||||
self.parse_ct_transponder(terr, tr_elem, TrType.Terrestrial)
|
||||
|
||||
# Cable.
|
||||
for elem in root.getElementsByTagName("cable"):
|
||||
if elem.hasAttributes():
|
||||
cable_attrs = get_xml_attributes(elem)
|
||||
cable = SP.join("{}{}{}".format(k, KSP, v) for k, v in cable_attrs.items())
|
||||
|
||||
for tr_elem in elem.getElementsByTagName("TS"):
|
||||
if tr_elem.hasAttributes():
|
||||
self.parse_ct_transponder(cable, tr_elem, TrType.Cable)
|
||||
|
||||
return self._services
|
||||
|
||||
def parse_sat_transponder(self, sat, sat_pos, tr_elem):
|
||||
tr_attr = get_xml_attributes(tr_elem)
|
||||
tr = SP.join("{}{}{}".format(k, KSP, v) for k, v in tr_attr.items())
|
||||
tr_id = tr_attr.get("id", "0").lstrip("0")
|
||||
on = tr_attr.get("on", "0")
|
||||
freq = tr_attr.get("frq", "0")
|
||||
rate = tr_attr.get("sr", "0")
|
||||
fec = tr_attr.get("fec", "0")
|
||||
|
||||
pol = POLARIZATION.get(tr_attr.get("pol", "0"))
|
||||
# Formatting displayed values.
|
||||
try:
|
||||
freq = "{}".format(int(freq) // 1000)
|
||||
rate = "{}".format(int(rate) // 1000)
|
||||
except ValueError as e:
|
||||
log("Neutrino parsing error [parse_transponder]: {}".format(e))
|
||||
|
||||
for srv_elem in tr_elem.getElementsByTagName("S"):
|
||||
if srv_elem.hasAttributes():
|
||||
at = get_xml_attributes(srv_elem)
|
||||
at["api"] = self._api
|
||||
ssid, name, s_type, sys = at.get("i", "0"), at.get("n", ""), at.get("t", "3"), at.get("s", "0")
|
||||
data_id = SP.join("{}{}{}".format(k, KSP, v) for k, v in at.items())
|
||||
fav_id = "{}:{}:{}".format(tr_id, on.lstrip("0"), ssid.lstrip("0"))
|
||||
picon_id = "{}{}{}.png".format(tr_id, on, ssid)
|
||||
prv = PROVIDER.get(int(on, 16), "")
|
||||
st = SERVICE_TYPE.get(str(int(s_type, 16)), SERVICE_TYPE.get("-2"))
|
||||
|
||||
srv = Service(sat, TrType.Satellite.value, None, name, None, None, prv, st, None, picon_id, ssid, freq,
|
||||
rate, pol, FEC.get(fec), SYSTEM.get(sys), sat_pos, data_id, fav_id, tr)
|
||||
self._services.append(srv)
|
||||
|
||||
def parse_ct_transponder(self, terr, tr_elem, tr_type):
|
||||
attrs = get_xml_attributes(tr_elem)
|
||||
tr = SP.join("{}{}{}".format(k, KSP, v) for k, v in attrs.items())
|
||||
tr_id, on, freq = attrs.get("id", "0").lstrip("0"), attrs.get("on", "0"), attrs.get("frq", "0")
|
||||
|
||||
for srv_elem in tr_elem.getElementsByTagName("S"):
|
||||
if srv_elem.hasAttributes():
|
||||
s_at = get_xml_attributes(srv_elem)
|
||||
s_at["api"] = self._api
|
||||
ssid, name, s_type, sys = s_at.get("i", "0"), s_at.get("n", ""), s_at.get("t", "3"), s_at.get("s", "0")
|
||||
data_id = SP.join("{}{}{}".format(k, KSP, v) for k, v in s_at.items())
|
||||
fav_id = "{}:{}:{}".format(tr_id, on.lstrip("0"), ssid.lstrip("0"))
|
||||
picon_id = "{}{}{}.png".format(tr_id, on, ssid)
|
||||
prv = PROVIDER.get(int(on, 16), "")
|
||||
st = SERVICE_TYPE.get(str(int(s_type, 16)), SERVICE_TYPE.get("-2"))
|
||||
|
||||
if tr_type is TrType.Terrestrial:
|
||||
sys = T_SYSTEM.get(sys)
|
||||
pos = "T"
|
||||
elif tr_type is TrType.Cable:
|
||||
sys = SystemCable(sys).name
|
||||
pos = "C"
|
||||
else:
|
||||
log("Parse transponder error: Not supported type [{}]".format(tr_type))
|
||||
break
|
||||
|
||||
srv = Service(terr, tr_type.value, None, name, None, None, prv, st, None, picon_id, ssid,
|
||||
freq, "0", None, None, sys, pos, data_id, fav_id, tr)
|
||||
self._services.append(srv)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,117 +1,200 @@
|
||||
""" Module foe parsing Satellites.xml
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
For more info see __COMMENT
|
||||
|
||||
""" Module for working with *.xml files.
|
||||
|
||||
For more info see comments.
|
||||
"""
|
||||
from functools import lru_cache
|
||||
from xml.dom.minidom import parse, Document
|
||||
import xml.etree.ElementTree as ETree
|
||||
|
||||
import os
|
||||
from .ecommons import Satellite, Terrestrial, Cable, Transponder, TerTransponder, CableTransponder
|
||||
|
||||
from .ecommons import POLARIZATION, FEC, SYSTEM, MODULATION, PLS_MODE, Transponder, Satellite, get_key_by_value
|
||||
_SAT_COMMENT = ("\tFile was created in DemonEditor.\n\n"
|
||||
"Usable flags are:\n"
|
||||
" 1: Network Scan\n"
|
||||
" 2: use BAT\n"
|
||||
" 4: use ONIT\n"
|
||||
" 8: skip NITs of known networks\n"
|
||||
" This is a bitmap and combinations can be used.\n\n"
|
||||
"Transponder parameters:\n"
|
||||
"\tpolarization: 0 - Horizontal, 1 - Vertical, 2 - Left Circular, 3 - Right Circular\n"
|
||||
"\tfec_inner: 0 - Auto, 1 - 1/2, 2 - 2/3, 3 - 3/4, 4 - 5/6, 5 - 7/8, 6 - 8/9, 7 - 3/5,\n"
|
||||
"\t8 - 4/5, 9 - 9/10, 15 - None\n"
|
||||
"\tmodulation: 0 - Auto, 1 - QPSK, 2 - 8PSK, 4 - 16APSK, 5 - 32APSK\n"
|
||||
"\trolloff: 0 - 0.35, 1 - 0.25, 2 - 0.20, 3 - Auto\n"
|
||||
"\tpilot: 0 - Off, 1 - On, 2 - Auto\n"
|
||||
"\tinversion: 0 = Off, 1 = On, 2 = Auto (default)\n"
|
||||
"\tsystem: 0 = DVB-S, 1 = DVB-S2\n"
|
||||
"\tis_id: 0 - 255\n"
|
||||
"\tpls_mode: 0 - Root, 1 - Gold, 2 - Combo\n"
|
||||
"\tpls_code: 0 - 262142\n\n")
|
||||
|
||||
__COMMENT = (" File was created in DemonEditor\n\n"
|
||||
"usable flags are\n"
|
||||
" 1: Network Scan\n"
|
||||
" 2: use BAT\n"
|
||||
" 4: use ONIT\n"
|
||||
" 8: skip NITs of known networks\n"
|
||||
" and combinations of this.\n\n"
|
||||
_TERRESTRIAL_COMMENT = ("\tFile was created in DemonEditor.\n\n"
|
||||
"Usable flags are:\n"
|
||||
" 1: Network Scan\n"
|
||||
" 2: use BAT\n"
|
||||
" 4: use ONIT\n"
|
||||
" 8: skip NITs of known networks\n"
|
||||
" This is a bitmap and combinations can be used.\n\n")
|
||||
|
||||
"transponder parameters:\n"
|
||||
"polarization: 0 - Horizontal, 1 - Vertical, 2 - Left Circular, 3 - Right Circular\n"
|
||||
"fec_inner: 0 - Auto, 1 - 1/2, 2 - 2/3, 3 - 3/4, 4 - 5/6, 5 - 7/8, 6 - 8/9, 7 - 3/5,\n"
|
||||
"8 - 4/5, 9 - 9/10, 15 - None\n"
|
||||
"modulation: 0 - Auto, 1 - QPSK, 2 - 8PSK, 3 - 16APSK, 5 - 32APSK\n"
|
||||
"rolloff: 0 - 0.35, 1 - 0.25, 2 - 0.20, 3 - Auto\n"
|
||||
"pilot: 0 - Off, 1 - On, 2 - Auto\n"
|
||||
"inversion: 0 = Off, 1 = On, 2 = Auto (default)\n"
|
||||
"system: 0 = DVB-S, 1 = DVB-S2\n"
|
||||
"is_id: 0 - 255\n"
|
||||
"pls_mode: 0 - Root, 1 - Gold, 2 - Combo\n"
|
||||
"pls_code: 0 - 262142\n\n")
|
||||
_CABLE_COMMENT = ("\tFile was created in DemonEditor.\n\n"
|
||||
"Transponder parameters:\n"
|
||||
"\tmodulation:\n"
|
||||
"\t3: QAM64\n"
|
||||
"\t5: QAM256\n")
|
||||
|
||||
|
||||
def get_satellites(path):
|
||||
return parse_satellites(path, os.path.getsize(path))
|
||||
""" Returns data [Satellite] list from *.xml. """
|
||||
return [Satellite(e.get("name", None),
|
||||
e.get("flags", None),
|
||||
e.get("position", None) or "0",
|
||||
get_sat_transponders(e)) for e in ETree.parse(path).iter("sat")]
|
||||
|
||||
|
||||
def write_satellites(satellites, data_path):
|
||||
""" Creation satellites.xml file """
|
||||
doc = Document()
|
||||
comment = doc.createComment(__COMMENT)
|
||||
doc.appendChild(comment)
|
||||
root = doc.createElement("satellites")
|
||||
doc.appendChild(root)
|
||||
|
||||
for sat in satellites:
|
||||
# Create Element
|
||||
sat_child = doc.createElement("sat")
|
||||
sat_child.setAttribute("name", sat.name)
|
||||
sat_child.setAttribute("flags", sat.flags)
|
||||
sat_child.setAttribute("position", sat.position)
|
||||
|
||||
for tr in sat.transponders:
|
||||
transponder_child = doc.createElement("transponder")
|
||||
transponder_child.setAttribute("frequency", tr.frequency)
|
||||
transponder_child.setAttribute("symbol_rate", tr.symbol_rate)
|
||||
transponder_child.setAttribute("polarization", get_key_by_value(POLARIZATION, tr.polarization))
|
||||
transponder_child.setAttribute("fec_inner", get_key_by_value(FEC, tr.fec_inner))
|
||||
transponder_child.setAttribute("system", get_key_by_value(SYSTEM, tr.system))
|
||||
transponder_child.setAttribute("modulation", get_key_by_value(MODULATION, tr.modulation))
|
||||
if tr.pls_mode:
|
||||
transponder_child.setAttribute("pls_mode", get_key_by_value(PLS_MODE, tr.pls_mode))
|
||||
if tr.pls_code:
|
||||
transponder_child.setAttribute("pls_code", tr.pls_code)
|
||||
if tr.is_id:
|
||||
transponder_child.setAttribute("is_id", tr.is_id)
|
||||
sat_child.appendChild(transponder_child)
|
||||
root.appendChild(sat_child)
|
||||
doc.writexml(open(data_path, "w"),
|
||||
# indent="",
|
||||
addindent=" ",
|
||||
newl='\n',
|
||||
encoding="iso-8859-1")
|
||||
doc.unlink()
|
||||
def get_sat_transponders(elem):
|
||||
""" Returns satellite transponders list. """
|
||||
return [Transponder(e.get("frequency", "0"),
|
||||
e.get("symbol_rate", "0"),
|
||||
e.get("polarization", None),
|
||||
e.get("fec_inner", None),
|
||||
e.get("system", None),
|
||||
e.get("modulation", None),
|
||||
e.get("pls_mode", None),
|
||||
e.get("pls_code", None),
|
||||
e.get("is_id", None),
|
||||
e.get("t2mi_plp_id", None)) for e in elem.iter("transponder")]
|
||||
|
||||
|
||||
def parse_transponders(elem):
|
||||
""" 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)
|
||||
return transponders
|
||||
def get_terrestrial(path):
|
||||
""" Returns data [Terrestrial] list from *.xml. """
|
||||
return [Terrestrial(e.get("name", None),
|
||||
e.get("flags", None),
|
||||
e.get("countrycode", None),
|
||||
[get_ter_transponder(e) for e in e.iter("transponder")]
|
||||
) for e in ETree.parse(path).iter("terrestrial")]
|
||||
|
||||
|
||||
def parse_sat(elem):
|
||||
""" Parsing satellite """
|
||||
return Satellite(elem.attributes["name"].value,
|
||||
elem.attributes["flags"].value,
|
||||
elem.attributes["position"].value,
|
||||
parse_transponders(elem))
|
||||
def get_ter_transponder(elem):
|
||||
""" Returns terrestrial transponder. """
|
||||
return TerTransponder(elem.get("centre_frequency", "0"),
|
||||
elem.get("system", None),
|
||||
elem.get("bandwidth", None),
|
||||
elem.get("constellation", None),
|
||||
elem.get("code_rate_hp", None),
|
||||
elem.get("code_rate_lp", None),
|
||||
elem.get("guard_interval", None),
|
||||
elem.get("transmission_mode", None),
|
||||
elem.get("hierarchy_information", None),
|
||||
elem.get("inversion", None),
|
||||
elem.get("plp_id", None))
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def parse_satellites(path, file_size):
|
||||
""" Parsing satellites from xml"""
|
||||
dom = parse(path)
|
||||
satellites = []
|
||||
def get_cable(path):
|
||||
""" Returns data [Cable] list from *.xml. """
|
||||
return [Cable(e.get("name", None),
|
||||
e.get("flags", None),
|
||||
e.get("satfeed", None),
|
||||
e.get("countrycode", None),
|
||||
get_cable_transponders(e)) for e in ETree.parse(path).iter("cable")]
|
||||
|
||||
for elem in dom.getElementsByTagName("sat"):
|
||||
if elem.hasAttributes():
|
||||
satellites.append(parse_sat(elem))
|
||||
|
||||
return satellites
|
||||
def get_cable_transponders(elem):
|
||||
""" Returns cable transponders list. """
|
||||
return [CableTransponder(e.get("frequency", "0"),
|
||||
e.get("symbol_rate", "0"),
|
||||
e.get("fec_inner", None),
|
||||
e.get("modulation", None)) for e in elem.iter("transponder")]
|
||||
|
||||
|
||||
def write_satellites(satellites, data_path, encoding="UTF-8"):
|
||||
""" Creates satellites.xml file. """
|
||||
write_xml("satellites", "sat", satellites, data_path, _SAT_COMMENT, encoding)
|
||||
|
||||
|
||||
def write_terrestrial(terrestrial, data_path, encoding="UTF-8"):
|
||||
""" Creates terrestrial.xml file. """
|
||||
write_xml("locations", "terrestrial", terrestrial, data_path, _TERRESTRIAL_COMMENT, encoding)
|
||||
|
||||
|
||||
def write_cable(cables, data_path, encoding="UTF-8"):
|
||||
""" Creates cables.xml file. """
|
||||
write_xml("cables", "cable", cables, data_path, _CABLE_COMMENT, encoding)
|
||||
|
||||
|
||||
def write_xml(root_name, sub_name, data, data_path, comment="", encoding="UTF-8"):
|
||||
""" Creates *.xml files. """
|
||||
xml = ETree.Element(root_name)
|
||||
[write_element(sub_name, "transponder", t, xml) for t in data]
|
||||
|
||||
tree = ETree.ElementTree(xml)
|
||||
indent(tree.getroot())
|
||||
|
||||
with open(data_path, "wb") as f:
|
||||
# To put comment on top.
|
||||
f.write(f'<?xml version="1.0" encoding="{encoding}"?>\n<!--\n{comment}-->\n\n'.encode("utf-8"))
|
||||
tree.write(f, encoding=encoding)
|
||||
|
||||
|
||||
def write_element(e_name, ch_name, e_data, root):
|
||||
""" Writes element with sub elements.
|
||||
|
||||
@param e_name: Element name.
|
||||
@param ch_name: Child element name.
|
||||
@param e_data: Element data -> defaultdict
|
||||
@param root: Parent of the element.
|
||||
"""
|
||||
t = e_data._asdict()
|
||||
subs = t.pop("transponders")
|
||||
root_sub = ETree.SubElement(root, e_name, {k: v for k, v in t.items() if v})
|
||||
[ETree.SubElement(root_sub, ch_name, {k: v for k, v in tr._asdict().items() if v}) for tr in subs]
|
||||
|
||||
|
||||
def indent(elem, parent=None, index=-1, level=0, space=" "):
|
||||
""" Appends whitespace to the subtree to indent the tree visually.
|
||||
|
||||
Since the minimum supported version < 3.9, we will use our own implementation.
|
||||
"""
|
||||
for i, sub in enumerate(elem):
|
||||
indent(sub, elem, i, level + 1)
|
||||
if parent:
|
||||
if index == 0:
|
||||
parent.text = f"\n{space * level}"
|
||||
else:
|
||||
parent[index - 1].tail = f"\n{space * level}"
|
||||
|
||||
if index == len(parent) - 1:
|
||||
elem.tail = f"\n{space * (level - 1)}"
|
||||
|
||||
|
||||
def get_pos_str(pos: int) -> str:
|
||||
""" Converts satellite position int value to readable string. """
|
||||
return f"{abs(pos / 10):0.1f}{'W' if pos < 0 else 'E'}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
180
app/ftp.py
180
app/ftp.py
@@ -1,180 +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", "lamedb5", "blacklist", "whitelist", # enigma 2
|
||||
"services.xml", "myservices.xml", "bouquets.xml", "ubouquets.xml") # neutrino
|
||||
|
||||
_SATELLITES_XML_FILE = "satellites.xml"
|
||||
_WEBTV_XML_FILE = "webtv.xml"
|
||||
|
||||
|
||||
class DownloadDataType(Enum):
|
||||
ALL = 0
|
||||
BOUQUETS = 1
|
||||
SATELLITES = 2
|
||||
PICONS = 3
|
||||
WEBTV = 4
|
||||
|
||||
|
||||
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"])
|
||||
ftp.encoding = "utf-8"
|
||||
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]
|
||||
download_file(ftp, name, save_path)
|
||||
# satellites.xml and webtv section
|
||||
if download_type in (DownloadDataType.ALL, DownloadDataType.SATELLITES, DownloadDataType.WEBTV):
|
||||
ftp.cwd(properties["satellites_xml_path"])
|
||||
files.clear()
|
||||
ftp.dir(files.append)
|
||||
|
||||
for file in files:
|
||||
name = str(file).strip()
|
||||
if download_type in (DownloadDataType.ALL, DownloadDataType.SATELLITES):
|
||||
if name.endswith(_SATELLITES_XML_FILE):
|
||||
download_file(ftp, _SATELLITES_XML_FILE, save_path)
|
||||
elif download_type in (DownloadDataType.ALL, DownloadDataType.WEBTV):
|
||||
if name.endswith(_WEBTV_XML_FILE):
|
||||
download_file(ftp, _WEBTV_XML_FILE, save_path)
|
||||
|
||||
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 neutrino
|
||||
tn.send("init 4")
|
||||
|
||||
with FTP(host=host) as ftp:
|
||||
ftp.login(user=properties["user"], passwd=properties["password"])
|
||||
ftp.encoding = "utf-8"
|
||||
|
||||
if download_type is DownloadDataType.ALL or download_type is DownloadDataType.SATELLITES:
|
||||
ftp.cwd(properties["satellites_xml_path"])
|
||||
send = send_file(_SATELLITES_XML_FILE, data_path, ftp)
|
||||
if download_type is DownloadDataType.SATELLITES:
|
||||
tn.send("init 3" if profile is Profile.ENIGMA_2 else "init 6")
|
||||
if callback is not None:
|
||||
callback()
|
||||
return send
|
||||
|
||||
if profile is Profile.NEUTRINO_MP and download_type in (DownloadDataType.ALL, DownloadDataType.WEBTV):
|
||||
ftp.cwd(properties["satellites_xml_path"])
|
||||
send = send_file(_WEBTV_XML_FILE, data_path, ftp)
|
||||
if download_type is DownloadDataType.WEBTV:
|
||||
tn.send("init 6")
|
||||
if callback is not None:
|
||||
callback()
|
||||
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_FILE or file_name == _WEBTV_XML_FILE:
|
||||
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 download_file(ftp, name, save_path):
|
||||
with open(save_path + name, "wb") as f:
|
||||
ftp.retrbinary("RETR " + name, f.write)
|
||||
|
||||
|
||||
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,68 +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/",
|
||||
"v5_support": False},
|
||||
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
|
||||
956
app/settings.py
Normal file
956
app/settings.py
Normal file
@@ -0,0 +1,956 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import copy
|
||||
import json
|
||||
import locale
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum, IntEnum
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from pprint import pformat
|
||||
from textwrap import dedent
|
||||
|
||||
SEP = os.sep
|
||||
HOME_PATH = str(Path.home())
|
||||
CONFIG_PATH = HOME_PATH + f"{SEP}.config{SEP}demon-editor{SEP}"
|
||||
CONFIG_FILE = CONFIG_PATH + "config.json"
|
||||
DATA_PATH = HOME_PATH + f"{SEP}DemonEditor{SEP}"
|
||||
GTK_PATH = os.environ.get("GTK_PATH", None)
|
||||
|
||||
IS_DARWIN = sys.platform == "darwin"
|
||||
IS_WIN = sys.platform == "win32"
|
||||
IS_LINUX = sys.platform == "linux"
|
||||
|
||||
USE_HEADER_BAR = int(bool(os.environ.get("GNOME_DESKTOP_SESSION_ID")))
|
||||
|
||||
|
||||
class Defaults(Enum):
|
||||
""" Default program settings """
|
||||
USER = "root"
|
||||
PASSWORD = ""
|
||||
HOST = "127.0.0.1"
|
||||
FTP_PORT = "21"
|
||||
HTTP_PORT = "80"
|
||||
TELNET_PORT = "23"
|
||||
HTTP_USE_SSL = False
|
||||
# Enigma2.
|
||||
BOX_SERVICES_PATH = "/etc/enigma2/"
|
||||
BOX_SATELLITE_PATH = "/etc/tuxbox/"
|
||||
BOX_EPG_PATH = "/etc/enigma2/"
|
||||
BOX_PICON_PATH = "/usr/share/enigma2/picon/"
|
||||
BOX_PICON_PATHS = ("/usr/share/enigma2/picon/",
|
||||
"/media/hdd/picon/",
|
||||
"/media/usb/picon/",
|
||||
"/media/mmc/picon/",
|
||||
"/media/cf/picon/",
|
||||
"/hdd/picon/",
|
||||
"/usb/picon/")
|
||||
# Neutrino.
|
||||
NEUTRINO_BOX_SERVICES_PATH = "/var/tuxbox/config/zapit/"
|
||||
NEUTRINO_BOX_SATELLITE_PATH = "/var/tuxbox/config/"
|
||||
NEUTRINO_BOX_PICON_PATH = "/usr/share/tuxbox/neutrino/icons/logo/"
|
||||
NEUTRINO_BOX_PICON_PATHS = ("/usr/share/tuxbox/neutrino/icons/logo/",)
|
||||
# Paths.
|
||||
BACKUP_PATH = f"{DATA_PATH}backup{SEP}"
|
||||
PICON_PATH = f"{DATA_PATH}picons{SEP}"
|
||||
|
||||
DEFAULT_PROFILE = "default"
|
||||
BACKUP_BEFORE_DOWNLOADING = True
|
||||
BACKUP_BEFORE_SAVE = True
|
||||
V5_SUPPORT = False
|
||||
UNLIMITED_COPY_BUFFER = False
|
||||
EXTENSIONS_SUPPORT = False
|
||||
FORCE_BQ_NAMES = False
|
||||
HTTP_API_SUPPORT = True
|
||||
ENABLE_YT_DL = False
|
||||
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
|
||||
RECORDINGS_PATH = f"{DATA_PATH}recordings{SEP}"
|
||||
ACTIVATE_TRANSCODING = False
|
||||
ACTIVE_TRANSCODING_PRESET = f"720p TV{SEP}device"
|
||||
|
||||
|
||||
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
|
||||
epg_path = Defaults.BOX_EPG_PATH.value
|
||||
http_timeout = 5
|
||||
telnet_timeout = 5
|
||||
else:
|
||||
srv_path = Defaults.NEUTRINO_BOX_SERVICES_PATH.value
|
||||
sat_path = Defaults.NEUTRINO_BOX_SATELLITE_PATH.value
|
||||
picons_path = Defaults.NEUTRINO_BOX_PICON_PATH.value
|
||||
epg_path = ""
|
||||
http_timeout = 2
|
||||
telnet_timeout = 1
|
||||
|
||||
return {"setting_type": self.value,
|
||||
"host": Defaults.HOST.value,
|
||||
"port": Defaults.FTP_PORT.value,
|
||||
"timeout": 5,
|
||||
"user": Defaults.USER.value,
|
||||
"password": Defaults.PASSWORD.value,
|
||||
"http_port": Defaults.HTTP_PORT.value,
|
||||
"http_timeout": http_timeout,
|
||||
"http_use_ssl": Defaults.HTTP_USE_SSL.value,
|
||||
"telnet_port": Defaults.TELNET_PORT.value,
|
||||
"telnet_timeout": telnet_timeout,
|
||||
"services_path": srv_path,
|
||||
"user_bouquet_path": srv_path,
|
||||
"satellites_xml_path": sat_path,
|
||||
"epg_dat_path": epg_path,
|
||||
"picons_path": picons_path}
|
||||
|
||||
|
||||
class SettingsException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SettingsReadException(SettingsException):
|
||||
pass
|
||||
|
||||
|
||||
class PlayStreamsMode(IntEnum):
|
||||
""" Behavior mode when opening streams. """
|
||||
BUILT_IN = 0
|
||||
WINDOW = 1
|
||||
M3U = 2
|
||||
|
||||
|
||||
class EpgSource(IntEnum):
|
||||
HTTP = 0 # HTTP API -> WebIf
|
||||
DAT = 1 # epg.dat file
|
||||
XML = 2 # XML TV
|
||||
|
||||
|
||||
class Settings:
|
||||
__INSTANCE = None
|
||||
__VERSION = 2
|
||||
|
||||
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 hosts(self):
|
||||
return self._cp_settings.get("hosts", [self.host, ])
|
||||
|
||||
@hosts.setter
|
||||
def hosts(self, value):
|
||||
self._cp_settings["hosts"] = value
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
return self._cp_settings.get("port", self.get_default("port"))
|
||||
|
||||
@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 epg_dat_path(self):
|
||||
return self._cp_settings.get("epg_dat_path", self.get_default("epg_dat_path"))
|
||||
|
||||
@epg_dat_path.setter
|
||||
def epg_dat_path(self, value):
|
||||
self._cp_settings["epg_dat_path"] = value
|
||||
|
||||
@property
|
||||
def picons_path(self):
|
||||
return self._cp_settings.get("picons_path", self.get_default("picons_path"))
|
||||
|
||||
@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"] = Settings.normalize_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"] = Settings.normalize_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"] = Settings.normalize_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 recordings_path(self):
|
||||
return self._settings.get("recordings_path", Defaults.RECORDINGS_PATH.value)
|
||||
|
||||
@recordings_path.setter
|
||||
def recordings_path(self, value):
|
||||
self._settings["recordings_path"] = Settings.normalize_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
|
||||
|
||||
@property
|
||||
def epg_source(self):
|
||||
return EpgSource(self._cp_settings.get("epg_source", EpgSource.HTTP))
|
||||
|
||||
@epg_source.setter
|
||||
def epg_source(self, value):
|
||||
self._cp_settings["epg_source"] = value
|
||||
|
||||
@property
|
||||
def epg_update_interval(self):
|
||||
return self._cp_settings.get("epg_update_interval", 5)
|
||||
|
||||
@epg_update_interval.setter
|
||||
def epg_update_interval(self, value):
|
||||
self._cp_settings["epg_update_interval"] = value
|
||||
|
||||
@property
|
||||
def epg_xml_source(self):
|
||||
return self._cp_settings.get("epg_xml_source", "")
|
||||
|
||||
@epg_xml_source.setter
|
||||
def epg_xml_source(self, value):
|
||||
self._cp_settings["epg_xml_source"] = value
|
||||
|
||||
# *********** FTP ************ #
|
||||
|
||||
@property
|
||||
def ftp_bookmarks(self):
|
||||
return self._cp_settings.get("ftp_bookmarks", [])
|
||||
|
||||
@ftp_bookmarks.setter
|
||||
def ftp_bookmarks(self, value):
|
||||
self._cp_settings["ftp_bookmarks"] = value
|
||||
|
||||
# ***** Program settings ***** #
|
||||
|
||||
@property
|
||||
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 unlimited_copy_buffer(self):
|
||||
return self._settings.get("unlimited_copy_buffer", Defaults.UNLIMITED_COPY_BUFFER.value)
|
||||
|
||||
@unlimited_copy_buffer.setter
|
||||
def unlimited_copy_buffer(self, value):
|
||||
self._settings["unlimited_copy_buffer"] = value
|
||||
|
||||
@property
|
||||
def extensions_support(self):
|
||||
return self._settings.get("extensions_support", Defaults.EXTENSIONS_SUPPORT.value)
|
||||
|
||||
@extensions_support.setter
|
||||
def extensions_support(self, value):
|
||||
self._settings["extensions_support"] = value
|
||||
|
||||
@property
|
||||
def force_bq_names(self):
|
||||
return self._settings.get("force_bq_names", Defaults.FORCE_BQ_NAMES.value)
|
||||
|
||||
@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 use_header_bar(self):
|
||||
return self._settings.get("use_header_bar", USE_HEADER_BAR)
|
||||
|
||||
@use_header_bar.setter
|
||||
def use_header_bar(self, value):
|
||||
self._settings["use_header_bar"] = value
|
||||
|
||||
@property
|
||||
def list_font(self):
|
||||
return self._settings.get("list_font", "")
|
||||
|
||||
@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 display_epg(self):
|
||||
return self._settings.get("display_epg", False)
|
||||
|
||||
@display_epg.setter
|
||||
def display_epg(self, value):
|
||||
self._settings["display_epg"] = value
|
||||
|
||||
@property
|
||||
def alternate_layout(self):
|
||||
return self._settings.get("alternate_layout", IS_DARWIN)
|
||||
|
||||
@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
|
||||
|
||||
# ************* Download ************** #
|
||||
|
||||
@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
|
||||
|
||||
@property
|
||||
def keep_power_mode(self):
|
||||
return self._settings.get("keep_power_mode", False)
|
||||
|
||||
@keep_power_mode.setter
|
||||
def keep_power_mode(self, value):
|
||||
self._settings["keep_power_mode"] = value
|
||||
|
||||
@property
|
||||
def compress_picons(self):
|
||||
return self._settings.get("compress_picons", False)
|
||||
|
||||
@compress_picons.setter
|
||||
def compress_picons(self, value):
|
||||
self._settings["compress_picons"] = value
|
||||
|
||||
# **************** Debug **************** #
|
||||
|
||||
@property
|
||||
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(config_file=CONFIG_FILE, default_settings=None):
|
||||
if not os.path.isfile(config_file) or os.stat(config_file).st_size == 0:
|
||||
df = Settings.get_default_settings() if default_settings is None else default_settings
|
||||
Settings.write_settings(df, config_file=config_file)
|
||||
|
||||
with open(config_file, "r", encoding="utf-8") as cf:
|
||||
try:
|
||||
return json.load(cf)
|
||||
except ValueError as e:
|
||||
raise SettingsReadException(e)
|
||||
|
||||
@staticmethod
|
||||
def get_default_settings(profile_name="default"):
|
||||
def_settings = SettingsType.ENIGMA_2.get_default_settings()
|
||||
|
||||
return {
|
||||
"version": Settings.__VERSION,
|
||||
"default_profile": Defaults.DEFAULT_PROFILE.value,
|
||||
"profiles": {profile_name: def_settings},
|
||||
"v5_support": Defaults.V5_SUPPORT.value,
|
||||
"http_api_support": Defaults.HTTP_API_SUPPORT.value,
|
||||
"enable_yt_dl": Defaults.ENABLE_YT_DL.value,
|
||||
"enable_send_to": Defaults.ENABLE_SEND_TO.value,
|
||||
"use_colors": Defaults.USE_COLORS.value,
|
||||
"new_color": Defaults.NEW_COLOR.value,
|
||||
"extra_color": Defaults.EXTRA_COLOR.value,
|
||||
"fav_click_mode": Defaults.FAV_CLICK_MODE.value,
|
||||
"profile_folder_is_default": Defaults.PROFILE_FOLDER_DEFAULT.value,
|
||||
"records_path": Defaults.RECORDINGS_PATH.value
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_default_transcoding_presets():
|
||||
return {"720p TV/device": {"vcodec": "h264", "vb": "1500", "width": "1280", "height": "720", "acodec": "mp3",
|
||||
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"},
|
||||
"1080p TV/device": {"vcodec": "h264", "vb": "3500", "width": "1920", "height": "1080", "acodec": "mp3",
|
||||
"ab": "192", "channels": "2", "samplerate": "44100", "scodec": "none"}}
|
||||
|
||||
@staticmethod
|
||||
def write_settings(config, config_path=CONFIG_PATH, config_file=CONFIG_FILE):
|
||||
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
||||
with open(config_file, "w", encoding="utf-8") as cf:
|
||||
json.dump(config, cf, indent=" ")
|
||||
|
||||
@staticmethod
|
||||
def normalize_path(path):
|
||||
return f"{os.path.normpath(path)}{SEP}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
441
app/tools/epg.py
Normal file
441
app/tools/epg.py
Normal file
@@ -0,0 +1,441 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Module for working with epg.dat file. """
|
||||
import abc
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import struct
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from collections import namedtuple
|
||||
from datetime import datetime, timezone
|
||||
from tempfile import NamedTemporaryFile
|
||||
from urllib.parse import urlparse
|
||||
from xml.dom.minidom import parse, Node, Document
|
||||
|
||||
import requests
|
||||
|
||||
from app.commons import log
|
||||
from app.eparser.ecommons import BqServiceType, BouquetService
|
||||
from app.settings import IS_WIN
|
||||
|
||||
ENCODING = "utf-8"
|
||||
DETECT_ENCODING = False
|
||||
try:
|
||||
import chardet
|
||||
except ModuleNotFoundError:
|
||||
pass
|
||||
else:
|
||||
DETECT_ENCODING = True
|
||||
|
||||
EpgEvent = namedtuple("EpgEvent", ["service_name", "title", "start", "end", "length", "desc", "event_data"])
|
||||
EpgEvent.__new__.__defaults__ = ("N/A", "N/A", 0, 0, 0, "N/A", None) # For Python3 < 3.7
|
||||
|
||||
|
||||
class Reader(metaclass=abc.ABCMeta):
|
||||
|
||||
@abc.abstractmethod
|
||||
def download(self, clb=None): pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_current_events(self, ids: set) -> dict: pass
|
||||
|
||||
|
||||
class EPG:
|
||||
""" Base EPG class. """
|
||||
# DVB/EPG count days with a 'modified Julian calendar' where day 1 is 17 November 1858.
|
||||
# datetime.datetime.toordinal(1858,11,17) => 678576
|
||||
ZERO_DAY = 678576
|
||||
|
||||
Event = namedtuple("EpgEvent", ["id", "data", "start", "duration", "title", "desc", "ext_desc"])
|
||||
|
||||
class EventData:
|
||||
""" Event data representation class. """
|
||||
__slots__ = ["raw_data", "crc", "size", "type"]
|
||||
|
||||
def __init__(self, size=0, e_type=0):
|
||||
self.raw_data = None
|
||||
self.crc = None
|
||||
self.size = size
|
||||
self.type = e_type
|
||||
|
||||
def get_event_id(self):
|
||||
return self.raw_data[0] << 8 | self.raw_data[1]
|
||||
|
||||
def get_start_time(self):
|
||||
""" Returns start time [sec.]. """
|
||||
# Date
|
||||
start_date = datetime.fromordinal((self.raw_data[2] << 8 | self.raw_data[3]) + EPG.ZERO_DAY).timestamp()
|
||||
# Time
|
||||
tm_hour = EPG.get_from_bcd(self.raw_data[4])
|
||||
tm_min = EPG.get_from_bcd(self.raw_data[5])
|
||||
tm_sec = EPG.get_from_bcd(self.raw_data[6])
|
||||
# UTC.
|
||||
s_time = start_date + tm_hour * 3600 + tm_min * 60 + tm_sec
|
||||
# Time zone correction.
|
||||
s_time += datetime.now(timezone.utc).astimezone().utcoffset().seconds
|
||||
|
||||
return s_time
|
||||
|
||||
def get_duration(self):
|
||||
""" Returns duration [sec.]."""
|
||||
return EPG.get_from_bcd(self.raw_data[7]) * 3600 + EPG.get_from_bcd(
|
||||
self.raw_data[8]) * 60 + EPG.get_from_bcd(self.raw_data[9])
|
||||
|
||||
class DatReader(Reader):
|
||||
""" The epd.dat file reading class.
|
||||
|
||||
The read algorithm was taken from the eEPGCache::load() function from this source:
|
||||
https://github.com/OpenPLi/enigma2/blob/44d9b92f5260c7de1b3b3a1b9a9cbe0f70ca4bf0/lib/dvb/epgcache.cpp#L1300
|
||||
"""
|
||||
|
||||
def __init__(self, path):
|
||||
self._path = path
|
||||
self._refs = {}
|
||||
self._desc = {}
|
||||
|
||||
def download(self, clb=None):
|
||||
pass
|
||||
|
||||
def get_current_events(self, ids: set) -> dict:
|
||||
pass
|
||||
|
||||
def get_refs(self):
|
||||
return self._refs.keys()
|
||||
|
||||
def get_services(self):
|
||||
return self._refs
|
||||
|
||||
def get_event(self, evd):
|
||||
title, desc, ext_desc = None, None, None
|
||||
e_id, start, duration = evd.get_event_id(), evd.get_start_time(), evd.get_duration()
|
||||
|
||||
for c in evd.crc:
|
||||
data = self._desc.get(c, None)
|
||||
if not data:
|
||||
continue
|
||||
|
||||
encoding = ENCODING
|
||||
if DETECT_ENCODING:
|
||||
# May be slow.
|
||||
encoding = chardet.detect(data).get("encoding", "utf-8") or encoding
|
||||
|
||||
desc_type = data[0]
|
||||
if desc_type == 77: # Short event descriptor -> 0x4d -> 77
|
||||
size = data[6]
|
||||
txt = data[7:-1].decode(encoding, errors="ignore")
|
||||
t_len = len(txt)
|
||||
st = 0
|
||||
|
||||
if size and size < t_len:
|
||||
st = abs(size - t_len)
|
||||
|
||||
if size < 32:
|
||||
title = txt
|
||||
else:
|
||||
desc = txt[st:]
|
||||
elif desc_type == 78: # Extended event descriptor -> 0x4e -> 78
|
||||
ext_desc = data[9:].decode(encoding, errors="ignore") if data[7] and data[8] < 32 else None
|
||||
|
||||
return EPG.Event(e_id, evd, start, duration, title, desc, ext_desc)
|
||||
|
||||
def get_events(self, ref):
|
||||
return self._refs.get(ref, {})
|
||||
|
||||
def read(self):
|
||||
with open(self._path, mode="rb") as f:
|
||||
crc = struct.unpack("I", f.read(4))[0]
|
||||
if crc != int(0x98765432):
|
||||
raise ValueError("Epg file has incorrect byte order!")
|
||||
|
||||
header = f.read(13).decode()
|
||||
if header == "ENIGMA_EPG_V7":
|
||||
epg_ver = 7
|
||||
elif header == "ENIGMA_EPG_V8":
|
||||
epg_ver = 8
|
||||
else:
|
||||
raise ValueError("Unsupported format of epd.dat file!")
|
||||
|
||||
channels_count = struct.unpack("I", f.read(4))[0]
|
||||
_len_read_size = 3 if epg_ver == 8 else 2
|
||||
_type_read_str = f"{'H' if epg_ver == 8 else 'B'}B"
|
||||
|
||||
for i in range(channels_count):
|
||||
sid, nid, tsid, events_size = struct.unpack("IIII", f.read(16))
|
||||
service_id = f"{sid:X}:{tsid:X}:{nid:X}"
|
||||
events = {}
|
||||
|
||||
for j in range(events_size):
|
||||
_type, _len = struct.unpack(_type_read_str, f.read(_len_read_size))
|
||||
event = EPG.EventData(size=_len, e_type=_type)
|
||||
event.raw_data = f.read(10)
|
||||
|
||||
n_crc = (_len - 10) // 4
|
||||
if n_crc > 0:
|
||||
event.crc = [struct.unpack("I", f.read(4))[0] for n in range(n_crc)]
|
||||
events[event.get_event_id()] = event
|
||||
|
||||
self._refs[service_id] = events
|
||||
|
||||
for i in range(struct.unpack("I", f.read(4))[0]):
|
||||
_id, ref_count = struct.unpack("II", f.read(8))
|
||||
header = struct.unpack("BB", f.read(2))
|
||||
_bytes = header[1] + 2
|
||||
f.seek(-2, os.SEEK_CUR)
|
||||
self._desc[_id] = f.read(_bytes)
|
||||
|
||||
@staticmethod
|
||||
def get_from_bcd(value: int):
|
||||
""" Converts a BCD to an integer. """
|
||||
if ((value & 0xF0) >= 0xA0) or ((value & 0xF) >= 0xA):
|
||||
return -1
|
||||
return ((value & 0xF0) >> 4) * 10 + (value & 0xF)
|
||||
|
||||
|
||||
class XmlTvReader(Reader):
|
||||
PR_TAG = "programme"
|
||||
CH_TAG = "channel"
|
||||
DSP_NAME_TAG = "display-name"
|
||||
ICON_TAG = "icon"
|
||||
TITLE_TAG = "title"
|
||||
DESC_TAG = "desc"
|
||||
|
||||
TIME_FORMAT_STR = "%Y%m%d%H%M%S %z"
|
||||
|
||||
Service = namedtuple("Service", ["id", "names", "logo", "events"])
|
||||
Event = namedtuple("EpgEvent", ["start", "duration", "title", "desc"])
|
||||
|
||||
def __init__(self, path, url):
|
||||
self._path = path
|
||||
self._url = url
|
||||
self._ids = {}
|
||||
|
||||
def download(self, clb=None):
|
||||
""" Downloads an XMLTV file. """
|
||||
res = urlparse(self._url)
|
||||
if not all((res.scheme, res.netloc)):
|
||||
log(f"{self.__class__.__name__} [download] error: Invalid URL {self._url}")
|
||||
return
|
||||
|
||||
with requests.get(url=self._url, stream=True) as request:
|
||||
if request.reason == "OK":
|
||||
suf = self._url[self._url.rfind("."):]
|
||||
if suf not in (".gz", ".xz", ".lzma"):
|
||||
log(f"{self.__class__.__name__} [download] error: Unsupported file extension.")
|
||||
return
|
||||
|
||||
data_len = request.headers.get("content-length")
|
||||
|
||||
with NamedTemporaryFile(suffix=suf, delete=not IS_WIN) as tf:
|
||||
downloaded = 0
|
||||
data_len = int(data_len)
|
||||
log("Downloading XMLTV file...")
|
||||
for data in request.iter_content(chunk_size=1024):
|
||||
downloaded += len(data)
|
||||
tf.write(data)
|
||||
done = int(50 * downloaded / data_len)
|
||||
sys.stdout.write(f"\rDownloading XMLTV file [{'=' * done}{' ' * (50 - done)}]")
|
||||
sys.stdout.flush()
|
||||
tf.seek(0)
|
||||
sys.stdout.write("\n")
|
||||
|
||||
os.makedirs(os.path.dirname(self._path), exist_ok=True)
|
||||
|
||||
if suf.endswith(".gz"):
|
||||
try:
|
||||
shutil.copyfile(tf.name, self._path)
|
||||
except OSError as e:
|
||||
log(f"{self.__class__.__name__} [download *.gz] error: {e}")
|
||||
elif self._url.endswith((".xz", ".lzma")):
|
||||
import lzma
|
||||
|
||||
try:
|
||||
with lzma.open(tf, "rb") as lzf:
|
||||
shutil.copyfileobj(lzf, self._path)
|
||||
except (lzma.LZMAError, OSError) as e:
|
||||
log(f"{self.__class__.__name__} [download *.xz] error: {e}")
|
||||
|
||||
if IS_WIN and os.path.isfile(tf.name):
|
||||
tf.close()
|
||||
os.remove(tf.name)
|
||||
else:
|
||||
log(f"{self.__class__.__name__} [download] error: {request.reason}")
|
||||
|
||||
if clb:
|
||||
clb()
|
||||
|
||||
def get_current_events(self, names: set) -> dict:
|
||||
events = {}
|
||||
|
||||
dt = datetime.utcnow()
|
||||
utc = dt.timestamp()
|
||||
offset = datetime.now() - dt
|
||||
|
||||
for srv in filter(lambda s: any(name in names for name in s.names), self._ids.values()):
|
||||
ev = max(filter(lambda s: s.start < utc, srv.events), key=lambda x: x.start, default=None)
|
||||
if ev:
|
||||
start = datetime.fromtimestamp(ev.start) + offset
|
||||
end_time = datetime.fromtimestamp(ev.duration) + offset
|
||||
start = start.timestamp()
|
||||
end_time = end_time.timestamp()
|
||||
|
||||
for n in srv.names:
|
||||
events[n] = EpgEvent(n, ev.title, start, end_time, int(ev.duration), ev.desc, ev)
|
||||
|
||||
return events
|
||||
|
||||
def parse(self):
|
||||
""" Parses XML. """
|
||||
try:
|
||||
import gzip
|
||||
|
||||
with gzip.open(self._path, "rb") as gzf:
|
||||
log("Processing XMLTV data...")
|
||||
list(map(self.process_node, ET.iterparse(gzf)))
|
||||
log("XMLTV data parsing is complete.")
|
||||
except OSError as e:
|
||||
log(f"{self.__class__.__name__} [parse] error: {e}")
|
||||
|
||||
def process_node(self, node):
|
||||
event, element = node
|
||||
if element.tag == self.CH_TAG:
|
||||
ch_id = element.get("id", None)
|
||||
logo = None # Currently not in use.
|
||||
# Since a service can have several names, we will store a set of names in the "names" field!
|
||||
self._ids[ch_id] = self.Service(ch_id, {c.text for c in element if c.tag == self.DSP_NAME_TAG}, logo, [])
|
||||
elif element.tag == self.PR_TAG:
|
||||
channel = self._ids.get(element.get(self.CH_TAG, None), None)
|
||||
if channel:
|
||||
events = channel[-1]
|
||||
start = element.get("start", None)
|
||||
if start:
|
||||
start = self.get_utc_time(start)
|
||||
|
||||
stop = element.get("stop", None)
|
||||
if stop:
|
||||
stop = self.get_utc_time(stop)
|
||||
|
||||
title, desc = None, None
|
||||
for c in element:
|
||||
if c.tag == self.TITLE_TAG:
|
||||
title = c.text
|
||||
elif c.tag == self.DESC_TAG:
|
||||
desc = c.text
|
||||
|
||||
if all((start, stop, title)):
|
||||
events.append(self.Event(start, stop, title, desc))
|
||||
|
||||
def to_epg_dat(self):
|
||||
""" Converts and saves imported data to 'epg.dat' file. """
|
||||
raise ValueError("Not implemented yet!")
|
||||
|
||||
@staticmethod
|
||||
def get_utc_time(time_str):
|
||||
""" Returns the UTC time in seconds. """
|
||||
t, sep, delta = time_str.partition(" ")
|
||||
t = datetime(*map(int, (t[:4], t[4:6], t[6:8], t[8:10], t[10:12], t[12:]))).timestamp()
|
||||
if delta:
|
||||
t -= (3600 * int(delta) // 100)
|
||||
return t
|
||||
|
||||
|
||||
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)
|
||||
pos_pat = re.compile(r"^\d+\.\d+[EW]$")
|
||||
|
||||
for elem in dom.getElementsByTagName("channels"):
|
||||
c_count = 0
|
||||
comment_count = 0
|
||||
data = ""
|
||||
ch_id = None
|
||||
pos = None
|
||||
ch_type = BqServiceType.DEFAULT
|
||||
|
||||
if elem.hasChildNodes():
|
||||
for n in elem.childNodes:
|
||||
if n.nodeType == Node.ELEMENT_NODE:
|
||||
ch_id = n.getAttribute("id")
|
||||
|
||||
if n.nodeType == Node.COMMENT_NODE:
|
||||
c_count += 1
|
||||
comment_count += 1
|
||||
txt = n.data.strip()
|
||||
|
||||
if re.match(pos_pat, txt):
|
||||
pos = txt
|
||||
|
||||
if comment_count:
|
||||
comment_count -= 1
|
||||
else:
|
||||
refs.append(BouquetService(name=txt, type=ch_type, data=data.upper(), num=(pos, ch_id)))
|
||||
|
||||
if n.hasChildNodes():
|
||||
for s_node in n.childNodes:
|
||||
if s_node.nodeType == Node.TEXT_NODE:
|
||||
comment_count -= 1
|
||||
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
|
||||
@@ -1,96 +1,575 @@
|
||||
from app.commons import run_idle
|
||||
from app.tools import vlc
|
||||
from app.ui.uicommons import Gtk, Gdk
|
||||
|
||||
MRL = "url"
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
class Player:
|
||||
_VLC_INSTANCE = None
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
def __init__(self, url):
|
||||
handlers = {"on_play": self.on_play,
|
||||
"on_stop": self.on_stop,
|
||||
"on_drawing_area_realize": self.on_drawing_area_realize,
|
||||
"on_press": self.on_press,
|
||||
"on_key_release": self.on_key_release,
|
||||
"on_state_changed": self.on_state_changed,
|
||||
"on_close_window": self.on_close_window}
|
||||
from gi.repository import Gdk, Gtk, GObject
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.add_objects_from_file("player.glade", ("player_main_window",))
|
||||
builder.connect_signals(handlers)
|
||||
self._main_window = builder.get_object("player_main_window")
|
||||
self._main_box = builder.get_object("main_box")
|
||||
self._buttonbox = builder.get_object("buttonbox")
|
||||
self._frame = builder.get_object("")
|
||||
self._drawing_area = builder.get_object("drawing_area")
|
||||
self._drawing_area.set_events(Gdk.ModifierType.BUTTON1_MASK)
|
||||
self._player = Player.get_vlc_instance().media_player_new()
|
||||
self._is_played = False
|
||||
self._url = url
|
||||
self._full_screen = False
|
||||
from app.commons import run_task, log, 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)
|
||||
parent.connect("pause", self.on_pause)
|
||||
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_pause(self, widget, state):
|
||||
self.pause()
|
||||
|
||||
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 get_vlc_instance():
|
||||
if Player._VLC_INSTANCE:
|
||||
return Player._VLC_INSTANCE
|
||||
_VLC_INSTANCE = vlc.Instance("--no-xlib")
|
||||
return _VLC_INSTANCE
|
||||
def make(name, mode, widget):
|
||||
""" Factory method. We will not use a separate factory to return a specific implementation.
|
||||
|
||||
def on_play(self, item):
|
||||
if not self._is_played:
|
||||
self._player.play()
|
||||
self._is_played = True
|
||||
@param name: implementation name.
|
||||
@param mode: current player mode [Built-in or windowed].
|
||||
@param widget: parent of video widget.
|
||||
|
||||
def on_stop(self, item):
|
||||
if self._is_played:
|
||||
self._player.stop()
|
||||
self._is_played = False
|
||||
|
||||
def on_press(self, area, event: Gdk.EventButton):
|
||||
if event.button == Gdk.BUTTON_PRIMARY and event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
|
||||
self.change_state()
|
||||
|
||||
def on_state_changed(self, window, event):
|
||||
if event.new_window_state & Gdk.WindowState.FULLSCREEN:
|
||||
if self._main_box in window:
|
||||
window.remove(self._main_box)
|
||||
self._drawing_area.reparent(self._main_window)
|
||||
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:
|
||||
if self._drawing_area in self._main_window:
|
||||
window.remove(self._drawing_area)
|
||||
window.add(self._main_box)
|
||||
self._main_box.pack_start(self._drawing_area, True, True, 0)
|
||||
self._main_box.reorder_child(self._drawing_area, 0)
|
||||
raise NameError(f"There is no such [{name}] implementation.")
|
||||
|
||||
def change_state(self):
|
||||
self._full_screen = not self._full_screen
|
||||
self._main_window.fullscreen() if self._full_screen else self._main_window.unfullscreen()
|
||||
|
||||
def on_key_release(self, area, key):
|
||||
if key.keyval in (Gdk.KEY_F, Gdk.KEY_f):
|
||||
self.change_state()
|
||||
class MpvPlayer(Player):
|
||||
""" Simple wrapper for MPV media player.
|
||||
|
||||
def on_drawing_area_realize(self, widget):
|
||||
win_id = widget.get_window().get_xid()
|
||||
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):
|
||||
self._player.pause = not self._player.pause
|
||||
|
||||
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):
|
||||
state = self._player.get_state(self.STATE.NULL).state
|
||||
if state == self.STATE.PLAYING:
|
||||
self._player.set_state(self.STATE.PAUSED)
|
||||
elif state == self.STATE.PAUSED:
|
||||
self._player.set_state(self.STATE.PLAYING)
|
||||
|
||||
def set_time(self, time):
|
||||
pass
|
||||
|
||||
@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_played = True
|
||||
self._player.set_xwindow(win_id)
|
||||
self._player.set_mrl(self._url)
|
||||
self._player.play()
|
||||
|
||||
@run_idle
|
||||
def on_close_window(self, *args):
|
||||
if self._player:
|
||||
self.on_stop(None)
|
||||
self._is_playing = False
|
||||
self._player.stop()
|
||||
self._player.release()
|
||||
Gtk.main_quit()
|
||||
self.__VLC_INSTANCE = None
|
||||
|
||||
def show(self):
|
||||
self._main_window.show()
|
||||
Gtk.main()
|
||||
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.recordings_path
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
d_now = datetime.now().strftime(LOG_DATE_FORMAT)
|
||||
d_now = d_now.replace(" ", "_").replace(":", "-") if IS_WIN else d_now.replace(" ", "_")
|
||||
path = f"{path}{name.replace(' ', '_')}_{d_now}"
|
||||
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__":
|
||||
Player(MRL).show()
|
||||
pass
|
||||
|
||||
1923
app/tools/mpv.py
Normal file
1923
app/tools/mpv.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,30 +1,252 @@
|
||||
# -*- 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
|
||||
|
||||
from app.commons import log, run_task
|
||||
from app.properties import Profile
|
||||
import requests
|
||||
|
||||
_ENIGMA2_PICON_KEY = "{:X}:{:X}:{:X}0000"
|
||||
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", "selected"])
|
||||
Picon = namedtuple("Picon", ["ref", "ssid", "v_pid"])
|
||||
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/3.0.0", "Referer": ""}
|
||||
_LINK_PATTERN = re.compile(r"((.*)-\d+x\d+)-(.*)_by_chocholousek.7z$")
|
||||
_FILE_PATTERN = re.compile(b"\\s+(\\w+\\.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}")
|
||||
with open(dest, mode="bw") as f:
|
||||
for data in request.iter_content(chunk_size=1024):
|
||||
f.write(data)
|
||||
self._appender(f"Extracting: {provider.on_id}")
|
||||
self.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",
|
||||
"piconSNPtransparent": "t50"
|
||||
}
|
||||
|
||||
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=' '):
|
||||
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 = []
|
||||
@@ -32,9 +254,9 @@ class PiconsParser(HTMLParser):
|
||||
self.picons = []
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == 'td':
|
||||
if tag == "td":
|
||||
self._is_td = True
|
||||
if tag == 'th':
|
||||
if tag == "th":
|
||||
self._is_th = True
|
||||
if tag == "img":
|
||||
self._current_row.append(attrs[0][1])
|
||||
@@ -45,28 +267,29 @@ class PiconsParser(HTMLParser):
|
||||
self._current_cell.append(data.strip())
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == 'td':
|
||||
if tag == "td":
|
||||
self._is_td = False
|
||||
elif tag == 'th':
|
||||
elif tag == "th":
|
||||
self._is_th = False
|
||||
|
||||
if tag in ('td', 'th'):
|
||||
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':
|
||||
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]))
|
||||
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 = []
|
||||
|
||||
@@ -74,29 +297,54 @@ class PiconsParser(HTMLParser):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def parse(open_path, picons_path, tmp_path, on_id, pos, picon_ids, 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:
|
||||
name = PiconsParser.format(p.ssid, on_id, p.v_pid, pos, picon_ids, profile)
|
||||
p_name = picons_path + (name if name else os.path.basename(p.ref))
|
||||
shutil.copyfile(tmp_path + "www.lyngsat.com/" + p.ref.lstrip("."), p_name)
|
||||
except (TypeError, ValueError) as e:
|
||||
log("Picons format parse error: {}".format(p) + "\n" + str(e))
|
||||
print(e)
|
||||
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, v_pid, pos, picon_ids, profile: Profile):
|
||||
tr_id = int(ssid[:-2] if len(ssid) < 4 else ssid[:2])
|
||||
if profile is Profile.ENIGMA_2:
|
||||
return picon_ids.get(_ENIGMA2_PICON_KEY.format(int(ssid), int(on_id), int(pos)), None)
|
||||
elif profile is Profile.NEUTRINO_MP:
|
||||
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)
|
||||
@@ -106,22 +354,30 @@ 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._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_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':
|
||||
@@ -129,11 +385,12 @@ class ProviderParser(HTMLParser):
|
||||
if tag == 'tr':
|
||||
self._is_th = True
|
||||
if tag == "img":
|
||||
if attrs[0][1].startswith("logo/"):
|
||||
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])
|
||||
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 """
|
||||
@@ -158,13 +415,48 @@ class ProviderParser(HTMLParser):
|
||||
if pos:
|
||||
self._positon = "".join(c for c in str(pos) if c.isdigit() or c in ".EW")
|
||||
|
||||
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)
|
||||
row[0] = self._positon
|
||||
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):
|
||||
@@ -174,31 +466,60 @@ class ProviderParser(HTMLParser):
|
||||
super().reset()
|
||||
|
||||
|
||||
def parse_providers(open_path):
|
||||
def parse_providers(url):
|
||||
""" Returns a list of providers sorted by logo [single channels after providers]. """
|
||||
parser = ProviderParser()
|
||||
parser.reset()
|
||||
|
||||
with open(open_path, encoding="utf-8", errors="replace") as f:
|
||||
parser.feed(f.read())
|
||||
rows = parser.rows
|
||||
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))
|
||||
|
||||
if rows:
|
||||
return [Provider(logo=r[2], name=r[5], pos=r[0], url=r[6], on_id=r[-2], selected=True) for r in rows]
|
||||
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):
|
||||
""" Downloads and saves the picon to file. """
|
||||
err_msg = "Picon download error: {} [{}]"
|
||||
timeout = (3, 5) # connect and read timeouts
|
||||
log("Downloading: {}.".format(os.path.basename(dest_path)))
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
|
||||
@run_task
|
||||
def convert_to(src_path, dest_path, profile, callback, done_callback):
|
||||
def convert_to(src_path, dest_path, s_type, done_callback):
|
||||
""" Converts names format of picons.
|
||||
|
||||
Copies resulting files from src to dest and writes state to callback.
|
||||
"""
|
||||
pattern = "/*_0_0_0.png" if profile is Profile.ENIGMA_2 else "/*.png"
|
||||
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))
|
||||
log('Converting "{}" to "{}"'.format(base_name, dest_file))
|
||||
shutil.copyfile(file, dest)
|
||||
|
||||
done_callback()
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.18.3 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.12"/>
|
||||
<object class="GtkApplicationWindow" id="player_main_window">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">Player</property>
|
||||
<property name="icon_name">vlc</property>
|
||||
<signal name="delete-event" handler="on_close_window" swapped="no"/>
|
||||
<signal name="window-state-event" handler="on_state_changed" swapped="no"/>
|
||||
<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="GtkDrawingArea" id="drawing_area">
|
||||
<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="has_focus">True</property>
|
||||
<signal name="button-press-event" handler="on_press" swapped="no"/>
|
||||
<signal name="key-release-event" handler="on_key_release" swapped="no"/>
|
||||
<signal name="realize" handler="on_drawing_area_realize" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="padding">2</property>
|
||||
<property name="position">2</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">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="buttonbox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">3</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="spacing">2</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="layout_style">start</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="play_button">
|
||||
<property name="label">gtk-media-play</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_play" 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="stop_button">
|
||||
<property name="label">gtk-media-stop</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_stop" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="close_button">
|
||||
<property name="label">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_window" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">2</property>
|
||||
<property name="secondary">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="padding">5</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
@@ -1,28 +1,112 @@
|
||||
""" Module for download satellites from internet ("flysat.com")
|
||||
for replace or update current satellites.xml file.
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Module for 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
|
||||
import requests
|
||||
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/satlist.php",)
|
||||
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. """
|
||||
|
||||
_HEADERS = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:45.0) Gecko/20100101 Firefox/59.02"}
|
||||
POS_PAT = re.compile(r".*?(\d+\.\d°?[EW]).*")
|
||||
|
||||
def __init__(self, source=SatelliteSource.FLYSAT, entities=False, separator=' '):
|
||||
|
||||
@@ -38,13 +122,21 @@ class SatellitesParser(HTMLParser):
|
||||
self._rows = []
|
||||
self._source = source
|
||||
|
||||
self.PLS_MODES = {v: k for k, v in PLS_MODE.items()}
|
||||
self.POLARIZATION = {v: k for k, v in POLARIZATION.items()}
|
||||
self.FEC = {v: k for k, v in FEC.items()}
|
||||
self.SYSTEM = {v: k for k, v in SYSTEM.items()}
|
||||
self.MODULATION = {v: k for k, v in MODULATION.items()}
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == 'td':
|
||||
if tag == "td":
|
||||
self._is_td = True
|
||||
if tag == 'tr':
|
||||
if tag == "tr":
|
||||
self._is_th = True
|
||||
if tag == "a":
|
||||
self._current_row.append(attrs[0][1])
|
||||
for atr in attrs:
|
||||
if atr[0] == "href":
|
||||
self._current_row.append(atr[1])
|
||||
|
||||
def handle_data(self, data):
|
||||
""" Save content to a cell """
|
||||
@@ -52,16 +144,16 @@ class SatellitesParser(HTMLParser):
|
||||
self._current_cell.append(data.strip())
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == 'td':
|
||||
if tag == "td":
|
||||
self._is_td = False
|
||||
elif tag == 'tr':
|
||||
elif tag == "tr":
|
||||
self._is_th = False
|
||||
|
||||
if tag in ('td', 'th'):
|
||||
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':
|
||||
elif tag == "tr":
|
||||
row = self._current_row
|
||||
self._rows.append(row)
|
||||
self._current_row = []
|
||||
@@ -76,111 +168,621 @@ class SatellitesParser(HTMLParser):
|
||||
self._source = source
|
||||
|
||||
for src in SatelliteSource.get_sources(self._source):
|
||||
request = requests.get(url=src, headers=self._HEADERS)
|
||||
reason = request.reason
|
||||
if reason == "OK":
|
||||
self.feed(request.text)
|
||||
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:
|
||||
print(reason)
|
||||
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:
|
||||
def get_sat(r):
|
||||
return r[1], self.parse_position(r[2]), r[3], r[0], False
|
||||
|
||||
return list(map(get_sat, filter(lambda x: all(x) and len(x) == 5, self._rows)))
|
||||
return self.get_satellites_for_fly_sat()
|
||||
elif self._source is SatelliteSource.LYNGSAT:
|
||||
rows = filter(lambda x: len(x) in (5, 7), self._rows)
|
||||
sats = []
|
||||
current_pos = "0"
|
||||
for row in rows:
|
||||
r_len = len(row)
|
||||
if r_len == 7:
|
||||
current_pos = self.parse_position(row[2])
|
||||
sats.append((row[4], current_pos, row[5], row[1], False))
|
||||
elif r_len == 5:
|
||||
sats.append((row[2], current_pos, row[3], row[1], False))
|
||||
return sats
|
||||
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=sat[0] + " ({})".format(pos),
|
||||
flags="0",
|
||||
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 "{}{}".format("-" if pos[-1] == "W" else "", pos[:-1])
|
||||
return f"{'-' if pos[-1] == 'W' else ''}{pos[:-1]}"
|
||||
|
||||
def get_transponders(self, sat_url):
|
||||
""" Getting transponders(sorted by frequency). """
|
||||
self._rows.clear()
|
||||
url = "https://www.flysat.com/" + sat_url if self._source is SatelliteSource.FLYSAT else sat_url
|
||||
request = requests.get(url=url, headers=self._HEADERS)
|
||||
reason = request.reason
|
||||
trs = []
|
||||
if reason == "OK":
|
||||
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)
|
||||
return 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 """
|
||||
if self._rows:
|
||||
zeros = "000"
|
||||
for r in self._rows:
|
||||
if len(r) < 3:
|
||||
continue
|
||||
data = r[2].split(" ")
|
||||
if len(data) != 2:
|
||||
continue
|
||||
sr, fec = data
|
||||
data = r[1].split(" ")
|
||||
if len(data) < 3:
|
||||
continue
|
||||
freq, pol, sys = data[0], data[1], data[2]
|
||||
sys = sys.split("/")
|
||||
if len(sys) != 2:
|
||||
continue
|
||||
sys, mod = sys
|
||||
mod = "QPSK" if sys == "DVB-S" else mod
|
||||
""" 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 = []
|
||||
|
||||
tr = Transponder(freq + zeros, sr + zeros, pol, fec, sys, mod, None, None, None)
|
||||
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",
|
||||
self.POLARIZATION.get(pol, None),
|
||||
self.FEC.get(fec, None),
|
||||
self.SYSTEM.get(sys, None),
|
||||
self.MODULATION.get(mod, None),
|
||||
pls_mode, pls_code, None, None)
|
||||
if is_transponder_valid(tr):
|
||||
trs.append(tr)
|
||||
|
||||
def get_transponders_for_lyng_sat(self, trs):
|
||||
""" Parsing transponders for LyngSat """
|
||||
frq_pol_pattern = re.compile("(\d{4,5}).*([RLHV])(.*\d$)")
|
||||
sr_fec_pattern = re.compile("^(\d{4,5})-(\d/\d)(.+PSK)?(.*)?$")
|
||||
sys_pattern = re.compile("(DVB-S[2]?)(.*)?")
|
||||
zeros = "000"
|
||||
for r in filter(lambda x: len(x) > 8, self._rows):
|
||||
freq = re.match(frq_pol_pattern, r[2])
|
||||
if not freq:
|
||||
continue
|
||||
frq, pol = freq.group(1), freq.group(2)
|
||||
sr_fec = re.match(sr_fec_pattern, r[-3])
|
||||
if not sr_fec:
|
||||
continue
|
||||
sr, fec, mod = sr_fec.group(1), sr_fec.group(2), sr_fec.group(3)
|
||||
mod = mod.strip() if mod else "Auto"
|
||||
sys = re.match(sys_pattern, r[-4])
|
||||
if not sys:
|
||||
continue
|
||||
sys = sys.group(1)
|
||||
is_ids.clear()
|
||||
trs.extend(n_trs)
|
||||
|
||||
tr = Transponder(frq + zeros, sr + zeros, pol, fec, sys, mod, None, None, None)
|
||||
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",
|
||||
self.POLARIZATION.get(pol, None),
|
||||
self.FEC.get(fec, None),
|
||||
self.SYSTEM.get(sys, None),
|
||||
self.MODULATION.get(mod, None),
|
||||
pls_mode, pls_code, is_id, None)
|
||||
if is_transponder_valid(tr):
|
||||
trs.append(tr)
|
||||
|
||||
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",
|
||||
self.POLARIZATION.get(pol, None),
|
||||
self.FEC.get(fec, None),
|
||||
self.SYSTEM.get(sys, None),
|
||||
self.MODULATION.get(mod, None),
|
||||
pls_id, pls_code, is_id, None)
|
||||
if is_transponder_valid(tr):
|
||||
trs.append(tr)
|
||||
|
||||
|
||||
class ServicesParser(HTMLParser):
|
||||
""" Services parser for LYNGSAT source. """
|
||||
|
||||
def __init__(self, source=SatelliteSource.LYNGSAT, entities=False, separator=' ', lang=None):
|
||||
|
||||
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._lang = "en"
|
||||
if lang:
|
||||
langs = {"en", "fr", "nl", "de", "se", "no", "pt", "es", "it", "pl",
|
||||
"cz", "gr", "fi", "ar", "tr", "ru", "sc", "ro", "hu", "sq"}
|
||||
lang, _, _ = lang.partition("_")
|
||||
self._lang = lang if lang in langs else self._lang
|
||||
|
||||
self._parse_html_entities = entities
|
||||
self._separator = separator
|
||||
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://{self._lang}.kingofsat.net/{t_cell.url}"
|
||||
t_cell.text = f"{r[2].text} {r[3].text} {r[6].text} {r[8].text}"
|
||||
trs.append(t_cell)
|
||||
return trs
|
||||
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"))
|
||||
reg, grp = r[3].text, r[4].text
|
||||
|
||||
name, pkg, cas, sid, v_pid, a_pid = r[2].text, r[5].text, r[6].text, r[7].text, None, None
|
||||
flags, sid, fav_id, picon_id, data_id = self.get_service_data(s_type, pkg, sid, tid, nid, nsp,
|
||||
v_pid, a_pid, cas, use_pids)
|
||||
services.append(Service(flags, "s", None, name, reg, grp, pkg, _s_type, None, picon_id,
|
||||
sid, str(freq), sr, pol, fec, sys, pos, data_id, fav_id, multi_tr or tr))
|
||||
|
||||
return services
|
||||
|
||||
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) or "0"
|
||||
# For negative (West) positions: 3600 - numeric position value!!!
|
||||
namespace = f"{3600 - abs(pos) if pos < 0 else pos:04x}0000"
|
||||
tr_flag = 1
|
||||
roll_off = 0 # 35% DVB-S2/DVB-S (default)
|
||||
pilot = 2 # Auto
|
||||
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
|
||||
|
||||
8330
app/tools/vlc.py
8330
app/tools/vlc.py
File diff suppressed because it is too large
Load Diff
471
app/tools/yt.py
Normal file
471
app/tools/yt.py
Normal file
@@ -0,0 +1,471 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Module for working with 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 fmts if i.get("itag", -1) in Quality and "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
|
||||
523
app/ui/app_menu.ui
Normal file
523
app/ui/app_menu.ui
Normal file
@@ -0,0 +1,523 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<interface>
|
||||
<menu id="menu_bar">
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Playback</attribute>
|
||||
<attribute name="action">app.hide_media_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Close</attribute>
|
||||
<attribute name="action">app.on_playback_close</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">File</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Import</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Bouquet</attribute>
|
||||
<attribute name="action">app.on_import_bouquet</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Bouquets and services</attribute>
|
||||
<attribute name="action">app.on_import_bouquets</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Import from Web</attribute>
|
||||
<attribute name="action">app.on_import_from_web</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">New empty configuration</attribute>
|
||||
<attribute name="action">app.on_new_configuration</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Open</attribute>
|
||||
<attribute name="action">app.on_data_open</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Extract...</attribute>
|
||||
<attribute name="action">app.on_archive_open</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Save</attribute>
|
||||
<attribute name="action">app.on_data_save</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Save as</attribute>
|
||||
<attribute name="action">app.on_data_save_as</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">FTP-transfer</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Download from the receiver</attribute>
|
||||
<attribute name="action">app.on_receive</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Transfer to receiver</attribute>
|
||||
<attribute name="action">app.on_send</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Settings</attribute>
|
||||
<attribute name="action">app.preferences</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Exit</attribute>
|
||||
<attribute name="action">app.quit</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Edit</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Lock</attribute>
|
||||
<attribute name="action">app.on_locked</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Hide</attribute>
|
||||
<attribute name="action">app.on_hide</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu id="view_menu">
|
||||
<attribute name="label" translatable="yes">View</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Bouquets</attribute>
|
||||
<attribute name="action">app.show_bouquets</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Satellites</attribute>
|
||||
<attribute name="action">app.show_satellites</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Picons</attribute>
|
||||
<attribute name="action">app.show_picons</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">EPG</attribute>
|
||||
<attribute name="action">app.show_epg</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Timers</attribute>
|
||||
<attribute name="action">app.show_timers</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Recordings</attribute>
|
||||
<attribute name="action">app.show_recordings</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">FTP</attribute>
|
||||
<attribute name="action">app.show_ftp</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Control</attribute>
|
||||
<attribute name="action">app.show_control</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Display picons</attribute>
|
||||
<attribute name="action">app.display_picons</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Display EPG in bouquet list</attribute>
|
||||
<attribute name="action">app.display_epg</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Alternate layout</attribute>
|
||||
<attribute name="action">app.set_alternate_layout</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Alternate window title</attribute>
|
||||
<attribute name="action">app.set_alternate_title</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu id="tools_menu">
|
||||
<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>
|
||||
<section id="extension_section">
|
||||
</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.about</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
</menu>
|
||||
<menu id="mac_app_menu">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">About</attribute>
|
||||
<attribute name="action">app.about</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Settings</attribute>
|
||||
<attribute name="action">app.preferences</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Exit</attribute>
|
||||
<attribute name="action">app.quit</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</menu>
|
||||
<menu id="mac_menu_bar">
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Playback</attribute>
|
||||
<attribute name="action">app.hide_media_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Play</attribute>
|
||||
<attribute name="action">app.on_play</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Stop</attribute>
|
||||
<attribute name="action">app.on_stop</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Close</attribute>
|
||||
<attribute name="action">app.on_playback_close</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">File</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Import</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Bouquet</attribute>
|
||||
<attribute name="action">app.on_import_bouquet</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Bouquets and services</attribute>
|
||||
<attribute name="action">app.on_import_bouquets</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Import from Web</attribute>
|
||||
<attribute name="action">app.on_import_from_web</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">New empty configuration</attribute>
|
||||
<attribute name="action">app.on_new_configuration</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Open</attribute>
|
||||
<attribute name="action">app.on_data_open</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Extract...</attribute>
|
||||
<attribute name="action">app.on_archive_open</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Save</attribute>
|
||||
<attribute name="action">app.on_data_save</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Save as</attribute>
|
||||
<attribute name="action">app.on_data_save_as</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">FTP-transfer</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Download from the receiver</attribute>
|
||||
<attribute name="action">app.on_receive</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Transfer to receiver</attribute>
|
||||
<attribute name="action">app.on_send</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">Edit</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Lock</attribute>
|
||||
<attribute name="action">app.on_locked</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Hide</attribute>
|
||||
<attribute name="action">app.on_hide</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<attribute name="label" translatable="yes">View</attribute>
|
||||
<attribute name="action">app.hide_menu_bar</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Bouquets</attribute>
|
||||
<attribute name="action">app.show_bouquets</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Satellites</attribute>
|
||||
<attribute name="action">app.show_satellites</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Picons</attribute>
|
||||
<attribute name="action">app.show_picons</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">EPG</attribute>
|
||||
<attribute name="action">app.show_epg</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Timers</attribute>
|
||||
<attribute name="action">app.show_timers</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Recordings</attribute>
|
||||
<attribute name="action">app.show_recordings</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">FTP</attribute>
|
||||
<attribute name="action">app.show_ftp</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Control</attribute>
|
||||
<attribute name="action">app.show_control</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Display picons</attribute>
|
||||
<attribute name="action">app.display_picons</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Display EPG in bouquet list</attribute>
|
||||
<attribute name="action">app.display_epg</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Alternate layout</attribute>
|
||||
<attribute name="action">app.set_alternate_layout</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">Alternate window title</attribute>
|
||||
<attribute name="action">app.set_alternate_title</attribute>
|
||||
<attribute name="hidden-when">action-disabled</attribute>
|
||||
</item>
|
||||
</section>
|
||||
</submenu>
|
||||
<submenu>
|
||||
<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>
|
||||
<section id="mac_extension_section">
|
||||
</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>
|
||||
272
app/ui/backup.py
Normal file
272
app/ui/backup.py
Normal file
@@ -0,0 +1,272 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import time
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
|
||||
from app.commons import run_idle, get_size_from_bytes
|
||||
from app.settings import SettingsType, SEP
|
||||
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, HeaderBar
|
||||
|
||||
|
||||
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 f"{self._data_path}backup{os.sep}"
|
||||
self._open_data_callback = callback
|
||||
self._dialog_window = builder.get_object("dialog_window")
|
||||
self._dialog_window.set_transient_for(transient)
|
||||
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")
|
||||
self._file_count_label = builder.get_object("file_count_label")
|
||||
|
||||
if self._settings.use_header_bar:
|
||||
header_bar = HeaderBar()
|
||||
self._dialog_window.set_titlebar(header_bar)
|
||||
|
||||
button_box = builder.get_object("main_button_box")
|
||||
button_box.set_margin_top(0)
|
||||
button_box.set_margin_bottom(0)
|
||||
button_box.set_margin_left(0)
|
||||
button_box.reparent(header_bar)
|
||||
|
||||
ch_button = builder.get_object("info_check_button")
|
||||
ch_button.set_margin_right(0)
|
||||
h_bar = builder.get_object("header_bar")
|
||||
h_bar.remove(ch_button)
|
||||
h_bar.set_visible(False)
|
||||
header_bar.pack_end(ch_button)
|
||||
|
||||
# Setting the last size of the dialog window if it was saved
|
||||
window_size = self._settings.get("backup_tool_window_size")
|
||||
if window_size:
|
||||
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)):
|
||||
p = Path(os.path.join(self._backup_path, file))
|
||||
if p.is_file():
|
||||
self._model.append((p.stem, get_size_from_bytes(p.stat().st_size)))
|
||||
else:
|
||||
os.makedirs(os.path.dirname(self._backup_path), exist_ok=True)
|
||||
|
||||
self._file_count_label.set_text(str(len(self._model)))
|
||||
|
||||
def on_restore_bouquets(self, item):
|
||||
self.restore(RestoreType.BOUQUETS)
|
||||
|
||||
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(f"{self._backup_path}{file_name}.zip")
|
||||
itrs_to_delete.append(itr)
|
||||
except FileNotFoundError as e:
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
else:
|
||||
list(map(model.remove, itrs_to_delete))
|
||||
|
||||
self._file_count_label.set_text(str(len(self._model)))
|
||||
|
||||
def on_view_popup_menu(self, menu, event):
|
||||
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
|
||||
menu.popup(None, None, None, None, event.button, event.time)
|
||||
|
||||
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(
|
||||
f"Created: {created}\n********** Files: **********\n")
|
||||
with zipfile.ZipFile(file_name) as zip_file:
|
||||
for name in zip_file.namelist():
|
||||
append_text_to_tview(name + "\n", self._text_view)
|
||||
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 = f"{backup_path}{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}{SEP}"
|
||||
os.makedirs(os.path.dirname(backup_path), exist_ok=True)
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
# Backup files in data dir(skipping dirs and *.xml).
|
||||
for file in filter(lambda f: not f.endswith(".xml") and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
|
||||
src, dst = os.path.join(path, file), backup_path + file
|
||||
shutil.move(src, dst) if move else shutil.copy(src, dst)
|
||||
# Compressing to zip and delete remaining files.
|
||||
zip_file = shutil.make_archive(backup_path.rstrip(SEP), "zip", backup_path)
|
||||
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 *.xml file. """
|
||||
for file in filter(lambda f: not f.endswith(".xml") and os.path.isfile(os.path.join(path, f)), os.listdir(path)):
|
||||
os.remove(os.path.join(path, file))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
461
app/ui/backup_dialog.glade
Normal file
461
app/ui/backup_dialog.glade
Normal file
@@ -0,0 +1,461 @@
|
||||
<?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 name -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name size -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkMenu" id="popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="restore_bouquets_popup_menu_item">
|
||||
<property name="label" translatable="yes">Restore bouquets</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_restore_bouquets" swapped="no"/>
|
||||
<accelerator key="r" signal="activate" modifiers="Primary"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="restore_all_popup_menu_item">
|
||||
<property name="label" translatable="yes">Restore all</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_stock">False</property>
|
||||
<signal name="activate" handler="on_restore_all" swapped="no"/>
|
||||
<accelerator key="e" signal="activate" modifiers="Primary"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="popup_menu_separator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="remove_popup_menu_item">
|
||||
<property name="label">gtk-remove</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="activate" handler="on_remove" swapped="no"/>
|
||||
<accelerator key="Delete" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkImage" id="remove_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">user-trash-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="restore_all_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-select-all-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="restore_bouquets_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">document-revert-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
<object class="GtkWindow" id="dialog_window">
|
||||
<property name="width_request">560</property>
|
||||
<property name="height_request">320</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="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="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Details</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="margin_right">15</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<signal name="toggled" handler="on_info_button_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="details_image1">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">emblem-important-symbolic</property>
|
||||
<property name="icon_size">1</property>
|
||||
</object>
|
||||
</child>
|
||||
<accelerator key="i" signal="clicked" modifiers="Primary"/>
|
||||
</object>
|
||||
<packing>
|
||||
<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="GtkFrame" id="main_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="label_xalign">0</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkPaned" id="main_paned">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="wide_handle">True</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="backups_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="main_view_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="main_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="model">main_list_store</property>
|
||||
<property name="search_column">0</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<property name="activate_on_single_click">True</property>
|
||||
<signal name="button-press-event" handler="on_view_popup_menu" object="popup_menu" swapped="no"/>
|
||||
<signal name="cursor-changed" handler="on_cursor_changed" swapped="no"/>
|
||||
<signal name="key-release-event" handler="on_key_release" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="main_view_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="backup_name_column">
|
||||
<property name="min_width">75</property>
|
||||
<property name="title" translatable="yes">Name</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="clickable">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="reorderable">True</property>
|
||||
<property name="sort_column_id">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="name_renderer">
|
||||
<property name="xpad">10</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="backup_size_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="fixed_width">120</property>
|
||||
<property name="title" translatable="yes">Size</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="size_renderer">
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="status_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">2</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="count_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">document-properties</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="file_count_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
<property name="width_chars">4</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="text_view_scrolled_window">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="text_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="pixels_above_lines">5</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="left_margin">10</property>
|
||||
<property name="right_margin">10</property>
|
||||
<property name="indent">10</property>
|
||||
<property name="cursor_visible">False</property>
|
||||
<property name="accepts_tab">False</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label_item">
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<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>
|
||||
1354
app/ui/control.glade
Normal file
1354
app/ui/control.glade
Normal file
File diff suppressed because it is too large
Load Diff
343
app/ui/control.py
Normal file
343
app/ui/control.py
Normal file
@@ -0,0 +1,343 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Receiver control module via HTTP API. """
|
||||
import os
|
||||
import re
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from .dialogs import get_builder, get_message
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH
|
||||
from ..commons import run_task, run_with_delay, log, run_idle
|
||||
from ..connections import HttpAPI
|
||||
from ..settings import IS_DARWIN, IS_LINUX, IS_WIN
|
||||
|
||||
|
||||
class ControlTool(Gtk.Box):
|
||||
|
||||
def __init__(self, app, settings, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._settings = settings
|
||||
self._app = app
|
||||
self._app.connect("layout-changed", self.on_layout_changed)
|
||||
self._pix = None
|
||||
|
||||
handlers = {"on_volume_changed": self.on_volume_changed,
|
||||
"on_screenshot_draw": self.on_screenshot_draw,
|
||||
"on_network_toggled": self.on_network_toggled,
|
||||
"on_network_view_query_tooltip": self.on_network_view_query_tooltip}
|
||||
|
||||
builder = get_builder(UI_RESOURCES_PATH + "control.glade", handlers)
|
||||
|
||||
self.pack_start(builder.get_object("control_box"), True, True, 0)
|
||||
self._remote_box = builder.get_object("remote_box")
|
||||
self._screenshot_area = builder.get_object("screenshot_area")
|
||||
self._screenshot_button_box = builder.get_object("screenshot_button_box")
|
||||
self._screenshot_check_button = builder.get_object("screenshot_check_button")
|
||||
self._screenshot_check_button.bind_property("active", self._screenshot_area, "visible")
|
||||
self._snr_value_label = builder.get_object("snr_value_label")
|
||||
self._ber_value_label = builder.get_object("ber_value_label")
|
||||
self._agc_value_label = builder.get_object("agc_value_label")
|
||||
self._snr_level_bar = builder.get_object("snr_level_bar")
|
||||
self._ber_level_bar = builder.get_object("ber_level_bar")
|
||||
self._agc_level_bar = builder.get_object("agc_level_bar")
|
||||
self._volume_button = builder.get_object("volume_button")
|
||||
self._header_box = builder.get_object("control_header_box")
|
||||
self._screenshot_button_box = builder.get_object("screenshot_button_box")
|
||||
# Network.
|
||||
self._network_button = builder.get_object("control_network_button")
|
||||
self._network_model = builder.get_object("network_model")
|
||||
|
||||
self.init_actions(app)
|
||||
|
||||
if settings.alternate_layout:
|
||||
self.on_layout_changed(app, True)
|
||||
|
||||
self.show()
|
||||
|
||||
def init_actions(self, app):
|
||||
# Remote controller actions.
|
||||
app.set_action("on_one", lambda a, v: self.on_remote_action(HttpAPI.Remote.ONE))
|
||||
app.set_action("on_two", lambda a, v: self.on_remote_action(HttpAPI.Remote.TWO))
|
||||
app.set_action("on_three", lambda a, v: self.on_remote_action(HttpAPI.Remote.THREE))
|
||||
app.set_action("on_four", lambda a, v: self.on_remote_action(HttpAPI.Remote.FOUR))
|
||||
app.set_action("on_five", lambda a, v: self.on_remote_action(HttpAPI.Remote.FIVE))
|
||||
app.set_action("on_six", lambda a, v: self.on_remote_action(HttpAPI.Remote.SIX))
|
||||
app.set_action("on_seven", lambda a, v: self.on_remote_action(HttpAPI.Remote.SEVEN))
|
||||
app.set_action("on_eight", lambda a, v: self.on_remote_action(HttpAPI.Remote.EIGHT))
|
||||
app.set_action("on_nine", lambda a, v: self.on_remote_action(HttpAPI.Remote.NINE))
|
||||
app.set_action("on_zero", lambda a, v: self.on_remote_action(HttpAPI.Remote.ZERO))
|
||||
app.set_action("on_up", lambda a, v: self.on_remote_action(HttpAPI.Remote.UP))
|
||||
app.set_action("on_down", lambda a, v: self.on_remote_action(HttpAPI.Remote.DOWN))
|
||||
app.set_action("on_left", lambda a, v: self.on_remote_action(HttpAPI.Remote.LEFT))
|
||||
app.set_action("on_right", lambda a, v: self.on_remote_action(HttpAPI.Remote.RIGHT))
|
||||
app.set_action("on_next", lambda a, v: self.on_remote_action(HttpAPI.Remote.NEXT))
|
||||
app.set_action("on_back", lambda a, v: self.on_remote_action(HttpAPI.Remote.BACK))
|
||||
app.set_action("on_info", lambda a, v: self.on_remote_action(HttpAPI.Remote.INFO))
|
||||
app.set_action("on_ok", lambda a, v: self.on_remote_action(HttpAPI.Remote.OK))
|
||||
app.set_action("on_menu", lambda a, v: self.on_remote_action(HttpAPI.Remote.MENU))
|
||||
app.set_action("on_exit", lambda a, v: self.on_remote_action(HttpAPI.Remote.EXIT))
|
||||
app.set_action("on_epg", lambda a, v: self.on_remote_action(HttpAPI.Remote.EPG))
|
||||
app.set_action("on_ch_up", lambda a, v: self.on_remote_action(HttpAPI.Remote.CH_UP))
|
||||
app.set_action("on_ch_down", lambda a, v: self.on_remote_action(HttpAPI.Remote.CH_DOWN))
|
||||
app.set_action("on_red", lambda a, v: self.on_remote_action(HttpAPI.Remote.RED))
|
||||
app.set_action("on_green", lambda a, v: self.on_remote_action(HttpAPI.Remote.GREEN))
|
||||
app.set_action("on_yellow", lambda a, v: self.on_remote_action(HttpAPI.Remote.YELLOW))
|
||||
app.set_action("on_blue", lambda a, v: self.on_remote_action(HttpAPI.Remote.BLUE))
|
||||
app.set_action("on_audio", lambda a, v: self.on_remote_action(HttpAPI.Remote.AUDIO))
|
||||
app.set_action("on_tv", lambda a, v: self.on_remote_action(HttpAPI.Remote.TV))
|
||||
app.set_action("on_radio", lambda a, v: self.on_remote_action(HttpAPI.Remote.RADIO))
|
||||
app.set_action("on_fav", lambda a, v: self.on_remote_action(HttpAPI.Remote.FAV))
|
||||
# Playback.
|
||||
app.set_action("on_prev_media", lambda a, v: self.on_player_action(HttpAPI.Request.PLAYER_PREV))
|
||||
app.set_action("on_play_media", lambda a, v: self.on_player_action(HttpAPI.Request.PLAYER_PLAY))
|
||||
app.set_action("on_stop_media", lambda a, v: self.on_player_action(HttpAPI.Request.PLAYER_STOP))
|
||||
app.set_action("on_next_media", lambda a, v: self.on_player_action(HttpAPI.Request.PLAYER_NEXT))
|
||||
# Power.
|
||||
app.set_action("on_standby", lambda a, v: self.on_power_action(HttpAPI.Power.STANDBY))
|
||||
app.set_action("on_wake_up", lambda a, v: self.on_power_action(HttpAPI.Power.WAKEUP))
|
||||
app.set_action("on_reboot", lambda a, v: self.on_power_action(HttpAPI.Power.REBOOT))
|
||||
app.set_action("on_restart_gui", lambda a, v: self.on_power_action(HttpAPI.Power.RESTART_GUI))
|
||||
app.set_action("on_shutdown", lambda a, v: self.on_power_action(HttpAPI.Power.DEEP_STANDBY))
|
||||
# Screenshots.
|
||||
app.set_action("on_screenshot_all", self.on_screenshot_all)
|
||||
app.set_action("on_screenshot_video", self.on_screenshot_video)
|
||||
app.set_action("on_screenshot_osd", self.on_screenshot_osd)
|
||||
|
||||
def on_layout_changed(self, app, alt_layout):
|
||||
children = self._remote_box.get_children()
|
||||
self._remote_box.reorder_child(children[0], len(children) - 1)
|
||||
self._remote_box.reorder_child(children[-1], 0)
|
||||
pack_type = Gtk.PackType.END if alt_layout else Gtk.PackType.START
|
||||
self._header_box.set_child_packing(self._network_button, False, False, 0, pack_type)
|
||||
pack_type = Gtk.PackType.START if alt_layout else Gtk.PackType.END
|
||||
self._header_box.set_child_packing(self._screenshot_button_box, False, False, 0, pack_type)
|
||||
|
||||
# ***************** Remote controller ********************* #
|
||||
|
||||
def on_remote(self, action, state=False):
|
||||
""" Shows/Hides [R key] remote controller. """
|
||||
action.set_state(state)
|
||||
self._remote_revealer.set_visible(state)
|
||||
self._remote_revealer.set_reveal_child(state)
|
||||
|
||||
if state:
|
||||
self._app.send_http_request(HttpAPI.Request.VOL, "state", self.update_volume)
|
||||
|
||||
def on_remote_action(self, action):
|
||||
self._app.send_http_request(HttpAPI.Request.REMOTE, action, self.on_response)
|
||||
|
||||
def on_player_action(self, action):
|
||||
self._app.send_http_request(action, "", self.on_response)
|
||||
|
||||
@run_with_delay(0.5)
|
||||
def on_volume_changed(self, button, value):
|
||||
self._app.send_http_request(HttpAPI.Request.VOL, f"{value:.0f}", self.on_response)
|
||||
|
||||
def update_volume(self, vol):
|
||||
if "error_code" in vol:
|
||||
return
|
||||
|
||||
GLib.idle_add(self._volume_button.set_value, int(vol.get("e2current", "0")))
|
||||
|
||||
def on_response(self, resp):
|
||||
if "error_code" in resp:
|
||||
return
|
||||
|
||||
if self._screenshot_check_button.get_active() and self._app.http_api:
|
||||
ref = "mode=all" if self._app.http_api.is_owif else "d="
|
||||
self._app.send_http_request(HttpAPI.Request.GRUB, ref, self.update_screenshot)
|
||||
|
||||
@run_task
|
||||
def update_screenshot(self, data):
|
||||
if "error_code" in data:
|
||||
return
|
||||
|
||||
data = data.get("img_data", None)
|
||||
if data:
|
||||
from gi.repository import GdkPixbuf
|
||||
|
||||
allocation = self._screenshot_area.get_parent().get_allocation()
|
||||
loader = GdkPixbuf.PixbufLoader.new_with_type("jpeg")
|
||||
loader.set_size(allocation.width, allocation.height)
|
||||
try:
|
||||
loader.write(data)
|
||||
pix = loader.get_pixbuf()
|
||||
except GLib.Error:
|
||||
pass # NOP
|
||||
else:
|
||||
self._pix = pix
|
||||
self._screenshot_area.queue_draw() # Redrawing the area!
|
||||
finally:
|
||||
loader.close()
|
||||
|
||||
def on_screenshot_draw(self, area, cr):
|
||||
""" Called to automatically resize the screenshot. """
|
||||
if self._pix:
|
||||
cr.scale(area.get_allocated_width() / self._pix.get_width(),
|
||||
area.get_allocated_height() / self._pix.get_height())
|
||||
img_surface = Gdk.cairo_surface_create_from_pixbuf(self._pix, 1, None)
|
||||
cr.set_source_surface(img_surface, 0, 0)
|
||||
cr.paint()
|
||||
|
||||
def on_screenshot_all(self, action, value=None):
|
||||
if self._app.http_api:
|
||||
self._app.send_http_request(HttpAPI.Request.GRUB, "mode=all" if self._app.http_api.is_owif else "d=",
|
||||
self.on_screenshot)
|
||||
|
||||
def on_screenshot_video(self, action, value=None):
|
||||
if self._app.http_api:
|
||||
self._app.send_http_request(HttpAPI.Request.GRUB, "mode=video" if self._app.http_api.is_owif else "v=",
|
||||
self.on_screenshot)
|
||||
|
||||
def on_screenshot_osd(self, action, value=None):
|
||||
if self._app.http_api:
|
||||
self._app.send_http_request(HttpAPI.Request.GRUB, "mode=osd" if self._app.http_api.is_owif else "o=",
|
||||
self.on_screenshot)
|
||||
|
||||
@run_task
|
||||
def on_screenshot(self, data):
|
||||
if "error_code" in data:
|
||||
return
|
||||
|
||||
img = data.get("img_data", None)
|
||||
if img:
|
||||
GLib.idle_add(self._screenshot_button_box.set_sensitive, not IS_LINUX)
|
||||
path = os.path.expanduser("~/Desktop") if not IS_LINUX else None
|
||||
|
||||
try:
|
||||
import tempfile
|
||||
import subprocess
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="wb", suffix=".jpg", dir=path, delete=IS_LINUX) as tf:
|
||||
tf.write(img)
|
||||
if IS_LINUX:
|
||||
cmd = ["xdg-open", tf.name]
|
||||
elif IS_DARWIN:
|
||||
cmd = ["open", tf.name]
|
||||
else:
|
||||
cmd = [tf.name]
|
||||
|
||||
if not IS_WIN:
|
||||
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
|
||||
# File must be closed.
|
||||
if IS_WIN:
|
||||
subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=IS_WIN).communicate()
|
||||
finally:
|
||||
GLib.idle_add(self._screenshot_button_box.set_sensitive, True)
|
||||
|
||||
def on_power_action(self, action):
|
||||
self._app.send_http_request(HttpAPI.Request.POWER, action, lambda resp: log("Power status changed..."))
|
||||
|
||||
def update_signal(self, sig):
|
||||
snr = sig.get("e2snr", "0 %").strip() if sig else "0 %"
|
||||
snr_db = sig.get("e2snrdb", "0 dB").strip() if sig else "0 dB"
|
||||
acg = sig.get("e2acg", "0 %").strip() if sig else "0 %"
|
||||
ber = (sig.get("e2ber", None) or "").strip() if sig else ""
|
||||
# Labels.
|
||||
self._snr_value_label.set_text(f"{snr_db} ({snr})")
|
||||
self._agc_value_label.set_text(acg)
|
||||
self._ber_value_label.set_text(ber)
|
||||
# Level bars.
|
||||
self._snr_level_bar.set_value(int(snr.strip("%N/A") or 0))
|
||||
self._agc_level_bar.set_value(int(acg.rstrip("%N/A") or 0))
|
||||
self._ber_level_bar.set_value(int(ber.rstrip("N/A") or 0))
|
||||
|
||||
# ***************** Network explorer ********************** #
|
||||
|
||||
def on_network_toggled(self, button):
|
||||
self._network_model.clear()
|
||||
if button.get_active():
|
||||
self.update_network()
|
||||
|
||||
@run_task
|
||||
def update_network(self):
|
||||
pattern = re.compile(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})')
|
||||
|
||||
ips = [match for match in re.findall(pattern, os.popen("arp -a").read())]
|
||||
for ip in ips:
|
||||
if not self._network_button.get_active():
|
||||
break
|
||||
|
||||
url = f"http://{ip}/web/{HttpAPI.Request.INFO.value}"
|
||||
try:
|
||||
resp = HttpAPI.get_response(HttpAPI.Request.INFO, url, timeout=5)
|
||||
except OSError as e:
|
||||
log(f"{ip} {e}")
|
||||
else:
|
||||
if resp.get("e2distroversion", None):
|
||||
log(f"Receiver found. Model: {resp.get('e2model', 'N/A')} [{ip} ]")
|
||||
self.append_box_data(resp)
|
||||
|
||||
@run_idle
|
||||
def append_box_data(self, data):
|
||||
ip = data.get('e2lanip', 'N/A')
|
||||
itr = self._network_model.append((data.get("e2model", "N/A"), ip, None, data, None))
|
||||
GLib.timeout_add_seconds(3, self.check_power_state, itr, priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def on_network_view_query_tooltip(self, view, x, y, keyboard_mode, tooltip):
|
||||
result = view.get_dest_row_at_pos(x, y)
|
||||
if not result:
|
||||
return False
|
||||
|
||||
path, pos = result
|
||||
model = view.get_model()
|
||||
data = model[path][3]
|
||||
|
||||
dist = data.get("e2distroversion", "N/A")
|
||||
img = data.get("e2imageversion", "N/A")
|
||||
txt = f"Distro version: {dist}\nImage version: {img}"
|
||||
tooltip.set_text(txt)
|
||||
view.set_tooltip_row(tooltip, path)
|
||||
return True
|
||||
|
||||
def check_power_state(self, itr):
|
||||
active = self._network_button.get_active()
|
||||
if not active:
|
||||
return False
|
||||
|
||||
data = self._network_model.get_value(itr, 3)
|
||||
url = f"http://{data.get('e2lanip', 'N/A')}/web/powerstate"
|
||||
self.update_power_state(itr, url)
|
||||
return active
|
||||
|
||||
@run_task
|
||||
def update_power_state(self, itr, url):
|
||||
try:
|
||||
resp = HttpAPI.get_response(HttpAPI.Request.POWER, url, timeout=2)
|
||||
except OSError as e:
|
||||
log(e)
|
||||
else:
|
||||
state = get_message("On" if resp.get("e2instandby", "N/A").strip() == "false" else "Standby")
|
||||
GLib.idle_add(self._network_model.set_value, itr, 2, state)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
1882
app/ui/dialogs.glade
1882
app/ui/dialogs.glade
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,80 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Common module for showing dialogs """
|
||||
import locale
|
||||
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, USE_HEADER_BAR
|
||||
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN
|
||||
|
||||
|
||||
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"
|
||||
CHOOSER = "path_chooser_dialog"
|
||||
ERROR = "error_dialog"
|
||||
QUESTION = "question_dialog"
|
||||
ABOUT = "about_dialog"
|
||||
WAIT = "wait_dialog"
|
||||
INPUT = "input"
|
||||
CHOOSER = "chooser"
|
||||
ERROR = "error"
|
||||
QUESTION = "question"
|
||||
INFO = "info"
|
||||
ABOUT = "about"
|
||||
WAIT = "wait"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
class WaitDialog:
|
||||
@@ -25,12 +82,17 @@ class WaitDialog:
|
||||
builder, dialog = get_dialog_from_xml(DialogType.WAIT, transient)
|
||||
self._dialog = dialog
|
||||
self._dialog.set_transient_for(transient)
|
||||
if text is not None:
|
||||
builder.get_object("wait_dialog_label").set_text(text)
|
||||
self._label = builder.get_object("wait_dialog_label")
|
||||
self._default_text = text or self._label.get_text()
|
||||
|
||||
def show(self):
|
||||
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()
|
||||
@@ -40,73 +102,155 @@ class WaitDialog:
|
||||
self._dialog.destroy()
|
||||
|
||||
|
||||
def show_dialog(dialog_type: DialogType, transient, text=None, options=None, action_type=None, file_filter=None):
|
||||
""" Shows dialogs by name """
|
||||
builder, dialog = get_dialog_from_xml(dialog_type, transient)
|
||||
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)
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
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 + "/"
|
||||
return show_dialog(dialog_type=DialogType.CHOOSER,
|
||||
transient=transient,
|
||||
settings=settings,
|
||||
action_type=Gtk.FileChooserAction.OPEN,
|
||||
file_filter=file_filter,
|
||||
title=title)
|
||||
|
||||
response = path
|
||||
dialog.destroy()
|
||||
|
||||
return response
|
||||
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 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()
|
||||
if file_filter is not None:
|
||||
dialog.add_filter(file_filter)
|
||||
|
||||
return txt if response == Gtk.ResponseType.OK else Gtk.ResponseType.CANCEL
|
||||
dialog.set_current_folder(settings.profile_data_path)
|
||||
response = dialog.run()
|
||||
|
||||
if text:
|
||||
dialog.set_markup(get_message(text))
|
||||
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=USE_HEADER_BAR)
|
||||
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.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)
|
||||
dialog.set_markup(get_message(text))
|
||||
response = dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def get_dialog_from_xml(dialog_type, transient):
|
||||
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)
|
||||
builder.add_objects_from_file(UI_RESOURCES_PATH + "dialogs.glade", (dialog_type.value,))
|
||||
dialog = builder.get_object(dialog_type.value)
|
||||
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_chooser_dialog(transient, options, pattern, name):
|
||||
file_filter = Gtk.FileFilter()
|
||||
file_filter.add_pattern(pattern)
|
||||
file_filter.set_name(name)
|
||||
|
||||
return show_dialog(dialog_type=DialogType.CHOOSER,
|
||||
transient=transient,
|
||||
options=options,
|
||||
action_type=Gtk.FileChooserAction.OPEN,
|
||||
file_filter=file_filter)
|
||||
|
||||
|
||||
def get_message(message):
|
||||
""" returns translated message """
|
||||
return locale.dgettext(TEXT_DOMAIN, 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=USE_HEADER_BAR), objects)
|
||||
else:
|
||||
builder.add_from_string(get_dialogs_string(path, tag).format(use_header=USE_HEADER_BAR))
|
||||
else:
|
||||
if objects:
|
||||
builder.add_objects_from_string(get_dialogs_string(path, tag), objects)
|
||||
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__":
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
from app.commons import run_idle, run_task
|
||||
from app.ftp import download_data, DownloadDataType, upload_data
|
||||
from app.properties import Profile
|
||||
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN
|
||||
from .dialogs import show_dialog, DialogType, get_message
|
||||
|
||||
|
||||
def show_download_dialog(transient, options, open_data, profile=Profile.ENIGMA_2):
|
||||
dialog = DownloadDialog(transient, options, open_data, profile)
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
|
||||
class DownloadDialog:
|
||||
def __init__(self, transient, properties, open_data, profile):
|
||||
self._properties = properties
|
||||
self._open_data = open_data
|
||||
self._profile = profile
|
||||
|
||||
handlers = {"on_receive": self.on_receive,
|
||||
"on_send": self.on_send,
|
||||
"on_info_bar_close": self.on_info_bar_close}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_file(UI_RESOURCES_PATH + "dialogs.glade", ("download_dialog",))
|
||||
builder.connect_signals(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._remove_unused_check_button = builder.get_object("remove_unused_check_button")
|
||||
self._all_radio_button = builder.get_object("all_radio_button")
|
||||
self._bouquets_radio_button = builder.get_object("bouquets_radio_button")
|
||||
self._satellites_radio_button = builder.get_object("satellites_radio_button")
|
||||
self._webtv_radio_button = builder.get_object("webtv_radio_button")
|
||||
if profile is Profile.NEUTRINO_MP:
|
||||
self._webtv_radio_button.set_visible(True)
|
||||
# self._dialog.get_content_area().set_border_width(0)
|
||||
|
||||
@run_idle
|
||||
def on_receive(self, item):
|
||||
self.download(True, self.get_download_type())
|
||||
|
||||
@run_idle
|
||||
def on_send(self, item):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) != Gtk.ResponseType.CANCEL:
|
||||
self.download(False, self.get_download_type())
|
||||
|
||||
def get_download_type(self):
|
||||
download_type = DownloadDataType.ALL
|
||||
if self._bouquets_radio_button.get_active():
|
||||
download_type = DownloadDataType.BOUQUETS
|
||||
elif self._satellites_radio_button.get_active():
|
||||
download_type = DownloadDataType.SATELLITES
|
||||
elif self._webtv_radio_button.get_active():
|
||||
download_type = DownloadDataType.WEBTV
|
||||
return download_type
|
||||
|
||||
def run(self):
|
||||
return self._dialog.run()
|
||||
|
||||
def destroy(self):
|
||||
self._dialog.destroy()
|
||||
|
||||
def on_info_bar_close(self, bar=None, resp=None):
|
||||
self._info_bar.set_visible(False)
|
||||
|
||||
@run_idle
|
||||
@run_task
|
||||
def download(self, download, d_type):
|
||||
""" Download/upload data from/to receiver """
|
||||
try:
|
||||
if download:
|
||||
download_data(properties=self._properties, download_type=d_type)
|
||||
else:
|
||||
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
|
||||
upload_data(properties=self._properties,
|
||||
download_type=d_type,
|
||||
remove_unused=self._remove_unused_check_button.get_active(),
|
||||
profile=self._profile,
|
||||
callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO))
|
||||
except Exception as e:
|
||||
message = str(getattr(e, "message", str(e)))
|
||||
self.show_info_message(message, Gtk.MessageType.ERROR)
|
||||
else:
|
||||
if download and d_type is not DownloadDataType.SATELLITES:
|
||||
self._open_data()
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
0
app/ui/epg/__init__.py
Normal file
0
app/ui/epg/__init__.py
Normal file
1560
app/ui/epg/dialog.glade
Normal file
1560
app/ui/epg/dialog.glade
Normal file
File diff suppressed because it is too large
Load Diff
1022
app/ui/epg/epg.py
Normal file
1022
app/ui/epg/epg.py
Normal file
File diff suppressed because it is too large
Load Diff
356
app/ui/epg/settings.glade
Normal file
356
app/ui/epg/settings.glade
Normal file
@@ -0,0 +1,356 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.16"/>
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkAdjustment" id="interval_adjustment">
|
||||
<property name="lower">3</property>
|
||||
<property name="upper">60</property>
|
||||
<property name="value">3</property>
|
||||
<property name="step_increment">1</property>
|
||||
<property name="page_increment">10</property>
|
||||
</object>
|
||||
<object class="GtkBox" id="main_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">10</property>
|
||||
<property name="margin_right">10</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="label" translatable="yes">Source:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="src_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="source_selection_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="layout_style">expand</property>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="http_src_button">
|
||||
<property name="label" translatable="yes">Receiver</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<property name="group">dat_src_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="xml_src_button">
|
||||
<property name="label" translatable="yes">XML TV</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<property name="group">dat_src_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkRadioButton" id="dat_src_button">
|
||||
<property name="label" translatable="yes">*.dat file</property>
|
||||
<property name="visible">False</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">False</property>
|
||||
<property name="active">True</property>
|
||||
<property name="draw_indicator">False</property>
|
||||
<property name="group">http_src_button</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="interval_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="interval_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Update interval (sec):</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="interval_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="max_width_chars">4</property>
|
||||
<property name="adjustment">interval_adjustment</property>
|
||||
<property name="climb_rate">1</property>
|
||||
<property name="numeric">True</property>
|
||||
<property name="value">3</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="xml_source_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="sensitive" bind-source="xml_src_button" bind-property="active"/>
|
||||
<child>
|
||||
<object class="GtkLabel" id="url_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Url to *.xml.gz file:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="url_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">network-transmit-receive-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="input_purpose">url</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="download_interval_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="download_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Update:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="download_interval_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="active">0</property>
|
||||
<property name="active_id">daily</property>
|
||||
<items>
|
||||
<item id="daily" translatable="yes">Daily</item>
|
||||
</items>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="dat_source_box">
|
||||
<property name="visible">False</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="sensitive" bind-source="dat_src_button" bind-property="active"/>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">STB path:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="dat_path_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="active">0</property>
|
||||
<property name="active_id">/etc/enigma2</property>
|
||||
<items>
|
||||
<item id="/etc/enigma2/">/etc/enigma2/</item>
|
||||
<item id="/media/hdd/">/media/hdd/</item>
|
||||
<item id="/media/usb/">/media/usb/</item>
|
||||
<item id="/media/mmc/">/media/mmc/</item>
|
||||
<item id="/media/cf/">/media/cf/</item>
|
||||
</items>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="actions_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<property name="homogeneous">True</property>
|
||||
<property name="layout_style">expand</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="apply_button">
|
||||
<property name="label" translatable="yes">Apply</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<signal name="clicked" handler="on_apply" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="close_button">
|
||||
<property name="label" translatable="yes">Close</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<signal name="clicked" handler="on_close" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">16</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
468
app/ui/epg/tab.glade
Normal file
468
app/ui/epg/tab.glade
Normal file
@@ -0,0 +1,468 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.22"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor for GNU/Linux. -->
|
||||
<!-- interface-copyright 2018-2023 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkListStore" id="epg_model">
|
||||
<columns>
|
||||
<!-- column-name service -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name title -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name start -->
|
||||
<column type="gint"/>
|
||||
<!-- column-name end -->
|
||||
<column type="gint"/>
|
||||
<!-- column-name length -->
|
||||
<column type="gint"/>
|
||||
<!-- column-name description -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name data -->
|
||||
<column type="PyObject"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkTreeModelFilter" id="epg_filter_model">
|
||||
<property name="child-model">epg_model</property>
|
||||
</object>
|
||||
<object class="GtkTreeModelSort" id="epg_sort_model">
|
||||
<property name="model">epg_filter_model</property>
|
||||
</object>
|
||||
<object class="GtkFrame" id="epg_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label-xalign">0.49</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="epg_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">2</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="epg_action_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="border-width">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child type="center">
|
||||
<object class="GtkToggleButton" id="multi_epg_button">
|
||||
<property name="label" translatable="yes">Multi EPG</property>
|
||||
<property name="name">header-button</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="toggled" handler="on_multi_epg_toggled" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="src_combo_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">EPG source</property>
|
||||
<property name="active">0</property>
|
||||
<property name="has-entry">True</property>
|
||||
<property name="active-id">0</property>
|
||||
<items>
|
||||
<item id="0" translatable="yes">Receiver</item>
|
||||
</items>
|
||||
<child internal-child="entry">
|
||||
<object class="GtkEntry">
|
||||
<property name="name">header-entry</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="width-chars">10</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="epg_filter_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Filter</property>
|
||||
<signal name="toggled" handler="on_epg_filter_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="epg_filter_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-find-replace-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="epg_add_timer_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Add timer</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="clicked" handler="on_timer_add" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="add_timer_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">alarm-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="epg_fs_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="epg_filter_entry">
|
||||
<property name="visible" bind-source="epg_filter_button" bind-property="active">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="primary-icon-name">edit-find-replace-symbolic</property>
|
||||
<property name="primary-icon-activatable">False</property>
|
||||
<property name="primary-icon-sensitive">False</property>
|
||||
<signal name="search-changed" handler="on_epg_filter_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="fav_search_box">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="epg_search_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="primary-icon-name">edit-find-symbolic</property>
|
||||
<property name="primary-icon-activatable">False</property>
|
||||
<property name="primary-icon-sensitive">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="epg_search_down_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<child>
|
||||
<object class="GtkArrow" id="epg_down_arrow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="arrow-type">down</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="epg_search_up_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<child>
|
||||
<object class="GtkArrow" id="epg_up_arrow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="arrow-type">up</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="group"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="epg_view_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="epg_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="model">epg_sort_model</property>
|
||||
<property name="fixed-height-mode">True</property>
|
||||
<property name="rubber-banding">True</property>
|
||||
<property name="enable-grid-lines">both</property>
|
||||
<property name="tooltip-column">6</property>
|
||||
<signal name="button-press-event" handler="on_epg_press" swapped="no"/>
|
||||
<signal name="query-tooltip" handler="on_view_query_tooltip" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="epg_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_service_column">
|
||||
<property name="visible" bind-source="multi_epg_button" bind-property="active">False</property>
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="fixed-width">100</property>
|
||||
<property name="min-width">40</property>
|
||||
<property name="title" translatable="yes">Service</property>
|
||||
<property name="alignment">0.49</property>
|
||||
<property name="sort-column-id">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_service_renderer">
|
||||
<property name="xpad">5</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_title_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">150</property>
|
||||
<property name="title" translatable="yes">Title</property>
|
||||
<property name="alignment">0.49</property>
|
||||
<property name="sort-column-id">1</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_title_renderer">
|
||||
<property name="xpad">5</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_start_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">50</property>
|
||||
<property name="title" translatable="yes">Start time</property>
|
||||
<property name="alignment">0.49</property>
|
||||
<property name="sort-column-id">2</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_start_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="xalign">0.49</property>
|
||||
<property name="width-chars">27</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_end_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">50</property>
|
||||
<property name="title" translatable="yes">End time</property>
|
||||
<property name="alignment">0.49</property>
|
||||
<property name="sort-column-id">3</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_end_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="xalign">0.49</property>
|
||||
<property name="width-chars">27</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_length_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="fixed-width">100</property>
|
||||
<property name="min-width">50</property>
|
||||
<property name="title" translatable="yes">Length</property>
|
||||
<property name="alignment">0.49</property>
|
||||
<property name="sort-column-id">4</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_length_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="xalign">0.49</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">4</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="epg_desc_column">
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min-width">100</property>
|
||||
<property name="title" translatable="yes">Description</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.49</property>
|
||||
<property name="sort-column-id">5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="epg_desc_renderer">
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">5</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="status_box">
|
||||
<property name="height-request">26</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="event_count_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">document-properties</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="event_count_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">0</property>
|
||||
<property name="width-chars">4</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="epg_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes">EPG</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
1283
app/ui/ftp.glade
Normal file
1283
app/ui/ftp.glade
Normal file
File diff suppressed because it is too large
Load Diff
920
app/ui/ftp.py
Normal file
920
app/ui/ftp.py
Normal file
@@ -0,0 +1,920 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Simple FTP client module. """
|
||||
import stat
|
||||
import subprocess
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from enum import IntEnum
|
||||
from ftplib import all_errors
|
||||
from io import TextIOWrapper, BytesIO
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
from urllib.parse import urlparse, unquote
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import log, run_task, run_idle, get_size_from_bytes
|
||||
from app.connections import UtfFTP
|
||||
from app.settings import IS_LINUX, IS_DARWIN, IS_WIN, SEP, USE_HEADER_BAR
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_builder, get_message
|
||||
from app.ui.main_helper import on_popup_menu
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, MOD_MASK, Page
|
||||
|
||||
File = namedtuple("File", ["icon", "name", "size", "date", "attr", "extra"])
|
||||
|
||||
|
||||
class BaseDialog(Gtk.Dialog):
|
||||
""" Base class for additional FTP dialogs. """
|
||||
|
||||
def __init__(self, title, use_header_bar=0, *args, **kwargs):
|
||||
super().__init__(title=title, use_header_bar=use_header_bar, *args, **kwargs)
|
||||
|
||||
self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_SAVE, Gtk.ResponseType.OK)
|
||||
self.set_modal(True)
|
||||
self.set_skip_pager_hint(True)
|
||||
self.set_skip_taskbar_hint(True)
|
||||
self.set_position(Gtk.PositionType.BOTTOM)
|
||||
self.set_default_icon_name("document-properties-symbolic")
|
||||
|
||||
|
||||
class TextEditDialog(BaseDialog):
|
||||
""" Simple text edit dialog. """
|
||||
|
||||
def __init__(self, path, use_header_bar=0, *args, **kwargs):
|
||||
super().__init__(title=f"DemonEditor [{path}]", use_header_bar=use_header_bar, *args, **kwargs)
|
||||
|
||||
content_box = self.get_content_area()
|
||||
self._search_entry = Gtk.SearchEntry(visible=True, primary_icon_name="system-search-symbolic")
|
||||
self._search_entry.connect("search-changed", self.on_search_changed)
|
||||
|
||||
if use_header_bar:
|
||||
bar = self.get_header_bar()
|
||||
bar.pack_start(self._search_entry)
|
||||
bar.set_title("DemonEditor")
|
||||
bar.set_subtitle(path)
|
||||
else:
|
||||
search_bar = Gtk.SearchBar(visible=True)
|
||||
search_bar.add(self._search_entry)
|
||||
search_bar.set_search_mode(True)
|
||||
content_box.pack_start(search_bar, False, False, 0)
|
||||
|
||||
scrolled_window = Gtk.ScrolledWindow(hexpand=True, vexpand=True,
|
||||
min_content_width=720,
|
||||
min_content_height=320)
|
||||
content_box.pack_start(scrolled_window, True, True, 0)
|
||||
|
||||
try:
|
||||
import gi
|
||||
|
||||
gi.require_version("GtkSource", "3.0")
|
||||
from gi.repository import GtkSource
|
||||
except (ImportError, ValueError) as e:
|
||||
self._text_view = Gtk.TextView()
|
||||
self._buf = self._text_view.get_buffer()
|
||||
log(e)
|
||||
else:
|
||||
self._text_view = GtkSource.View(show_line_numbers=True, show_line_marks=True)
|
||||
self._buf = self._text_view.get_buffer()
|
||||
self._buf.set_highlight_syntax(True)
|
||||
self._buf.set_highlight_matching_brackets(True)
|
||||
lang_manager = GtkSource.LanguageManager.new()
|
||||
self._buf.set_language(lang_manager.guess_language(path))
|
||||
# Style
|
||||
self._buf.set_style_scheme(GtkSource.StyleSchemeManager().get_default().get_scheme("tango"))
|
||||
|
||||
self._tag_found = self._buf.create_tag("found", background="yellow")
|
||||
scrolled_window.add(self._text_view)
|
||||
|
||||
self.show_all()
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return self._buf.get_text(self._buf.get_start_iter(), self._buf.get_end_iter(), include_hidden_chars=True)
|
||||
|
||||
@text.setter
|
||||
def text(self, value):
|
||||
self._buf.set_text(value)
|
||||
|
||||
def on_search_changed(self, entry):
|
||||
self._buf.remove_tag(self._tag_found, self._buf.get_start_iter(), self._buf.get_end_iter())
|
||||
cursor_mark = self._buf.get_insert()
|
||||
start = self._buf.get_iter_at_mark(cursor_mark)
|
||||
if start.get_offset() == self._buf.get_char_count():
|
||||
start = self._buf.get_start_iter()
|
||||
|
||||
self.search_and_mark(entry.get_text(), start)
|
||||
|
||||
def search_and_mark(self, text, start, first=True):
|
||||
end = self._buf.get_end_iter()
|
||||
match = start.forward_search(text, 0, end)
|
||||
|
||||
if match is not None:
|
||||
match_start, match_end = match
|
||||
self._buf.apply_tag(self._tag_found, match_start, match_end)
|
||||
if first:
|
||||
self._text_view.scroll_to_iter(match_start, 0.0, False, 0.0, 0.0)
|
||||
GLib.idle_add(self.search_and_mark, text, match_end, False)
|
||||
|
||||
|
||||
class AttributesDialog(BaseDialog):
|
||||
""" Dialog for editing file attributes (permissions). """
|
||||
|
||||
def __init__(self, attrs, use_header_bar=0, *args, **kwargs):
|
||||
super().__init__(title=get_message("Permissions"), use_header_bar=use_header_bar, *args, **kwargs)
|
||||
|
||||
self.set_default_size(360, 100)
|
||||
self.set_resizable(False)
|
||||
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}ftp.glade", use_str=True, objects=("attributes_box",))
|
||||
content_box = self.get_content_area()
|
||||
content_box.pack_start(builder.get_object("attributes_box"), True, True, 0)
|
||||
self._num_value_entry = builder.get_object("num_value_entry")
|
||||
# Buttons.
|
||||
self._owner_read_button = builder.get_object("owner_read_button")
|
||||
self._group_read_button = builder.get_object("group_read_button")
|
||||
self._others_read_button = builder.get_object("others_read_button")
|
||||
self._owner_write_button = builder.get_object("owner_write_button")
|
||||
self._group_write_button = builder.get_object("group_write_button")
|
||||
self._others_write_button = builder.get_object("others_write_button")
|
||||
self._owner_exec_button = builder.get_object("owner_exec_button")
|
||||
self._group_exec_button = builder.get_object("group_exec_button")
|
||||
self._others_exec_button = builder.get_object("others_exec_button")
|
||||
self.init_attrs(attrs)
|
||||
|
||||
for b in (self._owner_read_button, self._group_read_button, self._others_read_button, self._owner_write_button,
|
||||
self._group_write_button, self._others_write_button, self._owner_exec_button, self._group_exec_button,
|
||||
self._others_exec_button):
|
||||
b.connect("toggled", self.update_num_value)
|
||||
|
||||
self.show_all()
|
||||
|
||||
@property
|
||||
def permissions(self):
|
||||
return self._num_value_entry.get_text()
|
||||
|
||||
def init_attrs(self, attrs):
|
||||
# Owner.
|
||||
self._owner_read_button.set_active(attrs[1] != "-")
|
||||
self._owner_write_button.set_active(attrs[2] != "-")
|
||||
self._owner_exec_button.set_active(attrs[3] != "-")
|
||||
# Group.
|
||||
self._group_read_button.set_active(attrs[4] != "-")
|
||||
self._group_write_button.set_active(attrs[5] != "-")
|
||||
self._group_exec_button.set_active(attrs[6] != "-")
|
||||
# Others.
|
||||
self._others_read_button.set_active(attrs[7] != "-")
|
||||
self._others_write_button.set_active(attrs[8] != "-")
|
||||
self._others_exec_button.set_active(attrs[9] != "-")
|
||||
|
||||
self.update_num_value()
|
||||
|
||||
def update_num_value(self, button=None):
|
||||
val = 0
|
||||
val |= stat.S_IRUSR if self._owner_read_button.get_active() else val
|
||||
val |= stat.S_IWUSR if self._owner_write_button.get_active() else val
|
||||
val |= stat.S_IXUSR if self._owner_exec_button.get_active() else val
|
||||
val |= stat.S_IRGRP if self._group_read_button.get_active() else val
|
||||
val |= stat.S_IWGRP if self._group_write_button.get_active() else val
|
||||
val |= stat.S_IXGRP if self._group_exec_button.get_active() else val
|
||||
val |= stat.S_IROTH if self._others_read_button.get_active() else val
|
||||
val |= stat.S_IWOTH if self._others_write_button.get_active() else val
|
||||
val |= stat.S_IXOTH if self._others_exec_button.get_active() else val
|
||||
|
||||
self._num_value_entry.set_text(f"{val:o}")
|
||||
|
||||
|
||||
class FtpClientBox(Gtk.HBox):
|
||||
""" Simple FTP client base class. """
|
||||
ROOT = ".."
|
||||
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
|
||||
|
||||
def __init__(self, app, settings, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_spacing(2)
|
||||
self.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
|
||||
self._app = app
|
||||
self._app.connect("data-receive", self.on_receive)
|
||||
self._app.connect("data-send", self.on_send)
|
||||
self._settings = settings
|
||||
self._ftp = None
|
||||
self._select_enabled = True
|
||||
|
||||
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_attr_change": self.on_ftp_attr_change,
|
||||
"on_ftp_copy": self.on_ftp_copy,
|
||||
"on_file_rename": self.on_file_rename,
|
||||
"on_file_renamed": self.on_file_renamed,
|
||||
"on_file_copy": self.on_file_copy,
|
||||
"on_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(f"{UI_RESOURCES_PATH}ftp.glade", handlers)
|
||||
|
||||
self.add(builder.get_object("main_ftp_box"))
|
||||
self._ftp_info_label = builder.get_object("ftp_info_label")
|
||||
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()
|
||||
|
||||
def on_receive(self, app, page):
|
||||
if page is Page.FTP:
|
||||
self.on_ftp_copy()
|
||||
|
||||
def on_send(self, app, page):
|
||||
if page is Page.FTP:
|
||||
self.on_file_copy()
|
||||
|
||||
@run_task
|
||||
def init_ftp(self):
|
||||
self.init_bookmarks()
|
||||
GLib.idle_add(self._ftp_model.clear)
|
||||
try:
|
||||
if self._ftp:
|
||||
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 = 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 = 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 = TextEditDialog(f_path, USE_HEADER_BAR)
|
||||
dialog.text = data
|
||||
ok = Gtk.ResponseType.OK
|
||||
if dialog.run() == ok and show_dialog(DialogType.QUESTION, self._app.app_window) == ok:
|
||||
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_ftp_attr_change(self, item):
|
||||
path = self.get_ftp_edit_path()
|
||||
if path:
|
||||
row = self._ftp_model[path]
|
||||
file = row[self.Column.NAME]
|
||||
if file == self.ROOT:
|
||||
self._app.show_error_message("Not allowed in this context!")
|
||||
return
|
||||
|
||||
attrs = row[self.Column.ATTR]
|
||||
if len(attrs) != 10:
|
||||
log(f"Init attributes error [{attrs}]. Invalid length!")
|
||||
return
|
||||
|
||||
dialog = AttributesDialog(attrs, USE_HEADER_BAR)
|
||||
ok = Gtk.ResponseType.OK
|
||||
if dialog.run() == ok and show_dialog(DialogType.QUESTION, self._app.app_window) == ok:
|
||||
log(self._ftp.sendcmd(f"SITE CHMOD {dialog.permissions} {file}"))
|
||||
f_data = self._ftp.sendcmd(f"STAT {file}").split()
|
||||
row[self.Column.ATTR] = f_data[2] if len(f_data) > 3 else attrs
|
||||
dialog.destroy()
|
||||
|
||||
def on_file_rename(self, renderer):
|
||||
model, paths = self._file_view.get_selection().get_selected_rows()
|
||||
if len(paths) > 1:
|
||||
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)
|
||||
|
||||
|
||||
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 |
1019
app/ui/imports.glade
Normal file
1019
app/ui/imports.glade
Normal file
File diff suppressed because it is too large
Load Diff
442
app/ui/imports.py
Normal file
442
app/ui/imports.py
Normal file
@@ -0,0 +1,442 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
from collections import defaultdict
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
|
||||
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, get_iptv_data
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, KeyboardKey, Column, Page, HeaderBar
|
||||
|
||||
|
||||
def import_bouquet(app, model, path, appender, file_path=None):
|
||||
""" Import of single bouquet """
|
||||
itr = model.get_iter(path)
|
||||
bq_type = BqType(model.get(itr, Column.BQ_TYPE)[0])
|
||||
pattern, f_pattern = None, None
|
||||
settings = app.app_settings
|
||||
transient = app.app_window
|
||||
services = app.current_services
|
||||
profile = settings.setting_type
|
||||
|
||||
if profile is SettingsType.ENIGMA_2:
|
||||
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(app, file_path, 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, app, path, appender, bouquets=None):
|
||||
handlers = {"on_import": self.on_import,
|
||||
"on_cursor_changed": self.on_cursor_changed,
|
||||
"on_service_changed": self.on_service_changed,
|
||||
"on_bq_selected_toggled": self.on_bq_selected_toggled,
|
||||
"on_sat_selected_toggled": self.on_sat_selected_toggled,
|
||||
"on_service_selected_toggled": self.on_service_selected_toggled,
|
||||
"on_services_model_changed": self.on_services_model_changed,
|
||||
"on_info_bar_close": self.on_info_bar_close,
|
||||
"on_select_all": self.on_select_all,
|
||||
"on_unselect_all": self.on_unselect_all,
|
||||
"on_sat_view_realize": self.on_sat_view_realize,
|
||||
"on_services_view_realize": self.on_services_view_realize,
|
||||
"on_popup_menu": on_popup_menu,
|
||||
"on_resize": self.on_resize,
|
||||
"on_main_paned_realize": self.on_main_paned_realize,
|
||||
"on_visible_page": self.on_visible_page,
|
||||
"on_bouquets_only_switch": self.on_bouquets_only_switch,
|
||||
"on_key_press": self.on_key_press}
|
||||
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}imports.glade", handlers)
|
||||
|
||||
self._app = app
|
||||
self._services = {}
|
||||
self._bq_services = {}
|
||||
self._sat_services = defaultdict(list)
|
||||
self._ids = self._app.current_services.keys()
|
||||
self._skip_import = defaultdict(set)
|
||||
self._append = appender
|
||||
self._profile = app.app_settings.setting_type
|
||||
self._settings = app.app_settings
|
||||
self._bouquets = bouquets
|
||||
self._current_bq = None
|
||||
self._current_sat = None
|
||||
self._existing_srv_background = None
|
||||
self._page = Page.SERVICES
|
||||
|
||||
self._dialog_window = builder.get_object("dialog_window")
|
||||
self._dialog_window.set_transient_for(app.app_window)
|
||||
self._info_bar = builder.get_object("info_bar")
|
||||
self._message_label = builder.get_object("message_label")
|
||||
# Options.
|
||||
self._replace_existing_switch = builder.get_object("replace_existing_switch")
|
||||
self._bouquets_only_switch = builder.get_object("bouquets_only_switch")
|
||||
self._bouquets_settings_box = builder.get_object("bouquets_settings_box")
|
||||
# Bouquets page.
|
||||
self._bq_model = builder.get_object("bq_list_store")
|
||||
self._bq_view = builder.get_object("bq_view")
|
||||
self._services_view = builder.get_object("services_view")
|
||||
self._services_model = builder.get_object("services_list_store")
|
||||
self._bouquets_count_label = builder.get_object("bouquets_count_label")
|
||||
self._services_count_label = builder.get_object("services_count_label")
|
||||
self._service_info_label = builder.get_object("service_info_label")
|
||||
self._service_exists_frame = builder.get_object("service_exists_frame")
|
||||
# Satellites page.
|
||||
self._sat_view = builder.get_object("sat_view")
|
||||
self._sat_model = builder.get_object("sat_list_store")
|
||||
self._sat_count_label = builder.get_object("sat_count_label")
|
||||
|
||||
if self._settings.use_header_bar:
|
||||
actions_box = builder.get_object("actions_box")
|
||||
builder.get_object("toolbar_box").set_visible(False)
|
||||
header_bar = HeaderBar()
|
||||
stack_switcher = builder.get_object("stack_switcher")
|
||||
actions_box.remove(stack_switcher)
|
||||
header_bar.set_custom_title(stack_switcher)
|
||||
button = builder.get_object("import_button")
|
||||
actions_box.remove(button)
|
||||
header_bar.pack_start(button)
|
||||
extra_box = builder.get_object("extra_header_box")
|
||||
actions_box.remove(extra_box)
|
||||
header_bar.pack_end(extra_box)
|
||||
|
||||
self._dialog_window.set_titlebar(header_bar)
|
||||
|
||||
window_size = self._settings.get("import_dialog_window_size")
|
||||
if window_size:
|
||||
self._dialog_window.resize(*window_size)
|
||||
|
||||
self.init_data(path)
|
||||
|
||||
def show(self):
|
||||
self._dialog_window.show()
|
||||
|
||||
@run_idle
|
||||
def init_data(self, path):
|
||||
self._bq_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._bq_model.append((bq.name, bq.type, True))
|
||||
self._bq_services[(bq.name, bq.type)] = bq.services
|
||||
self._bouquets_count_label.set_text(str(len(self._bq_model)))
|
||||
|
||||
if self._profile is SettingsType.ENIGMA_2:
|
||||
services = get_services(path, self._profile, 5 if self._settings.v5_support else 4)
|
||||
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(f"Import error [init data]: {e}")
|
||||
self.show_info_message(str(e), Gtk.MessageType.ERROR)
|
||||
|
||||
def on_import(self, item):
|
||||
if self._page is Page.SERVICES:
|
||||
if not any(r[-1] for r in self._bq_model):
|
||||
self.show_info_message(get_message("No selected item!"), Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
if not self._bouquets or show_dialog(DialogType.QUESTION, self._dialog_window) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
self.import_bouquets_data()
|
||||
else:
|
||||
self.import_satellites_data()
|
||||
|
||||
@run_idle
|
||||
def import_bouquets_data(self):
|
||||
""" Importing data into models. """
|
||||
if not self._bouquets:
|
||||
return
|
||||
|
||||
log("Importing data...")
|
||||
services = set()
|
||||
to_delete = set()
|
||||
for row in self._bq_model:
|
||||
bq = (row[0], row[1])
|
||||
if row[-1]:
|
||||
skip = self._skip_import[bq]
|
||||
for bq_srv in self._bq_services.get(bq, []):
|
||||
srv = self._services.get(bq_srv.data, None)
|
||||
if srv and srv.fav_id not in skip:
|
||||
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)
|
||||
else:
|
||||
skip = self._skip_import[(bq.name, bq.type)]
|
||||
bq_services = [srv for srv in bq.services if srv.data not in skip]
|
||||
bq.services.clear()
|
||||
bq.services.extend(bq_services)
|
||||
for bqs in self._bouquets:
|
||||
bq = bqs.bouquets
|
||||
for b in bqs_to_delete:
|
||||
with suppress(ValueError):
|
||||
bq.remove(b)
|
||||
|
||||
if self._bouquets_only_switch.get_active():
|
||||
services = ()
|
||||
else:
|
||||
services = list(filter(lambda s: s.fav_id not in self._ids, services))
|
||||
|
||||
self._append(self._bouquets, services)
|
||||
|
||||
if self._replace_existing_switch.get_active():
|
||||
self._app.emit("services_update", {s.fav_id: s for s in filter(lambda s: s.fav_id in self._ids, services)})
|
||||
|
||||
self._dialog_window.destroy()
|
||||
|
||||
def import_satellites_data(self):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog_window) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
replace_existing = self._replace_existing_switch.get_active()
|
||||
services = []
|
||||
current_services = self._app.current_services
|
||||
to_replace = {}
|
||||
|
||||
for row in self._sat_model:
|
||||
if row[-1]:
|
||||
sat = (row[0], row[1])
|
||||
skip = self._skip_import[sat]
|
||||
for s in filter(lambda srv: srv.fav_id not in skip, self._sat_services.get(sat[0], ())):
|
||||
if replace_existing and s.fav_id in self._ids:
|
||||
current_services[s.fav_id] = s
|
||||
to_replace[s.fav_id] = s
|
||||
elif s.fav_id not in self._ids:
|
||||
services.append(s)
|
||||
|
||||
self._append((), services)
|
||||
if to_replace:
|
||||
self._app.emit("services_update", to_replace)
|
||||
|
||||
self._dialog_window.destroy()
|
||||
|
||||
@run_idle
|
||||
def on_cursor_changed(self, view):
|
||||
self._services_model.clear()
|
||||
self._service_info_label.set_text("")
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if not paths:
|
||||
return
|
||||
|
||||
if self._page is Page.SERVICES:
|
||||
self._current_bq = model.get(model.get_iter(paths[0]), 0, 1)
|
||||
self.update_bq_services()
|
||||
else:
|
||||
self._current_sat = model.get(model.get_iter(paths[0]), 0, 1)
|
||||
self.update_sat_services()
|
||||
|
||||
self._services_count_label.set_text(str(len(self._services_model)))
|
||||
|
||||
def update_bq_services(self):
|
||||
bq_services = self._bq_services.get(self._current_bq)
|
||||
skip = self._skip_import[self._current_bq]
|
||||
|
||||
for bq_srv in bq_services:
|
||||
if bq_srv.type is BqServiceType.DEFAULT:
|
||||
srv = self._services.get(bq_srv.data, None)
|
||||
if srv:
|
||||
bg = self._existing_srv_background if srv.fav_id in self._ids else None
|
||||
self._services_model.append((srv.service, srv.service_type, srv.fav_id not in skip, bg, srv.fav_id))
|
||||
else:
|
||||
bg = self._existing_srv_background if bq_srv.data in self._ids else None
|
||||
self._services_model.append((bq_srv.name, bq_srv.type.value, bq_srv.data not in skip, bg, bq_srv.data))
|
||||
|
||||
def update_sat_services(self):
|
||||
sat_services = self._sat_services.get(self._current_sat[0])
|
||||
skip = self._skip_import[self._current_sat]
|
||||
for srv in sat_services:
|
||||
bg = self._existing_srv_background if srv.fav_id in self._ids else None
|
||||
self._services_model.append((srv.service, srv.service_type, srv.fav_id not in skip, bg, srv.fav_id))
|
||||
|
||||
def on_service_changed(self, view):
|
||||
path, column = view.get_cursor()
|
||||
if path:
|
||||
row = self._services_model[path][:]
|
||||
if row[1] == "IPTV":
|
||||
ref, url = get_iptv_data(row[-1])
|
||||
ref = f"{get_message('Service reference')}: {ref}"
|
||||
info = f"{get_message('Name')}: {row[0]}\n{ref}\nURL: {url}"
|
||||
self._service_info_label.set_text(info)
|
||||
else:
|
||||
srv = self._services.get(row[-1], None)
|
||||
self._service_info_label.set_text(self._app.get_hint_for_fav_list(srv) if srv else "")
|
||||
|
||||
def on_bq_selected_toggled(self, toggle, path):
|
||||
self._bq_model.set_value(self._bq_model.get_iter(path), 2, not toggle.get_active())
|
||||
|
||||
def on_sat_selected_toggled(self, toggle, path):
|
||||
self._sat_model.set_value(self._sat_model.get_iter(path), 2, not toggle.get_active())
|
||||
|
||||
def on_service_selected_toggled(self, toggle, path):
|
||||
self._services_model.set_value(self._services_model.get_iter(path), 2, not toggle.get_active())
|
||||
|
||||
def on_services_model_changed(self, model, path, itr):
|
||||
row = model[itr][:]
|
||||
fav_id = row[-1]
|
||||
skip = self._skip_import[self._current_bq if self._page is Page.SERVICES else self._current_sat]
|
||||
if row[2]:
|
||||
if fav_id in skip:
|
||||
skip.remove(fav_id)
|
||||
else:
|
||||
skip.add(fav_id)
|
||||
|
||||
@run_idle
|
||||
def show_info_message(self, text, message_type):
|
||||
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_sat_view_realize(self, view):
|
||||
if not self._services:
|
||||
return True
|
||||
|
||||
for srv in self._services.values():
|
||||
self._sat_services[srv.pos].append(srv)
|
||||
|
||||
list(map(lambda s: self._sat_model.append((s, None, True)), self._sat_services))
|
||||
self._sat_count_label.set_text(str(len(self._sat_model)))
|
||||
|
||||
def on_services_view_realize(self, view):
|
||||
if self._settings.use_colors:
|
||||
background = Gdk.RGBA()
|
||||
self._existing_srv_background = background if background.parse(self._settings.extra_color) else None
|
||||
self._service_exists_frame.modify_bg(Gtk.StateType.NORMAL, background.to_color())
|
||||
|
||||
def on_resize(self, window):
|
||||
if self._settings:
|
||||
self._settings.add("import_dialog_window_size", window.get_size())
|
||||
|
||||
def on_main_paned_realize(self, paned):
|
||||
width = paned.get_allocated_width()
|
||||
paned.set_position(width * 0.35)
|
||||
|
||||
def on_visible_page(self, stack, param):
|
||||
self._page = Page(stack.get_visible_child_name())
|
||||
self._bouquets_settings_box.set_sensitive(self._page is Page.SERVICES)
|
||||
|
||||
def on_bouquets_only_switch(self, switch, state):
|
||||
if state:
|
||||
self._replace_existing_switch.set_active(False)
|
||||
|
||||
def on_key_press(self, view, event):
|
||||
""" Handling keystrokes """
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
return
|
||||
key = KeyboardKey(key_code)
|
||||
|
||||
if key is KeyboardKey.SPACE:
|
||||
model = view.get_model()
|
||||
path, column = view.get_cursor()
|
||||
itr = model.get_iter(path)
|
||||
selected = model.get_value(itr, 2)
|
||||
model.set_value(itr, 2, not selected)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
1900
app/ui/iptv.glade
Normal file
1900
app/ui/iptv.glade
Normal file
File diff suppressed because it is too large
Load Diff
1106
app/ui/iptv.py
1106
app/ui/iptv.py
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.
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.
145
app/ui/logs.glade
Normal file
145
app/ui/logs.glade
Normal file
@@ -0,0 +1,145 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.18"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellite list editor. -->
|
||||
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkFrame" id="log_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0.49000000953674316</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="header_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">2</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="clear_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Clear</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<signal name="clicked" handler="on_clear" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="clear_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-clear</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="close_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Close</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_close" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="close_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="stock">gtk-close</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack_type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTextView" id="log_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="left_margin">5</property>
|
||||
<property name="right_margin">5</property>
|
||||
<property name="top_margin">5</property>
|
||||
<property name="bottom_margin">5</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="log_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Logs</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
74
app/ui/logs.py
Normal file
74
app/ui/logs.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import logging
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import LOGGER_NAME, LOG_FORMAT, LOG_DATE_FORMAT
|
||||
from app.ui.dialogs import get_builder
|
||||
from app.ui.main_helper import append_text_to_tview
|
||||
from app.ui.uicommons import Gtk, UI_RESOURCES_PATH
|
||||
|
||||
|
||||
class LogsClient(Gtk.Box):
|
||||
""" Logger GUI client. """
|
||||
|
||||
class LogHandler(logging.Handler):
|
||||
def __init__(self, view):
|
||||
logging.Handler.__init__(self)
|
||||
self._view = view
|
||||
self.setFormatter(logging.Formatter(fmt=LOG_FORMAT, datefmt=LOG_DATE_FORMAT))
|
||||
|
||||
def handle(self, rec: logging.LogRecord):
|
||||
GLib.idle_add(append_text_to_tview, f"{self.format(rec)}\n", self._view)
|
||||
|
||||
def __init__(self, app, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._app = app
|
||||
|
||||
handlers = {"on_clear": self.on_clear, "on_close": self.on_close}
|
||||
builder = get_builder(UI_RESOURCES_PATH + "logs.glade", handlers)
|
||||
|
||||
self._log_view = builder.get_object("log_view")
|
||||
self.pack_start(builder.get_object("log_frame"), True, True, 0)
|
||||
|
||||
logger = logging.getLogger(LOGGER_NAME)
|
||||
logger.addHandler(LogsClient.LogHandler(self._log_view))
|
||||
|
||||
self.show()
|
||||
|
||||
def on_clear(self, button):
|
||||
GLib.idle_add(self._log_view.get_buffer().set_text, "")
|
||||
|
||||
def on_close(self, button):
|
||||
self._app.change_action_state("on_logs_show", GLib.Variant.new_boolean(False))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
71
app/ui/mac_style.css
Normal file
71
app/ui/mac_style.css
Normal file
@@ -0,0 +1,71 @@
|
||||
* {
|
||||
background-clip: padding-box;
|
||||
-GtkScrolledWindow-scrollbar-spacing: 0;
|
||||
-GtkToolItemGroup-expander-size: 11;
|
||||
-GtkWidget-text-handle-width: 20;
|
||||
-GtkWidget-text-handle-height: 20;
|
||||
-GtkDialog-button-spacing: 12;
|
||||
-GtkDialog-action-area-border: 6;
|
||||
}
|
||||
|
||||
entry {
|
||||
min-height: 2.0em;
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
entry > image {
|
||||
padding-left: 0.3em;
|
||||
padding-right: 0.3em;
|
||||
}
|
||||
|
||||
button {
|
||||
min-height: 1.2em;
|
||||
min-width: 1.5em;
|
||||
padding-top: 0.3em;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
button:active, button:checked {
|
||||
color: @theme_selected_fg_color;
|
||||
background-image: linear-gradient(@theme_selected_bg_color, @theme_selected_bg_color);
|
||||
}
|
||||
|
||||
combobox {
|
||||
min-height: 2.2em;
|
||||
}
|
||||
|
||||
spinbutton {
|
||||
min-height: 1.5em;
|
||||
}
|
||||
|
||||
toolbutton {
|
||||
padding: 0.1em;
|
||||
}
|
||||
|
||||
spinner {
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
|
||||
infobar {
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
revealer > box > button {
|
||||
padding: 0.2em;
|
||||
}
|
||||
|
||||
switch slider {
|
||||
min-height: 1.5em;
|
||||
min-width: 1.5em;
|
||||
}
|
||||
|
||||
.font > box {
|
||||
min-height: 1.5em;
|
||||
padding-top: 0.1em;
|
||||
padding-bottom: 0.1em;
|
||||
}
|
||||
|
||||
.dialog-action-area button {
|
||||
margin-bottom: 0.6em;
|
||||
}
|
||||
5041
app/ui/main.glade
Normal file
5041
app/ui/main.glade
Normal file
File diff suppressed because it is too large
Load Diff
4594
app/ui/main.py
Normal file
4594
app/ui/main.py
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1794
app/ui/picons.glade
Normal file
1794
app/ui/picons.glade
Normal file
File diff suppressed because it is too large
Load Diff
1043
app/ui/picons.py
Normal file
1043
app/ui/picons.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,842 +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 pos -->
|
||||
<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="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="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="label2">
|
||||
<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="label3">
|
||||
<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="GtkNotebook" id="notebook">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<signal name="switch-page" handler="on_notebook_switch_page" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkBox" id="downloader_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">1</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="url_label">
|
||||
<property name="height_request">24</property>
|
||||
<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>
|
||||
<style>
|
||||
<class name="primary-toolbar"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="padding">1</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="url_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">network-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">1</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="pos_column">
|
||||
<property name="sizing">autosize</property>
|
||||
<property name="title" translatable="yes">Position</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="pos_cellrenderertext1">
|
||||
<property name="xalign">0.50999999046325684</property>
|
||||
<property name="editable">True</property>
|
||||
<signal name="edited" handler="on_position_edited" swapped="no"/>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">2</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="url_cellrenderertext"/>
|
||||
<attributes>
|
||||
<attribute name="text">3</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="onid_cellrenderertext"/>
|
||||
<attributes>
|
||||
<attribute name="text">4</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">5</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="tab">
|
||||
<object class="GtkLabel" id="downloader_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Downloader</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="tab_fill">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="converter_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="GtkLabel" id="converter_header_label">
|
||||
<property name="height_request">24</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Converter between name formats</property>
|
||||
<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="GtkGrid" id="converter_grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="row_spacing">5</property>
|
||||
<property name="column_spacing">2</property>
|
||||
<property name="column_homogeneous">True</property>
|
||||
<child>
|
||||
<object class="GtkFileChooserButton" id="enigma2_path_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="action">select-folder</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="picons_path_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Path to Enigma2 picons:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="save_to_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Path to save:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFileChooserButton" id="save_to_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="action">select-folder</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child type="tab">
|
||||
<object class="GtkLabel" id="converter_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="tooltip_text" translatable="yes">Converter between name formats</property>
|
||||
<property name="label" translatable="yes">Converter</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="position">1</property>
|
||||
<property name="tab_fill">False</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child type="tab">
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">8</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">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">False</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="convert_tool_button">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="is_important">True</property>
|
||||
<property name="label" translatable="yes">Convert</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="stock_id">gtk-execute</property>
|
||||
<signal name="clicked" handler="on_convert" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="homogeneous">True</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">True</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">False</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">Extra:</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">13</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="info_bar">
|
||||
<property name="app_paintable">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="spacing">2</property>
|
||||
<property name="show_close_button">True</property>
|
||||
<signal name="response" handler="on_info_bar_close" swapped="no"/>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox" id="infobar-action_area1">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="layout_style">end</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<object class="GtkBox" id="infobar-content_area1">
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="info_bar_message_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label">Info</property>
|
||||
<property name="justify">center</property>
|
||||
<property name="wrap">True</property>
|
||||
<property name="wrap_mode">word-char</property>
|
||||
<property name="lines">2</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">14</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">15</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkListStore" id="sat_position_list_store">
|
||||
<columns>
|
||||
<!-- column-name pos -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
</interface>
|
||||
@@ -1,288 +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.tools.picons import PiconsParser, parse_providers, Provider, convert_to
|
||||
from app.properties import Profile
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN
|
||||
from .dialogs import show_dialog, DialogType, get_message
|
||||
from .main_helper import update_entry_data, append_text_to_tview
|
||||
|
||||
|
||||
class PiconsDialog:
|
||||
def __init__(self, transient, options, picon_ids, profile=Profile.ENIGMA_2):
|
||||
self._picon_ids = picon_ids
|
||||
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._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,
|
||||
"on_position_edited": self.on_position_edited,
|
||||
"on_notebook_switch_page": self.on_notebook_switch_page,
|
||||
"on_convert": self.on_convert}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
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._convert_tool_button = builder.get_object("convert_tool_button")
|
||||
self._enigma2_path_button = builder.get_object("enigma2_path_button")
|
||||
self._save_to_button = builder.get_object("save_to_button")
|
||||
self._send_tool_button = builder.get_object("send_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.get(profile.value)
|
||||
self._profile = profile
|
||||
self._ip_entry.set_text(self._properties.get("host", ""))
|
||||
self._picons_entry.set_text(self._properties.get("picons_path", ""))
|
||||
self._picons_path = self._properties.get("picons_dir_path", "")
|
||||
self._picons_dir_entry.set_text(self._picons_path)
|
||||
self._enigma2_picons_path = self._picons_path
|
||||
if profile is Profile.NEUTRINO_MP:
|
||||
self._enigma2_picons_path = options.get(Profile.ENIGMA_2.value).get("picons_dir_path", "")
|
||||
if not len(self._picon_ids) and self._profile is Profile.ENIGMA_2:
|
||||
message = get_message("To automatically set the identifiers for picons,\n"
|
||||
"first load the required services list into the main application window.")
|
||||
self.show_info_message(message, Gtk.MessageType.WARNING)
|
||||
|
||||
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)
|
||||
model = self._providers_tree_view.get_model()
|
||||
model.clear()
|
||||
self.update_receive_button_state()
|
||||
self.append_providers(url, model)
|
||||
|
||||
@run_task
|
||||
def append_providers(self, url, model):
|
||||
self._current_process.wait()
|
||||
providers = parse_providers(self._TMP_DIR + url[url.find("w"):])
|
||||
if providers:
|
||||
for p in providers:
|
||||
model.append((self.get_pixbuf(p[0]), p.name, p.pos, 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)
|
||||
|
||||
providers = self.get_selected_providers()
|
||||
for prv in providers:
|
||||
if not prv[2] and prv[2][:-2].isdigit():
|
||||
self.show_info_message(
|
||||
get_message("Specify the correct position value for the provider!"), Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
for prv in providers:
|
||||
if self._terminate:
|
||||
break
|
||||
self.process_provider(Provider(*prv))
|
||||
|
||||
def process_provider(self, prv):
|
||||
url = prv.url
|
||||
self.show_info_message(get_message("Please, wait..."), Gtk.MessageType.INFO)
|
||||
self._current_process = subprocess.Popen(["wget", "-pkP", self._TMP_DIR, url],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
universal_newlines=True)
|
||||
GLib.io_add_watch(self._current_process.stderr, GLib.IO_IN, self.write_to_buffer)
|
||||
self._current_process.wait()
|
||||
path = self._TMP_DIR + self._BASE_URL + url[url.rfind("/") + 1:]
|
||||
pos = "".join(c for c in prv.pos if c.isdigit())
|
||||
PiconsParser.parse(path, self._picons_path, self._TMP_DIR, prv.on_id, pos,
|
||||
self._picon_ids, self.get_picons_format())
|
||||
self.resize(self._picons_path)
|
||||
self.show_info_message(get_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):
|
||||
append_text_to_tview(char, self._text_view)
|
||||
|
||||
def resize(self, path):
|
||||
if self._resize_no_radio_button.get_active():
|
||||
return
|
||||
|
||||
self.show_info_message(get_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(get_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(get_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), 5, 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)
|
||||
|
||||
def on_position_edited(self, render, path, value):
|
||||
model = self._providers_tree_view.get_model()
|
||||
model.set_value(model.get_iter(path), 2, value)
|
||||
|
||||
@run_idle
|
||||
def on_notebook_switch_page(self, nb, box, tab_num):
|
||||
self._load_providers_tool_button.set_visible(not tab_num)
|
||||
self._receive_tool_button.set_visible(not tab_num)
|
||||
self._convert_tool_button.set_visible(tab_num)
|
||||
self._send_tool_button.set_sensitive(not tab_num)
|
||||
|
||||
if self._enigma2_path_button.get_filename() is None:
|
||||
self._enigma2_path_button.set_current_folder(self._enigma2_picons_path)
|
||||
|
||||
@run_idle
|
||||
def on_convert(self, item):
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
picons_path = self._enigma2_path_button.get_filename()
|
||||
save_path = self._save_to_button.get_filename()
|
||||
if not picons_path or not save_path:
|
||||
show_dialog(DialogType.ERROR, transient=self._dialog, text="Select paths!")
|
||||
return
|
||||
|
||||
self._expander.set_expanded(True)
|
||||
convert_to(src_path=picons_path,
|
||||
dest_path=save_path,
|
||||
profile=Profile.ENIGMA_2,
|
||||
callback=self.append_output,
|
||||
done_callback=lambda: self.show_info_message(get_message("Done!"), Gtk.MessageType.INFO))
|
||||
|
||||
@run_idle
|
||||
def update_receive_button_state(self):
|
||||
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[5]]
|
||||
|
||||
@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>
|
||||
461
app/ui/playback.py
Normal file
461
app/ui/playback.py
Normal file
@@ -0,0 +1,461 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" 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, USE_HEADER_BAR
|
||||
from app.tools.media import Player
|
||||
from app.ui.dialogs import get_builder, get_message
|
||||
from app.ui.main_helper import get_iptv_url
|
||||
from app.ui.uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, Column, Page
|
||||
|
||||
|
||||
class PlayerBox(Gtk.Box):
|
||||
|
||||
def __init__(self, app, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
# Signals.
|
||||
GObject.signal_new("playback-full-screen", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
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,))
|
||||
GObject.signal_new("pause", self, GObject.SIGNAL_RUN_LAST,
|
||||
GObject.TYPE_PYOBJECT, (GObject.TYPE_PYOBJECT,))
|
||||
|
||||
self._app = app
|
||||
self._app.connect("fav-clicked", self.on_fav_clicked)
|
||||
self._app.connect("srv-clicked", self.on_srv_clicked)
|
||||
self._app.connect("iptv-clicked", self.on_iptv_clicked)
|
||||
self._app.connect("page-changed", self.on_page_changed)
|
||||
self._app.connect("play-current", self.on_play_current)
|
||||
self._app.connect("play-recording", self.on_play_recording)
|
||||
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
|
||||
|
||||
if len(self._fav_view.get_model()) == 0:
|
||||
return
|
||||
|
||||
self._fav_view.set_sensitive(False)
|
||||
if mode is FavClickMode.STREAM:
|
||||
self.on_play_stream()
|
||||
elif mode is FavClickMode.ZAP_PLAY:
|
||||
self._app.on_zap(self.on_watch)
|
||||
elif mode is FavClickMode.PLAY:
|
||||
self.on_play_service()
|
||||
|
||||
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 USE_HEADER_BAR:
|
||||
menu_bar = self._app.get_menubar()
|
||||
menu_bar.insert_section(1, None, audio_menu)
|
||||
menu_bar.insert_section(2, None, video_menu)
|
||||
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.BUTTON_PRESS:
|
||||
self.emit("pause", None)
|
||||
elif event.type == Gdk.EventType.DOUBLE_BUTTON_PRESS:
|
||||
self.on_full_screen()
|
||||
|
||||
def on_key_press(self, widget, event):
|
||||
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
|
||||
581
app/ui/recordings.glade
Normal file
581
app/ui/recordings.glade
Normal file
@@ -0,0 +1,581 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface domain="demon-editor">
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-description Enigma2 channel and satellites list editor. -->
|
||||
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkMenu" id="popup_menu">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="play_menu_item">
|
||||
<property name="label">gtk-media-play</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="activate" handler="on_play" swapped="no"/>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparatorMenuItem" id="menu_separator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkImageMenuItem" id="remove_menu_item">
|
||||
<property name="label">gtk-remove</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="use_underline">True</property>
|
||||
<property name="use_stock">True</property>
|
||||
<signal name="activate" handler="on_recording_remove" swapped="no"/>
|
||||
<accelerator key="Delete" signal="activate"/>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkListStore" id="rec_paths_model">
|
||||
<columns>
|
||||
<!-- column-name icon -->
|
||||
<column type="GdkPixbuf"/>
|
||||
<!-- column-name title -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name path -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkListStore" id="recordings_model">
|
||||
<columns>
|
||||
<!-- column-name logo -->
|
||||
<column type="GdkPixbuf"/>
|
||||
<!-- column-name service -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name title -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name time -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name length -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name file -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name desc -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name data -->
|
||||
<column type="PyObject"/>
|
||||
</columns>
|
||||
</object>
|
||||
<object class="GtkTreeModelFilter" id="recordings_filter_model">
|
||||
<property name="child_model">recordings_model</property>
|
||||
</object>
|
||||
<object class="GtkTreeModelSort" id="recordings_sort_model">
|
||||
<property name="model">recordings_filter_model</property>
|
||||
<signal name="row-deleted" handler="on_recordings_model_changed" swapped="no"/>
|
||||
<signal name="row-inserted" handler="on_recordings_model_changed" swapped="no"/>
|
||||
</object>
|
||||
<object class="GtkBox" id="recordings_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkPaned" id="recordings_paned">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="wide_handle">True</property>
|
||||
<child>
|
||||
<object class="GtkFrame" id="recordings_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0.49000000953674316</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="recordings_main_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">2</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="recordings_header_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkToggleButton" id="recordings_filter_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="focus_on_click">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Filter</property>
|
||||
<signal name="toggled" handler="on_recordings_filter_toggled" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="recordings_filter_button_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">edit-find-replace-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="recordings_remove_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="tooltip_text" translatable="yes">Remove</property>
|
||||
<property name="always_show_image">True</property>
|
||||
<signal name="clicked" handler="on_recording_remove" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage" id="remove_recording_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">user-trash-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="recordings_fs_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="spacing">10</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="recordings_filter_entry">
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">edit-find-replace-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="primary_icon_sensitive">False</property>
|
||||
<property name="visible" bind-source="recordings_filter_button" bind-property="active"/>
|
||||
<signal name="search-changed" handler="on_recordings_filter_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="recordings_search_box">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="recordings_search_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">edit-find-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="primary_icon_sensitive">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="recordings_search_down_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<child>
|
||||
<object class="GtkArrow" id="recordings_down_arrow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="arrow_type">down</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="recordings_search_up_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<child>
|
||||
<object class="GtkArrow" id="recordings_up_arrow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="arrow_type">up</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="group"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="recordings_view_scrolled_window">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="recordings_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">recordings_sort_model</property>
|
||||
<property name="fixed_height_mode">True</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<property name="enable_grid_lines">both</property>
|
||||
<property name="tooltip_column">6</property>
|
||||
<signal name="button-press-event" handler="on_popup_menu" object="popup_menu" swapped="no"/>
|
||||
<signal name="key-press-event" handler="on_recordings_key_press" swapped="no"/>
|
||||
<signal name="row-activated" handler="on_recordings_activated" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="recordings_view_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_service_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">150</property>
|
||||
<property name="title" translatable="yes">Service</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">1</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererPixbuf" id="rec_log_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="ypad">2</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_service_renderer">
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_title_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">150</property>
|
||||
<property name="title" translatable="yes">Title</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">2</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_title_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_time_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="fixed_width">180</property>
|
||||
<property name="min_width">100</property>
|
||||
<property name="title" translatable="yes">Time</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">3</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_time_renderer">
|
||||
<property name="xpad">5</property>
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_len_column">
|
||||
<property name="min_width">100</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="title" translatable="yes">Length</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">4</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_len_renderer">
|
||||
<property name="xalign">0.49000000953674316</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">4</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_file_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="min_width">100</property>
|
||||
<property name="title" translatable="yes">File</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">5</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_file_renderer">
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">5</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_desc_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="sizing">fixed</property>
|
||||
<property name="title" translatable="yes">Description</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">6</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="rec_desc_renderer">
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">6</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="recordings_status_box">
|
||||
<property name="height_request">26</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkImage" id="recordings_count_image">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="icon_name">document-properties</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="recordings_count_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="recordings_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Recordings</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">True</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame" id="recordings_paths_frame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label_xalign">0.49000000953674316</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="paths_view_scrolled_window">
|
||||
<property name="width_request">250</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="shadow_type">in</property>
|
||||
<property name="min_content_height">100</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="recordings_paths_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="model">rec_paths_model</property>
|
||||
<property name="headers_visible">False</property>
|
||||
<property name="search_column">1</property>
|
||||
<property name="rubber_banding">True</property>
|
||||
<property name="activate_on_single_click">True</property>
|
||||
<signal name="button-press-event" handler="on_path_press" swapped="no"/>
|
||||
<signal name="row-activated" handler="on_path_activated" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="rec_paths_selection">
|
||||
<property name="mode">multiple</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="rec_paths_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="min_width">100</property>
|
||||
<property name="title" translatable="yes">Paths</property>
|
||||
<property name="expand">True</property>
|
||||
<property name="clickable">True</property>
|
||||
<property name="alignment">0.5</property>
|
||||
<property name="sort_column_id">1</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererPixbuf" id="ftp_icon_column_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="pixbuf">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="ftp_name_column_renderer">
|
||||
<property name="xalign">0.019999999552965164</property>
|
||||
<property name="ellipsize">end</property>
|
||||
</object>
|
||||
<attributes>
|
||||
<attribute name="text">1</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="recordings_path_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Paths</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="resize">False</property>
|
||||
<property name="shrink">True</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
337
app/ui/recordings.py
Normal file
337
app/ui/recordings.py
Normal file
@@ -0,0 +1,337 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Module for working with recordings. """
|
||||
import os
|
||||
from datetime import datetime
|
||||
from ftplib import all_errors
|
||||
from io import BytesIO, TextIOWrapper
|
||||
from urllib.parse import quote
|
||||
|
||||
from app.ui.tasks import BGTaskWidget
|
||||
from .dialogs import get_builder, show_dialog, DialogType
|
||||
from .main_helper import get_base_paths, get_base_model, on_popup_menu
|
||||
from .uicommons import Gtk, Gdk, GLib, UI_RESOURCES_PATH, Column, KeyboardKey, Page
|
||||
from ..commons import run_task, run_idle, log
|
||||
from ..connections import UtfFTP, HttpAPI
|
||||
from ..settings import IS_DARWIN, PlayStreamsMode
|
||||
|
||||
|
||||
class RecordingsTool(Gtk.Box):
|
||||
ROOT = ".."
|
||||
DEFAULT_PATH = "/hdd"
|
||||
|
||||
def __init__(self, app, settings, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._app = app
|
||||
self._app.connect("layout-changed", self.on_layout_changed)
|
||||
self._app.connect("data-receive", self.on_data_receive)
|
||||
self._app.connect("profile-changed", self.init)
|
||||
|
||||
self._settings = settings
|
||||
self._ftp = None
|
||||
self._logos = {}
|
||||
# Icon.
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
icon = "folder-symbolic" if IS_DARWIN else "folder"
|
||||
self._icon = theme.load_icon(icon, 24, 0) if theme.lookup_icon(icon, 24, 0) else None
|
||||
|
||||
handlers = {"on_path_press": self.on_path_press,
|
||||
"on_path_activated": self.on_path_activated,
|
||||
"on_recordings_activated": self.on_recordings_activated,
|
||||
"on_play": self.on_play,
|
||||
"on_recording_remove": self.on_recording_remove,
|
||||
"on_recordings_model_changed": self.on_recordings_model_changed,
|
||||
"on_recordings_filter_changed": self.on_recordings_filter_changed,
|
||||
"on_recordings_filter_toggled": self.on_recordings_filter_toggled,
|
||||
"on_recordings_key_press": self.on_recordings_key_press,
|
||||
"on_popup_menu": on_popup_menu}
|
||||
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}recordings.glade", handlers)
|
||||
|
||||
self._rec_view = builder.get_object("recordings_view")
|
||||
self._paths_view = builder.get_object("recordings_paths_view")
|
||||
self._paned = builder.get_object("recordings_paned")
|
||||
self._model = builder.get_object("recordings_model")
|
||||
self._filter_model = builder.get_object("recordings_filter_model")
|
||||
self._filter_model.set_visible_func(self.recordings_filter_function)
|
||||
self._filter_entry = builder.get_object("recordings_filter_entry")
|
||||
self._recordings_count_label = builder.get_object("recordings_count_label")
|
||||
self.pack_start(builder.get_object("recordings_box"), True, True, 0)
|
||||
self._rec_view.get_model().set_sort_func(3, self.time_sort_func, 3)
|
||||
|
||||
srv_column = builder.get_object("rec_service_column")
|
||||
renderer = builder.get_object("rec_log_renderer")
|
||||
size = self._app.app_settings.list_picon_size
|
||||
renderer.set_fixed_size(size, size * 0.65)
|
||||
srv_column.set_cell_data_func(renderer, self.logo_data_func)
|
||||
|
||||
if settings.alternate_layout:
|
||||
self.on_layout_changed(app, True)
|
||||
|
||||
self.init()
|
||||
self.show()
|
||||
|
||||
def clear_data(self):
|
||||
self._model.clear()
|
||||
self._paths_view.get_model().clear()
|
||||
|
||||
def on_layout_changed(self, app, alt_layout):
|
||||
ch1 = self._paned.get_child1()
|
||||
ch2 = self._paned.get_child2()
|
||||
self._paned.remove(ch1)
|
||||
self._paned.remove(ch2)
|
||||
self._paned.add1(ch2)
|
||||
self._paned.add(ch1)
|
||||
|
||||
def on_data_receive(self, app, page):
|
||||
if page is Page.RECORDINGS:
|
||||
model, paths = self._rec_view.get_selection().get_selected_rows()
|
||||
if not paths:
|
||||
self._app.show_error_message("No selected item!")
|
||||
return
|
||||
|
||||
response = show_dialog(DialogType.CHOOSER, self._app.app_window, settings=self._settings,
|
||||
title="Open folder", create_dir=True)
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
|
||||
files = (model[p][5] for p in paths)
|
||||
bgw = BGTaskWidget(self._app, "Downloading recordings...", self.download_recordings, files, response)
|
||||
self._app.emit("add-background-task", bgw)
|
||||
|
||||
def download_recordings(self, files, dst):
|
||||
for file in files:
|
||||
try:
|
||||
with open(os.path.join(dst, os.path.basename(file)), "wb") as f:
|
||||
log(f"Downloading recording: {file}. Status: {self._ftp.download_binary(file, f)}".rstrip())
|
||||
except OSError as e:
|
||||
log(str(e))
|
||||
|
||||
@run_task
|
||||
def init(self, app=None, arg=None):
|
||||
GLib.idle_add(self.clear_data)
|
||||
try:
|
||||
if self._ftp:
|
||||
self._ftp.close()
|
||||
|
||||
self._ftp = UtfFTP(host=self._settings.host, user=self._settings.user, passwd=self._settings.password)
|
||||
self._ftp.encoding = "utf-8"
|
||||
except all_errors:
|
||||
pass # NOP
|
||||
else:
|
||||
self.init_paths(self.DEFAULT_PATH)
|
||||
|
||||
@run_idle
|
||||
def init_paths(self, path=None):
|
||||
self.clear_data()
|
||||
if not self._ftp:
|
||||
return
|
||||
|
||||
if path:
|
||||
try:
|
||||
self._ftp.cwd(path)
|
||||
except all_errors as e:
|
||||
pass
|
||||
|
||||
files = []
|
||||
try:
|
||||
self._ftp.dir(files.append)
|
||||
except all_errors as e:
|
||||
log(e)
|
||||
else:
|
||||
self.append_paths(files)
|
||||
|
||||
@run_idle
|
||||
def append_paths(self, files):
|
||||
model = self._paths_view.get_model()
|
||||
model.clear()
|
||||
model.append((None, self.ROOT, self._ftp.pwd()))
|
||||
|
||||
for f in files:
|
||||
f_data = self._ftp.get_file_data(f)
|
||||
if len(f_data) < 9:
|
||||
log(f"{__class__.__name__}. Folder data parsing error. [{f}]")
|
||||
continue
|
||||
|
||||
f_type = f_data[0][0]
|
||||
|
||||
if f_type == "d":
|
||||
model.append((self._icon, f_data[8], self._ftp.pwd()))
|
||||
|
||||
def on_path_activated(self, view, path, column):
|
||||
row = view.get_model()[path][:]
|
||||
path = f"{row[-1]}/{row[1]}/"
|
||||
self._app.send_http_request(HttpAPI.Request.RECORDINGS, quote(path), self.update_recordings_data)
|
||||
|
||||
def on_path_press(self, view, event):
|
||||
target = view.get_path_at_pos(event.x, event.y)
|
||||
if not target or event.button != Gdk.BUTTON_PRIMARY:
|
||||
return
|
||||
|
||||
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS:
|
||||
self.init_paths(self._paths_view.get_model()[target[0]][1])
|
||||
|
||||
@run_idle
|
||||
def update_recordings_data(self, recordings):
|
||||
self._model.clear()
|
||||
recs = recordings.get("recordings", [])
|
||||
list(map(self._model.append, (self.get_recordings_row(r) for r in recs)))
|
||||
list(map(self.get_rec_service_logo, recs))
|
||||
|
||||
def get_recordings_row(self, rec):
|
||||
service = rec.get("e2servicename")
|
||||
title = rec.get("e2title", "")
|
||||
r_time = datetime.fromtimestamp(int(rec.get("e2time", "0"))).strftime("%a, %x, %H:%M")
|
||||
length = rec.get("e2length", "0")
|
||||
file = rec.get("e2filename", "")
|
||||
desc = rec.get("e2description", "")
|
||||
|
||||
return None, service, title, r_time, length, file, desc, rec
|
||||
|
||||
def get_rec_service_logo(self, rec_data):
|
||||
if not rec_data.get("e2servicename", None):
|
||||
return
|
||||
|
||||
ref = rec_data.get("e2servicereference", None)
|
||||
logo = self._logos.get(rec_data.get("e2servicereference", None))
|
||||
|
||||
if not logo:
|
||||
file = rec_data.get("e2filename", None)
|
||||
if file:
|
||||
meta = f"RETR {file}.meta"
|
||||
io = BytesIO()
|
||||
try:
|
||||
self._ftp.retrbinary(meta, io.write)
|
||||
except all_errors:
|
||||
pass
|
||||
else:
|
||||
io.seek(0)
|
||||
f_ref, sep, name = TextIOWrapper(io, errors="ignore").readline().partition("::")
|
||||
self._logos[ref] = self._app.picons.get(f"{f_ref.replace(':', '_')}.png")
|
||||
|
||||
def on_recordings_activated(self, view, path, column):
|
||||
rec = view.get_model()[path][-1]
|
||||
self._app.send_http_request(HttpAPI.Request.STREAM_TS, rec.get("e2filename", ""), self.on_play_recording)
|
||||
|
||||
def on_play(self, item):
|
||||
path, column = self._rec_view.get_cursor()
|
||||
if not path:
|
||||
self._app.show_error_message("No selected item!")
|
||||
return
|
||||
|
||||
self.on_recordings_activated(self._rec_view, path, column)
|
||||
|
||||
def on_play_recording(self, m3u):
|
||||
url = self._app.get_url_from_m3u(m3u)
|
||||
if url:
|
||||
self._app.emit("play-recording", url)
|
||||
|
||||
def on_recording_remove(self, action=None, value=None):
|
||||
""" Removes recordings via FTP. """
|
||||
model, paths = self._rec_view.get_selection().get_selected_rows()
|
||||
if not paths:
|
||||
self._app.show_error_message("No selected item!")
|
||||
return
|
||||
|
||||
if show_dialog(DialogType.QUESTION, self._app.app_window) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
paths = get_base_paths(paths, model)
|
||||
model = get_base_model(model)
|
||||
to_delete = []
|
||||
|
||||
if paths and self._ftp:
|
||||
for file, itr in ((model[p][-1].get("e2filename", ""), model.get_iter(p)) for p in paths):
|
||||
resp = self._ftp.delete_file(file)
|
||||
if resp.startswith("2"):
|
||||
to_delete.append((itr, file))
|
||||
else:
|
||||
self._app.show_error_message(resp)
|
||||
break
|
||||
|
||||
[self.remove_meta_files(f) for i, f in to_delete if model.remove(i) or True]
|
||||
|
||||
@run_task
|
||||
def remove_meta_files(self, file):
|
||||
name, ex = os.path.splitext(file)
|
||||
[self._ftp.delete_file(f"{name}{suf}") for suf in (f"{ex}.ap", f"{ex}.cuts", f"{ex}.meta", f"{ex}.sc", ".eit")]
|
||||
|
||||
def on_recordings_model_changed(self, model, path, itr=None):
|
||||
self._recordings_count_label.set_text(str(len(model)))
|
||||
|
||||
def on_recordings_filter_changed(self, entry):
|
||||
self._filter_model.refilter()
|
||||
|
||||
def recordings_filter_function(self, model, itr, data):
|
||||
txt = self._filter_entry.get_text().upper()
|
||||
return next((s for s in model.get(itr, 1, 2, 3, 5, 6) if s and txt in s.upper()), False)
|
||||
|
||||
def on_recordings_filter_toggled(self, button):
|
||||
if not button.get_active():
|
||||
self._filter_entry.set_text("")
|
||||
|
||||
def on_recordings_key_press(self, view, event):
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
return
|
||||
|
||||
key = KeyboardKey(key_code)
|
||||
if key is KeyboardKey.DELETE:
|
||||
self.on_recording_remove()
|
||||
|
||||
def on_playback(self, box, state):
|
||||
""" Updates state of the UI elements for playback mode. """
|
||||
if self._settings.play_streams_mode is PlayStreamsMode.BUILT_IN:
|
||||
self._paned.set_orientation(Gtk.Orientation.VERTICAL)
|
||||
self.update_rec_columns_visibility(False)
|
||||
|
||||
def on_playback_close(self, box, state):
|
||||
""" Restores UI elements state after playback mode. """
|
||||
self._paned.set_orientation(Gtk.Orientation.HORIZONTAL)
|
||||
self.update_rec_columns_visibility(True)
|
||||
|
||||
def update_rec_columns_visibility(self, state):
|
||||
for c in (Column.REC_TIME, Column.REC_LEN, Column.REC_FILE, Column.REC_DESC):
|
||||
self._rec_view.get_column(c).set_visible(state)
|
||||
|
||||
def logo_data_func(self, column, renderer, model, itr, data):
|
||||
rec_data = model.get_value(itr, 7)
|
||||
renderer.set_property("pixbuf", self._logos.get(rec_data.get("e2servicereference", None)))
|
||||
|
||||
def time_sort_func(self, model, iter1, iter2, column):
|
||||
""" Custom sort function for time column. """
|
||||
rec1 = model.get_value(iter1, 7)
|
||||
rec2 = model.get_value(iter2, 7)
|
||||
|
||||
return int(rec1.get("e2time", "0")) - int(rec2.get("e2time", "0"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,670 +0,0 @@
|
||||
import re
|
||||
import time
|
||||
import concurrent.futures
|
||||
from math import fabs
|
||||
|
||||
from app.commons import run_idle, run_task
|
||||
from app.eparser import get_satellites, write_satellites, Satellite, Transponder
|
||||
from app.tools.satellites import SatellitesParser, SatelliteSource
|
||||
from .search import SearchProvider
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, MOVE_KEYS
|
||||
from .dialogs import show_dialog, DialogType, WaitDialog
|
||||
from .main_helper import move_items, scroll_to, append_text_to_tview, get_base_model
|
||||
|
||||
|
||||
def show_satellites_dialog(transient, options):
|
||||
SatellitesDialog(transient, options).show()
|
||||
|
||||
|
||||
class SatellitesDialog:
|
||||
_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_update": self.on_update,
|
||||
"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.set_translation_domain(TEXT_DOMAIN)
|
||||
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", "receive_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")
|
||||
self._wait_dialog = WaitDialog(self._dialog)
|
||||
# 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())
|
||||
|
||||
@run_idle
|
||||
def show(self):
|
||||
self._dialog.run()
|
||||
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._dialog.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 MOVE_KEYS:
|
||||
move_items(key, self._sat_view)
|
||||
elif key == Gdk.KEY_Left or key == Gdk.KEY_Right:
|
||||
view.do_unselect_all(view)
|
||||
|
||||
@run_idle
|
||||
def on_satellites_list_load(self, model):
|
||||
""" Load satellites data into model """
|
||||
try:
|
||||
self._wait_dialog.show()
|
||||
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)
|
||||
finally:
|
||||
self._wait_dialog.hide()
|
||||
|
||||
@run_idle
|
||||
def append_data(self, model, satellites):
|
||||
for sat in satellites:
|
||||
append_satellite(model, sat)
|
||||
|
||||
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()
|
||||
if len(paths) > 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()
|
||||
|
||||
for itr in [model.get_iter(path) for path in paths]:
|
||||
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)
|
||||
|
||||
def on_update(self, item):
|
||||
dialog = SatellitesUpdateDialog(self._dialog, self._sat_view.get_model())
|
||||
dialog.run()
|
||||
dialog.destroy()
|
||||
|
||||
@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)
|
||||
|
||||
|
||||
# ***************** 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}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
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
|
||||
|
||||
|
||||
# ***************** Satellite dialog *******************#
|
||||
|
||||
class SatelliteDialog:
|
||||
""" Shows dialog for adding or edit satellite """
|
||||
|
||||
def __init__(self, transient, satellite: Satellite = None):
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
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)
|
||||
|
||||
|
||||
# ***************** Satellite update dialog *******************#
|
||||
|
||||
class SatellitesUpdateDialog:
|
||||
""" Dialog for update satellites over internet """
|
||||
|
||||
def __init__(self, transient, main_model):
|
||||
handlers = {"on_update_satellites_list": self.on_update_satellites_list,
|
||||
"on_receive_satellites_list": self.on_receive_satellites_list,
|
||||
"on_cancel_receive": self.on_cancel_receive,
|
||||
"on_selected_toggled": self.on_selected_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_filter": self.on_filter,
|
||||
"on_search": self.on_search,
|
||||
"on_search_down": self.on_search_down,
|
||||
"on_search_up": self.on_search_up,
|
||||
"on_quit": self.on_quit}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
|
||||
("satellites_update_dialog", "update_source_store", "update_sat_list_store",
|
||||
"update_sat_list_model_filter", "update_sat_list_model_sort", "side_store",
|
||||
"pos_adjustment", "pos_adjustment2"))
|
||||
builder.connect_signals(handlers)
|
||||
|
||||
self._dialog = builder.get_object("satellites_update_dialog")
|
||||
self._dialog.set_transient_for(transient)
|
||||
self._main_model = main_model
|
||||
# self._dialog.get_content_area().set_border_width(0)
|
||||
self._sat_view = builder.get_object("sat_update_tree_view")
|
||||
self._source_box = builder.get_object("source_combo_box")
|
||||
self._sat_update_expander = builder.get_object("sat_update_expander")
|
||||
self._text_view = builder.get_object("text_view")
|
||||
self._receive_button = builder.get_object("receive_sat_list_tool_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")
|
||||
# Filter
|
||||
self._filter_info_bar = builder.get_object("sat_update_filter_info_bar")
|
||||
self._from_pos_button = builder.get_object("from_pos_button")
|
||||
self._to_pos_button = builder.get_object("to_pos_button")
|
||||
self._filter_from_combo_box = builder.get_object("filter_from_combo_box")
|
||||
self._filter_to_combo_box = builder.get_object("filter_to_combo_box")
|
||||
self._filter_model = builder.get_object("update_sat_list_model_filter")
|
||||
self._filter_model.set_visible_func(self.filter_function)
|
||||
self._filter_positions = (0, 0)
|
||||
# Search
|
||||
self._search_info_bar = builder.get_object("sat_update_search_info_bar")
|
||||
self._search_provider = SearchProvider((self._sat_view,),
|
||||
builder.get_object("sat_update_search_down_button"),
|
||||
builder.get_object("sat_update_search_up_button"))
|
||||
|
||||
self._download_task = False
|
||||
self._parser = None
|
||||
|
||||
def run(self):
|
||||
if self._dialog.run() == Gtk.ResponseType.CANCEL:
|
||||
self._download_task = False
|
||||
return
|
||||
|
||||
def destroy(self):
|
||||
self._dialog.destroy()
|
||||
|
||||
def on_update_satellites_list(self, item):
|
||||
if self._download_task:
|
||||
show_dialog(DialogType.ERROR, self._dialog, "The task is already running!")
|
||||
return
|
||||
|
||||
model = get_base_model(self._sat_view.get_model())
|
||||
model.clear()
|
||||
self._download_task = True
|
||||
src = self._source_box.get_active()
|
||||
if not self._parser:
|
||||
self._parser = SatellitesParser()
|
||||
|
||||
self.get_sat_list(src, self.append_satellites)
|
||||
|
||||
@run_task
|
||||
def get_sat_list(self, src, callback):
|
||||
sats = self._parser.get_satellites_list(SatelliteSource.FLYSAT if src == 0 else SatelliteSource.LYNGSAT)
|
||||
if sats:
|
||||
callback(sats)
|
||||
self._download_task = False
|
||||
|
||||
@run_idle
|
||||
def append_satellites(self, sats):
|
||||
model = get_base_model(self._sat_view.get_model())
|
||||
for sat in sats:
|
||||
model.append(sat)
|
||||
|
||||
@run_task
|
||||
def on_receive_satellites_list(self, item):
|
||||
if self._download_task:
|
||||
show_dialog(DialogType.ERROR, self._dialog, "The task is already running!")
|
||||
return
|
||||
self.receive_satellites()
|
||||
|
||||
@run_task
|
||||
def receive_satellites(self):
|
||||
self._download_task = True
|
||||
self._sat_update_expander.set_expanded(True)
|
||||
self._text_view.get_buffer().set_text("", 0)
|
||||
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._download_task:
|
||||
executor.shutdown()
|
||||
appender.send("\nCanceled\n")
|
||||
appender.close()
|
||||
return
|
||||
data = future.result()
|
||||
appender.send(text.format(data[0]))
|
||||
sats.append(data)
|
||||
|
||||
appender.send("-" * 75 + "\n")
|
||||
appender.send("Consumed : {:0.0f}s, {} satellites received.".format(start - time.time(), len(sats)))
|
||||
appender.close()
|
||||
# self.show_info_message(message, Gtk.MessageType.INFO)
|
||||
sats = {s[2]: s for s in sats} # key = position, v = satellite
|
||||
|
||||
for row in self._main_model:
|
||||
pos = row[-1]
|
||||
if pos in sats:
|
||||
sat = sats.pop(pos)
|
||||
itr = row.iter
|
||||
self.update_satellite(itr, row, sat)
|
||||
|
||||
for sat in sats.values():
|
||||
append_satellite(self._main_model, sat)
|
||||
|
||||
self._download_task = False
|
||||
|
||||
@run_idle
|
||||
def update_satellite(self, itr, row, sat):
|
||||
if self._main_model.iter_has_child(itr):
|
||||
children = row.iterchildren()
|
||||
for ch in children:
|
||||
self._main_model.remove(ch.iter)
|
||||
|
||||
for tr in sat[3]:
|
||||
self._main_model.append(itr, ["Transponder:", *tr, None, None])
|
||||
|
||||
def append_output(self):
|
||||
@run_idle
|
||||
def append(t):
|
||||
append_text_to_tview(t, self._text_view)
|
||||
|
||||
while True:
|
||||
text = yield
|
||||
append(text)
|
||||
|
||||
@run_idle
|
||||
def on_cancel_receive(self, item=None):
|
||||
self._download_task = False
|
||||
|
||||
def on_selected_toggled(self, toggle, path):
|
||||
s_model = self._sat_view.get_model()
|
||||
itr = self._filter_model.convert_iter_to_child_iter(s_model.convert_iter_to_child_iter(s_model.get_iter(path)))
|
||||
self._filter_model.get_model().set_value(itr, 4, not toggle.get_active())
|
||||
self.update_receive_button_state(self._filter_model)
|
||||
|
||||
@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_info_bar.set_visible(button.get_active())
|
||||
|
||||
def on_filter_toggled(self, button: Gtk.ToggleToolButton):
|
||||
self._filter_info_bar.set_visible(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, iter, 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(iter, 1)[0])) <= to_pos
|
||||
|
||||
def get_positions(self):
|
||||
from_pos = round(self._from_pos_button.get_value(), 1) * (-1 if self._filter_from_combo_box.get_active() else 1)
|
||||
to_pos = round(self._to_pos_button.get_value(), 1) * (-1 if self._filter_to_combo_box.get_active() else 1)
|
||||
return from_pos, to_pos
|
||||
|
||||
def on_search(self, entry):
|
||||
self._search_provider.search(entry.get_text())
|
||||
|
||||
def on_search_down(self, item):
|
||||
self._search_provider.on_search_down()
|
||||
|
||||
def on_search_up(self, item):
|
||||
self._search_provider.on_search_up()
|
||||
|
||||
def on_quit(self):
|
||||
self._download_task = False
|
||||
|
||||
|
||||
# ***************** Commons *******************#
|
||||
|
||||
@run_idle
|
||||
def append_satellite(model, sat):
|
||||
""" Common function for append satellite to the model """
|
||||
name, flags, pos, transponders = sat
|
||||
parent = model.append(None, [name, *(None,) * 9, flags, pos])
|
||||
for transponder in transponders:
|
||||
model.append(parent, ["Transponder:", *transponder, None, None])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
@@ -1,47 +1,58 @@
|
||||
""" This is helper module for search features """
|
||||
from app.commons import run_with_delay
|
||||
|
||||
|
||||
class SearchProvider:
|
||||
def __init__(self, views, down_button, up_button):
|
||||
def __init__(self, view, entry, down_button, up_button, columns=None):
|
||||
self._paths = []
|
||||
self._current_index = -1
|
||||
self._max_indexes = 0
|
||||
self._views = views
|
||||
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()
|
||||
for view in self._views:
|
||||
model = view.get_model()
|
||||
selection = view.get_selection()
|
||||
selection.unselect_all()
|
||||
if not text:
|
||||
continue
|
||||
model = self._view.get_model()
|
||||
selection = self._view.get_selection()
|
||||
if not selection:
|
||||
return
|
||||
|
||||
text = text.upper()
|
||||
for r in model:
|
||||
if text in str(r[:]).upper():
|
||||
path = r.path
|
||||
selection.select_path(r.path)
|
||||
self._paths.append((view, path))
|
||||
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()
|
||||
|
||||
def scroll_to(self, index):
|
||||
view, path = self._paths[index]
|
||||
view.scroll_to_cell(path, None)
|
||||
self.update_navigation_buttons()
|
||||
|
||||
def on_search_down(self):
|
||||
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):
|
||||
def on_search_up(self, button=None):
|
||||
if self._current_index > -1:
|
||||
self._current_index -= 1
|
||||
self.scroll_to(self._current_index)
|
||||
@@ -50,6 +61,13 @@ class SearchProvider:
|
||||
self._up_button.set_sensitive(self._current_index > 0)
|
||||
self._down_button.set_sensitive(self._current_index < self._max_indexes)
|
||||
|
||||
@run_with_delay(1)
|
||||
def on_search(self, entry):
|
||||
self.search(entry.get_text())
|
||||
|
||||
def on_search_toggled(self, action, value=None):
|
||||
self._entry.grab_focus() if action.get_active() else self._entry.set_text("")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,47 @@
|
||||
import re
|
||||
import os
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
from app.commons import run_idle
|
||||
|
||||
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
|
||||
from app.properties import Profile
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, HIDE_ICON, TEXT_DOMAIN, CODED_ICON
|
||||
from .dialogs import show_dialog, DialogType, Action
|
||||
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:
|
||||
@@ -20,8 +53,6 @@ class ServiceDetailsDialog:
|
||||
|
||||
_NEUTRINO_FAV_ID = "{:x}:{:x}:{:x}"
|
||||
|
||||
_NEUTRINO_TRANSPONDER_DATA = "{:04x}:{:04x}:{}:{}:{}:{}:{}:{}:{}"
|
||||
|
||||
_DIGIT_ENTRY_ELEMENTS = ("bitstream_entry", "pcm_entry", "video_pid_entry", "pcr_pid_entry", "srv_type_entry",
|
||||
"ac3_pid_entry", "ac3plus_pid_entry", "acc_pid_entry", "he_acc_pid_entry",
|
||||
"teletext_pid_entry", "pls_code_entry", "stream_id_entry", "tr_flag_entry",
|
||||
@@ -31,40 +62,42 @@ class ServiceDetailsDialog:
|
||||
|
||||
_DIGIT_ENTRY_NAME = "digit-entry"
|
||||
|
||||
def __init__(self, transient, options, srv_view, fav_view, services, bouquets, action=Action.EDIT):
|
||||
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_non_empty_entry_changed": self.on_non_empty_entry_changed,
|
||||
"on_cancel": lambda item: self._dialog.destroy()}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_from_file(UI_RESOURCES_PATH + "service_details_dialog.glade")
|
||||
builder.connect_signals(handlers)
|
||||
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(transient)
|
||||
self._profile = Profile(options["profile"])
|
||||
self._satellites_xml_path = options.get(self._profile.value)["data_dir_path"] + "satellites.xml"
|
||||
self._picons_dir_path = options.get(self._profile.value)["picons_dir_path"]
|
||||
self._services_view = srv_view
|
||||
self._fav_view = fav_view
|
||||
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 = services
|
||||
self._bouquets = bouquets
|
||||
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-z]{4})(,C:[0-9a-z]{4})*")
|
||||
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")
|
||||
@@ -100,6 +133,7 @@ class ServiceDetailsDialog:
|
||||
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")
|
||||
@@ -114,8 +148,10 @@ class ServiceDetailsDialog:
|
||||
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")
|
||||
@@ -130,7 +166,7 @@ class ServiceDetailsDialog:
|
||||
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_entry, self._rate_lp_combo_box, self._pos_side_box)
|
||||
|
||||
if self._action is Action.EDIT:
|
||||
self.update_data_elements()
|
||||
@@ -138,12 +174,7 @@ class ServiceDetailsDialog:
|
||||
self.init_default_data_elements()
|
||||
|
||||
def show(self):
|
||||
response = self._dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
pass
|
||||
self._dialog.destroy()
|
||||
|
||||
return response
|
||||
self._dialog.show()
|
||||
|
||||
@run_idle
|
||||
def init_default_data_elements(self):
|
||||
@@ -164,12 +195,26 @@ class ServiceDetailsDialog:
|
||||
|
||||
def update_data_elements(self):
|
||||
model, paths = self._services_view.get_selection().get_selected_rows()
|
||||
itr = model.get_iter(paths)
|
||||
# Unpacking to search for an iterator for the base model
|
||||
filter_model = model.get_model()
|
||||
itr = filter_model.convert_iter_to_child_iter(model.convert_iter_to_child_iter(itr))
|
||||
self._current_model = get_base_model(model)
|
||||
srv = Service(*self._current_model[itr][:])
|
||||
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
|
||||
@@ -177,17 +222,25 @@ class ServiceDetailsDialog:
|
||||
self._package_entry.set_text(srv.package)
|
||||
self._sid_entry.set_text(str(int(srv.ssid, 16)))
|
||||
# Transponder
|
||||
self._tr_type = TrType(srv.transponder_type)
|
||||
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)
|
||||
self.set_sat_positions(srv.pos)
|
||||
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._profile is Profile.ENIGMA_2:
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
self.init_enigma2_service_data(srv)
|
||||
self.init_enigma2_transponder_data(srv)
|
||||
elif self._profile is Profile.NEUTRINO_MP:
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
self.init_neutrino_data(srv)
|
||||
self.init_neutrino_ui_elements()
|
||||
|
||||
@@ -206,7 +259,7 @@ class ServiceDetailsDialog:
|
||||
def init_enigma2_flags(self, flags):
|
||||
f_flags = list(filter(lambda x: x.startswith("f:"), flags))
|
||||
if f_flags:
|
||||
value = int(f_flags[0][2:])
|
||||
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))
|
||||
@@ -220,6 +273,7 @@ class ServiceDetailsDialog:
|
||||
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)))
|
||||
@@ -232,60 +286,96 @@ class ServiceDetailsDialog:
|
||||
elif pid.startswith(Pids.AC3.value):
|
||||
self._ac3_pid_entry.set_text(str(int(pid[4:], 16)))
|
||||
elif pid.startswith(Pids.VIDEO_TYPE.value):
|
||||
pass
|
||||
extra_pids.append(pid)
|
||||
elif pid.startswith(Pids.AUDIO_CHANNEL.value):
|
||||
pass
|
||||
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):
|
||||
pass
|
||||
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(":")
|
||||
|
||||
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]))
|
||||
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)))
|
||||
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[5]).name)
|
||||
|
||||
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):
|
||||
tr_data = srv.transponder.split(":")
|
||||
self._transponder_id_entry.set_text(str(int(tr_data[0], 16)))
|
||||
self._network_id_entry.set_text(str(int(tr_data[1], 16)))
|
||||
self.select_active_text(self._invertion_combo_box, Inversion(tr_data[3]).name)
|
||||
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)
|
||||
self._builder.get_object("tr_grid").remove_column(7)
|
||||
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))
|
||||
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():
|
||||
@@ -318,6 +408,10 @@ class ServiceDetailsDialog:
|
||||
self.save_data()
|
||||
|
||||
def save_data(self):
|
||||
if self._s_type is SettingsType.NEUTRINO_MP and self._tr_type is not TrType.Satellite:
|
||||
show_dialog(DialogType.ERROR, transient=self._dialog, text="Not implemented yet!")
|
||||
return
|
||||
|
||||
if not self.is_data_correct():
|
||||
show_dialog(DialogType.ERROR, self._dialog, "Error. Verify the data!")
|
||||
return
|
||||
@@ -325,27 +419,63 @@ class ServiceDetailsDialog:
|
||||
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
self.on_edit() if self._action is Action.EDIT else self.on_new()
|
||||
self._dialog.destroy()
|
||||
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
|
||||
transponder = self._old_service.transponder
|
||||
if self._tr_edit_switch.get_active():
|
||||
transponder = self.get_transponder_data()
|
||||
if self._transponder_services_iters:
|
||||
self.update_transponder_services(transponder)
|
||||
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)
|
||||
@@ -361,32 +491,35 @@ class ServiceDetailsDialog:
|
||||
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):
|
||||
model.set(row.iter, {1: new_service.coded,
|
||||
2: new_service.service,
|
||||
3: new_service.locked,
|
||||
4: new_service.hide,
|
||||
5: new_service.service_type,
|
||||
6: new_service.pos,
|
||||
7: new_service.fav_id,
|
||||
8: new_service.picon})
|
||||
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):
|
||||
for file_name in os.listdir(self._picons_dir_path):
|
||||
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_dir_path, old_name)
|
||||
new_file = os.path.join(self._picons_dir_path, new_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
|
||||
|
||||
def on_new(self):
|
||||
service = self.get_service(*self.get_srv_data(), self.get_transponder_data())
|
||||
print(service)
|
||||
show_dialog(DialogType.ERROR, transient=self._dialog, text="Not implemented yet!")
|
||||
# ***************** 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="s",
|
||||
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,
|
||||
@@ -407,10 +540,12 @@ class ServiceDetailsDialog:
|
||||
transponder=transponder)
|
||||
|
||||
def get_flags(self):
|
||||
if self._profile is Profile.ENIGMA_2:
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
return self.get_enigma2_flags()
|
||||
elif self._profile is Profile.NEUTRINO_MP:
|
||||
return self._old_service.flags_cas
|
||||
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())]
|
||||
@@ -440,6 +575,9 @@ class ServiceDetailsDialog:
|
||||
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
|
||||
@@ -455,35 +593,50 @@ class ServiceDetailsDialog:
|
||||
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._profile is Profile.ENIGMA_2:
|
||||
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._profile is Profile.NEUTRINO_MP:
|
||||
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, self._old_service.data_id
|
||||
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()
|
||||
rate = self._rate_entry.get_text()
|
||||
pol = self._pol_combo_box.get_active_id()
|
||||
fec = self._fec_combo_box.get_active_id()
|
||||
system = self._sys_combo_box.get_active_id()
|
||||
pos = str(round(self._sat_pos_button.get_value(), 1))
|
||||
return freq, rate, pol, fec, system, pos
|
||||
o_srv = self._old_service
|
||||
|
||||
def get_transponder_data(self):
|
||||
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 = self._freq_entry.get_text()
|
||||
rate = self._rate_entry.get_text()
|
||||
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 = str(round(self._sat_pos_button.get_value(), 1)).replace(".", "")
|
||||
sat_pos = self.get_sat_position()
|
||||
|
||||
inv = get_value_by_name(Inversion, self._invertion_combo_box.get_active_id())
|
||||
srv_sys = "0" # !!!
|
||||
|
||||
if self._profile is Profile.ENIGMA_2:
|
||||
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
|
||||
@@ -496,25 +649,88 @@ class ServiceDetailsDialog:
|
||||
pls_code = self._pls_code_entry.get_text()
|
||||
st_id = self._stream_id_entry.get_text()
|
||||
pls = ":{}:{}:{}".format(st_id, pls_code, pls_mode) if pls_mode and pls_code and st_id else ""
|
||||
return "{}:{}:{}:{}:{}{}".format(dvb_s_tr, flag, mod, roll_off, pilot, pls)
|
||||
elif self._profile is Profile.NEUTRINO_MP:
|
||||
on_id, tr_id = int(self._network_id_entry.get_text()), int(self._transponder_id_entry.get_text())
|
||||
mod = self.get_value_from_combobox_id(self._mod_combo_box, MODULATION) if sys == "DVB-S2" else None
|
||||
srv_sys = None
|
||||
return self._NEUTRINO_TRANSPONDER_DATA.format(tr_id, on_id, freq, inv, rate, fec, pol, mod, srv_sys)
|
||||
|
||||
def update_transponder_services(self, transponder):
|
||||
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[-9], srv[-8], srv[-7], srv[-6], srv[-5], srv[-4] = self.get_transponder_values()
|
||||
srv[-1] = transponder
|
||||
srv = Service(*srv)
|
||||
self._services[srv.fav_id] = self._services.pop(srv.fav_id)._replace(transponder=transponder)
|
||||
self._current_model.set(itr, {i: v for i, v in enumerate(srv)})
|
||||
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: Gtk.ComboBox, text):
|
||||
def select_active_text(self, box, text):
|
||||
model = box.get_model()
|
||||
for index, row in enumerate(model):
|
||||
if row[0] == text:
|
||||
@@ -530,25 +746,28 @@ class ServiceDetailsDialog:
|
||||
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: Gtk.Switch, active):
|
||||
|
||||
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._current_model,
|
||||
self._services_view,
|
||||
self._old_service.transponder,
|
||||
self._transponder_services_iters).show()
|
||||
if response == Gtk.ResponseType.CANCEL or response == -4:
|
||||
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")
|
||||
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)
|
||||
@@ -562,42 +781,171 @@ class ServiceDetailsDialog:
|
||||
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._profile is Profile.NEUTRINO_MP):
|
||||
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 = 0 if self._srv_type_entry.get_text() == "2" else 1
|
||||
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._profile is Profile.ENIGMA_2:
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
on_id = int(self._namespace_entry.get_text())
|
||||
ref = "1:0:{}:{:X}:{:X}:{:X}:{:X}:0:0:0".format(srv_type, ssid, tid, nid, on_id)
|
||||
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, model, transponder, tr_iters):
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_file(UI_RESOURCES_PATH + "service_details_dialog.glade",
|
||||
("tr_services_dialog", "transponder_services_liststore"))
|
||||
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(model, transponder, tr_iters)
|
||||
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, model, transponder, tr_iters):
|
||||
def append_services(self, view, transponder, tr_iters):
|
||||
model = view.get_model()
|
||||
filter_model = model.get_model()
|
||||
for row in model:
|
||||
if row[-1] == transponder:
|
||||
self._srv_model.append((row[3], row[6], row[7], row[10], row[11], row[16]))
|
||||
tr_iters.append(model.get_iter(row.path))
|
||||
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()
|
||||
|
||||
3936
app/ui/settings_dialog.glade
Normal file
3936
app/ui/settings_dialog.glade
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,129 +1,881 @@
|
||||
from app.properties import write_config, Profile, get_default_settings
|
||||
from .uicommons import Gtk, UI_RESOURCES_PATH, TEXT_DOMAIN
|
||||
from .main_helper import update_entry_data
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
def show_settings_dialog(transient, options):
|
||||
return SettingsDialog(transient, options).show()
|
||||
import os
|
||||
import re
|
||||
from collections import Counter
|
||||
|
||||
from app.commons import run_task, run_idle, log
|
||||
from app.connections import test_telnet, test_ftp, TestException, test_http, HttpApiException
|
||||
from app.settings import SettingsType, Settings, PlayStreamsMode, IS_LINUX, SEP, IS_WIN
|
||||
from app.ui.dialogs import show_dialog, DialogType, get_message, get_chooser_dialog, get_builder
|
||||
from .main_helper import update_entry_data, scroll_to, get_picon_pixbuf
|
||||
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, FavClickMode, DEFAULT_ICON, APP_FONT, HeaderBar
|
||||
|
||||
|
||||
class SettingsDialog:
|
||||
_DIGIT_ENTRY_NAME = "digit-entry"
|
||||
_DIGIT_PATTERN = re.compile("(?:^[\\s]*$|\\D)")
|
||||
|
||||
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,
|
||||
def __init__(self, transient, settings: Settings):
|
||||
handlers = {"on_field_button_press": self.on_field_button_press,
|
||||
"on_settings_type_changed": self.on_settings_type_changed,
|
||||
"on_reset": self.on_reset,
|
||||
"apply_settings": self.apply_settings}
|
||||
"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_host_focus_in": self.on_host_focus_in,
|
||||
"on_host_focus_out": self.on_host_focus_out,
|
||||
"on_add_host": self.on_add_host,
|
||||
"on_remove_host": self.on_remove_host,
|
||||
"on_add_picon_path": self.on_add_picon_path,
|
||||
"on_remove_picon_path": self.on_remove_picon_path,
|
||||
"on_lang_changed": self.on_lang_changed,
|
||||
"on_main_settings_visible": self.on_main_settings_visible,
|
||||
"on_http_use_ssl_toggled": self.on_http_use_ssl_toggled,
|
||||
"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}
|
||||
|
||||
builder = Gtk.Builder()
|
||||
builder.set_translation_domain(TEXT_DOMAIN)
|
||||
builder.add_objects_from_file(UI_RESOURCES_PATH + "dialogs.glade",
|
||||
("settings_dialog", "telnet_timeout_adjustment"))
|
||||
builder.connect_signals(handlers)
|
||||
# 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_iter = None
|
||||
self._host_field = builder.get_object("host_field")
|
||||
self._hosts_box = builder.get_object("hosts_box")
|
||||
self._remove_host_button = builder.get_object("remove_host_button")
|
||||
self._port_field = builder.get_object("port_field")
|
||||
self._login_field = builder.get_object("login_field")
|
||||
self._password_field = builder.get_object("password_field")
|
||||
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._epg_dat_box = builder.get_object("epg_dat_box")
|
||||
self._picons_paths_box = builder.get_object("picons_paths_box")
|
||||
self._remove_picon_path_button = builder.get_object("remove_picon_path_button")
|
||||
# Paths.
|
||||
self._picons_path_field = builder.get_object("picons_path_field")
|
||||
self._data_path_field = builder.get_object("data_path_field")
|
||||
self._backup_path_field = builder.get_object("backup_path_field")
|
||||
self._recordings_path_field = builder.get_object("recordings_path_field")
|
||||
self._default_data_paths_switch = builder.get_object("default_data_paths_switch")
|
||||
self._default_data_paths_switch.bind_property("active", builder.get_object("picons_path_box"), "sensitive", 4)
|
||||
self._default_data_paths_switch.bind_property("active", builder.get_object("backup_path_box"), "sensitive", 4)
|
||||
# Info bar.
|
||||
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_check_button = builder.get_object("support_ver5_check_button")
|
||||
# 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._use_http_switch = builder.get_object("use_http_switch")
|
||||
self._remove_unused_bq_switch = builder.get_object("remove_unused_bq_switch")
|
||||
self._keep_power_mode_switch = builder.get_object("keep_power_mode_switch")
|
||||
self._compress_picons_switch = builder.get_object("compress_picons_switch")
|
||||
self._force_bq_name_switch = builder.get_object("force_bq_name_switch")
|
||||
self._support_ver5_switch = builder.get_object("support_ver5_switch")
|
||||
self._unlimited_buffer_switch = builder.get_object("unlimited_buffer_switch")
|
||||
self._enable_extensions_switch = builder.get_object("enable_extensions_switch")
|
||||
self._support_http_api_switch = builder.get_object("support_http_api_switch")
|
||||
self._enable_yt_dl_switch = builder.get_object("enable_yt_dl_switch")
|
||||
self._enable_update_yt_dl_switch = builder.get_object("enable_update_yt_dl_switch")
|
||||
self._enable_send_to_switch = builder.get_object("enable_send_to_switch")
|
||||
self._enable_exp_switch = builder.get_object("enable_experimental_switch")
|
||||
# Enigma2 only.
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("bq_naming_grid"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("program_frame"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("experimental_box"), "sensitive")
|
||||
self._enigma_radio_button.bind_property("active", builder.get_object("allow_double_click_box"), "sensitive")
|
||||
# Profiles.
|
||||
self._profile_view = builder.get_object("profile_tree_view")
|
||||
self._profile_add_button = builder.get_object("profile_add_button")
|
||||
self._profile_remove_button = builder.get_object("profile_remove_button")
|
||||
# Network.
|
||||
# Separated due to a bug with response (presumably in the builder) in ubuntu 18.04 and derivatives.
|
||||
builder.get_object("network_settings_frame").add(builder.get_object("network_grid"))
|
||||
# Style.
|
||||
style_provider = Gtk.CssProvider()
|
||||
style_provider.load_from_path(f"{UI_RESOURCES_PATH}style.css")
|
||||
screen = Gdk.Screen.get_default()
|
||||
self._digit_elems = (self._port_field, self._http_port_field, self._telnet_port_field, self._video_width_field,
|
||||
self._video_bitrate_field, self._video_height_field, self._audio_bitrate_field)
|
||||
[self.init_element_style(el, screen, style_provider) for el in self._digit_elems]
|
||||
self.init_element_style(self._host_field, screen, style_provider)
|
||||
|
||||
self._options = options
|
||||
self._active_profile = options.get("profile")
|
||||
self.set_settings()
|
||||
profile = Profile(self._active_profile)
|
||||
self._neutrino_radio_button.set_active(profile is Profile.NEUTRINO_MP)
|
||||
self._support_ver5_check_button.set_sensitive(profile is not Profile.NEUTRINO_MP)
|
||||
if self._settings.use_header_bar:
|
||||
switcher = builder.get_object("main_stack_switcher")
|
||||
switcher.set_margin_top(0)
|
||||
switcher.set_margin_bottom(0)
|
||||
builder.get_object("main_box").remove(switcher)
|
||||
header_bar = HeaderBar()
|
||||
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._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 init_element_style(self, elem, screen, provider):
|
||||
elem.get_style_context().add_provider_for_screen(screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
def update_title(self):
|
||||
title = "{} [{}]"
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
self._dialog.set_title(title.format(get_message("Options"), self._enigma_radio_button.get_label()))
|
||||
elif self._s_type is SettingsType.NEUTRINO_MP:
|
||||
self._dialog.set_title(title.format(get_message("Options"), self._neutrino_radio_button.get_label()))
|
||||
|
||||
def update_picon_paths(self):
|
||||
model = self._picons_paths_box.get_model()
|
||||
model.clear()
|
||||
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_button_press(self, entry):
|
||||
update_entry_data(entry, self._dialog, self._settings)
|
||||
|
||||
def on_profile_changed(self, item):
|
||||
profile = Profile.ENIGMA_2 if self._enigma_radio_button.get_active() else Profile.NEUTRINO_MP
|
||||
self.set_profile(profile)
|
||||
self._support_ver5_check_button.set_sensitive(profile is Profile.ENIGMA_2)
|
||||
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", ""))
|
||||
if Profile(self._active_profile) is Profile.ENIGMA_2:
|
||||
self._support_ver5_check_button.set_active(options.get("v5_support", False))
|
||||
self._s_type = self._settings.setting_type
|
||||
self._hosts_box.remove_all()
|
||||
self._remove_host_button.set_sensitive(len([self._hosts_box.append(h, h) for h in self._settings.hosts]) > 1)
|
||||
self._hosts_box.set_active_id(self._settings.host)
|
||||
self._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._epg_dat_box.set_active_id(self._settings.epg_dat_path)
|
||||
self._picons_paths_box.set_active_id(self._settings.picons_path)
|
||||
self._data_path_field.set_text(self._settings.default_data_path)
|
||||
self._picons_path_field.set_text(self._settings.default_picon_path)
|
||||
self._backup_path_field.set_text(self._settings.default_backup_path)
|
||||
self._recordings_path_field.set_text(self._settings.recordings_path)
|
||||
self._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 if self._enigma_radio_button.get_active() else Profile.NEUTRINO_MP
|
||||
self._active_profile = profile.value
|
||||
self._options["profile"] = self._active_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 profile is Profile.ENIGMA_2:
|
||||
options["v5_support"] = self._support_ver5_check_button.get_active()
|
||||
if self._s_type is SettingsType.ENIGMA_2:
|
||||
self._enable_exp_switch.set_active(self._settings.is_enable_experimental)
|
||||
self._support_ver5_switch.set_active(self._settings.v5_support)
|
||||
self._unlimited_buffer_switch.set_active(self._settings.unlimited_copy_buffer)
|
||||
self._enable_extensions_switch.set_active(self._settings.extensions_support)
|
||||
self._use_http_switch.set_active(self._settings.use_http)
|
||||
self._remove_unused_bq_switch.set_active(self._settings.remove_unused_bouquets)
|
||||
self._keep_power_mode_switch.set_active(self._settings.keep_power_mode)
|
||||
self._compress_picons_switch.set_active(self._settings.compress_picons)
|
||||
self._force_bq_name_switch.set_active(self._settings.force_bq_names)
|
||||
self._enable_yt_dl_switch.set_active(self._settings.enable_yt_dl)
|
||||
self._enable_update_yt_dl_switch.set_active(self._settings.enable_yt_dl_update)
|
||||
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.hosts = [h[1] for h in self._hosts_box.get_model()]
|
||||
self._settings.port = self._port_field.get_text()
|
||||
self._settings.user = self._login_field.get_text()
|
||||
self._settings.password = self._password_field.get_text()
|
||||
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.epg_dat_path = self._epg_dat_box.get_active_id()
|
||||
self._settings.picons_path = self._picons_paths_box.get_active_id()
|
||||
|
||||
def on_save_settings(self, item=None):
|
||||
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.recordings_path = self._recordings_path_field.get_text()
|
||||
self._ext_settings.activate_transcoding = self._transcoding_switch.get_active()
|
||||
self._ext_settings.active_preset = self._presets_combo_box.get_active_id()
|
||||
self._ext_settings.list_picon_size = int(self._picons_size_button.get_active_id())
|
||||
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()
|
||||
|
||||
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.unlimited_copy_buffer = self._unlimited_buffer_switch.get_active()
|
||||
self._ext_settings.extensions_support = self._enable_extensions_switch.get_active()
|
||||
self._ext_settings.use_http = self._use_http_switch.get_active()
|
||||
self._ext_settings.remove_unused_bouquets = self._remove_unused_bq_switch.get_active()
|
||||
self._ext_settings.keep_power_mode = self._keep_power_mode_switch.get_active()
|
||||
self._ext_settings.compress_picons = self._compress_picons_switch.get_active()
|
||||
self._ext_settings.force_bq_names = self._force_bq_name_switch.get_active()
|
||||
self._ext_settings.enable_yt_dl = self._enable_yt_dl_switch.get_active()
|
||||
self._ext_settings.enable_yt_dl_update = self._enable_update_yt_dl_switch.get_active()
|
||||
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._unlimited_buffer_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_host_focus_in(self, entry, event):
|
||||
self._host_iter = self._hosts_box.get_active_iter()
|
||||
|
||||
def on_host_focus_out(self, entry, event=None):
|
||||
if self._host_iter:
|
||||
model = self._hosts_box.get_model()
|
||||
host = entry.get_text()
|
||||
model.set_value(self._host_iter, 0, host)
|
||||
model.set_value(self._host_iter, 1, host)
|
||||
|
||||
if Counter(r[0] for r in model).get(host, 0) > 1:
|
||||
self._host_field.set_name(self._DIGIT_ENTRY_NAME)
|
||||
self.show_info_message("The host already exists!", Gtk.MessageType.WARNING)
|
||||
else:
|
||||
self._host_field.set_name("GtkEntry")
|
||||
self.on_info_bar_close()
|
||||
|
||||
def on_add_host(self, button):
|
||||
model = self._hosts_box.get_model()
|
||||
count = 1
|
||||
host = "127.0.0.1"
|
||||
hosts = {r[0] for r in model}
|
||||
|
||||
while host in hosts:
|
||||
count += 1
|
||||
host = f"127.0.0.{count}"
|
||||
|
||||
self._hosts_box.append(host, host)
|
||||
self._hosts_box.set_active_id(host)
|
||||
self._remove_host_button.set_sensitive(len(model) > 1)
|
||||
|
||||
def on_remove_host(self, button):
|
||||
self._hosts_box.remove(self._hosts_box.get_active())
|
||||
self._hosts_box.set_active(0)
|
||||
self._remove_host_button.set_sensitive(len(self._hosts_box.get_model()) > 1)
|
||||
|
||||
def on_add_picon_path(self, button):
|
||||
response = show_dialog(DialogType.INPUT, self._dialog, self._settings.picons_path)
|
||||
if response is Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
sep = "/"
|
||||
path = response if response.endswith(sep) else response + sep
|
||||
|
||||
if path in self._settings.picons_paths:
|
||||
self.show_info_message("This path already exists!", Gtk.MessageType.ERROR)
|
||||
return
|
||||
|
||||
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__":
|
||||
|
||||
107
app/ui/style.css
107
app/ui/style.css
@@ -1,3 +1,106 @@
|
||||
* {
|
||||
-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;
|
||||
}
|
||||
|
||||
#task-button {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#header-button {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#header-entry {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
#header-stack-switcher > button {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
buttonbox {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
paned > separator {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
paned.horizontal > separator {
|
||||
background-size: 2px 24px;
|
||||
}
|
||||
|
||||
paned.vertical > separator {
|
||||
background-size: 24px 2px;
|
||||
}
|
||||
|
||||
.red-button {
|
||||
background-image: none;
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
86
app/ui/tasks.py
Normal file
86
app/ui/tasks.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
from app.ui.dialogs import get_message
|
||||
from .uicommons import Gtk, GLib
|
||||
|
||||
|
||||
class BGTaskWidget(Gtk.Box):
|
||||
""" Widget for displaying and running background tasks. """
|
||||
|
||||
TASK_LIMIT = 1
|
||||
|
||||
def __init__(self, app, text, target, *args):
|
||||
super().__init__(spacing=2, orientation=Gtk.Orientation.HORIZONTAL, valign=Gtk.Align.CENTER)
|
||||
self._app = app
|
||||
|
||||
self._label = Gtk.Label(get_message(text))
|
||||
self.pack_start(self._label, False, False, 0)
|
||||
|
||||
self._spinner = Gtk.Spinner(active=True)
|
||||
self.pack_start(self._spinner, False, False, 0)
|
||||
|
||||
close_button = Gtk.Button.new_from_icon_name("window-close", Gtk.IconSize.MENU)
|
||||
close_button.set_relief(Gtk.ReliefStyle.NONE)
|
||||
close_button.set_valign(Gtk.Align.CENTER)
|
||||
close_button.set_tooltip_text(get_message("Cancel"))
|
||||
close_button.set_name("task-button")
|
||||
close_button.connect("clicked", lambda b: self._app.emit("task-cancel", self))
|
||||
self.pack_start(close_button, False, False, 0)
|
||||
|
||||
self.show_all()
|
||||
|
||||
# Just prototype. -> It may not work properly!
|
||||
# TODO: Different options need to be tested. Possibly with normal threads.
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
self._executor = ThreadPoolExecutor(max_workers=self.TASK_LIMIT)
|
||||
future = self._executor.submit(target, *args)
|
||||
future.add_done_callback(lambda f: GLib.idle_add(self._app.emit, "task-done", self))
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
return self._label.get_text()
|
||||
|
||||
@text.setter
|
||||
def text(self, value):
|
||||
self._label.set_text(value)
|
||||
|
||||
@property
|
||||
def tooltip(self):
|
||||
return self.get_tooltip_text()
|
||||
|
||||
@tooltip.setter
|
||||
def tooltip(self, value):
|
||||
self.set_tooltip_text(value)
|
||||
|
||||
def cancel(self):
|
||||
self._executor.shutdown(wait=False)
|
||||
self._app.emit("task-canceled", None)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pass
|
||||
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
|
||||
1765
app/ui/timers.glade
Normal file
1765
app/ui/timers.glade
Normal file
File diff suppressed because it is too large
Load Diff
561
app/ui/timers.py
Normal file
561
app/ui/timers.py
Normal file
@@ -0,0 +1,561 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
""" Module for working with timers. """
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from urllib.parse import quote
|
||||
|
||||
from app.settings import USE_HEADER_BAR
|
||||
from app.ui.main_helper import on_popup_menu
|
||||
from .dialogs import get_builder, get_message, show_dialog, DialogType
|
||||
from .uicommons import Gtk, Gdk, GLib, UI_RESOURCES_PATH, Page, Column, KeyboardKey, MOD_MASK
|
||||
from ..commons import run_idle, log
|
||||
from ..connections import HttpAPI
|
||||
from ..eparser.ecommons import BqServiceType
|
||||
|
||||
|
||||
class TimerTool(Gtk.Box):
|
||||
TIME_STR = "%Y-%m-%d %H:%M"
|
||||
|
||||
ACTION = {"0": "Record", "1": "Zap"}
|
||||
|
||||
AFTER_EVENT = {"0": "Do Nothing",
|
||||
"1": "Standby",
|
||||
"2": "Shut down",
|
||||
"3": "Auto"}
|
||||
|
||||
class TimerAction(Enum):
|
||||
ADD = 0
|
||||
EVENT = 1
|
||||
CHANGE = 2
|
||||
|
||||
class TimerDialog(Gtk.Dialog):
|
||||
def __init__(self, parent, action=None, timer_data=None, *args, **kwargs):
|
||||
super().__init__(use_header_bar=USE_HEADER_BAR, *args, **kwargs)
|
||||
|
||||
self._action = action or TimerTool.TimerAction.ADD
|
||||
self._timer_data = timer_data or {}
|
||||
self._request = ""
|
||||
|
||||
handlers = {"on_timer_begins_set": self.on_timer_begins_set,
|
||||
"on_timer_ends_set": self.on_timer_ends_set}
|
||||
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}timers.glade", handlers,
|
||||
objects=("timer_dialog_frame", "timer_ends_popover", "end_hour_adjustment",
|
||||
"min_end_adjustment", "timer_begins_popover", "begins_hour_adjustment",
|
||||
"min_begins_adjustment"))
|
||||
|
||||
self.set_title(get_message("Timer"))
|
||||
self.set_modal(True)
|
||||
self.set_skip_pager_hint(True)
|
||||
self.set_skip_taskbar_hint(True)
|
||||
self.set_transient_for(parent)
|
||||
self.set_position(Gtk.WindowPosition.CENTER_ON_PARENT)
|
||||
self.set_resizable(False)
|
||||
|
||||
self._timer_name_entry = builder.get_object("timer_name_entry")
|
||||
self._timer_desc_entry = builder.get_object("timer_desc_entry")
|
||||
self._timer_service_entry = builder.get_object("timer_service_entry")
|
||||
self._timer_service_ref_entry = builder.get_object("timer_service_ref_entry")
|
||||
self._timer_event_id_entry = builder.get_object("timer_event_id_entry")
|
||||
self._timer_begins_entry = builder.get_object("timer_begins_entry")
|
||||
self._timer_ends_entry = builder.get_object("timer_ends_entry")
|
||||
self._timer_begins_calendar = builder.get_object("timer_begins_calendar")
|
||||
self._timer_begins_hr_button = builder.get_object("timer_begins_hr_button")
|
||||
self._timer_begins_min_button = builder.get_object("timer_begins_min_button")
|
||||
self._timer_ends_calendar = builder.get_object("timer_ends_calendar")
|
||||
self._timer_ends_hr_button = builder.get_object("timer_ends_hr_button")
|
||||
self._timer_ends_min_button = builder.get_object("timer_ends_min_button")
|
||||
self._timer_enabled_switch = builder.get_object("timer_enabled_switch")
|
||||
self._timer_action_combo_box = builder.get_object("timer_action_combo_box")
|
||||
self._timer_after_combo_box = builder.get_object("timer_after_combo_box")
|
||||
self._days_buttons = (builder.get_object("timer_mo_check_button"),
|
||||
builder.get_object("timer_tu_check_button"),
|
||||
builder.get_object("timer_we_check_button"),
|
||||
builder.get_object("timer_th_check_button"),
|
||||
builder.get_object("timer_fr_check_button"),
|
||||
builder.get_object("timer_sa_check_button"),
|
||||
builder.get_object("timer_su_check_button"))
|
||||
|
||||
self._timer_location_switch = builder.get_object("timer_location_switch")
|
||||
self._timer_location_entry = builder.get_object("timer_location_entry")
|
||||
self._timer_location_switch.bind_property("active", self._timer_location_entry, "sensitive")
|
||||
# Disable DnD for timer entries.
|
||||
self._timer_name_entry.drag_dest_unset()
|
||||
self._timer_desc_entry.drag_dest_unset()
|
||||
self._timer_service_entry.drag_dest_unset()
|
||||
|
||||
self.add_buttons(get_message("Cancel"), Gtk.ResponseType.CANCEL, get_message("Save"), Gtk.ResponseType.OK)
|
||||
self.get_content_area().pack_start(builder.get_object("timer_dialog_frame"), True, True, 5)
|
||||
|
||||
if self._action is TimerTool.TimerAction.ADD:
|
||||
self.set_timer_for_add()
|
||||
elif self._action is TimerTool.TimerAction.CHANGE:
|
||||
self.set_timer_for_edit()
|
||||
elif self._action is TimerTool.TimerAction.EVENT:
|
||||
self.set_timer_from_event_data()
|
||||
else:
|
||||
log(f"{__class__.__name__} error: No action set for timer!")
|
||||
|
||||
@property
|
||||
def request(self):
|
||||
return self._request
|
||||
|
||||
def run(self):
|
||||
resp = super().run()
|
||||
if resp == Gtk.ResponseType.OK:
|
||||
self._request = self.get_request()
|
||||
return resp
|
||||
|
||||
def get_request(self):
|
||||
""" Constructs str representation of add/update request. """
|
||||
args = []
|
||||
t_data = self.get_timer_data()
|
||||
s_ref = quote(t_data.get("sRef", ""))
|
||||
|
||||
if self._action is TimerTool.TimerAction.EVENT:
|
||||
args.append(f"timeraddbyeventid?sRef={s_ref}")
|
||||
args.append(f"eventid={t_data.get('eit', '0')}")
|
||||
args.append(f"justplay={t_data.get('justplay', '')}")
|
||||
args.append(f"tags={''}")
|
||||
else:
|
||||
if self._action is TimerTool.TimerAction.ADD:
|
||||
args.append(f"timeradd?sRef={s_ref}")
|
||||
args.append(f"deleteOldOnSave={0}")
|
||||
elif self._action is TimerTool.TimerAction.CHANGE:
|
||||
args.append(f"timerchange?sRef={s_ref}")
|
||||
args.append(f"channelOld={s_ref}")
|
||||
args.append(f"beginOld={self._timer_data.get('e2timebegin', '0')}")
|
||||
args.append(f"endOld={self._timer_data.get('e2timeend', '0')}")
|
||||
args.append(f"deleteOldOnSave={1}")
|
||||
|
||||
args.append(f"begin={t_data.get('begin', '')}")
|
||||
args.append(f"end={t_data.get('end', '')}")
|
||||
args.append(f"name={quote(t_data.get('name', ''))}")
|
||||
args.append(f"description={quote(t_data.get('description', ''))}")
|
||||
args.append(f"tags={''}")
|
||||
args.append(f"eit={'0'}")
|
||||
args.append(f"disabled={t_data.get('disabled', '1')}")
|
||||
args.append(f"justplay={t_data.get('justplay', '1')}")
|
||||
args.append(f"afterevent={t_data.get('afterevent', '0')}")
|
||||
args.append(f"repeated={TimerTool.get_repetition_flags(self._days_buttons)}")
|
||||
|
||||
if self._timer_location_switch.get_active():
|
||||
args.append(f"dirname={self._timer_location_entry.get_text()}")
|
||||
|
||||
return "&".join(args)
|
||||
|
||||
def on_timer_begins_set(self, action, value=None):
|
||||
b_date = self.get_begins_date()
|
||||
if b_date > self.get_ends_date():
|
||||
self.set_ends_date(b_date + timedelta(hours=1))
|
||||
self.set_begins_date(b_date)
|
||||
|
||||
def on_timer_ends_set(self, action, value=None):
|
||||
self.set_ends_date(self.get_ends_date())
|
||||
|
||||
def get_begins_date(self):
|
||||
date = self._timer_begins_calendar.get_date()
|
||||
return datetime(year=date.year, month=date.month + 1, day=date.day,
|
||||
hour=int(self._timer_begins_hr_button.get_value()),
|
||||
minute=int(self._timer_begins_min_button.get_value()))
|
||||
|
||||
def set_begins_date(self, date):
|
||||
hour = date.hour
|
||||
minute = date.minute
|
||||
self._timer_begins_hr_button.set_value(hour)
|
||||
self._timer_begins_min_button.set_value(minute)
|
||||
self._timer_begins_calendar.select_day(date.day)
|
||||
self._timer_begins_calendar.select_month(date.month - 1, date.year)
|
||||
self._timer_begins_entry.set_text(f"{date.year}-{date.month:02d}-{date.day:02d} {hour:02d}:{minute:02d}")
|
||||
|
||||
def get_ends_date(self):
|
||||
date = self._timer_ends_calendar.get_date()
|
||||
return datetime(year=date.year, month=date.month + 1, day=date.day,
|
||||
hour=int(self._timer_ends_hr_button.get_value()),
|
||||
minute=int(self._timer_ends_min_button.get_value()))
|
||||
|
||||
def set_ends_date(self, date):
|
||||
hour = date.hour
|
||||
minute = date.minute
|
||||
self._timer_ends_hr_button.set_value(hour)
|
||||
self._timer_ends_min_button.set_value(minute)
|
||||
self._timer_ends_calendar.select_day(date.day)
|
||||
self._timer_ends_calendar.select_month(date.month - 1, date.year)
|
||||
self._timer_ends_entry.set_text(f"{date.year}-{date.month:02d}-{date.day:02d} {hour:02d}:{minute:02d}")
|
||||
|
||||
def set_timer_for_add(self):
|
||||
self._timer_service_entry.set_text(self._timer_data.get("e2servicename", ""))
|
||||
self._timer_service_ref_entry.set_text(self._timer_data.get("e2servicereference", ""))
|
||||
date = datetime.now()
|
||||
self.set_begins_date(date)
|
||||
self.set_ends_date(date + timedelta(hours=1))
|
||||
self._timer_event_id_entry.set_text("")
|
||||
self._timer_location_switch.set_active(False)
|
||||
TimerTool.set_repetition_flags(0, self._days_buttons)
|
||||
|
||||
def set_timer_for_edit(self):
|
||||
self._timer_name_entry.set_text(self._timer_data.get("e2name", ""))
|
||||
self._timer_desc_entry.set_text(self._timer_data.get("e2description", "") or "")
|
||||
self._timer_service_entry.set_text(self._timer_data.get("e2servicename", "") or "")
|
||||
self._timer_service_ref_entry.set_text(self._timer_data.get("e2servicereference", ""))
|
||||
self._timer_event_id_entry.set_text(self._timer_data.get("e2eit", ""))
|
||||
self._timer_enabled_switch.set_active((self._timer_data.get("e2disabled", "0") == "0"))
|
||||
self._timer_action_combo_box.set_active_id(self._timer_data.get("e2justplay", "0"))
|
||||
self._timer_after_combo_box.set_active_id(self._timer_data.get("e2afterevent", "0"))
|
||||
self.set_time_data(int(self._timer_data.get("e2timebegin", "0")),
|
||||
int(self._timer_data.get("e2timeend", "0")))
|
||||
location = self._timer_data.get("e2location", "")
|
||||
self._timer_location_entry.set_text("" if location == "None" else location)
|
||||
TimerTool.set_repetition_flags(int(self._timer_data.get("e2repeated", "0")), self._days_buttons)
|
||||
|
||||
def set_timer_from_event_data(self):
|
||||
self._timer_name_entry.set_text(self._timer_data.get("e2eventtitle", ""))
|
||||
self._timer_desc_entry.set_text(self._timer_data.get("e2eventdescription", ""))
|
||||
self._timer_service_entry.set_text(self._timer_data.get("e2eventservicename", ""))
|
||||
self._timer_service_ref_entry.set_text(self._timer_data.get("e2eventservicereference", ""))
|
||||
self._timer_event_id_entry.set_text(self._timer_data.get("e2eventid", ""))
|
||||
self._timer_action_combo_box.set_active_id("1")
|
||||
self._timer_after_combo_box.set_active_id("3")
|
||||
start_time = int(self._timer_data.get("e2eventstart", "0"))
|
||||
self.set_time_data(start_time, start_time + int(self._timer_data.get("e2eventduration", "0")))
|
||||
|
||||
def set_time_data(self, start_time, end_time):
|
||||
""" Sets values for time widgets. """
|
||||
now = datetime.now()
|
||||
ev_time_start = datetime.fromtimestamp(start_time) or now
|
||||
ev_time_end = datetime.fromtimestamp(end_time) or now + timedelta(hours=1)
|
||||
self._timer_begins_entry.set_text(ev_time_start.strftime(TimerTool.TIME_STR))
|
||||
self._timer_ends_entry.set_text(ev_time_end.strftime(TimerTool.TIME_STR))
|
||||
self._timer_begins_calendar.select_day(ev_time_start.day)
|
||||
self._timer_begins_calendar.select_month(ev_time_start.month - 1, ev_time_start.year)
|
||||
self._timer_ends_calendar.select_day(ev_time_end.day)
|
||||
self._timer_ends_calendar.select_month(ev_time_end.month - 1, ev_time_end.year)
|
||||
self._timer_begins_hr_button.set_value(ev_time_start.hour)
|
||||
self._timer_begins_min_button.set_value(ev_time_start.minute)
|
||||
self._timer_ends_hr_button.set_value(ev_time_end.hour)
|
||||
self._timer_ends_min_button.set_value(ev_time_end.minute)
|
||||
|
||||
def get_timer_data(self):
|
||||
""" Returns timer data as a dict. """
|
||||
return {"sRef": self._timer_service_ref_entry.get_text(),
|
||||
"begin": int(
|
||||
datetime.strptime(self._timer_begins_entry.get_text(), TimerTool.TIME_STR).timestamp()),
|
||||
"end": int(datetime.strptime(self._timer_ends_entry.get_text(), TimerTool.TIME_STR).timestamp()),
|
||||
"name": self._timer_name_entry.get_text(),
|
||||
"description": self._timer_desc_entry.get_text(),
|
||||
"dirname": "",
|
||||
"eit": self._timer_event_id_entry.get_text(),
|
||||
"disabled": int(not self._timer_enabled_switch.get_active()),
|
||||
"justplay": self._timer_action_combo_box.get_active_id(),
|
||||
"afterevent": self._timer_after_combo_box.get_active_id(),
|
||||
"repeated": TimerTool.get_repetition_flags(self._days_buttons)}
|
||||
|
||||
def __init__(self, app, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._app = app
|
||||
self._app.connect("page-changed", self.update_timer_list)
|
||||
# Icon.
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
icon = "alarm-symbolic"
|
||||
self._icon = theme.load_icon(icon, 16, 0) if theme.lookup_icon(icon, 16, 0) else None
|
||||
|
||||
handlers = {"on_timer_add": self.on_timer_add,
|
||||
"on_timer_edit": self.on_timer_edit,
|
||||
"on_timer_remove": self.on_timer_remove,
|
||||
"on_model_changed": self.on_model_changed,
|
||||
"on_timers_press": self.on_timers_press,
|
||||
"on_timers_key_press": self.on_timers_key_press,
|
||||
"on_timer_cursor_changed": self.on_timer_cursor_changed,
|
||||
"on_timers_drag_data_received": self.on_timers_drag_data_received}
|
||||
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}timers.glade", handlers,
|
||||
objects=("timers_frame", "timer_model", "popup_menu", "popup_menu_add_image"))
|
||||
|
||||
self._view = builder.get_object("timer_view")
|
||||
self._remove_button = builder.get_object("timer_remove_button")
|
||||
self._remove_button.bind_property("sensitive", builder.get_object("timer_edit_button"), "sensitive")
|
||||
self._remove_button.bind_property("sensitive", builder.get_object("edit_menu_item"), "sensitive")
|
||||
self._remove_button.bind_property("sensitive", builder.get_object("remove_menu_item"), "sensitive")
|
||||
self._info_button = builder.get_object("timer_info_check_button")
|
||||
self._info_button.bind_property("active", builder.get_object("timer_info_frame"), "visible")
|
||||
self._info_enabled_switch = builder.get_object("timer_info_enabled_switch")
|
||||
self._timers_count_label = builder.get_object("timers_count_label")
|
||||
self._ref_info_label = builder.get_object("timer_ref_value_label")
|
||||
self._event_id_info_label = builder.get_object("timer_event_id_value_label")
|
||||
self._begins_info_label = builder.get_object("timer_begins_value_label")
|
||||
self._ends_info_label = builder.get_object("timer_ends_value_label")
|
||||
self._action_info_label = builder.get_object("timer_action_value_label")
|
||||
self._after_info_label = builder.get_object("timer_after_value_label")
|
||||
self._timer_location_switch = builder.get_object("timer_location_switch")
|
||||
self._info_location_entry = builder.get_object("timer_info_location_entry")
|
||||
self._days_buttons = (builder.get_object("timer_info_mo_check_button"),
|
||||
builder.get_object("timer_info_tu_check_button"),
|
||||
builder.get_object("timer_info_we_check_button"),
|
||||
builder.get_object("timer_info_th_check_button"),
|
||||
builder.get_object("timer_info_fr_check_button"),
|
||||
builder.get_object("timer_info_sa_check_button"),
|
||||
builder.get_object("timer_info_su_check_button"))
|
||||
# Disable button presses.
|
||||
list(map(lambda b: b.connect("button-press-event", lambda bx, e: True), self._days_buttons))
|
||||
self._info_enabled_switch.connect("button-press-event", lambda b, e: True)
|
||||
# DnD initialization for the timer list.
|
||||
self._view.drag_dest_set(Gtk.DestDefaults.ALL, [], Gdk.DragAction.DEFAULT | Gdk.DragAction.COPY)
|
||||
self._view.drag_dest_add_text_targets()
|
||||
|
||||
self.pack_start(builder.get_object("timers_frame"), True, True, 0)
|
||||
self.show()
|
||||
|
||||
def update_timer_list(self, app, page):
|
||||
if page is Page.TIMERS:
|
||||
self._app.wait_dialog.show()
|
||||
self._app.send_http_request(HttpAPI.Request.TIMER_LIST, "", self.update_timers_data)
|
||||
|
||||
@run_idle
|
||||
def update_timers_data(self, timers):
|
||||
model = self._view.get_model()
|
||||
model.clear()
|
||||
list(map(model.append, (self.get_timer_row(t) for t in timers.get("timer_list", []))))
|
||||
self._remove_button.set_sensitive(len(model))
|
||||
self._app.wait_dialog.hide()
|
||||
|
||||
def get_timer_row(self, timer):
|
||||
disabled = self._icon if timer.get("e2disabled", "0") == "0" else None
|
||||
name = timer.get("e2name", "") or ""
|
||||
description = timer.get("e2description", "") or ""
|
||||
service = timer.get("e2servicename", "") or ""
|
||||
start_time = datetime.fromtimestamp(int(timer.get("e2timebegin", "0")))
|
||||
end_time = datetime.fromtimestamp(int(timer.get("e2timeend", "0")))
|
||||
time = f"{start_time.strftime('%a, %x, %H:%M')} - {end_time.strftime('%H:%M')}"
|
||||
|
||||
return disabled, name, service, time, description, timer
|
||||
|
||||
def on_timer_add(self, timer=None, value=None):
|
||||
model, paths = self._app.fav_view.get_selection().get_selected_rows()
|
||||
p_count = len(paths)
|
||||
|
||||
if p_count == 1:
|
||||
service = self._app.current_services.get(model[paths][Column.FAV_ID], None)
|
||||
if service:
|
||||
self.add_timer({"e2servicename": service.service,
|
||||
"e2servicereference": service.picon_id.rstrip(".png").replace("_", ":")})
|
||||
elif p_count > 1:
|
||||
self._app.show_error_message("Please, select only one item!")
|
||||
else:
|
||||
self._app.show_error_message("No selected item!")
|
||||
|
||||
def add_timer(self, timer_data):
|
||||
dialog = self.TimerDialog(self._app.app_window, self.TimerAction.ADD, timer_data)
|
||||
response = dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
self._app.send_http_request(HttpAPI.Request.TIMER, dialog.request, self.timer_add_edit_callback)
|
||||
dialog.destroy()
|
||||
|
||||
def on_timer_edit(self, action=None, value=None):
|
||||
model, paths = self._view.get_selection().get_selected_rows()
|
||||
if len(paths) > 1:
|
||||
self._app.show_error_message("Please, select only one item!")
|
||||
return
|
||||
|
||||
dialog = self.TimerDialog(self._app.app_window, self.TimerAction.CHANGE, model[paths][-1])
|
||||
response = dialog.run()
|
||||
if response == Gtk.ResponseType.OK:
|
||||
self._app.send_http_request(HttpAPI.Request.TIMER, dialog.request, self.timer_add_edit_callback)
|
||||
dialog.destroy()
|
||||
|
||||
@run_idle
|
||||
def timer_add_edit_callback(self, resp):
|
||||
if "error_code" in resp:
|
||||
msg = f"Error getting timer status.\n{resp.get('error_code')}"
|
||||
self._app.show_error_message(msg)
|
||||
log(msg)
|
||||
return
|
||||
|
||||
state = resp.get("e2state", None)
|
||||
if state == "False":
|
||||
msg = resp.get("e2statetext", "")
|
||||
self._app.show_error_message(msg)
|
||||
log(msg)
|
||||
if state == "True":
|
||||
msg = resp.get("e2statetext", "")
|
||||
log(msg)
|
||||
self._app.show_info_message(msg, Gtk.MessageType.INFO)
|
||||
self.update_timer_list(self._app, Page.TIMERS)
|
||||
else:
|
||||
log("Error getting timer status. No response!")
|
||||
|
||||
def on_timer_remove(self, action=None, value=None):
|
||||
model, paths = self._view.get_selection().get_selected_rows()
|
||||
if not paths or show_dialog(DialogType.QUESTION, self._app.app_window) != Gtk.ResponseType.OK:
|
||||
return
|
||||
|
||||
refs = {}
|
||||
for path in paths:
|
||||
timer = model[path][-1]
|
||||
ref = "timerdelete?sRef={}&begin={}&end={}".format(quote(timer.get("e2servicereference", "")),
|
||||
timer.get("e2timebegin", ""),
|
||||
timer.get("e2timeend", ""))
|
||||
refs[ref] = model.get_iter(path)
|
||||
|
||||
self._app.wait_dialog.show("Deleting data...")
|
||||
gen = self.remove_timers(refs)
|
||||
GLib.idle_add(lambda: next(gen, False))
|
||||
|
||||
def remove_timers(self, refs):
|
||||
tasks = list(refs)
|
||||
removed = set()
|
||||
for ref in refs:
|
||||
yield from self.remove_timer(ref, removed, tasks)
|
||||
|
||||
while tasks:
|
||||
yield True
|
||||
|
||||
model = self._view.get_model()
|
||||
list(map(model.remove, (refs[ref] for ref in refs if ref in removed)))
|
||||
self._app.wait_dialog.hide()
|
||||
self._remove_button.set_sensitive(len(model))
|
||||
yield True
|
||||
|
||||
def remove_timer(self, ref, removed, tasks=None):
|
||||
def callback(resp):
|
||||
if resp.get("e2state", "") == "True":
|
||||
log(resp.get("e2statetext", ""))
|
||||
removed.add(ref)
|
||||
else:
|
||||
log(resp.get("e2statetext", None) or "Timer deletion error.")
|
||||
if tasks:
|
||||
tasks.pop()
|
||||
|
||||
self._app.send_http_request(HttpAPI.Request.TIMER, ref, callback)
|
||||
yield True
|
||||
|
||||
def on_model_changed(self, model, path, itr=None):
|
||||
self._timers_count_label.set_text(str(len(model)))
|
||||
|
||||
def on_timers_press(self, menu, event):
|
||||
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS and len(self._view.get_model()) > 0:
|
||||
self.on_timer_edit()
|
||||
else:
|
||||
on_popup_menu(menu, event)
|
||||
|
||||
def on_timers_key_press(self, view, event):
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
return
|
||||
|
||||
key = KeyboardKey(key_code)
|
||||
ctrl = event.state & MOD_MASK
|
||||
|
||||
if key is KeyboardKey.DELETE:
|
||||
self.on_timer_remove()
|
||||
elif key is KeyboardKey.INSERT:
|
||||
self.on_timer_add()
|
||||
elif ctrl and key is KeyboardKey.E:
|
||||
self.on_timer_edit()
|
||||
|
||||
def on_timer_cursor_changed(self, view):
|
||||
path, column = view.get_cursor()
|
||||
if not path:
|
||||
return
|
||||
|
||||
timer = view.get_model()[path][-1]
|
||||
self._info_enabled_switch.set_active((timer.get("e2disabled", "0") == "0"))
|
||||
self._ref_info_label.set_text(timer.get("e2servicereference", ""))
|
||||
self._event_id_info_label.set_text(timer.get("e2eit", ""))
|
||||
self._action_info_label.set_text(get_message(self.ACTION.get(timer.get("e2justplay", "0"), "0")))
|
||||
self._after_info_label.set_text(get_message(self.AFTER_EVENT.get(timer.get("e2afterevent", "0"), "0")))
|
||||
self._begins_info_label.set_text(str(datetime.fromtimestamp(int(timer.get("e2timebegin", "0")))))
|
||||
self._ends_info_label.set_text(str(datetime.fromtimestamp(int(timer.get("e2timeend", "0")))))
|
||||
self.set_repetition_flags(int(timer.get("e2repeated", "0")), self._days_buttons)
|
||||
location = timer.get("e2location", "")
|
||||
self._info_location_entry.set_text("" if location == "None" else location)
|
||||
|
||||
@staticmethod
|
||||
def get_repetition_flags(boxes):
|
||||
""" Returns flags for repetition.
|
||||
|
||||
@param boxes: Buttons tuple for the days of the week.
|
||||
"""
|
||||
day_flags = 0
|
||||
for i, box in enumerate(boxes):
|
||||
if box.get_active():
|
||||
day_flags = day_flags | (1 << i)
|
||||
|
||||
return day_flags
|
||||
|
||||
@staticmethod
|
||||
def set_repetition_flags(flags, boxes):
|
||||
""" Sets flags for repetition.
|
||||
|
||||
@param flags: Flags value.
|
||||
@param boxes: Buttons tuple for the days of the week.
|
||||
"""
|
||||
for i, box in enumerate(boxes):
|
||||
box.set_active(flags & 1 == 1)
|
||||
flags = flags >> 1
|
||||
|
||||
# ***************** Drag-and-drop ********************* #
|
||||
|
||||
def on_timers_drag_data_received(self, box, context, x, y, data, info, time):
|
||||
txt = data.get_text()
|
||||
if txt:
|
||||
itr_str, sep, source = txt.partition(self._app.DRAG_SEP)
|
||||
if not source:
|
||||
return
|
||||
|
||||
itrs = itr_str.split(",")
|
||||
if len(itrs) > 1:
|
||||
self._app.show_error_message("Please, select only one item!")
|
||||
return
|
||||
|
||||
fav_id = None
|
||||
if source == self._app.FAV_MODEL:
|
||||
model = self._app.fav_view.get_model()
|
||||
fav_id = model.get_value(model.get_iter_from_string(itrs[0]), Column.FAV_ID)
|
||||
elif source == self._app.SERVICE_MODEL:
|
||||
model = self._app.services_view.get_model()
|
||||
fav_id = model.get_value(model.get_iter_from_string(itrs[0]), Column.SRV_FAV_ID)
|
||||
|
||||
service = self._app.current_services.get(fav_id, None)
|
||||
if service:
|
||||
if service.service_type == BqServiceType.ALT.name:
|
||||
msg = "Alternative service.\n\n {get_message('Not implemented yet!')}"
|
||||
show_dialog(DialogType.ERROR, transient=self._app.app_window, text=msg)
|
||||
context.finish(False, False, time)
|
||||
return
|
||||
|
||||
self.add_timer({"e2servicename": service.service,
|
||||
"e2servicereference": service.picon_id.rstrip(".png").replace("_", ":")})
|
||||
|
||||
context.finish(True, False, time)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
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
|
||||
@@ -1,45 +1,210 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
import locale
|
||||
import os
|
||||
from enum import Enum, IntEnum
|
||||
from functools import lru_cache
|
||||
|
||||
import gi
|
||||
from enum import Enum
|
||||
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk, Gdk
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("Gdk", "3.0")
|
||||
from gi.repository import Gtk, Gdk, GLib
|
||||
|
||||
# path to *.glade files
|
||||
UI_RESOURCES_PATH = "app/ui/" if os.path.exists("app/ui/") else "/usr/share/demoneditor/app/ui/"
|
||||
from app.settings import Settings, SettingsException, IS_DARWIN, GTK_PATH, IS_LINUX
|
||||
|
||||
# translation
|
||||
# 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"
|
||||
if UI_RESOURCES_PATH == "app/ui/":
|
||||
LANG_DIR = UI_RESOURCES_PATH + "lang"
|
||||
locale.bindtextdomain(TEXT_DOMAIN, UI_RESOURCES_PATH + "lang")
|
||||
|
||||
NOTIFY_IS_INIT = False
|
||||
APP_FONT = None
|
||||
|
||||
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, ValueError):
|
||||
pass # NOP
|
||||
else:
|
||||
NOTIFY_IS_INIT = Notify.init("DemonEditor")
|
||||
elif IS_DARWIN:
|
||||
import gettext
|
||||
|
||||
if GTK_PATH:
|
||||
LANG_PATH = GTK_PATH + "/share/locale"
|
||||
gettext.bindtextdomain(TEXT_DOMAIN, LANG_PATH)
|
||||
# For launching from the bundle.
|
||||
if os.getcwd() == "/" and GTK_PATH:
|
||||
os.chdir(GTK_PATH)
|
||||
else:
|
||||
locale.setlocale(locale.LC_NUMERIC, "C")
|
||||
|
||||
# Icons.
|
||||
theme = Gtk.IconTheme.get_default()
|
||||
_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
|
||||
IPTV_ICON = theme.load_icon("emblem-shared", 16, 0) if theme.load_icon("emblem-shared", 16, 0) else None
|
||||
theme.append_search_path(UI_RESOURCES_PATH + "icons")
|
||||
|
||||
# keys for move in lists
|
||||
MOVE_KEYS = (Gdk.KEY_Up, Gdk.KEY_Page_Up, Gdk.KEY_Down, Gdk.KEY_Page_Down, Gdk.KEY_Home, Gdk.KEY_End,
|
||||
Gdk.KEY_KP_Page_Up, Gdk.KEY_KP_Page_Down) # KEY_KP_Page_Up(Down) for laptop!
|
||||
|
||||
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 HeaderBar(Gtk.HeaderBar):
|
||||
""" Custom header bar widget. """
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.set_visible(True)
|
||||
self.set_show_close_button(True)
|
||||
|
||||
if IS_DARWIN:
|
||||
self.set_decoration_layout("close,minimize,maximize")
|
||||
|
||||
|
||||
class Page(Enum):
|
||||
""" Main stack widget page. """
|
||||
INFO = "info"
|
||||
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 """
|
||||
""" Used for set target view. """
|
||||
BOUQUET = 0
|
||||
FAV = 1
|
||||
SERVICES = 2
|
||||
IPTV = 3
|
||||
|
||||
|
||||
class BqGenType(Enum):
|
||||
""" Bouquet generation type """
|
||||
""" Bouquet generation type. """
|
||||
SAT = 0
|
||||
EACH_SAT = 1
|
||||
PACKAGE = 2
|
||||
@@ -48,5 +213,220 @@ class BqGenType(Enum):
|
||||
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_URL = 4
|
||||
IPTV_FAV_ID = 5
|
||||
IPTV_PICON_ID = 6
|
||||
IPTV_TOOLTIP = 7
|
||||
# EPG view
|
||||
EPG_SERVICE = 0
|
||||
EPG_TITLE = 1
|
||||
EPG_START = 2
|
||||
EPG_END = 3
|
||||
EPG_LENGTH = 4
|
||||
EPG_DESC = 5
|
||||
EPG_DATA = 6
|
||||
|
||||
def __index__(self):
|
||||
""" Overridden to get the index in slices directly """
|
||||
return self.value
|
||||
|
||||
|
||||
# *************** Keyboard keys *************** #
|
||||
|
||||
class BaseKeyboardKey(Enum):
|
||||
@classmethod
|
||||
def value_exist(cls, value):
|
||||
return value in (val.value for val in cls.__members__.values())
|
||||
|
||||
|
||||
if IS_LINUX:
|
||||
class KeyboardKey(BaseKeyboardKey):
|
||||
""" The raw(hardware) codes [Linux] of the keyboard keys. """
|
||||
E = 26
|
||||
R = 27
|
||||
T = 28
|
||||
P = 33
|
||||
S = 39
|
||||
F = 41
|
||||
X = 53
|
||||
C = 54
|
||||
V = 55
|
||||
W = 25
|
||||
Z = 52
|
||||
INSERT = 118
|
||||
HOME = 110
|
||||
END = 115
|
||||
UP = 111
|
||||
DOWN = 116
|
||||
PAGE_UP = 112
|
||||
PAGE_DOWN = 117
|
||||
LEFT = 113
|
||||
RIGHT = 114
|
||||
F2 = 68
|
||||
F4 = 70
|
||||
F5 = 71
|
||||
F7 = 73
|
||||
SPACE = 65
|
||||
DELETE = 119
|
||||
BACK_SPACE = 22
|
||||
RETURN = 36
|
||||
CTRL_L = 37
|
||||
CTRL_R = 105
|
||||
# Laptop codes
|
||||
HOME_KP = 79
|
||||
END_KP = 87
|
||||
PAGE_UP_KP = 81
|
||||
PAGE_DOWN_KP = 89
|
||||
|
||||
elif IS_DARWIN:
|
||||
class KeyboardKey(BaseKeyboardKey):
|
||||
""" The raw(hardware) codes [macOS] of the keyboard keys. """
|
||||
F = 3
|
||||
E = 14
|
||||
R = 15
|
||||
T = 17
|
||||
P = 35
|
||||
S = 1
|
||||
H = 4
|
||||
L = 37
|
||||
X = 7
|
||||
C = 8
|
||||
V = 9
|
||||
W = 13
|
||||
Z = 6
|
||||
INSERT = -1
|
||||
HOME = -1
|
||||
END = -1
|
||||
UP = 126
|
||||
DOWN = 125
|
||||
PAGE_UP = -1
|
||||
PAGE_DOWN = -1
|
||||
LEFT = 123
|
||||
RIGHT = 123
|
||||
F2 = 120
|
||||
F4 = 118
|
||||
F5 = 96
|
||||
F7 = 98
|
||||
SPACE = 49
|
||||
DELETE = 51
|
||||
BACK_SPACE = 76
|
||||
RETURN = 36
|
||||
CTRL_L = 55
|
||||
CTRL_R = 55
|
||||
# Laptop codes.
|
||||
HOME_KP = -1
|
||||
END_KP = -1
|
||||
PAGE_UP_KP = -1
|
||||
PAGE_DOWN_KP = -1
|
||||
|
||||
else:
|
||||
class KeyboardKey(BaseKeyboardKey):
|
||||
""" The raw(hardware) codes [Windows] of the keyboard keys. """
|
||||
E = 69
|
||||
R = 82
|
||||
T = 84
|
||||
P = 80
|
||||
S = 83
|
||||
F = 70
|
||||
X = 88
|
||||
C = 67
|
||||
V = 86
|
||||
W = 87
|
||||
Z = 90
|
||||
INSERT = 45
|
||||
HOME = 36
|
||||
END = 35
|
||||
UP = 38
|
||||
DOWN = 40
|
||||
PAGE_UP = 33
|
||||
PAGE_DOWN = 34
|
||||
LEFT = 37
|
||||
RIGHT = 39
|
||||
F2 = 113
|
||||
F4 = 115
|
||||
F5 = 116
|
||||
F7 = 118
|
||||
SPACE = 32
|
||||
DELETE = 46
|
||||
BACK_SPACE = 8
|
||||
RETURN = 13
|
||||
CTRL_L = 17
|
||||
CTRL_R = 163
|
||||
# Laptop codes.
|
||||
HOME_KP = -1
|
||||
END_KP = -1
|
||||
PAGE_UP_KP = -1
|
||||
PAGE_DOWN_KP = -1
|
||||
|
||||
# Keys for move in lists. KEY_KP_(NAME) for laptop!
|
||||
MOVE_KEYS = {KeyboardKey.UP, KeyboardKey.PAGE_UP,
|
||||
KeyboardKey.DOWN, KeyboardKey.PAGE_DOWN,
|
||||
KeyboardKey.HOME, KeyboardKey.END,
|
||||
KeyboardKey.HOME_KP, KeyboardKey.END_KP,
|
||||
KeyboardKey.PAGE_UP_KP, KeyboardKey.PAGE_DOWN_KP}
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
|
||||
24
app/ui/win_style.css
Normal file
24
app/ui/win_style.css
Normal file
@@ -0,0 +1,24 @@
|
||||
* {
|
||||
-GtkDialog-action-area-border: 12;
|
||||
}
|
||||
|
||||
switch {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
spinbutton entry {
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
button > image {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
grid > button {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
popover .view {
|
||||
background-color: transparent;
|
||||
}
|
||||
0
app/ui/xml/__init__.py
Normal file
0
app/ui/xml/__init__.py
Normal file
984
app/ui/xml/dialogs.glade
Normal file
984
app/ui/xml/dialogs.glade
Normal file
@@ -0,0 +1,984 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.2
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-2022 Dmitriy Yefremov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
Author: Dmitriy Yefremov
|
||||
|
||||
-->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<!-- interface-css-provider-path style.css -->
|
||||
<!-- interface-license-type mit -->
|
||||
<!-- interface-name DemonEditor -->
|
||||
<!-- interface-copyright 2018-2022 Dmitriy Yefremov -->
|
||||
<!-- interface-authors Dmitriy Yefremov -->
|
||||
<object class="GtkGrid" id="cable_tr_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="column_spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="cable_freq_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Freq</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="cable_rate_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Rate</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="cable_rate_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">12</property>
|
||||
<property name="max_width_chars">14</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="placeholder_text">6900000</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="cable_fec_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">FEC</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="cable_mod_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Mod</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="cable_freq_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">12</property>
|
||||
<property name="max_width_chars">14</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_sensitive">False</property>
|
||||
<property name="placeholder_text">120000</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="cable_fec_box">
|
||||
<property name="width_request">75</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="cable_mod_box">
|
||||
<property name="width_request">100</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkListStore" id="fec_store">
|
||||
<columns>
|
||||
<!-- column-name fec -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0">1/2</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">2/3</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">3/4</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">5/6</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">6/7</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">7/8</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">8/9</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">3/5</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">4/5</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">9/10</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0" translatable="yes">Auto</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkListStore" id="mod_store">
|
||||
<columns>
|
||||
<!-- column-name mod -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0">Auto</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">QPSK</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">8PSK</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">16APSK</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">32APSK</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkListStore" id="pls_mode_store">
|
||||
<columns>
|
||||
<!-- column-name pls_mode -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0">Root</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">Gold</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">Combo</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkListStore" id="pol_store">
|
||||
<columns>
|
||||
<!-- column-name pol -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0">H</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">V</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">R</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">L</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkAdjustment" id="pos_adjustment">
|
||||
<property name="upper">180</property>
|
||||
<property name="step_increment">0.10000000000000001</property>
|
||||
<property name="page_increment">10</property>
|
||||
</object>
|
||||
<object class="GtkListStore" id="side_store">
|
||||
<columns>
|
||||
<!-- column-name side -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0">E</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">W</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkGrid" id="sat_dialog_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="column_spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label11">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Name</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label12">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Position</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="sat_name_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_sensitive">False</property>
|
||||
<property name="placeholder_text" translatable="yes">satellite name</property>
|
||||
<property name="input_purpose">name</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="sat_position_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">5</property>
|
||||
<property name="secondary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_sensitive">False</property>
|
||||
<property name="input_purpose">number</property>
|
||||
<property name="adjustment">pos_adjustment</property>
|
||||
<property name="digits">1</property>
|
||||
<property name="numeric">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="side_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">side_store</property>
|
||||
<property name="active">0</property>
|
||||
<property name="id_column">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="side_cellrenderertext"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkListStore" id="system_store">
|
||||
<columns>
|
||||
<!-- column-name system -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
<data>
|
||||
<row>
|
||||
<col id="0">DVB-S</col>
|
||||
</row>
|
||||
<row>
|
||||
<col id="0">DVB-S2</col>
|
||||
</row>
|
||||
</data>
|
||||
</object>
|
||||
<object class="GtkBox" id="sat_tr_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkGrid" id="sat_tr_dialog_grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="column_spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="sat_freq_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Freq</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="sat_rate_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Rate</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="sat_pol_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Pol</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="sat_fec_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">FEC</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="sat_sys_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">System</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">4</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="sat_mod_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Mod</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">5</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="freq_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">12</property>
|
||||
<property name="max_width_chars">14</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_sensitive">False</property>
|
||||
<property name="placeholder_text">11700000</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="rate_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">12</property>
|
||||
<property name="max_width_chars">14</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="placeholder_text">27500000</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="pol_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">pol_store</property>
|
||||
<property name="id_column">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="pol_cellrenderertext"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="fec_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">fec_store</property>
|
||||
<property name="id_column">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="cellrenderertext4"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="sys_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">system_store</property>
|
||||
<property name="id_column">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="cellrenderertext5"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">4</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="mod_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">mod_store</property>
|
||||
<property name="id_column">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="cellrenderertext6"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">5</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkExpander" id="expander">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="resize_toplevel">True</property>
|
||||
<child>
|
||||
<object class="GtkGrid" id="tr_dialog_grid2">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="column_spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="tr_pls_mode_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Pls mode</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="tr_pls_code_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Pls code</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="id_id_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Is ID</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBox" id="pls_mode_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="model">pls_mode_store</property>
|
||||
<property name="id_column">0</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="pls_mode_cellrenderertext1"/>
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="pls_code_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">5</property>
|
||||
<property name="max_width_chars">12</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="placeholder_text" translatable="yes">0 - 262142</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="is_id_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">5</property>
|
||||
<property name="max_width_chars">12</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="placeholder_text" translatable="yes">0 - 255</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="tr_t2mi_plp_id_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">T2-MI PLP ID</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="t2mi_plp_id_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">5</property>
|
||||
<property name="max_width_chars">12</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="placeholder_text" translatable="yes">0 - 255</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label">
|
||||
<object class="GtkLabel" id="expander_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Extra:</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<object class="GtkBox" id="ter_tr_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_left">5</property>
|
||||
<property name="margin_right">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkGrid" id="ter_tr_grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="column_spacing">5</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_freq_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Freq</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="ter_freq_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">12</property>
|
||||
<property name="max_width_chars">14</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="primary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_activatable">False</property>
|
||||
<property name="secondary_icon_sensitive">False</property>
|
||||
<property name="placeholder_text">170000000</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_sys_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">System</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="ter_sys_box">
|
||||
<property name="width_request">100</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="ter_bandwidth_box">
|
||||
<property name="width_request">110</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_bandwidth_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Bandwidth</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_constellation_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Constellation</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_sr_hp_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">SR (HP)</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">4</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_sr_lp_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">SR (LP)</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">5</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="ter_sr_hp_box">
|
||||
<property name="width_request">75</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">4</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="ter_sr_lp_box">
|
||||
<property name="width_request">75</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">5</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="ter_constellation_box">
|
||||
<property name="width_request">100</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkGrid" id="ter_tr_ext_grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<property name="column_spacing">5</property>
|
||||
<property name="column_homogeneous">True</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_guard_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Guard</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_transmission_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Transmission</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_hierarchy_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Hierarchy</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_inversion_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">Inversion</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="ter_guard_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">0</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="ter_transmission_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">1</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="ter_hierarchy_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">2</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkComboBoxText" id="ter_inversion_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">3</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkEntry" id="ter_plp_id_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="width_chars">8</property>
|
||||
<property name="max_width_chars">8</property>
|
||||
<property name="primary_icon_name">document-edit-symbolic</property>
|
||||
<property name="placeholder_text">0-255</property>
|
||||
<property name="input_purpose">digits</property>
|
||||
<signal name="changed" handler="on_entry_changed" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">4</property>
|
||||
<property name="top_attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="ter_plp_id_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="label" translatable="yes">PLP ID</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left_attach">4</property>
|
||||
<property name="top_attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
1105
app/ui/xml/dialogs.py
Normal file
1105
app/ui/xml/dialogs.py
Normal file
File diff suppressed because it is too large
Load Diff
584
app/ui/xml/edit.py
Normal file
584
app/ui/xml/edit.py
Normal file
@@ -0,0 +1,584 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2018-2023 Dmitriy Yefremov
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
# Author: Dmitriy Yefremov
|
||||
#
|
||||
|
||||
|
||||
from enum import Enum
|
||||
from pyexpat import ExpatError
|
||||
|
||||
from gi.repository import GLib
|
||||
|
||||
from app.commons import run_idle
|
||||
from app.connections import DownloadType
|
||||
from app.eparser import get_satellites, write_satellites, Satellite, Transponder
|
||||
from app.eparser.ecommons import (POLARIZATION, FEC, SYSTEM, MODULATION, T_SYSTEM, BANDWIDTH, CONSTELLATION, T_FEC,
|
||||
GUARD_INTERVAL, TRANSMISSION_MODE, HIERARCHY, Inversion, FEC_DEFAULT, C_MODULATION,
|
||||
Terrestrial, Cable, CableTransponder, TerTransponder)
|
||||
from app.eparser.satxml import get_terrestrial, get_cable, write_terrestrial, write_cable, get_pos_str
|
||||
from .dialogs import (SatelliteDialog, SatellitesUpdateDialog, TerrestrialDialog, CableDialog, SatTransponderDialog,
|
||||
CableTransponderDialog, TerTransponderDialog)
|
||||
from ..dialogs import show_dialog, DialogType, get_chooser_dialog, get_message, get_builder
|
||||
from ..main_helper import move_items, on_popup_menu, scroll_to
|
||||
from ..uicommons import Gtk, Gdk, UI_RESOURCES_PATH, MOVE_KEYS, KeyboardKey, MOD_MASK, Page
|
||||
|
||||
|
||||
class SatellitesTool(Gtk.Box):
|
||||
""" Class to processing *.xml data. """
|
||||
|
||||
class DVB(str, Enum):
|
||||
SAT = "satellites"
|
||||
TERRESTRIAL = "terrestrial"
|
||||
CABLE = "cable"
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
def __init__(self, app, settings, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._app = app
|
||||
self._app.connect("data-save", self.on_save)
|
||||
self._app.connect("data-save-as", self.on_save_as)
|
||||
self._app.connect("data-receive", self.on_download)
|
||||
self._app.connect("data-send", self.on_upload)
|
||||
|
||||
self._settings = settings
|
||||
self._current_sat_path = None
|
||||
self._current_ter_path = None
|
||||
self._current_cable_path = None
|
||||
self._dvb_type = self.DVB.SAT
|
||||
|
||||
handlers = {"on_satellite_view_realize": self.on_satellite_view_realize,
|
||||
"on_terrestrial_view_realize": self.on_terrestrial_view_realize,
|
||||
"on_cable_view_realize": self.on_cable_view_realize,
|
||||
"on_update": self.on_update,
|
||||
"on_up": self.on_up,
|
||||
"on_down": self.on_down,
|
||||
"on_button_press": self.on_button_press,
|
||||
"on_tr_button_press": self.on_tr_button_press,
|
||||
"on_add": self.on_add,
|
||||
"on_edit": self.on_edit,
|
||||
"on_remove": self.on_remove,
|
||||
"on_transponder_add": self.on_transponder_add,
|
||||
"on_transponder_edit": self.on_transponder_edit,
|
||||
"on_transponder_remove": self.on_transponder_remove,
|
||||
"on_key_press": self.on_key_press,
|
||||
"on_tr_key_press": self.on_tr_key_press,
|
||||
"on_visible_page": self.on_visible_page,
|
||||
"on_satellite_selection": self.on_satellite_selection,
|
||||
"on_terrestrial_selection": self.on_terrestrial_selection,
|
||||
"on_cable_selection": self.on_cable_selection,
|
||||
"on_sat_model_changed": self.on_sat_model_changed,
|
||||
"on_sat_tr_model_changed": self.on_sat_tr_model_changed,
|
||||
"on_ter_model_changed": self.on_ter_model_changed,
|
||||
"on_ter_tr_model_changed": self.on_ter_tr_model_changed,
|
||||
"on_cable_model_changed": self.on_cable_model_changed,
|
||||
"on_cable_tr_model_changed": self.on_cable_tr_model_changed}
|
||||
|
||||
builder = get_builder(f"{UI_RESOURCES_PATH}xml/editor.glade", handlers)
|
||||
|
||||
self._satellite_view = builder.get_object("satellite_view")
|
||||
self._terrestrial_view = builder.get_object("terrestrial_view")
|
||||
self._cable_view = builder.get_object("cable_view")
|
||||
self._sat_tr_view = builder.get_object("sat_tr_view")
|
||||
self._ter_tr_view = builder.get_object("ter_tr_view")
|
||||
self._cable_tr_view = builder.get_object("cable_tr_view")
|
||||
|
||||
self._sat_count_label = builder.get_object("sat_count_label")
|
||||
self._sat_tr_count_label = builder.get_object("sat_tr_count_label")
|
||||
self._ter_count_label = builder.get_object("ter_count_label")
|
||||
self._ter_tr_count_label = builder.get_object("ter_tr_count_label")
|
||||
self._cable_count_label = builder.get_object("cable_count_label")
|
||||
self._cable_tr_count_label = builder.get_object("cable_tr_count_label")
|
||||
|
||||
self._transponders_stack = builder.get_object("transponders_stack")
|
||||
self._add_header_button = builder.get_object("add_header_button")
|
||||
self._update_header_button = builder.get_object("update_header_button")
|
||||
self.pack_start(builder.get_object("main_paned"), True, True, 0)
|
||||
self._app.connect("profile-changed", self.on_profile_changed)
|
||||
# Custom renderers.
|
||||
renderer = builder.get_object("sat_pos_renderer")
|
||||
builder.get_object("sat_pos_column").set_cell_data_func(renderer, self.sat_pos_func)
|
||||
# Satellite.
|
||||
renderer = builder.get_object("sat_pol_renderer")
|
||||
builder.get_object("pol_column").set_cell_data_func(renderer, self.sat_pol_func)
|
||||
renderer = builder.get_object("sat_fec_renderer")
|
||||
builder.get_object("fec_column").set_cell_data_func(renderer, self.sat_fec_func)
|
||||
renderer = builder.get_object("sat_sys_renderer")
|
||||
builder.get_object("sys_column").set_cell_data_func(renderer, self.sat_sys_func)
|
||||
renderer = builder.get_object("sat_mod_renderer")
|
||||
builder.get_object("mod_column").set_cell_data_func(renderer, self.sat_mod_func)
|
||||
# Terrestrial.
|
||||
renderer = builder.get_object("ter_system_renderer")
|
||||
builder.get_object("ter_system_column").set_cell_data_func(renderer, self.ter_sys_func)
|
||||
renderer = builder.get_object("ter_bandwidth_renderer")
|
||||
builder.get_object("ter_bandwidth_column").set_cell_data_func(renderer, self.ter_bandwidth_func)
|
||||
renderer = builder.get_object("ter_constellation_renderer")
|
||||
builder.get_object("ter_constellation_column").set_cell_data_func(renderer, self.ter_constellation_func)
|
||||
renderer = builder.get_object("ter_rate_hp_renderer")
|
||||
builder.get_object("ter_rate_hp_column").set_cell_data_func(renderer, self.ter_fec_hp_func)
|
||||
renderer = builder.get_object("ter_rate_lp_renderer")
|
||||
builder.get_object("ter_rate_lp_column").set_cell_data_func(renderer, self.ter_fec_lp_func)
|
||||
renderer = builder.get_object("ter_guard_renderer")
|
||||
builder.get_object("ter_guard_column").set_cell_data_func(renderer, self.ter_guard_func)
|
||||
renderer = builder.get_object("ter_tr_mode_renderer")
|
||||
builder.get_object("ter_tr_mode_column").set_cell_data_func(renderer, self.ter_transmission_func)
|
||||
renderer = builder.get_object("ter_hierarchy_renderer")
|
||||
builder.get_object("ter_hierarchy_column").set_cell_data_func(renderer, self.ter_hierarchy_func)
|
||||
renderer = builder.get_object("ter_inversion_renderer")
|
||||
builder.get_object("ter_inversion_column").set_cell_data_func(renderer, self.ter_inversion_func)
|
||||
# Cable.
|
||||
renderer = builder.get_object("cable_fec_renderer")
|
||||
builder.get_object("cable_fec_column").set_cell_data_func(renderer, self.cable_fec_func)
|
||||
renderer = builder.get_object("cable_mod_renderer")
|
||||
builder.get_object("cable_mod_column").set_cell_data_func(renderer, self.cable_mod_func)
|
||||
|
||||
self.show()
|
||||
|
||||
# ******************** Custom renderers ******************** #
|
||||
|
||||
def sat_pos_func(self, column, renderer, model, itr, data):
|
||||
""" Converts and sets the satellite position value to a readable format. """
|
||||
renderer.set_property("text", get_pos_str(int(model.get_value(itr, 2))))
|
||||
|
||||
def sat_pol_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", POLARIZATION.get(model.get_value(itr, 2), None))
|
||||
|
||||
def sat_fec_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", FEC.get(model.get_value(itr, 3), None))
|
||||
|
||||
def sat_sys_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", SYSTEM.get(model.get_value(itr, 4), None))
|
||||
|
||||
def sat_mod_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", MODULATION.get(model.get_value(itr, 5), None))
|
||||
|
||||
def ter_sys_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", T_SYSTEM.get(model.get_value(itr, 1), None))
|
||||
|
||||
def ter_bandwidth_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", BANDWIDTH.get(model.get_value(itr, 2), None))
|
||||
|
||||
def ter_constellation_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", CONSTELLATION.get(model.get_value(itr, 3), None))
|
||||
|
||||
def ter_fec_hp_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", T_FEC.get(model.get_value(itr, 4), None))
|
||||
|
||||
def ter_fec_lp_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", T_FEC.get(model.get_value(itr, 5), None))
|
||||
|
||||
def ter_guard_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", GUARD_INTERVAL.get(model.get_value(itr, 6), None))
|
||||
|
||||
def ter_transmission_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", TRANSMISSION_MODE.get(model.get_value(itr, 7), None))
|
||||
|
||||
def ter_hierarchy_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", HIERARCHY.get(model.get_value(itr, 8), None))
|
||||
|
||||
def ter_inversion_func(self, column, renderer, model, itr, data):
|
||||
value = model.get_value(itr, 9)
|
||||
if value:
|
||||
value = Inversion(value).name
|
||||
renderer.set_property("text", value)
|
||||
|
||||
def cable_fec_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", FEC_DEFAULT.get(model.get_value(itr, 2), None))
|
||||
|
||||
def cable_mod_func(self, column, renderer, model, itr, data):
|
||||
renderer.set_property("text", C_MODULATION.get(model.get_value(itr, 3), None))
|
||||
|
||||
def on_satellite_view_realize(self, view):
|
||||
self.load_satellites_list()
|
||||
|
||||
def on_terrestrial_view_realize(self, view):
|
||||
self.load_terrestrial_list()
|
||||
|
||||
def on_cable_view_realize(self, view):
|
||||
self.load_cable_list()
|
||||
|
||||
def load_satellites_list(self, path=None):
|
||||
gen = self.on_satellites_list_load(path)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def load_terrestrial_list(self, path=None):
|
||||
gen = self.on_terrestrial_list_load(path)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def load_cable_list(self, path=None):
|
||||
gen = self.on_cable_list_load(path)
|
||||
GLib.idle_add(lambda: next(gen, False), priority=GLib.PRIORITY_LOW)
|
||||
|
||||
def on_visible_page(self, stack, param):
|
||||
self._dvb_type = self.DVB(stack.get_visible_child_name())
|
||||
self._transponders_stack.set_visible_child_name(self._dvb_type)
|
||||
self._update_header_button.set_sensitive(self._dvb_type is self.DVB.SAT)
|
||||
|
||||
if self._dvb_type is self.DVB.SAT:
|
||||
self._app.on_info_bar_close()
|
||||
|
||||
else:
|
||||
self._app.show_info_message("EXPERIMENTAL!", Gtk.MessageType.WARNING)
|
||||
|
||||
def on_satellite_selection(self, view):
|
||||
model = self._sat_tr_view.get_model()
|
||||
model.clear()
|
||||
|
||||
self._current_sat_path, column = view.get_cursor()
|
||||
if self._current_sat_path:
|
||||
sat_model = view.get_model()
|
||||
list(map(model.append, sat_model[self._current_sat_path][-1]))
|
||||
|
||||
def on_terrestrial_selection(self, view):
|
||||
model = self._ter_tr_view.get_model()
|
||||
model.clear()
|
||||
|
||||
self._current_ter_path, column = view.get_cursor()
|
||||
if self._current_ter_path:
|
||||
ter_model = view.get_model()
|
||||
list(map(model.append, ter_model[self._current_ter_path][-1]))
|
||||
|
||||
def on_cable_selection(self, view):
|
||||
model = self._cable_tr_view.get_model()
|
||||
model.clear()
|
||||
|
||||
self._current_cable_path, column = view.get_cursor()
|
||||
if self._current_cable_path:
|
||||
cable_model = view.get_model()
|
||||
list(map(model.append, cable_model[self._current_cable_path][-1]))
|
||||
|
||||
def on_sat_model_changed(self, model, path, itr=None):
|
||||
self._sat_count_label.set_text(str(len(model)))
|
||||
|
||||
def on_sat_tr_model_changed(self, model, path, itr=None):
|
||||
self._sat_tr_count_label.set_text(str(len(model)))
|
||||
|
||||
def on_ter_model_changed(self, model, path, itr=None):
|
||||
self._ter_count_label.set_text(str(len(model)))
|
||||
|
||||
def on_ter_tr_model_changed(self, model, path, itr=None):
|
||||
self._ter_tr_count_label.set_text(str(len(model)))
|
||||
|
||||
def on_cable_model_changed(self, model, path, itr=None):
|
||||
self._cable_count_label.set_text(str(len(model)))
|
||||
|
||||
def on_cable_tr_model_changed(self, model, path, itr=None):
|
||||
self._cable_tr_count_label.set_text(str(len(model)))
|
||||
|
||||
def on_up(self, item):
|
||||
move_items(KeyboardKey.UP, self._satellite_view)
|
||||
|
||||
def on_down(self, item):
|
||||
move_items(KeyboardKey.DOWN, self._satellite_view)
|
||||
|
||||
def on_button_press(self, menu, event):
|
||||
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS:
|
||||
self.on_edit()
|
||||
else:
|
||||
on_popup_menu(menu, event)
|
||||
|
||||
def on_tr_button_press(self, menu, event):
|
||||
if event.get_event_type() == Gdk.EventType.DOUBLE_BUTTON_PRESS:
|
||||
self.on_transponder_edit()
|
||||
else:
|
||||
on_popup_menu(menu, event)
|
||||
|
||||
def on_key_press(self, view, event):
|
||||
""" Handling keystrokes. """
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
return
|
||||
|
||||
key = KeyboardKey(key_code)
|
||||
ctrl = event.state & MOD_MASK
|
||||
|
||||
if key is KeyboardKey.DELETE:
|
||||
self.on_remove(view)
|
||||
elif key is KeyboardKey.INSERT:
|
||||
self.on_edit(force=True)
|
||||
elif ctrl and key is KeyboardKey.E:
|
||||
self.on_edit()
|
||||
elif ctrl and key in MOVE_KEYS:
|
||||
move_items(key, view)
|
||||
elif key is KeyboardKey.LEFT or key is KeyboardKey.RIGHT:
|
||||
view.do_unselect_all(view)
|
||||
|
||||
def on_tr_key_press(self, view, event):
|
||||
""" Handling transponder view keystrokes. """
|
||||
key_code = event.hardware_keycode
|
||||
if not KeyboardKey.value_exist(key_code):
|
||||
return
|
||||
|
||||
key = KeyboardKey(key_code)
|
||||
ctrl = event.state & MOD_MASK
|
||||
|
||||
if key is KeyboardKey.DELETE:
|
||||
self.on_transponder_remove()
|
||||
elif key is KeyboardKey.INSERT:
|
||||
self.on_transponder_edit(force=True)
|
||||
elif ctrl and key is KeyboardKey.E:
|
||||
self.on_transponder_edit()
|
||||
elif ctrl and key in MOVE_KEYS:
|
||||
move_items(key, view)
|
||||
elif key is KeyboardKey.LEFT or key is KeyboardKey.RIGHT:
|
||||
view.do_unselect_all(view)
|
||||
|
||||
def on_satellites_list_load(self, path=None):
|
||||
""" Load satellites data into model """
|
||||
path = path or f"{self._settings.profile_data_path}satellites.xml"
|
||||
yield from self.load_data(self._satellite_view, get_satellites, path)
|
||||
|
||||
def on_terrestrial_list_load(self, path=None):
|
||||
path = path or f"{self._settings.profile_data_path}terrestrial.xml"
|
||||
yield from self.load_data(self._terrestrial_view, get_terrestrial, path)
|
||||
|
||||
def on_cable_list_load(self, path=None):
|
||||
path = path or f"{self._settings.profile_data_path}cables.xml"
|
||||
yield from self.load_data(self._cable_view, get_cable, path)
|
||||
|
||||
def load_data(self, view, func, path):
|
||||
model = view.get_model()
|
||||
model.clear()
|
||||
|
||||
try:
|
||||
data = func(path)
|
||||
yield True
|
||||
except FileNotFoundError as e:
|
||||
msg = get_message("Please, download files from receiver or setup your path for read data!")
|
||||
self._app.show_error_message(f"{e}\n{msg}")
|
||||
except ExpatError as e:
|
||||
msg = f"The file [{path}] is not formatted correctly or contains invalid characters! Cause: {e}"
|
||||
self._app.show_error_message(msg)
|
||||
else:
|
||||
for d in data:
|
||||
yield model.append(d)
|
||||
|
||||
def on_add(self, item):
|
||||
""" Common adding. """
|
||||
self.on_edit(item, force=True)
|
||||
|
||||
def on_transponder_add(self, item):
|
||||
self.on_transponder_edit(force=True)
|
||||
|
||||
def on_edit(self, item=None, force=False):
|
||||
self.on_data_edit(self.get_active_dvb_view(), force)
|
||||
|
||||
def on_transponder_edit(self, item=None, force=False):
|
||||
self.on_data_edit(self.get_active_transponder_view(), force)
|
||||
|
||||
def on_data_edit(self, view, force=False):
|
||||
""" Common edit. """
|
||||
if force:
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
else:
|
||||
paths = self.check_selection(view, "Please, select only one item!")
|
||||
if not paths:
|
||||
return
|
||||
|
||||
model = view.get_model()
|
||||
row = model[paths][:] if paths else None
|
||||
itr = model.get_iter(paths) if paths else None
|
||||
|
||||
if view is self._satellite_view:
|
||||
self.on_dvb_data_edit(SatelliteDialog, "Satellite", view, None if force else Satellite(*row), itr)
|
||||
elif view is self._terrestrial_view:
|
||||
self.on_dvb_data_edit(TerrestrialDialog, "Region", view, None if force else Terrestrial(*row), itr)
|
||||
elif view is self._cable_view:
|
||||
self.on_dvb_data_edit(CableDialog, "Provider", view, None if force else Cable(*row), itr)
|
||||
elif view is self._sat_tr_view:
|
||||
data = None if force else Transponder(*row)
|
||||
self.on_transponder_data_edit(SatTransponderDialog, "Transponder", view, self._satellite_view, data, itr)
|
||||
elif view is self._ter_tr_view:
|
||||
data = None if force else TerTransponder(*row)
|
||||
self.on_transponder_data_edit(TerTransponderDialog, "Transponder", view, self._terrestrial_view, data, itr)
|
||||
elif view is self._cable_tr_view:
|
||||
data = None if force else CableTransponder(*row)
|
||||
self.on_transponder_data_edit(CableTransponderDialog, "Transponder", view, self._cable_view, data, itr)
|
||||
else:
|
||||
self._app.show_error_message("Not implemented yet!")
|
||||
|
||||
def on_dvb_data_edit(self, dialog, title, view, data=None, edited_itr=None):
|
||||
""" Creates or edits DVB data. """
|
||||
dialog = dialog(self._app.get_active_window(), title, data)
|
||||
if dialog.run() == Gtk.ResponseType.OK:
|
||||
dvb_data = dialog.data
|
||||
if dvb_data:
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if data and edited_itr:
|
||||
model.set(edited_itr, {i: v for i, v in enumerate(dvb_data)})
|
||||
else:
|
||||
if paths:
|
||||
index = paths[0].get_indices()[0] + 1
|
||||
model.insert(index, dvb_data)
|
||||
else:
|
||||
model.append(dvb_data)
|
||||
scroll_to(len(model) - 1, view)
|
||||
dialog.destroy()
|
||||
|
||||
def on_transponder_data_edit(self, dialog, title, view, src_view, data=None, edited_itr=None):
|
||||
""" Creates or edits transponder data. """
|
||||
paths = self.check_selection(src_view, "Please, select only one item!")
|
||||
if paths is None:
|
||||
return
|
||||
elif len(paths) == 0:
|
||||
self._app.show_error_message("No source selected!")
|
||||
return
|
||||
|
||||
dialog = dialog(self._app.app_window, title, data)
|
||||
if dialog.run() == Gtk.ResponseType.OK:
|
||||
tr = dialog.data
|
||||
if tr:
|
||||
src_model = src_view.get_model()
|
||||
transponders = src_model[paths][-1]
|
||||
tr_model, tr_paths = view.get_selection().get_selected_rows()
|
||||
|
||||
if data and edited_itr:
|
||||
tr_model.set(edited_itr, {i: v for i, v in enumerate(tr)})
|
||||
transponders[tr_model.get_path(edited_itr).get_indices()[0]] = tr
|
||||
else:
|
||||
index = paths[0].get_indices()[0] + 1
|
||||
tr_model.insert(index, tr)
|
||||
transponders.insert(index, tr)
|
||||
dialog.destroy()
|
||||
|
||||
def check_selection(self, view, message):
|
||||
""" Checks if any row is selected. Shows error dialog if selected more than one.
|
||||
|
||||
Returns selected path or None.
|
||||
"""
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
if len(paths) > 1:
|
||||
self._app.show_error_message(message)
|
||||
return
|
||||
|
||||
return paths
|
||||
|
||||
def on_remove(self, view=None):
|
||||
""" Removes selected satellites and transponders. """
|
||||
view = self.get_active_dvb_view()
|
||||
selection = view.get_selection()
|
||||
model, paths = selection.get_selected_rows()
|
||||
list(map(model.remove, [model.get_iter(path) for path in paths]))
|
||||
|
||||
def on_transponder_remove(self, item=None):
|
||||
view = self.get_active_transponder_view()
|
||||
trs = None
|
||||
if view is self._sat_tr_view:
|
||||
if self._current_sat_path:
|
||||
trs = self._satellite_view.get_model()[self._current_sat_path][-1]
|
||||
else:
|
||||
self._app.show_error_message("No satellite is selected!")
|
||||
elif view is self._ter_tr_view:
|
||||
if self._current_ter_path:
|
||||
trs = self._terrestrial_view.get_model()[self._current_ter_path][-1]
|
||||
else:
|
||||
self._app.show_error_message("No terrestrial is selected!")
|
||||
elif view is self._cable_tr_view:
|
||||
if self._current_cable_path:
|
||||
trs = self._cable_view.get_model()[self._current_cable_path][-1]
|
||||
else:
|
||||
self._app.show_error_message("No cable is selected!")
|
||||
|
||||
if trs:
|
||||
model, paths = view.get_selection().get_selected_rows()
|
||||
list(map(trs.pop, sorted(map(lambda p: p.get_indices()[0], paths), reverse=True)))
|
||||
list(map(model.remove, [model.get_iter(path) for path in paths]))
|
||||
|
||||
def get_active_dvb_view(self):
|
||||
if self._dvb_type is self.DVB.SAT:
|
||||
return self._satellite_view
|
||||
elif self._dvb_type is self.DVB.TERRESTRIAL:
|
||||
return self._terrestrial_view
|
||||
return self._cable_view
|
||||
|
||||
def get_active_transponder_view(self):
|
||||
if self._dvb_type is self.DVB.SAT:
|
||||
return self._sat_tr_view
|
||||
elif self._dvb_type is self.DVB.TERRESTRIAL:
|
||||
return self._ter_tr_view
|
||||
return self._cable_tr_view
|
||||
|
||||
@run_idle
|
||||
def on_open(self):
|
||||
xml_file = "satellites.xml"
|
||||
if self._dvb_type is self.DVB.TERRESTRIAL:
|
||||
xml_file = "terrestrial.xml"
|
||||
elif self._dvb_type is self.DVB.CABLE:
|
||||
xml_file = "cables.xml"
|
||||
|
||||
response = get_chooser_dialog(self._app.app_window, self._settings, xml_file, ("*.xml",))
|
||||
if response in (Gtk.ResponseType.CANCEL, Gtk.ResponseType.DELETE_EVENT):
|
||||
return
|
||||
|
||||
if not str(response).endswith(xml_file):
|
||||
self._app.show_error_message(f"No {xml_file} file is selected!")
|
||||
return
|
||||
|
||||
if self._dvb_type is self.DVB.SAT:
|
||||
self.load_satellites_list(response)
|
||||
elif self._dvb_type is self.DVB.TERRESTRIAL:
|
||||
self.load_terrestrial_list(response)
|
||||
else:
|
||||
self.load_cable_list(response)
|
||||
|
||||
@run_idle
|
||||
def on_profile_changed(self, app, profile):
|
||||
self.load_satellites_list()
|
||||
self.load_terrestrial_list()
|
||||
self.load_cable_list()
|
||||
|
||||
@run_idle
|
||||
def on_save(self, app, page):
|
||||
if page is Page.SATELLITE and show_dialog(DialogType.QUESTION, self._app.app_window) == Gtk.ResponseType.OK:
|
||||
if self._dvb_type is self.DVB.SAT:
|
||||
write_satellites((Satellite(*r) for r in self._satellite_view.get_model()),
|
||||
f"{self._settings.profile_data_path}satellites.xml")
|
||||
elif self._dvb_type is self.DVB.TERRESTRIAL:
|
||||
write_terrestrial((Terrestrial(*r) for r in self._terrestrial_view.get_model()),
|
||||
f"{self._settings.profile_data_path}terrestrial.xml")
|
||||
else:
|
||||
write_cable((Cable(*r) for r in self._cable_view.get_model()),
|
||||
f"{self._settings.profile_data_path}cables.xml")
|
||||
|
||||
def on_save_as(self, app, page):
|
||||
self._app.show_error_message("Not implemented yet!")
|
||||
|
||||
def on_download(self, app, page):
|
||||
if page is Page.SATELLITE:
|
||||
self._app.on_download_data(DownloadType.SATELLITES)
|
||||
|
||||
def on_upload(self, app, page):
|
||||
if page is Page.SATELLITE:
|
||||
self._app.upload_data(DownloadType.SATELLITES)
|
||||
|
||||
@run_idle
|
||||
def on_update(self, item):
|
||||
SatellitesUpdateDialog(self._app.get_active_window(), self._settings, self._satellite_view.get_model()).show()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
1399
app/ui/xml/editor.glade
Normal file
1399
app/ui/xml/editor.glade
Normal file
File diff suppressed because it is too large
Load Diff
1335
app/ui/xml/update.glade
Normal file
1335
app/ui/xml/update.glade
Normal file
File diff suppressed because it is too large
Load Diff
19
build-deb.sh
19
build-deb.sh
@@ -1,19 +0,0 @@
|
||||
#!/bin/bash
|
||||
VER="0.3.2_Pre-alpha"
|
||||
B_PATH="dist/DemonEditor"
|
||||
DEB_PATH="$B_PATH/usr/share/demoneditor"
|
||||
|
||||
mkdir -p $B_PATH
|
||||
cp -TRv deb $B_PATH
|
||||
rsync --exclude=app/ui/lang -arv app $DEB_PATH
|
||||
cp -Rv start.py $DEB_PATH
|
||||
|
||||
cd dist
|
||||
fakeroot dpkg-deb --build DemonEditor
|
||||
mv DemonEditor.deb DemonEditor_$VER.deb
|
||||
|
||||
rm -R DemonEditor
|
||||
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user