Compare commits
964 Commits
v0.1.0
...
782106e44a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
782106e44a | ||
|
|
0e0a8b853f | ||
|
|
8c716130a3 | ||
|
|
dfc9be33d7 | ||
|
|
9b592cf761 | ||
|
|
d3122416cf | ||
|
|
a6af9d856b | ||
|
|
51aaa1f603 | ||
|
|
a71efe14c1 | ||
|
|
1879a8ed01 | ||
|
|
7d6ee45263 | ||
|
|
44ad891242 | ||
|
|
a94575f83c | ||
|
|
1c75b09824 | ||
|
|
ebd59760ae | ||
|
|
3184fa6f89 | ||
|
|
32f2b12708 | ||
|
|
b9a6b5056a | ||
|
|
238d30ab94 | ||
|
|
7d26589cf6 | ||
|
|
10857e39b1 | ||
|
|
30a07327bc | ||
|
|
50f2f1ac3c | ||
|
|
8041a0b9d8 | ||
|
|
d5920a381e | ||
|
|
36b2891353 | ||
|
|
4fa6d7d643 | ||
|
|
90151c2958 | ||
|
|
4133c03759 | ||
|
|
aab694c302 | ||
|
|
c963176bba | ||
|
|
06ffff3031 | ||
|
|
2021f76dfd | ||
|
|
112e244fbc | ||
|
|
a655d49ce3 | ||
|
|
ddb47065bb | ||
|
|
e94873c011 | ||
|
|
8f07d44a4d | ||
|
|
d63b4321c2 | ||
|
|
b1c9f3877b | ||
|
|
d49eac5666 | ||
|
|
a24f921804 | ||
|
|
f1d86743f3 | ||
|
|
0b6c14b540 | ||
|
|
10c6b116da | ||
|
|
17c3ed2625 | ||
|
|
296acefb09 | ||
|
|
2bb737bfd9 | ||
|
|
053eb71967 | ||
|
|
ed2190c03d | ||
|
|
cd210900f8 | ||
|
|
b2d09191f3 | ||
|
|
19e71f8e6e | ||
|
|
45ab93c527 | ||
|
|
4c58546fd6 | ||
|
|
854d72fe15 | ||
|
|
786f01cd7e | ||
|
|
69a95f04d7 | ||
|
|
7ef4d0c7bd | ||
|
|
9c1d34d148 | ||
|
|
3d3bab2fec | ||
|
|
5bac2f8e87 | ||
|
|
d54e40306b | ||
|
|
7699a2820d | ||
|
|
2b9be31e6e | ||
|
|
b67b9c0e9d | ||
|
|
4ecba55765 | ||
|
|
58c8c90099 | ||
|
|
1d378cc52d | ||
|
|
f108a84b4f | ||
|
|
e79c8ba10a | ||
|
|
a5021d4686 | ||
|
|
8fa027e98f | ||
|
|
75437d8f66 | ||
|
|
b6f374af2d | ||
|
|
00e751373b | ||
|
|
e7592dc4f5 | ||
|
|
90627f177b | ||
|
|
dd5d72c607 | ||
|
|
12a32c0793 | ||
|
|
4d8df33a0a | ||
|
|
eecf1d3fe2 | ||
|
|
056948d33d | ||
|
|
cc508cdfa5 | ||
|
|
ff1a6df0e8 | ||
|
|
11e9221206 | ||
|
|
737d86faaf | ||
|
|
5229b70e5e | ||
|
|
f151f5ae19 | ||
|
|
a4d88c72a4 | ||
|
|
f303314237 | ||
|
|
a3928ca921 | ||
|
|
c3d13c7c69 | ||
|
|
4ffbb6bb8d | ||
|
|
4324659e67 | ||
|
|
a564c52df0 | ||
|
|
1ac78e45d6 | ||
|
|
1dc6879f2f | ||
|
|
6de9ead965 | ||
|
|
f4ccd1d56f | ||
|
|
1d103ad848 | ||
|
|
96d837732b | ||
|
|
3572dcc555 | ||
|
|
b6b19f5a23 | ||
|
|
bb1284deff | ||
|
|
bc125bb0a3 | ||
|
|
3d30fcfc3e | ||
|
|
b5c9ecdf5c | ||
|
|
a09379f47b | ||
|
|
2a191a310e | ||
|
|
50be0c9bde | ||
|
|
5cfef6c6c0 | ||
|
|
5015a3b900 | ||
|
|
c7050ac9a1 | ||
|
|
a0c192e567 | ||
|
|
06c08d76e7 | ||
|
|
0ea4a2e272 | ||
|
|
3c51fd3e71 | ||
|
|
52ae591b80 | ||
|
|
b17dd47436 | ||
|
|
954d42c60a | ||
|
|
50d82cba1d | ||
|
|
8b65867836 | ||
|
|
79d9ec82f8 | ||
|
|
80cdfc1f0c | ||
|
|
32249d1db1 | ||
|
|
53333dd88b | ||
|
|
065f6a2dce | ||
|
|
a07c04d786 | ||
|
|
831fcf7e98 | ||
|
|
8e805b24f6 | ||
|
|
1086ea52df | ||
|
|
0f393227b0 | ||
|
|
6987caa465 | ||
|
|
f0267a076b | ||
|
|
733cc9780d | ||
|
|
4a8719a5f3 | ||
|
|
41658cbc19 | ||
|
|
f1af58d934 | ||
|
|
eb0ddca1c3 | ||
|
|
38ff2607cf | ||
|
|
3ae9a62c80 | ||
|
|
06d32fb9c2 | ||
|
|
fb96a34d8c | ||
|
|
23b88f96b2 | ||
|
|
4e58ccad8d | ||
|
|
c20b3913b5 | ||
|
|
36c20f30b1 | ||
|
|
5b102372ff | ||
|
|
b33a365685 | ||
|
|
995f95e7bd | ||
|
|
fb8adbe27c | ||
|
|
035f946c05 | ||
|
|
78ab2344be | ||
|
|
aa799e32b6 | ||
|
|
3e621f8f33 | ||
|
|
52670e7b2b | ||
|
|
a70cbbcae1 | ||
|
|
1f0ea54629 | ||
|
|
15b36c15d7 | ||
|
|
39daa502ad | ||
|
|
43dc2a956e | ||
|
|
512556dfba | ||
|
|
af54d611b1 | ||
|
|
d214167402 | ||
|
|
6f5fab57db | ||
|
|
bb73fd9192 | ||
|
|
39d16921ad | ||
|
|
761dc8e1c5 | ||
|
|
f20d0f9a69 | ||
|
|
9a7a6e6896 | ||
|
|
316d4ef3e2 | ||
|
|
ccb1e6f6f7 | ||
|
|
0fe60ed82c | ||
|
|
007a0a6b04 | ||
|
|
c32d6bce8a | ||
|
|
b6983eb60e | ||
|
|
41d6e2e54f | ||
|
|
7062c7a854 | ||
|
|
2c5ba0798b | ||
|
|
de30a11b17 | ||
|
|
fdcc9d2d52 | ||
|
|
7da2ff2026 | ||
|
|
9e402d3eec | ||
|
|
73dae33358 | ||
|
|
5d0fd36530 | ||
|
|
30ea9e794c | ||
|
|
78af3e02ad | ||
|
|
c9def4dd03 | ||
|
|
cf0544b118 | ||
|
|
fe3e5c979e | ||
|
|
9b75d0c859 | ||
|
|
248370f363 | ||
|
|
f7a63a0f70 | ||
|
|
780146e429 | ||
|
|
ce0fc82b20 | ||
|
|
29151d837a | ||
|
|
de74b0a912 | ||
|
|
124ae358ff | ||
|
|
cf62e5f29c | ||
|
|
ceeefaf4fb | ||
|
|
3db9aced52 | ||
|
|
889b970fa1 | ||
|
|
3b5984757c | ||
|
|
f5f82bf10e | ||
|
|
875020007d | ||
|
|
2f89201049 | ||
|
|
8f22f71e3f | ||
|
|
61ca0d4211 | ||
|
|
4feab32b04 | ||
|
|
1d7477bac9 | ||
|
|
4a4cc53717 | ||
|
|
4bfe4ec690 | ||
|
|
eb3bb2f1bd | ||
|
|
4bba48afd2 | ||
|
|
e503ddb053 | ||
|
|
4f8e5a6053 | ||
|
|
c86112f2ac | ||
|
|
528a355d8a | ||
|
|
37bfe3bf72 | ||
|
|
3b0c06295f | ||
|
|
090b684ffc | ||
|
|
eab7252bb1 | ||
|
|
212444b3e0 | ||
|
|
c8e0f64a98 | ||
|
|
feca38abdc | ||
|
|
1ccfa5b3d7 | ||
|
|
8b46c68c17 | ||
|
|
1c86e49f2a | ||
|
|
06d219e9ad | ||
|
|
aabd1b63e6 | ||
|
|
a64589bff8 | ||
|
|
6e87ce8a79 | ||
|
|
8ba89854fd | ||
|
|
9ce7903330 | ||
|
|
aec72afffc | ||
|
|
7d278a00df | ||
|
|
ec3682a0c8 | ||
|
|
6f3a9e20ac | ||
|
|
6681ee60fe | ||
|
|
4c93037725 | ||
|
|
8957956a51 | ||
|
|
40f978a777 | ||
|
|
0da8e08be5 | ||
|
|
3058ce6875 | ||
|
|
2813086f85 | ||
|
|
d4450d51ea | ||
|
|
517fbc8ed7 | ||
|
|
07e7a9f791 | ||
|
|
1477b112a6 | ||
|
|
e2e569e108 | ||
|
|
43de4d7b52 | ||
|
|
e69009dbdd | ||
|
|
1ea1d33976 | ||
|
|
aa8c24996c | ||
|
|
3aac126fbc | ||
|
|
9427e0fa78 | ||
|
|
ef95b68f7d | ||
|
|
101ed07224 | ||
|
|
a7f34a23c1 | ||
|
|
b68f208a52 | ||
|
|
721272228b | ||
|
|
7608e6e88b | ||
|
|
b6c0689bbf | ||
|
|
a72477df2e | ||
|
|
2ba1306631 | ||
|
|
31599ccf78 | ||
|
|
daad195853 | ||
|
|
7fdb54fd52 | ||
|
|
c7fe3b7663 | ||
|
|
24b8b5b320 | ||
|
|
ce25e984bd | ||
|
|
a69628daa5 | ||
|
|
5ea15ce8a4 | ||
|
|
f35a1e3d0f | ||
|
|
a99f9ee33e | ||
|
|
75c20183bb | ||
|
|
33d9f61a68 | ||
|
|
29daeab6ff | ||
|
|
c42f8d4bb6 | ||
|
|
84f4f444cb | ||
|
|
d4516d5273 | ||
|
|
87e795f17d | ||
|
|
08e846c4f0 | ||
|
|
60936a7845 | ||
|
|
6269b28f27 | ||
|
|
e35510124a | ||
|
|
e55af6cde4 | ||
|
|
a0e6be274f | ||
|
|
d1948d2e81 | ||
|
|
2e556114ad | ||
|
|
0a9b3be145 | ||
|
|
f573685af9 | ||
|
|
dc5bbfb996 | ||
|
|
31368905d6 | ||
|
|
b36cb88a01 | ||
|
|
88f3437a5f | ||
|
|
7de57576da | ||
|
|
c81c01fa4f | ||
|
|
641e0f576b | ||
|
|
a94b27332f | ||
|
|
a95ba4981f | ||
|
|
4ac4ccc63d | ||
|
|
e2b5d2cf8a | ||
|
|
92d26a101f | ||
|
|
63693d28ce | ||
|
|
287325b034 | ||
|
|
325c6b661c | ||
|
|
26f8fe6b02 | ||
|
|
68aabbd91d | ||
|
|
e3b02eee88 | ||
|
|
1c7c981dbc | ||
|
|
52ecf1a4dc | ||
|
|
18110ee703 | ||
|
|
e81998e222 | ||
|
|
2d5c3bd5e2 | ||
|
|
23a53d47c2 | ||
|
|
2f91443d2e | ||
|
|
166ffc4428 | ||
|
|
82e293d12e | ||
|
|
4bf8c364ee | ||
|
|
b5cbaaeb66 | ||
|
|
4bbeeba20e | ||
|
|
ad7fbeb31b | ||
|
|
701ece62f3 | ||
|
|
f49af402d0 | ||
|
|
e7c40ad63f | ||
|
|
a6841d5034 | ||
|
|
161c6d8433 | ||
|
|
1572f625d3 | ||
|
|
bde805a3a2 | ||
|
|
d8fca5b16f | ||
|
|
5e330658ec | ||
|
|
1f293845ea | ||
|
|
4fa19dc884 | ||
|
|
4afeb20b44 | ||
|
|
b9936e4b77 | ||
|
|
41446bc563 | ||
|
|
9313666fd2 | ||
|
|
2b49cc7a6f | ||
|
|
31151260df | ||
|
|
f8e8780de1 | ||
|
|
1e933c859a | ||
|
|
ce3c40dc1e | ||
|
|
df87fa8151 | ||
|
|
c6d9fc1e4a | ||
|
|
cf8dce1220 | ||
|
|
63a8e845f9 | ||
|
|
5e8059f223 | ||
|
|
6a35613a62 | ||
|
|
931d0396ce | ||
|
|
cf06e22643 | ||
|
|
86be028e41 | ||
|
|
e0afcf0b8e | ||
|
|
ba5253c09d | ||
|
|
4317c9c7bd | ||
|
|
be4fe5a44f | ||
|
|
406deccfd9 | ||
|
|
48eb9dd1b8 | ||
|
|
88d2d80678 | ||
|
|
18ad8e6f89 | ||
|
|
36956fc8a8 | ||
|
|
cd5cf56c3b | ||
|
|
e733ed7134 | ||
|
|
2cff552012 | ||
|
|
10fe06a8a7 | ||
|
|
3c89de64cb | ||
|
|
79551c52c8 | ||
|
|
4d3011e23b | ||
|
|
2cd7c3bd7f | ||
|
|
6cc9583604 | ||
|
|
352f6594e3 | ||
|
|
2373ac2d19 | ||
|
|
dfe7f6372a | ||
|
|
3e02b72dde | ||
|
|
88b17be010 | ||
|
|
8dd3d774c4 | ||
|
|
974f233431 | ||
|
|
4fd3ccefb0 | ||
|
|
2067f158fe | ||
|
|
1ddf270847 | ||
|
|
7073aeb307 | ||
|
|
9af3415ac6 | ||
|
|
61720a5f90 | ||
|
|
e15a87ef7f | ||
|
|
997f6985ba | ||
|
|
b132f63871 | ||
|
|
6c4dad03b5 | ||
|
|
7efcb96921 | ||
|
|
22dcbe1c13 | ||
|
|
8ae8b5639f | ||
|
|
8b8d32e20b | ||
|
|
d004716c4b | ||
|
|
2b154a4cff | ||
|
|
df411a0e00 | ||
|
|
381ecb95a9 | ||
|
|
886160ca0f | ||
|
|
b178f785de | ||
|
|
9266b05fad | ||
|
|
973fc68893 | ||
|
|
a0b8af9aa0 | ||
|
|
4a97979027 | ||
|
|
c5f5834b64 | ||
|
|
8e10ff79b1 | ||
|
|
7950610b4d | ||
|
|
93277c7d4f | ||
|
|
9523d8252d | ||
|
|
f16f2748a6 | ||
|
|
f04c4d70b9 | ||
|
|
f8e337ea92 | ||
|
|
347ab47672 | ||
|
|
d25de226d0 | ||
|
|
aa5b813929 | ||
|
|
bf513dee43 | ||
|
|
cb68387437 | ||
|
|
f8da1f5337 | ||
|
|
952eb31cc2 | ||
|
|
1d9b401405 | ||
|
|
70ec2d8773 | ||
|
|
383e1fd67b | ||
|
|
a013c8efb0 | ||
|
|
1b255f211e | ||
|
|
7b64a7cdd3 | ||
|
|
e3b34b81fd | ||
|
|
d70d2c3374 | ||
|
|
af06f09269 | ||
|
|
b32043cfd9 | ||
|
|
00b611d6f6 | ||
|
|
afea58c53a | ||
|
|
0db36ea1b8 | ||
|
|
5aa3b543ce | ||
|
|
3dfde5eb04 | ||
|
|
351c872c46 | ||
|
|
802f93c30c | ||
|
|
94b0dce77e | ||
|
|
2d2d378dca | ||
|
|
f84afcd7ab | ||
|
|
b7f507271e | ||
|
|
399e5b1683 | ||
|
|
0140dc6ff4 | ||
|
|
d1d403815e | ||
|
|
af2dabaa9d | ||
|
|
c9a8cec161 | ||
|
|
190d439d36 | ||
|
|
0a53312dee | ||
|
|
1759115a80 | ||
|
|
be477f2433 | ||
|
|
d3a3e27649 | ||
|
|
c832f74a26 | ||
|
|
721aa664a8 | ||
|
|
96982d52f1 | ||
|
|
a59d711c1e | ||
|
|
0559903fd6 | ||
|
|
a904c6a62e | ||
|
|
d73e144511 | ||
|
|
4730138dc1 | ||
|
|
6dd37bd8d2 | ||
|
|
05452e6f46 | ||
|
|
b7593b4042 | ||
|
|
000a48cc57 | ||
|
|
a89d0a6dc2 | ||
|
|
caa014a0ac | ||
|
|
8199642f35 | ||
|
|
da6525120d | ||
|
|
362760d3ab | ||
|
|
1cce67702d | ||
|
|
ff761dd088 | ||
|
|
beda70b1f9 | ||
|
|
eb9eb2009f | ||
|
|
73bcfef007 | ||
|
|
3104d5aec9 | ||
|
|
0d3c39cc0a | ||
|
|
e57059cf3c | ||
|
|
18e9a99d17 | ||
|
|
7b7549fef0 | ||
|
|
251f43fb77 | ||
|
|
6c4077b143 | ||
|
|
00a7e92ef7 | ||
|
|
61c1a3db8e | ||
|
|
bdcd65d6c9 | ||
|
|
f7a427b809 | ||
|
|
e96e727c7c | ||
|
|
f2ea26b529 | ||
|
|
7b10c48e8f | ||
|
|
0c71e82675 | ||
|
|
7fa7c8f9f8 | ||
|
|
102eb934af | ||
|
|
099e0193e9 | ||
|
|
c68b4bf392 | ||
|
|
14b2a10fdf | ||
|
|
a9ba96e08b | ||
|
|
d4c3ebab24 | ||
|
|
4f5396f945 | ||
|
|
59a5f72f7c | ||
|
|
47fbf495f1 | ||
|
|
55529f49e3 | ||
|
|
dd0f410161 | ||
|
|
6ee2f9c962 | ||
|
|
f24e20a902 | ||
|
|
1cf0155622 | ||
|
|
aa208d2ae5 | ||
|
|
1d8b9824ad | ||
|
|
bd1917a8a7 | ||
|
|
50a43e8e01 | ||
|
|
bee8be9698 | ||
|
|
774c9ba12d | ||
|
|
8be5c1b9eb | ||
|
|
9ada09e809 | ||
|
|
007a997f8f | ||
|
|
d70f9290f5 | ||
|
|
d32be4aba7 | ||
|
|
359dd05972 | ||
|
|
c005d331ee | ||
|
|
e51f136c4e | ||
|
|
95ee6e4e24 | ||
|
|
677d3a4a76 | ||
|
|
bd9f63de4c | ||
|
|
0a51bd366e | ||
|
|
2c959b0e00 | ||
|
|
291c3533e2 | ||
|
|
6080a3e6ec | ||
|
|
97785abff4 | ||
|
|
511f44e806 | ||
|
|
f5a97b4730 | ||
|
|
a47ba79413 | ||
|
|
429eaf76a5 | ||
|
|
da9136a08b | ||
|
|
cb0097a4e0 | ||
|
|
f66d2c55ca | ||
|
|
a30e58677d | ||
|
|
11d1898dc2 | ||
|
|
24548a081c | ||
|
|
09982b9f60 | ||
|
|
b56036b71e | ||
|
|
52fe89170f | ||
|
|
474043a569 | ||
|
|
7451605dbb | ||
|
|
860c487074 | ||
|
|
b1303413c6 | ||
|
|
6ad76560ed | ||
|
|
d6f449930c | ||
|
|
ebdf501115 | ||
|
|
34861fb9b9 | ||
|
|
2f3fe8ae6b | ||
|
|
4ee91654ea | ||
|
|
3190658e0c | ||
|
|
c330523b9e | ||
|
|
5a5c865135 | ||
|
|
f5ae301c97 | ||
|
|
d910441470 | ||
|
|
b667bf4117 | ||
|
|
4d8b4c9d43 | ||
|
|
02c44eef82 | ||
|
|
c21d10d7f9 | ||
|
|
d2d14e4c19 | ||
|
|
1da8bca9d1 | ||
|
|
dffbfd8860 | ||
|
|
7247c51b10 | ||
|
|
2517e30f55 | ||
|
|
caa0e47399 | ||
|
|
8ce6ec49e1 | ||
|
|
276b18650c | ||
|
|
a69a8a269f | ||
|
|
921628b2a1 | ||
|
|
53225b427c | ||
|
|
6aad1eb92d | ||
|
|
d4bccb9b05 | ||
|
|
02e9886e2e | ||
|
|
a05363d202 | ||
|
|
78fd1e206b | ||
|
|
74951b9dec | ||
|
|
5ac21ea240 | ||
|
|
086e92da4b | ||
|
|
e54c4c1bec | ||
|
|
5bb20e482f | ||
|
|
569462f755 | ||
|
|
658050548c | ||
|
|
4d635f6eb4 | ||
|
|
a0b1e9177b | ||
|
|
7a1a7843e6 | ||
|
|
66d4e02024 | ||
|
|
04a46f0cd9 | ||
|
|
4e0ac5330f | ||
|
|
32288aba97 | ||
|
|
801f119a5c | ||
|
|
567a02872b | ||
|
|
cfc19cafc5 | ||
|
|
44823f5043 | ||
|
|
bad5f8e561 | ||
|
|
075e1e8974 | ||
|
|
06f78ce620 | ||
|
|
cce9acb182 | ||
|
|
048b8f0385 | ||
|
|
fa318ac751 | ||
|
|
071ad16f83 | ||
|
|
eb44fc0ef7 | ||
|
|
f35239e1a4 | ||
|
|
5534878dcd | ||
|
|
f1b82cf89a | ||
|
|
4ae81b9f52 | ||
|
|
7af412e6c5 | ||
|
|
52baa2e8cc | ||
|
|
d48c159283 | ||
|
|
fae51bd9b2 | ||
|
|
9a6e16f505 | ||
|
|
44afd62989 | ||
|
|
69986cc40d | ||
|
|
9769a36c30 | ||
|
|
1621510b15 | ||
|
|
85f910dd48 | ||
|
|
e260124351 | ||
|
|
1eb2d21086 | ||
|
|
fedc67e1e2 | ||
|
|
c88a7b134f | ||
|
|
20dfc02624 | ||
|
|
2ca11e53be | ||
|
|
1b79794ede | ||
|
|
806de6910a | ||
|
|
ba30fc1613 | ||
|
|
cd5774bfd3 | ||
|
|
df9cd5fe48 | ||
|
|
76d189b8de | ||
|
|
0a0fd8a9a3 | ||
|
|
f78764d18a | ||
|
|
59b352df35 | ||
|
|
c241cc9df9 | ||
|
|
5ba6291fd9 | ||
|
|
c8715218c8 | ||
|
|
cc9caf3d54 | ||
|
|
6d298daea4 | ||
|
|
07ef6b6f41 | ||
|
|
6c8e193130 | ||
|
|
b7c31fa7a0 | ||
|
|
d25c7b351e | ||
|
|
90103b9fba | ||
|
|
7c39b06297 | ||
|
|
2ead20b2f7 | ||
|
|
c8821b1055 | ||
|
|
50b6f773f5 | ||
|
|
11705d46f2 | ||
|
|
3e96211433 | ||
|
|
0c3cefc0ff | ||
|
|
deab5fc2c2 | ||
|
|
9c68120447 | ||
|
|
6f3181a643 | ||
|
|
416b04fdec | ||
|
|
c2958445cb | ||
|
|
1176db8f41 | ||
|
|
136e22bb84 | ||
|
|
78b7f38f61 | ||
|
|
05ad58b46e | ||
|
|
8040349718 | ||
|
|
fca212edc3 | ||
|
|
7237fd2c63 | ||
|
|
26c02fdbac | ||
|
|
aa4a3531e9 | ||
|
|
4495129a66 | ||
|
|
791ed3fe32 | ||
|
|
b29688dc8d | ||
|
|
2936a12e25 | ||
|
|
213fa15ef3 | ||
|
|
80f25d016c | ||
|
|
a6ad9b5187 | ||
|
|
2f5765cc3f | ||
|
|
d08b5b7041 | ||
|
|
2772cf6389 | ||
|
|
6317aec577 | ||
|
|
43f576d1c5 | ||
|
|
d8493bc6bf | ||
|
|
96010b48c1 | ||
|
|
a803e007d0 | ||
|
|
0699bdd141 | ||
|
|
c5b1bb766b | ||
|
|
e0eff168bf | ||
|
|
1c88173b3e | ||
|
|
6c2e005cd7 | ||
|
|
08780edcc1 | ||
|
|
d86d785061 | ||
|
|
67cf11966b | ||
|
|
9c96cc4044 | ||
|
|
d1e4c1d09f | ||
|
|
944f3a363f | ||
|
|
79bd4b1925 | ||
|
|
8ea3351eee | ||
|
|
8d90ca0892 | ||
|
|
4bb7a95e73 | ||
|
|
13e553d818 | ||
|
|
92b8977797 | ||
|
|
a347255752 | ||
|
|
e8b329e688 | ||
|
|
d77d326950 | ||
|
|
9832e6b011 | ||
|
|
5bae9e72c9 | ||
|
|
dba2ee1556 | ||
|
|
357f28321c | ||
|
|
3402ef8d11 | ||
|
|
643e1d2ac8 | ||
|
|
f16ef03927 | ||
|
|
bf54539c39 | ||
|
|
e71714dbb8 | ||
|
|
a482172be4 | ||
|
|
6fbbd63ad6 | ||
|
|
6126d617f5 | ||
|
|
f507efdef7 | ||
|
|
7d4f50add1 | ||
|
|
201521a4d8 | ||
|
|
8c9289ef1c | ||
|
|
8ba1b0b0c0 | ||
|
|
7871422354 | ||
|
|
eb19d80b97 | ||
|
|
6888cbf7b8 | ||
|
|
5c1842877d | ||
|
|
0bf1d75e0c | ||
|
|
a3e3c33855 | ||
|
|
d5d5e7f74c | ||
|
|
8adfcbe20d | ||
|
|
f8f79ecc75 | ||
|
|
289f42392b | ||
|
|
71cea26ada | ||
|
|
5043a8db4e | ||
|
|
5d3199e306 | ||
|
|
37a132164f | ||
|
|
d02cad0fca | ||
|
|
3530d158a6 | ||
|
|
7eef3a30a8 | ||
|
|
36b23d0b19 | ||
|
|
816277512d | ||
|
|
bec7e06628 | ||
|
|
798a00ae3a | ||
|
|
c852d1e434 | ||
|
|
08a829ca97 | ||
|
|
1c18753511 | ||
|
|
6b1c3ccc39 | ||
|
|
bb207f1aee | ||
|
|
91cc66a8d1 | ||
|
|
cfbfb44307 | ||
|
|
8d29ffd7a7 | ||
|
|
44c5193f54 | ||
|
|
342eec8c26 | ||
|
|
4973404684 | ||
|
|
e45a59c184 | ||
|
|
6efa62ced2 | ||
|
|
a316a6e094 | ||
|
|
297b25a739 | ||
|
|
3aa4ee3548 | ||
|
|
e6e3c27371 | ||
|
|
bdf7298afa | ||
|
|
a81e125a29 | ||
|
|
a454eb16de | ||
|
|
50512e863d | ||
|
|
7ff8e12c4e | ||
|
|
a2815081d3 | ||
|
|
e14fa4b7de | ||
|
|
4c95caf957 | ||
|
|
4a242475c0 | ||
|
|
fb1348a306 | ||
|
|
1b3cea3042 | ||
|
|
264145c4b9 | ||
|
|
daea47bf38 | ||
|
|
2ff5a2e9fc | ||
|
|
fc690abee9 | ||
|
|
157a477a10 | ||
|
|
aa7a412a94 | ||
|
|
2832c21ffd | ||
|
|
3751794f81 | ||
|
|
5c3740b8a9 | ||
|
|
303f049004 | ||
|
|
a52ede2a3d | ||
|
|
f249eb7cae | ||
|
|
2d20dc4282 | ||
|
|
95f8dc8eb3 | ||
|
|
1ae1e824c0 | ||
|
|
72413ef3b4 | ||
|
|
d45c45a868 | ||
|
|
3b646da0a8 | ||
|
|
6121537216 | ||
|
|
2482f11a5a | ||
|
|
ade08f74a4 | ||
|
|
5eade9abe5 | ||
|
|
203986d54c | ||
|
|
8e8c376df3 | ||
|
|
8a6886c71d | ||
|
|
7f02ca1bca | ||
|
|
7df265b357 | ||
|
|
5427168f01 | ||
|
|
8ce7d851cc | ||
|
|
22d3a23099 | ||
|
|
b6dd0285a8 | ||
|
|
9d45d42efe | ||
|
|
6e6e753475 | ||
|
|
3b11c931d4 | ||
|
|
742fbb224f | ||
|
|
b35adac318 | ||
|
|
c33274709e | ||
|
|
89748156a6 | ||
|
|
c01a792e25 | ||
|
|
e4d0937782 | ||
|
|
524110dee9 | ||
|
|
ae480283a3 | ||
|
|
ccaf5878ae | ||
|
|
cb67d4b194 | ||
|
|
701ca68db7 | ||
|
|
cf9c3290b5 | ||
|
|
964994dd90 | ||
|
|
edbd3d37da | ||
|
|
c2d36e2ac2 | ||
|
|
b2c0caaa43 | ||
|
|
8155e77210 | ||
|
|
fdda442dc8 | ||
|
|
b962d9597b | ||
|
|
9e624d0db2 | ||
|
|
4f52ba2d4d | ||
|
|
c9a22c517c | ||
|
|
023cd5f720 | ||
|
|
8b2b03bf0a | ||
|
|
2a14caceeb | ||
|
|
0fd3687843 | ||
|
|
d1c205a588 | ||
|
|
21103ecaac | ||
|
|
2fcbbfce27 | ||
|
|
5e8eda0ea4 | ||
|
|
1e9ea80685 | ||
|
|
427ad71880 | ||
|
|
fbe2ebe98f | ||
|
|
8b8d61eacf | ||
|
|
7d07980a15 | ||
|
|
84829ba83d | ||
|
|
bb3e52d27f | ||
|
|
63d2a4cffc | ||
|
|
5c23ac47b2 | ||
|
|
a52a4a47f3 | ||
|
|
5f5328a280 | ||
|
|
dd81b1babf | ||
|
|
8adf8a2a05 | ||
|
|
4c8914ad8d | ||
|
|
cfe36f16f2 | ||
|
|
46a6d84101 | ||
|
|
2a5dd1c418 | ||
|
|
61253d7c9d | ||
|
|
d4b7262105 | ||
|
|
a644d4ffda | ||
|
|
84ea523d16 | ||
|
|
3ea93cbf13 | ||
|
|
3f4dc08dc7 | ||
|
|
ee8ae6f492 | ||
|
|
984c2dab54 | ||
|
|
d215d087b3 | ||
|
|
42187327d6 | ||
|
|
58a256c121 | ||
|
|
e3ec31ae99 | ||
|
|
f81301f62d | ||
|
|
b7601fda7b | ||
|
|
e3b205046b | ||
|
|
2c70cedaa6 | ||
|
|
cea6c532e0 | ||
|
|
2e28b5904d | ||
|
|
4288c032db | ||
|
|
04a5378a87 | ||
|
|
ca5918ded9 | ||
|
|
2e7737c1af | ||
|
|
441bb10624 | ||
|
|
9adb23b280 | ||
|
|
7b547b2bc8 | ||
|
|
4ec75ad266 | ||
|
|
7dcf68d2be | ||
|
|
9199eb4290 | ||
|
|
8831af3fb4 | ||
|
|
20b1686b04 | ||
|
|
ae028f485a | ||
|
|
d5af0c8d7e | ||
|
|
e596998a72 | ||
|
|
f224c0b94a | ||
|
|
bc06467784 | ||
|
|
b2f369de10 | ||
|
|
0e070308db | ||
|
|
1b7c3ffae0 | ||
|
|
1069bf73e7 | ||
|
|
483b708def | ||
|
|
be12c0d21f | ||
|
|
bc57964aed | ||
|
|
a6f2abaab9 | ||
|
|
304ff1a42c | ||
|
|
a5048b317d | ||
|
|
f42b5c2a99 | ||
|
|
5827b42732 | ||
|
|
6e62571cce | ||
|
|
a68e42657f | ||
|
|
bad47421c0 | ||
|
|
757f0a411c | ||
|
|
cba411658f | ||
|
|
e560b1e591 | ||
|
|
e280b82582 | ||
|
|
a49900a2d7 | ||
|
|
8ece4ae651 | ||
|
|
1949e8a9b7 | ||
|
|
52207a5ed8 | ||
|
|
f90db72f8b | ||
|
|
d298b4caa2 | ||
|
|
8109f5ae41 | ||
|
|
e4ba7b0eba | ||
|
|
ed3087a222 | ||
|
|
c6f6601f3c | ||
|
|
fb6a1c1329 | ||
|
|
920c012338 | ||
|
|
b629756f3e | ||
|
|
0cf4643d5f | ||
|
|
73d757122a | ||
|
|
975c953d78 | ||
|
|
9de1af4204 | ||
|
|
7b7502fa2f | ||
|
|
b565005219 | ||
|
|
68008c675e | ||
|
|
2cf86eb6ae | ||
|
|
b0a1f9a680 | ||
|
|
3f7816762e | ||
|
|
8bade51eb5 | ||
|
|
7c4398bfb5 | ||
|
|
8f736e8bd3 | ||
|
|
65a9b11dc5 | ||
|
|
7ffd16df4b | ||
|
|
e2628d27dc | ||
|
|
8799c2bb5e | ||
|
|
4ba0b64d2c | ||
|
|
4b2f43e8a2 | ||
|
|
df774ca3c5 | ||
|
|
084fab576e | ||
|
|
1733e64403 | ||
|
|
1d0360c0c3 | ||
|
|
45823ccd96 | ||
|
|
ef3b64cf51 | ||
|
|
a424fb8793 | ||
|
|
b6ae819d32 | ||
|
|
8711ae2452 | ||
|
|
a90c2c2fa8 | ||
|
|
03a89d4f43 | ||
|
|
d48b2bdf2a | ||
|
|
6a6ca8c642 | ||
|
|
d3ffecb866 | ||
|
|
083f91611a | ||
|
|
01e9c45df6 | ||
|
|
b327ebc5bd | ||
|
|
ec1a5add73 | ||
|
|
b7110a7222 | ||
|
|
2e19d2eac1 | ||
|
|
ad7f388d68 | ||
|
|
f28c1e7fae | ||
|
|
6ce342c0b3 | ||
|
|
622bdf613c | ||
|
|
a0efd1087f | ||
|
|
9813295fd3 | ||
|
|
303fdfc9ad | ||
|
|
90d88a998c | ||
|
|
1999e1daf2 | ||
|
|
433f39dd38 | ||
|
|
110746e859 | ||
|
|
640860a3af | ||
|
|
b21fa807db | ||
|
|
446dd921bf | ||
|
|
594fccd602 | ||
|
|
47993b612f | ||
|
|
2302f2dbef | ||
|
|
62469e806a | ||
|
|
086a068fb1 | ||
|
|
97342edee0 |
@@ -1,11 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<addon id="plugin.video.jellycon"
|
||||
name="JellyCon"
|
||||
version="1.9.100"
|
||||
provider-name="Team B">
|
||||
version=""
|
||||
provider-name="Jellyfin Contributors">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="2.25.0"/>
|
||||
<import addon="script.module.pil" version="1.1.7"/>
|
||||
</requires>
|
||||
<extension point="xbmc.python.pluginsource" library="default.py">
|
||||
<provides>video audio</provides>
|
||||
@@ -20,7 +18,7 @@
|
||||
<website>https://github.com/jellyfin/jellycon/wiki</website>
|
||||
<source>https://github.com/jellyfin/jellycon</source>
|
||||
<summary lang="en_GB">Browse and play your Jellyfin server media library.</summary>
|
||||
<description lang="en_GB">An addon to allow you to browse and playback your Jellyfin (www.jellyfin.org) Movie, TV Show and Music collections.</description>
|
||||
<description lang="en_GB">An addon to allow you to browse and playback your Jellyfin (https://jellyfin.org) Movie, TV Show and Music collections.</description>
|
||||
<assets>
|
||||
<icon>icon.png</icon>
|
||||
<fanart>fanart.jpg</fanart>
|
||||
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**To Reproduce**
|
||||
<!-- Steps to reproduce the behavior: -->
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen. -->
|
||||
|
||||
**Logs**
|
||||
<!-- Please paste any log errors. -->
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**System (please complete the following information):**
|
||||
- OS: [e.g. Android, Debian, Windows]
|
||||
- Jellyfin Version: [e.g. 10.0.1]
|
||||
- Kodi Version: [e.g. 18.3]
|
||||
- Addon Version: [e.g. 0.1.1]
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here. -->
|
||||
16
.github/dependabot.yaml
vendored
Normal file
16
.github/dependabot.yaml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
labels:
|
||||
- ci
|
||||
- github-actions
|
||||
- package-ecosystem: pip
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
labels:
|
||||
- pip
|
||||
- dependencies
|
||||
22
.github/release-drafter.yml
vendored
Normal file
22
.github/release-drafter.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
_extends: jellyfin/jellyfin-meta-plugins
|
||||
|
||||
name-template: "Release $RESOLVED_VERSION"
|
||||
tag-template: "v$RESOLVED_VERSION"
|
||||
version-template: "$MAJOR.$MINOR.$PATCH"
|
||||
|
||||
version-resolver:
|
||||
major:
|
||||
labels:
|
||||
- 'major'
|
||||
minor:
|
||||
labels:
|
||||
- 'minor'
|
||||
patch:
|
||||
labels:
|
||||
- 'patch'
|
||||
default: patch
|
||||
|
||||
template: |
|
||||
## :sparkles: What's New
|
||||
|
||||
$CHANGES
|
||||
12
.github/releasing.md
vendored
Normal file
12
.github/releasing.md
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# Releasing a new Version via GitHub Actions
|
||||
|
||||
0. (optional) label the PRs you want to include in this release (if you want to group them in the GH release based on topics). \
|
||||
Supported labels can be found in the Release Drafter [config-file](https://github.com/jellyfin/jellyfin-meta-plugins/blob/master/.github/release-drafter.yml) (currently inherited from `jellyfin/jellyfin-meta-plugins`)
|
||||
1. ensure you have merged the PRs you want to include in the release and that the so far drafted GitHub release has captured them
|
||||
2. Create a `release-prep` PR by manually triggering the 'Create Prepare-Release PR' Workflow from the Actions tab on GitHub
|
||||
3. check the newly created `Prepare for release vx.y.z` PR if updated the `release.yaml` properly (update it manually if need be)
|
||||
4. merge the `Prepare for release vx.y.z` and let the Actions triggered by doing that finis (should just be a couple of seconds)
|
||||
5. FINALLY, trigger the `Publish JellyCon` manually from the Actions tab on GitHub.
|
||||
1. this will release the up to that point drafted GitHub Release and tag the default branch accordingly
|
||||
2. this will package and deploy `JellyCon` in the new version to the deployment server and trigger the 'kodirepo' script on it
|
||||
6. Done, assuming everything ran successfully, you have now successfully published a new version! :tada:
|
||||
87
.github/tools/reformat_changelog.py
vendored
Executable file
87
.github/tools/reformat_changelog.py
vendored
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3.8
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import re
|
||||
from typing import Dict, List, Pattern, Union, TypedDict
|
||||
|
||||
from emoji.core import emojize, demojize, replace_emoji
|
||||
|
||||
|
||||
ITEM_FORMAT = "+ {title} (#{issue}) @{username}"
|
||||
OUTPUT_EMOJI = False
|
||||
|
||||
ITEM_PATTERN: Pattern = re.compile(
|
||||
r"^\s*(?P<old_listchar>[-*+])\s*(?P<title>.*?)\s*\(#(?P<issue>[0-9]+)\)\s*@(?P<username>[^\s]*)$"
|
||||
)
|
||||
|
||||
|
||||
class SectionType(TypedDict):
|
||||
title: str
|
||||
items: List[Dict[str, str]]
|
||||
|
||||
|
||||
def reformat(item_format: str, output_emoji: bool) -> None:
|
||||
data = [
|
||||
emojize(x.strip(), variant="emoji_type")
|
||||
for x in sys.stdin.readlines()
|
||||
if x.strip()
|
||||
]
|
||||
|
||||
sections = []
|
||||
|
||||
section: Union[SectionType, Dict] = {}
|
||||
for line in data:
|
||||
if line.startswith("## "):
|
||||
pass
|
||||
if line.startswith("### "):
|
||||
if section:
|
||||
sections.append(section)
|
||||
_section: SectionType = {
|
||||
"title": line.strip("# "),
|
||||
"items": [],
|
||||
}
|
||||
section = _section
|
||||
|
||||
m = ITEM_PATTERN.match(line)
|
||||
if m:
|
||||
gd = m.groupdict()
|
||||
section["items"].append(gd)
|
||||
|
||||
sections.append(section)
|
||||
|
||||
first = True
|
||||
|
||||
for section in sections:
|
||||
if not section:
|
||||
continue
|
||||
if first:
|
||||
first = False
|
||||
else:
|
||||
print()
|
||||
|
||||
title = section["title"]
|
||||
if not output_emoji:
|
||||
title = replace_emoji(title).strip()
|
||||
|
||||
print(title)
|
||||
print("-" * len(title))
|
||||
|
||||
for item in section["items"]:
|
||||
formatted_item = item_format.format(**item)
|
||||
if not output_emoji:
|
||||
formatted_item = demojize(formatted_item)
|
||||
print(formatted_item)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--format", type=str, default=ITEM_FORMAT)
|
||||
|
||||
parser.add_argument("--no-emoji", dest="emoji", action="store_false")
|
||||
parser.add_argument("--emoji", dest="emoji", action="store_true")
|
||||
parser.set_defaults(emoji=OUTPUT_EMOJI)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
reformat(args.format, args.emoji)
|
||||
39
.github/workflows/build.yaml
vendored
Normal file
39
.github/workflows/build.yaml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Build JellyCon
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
py_version: [ 'py2', 'py3' ]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.x
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install pyyaml
|
||||
|
||||
- name: Create ${{ matrix.py_version }} addon.xml
|
||||
run: python build.py --version ${{ matrix.py_version }}
|
||||
|
||||
- name: Publish Build Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 14
|
||||
name: ${{ matrix.py_version }}-build-artifact
|
||||
path: |
|
||||
*.zip
|
||||
41
.github/workflows/codeql.yaml
vendored
Normal file
41
.github/workflows/codeql.yaml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: CodeQL Analysis
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
schedule:
|
||||
- cron: '38 8 * * 6'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'jellyfin/jellycon' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'python' ]
|
||||
version: ['3.9']
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-and-quality
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.version }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
72
.github/workflows/create-prepare-release-pr.yaml
vendored
Normal file
72
.github/workflows/create-prepare-release-pr.yaml
vendored
Normal file
@@ -0,0 +1,72 @@
|
||||
name: Create Prepare-Release PR
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
create_pr:
|
||||
name: "Create Pump Version PR"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Update Draft
|
||||
uses: release-drafter/release-drafter@v6.1.0
|
||||
id: draft
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Setup YQ
|
||||
uses: chrisdickinson/setup-yq@latest
|
||||
with:
|
||||
yq-version: v4.9.1
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Parse Changelog
|
||||
run: |
|
||||
pip install emoji
|
||||
cat << EOF >> cl.md
|
||||
${{ steps.draft.outputs.body }}
|
||||
EOF
|
||||
TAG="${{ steps.draft.outputs.tag_name }}"
|
||||
echo "VERSION=${TAG#v}" >> $GITHUB_ENV
|
||||
echo "YAML_CHANGELOG<<EOF" >> $GITHUB_ENV
|
||||
cat cl.md | python .github/tools/reformat_changelog.py --no-emoji >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
|
||||
cat cl.md | python .github/tools/reformat_changelog.py --emoji --format='+ #{issue} by @{username}' >> $GITHUB_ENV
|
||||
echo "EOF" >> $GITHUB_ENV
|
||||
rm cl.md
|
||||
|
||||
- name: Update release.yaml
|
||||
run: |
|
||||
yq eval '.version = env(VERSION) | .changelog = strenv(YAML_CHANGELOG) | .changelog style="literal"' -i release.yaml
|
||||
|
||||
- name: Commit Changes
|
||||
run: |
|
||||
git config user.name "jellyfin-bot"
|
||||
git config user.email "team@jellyfin.org"
|
||||
|
||||
git checkout -b prepare-${{ env.VERSION }}
|
||||
git commit -am "bump version to ${{ env.VERSION }}"
|
||||
|
||||
if [[ -z "$(git ls-remote --heads origin prepare-${{ env.VERSION }})" ]]; then
|
||||
git push origin prepare-${{ env.VERSION }}
|
||||
else
|
||||
git push -f origin prepare-${{ env.VERSION }}
|
||||
fi
|
||||
|
||||
- name: Create or Update PR
|
||||
uses: k3rnels-actions/pr-update@v2
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
pr_title: Prepare for release ${{ steps.draft.outputs.tag_name }}
|
||||
pr_source: prepare-${{ env.VERSION }}
|
||||
pr_labels: 'release-prep,skip-changelog'
|
||||
pr_body: |
|
||||
:robot: This is a generated PR to bump the `release.yaml` version and update the changelog.
|
||||
|
||||
---
|
||||
|
||||
${{ env.CHANGELOG }}
|
||||
64
.github/workflows/publish.yaml
vendored
Normal file
64
.github/workflows/publish.yaml
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
name: Publish JellyCon
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
py_version: [ 'py2', 'py3' ]
|
||||
steps:
|
||||
- name: Update Draft
|
||||
uses: release-drafter/release-drafter@v6.1.0
|
||||
if: ${{ matrix.py_version == 'py3' }}
|
||||
with:
|
||||
publish: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.x
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install pyyaml
|
||||
|
||||
- name: Create ${{ matrix.py_version }} addon.xml
|
||||
run: python build.py --version ${{ matrix.py_version }}
|
||||
|
||||
- name: Publish Build Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 14
|
||||
name: ${{ matrix.py_version }}-build-artifact
|
||||
path: |
|
||||
*.zip
|
||||
|
||||
- name: Upload to repo server
|
||||
uses: burnett01/rsync-deployments@5.2
|
||||
with:
|
||||
switches: -vrptz
|
||||
path: '*.zip'
|
||||
remote_path: /srv/incoming/kodi
|
||||
remote_host: ${{ secrets.REPO_HOST }}
|
||||
remote_user: ${{ secrets.REPO_USER }}
|
||||
remote_key: ${{ secrets.REPO_KEY }}
|
||||
|
||||
- name: Add to Kodi repo and clean up
|
||||
uses: appleboy/ssh-action@v1.2.0
|
||||
with:
|
||||
host: ${{ secrets.REPO_HOST }}
|
||||
username: ${{ secrets.REPO_USER }}
|
||||
key: ${{ secrets.REPO_KEY }}
|
||||
script_stop: true
|
||||
script: |
|
||||
python3 /usr/local/bin/kodirepo add /srv/incoming/kodi/plugin.video.jellycon+${{ matrix.py_version }}.zip --datadir /srv/repository/main/client/kodi/${{ matrix.py_version }};
|
||||
rm /srv/incoming/kodi/plugin.video.jellycon+${{ matrix.py_version }}.zip;
|
||||
16
.github/workflows/release-drafter.yaml
vendored
Normal file
16
.github/workflows/release-drafter.yaml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
name: Release Drafter
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
update_release_draft:
|
||||
name: Update release draft
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Update Release Draft
|
||||
uses: release-drafter/release-drafter@v6.1.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.JF_BOT_TOKEN }}
|
||||
49
.github/workflows/test.yaml
vendored
Normal file
49
.github/workflows/test.yaml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Test JellyCon
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
PR_TRIGGERED: ${{ github.event_name == 'pull_request' && github.repository == 'jellyfin/jellycon' }}
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
py_version: ['3.9']
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.py_version }}
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.py_version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r requirements-dev.txt
|
||||
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --output-file=flake8.output
|
||||
cat flake8.output
|
||||
|
||||
- name: Publish Test Atrifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 14
|
||||
name: ${{ matrix.py_version }}-test-results
|
||||
path: |
|
||||
flake8.output
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -220,3 +220,8 @@ pip-log.txt
|
||||
|
||||
#Mr Developer
|
||||
.mr.developer.cfg
|
||||
|
||||
venv
|
||||
|
||||
# Addon files
|
||||
addon.xml
|
||||
|
||||
144
PATCHES.md
Normal file
144
PATCHES.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# JellyCon Patches
|
||||
|
||||
Diese Patches fügen neue Features und Fixes zu JellyCon hinzu.
|
||||
|
||||
## Patches Übersicht
|
||||
|
||||
### 1. websocket-keepalive-fix.patch (3.1 KB)
|
||||
**Was es behebt:** Session-Flapping Problem (ständige Reconnects alle 2 Minuten)
|
||||
|
||||
**Änderungen:**
|
||||
- Implementiert Jellyfin's ForceKeepAlive/KeepAlive Protokoll
|
||||
- Entfernt problematische `ping_timeout` und `reconnect` Parameter
|
||||
- WebSocketApp wird bei jedem Reconnect neu erstellt (verhindert Memory-Leaks)
|
||||
- Fügt `on_close` Callback hinzu für besseres Debugging
|
||||
|
||||
**Betroffene Dateien:**
|
||||
- `resources/lib/websocket_client.py`
|
||||
|
||||
**Anwendung:**
|
||||
```bash
|
||||
cd plugin.video.jellycon
|
||||
patch -p1 < websocket-keepalive-fix.patch
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. transcode-target-codec.patch (4.4 KB)
|
||||
**Was es hinzufügt:** Konfigurierbare Transcode-Ziel-Codecs (H.264, H.265, AV1)
|
||||
|
||||
**Features:**
|
||||
- Neue Dropdown-Option: "Transcode target video codec"
|
||||
- H.264 (default)
|
||||
- H.265 (HEVC) - Perfekt für Raspberry Pi 5!
|
||||
- AV1
|
||||
- Neue Checkbox: "Force transcode h264"
|
||||
- Unabhängige Kontrolle über Quell- und Ziel-Codecs
|
||||
|
||||
**Perfekt für:**
|
||||
- Raspberry Pi 5 (H.265 Hardware-Decode)
|
||||
- Moderne GPUs mit AV1-Unterstützung
|
||||
- Optimierung der Client-Hardware-Beschleunigung
|
||||
|
||||
**Betroffene Dateien:**
|
||||
- `resources/lib/play_utils.py`
|
||||
- `resources/settings.xml`
|
||||
- `resources/language/resource.language.en_gb/strings.po`
|
||||
- `resources/language/resource.language.de/strings.po`
|
||||
|
||||
**Anwendung:**
|
||||
```bash
|
||||
cd plugin.video.jellycon
|
||||
patch -p1 < transcode-target-codec.patch
|
||||
```
|
||||
|
||||
**Konfiguration nach Installation:**
|
||||
```
|
||||
Kodi Settings → Add-ons → JellyCon → Playback
|
||||
|
||||
1. "Transcode target video codec" → H.265 (HEVC)
|
||||
2. "Force transcode h264" → ☑ Aktivieren
|
||||
3. "Force transcode h265" → ☐ Aus
|
||||
|
||||
Ergebnis:
|
||||
- H.264 Content → Server transcodiert zu H.265 → Pi 5 Hardware-Decode
|
||||
- H.265 Content → DirectPlay (Hardware!)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. disable-disk-cache.patch (3.8 KB)
|
||||
**Was es hinzufügt:** Option zum Deaktivieren von Disk-Caching
|
||||
|
||||
**Features:**
|
||||
- Neue Checkbox: "Disable disk caching (RAM only)"
|
||||
- Verhindert Schreiben von `.pickle` Cache-Dateien
|
||||
- Deaktiviert Artwork-Preloading
|
||||
- Daten bleiben nur im RAM
|
||||
|
||||
**Perfekt für:**
|
||||
- SD-Karten (Raspberry Pi) - reduziert Schreibzyklen massiv
|
||||
- Privacy - keine dauerhaften Cache-Dateien
|
||||
- Immer frische Daten vom Server
|
||||
|
||||
**Betroffene Dateien:**
|
||||
- `resources/lib/datamanager.py`
|
||||
- `resources/lib/cache_images.py`
|
||||
- `resources/settings.xml`
|
||||
- `resources/language/resource.language.en_gb/strings.po`
|
||||
- `resources/language/resource.language.de/strings.po`
|
||||
|
||||
**Anwendung:**
|
||||
```bash
|
||||
cd plugin.video.jellycon
|
||||
patch -p1 < disable-disk-cache.patch
|
||||
```
|
||||
|
||||
**Konfiguration nach Installation:**
|
||||
```
|
||||
Kodi Settings → Add-ons → JellyCon → Advanced
|
||||
|
||||
"Disable disk caching (RAM only)" → ☑ Aktivieren
|
||||
```
|
||||
|
||||
**Hinweis:** Kodi's eigener Texture-Cache (`~/.kodi/userdata/Thumbnails/`) läuft weiterhin!
|
||||
|
||||
---
|
||||
|
||||
## Alle Patches auf einmal anwenden
|
||||
|
||||
```bash
|
||||
cd plugin.video.jellycon
|
||||
patch -p1 < websocket-keepalive-fix.patch
|
||||
patch -p1 < transcode-target-codec.patch
|
||||
patch -p1 < disable-disk-cache.patch
|
||||
```
|
||||
|
||||
## Patches rückgängig machen
|
||||
|
||||
```bash
|
||||
cd plugin.video.jellycon
|
||||
patch -p1 -R < websocket-keepalive-fix.patch
|
||||
patch -p1 -R < transcode-target-codec.patch
|
||||
patch -p1 -R < disable-disk-cache.patch
|
||||
```
|
||||
|
||||
## Zusammenfassung der Änderungen
|
||||
|
||||
```
|
||||
7 Dateien geändert, 134 Zeilen hinzugefügt, 15 Zeilen entfernt
|
||||
|
||||
+ resources/lib/websocket_client.py (KeepAlive Fix)
|
||||
+ resources/lib/play_utils.py (Transcode Target)
|
||||
+ resources/lib/datamanager.py (Disk Cache Control)
|
||||
+ resources/lib/cache_images.py (Disk Cache Control)
|
||||
+ resources/settings.xml (Neue Settings)
|
||||
+ resources/language/.../strings.po (Sprachstrings DE+EN)
|
||||
```
|
||||
|
||||
## Getestet mit
|
||||
|
||||
- JellyCon Version: 0.9.0+py3
|
||||
- Kodi Version: Matrix 19+
|
||||
- Jellyfin Server: 10.8+
|
||||
- Raspberry Pi 5 (primärer Use-Case)
|
||||
43
README.md
43
README.md
@@ -1,6 +1,47 @@
|
||||
# JellyCon
|
||||
|
||||
JellyCon is a lightweight Kodi addon that lets you browse and play media files from your Jellyfin server directly within the Kodi interface.
|
||||
JellyCon is a lightweight Kodi add-on that lets you browse and play media files directly from your Jellyfin server within the Kodi interface. It can be thought of as a thin frontend for a Jellyfin server.
|
||||
|
||||
JellyCon can be used with Movie, TV Show, Music Video, and Music libraries, in addition to viewing LiveTV from the server. It can easily switch between multiple user accounts at will. It's easy to integrate with any customizable Kodi skin with a large collection of custom menus. Media items are populated from the server dynamically, and menu speed will vary based on local device speed.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
#### 1. Adding the Jellyfin repository
|
||||
|
||||
https://jellyfin.org/docs/general/clients/kodi.html#install-add-on-repository
|
||||
|
||||
#### 2. Install JellyCon Add-on
|
||||
|
||||
- From within Kodi, navigate to "Add-on Browser"
|
||||
- Select "Install from Repository"
|
||||
- Choose "Kodi Jellyfin Add-ons", followed by "Video Add-ons"
|
||||
- Select the JellyCon add-on and choose install
|
||||
|
||||
#### 3. Login
|
||||
|
||||
- Within a few seconds after the installation you should be prompted for your server details.
|
||||
- If a Jellyfin server is detected on your local network, it will displayed in a dialog. Otherwise, you will be prompted to enter the URL of your Jellyfin server
|
||||
- If Quick Connect is enabled in the server, a code will be displayed that you can use to log in via Quick Connect in the web UI or a mobile app.
|
||||
- If Quick Connect is not enabled, or if you select the "Manual Login" button, you will be able to select a user from the list, or manually login using your username and password.
|
||||
|
||||
|
||||
## Configuration
|
||||
|
||||
#### Configuring Home
|
||||
|
||||
Many Kodi skins allow for customizing of the home menu with custom nodes and widgets. However, all of these use slightly different layouts and terminology. Rather than a step by step guide, this section serves as an barebones introduction to customizing a skin.
|
||||
Examples
|
||||
|
||||
If you would like a link on the home screen to open a library in your Jellyfin server called "Kid's Movies", you would point the menu item to the path: Add-On -> Video Add-On -> JellyCon -> Jellyfin Libraries -> Kid's Movies -> Create menu item to here.
|
||||
|
||||
Beyond just modifying where the home menu headers go, many skins also allow you to use widgets. Widgets help populate the home screen with data, often the posters of media in the selected image. If you would like to display the most recent movies across all of your Jellyfin libraries on the home screen, the path would be: Add-On -> Video Add-On -> JellyCon -> Global Lists -> Movies -> Movies - Recently Added (20) -> Use as widget
|
||||
|
||||
Another common use case of widgets would be to display the next available episodes of shows that you may be watching. As above, this can be done both with individual libraries or with all libraries combined:
|
||||
|
||||
Add-On -> Video Add-On -> JellyCon -> Jellyfin Libraries -> Anime -> Anime - Next Up (20) -> Use as widget
|
||||
Add-On -> Video Add-On -> JellyCon -> Global Lists -> TV Shows -> TV Shows - Next Up (20) -> Use as widget
|
||||
|
||||
|
||||
## License
|
||||
|
||||
|
||||
145
build.py
Executable file
145
build.py
Executable file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import xml.etree.ElementTree as ET
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
def indent(elem: ET.Element, level: int = 0) -> None:
|
||||
"""
|
||||
Nicely formats output xml with newlines and spaces
|
||||
https://stackoverflow.com/a/33956544
|
||||
"""
|
||||
i = "\n" + level * " "
|
||||
if len(elem):
|
||||
if not elem.text or not elem.text.strip():
|
||||
elem.text = i + " "
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
for elem in elem:
|
||||
indent(elem, level + 1)
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
else:
|
||||
if level and (not elem.tail or not elem.tail.strip()):
|
||||
elem.tail = i
|
||||
|
||||
|
||||
def create_addon_xml(config: dict, source: str, py_version: str) -> None:
|
||||
"""
|
||||
Create addon.xml from template file
|
||||
"""
|
||||
# Load template file
|
||||
with open(f'{source}/.config/template.xml', 'r') as f:
|
||||
tree = ET.parse(f)
|
||||
root = tree.getroot()
|
||||
|
||||
# Populate dependencies in template
|
||||
dependencies = config['dependencies'].get(py_version)
|
||||
for dep in dependencies:
|
||||
ET.SubElement(root.find('requires'), 'import', attrib=dep)
|
||||
|
||||
# Populate version string
|
||||
addon_version = config.get('version')
|
||||
root.attrib['version'] = f'{addon_version}+{py_version}'
|
||||
|
||||
# Populate Changelog
|
||||
date = datetime.today().strftime('%Y-%m-%d')
|
||||
changelog = config.get('changelog')
|
||||
for section in root.findall('extension'):
|
||||
news = section.findall('news')
|
||||
if news:
|
||||
news[0].text = f'v{addon_version} ({date}):\n{changelog}'
|
||||
|
||||
# Format xml tree
|
||||
indent(root)
|
||||
|
||||
# Write addon.xml
|
||||
tree.write(f'{source}/addon.xml', encoding='utf-8', xml_declaration=True)
|
||||
|
||||
|
||||
def zip_files(py_version: str, source: str, target: str, dev: bool) -> None:
|
||||
"""
|
||||
Create installable addon zip archive
|
||||
"""
|
||||
archive_name = f'plugin.video.jellycon+{py_version}.zip'
|
||||
|
||||
with zipfile.ZipFile(f'{target}/{archive_name}', 'w') as z:
|
||||
for root, dirs, files in os.walk(args.source):
|
||||
for filename in filter(file_filter, files):
|
||||
file_path = os.path.join(root, filename)
|
||||
if dev or folder_filter(file_path):
|
||||
relative_path = os.path.join('plugin.video.jellycon', os.path.relpath(file_path, source))
|
||||
z.write(file_path, relative_path)
|
||||
|
||||
|
||||
def file_filter(file_name: str) -> bool:
|
||||
"""
|
||||
True if file_name is meant to be included
|
||||
"""
|
||||
return (
|
||||
not (file_name.startswith('plugin.video.jellycon') and file_name.endswith('.zip'))
|
||||
and not file_name.endswith('.pyo')
|
||||
and not file_name.endswith('.pyc')
|
||||
and not file_name.endswith('.pyd')
|
||||
)
|
||||
|
||||
|
||||
def folder_filter(folder_name: str) -> bool:
|
||||
"""
|
||||
True if folder_name is meant to be included
|
||||
"""
|
||||
filters = [
|
||||
'.ci',
|
||||
'.git',
|
||||
'.github',
|
||||
'.config',
|
||||
'.mypy_cache',
|
||||
'.pytest_cache',
|
||||
'__pycache__',
|
||||
'venv',
|
||||
]
|
||||
for f in filters:
|
||||
if f in folder_name.split(os.path.sep):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description='Build flags:')
|
||||
parser.add_argument(
|
||||
'--version',
|
||||
type=str,
|
||||
choices=('py2', 'py3'),
|
||||
default='py3')
|
||||
|
||||
parser.add_argument(
|
||||
'--source',
|
||||
type=Path,
|
||||
default=Path(__file__).absolute().parent)
|
||||
|
||||
parser.add_argument(
|
||||
'--target',
|
||||
type=Path,
|
||||
default=Path(__file__).absolute().parent)
|
||||
|
||||
parser.add_argument('--dev', dest='dev', action='store_true')
|
||||
parser.set_defaults(dev=False)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load config file
|
||||
config_path = os.path.join(args.source, 'release.yaml')
|
||||
with open(config_path, 'r') as fh:
|
||||
release_config = yaml.safe_load(fh)
|
||||
|
||||
create_addon_xml(release_config, args.source, args.version)
|
||||
|
||||
zip_files(args.version, args.source, args.target, args.dev)
|
||||
19
default.py
19
default.py
@@ -2,20 +2,21 @@
|
||||
|
||||
import xbmcaddon
|
||||
|
||||
from resources.lib.simple_logging import SimpleLogging
|
||||
from resources.lib.lazylogger import LazyLogger
|
||||
from resources.lib.functions import main_entry_point
|
||||
from resources.lib.tracking import set_timing_enabled
|
||||
|
||||
log = SimpleLogging('default')
|
||||
log = LazyLogger('default')
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
log_timing_data = settings.getSetting('log_timing') == "true"
|
||||
if log_timing_data:
|
||||
set_timing_enabled(True)
|
||||
try:
|
||||
settings = xbmcaddon.Addon()
|
||||
log_timing_data = settings.getSetting('log_timing') == "true"
|
||||
if log_timing_data:
|
||||
set_timing_enabled(True)
|
||||
except Exception:
|
||||
# During installation/update, addon might not be fully registered yet
|
||||
pass
|
||||
|
||||
log.debug("About to enter mainEntryPoint()")
|
||||
|
||||
main_entry_point()
|
||||
|
||||
# clear done and exit.
|
||||
# sys.modules.clear()
|
||||
|
||||
BIN
fanart.jpg
BIN
fanart.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 32 KiB |
BIN
icon.png
BIN
icon.png
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 13 KiB |
BIN
kodi.png
BIN
kodi.png
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 15 KiB |
38
release.yaml
Normal file
38
release.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
version: '0.9.1'
|
||||
changelog: |-
|
||||
Bug Fixes
|
||||
---------
|
||||
+ Implement proper keepalives for websocket (#401) @mcarlton00
|
||||
dependencies:
|
||||
py2:
|
||||
- addon: 'xbmc.python'
|
||||
version: '2.25.0'
|
||||
- addon: 'script.module.requests'
|
||||
version: '2.22.0'
|
||||
- addon: 'script.module.dateutil'
|
||||
version: '2.8.1'
|
||||
- addon: 'script.module.six'
|
||||
version: '1.13.0'
|
||||
- addon: 'script.module.kodi-six'
|
||||
version: '0.0.7'
|
||||
- addon: 'script.module.addon.signals'
|
||||
version: '0.0.5'
|
||||
- addon: 'script.module.futures'
|
||||
version: '2.2.0'
|
||||
- addon: 'script.module.websocket'
|
||||
version: '0.57.0'
|
||||
py3:
|
||||
- addon: 'xbmc.python'
|
||||
version: '3.0.0'
|
||||
- addon: 'script.module.requests'
|
||||
version: '2.22.0+matrix.1'
|
||||
- addon: 'script.module.dateutil'
|
||||
version: '2.8.1+matrix.1'
|
||||
- addon: 'script.module.six'
|
||||
version: '1.14.0+matrix.2'
|
||||
- addon: 'script.module.kodi-six'
|
||||
version: '0.1.3+1'
|
||||
- addon: 'script.module.addon.signals'
|
||||
version: '0.0.5+matrix.1'
|
||||
- addon: 'script.module.websocket'
|
||||
version: '0.57.0+matrix.1'
|
||||
16
requirements-dev.txt
Normal file
16
requirements-dev.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
pyyaml
|
||||
setuptools >= 44.1.1 # Old setuptools causes script.module.addon.signals to fail installing
|
||||
six >= 1.13
|
||||
python-dateutil >= 2.8.1
|
||||
requests >= 2.22
|
||||
futures >= 2.2; python_version < '3.0'
|
||||
|
||||
Kodistubs ~= 18.0; python_version < '3.0'
|
||||
Kodistubs ~= 21.0; python_version >= '3.6'
|
||||
|
||||
git+https://github.com/romanvm/kodi.six
|
||||
git+https://github.com/ruuk/script.module.addon.signals
|
||||
|
||||
flake8 >= 3.8
|
||||
flake8-import-order >= 0.18
|
||||
websocket-client >= 0.57.0
|
||||
1225
resources/language/resource.language.ar/strings.po
Normal file
1225
resources/language/resource.language.ar/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
67
resources/language/resource.language.be/strings.po
Normal file
67
resources/language/resource.language.be/strings.po
Normal file
@@ -0,0 +1,67 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2025-10-05 02:38+0000\n"
|
||||
"Last-Translator: Pavel Miniutka <pavel.miniutka@gmail.com>\n"
|
||||
"Language-Team: Belarusian <https://translate.jellyfin.org/projects/jellycon/"
|
||||
"jellycon/be/>\n"
|
||||
"Language: be\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
|
||||
"X-Generator: Weblate 5.11.4\n"
|
||||
|
||||
msgctxt "#30001"
|
||||
msgid "Port"
|
||||
msgstr "Порт"
|
||||
|
||||
msgctxt "#30006"
|
||||
msgid "Password"
|
||||
msgstr "Пароль"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30007"
|
||||
msgid "Samba username"
|
||||
msgstr "Імя карыстальніка Samba"
|
||||
|
||||
msgctxt "#30018"
|
||||
msgid "Number of items to show in filtered lists"
|
||||
msgstr "Лічба паказаных элементаў у фільтраваных спісах"
|
||||
|
||||
msgctxt "#30000"
|
||||
msgid "Host"
|
||||
msgstr "Хост"
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username"
|
||||
msgstr "Імя карыстальніка"
|
||||
|
||||
msgctxt "#30003"
|
||||
msgid "Verify HTTPS certificate"
|
||||
msgstr "Спраўдзіць сертыфікат HTTPS"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30008"
|
||||
msgid "Samba password"
|
||||
msgstr "Пароль Samba"
|
||||
|
||||
msgctxt "#30011"
|
||||
msgid "[Detect local server]"
|
||||
msgstr "[Выявіць лякальны сервер]"
|
||||
|
||||
msgctxt "#30012"
|
||||
msgid "[Change user]"
|
||||
msgstr "[Змяніць карыстальніка]"
|
||||
|
||||
msgctxt "#30014"
|
||||
msgid "Jellyfin"
|
||||
msgstr "Jellyfin"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device display name"
|
||||
msgstr "Імя прылады"
|
||||
|
||||
msgctxt "#30017"
|
||||
msgid "Show connected clients"
|
||||
msgstr "Паказаць падлучаныя прылады"
|
||||
1228
resources/language/resource.language.ca/strings.po
Normal file
1228
resources/language/resource.language.ca/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
1225
resources/language/resource.language.cs/strings.po
Normal file
1225
resources/language/resource.language.cs/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
418
resources/language/resource.language.cy/strings.po
Normal file
418
resources/language/resource.language.cy/strings.po
Normal file
@@ -0,0 +1,418 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2022-05-08 11:13+0000\n"
|
||||
"Last-Translator: Rhodri <rhodrilld@gmail.com>\n"
|
||||
"Language-Team: Welsh <https://translate.jellyfin.org/projects/jellycon/"
|
||||
"jellycon/cy/>\n"
|
||||
"Language: cy\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=6; plural=(n==0) ? 0 : (n==1) ? 1 : (n==2) ? 2 : "
|
||||
"(n==3) ? 3 :(n==6) ? 4 : 5;\n"
|
||||
"X-Generator: Weblate 4.10.1\n"
|
||||
|
||||
msgctxt "#30006"
|
||||
msgid "Password"
|
||||
msgstr "Cyfrinair"
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username"
|
||||
msgstr "Enw defnyddiwr"
|
||||
|
||||
msgctxt "#30003"
|
||||
msgid "Verify HTTPS certificate"
|
||||
msgstr "Gwiriwch dystysgrif HTTPS"
|
||||
|
||||
msgctxt "#30001"
|
||||
msgid "Port"
|
||||
msgstr "Port"
|
||||
|
||||
msgctxt "#30000"
|
||||
msgid "Host"
|
||||
msgstr "Gwesteiwr"
|
||||
|
||||
msgctxt "#30021"
|
||||
msgid "Show all episodes item"
|
||||
msgstr "Dangos bob pennod"
|
||||
|
||||
msgctxt "#30020"
|
||||
msgid "Flatten single season"
|
||||
msgstr "Gwastadu tymor sengl"
|
||||
|
||||
msgctxt "#30019"
|
||||
msgid "Filtered episode name format"
|
||||
msgstr "Fformat enw pennod wedi'i hidlo"
|
||||
|
||||
msgctxt "#30018"
|
||||
msgid "Number of items to show in filtered lists"
|
||||
msgstr "Nifer yr eitemau i'w dangos mewn rhestrau wedi'u hidlo"
|
||||
|
||||
msgctxt "#30017"
|
||||
msgid "Show connected clients"
|
||||
msgstr "Dangos cleients cysylltiedig"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device display name"
|
||||
msgstr "Enw arddangos dyfais"
|
||||
|
||||
msgctxt "#30015"
|
||||
msgid "Log timing data"
|
||||
msgstr "Logio data amseru"
|
||||
|
||||
msgctxt "#30014"
|
||||
msgid "Jellyfin"
|
||||
msgstr "Jellyfin"
|
||||
|
||||
msgctxt "#30012"
|
||||
msgid "[Change user]"
|
||||
msgstr "[Newid defnyddiwr]"
|
||||
|
||||
msgctxt "#30011"
|
||||
msgid "[Detect local server]"
|
||||
msgstr "[Canfod gweinydd lleol]"
|
||||
|
||||
msgctxt "#30010"
|
||||
msgid "Number of performance profiles to capture"
|
||||
msgstr "Nifer y proffiliau perfformiad i'w gynhyrchu"
|
||||
|
||||
msgctxt "#30008"
|
||||
msgid "Samba password"
|
||||
msgstr "Cyfrinair Samba"
|
||||
|
||||
msgctxt "#30007"
|
||||
msgid "Samba username"
|
||||
msgstr "Enw defnyddiwr Samba"
|
||||
|
||||
msgctxt "#30169"
|
||||
msgid "Address: "
|
||||
msgstr "Cyfeiriad:"
|
||||
|
||||
msgctxt "#30167"
|
||||
msgid "Selected Server Address"
|
||||
msgstr "Cyfeiriad Gweinydd a ddewiswyd"
|
||||
|
||||
msgctxt "#30166"
|
||||
msgid "Select Server"
|
||||
msgstr "Dewis Gweinydd"
|
||||
|
||||
msgctxt "#30135"
|
||||
msgid "Error"
|
||||
msgstr "Gwall"
|
||||
|
||||
msgctxt "#30126"
|
||||
msgid "Processing Item : "
|
||||
msgstr "Prosesi'r eitem:"
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "Done"
|
||||
msgstr "Wedi gorffen"
|
||||
|
||||
msgctxt "#30121"
|
||||
msgid "On resume"
|
||||
msgstr "Ar barhad"
|
||||
|
||||
msgctxt "#30113"
|
||||
msgid "Retrieving Data"
|
||||
msgstr "Adalw Data"
|
||||
|
||||
msgctxt "#30112"
|
||||
msgid "Loading Content"
|
||||
msgstr "Llwytho Cynnwys"
|
||||
|
||||
msgctxt "#30111"
|
||||
msgid "Services"
|
||||
msgstr "Gwasanaethau"
|
||||
|
||||
msgctxt "#30092"
|
||||
msgid "Warning: This action will delete the media files from the server."
|
||||
msgstr ""
|
||||
"Rhybudd: Bydd y weithred hon yn dileu'r ffeiliau cyfryngau o'r gweinydd."
|
||||
|
||||
msgctxt "#30091"
|
||||
msgid "Confirm delete?"
|
||||
msgstr "Cadarnhau dileu?"
|
||||
|
||||
msgctxt "#30063"
|
||||
msgid "N/A"
|
||||
msgstr "N/A"
|
||||
|
||||
msgctxt "#30053"
|
||||
msgid "Waiting for server to delete"
|
||||
msgstr "Aros i'r gweinydd i'w ddileu"
|
||||
|
||||
msgctxt "#30052"
|
||||
msgid "Deleting"
|
||||
msgstr "Wrthi'n dileu"
|
||||
|
||||
msgctxt "#30045"
|
||||
msgid "Username not found"
|
||||
msgstr "Enw defnyddiwr ddim yn bodoli"
|
||||
|
||||
msgctxt "#30044"
|
||||
msgid "Incorrect Username/Password"
|
||||
msgstr "Enw defnyddiwr/Cyfrinair anghywir"
|
||||
|
||||
msgctxt "#30025"
|
||||
msgid "Password:"
|
||||
msgstr "Cyfrinair:"
|
||||
|
||||
msgctxt "#30024"
|
||||
msgid "Username:"
|
||||
msgstr "Enw defnyddiwr:"
|
||||
|
||||
msgctxt "#30022"
|
||||
msgid "Advanced"
|
||||
msgstr "Uwchraddol"
|
||||
|
||||
msgctxt "#30216"
|
||||
msgid "Item Details"
|
||||
msgstr "Gwybodaeth Eitem"
|
||||
|
||||
msgctxt "#30200"
|
||||
msgid "URL error"
|
||||
msgstr "gwall URL"
|
||||
|
||||
msgctxt "#30180"
|
||||
msgid "Select User"
|
||||
msgstr "Dewis Defnyddiwr"
|
||||
|
||||
msgctxt "#30429"
|
||||
msgid "Genre"
|
||||
msgstr "Genre"
|
||||
|
||||
msgctxt "#30430"
|
||||
msgid "Label"
|
||||
msgstr "Label"
|
||||
|
||||
msgctxt "#30426"
|
||||
msgid "Title"
|
||||
msgstr "Teitl"
|
||||
|
||||
msgctxt "#30425"
|
||||
msgid "Year"
|
||||
msgstr "Blwyddyn"
|
||||
|
||||
msgctxt "#30401"
|
||||
msgid "Info"
|
||||
msgstr "Gwybodaeth"
|
||||
|
||||
msgctxt "#30399"
|
||||
msgid "Hide"
|
||||
msgstr "Cuddio"
|
||||
|
||||
msgctxt "#30392"
|
||||
msgid "HTTPS"
|
||||
msgstr "HTTPS"
|
||||
|
||||
msgctxt "#30391"
|
||||
msgid "HTTP"
|
||||
msgstr "HTTP"
|
||||
|
||||
msgctxt "#30380"
|
||||
msgid "Never"
|
||||
msgstr "Byth"
|
||||
|
||||
msgctxt "#30339"
|
||||
msgid "Person"
|
||||
msgstr "Person"
|
||||
|
||||
msgctxt "#30338"
|
||||
msgid "Album"
|
||||
msgstr "Albwm"
|
||||
|
||||
msgctxt "#30337"
|
||||
msgid "Song"
|
||||
msgstr "Cân"
|
||||
|
||||
msgctxt "#30314"
|
||||
msgid "Play"
|
||||
msgstr "Chwarae"
|
||||
|
||||
msgctxt "#30313"
|
||||
msgid "Menu"
|
||||
msgstr "Dewislen"
|
||||
|
||||
msgctxt "#30296"
|
||||
msgid "Delete"
|
||||
msgstr "Dileu"
|
||||
|
||||
msgctxt "#30274"
|
||||
msgid "Delete"
|
||||
msgstr "Dileu"
|
||||
|
||||
msgctxt "#30256"
|
||||
msgid "Movies"
|
||||
msgstr "Ffilmiau"
|
||||
|
||||
msgctxt "#30250"
|
||||
msgid "Unknown"
|
||||
msgstr "Anhysbys"
|
||||
|
||||
msgctxt "#30246"
|
||||
msgid "Search"
|
||||
msgstr "Chwilio"
|
||||
|
||||
msgctxt "#30235"
|
||||
msgid "Episodes"
|
||||
msgstr "Pennodau"
|
||||
|
||||
msgctxt "#30231"
|
||||
msgid "Movies"
|
||||
msgstr "Ffilmiau"
|
||||
|
||||
msgctxt "#30218"
|
||||
msgid "Play next episode after %"
|
||||
msgstr "Chwarae'r bennod nesaf ar ôl %"
|
||||
|
||||
msgctxt "#30214"
|
||||
msgid "Events"
|
||||
msgstr "Digwyddiadau"
|
||||
|
||||
msgctxt "#30211"
|
||||
msgid "Transcode options"
|
||||
msgstr "Opsiynau trawsgodio"
|
||||
|
||||
msgctxt "#30201"
|
||||
msgid "Unable to connect to server"
|
||||
msgstr "Methu cysylltu â'r gweinydd"
|
||||
|
||||
msgctxt "#30183"
|
||||
msgid "Include people"
|
||||
msgstr "Cynnwys pobl"
|
||||
|
||||
msgctxt "#30181"
|
||||
msgid "Include plot"
|
||||
msgstr "Cynnwys plot"
|
||||
|
||||
msgctxt "#30163"
|
||||
msgid "Add (cc) if subtitle is available"
|
||||
msgstr "Ychwanegu (cc) os oes is-deitlau ar gael"
|
||||
|
||||
msgctxt "#30114"
|
||||
msgid "Jump back seconds"
|
||||
msgstr "Neidio yn ôl eiliadau"
|
||||
|
||||
msgctxt "#30110"
|
||||
msgid "Interface"
|
||||
msgstr "Rhyngwyneb"
|
||||
|
||||
msgctxt "#30027"
|
||||
msgid "Enable debug logging"
|
||||
msgstr "Actifadu logio dadfygio"
|
||||
|
||||
msgctxt "#30023"
|
||||
msgid "Hide unwatched episode details"
|
||||
msgstr "Cuddio manylion penodau heb eu gwylio"
|
||||
|
||||
msgctxt "#30381"
|
||||
msgid "More than one"
|
||||
msgstr "Mwy na un"
|
||||
|
||||
msgctxt "#30362"
|
||||
msgid " - Recordings"
|
||||
msgstr "- Recordiadau"
|
||||
|
||||
msgctxt "#30361"
|
||||
msgid " - Programs"
|
||||
msgstr "- Rhaglenni"
|
||||
|
||||
msgctxt "#30360"
|
||||
msgid " - Channels"
|
||||
msgstr "- Sianeli"
|
||||
|
||||
msgctxt "#30325"
|
||||
msgid " - Genres"
|
||||
msgstr "- Genres"
|
||||
|
||||
msgctxt "#30316"
|
||||
msgid "Connection Error"
|
||||
msgstr "Gwall cysylltiad"
|
||||
|
||||
msgctxt "#30312"
|
||||
msgid "All - "
|
||||
msgstr "Pobeth -"
|
||||
|
||||
msgctxt "#30311"
|
||||
msgid "Library - "
|
||||
msgstr "Llyfrgell -"
|
||||
|
||||
msgctxt "#30294"
|
||||
msgid "Notice"
|
||||
msgstr "Rhybydd"
|
||||
|
||||
msgctxt "#30290"
|
||||
msgid "All"
|
||||
msgstr "Pobeth"
|
||||
|
||||
msgctxt "#30288"
|
||||
msgid " - Latest"
|
||||
msgstr "- Diweddaraf"
|
||||
|
||||
msgctxt "#30285"
|
||||
msgid " - Unwatched"
|
||||
msgstr "- Heb ei wylio"
|
||||
|
||||
msgctxt "#30283"
|
||||
msgid "Play Next Episode?"
|
||||
msgstr "Chwarae'r bennod nesaf?"
|
||||
|
||||
msgctxt "#30280"
|
||||
msgid "Missing Title"
|
||||
msgstr "Teitl ar goll"
|
||||
|
||||
msgctxt "#30278"
|
||||
msgid " - Next Up"
|
||||
msgstr "- Nesaf"
|
||||
|
||||
msgctxt "#30286"
|
||||
msgid "Movies - Unwatched"
|
||||
msgstr "Ffilmiau - Heb ei wylio"
|
||||
|
||||
msgctxt "#30252"
|
||||
msgid "Movies - A-Z"
|
||||
msgstr "Ffilmiau - A-Z"
|
||||
|
||||
msgctxt "#30251"
|
||||
msgid "Movies - Genres"
|
||||
msgstr "Ffilmiau - Genres"
|
||||
|
||||
msgctxt "#30259"
|
||||
msgid "Movies - Favorites"
|
||||
msgstr "Ffilmiau - Ffefrynnau"
|
||||
|
||||
msgctxt "#30415"
|
||||
msgid " - Favorite Collections"
|
||||
msgstr "- Hoff Gasgliadau"
|
||||
|
||||
msgctxt "#30414"
|
||||
msgid " - Favorites"
|
||||
msgstr "- Ffefrynnau"
|
||||
|
||||
msgctxt "#30289"
|
||||
msgid "TV Shows - Genres"
|
||||
msgstr "Rhaglenni teledu - Genres"
|
||||
|
||||
msgctxt "#30287"
|
||||
msgid "TV Shows - Latest"
|
||||
msgstr "Rhaglenni teledu - Diweddaraf"
|
||||
|
||||
msgctxt "#30279"
|
||||
msgid "TV Shows - Unwatched"
|
||||
msgstr "Rhaglenni teledu - Heb ei wylio"
|
||||
|
||||
msgctxt "#30262"
|
||||
msgid "TV Shows - Favorites"
|
||||
msgstr "Rhaglenni teledu - Ffefrynnau"
|
||||
|
||||
msgctxt "#30261"
|
||||
msgid "TV Shows"
|
||||
msgstr "Rhaglenni teledu"
|
||||
|
||||
msgctxt "#30255"
|
||||
msgid "TV Shows - A-Z"
|
||||
msgstr "Rhaglenni teledu - A-Z"
|
||||
|
||||
msgctxt "#30229"
|
||||
msgid "TV Shows"
|
||||
msgstr "Rhaglenni teledu"
|
||||
1205
resources/language/resource.language.da/strings.po
Normal file
1205
resources/language/resource.language.da/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
1347
resources/language/resource.language.de/strings.po
Normal file
1347
resources/language/resource.language.de/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
1198
resources/language/resource.language.el/strings.po
Normal file
1198
resources/language/resource.language.el/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
29
resources/language/resource.language.enm/strings.po
Normal file
29
resources/language/resource.language.enm/strings.po
Normal file
@@ -0,0 +1,29 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2025-05-28 22:01+0000\n"
|
||||
"Last-Translator: Cjfly <mrlaylowcj@gmail.com>\n"
|
||||
"Language-Team: English (Middle) <https://translate.jellyfin.org/projects/"
|
||||
"jellycon/jellycon/enm/>\n"
|
||||
"Language: enm\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.11.4\n"
|
||||
|
||||
msgctxt "#30000"
|
||||
msgid "Host"
|
||||
msgstr "127.0.0.1"
|
||||
|
||||
msgctxt "#30006"
|
||||
msgid "Password"
|
||||
msgstr "232542684Cc"
|
||||
|
||||
#, fuzzy
|
||||
msgctxt "#30005"
|
||||
msgid "Username"
|
||||
msgstr "Courtney"
|
||||
|
||||
msgctxt "#30001"
|
||||
msgid "Port"
|
||||
msgstr "3000"
|
||||
1103
resources/language/resource.language.eo/strings.po
Normal file
1103
resources/language/resource.language.eo/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
1231
resources/language/resource.language.es/strings.po
Normal file
1231
resources/language/resource.language.es/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
1223
resources/language/resource.language.et/strings.po
Normal file
1223
resources/language/resource.language.et/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
1231
resources/language/resource.language.fi/strings.po
Normal file
1231
resources/language/resource.language.fi/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
1210
resources/language/resource.language.fr/strings.po
Normal file
1210
resources/language/resource.language.fr/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
720
resources/language/resource.language.hi/strings.po
Normal file
720
resources/language/resource.language.hi/strings.po
Normal file
@@ -0,0 +1,720 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2024-07-05 15:41+0000\n"
|
||||
"Last-Translator: Viswanadha Y Manu Sharma <manu.viswanad@gmail.com>\n"
|
||||
"Language-Team: Hindi <https://translate.jellyfin.org/projects/jellycon/"
|
||||
"jellycon/hi/>\n"
|
||||
"Language: hi\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n > 1;\n"
|
||||
"X-Generator: Weblate 5.4.2\n"
|
||||
|
||||
msgctxt "#30407"
|
||||
msgid "Global Lists"
|
||||
msgstr "वैश्विक सूची"
|
||||
|
||||
msgctxt "#30408"
|
||||
msgid "Custom Widgets"
|
||||
msgstr "कस्टम विड्जेट"
|
||||
|
||||
msgctxt "#30409"
|
||||
msgid "Add-on Actions"
|
||||
msgstr "अतिरिक्त कार्य"
|
||||
|
||||
msgctxt "#30410"
|
||||
msgid " - Collections"
|
||||
msgstr "संग्रह"
|
||||
|
||||
msgctxt "#30417"
|
||||
msgid "You do not have permision to delete this item"
|
||||
msgstr "आपके पास इस वस्तु को मिटाने की अनुमति नहीं है"
|
||||
|
||||
msgctxt "#30416"
|
||||
msgid "HTTP timeout seconds"
|
||||
msgstr "एचटीटीपी के समय समाप्ति (सेकड़ो में)"
|
||||
|
||||
msgctxt "#30413"
|
||||
msgid " - Tags"
|
||||
msgstr "चिप्पी"
|
||||
|
||||
msgctxt "#30418"
|
||||
msgid "Audio bitrate (Kbits)"
|
||||
msgstr "ऑडियो बितरते (किलो बिट प्रति घंटा)"
|
||||
|
||||
msgctxt "#30419"
|
||||
msgid "Audio codec"
|
||||
msgstr "ऑडियो कोडेक"
|
||||
|
||||
msgctxt "#30420"
|
||||
msgid "Audio max channels"
|
||||
msgstr "अधिकतम ऑडियो चैनल"
|
||||
|
||||
msgctxt "#30421"
|
||||
msgid "Views"
|
||||
msgstr "कितनी बार देखा गया"
|
||||
|
||||
msgctxt "#30422"
|
||||
msgid "Sorting"
|
||||
msgstr "छंटाई"
|
||||
|
||||
msgctxt "#30423"
|
||||
msgid "NotSet"
|
||||
msgstr "नहीं लगाया गया"
|
||||
|
||||
msgctxt "#30424"
|
||||
msgid "Default"
|
||||
msgstr "पहले से चुना हुआ"
|
||||
|
||||
msgctxt "#30425"
|
||||
msgid "Year"
|
||||
msgstr "साल"
|
||||
|
||||
msgctxt "#30434"
|
||||
msgid "Force transcode stream bitrate (Kbits)"
|
||||
msgstr "जबरदस्ती बिट्रेट तय करे (किलो बिट प्रति सेकंड)"
|
||||
|
||||
msgctxt "#30436"
|
||||
msgid "Speed test data size (MB)"
|
||||
msgstr "इंटरनेट की रफ़्तार नापने के लिए डाटा का माप (मेगा बाईट)"
|
||||
|
||||
msgctxt "#30433"
|
||||
msgid "Allow direct file playback"
|
||||
msgstr "सीधे फाइल से प्लेबैक की अनुमति दें"
|
||||
|
||||
msgctxt "#30437"
|
||||
msgid "Playback options"
|
||||
msgstr "चलने के विकल्प दिखाएं"
|
||||
|
||||
msgctxt "#30439"
|
||||
msgid "Show play next episode at time left"
|
||||
msgstr "कितने समय पहले अगले अध्याय पर जाने का बटन दिखाएं"
|
||||
|
||||
msgctxt "#30438"
|
||||
msgid "Play cinema intros"
|
||||
msgstr "सिनेमा उपक्षेप चलायें"
|
||||
|
||||
msgctxt "#30441"
|
||||
msgid "Use cached widget data"
|
||||
msgstr "पुराणी विद्गट जानकारी इस्तेमाल करें"
|
||||
|
||||
msgctxt "#30442"
|
||||
msgid "Simple new content check"
|
||||
msgstr "नए कंटेंट के लिए चेक करें"
|
||||
|
||||
msgctxt "#30435"
|
||||
msgid "Connection speed test"
|
||||
msgstr "कनेक्शन की स्पीड नापें"
|
||||
|
||||
msgctxt "#30440"
|
||||
msgid "Play next"
|
||||
msgstr "अगला चलाएं"
|
||||
|
||||
msgctxt "#30114"
|
||||
msgid "Jump back seconds"
|
||||
msgstr "कुछ सेकंड पीछे जाएं"
|
||||
|
||||
msgctxt "#30113"
|
||||
msgid "Retrieving Data"
|
||||
msgstr "देता प्राप्त कर रहे है"
|
||||
|
||||
msgctxt "#30112"
|
||||
msgid "Loading Content"
|
||||
msgstr "कंटेंट लोड हो रहा है"
|
||||
|
||||
msgctxt "#30027"
|
||||
msgid "Enable debug logging"
|
||||
msgstr "डीबग सूचि बनाएं"
|
||||
|
||||
msgctxt "#30026"
|
||||
msgid "Widget item select action"
|
||||
msgstr "विद्गट वास्तु का कार्य चुनें"
|
||||
|
||||
msgctxt "#30020"
|
||||
msgid "Flatten single season"
|
||||
msgstr "सिर्फ एक सत्र वाले शो के लिए सत्र पेज हटाएं"
|
||||
|
||||
msgctxt "#30415"
|
||||
msgid " - Favorite Collections"
|
||||
msgstr "पसंदीदा संग्रह"
|
||||
|
||||
msgctxt "#30414"
|
||||
msgid " - Favorites"
|
||||
msgstr "पसंदीदा"
|
||||
|
||||
msgctxt "#30411"
|
||||
msgid " - Years"
|
||||
msgstr "साल"
|
||||
|
||||
msgctxt "#30399"
|
||||
msgid "Hide"
|
||||
msgstr "छुपाएँ"
|
||||
|
||||
msgctxt "#30412"
|
||||
msgid " - Decades"
|
||||
msgstr "दशक"
|
||||
|
||||
msgctxt "#30432"
|
||||
msgid "Hide watched items in lists"
|
||||
msgstr "देखि हुईं वस्तुएं हटाएं"
|
||||
|
||||
msgctxt "#30431"
|
||||
msgid "Seasons"
|
||||
msgstr "सत्र"
|
||||
|
||||
msgctxt "#30430"
|
||||
msgid "Label"
|
||||
msgstr "लेबल"
|
||||
|
||||
msgctxt "#30427"
|
||||
msgid "Added"
|
||||
msgstr "जोड़ा हुआ"
|
||||
|
||||
msgctxt "#30426"
|
||||
msgid "Title"
|
||||
msgstr "शीर्षक"
|
||||
|
||||
msgctxt "#30428"
|
||||
msgid "Rating"
|
||||
msgstr "रेटिंग"
|
||||
|
||||
msgctxt "#30429"
|
||||
msgid "Genre"
|
||||
msgstr "शैली"
|
||||
|
||||
msgctxt "#30019"
|
||||
msgid "Filtered episode name format"
|
||||
msgstr "छठें हुएं अध्यायों के नामों का प्रारूप"
|
||||
|
||||
msgctxt "#30018"
|
||||
msgid "Number of items to show in filtered lists"
|
||||
msgstr "छांटी हुई सूचि में कितने वास्तु दिखाए"
|
||||
|
||||
msgctxt "#30015"
|
||||
msgid "Log timing data"
|
||||
msgstr "समय की जानकारी लिखें"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device display name"
|
||||
msgstr "उपकरण का नाम"
|
||||
|
||||
msgctxt "#30017"
|
||||
msgid "Show connected clients"
|
||||
msgstr "जुड़े हुए क्लाइंट्स दिखाएं"
|
||||
|
||||
msgctxt "#30023"
|
||||
msgid "Hide unwatched episode details"
|
||||
msgstr "अंधेके एपिसोडों का विवरण हटाएँ"
|
||||
|
||||
msgctxt "#30021"
|
||||
msgid "Show all episodes item"
|
||||
msgstr "सारे एपिसोड दिखाएं"
|
||||
|
||||
msgctxt "#30025"
|
||||
msgid "Password:"
|
||||
msgstr "पासवर्ड :"
|
||||
|
||||
msgctxt "#30246"
|
||||
msgid "Search"
|
||||
msgstr "खोज"
|
||||
|
||||
msgctxt "#30235"
|
||||
msgid "Episodes"
|
||||
msgstr "अध्याय"
|
||||
|
||||
msgctxt "#30229"
|
||||
msgid "TV Shows"
|
||||
msgstr "टीवी शो"
|
||||
|
||||
msgctxt "#30231"
|
||||
msgid "Movies"
|
||||
msgstr "फ़िल्म"
|
||||
|
||||
msgctxt "#30216"
|
||||
msgid "Item Details"
|
||||
msgstr "वास्तु का विवरण"
|
||||
|
||||
msgctxt "#30169"
|
||||
msgid "Address: "
|
||||
msgstr "पता :"
|
||||
|
||||
msgctxt "#30044"
|
||||
msgid "Incorrect Username/Password"
|
||||
msgstr "गलत उपयोगरता का नाम / पासवर्ड"
|
||||
|
||||
msgctxt "#30045"
|
||||
msgid "Username not found"
|
||||
msgstr "उपयोगकर्ता का नाम नहीं पाया गया"
|
||||
|
||||
msgctxt "#30022"
|
||||
msgid "Advanced"
|
||||
msgstr "उन्नत"
|
||||
|
||||
msgctxt "#30092"
|
||||
msgid "Warning: This action will delete the media files from the server."
|
||||
msgstr "चेतावनी : यह कार्य आपकी मिडिया मिटा देगा |"
|
||||
|
||||
msgctxt "#30135"
|
||||
msgid "Error"
|
||||
msgstr "समस्या"
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "Done"
|
||||
msgstr "खत्म"
|
||||
|
||||
msgctxt "#30181"
|
||||
msgid "Include plot"
|
||||
msgstr "कहानी जोड़ें"
|
||||
|
||||
msgctxt "#30180"
|
||||
msgid "Select User"
|
||||
msgstr "उपयोगकर्ता चुनें"
|
||||
|
||||
msgctxt "#30167"
|
||||
msgid "Selected Server Address"
|
||||
msgstr "चुनें हुए सर्वर का पता"
|
||||
|
||||
msgctxt "#30166"
|
||||
msgid "Select Server"
|
||||
msgstr "सर्वर चुनें"
|
||||
|
||||
msgctxt "#30111"
|
||||
msgid "Services"
|
||||
msgstr "सेवाएं"
|
||||
|
||||
msgctxt "#30110"
|
||||
msgid "Interface"
|
||||
msgstr "अंतराफलक"
|
||||
|
||||
msgctxt "#30091"
|
||||
msgid "Confirm delete?"
|
||||
msgstr "क्या आप पक्का मिटाना चाहतें हैं?"
|
||||
|
||||
msgctxt "#30063"
|
||||
msgid "N/A"
|
||||
msgstr "उपलब्ध नहीं है"
|
||||
|
||||
msgctxt "#30053"
|
||||
msgid "Waiting for server to delete"
|
||||
msgstr "सर्वर द्वारा हटाए जानें का इंतज़ार कर रहें हैं"
|
||||
|
||||
msgctxt "#30052"
|
||||
msgid "Deleting"
|
||||
msgstr "हटा रहें हैं"
|
||||
|
||||
msgctxt "#30024"
|
||||
msgid "Username:"
|
||||
msgstr "उपयोगकर्ता का नाम :"
|
||||
|
||||
msgctxt "#30010"
|
||||
msgid "Number of performance profiles to capture"
|
||||
msgstr "कितने कार्य प्रोफाइल रखने हैं"
|
||||
|
||||
msgctxt "#30011"
|
||||
msgid "[Detect local server]"
|
||||
msgstr "स्थानीय सर्वर का पता करें"
|
||||
|
||||
msgctxt "#30012"
|
||||
msgid "[Change user]"
|
||||
msgstr "उपयोगकर्ता बदलें"
|
||||
|
||||
msgctxt "#30014"
|
||||
msgid "Jellyfin"
|
||||
msgstr "जेलीफिन"
|
||||
|
||||
msgctxt "#30008"
|
||||
msgid "Samba password"
|
||||
msgstr "साम्बा में पासवर्ड"
|
||||
|
||||
msgctxt "#30007"
|
||||
msgid "Samba username"
|
||||
msgstr "साम्बा में उपयोगकर्ता का नाम"
|
||||
|
||||
msgctxt "#30006"
|
||||
msgid "Password"
|
||||
msgstr "पासवर्ड"
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username"
|
||||
msgstr "उपयोगकर्ता का नाम"
|
||||
|
||||
msgctxt "#30003"
|
||||
msgid "Verify HTTPS certificate"
|
||||
msgstr "HTTPS प्रमाणपत्र की जांच करें"
|
||||
|
||||
msgctxt "#30001"
|
||||
msgid "Port"
|
||||
msgstr "द्वार"
|
||||
|
||||
msgctxt "#30000"
|
||||
msgid "Host"
|
||||
msgstr "आतिथेय"
|
||||
|
||||
msgctxt "#30118"
|
||||
msgid "Add resume percent to names"
|
||||
msgstr "नामों में फिर शुरू करनेका प्रतिशत जोड़ें"
|
||||
|
||||
msgctxt "#30116"
|
||||
msgid "Add unwatched counts to names"
|
||||
msgstr "नामों में न देखी गई गिनती जोड़ें"
|
||||
|
||||
msgctxt "#30120"
|
||||
msgid "Show load progress"
|
||||
msgstr "लोड प्रगति दिखाएं"
|
||||
|
||||
msgctxt "#30121"
|
||||
msgid "On resume"
|
||||
msgstr "फिर से शुरू करने पर"
|
||||
|
||||
msgctxt "#30139"
|
||||
msgid "No Media Type Set"
|
||||
msgstr "कोई मीडिया प्रकार सेट नहीं"
|
||||
|
||||
msgctxt "#30183"
|
||||
msgid "Include people"
|
||||
msgstr "लोगों को शामिल करें"
|
||||
|
||||
msgctxt "#30208"
|
||||
msgid "Max stream bitrate (Kbits)"
|
||||
msgstr "अधिकतम स्ट्रीम बिटरेट (Kbps)"
|
||||
|
||||
msgctxt "#30212"
|
||||
msgid "Video max width"
|
||||
msgstr "वीडियो की अधिकतम चौड़ाई"
|
||||
|
||||
msgctxt "#30217"
|
||||
msgid "Prompt to delete episode after %"
|
||||
msgstr "% के बाद एपिसोड हटाने का संकेत"
|
||||
|
||||
msgctxt "#30219"
|
||||
msgid " - Prompt before play"
|
||||
msgstr "- खेलने से पहले संकेत दें"
|
||||
|
||||
msgctxt "#30220"
|
||||
msgid "Prompt to delete movie after %"
|
||||
msgstr "% के बाद मूवी हटाने का संकेत"
|
||||
|
||||
msgctxt "#30257"
|
||||
msgid "Movies - Recently Added"
|
||||
msgstr "फ़िल्में - हाल ही में जोड़ी गईं"
|
||||
|
||||
msgctxt "#30265"
|
||||
msgid "Episodes - Next Up"
|
||||
msgstr "एपिसोड - अगला"
|
||||
|
||||
msgctxt "#30163"
|
||||
msgid "Add (cc) if subtitle is available"
|
||||
msgstr "यदि उपशीर्षक उपलब्ध है तो (सीसी) जोड़ें"
|
||||
|
||||
msgctxt "#30182"
|
||||
msgid "Include media stream info"
|
||||
msgstr "मीडिया स्ट्रीम जानकारी शामिल करें"
|
||||
|
||||
msgctxt "#30200"
|
||||
msgid "URL error"
|
||||
msgstr "यूआरएल त्रुटि"
|
||||
|
||||
msgctxt "#30201"
|
||||
msgid "Unable to connect to server"
|
||||
msgstr "सर्वर से कनेक्ट करने में असमर्थ है"
|
||||
|
||||
msgctxt "#30206"
|
||||
msgid "Playback type"
|
||||
msgstr "प्लेबैक प्रकार"
|
||||
|
||||
msgctxt "#30207"
|
||||
msgid "Playback"
|
||||
msgstr "प्लेबैक"
|
||||
|
||||
msgctxt "#30209"
|
||||
msgid "File direct path"
|
||||
msgstr "फ़ाइल का सीधा पथ"
|
||||
|
||||
msgctxt "#30210"
|
||||
msgid "HTTP direct stream"
|
||||
msgstr "HTTP डायरेक्ट स्ट्रीम"
|
||||
|
||||
msgctxt "#30211"
|
||||
msgid "Transcode options"
|
||||
msgstr "ट्रांसकोड विकल्प"
|
||||
|
||||
msgctxt "#30213"
|
||||
msgid "Video force 8 bit"
|
||||
msgstr "वीडियो बल 8 बिट"
|
||||
|
||||
msgctxt "#30214"
|
||||
msgid "Events"
|
||||
msgstr "आयोजन"
|
||||
|
||||
msgctxt "#30215"
|
||||
msgid "On playback stop (100% = disabled)"
|
||||
msgstr "प्लेबैक स्टॉप पर (100% = अक्षम)"
|
||||
|
||||
msgctxt "#30218"
|
||||
msgid "Play next episode after %"
|
||||
msgstr "% के बाद अगला एपिसोड चलाएं"
|
||||
|
||||
msgctxt "#30222"
|
||||
msgid "Item Layout"
|
||||
msgstr "आइटम लेआउट"
|
||||
|
||||
msgctxt "#30223"
|
||||
msgid "Page Size and Filtering"
|
||||
msgstr "पृष्ठ आकार और फ़िल्टरिंग"
|
||||
|
||||
msgctxt "#30224"
|
||||
msgid "Interaction"
|
||||
msgstr "इंटरैक्शन"
|
||||
|
||||
msgctxt "#30258"
|
||||
msgid "Movies - In Progress"
|
||||
msgstr "फ़िल्में - प्रगति पर हैं"
|
||||
|
||||
msgctxt "#30126"
|
||||
msgid "Processing Item : "
|
||||
msgstr "आइटम का प्रसंस्करण:"
|
||||
|
||||
msgctxt "#30267"
|
||||
msgid " - In Progress"
|
||||
msgstr "- प्रगति पर है"
|
||||
|
||||
msgctxt "#30268"
|
||||
msgid " - Recently Added"
|
||||
msgstr "- हाल ही में जोड़ा"
|
||||
|
||||
msgctxt "#30225"
|
||||
msgid "Interface Mode"
|
||||
msgstr "इंटरफ़ेस मोड"
|
||||
|
||||
msgctxt "#30226"
|
||||
msgid "Default"
|
||||
msgstr "डिफ़ॉल्ट"
|
||||
|
||||
msgctxt "#30227"
|
||||
msgid "Simple"
|
||||
msgstr "सरल"
|
||||
|
||||
msgctxt "#30236"
|
||||
msgid "Force transcode h265 (hevc)"
|
||||
msgstr "फोर्स ट्रांसकोड h265 (hevc)"
|
||||
|
||||
msgctxt "#30237"
|
||||
msgid "Start from beginning"
|
||||
msgstr "शुरू से शुरू करो"
|
||||
|
||||
msgctxt "#30238"
|
||||
msgid "Playback stream options"
|
||||
msgstr "प्लेबैक स्ट्रीम विकल्प"
|
||||
|
||||
msgctxt "#30239"
|
||||
msgid "Force transcode mpeg2"
|
||||
msgstr "फोर्स ट्रांसकोड mpeg2"
|
||||
|
||||
msgctxt "#30240"
|
||||
msgid "Force transcode msmpeg4v3 (divx)"
|
||||
msgstr "फोर्स ट्रांसकोड msmpeg4v3 (divx)"
|
||||
|
||||
msgctxt "#30241"
|
||||
msgid "Force transcode mpeg4"
|
||||
msgstr "फोर्स ट्रांसकोड mpeg4"
|
||||
|
||||
msgctxt "#30247"
|
||||
msgid "Custom Widget Content"
|
||||
msgstr "कस्टम विजेट सामग्री"
|
||||
|
||||
msgctxt "#30250"
|
||||
msgid "Unknown"
|
||||
msgstr "अज्ञात"
|
||||
|
||||
msgctxt "#30251"
|
||||
msgid "Movies - Genres"
|
||||
msgstr "फ़िल्में - शैलियाँ"
|
||||
|
||||
msgctxt "#30252"
|
||||
msgid "Movies - A-Z"
|
||||
msgstr "फ़िल्में - ए-जेड"
|
||||
|
||||
msgctxt "#30254"
|
||||
msgid "Show add-on settings"
|
||||
msgstr "ऐड-ऑन सेटिंग दिखाएं"
|
||||
|
||||
msgctxt "#30255"
|
||||
msgid "TV Shows - A-Z"
|
||||
msgstr "टीवी शो - ए-जेड"
|
||||
|
||||
msgctxt "#30256"
|
||||
msgid "Movies"
|
||||
msgstr "फ़िल्म"
|
||||
|
||||
msgctxt "#30259"
|
||||
msgid "Movies - Favorites"
|
||||
msgstr "फ़िल्में - पसंदीदा"
|
||||
|
||||
msgctxt "#30260"
|
||||
msgid "BoxSets"
|
||||
msgstr "बॉक्ससेट"
|
||||
|
||||
msgctxt "#30261"
|
||||
msgid "TV Shows"
|
||||
msgstr "टीवी शो"
|
||||
|
||||
msgctxt "#30262"
|
||||
msgid "TV Shows - Favorites"
|
||||
msgstr "टीवी शो - पसंदीदा"
|
||||
|
||||
msgctxt "#30263"
|
||||
msgid "Episodes - Recently Added"
|
||||
msgstr "एपिसोड - हाल ही में जोड़े गए"
|
||||
|
||||
msgctxt "#30264"
|
||||
msgid "Episodes - In Progress"
|
||||
msgstr "एपिसोड - प्रगति पर है"
|
||||
|
||||
msgctxt "#30266"
|
||||
msgid "Movies - Pages"
|
||||
msgstr "फ़िल्में - पन्ने"
|
||||
|
||||
msgctxt "#30269"
|
||||
msgid "Movies - Random"
|
||||
msgstr "फ़िल्में - यादृच्छिक"
|
||||
|
||||
msgctxt "#30270"
|
||||
msgid "Mark Watched"
|
||||
msgstr "मार्क वॉचड"
|
||||
|
||||
msgctxt "#30271"
|
||||
msgid "Mark Unwatched"
|
||||
msgstr "मार्क अनवॉच्ड"
|
||||
|
||||
msgctxt "#30272"
|
||||
msgid "Set Favourite"
|
||||
msgstr "पसंदीदा सेट करें"
|
||||
|
||||
msgctxt "#30273"
|
||||
msgid "Unset Favourite"
|
||||
msgstr "पसंदीदा को अनसेट करें"
|
||||
|
||||
msgctxt "#30274"
|
||||
msgid "Delete"
|
||||
msgstr "मिटाना"
|
||||
|
||||
msgctxt "#30275"
|
||||
msgid "Force Transcode"
|
||||
msgstr "फोर्स ट्रांसकोड"
|
||||
|
||||
msgctxt "#30296"
|
||||
msgid "Delete"
|
||||
msgstr "मिटाना"
|
||||
|
||||
msgctxt "#30300"
|
||||
msgid "Cache all Jellyfin images as local Kodi images?"
|
||||
msgstr "सभी जेलीफ़िन छवियों को स्थानीय कोडी छवियों के रूप में कैश करें?"
|
||||
|
||||
msgctxt "#30301"
|
||||
msgid "Caching Images"
|
||||
msgstr "छवियाँ कैशिंग"
|
||||
|
||||
msgctxt "#30278"
|
||||
msgid " - Next Up"
|
||||
msgstr "- अगला"
|
||||
|
||||
msgctxt "#30302"
|
||||
msgid "Existing images : "
|
||||
msgstr "मौजूदा छवियाँ:"
|
||||
|
||||
msgctxt "#30242"
|
||||
msgid "Force transcode av1"
|
||||
msgstr "फोर्स ट्रांसकोड av1"
|
||||
|
||||
msgctxt "#30288"
|
||||
msgid " - Latest"
|
||||
msgstr "- नवीनतम"
|
||||
|
||||
msgctxt "#30289"
|
||||
msgid "TV Shows - Genres"
|
||||
msgstr "टीवी शो - शैलियाँ"
|
||||
|
||||
msgctxt "#30297"
|
||||
msgid "Delete unused images?"
|
||||
msgstr "अप्रयुक्त छवियाँ हटाएँ?"
|
||||
|
||||
msgctxt "#30298"
|
||||
msgid "Deleting Kodi Images"
|
||||
msgstr "कोडी छवियाँ हटाना"
|
||||
|
||||
msgctxt "#30299"
|
||||
msgid "Cache Images"
|
||||
msgstr "कैश छवियाँ"
|
||||
|
||||
msgctxt "#30303"
|
||||
msgid "Missing Jellyfin images : "
|
||||
msgstr "गुम जेलीफ़िन छवियाँ:"
|
||||
|
||||
msgctxt "#30279"
|
||||
msgid "TV Shows - Unwatched"
|
||||
msgstr "टीवी शो - नहीं देखे गए"
|
||||
|
||||
msgctxt "#30280"
|
||||
msgid "Missing Title"
|
||||
msgstr "गुम शीर्षक"
|
||||
|
||||
msgctxt "#30282"
|
||||
msgid "No Jellyfin servers detected on your local network."
|
||||
msgstr "आपके स्थानीय नेटवर्क पर कोई जेलीफ़िन सर्वर नहीं पाया गया।"
|
||||
|
||||
msgctxt "#30283"
|
||||
msgid "Play Next Episode?"
|
||||
msgstr "अगला एपिसोड खेलें?"
|
||||
|
||||
msgctxt "#30281"
|
||||
msgid "Refresh Cached Images"
|
||||
msgstr "कैश्ड छवियाँ ताज़ा करें"
|
||||
|
||||
msgctxt "#30285"
|
||||
msgid " - Unwatched"
|
||||
msgstr "- न देखा गया"
|
||||
|
||||
msgctxt "#30286"
|
||||
msgid "Movies - Unwatched"
|
||||
msgstr "फिल्में - नहीं देखी गईं"
|
||||
|
||||
msgctxt "#30287"
|
||||
msgid "TV Shows - Latest"
|
||||
msgstr "टीवी शो - नवीनतम"
|
||||
|
||||
msgctxt "#30294"
|
||||
msgid "Notice"
|
||||
msgstr "सूचना"
|
||||
|
||||
msgctxt "#30295"
|
||||
msgid "To use this feature you need HTTP control enabled"
|
||||
msgstr "इस सुविधा का उपयोग करने के लिए आपको HTTP नियंत्रण सक्षम करना होगा"
|
||||
|
||||
msgctxt "#30290"
|
||||
msgid "All"
|
||||
msgstr "सभी"
|
||||
|
||||
msgctxt "#30291"
|
||||
msgid "Select Audio Stream"
|
||||
msgstr "ऑडियो स्ट्रीम चुनें"
|
||||
|
||||
msgctxt "#30292"
|
||||
msgid "Select Subtitle Stream"
|
||||
msgstr "उपशीर्षक स्ट्रीम का चयन करें"
|
||||
|
||||
msgctxt "#30293"
|
||||
msgid "Cache images"
|
||||
msgstr "कैश छवियाँ"
|
||||
|
||||
msgctxt "#30304"
|
||||
msgid "Cached Jellyfin images : "
|
||||
msgstr "कैश्ड जेलीफ़िन छवियाँ:"
|
||||
|
||||
msgctxt "#30305"
|
||||
msgid "Not Found"
|
||||
msgstr "नहीं मिला"
|
||||
|
||||
msgctxt "#30306"
|
||||
msgid "Playback starting"
|
||||
msgstr "प्लेबैक प्रारंभ हो रहा है"
|
||||
1229
resources/language/resource.language.hu/strings.po
Normal file
1229
resources/language/resource.language.hu/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
1213
resources/language/resource.language.id/strings.po
Normal file
1213
resources/language/resource.language.id/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
1224
resources/language/resource.language.it/strings.po
Normal file
1224
resources/language/resource.language.it/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
770
resources/language/resource.language.ja/strings.po
Normal file
770
resources/language/resource.language.ja/strings.po
Normal file
@@ -0,0 +1,770 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2025-09-23 16:39+0000\n"
|
||||
"Last-Translator: S H <translate@hayakawa.work>\n"
|
||||
"Language-Team: Japanese <https://translate.jellyfin.org/projects/jellycon/"
|
||||
"jellycon/ja/>\n"
|
||||
"Language: ja\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 5.11.4\n"
|
||||
|
||||
msgctxt "#30112"
|
||||
msgid "Loading Content"
|
||||
msgstr "コンテンツをロード中"
|
||||
|
||||
msgctxt "#30008"
|
||||
msgid "Samba password"
|
||||
msgstr "Samba パスワード"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device display name"
|
||||
msgstr "デバイスディスプレイ名"
|
||||
|
||||
msgctxt "#30021"
|
||||
msgid "Show all episodes item"
|
||||
msgstr "全てのエピソードを見る"
|
||||
|
||||
msgctxt "#30044"
|
||||
msgid "Incorrect Username/Password"
|
||||
msgstr "誤ったユーザー名もしくはパスワード"
|
||||
|
||||
msgctxt "#30000"
|
||||
msgid "Host"
|
||||
msgstr "ホスト"
|
||||
|
||||
msgctxt "#30001"
|
||||
msgid "Port"
|
||||
msgstr "ポート"
|
||||
|
||||
msgctxt "#30003"
|
||||
msgid "Verify HTTPS certificate"
|
||||
msgstr "HTTPS証明書を検証"
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username"
|
||||
msgstr "ユーザー名"
|
||||
|
||||
msgctxt "#30006"
|
||||
msgid "Password"
|
||||
msgstr "パスワード"
|
||||
|
||||
msgctxt "#30007"
|
||||
msgid "Samba username"
|
||||
msgstr "Samba ユーザー名"
|
||||
|
||||
msgctxt "#30010"
|
||||
msgid "Number of performance profiles to capture"
|
||||
msgstr "キャプチャするパフォーマンスプロファイルの数"
|
||||
|
||||
msgctxt "#30011"
|
||||
msgid "[Detect local server]"
|
||||
msgstr "[ローカルサーバーを検出]"
|
||||
|
||||
msgctxt "#30012"
|
||||
msgid "[Change user]"
|
||||
msgstr "[ユーザーを変更]"
|
||||
|
||||
msgctxt "#30014"
|
||||
msgid "Jellyfin"
|
||||
msgstr "Jellyfin"
|
||||
|
||||
msgctxt "#30015"
|
||||
msgid "Log timing data"
|
||||
msgstr "ログタイミングデータ"
|
||||
|
||||
msgctxt "#30017"
|
||||
msgid "Show connected clients"
|
||||
msgstr "接続されたクライアントを表示"
|
||||
|
||||
msgctxt "#30018"
|
||||
msgid "Number of items to show in filtered lists"
|
||||
msgstr "フィルターされたリストに表示するアイテムの数"
|
||||
|
||||
msgctxt "#30019"
|
||||
msgid "Filtered episode name format"
|
||||
msgstr "フィルターされたエピソードの名前フォーマット"
|
||||
|
||||
msgctxt "#30022"
|
||||
msgid "Advanced"
|
||||
msgstr "高度"
|
||||
|
||||
msgctxt "#30023"
|
||||
msgid "Hide unwatched episode details"
|
||||
msgstr "観ていないエピソードの詳細を隠す"
|
||||
|
||||
msgctxt "#30024"
|
||||
msgid "Username:"
|
||||
msgstr "ユーザー名:"
|
||||
|
||||
msgctxt "#30025"
|
||||
msgid "Password:"
|
||||
msgstr "パスワード:"
|
||||
|
||||
msgctxt "#30027"
|
||||
msgid "Enable debug logging"
|
||||
msgstr "デバッグロギングを有効化"
|
||||
|
||||
msgctxt "#30045"
|
||||
msgid "Username not found"
|
||||
msgstr "ユーザー名が見つからない"
|
||||
|
||||
msgctxt "#30052"
|
||||
msgid "Deleting"
|
||||
msgstr "削除中"
|
||||
|
||||
msgctxt "#30053"
|
||||
msgid "Waiting for server to delete"
|
||||
msgstr "サーバーが削除するのを待機中"
|
||||
|
||||
msgctxt "#30091"
|
||||
msgid "Confirm delete?"
|
||||
msgstr "削除しますか?"
|
||||
|
||||
msgctxt "#30092"
|
||||
msgid "Warning: This action will delete the media files from the server."
|
||||
msgstr "警告: この操作はサーバーからメディアファイルを削除します。"
|
||||
|
||||
msgctxt "#30110"
|
||||
msgid "Interface"
|
||||
msgstr "インターフェース"
|
||||
|
||||
msgctxt "#30111"
|
||||
msgid "Services"
|
||||
msgstr "サービス"
|
||||
|
||||
msgctxt "#30113"
|
||||
msgid "Retrieving Data"
|
||||
msgstr "データを取得中"
|
||||
|
||||
msgctxt "#30222"
|
||||
msgid "Item Layout"
|
||||
msgstr "アイテムレイアウト"
|
||||
|
||||
msgctxt "#30224"
|
||||
msgid "Interaction"
|
||||
msgstr "インタラクション"
|
||||
|
||||
msgctxt "#30225"
|
||||
msgid "Interface Mode"
|
||||
msgstr "インターフェースモード"
|
||||
|
||||
msgctxt "#30226"
|
||||
msgid "Default"
|
||||
msgstr "既定"
|
||||
|
||||
msgctxt "#30227"
|
||||
msgid "Simple"
|
||||
msgstr "シンプル"
|
||||
|
||||
msgctxt "#30229"
|
||||
msgid "TV Shows"
|
||||
msgstr "TV番組"
|
||||
|
||||
msgctxt "#30231"
|
||||
msgid "Movies"
|
||||
msgstr "映画"
|
||||
|
||||
msgctxt "#30235"
|
||||
msgid "Episodes"
|
||||
msgstr "エピソード"
|
||||
|
||||
msgctxt "#30237"
|
||||
msgid "Start from beginning"
|
||||
msgstr "最初から始める"
|
||||
|
||||
msgctxt "#30239"
|
||||
msgid "Force transcode mpeg2"
|
||||
msgstr "mpeg2でのトランスコードを強制する"
|
||||
|
||||
msgctxt "#30241"
|
||||
msgid "Force transcode mpeg4"
|
||||
msgstr "mpeg4でのトランスコードを強制する"
|
||||
|
||||
msgctxt "#30246"
|
||||
msgid "Search"
|
||||
msgstr "検索"
|
||||
|
||||
msgctxt "#30250"
|
||||
msgid "Unknown"
|
||||
msgstr "不明"
|
||||
|
||||
msgctxt "#30252"
|
||||
msgid "Movies - A-Z"
|
||||
msgstr "映画 - A-Z"
|
||||
|
||||
msgctxt "#30254"
|
||||
msgid "Show add-on settings"
|
||||
msgstr "アドオン設定を見る"
|
||||
|
||||
msgctxt "#30255"
|
||||
msgid "TV Shows - A-Z"
|
||||
msgstr "TV番組 - A-Z"
|
||||
|
||||
msgctxt "#30256"
|
||||
msgid "Movies"
|
||||
msgstr "映画"
|
||||
|
||||
msgctxt "#30257"
|
||||
msgid "Movies - Recently Added"
|
||||
msgstr "映画 - 最近追加された"
|
||||
|
||||
msgctxt "#30258"
|
||||
msgid "Movies - In Progress"
|
||||
msgstr "映画 - 途中"
|
||||
|
||||
msgctxt "#30259"
|
||||
msgid "Movies - Favorites"
|
||||
msgstr "映画 - お気に入り"
|
||||
|
||||
msgctxt "#30260"
|
||||
msgid "BoxSets"
|
||||
msgstr "ボックス・セット"
|
||||
|
||||
msgctxt "#30261"
|
||||
msgid "TV Shows"
|
||||
msgstr "TV番組"
|
||||
|
||||
msgctxt "#30262"
|
||||
msgid "TV Shows - Favorites"
|
||||
msgstr "TV番組 - お気に入り"
|
||||
|
||||
msgctxt "#30264"
|
||||
msgid "Episodes - In Progress"
|
||||
msgstr "エピソード - 途中"
|
||||
|
||||
msgctxt "#30265"
|
||||
msgid "Episodes - Next Up"
|
||||
msgstr "エピソード - 次"
|
||||
|
||||
msgctxt "#30266"
|
||||
msgid "Movies - Pages"
|
||||
msgstr "映画 - ページ"
|
||||
|
||||
msgctxt "#30267"
|
||||
msgid " - In Progress"
|
||||
msgstr "- 途中"
|
||||
|
||||
msgctxt "#30268"
|
||||
msgid " - Recently Added"
|
||||
msgstr "- 最近追加された"
|
||||
|
||||
msgctxt "#30269"
|
||||
msgid "Movies - Random"
|
||||
msgstr "映画 - ランダム"
|
||||
|
||||
msgctxt "#30270"
|
||||
msgid "Mark Watched"
|
||||
msgstr "視聴済みにする"
|
||||
|
||||
msgctxt "#30271"
|
||||
msgid "Mark Unwatched"
|
||||
msgstr "未視聴にする"
|
||||
|
||||
msgctxt "#30272"
|
||||
msgid "Set Favourite"
|
||||
msgstr "お気に入りにする"
|
||||
|
||||
msgctxt "#30273"
|
||||
msgid "Unset Favourite"
|
||||
msgstr "お気に入りから外す"
|
||||
|
||||
msgctxt "#30274"
|
||||
msgid "Delete"
|
||||
msgstr "削除"
|
||||
|
||||
msgctxt "#30275"
|
||||
msgid "Force Transcode"
|
||||
msgstr "トランスコードを強制する"
|
||||
|
||||
msgctxt "#30276"
|
||||
msgid "Extra Resume Prompt Detected"
|
||||
msgstr "追加の再開プロンプトが検出された"
|
||||
|
||||
msgctxt "#30278"
|
||||
msgid " - Next Up"
|
||||
msgstr "- 次"
|
||||
|
||||
msgctxt "#30279"
|
||||
msgid "TV Shows - Unwatched"
|
||||
msgstr "TV番組 - 未視聴"
|
||||
|
||||
msgctxt "#30280"
|
||||
msgid "Missing Title"
|
||||
msgstr "タイトル欠落"
|
||||
|
||||
msgctxt "#30281"
|
||||
msgid "Refresh Cached Images"
|
||||
msgstr "キャッシュしたイメージをリフレッシュする"
|
||||
|
||||
msgctxt "#30282"
|
||||
msgid "No Jellyfin servers detected on your local network."
|
||||
msgstr "ローカルネットワークにJellyfinサーバーは検出されませんでした。"
|
||||
|
||||
msgctxt "#30283"
|
||||
msgid "Play Next Episode?"
|
||||
msgstr "次のエピソードを再生しますか?"
|
||||
|
||||
msgctxt "#30285"
|
||||
msgid " - Unwatched"
|
||||
msgstr "- 未視聴"
|
||||
|
||||
msgctxt "#30286"
|
||||
msgid "Movies - Unwatched"
|
||||
msgstr "映画 - 未視聴"
|
||||
|
||||
msgctxt "#30287"
|
||||
msgid "TV Shows - Latest"
|
||||
msgstr "TV番組 - 最新"
|
||||
|
||||
msgctxt "#30288"
|
||||
msgid " - Latest"
|
||||
msgstr "- 最新"
|
||||
|
||||
msgctxt "#30290"
|
||||
msgid "All"
|
||||
msgstr "全て"
|
||||
|
||||
msgctxt "#30291"
|
||||
msgid "Select Audio Stream"
|
||||
msgstr "音声ストリームを選択"
|
||||
|
||||
msgctxt "#30292"
|
||||
msgid "Select Subtitle Stream"
|
||||
msgstr "サブタイトルストリームを選択"
|
||||
|
||||
msgctxt "#30293"
|
||||
msgid "Cache images"
|
||||
msgstr "イメージをキャッシュする"
|
||||
|
||||
msgctxt "#30294"
|
||||
msgid "Notice"
|
||||
msgstr "通知"
|
||||
|
||||
msgctxt "#30295"
|
||||
msgid "To use this feature you need HTTP control enabled"
|
||||
msgstr "この機能を使うにはHTTPコントロールが有効化されている必要があります"
|
||||
|
||||
msgctxt "#30296"
|
||||
msgid "Delete"
|
||||
msgstr "削除"
|
||||
|
||||
msgctxt "#30297"
|
||||
msgid "Delete unused images?"
|
||||
msgstr "使用されていないイメージを削除しますか?"
|
||||
|
||||
msgctxt "#30299"
|
||||
msgid "Cache Images"
|
||||
msgstr "イメージをキャッシュする"
|
||||
|
||||
msgctxt "#30301"
|
||||
msgid "Caching Images"
|
||||
msgstr "イメージをキャッシュ中"
|
||||
|
||||
msgctxt "#30302"
|
||||
msgid "Existing images : "
|
||||
msgstr "既存のイメージ:"
|
||||
|
||||
msgctxt "#30303"
|
||||
msgid "Missing Jellyfin images : "
|
||||
msgstr "欠落したJellyfinイメージ:"
|
||||
|
||||
msgctxt "#30304"
|
||||
msgid "Cached Jellyfin images : "
|
||||
msgstr "キャッシュされたJellyfinイメージ:"
|
||||
|
||||
msgctxt "#30305"
|
||||
msgid "Not Found"
|
||||
msgstr "見つかりませんでした"
|
||||
|
||||
msgctxt "#30306"
|
||||
msgid "Playback starting"
|
||||
msgstr "再生を開始中"
|
||||
|
||||
msgctxt "#30307"
|
||||
msgid "Play Trailer"
|
||||
msgstr "トレーラーを再生"
|
||||
|
||||
msgctxt "#30308"
|
||||
msgid "Select Trailer"
|
||||
msgstr "トレーラーを選択"
|
||||
|
||||
msgctxt "#30309"
|
||||
msgid "Select Media Source"
|
||||
msgstr "メディアソースを選択"
|
||||
|
||||
msgctxt "#30311"
|
||||
msgid "Library - "
|
||||
msgstr "ライブラリー -"
|
||||
|
||||
msgctxt "#30312"
|
||||
msgid "All - "
|
||||
msgstr "全て -"
|
||||
|
||||
msgctxt "#30313"
|
||||
msgid "Menu"
|
||||
msgstr "メニュー"
|
||||
|
||||
msgctxt "#30314"
|
||||
msgid "Play"
|
||||
msgstr "再生"
|
||||
|
||||
msgctxt "#30315"
|
||||
msgid "Suppress notifications for connection errors"
|
||||
msgstr "接続エラーについての通知を抑制"
|
||||
|
||||
msgctxt "#30316"
|
||||
msgid "Connection Error"
|
||||
msgstr "接続エラー"
|
||||
|
||||
msgctxt "#30317"
|
||||
msgid "Play All"
|
||||
msgstr "全て再生"
|
||||
|
||||
msgctxt "#30318"
|
||||
msgid "Music - Albums"
|
||||
msgstr "音楽 - アルバム"
|
||||
|
||||
msgctxt "#30320"
|
||||
msgid " - Albums"
|
||||
msgstr "- アルバム"
|
||||
|
||||
msgctxt "#30322"
|
||||
msgid "Auto resume"
|
||||
msgstr "自動再開"
|
||||
|
||||
msgctxt "#30323"
|
||||
msgid "Artists"
|
||||
msgstr "アーティスト"
|
||||
|
||||
msgctxt "#30325"
|
||||
msgid " - Genres"
|
||||
msgstr "- ジャンル"
|
||||
|
||||
msgctxt "#30327"
|
||||
msgid "Go To Season"
|
||||
msgstr "シーズンに行く"
|
||||
|
||||
msgctxt "#30328"
|
||||
msgid "Show empty folders (shows, seasons, collections)"
|
||||
msgstr "空のフォルダー(番組, シーズン, コレクション)を見る"
|
||||
|
||||
msgctxt "#30329"
|
||||
msgid "Screensaver"
|
||||
msgstr "スクリーンセーバー"
|
||||
|
||||
msgctxt "#30330"
|
||||
msgid "Show change user dialog"
|
||||
msgstr "ユーザー変更ダイアログを見る"
|
||||
|
||||
msgctxt "#30331"
|
||||
msgid "Movies per page"
|
||||
msgstr "ページごとの映画"
|
||||
|
||||
msgctxt "#30332"
|
||||
msgid "Stop media playback on screensaver activation"
|
||||
msgstr "スクリーンセーバーが始まったらメディア再生をやめる"
|
||||
|
||||
msgctxt "#30334"
|
||||
msgid "Use JellyCon context menu"
|
||||
msgstr "JellyConコンテキストメニューを使う"
|
||||
|
||||
msgctxt "#30337"
|
||||
msgid "Song"
|
||||
msgstr "曲"
|
||||
|
||||
msgctxt "#30338"
|
||||
msgid "Album"
|
||||
msgstr "アルバム"
|
||||
|
||||
msgctxt "#30339"
|
||||
msgid "Person"
|
||||
msgstr "人物"
|
||||
|
||||
msgctxt "#30340"
|
||||
msgid "Group movies into collections"
|
||||
msgstr "映画のグループをコレクションに追加"
|
||||
|
||||
msgctxt "#30341"
|
||||
msgid "Background image update interval (0 = disabled)"
|
||||
msgstr "バックグラウンドのイメージ更新間隔 (0 = 無効)"
|
||||
|
||||
msgctxt "#30342"
|
||||
msgid "New content check interval (0 = disabled)"
|
||||
msgstr "新しいコンテンツの確認間隔 (0 = 無効)"
|
||||
|
||||
msgctxt "#30343"
|
||||
msgid "Changes Require Kodi Restart"
|
||||
msgstr "変更はKodiの再起動を必要とします"
|
||||
|
||||
msgctxt "#30344"
|
||||
msgid "Number of images removed from cache"
|
||||
msgstr "キャッシュから削除されたイメージの数"
|
||||
|
||||
msgctxt "#30345"
|
||||
msgid "Cache Jellyfin server data requests"
|
||||
msgstr "Jellyfinサーバーデータリクエストをキャッシュする"
|
||||
|
||||
msgctxt "#30346"
|
||||
msgid "Deleteing Cached Images"
|
||||
msgstr "キャッシュされたイメージを削除中"
|
||||
|
||||
msgctxt "#30347"
|
||||
msgid "Getting Existing Images"
|
||||
msgstr "既存のイメージを取得中"
|
||||
|
||||
msgctxt "#30348"
|
||||
msgid "Add user ratings"
|
||||
msgstr "ユーザーレーティングを追加"
|
||||
|
||||
msgctxt "#30350"
|
||||
msgid "Music - Recently Added"
|
||||
msgstr "音楽 - 最近追加された"
|
||||
|
||||
msgctxt "#30349"
|
||||
msgid " - Recently Played"
|
||||
msgstr "- 最近再生された"
|
||||
|
||||
msgctxt "#30351"
|
||||
msgid "Music - Recently Played"
|
||||
msgstr "音楽 - 最近再生された"
|
||||
|
||||
msgctxt "#30352"
|
||||
msgid "Music - Frequently Played"
|
||||
msgstr "音楽 - よく再生される"
|
||||
|
||||
msgctxt "#30319"
|
||||
msgid "Music - All Album Artists"
|
||||
msgstr "音楽 - 全てのアルバムアーティスト"
|
||||
|
||||
msgctxt "#30321"
|
||||
msgid " - Album Artists"
|
||||
msgstr "- アルバムアーティスト"
|
||||
|
||||
msgctxt "#30353"
|
||||
msgid " - Frequently Played"
|
||||
msgstr "- よく再生される"
|
||||
|
||||
msgctxt "#30354"
|
||||
msgid "Go To Series"
|
||||
msgstr "シリーズに行く"
|
||||
|
||||
msgctxt "#30356"
|
||||
msgid "Loading existing image list"
|
||||
msgstr "既存のイメージリストを読み込み中"
|
||||
|
||||
msgctxt "#30357"
|
||||
msgid "Processing existing image list"
|
||||
msgstr "既存のイメージリストを処理中"
|
||||
|
||||
msgctxt "#30358"
|
||||
msgid "Retreiving remote image list"
|
||||
msgstr "リモートイメージリストを取得中"
|
||||
|
||||
msgctxt "#30359"
|
||||
msgid "Building full image list"
|
||||
msgstr "フルイメージリストを構築中"
|
||||
|
||||
msgctxt "#30360"
|
||||
msgid " - Channels"
|
||||
msgstr "- チャンネル"
|
||||
|
||||
msgctxt "#30361"
|
||||
msgid " - Programs"
|
||||
msgstr "- 番組"
|
||||
|
||||
msgctxt "#30362"
|
||||
msgid " - Recordings"
|
||||
msgstr "- 録音"
|
||||
|
||||
msgctxt "#30363"
|
||||
msgid "Save Password?"
|
||||
msgstr "パスワードを保存しますか?"
|
||||
|
||||
msgctxt "#30364"
|
||||
msgid "Do you want to save the password?"
|
||||
msgstr "パスワードを保存しますか?"
|
||||
|
||||
msgctxt "#30365"
|
||||
msgid "Manual Login"
|
||||
msgstr "手動ログイン"
|
||||
|
||||
msgctxt "#30366"
|
||||
msgid "Manually enter user details"
|
||||
msgstr "ユーザー詳細を手動で入力"
|
||||
|
||||
msgctxt "#30367"
|
||||
msgid "Allow fast user switching password saving"
|
||||
msgstr "切り替えたユーザーのパスワードの保存を許可"
|
||||
|
||||
msgctxt "#30368"
|
||||
msgid "Clear Password?"
|
||||
msgstr "パスワードを消去しますか?"
|
||||
|
||||
msgctxt "#30369"
|
||||
msgid "Do you want to clear your saved password?"
|
||||
msgstr "保存されたパスワードを消去しますか?"
|
||||
|
||||
msgctxt "#30370"
|
||||
msgid "Do you want to manually enter a server url?"
|
||||
msgstr "サーバーURLを手動で入力しますか?"
|
||||
|
||||
msgctxt "#30371"
|
||||
msgid "Could not connect to the URL you entered, do you want to try again?"
|
||||
msgstr "入力されたURLに接続できませんでした。再試行しますか?"
|
||||
|
||||
msgctxt "#30372"
|
||||
msgid "Server URL"
|
||||
msgstr "サーバーURL"
|
||||
|
||||
msgctxt "#30373"
|
||||
msgid "Scanning for local servers"
|
||||
msgstr "ローカルサーバーを検索中"
|
||||
|
||||
msgctxt "#30374"
|
||||
msgid "Sending request"
|
||||
msgstr "リクエストを送信中"
|
||||
|
||||
msgctxt "#30377"
|
||||
msgid "Sending request"
|
||||
msgstr "リクエストを送信中"
|
||||
|
||||
msgctxt "#30378"
|
||||
msgid "Persist user details"
|
||||
msgstr "ユーザー詳細を保持する"
|
||||
|
||||
msgctxt "#30376"
|
||||
msgid "Checking server url"
|
||||
msgstr "サーバーURLを検証中"
|
||||
|
||||
msgctxt "#30236"
|
||||
msgid "Force transcode h265 (hevc)"
|
||||
msgstr "h265 (hevc)でのトランスコードを強制する"
|
||||
|
||||
msgctxt "#30240"
|
||||
msgid "Force transcode msmpeg4v3 (divx)"
|
||||
msgstr "msmpeg4v3 (divx)でのトランスコードを強制する"
|
||||
|
||||
msgctxt "#30251"
|
||||
msgid "Movies - Genres"
|
||||
msgstr "映画 - ジャンル"
|
||||
|
||||
msgctxt "#30289"
|
||||
msgid "TV Shows - Genres"
|
||||
msgstr "TV番組 - ジャンル"
|
||||
|
||||
msgctxt "#30310"
|
||||
msgid "Enable Jellyfin remote control"
|
||||
msgstr "Jellyfinリモートコントロールを有効化"
|
||||
|
||||
msgctxt "#30333"
|
||||
msgid "Cache artwork in the background"
|
||||
msgstr "アートワークをバックグラウンドでキャッシュ"
|
||||
|
||||
msgctxt "#30223"
|
||||
msgid "Page Size and Filtering"
|
||||
msgstr "ページサイズとフィルタリング"
|
||||
|
||||
msgctxt "#30247"
|
||||
msgid "Custom Widget Content"
|
||||
msgstr "カスタムウィジェットコンテンツ"
|
||||
|
||||
msgctxt "#30263"
|
||||
msgid "Episodes - Recently Added"
|
||||
msgstr "エピソード - 最近追加された"
|
||||
|
||||
msgctxt "#30298"
|
||||
msgid "Deleting Kodi Images"
|
||||
msgstr "Kodiイメージを削除中"
|
||||
|
||||
msgctxt "#30375"
|
||||
msgid "Receiving data packet"
|
||||
msgstr "データパケットを受信中"
|
||||
|
||||
msgctxt "#30238"
|
||||
msgid "Playback stream options"
|
||||
msgstr "再生ストリームオプション"
|
||||
|
||||
msgctxt "#30242"
|
||||
msgid "Force transcode av1"
|
||||
msgstr "av1でのトランスコードを強制する"
|
||||
|
||||
msgctxt "#30300"
|
||||
msgid "Cache all Jellyfin images as local Kodi images?"
|
||||
msgstr "全てのJellyfinイメージをローカルなKodiイメージとしてキャッシュしますか?"
|
||||
|
||||
msgctxt "#30277"
|
||||
msgid "JellyCon needs to prompt for resume on partily played items, Kodi can also prompt, this can cause a double prompt. Do you want to remove the double prompt?"
|
||||
msgstr ""
|
||||
"JellyConは途中まで再生したアイテムを途中から再生するか質問しますが、Kodiも同"
|
||||
"じ質問をすることがあります。重複した質問を取り除きますか?"
|
||||
|
||||
msgctxt "#30114"
|
||||
msgid "Jump back seconds"
|
||||
msgstr "数秒戻す"
|
||||
|
||||
msgctxt "#30120"
|
||||
msgid "Show load progress"
|
||||
msgstr "読込み状況を表示"
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "Done"
|
||||
msgstr "完了"
|
||||
|
||||
msgctxt "#30135"
|
||||
msgid "Error"
|
||||
msgstr "エラー"
|
||||
|
||||
msgctxt "#30020"
|
||||
msgid "Flatten single season"
|
||||
msgstr "1シーズンを畳む"
|
||||
|
||||
msgctxt "#30116"
|
||||
msgid "Add unwatched counts to names"
|
||||
msgstr "名前に未視聴の数を追加"
|
||||
|
||||
msgctxt "#30121"
|
||||
msgid "On resume"
|
||||
msgstr "再開時"
|
||||
|
||||
msgctxt "#30126"
|
||||
msgid "Processing Item : "
|
||||
msgstr "次の項目を処理中:"
|
||||
|
||||
msgctxt "#30139"
|
||||
msgid "No Media Type Set"
|
||||
msgstr "メディアタイプ未設定"
|
||||
|
||||
msgctxt "#30163"
|
||||
msgid "Add (cc) if subtitle is available"
|
||||
msgstr "字幕がある場合(cc)を追加する"
|
||||
|
||||
msgctxt "#30166"
|
||||
msgid "Select Server"
|
||||
msgstr "サーバーを選択する"
|
||||
|
||||
msgctxt "#30167"
|
||||
msgid "Selected Server Address"
|
||||
msgstr "選択されたサーバーアドレス"
|
||||
|
||||
msgctxt "#30169"
|
||||
msgid "Address: "
|
||||
msgstr "アドレス:"
|
||||
|
||||
msgctxt "#30180"
|
||||
msgid "Select User"
|
||||
msgstr "ユーザー選択"
|
||||
|
||||
msgctxt "#30026"
|
||||
msgid "Widget item select action"
|
||||
msgstr "ウィジェット項目選択操作"
|
||||
|
||||
msgctxt "#30200"
|
||||
msgid "URL error"
|
||||
msgstr "URLエラー"
|
||||
|
||||
msgctxt "#30201"
|
||||
msgid "Unable to connect to server"
|
||||
msgstr "サーバーに接続できません"
|
||||
1107
resources/language/resource.language.kk/strings.po
Normal file
1107
resources/language/resource.language.kk/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
396
resources/language/resource.language.ko/strings.po
Normal file
396
resources/language/resource.language.ko/strings.po
Normal file
@@ -0,0 +1,396 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2022-11-23 06:51+0000\n"
|
||||
"Last-Translator: wolfwork <wolfdate25@naver.com>\n"
|
||||
"Language-Team: Korean <https://translate.jellyfin.org/projects/jellycon/"
|
||||
"jellycon/ko/>\n"
|
||||
"Language: ko\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 4.14.1\n"
|
||||
|
||||
msgctxt "#30006"
|
||||
msgid "Password"
|
||||
msgstr "비밀번호"
|
||||
|
||||
msgctxt "#30025"
|
||||
msgid "Password:"
|
||||
msgstr "비밀번호:"
|
||||
|
||||
msgctxt "#30026"
|
||||
msgid "Widget item select action"
|
||||
msgstr "위젯 항목 선택 작업"
|
||||
|
||||
msgctxt "#30045"
|
||||
msgid "Username not found"
|
||||
msgstr "사용자 이름을 찾을 수 없습니다"
|
||||
|
||||
msgctxt "#30112"
|
||||
msgid "Loading Content"
|
||||
msgstr "콘텐츠 로드 중"
|
||||
|
||||
msgctxt "#30120"
|
||||
msgid "Show load progress"
|
||||
msgstr "로드 진행률 표시"
|
||||
|
||||
msgctxt "#30121"
|
||||
msgid "On resume"
|
||||
msgstr "재개 시"
|
||||
|
||||
msgctxt "#30166"
|
||||
msgid "Select Server"
|
||||
msgstr "서버 선택"
|
||||
|
||||
msgctxt "#30212"
|
||||
msgid "Video max width"
|
||||
msgstr "비디오 최대 길이"
|
||||
|
||||
msgctxt "#30251"
|
||||
msgid "Movies - Genres"
|
||||
msgstr "영화 - 장르"
|
||||
|
||||
msgctxt "#30264"
|
||||
msgid "Episodes - In Progress"
|
||||
msgstr "에피소드 - 진행 중"
|
||||
|
||||
msgctxt "#30269"
|
||||
msgid "Movies - Random"
|
||||
msgstr "영화 - 랜덤"
|
||||
|
||||
msgctxt "#30000"
|
||||
msgid "Host"
|
||||
msgstr "호스트"
|
||||
|
||||
msgctxt "#30001"
|
||||
msgid "Port"
|
||||
msgstr "포트"
|
||||
|
||||
msgctxt "#30003"
|
||||
msgid "Verify HTTPS certificate"
|
||||
msgstr "HTTPS 인증서 확인"
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username"
|
||||
msgstr "사용자 이름"
|
||||
|
||||
msgctxt "#30007"
|
||||
msgid "Samba username"
|
||||
msgstr "Samba 사용자이름"
|
||||
|
||||
msgctxt "#30008"
|
||||
msgid "Samba password"
|
||||
msgstr "Samba 비밀번호"
|
||||
|
||||
msgctxt "#30011"
|
||||
msgid "[Detect local server]"
|
||||
msgstr "[로컬 서버 찾기]"
|
||||
|
||||
msgctxt "#30012"
|
||||
msgid "[Change user]"
|
||||
msgstr "[사용자 변경]"
|
||||
|
||||
msgctxt "#30015"
|
||||
msgid "Log timing data"
|
||||
msgstr "로그 타이밍 데이터"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device display name"
|
||||
msgstr "장치 표시 이름"
|
||||
|
||||
msgctxt "#30017"
|
||||
msgid "Show connected clients"
|
||||
msgstr "연결된 클라이언트 표시"
|
||||
|
||||
msgctxt "#30018"
|
||||
msgid "Number of items to show in filtered lists"
|
||||
msgstr "필터링된 목록에 표시할 항목 수"
|
||||
|
||||
msgctxt "#30019"
|
||||
msgid "Filtered episode name format"
|
||||
msgstr "필터링된 에피소드 이름 형식"
|
||||
|
||||
msgctxt "#30021"
|
||||
msgid "Show all episodes item"
|
||||
msgstr "모든 에피소드 보기"
|
||||
|
||||
msgctxt "#30022"
|
||||
msgid "Advanced"
|
||||
msgstr "고급"
|
||||
|
||||
msgctxt "#30023"
|
||||
msgid "Hide unwatched episode details"
|
||||
msgstr "시청하지 않은 에피소드 세부정보 숨기기"
|
||||
|
||||
msgctxt "#30024"
|
||||
msgid "Username:"
|
||||
msgstr "사용자이름:"
|
||||
|
||||
msgctxt "#30027"
|
||||
msgid "Enable debug logging"
|
||||
msgstr "디버그 로깅 활성화"
|
||||
|
||||
msgctxt "#30044"
|
||||
msgid "Incorrect Username/Password"
|
||||
msgstr "잘못된 사용자이름/비밀번호"
|
||||
|
||||
msgctxt "#30052"
|
||||
msgid "Deleting"
|
||||
msgstr "삭제 중"
|
||||
|
||||
msgctxt "#30053"
|
||||
msgid "Waiting for server to delete"
|
||||
msgstr "서버에서 삭제 대기 중"
|
||||
|
||||
msgctxt "#30091"
|
||||
msgid "Confirm delete?"
|
||||
msgstr "삭제하시겠습니까?"
|
||||
|
||||
msgctxt "#30092"
|
||||
msgid "Warning: This action will delete the media files from the server."
|
||||
msgstr "경고: 이 작업은 서버에서 미디어 파일을 삭제합니다."
|
||||
|
||||
msgctxt "#30110"
|
||||
msgid "Interface"
|
||||
msgstr "인터페이스"
|
||||
|
||||
msgctxt "#30111"
|
||||
msgid "Services"
|
||||
msgstr "서비스"
|
||||
|
||||
msgctxt "#30113"
|
||||
msgid "Retrieving Data"
|
||||
msgstr "데이터 수신 중"
|
||||
|
||||
msgctxt "#30114"
|
||||
msgid "Jump back seconds"
|
||||
msgstr "초 뒤로 이동"
|
||||
|
||||
msgctxt "#30116"
|
||||
msgid "Add unwatched counts to names"
|
||||
msgstr "이름에 시청하지 않은 카운트 수 추가"
|
||||
|
||||
msgctxt "#30118"
|
||||
msgid "Add resume percent to names"
|
||||
msgstr "이름에 재개율 추가"
|
||||
|
||||
msgctxt "#30125"
|
||||
msgid "Done"
|
||||
msgstr "완료"
|
||||
|
||||
msgctxt "#30126"
|
||||
msgid "Processing Item : "
|
||||
msgstr "진행중인 항목:"
|
||||
|
||||
msgctxt "#30135"
|
||||
msgid "Error"
|
||||
msgstr "오류"
|
||||
|
||||
msgctxt "#30139"
|
||||
msgid "No Media Type Set"
|
||||
msgstr "미디어 타입 설정 안됨"
|
||||
|
||||
msgctxt "#30163"
|
||||
msgid "Add (cc) if subtitle is available"
|
||||
msgstr "자막이 존재할 경우 (cc) 추가"
|
||||
|
||||
msgctxt "#30167"
|
||||
msgid "Selected Server Address"
|
||||
msgstr "선택된 서버 주소"
|
||||
|
||||
msgctxt "#30169"
|
||||
msgid "Address: "
|
||||
msgstr "주소:"
|
||||
|
||||
msgctxt "#30180"
|
||||
msgid "Select User"
|
||||
msgstr "사용자 선택"
|
||||
|
||||
msgctxt "#30181"
|
||||
msgid "Include plot"
|
||||
msgstr "플롯 포함"
|
||||
|
||||
msgctxt "#30182"
|
||||
msgid "Include media stream info"
|
||||
msgstr "미디어 스트림 정보 포함"
|
||||
|
||||
msgctxt "#30183"
|
||||
msgid "Include people"
|
||||
msgstr "사람 포함"
|
||||
|
||||
msgctxt "#30200"
|
||||
msgid "URL error"
|
||||
msgstr "URL 오류"
|
||||
|
||||
msgctxt "#30201"
|
||||
msgid "Unable to connect to server"
|
||||
msgstr "서버에 연결할 수 없습니다"
|
||||
|
||||
msgctxt "#30206"
|
||||
msgid "Playback type"
|
||||
msgstr "재생 유형"
|
||||
|
||||
msgctxt "#30207"
|
||||
msgid "Playback"
|
||||
msgstr "재생"
|
||||
|
||||
msgctxt "#30208"
|
||||
msgid "Max stream bitrate (Kbits)"
|
||||
msgstr "최대 스트림 비트레이트 (Kbps)"
|
||||
|
||||
msgctxt "#30209"
|
||||
msgid "File direct path"
|
||||
msgstr "파일 직접 경로"
|
||||
|
||||
msgctxt "#30210"
|
||||
msgid "HTTP direct stream"
|
||||
msgstr "HTTP 직접 스트림"
|
||||
|
||||
msgctxt "#30211"
|
||||
msgid "Transcode options"
|
||||
msgstr "트랜스코드 옵션"
|
||||
|
||||
msgctxt "#30213"
|
||||
msgid "Video force 8 bit"
|
||||
msgstr "비디오 강제 8 bit"
|
||||
|
||||
msgctxt "#30214"
|
||||
msgid "Events"
|
||||
msgstr "이벤트"
|
||||
|
||||
msgctxt "#30215"
|
||||
msgid "On playback stop (100% = disabled)"
|
||||
msgstr "재생 중지 시(100% = 비활성화됨)"
|
||||
|
||||
msgctxt "#30216"
|
||||
msgid "Item Details"
|
||||
msgstr "항목 세부정보"
|
||||
|
||||
msgctxt "#30218"
|
||||
msgid "Play next episode after %"
|
||||
msgstr "% 후 다음 에피소드 재생"
|
||||
|
||||
msgctxt "#30222"
|
||||
msgid "Item Layout"
|
||||
msgstr "항목 레이아웃"
|
||||
|
||||
msgctxt "#30223"
|
||||
msgid "Page Size and Filtering"
|
||||
msgstr "페이지 크기와 필터링"
|
||||
|
||||
msgctxt "#30224"
|
||||
msgid "Interaction"
|
||||
msgstr "상호작용"
|
||||
|
||||
msgctxt "#30229"
|
||||
msgid "TV Shows"
|
||||
msgstr "TV 쇼"
|
||||
|
||||
msgctxt "#30231"
|
||||
msgid "Movies"
|
||||
msgstr "영화"
|
||||
|
||||
msgctxt "#30235"
|
||||
msgid "Episodes"
|
||||
msgstr "에피소드"
|
||||
|
||||
msgctxt "#30236"
|
||||
msgid "Force transcode h265 (hevc)"
|
||||
msgstr "강제 트랜스코드 h265 (hevc)"
|
||||
|
||||
msgctxt "#30237"
|
||||
msgid "Start from beginning"
|
||||
msgstr "처음부터 시작"
|
||||
|
||||
msgctxt "#30238"
|
||||
msgid "Playback stream options"
|
||||
msgstr "재생 스트림 옵션"
|
||||
|
||||
msgctxt "#30239"
|
||||
msgid "Force transcode mpeg2"
|
||||
msgstr "강제 트랜스코드 mpeg2"
|
||||
|
||||
msgctxt "#30240"
|
||||
msgid "Force transcode msmpeg4v3 (divx)"
|
||||
msgstr "강제 트랜스코드 msmpeg4v3 (divx)"
|
||||
|
||||
msgctxt "#30241"
|
||||
msgid "Force transcode mpeg4"
|
||||
msgstr "강제 트랜스코드 mpeg4"
|
||||
|
||||
msgctxt "#30246"
|
||||
msgid "Search"
|
||||
msgstr "검색"
|
||||
|
||||
msgctxt "#30247"
|
||||
msgid "Custom Widget Content"
|
||||
msgstr "사용자 지정 위젯 콘텐츠"
|
||||
|
||||
msgctxt "#30250"
|
||||
msgid "Unknown"
|
||||
msgstr "알수없음"
|
||||
|
||||
msgctxt "#30252"
|
||||
msgid "Movies - A-Z"
|
||||
msgstr "영화 - A-Z"
|
||||
|
||||
msgctxt "#30254"
|
||||
msgid "Show add-on settings"
|
||||
msgstr "에드온 설정 보기"
|
||||
|
||||
msgctxt "#30255"
|
||||
msgid "TV Shows - A-Z"
|
||||
msgstr "TV 쇼 - A-Z"
|
||||
|
||||
msgctxt "#30256"
|
||||
msgid "Movies"
|
||||
msgstr "영화"
|
||||
|
||||
msgctxt "#30257"
|
||||
msgid "Movies - Recently Added"
|
||||
msgstr "영화 - 최근에 추가됨"
|
||||
|
||||
msgctxt "#30258"
|
||||
msgid "Movies - In Progress"
|
||||
msgstr "영화 - 진행 중"
|
||||
|
||||
msgctxt "#30259"
|
||||
msgid "Movies - Favorites"
|
||||
msgstr "영화 - 즐겨찾기"
|
||||
|
||||
msgctxt "#30261"
|
||||
msgid "TV Shows"
|
||||
msgstr "TV 쇼"
|
||||
|
||||
msgctxt "#30262"
|
||||
msgid "TV Shows - Favorites"
|
||||
msgstr "TV 쇼 - 즐겨찾기"
|
||||
|
||||
msgctxt "#30263"
|
||||
msgid "Episodes - Recently Added"
|
||||
msgstr "에피소드 - 최근에 추가됨"
|
||||
|
||||
msgctxt "#30266"
|
||||
msgid "Movies - Pages"
|
||||
msgstr "영화 - 페이지"
|
||||
|
||||
msgctxt "#30267"
|
||||
msgid " - In Progress"
|
||||
msgstr "- 진행 중"
|
||||
|
||||
msgctxt "#30268"
|
||||
msgid " - Recently Added"
|
||||
msgstr "- 최근에 추가됨"
|
||||
|
||||
msgctxt "#30270"
|
||||
msgid "Mark Watched"
|
||||
msgstr "본 것으로 표시"
|
||||
|
||||
msgctxt "#30271"
|
||||
msgid "Mark Unwatched"
|
||||
msgstr "본 것으로 표시해제"
|
||||
|
||||
msgctxt "#30272"
|
||||
msgid "Set Favourite"
|
||||
msgstr "즐겨찾기 설정"
|
||||
2
resources/language/resource.language.mk/strings.po
Normal file
2
resources/language/resource.language.mk/strings.po
Normal file
@@ -0,0 +1,2 @@
|
||||
msgid ""
|
||||
msgstr "X-Generator: Weblate\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit"
|
||||
16
resources/language/resource.language.mn/strings.po
Normal file
16
resources/language/resource.language.mn/strings.po
Normal file
@@ -0,0 +1,16 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2025-10-26 00:17+0000\n"
|
||||
"Last-Translator: Battseren Badral <bbattseren88@gmail.com>\n"
|
||||
"Language-Team: Mongolian <https://translate.jellyfin.org/projects/jellycon/"
|
||||
"jellycon/mn/>\n"
|
||||
"Language: mn\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.14\n"
|
||||
|
||||
msgctxt "#30000"
|
||||
msgid "Host"
|
||||
msgstr "Хост"
|
||||
1223
resources/language/resource.language.nb_NO/strings.po
Normal file
1223
resources/language/resource.language.nb_NO/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
1206
resources/language/resource.language.nl/strings.po
Normal file
1206
resources/language/resource.language.nl/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
1231
resources/language/resource.language.pl/strings.po
Normal file
1231
resources/language/resource.language.pl/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
1229
resources/language/resource.language.ru/strings.po
Normal file
1229
resources/language/resource.language.ru/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
1205
resources/language/resource.language.sk/strings.po
Normal file
1205
resources/language/resource.language.sk/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
60
resources/language/resource.language.sq/strings.po
Normal file
60
resources/language/resource.language.sq/strings.po
Normal file
@@ -0,0 +1,60 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"PO-Revision-Date: 2025-12-13 11:55+0000\n"
|
||||
"Last-Translator: A Boci <ami@boci.co.uk>\n"
|
||||
"Language-Team: Albanian <https://translate.jellyfin.org/projects/jellycon/"
|
||||
"jellycon/sq/>\n"
|
||||
"Language: sq\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.14\n"
|
||||
|
||||
msgctxt "#30001"
|
||||
msgid "Port"
|
||||
msgstr "Port"
|
||||
|
||||
msgctxt "#30003"
|
||||
msgid "Verify HTTPS certificate"
|
||||
msgstr "Verifiko çertifikatën HTTPS"
|
||||
|
||||
msgctxt "#30006"
|
||||
msgid "Password"
|
||||
msgstr "Fjalkalimi"
|
||||
|
||||
msgctxt "#30007"
|
||||
msgid "Samba username"
|
||||
msgstr "Përdoruesi Samba"
|
||||
|
||||
msgctxt "#30008"
|
||||
msgid "Samba password"
|
||||
msgstr "Fjalkalimi Samba"
|
||||
|
||||
msgctxt "#30012"
|
||||
msgid "[Change user]"
|
||||
msgstr "[Ndrysho përdoruesin]"
|
||||
|
||||
msgctxt "#30014"
|
||||
msgid "Jellyfin"
|
||||
msgstr "Jellyfin"
|
||||
|
||||
msgctxt "#30016"
|
||||
msgid "Device display name"
|
||||
msgstr "Emri që shfaqet"
|
||||
|
||||
msgctxt "#30000"
|
||||
msgid "Host"
|
||||
msgstr "Host"
|
||||
|
||||
msgctxt "#30005"
|
||||
msgid "Username"
|
||||
msgstr "Përdoruesi"
|
||||
|
||||
msgctxt "#30011"
|
||||
msgid "[Detect local server]"
|
||||
msgstr "[Zbuloni serverin lokal]"
|
||||
|
||||
msgctxt "#30017"
|
||||
msgid "Show connected clients"
|
||||
msgstr "Trego klientët e lidhur"
|
||||
1203
resources/language/resource.language.sv/strings.po
Normal file
1203
resources/language/resource.language.sv/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
1206
resources/language/resource.language.ta/strings.po
Normal file
1206
resources/language/resource.language.ta/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
1225
resources/language/resource.language.tr/strings.po
Normal file
1225
resources/language/resource.language.tr/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
1230
resources/language/resource.language.uk/strings.po
Normal file
1230
resources/language/resource.language.uk/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
1224
resources/language/resource.language.zh_Hans/strings.po
Normal file
1224
resources/language/resource.language.zh_Hans/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
1145
resources/language/resource.language.zh_Hant/strings.po
Normal file
1145
resources/language/resource.language.zh_Hant/strings.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,6 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import time
|
||||
import threading
|
||||
@@ -6,9 +8,9 @@ import threading
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from .lazylogger import LazyLogger
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class ActionAutoClose(threading.Thread):
|
||||
@@ -25,9 +27,9 @@ class ActionAutoClose(threading.Thread):
|
||||
|
||||
def run(self):
|
||||
log.debug("ActionAutoClose Running")
|
||||
while not xbmc.abortRequested and not self.stop_thread:
|
||||
while not xbmc.Monitor().abortRequested() and not self.stop_thread:
|
||||
time_since_last = time.time() - self.last_interaction
|
||||
log.debug("ActionAutoClose time_since_last : {0}", time_since_last)
|
||||
log.debug("ActionAutoClose time_since_last : {0}".format(time_since_last))
|
||||
|
||||
if time_since_last > 20:
|
||||
log.debug("ActionAutoClose Closing Parent")
|
||||
@@ -40,7 +42,7 @@ class ActionAutoClose(threading.Thread):
|
||||
|
||||
def set_last(self):
|
||||
self.last_interaction = time.time()
|
||||
log.debug("ActionAutoClose set_last : {0}", self.last_interaction)
|
||||
log.debug("ActionAutoClose set_last : {0}".format(self.last_interaction))
|
||||
|
||||
def stop(self):
|
||||
log.debug("ActionAutoClose stop_thread called")
|
||||
@@ -69,9 +71,6 @@ class ActionMenu(xbmcgui.WindowXMLDialog):
|
||||
self.listControl.addItems(self.action_items)
|
||||
self.setFocus(self.listControl)
|
||||
|
||||
# bg_image = self.getControl(3010)
|
||||
# bg_image.setHeight(50 * len(self.action_items) + 20)
|
||||
|
||||
def onFocus(self, control_id):
|
||||
pass
|
||||
|
||||
@@ -79,7 +78,7 @@ class ActionMenu(xbmcgui.WindowXMLDialog):
|
||||
pass
|
||||
|
||||
def onMessage(self, message):
|
||||
log.debug("ActionMenu: onMessage: {0}", message)
|
||||
log.debug("ActionMenu: onMessage: {0}".format(message))
|
||||
|
||||
def onAction(self, action):
|
||||
|
||||
@@ -91,12 +90,12 @@ class ActionMenu(xbmcgui.WindowXMLDialog):
|
||||
self.close()
|
||||
else:
|
||||
self.auto_close_thread.set_last()
|
||||
log.debug("ActionMenu: onAction: {0}", action.getId())
|
||||
log.debug("ActionMenu: onAction: {0}".format(action.getId()))
|
||||
|
||||
def onClick(self, control_id):
|
||||
if control_id == 3000:
|
||||
self.selected_action = self.listControl.getSelectedItem()
|
||||
log.debug("ActionMenu: Selected Item: {0}", self.selected_action)
|
||||
log.debug("ActionMenu: Selected Item: {0}".format(self.selected_action))
|
||||
self.auto_close_thread.stop()
|
||||
self.close()
|
||||
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
|
||||
|
||||
class BitrateDialog(xbmcgui.WindowXMLDialog):
|
||||
|
||||
slider_control = None
|
||||
bitrate_label = None
|
||||
initial_bitrate_value = 0
|
||||
selected_transcode_value = 0
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
log.debug("BitrateDialog: __init__")
|
||||
xbmcgui.WindowXML.__init__(self, *args, **kwargs)
|
||||
|
||||
def onInit(self):
|
||||
log.debug("ActionMenu: onInit")
|
||||
self.action_exitkeys_id = [10, 13]
|
||||
|
||||
self.slider_control = self.getControl(3000)
|
||||
self.slider_control.setInt(self.initial_bitrate_value, 400, 100, 15000)
|
||||
|
||||
self.bitrate_label = self.getControl(3030)
|
||||
bitrate_label_string = str(self.slider_control.getInt()) + " Kbs"
|
||||
self.bitrate_label.setLabel(bitrate_label_string)
|
||||
|
||||
def onFocus(self, control_id):
|
||||
pass
|
||||
|
||||
def doAction(self, action_id):
|
||||
pass
|
||||
|
||||
def onMessage(self, message):
|
||||
log.debug("ActionMenu: onMessage: {0}", message)
|
||||
|
||||
def onAction(self, action):
|
||||
|
||||
# log.debug("onAction: onAction: {0} {1}", action.getId(), self.slider_control.getInt())
|
||||
|
||||
bitrate_label_string = str(self.slider_control.getInt()) + " Kbs"
|
||||
self.bitrate_label.setLabel(bitrate_label_string)
|
||||
|
||||
if action.getId() == 10: # ACTION_PREVIOUS_MENU
|
||||
self.close()
|
||||
elif action.getId() == 92: # ACTION_NAV_BACK
|
||||
self.close()
|
||||
elif action.getId() == 7: # ENTER
|
||||
self.selected_transcode_value = self.slider_control.getInt()
|
||||
self.close()
|
||||
|
||||
def onClick(self, control_id):
|
||||
if control_id == 3000:
|
||||
log.debug("ActionMenu: Selected Item: {0}", control_id)
|
||||
#self.close()
|
||||
@@ -1,8 +1,7 @@
|
||||
# coding=utf-8
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import urllib
|
||||
import httplib
|
||||
import base64
|
||||
import sys
|
||||
import threading
|
||||
@@ -12,17 +11,17 @@ import xbmcgui
|
||||
import xbmcplugin
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
import requests
|
||||
from six.moves.urllib.parse import unquote
|
||||
|
||||
from .downloadutils import DownloadUtils
|
||||
from .simple_logging import SimpleLogging
|
||||
from .jsonrpc import JsonRpc
|
||||
from .translation import string_load
|
||||
from .datamanager import DataManager
|
||||
from .utils import get_art, double_urlencode
|
||||
from .jellyfin import api
|
||||
from .lazylogger import LazyLogger
|
||||
from .jsonrpc import JsonRpc, get_value
|
||||
from .utils import translate_string, load_user_details
|
||||
from .kodi_utils import HomeWindow
|
||||
from .item_functions import get_art
|
||||
|
||||
downloadUtils = DownloadUtils()
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class CacheArtwork(threading.Thread):
|
||||
@@ -42,6 +41,13 @@ class CacheArtwork(threading.Thread):
|
||||
last_update = 0
|
||||
home_window = HomeWindow()
|
||||
settings = xbmcaddon.Addon()
|
||||
|
||||
# Check if disk caching is disabled
|
||||
disable_disk_cache = settings.getSetting('disable_disk_cache') == 'true'
|
||||
if disable_disk_cache:
|
||||
log.debug("CacheArtwork : Disk caching disabled, artwork caching skipped")
|
||||
return
|
||||
|
||||
latest_content_hash = "never"
|
||||
check_interval = int(settings.getSetting('cacheImagesOnScreenSaver_interval'))
|
||||
check_interval = check_interval * 60
|
||||
@@ -62,34 +68,34 @@ class CacheArtwork(threading.Thread):
|
||||
|
||||
monitor.waitForAbort(5)
|
||||
|
||||
log.debug("CacheArtwork background thread exited : stop_all_activity : {0}", self.stop_all_activity)
|
||||
log.debug("CacheArtwork background thread exited : stop_all_activity : {0}".format(self.stop_all_activity))
|
||||
|
||||
@staticmethod
|
||||
def delete_cached_images(item_id):
|
||||
log.debug("cache_delete_for_links")
|
||||
|
||||
progress = xbmcgui.DialogProgress()
|
||||
progress.create(string_load(30281))
|
||||
progress.update(30, string_load(30347))
|
||||
progress.create(translate_string(30281))
|
||||
progress.update(30, translate_string(30347))
|
||||
|
||||
item_image_url_part = "Items/%s/Images/" % item_id
|
||||
item_image_url_part = item_image_url_part.replace("/", "%2f")
|
||||
log.debug("texture ids: {0}", item_image_url_part)
|
||||
log.debug("texture ids: {0}".format(item_image_url_part))
|
||||
|
||||
# is the web server enabled
|
||||
web_query = {"setting": "services.webserver"}
|
||||
result = JsonRpc('Settings.GetSettingValue').execute(web_query)
|
||||
xbmc_webserver_enabled = result['result']['value']
|
||||
if not xbmc_webserver_enabled:
|
||||
xbmcgui.Dialog().ok(string_load(30294), string_load(30295))
|
||||
xbmcgui.Dialog().ok(translate_string(30294), translate_string(30295))
|
||||
return
|
||||
|
||||
params = {"properties": ["url"]}
|
||||
json_result = JsonRpc('Textures.GetTextures').execute(params)
|
||||
textures = json_result.get("result", {}).get("textures", [])
|
||||
log.debug("texture ids: {0}", textures)
|
||||
log.debug("texture ids: {0}".format(textures))
|
||||
|
||||
progress.update(70, string_load(30346))
|
||||
progress.update(70, translate_string(30346))
|
||||
|
||||
delete_count = 0
|
||||
for texture in textures:
|
||||
@@ -97,16 +103,16 @@ class CacheArtwork(threading.Thread):
|
||||
texture_url = texture["url"]
|
||||
if item_image_url_part in texture_url:
|
||||
delete_count += 1
|
||||
log.debug("removing texture id: {0}", texture_id)
|
||||
log.debug("removing texture id: {0}".format(texture_id))
|
||||
params = {"textureid": int(texture_id)}
|
||||
JsonRpc('Textures.RemoveTexture').execute(params)
|
||||
|
||||
del textures
|
||||
|
||||
progress.update(100, string_load(30125))
|
||||
progress.update(100, translate_string(30125))
|
||||
progress.close()
|
||||
|
||||
xbmcgui.Dialog().ok(string_load(30281), string_load(30344) % delete_count)
|
||||
xbmcgui.Dialog().ok(translate_string(30281), '{}: {}'.format(translate_string(30344), delete_count))
|
||||
|
||||
def cache_artwork_interactive(self):
|
||||
log.debug("cache_artwork_interactive")
|
||||
@@ -118,21 +124,21 @@ class CacheArtwork(threading.Thread):
|
||||
result = JsonRpc('Settings.GetSettingValue').execute(web_query)
|
||||
xbmc_webserver_enabled = result['result']['value']
|
||||
if not xbmc_webserver_enabled:
|
||||
xbmcgui.Dialog().ok(string_load(30294), string_load(30295), string_load(30355))
|
||||
xbmcgui.Dialog().ok(translate_string(30294), '{} - {}'.format(translate_string(30295), translate_string(30355)))
|
||||
xbmc.executebuiltin('ActivateWindow(servicesettings)')
|
||||
return
|
||||
|
||||
result_report = []
|
||||
|
||||
# ask questions
|
||||
question_delete_unused = xbmcgui.Dialog().yesno(string_load(30296), string_load(30297))
|
||||
question_cache_images = xbmcgui.Dialog().yesno(string_load(30299), string_load(30300))
|
||||
question_delete_unused = xbmcgui.Dialog().yesno(translate_string(30296), translate_string(30297))
|
||||
question_cache_images = xbmcgui.Dialog().yesno(translate_string(30299), translate_string(30300))
|
||||
|
||||
delete_canceled = False
|
||||
# now do work - delete unused
|
||||
if question_delete_unused:
|
||||
delete_pdialog = xbmcgui.DialogProgress()
|
||||
delete_pdialog.create(string_load(30298), "")
|
||||
delete_pdialog.create(translate_string(30298), "")
|
||||
index = 0
|
||||
|
||||
params = {"properties": ["url"]}
|
||||
@@ -141,23 +147,22 @@ class CacheArtwork(threading.Thread):
|
||||
|
||||
jellyfin_texture_urls = self.get_jellyfin_artwork(delete_pdialog)
|
||||
|
||||
log.debug("kodi textures: {0}", textures)
|
||||
log.debug("jellyfin texture urls: {0}", jellyfin_texture_urls)
|
||||
log.debug("kodi textures: {0}".format(textures))
|
||||
log.debug("jellyfin texture urls: {0}".format(jellyfin_texture_urls))
|
||||
|
||||
if jellyfin_texture_urls is not None:
|
||||
|
||||
unused_texture_ids = set()
|
||||
for texture in textures:
|
||||
url = texture.get("url")
|
||||
url = urllib.unquote(url)
|
||||
url = unquote(url)
|
||||
url = url.replace("image://", "")
|
||||
url = url[0:-1]
|
||||
if url.find("/") > -1 and url not in jellyfin_texture_urls or url.find("localhost:24276") > -1:
|
||||
# log.debug("adding unused texture url: {0}", url)
|
||||
unused_texture_ids.add(texture["textureid"])
|
||||
|
||||
total = len(unused_texture_ids)
|
||||
log.debug("unused texture ids: {0}", unused_texture_ids)
|
||||
log.debug("unused texture ids: {0}".format(unused_texture_ids))
|
||||
|
||||
for texture_id in unused_texture_ids:
|
||||
params = {"textureid": int(texture_id)}
|
||||
@@ -171,9 +176,9 @@ class CacheArtwork(threading.Thread):
|
||||
delete_canceled = True
|
||||
break
|
||||
|
||||
result_report.append(string_load(30385) + str(len(textures)))
|
||||
result_report.append(string_load(30386) + str(len(unused_texture_ids)))
|
||||
result_report.append(string_load(30387) + str(index))
|
||||
result_report.append(translate_string(30385) + str(len(textures)))
|
||||
result_report.append(translate_string(30386) + str(len(unused_texture_ids)))
|
||||
result_report.append(translate_string(30387) + str(index))
|
||||
|
||||
del textures
|
||||
del jellyfin_texture_urls
|
||||
@@ -187,7 +192,7 @@ class CacheArtwork(threading.Thread):
|
||||
# now do work - cache images
|
||||
if question_cache_images:
|
||||
cache_pdialog = xbmcgui.DialogProgress()
|
||||
cache_pdialog.create(string_load(30301), "")
|
||||
cache_pdialog.create(translate_string(30301), "")
|
||||
cache_report = self.cache_artwork(cache_pdialog)
|
||||
cache_pdialog.close()
|
||||
del cache_pdialog
|
||||
@@ -196,27 +201,29 @@ class CacheArtwork(threading.Thread):
|
||||
|
||||
if len(result_report) > 0:
|
||||
msg = "\r\n".join(result_report)
|
||||
xbmcgui.Dialog().textviewer(string_load(30125), msg, usemono=True)
|
||||
xbmcgui.Dialog().textviewer(translate_string(30125), msg, usemono=True)
|
||||
|
||||
def cache_artwork_background(self):
|
||||
log.debug("cache_artwork_background")
|
||||
dp = xbmcgui.DialogProgressBG()
|
||||
dp.create(string_load(30301), "")
|
||||
dp.create(translate_string(30301), "")
|
||||
result_text = None
|
||||
try:
|
||||
result_text = self.cache_artwork(dp)
|
||||
except Exception as err:
|
||||
log.error("Cache Images Failed : {0}", err)
|
||||
log.error("Cache Images Failed : {0}".format(err))
|
||||
dp.close()
|
||||
del dp
|
||||
if result_text is not None:
|
||||
log.debug("Cache Images reuslt : {0}", " - ".join(result_text))
|
||||
log.debug("Cache Images result : {0}".format(" - ".join(result_text)))
|
||||
|
||||
def get_jellyfin_artwork(self, progress):
|
||||
log.debug("get_jellyfin_artwork")
|
||||
user_details = load_user_details()
|
||||
user_id = user_details.get('user_id')
|
||||
|
||||
url = ""
|
||||
url += "{server}/Users/{userid}/Items"
|
||||
url += "/Users/{}/Items".format(user_id)
|
||||
url += "?Recursive=true"
|
||||
url += "&EnableUserData=False"
|
||||
url += "&Fields=BasicSyncInfo"
|
||||
@@ -224,25 +231,24 @@ class CacheArtwork(threading.Thread):
|
||||
url += "&ImageTypeLimit=1"
|
||||
url += "&format=json"
|
||||
|
||||
data_manager = DataManager()
|
||||
results = data_manager.get_content(url)
|
||||
results = api.get(url)
|
||||
if results is None:
|
||||
results = []
|
||||
|
||||
if isinstance(results, dict):
|
||||
results = results.get("Items")
|
||||
|
||||
server = downloadUtils.get_server()
|
||||
log.debug("Jellyfin Item Count Count: {0}", len(results))
|
||||
settings = xbmcaddon.Addon()
|
||||
server = settings.getSetting('server_address')
|
||||
log.debug("Jellyfin Item Count Count: {0}".format(len(results)))
|
||||
|
||||
if self.stop_all_activity:
|
||||
return None
|
||||
|
||||
progress.update(0, string_load(30359))
|
||||
progress.update(0, translate_string(30359))
|
||||
|
||||
texture_urls = set()
|
||||
|
||||
# image_types = ["thumb", "poster", "banner", "clearlogo", "tvshow.poster", "tvshow.banner", "tvshow.landscape"]
|
||||
for item in results:
|
||||
art = get_art(item, server)
|
||||
for art_type in art:
|
||||
@@ -254,46 +260,37 @@ class CacheArtwork(threading.Thread):
|
||||
log.debug("cache_artwork")
|
||||
|
||||
# is the web server enabled
|
||||
web_query = {"setting": "services.webserver"}
|
||||
result = JsonRpc('Settings.GetSettingValue').execute(web_query)
|
||||
xbmc_webserver_enabled = result['result']['value']
|
||||
if not xbmc_webserver_enabled:
|
||||
if not get_value("services.webserver"):
|
||||
log.error("Kodi web server not enabled, can not cache images")
|
||||
return
|
||||
|
||||
# get the port
|
||||
web_port = {"setting": "services.webserverport"}
|
||||
result = JsonRpc('Settings.GetSettingValue').execute(web_port)
|
||||
xbmc_port = result['result']['value']
|
||||
log.debug("xbmc_port: {0}", xbmc_port)
|
||||
xbmc_port = get_value("services.webserverport")
|
||||
log.debug("xbmc_port: {0}".format(xbmc_port))
|
||||
|
||||
# get the user
|
||||
web_user = {"setting": "services.webserverusername"}
|
||||
result = JsonRpc('Settings.GetSettingValue').execute(web_user)
|
||||
xbmc_username = result['result']['value']
|
||||
log.debug("xbmc_username: {0}", xbmc_username)
|
||||
xbmc_username = get_value("services.webserverusername")
|
||||
log.debug("xbmc_username: {0}".format(xbmc_username))
|
||||
|
||||
# get the password
|
||||
web_pass = {"setting": "services.webserverpassword"}
|
||||
result = JsonRpc('Settings.GetSettingValue').execute(web_pass)
|
||||
xbmc_password = result['result']['value']
|
||||
xbmc_password = get_value("services.webserverpassword")
|
||||
|
||||
progress.update(0, string_load(30356))
|
||||
progress.update(0, translate_string(30356))
|
||||
|
||||
params = {"properties": ["url"]}
|
||||
json_result = JsonRpc('Textures.GetTextures').execute(params)
|
||||
textures = json_result.get("result", {}).get("textures", [])
|
||||
log.debug("Textures.GetTextures Count: {0}", len(textures))
|
||||
log.debug("Textures.GetTextures Count: {0}".format(len(textures)))
|
||||
|
||||
if self.stop_all_activity:
|
||||
return
|
||||
|
||||
progress.update(0, string_load(30357))
|
||||
progress.update(0, translate_string(30357))
|
||||
|
||||
texture_urls = set()
|
||||
for texture in textures:
|
||||
url = texture.get("url")
|
||||
url = urllib.unquote(url)
|
||||
url = unquote(url)
|
||||
url = url.replace("image://", "")
|
||||
url = url[0:-1]
|
||||
texture_urls.add(url)
|
||||
@@ -301,19 +298,19 @@ class CacheArtwork(threading.Thread):
|
||||
del textures
|
||||
del json_result
|
||||
|
||||
log.debug("texture_urls Count: {0}", len(texture_urls))
|
||||
log.debug("texture_urls Count: {0}".format(len(texture_urls)))
|
||||
|
||||
if self.stop_all_activity:
|
||||
return
|
||||
|
||||
progress.update(0, string_load(30358))
|
||||
progress.update(0, translate_string(30358))
|
||||
|
||||
jellyfin_texture_urls = self.get_jellyfin_artwork(progress)
|
||||
if jellyfin_texture_urls is None:
|
||||
return
|
||||
|
||||
missing_texture_urls = set()
|
||||
# image_types = ["thumb", "poster", "banner", "clearlogo", "tvshow.poster", "tvshow.banner", "tvshow.landscape"]
|
||||
|
||||
for image_url in jellyfin_texture_urls:
|
||||
if image_url not in texture_urls and not image_url.endswith("&Tag=") and len(image_url) > 0:
|
||||
missing_texture_urls.add(image_url)
|
||||
@@ -321,10 +318,10 @@ class CacheArtwork(threading.Thread):
|
||||
if self.stop_all_activity:
|
||||
return
|
||||
|
||||
log.debug("texture_urls: {0}", texture_urls)
|
||||
log.debug("missing_texture_urls: {0}", missing_texture_urls)
|
||||
log.debug("Number of existing textures: {0}", len(texture_urls))
|
||||
log.debug("Number of missing textures: {0}", len(missing_texture_urls))
|
||||
log.debug("texture_urls: {0}".format(texture_urls))
|
||||
log.debug("missing_texture_urls: {0}".format(missing_texture_urls))
|
||||
log.debug("Number of existing textures: {0}".format(len(texture_urls)))
|
||||
log.debug("Number of missing textures: {0}".format(len(missing_texture_urls)))
|
||||
|
||||
kodi_http_server = "localhost:" + str(xbmc_port)
|
||||
headers = {}
|
||||
@@ -333,29 +330,23 @@ class CacheArtwork(threading.Thread):
|
||||
headers = {'Authorization': 'Basic %s' % base64.b64encode(auth)}
|
||||
|
||||
total = len(missing_texture_urls)
|
||||
index = 1
|
||||
|
||||
count_done = 0
|
||||
for get_url in missing_texture_urls:
|
||||
# log.debug("texture_url: {0}", get_url)
|
||||
url = double_urlencode(get_url)
|
||||
kodi_texture_url = ("/image/image://%s" % url)
|
||||
log.debug("kodi_texture_url: {0}", kodi_texture_url)
|
||||
for index, get_url in enumerate(missing_texture_urls, 1):
|
||||
kodi_texture_url = "/image/image://{0}".format(get_url)
|
||||
log.debug("kodi_texture_url: {0}".format(kodi_texture_url))
|
||||
|
||||
percentage = int((float(index) / float(total)) * 100)
|
||||
message = "%s of %s" % (index, total)
|
||||
progress.update(percentage, message)
|
||||
|
||||
conn = httplib.HTTPConnection(kodi_http_server, timeout=20)
|
||||
conn.request(method="GET", url=kodi_texture_url, headers=headers)
|
||||
data = conn.getresponse()
|
||||
if data.status == 200:
|
||||
count_done += 1
|
||||
log.debug("Get Image Result: {0}", data.status)
|
||||
cache_url = "http://%s%s" % (kodi_http_server, kodi_texture_url)
|
||||
data = requests.get(cache_url, timeout=20, headers=headers)
|
||||
|
||||
if data.status_code == 200:
|
||||
count_done += 1
|
||||
log.debug("Get Image Result: {0}".format(data.status_code))
|
||||
|
||||
index += 1
|
||||
# if progress.iscanceled():
|
||||
# if "iscanceled" in dir(progress) and progress.iscanceled():
|
||||
if isinstance(progress, xbmcgui.DialogProgress) and progress.iscanceled():
|
||||
break
|
||||
|
||||
@@ -363,7 +354,7 @@ class CacheArtwork(threading.Thread):
|
||||
break
|
||||
|
||||
result_report = []
|
||||
result_report.append(string_load(30302) + str(len(texture_urls)))
|
||||
result_report.append(string_load(30303) + str(len(missing_texture_urls)))
|
||||
result_report.append(string_load(30304) + str(count_done))
|
||||
result_report.append(translate_string(30302) + str(len(texture_urls)))
|
||||
result_report.append(translate_string(30303) + str(len(missing_texture_urls)))
|
||||
result_report.append(translate_string(30304) + str(count_done))
|
||||
return result_report
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
|
||||
from uuid import uuid4 as uuid4
|
||||
import xbmcaddon
|
||||
import xbmc
|
||||
import xbmcvfs
|
||||
|
||||
from .kodi_utils import HomeWindow
|
||||
from .simple_logging import SimpleLogging
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
|
||||
|
||||
class ClientInformation:
|
||||
|
||||
@staticmethod
|
||||
def get_device_id():
|
||||
|
||||
window = HomeWindow()
|
||||
client_id = window.get_property("client_id")
|
||||
|
||||
if client_id:
|
||||
return client_id
|
||||
|
||||
jellyfin_guid_path = xbmc.translatePath("special://temp/jellycon_guid").decode('utf-8')
|
||||
log.debug("jellyfin_guid_path: {0}", jellyfin_guid_path)
|
||||
guid = xbmcvfs.File(jellyfin_guid_path)
|
||||
client_id = guid.read()
|
||||
guid.close()
|
||||
|
||||
if not client_id:
|
||||
client_id = str("%012X" % uuid4())
|
||||
log.debug("Generating a new guid: {0}", client_id)
|
||||
guid = xbmcvfs.File(jellyfin_guid_path, 'w')
|
||||
guid.write(client_id)
|
||||
guid.close()
|
||||
log.debug("jellyfin_client_id (NEW): {0}", client_id)
|
||||
else:
|
||||
log.debug("jellyfin_client_id: {0}", client_id)
|
||||
|
||||
window.set_property("client_id", client_id)
|
||||
return client_id
|
||||
|
||||
@staticmethod
|
||||
def get_version():
|
||||
addon = xbmcaddon.Addon()
|
||||
version = addon.getAddonInfo("version")
|
||||
return version
|
||||
|
||||
@staticmethod
|
||||
def get_client():
|
||||
return 'Kodi JellyCon'
|
||||
@@ -1,75 +0,0 @@
|
||||
import threading
|
||||
import xbmc
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from resources.lib.functions import show_menu
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
|
||||
|
||||
class ContextMonitor(threading.Thread):
|
||||
|
||||
stop_thread = False
|
||||
|
||||
def run(self):
|
||||
|
||||
item_id = None
|
||||
log.debug("ContextMonitor Thread Started")
|
||||
|
||||
while not xbmc.abortRequested and not self.stop_thread:
|
||||
|
||||
if xbmc.getCondVisibility("Window.IsActive(fullscreenvideo) | Window.IsActive(visualisation)"):
|
||||
xbmc.sleep(1000)
|
||||
else:
|
||||
if xbmc.getCondVisibility("Window.IsVisible(contextmenu)"):
|
||||
if item_id:
|
||||
xbmc.executebuiltin("Dialog.Close(contextmenu,true)")
|
||||
params = {}
|
||||
params["item_id"] = item_id
|
||||
show_menu(params)
|
||||
|
||||
container_id = xbmc.getInfoLabel("System.CurrentControlID")
|
||||
item_id = xbmc.getInfoLabel("Container(" + str(container_id) + ").ListItem.Property(id)")
|
||||
|
||||
xbmc.sleep(100)
|
||||
|
||||
'''
|
||||
context_up = False
|
||||
is_jellycon_item = False
|
||||
|
||||
while not xbmc.abortRequested and not self.stop_thread:
|
||||
|
||||
if xbmc.getCondVisibility("Window.IsActive(fullscreenvideo) | Window.IsActive(visualisation)"):
|
||||
xbmc.sleep(1000)
|
||||
else:
|
||||
if xbmc.getCondVisibility("Window.IsVisible(contextmenu)"):
|
||||
context_up = True
|
||||
if is_jellycon_item:
|
||||
xbmc.executebuiltin("Dialog.Close(contextmenu,true)")
|
||||
else:
|
||||
if context_up: # context now down, do something
|
||||
context_up = False
|
||||
container_id = xbmc.getInfoLabel("System.CurrentControlID")
|
||||
log.debug("ContextMonitor Container ID: {0}", container_id)
|
||||
item_id = xbmc.getInfoLabel("Container(" + str(container_id) + ").ListItem.Property(id)")
|
||||
log.debug("ContextMonitor Item ID: {0}", item_id)
|
||||
if item_id:
|
||||
params = {}
|
||||
params["item_id"] = item_id
|
||||
show_menu(params)
|
||||
|
||||
container_id = xbmc.getInfoLabel("System.CurrentControlID")
|
||||
condition = ("String.StartsWith(Container(" + str(container_id) +
|
||||
").ListItem.Path,plugin://plugin.video.jellycon) + !String.IsEmpty(Container(" +
|
||||
str(container_id) + ").ListItem.Property(id))")
|
||||
is_jellycon_item = xbmc.getCondVisibility(condition)
|
||||
|
||||
xbmc.sleep(200)
|
||||
|
||||
'''
|
||||
|
||||
log.debug("ContextMonitor Thread Exited")
|
||||
|
||||
def stop_monitor(self):
|
||||
log.debug("ContextMonitor Stop Called")
|
||||
self.stop_thread = True
|
||||
@@ -1,27 +1,27 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import json
|
||||
from collections import defaultdict
|
||||
import threading
|
||||
import hashlib
|
||||
import os
|
||||
import cPickle
|
||||
import time
|
||||
|
||||
from .downloadutils import DownloadUtils
|
||||
from .simple_logging import SimpleLogging
|
||||
from .item_functions import extract_item_info
|
||||
from .kodi_utils import HomeWindow
|
||||
from .translation import string_load
|
||||
from .tracking import timer
|
||||
from .filelock import FileLock
|
||||
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
import xbmcvfs
|
||||
import xbmcgui
|
||||
from six.moves import cPickle
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
from .jellyfin import api
|
||||
from .lazylogger import LazyLogger
|
||||
from .item_functions import extract_item_info
|
||||
from .kodi_utils import HomeWindow
|
||||
from .tracking import timer
|
||||
from .filelock import FileLock
|
||||
from .utils import translate_string, load_user_details, translate_path
|
||||
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class CacheItem:
|
||||
@@ -40,42 +40,35 @@ class CacheItem:
|
||||
|
||||
class DataManager:
|
||||
|
||||
addon_dir = xbmc.translatePath(xbmcaddon.Addon().getAddonInfo('profile'))
|
||||
addon_dir = translate_path(xbmcaddon.Addon().getAddonInfo('profile'))
|
||||
|
||||
def __init__(self, *args):
|
||||
# log.debug("DataManager __init__")
|
||||
pass
|
||||
self.user_details = load_user_details()
|
||||
|
||||
@staticmethod
|
||||
def load_json_data(json_data):
|
||||
return json.loads(json_data, object_hook=lambda d: defaultdict(lambda: None, d))
|
||||
|
||||
@timer
|
||||
def get_content(self, url):
|
||||
json_data = DownloadUtils().download_url(url)
|
||||
result = self.load_json_data(json_data)
|
||||
return result
|
||||
self.api = api
|
||||
|
||||
@timer
|
||||
def get_items(self, url, gui_options, use_cache=False):
|
||||
|
||||
home_window = HomeWindow()
|
||||
log.debug("last_content_url : use_cache={0} url={1}", use_cache, url)
|
||||
log.debug("last_content_url : use_cache={0} url={1}".format(use_cache, url))
|
||||
home_window.set_property("last_content_url", url)
|
||||
|
||||
download_utils = DownloadUtils()
|
||||
user_id = download_utils.get_user_id()
|
||||
server = download_utils.get_server()
|
||||
# Check if disk caching is disabled
|
||||
settings = xbmcaddon.Addon()
|
||||
disable_disk_cache = settings.getSetting('disable_disk_cache') == 'true'
|
||||
if disable_disk_cache:
|
||||
use_cache = False
|
||||
log.debug("Disk caching disabled - data will be kept in RAM only")
|
||||
|
||||
user_id = self.user_details.get('user_id')
|
||||
server = self.api.server
|
||||
|
||||
m = hashlib.md5()
|
||||
m.update(user_id + "|" + str(server) + "|" + url)
|
||||
m.update('{}|{}|{}'.format(user_id, server, url).encode())
|
||||
url_hash = m.hexdigest()
|
||||
cache_file = os.path.join(self.addon_dir, "cache_" + url_hash + ".pickle")
|
||||
|
||||
# changed_url = url + "&MinDateLastSavedForUser=" + urllib.unquote("2019-09-16T13:45:30")
|
||||
# results = self.GetContent(changed_url)
|
||||
# log.debug("DataManager Changes Since Date : {0}", results)
|
||||
|
||||
item_list = None
|
||||
total_records = 0
|
||||
baseline_name = None
|
||||
@@ -102,14 +95,14 @@ class DataManager:
|
||||
item_list = cache_item.item_list
|
||||
total_records = cache_item.total_records
|
||||
except Exception as err:
|
||||
log.error("Pickle Data Load Failed : {0}", err)
|
||||
log.error("Pickle Data Load Failed : {0}".format(err))
|
||||
item_list = None
|
||||
|
||||
# we need to load the list item data form the server
|
||||
if item_list is None or len(item_list) == 0:
|
||||
log.debug("Loading url data from server")
|
||||
|
||||
results = self.get_content(url)
|
||||
results = self.api.get(url)
|
||||
|
||||
if results is None:
|
||||
results = []
|
||||
@@ -141,7 +134,6 @@ class DataManager:
|
||||
cache_item.total_records = total_records
|
||||
|
||||
cache_thread.cached_item = cache_item
|
||||
# copy.deepcopy(item_list)
|
||||
|
||||
if not use_cache:
|
||||
cache_thread = None
|
||||
@@ -177,9 +169,15 @@ class CacheManagerThread(threading.Thread):
|
||||
def run(self):
|
||||
|
||||
log.debug("CacheManagerThread : Started")
|
||||
# log.debug("CacheManagerThread : Cache Item : {0}", self.cached_item.__dict__)
|
||||
|
||||
home_window = HomeWindow()
|
||||
settings = xbmcaddon.Addon()
|
||||
disable_disk_cache = settings.getSetting('disable_disk_cache') == 'true'
|
||||
|
||||
if disable_disk_cache:
|
||||
log.debug("CacheManagerThread : Disk caching disabled, skipping cache operations")
|
||||
return
|
||||
|
||||
is_fresh = False
|
||||
|
||||
# if the data is fresh then just save it
|
||||
@@ -206,10 +204,10 @@ class CacheManagerThread(threading.Thread):
|
||||
else:
|
||||
log.debug("CacheManagerThread : Reloading to recheck data hashes")
|
||||
cached_hash = self.cached_item.item_list_hash
|
||||
log.debug("CacheManagerThread : Cache Hash : {0}", cached_hash)
|
||||
log.debug("CacheManagerThread : Cache Hash : {0}".format(cached_hash))
|
||||
|
||||
data_manager = DataManager()
|
||||
results = data_manager.get_content(self.cached_item.items_url)
|
||||
results = data_manager.api.get(self.cached_item.items_url)
|
||||
if results is None:
|
||||
results = []
|
||||
|
||||
@@ -232,7 +230,7 @@ class CacheManagerThread(threading.Thread):
|
||||
return
|
||||
|
||||
loaded_hash = self.get_data_hash(loaded_items)
|
||||
log.debug("CacheManagerThread : Loaded Hash : {0}", loaded_hash)
|
||||
log.debug("CacheManagerThread : Loaded Hash : {0}".format(loaded_hash))
|
||||
|
||||
# if they dont match then save the data and trigger a content reload
|
||||
if cached_hash != loaded_hash:
|
||||
@@ -252,7 +250,7 @@ class CacheManagerThread(threading.Thread):
|
||||
# TODO: probably should only set this in simple check mode
|
||||
current_time_stamp = str(time.time())
|
||||
home_window.set_property("jellycon_widget_reload", current_time_stamp)
|
||||
log.debug("Setting New Widget Hash: {0}", current_time_stamp)
|
||||
log.debug("Setting New Widget Hash: {0}".format(current_time_stamp))
|
||||
|
||||
log.debug("CacheManagerThread : Sending container refresh")
|
||||
xbmc.executebuiltin("Container.Refresh")
|
||||
@@ -270,30 +268,31 @@ class CacheManagerThread(threading.Thread):
|
||||
def clear_cached_server_data():
|
||||
log.debug("clear_cached_server_data() called")
|
||||
|
||||
addon_dir = xbmc.translatePath(xbmcaddon.Addon().getAddonInfo('profile'))
|
||||
addon_dir = translate_path(xbmcaddon.Addon().getAddonInfo('profile'))
|
||||
dirs, files = xbmcvfs.listdir(addon_dir)
|
||||
|
||||
del_count = 0
|
||||
for filename in files:
|
||||
if filename.startswith("cache_") and filename.endswith(".pickle"):
|
||||
log.debug("Deleteing CacheFile: {0}", filename)
|
||||
log.debug("Deleting CacheFile: {0}".format(filename))
|
||||
xbmcvfs.delete(os.path.join(addon_dir, filename))
|
||||
del_count += 1
|
||||
|
||||
msg = string_load(30394) % del_count
|
||||
xbmcgui.Dialog().ok(string_load(30393), msg)
|
||||
log.debug('Deleted {} files'.format(del_count))
|
||||
msg = translate_string(30394)
|
||||
xbmcgui.Dialog().ok(translate_string(30393), msg)
|
||||
|
||||
|
||||
def clear_old_cache_data():
|
||||
log.debug("clear_old_cache_data() : called")
|
||||
|
||||
addon_dir = xbmc.translatePath(xbmcaddon.Addon().getAddonInfo('profile'))
|
||||
addon_dir = translate_path(xbmcaddon.Addon().getAddonInfo('profile'))
|
||||
dirs, files = xbmcvfs.listdir(addon_dir)
|
||||
|
||||
del_count = 0
|
||||
for filename in files:
|
||||
if filename.startswith("cache_") and filename.endswith(".pickle"):
|
||||
log.debug("clear_old_cache_data() : Checking CacheFile : {0}", filename)
|
||||
log.debug("clear_old_cache_data() : Checking CacheFile : {0}".format(filename))
|
||||
|
||||
cache_item = None
|
||||
for x in range(0, 5):
|
||||
@@ -304,7 +303,7 @@ def clear_old_cache_data():
|
||||
cache_item = cPickle.load(handle)
|
||||
break
|
||||
except Exception as error:
|
||||
log.debug("clear_old_cache_data() : Pickle load error : {0}", error)
|
||||
log.debug("clear_old_cache_data() : Pickle load error : {0}".format(error))
|
||||
cache_item = None
|
||||
xbmc.sleep(1000)
|
||||
|
||||
@@ -313,9 +312,9 @@ def clear_old_cache_data():
|
||||
if cache_item.date_last_used is not None:
|
||||
item_last_used = time.time() - cache_item.date_last_used
|
||||
|
||||
log.debug("clear_old_cache_data() : Cache item last used : {0} sec ago", item_last_used)
|
||||
log.debug("clear_old_cache_data() : Cache item last used : {0} sec ago".format(item_last_used))
|
||||
if item_last_used == -1 or item_last_used > (3600 * 24 * 7):
|
||||
log.debug("clear_old_cache_data() : Deleting cache item age : {0}", item_last_used)
|
||||
log.debug("clear_old_cache_data() : Deleting cache item age : {0}".format(item_last_used))
|
||||
data_file = os.path.join(addon_dir, filename)
|
||||
with FileLock(data_file + ".locked", timeout=5):
|
||||
xbmcvfs.delete(data_file)
|
||||
@@ -326,4 +325,4 @@ def clear_old_cache_data():
|
||||
with FileLock(data_file + ".locked", timeout=5):
|
||||
xbmcvfs.delete(data_file)
|
||||
|
||||
log.debug("clear_old_cache_data() : Cache items deleted : {0}", del_count)
|
||||
log.debug("clear_old_cache_data() : Cache items deleted : {0}".format(del_count))
|
||||
|
||||
264
resources/lib/dialogs.py
Normal file
264
resources/lib/dialogs.py
Normal file
@@ -0,0 +1,264 @@
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import xbmcgui
|
||||
import xbmc
|
||||
|
||||
|
||||
from .lazylogger import LazyLogger
|
||||
from .utils import seconds_to_ticks, ticks_to_seconds, translate_string, send_event_notification
|
||||
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class BitrateDialog(xbmcgui.WindowXMLDialog):
|
||||
|
||||
slider_control = None
|
||||
bitrate_label = None
|
||||
initial_bitrate_value = 0
|
||||
selected_transcode_value = 0
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
log.debug("BitrateDialog: __init__")
|
||||
xbmcgui.WindowXML.__init__(self, *args, **kwargs)
|
||||
|
||||
def onInit(self):
|
||||
log.debug("ActionMenu: onInit")
|
||||
self.action_exitkeys_id = [10, 13]
|
||||
|
||||
self.slider_control = self.getControl(3000)
|
||||
self.slider_control.setInt(self.initial_bitrate_value, 400, 100, 15000)
|
||||
|
||||
self.bitrate_label = self.getControl(3030)
|
||||
bitrate_label_string = str(self.slider_control.getInt()) + " Kbs"
|
||||
self.bitrate_label.setLabel(bitrate_label_string)
|
||||
self.getControl(3011).setLabel(translate_string(30314))
|
||||
|
||||
def onFocus(self, control_id):
|
||||
pass
|
||||
|
||||
def doAction(self, action_id):
|
||||
pass
|
||||
|
||||
def onMessage(self, message):
|
||||
log.debug("ActionMenu: onMessage: {0}".format(message))
|
||||
|
||||
def onAction(self, action):
|
||||
|
||||
bitrate_label_string = str(self.slider_control.getInt()) + " Kbs"
|
||||
self.bitrate_label.setLabel(bitrate_label_string)
|
||||
|
||||
if action.getId() == 10: # ACTION_PREVIOUS_MENU
|
||||
self.close()
|
||||
elif action.getId() == 92: # ACTION_NAV_BACK
|
||||
self.close()
|
||||
elif action.getId() == 7: # ENTER
|
||||
self.selected_transcode_value = self.slider_control.getInt()
|
||||
self.close()
|
||||
|
||||
def onClick(self, control_id):
|
||||
if control_id == 3000:
|
||||
log.debug("ActionMenu: Selected Item: {0}".format(control_id))
|
||||
|
||||
|
||||
class ResumeDialog(xbmcgui.WindowXMLDialog):
|
||||
resumePlay = -1
|
||||
resumeTimeStamp = ""
|
||||
action_exitkeys_id = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
|
||||
log.debug("ResumeDialog INITIALISED")
|
||||
|
||||
def onInit(self):
|
||||
self.action_exitkeys_id = [10, 13]
|
||||
self.getControl(3010).setLabel(self.resumeTimeStamp)
|
||||
self.getControl(3011).setLabel(translate_string(30237))
|
||||
|
||||
def onFocus(self, controlId):
|
||||
pass
|
||||
|
||||
def doAction(self, actionID):
|
||||
pass
|
||||
|
||||
def onClick(self, controlID):
|
||||
|
||||
if controlID == 3010:
|
||||
self.resumePlay = 0
|
||||
self.close()
|
||||
if controlID == 3011:
|
||||
self.resumePlay = 1
|
||||
self.close()
|
||||
|
||||
def setResumeTime(self, timeStamp):
|
||||
self.resumeTimeStamp = timeStamp
|
||||
|
||||
def getResumeAction(self):
|
||||
return self.resumePlay
|
||||
|
||||
|
||||
class SafeDeleteDialog(xbmcgui.WindowXMLDialog):
|
||||
|
||||
confirm = False
|
||||
message = "Demo Message"
|
||||
heading = "Demo Heading"
|
||||
action_exitkeys_id = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
log.debug("SafeDeleteDialog: __init__")
|
||||
xbmcgui.WindowXML.__init__(self, *args, **kwargs)
|
||||
|
||||
def onInit(self):
|
||||
log.debug("SafeDeleteDialog: onInit")
|
||||
self.action_exitkeys_id = [10, 13]
|
||||
|
||||
message_control = self.getControl(3)
|
||||
message_control.setText(self.message)
|
||||
|
||||
message_control = self.getControl(4)
|
||||
message_control.setLabel(self.heading)
|
||||
|
||||
def onFocus(self, controlId):
|
||||
pass
|
||||
|
||||
def doAction(self, actionID):
|
||||
pass
|
||||
|
||||
def onMessage(self, message):
|
||||
log.debug("SafeDeleteDialog: onMessage: {0}".format(message))
|
||||
|
||||
def onAction(self, action):
|
||||
|
||||
if action.getId() == 10: # ACTION_PREVIOUS_MENU
|
||||
self.close()
|
||||
elif action.getId() == 92: # ACTION_NAV_BACK
|
||||
self.close()
|
||||
else:
|
||||
log.debug("SafeDeleteDialog: onAction: {0}".format(action.getId()))
|
||||
|
||||
def onClick(self, controlID):
|
||||
if controlID == 1:
|
||||
self.confirm = True
|
||||
self.close()
|
||||
elif controlID == 2:
|
||||
self.confirm = False
|
||||
self.close()
|
||||
|
||||
|
||||
class PlayNextDialog(xbmcgui.WindowXMLDialog):
|
||||
|
||||
action_exitkeys_id = None
|
||||
episode_info = None
|
||||
play_called = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
log.debug("PlayNextDialog: __init__")
|
||||
xbmcgui.WindowXML.__init__(self, *args, **kwargs)
|
||||
|
||||
def onInit(self):
|
||||
log.debug("PlayNextDialog: onInit")
|
||||
self.action_exitkeys_id = [10, 13]
|
||||
|
||||
index = self.episode_info.get("IndexNumber", -1)
|
||||
series_name = self.episode_info.get("SeriesName")
|
||||
next_epp_name = "Episode %02d - (%s)" % (index, self.episode_info.get("Name", "n/a"))
|
||||
|
||||
series_label = self.getControl(3011)
|
||||
series_label.setLabel(series_name)
|
||||
|
||||
series_label = self.getControl(3012)
|
||||
series_label.setLabel(next_epp_name)
|
||||
|
||||
def onFocus(self, control_id):
|
||||
pass
|
||||
|
||||
def doAction(self, action_id):
|
||||
pass
|
||||
|
||||
def onMessage(self, message):
|
||||
log.debug("PlayNextDialog: onMessage: {0}".format(message))
|
||||
|
||||
def onAction(self, action):
|
||||
|
||||
if action.getId() == 10: # ACTION_PREVIOUS_MENU
|
||||
self.close()
|
||||
elif action.getId() == 92: # ACTION_NAV_BACK
|
||||
self.close()
|
||||
else:
|
||||
log.debug("PlayNextDialog: onAction: {0}".format(action.getId()))
|
||||
|
||||
def onClick(self, control_id):
|
||||
if control_id == 3013:
|
||||
log.debug("PlayNextDialog: Play Next Episode")
|
||||
self.play_called
|
||||
self.close()
|
||||
next_item_id = self.episode_info.get("Id")
|
||||
log.debug("Playing Next Episode: {0}".format(next_item_id))
|
||||
play_info = {}
|
||||
play_info["item_id"] = next_item_id
|
||||
play_info["auto_resume"] = "-1"
|
||||
play_info["force_transcode"] = False
|
||||
send_event_notification("jellycon_play_action", play_info)
|
||||
elif control_id == 3014:
|
||||
self.close()
|
||||
|
||||
def set_episode_info(self, info):
|
||||
self.episode_info = info
|
||||
|
||||
def get_play_called(self):
|
||||
return self.play_called
|
||||
|
||||
class SkipDialog(xbmcgui.WindowXMLDialog):
|
||||
|
||||
action_exitkeys_id = None
|
||||
media_id = None
|
||||
start = None
|
||||
end = None
|
||||
|
||||
has_been_dissmissed = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
log.debug("SkipDialog: __init__")
|
||||
xbmcgui.WindowXML.__init__(self, *args, **kwargs)
|
||||
|
||||
def onInit(self):
|
||||
log.debug("SkipDialog: onInit")
|
||||
self.action_exitkeys_id = [10, 13]
|
||||
|
||||
def onFocus(self, control_id):
|
||||
pass
|
||||
|
||||
def doAction(self, action_id):
|
||||
pass
|
||||
|
||||
def onMessage(self, message):
|
||||
log.debug("SkipDialog: onMessage: {0}".format(message))
|
||||
|
||||
def onAction(self, action):
|
||||
log.debug("SkipDialog: onAction: {0}".format(action.getId()))
|
||||
if action.getId() == 10 or action.getId() == 92: # ACTION_PREVIOUS_MENU & ACTION_NAV_BACK
|
||||
log.debug("SkipDialog: dismissing dialog so it does not open again")
|
||||
self.has_been_dissmissed = True
|
||||
self.close()
|
||||
|
||||
def onClick(self, control_id):
|
||||
log.debug("SkipDialog: onClick: {0}".format(control_id))
|
||||
player = xbmc.Player()
|
||||
current_ticks = seconds_to_ticks(player.getTime())
|
||||
if self.start is not None and self.end is not None and current_ticks >= self.start and current_ticks <= self.end:
|
||||
log.debug("SkipDialog: skipping segment because current ticks ({0}) is in range".format(current_ticks))
|
||||
# If click during segment, skip it
|
||||
player.seekTime(ticks_to_seconds(self.end))
|
||||
|
||||
self.close()
|
||||
|
||||
def get_play_called(self):
|
||||
return self.play_called
|
||||
|
||||
def is_button_shown(self):
|
||||
try:
|
||||
self.getFocus()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
@@ -1,23 +1,25 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import sys
|
||||
import re
|
||||
|
||||
import xbmcaddon
|
||||
import xbmcplugin
|
||||
import xbmcgui
|
||||
|
||||
import urllib
|
||||
import sys
|
||||
import re
|
||||
from six.moves.urllib.parse import quote, unquote, parse_qsl
|
||||
|
||||
from .datamanager import DataManager
|
||||
from .kodi_utils import HomeWindow
|
||||
from .downloadutils import DownloadUtils
|
||||
from .translation import string_load
|
||||
from .simple_logging import SimpleLogging
|
||||
from .lazylogger import LazyLogger
|
||||
from .item_functions import add_gui_item, ItemDetails
|
||||
from .utils import send_event_notification
|
||||
from .tracking import timer
|
||||
from .utils import (
|
||||
send_event_notification, translate_string,
|
||||
load_user_details, get_default_filters
|
||||
)
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
@timer
|
||||
@@ -27,10 +29,10 @@ def get_content(url, params):
|
||||
default_sort = params.get("sort")
|
||||
media_type = params.get("media_type", None)
|
||||
if not media_type:
|
||||
xbmcgui.Dialog().ok(string_load(30135), string_load(30139))
|
||||
xbmcgui.Dialog().ok(translate_string(30135), translate_string(30139))
|
||||
|
||||
log.debug("URL: {0}", url)
|
||||
log.debug("MediaType: {0}", media_type)
|
||||
log.debug("URL: {0}".format(url))
|
||||
log.debug("MediaType: {0}".format(media_type))
|
||||
pluginhandle = int(sys.argv[1])
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
@@ -38,6 +40,7 @@ def get_content(url, params):
|
||||
view_type = ""
|
||||
content_type = ""
|
||||
media_type = str(media_type).lower().strip()
|
||||
url_params = dict(parse_qsl(url))
|
||||
if media_type.startswith("movie"):
|
||||
view_type = "Movies"
|
||||
content_type = 'movies'
|
||||
@@ -70,47 +73,62 @@ def get_content(url, params):
|
||||
content_type = 'episodes'
|
||||
elif media_type == "playlists":
|
||||
view_type = "Playlists"
|
||||
elif media_type == "musicvideos":
|
||||
view_type = "Music Videos"
|
||||
content_type = 'musicvideos'
|
||||
elif media_type == "mixed":
|
||||
content_type = 'videos'
|
||||
|
||||
log.debug("media_type:{0} content_type:{1} view_type:{2} ", media_type, content_type, view_type)
|
||||
log.debug("media_type:{0} content_type:{1} view_type:{2} ".format(media_type, content_type, view_type))
|
||||
|
||||
# show a progress indicator if needed
|
||||
progress = None
|
||||
if settings.getSetting('showLoadProgress') == "true":
|
||||
progress = xbmcgui.DialogProgress()
|
||||
progress.create(string_load(30112))
|
||||
progress.update(0, string_load(30113))
|
||||
progress.create(translate_string(30112))
|
||||
progress.update(0, translate_string(30113))
|
||||
|
||||
# update url for paging
|
||||
start_index = 0
|
||||
page_limit = int(settings.getSetting('moviePageSize'))
|
||||
start_index = int(url_params.get("StartIndex", 0))
|
||||
url_limit = url_params.get("Limit")
|
||||
movie_page_limit = int(settings.getSetting('moviePageSize'))
|
||||
show_page_limit = int(settings.getSetting('showPageSize'))
|
||||
url_prev = None
|
||||
url_next = None
|
||||
if page_limit > 0 and media_type.startswith("movie"):
|
||||
m = re.search('StartIndex=([0-9]{1,4})', url)
|
||||
if m and m.group(1):
|
||||
log.debug("UPDATING NEXT URL: {0}", url)
|
||||
start_index = int(m.group(1))
|
||||
log.debug("current_start : {0}", start_index)
|
||||
if not url_limit or start_index > 0:
|
||||
if movie_page_limit > 0 and media_type.startswith("movie"):
|
||||
log.debug("UPDATING NEXT URL: {0}".format(url))
|
||||
log.debug("current_start : {0}".format(start_index))
|
||||
if start_index > 0:
|
||||
prev_index = start_index - page_limit
|
||||
prev_index = start_index - movie_page_limit
|
||||
if prev_index < 0:
|
||||
prev_index = 0
|
||||
url_prev = re.sub('StartIndex=([0-9]{1,4})', 'StartIndex=' + str(prev_index), url)
|
||||
url_next = re.sub('StartIndex=([0-9]{1,4})', 'StartIndex=' + str(start_index + page_limit), url)
|
||||
log.debug("UPDATING NEXT URL: {0}", url_next)
|
||||
url_next = re.sub('StartIndex=([0-9]{1,4})', 'StartIndex=' + str(start_index + movie_page_limit), url)
|
||||
log.debug("UPDATING NEXT URL: {0}".format(url_next))
|
||||
|
||||
else:
|
||||
log.debug("ADDING NEXT URL: {0}", url)
|
||||
url_next = url + "&StartIndex=" + str(start_index + page_limit) + "&Limit=" + str(page_limit)
|
||||
url = url + "&StartIndex=" + str(start_index) + "&Limit=" + str(page_limit)
|
||||
log.debug("ADDING NEXT URL: {0}", url_next)
|
||||
else:
|
||||
log.debug("ADDING NEXT URL: {0}".format(url))
|
||||
url_next = url + "&StartIndex=" + str(start_index + movie_page_limit) + "&Limit=" + str(movie_page_limit)
|
||||
url = url + "&StartIndex=" + str(start_index) + "&Limit=" + str(movie_page_limit)
|
||||
log.debug("ADDING NEXT URL: {0}".format(url_next))
|
||||
|
||||
# use the data manager to get the data
|
||||
# result = dataManager.GetContent(url)
|
||||
if show_page_limit > 0 and media_type.startswith("tvshow"):
|
||||
log.debug("UPDATING NEXT URL: {0}".format(url))
|
||||
log.debug("current_start : {0}".format(start_index))
|
||||
if start_index > 0:
|
||||
prev_index = start_index - show_page_limit
|
||||
if prev_index < 0:
|
||||
prev_index = 0
|
||||
url_prev = re.sub('StartIndex=([0-9]{1,4})', 'StartIndex=' + str(prev_index), url)
|
||||
url_next = re.sub('StartIndex=([0-9]{1,4})', 'StartIndex=' + str(start_index + show_page_limit), url)
|
||||
log.debug("UPDATING NEXT URL: {0}".format(url_next))
|
||||
|
||||
# total_records = 0
|
||||
# if result is not None and isinstance(result, dict):
|
||||
# total_records = result.get("TotalRecordCount", 0)
|
||||
else:
|
||||
log.debug("ADDING NEXT URL: {0}".format(url))
|
||||
url_next = url + "&StartIndex=" + str(start_index + show_page_limit) + "&Limit=" + str(show_page_limit)
|
||||
url = url + "&StartIndex=" + str(start_index) + "&Limit=" + str(show_page_limit)
|
||||
log.debug("ADDING NEXT URL: {0}".format(url_next))
|
||||
|
||||
use_cache = params.get("use_cache", "true") == "true"
|
||||
|
||||
@@ -118,33 +136,52 @@ def get_content(url, params):
|
||||
if dir_items is None:
|
||||
return
|
||||
|
||||
log.debug("total_records: {0}", total_records)
|
||||
log.debug("total_records: {0}".format(total_records))
|
||||
|
||||
# add paging items
|
||||
if page_limit > 0 and media_type.startswith("movie"):
|
||||
if url_prev:
|
||||
list_item = xbmcgui.ListItem("Prev Page (" + str(start_index - page_limit + 1) + "-" + str(start_index) +
|
||||
" of " + str(total_records) + ")")
|
||||
u = sys.argv[0] + "?url=" + urllib.quote(url_prev) + "&mode=GET_CONTENT&media_type=movies"
|
||||
log.debug("ADDING PREV ListItem: {0} - {1}", u, list_item)
|
||||
dir_items.insert(0, (u, list_item, True))
|
||||
if not url_limit or start_index > 0:
|
||||
# add paging items
|
||||
if movie_page_limit > 0 and media_type.startswith("movie"):
|
||||
if url_prev:
|
||||
list_item = xbmcgui.ListItem("Prev Page (" + str(start_index - movie_page_limit + 1) + "-" + str(start_index) +
|
||||
" of " + str(total_records) + ")")
|
||||
u = sys.argv[0] + "?url=" + quote(url_prev) + "&mode=GET_CONTENT&media_type=movies"
|
||||
log.debug("ADDING PREV ListItem: {0} - {1}".format(u, list_item))
|
||||
dir_items.insert(0, (u, list_item, True))
|
||||
|
||||
if start_index + page_limit < total_records:
|
||||
upper_count = start_index + (page_limit * 2)
|
||||
if upper_count > total_records:
|
||||
upper_count = total_records
|
||||
list_item = xbmcgui.ListItem("Next Page (" + str(start_index + page_limit + 1) + "-" +
|
||||
str(upper_count) + " of " + str(total_records) + ")")
|
||||
u = sys.argv[0] + "?url=" + urllib.quote(url_next) + "&mode=GET_CONTENT&media_type=movies"
|
||||
log.debug("ADDING NEXT ListItem: {0} - {1}", u, list_item)
|
||||
dir_items.append((u, list_item, True))
|
||||
if start_index + movie_page_limit < total_records:
|
||||
upper_count = start_index + (movie_page_limit * 2)
|
||||
if upper_count > total_records:
|
||||
upper_count = total_records
|
||||
list_item = xbmcgui.ListItem("Next Page (" + str(start_index + movie_page_limit + 1) + "-" +
|
||||
str(upper_count) + " of " + str(total_records) + ")")
|
||||
u = sys.argv[0] + "?url=" + quote(url_next) + "&mode=GET_CONTENT&media_type=movies"
|
||||
log.debug("ADDING NEXT ListItem: {0} - {1}".format(u, list_item))
|
||||
dir_items.append((u, list_item, True))
|
||||
# add paging items
|
||||
if show_page_limit > 0 and media_type.startswith("tvshow"):
|
||||
if url_prev:
|
||||
list_item = xbmcgui.ListItem("Prev Page (" + str(start_index - show_page_limit + 1) + "-" + str(start_index) +
|
||||
" of " + str(total_records) + ")")
|
||||
u = sys.argv[0] + "?url=" + quote(url_prev) + "&mode=GET_CONTENT&media_type=tvshows"
|
||||
log.debug("ADDING PREV ListItem: {0} - {1}".format(u, list_item))
|
||||
dir_items.insert(0, (u, list_item, True))
|
||||
|
||||
if start_index + show_page_limit < total_records:
|
||||
upper_count = start_index + (show_page_limit * 2)
|
||||
if upper_count > total_records:
|
||||
upper_count = total_records
|
||||
list_item = xbmcgui.ListItem("Next Page (" + str(start_index + show_page_limit + 1) + "-" +
|
||||
str(upper_count) + " of " + str(total_records) + ")")
|
||||
u = sys.argv[0] + "?url=" + quote(url_next) + "&mode=GET_CONTENT&media_type=tvshows"
|
||||
log.debug("ADDING NEXT ListItem: {0} - {1}".format(u, list_item))
|
||||
dir_items.append((u, list_item, True))
|
||||
|
||||
# set the Kodi content type
|
||||
if content_type:
|
||||
xbmcplugin.setContent(pluginhandle, content_type)
|
||||
elif detected_type is not None:
|
||||
# if the media type is not set then try to use the detected type
|
||||
log.debug("Detected content type: {0}", detected_type)
|
||||
log.debug("Detected content type: {0}".format(detected_type))
|
||||
if detected_type == "Movie":
|
||||
view_type = "Movies"
|
||||
content_type = 'movies'
|
||||
@@ -154,7 +191,9 @@ def get_content(url, params):
|
||||
xbmcplugin.setContent(pluginhandle, content_type)
|
||||
|
||||
# set the sort items
|
||||
if page_limit > 0 and media_type.startswith("movie"):
|
||||
if movie_page_limit > 0 and media_type.startswith("movie"):
|
||||
xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_UNSORTED)
|
||||
elif show_page_limit > 0 and media_type.startswith("tvshow"):
|
||||
xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_UNSORTED)
|
||||
else:
|
||||
set_sort(pluginhandle, view_type, default_sort)
|
||||
@@ -166,26 +205,21 @@ def get_content(url, params):
|
||||
view_key = "view-" + content_type
|
||||
view_id = settings.getSetting(view_key)
|
||||
if view_id:
|
||||
log.debug("Setting view for type:{0} to id:{1}", view_key, view_id)
|
||||
log.debug("Setting view for type:{0} to id:{1}".format(view_key, view_id))
|
||||
display_items_notification = {"view_id": view_id}
|
||||
send_event_notification("set_view", display_items_notification)
|
||||
else:
|
||||
log.debug("No view id for view type:{0}", view_key)
|
||||
|
||||
# send display items event
|
||||
# display_items_notification = {"view_type": view_type}
|
||||
# log.debug("Sending display_items with data {0}", display_items_notification)
|
||||
# send_event_notification("display_items", display_items_notification)
|
||||
log.debug("No view id for view type:{0}".format(view_key))
|
||||
|
||||
if progress is not None:
|
||||
progress.update(100, string_load(30125))
|
||||
progress.update(100, translate_string(30125))
|
||||
progress.close()
|
||||
|
||||
return
|
||||
|
||||
|
||||
def set_sort(pluginhandle, view_type, default_sort):
|
||||
log.debug("SETTING_SORT for media type: {0}", view_type)
|
||||
log.debug("SETTING_SORT for media type: {0}".format(view_type))
|
||||
|
||||
if default_sort == "none":
|
||||
xbmcplugin.addSortMethod(pluginhandle, xbmcplugin.SORT_METHOD_UNSORTED)
|
||||
@@ -202,7 +236,7 @@ def set_sort(pluginhandle, view_type, default_sort):
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
preset_sort_order = settings.getSetting("sort-" + view_type)
|
||||
log.debug("SETTING_SORT preset_sort_order: {0}", preset_sort_order)
|
||||
log.debug("SETTING_SORT preset_sort_order: {0}".format(preset_sort_order))
|
||||
if preset_sort_order in sorting_order_mapping:
|
||||
xbmcplugin.addSortMethod(pluginhandle, sorting_order_mapping[preset_sort_order])
|
||||
|
||||
@@ -231,13 +265,14 @@ def process_directory(url, progress, params, use_cache_data=False):
|
||||
|
||||
data_manager = DataManager()
|
||||
settings = xbmcaddon.Addon()
|
||||
download_utils = DownloadUtils()
|
||||
server = download_utils.get_server()
|
||||
server = settings.getSetting('server_address')
|
||||
user_details = load_user_details()
|
||||
user_id = user_details.get('user_id')
|
||||
|
||||
name_format = params.get("name_format", None)
|
||||
name_format_type = None
|
||||
if name_format is not None:
|
||||
name_format = urllib.unquote(name_format)
|
||||
name_format = unquote(name_format)
|
||||
tokens = name_format.split("|")
|
||||
if len(tokens) == 2:
|
||||
name_format_type = tokens[0]
|
||||
@@ -252,22 +287,34 @@ def process_directory(url, progress, params, use_cache_data=False):
|
||||
gui_options["name_format_type"] = name_format_type
|
||||
|
||||
use_cache = settings.getSetting("use_cache") == "true" and use_cache_data
|
||||
default_filters = get_default_filters()
|
||||
|
||||
# Fix skin shortcuts from pre-0.5.0
|
||||
item_limit = int(settings.getSetting("show_x_filtered_items"))
|
||||
url = url.replace('{server}', '')
|
||||
url = url.replace('{field_filters}', default_filters)
|
||||
url = url.replace('{ItemLimit}', str(item_limit))
|
||||
|
||||
# Need to replace at runtime so it always pulls the current user
|
||||
url = unquote(url)
|
||||
url = url.replace('{userid}', user_id)
|
||||
|
||||
cache_file, item_list, total_records, cache_thread = data_manager.get_items(url, gui_options, use_cache)
|
||||
|
||||
# flatten single season
|
||||
# if there is only one result and it is a season and you have flatten signle season turned on then
|
||||
# if there is only one result and it is a season and you have flatten single season turned on then
|
||||
# build a new url, set the content media type and call get content again
|
||||
flatten_single_season = settings.getSetting("flatten_single_season") == "true"
|
||||
if flatten_single_season and len(item_list) == 1 and item_list[0].item_type == "Season":
|
||||
season_id = item_list[0].id
|
||||
series_id = item_list[0].series_id
|
||||
season_url = ('{server}/Shows/' + series_id +
|
||||
season_url = ('/Shows/' + series_id +
|
||||
'/Episodes'
|
||||
'?userId={userid}' +
|
||||
'&seasonId=' + season_id +
|
||||
'&IsVirtualUnAired=false' +
|
||||
'&IsMissing=false' +
|
||||
'&Fields=SpecialEpisodeNumbers,{field_filters}' +
|
||||
'&Fields=SpecialEpisodeNumbers,{}'.format(default_filters) +
|
||||
'&format=json')
|
||||
if progress is not None:
|
||||
progress.close()
|
||||
@@ -294,14 +341,15 @@ def process_directory(url, progress, params, use_cache_data=False):
|
||||
|
||||
detected_type = None
|
||||
dir_items = []
|
||||
|
||||
OnlyTotallyUnwatchedTvShow = params.get("OnlyTotallyUnwatchedTvShow", None)
|
||||
for item_details in item_list:
|
||||
|
||||
if OnlyTotallyUnwatchedTvShow == "1" and item_details.watched_episodes > 0:
|
||||
continue
|
||||
item_details.total_items = item_count
|
||||
|
||||
if progress is not None:
|
||||
percent_done = (float(current_item) / float(item_count)) * 100
|
||||
progress.update(int(percent_done), string_load(30126) + str(current_item))
|
||||
progress.update(int(percent_done), translate_string(30126) + str(current_item))
|
||||
current_item = current_item + 1
|
||||
|
||||
if detected_type is not None:
|
||||
@@ -311,7 +359,7 @@ def process_directory(url, progress, params, use_cache_data=False):
|
||||
detected_type = item_details.item_type
|
||||
|
||||
if item_details.item_type == "Season" and first_season_item is None:
|
||||
log.debug("Setting First Season to : {0}", item_details.__dict__)
|
||||
log.debug("Setting First Season to : {0}".format(item_details.__dict__))
|
||||
first_season_item = item_details
|
||||
|
||||
total_unwatched += item_details.unwatched_episodes
|
||||
@@ -326,28 +374,30 @@ def process_directory(url, progress, params, use_cache_data=False):
|
||||
|
||||
if item_details.is_folder is True:
|
||||
if item_details.item_type == "Series":
|
||||
u = ('{server}/Shows/' + item_details.id +
|
||||
u = ('/Shows/' + item_details.id +
|
||||
'/Seasons'
|
||||
'?userId={userid}' +
|
||||
'&Fields={field_filters}' +
|
||||
'&Fields={}'.format(default_filters) +
|
||||
'&format=json')
|
||||
if not show_empty_folders:
|
||||
u = u + '&isMissing=False'
|
||||
|
||||
elif item_details.item_type == "Season":
|
||||
u = ('{server}/Shows/' + item_details.series_id +
|
||||
u = ('/Shows/' + item_details.series_id +
|
||||
'/Episodes'
|
||||
'?userId={userid}' +
|
||||
'&seasonId=' + item_details.id +
|
||||
'&IsVirtualUnAired=false' +
|
||||
'&IsMissing=false' +
|
||||
'&Fields=SpecialEpisodeNumbers,{field_filters}' +
|
||||
'&Fields=SpecialEpisodeNumbers,{}'.format(default_filters) +
|
||||
'&format=json')
|
||||
|
||||
else:
|
||||
u = ('{server}/Users/{userid}/items' +
|
||||
u = ('/Users/{userid}/items' +
|
||||
'?ParentId=' + item_details.id +
|
||||
'&IsVirtualUnAired=false' +
|
||||
'&IsMissing=false' +
|
||||
'&Fields={field_filters}' +
|
||||
'&Fields={}'.format(default_filters) +
|
||||
'&format=json')
|
||||
|
||||
default_sort = item_details.item_type == "Playlist"
|
||||
@@ -357,10 +407,10 @@ def process_directory(url, progress, params, use_cache_data=False):
|
||||
if gui_item:
|
||||
dir_items.append(gui_item)
|
||||
else:
|
||||
log.debug("Dropping empty folder item : {0}", item_details.__dict__)
|
||||
log.debug("Dropping empty folder item : {0}".format(item_details.__dict__))
|
||||
|
||||
elif item_details.item_type == "MusicArtist":
|
||||
u = ('{server}/Users/{userid}/items' +
|
||||
u = ('/Users/{userid}/items' +
|
||||
'?ArtistIds=' + item_details.id +
|
||||
'&IncludeItemTypes=MusicAlbum' +
|
||||
'&CollapseBoxSetItems=false' +
|
||||
@@ -382,13 +432,12 @@ def process_directory(url, progress, params, use_cache_data=False):
|
||||
and first_season_item is not None
|
||||
and len(dir_items) > 1
|
||||
and first_season_item.series_id is not None):
|
||||
series_url = ('{server}/Shows/' + first_season_item.series_id +
|
||||
series_url = ('/Shows/' + first_season_item.series_id +
|
||||
'/Episodes'
|
||||
'?userId={userid}' +
|
||||
# '&seasonId=' + season_id +
|
||||
'&IsVirtualUnAired=false' +
|
||||
'&IsMissing=false' +
|
||||
'&Fields=SpecialEpisodeNumbers,{field_filters}' +
|
||||
'&Fields=SpecialEpisodeNumbers,{}'.format(default_filters) +
|
||||
'&format=json')
|
||||
played = 0
|
||||
overlay = "7"
|
||||
@@ -399,7 +448,7 @@ def process_directory(url, progress, params, use_cache_data=False):
|
||||
item_details = ItemDetails()
|
||||
|
||||
item_details.id = first_season_item.id
|
||||
item_details.name = string_load(30290)
|
||||
item_details.name = translate_string(30290)
|
||||
item_details.art = first_season_item.art
|
||||
item_details.play_count = played
|
||||
item_details.overlay = overlay
|
||||
|
||||
@@ -1,839 +0,0 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
|
||||
import httplib
|
||||
import hashlib
|
||||
import ssl
|
||||
import StringIO
|
||||
import gzip
|
||||
import json
|
||||
from urlparse import urlparse
|
||||
import urllib
|
||||
from base64 import b64encode
|
||||
from collections import defaultdict
|
||||
|
||||
from .kodi_utils import HomeWindow
|
||||
from .clientinfo import ClientInformation
|
||||
from .simple_logging import SimpleLogging
|
||||
from .translation import string_load
|
||||
from .tracking import timer
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
|
||||
|
||||
def save_user_details(settings, user_name, user_password):
|
||||
save_user_to_settings = settings.getSetting("save_user_to_settings") == "true"
|
||||
if save_user_to_settings:
|
||||
settings.setSetting("username", user_name)
|
||||
settings.setSetting("password", user_password)
|
||||
else:
|
||||
settings.setSetting("username", "")
|
||||
settings.setSetting("password", "")
|
||||
home_window = HomeWindow()
|
||||
home_window.set_property("username", user_name)
|
||||
home_window.set_property("password", user_password)
|
||||
|
||||
|
||||
def load_user_details(settings):
|
||||
save_user_to_settings = settings.getSetting("save_user_to_settings") == "true"
|
||||
if save_user_to_settings:
|
||||
user_name = settings.getSetting("username")
|
||||
user_password = settings.getSetting("password")
|
||||
else:
|
||||
home_window = HomeWindow()
|
||||
user_name = home_window.get_property("username")
|
||||
user_password = home_window.get_property("password")
|
||||
|
||||
user_details = {}
|
||||
user_details["username"] = user_name
|
||||
user_details["password"] = user_password
|
||||
return user_details
|
||||
|
||||
|
||||
def get_details_string():
|
||||
|
||||
addon_settings = xbmcaddon.Addon()
|
||||
include_media = addon_settings.getSetting("include_media") == "true"
|
||||
include_people = addon_settings.getSetting("include_people") == "true"
|
||||
include_overview = addon_settings.getSetting("include_overview") == "true"
|
||||
|
||||
filer_list = [
|
||||
"DateCreated",
|
||||
"EpisodeCount",
|
||||
"SeasonCount",
|
||||
"Path",
|
||||
"Genres",
|
||||
"Studios",
|
||||
"Etag",
|
||||
"Taglines",
|
||||
"SortName",
|
||||
"RecursiveItemCount",
|
||||
"ChildCount",
|
||||
"ProductionLocations",
|
||||
"CriticRating",
|
||||
"OfficialRating",
|
||||
"CommunityRating",
|
||||
"PremiereDate",
|
||||
"ProductionYear",
|
||||
"AirTime",
|
||||
"Status",
|
||||
"Tags"
|
||||
]
|
||||
|
||||
if include_media:
|
||||
filer_list.append("MediaStreams")
|
||||
|
||||
if include_people:
|
||||
filer_list.append("People")
|
||||
|
||||
if include_overview:
|
||||
filer_list.append("Overview")
|
||||
|
||||
return ",".join(filer_list)
|
||||
|
||||
|
||||
class DownloadUtils:
|
||||
use_https = False
|
||||
verify_cert = False
|
||||
|
||||
def __init__(self, *args):
|
||||
settings = xbmcaddon.Addon()
|
||||
|
||||
self.use_https = False
|
||||
if settings.getSetting('protocol') == "1":
|
||||
self.use_https = True
|
||||
log.debug("use_https: {0}", self.use_https)
|
||||
|
||||
self.verify_cert = settings.getSetting('verify_cert') == 'true'
|
||||
log.debug("verify_cert: {0}", self.verify_cert)
|
||||
|
||||
def post_capabilities(self):
|
||||
|
||||
url = "{server}/Sessions/Capabilities/Full?format=json"
|
||||
data = {
|
||||
'IconUrl': "https://raw.githubusercontent.com/faush01/plugin.video.jellycon/develop/kodi.png",
|
||||
'SupportsMediaControl': True,
|
||||
'PlayableMediaTypes': ["Video", "Audio"],
|
||||
'SupportedCommands': ["MoveUp",
|
||||
"MoveDown",
|
||||
"MoveLeft",
|
||||
"MoveRight",
|
||||
"Select",
|
||||
"Back",
|
||||
"ToggleContextMenu",
|
||||
"ToggleFullscreen",
|
||||
"ToggleOsdMenu",
|
||||
"GoHome",
|
||||
"PageUp",
|
||||
"NextLetter",
|
||||
"GoToSearch",
|
||||
"GoToSettings",
|
||||
"PageDown",
|
||||
"PreviousLetter",
|
||||
"TakeScreenshot",
|
||||
"VolumeUp",
|
||||
"VolumeDown",
|
||||
"ToggleMute",
|
||||
"SendString",
|
||||
"DisplayMessage",
|
||||
"SetAudioStreamIndex",
|
||||
"SetSubtitleStreamIndex",
|
||||
"SetRepeatMode",
|
||||
"Mute",
|
||||
"Unmute",
|
||||
"SetVolume",
|
||||
"PlayNext",
|
||||
"Play",
|
||||
"Playstate",
|
||||
"PlayMediaSource"]
|
||||
}
|
||||
|
||||
self.download_url(url, post_body=data, method="POST")
|
||||
log.debug("Posted Capabilities: {0}", data)
|
||||
|
||||
def get_item_playback_info(self, item_id, force_transcode):
|
||||
|
||||
addon_settings = xbmcaddon.Addon()
|
||||
|
||||
# ["hevc", "h265", "h264", "mpeg4", "msmpeg4v3", "mpeg2video", "vc1"]
|
||||
filtered_codecs = []
|
||||
if addon_settings.getSetting("force_transcode_h265") == "true":
|
||||
filtered_codecs.append("hevc")
|
||||
filtered_codecs.append("h265")
|
||||
if addon_settings.getSetting("force_transcode_mpeg2") == "true":
|
||||
filtered_codecs.append("mpeg2video")
|
||||
if addon_settings.getSetting("force_transcode_msmpeg4v3") == "true":
|
||||
filtered_codecs.append("msmpeg4v3")
|
||||
if addon_settings.getSetting("force_transcode_mpeg4") == "true":
|
||||
filtered_codecs.append("mpeg4")
|
||||
|
||||
playback_bitrate = addon_settings.getSetting("max_stream_bitrate")
|
||||
force_playback_bitrate = addon_settings.getSetting("force_max_stream_bitrate")
|
||||
if force_transcode:
|
||||
playback_bitrate = force_playback_bitrate
|
||||
|
||||
audio_codec = addon_settings.getSetting("audio_codec")
|
||||
audio_playback_bitrate = addon_settings.getSetting("audio_playback_bitrate")
|
||||
audio_max_channels = addon_settings.getSetting("audio_max_channels")
|
||||
|
||||
audio_bitrate = int(audio_playback_bitrate) * 1000
|
||||
bitrate = int(playback_bitrate) * 1000
|
||||
|
||||
profile = {
|
||||
"Name": "Kodi",
|
||||
"MaxStaticBitrate": bitrate,
|
||||
"MaxStreamingBitrate": bitrate,
|
||||
"MusicStreamingTranscodingBitrate": audio_bitrate,
|
||||
"TimelineOffsetSeconds": 5,
|
||||
"TranscodingProfiles": [
|
||||
{
|
||||
"Type": "Audio"
|
||||
},
|
||||
{
|
||||
"Container": "ts",
|
||||
"Protocol": "hls",
|
||||
"Type": "Video",
|
||||
"AudioCodec": audio_codec,
|
||||
"VideoCodec": "h264",
|
||||
"MaxAudioChannels": audio_max_channels
|
||||
},
|
||||
{
|
||||
"Container": "jpeg",
|
||||
"Type": "Photo"
|
||||
}
|
||||
],
|
||||
"DirectPlayProfiles": [
|
||||
{
|
||||
"Type": "Video"
|
||||
},
|
||||
{
|
||||
"Type": "Audio"
|
||||
},
|
||||
{
|
||||
"Type": "Photo"
|
||||
}
|
||||
],
|
||||
"ResponseProfiles": [],
|
||||
"ContainerProfiles": [],
|
||||
"CodecProfiles": [],
|
||||
"SubtitleProfiles": [
|
||||
{
|
||||
"Format": "srt",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "srt",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "ass",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "ass",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "sub",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "sub",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "ssa",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "ssa",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "smi",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "smi",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "pgssub",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "pgssub",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "dvdsub",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "dvdsub",
|
||||
"Method": "External"
|
||||
},
|
||||
{
|
||||
"Format": "pgs",
|
||||
"Method": "Embed"
|
||||
},
|
||||
{
|
||||
"Format": "pgs",
|
||||
"Method": "External"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
if len(filtered_codecs) > 0:
|
||||
profile['DirectPlayProfiles'][0]['VideoCodec'] = "-%s" % ",".join(filtered_codecs)
|
||||
|
||||
if force_transcode:
|
||||
profile['DirectPlayProfiles'] = []
|
||||
|
||||
if addon_settings.getSetting("playback_video_force_8") == "true":
|
||||
profile['CodecProfiles'].append(
|
||||
{
|
||||
"Type": "Video",
|
||||
"Codec": "h264",
|
||||
"Conditions": [
|
||||
{
|
||||
"Condition": "LessThanEqual",
|
||||
"Property": "VideoBitDepth",
|
||||
"Value": "8",
|
||||
"IsRequired": False
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
profile['CodecProfiles'].append(
|
||||
{
|
||||
"Type": "Video",
|
||||
"Codec": "h265,hevc",
|
||||
"Conditions": [
|
||||
{
|
||||
"Condition": "EqualsAny",
|
||||
"Property": "VideoProfile",
|
||||
"Value": "main"
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
playback_info = {
|
||||
'UserId': self.get_user_id(),
|
||||
'DeviceProfile': profile,
|
||||
'AutoOpenLiveStream': True
|
||||
}
|
||||
|
||||
if force_transcode:
|
||||
url = "{server}/Items/%s/PlaybackInfo?MaxStreamingBitrate=%s&EnableDirectPlay=false&EnableDirectStream=false" % (item_id, bitrate)
|
||||
else:
|
||||
url = "{server}/Items/%s/PlaybackInfo?MaxStreamingBitrate=%s" % (item_id, bitrate)
|
||||
|
||||
log.debug("PlaybackInfo : {0}", url)
|
||||
log.debug("PlaybackInfo : {0}", profile)
|
||||
play_info_result = self.download_url(url, post_body=playback_info, method="POST")
|
||||
play_info_result = json.loads(play_info_result)
|
||||
log.debug("PlaybackInfo : {0}", play_info_result)
|
||||
|
||||
return play_info_result
|
||||
|
||||
def get_server(self):
|
||||
settings = xbmcaddon.Addon()
|
||||
host = settings.getSetting('ipaddress')
|
||||
|
||||
if len(host) == 0 or host == "<none>":
|
||||
return None
|
||||
|
||||
port = settings.getSetting('port')
|
||||
|
||||
if not port and self.use_https:
|
||||
port = "443"
|
||||
settings.setSetting("port", port)
|
||||
elif not port:
|
||||
port = "80"
|
||||
settings.setSetting("port", port)
|
||||
|
||||
# if user entered a full path i.e. http://some_host:port
|
||||
if host.lower().strip().startswith("http://") or host.lower().strip().startswith("https://"):
|
||||
log.debug("Extracting host info from url: {0}", host)
|
||||
url_bits = urlparse(host.strip())
|
||||
|
||||
if host.lower().strip().startswith("http://"):
|
||||
settings.setSetting('protocol', '0')
|
||||
self.use_https = False
|
||||
elif host.lower().strip().startswith("https://"):
|
||||
settings.setSetting('protocol', '1')
|
||||
self.use_https = True
|
||||
|
||||
if url_bits.hostname is not None and len(url_bits.hostname) > 0:
|
||||
host = url_bits.hostname
|
||||
|
||||
if url_bits.username and url_bits.password:
|
||||
host = "%s:%s@" % (url_bits.username, url_bits.password) + host
|
||||
|
||||
settings.setSetting("ipaddress", host)
|
||||
|
||||
if url_bits.port is not None and url_bits.port > 0:
|
||||
port = str(url_bits.port)
|
||||
settings.setSetting("port", port)
|
||||
|
||||
if self.use_https:
|
||||
server = "https://" + host + ":" + port
|
||||
else:
|
||||
server = "http://" + host + ":" + port
|
||||
|
||||
return server
|
||||
|
||||
@staticmethod
|
||||
def get_all_artwork(item, server):
|
||||
all_art = defaultdict(lambda: "")
|
||||
|
||||
item_id = item["Id"]
|
||||
item_type = item["Type"]
|
||||
image_tags = item["ImageTags"]
|
||||
# bg_item_tags = item["ParentBackdropImageTags"]
|
||||
|
||||
# All the image tags
|
||||
for tag_name in image_tags:
|
||||
tag = image_tags[tag_name]
|
||||
art_url = "%s/Items/%s/Images/%s/0?Format=original&Tag=%s" % (server, item_id, tag_name, tag)
|
||||
all_art[tag_name] = art_url
|
||||
|
||||
# Series images
|
||||
if item_type in ["Episode", "Season"]:
|
||||
image_tag = item["SeriesPrimaryImageTag"]
|
||||
series_id = item["SeriesId"]
|
||||
if image_tag and series_id:
|
||||
art_url = "%s/Items/%s/Images/Primary/0?Format=original&Tag=%s" % (server, series_id, image_tag)
|
||||
all_art["Primary.Series"] = art_url
|
||||
|
||||
return all_art
|
||||
|
||||
def get_artwork(self, data, art_type, parent=False, index=0, server=None):
|
||||
|
||||
item_id = data["Id"]
|
||||
item_type = data["Type"]
|
||||
|
||||
if item_type in ["Episode", "Season"]:
|
||||
if art_type != "Primary" or parent is True:
|
||||
item_id = data["SeriesId"]
|
||||
|
||||
image_tag = ""
|
||||
# "e3ab56fe27d389446754d0fb04910a34" # a place holder tag, needs to be in this format
|
||||
|
||||
# for episodes always use the parent BG
|
||||
if item_type == "Episode" and art_type == "Backdrop":
|
||||
item_id = data["ParentBackdropItemId"]
|
||||
bg_item_tags = data["ParentBackdropImageTags"]
|
||||
if bg_item_tags is not None and len(bg_item_tags) > 0:
|
||||
image_tag = bg_item_tags[0]
|
||||
elif art_type == "Backdrop" and parent is True:
|
||||
item_id = data["ParentBackdropItemId"]
|
||||
bg_item_tags = data["ParentBackdropImageTags"]
|
||||
if bg_item_tags is not None and len(bg_item_tags) > 0:
|
||||
image_tag = bg_item_tags[0]
|
||||
elif art_type == "Backdrop":
|
||||
bg_tags = data["BackdropImageTags"]
|
||||
if bg_tags is not None and len(bg_tags) > index:
|
||||
image_tag = bg_tags[index]
|
||||
# log.debug("Background Image Tag: {0}", imageTag)
|
||||
elif parent is False:
|
||||
image_tags = data["ImageTags"]
|
||||
if image_tags is not None:
|
||||
image_tag_type = image_tags[art_type]
|
||||
if image_tag_type is not None:
|
||||
image_tag = image_tag_type
|
||||
# log.debug("Image Tag: {0}", imageTag)
|
||||
elif parent is True:
|
||||
if (item_type == "Episode" or item_type == "Season") and art_type == 'Primary':
|
||||
tag_name = 'SeriesPrimaryImageTag'
|
||||
id_name = 'SeriesId'
|
||||
else:
|
||||
tag_name = 'Parent%sImageTag' % art_type
|
||||
id_name = 'Parent%sItemId' % art_type
|
||||
parent_image_id = data[id_name]
|
||||
parent_image_tag = data[tag_name]
|
||||
if parent_image_id is not None and parent_image_tag is not None:
|
||||
item_id = parent_image_id
|
||||
image_tag = parent_image_tag
|
||||
# log.debug("Parent Image Tag: {0}", imageTag)
|
||||
|
||||
# ParentTag not passed for Banner and Art
|
||||
if not image_tag and not ((art_type == 'Banner' or art_type == 'Art') and parent is True):
|
||||
# log.debug("No Image Tag for request:{0} item:{1} parent:{2}", art_type, item_type, parent)
|
||||
return ""
|
||||
|
||||
artwork = "%s/Items/%s/Images/%s/%s?Format=original&Tag=%s" % (server, item_id, art_type, index, image_tag)
|
||||
|
||||
if self.use_https and not self.verify_cert:
|
||||
artwork += "|verifypeer=false"
|
||||
|
||||
# log.debug("getArtwork: request:{0} item:{1} parent:{2} link:{3}", art_type, item_type, parent, artwork)
|
||||
|
||||
'''
|
||||
# do not return non-existing images
|
||||
if ( (art_type != "Backdrop" and imageTag == "") |
|
||||
(art_type == "Backdrop" and data.get("BackdropImageTags") != None and len(data.get("BackdropImageTags")) == 0) |
|
||||
(art_type == "Backdrop" and data.get("BackdropImageTag") != None and len(data.get("BackdropImageTag")) == 0)
|
||||
):
|
||||
artwork = ''
|
||||
'''
|
||||
|
||||
return artwork
|
||||
|
||||
def image_url(self, item_id, art_type, index, width, height, image_tag, server):
|
||||
|
||||
# test imageTag e3ab56fe27d389446754d0fb04910a34
|
||||
artwork = "%s/Items/%s/Images/%s/%s?Format=original&Tag=%s" % (server, item_id, art_type, index, image_tag)
|
||||
if int(width) > 0:
|
||||
artwork += '&MaxWidth=%s' % width
|
||||
if int(height) > 0:
|
||||
artwork += '&MaxHeight=%s' % height
|
||||
|
||||
if self.use_https and not self.verify_cert:
|
||||
artwork += "|verifypeer=false"
|
||||
|
||||
return artwork
|
||||
|
||||
def get_user_artwork(self, user, item_type):
|
||||
|
||||
if "PrimaryImageTag" not in user:
|
||||
return ""
|
||||
user_id = user.get("Id")
|
||||
tag = user.get("PrimaryImageTag")
|
||||
server = self.get_server()
|
||||
|
||||
artwork = "%s/Users/%s/Images/%s?Format=original&tag=%s" % (server, user_id, item_type, tag)
|
||||
|
||||
if self.use_https and not self.verify_cert:
|
||||
artwork += "|verifypeer=false"
|
||||
|
||||
return artwork
|
||||
|
||||
def get_user_id(self):
|
||||
|
||||
window = HomeWindow()
|
||||
userid = window.get_property("userid")
|
||||
user_image = window.get_property("userimage")
|
||||
|
||||
if userid and user_image:
|
||||
log.debug("JellyCon DownloadUtils -> Returning saved UserID: {0}", userid)
|
||||
return userid
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
user_details = load_user_details(settings)
|
||||
user_name = user_details.get("username", "")
|
||||
|
||||
if not user_name:
|
||||
return ""
|
||||
log.debug("Looking for user name: {0}", user_name)
|
||||
|
||||
try:
|
||||
json_data = self.download_url("{server}/Users/Public?format=json", suppress=True, authenticate=False)
|
||||
except Exception as msg:
|
||||
log.error("Get User unable to connect: {0}", msg)
|
||||
return ""
|
||||
|
||||
log.debug("GETUSER_JSONDATA_01: {0}", json_data)
|
||||
|
||||
try:
|
||||
result = json.loads(json_data)
|
||||
except Exception as e:
|
||||
log.debug("Could not load user data: {0}", e)
|
||||
return ""
|
||||
|
||||
if result is None:
|
||||
return ""
|
||||
|
||||
log.debug("GETUSER_JSONDATA_02: {0}", result)
|
||||
|
||||
secure = False
|
||||
for user in result:
|
||||
if user.get("Name") == unicode(user_name, "utf-8"):
|
||||
userid = user.get("Id")
|
||||
user_image = self.get_user_artwork(user, 'Primary')
|
||||
log.debug("Username Found: {0}", user.get("Name"))
|
||||
if user.get("HasPassword", False):
|
||||
secure = True
|
||||
log.debug("Username Is Secure (HasPassword=True)")
|
||||
break
|
||||
|
||||
if secure or not userid:
|
||||
auth_ok = self.authenticate()
|
||||
if auth_ok == "":
|
||||
xbmcgui.Dialog().notification(string_load(30316),
|
||||
string_load(30044),
|
||||
icon="special://home/addons/plugin.video.jellycon/icon.png")
|
||||
return ""
|
||||
if not userid:
|
||||
userid = window.get_property("userid")
|
||||
|
||||
if userid and not user_image:
|
||||
user_image = 'DefaultUser.png'
|
||||
|
||||
if userid == "":
|
||||
xbmcgui.Dialog().notification(string_load(30316),
|
||||
string_load(30045),
|
||||
icon="special://home/addons/plugin.video.jellycon/icon.png")
|
||||
|
||||
log.debug("userid: {0}", userid)
|
||||
|
||||
window.set_property("userid", userid)
|
||||
window.set_property("userimage", user_image)
|
||||
|
||||
return userid
|
||||
|
||||
def authenticate(self):
|
||||
|
||||
window = HomeWindow()
|
||||
|
||||
token = window.get_property("AccessToken")
|
||||
if token is not None and token != "":
|
||||
log.debug("JellyCon DownloadUtils -> Returning saved AccessToken: {0}", token)
|
||||
return token
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
port = settings.getSetting("port")
|
||||
host = settings.getSetting("ipaddress")
|
||||
if host is None or host == "" or port is None or port == "":
|
||||
return ""
|
||||
|
||||
url = "{server}/Users/AuthenticateByName?format=json"
|
||||
|
||||
user_details = load_user_details(settings)
|
||||
user_name = urllib.quote(user_details.get("username", ""))
|
||||
pwd_text = urllib.quote(user_details.get("password", ""))
|
||||
|
||||
message_data = "username=" + user_name + "&pw=" + pwd_text
|
||||
|
||||
resp = self.download_url(url, post_body=message_data, method="POST", suppress=True, authenticate=False)
|
||||
log.debug("AuthenticateByName: {0}", resp)
|
||||
|
||||
access_token = None
|
||||
userid = None
|
||||
try:
|
||||
result = json.loads(resp)
|
||||
access_token = result.get("AccessToken")
|
||||
# userid = result["SessionInfo"].get("UserId")
|
||||
userid = result["User"].get("Id")
|
||||
except:
|
||||
pass
|
||||
|
||||
if access_token is not None:
|
||||
log.debug("User Authenticated: {0}", access_token)
|
||||
log.debug("User Id: {0}", userid)
|
||||
window.set_property("AccessToken", access_token)
|
||||
window.set_property("userid", userid)
|
||||
# WINDOW.setProperty("userimage", "")
|
||||
|
||||
self.post_capabilities()
|
||||
|
||||
return access_token
|
||||
else:
|
||||
log.debug("User NOT Authenticated")
|
||||
window.set_property("AccessToken", "")
|
||||
window.set_property("userid", "")
|
||||
window.set_property("userimage", "")
|
||||
return ""
|
||||
|
||||
def get_auth_header(self, authenticate=True):
|
||||
client_info = ClientInformation()
|
||||
txt_mac = client_info.get_device_id()
|
||||
version = client_info.get_version()
|
||||
client = client_info.get_client()
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
device_name = settings.getSetting('deviceName')
|
||||
# remove none ascii chars
|
||||
device_name = device_name.decode("ascii", errors='ignore')
|
||||
# remove some chars not valid for names
|
||||
device_name = device_name.replace("\"", "_")
|
||||
if len(device_name) == 0:
|
||||
device_name = "JellyCon"
|
||||
|
||||
headers = {}
|
||||
headers["Accept-encoding"] = "gzip"
|
||||
headers["Accept-Charset"] = "UTF-8,*"
|
||||
|
||||
if authenticate is False:
|
||||
auth_string = "MediaBrowser Client=\"" + client + "\",Device=\"" + device_name + "\",DeviceId=\"" + txt_mac + "\",Version=\"" + version + "\""
|
||||
# headers["Authorization"] = authString
|
||||
headers['X-Emby-Authorization'] = auth_string
|
||||
return headers
|
||||
else:
|
||||
userid = self.get_user_id()
|
||||
auth_string = "MediaBrowser UserId=\"" + userid + "\",Client=\"" + client + "\",Device=\"" + device_name + "\",DeviceId=\"" + txt_mac + "\",Version=\"" + version + "\""
|
||||
# headers["Authorization"] = authString
|
||||
headers['X-Emby-Authorization'] = auth_string
|
||||
|
||||
auth_token = self.authenticate()
|
||||
if auth_token != "":
|
||||
headers["X-MediaBrowser-Token"] = auth_token
|
||||
|
||||
log.debug("JellyCon Authentication Header: {0}", headers)
|
||||
return headers
|
||||
|
||||
@timer
|
||||
def download_url(self, url, suppress=False, post_body=None, method="GET", authenticate=True, headers=None):
|
||||
log.debug("downloadUrl")
|
||||
|
||||
return_data = "null"
|
||||
settings = xbmcaddon.Addon()
|
||||
user_details = load_user_details(settings)
|
||||
username = user_details.get("username", "")
|
||||
server = None
|
||||
|
||||
http_timeout = int(settings.getSetting("http_timeout"))
|
||||
|
||||
if authenticate and username == "":
|
||||
return return_data
|
||||
|
||||
if settings.getSetting("suppressErrors") == "true":
|
||||
suppress = True
|
||||
|
||||
log.debug("Before: {0}", url)
|
||||
|
||||
if url.find("{server}") != -1:
|
||||
server = self.get_server()
|
||||
if server is None:
|
||||
return return_data
|
||||
url = url.replace("{server}", server)
|
||||
|
||||
if url.find("{userid}") != -1:
|
||||
userid = self.get_user_id()
|
||||
if not userid:
|
||||
return return_data
|
||||
url = url.replace("{userid}", userid)
|
||||
|
||||
if url.find("{ItemLimit}") != -1:
|
||||
show_x_filtered_items = settings.getSetting("show_x_filtered_items")
|
||||
url = url.replace("{ItemLimit}", show_x_filtered_items)
|
||||
|
||||
if url.find("{field_filters}") != -1:
|
||||
filter_string = get_details_string()
|
||||
url = url.replace("{field_filters}", filter_string)
|
||||
|
||||
if url.find("{random_movies}") != -1:
|
||||
home_window = HomeWindow()
|
||||
random_movies = home_window.get_property("random-movies")
|
||||
if not random_movies:
|
||||
return return_data
|
||||
url = url.replace("{random_movies}", random_movies)
|
||||
|
||||
log.debug("After: {0}", url)
|
||||
conn = None
|
||||
|
||||
try:
|
||||
|
||||
url_bits = urlparse(url.strip())
|
||||
|
||||
protocol = url_bits.scheme
|
||||
host_name = url_bits.hostname
|
||||
port = url_bits.port
|
||||
user_name = url_bits.username
|
||||
user_password = url_bits.password
|
||||
url_path = url_bits.path
|
||||
url_puery = url_bits.query
|
||||
|
||||
if not host_name or host_name == "<none>":
|
||||
return return_data
|
||||
|
||||
local_use_https = False
|
||||
if protocol.lower() == "https":
|
||||
local_use_https = True
|
||||
|
||||
server = "%s:%s" % (host_name, port)
|
||||
url_path = url_path + "?" + url_puery
|
||||
|
||||
if local_use_https and self.verify_cert:
|
||||
log.debug("Connection: HTTPS, Cert checked")
|
||||
conn = httplib.HTTPSConnection(server, timeout=http_timeout)
|
||||
elif local_use_https and not self.verify_cert:
|
||||
log.debug("Connection: HTTPS, Cert NOT checked")
|
||||
conn = httplib.HTTPSConnection(server, timeout=http_timeout, context=ssl._create_unverified_context())
|
||||
else:
|
||||
log.debug("Connection: HTTP")
|
||||
conn = httplib.HTTPConnection(server, timeout=http_timeout)
|
||||
|
||||
head = self.get_auth_header(authenticate)
|
||||
|
||||
if user_name and user_password:
|
||||
# add basic auth headers
|
||||
user_and_pass = b64encode(b"%s:%s" % (user_name, user_password)).decode("ascii")
|
||||
head["Authorization"] = 'Basic %s' % user_and_pass
|
||||
|
||||
head["User-Agent"] = "JellyCon-" + ClientInformation().get_version()
|
||||
log.debug("HEADERS: {0}", head)
|
||||
|
||||
if post_body is not None:
|
||||
if isinstance(post_body, dict):
|
||||
content_type = "application/json"
|
||||
post_body = json.dumps(post_body)
|
||||
else:
|
||||
content_type = "application/x-www-form-urlencoded"
|
||||
|
||||
head["Content-Type"] = content_type
|
||||
log.debug("Content-Type: {0}", content_type)
|
||||
|
||||
log.debug("POST DATA: {0}", post_body)
|
||||
conn.request(method=method, url=url_path, body=post_body, headers=head)
|
||||
else:
|
||||
conn.request(method=method, url=url_path, headers=head)
|
||||
|
||||
data = conn.getresponse()
|
||||
log.debug("HTTP response: {0} {1}", data.status, data.reason)
|
||||
log.debug("GET URL HEADERS: {0}", data.getheaders())
|
||||
|
||||
if int(data.status) == 200:
|
||||
ret_data = data.read()
|
||||
content_type = data.getheader('content-encoding')
|
||||
log.debug("Data Len Before: {0}", len(ret_data))
|
||||
if content_type == "gzip":
|
||||
ret_data = StringIO.StringIO(ret_data)
|
||||
gzipper = gzip.GzipFile(fileobj=ret_data)
|
||||
return_data = gzipper.read()
|
||||
else:
|
||||
return_data = ret_data
|
||||
if headers is not None and isinstance(headers, dict):
|
||||
headers.update(data.getheaders())
|
||||
log.debug("Data Len After: {0}", len(return_data))
|
||||
log.debug("====== 200 returned =======")
|
||||
log.debug("Content-Type: {0}", content_type)
|
||||
log.debug("{0}", return_data)
|
||||
log.debug("====== 200 finished ======")
|
||||
|
||||
elif int(data.status) >= 400:
|
||||
|
||||
if int(data.status) == 401:
|
||||
# remove any saved password
|
||||
m = hashlib.md5()
|
||||
m.update(username)
|
||||
hashed_username = m.hexdigest()
|
||||
log.error("HTTP response error 401 auth error, removing any saved passwords for user: {0}", hashed_username)
|
||||
settings.setSetting("saved_user_password_" + hashed_username, "")
|
||||
save_user_details(settings, "", "")
|
||||
|
||||
log.error("HTTP response error: {0} {1}", data.status, data.reason)
|
||||
if suppress is False:
|
||||
xbmcgui.Dialog().notification(string_load(30316),
|
||||
string_load(30200) % str(data.reason),
|
||||
icon="special://home/addons/plugin.video.jellycon/icon.png")
|
||||
|
||||
except Exception as msg:
|
||||
log.error("Unable to connect to {0} : {1}", server, msg)
|
||||
if suppress is False:
|
||||
xbmcgui.Dialog().notification(string_load(30316),
|
||||
str(msg),
|
||||
icon="special://home/addons/plugin.video.jellycon/icon.png")
|
||||
|
||||
finally:
|
||||
try:
|
||||
log.debug("Closing HTTP connection: {0}", conn)
|
||||
conn.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
return return_data
|
||||
@@ -90,9 +90,6 @@ import sys
|
||||
import time
|
||||
import errno
|
||||
|
||||
# from .simple_logging import SimpleLogging
|
||||
# log = SimpleLogging(__name__)
|
||||
|
||||
|
||||
class FileLock(object):
|
||||
""" A file locking mechanism that has context-manager support so
|
||||
@@ -201,7 +198,7 @@ if __name__ == "__main__":
|
||||
import threading
|
||||
import tempfile
|
||||
from builtins import range
|
||||
|
||||
|
||||
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
protected_filepath = os.path.join(temp_dir, "somefile.txt")
|
||||
@@ -236,4 +233,4 @@ if __name__ == "__main__":
|
||||
# Please manually inspect the output. Does it look like the operations were atomic?
|
||||
with open(protected_filepath, "r") as f:
|
||||
sys.stdout.write(f.read())
|
||||
"""
|
||||
"""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,49 +1,44 @@
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import threading
|
||||
import io
|
||||
import base64
|
||||
import re
|
||||
from random import shuffle
|
||||
|
||||
import xbmcvfs
|
||||
import xbmc
|
||||
import base64
|
||||
import re
|
||||
from urlparse import urlparse
|
||||
from random import shuffle
|
||||
import xbmcaddon
|
||||
import requests
|
||||
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
from six.moves.urllib.parse import urlparse
|
||||
from six import ensure_text
|
||||
|
||||
import threading
|
||||
import httplib
|
||||
import io
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from .datamanager import DataManager
|
||||
from .downloadutils import DownloadUtils
|
||||
from .utils import get_art
|
||||
from .jellyfin import api
|
||||
from .lazylogger import LazyLogger
|
||||
from .item_functions import get_art
|
||||
from .utils import translate_path
|
||||
|
||||
pil_loaded = False
|
||||
try:
|
||||
from PIL import ImageFilter, Image, ImageOps
|
||||
from PIL import Image, ImageOps
|
||||
pil_loaded = True
|
||||
except Exception as err:
|
||||
except ImportError:
|
||||
pil_loaded = False
|
||||
|
||||
PORT_NUMBER = 24276
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
def get_image_links(url):
|
||||
|
||||
download_utils = DownloadUtils()
|
||||
server = download_utils.get_server()
|
||||
settings = xbmcaddon.Addon()
|
||||
server = settings.getSetting('server_address')
|
||||
if server is None:
|
||||
return []
|
||||
|
||||
# url = re.sub("(?i)limit=[0-9]+", "limit=4", url)
|
||||
# url = url.replace("{ItemLimit}", "4")
|
||||
# url = re.sub("(?i)SortBy=[a-zA-Z]+", "SortBy=Random", url)
|
||||
|
||||
# if not re.search('limit=', url, re.IGNORECASE):
|
||||
# url += "&Limit=4"
|
||||
|
||||
# if not re.search('sortBy=', url, re.IGNORECASE):
|
||||
# url += "&SortBy=Random"
|
||||
|
||||
url = re.sub("(?i)EnableUserData=[a-z]+", "EnableUserData=False", url)
|
||||
url = re.sub("(?i)EnableImageTypes=[,a-z]+", "EnableImageTypes=Primary", url)
|
||||
url = url.replace("{field_filters}", "BasicSyncInfo")
|
||||
@@ -58,8 +53,7 @@ def get_image_links(url):
|
||||
if not re.search('EnableUserData=', url, re.IGNORECASE):
|
||||
url += "&EnableUserData=False"
|
||||
|
||||
data_manager = DataManager()
|
||||
result = data_manager.get_content(url)
|
||||
result = api.get(url)
|
||||
|
||||
items = result.get("Items")
|
||||
if not items:
|
||||
@@ -78,15 +72,15 @@ def get_image_links(url):
|
||||
def build_image(path):
|
||||
log.debug("build_image()")
|
||||
|
||||
log.debug("Request Path : {0}", path)
|
||||
log.debug("Request Path : {0}".format(path))
|
||||
|
||||
request_path = path[1:]
|
||||
|
||||
if request_path == "favicon.ico":
|
||||
return []
|
||||
|
||||
decoded_url = base64.b64decode(request_path)
|
||||
log.debug("decoded_url : {0}", decoded_url)
|
||||
decoded_url = ensure_text(base64.b64decode(request_path))
|
||||
log.debug("decoded_url : {0}".format(decoded_url))
|
||||
|
||||
image_urls = get_image_links(decoded_url)
|
||||
|
||||
@@ -108,21 +102,17 @@ def build_image(path):
|
||||
|
||||
host_name = url_bits.hostname
|
||||
port = url_bits.port
|
||||
# user_name = url_bits.username
|
||||
# user_password = url_bits.password
|
||||
url_path = url_bits.path
|
||||
url_query = url_bits.query
|
||||
|
||||
server = "%s:%s" % (host_name, port)
|
||||
url_full_path = url_path + "?" + url_query
|
||||
|
||||
log.debug("Loading image from : {0} {1} {2}", image_count, server, url_full_path)
|
||||
log.debug("Loading image from : {0} {1} {2}".format(image_count, server, url_full_path))
|
||||
|
||||
try:
|
||||
conn = httplib.HTTPConnection(server)
|
||||
conn.request("GET", url_full_path)
|
||||
image_responce = conn.getresponse()
|
||||
image_data = image_responce.read()
|
||||
image_response = requests.get(thumb_url)
|
||||
image_data = image_response.content
|
||||
|
||||
loaded_image = Image.open(io.BytesIO(image_data))
|
||||
image = ImageOps.fit(loaded_image, size, method=Image.ANTIALIAS, bleed=0.0, centering=(0.5, 0.5))
|
||||
@@ -136,7 +126,7 @@ def build_image(path):
|
||||
del image_data
|
||||
|
||||
except Exception as con_err:
|
||||
log.debug("Error loading image : {0}", str(con_err))
|
||||
log.debug("Error loading image : {0}".format(con_err))
|
||||
|
||||
image_count += 1
|
||||
|
||||
@@ -170,12 +160,6 @@ class HttpImageHandler(BaseHTTPRequestHandler):
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
def do_QUIT(self):
|
||||
log.debug("HttpImageHandler:do_QUIT()")
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
return
|
||||
|
||||
def serve_image(self):
|
||||
|
||||
if pil_loaded:
|
||||
@@ -189,7 +173,7 @@ class HttpImageHandler(BaseHTTPRequestHandler):
|
||||
|
||||
else:
|
||||
|
||||
image_path = xbmc.translatePath("special://home/addons/plugin.video.jellycon/icon.png").decode('utf-8')
|
||||
image_path = translate_path("special://home/addons/plugin.video.jellycon/icon.png").decode('utf-8')
|
||||
self.send_response(200)
|
||||
self.send_header('Content-type', 'image/png')
|
||||
modified = xbmcvfs.Stat(image_path).st_mtime()
|
||||
@@ -205,26 +189,21 @@ class HttpImageHandler(BaseHTTPRequestHandler):
|
||||
|
||||
class HttpImageServerThread(threading.Thread):
|
||||
|
||||
keep_running = True
|
||||
|
||||
def __init__(self):
|
||||
threading.Thread.__init__(self)
|
||||
self.keep_running = True
|
||||
|
||||
def stop(self):
|
||||
self.keep_running = False
|
||||
log.debug("HttpImageServerThread:stop called")
|
||||
try:
|
||||
conn = httplib.HTTPConnection("localhost:%d" % PORT_NUMBER)
|
||||
conn.request("QUIT", "/")
|
||||
conn.getresponse()
|
||||
except:
|
||||
pass
|
||||
self.keep_running = False
|
||||
self.server.shutdown()
|
||||
|
||||
def run(self):
|
||||
log.debug("HttpImageServerThread:started")
|
||||
server = HTTPServer(('', PORT_NUMBER), HttpImageHandler)
|
||||
self.server = HTTPServer(('', PORT_NUMBER), HttpImageHandler)
|
||||
|
||||
while self.keep_running:
|
||||
server.handle_request()
|
||||
self.server.serve_forever()
|
||||
xbmc.sleep(1000)
|
||||
|
||||
log.debug("HttpImageServerThread:exiting")
|
||||
|
||||
158
resources/lib/intro_skipper.py
Normal file
158
resources/lib/intro_skipper.py
Normal file
@@ -0,0 +1,158 @@
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import os
|
||||
import threading
|
||||
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
import xbmcgui
|
||||
|
||||
from resources.lib.play_utils import get_media_segments
|
||||
from resources.lib.utils import seconds_to_ticks, ticks_to_seconds, translate_path
|
||||
from resources.lib.intro_skipper_utils import get_setting_skip_action, set_correct_skip_info
|
||||
|
||||
|
||||
from .lazylogger import LazyLogger
|
||||
from .dialogs import SkipDialog
|
||||
|
||||
from typing import Literal
|
||||
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class IntroSkipperService(threading.Thread):
|
||||
|
||||
stop_thread = False
|
||||
monitor = None
|
||||
|
||||
def __init__(self, play_monitor):
|
||||
super(IntroSkipperService, self).__init__()
|
||||
self.monitor = play_monitor
|
||||
|
||||
def run(self):
|
||||
|
||||
from .play_utils import get_jellyfin_playing_item
|
||||
settings = xbmcaddon.Addon()
|
||||
plugin_path = settings.getAddonInfo('path')
|
||||
plugin_path_real = translate_path(os.path.join(plugin_path))
|
||||
|
||||
skip_intro_dialog = None
|
||||
skip_credit_dialog = None
|
||||
skip_commercial_dialog = None
|
||||
skip_preview_dialog = None
|
||||
skip_recap_dialog = None
|
||||
|
||||
segments = None
|
||||
playing_item_id = None
|
||||
|
||||
log.debug("SkipService: starting service")
|
||||
|
||||
while not xbmc.Monitor().abortRequested() and not self.stop_thread:
|
||||
player = xbmc.Player()
|
||||
if player.isPlaying():
|
||||
item_id = get_jellyfin_playing_item()
|
||||
if item_id is not None:
|
||||
log.debug("SkipService: playing item is from jellyfin : {0}".format(item_id))
|
||||
|
||||
# If item id has changed or is new, retrieve segments
|
||||
if playing_item_id is None or playing_item_id != item_id :
|
||||
log.debug("SkipService: item is new, retrieving media segments : {0}".format(item_id))
|
||||
segments = get_media_segments(item_id)
|
||||
|
||||
# Setting global playing item to current playing item
|
||||
playing_item_id = item_id
|
||||
|
||||
# Handle skip only on jellyfin items
|
||||
current_ticks = seconds_to_ticks(player.getTime())
|
||||
|
||||
# Handle Intros
|
||||
skip_intro_dialog = self.handle_dialog(plugin_path_real, skip_intro_dialog, item_id, current_ticks, player, segments, "Intro")
|
||||
# Handle Credits
|
||||
skip_credit_dialog = self.handle_dialog(plugin_path_real, skip_credit_dialog, item_id, current_ticks, player, segments, "Outro")
|
||||
# Handle commercial
|
||||
skip_commercial_dialog = self.handle_dialog(plugin_path_real, skip_commercial_dialog, item_id, current_ticks, player, segments, "Commercial")
|
||||
# Handle preview
|
||||
skip_preview_dialog = self.handle_dialog(plugin_path_real, skip_preview_dialog, item_id, current_ticks, player, segments, "Preview")
|
||||
# Handle recap
|
||||
skip_recap_dialog = self.handle_dialog(plugin_path_real, skip_recap_dialog, item_id, current_ticks, player, segments, "Recap")
|
||||
|
||||
else:
|
||||
playing_item_id = None
|
||||
if skip_intro_dialog is not None:
|
||||
log.debug("SkipService: Playback stopped, killing Intro dialog")
|
||||
skip_intro_dialog.close()
|
||||
skip_intro_dialog = None
|
||||
|
||||
if skip_credit_dialog is not None:
|
||||
log.debug("SkipService: Playback stopped, killing Outro dialog")
|
||||
skip_credit_dialog.close()
|
||||
skip_credit_dialog = None
|
||||
|
||||
if skip_commercial_dialog is not None:
|
||||
log.debug("SkipService: Playback stopped, killing Commercial dialog")
|
||||
skip_commercial_dialog.close()
|
||||
skip_commercial_dialog = None
|
||||
|
||||
if skip_preview_dialog is not None:
|
||||
log.debug("SkipService: Playback stopped, killing Preview dialog")
|
||||
skip_preview_dialog.close()
|
||||
skip_preview_dialog = None
|
||||
|
||||
if skip_recap_dialog is not None:
|
||||
log.debug("SkipService: Playback stopped, killing Recap dialog")
|
||||
skip_recap_dialog.close()
|
||||
skip_recap_dialog = None
|
||||
|
||||
if xbmc.Monitor().waitForAbort(1):
|
||||
break
|
||||
|
||||
xbmc.sleep(200)
|
||||
|
||||
|
||||
def handle_dialog(self, plugin_path_real: str, dialog: SkipDialog, item_id: str, current_ticks: float, player: xbmc.Player, segments, type: Literal["Commercial", "Preview", "Recap", "Outro", "Intro"]):
|
||||
skip_action = get_setting_skip_action(type)
|
||||
|
||||
# In case do nothing is selected return
|
||||
if skip_action == "2":
|
||||
log.debug("SkipService: ignore {0} is selected".format(type))
|
||||
return None
|
||||
|
||||
if dialog is None:
|
||||
log.debug("SkipService: init dialog")
|
||||
dialog = SkipDialog("SkipDialog.xml", plugin_path_real, "default", "720p")
|
||||
|
||||
set_correct_skip_info(item_id, dialog, segments, type)
|
||||
|
||||
is_segment = False
|
||||
if dialog.start is not None and dialog.end is not None:
|
||||
# Resets the dismiss var so that button can reappear in case of navigation in the timecodes
|
||||
if (current_ticks < dialog.start or current_ticks > dialog.end) and dialog.has_been_dissmissed is True:
|
||||
log.debug("SkipService: {0} skip was dismissed. It is reset beacause timecode is outside of segment")
|
||||
dialog.has_been_dissmissed = False
|
||||
|
||||
# Checks if segment is playing
|
||||
is_segment = current_ticks >= dialog.start and current_ticks <= dialog.end
|
||||
|
||||
if skip_action == "1" and is_segment:
|
||||
log.debug("SkipService: {0} is set to automatic skip, skipping segment".format(type))
|
||||
# If auto skip is enabled, skips to semgent ends automatically
|
||||
player.seekTime(ticks_to_seconds(dialog.end))
|
||||
xbmcgui.Dialog().notification("JellyCon", "{0} Skipped".format(type))
|
||||
elif skip_action == "0":
|
||||
# Otherwise show skip dialog
|
||||
if is_segment and not dialog.has_been_dissmissed:
|
||||
log.debug("SkipService: {0} is playing, showing dialog".format(type))
|
||||
dialog.show()
|
||||
else:
|
||||
# Could not find doc on what happens when closing a closed dialog, but it seems fine
|
||||
log.debug("SkipService: {0} is not playing, closing dialog".format(type))
|
||||
dialog.close()
|
||||
|
||||
return dialog
|
||||
|
||||
|
||||
def stop_service(self):
|
||||
log.debug("IntroSkipperService Stop Called")
|
||||
self.stop_thread = True
|
||||
72
resources/lib/intro_skipper_utils.py
Normal file
72
resources/lib/intro_skipper_utils.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from typing import Literal
|
||||
|
||||
import xbmcaddon
|
||||
|
||||
from .lazylogger import LazyLogger
|
||||
from .dialogs import SkipDialog
|
||||
|
||||
from .utils import seconds_to_ticks
|
||||
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
def get_setting_skip_action(type: Literal["Commercial", "Preview", "Recap", "Outro", "Intro"]):
|
||||
settings = xbmcaddon.Addon()
|
||||
if (type == "Commercial"):
|
||||
return settings.getSetting("commercial_skipper_action")
|
||||
elif (type == "Preview"):
|
||||
return settings.getSetting("preview_skipper_action")
|
||||
elif (type == "Recap"):
|
||||
return settings.getSetting("recap_skipper_action")
|
||||
elif (type == "Outro"):
|
||||
return settings.getSetting("credit_skipper_action")
|
||||
elif (type == "Intro"):
|
||||
return settings.getSetting("intro_skipper_action")
|
||||
return ""
|
||||
|
||||
def get_setting_skip_start_offset(type: Literal["Commercial", "Preview", "Recap", "Outro", "Intro"]):
|
||||
settings = xbmcaddon.Addon()
|
||||
if (type == "Commercial"):
|
||||
return settings.getSettingInt("commercial_skipper_start_offset")
|
||||
elif (type == "Preview"):
|
||||
return settings.getSettingInt("preview_skipper_start_offset")
|
||||
elif (type == "Recap"):
|
||||
return settings.getSettingInt("recap_skipper_start_offset")
|
||||
elif (type == "Outro"):
|
||||
return settings.getSettingInt("credit_skipper_start_offset")
|
||||
elif (type == "Intro"):
|
||||
return settings.getSettingInt("intro_skipper_start_offset")
|
||||
return 0
|
||||
|
||||
def get_setting_skip_end_offset(type: Literal["Commercial", "Preview", "Recap", "Outro", "Intro"]):
|
||||
settings = xbmcaddon.Addon()
|
||||
if (type == "Commercial"):
|
||||
return settings.getSettingInt("commercial_skipper_end_offset")
|
||||
elif (type == "Preview"):
|
||||
return settings.getSettingInt("preview_skipper_end_offset")
|
||||
elif (type == "Recap"):
|
||||
return settings.getSettingInt("recap_skipper_end_offset")
|
||||
elif (type == "Outro"):
|
||||
return settings.getSettingInt("credit_skipper_end_offset")
|
||||
elif (type == "Intro"):
|
||||
return settings.getSettingInt("intro_skipper_end_offset")
|
||||
return 0
|
||||
|
||||
def set_correct_skip_info(item_id: str, skip_dialog: SkipDialog, segments, type: Literal["Commercial", "Preview", "Recap", "Outro", "Intro"]):
|
||||
if (skip_dialog.media_id is None or skip_dialog.media_id != item_id) and item_id is not None:
|
||||
# If playback item has changed (or is new), sets its id and set media segments info
|
||||
log.debug("SkipDialogInfo : Media Id has changed to {0}, setting segments".format(item_id))
|
||||
skip_dialog.media_id = item_id
|
||||
skip_dialog.has_been_dissmissed = False
|
||||
if segments is not None:
|
||||
# Find the intro and outro timings
|
||||
start = next((segment["StartTicks"] for segment in segments if segment["Type"] == type), None)
|
||||
end = next((segment["EndTicks"] for segment in segments if segment["Type"] == type), None)
|
||||
|
||||
# Sets timings with offsets if defined in settings
|
||||
if start is not None:
|
||||
skip_dialog.start = start + seconds_to_ticks(get_setting_skip_start_offset(type))
|
||||
log.debug("SkipDialogInfo : Setting {0} start to {1}".format(type, skip_dialog.start))
|
||||
if end is not None:
|
||||
skip_dialog.end = end - seconds_to_ticks(get_setting_skip_end_offset(type))
|
||||
log.debug("SkipDialogInfo : Setting {0} end to {1}".format(type, skip_dialog.end))
|
||||
|
||||
@@ -1,30 +1,20 @@
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import sys
|
||||
import os
|
||||
import urllib
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
from dateutil import tz
|
||||
from six import ensure_text
|
||||
from six.moves.urllib.parse import quote
|
||||
import xbmcgui
|
||||
|
||||
from .utils import get_art, datetime_from_string
|
||||
from .simple_logging import SimpleLogging
|
||||
from .downloadutils import DownloadUtils
|
||||
from .kodi_utils import HomeWindow
|
||||
from .utils import (
|
||||
datetime_from_string, get_art_url, image_url, get_current_datetime
|
||||
)
|
||||
from .lazylogger import LazyLogger
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
|
||||
|
||||
addon_instance = xbmcaddon.Addon()
|
||||
addon_path = addon_instance.getAddonInfo('path')
|
||||
PLUGINPATH = xbmc.translatePath(os.path.join(addon_path))
|
||||
|
||||
download_utils = DownloadUtils()
|
||||
home_window = HomeWindow()
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class ItemDetails:
|
||||
@@ -100,27 +90,27 @@ def extract_item_info(item, gui_options):
|
||||
|
||||
item_details = ItemDetails()
|
||||
|
||||
item_details.id = item["Id"]
|
||||
item_details.etag = item["Etag"]
|
||||
item_details.is_folder = item["IsFolder"]
|
||||
item_details.item_type = item["Type"]
|
||||
item_details.location_type = item["LocationType"]
|
||||
item_details.name = item["Name"]
|
||||
item_details.sort_name = item["SortName"]
|
||||
item_details.id = item.get("Id")
|
||||
item_details.etag = item.get("Etag")
|
||||
item_details.is_folder = item.get("IsFolder")
|
||||
item_details.item_type = item.get("Type")
|
||||
item_details.location_type = item.get("LocationType")
|
||||
item_details.name = item.get("Name")
|
||||
item_details.sort_name = item.get("SortName")
|
||||
item_details.original_title = item_details.name
|
||||
|
||||
if item_details.item_type == "Episode":
|
||||
item_details.episode_number = item["IndexNumber"]
|
||||
item_details.season_number = item["ParentIndexNumber"]
|
||||
item_details.series_id = item["SeriesId"]
|
||||
item_details.episode_number = item.get("IndexNumber")
|
||||
item_details.season_number = item.get("ParentIndexNumber")
|
||||
item_details.series_id = item.get("SeriesId")
|
||||
|
||||
if item_details.season_number != 0:
|
||||
item_details.season_sort_number = item_details.season_number
|
||||
item_details.episode_sort_number = item_details.episode_number
|
||||
else:
|
||||
special_after_season = item["AirsAfterSeasonNumber"]
|
||||
special_before_season = item["AirsBeforeSeasonNumber"]
|
||||
special_before_episode = item["AirsBeforeEpisodeNumber"]
|
||||
special_after_season = item.get("AirsAfterSeasonNumber")
|
||||
special_before_season = item.get("AirsBeforeSeasonNumber")
|
||||
special_before_episode = item.get("AirsBeforeEpisodeNumber")
|
||||
|
||||
if special_after_season:
|
||||
item_details.season_sort_number = special_after_season + 1
|
||||
@@ -131,21 +121,21 @@ def extract_item_info(item, gui_options):
|
||||
item_details.episode_sort_number = special_before_episode - 1
|
||||
|
||||
elif item_details.item_type == "Season":
|
||||
item_details.season_number = item["IndexNumber"]
|
||||
item_details.series_id = item["SeriesId"]
|
||||
item_details.season_number = item.get("IndexNumber")
|
||||
item_details.series_id = item.get("SeriesId")
|
||||
|
||||
elif item_details.item_type == "Series":
|
||||
item_details.status = item["Status"]
|
||||
item_details.status = item.get("Status")
|
||||
|
||||
elif item_details.item_type == "Audio":
|
||||
item_details.track_number = item["IndexNumber"]
|
||||
item_details.album_name = item["Album"]
|
||||
artists = item["Artists"]
|
||||
if artists is not None and len(artists) > 0:
|
||||
item_details.track_number = item.get("IndexNumber")
|
||||
item_details.album_name = item.get("Album")
|
||||
artists = item.get("Artists", [])
|
||||
if artists:
|
||||
item_details.song_artist = artists[0] # get first artist
|
||||
|
||||
elif item_details.item_type == "MusicAlbum":
|
||||
item_details.album_artist = item["AlbumArtist"]
|
||||
item_details.album_artist = item.get("AlbumArtist")
|
||||
item_details.album_name = item_details.name
|
||||
|
||||
if item_details.season_number is None:
|
||||
@@ -153,34 +143,34 @@ def extract_item_info(item, gui_options):
|
||||
if item_details.episode_number is None:
|
||||
item_details.episode_number = 0
|
||||
|
||||
if item["Taglines"] is not None and len(item["Taglines"]) > 0:
|
||||
item_details.tagline = item["Taglines"][0]
|
||||
if item.get("Taglines", []):
|
||||
item_details.tagline = item.get("Taglines")[0]
|
||||
|
||||
item_details.tags = []
|
||||
if item["TagItems"] is not None and len(item["TagItems"]) > 0:
|
||||
for tag_info in item["TagItems"]:
|
||||
item_details.tags.append(tag_info["Name"])
|
||||
if item.get("TagItems", []):
|
||||
for tag_info in item.get("TagItems"):
|
||||
item_details.tags.append(tag_info.get("Name"))
|
||||
|
||||
# set the item name
|
||||
# override with name format string from request
|
||||
name_format = gui_options["name_format"]
|
||||
name_format_type = gui_options["name_format_type"]
|
||||
name_format = gui_options.get("name_format")
|
||||
name_format_type = gui_options.get("name_format_type")
|
||||
|
||||
if name_format is not None and item_details.item_type == name_format_type:
|
||||
name_info = {}
|
||||
name_info["ItemName"] = item["Name"]
|
||||
season_name = item["SeriesName"]
|
||||
name_info["ItemName"] = item.get("Name")
|
||||
season_name = item.get("SeriesName")
|
||||
if season_name:
|
||||
name_info["SeriesName"] = season_name
|
||||
else:
|
||||
name_info["SeriesName"] = ""
|
||||
name_info["SeasonIndex"] = u"%02d" % item_details.season_number
|
||||
name_info["EpisodeIndex"] = u"%02d" % item_details.episode_number
|
||||
log.debug("FormatName: {0} | {1}", name_format, name_info)
|
||||
item_details.name = unicode(name_format).format(**name_info).strip()
|
||||
log.debug("FormatName: {0} | {1}".format(name_format, name_info))
|
||||
item_details.name = ensure_text(name_format).format(**name_info).strip()
|
||||
|
||||
year = item["ProductionYear"]
|
||||
prem_date = item["PremiereDate"]
|
||||
year = item.get("ProductionYear")
|
||||
prem_date = item.get("PremiereDate")
|
||||
|
||||
if year is not None:
|
||||
item_details.year = year
|
||||
@@ -191,39 +181,39 @@ def extract_item_info(item, gui_options):
|
||||
tokens = prem_date.split("T")
|
||||
item_details.premiere_date = tokens[0]
|
||||
|
||||
create_date = item["DateCreated"]
|
||||
if create_date is not None:
|
||||
create_date = item.get("DateCreated")
|
||||
if create_date:
|
||||
item_details.date_added = create_date.split('.')[0].replace('T', " ")
|
||||
|
||||
# add the premiered date for Upcoming TV
|
||||
if item_details.location_type == "Virtual":
|
||||
airtime = item["AirTime"]
|
||||
airtime = item.get("AirTime")
|
||||
item_details.name = item_details.name + ' - ' + item_details.premiere_date + ' - ' + str(airtime)
|
||||
|
||||
if item_details.item_type == "Program":
|
||||
item_details.program_channel_name = item["ChannelName"]
|
||||
item_details.program_start_date = item["StartDate"]
|
||||
item_details.program_end_date = item["EndDate"]
|
||||
item_details.program_channel_name = item.get("ChannelName")
|
||||
item_details.program_start_date = item.get("StartDate")
|
||||
item_details.program_end_date = item.get("EndDate")
|
||||
|
||||
# Process MediaStreams
|
||||
media_streams = item["MediaStreams"]
|
||||
if media_streams is not None:
|
||||
media_streams = item.get("MediaStreams", [])
|
||||
if media_streams:
|
||||
media_info_list = []
|
||||
for mediaStream in media_streams:
|
||||
stream_type = mediaStream["Type"]
|
||||
stream_type = mediaStream.get("Type")
|
||||
if stream_type == "Video":
|
||||
media_info = {}
|
||||
media_info["type"] = "video"
|
||||
media_info["codec"] = mediaStream["Codec"]
|
||||
media_info["height"] = mediaStream["Height"]
|
||||
media_info["width"] = mediaStream["Width"]
|
||||
aspect_ratio = mediaStream["AspectRatio"]
|
||||
media_info["apect"] = aspect_ratio
|
||||
if aspect_ratio is not None and len(aspect_ratio) >= 3:
|
||||
media_info["codec"] = mediaStream.get("Codec")
|
||||
media_info["height"] = mediaStream.get("Height")
|
||||
media_info["width"] = mediaStream.get("Width")
|
||||
aspect_ratio = mediaStream.get("AspectRatio")
|
||||
media_info["aspect"] = aspect_ratio
|
||||
if aspect_ratio and len(aspect_ratio) >= 3:
|
||||
try:
|
||||
aspect_width, aspect_height = aspect_ratio.split(':')
|
||||
media_info["apect_ratio"] = float(aspect_width) / float(aspect_height)
|
||||
except:
|
||||
except: # noqa
|
||||
media_info["apect_ratio"] = 1.85
|
||||
else:
|
||||
media_info["apect_ratio"] = 1.85
|
||||
@@ -231,40 +221,38 @@ def extract_item_info(item, gui_options):
|
||||
if stream_type == "Audio":
|
||||
media_info = {}
|
||||
media_info["type"] = "audio"
|
||||
media_info["codec"] = mediaStream["Codec"]
|
||||
media_info["channels"] = mediaStream["Channels"]
|
||||
media_info["language"] = mediaStream["Language"]
|
||||
media_info["codec"] = mediaStream.get("Codec")
|
||||
media_info["channels"] = mediaStream.get("Channels")
|
||||
media_info["language"] = mediaStream.get("Language")
|
||||
media_info_list.append(media_info)
|
||||
if stream_type == "Subtitle":
|
||||
item_details.subtitle_available = True
|
||||
media_info = {}
|
||||
media_info["type"] = "sub"
|
||||
media_info["language"] = mediaStream["Language"]
|
||||
media_info["language"] = mediaStream.get("Language", '')
|
||||
media_info_list.append(media_info)
|
||||
|
||||
item_details.media_streams = media_info_list
|
||||
|
||||
# Process People
|
||||
people = item["People"]
|
||||
people = item.get("People", [])
|
||||
if people is not None:
|
||||
cast = []
|
||||
for person in people:
|
||||
person_type = person["Type"]
|
||||
person_type = person.get("Type")
|
||||
if person_type == "Director":
|
||||
item_details.director = item_details.director + person["Name"] + ' '
|
||||
item_details.director = item_details.director + person.get("Name") + ' '
|
||||
elif person_type == "Writing":
|
||||
item_details.writer = person["Name"]
|
||||
elif person_type == "Actor":
|
||||
# log.debug("Person: {0}", person)
|
||||
person_name = person["Name"]
|
||||
person_role = person["Role"]
|
||||
person_id = person["Id"]
|
||||
person_tag = person["PrimaryImageTag"]
|
||||
if person_tag is not None:
|
||||
person_thumbnail = download_utils.image_url(person_id,
|
||||
"Primary", 0, 400, 400,
|
||||
person_tag,
|
||||
server=gui_options["server"])
|
||||
person_name = person.get("Name")
|
||||
person_role = person.get("Role")
|
||||
person_id = person.get("Id")
|
||||
person_tag = person.get("PrimaryImageTag")
|
||||
if person_tag:
|
||||
person_thumbnail = image_url(person_id, "Primary", 0, 400,
|
||||
400, person_tag,
|
||||
server=gui_options["server"])
|
||||
else:
|
||||
person_thumbnail = ""
|
||||
person = {"name": person_name, "role": person_role, "thumbnail": person_thumbnail}
|
||||
@@ -272,64 +260,61 @@ def extract_item_info(item, gui_options):
|
||||
item_details.cast = cast
|
||||
|
||||
# Process Studios
|
||||
studios = item["Studios"]
|
||||
studios = item.get("Studios", [])
|
||||
if studios is not None:
|
||||
for studio in studios:
|
||||
if item_details.studio is None: # Just take the first one
|
||||
studio_name = studio["Name"]
|
||||
studio_name = studio.get("Name")
|
||||
item_details.studio = studio_name
|
||||
break
|
||||
|
||||
# production location
|
||||
prod_location = item["ProductionLocations"]
|
||||
# log.debug("ProductionLocations : {0}", prod_location)
|
||||
if prod_location and len(prod_location) > 0:
|
||||
prod_location = item.get("ProductionLocations", [])
|
||||
if prod_location:
|
||||
item_details.production_location = prod_location[0]
|
||||
|
||||
# Process Genres
|
||||
genres = item["Genres"]
|
||||
if genres is not None and len(genres) > 0:
|
||||
genres = item.get("Genres", [])
|
||||
if genres:
|
||||
item_details.genres = genres
|
||||
|
||||
# Process UserData
|
||||
user_data = item["UserData"]
|
||||
if user_data is None:
|
||||
user_data = defaultdict(lambda: None, {})
|
||||
user_data = item.get("UserData", {})
|
||||
|
||||
if user_data["Played"] is True:
|
||||
if user_data.get("Played"):
|
||||
item_details.overlay = "6"
|
||||
item_details.play_count = 1
|
||||
else:
|
||||
item_details.overlay = "7"
|
||||
item_details.play_count = 0
|
||||
|
||||
if user_data["IsFavorite"] is True:
|
||||
if user_data.get("IsFavorite"):
|
||||
item_details.overlay = "5"
|
||||
item_details.favorite = "true"
|
||||
else:
|
||||
item_details.favorite = "false"
|
||||
|
||||
reasonable_ticks = user_data["PlaybackPositionTicks"]
|
||||
if reasonable_ticks is not None:
|
||||
reasonable_ticks = user_data.get("PlaybackPositionTicks", 0)
|
||||
if reasonable_ticks:
|
||||
reasonable_ticks = int(reasonable_ticks) / 1000
|
||||
item_details.resume_time = int(reasonable_ticks / 10000)
|
||||
|
||||
item_details.series_name = item["SeriesName"]
|
||||
item_details.plot = item["Overview"]
|
||||
item_details.series_name = item.get("SeriesName", '')
|
||||
item_details.plot = item.get("Overview", '')
|
||||
|
||||
runtime = item["RunTimeTicks"]
|
||||
if item_details.is_folder is False and runtime is not None:
|
||||
item_details.duration = long(runtime) / 10000000
|
||||
runtime = item.get("RunTimeTicks")
|
||||
if item_details.is_folder is False and runtime:
|
||||
item_details.duration = runtime / 10000000
|
||||
|
||||
child_count = item["ChildCount"]
|
||||
if child_count is not None:
|
||||
child_count = item.get("ChildCount")
|
||||
if child_count:
|
||||
item_details.total_seasons = child_count
|
||||
|
||||
recursive_item_count = item["RecursiveItemCount"]
|
||||
if recursive_item_count is not None:
|
||||
recursive_item_count = item.get("RecursiveItemCount")
|
||||
if recursive_item_count:
|
||||
item_details.total_episodes = recursive_item_count
|
||||
|
||||
unplayed_item_count = user_data["UnplayedItemCount"]
|
||||
unplayed_item_count = user_data.get("UnplayedItemCount")
|
||||
if unplayed_item_count is not None:
|
||||
item_details.unwatched_episodes = unplayed_item_count
|
||||
item_details.watched_episodes = item_details.total_episodes - unplayed_item_count
|
||||
@@ -337,20 +322,20 @@ def extract_item_info(item, gui_options):
|
||||
item_details.number_episodes = item_details.total_episodes
|
||||
|
||||
item_details.art = get_art(item, gui_options["server"])
|
||||
item_details.rating = item["OfficialRating"]
|
||||
item_details.mpaa = item["OfficialRating"]
|
||||
item_details.rating = item.get("OfficialRating")
|
||||
item_details.mpaa = item.get("OfficialRating")
|
||||
|
||||
item_details.community_rating = item["CommunityRating"]
|
||||
if item_details.community_rating is None:
|
||||
item_details.community_rating = item.get("CommunityRating")
|
||||
if not item_details.community_rating:
|
||||
item_details.community_rating = 0.0
|
||||
|
||||
item_details.critic_rating = item["CriticRating"]
|
||||
if item_details.critic_rating is None:
|
||||
item_details.critic_rating = item.get("CriticRating")
|
||||
if not item_details.critic_rating:
|
||||
item_details.critic_rating = 0.0
|
||||
|
||||
item_details.location_type = item["LocationType"]
|
||||
item_details.recursive_item_count = item["RecursiveItemCount"]
|
||||
item_details.recursive_unplayed_items_count = user_data["UnplayedItemCount"]
|
||||
item_details.location_type = item.get("LocationType")
|
||||
item_details.recursive_item_count = item.get("RecursiveItemCount")
|
||||
item_details.recursive_unplayed_items_count = user_data.get("UnplayedItemCount")
|
||||
|
||||
item_details.mode = "GET_CONTENT"
|
||||
|
||||
@@ -359,8 +344,6 @@ def extract_item_info(item, gui_options):
|
||||
|
||||
def add_gui_item(url, item_details, display_options, folder=True, default_sort=False):
|
||||
|
||||
# log.debug("item_details: {0}", item_details.__dict__)
|
||||
|
||||
if not item_details.name:
|
||||
return None
|
||||
|
||||
@@ -371,17 +354,14 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
|
||||
# Create the URL to pass to the item
|
||||
if folder:
|
||||
u = sys.argv[0] + "?url=" + urllib.quote(url) + mode + "&media_type=" + item_details.item_type
|
||||
u = sys.argv[0] + "?url=" + quote(url) + mode + "&media_type=" + item_details.item_type
|
||||
if item_details.name_format:
|
||||
u += '&name_format=' + urllib.quote(item_details.name_format)
|
||||
u += '&name_format=' + quote(item_details.name_format)
|
||||
if default_sort:
|
||||
u += '&sort=none'
|
||||
else:
|
||||
u = sys.argv[0] + "?item_id=" + url + "&mode=PLAY"
|
||||
|
||||
# Create the ListItem that will be displayed
|
||||
thumb_path = item_details.art["thumb"]
|
||||
|
||||
list_item_name = item_details.name
|
||||
item_type = item_details.item_type.lower()
|
||||
is_video = item_type not in ['musicalbum', 'audio', 'music']
|
||||
@@ -422,20 +402,27 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
end_time = datetime_from_string(item_details.program_end_date)
|
||||
|
||||
duration = (end_time - start_time).total_seconds()
|
||||
time_done = (datetime.now() - start_time).total_seconds()
|
||||
now = get_current_datetime()
|
||||
time_done = (now - start_time).total_seconds()
|
||||
percentage_done = (float(time_done) / float(duration)) * 100.0
|
||||
capped_percentage = int(percentage_done)
|
||||
|
||||
start_time_string = start_time.strftime("%H:%M")
|
||||
end_time_string = end_time.strftime("%H:%M")
|
||||
# Convert dates to local timezone for display
|
||||
local = tz.tzlocal()
|
||||
start_time_string = start_time.astimezone(local).strftime("%H:%M")
|
||||
end_time_string = end_time.astimezone(local).strftime("%H:%M")
|
||||
|
||||
item_details.duration = int(duration)
|
||||
item_details.resume_time = int(time_done)
|
||||
|
||||
list_item_name = (item_details.program_channel_name +
|
||||
" - " + list_item_name +
|
||||
" - " + start_time_string + " to " + end_time_string +
|
||||
" (" + str(int(percentage_done)) + "%)")
|
||||
if item_details.program_channel_name:
|
||||
list_item_name = '{} - {} - {} to {} ({}%)'.format(
|
||||
item_details.program_channel_name, list_item_name,
|
||||
start_time_string, end_time_string, capped_percentage)
|
||||
else:
|
||||
list_item_name = '{} - {} to {} ({}%)'.format(
|
||||
list_item_name, start_time_string, end_time_string,
|
||||
capped_percentage)
|
||||
|
||||
time_info = "Start : " + start_time_string + "\n"
|
||||
time_info += "End : " + end_time_string + "\n"
|
||||
@@ -445,12 +432,7 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
else:
|
||||
item_details.plot = time_info
|
||||
|
||||
if kodi_version > 17:
|
||||
list_item = xbmcgui.ListItem(list_item_name, offscreen=True)
|
||||
else:
|
||||
list_item = xbmcgui.ListItem(list_item_name, iconImage=thumb_path, thumbnailImage=thumb_path)
|
||||
|
||||
# log.debug("Setting thumbnail as: {0}", thumbPath)
|
||||
list_item = xbmcgui.ListItem(list_item_name, offscreen=True)
|
||||
|
||||
item_properties = {}
|
||||
|
||||
@@ -477,11 +459,8 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
info_labels = {}
|
||||
|
||||
# add cast
|
||||
if item_details.cast is not None:
|
||||
if kodi_version >= 17:
|
||||
list_item.setCast(item_details.cast)
|
||||
else:
|
||||
info_labels['cast'] = info_labels['castandrole'] = [(cast_member['name'], cast_member['role']) for cast_member in item_details.cast]
|
||||
if item_details.cast:
|
||||
list_item.setCast(item_details.cast)
|
||||
|
||||
info_labels["title"] = list_item_name
|
||||
if item_details.sort_name:
|
||||
@@ -497,11 +476,11 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
info_labels["rating"] = item_details.rating
|
||||
info_labels["year"] = item_details.year
|
||||
|
||||
if item_details.genres is not None and len(item_details.genres) > 0:
|
||||
if item_details.genres:
|
||||
genres_list = []
|
||||
for genre in item_details.genres:
|
||||
genres_list.append(urllib.quote(genre.encode('utf8')))
|
||||
item_properties["genres"] = urllib.quote("|".join(genres_list))
|
||||
genres_list.append(quote(genre.encode('utf8')))
|
||||
item_properties["genres"] = quote("|".join(genres_list))
|
||||
|
||||
info_labels["genre"] = " / ".join(item_details.genres)
|
||||
|
||||
@@ -523,6 +502,8 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
mediatype = 'artist'
|
||||
elif item_type == 'audio' or item_type == 'music':
|
||||
mediatype = 'song'
|
||||
elif item_type == 'musicvideo':
|
||||
mediatype = 'musicvideo'
|
||||
|
||||
info_labels["mediatype"] = mediatype
|
||||
|
||||
@@ -569,7 +550,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
info_labels["trailer"] = "plugin://plugin.video.jellycon?mode=playTrailer&id=" + item_details.id
|
||||
|
||||
list_item.setInfo('video', info_labels)
|
||||
# log.debug("info_labels: {0}", info_labels)
|
||||
|
||||
if item_details.media_streams is not None:
|
||||
for stream in item_details.media_streams:
|
||||
@@ -591,12 +571,9 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
|
||||
item_properties["TotalSeasons"] = str(item_details.total_seasons)
|
||||
item_properties["TotalEpisodes"] = str(item_details.total_episodes)
|
||||
item_properties["WatchedEpisodes"] = str(item_details.watched_episodes)
|
||||
item_properties["UnWatchedEpisodes"] = str(item_details.unwatched_episodes)
|
||||
item_properties["NumEpisodes"] = str(item_details.number_episodes)
|
||||
|
||||
list_item.setRating("imdb", item_details.community_rating, 0, True)
|
||||
# list_item.setRating("rt", item_details.critic_rating, 0, False)
|
||||
item_properties["TotalTime"] = str(item_details.duration)
|
||||
|
||||
else:
|
||||
@@ -607,7 +584,6 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
info_labels["artist"] = item_details.song_artist
|
||||
info_labels["album"] = item_details.album_name
|
||||
|
||||
# log.debug("info_labels: {0}", info_labels)
|
||||
list_item.setInfo('music', info_labels)
|
||||
|
||||
list_item.setContentLookup(False)
|
||||
@@ -617,11 +593,89 @@ def add_gui_item(url, item_details, display_options, folder=True, default_sort=F
|
||||
if item_details.baseline_itemname is not None:
|
||||
item_properties["suggested_from_watching"] = item_details.baseline_itemname
|
||||
|
||||
# log.debug("item_properties: {0}", item_properties)
|
||||
if kodi_version > 17:
|
||||
list_item.setProperties(item_properties)
|
||||
else:
|
||||
for key, value in item_properties.iteritems():
|
||||
list_item.setProperty(key, value)
|
||||
list_item.setProperties(item_properties)
|
||||
|
||||
return u, list_item, folder
|
||||
|
||||
|
||||
def get_art(item, server):
|
||||
|
||||
art = {
|
||||
'thumb': '',
|
||||
'fanart': '',
|
||||
'poster': '',
|
||||
'banner': '',
|
||||
'clearlogo': '',
|
||||
'clearart': '',
|
||||
'discart': '',
|
||||
'landscape': '',
|
||||
'tvshow.fanart': '',
|
||||
'tvshow.poster': '',
|
||||
'tvshow.clearart': '',
|
||||
'tvshow.clearlogo': '',
|
||||
'tvshow.banner': '',
|
||||
'tvshow.landscape': ''
|
||||
}
|
||||
|
||||
image_tags = item.get("ImageTags", {})
|
||||
if image_tags and image_tags.get("Primary"):
|
||||
art['thumb'] = get_art_url(item, "Primary", server=server)
|
||||
|
||||
item_type = item["Type"]
|
||||
|
||||
if item_type == "Genre":
|
||||
art['poster'] = get_art_url(item, "Primary", server=server)
|
||||
elif item_type == "Episode":
|
||||
art['tvshow.poster'] = get_art_url(item, "Primary", parent=True, server=server)
|
||||
art['tvshow.clearart'] = get_art_url(item, "Art", parent=True, server=server)
|
||||
art['clearart'] = get_art_url(item, "Art", parent=True, server=server)
|
||||
art['tvshow.clearlogo'] = get_art_url(item, "Logo", parent=True, server=server)
|
||||
art['clearlogo'] = get_art_url(item, "Logo", parent=True, server=server)
|
||||
art['tvshow.banner'] = get_art_url(item, "Banner", parent=True, server=server)
|
||||
art['banner'] = get_art_url(item, "Banner", parent=True, server=server)
|
||||
art['tvshow.landscape'] = get_art_url(item, "Thumb", parent=True, server=server)
|
||||
art['landscape'] = get_art_url(item, "Thumb", parent=True, server=server)
|
||||
art['tvshow.fanart'] = get_art_url(item, "Backdrop", parent=True, server=server)
|
||||
art['fanart'] = get_art_url(item, "Backdrop", parent=True, server=server)
|
||||
elif item_type == "Season":
|
||||
art['tvshow.poster'] = get_art_url(item, "Primary", parent=True, server=server)
|
||||
art['season.poster'] = get_art_url(item, "Primary", parent=False, server=server)
|
||||
art['poster'] = get_art_url(item, "Primary", parent=False, server=server)
|
||||
art['tvshow.clearart'] = get_art_url(item, "Art", parent=True, server=server)
|
||||
art['clearart'] = get_art_url(item, "Art", parent=True, server=server)
|
||||
art['tvshow.clearlogo'] = get_art_url(item, "Logo", parent=True, server=server)
|
||||
art['clearlogo'] = get_art_url(item, "Logo", parent=True, server=server)
|
||||
art['tvshow.banner'] = get_art_url(item, "Banner", parent=True, server=server)
|
||||
art['season.banner'] = get_art_url(item, "Banner", parent=False, server=server)
|
||||
art['banner'] = get_art_url(item, "Banner", parent=False, server=server)
|
||||
art['tvshow.landscape'] = get_art_url(item, "Thumb", parent=True, server=server)
|
||||
art['season.landscape'] = get_art_url(item, "Thumb", parent=False, server=server)
|
||||
art['landscape'] = get_art_url(item, "Thumb", parent=False, server=server)
|
||||
art['tvshow.fanart'] = get_art_url(item, "Backdrop", parent=True, server=server)
|
||||
art['fanart'] = get_art_url(item, "Backdrop", parent=True, server=server)
|
||||
elif item_type == "Series":
|
||||
art['tvshow.poster'] = get_art_url(item, "Primary", parent=False, server=server)
|
||||
art['poster'] = get_art_url(item, "Primary", parent=False, server=server)
|
||||
art['tvshow.clearart'] = get_art_url(item, "Art", parent=False, server=server)
|
||||
art['clearart'] = get_art_url(item, "Art", parent=False, server=server)
|
||||
art['tvshow.clearlogo'] = get_art_url(item, "Logo", parent=False, server=server)
|
||||
art['clearlogo'] = get_art_url(item, "Logo", parent=False, server=server)
|
||||
art['tvshow.banner'] = get_art_url(item, "Banner", parent=False, server=server)
|
||||
art['banner'] = get_art_url(item, "Banner", parent=False, server=server)
|
||||
art['tvshow.landscape'] = get_art_url(item, "Thumb", parent=False, server=server)
|
||||
art['landscape'] = get_art_url(item, "Thumb", parent=False, server=server)
|
||||
art['tvshow.fanart'] = get_art_url(item, "Backdrop", parent=False, server=server)
|
||||
art['fanart'] = get_art_url(item, "Backdrop", parent=False, server=server)
|
||||
elif item_type == "Movie" or item_type == "BoxSet":
|
||||
art['poster'] = get_art_url(item, "Primary", server=server)
|
||||
art['landscape'] = get_art_url(item, "Thumb", server=server)
|
||||
art['banner'] = get_art_url(item, "Banner", server=server)
|
||||
art['clearlogo'] = get_art_url(item, "Logo", server=server)
|
||||
art['clearart'] = get_art_url(item, "Art", server=server)
|
||||
art['discart'] = get_art_url(item, "Disc", server=server)
|
||||
|
||||
art['fanart'] = get_art_url(item, "Backdrop", server=server)
|
||||
if not art['fanart']:
|
||||
art['fanart'] = get_art_url(item, "Backdrop", parent=True, server=server)
|
||||
|
||||
return art
|
||||
|
||||
196
resources/lib/jellyfin.py
Normal file
196
resources/lib/jellyfin.py
Normal file
@@ -0,0 +1,196 @@
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import json
|
||||
|
||||
import requests
|
||||
import xbmcaddon
|
||||
from kodi_six.utils import py2_decode
|
||||
|
||||
from .utils import get_device_id, get_version, load_user_details
|
||||
from .lazylogger import LazyLogger
|
||||
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class API:
|
||||
def __init__(self, server=None, user_id=None, token=None):
|
||||
self.server = server
|
||||
self.user_id = user_id
|
||||
self.token = token
|
||||
|
||||
self.settings = xbmcaddon.Addon()
|
||||
|
||||
self.headers = {}
|
||||
self.create_headers()
|
||||
self.verify_cert = settings.getSetting('verify_cert') == 'true'
|
||||
|
||||
def get(self, path):
|
||||
if 'x-mediabrowser-token' not in self.headers or self.token not in self.headers:
|
||||
self.create_headers(True)
|
||||
|
||||
# Fixes initial login where class is initialized before wizard completes
|
||||
if not self.server:
|
||||
self.settings = xbmcaddon.Addon()
|
||||
self.server = self.settings.getSetting('server_address')
|
||||
|
||||
url = '{}{}'.format(self.server, path)
|
||||
|
||||
r = requests.get(url, headers=self.headers, verify=self.verify_cert)
|
||||
try:
|
||||
try:
|
||||
'''
|
||||
The requests library defaults to using simplejson to handle
|
||||
json decoding. On low power devices and using Py3, this is
|
||||
significantly slower than the builtin json library. Skip that
|
||||
and just parse the json ourselves. Fall back to using
|
||||
requests/simplejson if there's a parsing error.
|
||||
'''
|
||||
r.raise_for_status()
|
||||
response_data = json.loads(r.text)
|
||||
except ValueError:
|
||||
response_data = r.json()
|
||||
except: # noqa
|
||||
response_data = {}
|
||||
return response_data
|
||||
|
||||
def post(self, url, payload={}):
|
||||
if 'x-mediabrowser-token' not in self.headers or self.token not in self.headers:
|
||||
self.create_headers(True)
|
||||
|
||||
url = '{}{}'.format(self.server, url)
|
||||
|
||||
r = requests.post(url, json=payload, headers=self.headers, verify=self.verify_cert)
|
||||
try:
|
||||
try:
|
||||
# Much faster on low power devices, see above comment
|
||||
response_data = json.loads(r.text)
|
||||
except ValueError:
|
||||
response_data = r.json()
|
||||
except: # noqa
|
||||
response_data = {}
|
||||
return response_data
|
||||
|
||||
def delete(self, url):
|
||||
if 'x-mediabrowser-token' not in self.headers or self.token not in self.headers:
|
||||
self.create_headers(True)
|
||||
|
||||
url = '{}{}'.format(self.server, url)
|
||||
|
||||
requests.delete(url, headers=self.headers, verify=self.verify_cert)
|
||||
|
||||
def authenticate(self, auth_data):
|
||||
# Always force create fresh headers during authentication
|
||||
self.create_headers(True)
|
||||
response = self.post('/Users/AuthenticateByName', auth_data)
|
||||
token = response.get('AccessToken')
|
||||
if token:
|
||||
self.token = token
|
||||
self.user_id = response.get('User').get('Id')
|
||||
# Create headers again to include auth token
|
||||
self.create_headers()
|
||||
return response
|
||||
else:
|
||||
log.error('Unable to authenticate to Jellyfin server')
|
||||
return {}
|
||||
|
||||
def create_headers(self, force=False):
|
||||
|
||||
# If the headers already exist with an auth token, return unless we're regenerating
|
||||
if self.headers and 'x-mediabrowser-token' in self.headers and force is False:
|
||||
return
|
||||
|
||||
headers = {}
|
||||
device_name = self.settings.getSetting('deviceName')
|
||||
if len(device_name) == 0:
|
||||
device_name = "JellyCon"
|
||||
# Ensure ascii and remove invalid characters
|
||||
device_name = py2_decode(device_name).replace('"', '_').replace(',', '_')
|
||||
device_id = get_device_id()
|
||||
version = get_version()
|
||||
|
||||
authorization = (
|
||||
'MediaBrowser Client="Kodi JellyCon", Device="{device}", '
|
||||
'DeviceId="{device_id}", Version="{version}"'
|
||||
).format(
|
||||
device=device_name,
|
||||
device_id=device_id,
|
||||
version=version
|
||||
)
|
||||
|
||||
headers['x-emby-authorization'] = authorization
|
||||
|
||||
# If we have a valid token, ensure it's included in the headers unless we're regenerating
|
||||
if self.token and force is False:
|
||||
headers['x-mediabrowser-token'] = self.token
|
||||
else:
|
||||
# Check for updated credentials since initialization
|
||||
user_details = load_user_details()
|
||||
token = user_details.get('token')
|
||||
if token:
|
||||
self.token = token
|
||||
headers['x-mediabrowser-token'] = self.token
|
||||
|
||||
# Make headers available to api calls
|
||||
self.headers = headers
|
||||
|
||||
def post_capabilities(self):
|
||||
url = '/Sessions/Capabilities/Full'
|
||||
|
||||
data = {
|
||||
'SupportsMediaControl': True,
|
||||
'PlayableMediaTypes': ["Video", "Audio"],
|
||||
'SupportedCommands': ["MoveUp",
|
||||
"MoveDown",
|
||||
"MoveLeft",
|
||||
"MoveRight",
|
||||
"Select",
|
||||
"Back",
|
||||
"ToggleContextMenu",
|
||||
"ToggleFullscreen",
|
||||
"ToggleOsdMenu",
|
||||
"GoHome",
|
||||
"PageUp",
|
||||
"NextLetter",
|
||||
"GoToSearch",
|
||||
"GoToSettings",
|
||||
"PageDown",
|
||||
"PreviousLetter",
|
||||
"TakeScreenshot",
|
||||
"VolumeUp",
|
||||
"VolumeDown",
|
||||
"ToggleMute",
|
||||
"SendString",
|
||||
"DisplayMessage",
|
||||
"SetAudioStreamIndex",
|
||||
"SetSubtitleStreamIndex",
|
||||
"SetRepeatMode",
|
||||
"Mute",
|
||||
"Unmute",
|
||||
"SetVolume",
|
||||
"PlayNext",
|
||||
"Play",
|
||||
"Playstate",
|
||||
"PlayMediaSource"]
|
||||
}
|
||||
|
||||
self.post(url, data)
|
||||
|
||||
def speedtest(self, test_data_size):
|
||||
self.create_headers()
|
||||
|
||||
url = '{}/playback/bitratetest?size={}'.format(self.server, test_data_size)
|
||||
# Because this needs the stream argument, this doesn't go through self.get()
|
||||
response = requests.get(url, stream=True, headers=self.headers, verify=self.verify_cert)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
user_details = load_user_details()
|
||||
api = API(
|
||||
settings.getSetting('server_address'),
|
||||
user_details.get('user_id'),
|
||||
user_details.get('token')
|
||||
)
|
||||
@@ -1,4 +1,9 @@
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import json
|
||||
|
||||
import xbmc
|
||||
|
||||
|
||||
@@ -9,7 +14,7 @@ class JsonRpc(object):
|
||||
params = None
|
||||
|
||||
def __init__(self, method, **kwargs):
|
||||
|
||||
|
||||
self.method = method
|
||||
|
||||
for arg in kwargs: # id_(int), jsonrpc(str)
|
||||
@@ -18,7 +23,7 @@ class JsonRpc(object):
|
||||
def _query(self):
|
||||
|
||||
query = {
|
||||
|
||||
|
||||
'jsonrpc': self.jsonrpc,
|
||||
'id': self.id_,
|
||||
'method': self.method,
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import xbmc
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import sys
|
||||
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
import xbmcaddon
|
||||
|
||||
import sys
|
||||
import json
|
||||
from .lazylogger import LazyLogger
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
addon = xbmcaddon.Addon()
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class HomeWindow:
|
||||
@@ -24,44 +25,25 @@ class HomeWindow:
|
||||
def get_property(self, key):
|
||||
key = self.id_string % key
|
||||
value = self.window.getProperty(key)
|
||||
# log.debug('HomeWindow: getProperty |{0}| -> |{1}|', key, value)
|
||||
return value
|
||||
|
||||
def set_property(self, key, value):
|
||||
key = self.id_string % key
|
||||
# log.debug('HomeWindow: setProperty |{0}| -> |{1}|', key, value)
|
||||
self.window.setProperty(key, value)
|
||||
|
||||
def clear_property(self, key):
|
||||
key = self.id_string % key
|
||||
# log.debug('HomeWindow: clearProperty |{0}|', key)
|
||||
self.window.clearProperty(key)
|
||||
|
||||
|
||||
def add_menu_directory_item(label, path, folder=True, art=None):
|
||||
li = xbmcgui.ListItem(label, path=path)
|
||||
def add_menu_directory_item(label, path, folder=True, art=None, properties=None):
|
||||
li = xbmcgui.ListItem(label, path=path, offscreen=True)
|
||||
if art is None:
|
||||
art = {}
|
||||
addon = xbmcaddon.Addon()
|
||||
art["thumb"] = addon.getAddonInfo('icon')
|
||||
if properties is not None:
|
||||
li.setProperties(properties)
|
||||
li.setArt(art)
|
||||
|
||||
xbmcplugin.addDirectoryItem(handle=int(sys.argv[1]), url=path, listitem=li, isFolder=folder)
|
||||
|
||||
|
||||
def get_kodi_version():
|
||||
|
||||
json_data = xbmc.executeJSONRPC(
|
||||
'{ "jsonrpc": "2.0", "method": "Application.GetProperties", "params": {"properties": ["version", "name"]}, "id": 1 }')
|
||||
|
||||
result = json.loads(json_data)
|
||||
|
||||
try:
|
||||
result = result.get("result")
|
||||
version_data = result.get("version")
|
||||
version = float(str(version_data.get("major")) + "." + str(version_data.get("minor")))
|
||||
log.debug("Version: {0} - {1}", version, version_data)
|
||||
except:
|
||||
version = 0.0
|
||||
log.error("Version Error : RAW Version Data: {0}", result)
|
||||
|
||||
return version
|
||||
|
||||
20
resources/lib/lazylogger.py
Normal file
20
resources/lib/lazylogger.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
|
||||
class LazyLogger(object):
|
||||
"""`helper.loghandler.getLogger()` is used everywhere.
|
||||
This class helps avoiding import errors.
|
||||
"""
|
||||
__logger = None
|
||||
__logger_name = None
|
||||
|
||||
def __init__(self, logger_name=None):
|
||||
self.__logger_name = logger_name
|
||||
|
||||
def __getattr__(self, name):
|
||||
if self.__logger is None:
|
||||
from .loghandler import getLogger
|
||||
self.__logger = getLogger(self.__logger_name)
|
||||
return getattr(self.__logger, name)
|
||||
@@ -1,45 +0,0 @@
|
||||
import threading
|
||||
import time
|
||||
|
||||
import xbmc
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from .widgets import check_for_new_content
|
||||
from .tracking import timer
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
|
||||
|
||||
class LibraryChangeMonitor(threading.Thread):
|
||||
|
||||
last_library_change_check = 0
|
||||
library_check_triggered = False
|
||||
exit_now = False
|
||||
time_between_checks = 10
|
||||
|
||||
def __init__(self):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
def stop(self):
|
||||
self.exit_now = True
|
||||
|
||||
@timer
|
||||
def check_for_updates(self):
|
||||
log.debug("Trigger check for updates")
|
||||
self.library_check_triggered = True
|
||||
|
||||
def run(self):
|
||||
log.debug("Library Monitor Started")
|
||||
monitor = xbmc.Monitor()
|
||||
while not self.exit_now and not monitor.abortRequested():
|
||||
|
||||
if self.library_check_triggered and not xbmc.Player().isPlaying():
|
||||
log.debug("Doing new content check")
|
||||
check_for_new_content()
|
||||
self.library_check_triggered = False
|
||||
self.last_library_change_check = time.time()
|
||||
|
||||
if self.exit_now or monitor.waitForAbort(self.time_between_checks):
|
||||
break
|
||||
|
||||
log.debug("Library Monitor Exited")
|
||||
157
resources/lib/loghandler.py
Normal file
157
resources/lib/loghandler.py
Normal file
@@ -0,0 +1,157 @@
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import os
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from six import ensure_text
|
||||
from kodi_six import xbmc, xbmcaddon
|
||||
|
||||
from .utils import translate_path
|
||||
|
||||
try:
|
||||
__addon__ = xbmcaddon.Addon(id='plugin.video.jellycon')
|
||||
__pluginpath__ = translate_path(__addon__.getAddonInfo('path'))
|
||||
except Exception:
|
||||
# During installation/update, addon might not be fully registered yet
|
||||
__addon__ = None
|
||||
__pluginpath__ = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
def getLogger(name=None):
|
||||
if name is None:
|
||||
return __LOGGER
|
||||
|
||||
return __LOGGER.getChild(name)
|
||||
|
||||
|
||||
class LogHandler(logging.StreamHandler):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
logging.StreamHandler.__init__(self)
|
||||
self.setFormatter(MyFormatter())
|
||||
|
||||
self.sensitive = {'Token': [], 'Server': []}
|
||||
|
||||
try:
|
||||
settings = xbmcaddon.Addon()
|
||||
self.server = settings.getSetting('server_address')
|
||||
self.debug = settings.getSetting('log_debug')
|
||||
except Exception:
|
||||
# During installation/update, settings might not be available yet
|
||||
self.server = ''
|
||||
self.debug = 'false'
|
||||
|
||||
def emit(self, record):
|
||||
|
||||
if self._get_log_level(record.levelno):
|
||||
try:
|
||||
string = self.format(record)
|
||||
|
||||
# Hide server URL in logs
|
||||
string = string.replace(
|
||||
self.server or "{server}", "{jellyfin-server}"
|
||||
)
|
||||
|
||||
py_version = sys.version_info.major
|
||||
# Log level notation changed in Kodi v19
|
||||
if py_version > 2:
|
||||
log_level = xbmc.LOGINFO
|
||||
else:
|
||||
log_level = xbmc.LOGNOTICE
|
||||
xbmc.log(string, level=log_level)
|
||||
except Exception:
|
||||
# Silently fail if logging is disabled globally in Kodi
|
||||
pass
|
||||
|
||||
def _get_log_level(self, level):
|
||||
|
||||
levels = {
|
||||
logging.ERROR: 0,
|
||||
logging.WARNING: 0,
|
||||
logging.INFO: 1,
|
||||
logging.DEBUG: 2
|
||||
}
|
||||
if self.debug == 'true':
|
||||
log_level = 2
|
||||
else:
|
||||
log_level = 1
|
||||
|
||||
return log_level >= levels[level]
|
||||
|
||||
|
||||
class MyFormatter(logging.Formatter):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fmt='%(name)s -> %(levelname)s::%(relpath)s:%(lineno)s %(message)s'
|
||||
):
|
||||
logging.Formatter.__init__(self, fmt)
|
||||
|
||||
def format(self, record):
|
||||
if record.pathname:
|
||||
record.pathname = ensure_text(
|
||||
record.pathname, get_filesystem_encoding()
|
||||
)
|
||||
|
||||
self._gen_rel_path(record)
|
||||
|
||||
# Call the original formatter class to do the grunt work
|
||||
result = logging.Formatter.format(self, record)
|
||||
|
||||
return result
|
||||
|
||||
def formatException(self, exc_info):
|
||||
try:
|
||||
_pluginpath_real = os.path.realpath(__pluginpath__)
|
||||
except Exception:
|
||||
_pluginpath_real = None
|
||||
res = []
|
||||
|
||||
for o in traceback.format_exception(*exc_info):
|
||||
o = ensure_text(o, get_filesystem_encoding())
|
||||
|
||||
if _pluginpath_real and o.startswith(' File "'):
|
||||
"""
|
||||
If this split can't handle your file names,
|
||||
you should seriously consider renaming your files.
|
||||
"""
|
||||
fn = o.split(' File "', 2)[1].split('", line ', 1)[0]
|
||||
rfn = os.path.realpath(fn)
|
||||
if rfn.startswith(_pluginpath_real):
|
||||
o = o.replace(fn, os.path.relpath(rfn, _pluginpath_real))
|
||||
|
||||
res.append(o)
|
||||
|
||||
return ''.join(res)
|
||||
|
||||
def _gen_rel_path(self, record):
|
||||
if record.pathname:
|
||||
try:
|
||||
record.relpath = os.path.relpath(record.pathname, __pluginpath__)
|
||||
except Exception:
|
||||
record.relpath = record.pathname
|
||||
|
||||
|
||||
def get_filesystem_encoding():
|
||||
enc = sys.getfilesystemencoding()
|
||||
|
||||
if not enc:
|
||||
enc = sys.getdefaultencoding()
|
||||
|
||||
if not enc or enc == 'ascii':
|
||||
enc = 'utf-8'
|
||||
|
||||
return enc
|
||||
|
||||
|
||||
__LOGGER = logging.getLogger('JELLYFIN')
|
||||
for handler in __LOGGER.handlers:
|
||||
__LOGGER.removeHandler(handler)
|
||||
|
||||
__LOGGER.addHandler(LogHandler())
|
||||
__LOGGER.setLevel(logging.DEBUG)
|
||||
File diff suppressed because it is too large
Load Diff
89
resources/lib/monitors.py
Normal file
89
resources/lib/monitors.py
Normal file
@@ -0,0 +1,89 @@
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
import xbmc
|
||||
|
||||
from .functions import show_menu
|
||||
from .lazylogger import LazyLogger
|
||||
from .widgets import check_for_new_content
|
||||
from .tracking import timer
|
||||
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class ContextMonitor(threading.Thread):
|
||||
|
||||
stop_thread = False
|
||||
|
||||
def run(self):
|
||||
|
||||
item_id = None
|
||||
log.debug("ContextMonitor Thread Started")
|
||||
|
||||
while not xbmc.Monitor().abortRequested() and not self.stop_thread:
|
||||
|
||||
visibility_check = (
|
||||
"Window.IsActive(fullscreenvideo) | "
|
||||
"Window.IsActive(visualisation)"
|
||||
)
|
||||
if xbmc.getCondVisibility(visibility_check):
|
||||
xbmc.sleep(1000)
|
||||
else:
|
||||
if xbmc.getCondVisibility("Window.IsVisible(contextmenu)"):
|
||||
if item_id:
|
||||
xbmc.executebuiltin("Dialog.Close(contextmenu,true)")
|
||||
params = {}
|
||||
params["item_id"] = item_id
|
||||
show_menu(params)
|
||||
|
||||
container_id = xbmc.getInfoLabel("System.CurrentControlID")
|
||||
item_id = xbmc.getInfoLabel(
|
||||
"Container({}).ListItem.Property(id)".format(container_id)
|
||||
)
|
||||
|
||||
xbmc.sleep(100)
|
||||
|
||||
log.debug("ContextMonitor Thread Exited")
|
||||
|
||||
def stop_monitor(self):
|
||||
log.debug("ContextMonitor Stop Called")
|
||||
self.stop_thread = True
|
||||
|
||||
|
||||
class LibraryChangeMonitor(threading.Thread):
|
||||
|
||||
last_library_change_check = 0
|
||||
library_check_triggered = False
|
||||
exit_now = False
|
||||
time_between_checks = 10
|
||||
|
||||
def __init__(self):
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
def stop(self):
|
||||
self.exit_now = True
|
||||
|
||||
@timer
|
||||
def check_for_updates(self):
|
||||
log.debug("Trigger check for updates")
|
||||
self.library_check_triggered = True
|
||||
|
||||
def run(self):
|
||||
log.debug("Library Monitor Started")
|
||||
monitor = xbmc.Monitor()
|
||||
while not self.exit_now and not monitor.abortRequested():
|
||||
|
||||
if self.library_check_triggered and not xbmc.Player().isPlaying():
|
||||
log.debug("Doing new content check")
|
||||
check_for_new_content()
|
||||
self.library_check_triggered = False
|
||||
self.last_library_change_check = time.time()
|
||||
|
||||
if self.exit_now or monitor.waitForAbort(self.time_between_checks):
|
||||
break
|
||||
|
||||
log.debug("Library Monitor Exited")
|
||||
@@ -1,10 +1,13 @@
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import xbmcgui
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from .lazylogger import LazyLogger
|
||||
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
|
||||
class PictureViewer(xbmcgui.WindowXMLDialog):
|
||||
picture_url = None
|
||||
@@ -21,11 +24,6 @@ class PictureViewer(xbmcgui.WindowXMLDialog):
|
||||
picture_control = self.getControl(3010)
|
||||
|
||||
picture_control.setImage(self.picture_url)
|
||||
# self.listControl.addItems(self.action_items)
|
||||
# self.setFocus(self.listControl)
|
||||
|
||||
# bg_image = self.getControl(3010)
|
||||
# bg_image.setHeight(50 * len(self.action_items) + 20)
|
||||
|
||||
def onFocus(self, controlId):
|
||||
pass
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1731
resources/lib/play_utils.py.orig
Normal file
1731
resources/lib/play_utils.py.orig
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,18 @@
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import os
|
||||
import threading
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcaddon
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from .play_utils import send_event_notification
|
||||
from .lazylogger import LazyLogger
|
||||
from .dialogs import PlayNextDialog
|
||||
from .utils import translate_path
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class PlayNextService(threading.Thread):
|
||||
@@ -30,6 +34,8 @@ class PlayNextService(threading.Thread):
|
||||
play_next_triggered = False
|
||||
is_playing = False
|
||||
|
||||
now_playing = None
|
||||
|
||||
while not xbmc.Monitor().abortRequested() and not self.stop_thread:
|
||||
|
||||
player = xbmc.Player()
|
||||
@@ -38,7 +44,14 @@ class PlayNextService(threading.Thread):
|
||||
if not is_playing:
|
||||
settings = xbmcaddon.Addon()
|
||||
play_next_trigger_time = int(settings.getSetting('play_next_trigger_time'))
|
||||
log.debug("New play_next_trigger_time value: {0}", play_next_trigger_time)
|
||||
log.debug("New play_next_trigger_time value: {0}".format(play_next_trigger_time))
|
||||
|
||||
now_playing_file = player.getPlayingFile()
|
||||
if now_playing_file != now_playing:
|
||||
# If the playing file has changed, reset the play next values
|
||||
play_next_dialog = None
|
||||
play_next_triggered = False
|
||||
now_playing = now_playing_file
|
||||
|
||||
duration = player.getTotalTime()
|
||||
position = player.getTime()
|
||||
@@ -47,10 +60,10 @@ class PlayNextService(threading.Thread):
|
||||
|
||||
if not play_next_triggered and (trigger_time > time_to_end) and play_next_dialog is None:
|
||||
play_next_triggered = True
|
||||
log.debug("play_next_triggered hit at {0} seconds from end", time_to_end)
|
||||
log.debug("play_next_triggered hit at {0} seconds from end".format(time_to_end))
|
||||
|
||||
play_data = get_playing_data(self.monitor.played_information)
|
||||
log.debug("play_next_triggered play_data : {0}", play_data)
|
||||
play_data = get_playing_data()
|
||||
log.debug("play_next_triggered play_data : {0}".format(play_data))
|
||||
|
||||
next_episode = play_data.get("next_episode")
|
||||
item_type = play_data.get("item_type")
|
||||
@@ -59,7 +72,7 @@ class PlayNextService(threading.Thread):
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
plugin_path = settings.getAddonInfo('path')
|
||||
plugin_path_real = xbmc.translatePath(os.path.join(plugin_path))
|
||||
plugin_path_real = translate_path(os.path.join(plugin_path))
|
||||
|
||||
play_next_dialog = PlayNextDialog("PlayNextDialog.xml", plugin_path_real, "default", "720p")
|
||||
play_next_dialog.set_episode_info(next_episode)
|
||||
@@ -76,74 +89,11 @@ class PlayNextService(threading.Thread):
|
||||
play_next_dialog = None
|
||||
|
||||
is_playing = False
|
||||
now_playing = None
|
||||
|
||||
if xbmc.Monitor().waitForAbort(1):
|
||||
break
|
||||
|
||||
def stop_servcie(self):
|
||||
def stop_service(self):
|
||||
log.debug("PlayNextService Stop Called")
|
||||
self.stop_thread = True
|
||||
|
||||
|
||||
class PlayNextDialog(xbmcgui.WindowXMLDialog):
|
||||
|
||||
action_exitkeys_id = None
|
||||
episode_info = None
|
||||
play_called = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
log.debug("PlayNextDialog: __init__")
|
||||
xbmcgui.WindowXML.__init__(self, *args, **kwargs)
|
||||
|
||||
def onInit(self):
|
||||
log.debug("PlayNextDialog: onInit")
|
||||
self.action_exitkeys_id = [10, 13]
|
||||
|
||||
index = self.episode_info.get("IndexNumber", -1)
|
||||
series_name = self.episode_info.get("SeriesName")
|
||||
next_epp_name = "Episode %02d - (%s)" % (index, self.episode_info.get("Name", "n/a"))
|
||||
|
||||
series_label = self.getControl(3011)
|
||||
series_label.setLabel(series_name)
|
||||
|
||||
series_label = self.getControl(3012)
|
||||
series_label.setLabel(next_epp_name)
|
||||
|
||||
def onFocus(self, control_id):
|
||||
pass
|
||||
|
||||
def doAction(self, action_id):
|
||||
pass
|
||||
|
||||
def onMessage(self, message):
|
||||
log.debug("PlayNextDialog: onMessage: {0}", message)
|
||||
|
||||
def onAction(self, action):
|
||||
|
||||
if action.getId() == 10: # ACTION_PREVIOUS_MENU
|
||||
self.close()
|
||||
elif action.getId() == 92: # ACTION_NAV_BACK
|
||||
self.close()
|
||||
else:
|
||||
log.debug("PlayNextDialog: onAction: {0}", action.getId())
|
||||
|
||||
def onClick(self, control_id):
|
||||
if control_id == 3013:
|
||||
log.debug("PlayNextDialog: Play Next Episode")
|
||||
self.play_called
|
||||
self.close()
|
||||
next_item_id = self.episode_info.get("Id")
|
||||
log.debug("Playing Next Episode: {0}", next_item_id)
|
||||
play_info = {}
|
||||
play_info["item_id"] = next_item_id
|
||||
play_info["auto_resume"] = "-1"
|
||||
play_info["force_transcode"] = False
|
||||
send_event_notification("jellycon_play_action", play_info)
|
||||
elif control_id == 3014:
|
||||
self.close()
|
||||
|
||||
def set_episode_info(self, info):
|
||||
self.episode_info = info
|
||||
|
||||
def get_play_called(self):
|
||||
return self.play_called
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
|
||||
import xbmcgui
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from .translation import string_load
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
|
||||
|
||||
class ResumeDialog(xbmcgui.WindowXMLDialog):
|
||||
resumePlay = -1
|
||||
resumeTimeStamp = ""
|
||||
action_exitkeys_id = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
xbmcgui.WindowXMLDialog.__init__(self, *args, **kwargs)
|
||||
log.debug("ResumeDialog INITIALISED")
|
||||
|
||||
def onInit(self):
|
||||
self.action_exitkeys_id = [10, 13]
|
||||
self.getControl(3010).setLabel(self.resumeTimeStamp)
|
||||
self.getControl(3011).setLabel(string_load(30237))
|
||||
|
||||
def onFocus(self, controlId):
|
||||
pass
|
||||
|
||||
def doAction(self, actionID):
|
||||
pass
|
||||
|
||||
def onClick(self, controlID):
|
||||
|
||||
if controlID == 3010:
|
||||
self.resumePlay = 0
|
||||
self.close()
|
||||
if controlID == 3011:
|
||||
self.resumePlay = 1
|
||||
self.close()
|
||||
|
||||
def setResumeTime(self, timeStamp):
|
||||
self.resumeTimeStamp = timeStamp
|
||||
|
||||
def getResumeAction(self):
|
||||
return self.resumePlay
|
||||
@@ -1,56 +0,0 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
|
||||
|
||||
class SafeDeleteDialog(xbmcgui.WindowXMLDialog):
|
||||
|
||||
confirm = False
|
||||
message = "Demo Message"
|
||||
heading = "Demo Heading"
|
||||
action_exitkeys_id = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
log.debug("SafeDeleteDialog: __init__")
|
||||
xbmcgui.WindowXML.__init__(self, *args, **kwargs)
|
||||
|
||||
def onInit(self):
|
||||
log.debug("SafeDeleteDialog: onInit")
|
||||
self.action_exitkeys_id = [10, 13]
|
||||
|
||||
message_control = self.getControl(3)
|
||||
message_control.setText(self.message)
|
||||
|
||||
message_control = self.getControl(4)
|
||||
message_control.setLabel(self.heading)
|
||||
|
||||
def onFocus(self, controlId):
|
||||
pass
|
||||
|
||||
def doAction(self, actionID):
|
||||
pass
|
||||
|
||||
def onMessage(self, message):
|
||||
log.debug("SafeDeleteDialog: onMessage: {0}", message)
|
||||
|
||||
def onAction(self, action):
|
||||
|
||||
if action.getId() == 10: # ACTION_PREVIOUS_MENU
|
||||
self.close()
|
||||
elif action.getId() == 92: # ACTION_NAV_BACK
|
||||
self.close()
|
||||
else:
|
||||
log.debug("SafeDeleteDialog: onAction: {0}", action.getId())
|
||||
|
||||
def onClick(self, controlID):
|
||||
if controlID == 1:
|
||||
self.confirm = True
|
||||
self.close()
|
||||
elif controlID == 2:
|
||||
self.confirm = False
|
||||
self.close()
|
||||
@@ -1,26 +1,24 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import socket
|
||||
import json
|
||||
from urlparse import urlparse
|
||||
import httplib
|
||||
import ssl
|
||||
import time
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
|
||||
import xbmcaddon
|
||||
import xbmcgui
|
||||
import xbmc
|
||||
|
||||
from .kodi_utils import HomeWindow
|
||||
from .downloadutils import DownloadUtils, save_user_details, load_user_details
|
||||
from .simple_logging import SimpleLogging
|
||||
from .translation import string_load
|
||||
from .utils import datetime_from_string
|
||||
from .clientinfo import ClientInformation
|
||||
from .jellyfin import API
|
||||
from .lazylogger import LazyLogger
|
||||
from .utils import (
|
||||
datetime_from_string, translate_string, save_user_details,
|
||||
load_user_details, get_current_datetime, get_saved_users
|
||||
)
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
__addon__ = xbmcaddon.Addon()
|
||||
__addon_name__ = __addon__.getAddonInfo('name')
|
||||
@@ -30,76 +28,43 @@ def check_connection_speed():
|
||||
log.debug("check_connection_speed")
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
verify_cert = settings.getSetting('verify_cert') == 'true'
|
||||
http_timeout = int(settings.getSetting("http_timeout"))
|
||||
speed_test_data_size = int(settings.getSetting("speed_test_data_size"))
|
||||
test_data_size = speed_test_data_size * 1000000
|
||||
user_details = load_user_details()
|
||||
|
||||
du = DownloadUtils()
|
||||
server = du.get_server()
|
||||
|
||||
url = server + "/playback/bitratetest?size=%s" % test_data_size
|
||||
|
||||
url_bits = urlparse(url.strip())
|
||||
|
||||
protocol = url_bits.scheme
|
||||
host_name = url_bits.hostname
|
||||
port = url_bits.port
|
||||
# user_name = url_bits.username
|
||||
# user_password = url_bits.password
|
||||
url_path = url_bits.path
|
||||
url_puery = url_bits.query
|
||||
|
||||
server = "%s:%s" % (host_name, port)
|
||||
url_path = url_path + "?" + url_puery
|
||||
|
||||
local_use_https = False
|
||||
if protocol.lower() == "https":
|
||||
local_use_https = True
|
||||
|
||||
if local_use_https and verify_cert:
|
||||
log.debug("Connection: HTTPS, Cert checked")
|
||||
conn = httplib.HTTPSConnection(server, timeout=http_timeout)
|
||||
elif local_use_https and not verify_cert:
|
||||
log.debug("Connection: HTTPS, Cert NOT checked")
|
||||
conn = httplib.HTTPSConnection(server, timeout=http_timeout, context=ssl._create_unverified_context())
|
||||
else:
|
||||
log.debug("Connection: HTTP")
|
||||
conn = httplib.HTTPConnection(server, timeout=http_timeout)
|
||||
|
||||
head = du.get_auth_header(True)
|
||||
head["User-Agent"] = "JellyCon-" + ClientInformation().get_version()
|
||||
|
||||
conn.request(method="GET", url=url_path, headers=head)
|
||||
api = API(
|
||||
settings.getSetting('server_address'),
|
||||
user_details.get('user_id'),
|
||||
user_details.get('token')
|
||||
)
|
||||
|
||||
progress_dialog = xbmcgui.DialogProgress()
|
||||
message = 'Testing with {0} MB of data'.format(speed_test_data_size)
|
||||
progress_dialog.create("JellyCon connection speed test", message)
|
||||
total_data_read = 0
|
||||
total_time = time.time()
|
||||
start_time = time.time()
|
||||
|
||||
log.debug("Starting Connection Speed Test")
|
||||
response = conn.getresponse()
|
||||
response = api.speedtest(test_data_size)
|
||||
|
||||
last_percentage_done = 0
|
||||
if int(response.status) == 200:
|
||||
data = response.read(10240)
|
||||
while len(data) > 0:
|
||||
total_data_read = 0
|
||||
if response.status_code == 200:
|
||||
for data in response.iter_content(chunk_size=10240):
|
||||
total_data_read += len(data)
|
||||
percentage_done = int(float(total_data_read) / float(test_data_size) * 100.0)
|
||||
if last_percentage_done != percentage_done:
|
||||
progress_dialog.update(percentage_done)
|
||||
last_percentage_done = percentage_done
|
||||
data = response.read(10240)
|
||||
else:
|
||||
log.error("HTTP response error: {0} {1}", response.status, response.reason)
|
||||
error_message = "HTTP response error: %s\n%s" % (response.status, response.reason)
|
||||
log.error("HTTP response error: {0} {1}".format(response.status_code, response.content))
|
||||
error_message = "HTTP response error: %s\n%s" % (response.status_code, response.content)
|
||||
xbmcgui.Dialog().ok("Speed Test Error", error_message)
|
||||
return -1
|
||||
|
||||
total_data_read_kbits = (total_data_read * 8) / 1000
|
||||
total_time = time.time() - total_time
|
||||
total_time = time.time() - start_time
|
||||
speed = int(total_data_read_kbits / total_time)
|
||||
log.debug("Finished Connection Speed Test, speed: {0} total_data: {1}, total_time: {2}", speed, total_data_read, total_time)
|
||||
log.debug("Finished Connection Speed Test, speed: {0} total_data: {1}, total_time: {2}".format(speed, total_data_read, total_time))
|
||||
|
||||
progress_dialog.close()
|
||||
del progress_dialog
|
||||
@@ -115,75 +80,44 @@ def check_connection_speed():
|
||||
return speed
|
||||
|
||||
|
||||
def check_safe_delete_available():
|
||||
log.debug("check_safe_delete_available")
|
||||
|
||||
du = DownloadUtils()
|
||||
json_data = du.download_url("{server}/Plugins")
|
||||
result = json.loads(json_data)
|
||||
if result is not None:
|
||||
log.debug("Server Plugin List: {0}", result)
|
||||
|
||||
safe_delete_found = False
|
||||
for plugin in result:
|
||||
if plugin["Name"] == "Safe Delete":
|
||||
safe_delete_found = True
|
||||
break
|
||||
|
||||
log.debug("Safe Delete Plugin Available: {0}", safe_delete_found)
|
||||
home_window = HomeWindow()
|
||||
if safe_delete_found:
|
||||
home_window.set_property("safe_delete_plugin_available", "true")
|
||||
else:
|
||||
home_window.clear_property("safe_delete_plugin_available")
|
||||
|
||||
else:
|
||||
log.debug("Error getting server plugin list")
|
||||
|
||||
|
||||
def get_server_details():
|
||||
log.debug("Getting Server Details from Network")
|
||||
servers = []
|
||||
|
||||
message = "who is JellyfinServer?"
|
||||
message = b"who is JellyfinServer?"
|
||||
multi_group = ("<broadcast>", 7359)
|
||||
# multi_group = ("127.0.0.1", 7359)
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(4.0)
|
||||
|
||||
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 3) # timeout
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
sock.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)
|
||||
sock.setsockopt(socket.IPPROTO_IP, socket.SO_REUSEADDR, 1)
|
||||
|
||||
log.debug("MutliGroup: {0}", multi_group)
|
||||
log.debug("Sending UDP Data: {0}", message)
|
||||
log.debug("MutliGroup: {0}".format(multi_group))
|
||||
log.debug("Sending UDP Data: {0}".format(message))
|
||||
|
||||
progress = xbmcgui.DialogProgress()
|
||||
progress.create(__addon_name__ + " : " + string_load(30373))
|
||||
progress.update(0, string_load(30374))
|
||||
progress.create('{} : {}'.format(__addon_name__, translate_string(30373)))
|
||||
progress.update(0, translate_string(30374))
|
||||
xbmc.sleep(1000)
|
||||
server_count = 0
|
||||
|
||||
# while True:
|
||||
try:
|
||||
sock.sendto(message, multi_group)
|
||||
while True:
|
||||
try:
|
||||
server_count += 1
|
||||
progress.update(server_count * 10, string_load(30375) % server_count)
|
||||
progress.update(server_count * 10, '{}: {}'.format(translate_string(30375), server_count))
|
||||
xbmc.sleep(1000)
|
||||
data, addr = sock.recvfrom(1024)
|
||||
servers.append(json.loads(data))
|
||||
except:
|
||||
except: # noqa
|
||||
break
|
||||
except Exception as e:
|
||||
log.error("UPD Discovery Error: {0}", e)
|
||||
log.error("UPD Discovery Error: {0}".format(e))
|
||||
|
||||
progress.close()
|
||||
|
||||
log.debug("Found Servers: {0}", servers)
|
||||
log.debug("Found Servers: {0}".format(servers))
|
||||
return servers
|
||||
|
||||
|
||||
@@ -191,18 +125,17 @@ def check_server(force=False, change_user=False, notify=False):
|
||||
log.debug("checkServer Called")
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
server_url = ""
|
||||
something_changed = False
|
||||
du = DownloadUtils()
|
||||
|
||||
# Initialize api object
|
||||
api = API()
|
||||
|
||||
if force is False:
|
||||
# if not forcing use server details from settings
|
||||
svr = du.get_server()
|
||||
if svr is not None:
|
||||
server_url = svr
|
||||
api.server = settings.getSetting('server_address')
|
||||
|
||||
# if the server is not set then try to detect it
|
||||
if server_url == "":
|
||||
if not api.server:
|
||||
|
||||
# scan for local server
|
||||
server_info = get_server_details()
|
||||
@@ -212,7 +145,7 @@ def check_server(force=False, change_user=False, notify=False):
|
||||
|
||||
server_list = []
|
||||
for server in server_info:
|
||||
server_item = xbmcgui.ListItem(server.get("Name", string_load(30063)))
|
||||
server_item = xbmcgui.ListItem(server.get("Name", translate_string(30063)))
|
||||
sub_line = server.get("Address")
|
||||
server_item.setLabel2(sub_line)
|
||||
server_item.setProperty("address", server.get("Address"))
|
||||
@@ -221,277 +154,280 @@ def check_server(force=False, change_user=False, notify=False):
|
||||
server_list.append(server_item)
|
||||
|
||||
if len(server_list) > 0:
|
||||
return_index = xbmcgui.Dialog().select(__addon_name__ + " : " + string_load(30166),
|
||||
return_index = xbmcgui.Dialog().select('{} : {}'.format(__addon_name__, translate_string(30166)),
|
||||
server_list,
|
||||
useDetails=True)
|
||||
if return_index != -1:
|
||||
server_url = server_info[return_index]["Address"]
|
||||
api.server = server_info[return_index]["Address"]
|
||||
|
||||
if not server_url:
|
||||
return_index = xbmcgui.Dialog().yesno(__addon_name__, string_load(30282), string_load(30370))
|
||||
if not api.server:
|
||||
return_index = xbmcgui.Dialog().yesno(__addon_name__, '{}\n{}'.format(translate_string(30282), translate_string(30370)))
|
||||
if not return_index:
|
||||
xbmc.executebuiltin("ActivateWindow(Home)")
|
||||
return
|
||||
|
||||
while True:
|
||||
kb = xbmc.Keyboard()
|
||||
kb.setHeading(string_load(30372))
|
||||
if server_url:
|
||||
kb.setDefault(server_url)
|
||||
kb.setHeading(translate_string(30372))
|
||||
if api.server:
|
||||
kb.setDefault(api.server)
|
||||
else:
|
||||
kb.setDefault("http://<server address>:8096")
|
||||
kb.setDefault("http://")
|
||||
kb.doModal()
|
||||
if kb.isConfirmed():
|
||||
server_url = kb.getText()
|
||||
api.server = kb.getText()
|
||||
else:
|
||||
xbmc.executebuiltin("ActivateWindow(Home)")
|
||||
return
|
||||
|
||||
url_bits = urlparse(server_url)
|
||||
server_address = url_bits.hostname
|
||||
server_port = str(url_bits.port)
|
||||
server_protocol = url_bits.scheme
|
||||
user_name = url_bits.username
|
||||
user_password = url_bits.password
|
||||
|
||||
if user_name and user_password:
|
||||
temp_url = "%s://%s:%s@%s:%s/Users/Public?format=json" % (server_protocol, user_name, user_password, server_address, server_port)
|
||||
else:
|
||||
temp_url = "%s://%s:%s/Users/Public?format=json" % (server_protocol, server_address, server_port)
|
||||
|
||||
log.debug("Testing_Url: {0}", temp_url)
|
||||
progress = xbmcgui.DialogProgress()
|
||||
progress.create(__addon_name__ + " : " + string_load(30376))
|
||||
progress.update(0, string_load(30377))
|
||||
json_data = du.download_url(temp_url, authenticate=False)
|
||||
progress.create('{} : {}'.format(__addon_name__, translate_string(30376)))
|
||||
progress.update(0, translate_string(30377))
|
||||
result = api.get('/System/Info/Public')
|
||||
progress.close()
|
||||
|
||||
result = json.loads(json_data)
|
||||
if result is not None:
|
||||
xbmcgui.Dialog().ok(__addon_name__ + " : " + string_load(30167),
|
||||
"%s://%s:%s/" % (server_protocol, server_address, server_port))
|
||||
if result:
|
||||
xbmcgui.Dialog().ok('{} : {}'.format(__addon_name__, translate_string(30167)),
|
||||
api.server)
|
||||
break
|
||||
else:
|
||||
return_index = xbmcgui.Dialog().yesno(__addon_name__ + " : " + string_load(30135),
|
||||
server_url,
|
||||
string_load(30371))
|
||||
return_index = xbmcgui.Dialog().yesno('{} : {}'.format(__addon_name__, translate_string(30135)),
|
||||
api.server,
|
||||
translate_string(30371))
|
||||
if not return_index:
|
||||
xbmc.executebuiltin("ActivateWindow(Home)")
|
||||
return
|
||||
|
||||
log.debug("Selected server: {0}", server_url)
|
||||
|
||||
# parse the url
|
||||
url_bits = urlparse(server_url)
|
||||
server_address = url_bits.hostname
|
||||
server_port = str(url_bits.port)
|
||||
server_protocol = url_bits.scheme
|
||||
user_name = url_bits.username
|
||||
user_password = url_bits.password
|
||||
log.debug("Detected server info {0} - {1} - {2}", server_protocol, server_address, server_port)
|
||||
|
||||
# save the server info
|
||||
settings.setSetting("port", server_port)
|
||||
|
||||
if user_name and user_password:
|
||||
server_address = "%s:%s@%s" % (url_bits.username, url_bits.password, server_address)
|
||||
|
||||
settings.setSetting("ipaddress", server_address)
|
||||
|
||||
if server_protocol == "https":
|
||||
settings.setSetting("protocol", "1")
|
||||
else:
|
||||
settings.setSetting("protocol", "0")
|
||||
|
||||
log.debug("Selected server: {0}".format(api.server))
|
||||
settings.setSetting("server_address", api.server)
|
||||
something_changed = True
|
||||
|
||||
# do we need to change the user
|
||||
user_details = load_user_details(settings)
|
||||
current_username = user_details.get("username", "")
|
||||
current_username = unicode(current_username, "utf-8")
|
||||
current_username = settings.getSetting('username')
|
||||
user_details = load_user_details()
|
||||
home_window = HomeWindow()
|
||||
home_window.set_property('user_name', current_username)
|
||||
|
||||
# if asked or we have no current user then show user selection screen
|
||||
if something_changed or change_user or len(current_username) == 0:
|
||||
if something_changed or change_user or len(current_username) == 0 or not user_details:
|
||||
|
||||
# stop playback when switching users
|
||||
xbmc.Player().stop()
|
||||
du = DownloadUtils()
|
||||
|
||||
# get a list of users
|
||||
log.debug("Getting user list")
|
||||
json_data = du.download_url(server_url + "/Users/Public?format=json", authenticate=False)
|
||||
# Initialize auth variable
|
||||
auth = {}
|
||||
|
||||
log.debug("jsonData: {0}", json_data)
|
||||
try:
|
||||
result = json.loads(json_data)
|
||||
except:
|
||||
result = None
|
||||
# Check if quick connect is active on the server, initiate connection
|
||||
quick = api.get('/QuickConnect/Initiate')
|
||||
code = quick.get('Code')
|
||||
secret = quick.get('Secret')
|
||||
users, user_selection = user_select(api, current_username, code)
|
||||
|
||||
if result is None:
|
||||
xbmcgui.Dialog().ok(string_load(30135),
|
||||
string_load(30201),
|
||||
string_load(30169) + server_url)
|
||||
if user_selection > -1:
|
||||
# The user made a selection in the dialog
|
||||
something_changed = True
|
||||
selected_user = users[user_selection]
|
||||
quick_connect = selected_user.getProperty("quickconnect") == "true"
|
||||
count = 0
|
||||
if quick_connect:
|
||||
# Try to authenticate to server with secret code 10 times
|
||||
while count < 10:
|
||||
log.debug('Checking for quick connect auth: attempt {}'.format(count))
|
||||
check = api.get('/QuickConnect/Connect?secret={}'.format(secret))
|
||||
if check.get('Authenticated'):
|
||||
break
|
||||
count += 1
|
||||
xbmc.sleep(1000)
|
||||
|
||||
else:
|
||||
selected_id = -1
|
||||
users = []
|
||||
for user in result:
|
||||
config = user.get("Configuration")
|
||||
if config is not None:
|
||||
if config.get("IsHidden", False) is False:
|
||||
name = user.get("Name")
|
||||
admin = user.get("Policy", {}).get("IsAdministrator", False)
|
||||
auth = api.post('/Users/AuthenticateWithQuickConnect',
|
||||
{'secret': secret})
|
||||
|
||||
time_ago = ""
|
||||
last_active = user.get("LastActivityDate")
|
||||
if last_active:
|
||||
last_active_date = datetime_from_string(last_active)
|
||||
log.debug("LastActivityDate: {0}", last_active_date)
|
||||
ago = datetime.now() - last_active_date
|
||||
log.debug("LastActivityDate: {0}", ago)
|
||||
days = divmod(ago.seconds, 86400)
|
||||
hours = divmod(days[1], 3600)
|
||||
minutes = divmod(hours[1], 60)
|
||||
log.debug("LastActivityDate: {0} {1} {2}", days[0], hours[0], minutes[0])
|
||||
if days[0]:
|
||||
time_ago += " %sd" % days[0]
|
||||
if hours[0]:
|
||||
time_ago += " %sh" % hours[0]
|
||||
if minutes[0]:
|
||||
time_ago += " %sm" % minutes[0]
|
||||
time_ago = time_ago.strip()
|
||||
if not time_ago:
|
||||
time_ago = "Active: now"
|
||||
else:
|
||||
time_ago = "Active: %s ago" % time_ago
|
||||
log.debug("LastActivityDate: {0}", time_ago)
|
||||
# If authentication was successful, save the username
|
||||
if auth:
|
||||
selected_user_name = auth['User'].get('Name')
|
||||
else:
|
||||
# Login failed, we don't want to change anything
|
||||
something_changed = False
|
||||
log.info("There was an error logging in with quick connect")
|
||||
|
||||
user_item = xbmcgui.ListItem(name)
|
||||
user_image = du.get_user_artwork(user, 'Primary')
|
||||
if not user_image:
|
||||
user_image = "DefaultUser.png"
|
||||
art = {"Thumb": user_image}
|
||||
user_item.setArt(art)
|
||||
user_item.setLabel2("TEST")
|
||||
|
||||
sub_line = time_ago
|
||||
|
||||
if user.get("HasPassword", False) is True:
|
||||
sub_line += ", Password"
|
||||
user_item.setProperty("secure", "true")
|
||||
|
||||
m = hashlib.md5()
|
||||
m.update(name)
|
||||
hashed_username = m.hexdigest()
|
||||
saved_password = settings.getSetting("saved_user_password_" + hashed_username)
|
||||
if saved_password:
|
||||
sub_line += ": Saved"
|
||||
|
||||
else:
|
||||
user_item.setProperty("secure", "false")
|
||||
|
||||
if admin:
|
||||
sub_line += ", Admin"
|
||||
else:
|
||||
sub_line += ", User"
|
||||
|
||||
user_item.setProperty("manual", "false")
|
||||
user_item.setLabel2(sub_line)
|
||||
users.append(user_item)
|
||||
|
||||
if current_username == name:
|
||||
selected_id = len(users) - 1
|
||||
|
||||
if current_username:
|
||||
selection_title = string_load(30180) + " (" + current_username + ")"
|
||||
else:
|
||||
selection_title = string_load(30180)
|
||||
|
||||
# add manual login
|
||||
user_item = xbmcgui.ListItem(string_load(30365))
|
||||
art = {"Thumb": "DefaultUser.png"}
|
||||
user_item.setArt(art)
|
||||
user_item.setLabel2(string_load(30366))
|
||||
user_item.setProperty("secure", "true")
|
||||
user_item.setProperty("manual", "true")
|
||||
users.append(user_item)
|
||||
|
||||
return_value = xbmcgui.Dialog().select(selection_title,
|
||||
users,
|
||||
preselect=selected_id,
|
||||
autoclose=20000,
|
||||
useDetails=True)
|
||||
|
||||
if return_value > -1 and return_value != selected_id:
|
||||
|
||||
something_changed = True
|
||||
selected_user = users[return_value]
|
||||
selected_user_name = selected_user.getLabel()
|
||||
secured = selected_user.getProperty("secure") == "true"
|
||||
manual = selected_user.getProperty("manual") == "true"
|
||||
selected_user_name = selected_user.getLabel()
|
||||
|
||||
log.debug("Selected User Name: {0} : {1}", return_value, selected_user_name)
|
||||
|
||||
# If using a manual login, ask for username
|
||||
if manual:
|
||||
kb = xbmc.Keyboard()
|
||||
kb.setHeading(string_load(30005))
|
||||
kb.setHeading(translate_string(30005))
|
||||
if current_username:
|
||||
kb.setDefault(current_username)
|
||||
kb.doModal()
|
||||
if kb.isConfirmed():
|
||||
selected_user_name = kb.getText()
|
||||
log.debug("Manual entered username: {0}", selected_user_name)
|
||||
log.debug("Manual entered username: {0}".format(selected_user_name))
|
||||
else:
|
||||
return
|
||||
|
||||
if secured:
|
||||
# we need a password, check the settings first
|
||||
m = hashlib.md5()
|
||||
m.update(selected_user_name)
|
||||
hashed_username = m.hexdigest()
|
||||
saved_password = settings.getSetting("saved_user_password_" + hashed_username)
|
||||
allow_password_saving = settings.getSetting("allow_password_saving") == "true"
|
||||
home_window.set_property('user_name', selected_user_name)
|
||||
settings.setSetting('username', selected_user_name)
|
||||
user_details = load_user_details()
|
||||
|
||||
# if not saving passwords but have a saved ask to clear it
|
||||
if not allow_password_saving and saved_password:
|
||||
clear_password = xbmcgui.Dialog().yesno(string_load(30368), string_load(30369))
|
||||
if clear_password:
|
||||
settings.setSetting("saved_user_password_" + hashed_username, "")
|
||||
|
||||
if saved_password:
|
||||
log.debug("Saving username and password: {0}", selected_user_name)
|
||||
log.debug("Using stored password for user: {0}", hashed_username)
|
||||
save_user_details(settings, selected_user_name, saved_password)
|
||||
|
||||
else:
|
||||
if not user_details:
|
||||
# Ask for password if user has one
|
||||
password = ''
|
||||
if secured and not user_details.get('token'):
|
||||
kb = xbmc.Keyboard()
|
||||
kb.setHeading(string_load(30006))
|
||||
kb.setHeading(translate_string(30006))
|
||||
kb.setHiddenInput(True)
|
||||
kb.doModal()
|
||||
if kb.isConfirmed():
|
||||
log.debug("Saving username and password: {0}", selected_user_name)
|
||||
save_user_details(settings, selected_user_name, kb.getText())
|
||||
password = kb.getText()
|
||||
|
||||
# should we save the password
|
||||
if allow_password_saving:
|
||||
save_password = xbmcgui.Dialog().yesno(string_load(30363), string_load(30364))
|
||||
if save_password:
|
||||
log.debug("Saving password for fast user switching: {0}", hashed_username)
|
||||
settings.setSetting("saved_user_password_" + hashed_username, kb.getText())
|
||||
else:
|
||||
log.debug("Saving username with no password: {0}", selected_user_name)
|
||||
save_user_details(settings, selected_user_name, "")
|
||||
auth_payload = {'username': selected_user_name, 'pw': password}
|
||||
auth = api.authenticate(auth_payload)
|
||||
if not auth:
|
||||
# Login failed, we don't want to change anything
|
||||
something_changed = False
|
||||
log.info('There was an error logging in with user {}'.format(selected_user_name))
|
||||
xbmcgui.Dialog().ok(__addon_name__, translate_string(30446))
|
||||
|
||||
if something_changed:
|
||||
home_window = HomeWindow()
|
||||
home_window.clear_property("userid")
|
||||
home_window.clear_property("AccessToken")
|
||||
home_window.clear_property("userimage")
|
||||
home_window.clear_property("jellycon_widget_reload")
|
||||
du = DownloadUtils()
|
||||
du.authenticate()
|
||||
du.get_user_id()
|
||||
if auth:
|
||||
token = auth.get('AccessToken')
|
||||
user_id = auth.get('User').get('Id')
|
||||
else:
|
||||
token = user_details.get('token')
|
||||
user_id = user_details.get('user_id')
|
||||
save_user_details(selected_user_name, user_id, token)
|
||||
xbmc.executebuiltin("ActivateWindow(Home)")
|
||||
if "estuary_jellycon" in xbmc.getSkinDir():
|
||||
xbmc.executebuiltin("SetFocus(9000, 0, absolute)")
|
||||
xbmc.executebuiltin("ReloadSkin()")
|
||||
|
||||
|
||||
def user_select(api, current_username, code):
|
||||
'''
|
||||
Display user selection screen
|
||||
'''
|
||||
# Retrieve list of public users from server
|
||||
public = api.get('/Users/Public')
|
||||
|
||||
# Get list of saved users
|
||||
saved_users = get_saved_users()
|
||||
|
||||
# Combine public and saved users
|
||||
for user in saved_users:
|
||||
name = user.get('Name')
|
||||
# Check if saved user is in public list
|
||||
if name not in [x.get('Name', '') for x in public]:
|
||||
# If saved user is not already in list, add it
|
||||
public.append(user)
|
||||
|
||||
# Build user display
|
||||
selected_id = -1
|
||||
users = []
|
||||
# If quick connect is active, make it the first entry
|
||||
if code:
|
||||
user_item = xbmcgui.ListItem(code)
|
||||
user_image = "DefaultUser.png"
|
||||
art = {"Thumb": user_image}
|
||||
user_item.setArt(art)
|
||||
user_item.setLabel2(translate_string(30443))
|
||||
user_item.setProperty('quickconnect', "true")
|
||||
users.append(user_item)
|
||||
|
||||
for user in public:
|
||||
user_item = create_user_listitem(api.server, user)
|
||||
if user_item:
|
||||
users.append(user_item)
|
||||
name = user.get("Name")
|
||||
|
||||
# Highlight currently logged in user
|
||||
if current_username == name:
|
||||
selected_id = len(users) - 1
|
||||
|
||||
if current_username:
|
||||
selection_title = translate_string(30180) + " (" + current_username + ")"
|
||||
else:
|
||||
selection_title = translate_string(30180)
|
||||
|
||||
# Add manual login item
|
||||
user_item = xbmcgui.ListItem(translate_string(30365))
|
||||
art = {"Thumb": "DefaultUser.png"}
|
||||
user_item.setArt(art)
|
||||
user_item.setLabel2(translate_string(30366))
|
||||
user_item.setProperty("secure", "true")
|
||||
user_item.setProperty("manual", "true")
|
||||
users.append(user_item)
|
||||
|
||||
user_selection = xbmcgui.Dialog().select(
|
||||
selection_title,
|
||||
users,
|
||||
preselect=selected_id,
|
||||
autoclose=60000,
|
||||
useDetails=True)
|
||||
|
||||
return (users, user_selection)
|
||||
|
||||
|
||||
def create_user_listitem(server, user):
|
||||
'''
|
||||
Create a user listitem for the user selection screen
|
||||
'''
|
||||
config = user.get("Configuration")
|
||||
now = get_current_datetime()
|
||||
if config is not None:
|
||||
name = user.get("Name")
|
||||
time_ago = ""
|
||||
last_active = user.get("LastActivityDate")
|
||||
# Calculate how long it's been since the user was last active
|
||||
if last_active:
|
||||
last_active_date = datetime_from_string(last_active)
|
||||
ago = now - last_active_date
|
||||
# Check days
|
||||
if ago.days > 0:
|
||||
time_ago += ' {}d'.format(ago.days)
|
||||
# Check minutes
|
||||
if ago.seconds > 60:
|
||||
hours = 0
|
||||
# Check hours
|
||||
if ago.seconds > 3600:
|
||||
hours = int(ago.seconds/3600)
|
||||
time_ago += ' {}h'.format(hours)
|
||||
minutes = int((ago.seconds - (hours * 3600)) / 60)
|
||||
time_ago += ' {}m'.format(minutes)
|
||||
time_ago = time_ago.strip()
|
||||
if not time_ago:
|
||||
time_ago = "Active: now"
|
||||
else:
|
||||
time_ago = "Active: {} ago".format(time_ago)
|
||||
|
||||
user_item = xbmcgui.ListItem(name)
|
||||
|
||||
# If the user doesn't have a profile image, user the default
|
||||
if 'PrimaryImageTag' not in user:
|
||||
user_image = "DefaultUser.png"
|
||||
else:
|
||||
user_id = user.get('Id')
|
||||
tag = user.get('PrimaryImageTag')
|
||||
user_image = '{}/Users/{}/Images/Primary?Format=original&tag={}'.format(
|
||||
server, user_id, tag
|
||||
)
|
||||
|
||||
art = {"Thumb": user_image}
|
||||
user_item.setArt(art)
|
||||
|
||||
sub_line = time_ago
|
||||
|
||||
if user.get("HasPassword", False) is True:
|
||||
user_item.setProperty("secure", "true")
|
||||
else:
|
||||
user_item.setProperty("secure", "false")
|
||||
|
||||
user_item.setProperty("manual", "false")
|
||||
user_item.setLabel2(sub_line)
|
||||
|
||||
return user_item
|
||||
return None
|
||||
|
||||
@@ -1,40 +1,45 @@
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
import xbmcgui
|
||||
import xbmcplugin
|
||||
import xbmcaddon
|
||||
|
||||
from .downloadutils import DownloadUtils
|
||||
from .simple_logging import SimpleLogging
|
||||
from .utils import get_art
|
||||
from .datamanager import DataManager
|
||||
from .jellyfin import api
|
||||
from .lazylogger import LazyLogger
|
||||
from .item_functions import get_art
|
||||
from .utils import load_user_details
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
def show_server_sessions():
|
||||
log.debug("showServerSessions Called")
|
||||
|
||||
handle = int(sys.argv[1])
|
||||
download_utils = DownloadUtils()
|
||||
data_manager = DataManager()
|
||||
|
||||
url = "{server}/Users/{userid}"
|
||||
results = data_manager.get_content(url)
|
||||
user_details = load_user_details()
|
||||
url = "/Users/{}".format(user_details.get('user_id'))
|
||||
results = api.get(url)
|
||||
|
||||
is_admin = results.get("Policy", {}).get("IsAdministrator", False)
|
||||
if not is_admin:
|
||||
xbmcplugin.endOfDirectory(handle, cacheToDisc=False)
|
||||
return
|
||||
|
||||
url = "{server}/Sessions"
|
||||
results = data_manager.get_content(url)
|
||||
log.debug("session_info: {0}", results)
|
||||
url = "/Sessions"
|
||||
results = api.get(url)
|
||||
log.debug("session_info: {0}".format(results))
|
||||
|
||||
if results is None:
|
||||
return
|
||||
|
||||
list_items = []
|
||||
settings = xbmcaddon.Addon()
|
||||
server = settings.getSetting('server_address')
|
||||
for session in results:
|
||||
device_name = session.get("DeviceName", "na")
|
||||
user_name = session.get("UserName", "na")
|
||||
@@ -45,10 +50,10 @@ def show_server_sessions():
|
||||
now_playing = session.get("NowPlayingItem", None)
|
||||
transcoding_info = session.get("TranscodingInfo", None)
|
||||
|
||||
session_info = user_name + " - " + client_name
|
||||
session_info = "{} - {}".format(user_name, client_name)
|
||||
user_session_details = ""
|
||||
|
||||
percenatge_played = 0
|
||||
percentage_played = 0
|
||||
position_ticks = 0
|
||||
runtime = 0
|
||||
play_method = "na"
|
||||
@@ -59,42 +64,54 @@ def show_server_sessions():
|
||||
|
||||
art = {}
|
||||
if now_playing:
|
||||
server = download_utils.get_server()
|
||||
art = get_art(now_playing, server)
|
||||
|
||||
runtime = now_playing.get("RunTimeTicks", 0)
|
||||
if position_ticks > 0 and runtime > 0:
|
||||
percenatge_played = (position_ticks / float(runtime)) * 100.0
|
||||
percenatge_played = int(percenatge_played)
|
||||
percentage_played = (position_ticks / float(runtime)) * 100.0
|
||||
percentage_played = int(percentage_played)
|
||||
|
||||
session_info += " (" + now_playing.get("Name", "na") + " " + str(percenatge_played) + "%)"
|
||||
user_session_details += now_playing.get("Name", "na") + " " + str(percenatge_played) + "%" + "\n"
|
||||
session_info += " {} {}%".format(
|
||||
now_playing.get("Name", "na"), percentage_played
|
||||
)
|
||||
user_session_details += "{} {}%\n".format(
|
||||
now_playing.get("Name", "na"), percentage_played
|
||||
)
|
||||
|
||||
else:
|
||||
session_info += " (idle)"
|
||||
user_session_details += "Idle" + "\n"
|
||||
user_session_details += "Idle\n"
|
||||
|
||||
transcoding_details = ""
|
||||
if transcoding_info:
|
||||
if not transcoding_info.get("IsVideoDirect", None):
|
||||
transcoding_details += "Video:" + transcoding_info.get("VideoCodec", "") + ":" + str(transcoding_info.get("Width", 0)) + "x" + str(transcoding_info.get("Height", 0)) + "\n"
|
||||
transcoding_details += "Video:{}:{}x{}\n".format(
|
||||
transcoding_info.get("VideoCodec", ""),
|
||||
transcoding_info.get("Width", 0),
|
||||
transcoding_info.get("Height", 0)
|
||||
)
|
||||
else:
|
||||
transcoding_details += "Video:direct\n"
|
||||
|
||||
if not transcoding_info.get("IsAudioDirect", None):
|
||||
transcoding_details += "Audio:" + transcoding_info.get("AudioCodec", "") + ":" + str(transcoding_info.get("AudioChannels", 0)) + "\n"
|
||||
transcoding_details += "Audio:{}:{}\n".format(
|
||||
transcoding_info.get("AudioCodec", ""),
|
||||
transcoding_info.get("AudioChannels", 0)
|
||||
)
|
||||
else:
|
||||
transcoding_details += "Audio:direct\n"
|
||||
|
||||
transcoding_details += "Bitrate:" + str(transcoding_info.get("Bitrate", 0)) + "\n"
|
||||
transcoding_details += "Bitrate:{}\n".format(
|
||||
transcoding_info.get("Bitrate", 0)
|
||||
)
|
||||
|
||||
list_item = xbmcgui.ListItem(label=session_info)
|
||||
list_item.setArt(art)
|
||||
|
||||
user_session_details += device_name + "(" + client_version + ")\n"
|
||||
user_session_details += client_name + "\n"
|
||||
user_session_details += play_method + "\n"
|
||||
user_session_details += transcoding_details + "\n"
|
||||
user_session_details += "{}({})\n".format(device_name, client_version)
|
||||
user_session_details += "{}\n".format(client_name)
|
||||
user_session_details += "{}\n".format(play_method)
|
||||
user_session_details += "{}\n".format(transcoding_details)
|
||||
|
||||
info_labels = {}
|
||||
info_labels["duration"] = str(runtime / 10000000)
|
||||
@@ -104,7 +121,7 @@ def show_server_sessions():
|
||||
|
||||
list_item.setProperty('TotalTime', str(runtime / 10000000))
|
||||
list_item.setProperty('ResumeTime', str(position_ticks / 10000000))
|
||||
list_item.setProperty("complete_percentage", str(percenatge_played))
|
||||
list_item.setProperty("complete_percentage", str(percentage_played))
|
||||
|
||||
item_tuple = ("", list_item, False)
|
||||
list_items.append(item_tuple)
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
from .jsonrpc import JsonRpc
|
||||
|
||||
|
||||
class SimpleLogging:
|
||||
name = ""
|
||||
enable_logging = False
|
||||
|
||||
def __init__(self, name):
|
||||
settings = xbmcaddon.Addon()
|
||||
prefix = settings.getAddonInfo('name')
|
||||
self.name = prefix + '.' + name
|
||||
self.enable_logging = settings.getSetting('log_debug') == "true"
|
||||
|
||||
# params = {"setting": "debug.showloginfo"}
|
||||
# setting_result = json_rpc('Settings.getSettingValue').execute(params)
|
||||
# current_value = setting_result.get("result", None)
|
||||
# if current_value is not None:
|
||||
# self.enable_logging = current_value.get("value", False)
|
||||
# xbmc.log("LOGGING_ENABLED %s : %s" % (self.name, str(self.enable_logging)), level=xbmc.LOGDEBUG)
|
||||
|
||||
def __str__(self):
|
||||
return "LoggingEnabled: " + str(self.enable_logging)
|
||||
|
||||
def info(self, fmt, *args, **kwargs):
|
||||
log_line = self.name + "|INFO|" + self.log_line(fmt, *args)
|
||||
xbmc.log(log_line, level=xbmc.LOGNOTICE)
|
||||
|
||||
def error(self, fmt, *args, **kwargs):
|
||||
log_line = self.name + "|ERROR|" + self.log_line(fmt, *args)
|
||||
xbmc.log(log_line, level=xbmc.LOGERROR)
|
||||
|
||||
def debug(self, fmt, *args, **kwargs):
|
||||
if self.enable_logging:
|
||||
log_line = self.name + "|DEBUG|" + self.log_line(fmt, *args)
|
||||
xbmc.log(log_line, level=xbmc.LOGNOTICE)
|
||||
|
||||
@staticmethod
|
||||
def log_line(fmt, *args):
|
||||
new_args = []
|
||||
# convert any unicode to utf-8 strings
|
||||
for arg in args:
|
||||
if isinstance(arg, unicode):
|
||||
new_args.append(arg.encode("utf-8"))
|
||||
else:
|
||||
new_args.append(arg)
|
||||
log_line = fmt.format(*new_args)
|
||||
return log_line
|
||||
@@ -1,25 +1,30 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import os
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
import xbmcvfs
|
||||
|
||||
from .jsonrpc import JsonRpc, get_value, set_value
|
||||
from .simple_logging import SimpleLogging
|
||||
from .lazylogger import LazyLogger
|
||||
from .utils import translate_path, kodi_version
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
ver = xbmc.getInfoLabel('System.BuildVersion')[:2]
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
def clone_default_skin():
|
||||
xbmc.executebuiltin("Dialog.Close(all,true)")
|
||||
xbmc.executebuiltin("ActivateWindow(Home)")
|
||||
|
||||
response = xbmcgui.Dialog().yesno("JellyCon Skin Cloner",
|
||||
"This will clone the default Estuary Kodi skin and add JellyCon functionality to it.",
|
||||
"Do you want to continue?")
|
||||
response = xbmcgui.Dialog().yesno(
|
||||
"JellyCon Skin Cloner",
|
||||
("This will clone the default Estuary Kodi skin and"
|
||||
"add JellyCon functionality to it."),
|
||||
"Do you want to continue?")
|
||||
if not response:
|
||||
return
|
||||
|
||||
@@ -27,9 +32,6 @@ def clone_default_skin():
|
||||
set_skin_settings()
|
||||
update_kodi_settings()
|
||||
|
||||
# xbmc.executebuiltin("ReloadSkin()")
|
||||
# xbmc.executebuiltin("ActivateWindow(Home)")
|
||||
|
||||
|
||||
def walk_path(root_path, relative_path, all_files):
|
||||
files = xbmcvfs.listdir(root_path)
|
||||
@@ -50,9 +52,9 @@ def walk_path(root_path, relative_path, all_files):
|
||||
def clone_skin():
|
||||
log.debug("Cloning Estuary Skin")
|
||||
|
||||
kodi_path = xbmc.translatePath("special://xbmc")
|
||||
kodi_path = translate_path("special://xbmc")
|
||||
kodi_skin_source = os.path.join(kodi_path, "addons", "skin.estuary")
|
||||
log.debug("Kodi Skin Source: {0}", kodi_skin_source)
|
||||
log.debug("Kodi Skin Source: {0}".format(kodi_skin_source))
|
||||
|
||||
pdialog = xbmcgui.DialogProgress()
|
||||
pdialog.create("JellyCon Skin Cloner", "")
|
||||
@@ -60,18 +62,20 @@ def clone_skin():
|
||||
all_files = []
|
||||
walk_path(kodi_skin_source, "", all_files)
|
||||
for found in all_files:
|
||||
log.debug("Found Path: {0}", found)
|
||||
log.debug("Found Path: {0}".format(found))
|
||||
|
||||
kodi_home_path = xbmc.translatePath("special://home")
|
||||
kodi_skin_destination = os.path.join(kodi_home_path, "addons", "skin.estuary_jellycon")
|
||||
log.debug("Kodi Skin Destination: {0}", kodi_skin_destination)
|
||||
kodi_home_path = translate_path("special://home")
|
||||
kodi_skin_destination = os.path.join(
|
||||
kodi_home_path, "addons", "skin.estuary_jellycon"
|
||||
)
|
||||
log.debug("Kodi Skin Destination: {0}".format(kodi_skin_destination))
|
||||
|
||||
# copy all skin files (clone)
|
||||
count = 0
|
||||
total = len(all_files)
|
||||
for skin_file in all_files:
|
||||
percentage_done = int(float(count) / float(total) * 100.0)
|
||||
pdialog.update(percentage_done, "%s" % skin_file)
|
||||
pdialog.update(percentage_done, skin_file)
|
||||
|
||||
source = os.path.join(kodi_skin_source, skin_file)
|
||||
destination = os.path.join(kodi_skin_destination, skin_file)
|
||||
@@ -81,22 +85,20 @@ def clone_skin():
|
||||
|
||||
# alter skin addon.xml
|
||||
addon_xml_path = os.path.join(kodi_skin_destination, "addon.xml")
|
||||
with open(addon_xml_path, "r") as addon_file:
|
||||
addon_xml_data = addon_file.read()
|
||||
addon_tree = ET.parse(addon_xml_path)
|
||||
addon_root = addon_tree.getroot()
|
||||
|
||||
addon_xml_data = addon_xml_data.replace("id=\"skin.estuary\"", "id=\"skin.estuary_jellycon\"")
|
||||
addon_xml_data = addon_xml_data.replace("name=\"Estuary\"", "name=\"Estuary JellyCon\"")
|
||||
addon_root.attrib['id'] = 'skin.estuary_jellycon'
|
||||
addon_root.attrib['name'] = 'Estuary JellyCon'
|
||||
|
||||
# log.debug("{0}", addon_xml_data)
|
||||
|
||||
# update the addon.xml
|
||||
with open(addon_xml_path, "w") as addon_file:
|
||||
addon_file.write(addon_xml_data)
|
||||
addon_tree.write(addon_xml_path)
|
||||
|
||||
# get jellycon path
|
||||
jellycon_path = os.path.join(kodi_home_path, "addons", "plugin.video.jellycon")
|
||||
jellycon_path = os.path.join(
|
||||
kodi_home_path, "addons", "plugin.video.jellycon"
|
||||
)
|
||||
|
||||
log.debug("Major Version: {0}", ver)
|
||||
log.debug("Major Version: {0}".format(kodi_version()))
|
||||
|
||||
file_list = ["Home.xml",
|
||||
"Includes_Home.xml",
|
||||
@@ -104,8 +106,12 @@ def clone_skin():
|
||||
"DialogSeekBar.xml",
|
||||
"VideoOSD.xml"]
|
||||
|
||||
# Copy customized skin files from our addon into cloned skin
|
||||
for file_name in file_list:
|
||||
source = os.path.join(jellycon_path, "resources", "skins", "skin.estuary", ver, "xml", file_name)
|
||||
source = os.path.join(
|
||||
jellycon_path, "resources", "skins", "skin.estuary",
|
||||
str(kodi_version), "xml", file_name
|
||||
)
|
||||
destination = os.path.join(kodi_skin_destination, "xml", file_name)
|
||||
xbmcvfs.copy(source, destination)
|
||||
|
||||
@@ -114,7 +120,10 @@ def clone_skin():
|
||||
pdialog.close()
|
||||
del pdialog
|
||||
|
||||
response = xbmcgui.Dialog().yesno("JellyCon Skin Cloner", "Do you want to switch to the new cloned skin?")
|
||||
response = xbmcgui.Dialog().yesno(
|
||||
"JellyCon Skin Cloner",
|
||||
"Do you want to switch to the new cloned skin?"
|
||||
)
|
||||
if not response:
|
||||
return
|
||||
|
||||
@@ -123,18 +132,21 @@ def clone_skin():
|
||||
'enabled': True
|
||||
}
|
||||
result = JsonRpc('Addons.SetAddonEnabled').execute(params)
|
||||
log.debug("Addons.SetAddonEnabled : {0}", result)
|
||||
log.debug("Addons.SetAddonEnabled : {0}".format(result))
|
||||
|
||||
log.debug("SkinCloner : Current Skin : " + get_value("lookandfeel.skin"))
|
||||
log.debug("SkinCloner : Current Skin : {}".format(
|
||||
get_value("lookandfeel.skin"))
|
||||
)
|
||||
set_result = set_value("lookandfeel.skin", "skin.estuary_jellycon")
|
||||
log.debug("Save Setting : lookandfeel.skin : {0}", set_result)
|
||||
log.debug("SkinCloner : Current Skin : " + get_value("lookandfeel.skin"))
|
||||
log.debug("Save Setting : lookandfeel.skin : {0}".format(set_result))
|
||||
log.debug("SkinCloner : Current Skin : {}".format(
|
||||
get_value("lookandfeel.skin"))
|
||||
)
|
||||
|
||||
|
||||
def update_kodi_settings():
|
||||
log.debug("Settings Kodi Settings")
|
||||
|
||||
# set_value("screensaver.mode", "script.screensaver.logoff")
|
||||
set_value("videoplayer.seekdelay", 0)
|
||||
set_value("filelists.showparentdiritems", False)
|
||||
set_value("filelists.showaddsourcebuttons", False)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import sys
|
||||
import functools
|
||||
import time
|
||||
from .simple_logging import SimpleLogging
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
from .lazylogger import LazyLogger
|
||||
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
enabled = False
|
||||
|
||||
@@ -27,6 +30,8 @@ def timer(func):
|
||||
data = args[1]
|
||||
elif func.__name__ == "main_entry_point" and len(sys.argv) > 2:
|
||||
data = sys.argv[2]
|
||||
log.info("timing_data|{0}|{1}|{2}|{3}", func.__name__, started, ended, data)
|
||||
log.info("timing_data|{0}|{1}|{2}|{3}".format(
|
||||
func.__name__, started, ended, data)
|
||||
)
|
||||
return value
|
||||
return wrapper
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
|
||||
import urllib
|
||||
import encodings
|
||||
|
||||
import xbmc
|
||||
import xbmcgui
|
||||
|
||||
from .simple_logging import SimpleLogging
|
||||
from .datamanager import DataManager
|
||||
|
||||
from .translation import string_load
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
dataManager = DataManager()
|
||||
|
||||
details_string = 'EpisodeCount,SeasonCount,Path,Etag,MediaStreams'
|
||||
icon = xbmc.translatePath('special://home/addons/plugin.video.jellycon/icon.png')
|
||||
|
||||
|
||||
def not_found(content_string):
|
||||
xbmcgui.Dialog().notification('JellyCon', string_load(30305) % content_string, icon=icon, sound=False)
|
||||
|
||||
|
||||
def playback_starting(content_string):
|
||||
xbmcgui.Dialog().notification('JellyCon', string_load(30306) % content_string, icon=icon, sound=False)
|
||||
|
||||
|
||||
def search(item_type, query):
|
||||
content_url = ('{server}/Search/Hints?searchTerm=' + query +
|
||||
'&IncludeItemTypes=' + item_type +
|
||||
'&UserId={userid}'
|
||||
'&StartIndex=0' +
|
||||
'&Limit=25' +
|
||||
'&IncludePeople=false&IncludeMedia=true&IncludeGenres=false&IncludeStudios=false&IncludeArtists=false')
|
||||
|
||||
result = dataManager.get_content(content_url)
|
||||
return result
|
||||
|
||||
|
||||
def get_items(video_type, item_id=None, parent_id=None):
|
||||
content_url = None
|
||||
result = dict()
|
||||
|
||||
if video_type == 'season':
|
||||
content_url = ('{server}/Shows/' + item_id +
|
||||
'/Seasons'
|
||||
'?userId={userid}' +
|
||||
'&Fields=' + details_string +
|
||||
'&format=json')
|
||||
|
||||
elif video_type == 'movie' or video_type == 'episode':
|
||||
content_url = ('{server}/Users/{userid}/items' +
|
||||
'?ParentId=' + parent_id +
|
||||
'&IsVirtualUnAired=false' +
|
||||
'&IsMissing=false' +
|
||||
'&Fields=' + details_string +
|
||||
'&format=json')
|
||||
|
||||
if content_url:
|
||||
result = dataManager.get_content(content_url)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_item(item_id):
|
||||
result = dataManager.get_content('{server}/Users/{userid}/Items/' + item_id + '?Fields=ProviderIds&format=json')
|
||||
return result
|
||||
|
||||
|
||||
def get_imdb_id(item_id):
|
||||
item = get_item(item_id)
|
||||
imdb = item.get('ProviderIds', {}).get('Imdb')
|
||||
return imdb
|
||||
|
||||
|
||||
def get_season_id(parent_id, season):
|
||||
season_items = get_items('season', parent_id)
|
||||
season_items = season_items.get('Items')
|
||||
|
||||
if season_items is None:
|
||||
season_items = []
|
||||
|
||||
for season_item in season_items:
|
||||
if season_item.get('IndexNumber') == int(season):
|
||||
season_id = season_item.get('Id')
|
||||
return season_id
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_episode_id(parent_id, episode):
|
||||
episode_items = get_items('episode', parent_id=parent_id)
|
||||
episode_items = episode_items.get('Items')
|
||||
|
||||
if episode_items is None:
|
||||
episode_items = []
|
||||
|
||||
for episode_item in episode_items:
|
||||
if episode_item.get('IndexNumber') == int(episode):
|
||||
episode_id = episode_item.get('Id')
|
||||
return episode_id
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_match(item_type, title, year, imdb_id):
|
||||
query = urllib.quote(title)
|
||||
|
||||
results = search(item_type, query=query)
|
||||
results = results.get('SearchHints')
|
||||
if results is None:
|
||||
results = []
|
||||
log.debug('SearchHints jsonData: {0}', results)
|
||||
|
||||
potential_matches = []
|
||||
|
||||
for item in results:
|
||||
name = item.get('Name')
|
||||
production_year = item.get('ProductionYear')
|
||||
if (name == title and int(year) == production_year) or (int(year) == production_year):
|
||||
potential_matches.append(item)
|
||||
|
||||
log.debug('Potential matches: {0}', potential_matches)
|
||||
|
||||
for item in potential_matches:
|
||||
item_imdb_id = get_imdb_id(item.get('ItemId'))
|
||||
if item_imdb_id == imdb_id:
|
||||
log.debug('Found match: {0}', item)
|
||||
return item
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def entry_point(parameters):
|
||||
item_type = None
|
||||
action = parameters.get('action', None)
|
||||
video_type = parameters.get('video_type', None)
|
||||
|
||||
title = urllib.unquote(parameters.get('title', ''))
|
||||
|
||||
year = parameters.get('year', '')
|
||||
episode = parameters.get('episode', '')
|
||||
season = parameters.get('season', '')
|
||||
imdb_id = parameters.get('imdb_id', '')
|
||||
|
||||
if video_type == 'show' or video_type == 'season' or video_type == 'episode':
|
||||
item_type = 'Series'
|
||||
elif video_type == 'movie':
|
||||
item_type = 'Movie'
|
||||
|
||||
if not item_type:
|
||||
return
|
||||
|
||||
match = get_match(item_type, title, year, imdb_id)
|
||||
|
||||
if not match:
|
||||
title_search_word = ''
|
||||
title_words = title.split(' ')
|
||||
|
||||
for word in title_words:
|
||||
if len(word) > len(title_search_word):
|
||||
title_search_word = word
|
||||
|
||||
title_search_word = title_search_word.replace(':', '')
|
||||
|
||||
if title_search_word:
|
||||
match = get_match(item_type, title_search_word, year, imdb_id)
|
||||
|
||||
str_season = str(season)
|
||||
if len(str_season) == 1:
|
||||
str_season = '0' + str_season
|
||||
str_episode = str(episode)
|
||||
if len(str_episode) == 1:
|
||||
str_episode = '0' + str_episode
|
||||
|
||||
if action == 'play':
|
||||
play_item_id = None
|
||||
|
||||
if video_type == 'movie':
|
||||
if match:
|
||||
play_item_id = match.get('ItemId')
|
||||
|
||||
if not play_item_id:
|
||||
not_found('{title} ({year})'.format(title=title, year=year))
|
||||
|
||||
elif video_type == 'episode':
|
||||
if not season or not episode:
|
||||
return
|
||||
|
||||
if match:
|
||||
item_id = match.get('ItemId')
|
||||
season_id = get_season_id(item_id, season)
|
||||
|
||||
if season_id:
|
||||
episode_id = get_episode_id(season_id, episode)
|
||||
if episode_id:
|
||||
play_item_id = episode_id
|
||||
|
||||
if not play_item_id:
|
||||
not_found('{title} ({year}) - S{season}E{episode}'.format(title=title, year=year, season=str_season, episode=str_episode))
|
||||
|
||||
if play_item_id:
|
||||
if video_type == 'episode':
|
||||
playback_starting('{title} ({year}) - S{season}E{episode}'.format(title=title, year=year, season=str_season, episode=str_episode))
|
||||
else:
|
||||
playback_starting('{title} ({year})'.format(title=title, year=year))
|
||||
xbmc.executebuiltin('RunPlugin(plugin://plugin.video.jellycon/?mode=PLAY&item_id={item_id})'.format(item_id=play_item_id))
|
||||
|
||||
elif action == 'open':
|
||||
url = media_type = None
|
||||
|
||||
if video_type == 'show':
|
||||
if match:
|
||||
item_id = match.get('ItemId')
|
||||
media_type = 'series'
|
||||
url = ('{server}/Shows/' + item_id +
|
||||
'/Seasons'
|
||||
'?userId={userid}' +
|
||||
'&Fields=' + details_string +
|
||||
'&format=json')
|
||||
|
||||
if not url:
|
||||
not_found('{title} ({year})'.format(title=title, year=year))
|
||||
|
||||
elif video_type == 'season':
|
||||
if not season:
|
||||
return
|
||||
|
||||
if match:
|
||||
item_id = match.get('ItemId')
|
||||
season_id = get_season_id(item_id, season)
|
||||
|
||||
if season_id:
|
||||
media_type = 'episodes'
|
||||
|
||||
url = ('{server}/Users/{userid}/items' +
|
||||
'?ParentId=' + season_id +
|
||||
'&IsVirtualUnAired=false' +
|
||||
'&IsMissing=false' +
|
||||
'&Fields=' + details_string +
|
||||
'&format=json')
|
||||
|
||||
if not url:
|
||||
not_found('{title} ({year}) - S{season}'.format(title=title, year=year, season=str_season))
|
||||
|
||||
if url and media_type:
|
||||
xbmc.executebuiltin('ActivateWindow(Videos, plugin://plugin.video.jellycon/?mode=GET_CONTENT&url={url}&media_type={media_type})'.format(url=urllib.quote(url), media_type=media_type))
|
||||
@@ -1,13 +0,0 @@
|
||||
import xbmcaddon
|
||||
from .simple_logging import SimpleLogging
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
addon = xbmcaddon.Addon()
|
||||
|
||||
|
||||
def string_load(string_id):
|
||||
try:
|
||||
return addon.getLocalizedString(string_id).encode('utf-8', 'ignore')
|
||||
except Exception as e:
|
||||
log.error('Failed String Load: {0} ({1})', string_id, e)
|
||||
return str(string_id)
|
||||
@@ -1,194 +1,60 @@
|
||||
# Gnu General Public License - see LICENSE.TXT
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import sys
|
||||
import binascii
|
||||
import string
|
||||
import random
|
||||
import json
|
||||
import time
|
||||
import math
|
||||
import os
|
||||
import hashlib
|
||||
import re
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
import requests
|
||||
from dateutil import tz
|
||||
import xbmcaddon
|
||||
import xbmc
|
||||
import xbmcvfs
|
||||
from kodi_six.utils import py2_encode, py2_decode
|
||||
from six import ensure_text, ensure_binary, text_type
|
||||
from six.moves.urllib.parse import urlencode
|
||||
|
||||
import string
|
||||
import random
|
||||
import urllib
|
||||
import json
|
||||
import base64
|
||||
import time
|
||||
import math
|
||||
from datetime import datetime
|
||||
import calendar
|
||||
import re
|
||||
|
||||
from .downloadutils import DownloadUtils
|
||||
from .simple_logging import SimpleLogging
|
||||
from .clientinfo import ClientInformation
|
||||
from .lazylogger import LazyLogger
|
||||
from .kodi_utils import HomeWindow
|
||||
|
||||
# hack to get datetime strptime loaded
|
||||
throwaway = time.strptime('20110101', '%Y%m%d')
|
||||
|
||||
# define our global download utils
|
||||
downloadUtils = DownloadUtils()
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
def get_jellyfin_url(base_url, params):
|
||||
def kodi_version():
|
||||
# Kodistubs returns empty string, causing Python 3 tests to choke on int()
|
||||
# TODO: Make Kodistubs version configurable for testing purposes
|
||||
if sys.version_info.major == 2:
|
||||
default_versionstring = "18"
|
||||
else:
|
||||
default_versionstring = "19.1 (19.1.0) Git:20210509-85e05228b4"
|
||||
|
||||
version_string = xbmc.getInfoLabel(
|
||||
'System.BuildVersion') or default_versionstring
|
||||
return int(version_string.split(' ', 1)[0].split('.', 1)[0])
|
||||
|
||||
|
||||
def get_jellyfin_url(path, params):
|
||||
params["format"] = "json"
|
||||
param_list = []
|
||||
for key in params:
|
||||
if params[key] is not None:
|
||||
value = params[key]
|
||||
if isinstance(value, unicode):
|
||||
value = value.encode("utf8")
|
||||
else:
|
||||
value = str(value)
|
||||
param_list.append(key + "=" + urllib.quote_plus(value, safe="{}"))
|
||||
param_string = "&".join(param_list)
|
||||
return base_url + "?" + param_string
|
||||
|
||||
|
||||
###########################################################################
|
||||
class PlayUtils:
|
||||
|
||||
@staticmethod
|
||||
def get_play_url(media_source, play_session_id):
|
||||
log.debug("get_play_url - media_source: {0}", media_source)
|
||||
|
||||
# check if strm file Container
|
||||
if media_source.get('Container') == 'strm':
|
||||
log.debug("Detected STRM Container")
|
||||
playurl, listitem_props = PlayUtils().get_strm_details(media_source)
|
||||
if playurl is None:
|
||||
log.debug("Error, no strm content")
|
||||
return None, None, None
|
||||
else:
|
||||
return playurl, "0", listitem_props
|
||||
|
||||
# get all the options
|
||||
addon_settings = xbmcaddon.Addon()
|
||||
server = downloadUtils.get_server()
|
||||
use_https = addon_settings.getSetting('protocol') == "1"
|
||||
verify_cert = addon_settings.getSetting('verify_cert') == 'true'
|
||||
allow_direct_file_play = addon_settings.getSetting('allow_direct_file_play') == 'true'
|
||||
|
||||
can_direct_play = media_source["SupportsDirectPlay"]
|
||||
can_direct_stream = media_source["SupportsDirectStream"]
|
||||
can_transcode = media_source["SupportsTranscoding"]
|
||||
container = media_source["Container"]
|
||||
|
||||
playurl = None
|
||||
playback_type = None
|
||||
|
||||
# check if file can be directly played
|
||||
if allow_direct_file_play and can_direct_play:
|
||||
direct_path = media_source["Path"]
|
||||
direct_path = direct_path.replace("\\", "/")
|
||||
direct_path = direct_path.strip()
|
||||
|
||||
# handle DVD structure
|
||||
if container == "dvd":
|
||||
direct_path = direct_path + "/VIDEO_TS/VIDEO_TS.IFO"
|
||||
elif container == "bluray":
|
||||
direct_path = direct_path + "/BDMV/index.bdmv"
|
||||
|
||||
if direct_path.startswith("//"):
|
||||
direct_path = "smb://" + direct_path[2:]
|
||||
|
||||
log.debug("playback_direct_path: {0}", direct_path)
|
||||
|
||||
if xbmcvfs.exists(direct_path):
|
||||
playurl = direct_path
|
||||
playback_type = "0"
|
||||
|
||||
# check if file can be direct streamed
|
||||
if can_direct_stream and playurl is None:
|
||||
item_id = media_source.get('Id')
|
||||
playurl = ("%s/Videos/%s/stream" +
|
||||
"?static=true" +
|
||||
"&PlaySessionId=%s" +
|
||||
"&MediaSourceId=%s")
|
||||
playurl = playurl % (server, item_id, play_session_id, item_id)
|
||||
if use_https and not verify_cert:
|
||||
playurl += "|verifypeer=false"
|
||||
playurl = direct_stream_path
|
||||
playback_type = "1"
|
||||
|
||||
# check is file can be transcoded
|
||||
if can_transcode and playurl is None:
|
||||
item_id = media_source.get('Id')
|
||||
client_info = ClientInformation()
|
||||
device_id = client_info.get_device_id()
|
||||
user_token = downloadUtils.authenticate()
|
||||
playback_bitrate = addon_settings.getSetting("force_max_stream_bitrate")
|
||||
bitrate = int(playback_bitrate) * 1000
|
||||
playback_max_width = addon_settings.getSetting("playback_max_width")
|
||||
audio_codec = addon_settings.getSetting("audio_codec")
|
||||
audio_playback_bitrate = addon_settings.getSetting("audio_playback_bitrate")
|
||||
audio_bitrate = int(audio_playback_bitrate) * 1000
|
||||
audio_max_channels = addon_settings.getSetting("audio_max_channels")
|
||||
playback_video_force_8 = addon_settings.getSetting("playback_video_force_8") == "true"
|
||||
|
||||
transcode_params = {
|
||||
"MediaSourceId": item_id,
|
||||
"DeviceId": device_id,
|
||||
"PlaySessionId": play_session_id,
|
||||
"api_key": user_token,
|
||||
"SegmentContainer": "ts",
|
||||
"VideoCodec": "h264",
|
||||
"VideoBitrate": bitrate,
|
||||
"MaxWidth": playback_max_width,
|
||||
"AudioCodec": audio_codec,
|
||||
"TranscodingMaxAudioChannels": audio_max_channels,
|
||||
"AudioBitrate": audio_bitrate
|
||||
}
|
||||
if playback_video_force_8:
|
||||
transcode_params.update({"MaxVideoBitDepth": "8"})
|
||||
|
||||
transcode_path = urllib.urlencode(transcode_params)
|
||||
|
||||
playurl = "%s/Videos/%s/master.m3u8?%s" % (server, item_id, transcode_path)
|
||||
|
||||
if use_https and not verify_cert:
|
||||
playurl += "|verifypeer=false"
|
||||
|
||||
playback_type = "2"
|
||||
|
||||
return playurl, playback_type, []
|
||||
|
||||
@staticmethod
|
||||
def get_strm_details(media_source):
|
||||
playurl = None
|
||||
listitem_props = []
|
||||
|
||||
contents = media_source.get('Path').encode('utf-8') # contains contents of strm file with linebreaks
|
||||
|
||||
line_break = '\r'
|
||||
if '\r\n' in contents:
|
||||
line_break = '\r\n'
|
||||
elif '\n' in contents:
|
||||
line_break = '\n'
|
||||
|
||||
lines = contents.split(line_break)
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
log.debug("STRM Line: {0}", line)
|
||||
if line.startswith('#KODIPROP:'):
|
||||
match = re.search('#KODIPROP:(?P<item_property>[^=]+?)=(?P<property_value>.+)', line)
|
||||
if match:
|
||||
item_property = match.group('item_property')
|
||||
property_value = match.group('property_value')
|
||||
log.debug("STRM property found: {0} value: {1}", item_property, property_value)
|
||||
listitem_props.append((item_property, property_value))
|
||||
else:
|
||||
log.debug("STRM #KODIPROP incorrect format")
|
||||
elif line.startswith('#'):
|
||||
# unrecognized, treat as comment
|
||||
log.debug("STRM unrecognized line identifier, ignored")
|
||||
elif line != '':
|
||||
playurl = line
|
||||
log.debug("STRM playback url found")
|
||||
|
||||
log.debug("Playback URL: {0} ListItem Properties: {1}", playurl, listitem_props)
|
||||
return playurl, listitem_props
|
||||
url_params = urlencode(params)
|
||||
return '{}?{}'.format(path, url_params)
|
||||
|
||||
|
||||
def get_checksum(item):
|
||||
userdata = item['UserData']
|
||||
checksum = "%s_%s_%s_%s_%s_%s_%s" % (
|
||||
checksum = "{}_{}_{}_{}_{}_{}_{}".format(
|
||||
item['Etag'],
|
||||
userdata['Played'],
|
||||
userdata['IsFavorite'],
|
||||
@@ -201,131 +67,71 @@ def get_checksum(item):
|
||||
return checksum
|
||||
|
||||
|
||||
def get_art(item, server):
|
||||
art = {
|
||||
'thumb': '',
|
||||
'fanart': '',
|
||||
'poster': '',
|
||||
'banner': '',
|
||||
'clearlogo': '',
|
||||
'clearart': '',
|
||||
'discart': '',
|
||||
'landscape': '',
|
||||
'tvshow.fanart': '',
|
||||
'tvshow.poster': '',
|
||||
'tvshow.clearart': '',
|
||||
'tvshow.clearlogo': '',
|
||||
'tvshow.banner': '',
|
||||
'tvshow.landscape': ''
|
||||
}
|
||||
|
||||
image_tags = item["ImageTags"]
|
||||
if image_tags is not None and image_tags["Primary"] is not None:
|
||||
# image_tag = image_tags["Primary"]
|
||||
art['thumb'] = downloadUtils.get_artwork(item, "Primary", server=server)
|
||||
|
||||
item_type = item["Type"]
|
||||
|
||||
if item_type == "Genre":
|
||||
art['poster'] = downloadUtils.get_artwork(item, "Primary", server=server)
|
||||
elif item_type == "Episode":
|
||||
art['tvshow.poster'] = downloadUtils.get_artwork(item, "Primary", parent=True, server=server)
|
||||
# art['poster'] = downloadUtils.getArtwork(item, "Primary", parent=True, server=server)
|
||||
art['tvshow.clearart'] = downloadUtils.get_artwork(item, "Art", parent=True, server=server)
|
||||
art['clearart'] = downloadUtils.get_artwork(item, "Art", parent=True, server=server)
|
||||
art['tvshow.clearlogo'] = downloadUtils.get_artwork(item, "Logo", parent=True, server=server)
|
||||
art['clearlogo'] = downloadUtils.get_artwork(item, "Logo", parent=True, server=server)
|
||||
art['tvshow.banner'] = downloadUtils.get_artwork(item, "Banner", parent=True, server=server)
|
||||
art['banner'] = downloadUtils.get_artwork(item, "Banner", parent=True, server=server)
|
||||
art['tvshow.landscape'] = downloadUtils.get_artwork(item, "Thumb", parent=True, server=server)
|
||||
art['landscape'] = downloadUtils.get_artwork(item, "Thumb", parent=True, server=server)
|
||||
art['tvshow.fanart'] = downloadUtils.get_artwork(item, "Backdrop", parent=True, server=server)
|
||||
art['fanart'] = downloadUtils.get_artwork(item, "Backdrop", parent=True, server=server)
|
||||
elif item_type == "Season":
|
||||
art['tvshow.poster'] = downloadUtils.get_artwork(item, "Primary", parent=True, server=server)
|
||||
art['season.poster'] = downloadUtils.get_artwork(item, "Primary", parent=False, server=server)
|
||||
art['poster'] = downloadUtils.get_artwork(item, "Primary", parent=False, server=server)
|
||||
art['tvshow.clearart'] = downloadUtils.get_artwork(item, "Art", parent=True, server=server)
|
||||
art['clearart'] = downloadUtils.get_artwork(item, "Art", parent=True, server=server)
|
||||
art['tvshow.clearlogo'] = downloadUtils.get_artwork(item, "Logo", parent=True, server=server)
|
||||
art['clearlogo'] = downloadUtils.get_artwork(item, "Logo", parent=True, server=server)
|
||||
art['tvshow.banner'] = downloadUtils.get_artwork(item, "Banner", parent=True, server=server)
|
||||
art['season.banner'] = downloadUtils.get_artwork(item, "Banner", parent=False, server=server)
|
||||
art['banner'] = downloadUtils.get_artwork(item, "Banner", parent=False, server=server)
|
||||
art['tvshow.landscape'] = downloadUtils.get_artwork(item, "Thumb", parent=True, server=server)
|
||||
art['season.landscape'] = downloadUtils.get_artwork(item, "Thumb", parent=False, server=server)
|
||||
art['landscape'] = downloadUtils.get_artwork(item, "Thumb", parent=False, server=server)
|
||||
art['tvshow.fanart'] = downloadUtils.get_artwork(item, "Backdrop", parent=True, server=server)
|
||||
art['fanart'] = downloadUtils.get_artwork(item, "Backdrop", parent=True, server=server)
|
||||
elif item_type == "Series":
|
||||
art['tvshow.poster'] = downloadUtils.get_artwork(item, "Primary", parent=False, server=server)
|
||||
art['poster'] = downloadUtils.get_artwork(item, "Primary", parent=False, server=server)
|
||||
art['tvshow.clearart'] = downloadUtils.get_artwork(item, "Art", parent=False, server=server)
|
||||
art['clearart'] = downloadUtils.get_artwork(item, "Art", parent=False, server=server)
|
||||
art['tvshow.clearlogo'] = downloadUtils.get_artwork(item, "Logo", parent=False, server=server)
|
||||
art['clearlogo'] = downloadUtils.get_artwork(item, "Logo", parent=False, server=server)
|
||||
art['tvshow.banner'] = downloadUtils.get_artwork(item, "Banner", parent=False, server=server)
|
||||
art['banner'] = downloadUtils.get_artwork(item, "Banner", parent=False, server=server)
|
||||
art['tvshow.landscape'] = downloadUtils.get_artwork(item, "Thumb", parent=False, server=server)
|
||||
art['landscape'] = downloadUtils.get_artwork(item, "Thumb", parent=False, server=server)
|
||||
art['tvshow.fanart'] = downloadUtils.get_artwork(item, "Backdrop", parent=False, server=server)
|
||||
art['fanart'] = downloadUtils.get_artwork(item, "Backdrop", parent=False, server=server)
|
||||
elif item_type == "Movie" or item_type == "BoxSet":
|
||||
art['poster'] = downloadUtils.get_artwork(item, "Primary", server=server)
|
||||
art['landscape'] = downloadUtils.get_artwork(item, "Thumb", server=server)
|
||||
art['banner'] = downloadUtils.get_artwork(item, "Banner", server=server)
|
||||
art['clearlogo'] = downloadUtils.get_artwork(item, "Logo", server=server)
|
||||
art['clearart'] = downloadUtils.get_artwork(item, "Art", server=server)
|
||||
art['discart'] = downloadUtils.get_artwork(item, "Disc", server=server)
|
||||
|
||||
art['fanart'] = downloadUtils.get_artwork(item, "Backdrop", server=server)
|
||||
if not art['fanart']:
|
||||
art['fanart'] = downloadUtils.get_artwork(item, "Backdrop", parent=True, server=server)
|
||||
|
||||
return art
|
||||
|
||||
|
||||
def id_generator(size=6, chars=string.ascii_uppercase + string.digits):
|
||||
return ''.join(random.choice(chars) for _ in range(size))
|
||||
|
||||
|
||||
def double_urlencode(text):
|
||||
text = single_urlencode(text)
|
||||
text = single_urlencode(text)
|
||||
return text
|
||||
|
||||
|
||||
def single_urlencode(text):
|
||||
# urlencode needs a utf- string
|
||||
text = urllib.urlencode({'blahblahblah': text.encode('utf-8')})
|
||||
text = urlencode({'blahblahblah': text.encode('utf-8')})
|
||||
text = text[13:]
|
||||
return text.decode('utf-8') # return the result again as unicode
|
||||
|
||||
|
||||
def send_event_notification(method, data):
|
||||
message_data = json.dumps(data)
|
||||
source_id = "jellycon"
|
||||
base64_data = base64.b64encode(message_data)
|
||||
escaped_data = '\\"[\\"{0}\\"]\\"'.format(base64_data)
|
||||
command = 'XBMC.NotifyAll({0}.SIGNAL,{1},{2})'.format(source_id, method, escaped_data)
|
||||
log.debug("Sending notification event data: {0}", command)
|
||||
xbmc.executebuiltin(command)
|
||||
def send_event_notification(method, data=None, hexlify=False):
|
||||
'''
|
||||
Send events through Kodi's notification system
|
||||
'''
|
||||
data = data or {}
|
||||
data_str = json.dumps(data)
|
||||
|
||||
if hexlify:
|
||||
# Used exclusively for the upnext plugin
|
||||
data_str = ensure_text(binascii.hexlify(ensure_binary(data_str)))
|
||||
data = '["{}"]'.format(data_str)
|
||||
else:
|
||||
data = '"[{}]"'.format(data_str.replace('"', '\\"'))
|
||||
|
||||
sender = 'plugin.video.jellycon'
|
||||
|
||||
xbmc.executebuiltin('NotifyAll({}, {}, {})'.format(sender, method, data))
|
||||
|
||||
|
||||
def datetime_from_string(time_string):
|
||||
|
||||
# Builtin python library can't handle ISO-8601 well. Make it compatible
|
||||
if time_string[-1:] == "Z":
|
||||
time_string = re.sub("[0-9]{1}Z", " UTC", time_string)
|
||||
elif time_string[-6:] == "+00:00":
|
||||
time_string = re.sub("[0-9]{1}\+00:00", " UTC", time_string)
|
||||
log.debug("New Time String : {0}", time_string)
|
||||
time_string = re.sub(
|
||||
"[0-9]{1}\+00:00", " UTC", time_string # noqa: W605
|
||||
)
|
||||
|
||||
start_time = time.strptime(time_string, "%Y-%m-%dT%H:%M:%S.%f %Z")
|
||||
dt = datetime(*(start_time[0:6]))
|
||||
timestamp = calendar.timegm(dt.timetuple())
|
||||
local_dt = datetime.fromtimestamp(timestamp)
|
||||
local_dt.replace(microsecond=dt.microsecond)
|
||||
return local_dt
|
||||
try:
|
||||
dt = datetime.strptime(time_string, "%Y-%m-%dT%H:%M:%S.%f %Z")
|
||||
except TypeError:
|
||||
# https://bugs.python.org/issue27400
|
||||
dt = datetime(*(
|
||||
time.strptime(time_string, "%Y-%m-%dT%H:%M:%S.%f %Z")[0:6])
|
||||
)
|
||||
|
||||
"""
|
||||
Dates received from the server are in UTC, but parsing them results
|
||||
in naive objects
|
||||
"""
|
||||
utc = tz.tzutc()
|
||||
utc_dt = dt.replace(tzinfo=utc)
|
||||
|
||||
return utc_dt
|
||||
|
||||
|
||||
def get_current_datetime():
|
||||
# Get current time in UTC
|
||||
now = datetime.utcnow()
|
||||
utc = tz.tzutc()
|
||||
now_dt = now.replace(tzinfo=utc)
|
||||
|
||||
return now_dt
|
||||
|
||||
|
||||
def convert_size(size_bytes):
|
||||
@@ -335,4 +141,326 @@ def convert_size(size_bytes):
|
||||
i = int(math.floor(math.log(size_bytes, 1024)))
|
||||
p = math.pow(1024, i)
|
||||
s = round(size_bytes / p, 2)
|
||||
return "%s %s" % (s, size_name[i])
|
||||
return "{} {}".format(s, size_name[i])
|
||||
|
||||
|
||||
def translate_string(string_id):
|
||||
try:
|
||||
addon = xbmcaddon.Addon()
|
||||
return py2_encode(addon.getLocalizedString(string_id))
|
||||
except Exception as e:
|
||||
log.error('Failed String Load: {0} ({1})', string_id, e)
|
||||
return str(string_id)
|
||||
|
||||
|
||||
def get_device_id():
|
||||
|
||||
window = HomeWindow()
|
||||
username = window.get_property('user_name')
|
||||
client_id = window.get_property("client_id")
|
||||
hashed_name = hashlib.md5(username.encode()).hexdigest()
|
||||
|
||||
if client_id and username:
|
||||
return '{}-{}'.format(client_id, hashed_name)
|
||||
elif client_id and not username:
|
||||
# Quick Connect, needs to be unique so sessions don't overwrite
|
||||
rand_id = uuid4().hex
|
||||
return '{}-{}'.format(client_id, rand_id)
|
||||
|
||||
jellyfin_guid_path = py2_decode(
|
||||
translate_path("special://temp/jellycon_guid")
|
||||
)
|
||||
log.debug("jellyfin_guid_path: {0}".format(jellyfin_guid_path))
|
||||
guid = xbmcvfs.File(jellyfin_guid_path)
|
||||
client_id = guid.read()
|
||||
guid.close()
|
||||
|
||||
if not client_id:
|
||||
client_id = uuid4().hex
|
||||
log.debug("Generating a new guid: {0}".format(client_id))
|
||||
guid = xbmcvfs.File(jellyfin_guid_path, 'w')
|
||||
guid.write(client_id)
|
||||
guid.close()
|
||||
log.debug("jellyfin_client_id (NEW): {0}".format(client_id))
|
||||
else:
|
||||
log.debug("jellyfin_client_id: {0}".format(client_id))
|
||||
|
||||
window.set_property("client_id", client_id)
|
||||
return '{}-{}'.format(client_id, hashed_name)
|
||||
|
||||
|
||||
def get_version():
|
||||
addon = xbmcaddon.Addon()
|
||||
version = addon.getAddonInfo("version")
|
||||
return version
|
||||
|
||||
|
||||
def save_user_details(user_name, user_id, token):
|
||||
settings = xbmcaddon.Addon()
|
||||
save_user_to_settings = settings.getSetting(
|
||||
'save_user_to_settings') == 'true'
|
||||
addon_data = translate_path(xbmcaddon.Addon().getAddonInfo('profile'))
|
||||
|
||||
# Save to a config file for reference later if desired
|
||||
if save_user_to_settings:
|
||||
try:
|
||||
with open(os.path.join(addon_data, 'auth.json'), 'rb') as infile:
|
||||
auth_data = json.load(infile)
|
||||
except: # noqa
|
||||
# File doesn't exist or is empty
|
||||
auth_data = {}
|
||||
|
||||
auth_data[user_name] = {
|
||||
'user_id': user_id,
|
||||
'token': token
|
||||
}
|
||||
|
||||
with open(os.path.join(addon_data, 'auth.json'), 'wb') as outfile:
|
||||
data = json.dumps(
|
||||
auth_data, sort_keys=True, indent=4, ensure_ascii=False)
|
||||
if isinstance(data, text_type):
|
||||
data = data.encode('utf-8')
|
||||
outfile.write(data)
|
||||
|
||||
# Make the username available for easy lookup
|
||||
window = HomeWindow()
|
||||
settings.setSetting('username', user_name)
|
||||
window.set_property('user_name', user_name)
|
||||
|
||||
|
||||
def load_user_details():
|
||||
settings = xbmcaddon.Addon()
|
||||
window = HomeWindow()
|
||||
# Check current variables first, then check settings
|
||||
user_name = window.get_property('user_name')
|
||||
if not user_name:
|
||||
user_name = settings.getSetting('username')
|
||||
save_user = settings.getSetting('save_user_to_settings') == 'true'
|
||||
addon_data = translate_path(xbmcaddon.Addon().getAddonInfo('profile'))
|
||||
|
||||
if save_user:
|
||||
try:
|
||||
with open(os.path.join(addon_data, 'auth.json'), 'rb') as infile:
|
||||
auth_data = json.load(infile)
|
||||
except: # noqa
|
||||
# File doesn't exist yet
|
||||
return {}
|
||||
|
||||
user_data = auth_data.get(user_name, {})
|
||||
# User doesn't exist yet
|
||||
if not user_data:
|
||||
return {}
|
||||
|
||||
user_id = user_data.get('user_id')
|
||||
auth_token = user_data.get('token')
|
||||
|
||||
# Payload to return to calling function
|
||||
user_details = {}
|
||||
user_details['user_name'] = user_name
|
||||
user_details['user_id'] = user_id
|
||||
user_details['token'] = auth_token
|
||||
return user_details
|
||||
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
def get_saved_users():
|
||||
settings = xbmcaddon.Addon()
|
||||
save_user = settings.getSetting('save_user_to_settings') == 'true'
|
||||
addon_data = translate_path(xbmcaddon.Addon().getAddonInfo('profile'))
|
||||
if not save_user:
|
||||
return []
|
||||
|
||||
try:
|
||||
with open(os.path.join(addon_data, 'auth.json'), 'rb') as infile:
|
||||
auth_data = json.load(infile)
|
||||
except: # noqa
|
||||
# File doesn't exist yet
|
||||
return []
|
||||
|
||||
users = []
|
||||
for user, values in auth_data.items():
|
||||
users.append(
|
||||
{
|
||||
'Name': user,
|
||||
'Id': values.get('user_id'),
|
||||
# We need something here for the listitem function
|
||||
'Configuration': {'Dummy': True}
|
||||
}
|
||||
)
|
||||
|
||||
return users
|
||||
|
||||
|
||||
def get_current_user_id():
|
||||
user_details = load_user_details()
|
||||
user_id = user_details.get('user_id')
|
||||
return user_id
|
||||
|
||||
|
||||
def get_art_url(data, art_type, parent=False, index=0, server=None):
|
||||
|
||||
item_id = data["Id"]
|
||||
item_type = data["Type"]
|
||||
|
||||
if item_type in ["Episode", "Season"]:
|
||||
if art_type != "Primary" or parent is True:
|
||||
item_id = data["SeriesId"]
|
||||
|
||||
image_tag = ""
|
||||
|
||||
# for episodes always use the parent BG
|
||||
if item_type == "Episode" and art_type == "Backdrop":
|
||||
item_id = data.get("ParentBackdropItemId")
|
||||
bg_item_tags = data.get("ParentBackdropImageTags", [])
|
||||
if bg_item_tags:
|
||||
image_tag = bg_item_tags[0]
|
||||
elif art_type == "Backdrop" and parent is True:
|
||||
item_id = data.get("ParentBackdropItemId")
|
||||
bg_item_tags = data.get("ParentBackdropImageTags", [])
|
||||
if bg_item_tags:
|
||||
image_tag = bg_item_tags[0]
|
||||
elif art_type == "Backdrop":
|
||||
bg_tags = data.get("BackdropImageTags", [])
|
||||
if bg_tags:
|
||||
image_tag = bg_tags[index]
|
||||
elif parent is False:
|
||||
image_tags = data.get("ImageTags", [])
|
||||
if image_tags:
|
||||
image_tag_type = image_tags.get(art_type)
|
||||
if image_tag_type:
|
||||
image_tag = image_tag_type
|
||||
elif parent is True:
|
||||
if ((item_type == "Episode" or item_type == "Season") and
|
||||
art_type == 'Primary'):
|
||||
tag_name = 'SeriesPrimaryImageTag'
|
||||
id_name = 'SeriesId'
|
||||
else:
|
||||
tag_name = 'Parent{}ImageTag'.format(art_type)
|
||||
id_name = 'Parent{}ItemId'.format(art_type)
|
||||
parent_image_id = data.get(id_name)
|
||||
parent_image_tag = data.get(tag_name)
|
||||
if parent_image_id is not None and parent_image_tag is not None:
|
||||
item_id = parent_image_id
|
||||
image_tag = parent_image_tag
|
||||
|
||||
# ParentTag not passed for Banner and Art
|
||||
if (not image_tag and
|
||||
not ((art_type == 'Banner' or art_type == 'Art') and
|
||||
parent is True)):
|
||||
return ""
|
||||
|
||||
artwork = "{}/Items/{}/Images/{}/{}?Format=original&Tag={}".format(
|
||||
server, item_id, art_type, index, image_tag)
|
||||
return artwork
|
||||
|
||||
|
||||
def image_url(item_id, art_type, index, width, height, image_tag, server):
|
||||
|
||||
# test imageTag e3ab56fe27d389446754d0fb04910a34
|
||||
artwork = "{}/Items/{}/Images/{}/{}?Format=original&Tag={}".format(
|
||||
server, item_id, art_type, index, image_tag
|
||||
)
|
||||
if int(width) > 0:
|
||||
artwork += '&MaxWidth={}'.format(width)
|
||||
if int(height) > 0:
|
||||
artwork += '&MaxHeight={}'.format(height)
|
||||
|
||||
return artwork
|
||||
|
||||
|
||||
def get_default_filters():
|
||||
|
||||
addon_settings = xbmcaddon.Addon()
|
||||
include_media = addon_settings.getSetting("include_media") == "true"
|
||||
include_people = addon_settings.getSetting("include_people") == "true"
|
||||
include_overview = addon_settings.getSetting("include_overview") == "true"
|
||||
|
||||
filer_list = [
|
||||
"DateCreated",
|
||||
"EpisodeCount",
|
||||
"SeasonCount",
|
||||
"Path",
|
||||
"Genres",
|
||||
"Studios",
|
||||
"Etag",
|
||||
"Taglines",
|
||||
"SortName",
|
||||
"RecursiveItemCount",
|
||||
"ChildCount",
|
||||
"ProductionLocations",
|
||||
"CriticRating",
|
||||
"OfficialRating",
|
||||
"CommunityRating",
|
||||
"PremiereDate",
|
||||
"ProductionYear",
|
||||
"AirTime",
|
||||
"Status",
|
||||
"Tags"
|
||||
]
|
||||
|
||||
if include_media:
|
||||
filer_list.append("MediaStreams")
|
||||
|
||||
if include_people:
|
||||
filer_list.append("People")
|
||||
|
||||
if include_overview:
|
||||
filer_list.append("Overview")
|
||||
|
||||
return ','.join(filer_list)
|
||||
|
||||
|
||||
def translate_path(path):
|
||||
'''
|
||||
Use new library location for translate path starting in Kodi 19
|
||||
'''
|
||||
version = kodi_version()
|
||||
|
||||
if version > 18:
|
||||
return xbmcvfs.translatePath(path)
|
||||
else:
|
||||
return xbmc.translatePath(path)
|
||||
|
||||
|
||||
def download_external_sub(language, codec, url, title):
|
||||
addon_settings = xbmcaddon.Addon()
|
||||
verify_cert = addon_settings.getSetting('verify_cert') == 'true'
|
||||
|
||||
# Download the subtitle file
|
||||
r = requests.get(url, verify=verify_cert)
|
||||
r.raise_for_status()
|
||||
|
||||
# Write the subtitle file to the local filesystem
|
||||
file_name = 'Stream.{}.{}.{}'.format(title, language, codec)
|
||||
file_path = py2_decode(
|
||||
translate_path('special://temp/{}'.format(file_name))
|
||||
)
|
||||
with open(file_path, 'wb') as f:
|
||||
f.write(r.content)
|
||||
|
||||
return file_path
|
||||
|
||||
|
||||
def get_bitrate(enum_value):
|
||||
''' Get the video quality based on add-on settings.
|
||||
Max bit rate supported by server: 2147483 (max signed 32bit integer)
|
||||
'''
|
||||
bitrate = [500, 1000, 1500, 2000, 2500, 3000, 4000, 5000, 6000,
|
||||
7000, 8000, 9000, 10000, 12000, 14000, 16000, 18000,
|
||||
20000, 25000, 30000, 35000, 40000, 100000, 1000000, 2147483]
|
||||
return bitrate[int(enum_value) if enum_value else 24] * 1000
|
||||
|
||||
def get_filtered_items_count_text():
|
||||
settings = xbmcaddon.Addon()
|
||||
if settings.getSetting("hide_x_filtered_items_count") == 'true' :
|
||||
return ""
|
||||
else:
|
||||
return " (" + settings.getSetting("show_x_filtered_items") + ")"
|
||||
|
||||
def seconds_to_ticks(seconds:float):
|
||||
return seconds * 10000000
|
||||
|
||||
def ticks_to_seconds(ticks:int):
|
||||
return round(ticks / 10000000, 1)
|
||||
|
||||
@@ -1,946 +0,0 @@
|
||||
"""
|
||||
websocket - WebSocket client library for Python
|
||||
|
||||
Copyright (C) 2010 Hiroki Ohtani(liris)
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
"""
|
||||
|
||||
|
||||
import socket
|
||||
from base64 import b64encode
|
||||
|
||||
try:
|
||||
import ssl
|
||||
from ssl import SSLError
|
||||
HAVE_SSL = True
|
||||
except ImportError:
|
||||
# dummy class of SSLError for ssl none-support environment.
|
||||
class SSLError(Exception):
|
||||
pass
|
||||
|
||||
HAVE_SSL = False
|
||||
|
||||
from urlparse import urlparse
|
||||
import os
|
||||
import array
|
||||
import struct
|
||||
import uuid
|
||||
import hashlib
|
||||
import base64
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
import traceback
|
||||
import sys
|
||||
|
||||
"""
|
||||
websocket python client.
|
||||
=========================
|
||||
|
||||
This version support only hybi-13.
|
||||
Please see http://tools.ietf.org/html/rfc6455 for protocol.
|
||||
"""
|
||||
|
||||
|
||||
# websocket supported version.
|
||||
VERSION = 13
|
||||
|
||||
# closing frame status codes.
|
||||
STATUS_NORMAL = 1000
|
||||
STATUS_GOING_AWAY = 1001
|
||||
STATUS_PROTOCOL_ERROR = 1002
|
||||
STATUS_UNSUPPORTED_DATA_TYPE = 1003
|
||||
STATUS_STATUS_NOT_AVAILABLE = 1005
|
||||
STATUS_ABNORMAL_CLOSED = 1006
|
||||
STATUS_INVALID_PAYLOAD = 1007
|
||||
STATUS_POLICY_VIOLATION = 1008
|
||||
STATUS_MESSAGE_TOO_BIG = 1009
|
||||
STATUS_INVALID_EXTENSION = 1010
|
||||
STATUS_UNEXPECTED_CONDITION = 1011
|
||||
STATUS_TLS_HANDSHAKE_ERROR = 1015
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
|
||||
class WebSocketException(Exception):
|
||||
"""
|
||||
websocket exeception class.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class WebSocketConnectionClosedException(WebSocketException):
|
||||
"""
|
||||
If remote host closed the connection or some network error happened,
|
||||
this exception will be raised.
|
||||
"""
|
||||
pass
|
||||
|
||||
class WebSocketTimeoutException(WebSocketException):
|
||||
"""
|
||||
WebSocketTimeoutException will be raised at socket timeout during read/write data.
|
||||
"""
|
||||
pass
|
||||
|
||||
default_timeout = None
|
||||
traceEnabled = False
|
||||
|
||||
|
||||
def enableTrace(tracable):
|
||||
"""
|
||||
turn on/off the tracability.
|
||||
|
||||
tracable: boolean value. if set True, tracability is enabled.
|
||||
"""
|
||||
global traceEnabled
|
||||
traceEnabled = tracable
|
||||
if tracable:
|
||||
if not logger.handlers:
|
||||
logger.addHandler(logging.StreamHandler())
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
def setdefaulttimeout(timeout):
|
||||
"""
|
||||
Set the global timeout setting to connect.
|
||||
|
||||
timeout: default socket timeout time. This value is second.
|
||||
"""
|
||||
global default_timeout
|
||||
default_timeout = timeout
|
||||
|
||||
|
||||
def getdefaulttimeout():
|
||||
"""
|
||||
Return the global timeout setting(second) to connect.
|
||||
"""
|
||||
return default_timeout
|
||||
|
||||
|
||||
def _wrap_sni_socket(sock, sslopt, hostname):
|
||||
context = ssl.SSLContext(sslopt.get('ssl_version', ssl.PROTOCOL_SSLv23))
|
||||
|
||||
if sslopt.get('cert_reqs', ssl.CERT_NONE) != ssl.CERT_NONE:
|
||||
capath = ssl.get_default_verify_paths().capath
|
||||
context.load_verify_locations(cafile=sslopt.get('ca_certs', None),
|
||||
capath=sslopt.get('ca_cert_path', capath))
|
||||
|
||||
return context.wrap_socket(
|
||||
sock,
|
||||
do_handshake_on_connect=sslopt.get('do_handshake_on_connect', True),
|
||||
suppress_ragged_eofs=sslopt.get('suppress_ragged_eofs', True),
|
||||
server_hostname=hostname,
|
||||
)
|
||||
|
||||
|
||||
def _parse_url(url):
|
||||
"""
|
||||
parse url and the result is tuple of
|
||||
(hostname, port, resource path and the flag of secure mode)
|
||||
|
||||
url: url string.
|
||||
"""
|
||||
if ":" not in url:
|
||||
raise ValueError("url is invalid")
|
||||
|
||||
scheme, url = url.split(":", 1)
|
||||
|
||||
parsed = urlparse(url, scheme="http")
|
||||
if parsed.hostname:
|
||||
hostname = parsed.hostname
|
||||
else:
|
||||
raise ValueError("hostname is invalid")
|
||||
port = 0
|
||||
if parsed.port:
|
||||
port = parsed.port
|
||||
|
||||
is_secure = False
|
||||
if scheme == "ws":
|
||||
if not port:
|
||||
port = 80
|
||||
elif scheme == "wss":
|
||||
is_secure = True
|
||||
if not port:
|
||||
port = 443
|
||||
else:
|
||||
raise ValueError("scheme %s is invalid" % scheme)
|
||||
|
||||
if parsed.path:
|
||||
resource = parsed.path
|
||||
else:
|
||||
resource = "/"
|
||||
|
||||
if parsed.query:
|
||||
resource += "?" + parsed.query
|
||||
|
||||
user_name = parsed.username
|
||||
user_password = parsed.password
|
||||
|
||||
return (hostname, port, resource, is_secure, user_name, user_password)
|
||||
|
||||
|
||||
def create_connection(url, timeout=None, **options):
|
||||
"""
|
||||
connect to url and return websocket object.
|
||||
|
||||
Connect to url and return the WebSocket object.
|
||||
Passing optional timeout parameter will set the timeout on the socket.
|
||||
If no timeout is supplied, the global default timeout setting returned by getdefauttimeout() is used.
|
||||
You can customize using 'options'.
|
||||
If you set "header" list object, you can set your own custom header.
|
||||
|
||||
>>> conn = create_connection("ws://echo.websocket.org/",
|
||||
... header=["User-Agent: MyProgram",
|
||||
... "x-custom: header"])
|
||||
|
||||
|
||||
timeout: socket timeout time. This value is integer.
|
||||
if you set None for this value, it means "use default_timeout value"
|
||||
|
||||
options: current support option is only "header".
|
||||
if you set header as dict value, the custom HTTP headers are added.
|
||||
"""
|
||||
sockopt = options.get("sockopt", [])
|
||||
sslopt = options.get("sslopt", {})
|
||||
websock = WebSocket(sockopt=sockopt, sslopt=sslopt)
|
||||
websock.settimeout(timeout if timeout is not None else default_timeout)
|
||||
websock.connect(url, **options)
|
||||
return websock
|
||||
|
||||
_MAX_INTEGER = (1 << 32) -1
|
||||
_AVAILABLE_KEY_CHARS = range(0x21, 0x2f + 1) + range(0x3a, 0x7e + 1)
|
||||
_MAX_CHAR_BYTE = (1<<8) -1
|
||||
|
||||
# ref. Websocket gets an update, and it breaks stuff.
|
||||
# http://axod.blogspot.com/2010/06/websocket-gets-update-and-it-breaks.html
|
||||
|
||||
|
||||
def _create_sec_websocket_key():
|
||||
uid = uuid.uuid4()
|
||||
return base64.encodestring(uid.bytes).strip()
|
||||
|
||||
|
||||
_HEADERS_TO_CHECK = {
|
||||
"upgrade": "websocket",
|
||||
"connection": "upgrade",
|
||||
}
|
||||
|
||||
|
||||
class ABNF(object):
|
||||
"""
|
||||
ABNF frame class.
|
||||
see http://tools.ietf.org/html/rfc5234
|
||||
and http://tools.ietf.org/html/rfc6455#section-5.2
|
||||
"""
|
||||
|
||||
# operation code values.
|
||||
OPCODE_CONT = 0x0
|
||||
OPCODE_TEXT = 0x1
|
||||
OPCODE_BINARY = 0x2
|
||||
OPCODE_CLOSE = 0x8
|
||||
OPCODE_PING = 0x9
|
||||
OPCODE_PONG = 0xa
|
||||
|
||||
# available operation code value tuple
|
||||
OPCODES = (OPCODE_CONT, OPCODE_TEXT, OPCODE_BINARY, OPCODE_CLOSE,
|
||||
OPCODE_PING, OPCODE_PONG)
|
||||
|
||||
# opcode human readable string
|
||||
OPCODE_MAP = {
|
||||
OPCODE_CONT: "cont",
|
||||
OPCODE_TEXT: "text",
|
||||
OPCODE_BINARY: "binary",
|
||||
OPCODE_CLOSE: "close",
|
||||
OPCODE_PING: "ping",
|
||||
OPCODE_PONG: "pong"
|
||||
}
|
||||
|
||||
# data length threashold.
|
||||
LENGTH_7 = 0x7d
|
||||
LENGTH_16 = 1 << 16
|
||||
LENGTH_63 = 1 << 63
|
||||
|
||||
def __init__(self, fin=0, rsv1=0, rsv2=0, rsv3=0,
|
||||
opcode=OPCODE_TEXT, mask=1, data=""):
|
||||
"""
|
||||
Constructor for ABNF.
|
||||
please check RFC for arguments.
|
||||
"""
|
||||
self.fin = fin
|
||||
self.rsv1 = rsv1
|
||||
self.rsv2 = rsv2
|
||||
self.rsv3 = rsv3
|
||||
self.opcode = opcode
|
||||
self.mask_value = mask
|
||||
self.data = data
|
||||
self.get_mask_key = os.urandom
|
||||
|
||||
def __str__(self):
|
||||
return "fin=" + str(self.fin) \
|
||||
+ " opcode=" + str(self.opcode) \
|
||||
+ " data=" + str(self.data)
|
||||
|
||||
@staticmethod
|
||||
def create_frame(data, opcode):
|
||||
"""
|
||||
create frame to send text, binary and other data.
|
||||
|
||||
data: data to send. This is string value(byte array).
|
||||
if opcode is OPCODE_TEXT and this value is uniocde,
|
||||
data value is conveted into unicode string, automatically.
|
||||
|
||||
opcode: operation code. please see OPCODE_XXX.
|
||||
"""
|
||||
if opcode == ABNF.OPCODE_TEXT and isinstance(data, unicode):
|
||||
data = data.encode("utf-8")
|
||||
# mask must be set if send data from client
|
||||
return ABNF(1, 0, 0, 0, opcode, 1, data)
|
||||
|
||||
def format(self):
|
||||
"""
|
||||
format this object to string(byte array) to send data to server.
|
||||
"""
|
||||
if any(x not in (0, 1) for x in [self.fin, self.rsv1, self.rsv2, self.rsv3]):
|
||||
raise ValueError("not 0 or 1")
|
||||
if self.opcode not in ABNF.OPCODES:
|
||||
raise ValueError("Invalid OPCODE")
|
||||
length = len(self.data)
|
||||
if length >= ABNF.LENGTH_63:
|
||||
raise ValueError("data is too long")
|
||||
|
||||
frame_header = chr(self.fin << 7
|
||||
| self.rsv1 << 6 | self.rsv2 << 5 | self.rsv3 << 4
|
||||
| self.opcode)
|
||||
if length < ABNF.LENGTH_7:
|
||||
frame_header += chr(self.mask_value << 7 | length)
|
||||
elif length < ABNF.LENGTH_16:
|
||||
frame_header += chr(self.mask_value << 7 | 0x7e)
|
||||
frame_header += struct.pack("!H", length)
|
||||
else:
|
||||
frame_header += chr(self.mask_value << 7 | 0x7f)
|
||||
frame_header += struct.pack("!Q", length)
|
||||
|
||||
if not self.mask_value:
|
||||
return frame_header + self.data
|
||||
else:
|
||||
mask_key = self.get_mask_key(4)
|
||||
return frame_header + self._get_masked(mask_key)
|
||||
|
||||
def _get_masked(self, mask_key):
|
||||
s = ABNF.mask(mask_key, self.data)
|
||||
return mask_key + "".join(s)
|
||||
|
||||
@staticmethod
|
||||
def mask(mask_key, data):
|
||||
"""
|
||||
mask or unmask data. Just do xor for each byte
|
||||
|
||||
mask_key: 4 byte string(byte).
|
||||
|
||||
data: data to mask/unmask.
|
||||
"""
|
||||
_m = array.array("B", mask_key)
|
||||
_d = array.array("B", data)
|
||||
for i in xrange(len(_d)):
|
||||
_d[i] ^= _m[i % 4]
|
||||
return _d.tostring()
|
||||
|
||||
|
||||
class WebSocket(object):
|
||||
"""
|
||||
Low level WebSocket interface.
|
||||
This class is based on
|
||||
The WebSocket protocol draft-hixie-thewebsocketprotocol-76
|
||||
http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol-76
|
||||
|
||||
We can connect to the websocket server and send/recieve data.
|
||||
The following example is a echo client.
|
||||
|
||||
>>> import websocket
|
||||
>>> ws = websocket.WebSocket()
|
||||
>>> ws.connect("ws://echo.websocket.org")
|
||||
>>> ws.send("Hello, Server")
|
||||
>>> ws.recv()
|
||||
'Hello, Server'
|
||||
>>> ws.close()
|
||||
|
||||
get_mask_key: a callable to produce new mask keys, see the set_mask_key
|
||||
function's docstring for more details
|
||||
sockopt: values for socket.setsockopt.
|
||||
sockopt must be tuple and each element is argument of sock.setscokopt.
|
||||
sslopt: dict object for ssl socket option.
|
||||
"""
|
||||
|
||||
def __init__(self, get_mask_key=None, sockopt=None, sslopt=None):
|
||||
"""
|
||||
Initalize WebSocket object.
|
||||
"""
|
||||
if sockopt is None:
|
||||
sockopt = []
|
||||
if sslopt is None:
|
||||
sslopt = {}
|
||||
self.connected = False
|
||||
self.sock = socket.socket()
|
||||
for opts in sockopt:
|
||||
self.sock.setsockopt(*opts)
|
||||
self.sslopt = sslopt
|
||||
self.get_mask_key = get_mask_key
|
||||
# Buffers over the packets from the layer beneath until desired amount
|
||||
# bytes of bytes are received.
|
||||
self._recv_buffer = []
|
||||
# These buffer over the build-up of a single frame.
|
||||
self._frame_header = None
|
||||
self._frame_length = None
|
||||
self._frame_mask = None
|
||||
self._cont_data = None
|
||||
|
||||
def fileno(self):
|
||||
return self.sock.fileno()
|
||||
|
||||
def set_mask_key(self, func):
|
||||
"""
|
||||
set function to create musk key. You can custumize mask key generator.
|
||||
Mainly, this is for testing purpose.
|
||||
|
||||
func: callable object. the fuct must 1 argument as integer.
|
||||
The argument means length of mask key.
|
||||
This func must be return string(byte array),
|
||||
which length is argument specified.
|
||||
"""
|
||||
self.get_mask_key = func
|
||||
|
||||
def gettimeout(self):
|
||||
"""
|
||||
Get the websocket timeout(second).
|
||||
"""
|
||||
return self.sock.gettimeout()
|
||||
|
||||
def settimeout(self, timeout):
|
||||
"""
|
||||
Set the timeout to the websocket.
|
||||
|
||||
timeout: timeout time(second).
|
||||
"""
|
||||
self.sock.settimeout(timeout)
|
||||
|
||||
timeout = property(gettimeout, settimeout)
|
||||
|
||||
def connect(self, url, **options):
|
||||
"""
|
||||
Connect to url. url is websocket url scheme. ie. ws://host:port/resource
|
||||
You can customize using 'options'.
|
||||
If you set "header" dict object, you can set your own custom header.
|
||||
|
||||
>>> ws = WebSocket()
|
||||
>>> ws.connect("ws://echo.websocket.org/",
|
||||
... header={"User-Agent: MyProgram",
|
||||
... "x-custom: header"})
|
||||
|
||||
timeout: socket timeout time. This value is integer.
|
||||
if you set None for this value,
|
||||
it means "use default_timeout value"
|
||||
|
||||
options: current support option is only "header".
|
||||
if you set header as dict value,
|
||||
the custom HTTP headers are added.
|
||||
|
||||
"""
|
||||
hostname, port, resource, is_secure, user_name, user_password = _parse_url(url)
|
||||
|
||||
# TODO: we need to support proxy
|
||||
self.sock.connect((hostname, port))
|
||||
if is_secure:
|
||||
if HAVE_SSL:
|
||||
if self.sslopt is None:
|
||||
sslopt = {}
|
||||
else:
|
||||
sslopt = self.sslopt
|
||||
if ssl.HAS_SNI:
|
||||
self.sock = _wrap_sni_socket(self.sock, sslopt, hostname)
|
||||
else:
|
||||
self.sock = ssl.wrap_socket(self.sock, **sslopt)
|
||||
else:
|
||||
raise WebSocketException("SSL not available.")
|
||||
|
||||
self._handshake(hostname, port, resource, user_name, user_password, **options)
|
||||
|
||||
def _handshake(self, host, port, resource, user_name, user_password, **options):
|
||||
headers = []
|
||||
headers.append("GET %s HTTP/1.1" % resource)
|
||||
|
||||
if user_name and user_password:
|
||||
# add basic auth headers
|
||||
userAndPass = b64encode(b"%s:%s" % (user_name, user_password)).decode("ascii")
|
||||
headers.append("Authorization: Basic %s" % userAndPass)
|
||||
|
||||
headers.append("User-Agent: JellyConWebSocket")
|
||||
headers.append("Upgrade: websocket")
|
||||
headers.append("Connection: Upgrade")
|
||||
if port == 80:
|
||||
hostport = host
|
||||
else:
|
||||
hostport = "%s:%d" % (host, port)
|
||||
headers.append("Host: %s" % hostport)
|
||||
|
||||
if "origin" in options:
|
||||
headers.append("Origin: %s" % options["origin"])
|
||||
else:
|
||||
headers.append("Origin: http://%s" % hostport)
|
||||
|
||||
key = _create_sec_websocket_key()
|
||||
headers.append("Sec-WebSocket-Key: %s" % key)
|
||||
headers.append("Sec-WebSocket-Version: %s" % VERSION)
|
||||
if "header" in options:
|
||||
headers.extend(options["header"])
|
||||
|
||||
headers.append("")
|
||||
headers.append("")
|
||||
|
||||
header_str = "\r\n".join(headers)
|
||||
self._send(header_str)
|
||||
if traceEnabled:
|
||||
logger.debug("--- request header ---")
|
||||
logger.debug(header_str)
|
||||
logger.debug("-----------------------")
|
||||
|
||||
status, resp_headers = self._read_headers()
|
||||
if status != 101:
|
||||
self.close()
|
||||
raise WebSocketException("Handshake Status %d" % status)
|
||||
|
||||
success = self._validate_header(resp_headers, key)
|
||||
if not success:
|
||||
self.close()
|
||||
raise WebSocketException("Invalid WebSocket Header")
|
||||
|
||||
self.connected = True
|
||||
|
||||
def _validate_header(self, headers, key):
|
||||
for k, v in _HEADERS_TO_CHECK.iteritems():
|
||||
r = headers.get(k, None)
|
||||
if not r:
|
||||
return False
|
||||
r = r.lower()
|
||||
if v != r:
|
||||
return False
|
||||
|
||||
result = headers.get("sec-websocket-accept", None)
|
||||
if not result:
|
||||
return False
|
||||
result = result.lower()
|
||||
|
||||
value = key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
hashed = base64.encodestring(hashlib.sha1(value).digest()).strip().lower()
|
||||
return hashed == result
|
||||
|
||||
def _read_headers(self):
|
||||
status = None
|
||||
headers = {}
|
||||
if traceEnabled:
|
||||
logger.debug("--- response header ---")
|
||||
|
||||
while True:
|
||||
line = self._recv_line()
|
||||
if line == "\r\n":
|
||||
break
|
||||
line = line.strip()
|
||||
if traceEnabled:
|
||||
logger.debug(line)
|
||||
if not status:
|
||||
status_info = line.split(" ", 2)
|
||||
status = int(status_info[1])
|
||||
else:
|
||||
kv = line.split(":", 1)
|
||||
if len(kv) == 2:
|
||||
key, value = kv
|
||||
headers[key.lower()] = value.strip().lower()
|
||||
else:
|
||||
raise WebSocketException("Invalid header")
|
||||
|
||||
if traceEnabled:
|
||||
logger.debug("-----------------------")
|
||||
|
||||
return status, headers
|
||||
|
||||
def send(self, payload, opcode=ABNF.OPCODE_TEXT):
|
||||
"""
|
||||
Send the data as string.
|
||||
|
||||
payload: Payload must be utf-8 string or unicoce,
|
||||
if the opcode is OPCODE_TEXT.
|
||||
Otherwise, it must be string(byte array)
|
||||
|
||||
opcode: operation code to send. Please see OPCODE_XXX.
|
||||
"""
|
||||
frame = ABNF.create_frame(payload, opcode)
|
||||
if self.get_mask_key:
|
||||
frame.get_mask_key = self.get_mask_key
|
||||
data = frame.format()
|
||||
length = len(data)
|
||||
if traceEnabled:
|
||||
logger.debug("send: " + repr(data))
|
||||
while data:
|
||||
l = self._send(data)
|
||||
data = data[l:]
|
||||
return length
|
||||
|
||||
def send_binary(self, payload):
|
||||
return self.send(payload, ABNF.OPCODE_BINARY)
|
||||
|
||||
def ping(self, payload=""):
|
||||
"""
|
||||
send ping data.
|
||||
|
||||
payload: data payload to send server.
|
||||
"""
|
||||
self.send(payload, ABNF.OPCODE_PING)
|
||||
|
||||
def pong(self, payload):
|
||||
"""
|
||||
send pong data.
|
||||
|
||||
payload: data payload to send server.
|
||||
"""
|
||||
self.send(payload, ABNF.OPCODE_PONG)
|
||||
|
||||
def recv(self):
|
||||
"""
|
||||
Receive string data(byte array) from the server.
|
||||
|
||||
return value: string(byte array) value.
|
||||
"""
|
||||
opcode, data = self.recv_data()
|
||||
return data
|
||||
|
||||
def recv_data(self):
|
||||
"""
|
||||
Recieve data with operation code.
|
||||
|
||||
return value: tuple of operation code and string(byte array) value.
|
||||
"""
|
||||
while True:
|
||||
frame = self.recv_frame()
|
||||
if not frame:
|
||||
# handle error:
|
||||
# 'NoneType' object has no attribute 'opcode'
|
||||
raise WebSocketException("Not a valid frame %s" % frame)
|
||||
elif frame.opcode in (ABNF.OPCODE_TEXT, ABNF.OPCODE_BINARY, ABNF.OPCODE_CONT):
|
||||
if frame.opcode == ABNF.OPCODE_CONT and not self._cont_data:
|
||||
raise WebSocketException("Illegal frame")
|
||||
if self._cont_data:
|
||||
self._cont_data[1] += frame.data
|
||||
else:
|
||||
self._cont_data = [frame.opcode, frame.data]
|
||||
|
||||
if frame.fin:
|
||||
data = self._cont_data
|
||||
self._cont_data = None
|
||||
return data
|
||||
elif frame.opcode == ABNF.OPCODE_CLOSE:
|
||||
self.send_close()
|
||||
return (frame.opcode, None)
|
||||
elif frame.opcode == ABNF.OPCODE_PING:
|
||||
self.pong(frame.data)
|
||||
|
||||
def recv_frame(self):
|
||||
"""
|
||||
recieve data as frame from server.
|
||||
|
||||
return value: ABNF frame object.
|
||||
"""
|
||||
# Header
|
||||
if self._frame_header is None:
|
||||
self._frame_header = self._recv_strict(2)
|
||||
b1 = ord(self._frame_header[0])
|
||||
fin = b1 >> 7 & 1
|
||||
rsv1 = b1 >> 6 & 1
|
||||
rsv2 = b1 >> 5 & 1
|
||||
rsv3 = b1 >> 4 & 1
|
||||
opcode = b1 & 0xf
|
||||
b2 = ord(self._frame_header[1])
|
||||
has_mask = b2 >> 7 & 1
|
||||
# Frame length
|
||||
if self._frame_length is None:
|
||||
length_bits = b2 & 0x7f
|
||||
if length_bits == 0x7e:
|
||||
length_data = self._recv_strict(2)
|
||||
self._frame_length = struct.unpack("!H", length_data)[0]
|
||||
elif length_bits == 0x7f:
|
||||
length_data = self._recv_strict(8)
|
||||
self._frame_length = struct.unpack("!Q", length_data)[0]
|
||||
else:
|
||||
self._frame_length = length_bits
|
||||
# Mask
|
||||
if self._frame_mask is None:
|
||||
self._frame_mask = self._recv_strict(4) if has_mask else ""
|
||||
# Payload
|
||||
payload = self._recv_strict(self._frame_length)
|
||||
if has_mask:
|
||||
payload = ABNF.mask(self._frame_mask, payload)
|
||||
# Reset for next frame
|
||||
self._frame_header = None
|
||||
self._frame_length = None
|
||||
self._frame_mask = None
|
||||
return ABNF(fin, rsv1, rsv2, rsv3, opcode, has_mask, payload)
|
||||
|
||||
|
||||
def send_close(self, status=STATUS_NORMAL, reason=""):
|
||||
"""
|
||||
send close data to the server.
|
||||
|
||||
status: status code to send. see STATUS_XXX.
|
||||
|
||||
reason: the reason to close. This must be string.
|
||||
"""
|
||||
if status < 0 or status >= ABNF.LENGTH_16:
|
||||
raise ValueError("code is invalid range")
|
||||
self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
|
||||
|
||||
def close(self, status=STATUS_NORMAL, reason=""):
|
||||
"""
|
||||
Close Websocket object
|
||||
|
||||
status: status code to send. see STATUS_XXX.
|
||||
|
||||
reason: the reason to close. This must be string.
|
||||
"""
|
||||
|
||||
try:
|
||||
self.sock.shutdown(socket.SHUT_RDWR)
|
||||
except:
|
||||
pass
|
||||
|
||||
'''
|
||||
if self.connected:
|
||||
if status < 0 or status >= ABNF.LENGTH_16:
|
||||
raise ValueError("code is invalid range")
|
||||
|
||||
try:
|
||||
self.send(struct.pack('!H', status) + reason, ABNF.OPCODE_CLOSE)
|
||||
timeout = self.sock.gettimeout()
|
||||
self.sock.settimeout(3)
|
||||
try:
|
||||
frame = self.recv_frame()
|
||||
if logger.isEnabledFor(logging.ERROR):
|
||||
recv_status = struct.unpack("!H", frame.data)[0]
|
||||
if recv_status != STATUS_NORMAL:
|
||||
logger.error("close status: " + repr(recv_status))
|
||||
except:
|
||||
pass
|
||||
self.sock.settimeout(timeout)
|
||||
self.sock.shutdown(socket.SHUT_RDWR)
|
||||
except:
|
||||
pass
|
||||
'''
|
||||
self._closeInternal()
|
||||
|
||||
def _closeInternal(self):
|
||||
self.connected = False
|
||||
self.sock.close()
|
||||
|
||||
def _send(self, data):
|
||||
try:
|
||||
return self.sock.send(data)
|
||||
except socket.timeout as e:
|
||||
raise WebSocketTimeoutException(e.args[0])
|
||||
except Exception as e:
|
||||
if "timed out" in e.args[0]:
|
||||
raise WebSocketTimeoutException(e.args[0])
|
||||
else:
|
||||
raise e
|
||||
|
||||
def _recv(self, bufsize):
|
||||
try:
|
||||
bytes = self.sock.recv(bufsize)
|
||||
except socket.timeout as e:
|
||||
raise WebSocketTimeoutException(e.args[0])
|
||||
except SSLError as e:
|
||||
if e.args[0] == "The read operation timed out":
|
||||
raise WebSocketTimeoutException(e.args[0])
|
||||
else:
|
||||
raise
|
||||
if not bytes:
|
||||
raise WebSocketConnectionClosedException()
|
||||
return bytes
|
||||
|
||||
|
||||
def _recv_strict(self, bufsize):
|
||||
shortage = bufsize - sum(len(x) for x in self._recv_buffer)
|
||||
while shortage > 0:
|
||||
bytes = self._recv(shortage)
|
||||
self._recv_buffer.append(bytes)
|
||||
shortage -= len(bytes)
|
||||
unified = "".join(self._recv_buffer)
|
||||
if shortage == 0:
|
||||
self._recv_buffer = []
|
||||
return unified
|
||||
else:
|
||||
self._recv_buffer = [unified[bufsize:]]
|
||||
return unified[:bufsize]
|
||||
|
||||
|
||||
def _recv_line(self):
|
||||
line = []
|
||||
while True:
|
||||
c = self._recv(1)
|
||||
line.append(c)
|
||||
if c == "\n":
|
||||
break
|
||||
return "".join(line)
|
||||
|
||||
|
||||
class WebSocketApp(object):
|
||||
"""
|
||||
Higher level of APIs are provided.
|
||||
The interface is like JavaScript WebSocket object.
|
||||
"""
|
||||
def __init__(self, url, header=[],
|
||||
on_open=None, on_message=None, on_error=None,
|
||||
on_close=None, keep_running=True, get_mask_key=None):
|
||||
"""
|
||||
url: websocket url.
|
||||
header: custom header for websocket handshake.
|
||||
on_open: callable object which is called at opening websocket.
|
||||
this function has one argument. The arugment is this class object.
|
||||
on_message: callbale object which is called when recieved data.
|
||||
on_message has 2 arguments.
|
||||
The 1st arugment is this class object.
|
||||
The passing 2nd arugment is utf-8 string which we get from the server.
|
||||
on_error: callable object which is called when we get error.
|
||||
on_error has 2 arguments.
|
||||
The 1st arugment is this class object.
|
||||
The passing 2nd arugment is exception object.
|
||||
on_close: callable object which is called when closed the connection.
|
||||
this function has one argument. The arugment is this class object.
|
||||
keep_running: a boolean flag indicating whether the app's main loop should
|
||||
keep running, defaults to True
|
||||
get_mask_key: a callable to produce new mask keys, see the WebSocket.set_mask_key's
|
||||
docstring for more information
|
||||
"""
|
||||
self.url = url
|
||||
self.header = header
|
||||
self.on_open = on_open
|
||||
self.on_message = on_message
|
||||
self.on_error = on_error
|
||||
self.on_close = on_close
|
||||
self.keep_running = keep_running
|
||||
self.get_mask_key = get_mask_key
|
||||
self.sock = None
|
||||
|
||||
def send(self, data, opcode=ABNF.OPCODE_TEXT):
|
||||
"""
|
||||
send message.
|
||||
data: message to send. If you set opcode to OPCODE_TEXT, data must be utf-8 string or unicode.
|
||||
opcode: operation code of data. default is OPCODE_TEXT.
|
||||
"""
|
||||
if self.sock.send(data, opcode) == 0:
|
||||
raise WebSocketConnectionClosedException()
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
close websocket connection.
|
||||
"""
|
||||
self.keep_running = False
|
||||
if(self.sock != None):
|
||||
self.sock.close()
|
||||
|
||||
def _send_ping(self, interval):
|
||||
while True:
|
||||
for i in range(interval):
|
||||
time.sleep(1)
|
||||
if not self.keep_running:
|
||||
return
|
||||
self.sock.ping()
|
||||
|
||||
def run_forever(self, sockopt=None, sslopt=None, ping_interval=0):
|
||||
"""
|
||||
run event loop for WebSocket framework.
|
||||
This loop is infinite loop and is alive during websocket is available.
|
||||
sockopt: values for socket.setsockopt.
|
||||
sockopt must be tuple and each element is argument of sock.setscokopt.
|
||||
sslopt: ssl socket optional dict.
|
||||
ping_interval: automatically send "ping" command every specified period(second)
|
||||
if set to 0, not send automatically.
|
||||
"""
|
||||
if sockopt is None:
|
||||
sockopt = []
|
||||
if sslopt is None:
|
||||
sslopt = {}
|
||||
if self.sock:
|
||||
raise WebSocketException("socket is already opened")
|
||||
thread = None
|
||||
self.keep_running = True
|
||||
|
||||
try:
|
||||
self.sock = WebSocket(self.get_mask_key, sockopt=sockopt, sslopt=sslopt)
|
||||
self.sock.settimeout(default_timeout)
|
||||
self.sock.connect(self.url, header=self.header)
|
||||
self._callback(self.on_open)
|
||||
|
||||
if ping_interval:
|
||||
thread = threading.Thread(target=self._send_ping, args=(ping_interval,))
|
||||
thread.setDaemon(True)
|
||||
thread.start()
|
||||
|
||||
while self.keep_running:
|
||||
|
||||
try:
|
||||
data = self.sock.recv()
|
||||
|
||||
if data is None or self.keep_running == False:
|
||||
break
|
||||
self._callback(self.on_message, data)
|
||||
|
||||
except Exception as e:
|
||||
found_timeout = False
|
||||
for arg in e.args:
|
||||
if isinstance(arg, str):
|
||||
if "timed out" in arg:
|
||||
found_timeout = True
|
||||
if not found_timeout:
|
||||
raise e
|
||||
|
||||
except Exception as e:
|
||||
self._callback(self.on_error, e)
|
||||
finally:
|
||||
if thread:
|
||||
self.keep_running = False
|
||||
self.sock.close()
|
||||
self._callback(self.on_close)
|
||||
self.sock = None
|
||||
|
||||
def _callback(self, callback, *args):
|
||||
if callback:
|
||||
try:
|
||||
callback(self, *args)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
if True:#logger.isEnabledFor(logging.DEBUG):
|
||||
_, _, tb = sys.exc_info()
|
||||
traceback.print_tb(tb)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
enableTrace(True)
|
||||
ws = create_connection("ws://echo.websocket.org/")
|
||||
print("Sending 'Hello, World'...")
|
||||
ws.send("Hello, World")
|
||||
print("Sent")
|
||||
print("Receiving...")
|
||||
result = ws.recv()
|
||||
print("Received '%s'" % result)
|
||||
ws.close()
|
||||
@@ -1,22 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#################################################################################################
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import json
|
||||
import threading
|
||||
import websocket
|
||||
import time
|
||||
|
||||
import xbmc
|
||||
import xbmcaddon
|
||||
import xbmcgui
|
||||
import websocket
|
||||
|
||||
from .jellyfin import API
|
||||
from .functions import play_action
|
||||
from .simple_logging import SimpleLogging
|
||||
from . import clientinfo
|
||||
from . import downloadutils
|
||||
from .lazylogger import LazyLogger
|
||||
from .jsonrpc import JsonRpc
|
||||
from .kodi_utils import HomeWindow
|
||||
from .utils import get_device_id, load_user_details
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
|
||||
class WebSocketClient(threading.Thread):
|
||||
@@ -32,10 +34,10 @@ class WebSocketClient(threading.Thread):
|
||||
self.__dict__ = self._shared_state
|
||||
self.monitor = xbmc.Monitor()
|
||||
|
||||
self.client_info = clientinfo.ClientInformation()
|
||||
self.device_id = self.client_info.get_device_id()
|
||||
self.device_id = get_device_id()
|
||||
|
||||
self._library_monitor = library_change_monitor
|
||||
self.websocket_error = False
|
||||
|
||||
threading.Thread.__init__(self)
|
||||
|
||||
@@ -65,10 +67,10 @@ class WebSocketClient(threading.Thread):
|
||||
self._general_commands(data)
|
||||
|
||||
else:
|
||||
log.debug("WebSocket Message Type: {0}", message)
|
||||
log.debug("WebSocket Message Type: {0}".format(message))
|
||||
|
||||
def _library_changed(self, data):
|
||||
log.debug("Library_Changed: {0}", data)
|
||||
log.debug("Library_Changed: {0}".format(data))
|
||||
self._library_monitor.check_for_updates()
|
||||
|
||||
def _play(self, data):
|
||||
@@ -81,7 +83,7 @@ class WebSocketClient(threading.Thread):
|
||||
home_screen.set_property("skip_select_user", "true")
|
||||
|
||||
startat = data.get('StartPositionTicks', -1)
|
||||
log.debug("WebSocket Message PlayNow: {0}", data)
|
||||
log.debug("WebSocket Message PlayNow: {0}".format(data))
|
||||
|
||||
media_source_id = data.get("MediaSourceId", "")
|
||||
subtitle_stream_index = data.get("SubtitleStreamIndex", None)
|
||||
@@ -103,7 +105,6 @@ class WebSocketClient(threading.Thread):
|
||||
params["audio_stream_index"] = audio_stream_index
|
||||
play_action(params)
|
||||
|
||||
|
||||
def _playstate(self, data):
|
||||
|
||||
command = data['Command']
|
||||
@@ -124,14 +125,14 @@ class WebSocketClient(threading.Thread):
|
||||
seek_to = data['SeekPositionTicks']
|
||||
seek_time = seek_to / 10000000.0
|
||||
player.seekTime(seek_time)
|
||||
log.debug("Seek to {0}", seek_time)
|
||||
log.debug("Seek to {0}".format(seek_time))
|
||||
|
||||
elif command in actions:
|
||||
actions[command]()
|
||||
log.debug("Command: {0} completed", command)
|
||||
log.debug("Command: {0} completed".format(command))
|
||||
|
||||
else:
|
||||
log.debug("Unknown command: {0}", command)
|
||||
log.debug("Unknown command: {0}".format(command))
|
||||
return
|
||||
|
||||
def _general_commands(self, data):
|
||||
@@ -156,7 +157,9 @@ class WebSocketClient(threading.Thread):
|
||||
|
||||
elif command == 'SetVolume':
|
||||
volume = arguments['Volume']
|
||||
xbmc.executebuiltin('SetVolume(%s[,showvolumebar])' % volume)
|
||||
xbmc.executebuiltin(
|
||||
'SetVolume({}[,showvolumebar])'.format(volume)
|
||||
)
|
||||
|
||||
elif command == 'SetAudioStreamIndex':
|
||||
index = int(arguments['Index'])
|
||||
@@ -168,14 +171,14 @@ class WebSocketClient(threading.Thread):
|
||||
|
||||
elif command == 'SetRepeatMode':
|
||||
mode = arguments['RepeatMode']
|
||||
xbmc.executebuiltin('xbmc.PlayerControl(%s)' % mode)
|
||||
xbmc.executebuiltin('xbmc.PlayerControl({})'.format(mode))
|
||||
|
||||
elif command == 'DisplayMessage':
|
||||
|
||||
# header = arguments['Header']
|
||||
text = arguments['Text']
|
||||
# show notification here
|
||||
log.debug("WebSocket DisplayMessage: {0}", text)
|
||||
log.debug("WebSocket DisplayMessage: {0}".format(text))
|
||||
xbmcgui.Dialog().notification("JellyCon", text)
|
||||
|
||||
elif command == 'SendString':
|
||||
@@ -226,47 +229,52 @@ class WebSocketClient(threading.Thread):
|
||||
if command in builtin:
|
||||
xbmc.executebuiltin(builtin[command])
|
||||
|
||||
def on_close(self, ws):
|
||||
log.debug("Closed")
|
||||
|
||||
def on_open(self, ws):
|
||||
# Wait to make sure previous keepalive cycle has ended
|
||||
if self.websocket_error:
|
||||
time.sleep(30)
|
||||
self.websocket_error = False
|
||||
log.debug("Connected")
|
||||
self.post_capabilities()
|
||||
self.send_keepalive(ws)
|
||||
|
||||
def on_error(self, ws, error):
|
||||
log.debug("Error: {0}", error)
|
||||
self.websocket_error = True
|
||||
log.debug("Error: {0}".format(error))
|
||||
|
||||
def run(self):
|
||||
|
||||
# websocket.enableTrace(True)
|
||||
download_utils = downloadutils.DownloadUtils()
|
||||
|
||||
token = None
|
||||
while token is None or token == "":
|
||||
token = download_utils.authenticate()
|
||||
user_details = load_user_details()
|
||||
token = user_details.get('token')
|
||||
if self.monitor.waitForAbort(10):
|
||||
return
|
||||
|
||||
# Get the appropriate prefix for the websocket
|
||||
server = download_utils.get_server()
|
||||
if "https" in server:
|
||||
server = server.replace('https', "wss")
|
||||
settings = xbmcaddon.Addon()
|
||||
server = settings.getSetting('server_address')
|
||||
if "https://" in server:
|
||||
server = server.replace('https://', 'wss://')
|
||||
else:
|
||||
server = server.replace('http', "ws")
|
||||
server = server.replace('http://', 'ws://')
|
||||
|
||||
websocket_url = "%s/websocket?api_key=%s&deviceId=%s" % (server, token, self.device_id)
|
||||
log.debug("websocket url: {0}", websocket_url)
|
||||
websocket_url = "{}/socket?api_key={}&deviceId={}".format(
|
||||
server, token, self.device_id
|
||||
)
|
||||
log.debug("websocket url: {0}".format(websocket_url))
|
||||
|
||||
self._client = websocket.WebSocketApp(
|
||||
websocket_url,
|
||||
on_open=lambda ws: self.on_open(ws),
|
||||
on_message=lambda ws, message: self.on_message(ws, message),
|
||||
on_error=lambda ws, error: self.on_error(ws, error))
|
||||
|
||||
self._client = websocket.WebSocketApp(websocket_url,
|
||||
on_open=self.on_open,
|
||||
on_message=self.on_message,
|
||||
on_error=self.on_error,
|
||||
on_close=self.on_close)
|
||||
log.debug("Starting WebSocketClient")
|
||||
|
||||
while not self.monitor.abortRequested():
|
||||
|
||||
self._client.run_forever(ping_interval=10)
|
||||
self._client.run_forever(reconnect=30)
|
||||
|
||||
if self._stop_websocket:
|
||||
break
|
||||
@@ -288,5 +296,32 @@ class WebSocketClient(threading.Thread):
|
||||
|
||||
def post_capabilities(self):
|
||||
|
||||
download_utils = downloadutils.DownloadUtils()
|
||||
download_utils.post_capabilities()
|
||||
settings = xbmcaddon.Addon()
|
||||
user_details = load_user_details()
|
||||
|
||||
api = API(
|
||||
settings.getSetting('server_address'),
|
||||
user_details.get('user_id'),
|
||||
user_details.get('token')
|
||||
)
|
||||
|
||||
api.post_capabilities()
|
||||
|
||||
def send_keepalive(self, ws):
|
||||
# Stop the keepalive cycle if an error has been detected
|
||||
if self.websocket_error:
|
||||
return
|
||||
keepalive_payload = json.dumps({"MessageType": "KeepAlive", "Data": 30})
|
||||
# Send the keepalive, or register an error
|
||||
try:
|
||||
ws.send(keepalive_payload)
|
||||
except:
|
||||
self.websocket_error = True
|
||||
return
|
||||
# Schedule the next message
|
||||
self.schedule_keepalive(ws)
|
||||
|
||||
def schedule_keepalive(self, ws):
|
||||
# Schedule a keepalive message in 30 seconds
|
||||
timer = threading.Timer(30, self.send_keepalive, kwargs={'ws': ws})
|
||||
timer.start()
|
||||
|
||||
73
resources/lib/websocket_client.py.rej
Normal file
73
resources/lib/websocket_client.py.rej
Normal file
@@ -0,0 +1,73 @@
|
||||
@@ -46,7 +46,12 @@
|
||||
result = json.loads(message)
|
||||
message_type = result['MessageType']
|
||||
|
||||
- if message_type == 'Play':
|
||||
+ if message_type == 'ForceKeepAlive':
|
||||
+ timeout_seconds = result.get('Data', 60)
|
||||
+ log.debug("Received ForceKeepAlive with timeout: {0}s".format(timeout_seconds))
|
||||
+ self._send_keep_alive()
|
||||
+
|
||||
+ elif message_type == 'Play':
|
||||
data = result['Data']
|
||||
self._play(data)
|
||||
|
||||
@@ -237,6 +242,9 @@
|
||||
def on_error(self, ws, error):
|
||||
log.debug("Error: {0}".format(error))
|
||||
|
||||
+ def on_close(self, ws, close_status_code, close_msg):
|
||||
+ log.debug("WebSocket closed. Code: {0}, Message: {1}".format(close_status_code, close_msg))
|
||||
+
|
||||
def run(self):
|
||||
|
||||
token = None
|
||||
@@ -259,17 +267,23 @@
|
||||
)
|
||||
log.debug("websocket url: {0}".format(websocket_url))
|
||||
|
||||
- self._client = websocket.WebSocketApp(
|
||||
- websocket_url,
|
||||
- on_open=lambda ws: self.on_open(ws),
|
||||
- on_message=lambda ws, message: self.on_message(ws, message),
|
||||
- on_error=lambda ws, error: self.on_error(ws, error))
|
||||
-
|
||||
log.debug("Starting WebSocketClient")
|
||||
|
||||
while not self.monitor.abortRequested():
|
||||
|
||||
- self._client.run_forever(ping_interval=5, reconnect=13, ping_timeout=2)
|
||||
+ # Create a new WebSocketApp for each connection attempt to avoid
|
||||
+ # memory leaks from reusing a potentially dirty connection object
|
||||
+ self._client = websocket.WebSocketApp(
|
||||
+ websocket_url,
|
||||
+ on_open=lambda ws: self.on_open(ws),
|
||||
+ on_message=lambda ws, message: self.on_message(ws, message),
|
||||
+ on_error=lambda ws, error: self.on_error(ws, error),
|
||||
+ on_close=lambda ws, status, msg: self.on_close(ws, status, msg))
|
||||
+
|
||||
+ # Use ping_interval without ping_timeout to keep connection alive
|
||||
+ # without forcing disconnection. The server's ForceKeepAlive/KeepAlive
|
||||
+ # mechanism handles the actual keepalive logic.
|
||||
+ self._client.run_forever(ping_interval=10)
|
||||
|
||||
if self._stop_websocket:
|
||||
break
|
||||
@@ -291,6 +305,17 @@
|
||||
self._client.close()
|
||||
log.debug("Stopping WebSocket (stop_client called)")
|
||||
|
||||
+ def _send_keep_alive(self):
|
||||
+ """Send a KeepAlive message to the server to maintain the connection."""
|
||||
+ try:
|
||||
+ keep_alive_message = json.dumps({
|
||||
+ 'MessageType': 'KeepAlive'
|
||||
+ })
|
||||
+ self._client.send(keep_alive_message)
|
||||
+ log.debug("Sent KeepAlive message")
|
||||
+ except Exception as error:
|
||||
+ log.debug("Error sending KeepAlive: {0}".format(error))
|
||||
+
|
||||
def post_capabilities(self):
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
@@ -1,24 +1,27 @@
|
||||
import xbmcaddon
|
||||
import xbmcplugin
|
||||
import xbmcgui
|
||||
import xbmc
|
||||
import json
|
||||
from __future__ import (
|
||||
division, absolute_import, print_function, unicode_literals
|
||||
)
|
||||
|
||||
import hashlib
|
||||
import random
|
||||
import time
|
||||
import datetime
|
||||
|
||||
from .downloadutils import DownloadUtils
|
||||
from .utils import get_jellyfin_url
|
||||
from .datamanager import DataManager
|
||||
from .simple_logging import SimpleLogging
|
||||
import xbmcaddon
|
||||
import xbmcplugin
|
||||
import xbmcgui
|
||||
|
||||
from .jellyfin import api
|
||||
from .utils import (
|
||||
get_jellyfin_url, image_url, get_current_user_id,
|
||||
get_art_url, get_default_filters
|
||||
)
|
||||
from .lazylogger import LazyLogger
|
||||
from .kodi_utils import HomeWindow
|
||||
from .dir_functions import process_directory
|
||||
from .tracking import timer
|
||||
|
||||
log = SimpleLogging(__name__)
|
||||
downloadUtils = DownloadUtils()
|
||||
dataManager = DataManager()
|
||||
kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
|
||||
log = LazyLogger(__name__)
|
||||
|
||||
background_items = []
|
||||
background_current_item = 0
|
||||
@@ -29,21 +32,22 @@ def set_random_movies():
|
||||
log.debug("set_random_movies Called")
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
item_limit = settings.getSetting("show_x_filtered_items")
|
||||
hide_watched = settings.getSetting("hide_watched") == "true"
|
||||
user_id = get_current_user_id()
|
||||
|
||||
url_params = {}
|
||||
url_params["Recursive"] = True
|
||||
url_params["limit"] = 20
|
||||
url_params["limit"] = item_limit
|
||||
if hide_watched:
|
||||
url_params["IsPlayed"] = False
|
||||
url_params["SortBy"] = "Random"
|
||||
url_params["IncludeItemTypes"] = "Movie"
|
||||
url_params["ImageTypeLimit"] = 0
|
||||
|
||||
url = get_jellyfin_url("{server}/Users/{userid}/Items", url_params)
|
||||
url = get_jellyfin_url("/Users/{}/Items".format(user_id), url_params)
|
||||
|
||||
results = downloadUtils.download_url(url, suppress=True)
|
||||
results = json.loads(results)
|
||||
results = api.get(url)
|
||||
|
||||
randon_movies_list = []
|
||||
if results is not None:
|
||||
@@ -55,49 +59,132 @@ def set_random_movies():
|
||||
movies_list_string = ",".join(randon_movies_list)
|
||||
home_window = HomeWindow()
|
||||
m = hashlib.md5()
|
||||
m.update(movies_list_string)
|
||||
m.update(movies_list_string.encode())
|
||||
new_widget_hash = m.hexdigest()
|
||||
|
||||
log.debug("set_random_movies : {0}", movies_list_string)
|
||||
log.debug("set_random_movies : {0}", new_widget_hash)
|
||||
log.debug("set_random_movies : {0}".format(movies_list_string))
|
||||
log.debug("set_random_movies : {0}".format(new_widget_hash))
|
||||
home_window.set_property("random-movies", movies_list_string)
|
||||
home_window.set_property("random-movies-changed", new_widget_hash)
|
||||
|
||||
@timer
|
||||
def set_random_tvshows():
|
||||
log.debug("set_random_tvshows Called")
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
item_limit = settings.getSetting("show_x_filtered_items")
|
||||
hide_watched = settings.getSetting("hide_watched") == "true"
|
||||
user_id = get_current_user_id()
|
||||
|
||||
url_params = {}
|
||||
url_params["Recursive"] = True
|
||||
url_params["limit"] = item_limit
|
||||
if hide_watched:
|
||||
url_params["IsPlayed"] = False
|
||||
url_params["SortBy"] = "Random"
|
||||
url_params["IncludeItemTypes"] = "Series"
|
||||
url_params["ImageTypeLimit"] = 0
|
||||
|
||||
url = get_jellyfin_url("/Users/{}/Items".format(user_id), url_params)
|
||||
|
||||
results = api.get(url)
|
||||
|
||||
random_tvshows_list = []
|
||||
if results is not None:
|
||||
items = results.get("Items", [])
|
||||
for item in items:
|
||||
random_tvshows_list.append(item.get("Id"))
|
||||
|
||||
random.shuffle(random_tvshows_list)
|
||||
tvshow_list_string = ",".join(random_tvshows_list)
|
||||
home_window = HomeWindow()
|
||||
m = hashlib.md5()
|
||||
m.update(tvshow_list_string.encode())
|
||||
new_widget_hash = m.hexdigest()
|
||||
|
||||
log.debug("set_random_tvshows: {0}".format(tvshow_list_string))
|
||||
log.debug("set_random_tvshows: {0}".format(new_widget_hash))
|
||||
home_window.set_property("random-tvshows", tvshow_list_string)
|
||||
home_window.set_property("random-tvshows-changed", new_widget_hash)
|
||||
|
||||
@timer
|
||||
def set_random_all():
|
||||
log.debug("set_random_all Called")
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
item_limit = settings.getSetting("show_x_filtered_items")
|
||||
hide_watched = settings.getSetting("hide_watched") == "true"
|
||||
user_id = get_current_user_id()
|
||||
|
||||
url_params = {}
|
||||
url_params["Recursive"] = True
|
||||
url_params["limit"] = item_limit
|
||||
if hide_watched:
|
||||
url_params["IsPlayed"] = False
|
||||
url_params["SortBy"] = "Random"
|
||||
url_params["IncludeItemTypes"] = "Movie,Series"
|
||||
url_params["ImageTypeLimit"] = 0
|
||||
|
||||
url = get_jellyfin_url("/Users/{}/Items".format(user_id), url_params)
|
||||
|
||||
results = api.get(url)
|
||||
|
||||
random_items = []
|
||||
if results is not None:
|
||||
items = results.get("Items", [])
|
||||
for item in items:
|
||||
random_items.append(item.get("Id"))
|
||||
|
||||
random.shuffle(random_items)
|
||||
item_list_string = ",".join(random_items)
|
||||
home_window = HomeWindow()
|
||||
m = hashlib.md5()
|
||||
m.update(item_list_string.encode())
|
||||
new_widget_hash = m.hexdigest()
|
||||
|
||||
log.debug("set_random_all: {0}".format(item_list_string))
|
||||
log.debug("set_random_all: {0}".format(new_widget_hash))
|
||||
home_window.set_property("random-all", item_list_string)
|
||||
home_window.set_property("random-all-changed", new_widget_hash)
|
||||
|
||||
def set_background_image(force=False):
|
||||
log.debug("set_background_image Called forced={0}", force)
|
||||
log.debug("set_background_image Called forced={0}".format(force))
|
||||
|
||||
global background_current_item
|
||||
global background_items
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
server = settings.getSetting('server_address')
|
||||
user_id = get_current_user_id()
|
||||
|
||||
if force:
|
||||
background_current_item = 0
|
||||
del background_items
|
||||
background_items = []
|
||||
|
||||
if len(background_items) == 0:
|
||||
log.debug("set_background_image: Need to load more backgrounds {0} - {1}",
|
||||
len(background_items), background_current_item)
|
||||
log.debug("Need to load more backgrounds {0} - {1}".format(
|
||||
len(background_items), background_current_item)
|
||||
)
|
||||
|
||||
url_params = {}
|
||||
url_params["Recursive"] = True
|
||||
# url_params["limit"] = 60
|
||||
url_params["limit"] = 100
|
||||
url_params["SortBy"] = "Random"
|
||||
url_params["IncludeItemTypes"] = "Movie,Series"
|
||||
url_params["ImageTypeLimit"] = 1
|
||||
|
||||
url = get_jellyfin_url('{server}/Users/{userid}/Items', url_params)
|
||||
url = get_jellyfin_url('/Users/{}/Items'.format(user_id), url_params)
|
||||
|
||||
server = downloadUtils.get_server()
|
||||
results = downloadUtils.download_url(url, suppress=True)
|
||||
results = json.loads(results)
|
||||
results = api.get(url)
|
||||
|
||||
if results is not None:
|
||||
items = results.get("Items", [])
|
||||
background_current_item = 0
|
||||
background_items = []
|
||||
for item in items:
|
||||
bg_image = downloadUtils.get_artwork(item, "Backdrop", server=server)
|
||||
bg_image = get_art_url(
|
||||
item, "Backdrop", server=server)
|
||||
if bg_image:
|
||||
label = item.get("Name")
|
||||
item_background = {}
|
||||
@@ -105,12 +192,18 @@ def set_background_image(force=False):
|
||||
item_background["name"] = label
|
||||
background_items.append(item_background)
|
||||
|
||||
log.debug("set_background_image: Loaded {0} more backgrounds", len(background_items))
|
||||
log.debug("set_background_image: Loaded {0} more backgrounds".format(
|
||||
len(background_items))
|
||||
)
|
||||
|
||||
if len(background_items) > 0:
|
||||
bg_image = background_items[background_current_item].get("image")
|
||||
label = background_items[background_current_item].get("name")
|
||||
log.debug("set_background_image: {0} - {1} - {2}", background_current_item, label, bg_image)
|
||||
log.debug(
|
||||
"set_background_image: {0} - {1} - {2}".format(
|
||||
background_current_item, label, bg_image
|
||||
)
|
||||
)
|
||||
|
||||
background_current_item += 1
|
||||
if background_current_item >= len(background_items):
|
||||
@@ -127,14 +220,16 @@ def check_for_new_content():
|
||||
|
||||
home_window = HomeWindow()
|
||||
settings = xbmcaddon.Addon()
|
||||
simple_new_content_check = settings.getSetting("simple_new_content_check") == "true"
|
||||
simple_new_content_check = settings.getSetting(
|
||||
"simple_new_content_check") == "true"
|
||||
|
||||
if simple_new_content_check:
|
||||
log.debug("Using simple new content check")
|
||||
current_time_stamp = str(time.time())
|
||||
home_window.set_property("jellycon_widget_reload", current_time_stamp)
|
||||
log.debug("Setting New Widget Hash: {0}", current_time_stamp)
|
||||
log.debug("Setting New Widget Hash: {0}".format(current_time_stamp))
|
||||
return
|
||||
user_id = get_current_user_id()
|
||||
|
||||
url_params = {}
|
||||
url_params["Recursive"] = True
|
||||
@@ -144,13 +239,11 @@ def check_for_new_content():
|
||||
url_params["SortOrder"] = "Descending"
|
||||
url_params["IncludeItemTypes"] = "Movie,Episode"
|
||||
url_params["ImageTypeLimit"] = 0
|
||||
url_params["format"] = "json"
|
||||
|
||||
added_url = get_jellyfin_url('{server}/Users/{userid}/Items', url_params)
|
||||
added_url = get_jellyfin_url('/Users/{}/Items'.format(user_id), url_params)
|
||||
|
||||
added_result = downloadUtils.download_url(added_url, suppress=True)
|
||||
result = json.loads(added_result)
|
||||
log.debug("LATEST_ADDED_ITEM: {0}", result)
|
||||
result = api.get(added_url)
|
||||
log.debug("LATEST_ADDED_ITEM: {0}".format(result))
|
||||
|
||||
last_added_date = ""
|
||||
if result is not None:
|
||||
@@ -158,7 +251,7 @@ def check_for_new_content():
|
||||
if len(items) > 0:
|
||||
item = items[0]
|
||||
last_added_date = item.get("Etag", "")
|
||||
log.debug("last_added_date: {0}", last_added_date)
|
||||
log.debug("last_added_date: {0}".format(last_added_date))
|
||||
|
||||
url_params = {}
|
||||
url_params["Recursive"] = True
|
||||
@@ -168,53 +261,56 @@ def check_for_new_content():
|
||||
url_params["SortOrder"] = "Descending"
|
||||
url_params["IncludeItemTypes"] = "Movie,Episode"
|
||||
url_params["ImageTypeLimit"] = 0
|
||||
url_params["format"] = "json"
|
||||
|
||||
played_url = get_jellyfin_url('{server}/Users/{userid}/Items', url_params)
|
||||
played_url = get_jellyfin_url(
|
||||
'/Users/{}/Items'.format(user_id), url_params
|
||||
)
|
||||
|
||||
played_result = downloadUtils.download_url(played_url, suppress=True)
|
||||
result = json.loads(played_result)
|
||||
log.debug("LATEST_PLAYED_ITEM: {0}", result)
|
||||
result = api.get(played_url)
|
||||
log.debug("LATEST_PLAYED_ITEM: {0}".format(result))
|
||||
|
||||
last_played_date = ""
|
||||
if result is not None:
|
||||
items = result.get("Items", [])
|
||||
if len(items) > 0:
|
||||
item = items[0]
|
||||
# last_played_date = item.get("Etag", "")
|
||||
user_data = item.get("UserData", None)
|
||||
if user_data is not None:
|
||||
last_played_date = user_data.get("LastPlayedDate", "")
|
||||
|
||||
log.debug("last_played_date: {0}", last_played_date)
|
||||
log.debug("last_played_date: {0}".format(last_played_date))
|
||||
|
||||
current_widget_hash = home_window.get_property("jellycon_widget_reload")
|
||||
log.debug("Current Widget Hash: {0}", current_widget_hash)
|
||||
log.debug("Current Widget Hash: {0}".format(current_widget_hash))
|
||||
|
||||
m = hashlib.md5()
|
||||
m.update(last_played_date + last_added_date)
|
||||
m.update((last_played_date + last_added_date).encode())
|
||||
new_widget_hash = m.hexdigest()
|
||||
log.debug("New Widget Hash: {0}", new_widget_hash)
|
||||
log.debug("New Widget Hash: {0}".format(new_widget_hash))
|
||||
|
||||
if current_widget_hash != new_widget_hash:
|
||||
home_window.set_property("jellycon_widget_reload", new_widget_hash)
|
||||
log.debug("Setting New Widget Hash: {0}", new_widget_hash)
|
||||
log.debug("Setting New Widget Hash: {0}".format(new_widget_hash))
|
||||
|
||||
|
||||
@timer
|
||||
def get_widget_content_cast(handle, params):
|
||||
log.debug("getWigetContentCast Called: {0}", params)
|
||||
server = downloadUtils.get_server()
|
||||
log.debug("getWigetContentCast Called: {0}".format(params))
|
||||
settings = xbmcaddon.Addon()
|
||||
server = settings.getSetting('server_address')
|
||||
user_id = get_current_user_id()
|
||||
|
||||
item_id = params["id"]
|
||||
data_manager = DataManager()
|
||||
result = data_manager.get_content("{server}/Users/{userid}/Items/" + item_id + "?format=json")
|
||||
log.debug("ItemInfo: {0}", result)
|
||||
result = api.get(
|
||||
"/Users/{}/Items/{}".format(user_id, item_id))
|
||||
log.debug("ItemInfo: {0}".format(result))
|
||||
|
||||
if not result:
|
||||
return
|
||||
|
||||
if result.get("Type", "") in ["Episode", "Season"] and params.get("auto", "true") == "true":
|
||||
if (result.get("Type", "") in ["Episode", "Season"] and
|
||||
params.get("auto", "true") == "true"):
|
||||
|
||||
series_id = result.get("SeriesId")
|
||||
if series_id:
|
||||
params["id"] = series_id
|
||||
@@ -227,12 +323,6 @@ def get_widget_content_cast(handle, params):
|
||||
people = []
|
||||
|
||||
for person in people:
|
||||
# if (person.get("Type") == "Director"):
|
||||
# director = director + person.get("Name") + ' '
|
||||
# if (person.get("Type") == "Writing"):
|
||||
# writer = person.get("Name")
|
||||
# if (person.get("Type") == "Writer"):
|
||||
# writer = person.get("Name")
|
||||
if person.get("Type") == "Actor":
|
||||
person_name = person.get("Name")
|
||||
person_role = person.get("Role")
|
||||
@@ -240,12 +330,12 @@ def get_widget_content_cast(handle, params):
|
||||
person_tag = person.get("PrimaryImageTag")
|
||||
person_thumbnail = None
|
||||
if person_tag:
|
||||
person_thumbnail = downloadUtils.image_url(person_id, "Primary", 0, 400, 400, person_tag, server=server)
|
||||
person_thumbnail = image_url(
|
||||
person_id, "Primary", 0, 400, 400,
|
||||
person_tag, server=server
|
||||
)
|
||||
|
||||
if kodi_version > 17:
|
||||
list_item = xbmcgui.ListItem(label=person_name, offscreen=True)
|
||||
else:
|
||||
list_item = xbmcgui.ListItem(label=person_name)
|
||||
list_item = xbmcgui.ListItem(label=person_name, offscreen=True)
|
||||
|
||||
list_item.setProperty("id", person_id)
|
||||
|
||||
@@ -262,8 +352,8 @@ def get_widget_content_cast(handle, params):
|
||||
if person_role:
|
||||
list_item.setLabel2(person_role)
|
||||
|
||||
item_tupple = ("", list_item, False)
|
||||
list_items.append(item_tupple)
|
||||
item_tuple = ("", list_item, False)
|
||||
list_items.append(item_tuple)
|
||||
|
||||
xbmcplugin.setContent(handle, 'artists')
|
||||
xbmcplugin.addDirectoryItems(handle, list_items)
|
||||
@@ -272,24 +362,26 @@ def get_widget_content_cast(handle, params):
|
||||
|
||||
@timer
|
||||
def get_widget_content(handle, params):
|
||||
log.debug("getWigetContent Called: {0}", params)
|
||||
log.debug("getWigetContent Called: {0}".format(params))
|
||||
|
||||
settings = xbmcaddon.Addon()
|
||||
item_limit = int(settings.getSetting("show_x_filtered_items"))
|
||||
hide_watched = settings.getSetting("hide_watched") == "true"
|
||||
use_cached_widget_data = settings.getSetting("use_cached_widget_data") == "true"
|
||||
use_cached_widget_data = settings.getSetting(
|
||||
"use_cached_widget_data") == "true"
|
||||
|
||||
widget_type = params.get("type")
|
||||
if widget_type is None:
|
||||
log.error("getWigetContent type not set")
|
||||
return
|
||||
user_id = get_current_user_id()
|
||||
|
||||
log.debug("widget_type: {0}", widget_type)
|
||||
log.debug("widget_type: {0}".format(widget_type))
|
||||
|
||||
url_verb = "{server}/Users/{userid}/Items"
|
||||
url_verb = "/Users/{}/Items".format(user_id)
|
||||
url_params = {}
|
||||
url_params["Limit"] = "{ItemLimit}"
|
||||
url_params["format"] = "json"
|
||||
url_params["Fields"] = "{field_filters}"
|
||||
url_params["Limit"] = item_limit
|
||||
url_params["Fields"] = get_default_filters()
|
||||
url_params["ImageTypeLimit"] = 1
|
||||
url_params["IsMissing"] = False
|
||||
|
||||
@@ -303,6 +395,7 @@ def get_widget_content(handle, params):
|
||||
url_params["IsPlayed"] = False
|
||||
url_params["IsVirtualUnaired"] = False
|
||||
url_params["IncludeItemTypes"] = "Movie"
|
||||
url_params["Limit"] = item_limit
|
||||
|
||||
elif widget_type == "inprogress_movies":
|
||||
xbmcplugin.setContent(handle, 'movies')
|
||||
@@ -312,26 +405,33 @@ def get_widget_content(handle, params):
|
||||
url_params["Filters"] = "IsResumable"
|
||||
url_params["IsVirtualUnaired"] = False
|
||||
url_params["IncludeItemTypes"] = "Movie"
|
||||
url_params["Limit"] = item_limit
|
||||
|
||||
elif widget_type == "random_movies":
|
||||
home_window = HomeWindow()
|
||||
xbmcplugin.setContent(handle, 'movies')
|
||||
url_params["Ids"] = "{random_movies}"
|
||||
url_params["Ids"] = home_window.get_property("random-movies")
|
||||
|
||||
elif widget_type == "random_tvshows":
|
||||
home_window = HomeWindow()
|
||||
xbmcplugin.setContent(handle, 'tvshows')
|
||||
url_params["Ids"] = home_window.get_property("random-tvshows")
|
||||
|
||||
elif widget_type == "recent_tvshows":
|
||||
xbmcplugin.setContent(handle, 'episodes')
|
||||
url_verb = '{server}/Users/{userid}/Items/Latest'
|
||||
url_verb = '/Users/{}/Items/Latest'.format(user_id)
|
||||
url_params["GroupItems"] = True
|
||||
url_params["Limit"] = 45
|
||||
url_params["Recursive"] = True
|
||||
url_params["SortBy"] = "DateCreated"
|
||||
url_params["SortOrder"] = "Descending"
|
||||
url_params["Fields"] = "{field_filters}"
|
||||
url_params["Fields"] = get_default_filters()
|
||||
if hide_watched:
|
||||
url_params["IsPlayed"] = False
|
||||
url_params["IsVirtualUnaired"] = False
|
||||
url_params["IncludeItemTypes"] = "Episode"
|
||||
url_params["ImageTypeLimit"] = 1
|
||||
url_params["format"] = "json"
|
||||
url_params["Limit"] = item_limit
|
||||
|
||||
elif widget_type == "recent_episodes":
|
||||
xbmcplugin.setContent(handle, 'episodes')
|
||||
@@ -343,6 +443,7 @@ def get_widget_content(handle, params):
|
||||
url_params["IsPlayed"] = False
|
||||
url_params["IsVirtualUnaired"] = False
|
||||
url_params["IncludeItemTypes"] = "Episode"
|
||||
url_params["Limit"] = item_limit
|
||||
|
||||
elif widget_type == "inprogress_episodes":
|
||||
xbmcplugin.setContent(handle, 'episodes')
|
||||
@@ -352,72 +453,142 @@ def get_widget_content(handle, params):
|
||||
url_params["Filters"] = "IsResumable"
|
||||
url_params["IsVirtualUnaired"] = False
|
||||
url_params["IncludeItemTypes"] = "Episode"
|
||||
url_params["Limit"] = item_limit
|
||||
|
||||
elif widget_type == "nextup_episodes":
|
||||
xbmcplugin.setContent(handle, 'episodes')
|
||||
url_verb = "{server}/Shows/NextUp"
|
||||
url_params["Limit"] = "{ItemLimit}"
|
||||
url_params["userid"] = "{userid}"
|
||||
url_verb = "/Shows/NextUp"
|
||||
url_params = url_params.copy()
|
||||
url_params["Limit"] = item_limit
|
||||
url_params["userid"] = user_id
|
||||
url_params["Recursive"] = True
|
||||
url_params["Fields"] = "{field_filters}"
|
||||
url_params["format"] = "json"
|
||||
url_params["enableResumable"] = False
|
||||
url_params["ImageTypeLimit"] = 1
|
||||
# check if rewatching is enabled and combine is disabled
|
||||
rewatch_days = int(settings.getSetting("rewatch_days"))
|
||||
if rewatch_days > 0 and settings.getSetting("rewatch_combine") != "true":
|
||||
rewatch_since = datetime.datetime.today() - datetime.timedelta(days=rewatch_days)
|
||||
url_params["nextUpDateCutoff"] = rewatch_since.strftime("%Y-%m-%d")
|
||||
url_params["enableRewatching"] = True
|
||||
# Collect InProgress items to be combined with NextUp
|
||||
inprogress_url_verb = "/Users/{}/Items".format(user_id)
|
||||
inprogress_url_params = url_params.copy()
|
||||
inprogress_url_params["Recursive"] = True
|
||||
inprogress_url_params["SortBy"] = "DatePlayed"
|
||||
inprogress_url_params["SortOrder"] = "Descending"
|
||||
inprogress_url_params["Filters"] = "IsResumable"
|
||||
inprogress_url_params["IsVirtualUnaired"] = False
|
||||
inprogress_url_params["IncludeItemTypes"] = "Episode"
|
||||
inprogress_url_params["Limit"] = item_limit
|
||||
|
||||
|
||||
|
||||
elif widget_type == "movie_recommendations":
|
||||
suggested_items_url_params = {}
|
||||
suggested_items_url_params["userId"] = "{userid}"
|
||||
suggested_items_url_params["userId"] = user_id
|
||||
suggested_items_url_params["categoryLimit"] = 15
|
||||
suggested_items_url_params["ItemLimit"] = 20
|
||||
suggested_items_url_params["ItemLimit"] = item_limit
|
||||
suggested_items_url_params["ImageTypeLimit"] = 0
|
||||
suggested_items_url = get_jellyfin_url("{server}/Movies/Recommendations", suggested_items_url_params)
|
||||
suggested_items_url = get_jellyfin_url(
|
||||
"/Movies/Recommendations", suggested_items_url_params)
|
||||
|
||||
data_manager = DataManager()
|
||||
suggested_items = data_manager.get_content(suggested_items_url)
|
||||
suggested_items = api.get(suggested_items_url)
|
||||
ids = []
|
||||
set_id = 0
|
||||
while len(ids) < 20 and suggested_items:
|
||||
while len(ids) < item_limit and suggested_items:
|
||||
items = suggested_items[set_id]
|
||||
log.debug("BaselineItemName : {0} - {1}", set_id, items.get("BaselineItemName"))
|
||||
log.debug(
|
||||
"BaselineItemName : {0} - {1}".format(
|
||||
set_id, items.get("BaselineItemName")
|
||||
)
|
||||
)
|
||||
items = items["Items"]
|
||||
rand = random.randint(0, len(items) - 1)
|
||||
# log.debug("random suggestions index : {0} {1}", rand, set_id)
|
||||
item = items[rand]
|
||||
if item["Type"] == "Movie" and item["Id"] not in ids and (not item["UserData"]["Played"] or not hide_watched):
|
||||
# log.debug("random suggestions adding : {0}", item["Id"])
|
||||
|
||||
if (item["Type"] == "Movie" and item["Id"] not in ids and
|
||||
(not item["UserData"]["Played"] or not hide_watched)):
|
||||
|
||||
ids.append(item["Id"])
|
||||
# else:
|
||||
# log.debug("random suggestions not valid : {0} - {1} - {2}", item["Id"], item["Type"], item["UserData"]["Played"])
|
||||
|
||||
del items[rand]
|
||||
# log.debug("items len {0}", len(items))
|
||||
if len(items) == 0:
|
||||
# log.debug("Removing Set {0}", set_id)
|
||||
del suggested_items[set_id]
|
||||
set_id += 1
|
||||
if set_id >= len(suggested_items):
|
||||
set_id = 0
|
||||
|
||||
id_list = ",".join(ids)
|
||||
log.debug("Recommended Items : {0}", len(ids), id_list)
|
||||
log.debug("Recommended Items : {0}".format(len(ids)))
|
||||
url_params["Ids"] = id_list
|
||||
|
||||
elif widget_type == "random_all":
|
||||
home_window = HomeWindow()
|
||||
xbmcplugin.setContent(handle, 'movies')
|
||||
url_params["Ids"] = home_window.get_property("random-all")
|
||||
|
||||
elif widget_type == "recent_all":
|
||||
xbmcplugin.setContent(handle, 'movies')
|
||||
url_verb = '/Users/{}/Items/Latest'.format(user_id)
|
||||
url_params["GroupItems"] = True
|
||||
url_params["Recursive"] = True
|
||||
url_params["SortBy"] = "DateCreated"
|
||||
url_params["SortOrder"] = "Descending"
|
||||
url_params["Limit"] = 45
|
||||
url_params["Filters"] = "IsNotFolder"
|
||||
url_params["Fields"] = get_default_filters()
|
||||
if hide_watched:
|
||||
url_params["IsPlayed"] = False
|
||||
url_params["IsVirtualUnaired"] = False
|
||||
url_params["IncludeItemTypes"] = "Episode, Movie"
|
||||
url_params["ImageTypeLimit"] = 1
|
||||
|
||||
elif widget_type == "favorites_all":
|
||||
home_window = HomeWindow()
|
||||
xbmcplugin.setContent(handle, 'movies')
|
||||
|
||||
url_params.update({
|
||||
"Filters": "IsFavorite",
|
||||
"IncludeItemTypes": "Movie,Series",
|
||||
"Recursive": True,
|
||||
"SortBy": "Name",
|
||||
"SortOrder": "Ascending",
|
||||
})
|
||||
|
||||
items_url = get_jellyfin_url(url_verb, url_params)
|
||||
|
||||
list_items, detected_type, total_records = process_directory(items_url, None, params, use_cached_widget_data)
|
||||
if (url_params.get('IncludeItemTypes', '') == 'Episode' or
|
||||
params.get('type', '') == 'nextup_episodes'):
|
||||
params["name_format"] = "Episode|episode_name_format"
|
||||
|
||||
# remove resumable items from next up
|
||||
list_items, detected_type, total_records = process_directory(
|
||||
items_url, None, params, use_cached_widget_data)
|
||||
|
||||
# Combine In Progress and Next Up Episodes, add next up after In Progress
|
||||
if widget_type == "nextup_episodes":
|
||||
filtered_list = []
|
||||
for item in list_items:
|
||||
resume_time = item[1].getProperty("ResumeTime")
|
||||
if resume_time is None or float(resume_time) == 0.0:
|
||||
filtered_list.append(item)
|
||||
list_items = filtered_list
|
||||
inprogress_url = get_jellyfin_url(
|
||||
inprogress_url_verb, inprogress_url_params)
|
||||
|
||||
# list_items = populateWidgetItems(items_url, widget_type)
|
||||
params["name_format"] = "Episode|episode_name_format"
|
||||
inprogress, detected_type, total_records = process_directory(
|
||||
inprogress_url, None, params, use_cached_widget_data)
|
||||
|
||||
list_items = inprogress + list_items
|
||||
|
||||
# add rewatch combine is enabled
|
||||
if rewatch_days > 0 and settings.getSetting("rewatch_combine") == "true":
|
||||
rewatch_since = datetime.datetime.today() - datetime.timedelta(days=rewatch_days)
|
||||
url_params["nextUpDateCutoff"] = rewatch_since.strftime("%Y-%m-%d")
|
||||
url_params["enableRewatching"] = True
|
||||
rewatch_items_url = get_jellyfin_url(url_verb, url_params)
|
||||
rewatch_items, detected_type, total_records = process_directory(rewatch_items_url, None, params, use_cached_widget_data)
|
||||
for ri in rewatch_items:
|
||||
if not any(i[1].getProperty("id") == ri[1].getProperty("id") for i in list_items):
|
||||
list_items.append(ri)
|
||||
|
||||
if detected_type is not None:
|
||||
# if the media type is not set then try to use the detected type
|
||||
log.debug("Detected content type: {0}", detected_type)
|
||||
log.debug("Detected content type: {0}".format(detected_type))
|
||||
content_type = None
|
||||
|
||||
if detected_type == "Movie":
|
||||
@@ -426,7 +597,7 @@ def get_widget_content(handle, params):
|
||||
content_type = 'episodes'
|
||||
elif detected_type == "Series":
|
||||
content_type = 'tvshows'
|
||||
elif detected_type == "Music" or detected_type == "Audio" or detected_type == "Musicalbum":
|
||||
elif detected_type in ["Music", "Audio", "Musicalbum"]:
|
||||
content_type = 'songs'
|
||||
|
||||
if content_type:
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
|
||||
<setting label="30388" type="lsep"/>
|
||||
<setting label="30011" type="action" action="RunScript(plugin.video.jellycon,0,?mode=DETECT_SERVER_USER)" option="close"/>
|
||||
<setting id="protocol" type="select" label="30390" lvalues="30391|30392" default="0" />
|
||||
<setting id="ipaddress" type="text" label="30000" default="<none>" visible="true" enable="true" />
|
||||
<setting id="port" type="text" label="30001" default="8096" visible="true" enable="true" />
|
||||
<setting id="verify_cert" type="bool" label="30003" default="false" visible="true" enable="true" />
|
||||
<setting id="ipaddress" type="text" label="30000" default="" visible="false" enable="false" />
|
||||
<setting id="protocol" type="select" label="30390" lvalues="30391|30392" default="0" visible="false"/>
|
||||
<setting id="port" type="text" label="30001" default="8096" visible="false" enable="false" />
|
||||
<setting id="server_address" type="text" label="30000" default="" visible="true" enable="true" />
|
||||
<setting id="verify_cert" type="bool" label="30003" default="true" visible="true" enable="true" />
|
||||
|
||||
<setting label="30389" type="lsep"/>
|
||||
<setting type="sep" />
|
||||
<setting label="30012" type="action" action="RunScript(plugin.video.jellycon,0,?mode=CHANGE_USER)" option="close"/>
|
||||
<setting id="username" type="text" label="30024" />
|
||||
<setting id="password" type="text" option="hidden" label="30025" />
|
||||
<setting id="save_user_to_settings" type="bool" label="30378" default="true" visible="true" enable="true" />
|
||||
<setting id="allow_password_saving" type="bool" label="30367" default="true" visible="true" enable="true" />
|
||||
|
||||
@@ -28,29 +28,36 @@
|
||||
|
||||
<setting label="30238" type="lsep"/>
|
||||
<setting type="sep" />
|
||||
<setting id="max_stream_bitrate" type="slider" label="30208" default="75000" range="400,100,100000" option="int" visible="true"/>
|
||||
<setting id="max_stream_bitrate" type="enum" label="30208" values="0.5 Mbps|1 Mbps|1.5 Mbps|2.0 Mbps|2.5 Mbps|3.0 Mbps|4.0 Mbps|5.0 Mbps|6.0 Mbps|7.0 Mbps|8.0 Mbps|9.0 Mbps|10.0 Mbps|12.0 Mbps|14.0 Mbps|16.0 Mbps|18.0 Mbps|20.0 Mbps|25.0 Mbps|30.0 Mbps|35.0 Mbps|40.0 Mbps|100.0 Mbps|1000.0 Mbps [default]|Maximum" visible="true" default="23" />
|
||||
<setting id="allow_direct_file_play" type="bool" label="30433" default="false" visible="true" enable="true" />
|
||||
<setting id="transcode_target_video_codec" type="select" label="30679" lvalues="30680|30681|30683" default="0" visible="true" />
|
||||
<setting id="force_transcode_h264" type="bool" label="30682" default="false" visible="true" />
|
||||
<setting id="force_transcode_h265" type="bool" label="30236" default="false" visible="true" enable="true" />
|
||||
<setting id="force_transcode_mpeg2" type="bool" label="30239" default="false" visible="true" enable="true" />
|
||||
<setting id="force_transcode_msmpeg4v3" type="bool" label="30240" default="false" visible="true" enable="true" />
|
||||
<setting id="force_transcode_mpeg4" type="bool" label="30241" default="false" visible="true" enable="true" />
|
||||
<setting id="force_transcode_av1" type="bool" label="30242" default="false" visible="true" enable="true" />
|
||||
<setting id="direct_stream_sub_select" type="select" label="30379" lvalues="30380|30381|30382" default="0" visible="true"/>
|
||||
|
||||
<setting label="30211" type="lsep"/>
|
||||
<setting type="sep" />
|
||||
<setting id="force_max_stream_bitrate" type="slider" label="30434" default="7000" range="400,100,15000" option="int" visible="true"/>
|
||||
<setting id="force_max_stream_bitrate" type="enum" label="30434" values="0.5 Mbps|1 Mbps|1.5 Mbps|2.0 Mbps|2.5 Mbps|3.0 Mbps|4.0 Mbps|5.0 Mbps|6.0 Mbps|7.0 Mbps [default]|8.0 Mbps|9.0 Mbps|10.0 Mbps|12.0 Mbps|14.0 Mbps|16.0 Mbps|18.0 Mbps|20.0 Mbps|25.0 Mbps|30.0 Mbps|35.0 Mbps|40.0 Mbps|100.0 Mbps|1000.0 Mbps|Maximum" visible="true" default="9" />
|
||||
<setting id="playback_max_width" type="select" label="30212" values="640|720|1024|1280|1440|1600|1920|2600|4096" default="1920" visible="true"/>
|
||||
<setting id="playback_video_force_8" type="bool" label="30213" default="false" visible="true" enable="true"/>
|
||||
<setting id="audio_codec" type="select" label="30419" values="ac3|aac" default="ac3"/>
|
||||
<setting id="audio_playback_bitrate" type="select" label="30418" values="128|160|192|256|320|384|448|640" default="256" visible="true"/>
|
||||
<setting id="audio_max_channels" type="slider" label="30420" default="8" range="2,1,8" option="int" visible="true"/>
|
||||
|
||||
<!--
|
||||
<setting label="30209" type="lsep"/>
|
||||
<setting label="30685" type="lsep"/>
|
||||
<setting type="sep" />
|
||||
<setting id="smbusername" type="text" label="30007" default="" enable="true" visible="true"/>
|
||||
<setting id="smbpassword" type="text" label="30008" default="" option="hidden" enable="true" visible="true"/>
|
||||
-->
|
||||
<setting id="preferred_audio_language" type="select" label="30686" lvalues="30691|30692|30693|30694|30695|30696|30697|30698" default="0" visible="true"/>
|
||||
<setting id="auto_select_default_audio" type="bool" label="30687" default="true" visible="true"/>
|
||||
<setting id="preferred_subtitle_language" type="select" label="30688" lvalues="30699|30691|30692|30693|30694|30695|30696|30697|30698" default="1" visible="true"/>
|
||||
<setting id="prefer_forced_subtitles" type="bool" label="30689" default="true" visible="true"/>
|
||||
<setting id="only_forced_subtitles" type="bool" label="30702" default="true" visible="true"/>
|
||||
<setting id="subtitle_codec_preference" type="select" label="30703" lvalues="30704|30705|30706" default="0" visible="true"/>
|
||||
<setting id="auto_no_subtitles_if_no_match" type="bool" label="30700" default="true" visible="true"/>
|
||||
<setting id="max_play_queue" type="slider" label="30447" default="200" range="20, 10, 1000" option="int" visible="true"/>
|
||||
|
||||
</category>
|
||||
<category label="30214">
|
||||
@@ -73,8 +80,8 @@
|
||||
|
||||
<setting label="30329" type="lsep"/>
|
||||
<setting type="sep" />
|
||||
<setting id="stopPlaybackOnScreensaver" type="bool" label="30332" default="true" visible="true" enable="true" />
|
||||
<setting id="changeUserOnScreenSaver" type="bool" label="30330" default="true" visible="true" enable="true" />
|
||||
<setting id="stopPlaybackOnScreensaver" type="bool" label="30332" default="false" visible="true" enable="true" />
|
||||
<setting id="changeUserOnScreenSaver" type="bool" label="30330" default="false" visible="true" enable="true" />
|
||||
<setting id="cacheImagesOnScreenSaver" type="bool" label="30333" default="true" visible="true" enable="true" />
|
||||
<setting id="cacheImagesOnScreenSaver_interval" type="slider" label="30400" default="0" range="0,1,60" option="int" visible="true"/>
|
||||
|
||||
@@ -86,12 +93,13 @@
|
||||
<setting id="addCounts" type="bool" label="30116" default="false" visible="true" enable="true" />
|
||||
<setting id="addResumePercent" type="bool" label="30118" default="false" visible="true" enable="true" />
|
||||
<setting id="addSubtitleAvailable" type="bool" label="30163" default="false" visible="true" enable="true" />
|
||||
<setting id="hide_x_filtered_items_count" type="bool" label="30453" default="false" visible="true" enable="true" />
|
||||
<setting id="include_overview" type="bool" label="30181" default="true" visible="true" enable="true" />
|
||||
<setting id="include_media" type="bool" label="30182" default="true" visible="true" enable="true" />
|
||||
<setting id="add_user_ratings" type="bool" label="30348" default="true" visible="true" enable="true" />
|
||||
<setting id="include_people" type="bool" label="30183" default="false" visible="true" enable="true" />
|
||||
<setting id="hide_unwatched_details" type="bool" label="30023" default="false" visible="true" enable="true" />
|
||||
<setting id="episode_name_format" type="select" label="30019" default="{SeriesName} - {ItemName}" values="{SeriesName} - {ItemName}|{ItemName}|s{SeasonIndex}e{EpisodeIndex} - {ItemName}|{SeriesName} - s{SeasonIndex}e{EpisodeIndex} - {ItemName}" />
|
||||
<setting id="episode_name_format" type="select" label="30019" default="{SeriesName} - {ItemName}" values="{SeriesName} - {ItemName}|{ItemName}|S{SeasonIndex}E{EpisodeIndex} - {ItemName}|{SeriesName} - S{SeasonIndex}E{EpisodeIndex} - {ItemName}" />
|
||||
|
||||
<setting label="30222" type="lsep"/>
|
||||
<setting type="sep" />
|
||||
@@ -101,16 +109,48 @@
|
||||
<setting id="show_empty_folders" type="bool" label="30328" default="false" visible="true" enable="true" />
|
||||
<setting id="hide_watched" type="bool" label="30432" default="true" visible="true" enable="true" />
|
||||
|
||||
<setting label="30450" type="lsep"/>
|
||||
<setting type="sep" />
|
||||
<setting id="rewatch_days" type="slider" label="30451" default="0" range="0,1,365" option="int" visible="true"/>
|
||||
<setting id="rewatch_combine" type="bool" label="30452" default="false" visible="true" enable="true" />
|
||||
|
||||
<setting label="30223" type="lsep"/>
|
||||
<setting type="sep" />
|
||||
<setting id="moviePageSize" type="slider" label="30331" default="0" range="0,1,100" option="int" visible="true"/>
|
||||
<setting id="showPageSize" type="slider" label="30678" default="0" range="0,1,100" option="int" visible="true"/>
|
||||
<setting id="show_x_filtered_items" type="slider" label="30018" default="20" range="5,1,100" option="int" visible="true"/>
|
||||
|
||||
<setting label="30224" type="lsep"/>
|
||||
<setting type="sep" />
|
||||
<setting id="widget_select_action" type="select" label="30026" lvalues="30313|30314" default="0" visible="true"/>
|
||||
<setting id="interface_mode" type="select" label="30225" lvalues="30226|30227" default="0" visible="true"/>
|
||||
|
||||
</category>
|
||||
|
||||
<category label="30666">
|
||||
|
||||
<setting label="30670" type="lsep"/>
|
||||
<setting id="intro_skipper_action" type="select" label="30667" lvalues="30673|30672|30674" default="0" visible="true" enable="true" />
|
||||
<setting id="intro_skipper_start_offset" type="slider" label="30668" default="0" range="0,1,3" option="int" visible="true" enable="true"/>
|
||||
<setting id="intro_skipper_end_offset" type="slider" label="30669" default="1" range="0,1,3" option="int" visible="true" enable="true"/>
|
||||
<setting label="30671" type="lsep"/>
|
||||
<setting id="credit_skipper_action" type="select" label="30667" lvalues="30673|30672|30674" default="0" visible="true" enable="true" />
|
||||
<setting id="credit_skipper_start_offset" type="slider" label="30668" default="0" range="0,1,3" option="int" visible="true" enable="true" />
|
||||
<setting id="credit_skipper_end_offset" type="slider" label="30669" default="1" range="0,1,3" option="int" visible="true" enable="true" />
|
||||
<setting label="30675" type="lsep"/>
|
||||
<setting id="commercial_skipper_action" type="select" label="30667" lvalues="30673|30672|30674" default="0" visible="true" enable="true" />
|
||||
<setting id="commercial_skipper_start_offset" type="slider" label="30668" default="0" range="0,1,3" option="int" visible="true" enable="true"/>
|
||||
<setting id="commercial_skipper_end_offset" type="slider" label="30669" default="1" range="0,1,3" option="int" visible="true" enable="true"/>
|
||||
<setting label="30676" type="lsep"/>
|
||||
<setting id="preview_skipper_action" type="select" label="30667" lvalues="30673|30672|30674" default="0" visible="true" enable="true" />
|
||||
<setting id="preview_skipper_start_offset" type="slider" label="30668" default="0" range="0,1,3" option="int" visible="true" enable="true" />
|
||||
<setting id="preview_skipper_end_offset" type="slider" label="30669" default="1" range="0,1,3" option="int" visible="true" enable="true" />
|
||||
<setting label="30677" type="lsep"/>
|
||||
<setting id="recap_skipper_action" type="select" label="30667" lvalues="30673|30672|30674" default="0" visible="true" enable="true" />
|
||||
<setting id="recap_skipper_start_offset" type="slider" label="30668" default="0" range="0,1,3" option="int" visible="true" enable="true"/>
|
||||
<setting id="recap_skipper_end_offset" type="slider" label="30669" default="1" range="0,1,3" option="int" visible="true" enable="true"/>
|
||||
</category>
|
||||
|
||||
<category label="30111">
|
||||
|
||||
<setting label="30343" type="lsep"/>
|
||||
@@ -129,11 +169,12 @@
|
||||
<setting id="profile_count" type="slider" label="30010" default="0" range="0,1,20" option="int" visible="true" />
|
||||
<setting id="log_debug" type="bool" label="30027" default="false" visible="true" enable="true" />
|
||||
<setting id="log_timing" type="bool" label="30015" default="false" visible="true" enable="true" />
|
||||
<setting id="use_cache" type="bool" label="30345" default="true" visible="true" enable="true" />
|
||||
<setting id="disable_disk_cache" type="bool" label="30684" default="true" visible="true" enable="true" />
|
||||
<setting id="use_cache" type="bool" label="30345" default="false" visible="true" enable="true" />
|
||||
<setting id="use_cached_widget_data" type="bool" label="30441" default="false" visible="true" enable="true" />
|
||||
<setting id="showLoadProgress" type="bool" label="30120" default="false" visible="true" enable="true" />
|
||||
<setting id="suppressErrors" type="bool" label="30315" default="false" visible="true" enable="true" />
|
||||
<setting id="speed_test_data_size" type="slider" label="30436" default="15" range="5,1,100" option="int" visible="true"/>
|
||||
<setting id="speed_test_data_size" type="slider" label="30436" default="10" range="1,1,10" option="int" visible="true"/>
|
||||
|
||||
</category>
|
||||
<category label="30421">
|
||||
@@ -154,4 +195,4 @@
|
||||
<setting id="sort-Episodes" type="select" label="30235" lvalues="30423|30424|30426|30425|30427|30429|30430|30428" default="0" visible="true"/>
|
||||
|
||||
</category>
|
||||
</settings>
|
||||
</settings>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user