Compare commits
592
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68dcba8853 | ||
|
|
ae8f66df1a | ||
|
|
5237ead5cb | ||
|
|
4cbc089de2 | ||
|
|
c45c04659f | ||
|
|
61a9037835 | ||
|
|
ad33c0bcca | ||
|
|
7afc524a9f | ||
|
|
c5a4bc9a1b | ||
|
|
1c0178dbaf | ||
|
|
dbf7feca3e | ||
|
|
d92de8ec4e | ||
|
|
8347da5a58 | ||
|
|
59e248d7de | ||
|
|
12e556e1d1 | ||
|
|
14d3ab93da | ||
|
|
e27ece512f | ||
|
|
b88d8efc8d | ||
|
|
9ec4c2837e | ||
|
|
ffc3fc9ec4 | ||
|
|
a566b4f428 | ||
|
|
69819460f3 | ||
|
|
24a8cb41fe | ||
|
|
1de0443492 | ||
|
|
bb19b380b4 | ||
|
|
b6b936ddf3 | ||
|
|
b00b6acba8 | ||
|
|
3941076c06 | ||
|
|
7401201af2 | ||
|
|
5c586fbf30 | ||
|
|
5c891b7e8e | ||
|
|
5425fcae9e | ||
|
|
4008cce12f | ||
|
|
d227105527 | ||
|
|
508649e6b6 | ||
|
|
b4e25d4345 | ||
|
|
733f925c75 | ||
|
|
40e910192e | ||
|
|
424a3cd892 | ||
|
|
9f6036c8d6 | ||
|
|
5af5749d4a | ||
|
|
f01449f14c | ||
|
|
03dff14ee9 | ||
|
|
e55302592a | ||
|
|
dbeaab052d | ||
|
|
9f07dd5a21 | ||
|
|
b933cd8718 | ||
|
|
c4aaa10308 | ||
|
|
d10536a829 | ||
|
|
1e7fa82e11 | ||
|
|
8d85dde2c6 | ||
|
|
eaaa8dc834 | ||
|
|
5350dbf0ce | ||
|
|
28ffad10ab | ||
|
|
a37517bf6a | ||
|
|
fab9b96c8e | ||
|
|
50d6882a98 | ||
|
|
94064ac7da | ||
|
|
1c5f19a68a | ||
|
|
a26abd0dbb | ||
|
|
fb9080c18a | ||
|
|
15980471b0 | ||
|
|
b11bb9cdac | ||
|
|
474cd45fc5 | ||
|
|
0b6843a1b9 | ||
|
|
fdcdf07fa6 | ||
|
|
5617953d39 | ||
|
|
964144996f | ||
|
|
37214dd413 | ||
|
|
5a08fed0b6 | ||
|
|
443866ef04 | ||
|
|
96275adaff | ||
|
|
33884deb6c | ||
|
|
7db291fc93 | ||
|
|
9df14bd55a | ||
|
|
1e183625c9 | ||
|
|
643be19711 | ||
|
|
21a1770f3f | ||
|
|
07063d855a | ||
|
|
cf95e417f1 | ||
|
|
3f92ed0ea0 | ||
|
|
578b74f2c0 | ||
|
|
8e17b9aea5 | ||
|
|
6f766aae8c | ||
|
|
5797348bb3 | ||
|
|
57ca5067ff | ||
|
|
e8ff8a41de | ||
|
|
0b5dfe826a | ||
|
|
67fbcc8238 | ||
|
|
dd61c7dc21 | ||
|
|
3786b5435f | ||
|
|
1857e047b0 | ||
|
|
648ac7793f | ||
|
|
664ce6421f | ||
|
|
cfb77a1076 | ||
|
|
f65ddbbb9e | ||
|
|
76126271fc | ||
|
|
3faece0b4c | ||
|
|
530d3cd91e | ||
|
|
e659aba176 | ||
|
|
a196a499ae | ||
|
|
58bd9cd7a1 | ||
|
|
9dd9f850c6 | ||
|
|
cbecb74307 | ||
|
|
8ae1e58614 | ||
|
|
83e8ae392d | ||
|
|
c0297f10cb | ||
|
|
41052ae508 | ||
|
|
2d243d51e4 | ||
|
|
fdec80f676 | ||
|
|
5d3b0deb4d | ||
|
|
f68c356944 | ||
|
|
553f8d6ccd | ||
|
|
60fb3e33ae | ||
|
|
9b7c1db509 | ||
|
|
963ce356fb | ||
|
|
dcd0364ecc | ||
|
|
a2da428777 | ||
|
|
876c602710 | ||
|
|
79cb716ced | ||
|
|
ba9c975335 | ||
|
|
ef407bcb3c | ||
|
|
2898a066fe | ||
|
|
7950c4bdb4 | ||
|
|
2499012d88 | ||
|
|
7788669de1 | ||
|
|
d7f6fad3dd | ||
|
|
699c562d34 | ||
|
|
36d8225389 | ||
|
|
17ba9ee96b | ||
|
|
2769fc28d3 | ||
|
|
f5f3cfba50 | ||
|
|
1b1c77d225 | ||
|
|
cfc49e286b | ||
|
|
a2b3677c59 | ||
|
|
e5cfafdb00 | ||
|
|
bff05925e8 | ||
|
|
05f4b2b8ce | ||
|
|
2eac294643 | ||
|
|
f6789f79ea | ||
|
|
0b5976bdb1 | ||
|
|
7d2b2b9809 | ||
|
|
cce92dc1f8 | ||
|
|
fa7e59e842 | ||
|
|
8635f0ddb2 | ||
|
|
c90a423012 | ||
|
|
f0daee669b | ||
|
|
d252b660f5 | ||
|
|
e717a49c0c | ||
|
|
426155e65c | ||
|
|
6b9b446e3d | ||
|
|
ab2b2cfe6e | ||
|
|
4b236c6ed6 | ||
|
|
2396fadf04 | ||
|
|
a3bffb5867 | ||
|
|
1b44fc40af | ||
|
|
b894139ca1 | ||
|
|
daa0662869 | ||
|
|
81de9529c3 | ||
|
|
6b06caf00d | ||
|
|
9370366112 | ||
|
|
32bcf6e615 | ||
|
|
aa804471a7 | ||
|
|
681d8b1ddc | ||
|
|
c82b1f51e3 | ||
|
|
6d048e0003 | ||
|
|
0314910bbe | ||
|
|
3bd831782c | ||
|
|
40f01dca6f | ||
|
|
8dead66b58 | ||
|
|
18807191c0 | ||
|
|
9d9630a27a | ||
|
|
8ac851555d | ||
|
|
27f331a1fc | ||
|
|
e6b4d32506 | ||
|
|
a28ee58a1f | ||
|
|
47749c2d73 | ||
|
|
d6d0ff724a | ||
|
|
ba65700aad | ||
|
|
84a7cfe07d | ||
|
|
9ccd4a5e84 | ||
|
|
616434a00f | ||
|
|
4cf62f73da | ||
|
|
0145aecab4 | ||
|
|
6c4184d1f5 | ||
|
|
9d011b42a9 | ||
|
|
bf81b5cacc | ||
|
|
8d2b6e4097 | ||
|
|
50d8399f09 | ||
|
|
bc99b77dbe | ||
|
|
1c7edc9487 | ||
|
|
90c06fb3c9 | ||
|
|
10a04c16ba | ||
|
|
90a618bd7e | ||
|
|
b630b84ab0 | ||
|
|
a5ee362fc0 | ||
|
|
7c0870b6b8 | ||
|
|
a42264b280 | ||
|
|
e714604ec0 | ||
|
|
c094120f04 | ||
|
|
6691c8ddd7 | ||
|
|
013705c318 | ||
|
|
bda6f92a4d | ||
|
|
7ceb8dc79c | ||
|
|
7f48210c97 | ||
|
|
23c440cd58 | ||
|
|
0097167dec | ||
|
|
21e5f156bb | ||
|
|
08f55314d5 | ||
|
|
577bf09859 | ||
|
|
c446cd2fb0 | ||
|
|
06a8414f12 | ||
|
|
1ac01456a9 | ||
|
|
b86853f06f | ||
|
|
311a2798dd | ||
|
|
fe9998fb9d | ||
|
|
ce648c5d35 | ||
|
|
5a2a9bbf9a | ||
|
|
0f8ab05fd4 | ||
|
|
b87c00c041 | ||
|
|
8999f51dc9 | ||
|
|
d5e19db5e6 | ||
|
|
675bee83ca | ||
|
|
33e5dd1fdb | ||
|
|
4ff2794c83 | ||
|
|
81f9302da1 | ||
|
|
93f4b8b537 | ||
|
|
0587d2f8db | ||
|
|
6ba25b5468 | ||
|
|
cc10969506 | ||
|
|
c2eb50a7ee | ||
|
|
33d24068fd | ||
|
|
3a4c191b11 | ||
|
|
e06b4ccb3f | ||
|
|
3c6b86ea28 | ||
|
|
c4a9a13d6c | ||
|
|
c0f1a3c603 | ||
|
|
9d3425061a | ||
|
|
c2dcd2f67d | ||
|
|
24b822aecd | ||
|
|
a7d3de766f | ||
|
|
b56c897e4b | ||
|
|
df14032107 | ||
|
|
66b4821f7f | ||
|
|
d301cde266 | ||
|
|
0590a0d722 | ||
|
|
fc71a03a12 | ||
|
|
923c794e39 | ||
|
|
e7fbff5b3f | ||
|
|
1bd556fbb3 | ||
|
|
18a870f8c3 | ||
|
|
3e2a2c3bee | ||
|
|
73e74881a6 | ||
|
|
b37112600e | ||
|
|
6172ce4960 | ||
|
|
3d277e1c01 | ||
|
|
b3b13899f1 | ||
|
|
7c4a59539a | ||
|
|
e6dfb3da16 | ||
|
|
8e220ededa | ||
|
|
11126f8083 | ||
|
|
ac8a13db22 | ||
|
|
5ab10ff97a | ||
|
|
f3b0346ba2 | ||
|
|
96c94f97f4 | ||
|
|
192c0200e5 | ||
|
|
03ae8f459c | ||
|
|
377fdd9e5e | ||
|
|
daec7d20fe | ||
|
|
66a149590b | ||
|
|
1b6f010df2 | ||
|
|
7e4bc29b59 | ||
|
|
0284fa9b0a | ||
|
|
e5bcea59b5 | ||
|
|
16f603ced2 | ||
|
|
bdcb3b7e33 | ||
|
|
0def6fcfe3 | ||
|
|
75a352fef3 | ||
|
|
07eb1f7f4c | ||
|
|
8e35c02763 | ||
|
|
c1f6d9a858 | ||
|
|
3e20a3bac7 | ||
|
|
818570fd2d | ||
|
|
bcd2d22fbf | ||
|
|
ffc99cd4f4 | ||
|
|
bb56750c1a | ||
|
|
b08d587a22 | ||
|
|
47f4132b39 | ||
|
|
faefab5554 | ||
|
|
243a033055 | ||
|
|
db1eeaae38 | ||
|
|
8c2960e891 | ||
|
|
d6a86e8616 | ||
|
|
5260f42378 | ||
|
|
84f28f3c54 | ||
|
|
860b6793fb | ||
|
|
df03409d7a | ||
|
|
6a81f2241d | ||
|
|
5ce817cee6 | ||
|
|
7cdf124f9d | ||
|
|
ff46aa0226 | ||
|
|
669e331f6c | ||
|
|
4179ba642b | ||
|
|
00954d98f7 | ||
|
|
037e77860b | ||
|
|
47e187449d | ||
|
|
06e9afbe69 | ||
|
|
bfe8aa5f5f | ||
|
|
e51ddd7a50 | ||
|
|
442552c024 | ||
|
|
ce4806df64 | ||
|
|
0c2e65c92b | ||
|
|
b01aa2b385 | ||
|
|
2e04890756 | ||
|
|
1657857b4a | ||
|
|
72383592ba | ||
|
|
d093f935f9 | ||
|
|
8cc7d101aa | ||
|
|
f39eebbd22 | ||
|
|
3ac8bc738a | ||
|
|
0eac041a26 | ||
|
|
ab0f5daaf3 | ||
|
|
b59a0f82ab | ||
|
|
9b75e6af5c | ||
|
|
aa37f2b0ef | ||
|
|
d22237a5cc | ||
|
|
26f5e8aa4b | ||
|
|
9072c6cae0 | ||
|
|
8739c1197f | ||
|
|
a477973862 | ||
|
|
95ce26d261 | ||
|
|
8c934c1ca8 | ||
|
|
349d7d4866 | ||
|
|
f1ea8fa693 | ||
|
|
685210aee3 | ||
|
|
ae42b62b3c | ||
|
|
7faa7c3dba | ||
|
|
eba36b6d57 | ||
|
|
84a2afe08f | ||
|
|
98a85f6950 | ||
|
|
c89c99b272 | ||
|
|
3f16dbd09c | ||
|
|
e547851905 | ||
|
|
cbb0462948 | ||
|
|
a185292578 | ||
|
|
cec1f54cdd | ||
|
|
0112a3141b | ||
|
|
5f93b08c23 | ||
|
|
ff0de896c4 | ||
|
|
6d98f67668 | ||
|
|
5d5cf5cf29 | ||
|
|
610edea20e | ||
|
|
8f4219a93c | ||
|
|
9540ae5a19 | ||
|
|
0f7c3f5d0f | ||
|
|
39fb3a1107 | ||
|
|
e609931d2c | ||
|
|
70d94cda8c | ||
|
|
5c89a52f23 | ||
|
|
686e0a9441 | ||
|
|
e8dcf5ee02 | ||
|
|
95369e79a5 | ||
|
|
eb0a8454bc | ||
|
|
4f059c2549 | ||
|
|
fb7dbd5716 | ||
|
|
07fc4b3728 | ||
|
|
2d5b02baf9 | ||
|
|
f8b2547a45 | ||
|
|
f8cc8acfec | ||
|
|
17787c5a4f | ||
|
|
304de5adb6 | ||
|
|
46db38c5bf | ||
|
|
99e77e409a | ||
|
|
6152ddbd5f | ||
|
|
f99a94d685 | ||
|
|
47f58ff45f | ||
|
|
f225066130 | ||
|
|
83c5d701b3 | ||
|
|
ffb3359e66 | ||
|
|
0861b21532 | ||
|
|
e7420367f1 | ||
|
|
1998c779c7 | ||
|
|
93eb33811a | ||
|
|
d7bf9dba01 | ||
|
|
d5c6942266 | ||
|
|
e870fab277 | ||
|
|
f3ae63c7a9 | ||
|
|
3df1f1b153 | ||
|
|
74fd7c684e | ||
|
|
745b262800 | ||
|
|
72f6516a1c | ||
|
|
7bb723d6b3 | ||
|
|
2ccdc8ffdc | ||
|
|
1cabf64993 | ||
|
|
c55bd5a35d | ||
|
|
77a3552797 | ||
|
|
81efd4bce7 | ||
|
|
98183ccc1e | ||
|
|
09df863b6c | ||
|
|
4e70c1882b | ||
|
|
d38d581d1d | ||
|
|
61c95240c2 | ||
|
|
59347400c3 | ||
|
|
f976e04597 | ||
|
|
1602fe88e6 | ||
|
|
d4eca60b1d | ||
|
|
5a4467adb9 | ||
|
|
f50852fee0 | ||
|
|
1f647b3cc7 | ||
|
|
caf4eab104 | ||
|
|
334078fc34 | ||
|
|
25b1d86c50 | ||
|
|
78e2ff4870 | ||
|
|
ad94cce283 | ||
|
|
b4610e5c23 | ||
|
|
e12dcc2fb8 | ||
|
|
a818276b6d | ||
|
|
269d779df7 | ||
|
|
b63f7b7e5d | ||
|
|
b4a3ac8081 | ||
|
|
bbaaaa72fb | ||
|
|
89c83001ca | ||
|
|
61f1fdabd1 | ||
|
|
28062eacb6 | ||
|
|
8bdbf8df2e | ||
|
|
27e4800ed2 | ||
|
|
37bc54e01e | ||
|
|
6115f83a09 | ||
|
|
a8159c9e55 | ||
|
|
f734e27d23 | ||
|
|
8a118df636 | ||
|
|
a19b75760f | ||
|
|
1224b98745 | ||
|
|
6243ed3bd5 | ||
|
|
41e94e1e22 | ||
|
|
8c6940c351 | ||
|
|
384a2e0e15 | ||
|
|
a691841756 | ||
|
|
ff94bd6a90 | ||
|
|
00419910b4 | ||
|
|
21c9d7fcc3 | ||
|
|
e314c605f1 | ||
|
|
8316b5cb29 | ||
|
|
be46ed12ac | ||
|
|
a2d22b6feb | ||
|
|
f4e373447e | ||
|
|
5b2dfffe0f | ||
|
|
b347f761a7 | ||
|
|
445724573d | ||
|
|
8c5e0cf0a7 | ||
|
|
c6016a25df | ||
|
|
5a0a5ad83b | ||
|
|
3f0a0f552b | ||
|
|
63fd35a95c | ||
|
|
db163e7bd1 | ||
|
|
f3cd569e77 | ||
|
|
a95671491d | ||
|
|
95295e47ab | ||
|
|
a54e9ddd9c | ||
|
|
0e5f89d7d6 | ||
|
|
742d5cbfb3 | ||
|
|
e3fa695ad4 | ||
|
|
d6675f3311 | ||
|
|
64850a45da | ||
|
|
6aa0b7c748 | ||
|
|
fd80728857 | ||
|
|
9618a8d543 | ||
|
|
ec3a6e65ae | ||
|
|
d74578ec66 | ||
|
|
865dd24901 | ||
|
|
e063028c7d | ||
|
|
db11c1b7a8 | ||
|
|
89d7a924fb | ||
|
|
fe16115b20 | ||
|
|
fbccba77a7 | ||
|
|
07db34beb9 | ||
|
|
f42fb2fdd2 | ||
|
|
5853a373f3 | ||
|
|
951fbdccbd | ||
|
|
c6d20eb91f | ||
|
|
b68cea3921 | ||
|
|
36125f1067 | ||
|
|
eee16c7a3d | ||
|
|
bf1d93f256 | ||
|
|
42e3c95f87 | ||
|
|
ee702d92e6 | ||
|
|
f5aae23111 | ||
|
|
178f770b16 | ||
|
|
1b2d72531f | ||
|
|
7e08454edd | ||
|
|
72d318323e | ||
|
|
b611a98bae | ||
|
|
8b2eb50f29 | ||
|
|
988d0d6e35 | ||
|
|
48ec6fc757 | ||
|
|
0921c5e160 | ||
|
|
83843ae210 | ||
|
|
e5e768c56f | ||
|
|
f775c9da0b | ||
|
|
6a9c3dac77 | ||
|
|
11aaaecb7b | ||
|
|
12c08154c5 | ||
|
|
79d6a6f85f | ||
|
|
4513f03e8f | ||
|
|
f3adfca9c5 | ||
|
|
0b61ec1e13 | ||
|
|
8492c9b214 | ||
|
|
2a60c52483 | ||
|
|
917e813607 | ||
|
|
c20f64685f | ||
|
|
471229216a | ||
|
|
28661ab11a | ||
|
|
11c348a3d7 | ||
|
|
720af9085a | ||
|
|
8916ea5299 | ||
|
|
bf9a43b3d1 | ||
|
|
bca597c4e2 | ||
|
|
33e2f63ed5 | ||
|
|
e3f6df7120 | ||
|
|
58f198ddad | ||
|
|
61edcfe4f3 | ||
|
|
85bc3ddde6 | ||
|
|
488b631c38 | ||
|
|
da1b430200 | ||
|
|
bef76f0118 | ||
|
|
dd6baa72fa | ||
|
|
519b832d8c | ||
|
|
131326675e | ||
|
|
73dfa232f9 | ||
|
|
3f64173905 | ||
|
|
5cc4260d8e | ||
|
|
7f33a3847c | ||
|
|
c9be74ce80 | ||
|
|
f5d29eafe0 | ||
|
|
274e2c1cc2 | ||
|
|
c2233f7474 | ||
|
|
f8bfd6fd3f | ||
|
|
f2bc735bc0 | ||
|
|
ca34cbd180 | ||
|
|
9f4ea662da | ||
|
|
a7a4499dd4 | ||
|
|
f78261ee32 | ||
|
|
cfa8702654 | ||
|
|
dd9c65db4c | ||
|
|
9e7e29f03f | ||
|
|
3d6e84e11c | ||
|
|
527d6ab7ff | ||
|
|
8a295c72ba | ||
|
|
d2b82a37b2 | ||
|
|
ec4e680d62 | ||
|
|
aa80ed3d4b | ||
|
|
1d4a7894e8 | ||
|
|
2d28cb6897 | ||
|
|
89e561c991 | ||
|
|
d16dd7c75d | ||
|
|
e51e6b2171 | ||
|
|
3eaf5c9bc0 | ||
|
|
4625c920c3 | ||
|
|
2ce0f7beb4 | ||
|
|
81ba95f540 | ||
|
|
b25e0ea393 | ||
|
|
2632b34438 | ||
|
|
ff60013335 | ||
|
|
a0bcb03dde | ||
|
|
f0044a9342 | ||
|
|
04fb81e071 | ||
|
|
642b665418 | ||
|
|
c1596f098c | ||
|
|
8f44dfcde5 | ||
|
|
6437079be3 | ||
|
|
1f982b7999 | ||
|
|
eb1556f3e8 | ||
|
|
061b79eac0 | ||
|
|
3bbeec513a | ||
|
|
a6be59bbea | ||
|
|
96e8a909d8 | ||
|
|
8724076601 | ||
|
|
4b356aba3e | ||
|
|
a13e0a75e8 | ||
|
|
ecf91d616b | ||
|
|
0d4d0f3126 | ||
|
|
b9a8ca14c3 | ||
|
|
f7e1a2a5eb | ||
|
|
c3bc9c8591 | ||
|
|
a609b401c4 | ||
|
|
99252074be | ||
|
|
63743dd2b6 | ||
|
|
a254886bad | ||
|
|
aab10fb599 | ||
|
|
00b613d2e0 | ||
|
|
fe24322f7c | ||
|
|
1120b4ab51 |
+11
-6
@@ -19,7 +19,12 @@ base_path = dirname(os.path.abspath(__file__))
|
|||||||
sys.path.insert(0, os.path.join(base_path, 'libs'))
|
sys.path.insert(0, os.path.join(base_path, 'libs'))
|
||||||
|
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from couchpotato.core.helpers.variable import getDataDir
|
from couchpotato.core.helpers.variable import getDataDir, removePyc
|
||||||
|
|
||||||
|
|
||||||
|
# Remove pyc files before dynamic load (sees .pyc files regular .py modules)
|
||||||
|
removePyc(base_path)
|
||||||
|
|
||||||
|
|
||||||
class Loader(object):
|
class Loader(object):
|
||||||
|
|
||||||
@@ -29,7 +34,7 @@ class Loader(object):
|
|||||||
|
|
||||||
# Get options via arg
|
# Get options via arg
|
||||||
from couchpotato.runner import getOptions
|
from couchpotato.runner import getOptions
|
||||||
self.options = getOptions(base_path, sys.argv[1:])
|
self.options = getOptions(sys.argv[1:])
|
||||||
|
|
||||||
# Load settings
|
# Load settings
|
||||||
settings = Env.get('settings')
|
settings = Env.get('settings')
|
||||||
@@ -50,7 +55,7 @@ class Loader(object):
|
|||||||
# Create logging dir
|
# Create logging dir
|
||||||
self.log_dir = os.path.join(self.data_dir, 'logs');
|
self.log_dir = os.path.join(self.data_dir, 'logs');
|
||||||
if not os.path.isdir(self.log_dir):
|
if not os.path.isdir(self.log_dir):
|
||||||
os.mkdir(self.log_dir)
|
os.makedirs(self.log_dir)
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
@@ -67,10 +72,11 @@ class Loader(object):
|
|||||||
signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1))
|
signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1))
|
||||||
|
|
||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
addEvent('app.after_shutdown', self.afterShutdown)
|
addEvent('app.do_shutdown', self.setRestart)
|
||||||
|
|
||||||
def afterShutdown(self, restart):
|
def setRestart(self, restart):
|
||||||
self.do_restart = restart
|
self.do_restart = restart
|
||||||
|
return True
|
||||||
|
|
||||||
def onExit(self, signal, frame):
|
def onExit(self, signal, frame):
|
||||||
from couchpotato.core.event import fireEvent
|
from couchpotato.core.event import fireEvent
|
||||||
@@ -98,7 +104,6 @@ class Loader(object):
|
|||||||
|
|
||||||
# Release log files and shutdown logger
|
# Release log files and shutdown logger
|
||||||
logging.shutdown()
|
logging.shutdown()
|
||||||
time.sleep(3)
|
|
||||||
|
|
||||||
args = [sys.executable] + [os.path.join(base_path, os.path.basename(__file__))] + sys.argv[1:]
|
args = [sys.executable] + [os.path.join(base_path, os.path.basename(__file__))] + sys.argv[1:]
|
||||||
subprocess.Popen(args)
|
subprocess.Popen(args)
|
||||||
|
|||||||
+2
-1
@@ -127,7 +127,7 @@ class WorkerThread(Thread):
|
|||||||
# Get options via arg
|
# Get options via arg
|
||||||
from couchpotato.runner import getOptions
|
from couchpotato.runner import getOptions
|
||||||
args = ['--quiet']
|
args = ['--quiet']
|
||||||
self.options = getOptions(base_path, args)
|
self.options = getOptions(args)
|
||||||
|
|
||||||
# Load settings
|
# Load settings
|
||||||
settings = Env.get('settings')
|
settings = Env.get('settings')
|
||||||
@@ -154,6 +154,7 @@ class WorkerThread(Thread):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
self._desktop.frame.Close()
|
self._desktop.frame.Close()
|
||||||
|
self._desktop.ExitMainLoop()
|
||||||
|
|
||||||
|
|
||||||
class CouchPotatoApp(wx.App, SoftwareUpdate):
|
class CouchPotatoApp(wx.App, SoftwareUpdate):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
CouchPotato Server
|
CouchPotato
|
||||||
=====
|
=====
|
||||||
|
|
||||||
CouchPotato (CP) is an automatic NZB and torrent downloader. You can keep a "movies I want"-list and it will search for NZBs/torrents of these movies every X hours.
|
CouchPotato (CP) is an automatic NZB and torrent downloader. You can keep a "movies I want"-list and it will search for NZBs/torrents of these movies every X hours.
|
||||||
@@ -7,7 +7,7 @@ Once a movie is found, it will send it to SABnzbd or download the torrent to a s
|
|||||||
|
|
||||||
## Running from Source
|
## Running from Source
|
||||||
|
|
||||||
CouchPotatoServer can be run from source. This will use *git* as updater, so make sure that is installed also.
|
CouchPotatoServer can be run from source. This will use *git* as updater, so make sure that is installed.
|
||||||
|
|
||||||
Windows, see [the CP forum](http://couchpota.to/forum/showthread.php?tid=14) for more details:
|
Windows, see [the CP forum](http://couchpota.to/forum/showthread.php?tid=14) for more details:
|
||||||
|
|
||||||
@@ -17,9 +17,9 @@ Windows, see [the CP forum](http://couchpota.to/forum/showthread.php?tid=14) for
|
|||||||
* Open up `Git Bash` (or CMD) and go to the folder you want to install CP. Something like Program Files.
|
* Open up `Git Bash` (or CMD) and go to the folder you want to install CP. Something like Program Files.
|
||||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`.
|
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`.
|
||||||
* You can now start CP via `CouchPotatoServer\CouchPotato.py` to start
|
* You can now start CP via `CouchPotatoServer\CouchPotato.py` to start
|
||||||
* Your browser should open up, but if it doesn't go to: `http://localhost:5050/`
|
* Your browser should open up, but if it doesn't go to `http://localhost:5050/`
|
||||||
|
|
||||||
OSx:
|
OS X:
|
||||||
|
|
||||||
* If you're on Leopard (10.5) install Python 2.6+: [Python 2.6.5](http://www.python.org/download/releases/2.6.5/)
|
* If you're on Leopard (10.5) install Python 2.6+: [Python 2.6.5](http://www.python.org/download/releases/2.6.5/)
|
||||||
* Install [GIT](http://git-scm.com/)
|
* Install [GIT](http://git-scm.com/)
|
||||||
@@ -27,20 +27,21 @@ OSx:
|
|||||||
* Go to your App folder `cd /Applications`
|
* Go to your App folder `cd /Applications`
|
||||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
||||||
* Then do `python CouchPotatoServer/CouchPotato.py`
|
* Then do `python CouchPotatoServer/CouchPotato.py`
|
||||||
* Your browser should open up, but if it doesn't go to: `http://localhost:5050/`
|
* Your browser should open up, but if it doesn't go to `http://localhost:5050/`
|
||||||
|
|
||||||
Linux (ubuntu / debian):
|
Linux (Ubuntu / Debian):
|
||||||
|
|
||||||
* Install [GIT](http://git-scm.com/) with `apt-get install git-core`
|
* Install [GIT](http://git-scm.com/) with `apt-get install git-core`
|
||||||
* 'cd' to the folder of your choosing.
|
* 'cd' to the folder of your choosing.
|
||||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
||||||
* Then do `python CouchPotatoServer/CouchPotato.py` to start
|
* Then do `python CouchPotatoServer/CouchPotato.py` to start
|
||||||
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
|
* To run on boot copy the init script `sudo cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
|
||||||
* Change the paths inside the init script. `sudo nano /etc/init.d/couchpotato`
|
* Copy the default paths file `sudo cp CouchPotatoServer/init/ubuntu.default /etc/default/couchpotato`
|
||||||
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
|
* Change the paths inside the default file `sudo nano /etc/default/couchpotato`
|
||||||
* Add it to defaults. `sudo update-rc.d couchpotato defaults`
|
* Make it executable `sudo chmod +x /etc/init.d/couchpotato`
|
||||||
* Open your browser and go to: `http://localhost:5050/`
|
* Add it to defaults `sudo update-rc.d couchpotato defaults`
|
||||||
|
* Open your browser and go to `http://localhost:5050/`
|
||||||
|
|
||||||
|
|
||||||
FreeBSD :
|
FreeBSD :
|
||||||
|
|
||||||
@@ -56,7 +57,7 @@ FreeBSD :
|
|||||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
||||||
* Then run `sudo python CouchPotatoServer/CouchPotato.py` to start for the first time
|
* Then run `sudo python CouchPotatoServer/CouchPotato.py` to start for the first time
|
||||||
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/freebsd /etc/rc.d/couchpotato`
|
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/freebsd /etc/rc.d/couchpotato`
|
||||||
* Change the paths inside the init script. `sudo vim /etc/init.d/couchpotato`
|
* Change the paths inside the init script. `sudo vim /etc/rc.d/couchpotato`
|
||||||
* Make init script executable. `sudo chmod +x /etc/rc.d/couchpotato`
|
* Make init script executable. `sudo chmod +x /etc/rc.d/couchpotato`
|
||||||
* Add init to startup. `sudo echo 'couchpotato_enable="YES"' >> /etc/rc.conf`
|
* Add init to startup. `sudo echo 'couchpotato_enable="YES"' >> /etc/rc.conf`
|
||||||
* Open your browser and go to: `http://server:5050/`
|
* Open your browser and go to: `http://server:5050/`
|
||||||
|
|||||||
+31
-20
@@ -1,25 +1,36 @@
|
|||||||
## Got a issue/feature request or submitting a pull request?
|
# Contributing to CouchPotatoServer
|
||||||
|
|
||||||
Make sure you think of the following things:
|
1. [Contributing](#contributing)
|
||||||
|
2. [Submitting an Issue](#issues)
|
||||||
|
3. [Submitting a Pull Request](#pull-requests)
|
||||||
|
|
||||||
## Issue
|
## Contributing
|
||||||
* Search through the existing (and closed) issues first, see if you can get your answer there.
|
Thank you for your interest in contributing to CouchPotato. There are several ways to help out, even if you've never worked on an open source project before.
|
||||||
* Double check the result manually, because it could be an external issue.
|
If you've found a bug or want to request a feature, you can report it by [posting an issue](https://github.com/RuudBurger/CouchPotatoServer/issues/new) - be sure to read the [guidelines](#issues) first!
|
||||||
* Post logs! Without seeing what is going on, I can't reproduce the error.
|
If you want to contribute your own work, please read the [guidelines](#pull-requests) for submitting a pull request.
|
||||||
* Also check the logs before submitting, obvious errors like permission or http errors are often not related to CP.
|
Lastly, for anything related to CouchPotato, feel free to stop by the [forum](http://couchpota.to/forum/) or the [#couchpotato](http://webchat.freenode.net/?channels=couchpotato) IRC channel at irc.freenode.net.
|
||||||
* What is the movie + quality you are searching for?
|
|
||||||
* What are you're settings for the specific problem?
|
## Issues
|
||||||
* What providers are you using? (While you're logs include these, scanning through hundred of lines of log isn't our hobby)
|
Issues are intended for reporting bugs and weird behaviour or suggesting improvements to CouchPotatoServer.
|
||||||
* Post the logs from config directory, please do not copy paste the UI. Use pastebin to store these logs!
|
Before you submit an issue, please go through the following checklist:
|
||||||
|
* Search through existing issues (*including closed issues!*) first: you might be able to get your answer there.
|
||||||
|
* Double check your issue manually, because it could be an external issue.
|
||||||
|
* Post logs with your issue: Without seeing what is going on, the developers can't reproduce the error.
|
||||||
|
* Check the logs yourself before submitting them. Obvious errors like permission or HTTP errors are often not related to CouchPotato.
|
||||||
|
* What movie and quality are you searching for?
|
||||||
|
* What are your settings for the specific problem?
|
||||||
|
* What providers are you using? (While your logs include these, scanning through hundreds of lines of logs isn't our hobby)
|
||||||
|
* Post the logs from the *config* directory, please do not copy paste the UI. Use pastebin to store these logs!
|
||||||
* Give a short step by step of how to reproduce the error.
|
* Give a short step by step of how to reproduce the error.
|
||||||
* What hardware / OS are you using and what are the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example.
|
* What hardware / OS are you using and what are its limitations? For example: NAS can be slow and maybe have a different version of python installed than when you use CP on OS X or Windows.
|
||||||
* I will mark issues with the "can't reproduce" tag. Don't go asking "why closed" if it clearly says the issue in the tag ;)
|
* Your issue might be marked with the "can't reproduce" tag. Don't ask why your issue was closed if it says so in the tag.
|
||||||
* If you're running on a NAS (QNAP, Austor etc..) with pre-made packages, make sure these are setup to use our source repo (RuudBurger/CouchPotatoServer) and nothing else!!
|
* If you're running on a NAS (QNAP, Austor, Synology etc.) with pre-made packages, make sure these are set up to use our source repository (RuudBurger/CouchPotatoServer) and nothing else!
|
||||||
|
|
||||||
## Pull Request
|
The more relevant information you provide, the more likely that your issue will be resolved.
|
||||||
* Make sure you're pull request is made for develop branch (or relevant feature branch)
|
|
||||||
|
## Pull Requests
|
||||||
|
Pull requests are intended for contributing code or documentation to the project. Before you submit a pull request, consider the following:
|
||||||
|
* Make sure your pull request is made for the *develop* branch (or relevant feature branch).
|
||||||
* Have you tested your PR? If not, why?
|
* Have you tested your PR? If not, why?
|
||||||
* Are there any limitations of your PR we should know of?
|
* Does your PR have any limitations we should know of?
|
||||||
* Make sure to keep you're PR up-to-date with the branch you're trying to push into.
|
* Is your PR up-to-date with the branch you're trying to push into?
|
||||||
|
|
||||||
**If we don't get enough info, the chance of the issue getting closed is a lot bigger ;)**
|
|
||||||
|
|||||||
+16
-6
@@ -1,3 +1,7 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
from couchpotato.api import api_docs, api_docs_missing, api
|
from couchpotato.api import api_docs, api_docs_missing, api
|
||||||
from couchpotato.core.event import fireEvent
|
from couchpotato.core.event import fireEvent
|
||||||
from couchpotato.core.helpers.variable import md5, tryInt
|
from couchpotato.core.helpers.variable import md5, tryInt
|
||||||
@@ -5,9 +9,6 @@ from couchpotato.core.logger import CPLog
|
|||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from tornado import template
|
from tornado import template
|
||||||
from tornado.web import RequestHandler, authenticated
|
from tornado.web import RequestHandler, authenticated
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -15,6 +16,7 @@ log = CPLog(__name__)
|
|||||||
views = {}
|
views = {}
|
||||||
template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates'))
|
template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates'))
|
||||||
|
|
||||||
|
|
||||||
class BaseHandler(RequestHandler):
|
class BaseHandler(RequestHandler):
|
||||||
|
|
||||||
def get_current_user(self):
|
def get_current_user(self):
|
||||||
@@ -44,12 +46,12 @@ class WebHandler(BaseHandler):
|
|||||||
self.write({'success': False, 'error': 'Failed returning results'})
|
self.write({'success': False, 'error': 'Failed returning results'})
|
||||||
|
|
||||||
|
|
||||||
def addView(route, func, static = False):
|
def addView(route, func):
|
||||||
views[route] = func
|
views[route] = func
|
||||||
|
|
||||||
|
|
||||||
def get_session():
|
def get_db():
|
||||||
return Env.getSession()
|
return Env.get('db')
|
||||||
|
|
||||||
|
|
||||||
# Web view
|
# Web view
|
||||||
@@ -71,8 +73,16 @@ def apiDocs():
|
|||||||
addView('docs', apiDocs)
|
addView('docs', apiDocs)
|
||||||
|
|
||||||
|
|
||||||
|
# Database debug manager
|
||||||
|
def databaseManage():
|
||||||
|
return template_loader.load('database.html').generate(fireEvent = fireEvent, Env = Env)
|
||||||
|
|
||||||
|
addView('database', databaseManage)
|
||||||
|
|
||||||
|
|
||||||
# Make non basic auth option to get api key
|
# Make non basic auth option to get api key
|
||||||
class KeyHandler(RequestHandler):
|
class KeyHandler(RequestHandler):
|
||||||
|
|
||||||
def get(self, *args, **kwargs):
|
def get(self, *args, **kwargs):
|
||||||
api_key = None
|
api_key = None
|
||||||
|
|
||||||
|
|||||||
+52
-29
@@ -1,15 +1,15 @@
|
|||||||
from couchpotato.core.helpers.request import getParams
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from tornado.gen import coroutine
|
|
||||||
from tornado.web import RequestHandler, asynchronous
|
|
||||||
import json
|
import json
|
||||||
import threading
|
import threading
|
||||||
import tornado
|
|
||||||
import traceback
|
import traceback
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
|
from couchpotato.core.helpers.request import getParams
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from tornado.web import RequestHandler, asynchronous
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -26,10 +26,18 @@ def run_async(func):
|
|||||||
def async_func(*args, **kwargs):
|
def async_func(*args, **kwargs):
|
||||||
func_hl = Thread(target = func, args = args, kwargs = kwargs)
|
func_hl = Thread(target = func, args = args, kwargs = kwargs)
|
||||||
func_hl.start()
|
func_hl.start()
|
||||||
return func_hl
|
|
||||||
|
|
||||||
return async_func
|
return async_func
|
||||||
|
|
||||||
|
@run_async
|
||||||
|
def run_handler(route, kwargs, callback = None):
|
||||||
|
try:
|
||||||
|
res = api[route](**kwargs)
|
||||||
|
callback(res, route)
|
||||||
|
except:
|
||||||
|
log.error('Failed doing api request "%s": %s', (route, traceback.format_exc()))
|
||||||
|
callback({'success': False, 'error': 'Failed returning results'}, route)
|
||||||
|
|
||||||
|
|
||||||
# NonBlock API handler
|
# NonBlock API handler
|
||||||
class NonBlockHandler(RequestHandler):
|
class NonBlockHandler(RequestHandler):
|
||||||
@@ -76,13 +84,18 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
|||||||
# Blocking API handler
|
# Blocking API handler
|
||||||
class ApiHandler(RequestHandler):
|
class ApiHandler(RequestHandler):
|
||||||
|
|
||||||
@coroutine
|
@asynchronous
|
||||||
def get(self, route, *args, **kwargs):
|
def get(self, route, *args, **kwargs):
|
||||||
route = route.strip('/')
|
route = route.strip('/')
|
||||||
if not api.get(route):
|
if not api.get(route):
|
||||||
self.write('API call doesn\'t seem to exist')
|
self.write('API call doesn\'t seem to exist')
|
||||||
|
self.finish()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Create lock if it doesn't exist
|
||||||
|
if route in api_locks and not api_locks.get(route):
|
||||||
|
api_locks[route] = threading.Lock()
|
||||||
|
|
||||||
api_locks[route].acquire()
|
api_locks[route].acquire()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -93,37 +106,47 @@ class ApiHandler(RequestHandler):
|
|||||||
|
|
||||||
# Split array arguments
|
# Split array arguments
|
||||||
kwargs = getParams(kwargs)
|
kwargs = getParams(kwargs)
|
||||||
|
kwargs['_request'] = self
|
||||||
|
|
||||||
# Remove t random string
|
# Remove t random string
|
||||||
try: del kwargs['t']
|
try: del kwargs['t']
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
# Add async callback handler
|
# Add async callback handler
|
||||||
@run_async
|
run_handler(route, kwargs, callback = self.taskFinished)
|
||||||
def run_handler(callback):
|
|
||||||
try:
|
|
||||||
res = api[route](**kwargs)
|
|
||||||
callback(res)
|
|
||||||
except:
|
|
||||||
log.error('Failed doing api request "%s": %s', (route, traceback.format_exc()))
|
|
||||||
callback({'success': False, 'error': 'Failed returning results'})
|
|
||||||
|
|
||||||
result = yield tornado.gen.Task(run_handler)
|
|
||||||
|
|
||||||
# Check JSONP callback
|
|
||||||
jsonp_callback = self.get_argument('callback_func', default = None)
|
|
||||||
|
|
||||||
if jsonp_callback:
|
|
||||||
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
|
|
||||||
self.set_header("Content-Type", "text/javascript")
|
|
||||||
elif isinstance(result, tuple) and result[0] == 'redirect':
|
|
||||||
self.redirect(result[1])
|
|
||||||
else:
|
|
||||||
self.write(result)
|
|
||||||
|
|
||||||
except:
|
except:
|
||||||
log.error('Failed doing api request "%s": %s', (route, traceback.format_exc()))
|
log.error('Failed doing api request "%s": %s', (route, traceback.format_exc()))
|
||||||
self.write({'success': False, 'error': 'Failed returning results'})
|
try:
|
||||||
|
self.write({'success': False, 'error': 'Failed returning results'})
|
||||||
|
self.finish()
|
||||||
|
except:
|
||||||
|
log.error('Failed write error "%s": %s', (route, traceback.format_exc()))
|
||||||
|
|
||||||
|
api_locks[route].release()
|
||||||
|
|
||||||
|
post = get
|
||||||
|
|
||||||
|
def taskFinished(self, result, route):
|
||||||
|
|
||||||
|
if not self.request.connection.stream.closed():
|
||||||
|
try:
|
||||||
|
# Check JSONP callback
|
||||||
|
jsonp_callback = self.get_argument('callback_func', default = None)
|
||||||
|
|
||||||
|
if jsonp_callback:
|
||||||
|
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
|
||||||
|
self.set_header("Content-Type", "text/javascript")
|
||||||
|
self.finish()
|
||||||
|
elif isinstance(result, tuple) and result[0] == 'redirect':
|
||||||
|
self.redirect(result[1])
|
||||||
|
else:
|
||||||
|
self.write(result)
|
||||||
|
self.finish()
|
||||||
|
except:
|
||||||
|
log.debug('Failed doing request, probably already closed: %s', (traceback.format_exc()))
|
||||||
|
try: self.finish({'success': False, 'error': 'Failed returning results'})
|
||||||
|
except: pass
|
||||||
|
|
||||||
api_locks[route].release()
|
api_locks[route].release()
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
from couchpotato.api import addApiView
|
|
||||||
from couchpotato.core.event import fireEvent, addEvent
|
|
||||||
from couchpotato.core.helpers.variable import cleanHost, md5
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.plugins.base import Plugin
|
|
||||||
from couchpotato.environment import Env
|
|
||||||
from tornado.ioloop import IOLoop
|
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
@@ -13,8 +6,19 @@ import time
|
|||||||
import traceback
|
import traceback
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
|
from couchpotato.api import addApiView
|
||||||
|
from couchpotato.core.event import fireEvent, addEvent
|
||||||
|
from couchpotato.core.helpers.variable import cleanHost, md5, isSubFolder
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
from tornado.ioloop import IOLoop
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
autoload = 'Core'
|
||||||
|
|
||||||
|
|
||||||
class Core(Plugin):
|
class Core(Plugin):
|
||||||
|
|
||||||
@@ -47,6 +51,7 @@ class Core(Plugin):
|
|||||||
addEvent('app.api_url', self.createApiUrl)
|
addEvent('app.api_url', self.createApiUrl)
|
||||||
addEvent('app.version', self.version)
|
addEvent('app.version', self.version)
|
||||||
addEvent('app.load', self.checkDataDir)
|
addEvent('app.load', self.checkDataDir)
|
||||||
|
addEvent('app.load', self.cleanUpFolders)
|
||||||
|
|
||||||
addEvent('setting.save.core.password', self.md5Password)
|
addEvent('setting.save.core.password', self.md5Password)
|
||||||
addEvent('setting.save.core.api_key', self.checkApikey)
|
addEvent('setting.save.core.api_key', self.checkApikey)
|
||||||
@@ -66,11 +71,15 @@ class Core(Plugin):
|
|||||||
return value if value and len(value) > 3 else uuid4().hex
|
return value if value and len(value) > 3 else uuid4().hex
|
||||||
|
|
||||||
def checkDataDir(self):
|
def checkDataDir(self):
|
||||||
if Env.get('app_dir') in Env.get('data_dir'):
|
if isSubFolder(Env.get('data_dir'), Env.get('app_dir')):
|
||||||
log.error('You should NOT use your CouchPotato directory to save your settings in. Files will get overwritten or be deleted.')
|
log.error('You should NOT use your CouchPotato directory to save your settings in. Files will get overwritten or be deleted.')
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def cleanUpFolders(self):
|
||||||
|
only_clean = ['couchpotato', 'libs', 'init']
|
||||||
|
self.deleteEmptyFolder(Env.get('app_dir'), show_error = False, only_clean = only_clean)
|
||||||
|
|
||||||
def available(self, **kwargs):
|
def available(self, **kwargs):
|
||||||
return {
|
return {
|
||||||
'success': True
|
'success': True
|
||||||
@@ -82,7 +91,11 @@ class Core(Plugin):
|
|||||||
|
|
||||||
def shutdown():
|
def shutdown():
|
||||||
self.initShutdown()
|
self.initShutdown()
|
||||||
IOLoop.current().add_callback(shutdown)
|
|
||||||
|
if IOLoop.current()._closing:
|
||||||
|
shutdown()
|
||||||
|
else:
|
||||||
|
IOLoop.current().add_callback(shutdown)
|
||||||
|
|
||||||
return 'shutdown'
|
return 'shutdown'
|
||||||
|
|
||||||
@@ -105,7 +118,7 @@ class Core(Plugin):
|
|||||||
|
|
||||||
self.shutdown_started = True
|
self.shutdown_started = True
|
||||||
|
|
||||||
fireEvent('app.do_shutdown')
|
fireEvent('app.do_shutdown', restart = restart)
|
||||||
log.debug('Every plugin got shutdown event')
|
log.debug('Every plugin got shutdown event')
|
||||||
|
|
||||||
loop = True
|
loop = True
|
||||||
@@ -130,8 +143,11 @@ class Core(Plugin):
|
|||||||
|
|
||||||
log.debug('Safe to shutdown/restart')
|
log.debug('Safe to shutdown/restart')
|
||||||
|
|
||||||
|
loop = IOLoop.current()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
IOLoop.current().stop()
|
if not loop._closing:
|
||||||
|
loop.stop()
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass
|
pass
|
||||||
except:
|
except:
|
||||||
@@ -181,8 +197,104 @@ class Core(Plugin):
|
|||||||
def signalHandler(self):
|
def signalHandler(self):
|
||||||
if Env.get('daemonized'): return
|
if Env.get('daemonized'): return
|
||||||
|
|
||||||
def signal_handler(signal, frame):
|
def signal_handler(*args, **kwargs):
|
||||||
fireEvent('app.shutdown', single = True)
|
fireEvent('app.shutdown', single = True)
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
signal.signal(signal.SIGTERM, signal_handler)
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'core',
|
||||||
|
'order': 1,
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'general',
|
||||||
|
'name': 'basics',
|
||||||
|
'description': 'Needs restart before changes take effect.',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'default': '',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'port',
|
||||||
|
'default': 5050,
|
||||||
|
'type': 'int',
|
||||||
|
'description': 'The port I should listen to.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'ssl_cert',
|
||||||
|
'description': 'Path to SSL server.crt',
|
||||||
|
'advanced': True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'ssl_key',
|
||||||
|
'description': 'Path to SSL server.key',
|
||||||
|
'advanced': True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'launch_browser',
|
||||||
|
'default': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Launch the browser when I start.',
|
||||||
|
'wizard': True,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'tab': 'general',
|
||||||
|
'name': 'advanced',
|
||||||
|
'description': "For those who know what they're doing",
|
||||||
|
'advanced': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'api_key',
|
||||||
|
'default': uuid4().hex,
|
||||||
|
'readonly': 1,
|
||||||
|
'description': 'Let 3rd party app do stuff. <a target="_self" href="../../docs/">Docs</a>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'debug',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Enable debugging.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'development',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Enable this if you\'re developing, and NOT in any other case, thanks.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'data_dir',
|
||||||
|
'type': 'directory',
|
||||||
|
'description': 'Where cache/logs/etc are stored. Keep empty for defaults.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'url_base',
|
||||||
|
'default': '',
|
||||||
|
'description': 'When using mod_proxy use this to append the url with this.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'permission_folder',
|
||||||
|
'default': '0755',
|
||||||
|
'label': 'Folder CHMOD',
|
||||||
|
'description': 'Can be either decimal (493) or octal (leading zero: 0755)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'permission_file',
|
||||||
|
'default': '0755',
|
||||||
|
'label': 'File CHMOD',
|
||||||
|
'description': 'Same as Folder CHMOD but for files',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
from .main import Core
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
|
|
||||||
def start():
|
|
||||||
return Core()
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'core',
|
|
||||||
'order': 1,
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'general',
|
|
||||||
'name': 'basics',
|
|
||||||
'description': 'Needs restart before changes take effect.',
|
|
||||||
'wizard': True,
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'username',
|
|
||||||
'default': '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'password',
|
|
||||||
'default': '',
|
|
||||||
'type': 'password',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'port',
|
|
||||||
'default': 5050,
|
|
||||||
'type': 'int',
|
|
||||||
'description': 'The port I should listen to.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'ssl_cert',
|
|
||||||
'description': 'Path to SSL server.crt',
|
|
||||||
'advanced': True,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'ssl_key',
|
|
||||||
'description': 'Path to SSL server.key',
|
|
||||||
'advanced': True,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'launch_browser',
|
|
||||||
'default': True,
|
|
||||||
'type': 'bool',
|
|
||||||
'description': 'Launch the browser when I start.',
|
|
||||||
'wizard': True,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'tab': 'general',
|
|
||||||
'name': 'advanced',
|
|
||||||
'description': "For those who know what they're doing",
|
|
||||||
'advanced': True,
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'api_key',
|
|
||||||
'default': uuid4().hex,
|
|
||||||
'readonly': 1,
|
|
||||||
'description': 'Let 3rd party app do stuff. <a target="_self" href="../../docs/">Docs</a>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'debug',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'bool',
|
|
||||||
'description': 'Enable debugging.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'development',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'bool',
|
|
||||||
'description': 'Enable this if you\'re developing, and NOT in any other case, thanks.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'data_dir',
|
|
||||||
'type': 'directory',
|
|
||||||
'description': 'Where cache/logs/etc are stored. Keep empty for defaults.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'url_base',
|
|
||||||
'default': '',
|
|
||||||
'description': 'When using mod_proxy use this to append the url with this.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'permission_folder',
|
|
||||||
'default': '0755',
|
|
||||||
'label': 'Folder CHMOD',
|
|
||||||
'description': 'Can be either decimal (493) or octal (leading zero: 0755)',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'permission_file',
|
|
||||||
'default': '0755',
|
|
||||||
'label': 'File CHMOD',
|
|
||||||
'description': 'Same as Folder CHMOD but for files',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
+11
-11
@@ -1,3 +1,7 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
|
||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.encoding import ss
|
from couchpotato.core.helpers.encoding import ss
|
||||||
from couchpotato.core.helpers.variable import tryInt
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
@@ -7,12 +11,12 @@ from couchpotato.environment import Env
|
|||||||
from minify.cssmin import cssmin
|
from minify.cssmin import cssmin
|
||||||
from minify.jsmin import jsmin
|
from minify.jsmin import jsmin
|
||||||
from tornado.web import StaticFileHandler
|
from tornado.web import StaticFileHandler
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
autoload = 'ClientScript'
|
||||||
|
|
||||||
|
|
||||||
class ClientScript(Plugin):
|
class ClientScript(Plugin):
|
||||||
|
|
||||||
@@ -45,21 +49,17 @@ class ClientScript(Plugin):
|
|||||||
'scripts/block/footer.js',
|
'scripts/block/footer.js',
|
||||||
'scripts/block/menu.js',
|
'scripts/block/menu.js',
|
||||||
'scripts/page/home.js',
|
'scripts/page/home.js',
|
||||||
'scripts/page/wanted.js',
|
|
||||||
'scripts/page/settings.js',
|
'scripts/page/settings.js',
|
||||||
'scripts/page/about.js',
|
'scripts/page/about.js',
|
||||||
'scripts/page/manage.js',
|
|
||||||
'scripts/misc/downloaders.js',
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
urls = {'style': {}, 'script': {}}
|
urls = {'style': {}, 'script': {}}
|
||||||
minified = {'style': {}, 'script': {}}
|
minified = {'style': {}, 'script': {}}
|
||||||
paths = {'style': {}, 'script': {}}
|
paths = {'style': {}, 'script': {}}
|
||||||
comment = {
|
comment = {
|
||||||
'style': '/*** %s:%d ***/\n',
|
'style': '/*** %s:%d ***/\n',
|
||||||
'script': '// %s:%d\n'
|
'script': '// %s:%d\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
html = {
|
html = {
|
||||||
@@ -91,7 +91,6 @@ class ClientScript(Plugin):
|
|||||||
else:
|
else:
|
||||||
self.registerStyle(core_url, file_path, position = 'front')
|
self.registerStyle(core_url, file_path, position = 'front')
|
||||||
|
|
||||||
|
|
||||||
def minify(self):
|
def minify(self):
|
||||||
|
|
||||||
# Create cache dir
|
# Create cache dir
|
||||||
@@ -125,7 +124,7 @@ class ClientScript(Plugin):
|
|||||||
data = cssmin(data)
|
data = cssmin(data)
|
||||||
data = data.replace('../images/', '../static/images/')
|
data = data.replace('../images/', '../static/images/')
|
||||||
data = data.replace('../fonts/', '../static/fonts/')
|
data = data.replace('../fonts/', '../static/fonts/')
|
||||||
data = data.replace('../../static/', '../static/') # Replace inside plugins
|
data = data.replace('../../static/', '../static/') # Replace inside plugins
|
||||||
|
|
||||||
raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data})
|
raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data})
|
||||||
|
|
||||||
@@ -188,6 +187,7 @@ class ClientScript(Plugin):
|
|||||||
|
|
||||||
prefix_properties = ['border-radius', 'transform', 'transition', 'box-shadow']
|
prefix_properties = ['border-radius', 'transform', 'transition', 'box-shadow']
|
||||||
prefix_tags = ['ms', 'moz', 'webkit']
|
prefix_tags = ['ms', 'moz', 'webkit']
|
||||||
|
|
||||||
def prefix(self, data):
|
def prefix(self, data):
|
||||||
|
|
||||||
trimmed_data = re.sub('(\t|\n|\r)+', '', data)
|
trimmed_data = re.sub('(\t|\n|\r)+', '', data)
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from .main import ClientScript
|
|
||||||
|
|
||||||
|
|
||||||
def start():
|
|
||||||
return ClientScript()
|
|
||||||
|
|
||||||
config = []
|
|
||||||
@@ -5,6 +5,9 @@ from couchpotato.environment import Env
|
|||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
autoload = 'Desktop'
|
||||||
|
|
||||||
|
|
||||||
if Env.get('desktop'):
|
if Env.get('desktop'):
|
||||||
|
|
||||||
class Desktop(Plugin):
|
class Desktop(Plugin):
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from .main import Desktop
|
|
||||||
|
|
||||||
|
|
||||||
def start():
|
|
||||||
return Desktop()
|
|
||||||
|
|
||||||
config = []
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
from .main import Downloader
|
||||||
|
|
||||||
|
|
||||||
|
def autoload():
|
||||||
|
return Downloader()
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'download_providers',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'label': 'Downloaders',
|
||||||
|
'description': 'You can select different downloaders for each type (usenet / torrent)',
|
||||||
|
'type': 'list',
|
||||||
|
'name': 'download_providers',
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'options': [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -1,38 +1,51 @@
|
|||||||
from base64 import b32decode, b16encode
|
from base64 import b32decode, b16encode
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
|
||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.variable import mergeDicts
|
from couchpotato.core.helpers.variable import mergeDicts
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.base import Provider
|
from couchpotato.core.media._base.providers.base import Provider
|
||||||
import random
|
from couchpotato.core.plugins.base import Plugin
|
||||||
import re
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Downloader(Provider):
|
## This is here to load the static files
|
||||||
|
class Downloader(Plugin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DownloaderBase(Provider):
|
||||||
|
|
||||||
protocol = []
|
protocol = []
|
||||||
http_time_between_calls = 0
|
http_time_between_calls = 0
|
||||||
status_support = True
|
status_support = True
|
||||||
|
|
||||||
torrent_sources = [
|
torrent_sources = [
|
||||||
|
'https://zoink.it/torrent/%s.torrent',
|
||||||
'http://torrage.com/torrent/%s.torrent',
|
'http://torrage.com/torrent/%s.torrent',
|
||||||
'https://torcache.net/torrent/%s.torrent',
|
'https://torcache.net/torrent/%s.torrent',
|
||||||
]
|
]
|
||||||
|
|
||||||
torrent_trackers = [
|
torrent_trackers = [
|
||||||
'http://tracker.publicbt.com/announce',
|
|
||||||
'udp://tracker.istole.it:80/announce',
|
'udp://tracker.istole.it:80/announce',
|
||||||
'udp://fr33domtracker.h33t.com:3310/announce',
|
|
||||||
'http://tracker.istole.it/announce',
|
'http://tracker.istole.it/announce',
|
||||||
'http://tracker.ccc.de/announce',
|
'udp://fr33domtracker.h33t.com:3310/announce',
|
||||||
|
'http://tracker.publicbt.com/announce',
|
||||||
'udp://tracker.publicbt.com:80/announce',
|
'udp://tracker.publicbt.com:80/announce',
|
||||||
|
'http://tracker.ccc.de/announce',
|
||||||
'udp://tracker.ccc.de:80/announce',
|
'udp://tracker.ccc.de:80/announce',
|
||||||
'http://exodus.desync.com/announce',
|
'http://exodus.desync.com/announce',
|
||||||
'http://exodus.desync.com:6969/announce',
|
'http://exodus.desync.com:6969/announce',
|
||||||
'http://tracker.publichd.eu/announce',
|
'http://tracker.publichd.eu/announce',
|
||||||
|
'udp://tracker.publichd.eu:80/announce',
|
||||||
'http://tracker.openbittorrent.com/announce',
|
'http://tracker.openbittorrent.com/announce',
|
||||||
|
'udp://tracker.openbittorrent.com/announce',
|
||||||
|
'udp://tracker.openbittorrent.com:80/announce',
|
||||||
|
'udp://open.demonii.com:1337/announce',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -60,6 +73,9 @@ class Downloader(Provider):
|
|||||||
return
|
return
|
||||||
return self.download(data = data, media = media, filedata = filedata)
|
return self.download(data = data, media = media, filedata = filedata)
|
||||||
|
|
||||||
|
def download(self, *args, **kwargs):
|
||||||
|
return False
|
||||||
|
|
||||||
def _getAllDownloadStatus(self, download_ids):
|
def _getAllDownloadStatus(self, download_ids):
|
||||||
if self.isDisabled(manual = True, data = {}):
|
if self.isDisabled(manual = True, data = {}):
|
||||||
return
|
return
|
||||||
@@ -156,11 +172,11 @@ class Downloader(Provider):
|
|||||||
if not data: data = {}
|
if not data: data = {}
|
||||||
|
|
||||||
d_manual = self.conf('manual', default = False)
|
d_manual = self.conf('manual', default = False)
|
||||||
return super(Downloader, self).isEnabled() and \
|
return super(DownloaderBase, self).isEnabled() and \
|
||||||
(d_manual and manual or d_manual is False) and \
|
(d_manual and manual or d_manual is False) and \
|
||||||
(not data or self.isCorrectProtocol(data.get('protocol')))
|
(not data or self.isCorrectProtocol(data.get('protocol')))
|
||||||
|
|
||||||
def _test(self):
|
def _test(self, **kwargs):
|
||||||
t = self.test()
|
t = self.test()
|
||||||
if isinstance(t, tuple):
|
if isinstance(t, tuple):
|
||||||
return {'success': t[0], 'msg': t[1]}
|
return {'success': t[0], 'msg': t[1]}
|
||||||
@@ -182,6 +198,7 @@ class Downloader(Provider):
|
|||||||
def pause(self, release_download, pause):
|
def pause(self, release_download, pause):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
class ReleaseDownloadList(list):
|
class ReleaseDownloadList(list):
|
||||||
|
|
||||||
provider = None
|
provider = None
|
||||||
@@ -208,7 +225,7 @@ class ReleaseDownloadList(list):
|
|||||||
'status': 'busy',
|
'status': 'busy',
|
||||||
'downloader': self.provider.getName(),
|
'downloader': self.provider.getName(),
|
||||||
'folder': '',
|
'folder': '',
|
||||||
'files': '',
|
'files': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
return mergeDicts(defaults, result)
|
return mergeDicts(defaults, result)
|
||||||
+6
-5
@@ -6,7 +6,7 @@ var DownloadersBase = new Class({
|
|||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
// Add test buttons to settings page
|
// Add test buttons to settings page
|
||||||
App.addEvent('load', self.addTestButtons.bind(self));
|
App.addEvent('loadSettings', self.addTestButtons.bind(self));
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -40,15 +40,16 @@ var DownloadersBase = new Class({
|
|||||||
|
|
||||||
button.set('text', button_name);
|
button.set('text', button_name);
|
||||||
|
|
||||||
|
var message;
|
||||||
if(json.success){
|
if(json.success){
|
||||||
var message = new Element('span.success', {
|
message = new Element('span.success', {
|
||||||
'text': 'Connection successful'
|
'text': 'Connection successful'
|
||||||
}).inject(button, 'after')
|
}).inject(button, 'after')
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
var msg_text = 'Connection failed. Check logs for details.';
|
var msg_text = 'Connection failed. Check logs for details.';
|
||||||
if(json.hasOwnProperty('msg')) msg_text = json.msg;
|
if(json.hasOwnProperty('msg')) msg_text = json.msg;
|
||||||
var message = new Element('span.failed', {
|
message = new Element('span.failed', {
|
||||||
'text': msg_text
|
'text': msg_text
|
||||||
}).inject(button, 'after')
|
}).inject(button, 'after')
|
||||||
}
|
}
|
||||||
@@ -68,8 +69,8 @@ var DownloadersBase = new Class({
|
|||||||
testButtonName: function(fieldset){
|
testButtonName: function(fieldset){
|
||||||
var name = String(fieldset.getElement('h2').innerHTML).substring(0,String(fieldset.getElement('h2').innerHTML).indexOf("<span"));
|
var name = String(fieldset.getElement('h2').innerHTML).substring(0,String(fieldset.getElement('h2').innerHTML).indexOf("<span"));
|
||||||
return 'Test '+name;
|
return 'Test '+name;
|
||||||
},
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
window.Downloaders = new DownloadersBase();
|
var Downloaders = new DownloadersBase();
|
||||||
@@ -5,6 +5,8 @@ from couchpotato.core.plugins.base import Plugin
|
|||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
autoload = 'Scheduler'
|
||||||
|
|
||||||
|
|
||||||
class Scheduler(Plugin):
|
class Scheduler(Plugin):
|
||||||
|
|
||||||
@@ -31,9 +33,9 @@ class Scheduler(Plugin):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def doShutdown(self):
|
def doShutdown(self, *args, **kwargs):
|
||||||
self.stop()
|
self.stop()
|
||||||
return super(Scheduler, self).doShutdown()
|
return super(Scheduler, self).doShutdown(*args, **kwargs)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
if self.started:
|
if self.started:
|
||||||
@@ -66,6 +68,8 @@ class Scheduler(Plugin):
|
|||||||
'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds)
|
'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def queue(self, handlers = None):
|
def queue(self, handlers = None):
|
||||||
if not handlers: handlers = []
|
if not handlers: handlers = []
|
||||||
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from .main import Scheduler
|
|
||||||
|
|
||||||
|
|
||||||
def start():
|
|
||||||
return Scheduler()
|
|
||||||
|
|
||||||
config = []
|
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
from .main import Updater
|
|
||||||
from couchpotato.environment import Env
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from .main import Updater
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
|
||||||
def start():
|
|
||||||
|
def autoload():
|
||||||
return Updater()
|
return Updater()
|
||||||
|
|
||||||
config = [{
|
config = [{
|
||||||
|
|||||||
@@ -1,28 +1,33 @@
|
|||||||
from couchpotato.api import addApiView
|
|
||||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
|
||||||
from couchpotato.core.helpers.encoding import ss
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.plugins.base import Plugin
|
|
||||||
from couchpotato.environment import Env
|
|
||||||
from datetime import datetime
|
|
||||||
from dateutil.parser import parse
|
|
||||||
from git.repository import LocalRepository
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import tarfile
|
import tarfile
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import version
|
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from datetime import datetime
|
||||||
|
from threading import RLock
|
||||||
|
|
||||||
|
from couchpotato.api import addApiView
|
||||||
|
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||||
|
from couchpotato.core.helpers.encoding import sp
|
||||||
|
from couchpotato.core.helpers.variable import removePyc
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
from dateutil.parser import parse
|
||||||
|
from git.repository import LocalRepository
|
||||||
|
import version
|
||||||
from six.moves import filter
|
from six.moves import filter
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Updater(Plugin):
|
class Updater(Plugin):
|
||||||
|
|
||||||
available_notified = False
|
available_notified = False
|
||||||
|
_lock = RLock()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
@@ -100,7 +105,17 @@ class Updater(Plugin):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def info(self, **kwargs):
|
def info(self, **kwargs):
|
||||||
return self.updater.info()
|
self._lock.acquire()
|
||||||
|
|
||||||
|
info = {}
|
||||||
|
try:
|
||||||
|
info = self.updater.info()
|
||||||
|
except:
|
||||||
|
log.error('Failed getting updater info: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
self._lock.release()
|
||||||
|
|
||||||
|
return info
|
||||||
|
|
||||||
def checkView(self, **kwargs):
|
def checkView(self, **kwargs):
|
||||||
return {
|
return {
|
||||||
@@ -127,6 +142,12 @@ class Updater(Plugin):
|
|||||||
'success': success
|
'success': success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def doShutdown(self, *args, **kwargs):
|
||||||
|
if not Env.get('dev'):
|
||||||
|
removePyc(Env.get('app_dir'), show_logs = False)
|
||||||
|
|
||||||
|
return super(Updater, self).doShutdown(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class BaseUpdater(Plugin):
|
class BaseUpdater(Plugin):
|
||||||
|
|
||||||
@@ -144,12 +165,15 @@ class BaseUpdater(Plugin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def info(self):
|
def info(self):
|
||||||
|
|
||||||
|
current_version = self.getVersion()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'last_check': self.last_check,
|
'last_check': self.last_check,
|
||||||
'update_version': self.update_version,
|
'update_version': self.update_version,
|
||||||
'version': self.getVersion(),
|
'version': current_version,
|
||||||
'repo_name': '%s/%s' % (self.repo_user, self.repo_name),
|
'repo_name': '%s/%s' % (self.repo_user, self.repo_name),
|
||||||
'branch': self.branch,
|
'branch': current_version.get('branch', self.branch),
|
||||||
}
|
}
|
||||||
|
|
||||||
def getVersion(self):
|
def getVersion(self):
|
||||||
@@ -158,30 +182,6 @@ class BaseUpdater(Plugin):
|
|||||||
def check(self):
|
def check(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def deletePyc(self, only_excess = True):
|
|
||||||
|
|
||||||
for root, dirs, files in os.walk(ss(Env.get('app_dir'))):
|
|
||||||
|
|
||||||
pyc_files = filter(lambda filename: filename.endswith('.pyc'), files)
|
|
||||||
py_files = set(filter(lambda filename: filename.endswith('.py'), files))
|
|
||||||
excess_pyc_files = filter(lambda pyc_filename: pyc_filename[:-1] not in py_files, pyc_files) if only_excess else pyc_files
|
|
||||||
|
|
||||||
for excess_pyc_file in excess_pyc_files:
|
|
||||||
full_path = os.path.join(root, excess_pyc_file)
|
|
||||||
log.debug('Removing old PYC file: %s', full_path)
|
|
||||||
try:
|
|
||||||
os.remove(full_path)
|
|
||||||
except:
|
|
||||||
log.error('Couldn\'t remove %s: %s', (full_path, traceback.format_exc()))
|
|
||||||
|
|
||||||
for dir_name in dirs:
|
|
||||||
full_path = os.path.join(root, dir_name)
|
|
||||||
if len(os.listdir(full_path)) == 0:
|
|
||||||
try:
|
|
||||||
os.rmdir(full_path)
|
|
||||||
except:
|
|
||||||
log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
|
|
||||||
|
|
||||||
|
|
||||||
class GitUpdater(BaseUpdater):
|
class GitUpdater(BaseUpdater):
|
||||||
|
|
||||||
@@ -194,9 +194,6 @@ class GitUpdater(BaseUpdater):
|
|||||||
log.info('Updating to latest version')
|
log.info('Updating to latest version')
|
||||||
self.repo.pull()
|
self.repo.pull()
|
||||||
|
|
||||||
# Delete leftover .pyc files
|
|
||||||
self.deletePyc()
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
log.error('Failed updating via GIT: %s', traceback.format_exc())
|
log.error('Failed updating via GIT: %s', traceback.format_exc())
|
||||||
@@ -212,10 +209,11 @@ class GitUpdater(BaseUpdater):
|
|||||||
output = self.repo.getHead() # Yes, please
|
output = self.repo.getHead() # Yes, please
|
||||||
log.debug('Git version output: %s', output.hash)
|
log.debug('Git version output: %s', output.hash)
|
||||||
self.version = {
|
self.version = {
|
||||||
'repr': 'git:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.branch, output.hash[:8], datetime.fromtimestamp(output.getDate())),
|
'repr': 'git:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.repo.getCurrentBranch().name or self.branch, output.hash[:8], datetime.fromtimestamp(output.getDate())),
|
||||||
'hash': output.hash[:8],
|
'hash': output.hash[:8],
|
||||||
'date': output.getDate(),
|
'date': output.getDate(),
|
||||||
'type': 'git',
|
'type': 'git',
|
||||||
|
'branch': self.repo.getCurrentBranch().name
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s', e)
|
log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s', e)
|
||||||
@@ -302,11 +300,12 @@ class SourceUpdater(BaseUpdater):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def replaceWith(self, path):
|
def replaceWith(self, path):
|
||||||
app_dir = ss(Env.get('app_dir'))
|
path = sp(path)
|
||||||
data_dir = ss(Env.get('data_dir'))
|
app_dir = Env.get('app_dir')
|
||||||
|
data_dir = Env.get('data_dir')
|
||||||
|
|
||||||
# Get list of files we want to overwrite
|
# Get list of files we want to overwrite
|
||||||
self.deletePyc()
|
removePyc(app_dir)
|
||||||
existing_files = []
|
existing_files = []
|
||||||
for root, subfiles, filenames in os.walk(app_dir):
|
for root, subfiles, filenames in os.walk(app_dir):
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ var UpdaterBase = new Class({
|
|||||||
initialize: function(){
|
initialize: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
App.addEvent('load', self.info.bind(self, 2000))
|
App.addEvent('load', self.info.bind(self, 2000));
|
||||||
App.addEvent('unload', function(){
|
App.addEvent('unload', function(){
|
||||||
if(self.timer)
|
if(self.timer)
|
||||||
clearTimeout(self.timer);
|
clearTimeout(self.timer);
|
||||||
@@ -66,7 +66,7 @@ var UpdaterBase = new Class({
|
|||||||
|
|
||||||
var changelog = 'https://github.com/'+data.repo_name+'/compare/'+data.version.hash+'...'+data.branch;
|
var changelog = 'https://github.com/'+data.repo_name+'/compare/'+data.version.hash+'...'+data.branch;
|
||||||
if(data.update_version.changelog)
|
if(data.update_version.changelog)
|
||||||
changelog = data.update_version.changelog + '#' + data.version.hash+'...'+data.update_version.hash
|
changelog = data.update_version.changelog + '#' + data.version.hash+'...'+data.update_version.hash;
|
||||||
|
|
||||||
self.message = new Element('div.message.update').adopt(
|
self.message = new Element('div.message.update').adopt(
|
||||||
new Element('span', {
|
new Element('span', {
|
||||||
|
|||||||
@@ -0,0 +1,576 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from CodernityDB.index import IndexException, IndexNotFoundException, IndexConflict
|
||||||
|
from couchpotato import CPLog
|
||||||
|
from couchpotato.api import addApiView
|
||||||
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
|
from couchpotato.core.helpers.encoding import toUnicode, sp
|
||||||
|
from couchpotato.core.helpers.variable import getImdb, tryInt
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Database(object):
|
||||||
|
|
||||||
|
indexes = None
|
||||||
|
db = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
|
||||||
|
self.indexes = {}
|
||||||
|
|
||||||
|
addApiView('database.list_documents', self.listDocuments)
|
||||||
|
addApiView('database.reindex', self.reindex)
|
||||||
|
addApiView('database.compact', self.compact)
|
||||||
|
addApiView('database.document.update', self.updateDocument)
|
||||||
|
addApiView('database.document.delete', self.deleteDocument)
|
||||||
|
|
||||||
|
addEvent('database.setup.after', self.startup_compact)
|
||||||
|
addEvent('database.setup_index', self.setupIndex)
|
||||||
|
|
||||||
|
addEvent('app.migrate', self.migrate)
|
||||||
|
addEvent('app.after_shutdown', self.close)
|
||||||
|
|
||||||
|
def getDB(self):
|
||||||
|
|
||||||
|
if not self.db:
|
||||||
|
from couchpotato import get_db
|
||||||
|
self.db = get_db()
|
||||||
|
|
||||||
|
return self.db
|
||||||
|
|
||||||
|
def close(self, **kwargs):
|
||||||
|
self.getDB().close()
|
||||||
|
|
||||||
|
def setupIndex(self, index_name, klass):
|
||||||
|
|
||||||
|
self.indexes[index_name] = klass
|
||||||
|
|
||||||
|
db = self.getDB()
|
||||||
|
|
||||||
|
# Category index
|
||||||
|
index_instance = klass(db.path, index_name)
|
||||||
|
try:
|
||||||
|
|
||||||
|
# Make sure store and bucket don't exist
|
||||||
|
exists = []
|
||||||
|
for x in ['buck', 'stor']:
|
||||||
|
full_path = os.path.join(db.path, '%s_%s' % (index_name, x))
|
||||||
|
if os.path.exists(full_path):
|
||||||
|
exists.append(full_path)
|
||||||
|
|
||||||
|
if index_name not in db.indexes_names:
|
||||||
|
|
||||||
|
# Remove existing buckets if index isn't there
|
||||||
|
for x in exists:
|
||||||
|
os.unlink(x)
|
||||||
|
|
||||||
|
# Add index (will restore buckets)
|
||||||
|
db.add_index(index_instance)
|
||||||
|
db.reindex_index(index_name)
|
||||||
|
else:
|
||||||
|
# Previous info
|
||||||
|
previous = db.indexes_names[index_name]
|
||||||
|
previous_version = previous._version
|
||||||
|
current_version = klass._version
|
||||||
|
|
||||||
|
# Only edit index if versions are different
|
||||||
|
if previous_version < current_version:
|
||||||
|
log.debug('Index "%s" already exists, updating and reindexing', index_name)
|
||||||
|
db.destroy_index(previous)
|
||||||
|
db.add_index(index_instance)
|
||||||
|
db.reindex_index(index_name)
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed adding index %s: %s', (index_name, traceback.format_exc()))
|
||||||
|
|
||||||
|
def deleteDocument(self, **kwargs):
|
||||||
|
|
||||||
|
db = self.getDB()
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
document_id = kwargs.get('_request').get_argument('id')
|
||||||
|
document = db.get('id', document_id)
|
||||||
|
db.delete(document)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True
|
||||||
|
}
|
||||||
|
except:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': traceback.format_exc()
|
||||||
|
}
|
||||||
|
|
||||||
|
def updateDocument(self, **kwargs):
|
||||||
|
|
||||||
|
db = self.getDB()
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
document = json.loads(kwargs.get('_request').get_argument('document'))
|
||||||
|
d = db.update(document)
|
||||||
|
document.update(d)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'document': document
|
||||||
|
}
|
||||||
|
except:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': traceback.format_exc()
|
||||||
|
}
|
||||||
|
|
||||||
|
def listDocuments(self, **kwargs):
|
||||||
|
db = self.getDB()
|
||||||
|
|
||||||
|
results = {
|
||||||
|
'unknown': []
|
||||||
|
}
|
||||||
|
|
||||||
|
for document in db.all('id'):
|
||||||
|
key = document.get('_t', 'unknown')
|
||||||
|
|
||||||
|
if kwargs.get('show') and key != kwargs.get('show'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not results.get(key):
|
||||||
|
results[key] = []
|
||||||
|
results[key].append(document)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def reindex(self, **kwargs):
|
||||||
|
|
||||||
|
success = True
|
||||||
|
try:
|
||||||
|
db = self.getDB()
|
||||||
|
db.reindex()
|
||||||
|
except:
|
||||||
|
log.error('Failed index: %s', traceback.format_exc())
|
||||||
|
success = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': success
|
||||||
|
}
|
||||||
|
|
||||||
|
def compact(self, try_repair = True, **kwargs):
|
||||||
|
|
||||||
|
success = False
|
||||||
|
db = self.getDB()
|
||||||
|
|
||||||
|
# Removing left over compact files
|
||||||
|
db_path = sp(db.path)
|
||||||
|
for f in os.listdir(sp(db.path)):
|
||||||
|
for x in ['_compact_buck', '_compact_stor']:
|
||||||
|
if f[-len(x):] == x:
|
||||||
|
os.unlink(os.path.join(db_path, f))
|
||||||
|
|
||||||
|
try:
|
||||||
|
start = time.time()
|
||||||
|
size = float(db.get_db_details().get('size', 0))
|
||||||
|
log.debug('Compacting database, current size: %sMB', round(size/1048576, 2))
|
||||||
|
|
||||||
|
db.compact()
|
||||||
|
new_size = float(db.get_db_details().get('size', 0))
|
||||||
|
log.debug('Done compacting database in %ss, new size: %sMB, saved: %sMB', (round(time.time()-start, 2), round(new_size/1048576, 2), round((size-new_size)/1048576, 2)))
|
||||||
|
success = True
|
||||||
|
except (IndexException, AttributeError):
|
||||||
|
if try_repair:
|
||||||
|
log.error('Something wrong with indexes, trying repair')
|
||||||
|
|
||||||
|
# Remove all indexes
|
||||||
|
old_indexes = self.indexes.keys()
|
||||||
|
for index_name in old_indexes:
|
||||||
|
try:
|
||||||
|
db.destroy_index(index_name)
|
||||||
|
except IndexNotFoundException:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
log.error('Failed removing old index %s', index_name)
|
||||||
|
|
||||||
|
# Add them again
|
||||||
|
for index_name in self.indexes:
|
||||||
|
klass = self.indexes[index_name]
|
||||||
|
|
||||||
|
# Category index
|
||||||
|
index_instance = klass(db.path, index_name)
|
||||||
|
try:
|
||||||
|
db.add_index(index_instance)
|
||||||
|
db.reindex_index(index_name)
|
||||||
|
except IndexConflict:
|
||||||
|
pass
|
||||||
|
except:
|
||||||
|
log.error('Failed adding index %s', index_name)
|
||||||
|
raise
|
||||||
|
|
||||||
|
self.compact(try_repair = False)
|
||||||
|
else:
|
||||||
|
log.error('Failed compact: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed compact: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': success
|
||||||
|
}
|
||||||
|
|
||||||
|
# Compact on start
|
||||||
|
def startup_compact(self):
|
||||||
|
from couchpotato import Env
|
||||||
|
|
||||||
|
db = self.getDB()
|
||||||
|
size = db.get_db_details().get('size')
|
||||||
|
prop_name = 'last_db_compact'
|
||||||
|
last_check = int(Env.prop(prop_name, default = 0))
|
||||||
|
|
||||||
|
if size > 26214400 and last_check < time.time()-604800: # 25MB / 7 days
|
||||||
|
self.compact()
|
||||||
|
Env.prop(prop_name, value = int(time.time()))
|
||||||
|
|
||||||
|
def migrate(self):
|
||||||
|
|
||||||
|
from couchpotato import Env
|
||||||
|
old_db = os.path.join(Env.get('data_dir'), 'couchpotato.db')
|
||||||
|
if not os.path.isfile(old_db): return
|
||||||
|
|
||||||
|
log.info('=' * 30)
|
||||||
|
log.info('Migrating database, hold on..')
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
if os.path.isfile(old_db):
|
||||||
|
|
||||||
|
migrate_start = time.time()
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect(old_db)
|
||||||
|
|
||||||
|
migrate_list = {
|
||||||
|
'category': ['id', 'label', 'order', 'required', 'preferred', 'ignored', 'destination'],
|
||||||
|
'profile': ['id', 'label', 'order', 'core', 'hide'],
|
||||||
|
'profiletype': ['id', 'order', 'finish', 'wait_for', 'quality_id', 'profile_id'],
|
||||||
|
'quality': ['id', 'identifier', 'order', 'size_min', 'size_max'],
|
||||||
|
'movie': ['id', 'last_edit', 'library_id', 'status_id', 'profile_id', 'category_id'],
|
||||||
|
'library': ['id', 'identifier', 'info'],
|
||||||
|
'librarytitle': ['id', 'title', 'default', 'libraries_id'],
|
||||||
|
'library_files__file_library': ['library_id', 'file_id'],
|
||||||
|
'release': ['id', 'identifier', 'movie_id', 'status_id', 'quality_id', 'last_edit'],
|
||||||
|
'releaseinfo': ['id', 'identifier', 'value', 'release_id'],
|
||||||
|
'release_files__file_release': ['release_id', 'file_id'],
|
||||||
|
'status': ['id', 'identifier'],
|
||||||
|
'properties': ['id', 'identifier', 'value'],
|
||||||
|
'file': ['id', 'path', 'type_id'],
|
||||||
|
'filetype': ['identifier', 'id']
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate_data = {}
|
||||||
|
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
for ml in migrate_list:
|
||||||
|
migrate_data[ml] = {}
|
||||||
|
rows = migrate_list[ml]
|
||||||
|
|
||||||
|
try:
|
||||||
|
c.execute('SELECT %s FROM `%s`' % ('`' + '`,`'.join(rows) + '`', ml))
|
||||||
|
except:
|
||||||
|
# ignore faulty destination_id database
|
||||||
|
if ml == 'category':
|
||||||
|
migrate_data[ml] = {}
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
for p in c.fetchall():
|
||||||
|
columns = {}
|
||||||
|
for row in migrate_list[ml]:
|
||||||
|
columns[row] = p[rows.index(row)]
|
||||||
|
|
||||||
|
if not migrate_data[ml].get(p[0]):
|
||||||
|
migrate_data[ml][p[0]] = columns
|
||||||
|
else:
|
||||||
|
if not isinstance(migrate_data[ml][p[0]], list):
|
||||||
|
migrate_data[ml][p[0]] = [migrate_data[ml][p[0]]]
|
||||||
|
migrate_data[ml][p[0]].append(columns)
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
log.info('Getting data took %s', time.time() - migrate_start)
|
||||||
|
|
||||||
|
db = self.getDB()
|
||||||
|
|
||||||
|
# Use properties
|
||||||
|
properties = migrate_data['properties']
|
||||||
|
log.info('Importing %s properties', len(properties))
|
||||||
|
for x in properties:
|
||||||
|
property = properties[x]
|
||||||
|
Env.prop(property.get('identifier'), property.get('value'))
|
||||||
|
|
||||||
|
# Categories
|
||||||
|
categories = migrate_data.get('category', [])
|
||||||
|
log.info('Importing %s categories', len(categories))
|
||||||
|
category_link = {}
|
||||||
|
for x in categories:
|
||||||
|
c = categories[x]
|
||||||
|
|
||||||
|
new_c = db.insert({
|
||||||
|
'_t': 'category',
|
||||||
|
'order': c.get('order', 999),
|
||||||
|
'label': toUnicode(c.get('label', '')),
|
||||||
|
'ignored': toUnicode(c.get('ignored', '')),
|
||||||
|
'preferred': toUnicode(c.get('preferred', '')),
|
||||||
|
'required': toUnicode(c.get('required', '')),
|
||||||
|
'destination': toUnicode(c.get('destination', '')),
|
||||||
|
})
|
||||||
|
|
||||||
|
category_link[x] = new_c.get('_id')
|
||||||
|
|
||||||
|
# Profiles
|
||||||
|
log.info('Importing profiles')
|
||||||
|
new_profiles = db.all('profile', with_doc = True)
|
||||||
|
new_profiles_by_label = {}
|
||||||
|
for x in new_profiles:
|
||||||
|
|
||||||
|
# Remove default non core profiles
|
||||||
|
if not x['doc'].get('core'):
|
||||||
|
db.delete(x['doc'])
|
||||||
|
else:
|
||||||
|
new_profiles_by_label[x['doc']['label']] = x['_id']
|
||||||
|
|
||||||
|
profiles = migrate_data['profile']
|
||||||
|
profile_link = {}
|
||||||
|
for x in profiles:
|
||||||
|
p = profiles[x]
|
||||||
|
|
||||||
|
exists = new_profiles_by_label.get(p.get('label'))
|
||||||
|
|
||||||
|
# Update existing with order only
|
||||||
|
if exists and p.get('core'):
|
||||||
|
profile = db.get('id', exists)
|
||||||
|
profile['order'] = tryInt(p.get('order'))
|
||||||
|
profile['hide'] = p.get('hide') in [1, True, 'true', 'True']
|
||||||
|
db.update(profile)
|
||||||
|
|
||||||
|
profile_link[x] = profile.get('_id')
|
||||||
|
else:
|
||||||
|
|
||||||
|
new_profile = {
|
||||||
|
'_t': 'profile',
|
||||||
|
'label': p.get('label'),
|
||||||
|
'order': int(p.get('order', 999)),
|
||||||
|
'core': p.get('core', False),
|
||||||
|
'qualities': [],
|
||||||
|
'wait_for': [],
|
||||||
|
'finish': []
|
||||||
|
}
|
||||||
|
|
||||||
|
types = migrate_data['profiletype']
|
||||||
|
for profile_type in types:
|
||||||
|
p_type = types[profile_type]
|
||||||
|
if types[profile_type]['profile_id'] == p['id']:
|
||||||
|
if p_type['quality_id']:
|
||||||
|
new_profile['finish'].append(p_type['finish'])
|
||||||
|
new_profile['wait_for'].append(p_type['wait_for'])
|
||||||
|
new_profile['qualities'].append(migrate_data['quality'][p_type['quality_id']]['identifier'])
|
||||||
|
|
||||||
|
if len(new_profile['qualities']) > 0:
|
||||||
|
new_profile.update(db.insert(new_profile))
|
||||||
|
profile_link[x] = new_profile.get('_id')
|
||||||
|
else:
|
||||||
|
log.error('Corrupt profile list for "%s", using default.', p.get('label'))
|
||||||
|
|
||||||
|
# Qualities
|
||||||
|
log.info('Importing quality sizes')
|
||||||
|
new_qualities = db.all('quality', with_doc = True)
|
||||||
|
new_qualities_by_identifier = {}
|
||||||
|
for x in new_qualities:
|
||||||
|
new_qualities_by_identifier[x['doc']['identifier']] = x['_id']
|
||||||
|
|
||||||
|
qualities = migrate_data['quality']
|
||||||
|
quality_link = {}
|
||||||
|
for x in qualities:
|
||||||
|
q = qualities[x]
|
||||||
|
q_id = new_qualities_by_identifier[q.get('identifier')]
|
||||||
|
|
||||||
|
quality = db.get('id', q_id)
|
||||||
|
quality['order'] = q.get('order')
|
||||||
|
quality['size_min'] = tryInt(q.get('size_min'))
|
||||||
|
quality['size_max'] = tryInt(q.get('size_max'))
|
||||||
|
db.update(quality)
|
||||||
|
|
||||||
|
quality_link[x] = quality
|
||||||
|
|
||||||
|
# Titles
|
||||||
|
titles = migrate_data['librarytitle']
|
||||||
|
titles_by_library = {}
|
||||||
|
for x in titles:
|
||||||
|
title = titles[x]
|
||||||
|
if title.get('default'):
|
||||||
|
titles_by_library[title.get('libraries_id')] = title.get('title')
|
||||||
|
|
||||||
|
# Releases
|
||||||
|
releaseinfos = migrate_data['releaseinfo']
|
||||||
|
for x in releaseinfos:
|
||||||
|
info = releaseinfos[x]
|
||||||
|
|
||||||
|
# Skip if release doesn't exist for this info
|
||||||
|
if not migrate_data['release'].get(info.get('release_id')):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not migrate_data['release'][info.get('release_id')].get('info'):
|
||||||
|
migrate_data['release'][info.get('release_id')]['info'] = {}
|
||||||
|
|
||||||
|
migrate_data['release'][info.get('release_id')]['info'][info.get('identifier')] = info.get('value')
|
||||||
|
|
||||||
|
releases = migrate_data['release']
|
||||||
|
releases_by_media = {}
|
||||||
|
for x in releases:
|
||||||
|
release = releases[x]
|
||||||
|
if not releases_by_media.get(release.get('movie_id')):
|
||||||
|
releases_by_media[release.get('movie_id')] = []
|
||||||
|
|
||||||
|
releases_by_media[release.get('movie_id')].append(release)
|
||||||
|
|
||||||
|
# Type ids
|
||||||
|
types = migrate_data['filetype']
|
||||||
|
type_by_id = {}
|
||||||
|
for t in types:
|
||||||
|
type = types[t]
|
||||||
|
type_by_id[type.get('id')] = type
|
||||||
|
|
||||||
|
# Media
|
||||||
|
log.info('Importing %s media items', len(migrate_data['movie']))
|
||||||
|
statuses = migrate_data['status']
|
||||||
|
libraries = migrate_data['library']
|
||||||
|
library_files = migrate_data['library_files__file_library']
|
||||||
|
releases_files = migrate_data['release_files__file_release']
|
||||||
|
all_files = migrate_data['file']
|
||||||
|
poster_type = migrate_data['filetype']['poster']
|
||||||
|
medias = migrate_data['movie']
|
||||||
|
for x in medias:
|
||||||
|
m = medias[x]
|
||||||
|
|
||||||
|
status = statuses.get(m['status_id']).get('identifier')
|
||||||
|
l = libraries.get(m['library_id'])
|
||||||
|
|
||||||
|
# Only migrate wanted movies, Skip if no identifier present
|
||||||
|
if not l or not getImdb(l.get('identifier')): continue
|
||||||
|
|
||||||
|
profile_id = profile_link.get(m['profile_id'])
|
||||||
|
category_id = category_link.get(m['category_id'])
|
||||||
|
title = titles_by_library.get(m['library_id'])
|
||||||
|
releases = releases_by_media.get(x, [])
|
||||||
|
info = json.loads(l.get('info', ''))
|
||||||
|
|
||||||
|
files = library_files.get(m['library_id'], [])
|
||||||
|
if not isinstance(files, list):
|
||||||
|
files = [files]
|
||||||
|
|
||||||
|
added_media = fireEvent('movie.add', {
|
||||||
|
'info': info,
|
||||||
|
'identifier': l.get('identifier'),
|
||||||
|
'profile_id': profile_id,
|
||||||
|
'category_id': category_id,
|
||||||
|
'title': title
|
||||||
|
}, force_readd = False, search_after = False, update_after = False, notify_after = False, status = status, single = True)
|
||||||
|
|
||||||
|
if not added_media:
|
||||||
|
log.error('Failed adding media %s: %s', (l.get('identifier'), info))
|
||||||
|
continue
|
||||||
|
|
||||||
|
added_media['files'] = added_media.get('files', {})
|
||||||
|
for f in files:
|
||||||
|
ffile = all_files[f.get('file_id')]
|
||||||
|
|
||||||
|
# Only migrate posters
|
||||||
|
if ffile.get('type_id') == poster_type.get('id'):
|
||||||
|
if ffile.get('path') not in added_media['files'].get('image_poster', []) and os.path.isfile(ffile.get('path')):
|
||||||
|
added_media['files']['image_poster'] = [ffile.get('path')]
|
||||||
|
break
|
||||||
|
|
||||||
|
if 'image_poster' in added_media['files']:
|
||||||
|
db.update(added_media)
|
||||||
|
|
||||||
|
for rel in releases:
|
||||||
|
|
||||||
|
empty_info = False
|
||||||
|
if not rel.get('info'):
|
||||||
|
empty_info = True
|
||||||
|
rel['info'] = {}
|
||||||
|
|
||||||
|
quality = quality_link.get(rel.get('quality_id'))
|
||||||
|
if not quality:
|
||||||
|
continue
|
||||||
|
|
||||||
|
release_status = statuses.get(rel.get('status_id')).get('identifier')
|
||||||
|
|
||||||
|
if rel['info'].get('download_id'):
|
||||||
|
status_support = rel['info'].get('download_status_support', False) in [True, 'true', 'True']
|
||||||
|
rel['info']['download_info'] = {
|
||||||
|
'id': rel['info'].get('download_id'),
|
||||||
|
'downloader': rel['info'].get('download_downloader'),
|
||||||
|
'status_support': status_support,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add status to keys
|
||||||
|
rel['info']['status'] = release_status
|
||||||
|
if not empty_info:
|
||||||
|
fireEvent('release.create_from_search', [rel['info']], added_media, quality, single = True)
|
||||||
|
else:
|
||||||
|
release = {
|
||||||
|
'_t': 'release',
|
||||||
|
'identifier': rel.get('identifier'),
|
||||||
|
'media_id': added_media.get('_id'),
|
||||||
|
'quality': quality.get('identifier'),
|
||||||
|
'status': release_status,
|
||||||
|
'last_edit': int(time.time()),
|
||||||
|
'files': {}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add downloader info if provided
|
||||||
|
try:
|
||||||
|
release['download_info'] = rel['info']['download_info']
|
||||||
|
del rel['download_info']
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Add files
|
||||||
|
release_files = releases_files.get(rel.get('id'), [])
|
||||||
|
if not isinstance(release_files, list):
|
||||||
|
release_files = [release_files]
|
||||||
|
|
||||||
|
if len(release_files) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for f in release_files:
|
||||||
|
rfile = all_files[f.get('file_id')]
|
||||||
|
file_type = type_by_id.get(rfile.get('type_id')).get('identifier')
|
||||||
|
|
||||||
|
if not release['files'].get(file_type):
|
||||||
|
release['files'][file_type] = []
|
||||||
|
|
||||||
|
release['files'][file_type].append(rfile.get('path'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
rls = db.get('release_identifier', rel.get('identifier'), with_doc = True)['doc']
|
||||||
|
rls.update(release)
|
||||||
|
db.update(rls)
|
||||||
|
except:
|
||||||
|
db.insert(release)
|
||||||
|
|
||||||
|
log.info('Total migration took %s', time.time() - migrate_start)
|
||||||
|
log.info('=' * 30)
|
||||||
|
|
||||||
|
# rename old database
|
||||||
|
log.info('Renaming old database to %s ', old_db + '.old')
|
||||||
|
os.rename(old_db, old_db + '.old')
|
||||||
|
|
||||||
|
if os.path.isfile(old_db + '-wal'):
|
||||||
|
os.rename(old_db + '-wal', old_db + '-wal.old')
|
||||||
|
if os.path.isfile(old_db + '-shm'):
|
||||||
|
os.rename(old_db + '-shm', old_db + '-shm.old')
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
config = [{
|
|
||||||
'name': 'download_providers',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'label': 'Downloaders',
|
|
||||||
'description': 'You can select different downloaders for each type (usenet / torrent)',
|
|
||||||
'type': 'list',
|
|
||||||
'name': 'download_providers',
|
|
||||||
'tab': 'downloaders',
|
|
||||||
'options': [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
|
|||||||
+61
-5
@@ -1,15 +1,20 @@
|
|||||||
from __future__ import with_statement
|
from __future__ import with_statement
|
||||||
from couchpotato.core.downloaders.base import Downloader
|
|
||||||
from couchpotato.core.helpers.encoding import sp
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.environment import Env
|
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
from couchpotato.core._base.downloader.main import DownloaderBase
|
||||||
|
from couchpotato.core.helpers.encoding import sp
|
||||||
|
from couchpotato.core.helpers.variable import getDownloadDir
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
autoload = 'Blackhole'
|
||||||
|
|
||||||
class Blackhole(Downloader):
|
|
||||||
|
class Blackhole(DownloaderBase):
|
||||||
|
|
||||||
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
||||||
status_support = False
|
status_support = False
|
||||||
@@ -100,3 +105,54 @@ class Blackhole(Downloader):
|
|||||||
|
|
||||||
return super(Blackhole, self).isEnabled(manual, data) and \
|
return super(Blackhole, self).isEnabled(manual, data) and \
|
||||||
((self.conf('use_for') in for_protocol))
|
((self.conf('use_for') in for_protocol))
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'blackhole',
|
||||||
|
'order': 30,
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
|
'name': 'blackhole',
|
||||||
|
'label': 'Black hole',
|
||||||
|
'description': 'Download the NZB/Torrent to a specific folder. <em>Note: Seeding and copying/linking features do <strong>not</strong> work with Black hole</em>.',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': True,
|
||||||
|
'type': 'enabler',
|
||||||
|
'radio_group': 'nzb,torrent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'directory',
|
||||||
|
'type': 'directory',
|
||||||
|
'description': 'Directory where the .nzb (or .torrent) file is saved to.',
|
||||||
|
'default': getDownloadDir()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'use_for',
|
||||||
|
'label': 'Use for',
|
||||||
|
'default': 'both',
|
||||||
|
'type': 'dropdown',
|
||||||
|
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'create_subdir',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Create a sub directory when saving the .nzb (or .torrent).',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'manual',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
from .main import Blackhole
|
|
||||||
from couchpotato.core.helpers.variable import getDownloadDir
|
|
||||||
|
|
||||||
|
|
||||||
def start():
|
|
||||||
return Blackhole()
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'blackhole',
|
|
||||||
'order': 30,
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'downloaders',
|
|
||||||
'list': 'download_providers',
|
|
||||||
'name': 'blackhole',
|
|
||||||
'label': 'Black hole',
|
|
||||||
'description': 'Download the NZB/Torrent to a specific folder. <em>Note: Seeding and copying/linking features do <strong>not</strong> work with Black hole</em>.',
|
|
||||||
'wizard': True,
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'enabled',
|
|
||||||
'default': True,
|
|
||||||
'type': 'enabler',
|
|
||||||
'radio_group': 'nzb,torrent',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'directory',
|
|
||||||
'type': 'directory',
|
|
||||||
'description': 'Directory where the .nzb (or .torrent) file is saved to.',
|
|
||||||
'default': getDownloadDir()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'use_for',
|
|
||||||
'label': 'Use for',
|
|
||||||
'default': 'both',
|
|
||||||
'type': 'dropdown',
|
|
||||||
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'create_subdir',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Create a sub directory when saving the .nzb (or .torrent).',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'manual',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
+99
-8
@@ -1,20 +1,24 @@
|
|||||||
from base64 import b64encode, b16encode, b32decode
|
from base64 import b64encode, b16encode, b32decode
|
||||||
from bencode import bencode as benc, bdecode
|
|
||||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
|
||||||
from couchpotato.core.helpers.encoding import isInt, sp
|
|
||||||
from couchpotato.core.helpers.variable import tryFloat, cleanHost
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from synchronousdeluge import DelugeClient
|
|
||||||
import os.path
|
import os.path
|
||||||
import re
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
from bencode import bencode as benc, bdecode
|
||||||
|
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||||||
|
from couchpotato.core.helpers.encoding import isInt, sp
|
||||||
|
from couchpotato.core.helpers.variable import tryFloat, cleanHost
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from synchronousdeluge import DelugeClient
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
autoload = 'Deluge'
|
||||||
|
|
||||||
class Deluge(Downloader):
|
|
||||||
|
class Deluge(DownloaderBase):
|
||||||
|
|
||||||
protocol = ['torrent', 'torrent_magnet']
|
protocol = ['torrent', 'torrent_magnet']
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -143,7 +147,7 @@ class Deluge(Downloader):
|
|||||||
'seed_ratio': torrent['ratio'],
|
'seed_ratio': torrent['ratio'],
|
||||||
'timeleft': str(timedelta(seconds = torrent['eta'])),
|
'timeleft': str(timedelta(seconds = torrent['eta'])),
|
||||||
'folder': sp(download_dir if len(torrent_files) == 1 else os.path.join(download_dir, torrent['name'])),
|
'folder': sp(download_dir if len(torrent_files) == 1 else os.path.join(download_dir, torrent['name'])),
|
||||||
'files': '|'.join(torrent_files),
|
'files': torrent_files,
|
||||||
})
|
})
|
||||||
|
|
||||||
return release_downloads
|
return release_downloads
|
||||||
@@ -291,3 +295,90 @@ class DelugeRPC(object):
|
|||||||
return torrent_hash
|
return torrent_hash
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'deluge',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
|
'name': 'deluge',
|
||||||
|
'label': 'Deluge',
|
||||||
|
'description': 'Use <a href="http://www.deluge-torrent.org/" target="_blank">Deluge</a> to download torrents.',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
'radio_group': 'torrent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'host',
|
||||||
|
'default': 'localhost:58846',
|
||||||
|
'description': 'Hostname with port. Usually <strong>localhost:58846</strong>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'directory',
|
||||||
|
'type': 'directory',
|
||||||
|
'description': 'Download to this directory. Keep empty for default Deluge download directory.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'completed_directory',
|
||||||
|
'type': 'directory',
|
||||||
|
'description': 'Move completed torrent to this directory. Keep empty for default Deluge options.',
|
||||||
|
'advanced': True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'label',
|
||||||
|
'description': 'Label to add to torrents in the Deluge UI.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'remove_complete',
|
||||||
|
'label': 'Remove torrent',
|
||||||
|
'type': 'bool',
|
||||||
|
'default': True,
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Remove the torrent from Deluge after it has finished seeding.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'delete_files',
|
||||||
|
'label': 'Remove files',
|
||||||
|
'default': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Also remove the leftover files.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'paused',
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'default': False,
|
||||||
|
'description': 'Add the torrent paused.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'manual',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'delete_failed',
|
||||||
|
'default': True,
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Delete a release after the download has failed.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
from .main import Deluge
|
|
||||||
|
|
||||||
|
|
||||||
def start():
|
|
||||||
return Deluge()
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'deluge',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'downloaders',
|
|
||||||
'list': 'download_providers',
|
|
||||||
'name': 'deluge',
|
|
||||||
'label': 'Deluge',
|
|
||||||
'description': 'Use <a href="http://www.deluge-torrent.org/" target="_blank">Deluge</a> to download torrents.',
|
|
||||||
'wizard': True,
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'enabled',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'enabler',
|
|
||||||
'radio_group': 'torrent',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'host',
|
|
||||||
'default': 'localhost:58846',
|
|
||||||
'description': 'Hostname with port. Usually <strong>localhost:58846</strong>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'username',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'password',
|
|
||||||
'type': 'password',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'directory',
|
|
||||||
'type': 'directory',
|
|
||||||
'description': 'Download to this directory. Keep empty for default Deluge download directory.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'completed_directory',
|
|
||||||
'type': 'directory',
|
|
||||||
'description': 'Move completed torrent to this directory. Keep empty for default Deluge options.',
|
|
||||||
'advanced': True,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'label',
|
|
||||||
'description': 'Label to add to torrents in the Deluge UI.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'remove_complete',
|
|
||||||
'label': 'Remove torrent',
|
|
||||||
'type': 'bool',
|
|
||||||
'default': True,
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Remove the torrent from Deluge after it has finished seeding.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'delete_files',
|
|
||||||
'label': 'Remove files',
|
|
||||||
'default': True,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Also remove the leftover files.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'paused',
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'default': False,
|
|
||||||
'description': 'Add the torrent paused.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'manual',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'delete_failed',
|
|
||||||
'default': True,
|
|
||||||
'advanced': True,
|
|
||||||
'type': 'bool',
|
|
||||||
'description': 'Delete a release after the download has failed.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
+83
-6
@@ -1,8 +1,4 @@
|
|||||||
from base64 import standard_b64encode
|
from base64 import standard_b64encode
|
||||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
|
||||||
from couchpotato.core.helpers.encoding import ss, sp
|
|
||||||
from couchpotato.core.helpers.variable import tryInt, md5, cleanHost
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
@@ -10,10 +6,18 @@ import socket
|
|||||||
import traceback
|
import traceback
|
||||||
import xmlrpclib
|
import xmlrpclib
|
||||||
|
|
||||||
|
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||||||
|
from couchpotato.core.helpers.encoding import ss, sp
|
||||||
|
from couchpotato.core.helpers.variable import tryInt, md5, cleanHost
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
autoload = 'NZBGet'
|
||||||
|
|
||||||
class NZBGet(Downloader):
|
|
||||||
|
class NZBGet(DownloaderBase):
|
||||||
|
|
||||||
protocol = ['nzb']
|
protocol = ['nzb']
|
||||||
rpc = 'xmlrpc'
|
rpc = 'xmlrpc'
|
||||||
@@ -142,7 +146,7 @@ class NZBGet(Downloader):
|
|||||||
'timeleft': timeleft,
|
'timeleft': timeleft,
|
||||||
})
|
})
|
||||||
|
|
||||||
for nzb in queue: # 'Parameters' is not passed in rpc.postqueue
|
for nzb in queue: # 'Parameters' is not passed in rpc.postqueue
|
||||||
if nzb['NZBID'] in ids:
|
if nzb['NZBID'] in ids:
|
||||||
log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename'])
|
log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename'])
|
||||||
release_downloads.append({
|
release_downloads.append({
|
||||||
@@ -214,3 +218,76 @@ class NZBGet(Downloader):
|
|||||||
def getRPC(self):
|
def getRPC(self):
|
||||||
url = cleanHost(host = self.conf('host'), ssl = self.conf('ssl'), username = self.conf('username'), password = self.conf('password')) + self.rpc
|
url = cleanHost(host = self.conf('host'), ssl = self.conf('ssl'), username = self.conf('username'), password = self.conf('password')) + self.rpc
|
||||||
return xmlrpclib.ServerProxy(url)
|
return xmlrpclib.ServerProxy(url)
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'nzbget',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
|
'name': 'nzbget',
|
||||||
|
'label': 'NZBGet',
|
||||||
|
'description': 'Use <a href="http://nzbget.sourceforge.net/Main_Page" target="_blank">NZBGet</a> to download NZBs.',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
'radio_group': 'nzb',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'host',
|
||||||
|
'default': 'localhost:6789',
|
||||||
|
'description': 'Hostname with port. Usually <strong>localhost:6789</strong>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'ssl',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
'default': 'nzbget',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Set a different username to connect. Default: nzbget',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'type': 'password',
|
||||||
|
'description': 'Default NZBGet password is <i>tegbzn6789</i>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'category',
|
||||||
|
'default': 'Movies',
|
||||||
|
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'priority',
|
||||||
|
'advanced': True,
|
||||||
|
'default': '0',
|
||||||
|
'type': 'dropdown',
|
||||||
|
'values': [('Very Low', -100), ('Low', -50), ('Normal', 0), ('High', 50), ('Very High', 100)],
|
||||||
|
'description': 'Only change this if you are using NZBget 9.0 or higher',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'manual',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'delete_failed',
|
||||||
|
'default': True,
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Delete a release after the download has failed.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
from .main import NZBGet
|
|
||||||
|
|
||||||
|
|
||||||
def start():
|
|
||||||
return NZBGet()
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'nzbget',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'downloaders',
|
|
||||||
'list': 'download_providers',
|
|
||||||
'name': 'nzbget',
|
|
||||||
'label': 'NZBGet',
|
|
||||||
'description': 'Use <a href="http://nzbget.sourceforge.net/Main_Page" target="_blank">NZBGet</a> to download NZBs.',
|
|
||||||
'wizard': True,
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'enabled',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'enabler',
|
|
||||||
'radio_group': 'nzb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'host',
|
|
||||||
'default': 'localhost:6789',
|
|
||||||
'description': 'Hostname with port. Usually <strong>localhost:6789</strong>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'ssl',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'username',
|
|
||||||
'default': 'nzbget',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Set a different username to connect. Default: nzbget',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'password',
|
|
||||||
'type': 'password',
|
|
||||||
'description': 'Default NZBGet password is <i>tegbzn6789</i>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'category',
|
|
||||||
'default': 'Movies',
|
|
||||||
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'priority',
|
|
||||||
'advanced': True,
|
|
||||||
'default': '0',
|
|
||||||
'type': 'dropdown',
|
|
||||||
'values': [('Very Low', -100), ('Low', -50), ('Normal', 0), ('High', 50), ('Very High', 100)],
|
|
||||||
'description': 'Only change this if you are using NZBget 9.0 or higher',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'manual',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'delete_failed',
|
|
||||||
'default': True,
|
|
||||||
'advanced': True,
|
|
||||||
'type': 'bool',
|
|
||||||
'description': 'Delete a release after the download has failed.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
+62
-5
@@ -1,8 +1,4 @@
|
|||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode, sp
|
|
||||||
from couchpotato.core.helpers.variable import cleanHost
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from urllib2 import URLError
|
from urllib2 import URLError
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
import hashlib
|
import hashlib
|
||||||
@@ -16,10 +12,18 @@ import time
|
|||||||
import traceback
|
import traceback
|
||||||
import urllib2
|
import urllib2
|
||||||
|
|
||||||
|
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||||||
|
from couchpotato.core.helpers.encoding import tryUrlencode, sp
|
||||||
|
from couchpotato.core.helpers.variable import cleanHost
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
autoload = 'NZBVortex'
|
||||||
|
|
||||||
class NZBVortex(Downloader):
|
|
||||||
|
class NZBVortex(DownloaderBase):
|
||||||
|
|
||||||
protocol = ['nzb']
|
protocol = ['nzb']
|
||||||
api_level = None
|
api_level = None
|
||||||
@@ -186,3 +190,56 @@ class HTTPSConnection(httplib.HTTPSConnection):
|
|||||||
class HTTPSHandler(urllib2.HTTPSHandler):
|
class HTTPSHandler(urllib2.HTTPSHandler):
|
||||||
def https_open(self, req):
|
def https_open(self, req):
|
||||||
return self.do_open(HTTPSConnection, req)
|
return self.do_open(HTTPSConnection, req)
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'nzbvortex',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
|
'name': 'nzbvortex',
|
||||||
|
'label': 'NZBVortex',
|
||||||
|
'description': 'Use <a href="http://www.nzbvortex.com/landing/" target="_blank">NZBVortex</a> to download NZBs.',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
'radio_group': 'nzb',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'host',
|
||||||
|
'default': 'localhost:4321',
|
||||||
|
'description': 'Hostname with port. Usually <strong>localhost:4321</strong>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'ssl',
|
||||||
|
'default': 1,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'api_key',
|
||||||
|
'label': 'Api Key',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'manual',
|
||||||
|
'default': False,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'delete_failed',
|
||||||
|
'default': True,
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Delete a release after the download has failed.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
from .main import NZBVortex
|
|
||||||
|
|
||||||
|
|
||||||
def start():
|
|
||||||
return NZBVortex()
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'nzbvortex',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'downloaders',
|
|
||||||
'list': 'download_providers',
|
|
||||||
'name': 'nzbvortex',
|
|
||||||
'label': 'NZBVortex',
|
|
||||||
'description': 'Use <a href="http://www.nzbvortex.com/landing/" target="_blank">NZBVortex</a> to download NZBs.',
|
|
||||||
'wizard': True,
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'enabled',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'enabler',
|
|
||||||
'radio_group': 'nzb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'host',
|
|
||||||
'default': 'localhost:4321',
|
|
||||||
'description': 'Hostname with port. Usually <strong>localhost:4321</strong>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'ssl',
|
|
||||||
'default': 1,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'api_key',
|
|
||||||
'label': 'Api Key',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'manual',
|
|
||||||
'default': False,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'delete_failed',
|
|
||||||
'default': True,
|
|
||||||
'advanced': True,
|
|
||||||
'type': 'bool',
|
|
||||||
'description': 'Delete a release after the download has failed.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
+42
-4
@@ -1,14 +1,18 @@
|
|||||||
from __future__ import with_statement
|
from __future__ import with_statement
|
||||||
from couchpotato.core.downloaders.base import Downloader
|
|
||||||
from couchpotato.core.helpers.encoding import sp
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
from couchpotato.core._base.downloader.main import DownloaderBase
|
||||||
|
from couchpotato.core.helpers.encoding import sp
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
autoload = 'Pneumatic'
|
||||||
|
|
||||||
class Pneumatic(Downloader):
|
|
||||||
|
class Pneumatic(DownloaderBase):
|
||||||
|
|
||||||
protocol = ['nzb']
|
protocol = ['nzb']
|
||||||
strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s'
|
strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s'
|
||||||
@@ -71,3 +75,37 @@ class Pneumatic(Downloader):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'pneumatic',
|
||||||
|
'order': 30,
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
|
'name': 'pneumatic',
|
||||||
|
'label': 'Pneumatic',
|
||||||
|
'description': 'Use <a href="http://forum.xbmc.org/showthread.php?tid=97657" target="_blank">Pneumatic</a> to download .strm files.',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'directory',
|
||||||
|
'type': 'directory',
|
||||||
|
'description': 'Directory where the .strm file is saved to.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'manual',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
from .main import Pneumatic
|
|
||||||
|
|
||||||
|
|
||||||
def start():
|
|
||||||
return Pneumatic()
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'pneumatic',
|
|
||||||
'order': 30,
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'downloaders',
|
|
||||||
'list': 'download_providers',
|
|
||||||
'name': 'pneumatic',
|
|
||||||
'label': 'Pneumatic',
|
|
||||||
'description': 'Use <a href="http://forum.xbmc.org/showthread.php?tid=97657" target="_blank">Pneumatic</a> to download .strm files.',
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'enabled',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'enabler',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'directory',
|
|
||||||
'type': 'directory',
|
|
||||||
'description': 'Directory where the .strm file is saved to.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'manual',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
from base64 import b16encode, b32decode
|
||||||
|
from hashlib import sha1
|
||||||
|
import os
|
||||||
|
|
||||||
|
from bencode import bencode, bdecode
|
||||||
|
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||||||
|
from couchpotato.core.helpers.encoding import sp
|
||||||
|
from couchpotato.core.helpers.variable import cleanHost
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from qbittorrent.client import QBittorrentClient
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
autoload = 'qBittorrent'
|
||||||
|
|
||||||
|
|
||||||
|
class qBittorrent(DownloaderBase):
|
||||||
|
|
||||||
|
protocol = ['torrent', 'torrent_magnet']
|
||||||
|
qb = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(qBittorrent, self).__init__()
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
if self.qb is not None:
|
||||||
|
return self.qb
|
||||||
|
|
||||||
|
url = cleanHost(self.conf('host'), protocol = True, ssl = False)
|
||||||
|
|
||||||
|
if self.conf('username') and self.conf('password'):
|
||||||
|
self.qb = QBittorrentClient(
|
||||||
|
url,
|
||||||
|
username = self.conf('username'),
|
||||||
|
password = self.conf('password')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.qb = QBittorrentClient(url)
|
||||||
|
|
||||||
|
return self.qb
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
if self.connect():
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def download(self, data = None, media = None, filedata = None):
|
||||||
|
if not media: media = {}
|
||||||
|
if not data: data = {}
|
||||||
|
|
||||||
|
log.debug('Sending "%s" to qBittorrent.', (data.get('name')))
|
||||||
|
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not filedata and data.get('protocol') == 'torrent':
|
||||||
|
log.error('Failed sending torrent, no data')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if data.get('protocol') == 'torrent_magnet':
|
||||||
|
filedata = self.magnetToTorrent(data.get('url'))
|
||||||
|
|
||||||
|
if filedata is False:
|
||||||
|
return False
|
||||||
|
|
||||||
|
data['protocol'] = 'torrent'
|
||||||
|
|
||||||
|
info = bdecode(filedata)["info"]
|
||||||
|
torrent_hash = sha1(bencode(info)).hexdigest()
|
||||||
|
|
||||||
|
# Convert base 32 to hex
|
||||||
|
if len(torrent_hash) == 32:
|
||||||
|
torrent_hash = b16encode(b32decode(torrent_hash))
|
||||||
|
|
||||||
|
# Send request to qBittorrent
|
||||||
|
try:
|
||||||
|
self.qb.add_file(filedata)
|
||||||
|
|
||||||
|
return self.downloadReturnId(torrent_hash)
|
||||||
|
except Exception as e:
|
||||||
|
log.error('Failed to send torrent to qBittorrent: %s', e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def getTorrentStatus(self, torrent):
|
||||||
|
|
||||||
|
if torrent.state in ('uploading', 'queuedUP', 'stalledUP'):
|
||||||
|
return 'seeding'
|
||||||
|
|
||||||
|
if torrent.progress == 1:
|
||||||
|
return 'completed'
|
||||||
|
|
||||||
|
return 'busy'
|
||||||
|
|
||||||
|
def getAllDownloadStatus(self, ids):
|
||||||
|
log.debug('Checking qBittorrent download status.')
|
||||||
|
|
||||||
|
if not self.connect():
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
torrents = self.qb.get_torrents()
|
||||||
|
|
||||||
|
release_downloads = ReleaseDownloadList(self)
|
||||||
|
|
||||||
|
for torrent in torrents:
|
||||||
|
if torrent.hash in ids:
|
||||||
|
torrent.update_general() # get extra info
|
||||||
|
torrent_filelist = torrent.get_files()
|
||||||
|
|
||||||
|
torrent_files = []
|
||||||
|
torrent_dir = os.path.join(torrent.save_path, torrent.name)
|
||||||
|
|
||||||
|
if os.path.isdir(torrent_dir):
|
||||||
|
torrent.save_path = torrent_dir
|
||||||
|
|
||||||
|
if len(torrent_filelist) > 1 and os.path.isdir(torrent_dir): # multi file torrent, path.isdir check makes sure we're not in the root download folder
|
||||||
|
for root, _, files in os.walk(torrent.save_path):
|
||||||
|
for f in files:
|
||||||
|
torrent_files.append(sp(os.path.join(root, f)))
|
||||||
|
|
||||||
|
else: # multi or single file placed directly in torrent.save_path
|
||||||
|
for f in torrent_filelist:
|
||||||
|
file_path = os.path.join(torrent.save_path, f.name)
|
||||||
|
if os.path.isfile(file_path):
|
||||||
|
torrent_files.append(sp(file_path))
|
||||||
|
|
||||||
|
release_downloads.append({
|
||||||
|
'id': torrent.hash,
|
||||||
|
'name': torrent.name,
|
||||||
|
'status': self.getTorrentStatus(torrent),
|
||||||
|
'seed_ratio': torrent.ratio,
|
||||||
|
'original_status': torrent.state,
|
||||||
|
'timeleft': torrent.progress * 100 if torrent.progress else -1, # percentage
|
||||||
|
'folder': sp(torrent.save_path),
|
||||||
|
'files': torrent_files
|
||||||
|
})
|
||||||
|
|
||||||
|
return release_downloads
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error('Failed to get status from qBittorrent: %s', e)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def pause(self, release_download, pause = True):
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
torrent = self.qb.get_torrent(release_download['id'])
|
||||||
|
if torrent is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if pause:
|
||||||
|
return torrent.pause()
|
||||||
|
return torrent.resume()
|
||||||
|
|
||||||
|
def removeFailed(self, release_download):
|
||||||
|
log.info('%s failed downloading, deleting...', release_download['name'])
|
||||||
|
return self.processComplete(release_download, delete_files = True)
|
||||||
|
|
||||||
|
def processComplete(self, release_download, delete_files):
|
||||||
|
log.debug('Requesting qBittorrent to remove the torrent %s%s.',
|
||||||
|
(release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||||
|
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
torrent = self.qb.find_torrent(release_download['id'])
|
||||||
|
|
||||||
|
if torrent is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if delete_files:
|
||||||
|
torrent.delete() # deletes torrent with data
|
||||||
|
else:
|
||||||
|
torrent.remove() # just removes the torrent, doesn't delete data
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'qbittorrent',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
|
'name': 'qbittorrent',
|
||||||
|
'label': 'qbittorrent',
|
||||||
|
'description': '',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
'radio_group': 'torrent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'host',
|
||||||
|
'default': 'http://localhost:8080/',
|
||||||
|
'description': 'RPC Communication URI. Usually <strong>http://localhost:8080/</strong>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'remove_complete',
|
||||||
|
'label': 'Remove torrent',
|
||||||
|
'default': False,
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Remove the torrent after it finishes seeding.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'delete_files',
|
||||||
|
'label': 'Remove files',
|
||||||
|
'default': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Also remove the leftover files.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'paused',
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'default': False,
|
||||||
|
'description': 'Add the torrent paused.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'manual',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
from .main import rTorrent
|
|
||||||
|
|
||||||
|
|
||||||
def start():
|
|
||||||
return rTorrent()
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'rtorrent',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'downloaders',
|
|
||||||
'list': 'download_providers',
|
|
||||||
'name': 'rtorrent',
|
|
||||||
'label': 'rTorrent',
|
|
||||||
'description': '',
|
|
||||||
'wizard': True,
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'enabled',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'enabler',
|
|
||||||
'radio_group': 'torrent',
|
|
||||||
},
|
|
||||||
# @RuudBurger: How do I migrate this?
|
|
||||||
# {
|
|
||||||
# 'name': 'url',
|
|
||||||
# 'default': 'http://localhost:80/RPC2',
|
|
||||||
# 'description': 'XML-RPC Endpoint URI. Usually <strong>scgi://localhost:5000</strong> '
|
|
||||||
# 'or <strong>http://localhost:80/RPC2</strong>'
|
|
||||||
# },
|
|
||||||
{
|
|
||||||
'name': 'host',
|
|
||||||
'default': 'localhost:80',
|
|
||||||
'description': 'RPC Communication URI. Usually <strong>scgi://localhost:5000</strong>, '
|
|
||||||
'<strong>httprpc://localhost/rutorrent</strong> or <strong>localhost:80</strong>'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'ssl',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'rpc_url',
|
|
||||||
'type': 'string',
|
|
||||||
'default': 'RPC2',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Change if your RPC mount is at a different path.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'username',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'password',
|
|
||||||
'type': 'password',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'label',
|
|
||||||
'description': 'Label to apply on added torrents.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'directory',
|
|
||||||
'type': 'directory',
|
|
||||||
'description': 'Download to this directory. Keep empty for default rTorrent download directory.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'remove_complete',
|
|
||||||
'label': 'Remove torrent',
|
|
||||||
'default': False,
|
|
||||||
'advanced': True,
|
|
||||||
'type': 'bool',
|
|
||||||
'description': 'Remove the torrent after it finishes seeding.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'delete_files',
|
|
||||||
'label': 'Remove files',
|
|
||||||
'default': True,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Also remove the leftover files.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'paused',
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'default': False,
|
|
||||||
'description': 'Add the torrent paused.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'manual',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
Executable → Regular
+118
-75
@@ -1,21 +1,24 @@
|
|||||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
|
||||||
from couchpotato.core.event import fireEvent, addEvent
|
|
||||||
from couchpotato.core.helpers.encoding import sp
|
|
||||||
from couchpotato.core.helpers.variable import cleanHost, splitString
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from base64 import b16encode, b32decode
|
from base64 import b16encode, b32decode
|
||||||
from bencode import bencode, bdecode
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from rtorrent import RTorrent
|
|
||||||
from rtorrent.err import MethodError
|
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||||||
|
from couchpotato.core.event import addEvent
|
||||||
|
from couchpotato.core.helpers.encoding import sp
|
||||||
|
from couchpotato.core.helpers.variable import cleanHost, splitString
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from bencode import bencode, bdecode
|
||||||
|
from rtorrent import RTorrent
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
autoload = 'rTorrent'
|
||||||
|
|
||||||
class rTorrent(Downloader):
|
|
||||||
|
class rTorrent(DownloaderBase):
|
||||||
|
|
||||||
protocol = ['torrent', 'torrent_magnet']
|
protocol = ['torrent', 'torrent_magnet']
|
||||||
rt = None
|
rt = None
|
||||||
@@ -54,27 +57,29 @@ class rTorrent(Downloader):
|
|||||||
return self.rt
|
return self.rt
|
||||||
|
|
||||||
url = cleanHost(self.conf('host'), protocol = True, ssl = self.conf('ssl'))
|
url = cleanHost(self.conf('host'), protocol = True, ssl = self.conf('ssl'))
|
||||||
|
|
||||||
|
# Automatically add '+https' to 'httprpc' protocol if SSL is enabled
|
||||||
|
if self.conf('ssl') and url.startswith('httprpc://'):
|
||||||
|
url = url.replace('httprpc://', 'httprpc+https://')
|
||||||
|
|
||||||
parsed = urlparse(url)
|
parsed = urlparse(url)
|
||||||
|
|
||||||
# rpc_url is only used on http/https scgi pass-through
|
# rpc_url is only used on http/https scgi pass-through
|
||||||
if parsed.scheme in ['http', 'https']:
|
if parsed.scheme in ['http', 'https']:
|
||||||
url += self.conf('rpc_url')
|
url += self.conf('rpc_url')
|
||||||
|
|
||||||
if self.conf('username') and self.conf('password'):
|
self.rt = RTorrent(
|
||||||
self.rt = RTorrent(
|
url,
|
||||||
url,
|
self.conf('username'),
|
||||||
self.conf('username'),
|
self.conf('password')
|
||||||
self.conf('password')
|
)
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.rt = RTorrent(url)
|
|
||||||
|
|
||||||
self.error_msg = ''
|
self.error_msg = ''
|
||||||
try:
|
try:
|
||||||
self.rt._verify_conn()
|
self.rt._verify_conn()
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
self.error_msg = e.message
|
self.error_msg = e.message
|
||||||
self.rt = None
|
self.rt = None
|
||||||
|
|
||||||
return self.rt
|
return self.rt
|
||||||
|
|
||||||
@@ -87,44 +92,6 @@ class rTorrent(Downloader):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def updateProviderGroup(self, name, data):
|
|
||||||
if data.get('seed_time'):
|
|
||||||
log.info('seeding time ignored, not supported')
|
|
||||||
|
|
||||||
if not name:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if not self.connect():
|
|
||||||
return False
|
|
||||||
|
|
||||||
views = self.rt.get_views()
|
|
||||||
|
|
||||||
if name not in views:
|
|
||||||
self.rt.create_group(name)
|
|
||||||
|
|
||||||
group = self.rt.get_group(name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
if data.get('seed_ratio'):
|
|
||||||
ratio = int(float(data.get('seed_ratio')) * 100)
|
|
||||||
log.debug('Updating provider ratio to %s, group name: %s', (ratio, name))
|
|
||||||
|
|
||||||
# Explicitly set all group options to ensure it is setup correctly
|
|
||||||
group.set_upload('1M')
|
|
||||||
group.set_min(ratio)
|
|
||||||
group.set_max(ratio)
|
|
||||||
group.set_command('d.stop')
|
|
||||||
group.enable()
|
|
||||||
else:
|
|
||||||
# Reset group action and disable it
|
|
||||||
group.set_command()
|
|
||||||
group.disable()
|
|
||||||
except MethodError as err:
|
|
||||||
log.error('Unable to set group options: %s', err.msg)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def download(self, data = None, media = None, filedata = None):
|
def download(self, data = None, media = None, filedata = None):
|
||||||
if not media: media = {}
|
if not media: media = {}
|
||||||
@@ -135,10 +102,6 @@ class rTorrent(Downloader):
|
|||||||
if not self.connect():
|
if not self.connect():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
group_name = 'cp_' + data.get('provider').lower()
|
|
||||||
if not self.updateProviderGroup(group_name, data):
|
|
||||||
return False
|
|
||||||
|
|
||||||
torrent_params = {}
|
torrent_params = {}
|
||||||
if self.conf('label'):
|
if self.conf('label'):
|
||||||
torrent_params['label'] = self.conf('label')
|
torrent_params['label'] = self.conf('label')
|
||||||
@@ -179,9 +142,6 @@ class rTorrent(Downloader):
|
|||||||
if self.conf('directory'):
|
if self.conf('directory'):
|
||||||
torrent.set_directory(self.conf('directory'))
|
torrent.set_directory(self.conf('directory'))
|
||||||
|
|
||||||
# Set Ratio Group
|
|
||||||
torrent.set_visible(group_name)
|
|
||||||
|
|
||||||
# Start torrent
|
# Start torrent
|
||||||
if not self.conf('paused', default = 0):
|
if not self.conf('paused', default = 0):
|
||||||
torrent.start()
|
torrent.start()
|
||||||
@@ -192,19 +152,13 @@ class rTorrent(Downloader):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def getTorrentStatus(self, torrent):
|
def getTorrentStatus(self, torrent):
|
||||||
if torrent.hashing or torrent.hash_checking or torrent.message:
|
|
||||||
return 'busy'
|
|
||||||
|
|
||||||
if not torrent.complete:
|
if not torrent.complete:
|
||||||
return 'busy'
|
return 'busy'
|
||||||
|
|
||||||
if not torrent.open:
|
if torrent.open:
|
||||||
return 'completed'
|
|
||||||
|
|
||||||
if torrent.state and torrent.active:
|
|
||||||
return 'seeding'
|
return 'seeding'
|
||||||
|
|
||||||
return 'busy'
|
return 'completed'
|
||||||
|
|
||||||
def getAllDownloadStatus(self, ids):
|
def getAllDownloadStatus(self, ids):
|
||||||
log.debug('Checking rTorrent download status.')
|
log.debug('Checking rTorrent download status.')
|
||||||
@@ -238,7 +192,7 @@ class rTorrent(Downloader):
|
|||||||
'original_status': torrent.state,
|
'original_status': torrent.state,
|
||||||
'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1,
|
'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1,
|
||||||
'folder': sp(torrent.directory),
|
'folder': sp(torrent.directory),
|
||||||
'files': '|'.join(torrent_files)
|
'files': torrent_files
|
||||||
})
|
})
|
||||||
|
|
||||||
return release_downloads
|
return release_downloads
|
||||||
@@ -282,7 +236,7 @@ class rTorrent(Downloader):
|
|||||||
if torrent.is_multi_file() and torrent.directory.endswith(torrent.name):
|
if torrent.is_multi_file() and torrent.directory.endswith(torrent.name):
|
||||||
# Remove empty directories bottom up
|
# Remove empty directories bottom up
|
||||||
try:
|
try:
|
||||||
for path, _, _ in os.walk(torrent.directory, topdown = False):
|
for path, _, _ in os.walk(sp(torrent.directory), topdown = False):
|
||||||
os.rmdir(path)
|
os.rmdir(path)
|
||||||
except OSError:
|
except OSError:
|
||||||
log.info('Directory "%s" contains extra files, unable to remove', torrent.directory)
|
log.info('Directory "%s" contains extra files, unable to remove', torrent.directory)
|
||||||
@@ -290,3 +244,92 @@ class rTorrent(Downloader):
|
|||||||
torrent.erase() # just removes the torrent, doesn't delete data
|
torrent.erase() # just removes the torrent, doesn't delete data
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'rtorrent',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
|
'name': 'rtorrent',
|
||||||
|
'label': 'rTorrent',
|
||||||
|
'description': '',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
'radio_group': 'torrent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'host',
|
||||||
|
'default': 'localhost:80',
|
||||||
|
'description': 'RPC Communication URI. Usually <strong>scgi://localhost:5000</strong>, '
|
||||||
|
'<strong>httprpc://localhost/rutorrent</strong> or <strong>localhost:80</strong>'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'ssl',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'rpc_url',
|
||||||
|
'type': 'string',
|
||||||
|
'default': 'RPC2',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Change if your RPC mount is at a different path.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'label',
|
||||||
|
'description': 'Label to apply on added torrents.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'directory',
|
||||||
|
'type': 'directory',
|
||||||
|
'description': 'Download to this directory. Keep empty for default rTorrent download directory.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'remove_complete',
|
||||||
|
'label': 'Remove torrent',
|
||||||
|
'default': False,
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Remove the torrent after it finishes seeding.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'delete_files',
|
||||||
|
'label': 'Remove files',
|
||||||
|
'default': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Also remove the leftover files.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'paused',
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'default': False,
|
||||||
|
'description': 'Add the torrent paused.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'manual',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
+84
-6
@@ -1,18 +1,22 @@
|
|||||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss, sp
|
|
||||||
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.environment import Env
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from urllib2 import URLError
|
from urllib2 import URLError
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||||||
|
from couchpotato.core.helpers.encoding import tryUrlencode, ss, sp
|
||||||
|
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
autoload = 'Sabnzbd'
|
||||||
|
|
||||||
class Sabnzbd(Downloader):
|
|
||||||
|
class Sabnzbd(DownloaderBase):
|
||||||
|
|
||||||
protocol = ['nzb']
|
protocol = ['nzb']
|
||||||
|
|
||||||
@@ -201,3 +205,77 @@ class Sabnzbd(Downloader):
|
|||||||
else:
|
else:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'sabnzbd',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
|
'name': 'sabnzbd',
|
||||||
|
'label': 'Sabnzbd',
|
||||||
|
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> (0.7+) to download NZBs.',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
'radio_group': 'nzb',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'host',
|
||||||
|
'default': 'localhost:8080',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'ssl',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'api_key',
|
||||||
|
'label': 'Api Key',
|
||||||
|
'description': 'Used for all calls to Sabnzbd.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'category',
|
||||||
|
'label': 'Category',
|
||||||
|
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'priority',
|
||||||
|
'label': 'Priority',
|
||||||
|
'type': 'dropdown',
|
||||||
|
'default': '0',
|
||||||
|
'advanced': True,
|
||||||
|
'values': [('Paused', -2), ('Low', -1), ('Normal', 0), ('High', 1), ('Forced', 2)],
|
||||||
|
'description': 'Add to the queue with this priority.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'manual',
|
||||||
|
'default': False,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'remove_complete',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Remove NZB',
|
||||||
|
'default': False,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Remove the NZB from history after it completed.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'delete_failed',
|
||||||
|
'default': True,
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Delete a release after the download has failed.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
from .main import Sabnzbd
|
|
||||||
|
|
||||||
|
|
||||||
def start():
|
|
||||||
return Sabnzbd()
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'sabnzbd',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'downloaders',
|
|
||||||
'list': 'download_providers',
|
|
||||||
'name': 'sabnzbd',
|
|
||||||
'label': 'Sabnzbd',
|
|
||||||
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> (0.7+) to download NZBs.',
|
|
||||||
'wizard': True,
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'enabled',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'enabler',
|
|
||||||
'radio_group': 'nzb',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'host',
|
|
||||||
'default': 'localhost:8080',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'ssl',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'api_key',
|
|
||||||
'label': 'Api Key',
|
|
||||||
'description': 'Used for all calls to Sabnzbd.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'category',
|
|
||||||
'label': 'Category',
|
|
||||||
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'priority',
|
|
||||||
'label': 'Priority',
|
|
||||||
'type': 'dropdown',
|
|
||||||
'default': '0',
|
|
||||||
'advanced': True,
|
|
||||||
'values': [('Paused', -2), ('Low', -1), ('Normal', 0), ('High', 1), ('Forced', 2)],
|
|
||||||
'description': 'Add to the queue with this priority.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'manual',
|
|
||||||
'default': False,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'remove_complete',
|
|
||||||
'advanced': True,
|
|
||||||
'label': 'Remove NZB',
|
|
||||||
'default': False,
|
|
||||||
'type': 'bool',
|
|
||||||
'description': 'Remove the NZB from history after it completed.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'delete_failed',
|
|
||||||
'default': True,
|
|
||||||
'advanced': True,
|
|
||||||
'type': 'bool',
|
|
||||||
'description': 'Delete a release after the download has failed.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
+70
-6
@@ -1,15 +1,19 @@
|
|||||||
from couchpotato.core.downloaders.base import Downloader
|
import json
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from couchpotato.core._base.downloader.main import DownloaderBase
|
||||||
from couchpotato.core.helpers.encoding import isInt
|
from couchpotato.core.helpers.encoding import isInt
|
||||||
from couchpotato.core.helpers.variable import cleanHost
|
from couchpotato.core.helpers.variable import cleanHost
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
import json
|
|
||||||
import requests
|
import requests
|
||||||
import traceback
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
autoload = 'Synology'
|
||||||
|
|
||||||
class Synology(Downloader):
|
|
||||||
|
class Synology(DownloaderBase):
|
||||||
|
|
||||||
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
||||||
status_support = False
|
status_support = False
|
||||||
@@ -29,7 +33,7 @@ class Synology(Downloader):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Send request to Synology
|
# Send request to Synology
|
||||||
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
|
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'), self.conf('destination'))
|
||||||
if data['protocol'] == 'torrent_magnet':
|
if data['protocol'] == 'torrent_magnet':
|
||||||
log.info('Adding torrent URL %s', data['url'])
|
log.info('Adding torrent URL %s', data['url'])
|
||||||
response = srpc.create_task(url = data['url'])
|
response = srpc.create_task(url = data['url'])
|
||||||
@@ -80,14 +84,16 @@ class SynologyRPC(object):
|
|||||||
|
|
||||||
"""SynologyRPC lite library"""
|
"""SynologyRPC lite library"""
|
||||||
|
|
||||||
def __init__(self, host = 'localhost', port = 5000, username = None, password = None):
|
def __init__(self, host = 'localhost', port = 5000, username = None, password = None, destination = None):
|
||||||
|
|
||||||
super(SynologyRPC, self).__init__()
|
super(SynologyRPC, self).__init__()
|
||||||
|
|
||||||
self.download_url = 'http://%s:%s/webapi/DownloadStation/task.cgi' % (host, port)
|
self.download_url = 'http://%s:%s/webapi/DownloadStation/task.cgi' % (host, port)
|
||||||
self.auth_url = 'http://%s:%s/webapi/auth.cgi' % (host, port)
|
self.auth_url = 'http://%s:%s/webapi/auth.cgi' % (host, port)
|
||||||
|
self.sid = None
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
|
self.destination = destination
|
||||||
self.session_name = 'DownloadStation'
|
self.session_name = 'DownloadStation'
|
||||||
|
|
||||||
def _login(self):
|
def _login(self):
|
||||||
@@ -140,6 +146,10 @@ class SynologyRPC(object):
|
|||||||
'version': '1',
|
'version': '1',
|
||||||
'method': 'create',
|
'method': 'create',
|
||||||
'_sid': self.sid}
|
'_sid': self.sid}
|
||||||
|
|
||||||
|
if self.destination and len(self.destination) > 0:
|
||||||
|
args['destination'] = self.destination
|
||||||
|
|
||||||
if url:
|
if url:
|
||||||
log.info('Login success, adding torrent URI')
|
log.info('Login success, adding torrent URI')
|
||||||
args['uri'] = url
|
args['uri'] = url
|
||||||
@@ -160,3 +170,57 @@ class SynologyRPC(object):
|
|||||||
|
|
||||||
def test(self):
|
def test(self):
|
||||||
return bool(self._login())
|
return bool(self._login())
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'synology',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
|
'name': 'synology',
|
||||||
|
'label': 'Synology',
|
||||||
|
'description': 'Use <a href="http://www.synology.com/dsm/home_home_applications_download_station.php" target="_blank">Synology Download Station</a> to download.',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
'radio_group': 'nzb,torrent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'host',
|
||||||
|
'default': 'localhost:5000',
|
||||||
|
'description': 'Hostname with port. Usually <strong>localhost:5000</strong>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'destination',
|
||||||
|
'description': 'Specify <strong>existing</strong> destination share to where your files will be downloaded, usually <strong>Downloads</strong>',
|
||||||
|
'advanced': True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'use_for',
|
||||||
|
'label': 'Use for',
|
||||||
|
'default': 'both',
|
||||||
|
'type': 'dropdown',
|
||||||
|
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'manual',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
from .main import Synology
|
|
||||||
|
|
||||||
|
|
||||||
def start():
|
|
||||||
return Synology()
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'synology',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'downloaders',
|
|
||||||
'list': 'download_providers',
|
|
||||||
'name': 'synology',
|
|
||||||
'label': 'Synology',
|
|
||||||
'description': 'Use <a href="http://www.synology.com/dsm/home_home_applications_download_station.php" target="_blank">Synology Download Station</a> to download.',
|
|
||||||
'wizard': True,
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'enabled',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'enabler',
|
|
||||||
'radio_group': 'nzb,torrent',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'host',
|
|
||||||
'default': 'localhost:5000',
|
|
||||||
'description': 'Hostname with port. Usually <strong>localhost:5000</strong>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'username',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'password',
|
|
||||||
'type': 'password',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'use_for',
|
|
||||||
'label': 'Use for',
|
|
||||||
'default': 'both',
|
|
||||||
'type': 'dropdown',
|
|
||||||
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'manual',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
+103
-8
@@ -1,8 +1,4 @@
|
|||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
|
||||||
from couchpotato.core.helpers.encoding import isInt, sp
|
|
||||||
from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import httplib
|
import httplib
|
||||||
import json
|
import json
|
||||||
@@ -10,10 +6,18 @@ import os.path
|
|||||||
import re
|
import re
|
||||||
import urllib2
|
import urllib2
|
||||||
|
|
||||||
|
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||||||
|
from couchpotato.core.helpers.encoding import isInt, sp
|
||||||
|
from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
autoload = 'Transmission'
|
||||||
|
|
||||||
class Transmission(Downloader):
|
|
||||||
|
class Transmission(DownloaderBase):
|
||||||
|
|
||||||
protocol = ['torrent', 'torrent_magnet']
|
protocol = ['torrent', 'torrent_magnet']
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -137,7 +141,7 @@ class Transmission(Downloader):
|
|||||||
'seed_ratio': torrent['uploadRatio'],
|
'seed_ratio': torrent['uploadRatio'],
|
||||||
'timeleft': str(timedelta(seconds = torrent['eta'])),
|
'timeleft': str(timedelta(seconds = torrent['eta'])),
|
||||||
'folder': sp(torrent_folder if len(torrent_files) == 1 else os.path.join(torrent_folder, torrent['name'])),
|
'folder': sp(torrent_folder if len(torrent_files) == 1 else os.path.join(torrent_folder, torrent['name'])),
|
||||||
'files': '|'.join(torrent_files)
|
'files': torrent_files
|
||||||
})
|
})
|
||||||
|
|
||||||
return release_downloads
|
return release_downloads
|
||||||
@@ -156,6 +160,7 @@ class Transmission(Downloader):
|
|||||||
log.debug('Requesting Transmission to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
log.debug('Requesting Transmission to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||||
return self.trpc.remove_torrent(release_download['id'], delete_files)
|
return self.trpc.remove_torrent(release_download['id'], delete_files)
|
||||||
|
|
||||||
|
|
||||||
class TransmissionRPC(object):
|
class TransmissionRPC(object):
|
||||||
|
|
||||||
"""TransmissionRPC lite library"""
|
"""TransmissionRPC lite library"""
|
||||||
@@ -169,8 +174,8 @@ class TransmissionRPC(object):
|
|||||||
self.session = {}
|
self.session = {}
|
||||||
if username and password:
|
if username and password:
|
||||||
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
||||||
password_manager.add_password(realm = None, uri = self.url, user = username, passwd = password)
|
password_manager.add_password(realm = 'Transmission', uri = self.url, user = username, passwd = password)
|
||||||
opener = urllib2.build_opener(urllib2.HTTPBasicAuthHandler(password_manager), urllib2.HTTPDigestAuthHandler(password_manager))
|
opener = urllib2.build_opener(urllib2.HTTPBasicAuthHandler(password_manager))
|
||||||
opener.addheaders = [('User-agent', 'couchpotato-transmission-client/1.0')]
|
opener.addheaders = [('User-agent', 'couchpotato-transmission-client/1.0')]
|
||||||
urllib2.install_opener(opener)
|
urllib2.install_opener(opener)
|
||||||
elif username or password:
|
elif username or password:
|
||||||
@@ -251,3 +256,93 @@ class TransmissionRPC(object):
|
|||||||
post_data = {'arguments': {'ids': torrent_id, 'delete-local-data': delete_local_data}, 'method': 'torrent-remove', 'tag': self.tag}
|
post_data = {'arguments': {'ids': torrent_id, 'delete-local-data': delete_local_data}, 'method': 'torrent-remove', 'tag': self.tag}
|
||||||
return self._request(post_data)
|
return self._request(post_data)
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'transmission',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
|
'name': 'transmission',
|
||||||
|
'label': 'Transmission',
|
||||||
|
'description': 'Use <a href="http://www.transmissionbt.com/" target="_blank">Transmission</a> to download torrents.',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
'radio_group': 'torrent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'host',
|
||||||
|
'default': 'localhost:9091',
|
||||||
|
'description': 'Hostname with port. Usually <strong>localhost:9091</strong>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'rpc_url',
|
||||||
|
'type': 'string',
|
||||||
|
'default': 'transmission',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Change if you don\'t run Transmission RPC at the default url.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'directory',
|
||||||
|
'type': 'directory',
|
||||||
|
'description': 'Download to this directory. Keep empty for default Transmission download directory.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'remove_complete',
|
||||||
|
'label': 'Remove torrent',
|
||||||
|
'default': True,
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Remove the torrent from Transmission after it finished seeding.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'delete_files',
|
||||||
|
'label': 'Remove files',
|
||||||
|
'default': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Also remove the leftover files.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'paused',
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'default': False,
|
||||||
|
'description': 'Add the torrent paused.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'manual',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'stalled_as_failed',
|
||||||
|
'default': True,
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Consider a stalled torrent as failed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'delete_failed',
|
||||||
|
'default': True,
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Delete a release after the download has failed.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
from .main import Transmission
|
|
||||||
|
|
||||||
|
|
||||||
def start():
|
|
||||||
return Transmission()
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'transmission',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'downloaders',
|
|
||||||
'list': 'download_providers',
|
|
||||||
'name': 'transmission',
|
|
||||||
'label': 'Transmission',
|
|
||||||
'description': 'Use <a href="http://www.transmissionbt.com/" target="_blank">Transmission</a> to download torrents.',
|
|
||||||
'wizard': True,
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'enabled',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'enabler',
|
|
||||||
'radio_group': 'torrent',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'host',
|
|
||||||
'default': 'localhost:9091',
|
|
||||||
'description': 'Hostname with port. Usually <strong>localhost:9091</strong>',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'rpc_url',
|
|
||||||
'type': 'string',
|
|
||||||
'default': 'transmission',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Change if you don\'t run Transmission RPC at the default url.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'username',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'password',
|
|
||||||
'type': 'password',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'directory',
|
|
||||||
'type': 'directory',
|
|
||||||
'description': 'Download to this directory. Keep empty for default Transmission download directory.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'remove_complete',
|
|
||||||
'label': 'Remove torrent',
|
|
||||||
'default': True,
|
|
||||||
'advanced': True,
|
|
||||||
'type': 'bool',
|
|
||||||
'description': 'Remove the torrent from Transmission after it finished seeding.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'delete_files',
|
|
||||||
'label': 'Remove files',
|
|
||||||
'default': True,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Also remove the leftover files.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'paused',
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'default': False,
|
|
||||||
'description': 'Add the torrent paused.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'manual',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'stalled_as_failed',
|
|
||||||
'default': True,
|
|
||||||
'advanced': True,
|
|
||||||
'type': 'bool',
|
|
||||||
'description': 'Consider a stalled torrent as failed',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'delete_failed',
|
|
||||||
'default': True,
|
|
||||||
'advanced': True,
|
|
||||||
'type': 'bool',
|
|
||||||
'description': 'Delete a release after the download has failed.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
+97
-18
@@ -1,12 +1,6 @@
|
|||||||
from base64 import b16encode, b32decode
|
from base64 import b16encode, b32decode
|
||||||
from bencode import bencode as benc, bdecode
|
|
||||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
|
||||||
from couchpotato.core.helpers.encoding import isInt, ss, sp
|
|
||||||
from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from multipartpost import MultipartPostHandler
|
|
||||||
import cookielib
|
import cookielib
|
||||||
import httplib
|
import httplib
|
||||||
import json
|
import json
|
||||||
@@ -17,22 +11,32 @@ import time
|
|||||||
import urllib
|
import urllib
|
||||||
import urllib2
|
import urllib2
|
||||||
|
|
||||||
|
from bencode import bencode as benc, bdecode
|
||||||
|
from couchpotato.core._base.downloader.main import DownloaderBase, ReleaseDownloadList
|
||||||
|
from couchpotato.core.helpers.encoding import isInt, ss, sp
|
||||||
|
from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from multipartpost import MultipartPostHandler
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
autoload = 'uTorrent'
|
||||||
|
|
||||||
class uTorrent(Downloader):
|
|
||||||
|
class uTorrent(DownloaderBase):
|
||||||
|
|
||||||
protocol = ['torrent', 'torrent_magnet']
|
protocol = ['torrent', 'torrent_magnet']
|
||||||
utorrent_api = None
|
utorrent_api = None
|
||||||
status_flags = {
|
status_flags = {
|
||||||
'STARTED' : 1,
|
'STARTED': 1,
|
||||||
'CHECKING' : 2,
|
'CHECKING': 2,
|
||||||
'CHECK-START' : 4,
|
'CHECK-START': 4,
|
||||||
'CHECKED' : 8,
|
'CHECKED': 8,
|
||||||
'ERROR' : 16,
|
'ERROR': 16,
|
||||||
'PAUSED' : 32,
|
'PAUSED': 32,
|
||||||
'QUEUED' : 64,
|
'QUEUED': 64,
|
||||||
'LOADED' : 128
|
'LOADED': 128
|
||||||
}
|
}
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
@@ -164,7 +168,7 @@ class uTorrent(Downloader):
|
|||||||
status = 'busy'
|
status = 'busy'
|
||||||
if (torrent[1] & self.status_flags['STARTED'] or torrent[1] & self.status_flags['QUEUED']) and torrent[4] == 1000:
|
if (torrent[1] & self.status_flags['STARTED'] or torrent[1] & self.status_flags['QUEUED']) and torrent[4] == 1000:
|
||||||
status = 'seeding'
|
status = 'seeding'
|
||||||
elif (torrent[1] & self.status_flags['ERROR']):
|
elif torrent[1] & self.status_flags['ERROR']:
|
||||||
status = 'failed'
|
status = 'failed'
|
||||||
elif torrent[4] == 1000:
|
elif torrent[4] == 1000:
|
||||||
status = 'completed'
|
status = 'completed'
|
||||||
@@ -180,7 +184,7 @@ class uTorrent(Downloader):
|
|||||||
'original_status': torrent[1],
|
'original_status': torrent[1],
|
||||||
'timeleft': str(timedelta(seconds = torrent[10])),
|
'timeleft': str(timedelta(seconds = torrent[10])),
|
||||||
'folder': sp(torrent[26]),
|
'folder': sp(torrent[26]),
|
||||||
'files': '|'.join(torrent_files)
|
'files': torrent_files
|
||||||
})
|
})
|
||||||
|
|
||||||
return release_downloads
|
return release_downloads
|
||||||
@@ -225,7 +229,6 @@ class uTorrentAPI(object):
|
|||||||
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
||||||
password_manager.add_password(realm = None, uri = self.url, user = username, passwd = password)
|
password_manager.add_password(realm = None, uri = self.url, user = username, passwd = password)
|
||||||
self.opener.add_handler(urllib2.HTTPBasicAuthHandler(password_manager))
|
self.opener.add_handler(urllib2.HTTPBasicAuthHandler(password_manager))
|
||||||
self.opener.add_handler(urllib2.HTTPDigestAuthHandler(password_manager))
|
|
||||||
elif username or password:
|
elif username or password:
|
||||||
log.debug('User or password missing, not using authentication.')
|
log.debug('User or password missing, not using authentication.')
|
||||||
self.token = self.get_token()
|
self.token = self.get_token()
|
||||||
@@ -340,3 +343,79 @@ class uTorrentAPI(object):
|
|||||||
return False
|
return False
|
||||||
response = json.loads(data)
|
response = json.loads(data)
|
||||||
return int(response.get('build'))
|
return int(response.get('build'))
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'utorrent',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
|
'name': 'utorrent',
|
||||||
|
'label': 'uTorrent',
|
||||||
|
'description': 'Use <a href="http://www.utorrent.com/" target="_blank">uTorrent</a> (3.0+) to download torrents.',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
'radio_group': 'torrent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'host',
|
||||||
|
'default': 'localhost:8000',
|
||||||
|
'description': 'Port can be found in settings when enabling WebUI.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'label',
|
||||||
|
'description': 'Label to add torrent as.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'remove_complete',
|
||||||
|
'label': 'Remove torrent',
|
||||||
|
'default': True,
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Remove the torrent from uTorrent after it finished seeding.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'delete_files',
|
||||||
|
'label': 'Remove files',
|
||||||
|
'default': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Also remove the leftover files.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'paused',
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'default': False,
|
||||||
|
'description': 'Add the torrent paused.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'manual',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'delete_failed',
|
||||||
|
'default': True,
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Delete a release after the download has failed.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
from .main import uTorrent
|
|
||||||
|
|
||||||
|
|
||||||
def start():
|
|
||||||
return uTorrent()
|
|
||||||
|
|
||||||
config = [{
|
|
||||||
'name': 'utorrent',
|
|
||||||
'groups': [
|
|
||||||
{
|
|
||||||
'tab': 'downloaders',
|
|
||||||
'list': 'download_providers',
|
|
||||||
'name': 'utorrent',
|
|
||||||
'label': 'uTorrent',
|
|
||||||
'description': 'Use <a href="http://www.utorrent.com/" target="_blank">uTorrent</a> (3.0+) to download torrents.',
|
|
||||||
'wizard': True,
|
|
||||||
'options': [
|
|
||||||
{
|
|
||||||
'name': 'enabled',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'enabler',
|
|
||||||
'radio_group': 'torrent',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'host',
|
|
||||||
'default': 'localhost:8000',
|
|
||||||
'description': 'Port can be found in settings when enabling WebUI.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'username',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'password',
|
|
||||||
'type': 'password',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'label',
|
|
||||||
'description': 'Label to add torrent as.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'remove_complete',
|
|
||||||
'label': 'Remove torrent',
|
|
||||||
'default': True,
|
|
||||||
'advanced': True,
|
|
||||||
'type': 'bool',
|
|
||||||
'description': 'Remove the torrent from uTorrent after it finished seeding.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'delete_files',
|
|
||||||
'label': 'Remove files',
|
|
||||||
'default': True,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Also remove the leftover files.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'paused',
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'default': False,
|
|
||||||
'description': 'Add the torrent paused.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'manual',
|
|
||||||
'default': 0,
|
|
||||||
'type': 'bool',
|
|
||||||
'advanced': True,
|
|
||||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'delete_failed',
|
|
||||||
'default': True,
|
|
||||||
'advanced': True,
|
|
||||||
'type': 'bool',
|
|
||||||
'description': 'Delete a release after the download has failed.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}]
|
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import threading
|
||||||
|
import traceback
|
||||||
|
|
||||||
from axl.axel import Event
|
from axl.axel import Event
|
||||||
from couchpotato.core.helpers.variable import mergeDicts, natsortKey
|
from couchpotato.core.helpers.variable import mergeDicts, natsortKey
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
import threading
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
events = {}
|
events = {}
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from string import ascii_letters, digits
|
from string import ascii_letters, digits
|
||||||
from urllib import quote_plus
|
from urllib import quote_plus
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from couchpotato.core.helpers.encoding import toUnicode
|
|
||||||
from couchpotato.core.helpers.variable import natsortKey
|
|
||||||
from urllib import unquote
|
from urllib import unquote
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
|
from couchpotato.core.helpers.variable import natsortKey
|
||||||
|
|
||||||
|
|
||||||
def getParams(params):
|
def getParams(params):
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ def getParams(params):
|
|||||||
|
|
||||||
return dictToList(temp)
|
return dictToList(temp)
|
||||||
|
|
||||||
|
non_decimal = re.compile(r'[^\d.]+')
|
||||||
|
|
||||||
def dictToList(params):
|
def dictToList(params):
|
||||||
|
|
||||||
@@ -52,7 +54,15 @@ def dictToList(params):
|
|||||||
convert = lambda text: int(text) if text.isdigit() else text.lower()
|
convert = lambda text: int(text) if text.isdigit() else text.lower()
|
||||||
alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
|
alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
|
||||||
sorted_keys = sorted(value.keys(), key = alphanum_key)
|
sorted_keys = sorted(value.keys(), key = alphanum_key)
|
||||||
new_value = [dictToList(value[k]) for k in sorted_keys]
|
|
||||||
|
all_ints = 0
|
||||||
|
for pnr in sorted_keys:
|
||||||
|
all_ints += 1 if non_decimal.sub('', pnr) == pnr else 0
|
||||||
|
|
||||||
|
if all_ints == len(sorted_keys):
|
||||||
|
new_value = [dictToList(value[k]) for k in sorted_keys]
|
||||||
|
else:
|
||||||
|
new_value = value
|
||||||
except:
|
except:
|
||||||
new_value = value
|
new_value = value
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from couchpotato.core.logger import CPLog
|
|
||||||
import xml.etree.ElementTree as XMLTree
|
import xml.etree.ElementTree as XMLTree
|
||||||
|
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
import collections
|
import collections
|
||||||
|
import ctypes
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
@@ -8,9 +7,14 @@ import random
|
|||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
import sys
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss, sp
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
import six
|
import six
|
||||||
from six.moves import map, zip, filter
|
from six.moves import map, zip, filter
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -214,6 +218,7 @@ def tryFloat(s):
|
|||||||
return float(s)
|
return float(s)
|
||||||
except: return 0
|
except: return 0
|
||||||
|
|
||||||
|
|
||||||
def natsortKey(string_):
|
def natsortKey(string_):
|
||||||
"""See http://www.codinghorror.com/blog/archives/001018.html"""
|
"""See http://www.codinghorror.com/blog/archives/001018.html"""
|
||||||
return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string_)]
|
return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string_)]
|
||||||
@@ -225,26 +230,28 @@ def toIterable(value):
|
|||||||
return [value]
|
return [value]
|
||||||
|
|
||||||
|
|
||||||
def getTitle(library_dict):
|
def getIdentifier(media):
|
||||||
|
return media.get('identifier') or media.get('identifiers', {}).get('imdb')
|
||||||
|
|
||||||
|
|
||||||
|
def getTitle(media_dict):
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
return library_dict['titles'][0]['title']
|
return media_dict['title']
|
||||||
except:
|
except:
|
||||||
try:
|
try:
|
||||||
for title in library_dict.titles:
|
return media_dict['titles'][0]
|
||||||
if title.default:
|
|
||||||
return title.title
|
|
||||||
except:
|
except:
|
||||||
try:
|
try:
|
||||||
return library_dict['info']['titles'][0]
|
return media_dict['info']['titles'][0]
|
||||||
except:
|
except:
|
||||||
log.error('Could not get title for %s', library_dict.identifier)
|
try:
|
||||||
return None
|
return media_dict['media']['info']['titles'][0]
|
||||||
|
except:
|
||||||
log.error('Could not get title for %s', library_dict['identifier'])
|
log.error('Could not get title for %s', getIdentifier(media_dict))
|
||||||
return None
|
return None
|
||||||
except:
|
except:
|
||||||
log.error('Could not get title for library item: %s', library_dict)
|
log.error('Could not get title for library item: %s', media_dict)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -285,12 +292,20 @@ def dictIsSubset(a, b):
|
|||||||
return all([k in b and b[k] == v for k, v in a.items()])
|
return all([k in b and b[k] == v for k, v in a.items()])
|
||||||
|
|
||||||
|
|
||||||
|
# Returns True if sub_folder is the same as or inside base_folder
|
||||||
def isSubFolder(sub_folder, base_folder):
|
def isSubFolder(sub_folder, base_folder):
|
||||||
# Returns True if sub_folder is the same as or inside base_folder
|
if base_folder and sub_folder:
|
||||||
return base_folder and sub_folder and ss(os.path.normpath(base_folder).rstrip(os.path.sep) + os.path.sep) in ss(os.path.normpath(sub_folder).rstrip(os.path.sep) + os.path.sep)
|
base = sp(os.path.realpath(base_folder)) + os.path.sep
|
||||||
|
subfolder = sp(os.path.realpath(sub_folder)) + os.path.sep
|
||||||
|
return os.path.commonprefix([subfolder, base]) == base
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# From SABNZBD
|
# From SABNZBD
|
||||||
re_password = [re.compile(r'([^/\\]+)[/\\](.+)'), re.compile(r'(.+){{([^{}]+)}}$'), re.compile(r'(.+)\s+password\s*=\s*(.+)$', re.I)]
|
re_password = [re.compile(r'(.+){{([^{}]+)}}$'), re.compile(r'(.+)\s+password\s*=\s*(.+)$', re.I)]
|
||||||
|
|
||||||
|
|
||||||
def scanForPassword(name):
|
def scanForPassword(name):
|
||||||
m = None
|
m = None
|
||||||
for reg in re_password:
|
for reg in re_password:
|
||||||
@@ -299,3 +314,69 @@ def scanForPassword(name):
|
|||||||
|
|
||||||
if m:
|
if m:
|
||||||
return m.group(1).strip('. '), m.group(2).strip()
|
return m.group(1).strip('. '), m.group(2).strip()
|
||||||
|
|
||||||
|
|
||||||
|
under_pat = re.compile(r'_([a-z])')
|
||||||
|
|
||||||
|
def underscoreToCamel(name):
|
||||||
|
return under_pat.sub(lambda x: x.group(1).upper(), name)
|
||||||
|
|
||||||
|
|
||||||
|
def removePyc(folder, only_excess = True, show_logs = True):
|
||||||
|
|
||||||
|
folder = sp(folder)
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(folder):
|
||||||
|
|
||||||
|
pyc_files = filter(lambda filename: filename.endswith('.pyc'), files)
|
||||||
|
py_files = set(filter(lambda filename: filename.endswith('.py'), files))
|
||||||
|
excess_pyc_files = filter(lambda pyc_filename: pyc_filename[:-1] not in py_files, pyc_files) if only_excess else pyc_files
|
||||||
|
|
||||||
|
for excess_pyc_file in excess_pyc_files:
|
||||||
|
full_path = os.path.join(root, excess_pyc_file)
|
||||||
|
if show_logs: log.debug('Removing old PYC file: %s', full_path)
|
||||||
|
try:
|
||||||
|
os.remove(full_path)
|
||||||
|
except:
|
||||||
|
log.error('Couldn\'t remove %s: %s', (full_path, traceback.format_exc()))
|
||||||
|
|
||||||
|
for dir_name in dirs:
|
||||||
|
full_path = os.path.join(root, dir_name)
|
||||||
|
if len(os.listdir(full_path)) == 0:
|
||||||
|
try:
|
||||||
|
os.rmdir(full_path)
|
||||||
|
except:
|
||||||
|
log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
|
||||||
|
|
||||||
|
|
||||||
|
def getFreeSpace(directories):
|
||||||
|
|
||||||
|
single = not isinstance(directories, (tuple, list))
|
||||||
|
if single:
|
||||||
|
directories = [directories]
|
||||||
|
|
||||||
|
free_space = {}
|
||||||
|
for folder in directories:
|
||||||
|
|
||||||
|
size = None
|
||||||
|
if os.path.isdir(folder):
|
||||||
|
if os.name == 'nt':
|
||||||
|
_, total, free = ctypes.c_ulonglong(), ctypes.c_ulonglong(), \
|
||||||
|
ctypes.c_ulonglong()
|
||||||
|
if sys.version_info >= (3,) or isinstance(folder, unicode):
|
||||||
|
fun = ctypes.windll.kernel32.GetDiskFreeSpaceExW #@UndefinedVariable
|
||||||
|
else:
|
||||||
|
fun = ctypes.windll.kernel32.GetDiskFreeSpaceExA #@UndefinedVariable
|
||||||
|
ret = fun(folder, ctypes.byref(_), ctypes.byref(total), ctypes.byref(free))
|
||||||
|
if ret == 0:
|
||||||
|
raise ctypes.WinError()
|
||||||
|
return [total.value, free.value]
|
||||||
|
else:
|
||||||
|
s = os.statvfs(folder)
|
||||||
|
size = [s.f_blocks * s.f_frsize / (1024 * 1024), (s.f_bavail * s.f_frsize) / (1024 * 1024)]
|
||||||
|
|
||||||
|
if single: return size
|
||||||
|
|
||||||
|
free_space[folder] = size
|
||||||
|
|
||||||
|
return free_space
|
||||||
|
|||||||
+35
-20
@@ -1,31 +1,33 @@
|
|||||||
from couchpotato.core.event import fireEvent
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from importhelper import import_module
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
from couchpotato.core.event import fireEvent
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from importhelper import import_module
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Loader(object):
|
class Loader(object):
|
||||||
plugins = {}
|
|
||||||
providers = {}
|
def __init__(self):
|
||||||
modules = {}
|
self.plugins = {}
|
||||||
|
self.providers = {}
|
||||||
|
self.modules = {}
|
||||||
|
self.paths = {}
|
||||||
|
|
||||||
def preload(self, root = ''):
|
def preload(self, root = ''):
|
||||||
core = os.path.join(root, 'couchpotato', 'core')
|
core = os.path.join(root, 'couchpotato', 'core')
|
||||||
|
|
||||||
self.paths = {
|
self.paths.update({
|
||||||
'core': (0, 'couchpotato.core._base', os.path.join(core, '_base')),
|
'core': (0, 'couchpotato.core._base', os.path.join(core, '_base')),
|
||||||
'plugin': (1, 'couchpotato.core.plugins', os.path.join(core, 'plugins')),
|
'plugin': (1, 'couchpotato.core.plugins', os.path.join(core, 'plugins')),
|
||||||
'notifications': (20, 'couchpotato.core.notifications', os.path.join(core, 'notifications')),
|
'notifications': (20, 'couchpotato.core.notifications', os.path.join(core, 'notifications')),
|
||||||
'downloaders': (20, 'couchpotato.core.downloaders', os.path.join(core, 'downloaders')),
|
'downloaders': (20, 'couchpotato.core.downloaders', os.path.join(core, 'downloaders')),
|
||||||
}
|
})
|
||||||
|
|
||||||
# Add providers to loader
|
|
||||||
self.addPath(root, ['couchpotato', 'core', 'providers'], 25, recursive = False)
|
|
||||||
|
|
||||||
# Add media to loader
|
# Add media to loader
|
||||||
self.addPath(root, ['couchpotato', 'core', 'media'], 25, recursive = True)
|
self.addPath(root, ['couchpotato', 'core', 'media'], 25, recursive = True)
|
||||||
@@ -57,12 +59,10 @@ class Loader(object):
|
|||||||
if m is None:
|
if m is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
log.info('Loading %s: %s', (plugin['type'], plugin['name']))
|
|
||||||
|
|
||||||
# Save default settings for plugin/provider
|
# Save default settings for plugin/provider
|
||||||
did_save += self.loadSettings(m, module_name, save = False)
|
did_save += self.loadSettings(m, module_name, save = False)
|
||||||
|
|
||||||
self.loadPlugins(m, plugin.get('name'))
|
self.loadPlugins(m, plugin.get('type'), plugin.get('name'))
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
# todo:: subclass ImportError for missing requirements.
|
# todo:: subclass ImportError for missing requirements.
|
||||||
if e.message.lower().startswith("missing"):
|
if e.message.lower().startswith("missing"):
|
||||||
@@ -96,14 +96,19 @@ class Loader(object):
|
|||||||
self.addModule(priority, plugin_type, module, os.path.basename(dir_name))
|
self.addModule(priority, plugin_type, module, os.path.basename(dir_name))
|
||||||
|
|
||||||
for name in os.listdir(dir_name):
|
for name in os.listdir(dir_name):
|
||||||
if os.path.isdir(os.path.join(dir_name, name)) and name != 'static' and os.path.isfile(os.path.join(dir_name, name, '__init__.py')):
|
path = os.path.join(dir_name, name)
|
||||||
|
ext = os.path.splitext(path)[1]
|
||||||
|
ext_length = len(ext)
|
||||||
|
if name != 'static' and ((os.path.isdir(path) and os.path.isfile(os.path.join(path, '__init__.py')))
|
||||||
|
or (os.path.isfile(path) and ext == '.py')):
|
||||||
|
name = name[:-ext_length] if ext_length > 0 else name
|
||||||
module_name = '%s.%s' % (module, name)
|
module_name = '%s.%s' % (module, name)
|
||||||
self.addModule(priority, plugin_type, module_name, name)
|
self.addModule(priority, plugin_type, module_name, name)
|
||||||
|
|
||||||
def loadSettings(self, module, name, save = True):
|
def loadSettings(self, module, name, save = True):
|
||||||
|
|
||||||
if not hasattr(module, 'config'):
|
if not hasattr(module, 'config'):
|
||||||
log.debug('Skip loading settings for plugin %s as it has no config section' % module.__file__)
|
#log.debug('Skip loading settings for plugin %s as it has no config section' % module.__file__)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -119,13 +124,20 @@ class Loader(object):
|
|||||||
log.debug('Failed loading settings for "%s": %s', (name, traceback.format_exc()))
|
log.debug('Failed loading settings for "%s": %s', (name, traceback.format_exc()))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def loadPlugins(self, module, name):
|
def loadPlugins(self, module, type, name):
|
||||||
|
|
||||||
if not hasattr(module, 'start'):
|
if not hasattr(module, 'autoload'):
|
||||||
log.debug('Skip startup for plugin %s as it has no start section' % module.__file__)
|
#log.debug('Skip startup for plugin %s as it has no start section' % module.__file__)
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
module.start()
|
# Load single file plugin
|
||||||
|
if isinstance(module.autoload, (str, unicode)):
|
||||||
|
getattr(module, module.autoload)()
|
||||||
|
# Load folder plugin
|
||||||
|
else:
|
||||||
|
module.autoload()
|
||||||
|
|
||||||
|
log.info('Loaded %s: %s', (type, name))
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
log.error('Failed loading plugin "%s": %s', (module.__file__, traceback.format_exc()))
|
log.error('Failed loading plugin "%s": %s', (module.__file__, traceback.format_exc()))
|
||||||
@@ -137,6 +149,9 @@ class Loader(object):
|
|||||||
self.modules[priority] = {}
|
self.modules[priority] = {}
|
||||||
|
|
||||||
module = module.lstrip('.')
|
module = module.lstrip('.')
|
||||||
|
if plugin_type.startswith('couchpotato_core'):
|
||||||
|
plugin_type = plugin_type[17:]
|
||||||
|
|
||||||
self.modules[priority][module] = {
|
self.modules[priority][module] = {
|
||||||
'priority': priority,
|
'priority': priority,
|
||||||
'module': module,
|
'module': module,
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ class CPLog(object):
|
|||||||
context = ''
|
context = ''
|
||||||
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key', 'passkey']
|
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key', 'passkey']
|
||||||
|
|
||||||
|
Env = None
|
||||||
|
is_develop = False
|
||||||
|
|
||||||
def __init__(self, context = ''):
|
def __init__(self, context = ''):
|
||||||
if context.endswith('.main'):
|
if context.endswith('.main'):
|
||||||
context = context[:-5]
|
context = context[:-5]
|
||||||
@@ -14,6 +17,20 @@ class CPLog(object):
|
|||||||
self.context = context
|
self.context = context
|
||||||
self.logger = logging.getLogger()
|
self.logger = logging.getLogger()
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
|
||||||
|
if not self.Env:
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
|
||||||
|
self.Env = Env
|
||||||
|
self.is_develop = Env.get('dev')
|
||||||
|
|
||||||
|
from couchpotato.core.event import addEvent
|
||||||
|
addEvent('app.after_shutdown', self.close)
|
||||||
|
|
||||||
|
def close(self, *args, **kwargs):
|
||||||
|
logging.shutdown()
|
||||||
|
|
||||||
def info(self, msg, replace_tuple = ()):
|
def info(self, msg, replace_tuple = ()):
|
||||||
self.logger.info(self.addContext(msg, replace_tuple))
|
self.logger.info(self.addContext(msg, replace_tuple))
|
||||||
|
|
||||||
@@ -37,7 +54,6 @@ class CPLog(object):
|
|||||||
|
|
||||||
def safeMessage(self, msg, replace_tuple = ()):
|
def safeMessage(self, msg, replace_tuple = ()):
|
||||||
|
|
||||||
from couchpotato.environment import Env
|
|
||||||
from couchpotato.core.helpers.encoding import ss, toUnicode
|
from couchpotato.core.helpers.encoding import ss, toUnicode
|
||||||
|
|
||||||
msg = ss(msg)
|
msg = ss(msg)
|
||||||
@@ -53,7 +69,8 @@ class CPLog(object):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error('Failed encoding stuff to log "%s": %s' % (msg, e))
|
self.logger.error('Failed encoding stuff to log "%s": %s' % (msg, e))
|
||||||
|
|
||||||
if not Env.get('dev'):
|
self.setup()
|
||||||
|
if not self.is_develop:
|
||||||
|
|
||||||
for replace in self.replace_private:
|
for replace in self.replace_private:
|
||||||
msg = re.sub('(\?%s=)[^\&]+' % replace, '?%s=xxx' % replace, msg)
|
msg = re.sub('(\?%s=)[^\&]+' % replace, '?%s=xxx' % replace, msg)
|
||||||
@@ -61,7 +78,7 @@ class CPLog(object):
|
|||||||
|
|
||||||
# Replace api key
|
# Replace api key
|
||||||
try:
|
try:
|
||||||
api_key = Env.setting('api_key')
|
api_key = self.Env.setting('api_key')
|
||||||
if api_key:
|
if api_key:
|
||||||
msg = msg.replace(api_key, 'API_KEY')
|
msg = msg.replace(api_key, 'API_KEY')
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
from couchpotato import get_session, CPLog
|
|
||||||
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
from couchpotato import CPLog
|
||||||
|
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||||
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Media
|
import six
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -11,35 +15,22 @@ class MediaBase(Plugin):
|
|||||||
|
|
||||||
_type = None
|
_type = None
|
||||||
|
|
||||||
default_dict = {
|
|
||||||
'profile': {'types': {'quality': {}}},
|
|
||||||
'releases': {'status': {}, 'quality': {}, 'files': {}, 'info': {}},
|
|
||||||
'library': {'titles': {}, 'files': {}},
|
|
||||||
'files': {},
|
|
||||||
'status': {},
|
|
||||||
'category': {},
|
|
||||||
}
|
|
||||||
|
|
||||||
def initType(self):
|
def initType(self):
|
||||||
addEvent('media.types', self.getType)
|
addEvent('media.types', self.getType)
|
||||||
|
|
||||||
def getType(self):
|
def getType(self):
|
||||||
return self._type
|
return self._type
|
||||||
|
|
||||||
def createOnComplete(self, id):
|
def createOnComplete(self, media_id):
|
||||||
|
|
||||||
def onComplete():
|
def onComplete():
|
||||||
try:
|
try:
|
||||||
db = get_session()
|
media = fireEvent('media.get', media_id, single = True)
|
||||||
media = db.query(Media).filter_by(id = id).first()
|
event_name = '%s.searcher.single' % media.get('type')
|
||||||
media_dict = media.to_dict(self.default_dict)
|
|
||||||
event_name = '%s.searcher.single' % media.type
|
|
||||||
|
|
||||||
fireEvent(event_name, media_dict, on_complete = self.createNotifyFront(id))
|
fireEventAsync(event_name, media, on_complete = self.createNotifyFront(media_id), manual = True)
|
||||||
except:
|
except:
|
||||||
log.error('Failed creating onComplete: %s', traceback.format_exc())
|
log.error('Failed creating onComplete: %s', traceback.format_exc())
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
return onComplete
|
return onComplete
|
||||||
|
|
||||||
@@ -47,15 +38,61 @@ class MediaBase(Plugin):
|
|||||||
|
|
||||||
def notifyFront():
|
def notifyFront():
|
||||||
try:
|
try:
|
||||||
db = get_session()
|
media = fireEvent('media.get', media_id, single = True)
|
||||||
media = db.query(Media).filter_by(id = media_id).first()
|
event_name = '%s.update' % media.get('type')
|
||||||
media_dict = media.to_dict(self.default_dict)
|
|
||||||
event_name = '%s.update' % media.type
|
|
||||||
|
|
||||||
fireEvent('notify.frontend', type = event_name, data = media_dict)
|
fireEvent('notify.frontend', type = event_name, data = media)
|
||||||
except:
|
except:
|
||||||
log.error('Failed creating onComplete: %s', traceback.format_exc())
|
log.error('Failed creating onComplete: %s', traceback.format_exc())
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
return notifyFront
|
return notifyFront
|
||||||
|
|
||||||
|
def getDefaultTitle(self, info, ):
|
||||||
|
|
||||||
|
# Set default title
|
||||||
|
default_title = toUnicode(info.get('title'))
|
||||||
|
titles = info.get('titles', [])
|
||||||
|
counter = 0
|
||||||
|
def_title = None
|
||||||
|
for title in titles:
|
||||||
|
if (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == six.u('') and toUnicode(titles[0]) == title):
|
||||||
|
def_title = toUnicode(title)
|
||||||
|
break
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
if not def_title:
|
||||||
|
def_title = toUnicode(titles[0])
|
||||||
|
|
||||||
|
return def_title or 'UNKNOWN'
|
||||||
|
|
||||||
|
def getPoster(self, image_urls, existing_files):
|
||||||
|
image_type = 'poster'
|
||||||
|
|
||||||
|
# Remove non-existing files
|
||||||
|
file_type = 'image_%s' % image_type
|
||||||
|
|
||||||
|
# Make existing unique
|
||||||
|
unique_files = list(set(existing_files.get(file_type, [])))
|
||||||
|
|
||||||
|
# Remove files that can't be found
|
||||||
|
for ef in unique_files:
|
||||||
|
if not os.path.isfile(ef):
|
||||||
|
unique_files.remove(ef)
|
||||||
|
|
||||||
|
# Replace new files list
|
||||||
|
existing_files[file_type] = unique_files
|
||||||
|
if len(existing_files) == 0:
|
||||||
|
del existing_files[file_type]
|
||||||
|
|
||||||
|
# Loop over type
|
||||||
|
for image in image_urls.get(image_type, []):
|
||||||
|
if not isinstance(image, (str, unicode)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if file_type not in existing_files or len(existing_files.get(file_type, [])) == 0:
|
||||||
|
file_path = fireEvent('file.download', url = image, single = True)
|
||||||
|
if file_path:
|
||||||
|
existing_files[file_type] = [file_path]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
from couchpotato.core.event import addEvent
|
from .main import Library
|
||||||
from couchpotato.core.plugins.base import Plugin
|
|
||||||
|
|
||||||
|
|
||||||
class LibraryBase(Plugin):
|
def autoload():
|
||||||
|
return Library()
|
||||||
|
|
||||||
_type = None
|
config = []
|
||||||
|
|
||||||
def initType(self):
|
|
||||||
addEvent('library.types', self.getType)
|
|
||||||
|
|
||||||
def getType(self):
|
|
||||||
return self._type
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from couchpotato.core.event import addEvent
|
||||||
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryBase(Plugin):
|
||||||
|
|
||||||
|
_type = None
|
||||||
|
|
||||||
|
def initType(self):
|
||||||
|
addEvent('library.types', self.getType)
|
||||||
|
|
||||||
|
def getType(self):
|
||||||
|
return self._type
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
|
from couchpotato.core.media._base.library.base import LibraryBase
|
||||||
|
|
||||||
|
|
||||||
|
class Library(LibraryBase):
|
||||||
|
def __init__(self):
|
||||||
|
addEvent('library.title', self.title)
|
||||||
|
|
||||||
|
def title(self, library):
|
||||||
|
return fireEvent(
|
||||||
|
'library.query',
|
||||||
|
library,
|
||||||
|
|
||||||
|
condense = False,
|
||||||
|
include_year = False,
|
||||||
|
include_identifier = False,
|
||||||
|
single = True
|
||||||
|
)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
from .main import Matcher
|
||||||
|
|
||||||
|
|
||||||
|
def autoload():
|
||||||
|
return Matcher()
|
||||||
|
|
||||||
|
config = []
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
from couchpotato.core.event import addEvent
|
||||||
|
from couchpotato.core.helpers.encoding import simplifyString
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MatcherBase(Plugin):
|
||||||
|
type = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
if self.type:
|
||||||
|
addEvent('%s.matcher.correct' % self.type, self.correct)
|
||||||
|
|
||||||
|
def correct(self, chain, release, media, quality):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def flattenInfo(self, info):
|
||||||
|
# Flatten dictionary of matches (chain info)
|
||||||
|
if isinstance(info, dict):
|
||||||
|
return dict([(key, self.flattenInfo(value)) for key, value in info.items()])
|
||||||
|
|
||||||
|
# Flatten matches
|
||||||
|
result = None
|
||||||
|
|
||||||
|
for match in info:
|
||||||
|
if isinstance(match, dict):
|
||||||
|
if result is None:
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
for key, value in match.items():
|
||||||
|
if key not in result:
|
||||||
|
result[key] = []
|
||||||
|
|
||||||
|
result[key].append(value)
|
||||||
|
else:
|
||||||
|
if result is None:
|
||||||
|
result = []
|
||||||
|
|
||||||
|
result.append(match)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def constructFromRaw(self, match):
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
|
||||||
|
parts = [
|
||||||
|
''.join([
|
||||||
|
y for y in x[1:] if y
|
||||||
|
]) for x in match
|
||||||
|
]
|
||||||
|
|
||||||
|
return ''.join(parts)[:-1].strip()
|
||||||
|
|
||||||
|
def simplifyValue(self, value):
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
|
||||||
|
if isinstance(value, basestring):
|
||||||
|
return simplifyString(value)
|
||||||
|
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [self.simplifyValue(x) for x in value]
|
||||||
|
|
||||||
|
raise ValueError("Unsupported value type")
|
||||||
|
|
||||||
|
def chainMatch(self, chain, group, tags):
|
||||||
|
info = self.flattenInfo(chain.info[group])
|
||||||
|
|
||||||
|
found_tags = []
|
||||||
|
for tag, accepted in tags.items():
|
||||||
|
values = [self.simplifyValue(x) for x in info.get(tag, [None])]
|
||||||
|
|
||||||
|
if any([val in accepted for val in values]):
|
||||||
|
found_tags.append(tag)
|
||||||
|
|
||||||
|
log.debug('tags found: %s, required: %s' % (found_tags, tags.keys()))
|
||||||
|
|
||||||
|
if set(tags.keys()) == set(found_tags):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return all([key in found_tags for key, value in tags.items()])
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
|
from couchpotato.core.helpers.variable import possibleTitles
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.matcher.base import MatcherBase
|
||||||
|
from caper import Caper
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Matcher(MatcherBase):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(Matcher, self).__init__()
|
||||||
|
|
||||||
|
self.caper = Caper()
|
||||||
|
|
||||||
|
addEvent('matcher.parse', self.parse)
|
||||||
|
addEvent('matcher.match', self.match)
|
||||||
|
|
||||||
|
addEvent('matcher.flatten_info', self.flattenInfo)
|
||||||
|
addEvent('matcher.construct_from_raw', self.constructFromRaw)
|
||||||
|
|
||||||
|
addEvent('matcher.correct_title', self.correctTitle)
|
||||||
|
addEvent('matcher.correct_quality', self.correctQuality)
|
||||||
|
|
||||||
|
def parse(self, name, parser='scene'):
|
||||||
|
return self.caper.parse(name, parser)
|
||||||
|
|
||||||
|
def match(self, release, media, quality):
|
||||||
|
match = fireEvent('matcher.parse', release['name'], single = True)
|
||||||
|
|
||||||
|
if len(match.chains) < 1:
|
||||||
|
log.info2('Wrong: %s, unable to parse release name (no chains)', release['name'])
|
||||||
|
return False
|
||||||
|
|
||||||
|
for chain in match.chains:
|
||||||
|
if fireEvent('%s.matcher.correct' % media['type'], chain, release, media, quality, single = True):
|
||||||
|
return chain
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def correctTitle(self, chain, media):
|
||||||
|
root_library = media['library']['root_library']
|
||||||
|
|
||||||
|
if 'show_name' not in chain.info or not len(chain.info['show_name']):
|
||||||
|
log.info('Wrong: missing show name in parsed result')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get the lower-case parsed show name from the chain
|
||||||
|
chain_words = [x.lower() for x in chain.info['show_name']]
|
||||||
|
|
||||||
|
# Build a list of possible titles of the media we are searching for
|
||||||
|
titles = root_library['info']['titles']
|
||||||
|
|
||||||
|
# Add year suffix titles (will result in ['<name_one>', '<name_one> <suffix_one>', '<name_two>', ...])
|
||||||
|
suffixes = [None, root_library['info']['year']]
|
||||||
|
|
||||||
|
titles = [
|
||||||
|
title + ((' %s' % suffix) if suffix else '')
|
||||||
|
for title in titles
|
||||||
|
for suffix in suffixes
|
||||||
|
]
|
||||||
|
|
||||||
|
# Check show titles match
|
||||||
|
# TODO check xem names
|
||||||
|
for title in titles:
|
||||||
|
for valid_words in [x.split(' ') for x in possibleTitles(title)]:
|
||||||
|
|
||||||
|
if valid_words == chain_words:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def correctQuality(self, chain, quality, quality_map):
|
||||||
|
if quality['identifier'] not in quality_map:
|
||||||
|
log.info2('Wrong: unknown preferred quality %s', quality['identifier'])
|
||||||
|
return False
|
||||||
|
|
||||||
|
if 'video' not in chain.info:
|
||||||
|
log.info2('Wrong: no video tags found')
|
||||||
|
return False
|
||||||
|
|
||||||
|
video_tags = quality_map[quality['identifier']]
|
||||||
|
|
||||||
|
if not self.chainMatch(chain, 'video', video_tags):
|
||||||
|
log.info2('Wrong: %s tags not in chain', video_tags)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
from .main import MediaPlugin
|
from .main import MediaPlugin
|
||||||
|
|
||||||
|
|
||||||
def start():
|
def autoload():
|
||||||
return MediaPlugin()
|
return MediaPlugin()
|
||||||
|
|
||||||
config = []
|
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
from string import ascii_letters
|
||||||
|
from hashlib import md5
|
||||||
|
|
||||||
|
from CodernityDB.tree_index import MultiTreeBasedIndex, TreeBasedIndex
|
||||||
|
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||||
|
|
||||||
|
|
||||||
|
class MediaIndex(MultiTreeBasedIndex):
|
||||||
|
_version = 3
|
||||||
|
|
||||||
|
custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs['key_format'] = '32s'
|
||||||
|
super(MediaIndex, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def make_key(self, key):
|
||||||
|
return md5(key).hexdigest()
|
||||||
|
|
||||||
|
def make_key_value(self, data):
|
||||||
|
if data.get('_t') == 'media' and (data.get('identifier') or data.get('identifiers')):
|
||||||
|
|
||||||
|
identifiers = data.get('identifiers', {})
|
||||||
|
if data.get('identifier') and 'imdb' not in identifiers:
|
||||||
|
identifiers['imdb'] = data.get('identifier')
|
||||||
|
|
||||||
|
ids = []
|
||||||
|
for x in identifiers:
|
||||||
|
ids.append(md5('%s-%s' % (x, identifiers[x])).hexdigest())
|
||||||
|
|
||||||
|
return ids, None
|
||||||
|
|
||||||
|
|
||||||
|
class MediaStatusIndex(TreeBasedIndex):
|
||||||
|
_version = 1
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs['key_format'] = '32s'
|
||||||
|
super(MediaStatusIndex, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def make_key(self, key):
|
||||||
|
return md5(key).hexdigest()
|
||||||
|
|
||||||
|
def make_key_value(self, data):
|
||||||
|
if data.get('_t') == 'media' and data.get('status'):
|
||||||
|
return md5(data.get('status')).hexdigest(), None
|
||||||
|
|
||||||
|
|
||||||
|
class MediaTypeIndex(TreeBasedIndex):
|
||||||
|
_version = 1
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs['key_format'] = '32s'
|
||||||
|
super(MediaTypeIndex, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def make_key(self, key):
|
||||||
|
return md5(key).hexdigest()
|
||||||
|
|
||||||
|
def make_key_value(self, data):
|
||||||
|
if data.get('_t') == 'media' and data.get('type'):
|
||||||
|
return md5(data.get('type')).hexdigest(), None
|
||||||
|
|
||||||
|
|
||||||
|
class TitleSearchIndex(MultiTreeBasedIndex):
|
||||||
|
_version = 1
|
||||||
|
|
||||||
|
custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex
|
||||||
|
from itertools import izip
|
||||||
|
from couchpotato.core.helpers.encoding import simplifyString"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs['key_format'] = '32s'
|
||||||
|
super(TitleSearchIndex, self).__init__(*args, **kwargs)
|
||||||
|
self.__l = kwargs.get('w_len', 2)
|
||||||
|
|
||||||
|
def make_key_value(self, data):
|
||||||
|
|
||||||
|
if data.get('_t') == 'media' and len(data.get('title', '')) > 0:
|
||||||
|
|
||||||
|
out = set()
|
||||||
|
title = str(simplifyString(data.get('title').lower()))
|
||||||
|
l = self.__l
|
||||||
|
title_split = title.split()
|
||||||
|
|
||||||
|
for x in range(len(title_split)):
|
||||||
|
combo = ' '.join(title_split[x:])[:32].strip()
|
||||||
|
out.add(combo.rjust(32, '_'))
|
||||||
|
combo_range = max(l, min(len(combo), 32))
|
||||||
|
|
||||||
|
for cx in range(1, combo_range):
|
||||||
|
ccombo = combo[:-cx].strip()
|
||||||
|
if len(ccombo) > l:
|
||||||
|
out.add(ccombo.rjust(32, '_'))
|
||||||
|
|
||||||
|
return out, None
|
||||||
|
|
||||||
|
def make_key(self, key):
|
||||||
|
return key.rjust(32, '_').lower()
|
||||||
|
|
||||||
|
|
||||||
|
class TitleIndex(TreeBasedIndex):
|
||||||
|
_version = 4
|
||||||
|
|
||||||
|
custom_header = """from CodernityDB.tree_index import TreeBasedIndex
|
||||||
|
from string import ascii_letters
|
||||||
|
from couchpotato.core.helpers.encoding import toUnicode, simplifyString"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs['key_format'] = '32s'
|
||||||
|
super(TitleIndex, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def make_key(self, key):
|
||||||
|
return self.simplify(key)
|
||||||
|
|
||||||
|
def make_key_value(self, data):
|
||||||
|
if data.get('_t') == 'media' and data.get('title') is not None and len(data.get('title')) > 0:
|
||||||
|
return self.simplify(data['title']), None
|
||||||
|
|
||||||
|
def simplify(self, title):
|
||||||
|
|
||||||
|
title = toUnicode(title)
|
||||||
|
|
||||||
|
nr_prefix = '' if title and len(title) > 0 and title[0] in ascii_letters else '#'
|
||||||
|
title = simplifyString(title)
|
||||||
|
|
||||||
|
for prefix in ['the ', 'an ', 'a ']:
|
||||||
|
if prefix == title[:len(prefix)]:
|
||||||
|
title = title[len(prefix):]
|
||||||
|
break
|
||||||
|
|
||||||
|
return str(nr_prefix + title).ljust(32, ' ')[:32]
|
||||||
|
|
||||||
|
|
||||||
|
class StartsWithIndex(TreeBasedIndex):
|
||||||
|
_version = 3
|
||||||
|
|
||||||
|
custom_header = """from CodernityDB.tree_index import TreeBasedIndex
|
||||||
|
from string import ascii_letters
|
||||||
|
from couchpotato.core.helpers.encoding import toUnicode, simplifyString"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs['key_format'] = '1s'
|
||||||
|
super(StartsWithIndex, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def make_key(self, key):
|
||||||
|
return self.first(key)
|
||||||
|
|
||||||
|
def make_key_value(self, data):
|
||||||
|
if data.get('_t') == 'media' and data.get('title') is not None:
|
||||||
|
return self.first(data['title']), None
|
||||||
|
|
||||||
|
def first(self, title):
|
||||||
|
title = toUnicode(title)
|
||||||
|
title = simplifyString(title)
|
||||||
|
|
||||||
|
for prefix in ['the ', 'an ', 'a ']:
|
||||||
|
if prefix == title[:len(prefix)]:
|
||||||
|
title = title[len(prefix):]
|
||||||
|
break
|
||||||
|
|
||||||
|
return str(title[0] if title and len(title) > 0 and title[0] in ascii_letters else '#').lower()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class MediaChildrenIndex(TreeBasedIndex):
|
||||||
|
_version = 1
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs['key_format'] = '32s'
|
||||||
|
super(MediaChildrenIndex, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def make_key(self, key):
|
||||||
|
return key
|
||||||
|
|
||||||
|
def make_key_value(self, data):
|
||||||
|
if data.get('_t') == 'media' and data.get('parent_id'):
|
||||||
|
return data.get('parent_id'), None
|
||||||
|
|
||||||
|
|
||||||
|
class MediaTagIndex(MultiTreeBasedIndex):
|
||||||
|
_version = 2
|
||||||
|
|
||||||
|
custom_header = """from CodernityDB.tree_index import MultiTreeBasedIndex"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs['key_format'] = '32s'
|
||||||
|
super(MediaTagIndex, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def make_key_value(self, data):
|
||||||
|
if data.get('_t') == 'media' and data.get('tags') and len(data.get('tags', [])) > 0:
|
||||||
|
|
||||||
|
tags = set()
|
||||||
|
for tag in data.get('tags', []):
|
||||||
|
tags.add(self.make_key(tag))
|
||||||
|
|
||||||
|
return list(tags), None
|
||||||
|
|
||||||
|
def make_key(self, key):
|
||||||
|
return md5(key).hexdigest()
|
||||||
@@ -1,22 +1,36 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
from operator import itemgetter
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from couchpotato import get_session, tryInt
|
from string import ascii_lowercase
|
||||||
|
|
||||||
|
from CodernityDB.database import RecordNotFound
|
||||||
|
from couchpotato import tryInt, get_db
|
||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.helpers.variable import mergeDicts, splitString, getImdb, getTitle
|
from couchpotato.core.helpers.variable import splitString, getImdb, getTitle
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.media import MediaBase
|
from couchpotato.core.media import MediaBase
|
||||||
from couchpotato.core.settings.model import Library, LibraryTitle, Release, \
|
from .index import MediaIndex, MediaStatusIndex, MediaTypeIndex, TitleSearchIndex, TitleIndex, StartsWithIndex, MediaChildrenIndex, MediaTagIndex
|
||||||
Media
|
|
||||||
from sqlalchemy.orm import joinedload_all
|
|
||||||
from sqlalchemy.sql.expression import or_, asc, not_, desc
|
|
||||||
from string import ascii_lowercase
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MediaPlugin(MediaBase):
|
class MediaPlugin(MediaBase):
|
||||||
|
|
||||||
|
_database = {
|
||||||
|
'media': MediaIndex,
|
||||||
|
'media_search_title': TitleSearchIndex,
|
||||||
|
'media_status': MediaStatusIndex,
|
||||||
|
'media_tag': MediaTagIndex,
|
||||||
|
'media_by_type': MediaTypeIndex,
|
||||||
|
'media_title': TitleIndex,
|
||||||
|
'media_startswith': StartsWithIndex,
|
||||||
|
'media_children': MediaChildrenIndex,
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
addApiView('media.refresh', self.refresh, docs = {
|
addApiView('media.refresh', self.refresh, docs = {
|
||||||
@@ -60,15 +74,19 @@ class MediaPlugin(MediaBase):
|
|||||||
|
|
||||||
addApiView('media.available_chars', self.charView)
|
addApiView('media.available_chars', self.charView)
|
||||||
|
|
||||||
addEvent('app.load', self.addSingleRefreshView)
|
addEvent('app.load', self.addSingleRefreshView, priority = 100)
|
||||||
addEvent('app.load', self.addSingleListView)
|
addEvent('app.load', self.addSingleListView, priority = 100)
|
||||||
addEvent('app.load', self.addSingleCharView)
|
addEvent('app.load', self.addSingleCharView, priority = 100)
|
||||||
addEvent('app.load', self.addSingleDeleteView)
|
addEvent('app.load', self.addSingleDeleteView, priority = 100)
|
||||||
|
|
||||||
addEvent('media.get', self.get)
|
addEvent('media.get', self.get)
|
||||||
|
addEvent('media.with_status', self.withStatus)
|
||||||
|
addEvent('media.with_identifiers', self.withIdentifiers)
|
||||||
addEvent('media.list', self.list)
|
addEvent('media.list', self.list)
|
||||||
addEvent('media.delete', self.delete)
|
addEvent('media.delete', self.delete)
|
||||||
addEvent('media.restatus', self.restatus)
|
addEvent('media.restatus', self.restatus)
|
||||||
|
addEvent('media.tag', self.tag)
|
||||||
|
addEvent('media.untag', self.unTag)
|
||||||
|
|
||||||
def refresh(self, id = '', **kwargs):
|
def refresh(self, id = '', **kwargs):
|
||||||
handlers = []
|
handlers = []
|
||||||
@@ -80,30 +98,27 @@ class MediaPlugin(MediaBase):
|
|||||||
if refresh_handler:
|
if refresh_handler:
|
||||||
handlers.append(refresh_handler)
|
handlers.append(refresh_handler)
|
||||||
|
|
||||||
fireEvent('notify.frontend', type = 'media.busy', data = {'id': [tryInt(x) for x in ids]})
|
fireEvent('notify.frontend', type = 'media.busy', data = {'_id': ids})
|
||||||
fireEventAsync('schedule.queue', handlers = handlers)
|
fireEventAsync('schedule.queue', handlers = handlers)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
}
|
}
|
||||||
|
|
||||||
def createRefreshHandler(self, id):
|
def createRefreshHandler(self, media_id):
|
||||||
db = get_session()
|
|
||||||
|
|
||||||
media = db.query(Media).filter_by(id = id).first()
|
try:
|
||||||
|
media = get_db().get('id', media_id)
|
||||||
if media:
|
event = '%s.update_info' % media.get('type')
|
||||||
|
|
||||||
default_title = getTitle(media.library)
|
|
||||||
identifier = media.library.identifier
|
|
||||||
event = 'library.update.%s' % media.type
|
|
||||||
|
|
||||||
def handler():
|
def handler():
|
||||||
fireEvent(event, identifier = identifier, default_title = default_title, on_complete = self.createOnComplete(id))
|
fireEvent(event, media_id = media_id, on_complete = self.createOnComplete(media_id))
|
||||||
|
|
||||||
if handler:
|
|
||||||
return handler
|
return handler
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Refresh handler for non existing media: %s', traceback.format_exc())
|
||||||
|
|
||||||
def addSingleRefreshView(self):
|
def addSingleRefreshView(self):
|
||||||
|
|
||||||
for media_type in fireEvent('media.types', merge = True):
|
for media_type in fireEvent('media.types', merge = True):
|
||||||
@@ -111,20 +126,30 @@ class MediaPlugin(MediaBase):
|
|||||||
|
|
||||||
def get(self, media_id):
|
def get(self, media_id):
|
||||||
|
|
||||||
db = get_session()
|
try:
|
||||||
|
db = get_db()
|
||||||
|
|
||||||
imdb_id = getImdb(str(media_id))
|
imdb_id = getImdb(str(media_id))
|
||||||
|
|
||||||
if imdb_id:
|
if imdb_id:
|
||||||
m = db.query(Media).filter(Media.library.has(identifier = imdb_id)).first()
|
media = db.get('media', 'imdb-%s' % imdb_id, with_doc = True)['doc']
|
||||||
else:
|
else:
|
||||||
m = db.query(Media).filter_by(id = media_id).first()
|
media = db.get('id', media_id)
|
||||||
|
|
||||||
results = None
|
if media:
|
||||||
if m:
|
|
||||||
results = m.to_dict(self.default_dict)
|
|
||||||
|
|
||||||
return results
|
# Attach category
|
||||||
|
try: media['category'] = db.get('id', media.get('category_id'))
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
media['releases'] = fireEvent('release.for_media', media['_id'], single = True)
|
||||||
|
|
||||||
|
return media
|
||||||
|
|
||||||
|
except RecordNotFound:
|
||||||
|
log.error('Media with id "%s" not found', media_id)
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
|
||||||
def getView(self, id = None, **kwargs):
|
def getView(self, id = None, **kwargs):
|
||||||
|
|
||||||
@@ -135,9 +160,39 @@ class MediaPlugin(MediaBase):
|
|||||||
'media': media,
|
'media': media,
|
||||||
}
|
}
|
||||||
|
|
||||||
def list(self, types = None, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
|
def withStatus(self, status, with_doc = True):
|
||||||
|
|
||||||
db = get_session()
|
db = get_db()
|
||||||
|
|
||||||
|
status = list(status if isinstance(status, (list, tuple)) else [status])
|
||||||
|
|
||||||
|
for s in status:
|
||||||
|
for ms in db.get_many('media_status', s):
|
||||||
|
if with_doc:
|
||||||
|
try:
|
||||||
|
doc = db.get('id', ms['_id'])
|
||||||
|
yield doc
|
||||||
|
except RecordNotFound:
|
||||||
|
log.debug('Record not found, skipping: %s', ms['_id'])
|
||||||
|
else:
|
||||||
|
yield ms
|
||||||
|
|
||||||
|
def withIdentifiers(self, identifiers, with_doc = False):
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
|
||||||
|
for x in identifiers:
|
||||||
|
try:
|
||||||
|
media = db.get('media', '%s-%s' % (x, identifiers[x]), with_doc = with_doc)
|
||||||
|
return media
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
log.debug('No media found with identifiers: %s', identifiers)
|
||||||
|
|
||||||
|
def list(self, types = None, status = None, release_status = None, status_or = False, limit_offset = None, with_tags = None, starts_with = None, search = None):
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
|
||||||
# Make a list from string
|
# Make a list from string
|
||||||
if status and not isinstance(status, (list, tuple)):
|
if status and not isinstance(status, (list, tuple)):
|
||||||
@@ -146,139 +201,100 @@ class MediaPlugin(MediaBase):
|
|||||||
release_status = [release_status]
|
release_status = [release_status]
|
||||||
if types and not isinstance(types, (list, tuple)):
|
if types and not isinstance(types, (list, tuple)):
|
||||||
types = [types]
|
types = [types]
|
||||||
|
if with_tags and not isinstance(with_tags, (list, tuple)):
|
||||||
|
with_tags = [with_tags]
|
||||||
|
|
||||||
# query movie ids
|
# query media ids
|
||||||
q = db.query(Media) \
|
if types:
|
||||||
.with_entities(Media.id) \
|
all_media_ids = set()
|
||||||
.group_by(Media.id)
|
for media_type in types:
|
||||||
|
all_media_ids = all_media_ids.union(set([x['_id'] for x in db.get_many('media_by_type', media_type)]))
|
||||||
|
else:
|
||||||
|
all_media_ids = set([x['_id'] for x in db.all('media')])
|
||||||
|
|
||||||
|
media_ids = list(all_media_ids)
|
||||||
|
filter_by = {}
|
||||||
|
|
||||||
# Filter on movie status
|
# Filter on movie status
|
||||||
if status and len(status) > 0:
|
if status and len(status) > 0:
|
||||||
statuses = fireEvent('status.get', status, single = len(status) > 1)
|
filter_by['media_status'] = set()
|
||||||
statuses = [s.get('id') for s in statuses]
|
for media_status in fireEvent('media.with_status', status, with_doc = False, single = True):
|
||||||
|
filter_by['media_status'].add(media_status.get('_id'))
|
||||||
q = q.filter(Media.status_id.in_(statuses))
|
|
||||||
|
|
||||||
# Filter on release status
|
# Filter on release status
|
||||||
if release_status and len(release_status) > 0:
|
if release_status and len(release_status) > 0:
|
||||||
q = q.join(Media.releases)
|
filter_by['release_status'] = set()
|
||||||
|
for release_status in fireEvent('release.with_status', release_status, with_doc = False, single = True):
|
||||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
filter_by['release_status'].add(release_status.get('media_id'))
|
||||||
statuses = [s.get('id') for s in statuses]
|
|
||||||
|
|
||||||
q = q.filter(Release.status_id.in_(statuses))
|
|
||||||
|
|
||||||
# Filter on type
|
|
||||||
if types and len(types) > 0:
|
|
||||||
try: q = q.filter(Media.type.in_(types))
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
# Only join when searching / ordering
|
|
||||||
if starts_with or search or order != 'release_order':
|
|
||||||
q = q.join(Media.library, Library.titles) \
|
|
||||||
.filter(LibraryTitle.default == True)
|
|
||||||
|
|
||||||
# Add search filters
|
# Add search filters
|
||||||
filter_or = []
|
|
||||||
if starts_with:
|
if starts_with:
|
||||||
starts_with = toUnicode(starts_with.lower())
|
starts_with = toUnicode(starts_with.lower())[0]
|
||||||
if starts_with in ascii_lowercase:
|
starts_with = starts_with if starts_with in ascii_lowercase else '#'
|
||||||
filter_or.append(LibraryTitle.simple_title.startswith(starts_with))
|
filter_by['starts_with'] = [x['_id'] for x in db.get_many('media_startswith', starts_with)]
|
||||||
else:
|
|
||||||
ignore = []
|
|
||||||
for letter in ascii_lowercase:
|
|
||||||
ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter)))
|
|
||||||
filter_or.append(not_(or_(*ignore)))
|
|
||||||
|
|
||||||
|
# Add tag filter
|
||||||
|
if with_tags:
|
||||||
|
filter_by['with_tags'] = set()
|
||||||
|
for tag in with_tags:
|
||||||
|
for x in db.get_many('media_tag', tag):
|
||||||
|
filter_by['with_tags'].add(x['_id'])
|
||||||
|
|
||||||
|
# Filter with search query
|
||||||
if search:
|
if search:
|
||||||
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
|
filter_by['search'] = [x['_id'] for x in db.get_many('media_search_title', search)]
|
||||||
|
|
||||||
if len(filter_or) > 0:
|
if status_or and 'media_status' in filter_by and 'release_status' in filter_by:
|
||||||
q = q.filter(or_(*filter_or))
|
filter_by['status'] = list(filter_by['media_status']) + list(filter_by['release_status'])
|
||||||
|
del filter_by['media_status']
|
||||||
|
del filter_by['release_status']
|
||||||
|
|
||||||
total_count = q.count()
|
# Filter by combining ids
|
||||||
|
for x in filter_by:
|
||||||
|
media_ids = [n for n in media_ids if n in filter_by[x]]
|
||||||
|
|
||||||
|
total_count = len(media_ids)
|
||||||
if total_count == 0:
|
if total_count == 0:
|
||||||
return 0, []
|
return 0, []
|
||||||
|
|
||||||
if order == 'release_order':
|
offset = 0
|
||||||
q = q.order_by(desc(Release.last_edit))
|
limit = -1
|
||||||
else:
|
|
||||||
q = q.order_by(asc(LibraryTitle.simple_title))
|
|
||||||
|
|
||||||
if limit_offset:
|
if limit_offset:
|
||||||
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
|
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
|
||||||
limit = splt[0]
|
limit = tryInt(splt[0])
|
||||||
offset = 0 if len(splt) is 1 else splt[1]
|
offset = tryInt(0 if len(splt) is 1 else splt[1])
|
||||||
q = q.limit(limit).offset(offset)
|
|
||||||
|
|
||||||
# Get all media_ids in sorted order
|
# List movies based on title order
|
||||||
media_ids = [m.id for m in q.all()]
|
medias = []
|
||||||
|
for m in db.all('media_title'):
|
||||||
|
media_id = m['_id']
|
||||||
|
if media_id not in media_ids: continue
|
||||||
|
if offset > 0:
|
||||||
|
offset -= 1
|
||||||
|
continue
|
||||||
|
|
||||||
# List release statuses
|
media = fireEvent('media.get', media_id, single = True)
|
||||||
releases = db.query(Release) \
|
|
||||||
.filter(Release.movie_id.in_(media_ids)) \
|
|
||||||
.all()
|
|
||||||
|
|
||||||
release_statuses = dict((m, set()) for m in media_ids)
|
|
||||||
releases_count = dict((m, 0) for m in media_ids)
|
|
||||||
for release in releases:
|
|
||||||
release_statuses[release.movie_id].add('%d,%d' % (release.status_id, release.quality_id))
|
|
||||||
releases_count[release.movie_id] += 1
|
|
||||||
|
|
||||||
# Get main movie data
|
|
||||||
q2 = db.query(Media) \
|
|
||||||
.options(joinedload_all('library.titles')) \
|
|
||||||
.options(joinedload_all('library.files')) \
|
|
||||||
.options(joinedload_all('status')) \
|
|
||||||
.options(joinedload_all('files'))
|
|
||||||
|
|
||||||
q2 = q2.filter(Media.id.in_(media_ids))
|
|
||||||
|
|
||||||
results = q2.all()
|
|
||||||
|
|
||||||
# Create dict by movie id
|
|
||||||
movie_dict = {}
|
|
||||||
for movie in results:
|
|
||||||
movie_dict[movie.id] = movie
|
|
||||||
|
|
||||||
# List movies based on media_ids order
|
|
||||||
movies = []
|
|
||||||
for media_id in media_ids:
|
|
||||||
|
|
||||||
releases = []
|
|
||||||
for r in release_statuses.get(media_id):
|
|
||||||
x = splitString(r)
|
|
||||||
releases.append({'status_id': x[0], 'quality_id': x[1]})
|
|
||||||
|
|
||||||
# Merge releases with movie dict
|
# Merge releases with movie dict
|
||||||
movies.append(mergeDicts(movie_dict[media_id].to_dict({
|
medias.append(media)
|
||||||
'library': {'titles': {}, 'files': {}},
|
|
||||||
'files': {},
|
|
||||||
}), {
|
|
||||||
'releases': releases,
|
|
||||||
'releases_count': releases_count.get(media_id),
|
|
||||||
}))
|
|
||||||
|
|
||||||
return total_count, movies
|
# remove from media ids
|
||||||
|
media_ids.remove(media_id)
|
||||||
|
if len(media_ids) == 0 or len(medias) == limit: break
|
||||||
|
|
||||||
|
return total_count, medias
|
||||||
|
|
||||||
def listView(self, **kwargs):
|
def listView(self, **kwargs):
|
||||||
|
|
||||||
types = splitString(kwargs.get('types'))
|
|
||||||
status = splitString(kwargs.get('status'))
|
|
||||||
release_status = splitString(kwargs.get('release_status'))
|
|
||||||
limit_offset = kwargs.get('limit_offset')
|
|
||||||
starts_with = kwargs.get('starts_with')
|
|
||||||
search = kwargs.get('search')
|
|
||||||
order = kwargs.get('order')
|
|
||||||
|
|
||||||
total_movies, movies = self.list(
|
total_movies, movies = self.list(
|
||||||
types = types,
|
types = splitString(kwargs.get('type')),
|
||||||
status = status,
|
status = splitString(kwargs.get('status')),
|
||||||
release_status = release_status,
|
release_status = splitString(kwargs.get('release_status')),
|
||||||
limit_offset = limit_offset,
|
status_or = kwargs.get('status_or') is not None,
|
||||||
starts_with = starts_with,
|
limit_offset = kwargs.get('limit_offset'),
|
||||||
search = search,
|
with_tags = splitString(kwargs.get('with_tags')),
|
||||||
order = order
|
starts_with = kwargs.get('starts_with'),
|
||||||
|
search = kwargs.get('search')
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -292,67 +308,57 @@ class MediaPlugin(MediaBase):
|
|||||||
|
|
||||||
for media_type in fireEvent('media.types', merge = True):
|
for media_type in fireEvent('media.types', merge = True):
|
||||||
def tempList(*args, **kwargs):
|
def tempList(*args, **kwargs):
|
||||||
return self.listView(types = media_type, *args, **kwargs)
|
return self.listView(types = media_type, **kwargs)
|
||||||
addApiView('%s.list' % media_type, tempList)
|
addApiView('%s.list' % media_type, tempList)
|
||||||
|
|
||||||
def availableChars(self, types = None, status = None, release_status = None):
|
def availableChars(self, types = None, status = None, release_status = None):
|
||||||
|
|
||||||
types = types or []
|
db = get_db()
|
||||||
status = status or []
|
|
||||||
release_status = release_status or []
|
|
||||||
|
|
||||||
db = get_session()
|
|
||||||
|
|
||||||
# Make a list from string
|
# Make a list from string
|
||||||
if not isinstance(status, (list, tuple)):
|
if status and not isinstance(status, (list, tuple)):
|
||||||
status = [status]
|
status = [status]
|
||||||
if release_status and not isinstance(release_status, (list, tuple)):
|
if release_status and not isinstance(release_status, (list, tuple)):
|
||||||
release_status = [release_status]
|
release_status = [release_status]
|
||||||
if types and not isinstance(types, (list, tuple)):
|
if types and not isinstance(types, (list, tuple)):
|
||||||
types = [types]
|
types = [types]
|
||||||
|
|
||||||
q = db.query(Media)
|
# query media ids
|
||||||
|
if types:
|
||||||
|
all_media_ids = set()
|
||||||
|
for media_type in types:
|
||||||
|
all_media_ids = all_media_ids.union(set([x['_id'] for x in db.get_many('media_by_type', media_type)]))
|
||||||
|
else:
|
||||||
|
all_media_ids = set([x['_id'] for x in db.all('media')])
|
||||||
|
|
||||||
|
media_ids = all_media_ids
|
||||||
|
filter_by = {}
|
||||||
|
|
||||||
# Filter on movie status
|
# Filter on movie status
|
||||||
if status and len(status) > 0:
|
if status and len(status) > 0:
|
||||||
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
|
filter_by['media_status'] = set()
|
||||||
statuses = [s.get('id') for s in statuses]
|
for media_status in fireEvent('media.with_status', status, with_doc = False, single = True):
|
||||||
|
filter_by['media_status'].add(media_status.get('_id'))
|
||||||
q = q.filter(Media.status_id.in_(statuses))
|
|
||||||
|
|
||||||
# Filter on release status
|
# Filter on release status
|
||||||
if release_status and len(release_status) > 0:
|
if release_status and len(release_status) > 0:
|
||||||
|
filter_by['release_status'] = set()
|
||||||
|
for release_status in fireEvent('release.with_status', release_status, with_doc = False, single = True):
|
||||||
|
filter_by['release_status'].add(release_status.get('media_id'))
|
||||||
|
|
||||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
# Filter by combining ids
|
||||||
statuses = [s.get('id') for s in statuses]
|
for x in filter_by:
|
||||||
|
media_ids = [n for n in media_ids if n in filter_by[x]]
|
||||||
q = q.join(Media.releases) \
|
|
||||||
.filter(Release.status_id.in_(statuses))
|
|
||||||
|
|
||||||
# Filter on type
|
|
||||||
if types and len(types) > 0:
|
|
||||||
try: q = q.filter(Media.type.in_(types))
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
q = q.join(Library, LibraryTitle) \
|
|
||||||
.with_entities(LibraryTitle.simple_title) \
|
|
||||||
.filter(LibraryTitle.default == True)
|
|
||||||
|
|
||||||
titles = q.all()
|
|
||||||
|
|
||||||
chars = set()
|
chars = set()
|
||||||
for title in titles:
|
for x in db.all('media_startswith'):
|
||||||
try:
|
if x['_id'] in media_ids:
|
||||||
char = title[0][0]
|
chars.add(x['key'])
|
||||||
char = char if char in ascii_lowercase else '#'
|
|
||||||
chars.add(str(char))
|
|
||||||
except:
|
|
||||||
log.error('Failed getting title for %s', title.libraries_id)
|
|
||||||
|
|
||||||
if len(chars) == 25:
|
if len(chars) == 25:
|
||||||
break
|
break
|
||||||
|
|
||||||
return ''.join(sorted(chars))
|
return list(chars)
|
||||||
|
|
||||||
def charView(self, **kwargs):
|
def charView(self, **kwargs):
|
||||||
|
|
||||||
@@ -371,59 +377,59 @@ class MediaPlugin(MediaBase):
|
|||||||
|
|
||||||
for media_type in fireEvent('media.types', merge = True):
|
for media_type in fireEvent('media.types', merge = True):
|
||||||
def tempChar(*args, **kwargs):
|
def tempChar(*args, **kwargs):
|
||||||
return self.charView(types = media_type, *args, **kwargs)
|
return self.charView(types = media_type, **kwargs)
|
||||||
addApiView('%s.available_chars' % media_type, tempChar)
|
addApiView('%s.available_chars' % media_type, tempChar)
|
||||||
|
|
||||||
def delete(self, media_id, delete_from = None):
|
def delete(self, media_id, delete_from = None):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db = get_session()
|
db = get_db()
|
||||||
|
|
||||||
media = db.query(Media).filter_by(id = media_id).first()
|
media = db.get('id', media_id)
|
||||||
if media:
|
if media:
|
||||||
deleted = False
|
deleted = False
|
||||||
|
|
||||||
|
media_releases = fireEvent('release.for_media', media['_id'], single = True)
|
||||||
|
|
||||||
if delete_from == 'all':
|
if delete_from == 'all':
|
||||||
|
# Delete connected releases
|
||||||
|
for release in media_releases:
|
||||||
|
db.delete(release)
|
||||||
|
|
||||||
db.delete(media)
|
db.delete(media)
|
||||||
db.commit()
|
|
||||||
deleted = True
|
deleted = True
|
||||||
else:
|
else:
|
||||||
done_status = fireEvent('status.get', 'done', single = True)
|
|
||||||
|
|
||||||
total_releases = len(media.releases)
|
total_releases = len(media_releases)
|
||||||
total_deleted = 0
|
total_deleted = 0
|
||||||
new_movie_status = None
|
new_media_status = None
|
||||||
for release in media.releases:
|
|
||||||
if delete_from in ['wanted', 'snatched', 'late']:
|
|
||||||
if release.status_id != done_status.get('id'):
|
|
||||||
db.delete(release)
|
|
||||||
total_deleted += 1
|
|
||||||
new_movie_status = 'done'
|
|
||||||
elif delete_from == 'manage':
|
|
||||||
if release.status_id == done_status.get('id'):
|
|
||||||
db.delete(release)
|
|
||||||
total_deleted += 1
|
|
||||||
new_movie_status = 'active'
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
if total_releases == total_deleted:
|
for release in media_releases:
|
||||||
|
if delete_from in ['wanted', 'snatched', 'late']:
|
||||||
|
if release.get('status') != 'done':
|
||||||
|
db.delete(release)
|
||||||
|
total_deleted += 1
|
||||||
|
new_media_status = 'done'
|
||||||
|
elif delete_from == 'manage':
|
||||||
|
if release.get('status') == 'done' or media.get('status') == 'done':
|
||||||
|
db.delete(release)
|
||||||
|
total_deleted += 1
|
||||||
|
|
||||||
|
if (total_releases == total_deleted and media['status'] != 'active') or (total_releases == 0 and not new_media_status) or (not new_media_status and delete_from == 'late'):
|
||||||
db.delete(media)
|
db.delete(media)
|
||||||
db.commit()
|
|
||||||
deleted = True
|
deleted = True
|
||||||
elif new_movie_status:
|
elif new_media_status:
|
||||||
new_status = fireEvent('status.get', new_movie_status, single = True)
|
media['status'] = new_media_status
|
||||||
media.profile_id = None
|
db.update(media)
|
||||||
media.status_id = new_status.get('id')
|
|
||||||
db.commit()
|
fireEvent('media.untag', media['_id'], 'recent', single = True)
|
||||||
else:
|
else:
|
||||||
fireEvent('media.restatus', media.id, single = True)
|
fireEvent('media.restatus', media.get('_id'), single = True)
|
||||||
|
|
||||||
if deleted:
|
if deleted:
|
||||||
fireEvent('notify.frontend', type = 'movie.deleted', data = media.to_dict())
|
fireEvent('notify.frontend', type = 'media.deleted', data = media)
|
||||||
except:
|
except:
|
||||||
log.error('Failed deleting media: %s', traceback.format_exc())
|
log.error('Failed deleting media: %s', traceback.format_exc())
|
||||||
db.rollback()
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -446,35 +452,82 @@ class MediaPlugin(MediaBase):
|
|||||||
|
|
||||||
def restatus(self, media_id):
|
def restatus(self, media_id):
|
||||||
|
|
||||||
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
|
try:
|
||||||
|
db = get_db()
|
||||||
|
|
||||||
|
m = db.get('id', media_id)
|
||||||
|
previous_status = m['status']
|
||||||
|
|
||||||
|
log.debug('Changing status for %s', getTitle(m))
|
||||||
|
if not m['profile_id']:
|
||||||
|
m['status'] = 'done'
|
||||||
|
else:
|
||||||
|
m['status'] = 'active'
|
||||||
|
|
||||||
|
try:
|
||||||
|
profile = db.get('id', m['profile_id'])
|
||||||
|
media_releases = fireEvent('release.for_media', m['_id'], single = True)
|
||||||
|
done_releases = [release for release in media_releases if release.get('status') == 'done']
|
||||||
|
|
||||||
|
if done_releases:
|
||||||
|
# Only look at latest added release
|
||||||
|
release = sorted(done_releases, key = itemgetter('last_edit'), reverse = True)[0]
|
||||||
|
|
||||||
|
# Check if we are finished with the media
|
||||||
|
if fireEvent('quality.isfinish', {'identifier': release['quality'], 'is_3d': release.get('is_3d', False)}, profile, timedelta(seconds = time.time() - release['last_edit']).days, single = True):
|
||||||
|
m['status'] = 'done'
|
||||||
|
elif previous_status == 'done':
|
||||||
|
m['status'] = 'done'
|
||||||
|
|
||||||
|
except RecordNotFound:
|
||||||
|
log.debug('Failed restatus, keeping previous: %s', traceback.format_exc())
|
||||||
|
m['status'] = previous_status
|
||||||
|
|
||||||
|
# Only update when status has changed
|
||||||
|
if previous_status != m['status']:
|
||||||
|
db.update(m)
|
||||||
|
|
||||||
|
# Tag media as recent
|
||||||
|
self.tag(media_id, 'recent')
|
||||||
|
|
||||||
|
return m['status']
|
||||||
|
except:
|
||||||
|
log.error('Failed restatus: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
def tag(self, media_id, tag):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db = get_session()
|
db = get_db()
|
||||||
|
m = db.get('id', media_id)
|
||||||
|
|
||||||
m = db.query(Media).filter_by(id = media_id).first()
|
tags = m.get('tags') or []
|
||||||
if not m or len(m.library.titles) == 0:
|
if tag not in tags:
|
||||||
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
|
tags.append(tag)
|
||||||
return False
|
m['tags'] = tags
|
||||||
|
db.update(m)
|
||||||
log.debug('Changing status for %s', m.library.titles[0].title)
|
|
||||||
if not m.profile:
|
|
||||||
m.status_id = done_status.get('id')
|
|
||||||
else:
|
|
||||||
move_to_wanted = True
|
|
||||||
|
|
||||||
for t in m.profile.types:
|
|
||||||
for release in m.releases:
|
|
||||||
if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish):
|
|
||||||
move_to_wanted = False
|
|
||||||
|
|
||||||
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
|
|
||||||
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
log.error('Failed restatus: %s', traceback.format_exc())
|
log.error('Failed tagging: %s', traceback.format_exc())
|
||||||
db.rollback()
|
|
||||||
finally:
|
|
||||||
db.close()
|
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def unTag(self, media_id, tag):
|
||||||
|
|
||||||
|
try:
|
||||||
|
db = get_db()
|
||||||
|
m = db.get('id', media_id)
|
||||||
|
|
||||||
|
tags = m.get('tags') or []
|
||||||
|
if tag in tags:
|
||||||
|
new_tags = list(set(tags))
|
||||||
|
new_tags.remove(tag)
|
||||||
|
|
||||||
|
m['tags'] = new_tags
|
||||||
|
db.update(m)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
log.error('Failed untagging: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
return False
|
||||||
|
|||||||
Executable → Regular
@@ -0,0 +1,8 @@
|
|||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.base import Provider
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AutomationBase(Provider):
|
||||||
|
pass
|
||||||
+36
-21
@@ -1,10 +1,3 @@
|
|||||||
from couchpotato.core.event import addEvent, fireEvent
|
|
||||||
from couchpotato.core.helpers.encoding import ss
|
|
||||||
from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \
|
|
||||||
possibleTitles, getTitle
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.plugins.base import Plugin
|
|
||||||
from couchpotato.environment import Env
|
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
@@ -12,6 +5,15 @@ import time
|
|||||||
import traceback
|
import traceback
|
||||||
import xml.etree.ElementTree as XMLTree
|
import xml.etree.ElementTree as XMLTree
|
||||||
|
|
||||||
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
|
from couchpotato.core.helpers.encoding import ss
|
||||||
|
from couchpotato.core.helpers.variable import tryFloat, mergeDicts, md5, \
|
||||||
|
possibleTitles
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -86,10 +88,14 @@ class Provider(Plugin):
|
|||||||
|
|
||||||
if data and len(data) > 0:
|
if data and len(data) > 0:
|
||||||
try:
|
try:
|
||||||
data = XMLTree.fromstring(ss(data))
|
data = XMLTree.fromstring(data)
|
||||||
return self.getElements(data, item_path)
|
return self.getElements(data, item_path)
|
||||||
except:
|
except:
|
||||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
try:
|
||||||
|
data = XMLTree.fromstring(ss(data))
|
||||||
|
return self.getElements(data, item_path)
|
||||||
|
except:
|
||||||
|
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -102,7 +108,6 @@ class Provider(Plugin):
|
|||||||
class YarrProvider(Provider):
|
class YarrProvider(Provider):
|
||||||
|
|
||||||
protocol = None # nzb, torrent, torrent_magnet
|
protocol = None # nzb, torrent, torrent_magnet
|
||||||
type = 'movie'
|
|
||||||
|
|
||||||
cat_ids = {}
|
cat_ids = {}
|
||||||
cat_backup_id = None
|
cat_backup_id = None
|
||||||
@@ -124,6 +129,9 @@ class YarrProvider(Provider):
|
|||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def buildUrl(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
|
|
||||||
# Check if we are still logged in every hour
|
# Check if we are still logged in every hour
|
||||||
@@ -176,11 +184,11 @@ class YarrProvider(Provider):
|
|||||||
try:
|
try:
|
||||||
return self.urlopen(url, headers = {'User-Agent': Env.getIdentifier()}, show_error = False)
|
return self.urlopen(url, headers = {'User-Agent': Env.getIdentifier()}, show_error = False)
|
||||||
except:
|
except:
|
||||||
log.error('Failed getting nzb from %s: %s', (self.getName(), traceback.format_exc()))
|
log.error('Failed getting release from %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
return 'try_next'
|
return 'try_next'
|
||||||
|
|
||||||
def search(self, movie, quality):
|
def search(self, media, quality):
|
||||||
|
|
||||||
if self.isDisabled():
|
if self.isDisabled():
|
||||||
return []
|
return []
|
||||||
@@ -192,15 +200,17 @@ class YarrProvider(Provider):
|
|||||||
|
|
||||||
# Create result container
|
# Create result container
|
||||||
imdb_results = hasattr(self, '_search')
|
imdb_results = hasattr(self, '_search')
|
||||||
results = ResultList(self, movie, quality, imdb_results = imdb_results)
|
results = ResultList(self, media, quality, imdb_results = imdb_results)
|
||||||
|
|
||||||
# Do search based on imdb id
|
# Do search based on imdb id
|
||||||
if imdb_results:
|
if imdb_results:
|
||||||
self._search(movie, quality, results)
|
self._search(media, quality, results)
|
||||||
# Search possible titles
|
# Search possible titles
|
||||||
else:
|
else:
|
||||||
for title in possibleTitles(getTitle(movie['library'])):
|
media_title = fireEvent('library.query', media, include_year = False, single = True)
|
||||||
self._searchOnTitle(title, movie, quality, results)
|
|
||||||
|
for title in possibleTitles(media_title):
|
||||||
|
self._searchOnTitle(title, media, quality, results)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@@ -241,11 +251,16 @@ class YarrProvider(Provider):
|
|||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def getCatId(self, identifier):
|
def getCatId(self, quality = None):
|
||||||
|
if not quality: quality = {}
|
||||||
|
identifier = quality.get('identifier')
|
||||||
|
|
||||||
for cats in self.cat_ids:
|
want_3d = False
|
||||||
ids, qualities = cats
|
if quality.get('custom'):
|
||||||
if identifier in qualities:
|
want_3d = quality['custom'].get('3d')
|
||||||
|
|
||||||
|
for ids, qualities in self.cat_ids:
|
||||||
|
if identifier in qualities or (want_3d and '3d' in qualities):
|
||||||
return ids
|
return ids
|
||||||
|
|
||||||
if self.cat_backup_id:
|
if self.cat_backup_id:
|
||||||
@@ -290,7 +305,7 @@ class ResultList(list):
|
|||||||
old_score = new_result['score']
|
old_score = new_result['score']
|
||||||
new_result['score'] = int(old_score * is_correct_weight)
|
new_result['score'] = int(old_score * is_correct_weight)
|
||||||
|
|
||||||
log.info('Found correct release with weight %.02f, old_score(%d) now scaled to score(%d)', (
|
log.info2('Found correct release with weight %.02f, old_score(%d) now scaled to score(%d)', (
|
||||||
is_correct_weight,
|
is_correct_weight,
|
||||||
old_score,
|
old_score,
|
||||||
new_result['score']
|
new_result['score']
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from couchpotato.core.media._base.providers.base import Provider
|
||||||
|
|
||||||
|
|
||||||
|
class BaseInfoProvider(Provider):
|
||||||
|
type = 'unknown'
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MetaDataBase(Plugin):
|
||||||
|
pass
|
||||||
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
from couchpotato.core.providers.base import YarrProvider
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from couchpotato.core.media._base.providers.base import YarrProvider
|
||||||
|
|
||||||
|
|
||||||
class NZBProvider(YarrProvider):
|
class NZBProvider(YarrProvider):
|
||||||
|
|
||||||
+44
-28
@@ -1,16 +1,16 @@
|
|||||||
from bs4 import BeautifulSoup
|
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
|
||||||
from couchpotato.core.helpers.variable import tryInt
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
|
||||||
from couchpotato.environment import Env
|
|
||||||
import re
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from couchpotato.core.helpers.variable import tryInt, simplifyString
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.nzb.base import NZBProvider
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class BinSearch(NZBProvider):
|
class Base(NZBProvider):
|
||||||
|
|
||||||
urls = {
|
urls = {
|
||||||
'download': 'https://www.binsearch.info/fcgi/nzb.fcgi?q=%s',
|
'download': 'https://www.binsearch.info/fcgi/nzb.fcgi?q=%s',
|
||||||
@@ -20,27 +20,15 @@ class BinSearch(NZBProvider):
|
|||||||
|
|
||||||
http_time_between_calls = 4 # Seconds
|
http_time_between_calls = 4 # Seconds
|
||||||
|
|
||||||
def _search(self, movie, quality, results):
|
def _search(self, media, quality, results):
|
||||||
|
|
||||||
arguments = tryUrlencode({
|
data = self.getHTMLData(self.urls['search'] % self.buildUrl(media, quality))
|
||||||
'q': movie['library']['identifier'],
|
|
||||||
'm': 'n',
|
|
||||||
'max': 400,
|
|
||||||
'adv_age': Env.setting('retention', 'nzb'),
|
|
||||||
'adv_sort': 'date',
|
|
||||||
'adv_col': 'on',
|
|
||||||
'adv_nfo': 'on',
|
|
||||||
'minsize': quality.get('size_min'),
|
|
||||||
'maxsize': quality.get('size_max'),
|
|
||||||
})
|
|
||||||
|
|
||||||
data = self.getHTMLData(self.urls['search'] % arguments)
|
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
try:
|
try:
|
||||||
|
|
||||||
html = BeautifulSoup(data)
|
html = BeautifulSoup(data)
|
||||||
main_table = html.find('table', attrs = {'id':'r2'})
|
main_table = html.find('table', attrs = {'id': 'r2'})
|
||||||
|
|
||||||
if not main_table:
|
if not main_table:
|
||||||
return
|
return
|
||||||
@@ -48,11 +36,11 @@ class BinSearch(NZBProvider):
|
|||||||
items = main_table.find_all('tr')
|
items = main_table.find_all('tr')
|
||||||
|
|
||||||
for row in items:
|
for row in items:
|
||||||
title = row.find('span', attrs = {'class':'s'})
|
title = row.find('span', attrs = {'class': 's'})
|
||||||
|
|
||||||
if not title: continue
|
if not title: continue
|
||||||
|
|
||||||
nzb_id = row.find('input', attrs = {'type':'checkbox'})['name']
|
nzb_id = row.find('input', attrs = {'type': 'checkbox'})['name']
|
||||||
info = row.find('span', attrs = {'class':'d'})
|
info = row.find('span', attrs = {'class':'d'})
|
||||||
size_match = re.search('size:.(?P<size>[0-9\.]+.[GMB]+)', info.text)
|
size_match = re.search('size:.(?P<size>[0-9\.]+.[GMB]+)', info.text)
|
||||||
|
|
||||||
@@ -62,10 +50,10 @@ class BinSearch(NZBProvider):
|
|||||||
|
|
||||||
def extra_check(item):
|
def extra_check(item):
|
||||||
parts = re.search('available:.(?P<parts>\d+)./.(?P<total>\d+)', info.text)
|
parts = re.search('available:.(?P<parts>\d+)./.(?P<total>\d+)', info.text)
|
||||||
total = tryInt(parts.group('total'))
|
total = float(tryInt(parts.group('total')))
|
||||||
parts = tryInt(parts.group('parts'))
|
parts = float(tryInt(parts.group('parts')))
|
||||||
|
|
||||||
if (total / parts) < 0.95 or ((total / parts) >= 0.95 and not ('par2' in info.text.lower() or 'pa3' in info.text.lower())):
|
if (total / parts) < 1 and ((total / parts) < 0.95 or ((total / parts) >= 0.95 and not ('par2' in info.text.lower() or 'pa3' in info.text.lower()))):
|
||||||
log.info2('Wrong: \'%s\', not complete: %s out of %s', (item['name'], parts, total))
|
log.info2('Wrong: \'%s\', not complete: %s out of %s', (item['name'], parts, total))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -77,7 +65,7 @@ class BinSearch(NZBProvider):
|
|||||||
|
|
||||||
results.append({
|
results.append({
|
||||||
'id': nzb_id,
|
'id': nzb_id,
|
||||||
'name': title.text,
|
'name': simplifyString(title.text),
|
||||||
'age': tryInt(age),
|
'age': tryInt(age),
|
||||||
'size': self.parseSize(size_match.group('size')),
|
'size': self.parseSize(size_match.group('size')),
|
||||||
'url': self.urls['download'] % nzb_id,
|
'url': self.urls['download'] % nzb_id,
|
||||||
@@ -102,3 +90,31 @@ class BinSearch(NZBProvider):
|
|||||||
|
|
||||||
return 'try_next'
|
return 'try_next'
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'binsearch',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'nzb_providers',
|
||||||
|
'name': 'binsearch',
|
||||||
|
'description': 'Free provider, less accurate. See <a href="https://www.binsearch.info/">BinSearch</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAATklEQVQY02NwQAMMWAXOnz+PKvD//3/CAvM//z+fgiwAAs+RBab4PP//vwbFjPlAffgEChzOo2r5fBuIfRAC5w8D+QUofkkp8MHjOWQAAM3Sbogztg2wAAAAAElFTkSuQmCC',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 0,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,266 @@
|
|||||||
|
from urlparse import urlparse
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
import re
|
||||||
|
|
||||||
|
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
|
||||||
|
from couchpotato.core.helpers.rss import RSS
|
||||||
|
from couchpotato.core.helpers.variable import cleanHost, splitString, tryInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.base import ResultList
|
||||||
|
from couchpotato.core.media._base.providers.nzb.base import NZBProvider
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
from dateutil.parser import parse
|
||||||
|
from requests import HTTPError
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(NZBProvider, RSS):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'detail': 'details/%s',
|
||||||
|
'download': 't=get&id=%s'
|
||||||
|
}
|
||||||
|
|
||||||
|
passwords_regex = 'password|wachtwoord'
|
||||||
|
limits_reached = {}
|
||||||
|
|
||||||
|
http_time_between_calls = 1 # Seconds
|
||||||
|
|
||||||
|
def search(self, media, quality):
|
||||||
|
hosts = self.getHosts()
|
||||||
|
|
||||||
|
results = ResultList(self, media, quality, imdb_results = True)
|
||||||
|
|
||||||
|
for host in hosts:
|
||||||
|
if self.isDisabled(host):
|
||||||
|
continue
|
||||||
|
|
||||||
|
self._searchOnHost(host, media, quality, results)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _searchOnHost(self, host, media, quality, results):
|
||||||
|
|
||||||
|
query = self.buildUrl(media, host)
|
||||||
|
url = '%s&%s' % (self.getUrl(host['host']), query)
|
||||||
|
nzbs = self.getRSSData(url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()})
|
||||||
|
|
||||||
|
for nzb in nzbs:
|
||||||
|
|
||||||
|
date = None
|
||||||
|
spotter = None
|
||||||
|
for item in nzb:
|
||||||
|
if date and spotter:
|
||||||
|
break
|
||||||
|
if item.attrib.get('name') == 'usenetdate':
|
||||||
|
date = item.attrib.get('value')
|
||||||
|
break
|
||||||
|
|
||||||
|
# Get the name of the person who posts the spot
|
||||||
|
if item.attrib.get('name') == 'poster':
|
||||||
|
if "@spot.net" in item.attrib.get('value'):
|
||||||
|
spotter = item.attrib.get('value').split("@")[0]
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not date:
|
||||||
|
date = self.getTextElement(nzb, 'pubDate')
|
||||||
|
|
||||||
|
nzb_id = self.getTextElement(nzb, 'guid').split('/')[-1:].pop()
|
||||||
|
name = self.getTextElement(nzb, 'title')
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
name_extra = ''
|
||||||
|
if spotter:
|
||||||
|
name_extra = spotter
|
||||||
|
|
||||||
|
description = ''
|
||||||
|
if "@spot.net" in nzb_id:
|
||||||
|
try:
|
||||||
|
# Get details for extended description to retrieve passwords
|
||||||
|
query = self.buildDetailsUrl(nzb_id, host['api_key'])
|
||||||
|
url = '%s&%s' % (self.getUrl(host['host']), query)
|
||||||
|
nzb_details = self.getRSSData(url, cache_timeout = 1800, headers = {'User-Agent': Env.getIdentifier()})[0]
|
||||||
|
|
||||||
|
description = self.getTextElement(nzb_details, 'description')
|
||||||
|
|
||||||
|
# Extract a password from the description
|
||||||
|
password = re.search('(?:' + self.passwords_regex + ')(?: *)(?:\:|\=)(?: *)(.*?)\<br\>|\n|$', description, flags = re.I).group(1)
|
||||||
|
if password:
|
||||||
|
name += ' {{%s}}' % password.strip()
|
||||||
|
except:
|
||||||
|
log.debug('Error getting details of "%s": %s', (name, traceback.format_exc()))
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'id': nzb_id,
|
||||||
|
'provider_extra': urlparse(host['host']).hostname or host['host'],
|
||||||
|
'name': toUnicode(name),
|
||||||
|
'name_extra': name_extra,
|
||||||
|
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
||||||
|
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
|
||||||
|
'url': ((self.getUrl(host['host']) + self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host),
|
||||||
|
'detail_url': (cleanHost(host['host']) + self.urls['detail']) % tryUrlencode(nzb_id),
|
||||||
|
'content': self.getTextElement(nzb, 'description'),
|
||||||
|
'description': description,
|
||||||
|
'score': host['extra_score'],
|
||||||
|
})
|
||||||
|
|
||||||
|
def getHosts(self):
|
||||||
|
|
||||||
|
uses = splitString(str(self.conf('use')), clean = False)
|
||||||
|
hosts = splitString(self.conf('host'), clean = False)
|
||||||
|
api_keys = splitString(self.conf('api_key'), clean = False)
|
||||||
|
extra_score = splitString(self.conf('extra_score'), clean = False)
|
||||||
|
custom_tags = splitString(self.conf('custom_tag'), clean = False)
|
||||||
|
|
||||||
|
list = []
|
||||||
|
for nr in range(len(hosts)):
|
||||||
|
|
||||||
|
try: key = api_keys[nr]
|
||||||
|
except: key = ''
|
||||||
|
|
||||||
|
try: host = hosts[nr]
|
||||||
|
except: host = ''
|
||||||
|
|
||||||
|
try: score = tryInt(extra_score[nr])
|
||||||
|
except: score = 0
|
||||||
|
|
||||||
|
try: custom_tag = custom_tags[nr]
|
||||||
|
except: custom_tag = ''
|
||||||
|
|
||||||
|
list.append({
|
||||||
|
'use': uses[nr],
|
||||||
|
'host': host,
|
||||||
|
'api_key': key,
|
||||||
|
'extra_score': score,
|
||||||
|
'custom_tag': custom_tag
|
||||||
|
})
|
||||||
|
|
||||||
|
return list
|
||||||
|
|
||||||
|
def belongsTo(self, url, provider = None, host = None):
|
||||||
|
|
||||||
|
hosts = self.getHosts()
|
||||||
|
|
||||||
|
for host in hosts:
|
||||||
|
result = super(Base, self).belongsTo(url, host = host['host'], provider = provider)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
|
||||||
|
def getUrl(self, host):
|
||||||
|
if '?page=newznabapi' in host:
|
||||||
|
return cleanHost(host)[:-1] + '&'
|
||||||
|
|
||||||
|
return cleanHost(host) + 'api?'
|
||||||
|
|
||||||
|
def isDisabled(self, host = None):
|
||||||
|
return not self.isEnabled(host)
|
||||||
|
|
||||||
|
def isEnabled(self, host = None):
|
||||||
|
|
||||||
|
# Return true if at least one is enabled and no host is given
|
||||||
|
if host is None:
|
||||||
|
for host in self.getHosts():
|
||||||
|
if self.isEnabled(host):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
return NZBProvider.isEnabled(self) and host['host'] and host['api_key'] and int(host['use'])
|
||||||
|
|
||||||
|
def getApiExt(self, host):
|
||||||
|
return '&apikey=%s' % host['api_key']
|
||||||
|
|
||||||
|
def download(self, url = '', nzb_id = ''):
|
||||||
|
host = urlparse(url).hostname
|
||||||
|
|
||||||
|
if self.limits_reached.get(host):
|
||||||
|
# Try again in 3 hours
|
||||||
|
if self.limits_reached[host] > time.time() - 10800:
|
||||||
|
return 'try_next'
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = self.urlopen(url, show_error = False)
|
||||||
|
self.limits_reached[host] = False
|
||||||
|
return data
|
||||||
|
except HTTPError as e:
|
||||||
|
if e.code == 503:
|
||||||
|
response = e.read().lower()
|
||||||
|
if 'maximum api' in response or 'download limit' in response:
|
||||||
|
if not self.limits_reached.get(host):
|
||||||
|
log.error('Limit reached for newznab provider: %s', host)
|
||||||
|
self.limits_reached[host] = time.time()
|
||||||
|
return 'try_next'
|
||||||
|
|
||||||
|
log.error('Failed download from %s: %s', (host, traceback.format_exc()))
|
||||||
|
|
||||||
|
return 'try_next'
|
||||||
|
|
||||||
|
def buildDetailsUrl(self, nzb_id, api_key):
|
||||||
|
query = tryUrlencode({
|
||||||
|
't': 'details',
|
||||||
|
'id': nzb_id,
|
||||||
|
'apikey': api_key,
|
||||||
|
})
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'newznab',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'nzb_providers',
|
||||||
|
'name': 'newznab',
|
||||||
|
'order': 10,
|
||||||
|
'description': 'Enable <a href="http://newznab.com/" target="_blank">NewzNab</a> such as <a href="https://nzb.su" target="_blank">NZB.su</a>, \
|
||||||
|
<a href="https://nzbs.org" target="_blank">NZBs.org</a>, <a href="http://dognzb.cr/" target="_blank">DOGnzb.cr</a>, \
|
||||||
|
<a href="https://github.com/spotweb/spotweb" target="_blank">Spotweb</a>, <a href="https://nzbgeek.info/" target="_blank">NZBGeek</a>, \
|
||||||
|
<a href="https://smackdownonyou.com" target="_blank">SmackDown</a>, <a href="https://www.nzbfinder.ws" target="_blank">NZBFinder</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAgMAAABinRfyAAAACVBMVEVjhwD///86aRovd/sBAAAAMklEQVQI12NgAIPQUCCRmQkjssDEShiRuRIqwZqZGcDAGBrqANUhGgIkWAOABKMDxCAA24UK50b26SAAAAAASUVORK5CYII=',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'use',
|
||||||
|
'default': '0,0,0,0,0,0'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'host',
|
||||||
|
'default': 'api.nzb.su,api.dognzb.cr,nzbs.org,https://index.nzbgeek.info, https://smackdownonyou.com, https://www.nzbfinder.ws',
|
||||||
|
'description': 'The hostname of your newznab provider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'default': '0,0,0,0,0,0',
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'custom_tag',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Custom tag',
|
||||||
|
'default': ',,,,,',
|
||||||
|
'description': 'Add custom tags, for example add rls=1 to get only scene releases from nzbs.org',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'api_key',
|
||||||
|
'default': ',,,,,',
|
||||||
|
'label': 'Api Key',
|
||||||
|
'description': 'Can be found on your profile page',
|
||||||
|
'type': 'combined',
|
||||||
|
'combine': ['use', 'host', 'api_key', 'extra_score', 'custom_tag'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
+39
-22
@@ -1,40 +1,28 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.helpers.rss import RSS
|
from couchpotato.core.helpers.rss import RSS
|
||||||
from couchpotato.core.helpers.variable import tryInt
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
from couchpotato.core.media._base.providers.nzb.base import NZBProvider
|
||||||
from dateutil.parser import parse
|
from dateutil.parser import parse
|
||||||
import time
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class NZBClub(NZBProvider, RSS):
|
class Base(NZBProvider, RSS):
|
||||||
|
|
||||||
urls = {
|
urls = {
|
||||||
'search': 'https://www.nzbclub.com/nzbfeeds.aspx?%s',
|
'search': 'https://www.nzbclub.com/nzbfeeds.aspx?%s',
|
||||||
}
|
}
|
||||||
|
|
||||||
http_time_between_calls = 4 #seconds
|
http_time_between_calls = 4 # seconds
|
||||||
|
|
||||||
def _searchOnTitle(self, title, movie, quality, results):
|
def _search(self, media, quality, results):
|
||||||
|
|
||||||
q = '"%s %s"' % (title, movie['library']['year'])
|
nzbs = self.getRSSData(self.urls['search'] % self.buildUrl(media))
|
||||||
|
|
||||||
q_param = tryUrlencode({
|
|
||||||
'q': q,
|
|
||||||
})
|
|
||||||
|
|
||||||
params = tryUrlencode({
|
|
||||||
'ig': 1,
|
|
||||||
'rpp': 200,
|
|
||||||
'st': 5,
|
|
||||||
'sp': 1,
|
|
||||||
'ns': 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
nzbs = self.getRSSData(self.urls['search'] % ('%s&%s' % (q_param, params)))
|
|
||||||
|
|
||||||
for nzb in nzbs:
|
for nzb in nzbs:
|
||||||
|
|
||||||
@@ -67,7 +55,7 @@ class NZBClub(NZBProvider, RSS):
|
|||||||
def getMoreInfo(self, item):
|
def getMoreInfo(self, item):
|
||||||
full_description = self.getCache('nzbclub.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
full_description = self.getCache('nzbclub.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
||||||
html = BeautifulSoup(full_description)
|
html = BeautifulSoup(full_description)
|
||||||
nfo_pre = html.find('pre', attrs = {'class':'nfo'})
|
nfo_pre = html.find('pre', attrs = {'class': 'nfo'})
|
||||||
description = toUnicode(nfo_pre.text) if nfo_pre else ''
|
description = toUnicode(nfo_pre.text) if nfo_pre else ''
|
||||||
|
|
||||||
item['description'] = description
|
item['description'] = description
|
||||||
@@ -81,3 +69,32 @@ class NZBClub(NZBProvider, RSS):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'nzbclub',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'nzb_providers',
|
||||||
|
'name': 'NZBClub',
|
||||||
|
'description': 'Free provider, less accurate. See <a href="https://www.nzbclub.com/">NZBClub</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACEUlEQVQ4y3VSMWgUQRR9/8/s7OzeJSdnTsVGghLEYBNQjBpQiRBFhIB2EcHG1kbs0murhZAmVocExEZQ0c7CxkLINYcJJpoYj9wZcnu72fF21uJSXMzuhyne58/j/fcf4b+KokgBIOSU53lxP5b9oNVqDT36dH+5UjoiKvIwPFEEgWBshGZ3E7/NOupL9fMjx0e+ZhKsrq+c/FPZKJi0w4FsQXMBDEJsd7BNW9h2tuyP9vfTALIJkMIu1hYRtINM+dpzcWc0sbkreK4fUEogyraAmKGF3+7vcT/wtR9QwkCabSAzQQuvk0uglAo5YaQ5DASGYjfMXcHVOqKu6NmR7iehlKAdHWUqWPv1c3i+9uwVdRlEBGaGEAJCCrDo9ShhvF6qPq8tL57bp+DbRn2sHtUuCY9YphLMu5921VhrwYJ5tbt0tt6sjQP4vEfB2Ikz7/ytwbeR6ljHkXCUA6UcOLtPOg4MYhtH8ZcLw5er+xQMDAwEURRNl96X596Y6oxFwsw9fmtTOAr2Ik19nL365FZpsLSdnQPPM8aYewc+lDcX4rkHqbQMAGTJXulOLzycmr1bKBTi3DOGYagajcahiaOT89fbM0/dxEsUu3aidfPljWO3HzebzYNBELi5Z5RSJlrrHd/3w8lT114MrVTWOn875fHRiYVisRhorWMpZXdvNnLKGCOstb0AMlulVJI19w/+nceU4D0aCwAAAABJRU5ErkJggg==',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 0,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
|
from couchpotato.core.helpers.rss import RSS
|
||||||
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.event import fireEvent
|
||||||
|
from couchpotato.core.media._base.providers.nzb.base import NZBProvider
|
||||||
|
from dateutil.parser import parse
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(NZBProvider, RSS):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'download': 'https://www.nzbindex.com/download/',
|
||||||
|
'search': 'https://www.nzbindex.com/rss/?%s',
|
||||||
|
}
|
||||||
|
|
||||||
|
http_time_between_calls = 1 # Seconds
|
||||||
|
|
||||||
|
def _search(self, media, quality, results):
|
||||||
|
|
||||||
|
nzbs = self.getRSSData(self.urls['search'] % self.buildUrl(media, quality))
|
||||||
|
|
||||||
|
for nzb in nzbs:
|
||||||
|
|
||||||
|
enclosure = self.getElement(nzb, 'enclosure').attrib
|
||||||
|
nzbindex_id = int(self.getTextElement(nzb, "link").split('/')[4])
|
||||||
|
|
||||||
|
title = self.getTextElement(nzb, "title")
|
||||||
|
|
||||||
|
match = fireEvent('matcher.parse', title, parser='usenet', single = True)
|
||||||
|
if not match.chains:
|
||||||
|
log.info('Unable to parse release with title "%s"', title)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# TODO should we consider other lower-weight chains here?
|
||||||
|
info = fireEvent('matcher.flatten_info', match.chains[0].info, single = True)
|
||||||
|
|
||||||
|
release_name = fireEvent('matcher.construct_from_raw', info.get('release_name'), single = True)
|
||||||
|
|
||||||
|
file_name = info.get('detail', {}).get('file_name')
|
||||||
|
file_name = file_name[0] if file_name else None
|
||||||
|
|
||||||
|
title = release_name or file_name
|
||||||
|
|
||||||
|
# Strip extension from parsed title (if one exists)
|
||||||
|
ext_pos = title.rfind('.')
|
||||||
|
|
||||||
|
# Assume extension if smaller than 4 characters
|
||||||
|
# TODO this should probably be done a better way
|
||||||
|
if len(title[ext_pos + 1:]) <= 4:
|
||||||
|
title = title[:ext_pos]
|
||||||
|
|
||||||
|
if not title:
|
||||||
|
log.info('Unable to find release name from match')
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
description = self.getTextElement(nzb, "description")
|
||||||
|
except:
|
||||||
|
description = ''
|
||||||
|
|
||||||
|
def extra_check(item):
|
||||||
|
if '#c20000' in item['description'].lower():
|
||||||
|
log.info('Wrong: Seems to be passworded: %s', item['name'])
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'id': nzbindex_id,
|
||||||
|
'name': title,
|
||||||
|
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
|
||||||
|
'size': tryInt(enclosure['length']) / 1024 / 1024,
|
||||||
|
'url': enclosure['url'],
|
||||||
|
'detail_url': enclosure['url'].replace('/download/', '/release/'),
|
||||||
|
'description': description,
|
||||||
|
'get_more_info': self.getMoreInfo,
|
||||||
|
'extra_check': extra_check,
|
||||||
|
})
|
||||||
|
|
||||||
|
def getMoreInfo(self, item):
|
||||||
|
try:
|
||||||
|
if '/nfo/' in item['description'].lower():
|
||||||
|
nfo_url = re.search('href=\"(?P<nfo>.+)\" ', item['description']).group('nfo')
|
||||||
|
full_description = self.getCache('nzbindex.%s' % item['id'], url = nfo_url, cache_timeout = 25920000)
|
||||||
|
html = BeautifulSoup(full_description)
|
||||||
|
item['description'] = toUnicode(html.find('pre', attrs = {'id': 'nfo0'}).text)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'nzbindex',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'nzb_providers',
|
||||||
|
'name': 'nzbindex',
|
||||||
|
'description': 'Free provider, less accurate. See <a href="https://www.nzbindex.com/">NZBIndex</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAo0lEQVR42t2SQQ2AMBAEcUCwUAv94QMLfHliAQtYqIVawEItYAG6yZFMLkUANNlk79Kbbtp2P1j9uKxVV9VWFeStl+Wh3fWK9hNwEoADZkJtMD49AqS5AUjWGx6A+m+ARICGrM5W+wSTB0gETKzdHZwCEZAJ8PGZQN4AiQAmkR9s06EBAugJiBoAAPFfAQcBgZcIHzwA6TYP4JsXeSg3P9L31w3eksbH3zMb/wAAAABJRU5ErkJggg==',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 0,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
from urlparse import urlparse, parse_qs
|
||||||
|
import time
|
||||||
|
|
||||||
|
from couchpotato.core.event import fireEvent
|
||||||
|
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||||
|
from couchpotato.core.helpers.rss import RSS
|
||||||
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.nzb.base import NZBProvider
|
||||||
|
from dateutil.parser import parse
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(NZBProvider, RSS):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'search': 'https://rss.omgwtfnzbs.org/rss-search.php?%s',
|
||||||
|
'detail_url': 'https://omgwtfnzbs.org/details.php?id=%s',
|
||||||
|
}
|
||||||
|
|
||||||
|
http_time_between_calls = 1 # Seconds
|
||||||
|
|
||||||
|
cat_ids = [
|
||||||
|
([15], ['dvdrip']),
|
||||||
|
([15, 16], ['brrip']),
|
||||||
|
([16], ['720p', '1080p', 'bd50']),
|
||||||
|
([17], ['dvdr']),
|
||||||
|
]
|
||||||
|
cat_backup_id = 'movie'
|
||||||
|
|
||||||
|
def search(self, movie, quality):
|
||||||
|
|
||||||
|
if quality['identifier'] in fireEvent('quality.pre_releases', single = True):
|
||||||
|
return []
|
||||||
|
|
||||||
|
return super(Base, self).search(movie, quality)
|
||||||
|
|
||||||
|
def _searchOnTitle(self, title, movie, quality, results):
|
||||||
|
|
||||||
|
q = '%s %s' % (title, movie['info']['year'])
|
||||||
|
params = tryUrlencode({
|
||||||
|
'search': q,
|
||||||
|
'catid': ','.join([str(x) for x in self.getCatId(quality)]),
|
||||||
|
'user': self.conf('username', default = ''),
|
||||||
|
'api': self.conf('api_key', default = ''),
|
||||||
|
})
|
||||||
|
|
||||||
|
nzbs = self.getRSSData(self.urls['search'] % params)
|
||||||
|
|
||||||
|
for nzb in nzbs:
|
||||||
|
|
||||||
|
enclosure = self.getElement(nzb, 'enclosure').attrib
|
||||||
|
nzb_id = parse_qs(urlparse(self.getTextElement(nzb, 'link')).query).get('id')[0]
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'id': nzb_id,
|
||||||
|
'name': toUnicode(self.getTextElement(nzb, 'title')),
|
||||||
|
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, 'pubDate')).timetuple()))),
|
||||||
|
'size': tryInt(enclosure['length']) / 1024 / 1024,
|
||||||
|
'url': enclosure['url'],
|
||||||
|
'detail_url': self.urls['detail_url'] % nzb_id,
|
||||||
|
'description': self.getTextElement(nzb, 'description')
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'omgwtfnzbs',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'nzb_providers',
|
||||||
|
'name': 'OMGWTFNZBs',
|
||||||
|
'description': 'See <a href="http://omgwtfnzbs.org/">OMGWTFNZBs</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAIAAADAAbR1AAADbElEQVR4AZ2UW0ybZRiAy/OvdHaLYvB0YTRIFi7GkM44zRLmIfNixkWdiRMyYoxRE8/TC7MYvXCGEBmr3mxLwVMwY0wYA7e6Wso4lB6h/U9taSlMGIfBXLYlJMyo0S///2dJI5lxN8/F2/f9nu9737e/jYmXr6KTbN9BGG9HE/NotQ76UWziNzrXFiETk/5ARUNH+7+0kW7fSgTl0VKGOLZzidOkmuuIo7q2oTArNLPIzhdIkqXkerFOm2CaD/5bcKrjIL2c3fkhPxOq93Kcb91v46fV9TQKF4TgV/TbUsQtzfCaK6jMOd5DJrguSIIhexmqqVxN0FXbRR8/ND/LYTTj6J7nl2gnL47OkDW4KJhnQHCa6JpKVNJGA3OC58nwBJoZ//ebbIyKpBxjrr0o1q1FMRkrKXZnHWF85VvxMrJxibwhGyd0f5bLnKzqJs1k0Sfo+EU8hdAUvkbcwKEgs2D0OiV4jmmD1zb+Tp6er0JMMvDxPo5xev9zTBF683NS+N56n1YiB95B5crr93KRuKhKI0tb0Kw2mgLLqTjLEWO8424i9IvURaYeOckwf3+/yCC9e3bQQ/MuD+Monk0k+XFXMUfx7z5EEP+XlXi5tLlMxH8zLppw7idJrugcus30kC86gc7UrQqjLIukM8zWHOACeU+TiMxXN6ExVOkgz4lvPEzice1GIVhxhG4CrZvpl6TH55giKWqXGLy9hZh5aUtgDSew/msSyCKpl+DDNfxJc8NBIsxUxUnz14O/oONu+IIIvso9TLBQ1SY5rUhuSzUhAqJ2mRXBLDOCeUtgUZXsaObT8BffhUJPqWgiV+3zKKzYH0ClvTRLhD77HIqVkyh5jThnivehoG+qJctIRSPn6bxvO4FCgTl9c1DmbpjLajbQFE8aW5SU3rg+zOPGUjTUF9NFpLEbH2c/KmGYlY69/GQJVtGMSUcEp9eCbB1nctbxHTLRdTUkGDf+B02uGWRG3OvpJ/zSMwzif+oxVBID3cQKBavLCiPmB2PM2UuSCUPgrX4VDb97AwEG67bh4+KTOlncvu3M31BwA5rLHbCfEjwkNDky9e/SSbSxnD46Pg0RJtpXRvhmBSZHpRjWtKwFybjuQeXaKxto4WjLZZZvVmC17pZLJFkwxm5++PS2Mrwc7nyIMYZe/IzoP5d6QgEybqTXAAAAAElFTkSuQmCC',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'api_key',
|
||||||
|
'label': 'Api Key',
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'default': 20,
|
||||||
|
'type': 'int',
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from couchpotato.core.helpers.variable import tryInt, getIdentifier
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(TorrentProvider):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'test': 'https://awesome-hd.net/',
|
||||||
|
'detail': 'https://awesome-hd.net/torrents.php?torrentid=%s',
|
||||||
|
'search': 'https://awesome-hd.net/searchapi.php?action=imdbsearch&passkey=%s&imdb=%s&internal=%s',
|
||||||
|
'download': 'https://awesome-hd.net/torrents.php?action=download&id=%s&authkey=%s&torrent_pass=%s',
|
||||||
|
}
|
||||||
|
http_time_between_calls = 1
|
||||||
|
|
||||||
|
def _search(self, movie, quality, results):
|
||||||
|
|
||||||
|
data = self.getHTMLData(self.urls['search'] % (self.conf('passkey'), getIdentifier(movie), self.conf('only_internal')))
|
||||||
|
|
||||||
|
if data:
|
||||||
|
try:
|
||||||
|
soup = BeautifulSoup(data)
|
||||||
|
|
||||||
|
if soup.find('error'):
|
||||||
|
log.error(soup.find('error').get_text())
|
||||||
|
return
|
||||||
|
|
||||||
|
authkey = soup.find('authkey').get_text()
|
||||||
|
entries = soup.find_all('torrent')
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
|
||||||
|
torrentscore = 0
|
||||||
|
torrent_id = entry.find('id').get_text()
|
||||||
|
name = entry.find('name').get_text()
|
||||||
|
year = entry.find('year').get_text()
|
||||||
|
releasegroup = entry.find('releasegroup').get_text()
|
||||||
|
resolution = entry.find('resolution').get_text()
|
||||||
|
encoding = entry.find('encoding').get_text()
|
||||||
|
freeleech = entry.find('freeleech').get_text()
|
||||||
|
torrent_desc = '/ %s / %s / %s ' % (releasegroup, resolution, encoding)
|
||||||
|
|
||||||
|
if freeleech == '0.25' and self.conf('prefer_internal'):
|
||||||
|
torrent_desc += '/ Internal'
|
||||||
|
torrentscore += 200
|
||||||
|
|
||||||
|
if encoding == 'x264' and self.conf('favor') in ['encode', 'both']:
|
||||||
|
torrentscore += 300
|
||||||
|
if re.search('Remux', encoding) and self.conf('favor') in ['remux', 'both']:
|
||||||
|
torrentscore += 200
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'id': torrent_id,
|
||||||
|
'name': re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) %s' % (name, year, torrent_desc)),
|
||||||
|
'url': self.urls['download'] % (torrent_id, authkey, self.conf('passkey')),
|
||||||
|
'detail_url': self.urls['detail'] % torrent_id,
|
||||||
|
'size': self.parseSize(entry.find('size').get_text()),
|
||||||
|
'seeders': tryInt(entry.find('seeders').get_text()),
|
||||||
|
'leechers': tryInt(entry.find('leechers').get_text()),
|
||||||
|
'score': torrentscore
|
||||||
|
})
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'awesomehd',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'Awesome-HD',
|
||||||
|
'description': '<a href="https://awesome-hd.net">AHD</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC+UlEQVR4AV1SO0y6dxQ9H4g8CoIoohZ5NA0aR2UgkYpNB5uocTSaLlrDblMH09Gt8d90r3YpJkanxjA4GGkbO7RNxSABq8jDGnkpD+UD5NV7Bxvbk9wvv+/3uPece66A/yEWi42FQqHVfD7/cbPZtIEglUpjOp3uZHR0dBvAn3gDIRqNgjE4OKj0+Xzf3NzcfD4wMCCjf5TLZbTbbajVatzf3+Pu7q5uNpt35ufnvwBQAScQRREEldfr9RWLxan+/n5YrVa+jFarhVfQQyQSCU4EhULhX15engEgSrjC0dHRVqlUmjQYDBgaGgKtuTqz4mTgIoVCASaTCX19fajVapOHh4dbFJBks9mxcDi8qtFoJEajkfVyJWi1WkxMTMDhcIAT8x6D7/Dd6+vr1fHx8TGp2+3+iqo5+YCzBwIBToK5ubl/mQwPDyMSibAs2Gw2UHNRrValz8/PDUk8Hv9EqVRCr9fj4uICTNflcqFer+Pg4AB7e3uoVCq8x9Rxfn6O7u5uqFQq8FspZXxHTekggByA3W4Hr9PpNDeRL3I1cMhkMrBrnZ2dyGQyvNYIs7OzVbJNPjIyAraLwYdcjR8wXl5eIJfLwRIFQQDLYkm3t7c1CdGPPT4+cpOImp4PODMeaK+n10As2jBbrHifHOjS6qAguVFimkqlwAMmIQnHV1dX4NDQhVwuhyZTV6pgIktzDzkkk0lEwhEEzs7ASQr5Ai4vL1nuccfCwsLO/v6+p9FoyJhF6ekJro/cPCzIZLNQa7rQoK77/SdgWWpKkCaJ5EB9aWnpe6nH40nRMBnJV4f5gw+FX3/5GX/8/htXRZdOzzqhJWn6nl6YbTZqqhrhULD16fT0d8FgcFtYW1vD5uamfGVl5cd4IjldKhZACdkJvKfWUANrxEaJV4hiGVaL1b+7653hXzwRZQr2X76xsfG1xWIRaZzbNPv/CdrjEL9cX/+WXFBSgEPgzxuwG3Yans9OT0+naBZMIJDNfzudzp8WFxd/APAX3uAf9WOTxOPLdosAAAAASUVORK5CYII=',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'passkey',
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_ratio',
|
||||||
|
'label': 'Seed ratio',
|
||||||
|
'type': 'float',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_time',
|
||||||
|
'label': 'Seed time',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 40,
|
||||||
|
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'only_internal',
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Only search for internal releases.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'prefer_internal',
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Favors internal releases over non-internal releases.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'favor',
|
||||||
|
'advanced': True,
|
||||||
|
'default': 'both',
|
||||||
|
'type': 'dropdown',
|
||||||
|
'values': [('Encodes & Remuxes', 'both'), ('Encodes', 'encode'), ('Remuxes', 'remux'), ('None', 'none')],
|
||||||
|
'description': 'Give extra scoring to encodes or remuxes.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'int',
|
||||||
|
'default': 20,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
|
|
||||||
+22
-3
@@ -1,9 +1,11 @@
|
|||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from couchpotato.core.helpers.variable import getImdb, md5, cleanHost
|
from couchpotato.core.helpers.variable import getImdb, md5, cleanHost
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.base import YarrProvider
|
from couchpotato.core.media._base.providers.base import YarrProvider
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
import time
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -15,6 +17,22 @@ class TorrentProvider(YarrProvider):
|
|||||||
proxy_domain = None
|
proxy_domain = None
|
||||||
proxy_list = []
|
proxy_list = []
|
||||||
|
|
||||||
|
def imdbMatch(self, url, imdbId):
|
||||||
|
if getImdb(url) == imdbId:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if url[:4] == 'http':
|
||||||
|
try:
|
||||||
|
cache_key = md5(url)
|
||||||
|
data = self.getCache(cache_key, url)
|
||||||
|
except IOError:
|
||||||
|
log.error('Failed to open %s.', url)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return getImdb(data) == imdbId
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def getDomain(self, url = ''):
|
def getDomain(self, url = ''):
|
||||||
|
|
||||||
forced_domain = self.conf('domain')
|
forced_domain = self.conf('domain')
|
||||||
@@ -26,7 +44,8 @@ class TorrentProvider(YarrProvider):
|
|||||||
|
|
||||||
prop_name = 'proxy.%s' % proxy
|
prop_name = 'proxy.%s' % proxy
|
||||||
last_check = float(Env.prop(prop_name, default = 0))
|
last_check = float(Env.prop(prop_name, default = 0))
|
||||||
if last_check > time.time() - 1209600:
|
|
||||||
|
if last_check > time.time() - 86400:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
data = ''
|
data = ''
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import traceback
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(TorrentProvider):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'test': 'http://www.bit-hdtv.com/',
|
||||||
|
'login': 'http://www.bit-hdtv.com/takelogin.php',
|
||||||
|
'login_check': 'http://www.bit-hdtv.com/messages.php',
|
||||||
|
'detail': 'http://www.bit-hdtv.com/details.php?id=%s',
|
||||||
|
'search': 'http://www.bit-hdtv.com/torrents.php?',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Searches for movies only - BiT-HDTV's subcategory and resolution search filters appear to be broken
|
||||||
|
http_time_between_calls = 1 # Seconds
|
||||||
|
|
||||||
|
def _search(self, media, quality, results):
|
||||||
|
|
||||||
|
query = self.buildUrl(media, quality)
|
||||||
|
|
||||||
|
url = "%s&%s" % (self.urls['search'], query)
|
||||||
|
|
||||||
|
data = self.getHTMLData(url)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
# Remove BiT-HDTV's output garbage so outdated BS4 versions successfully parse the HTML
|
||||||
|
split_data = data.partition('-->')
|
||||||
|
if '## SELECT COUNT(' in split_data[0]:
|
||||||
|
data = split_data[2]
|
||||||
|
|
||||||
|
html = BeautifulSoup(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result_table = html.find('table', attrs = {'width': '750', 'class': ''})
|
||||||
|
if result_table is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
entries = result_table.find_all('tr')
|
||||||
|
for result in entries[1:]:
|
||||||
|
|
||||||
|
cells = result.find_all('td')
|
||||||
|
link = cells[2].find('a')
|
||||||
|
torrent_id = link['href'].replace('/details.php?id=', '')
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'id': torrent_id,
|
||||||
|
'name': link.contents[0].get_text(),
|
||||||
|
'url': cells[0].find('a')['href'],
|
||||||
|
'detail_url': self.urls['detail'] % torrent_id,
|
||||||
|
'size': self.parseSize(cells[6].get_text()),
|
||||||
|
'seeders': tryInt(cells[8].string),
|
||||||
|
'leechers': tryInt(cells[9].string),
|
||||||
|
'get_more_info': self.getMoreInfo,
|
||||||
|
})
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
|
def getLoginParams(self):
|
||||||
|
return {
|
||||||
|
'username': self.conf('username'),
|
||||||
|
'password': self.conf('password'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def getMoreInfo(self, item):
|
||||||
|
full_description = self.getCache('bithdtv.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
||||||
|
html = BeautifulSoup(full_description)
|
||||||
|
nfo_pre = html.find('table', attrs = {'class': 'detail'})
|
||||||
|
description = toUnicode(nfo_pre.text) if nfo_pre else ''
|
||||||
|
|
||||||
|
item['description'] = description
|
||||||
|
return item
|
||||||
|
|
||||||
|
def loginSuccess(self, output):
|
||||||
|
return 'logout.php' in output.lower()
|
||||||
|
|
||||||
|
loginCheckSuccess = loginSuccess
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'bithdtv',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'BiT-HDTV',
|
||||||
|
'description': '<a href="http://bit-hdtv.com">BiT-HDTV</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAABMklEQVR4AZ3Qu0ojcQCF8W9MJcQbJNgEEQUbQVIqWgnaWfkIvoCgggixEAmIhRtY2GV3w7KwU61B0EYIxmiw0YCik84ipaCuc0nmP5dcjIUgOjqDvxf4OAdf9mnMLcUJyPyGSCP+YRdC+Kp8iagJKhuS+InYRhTGgDbeV2uEMand4ZRxizjXHQEimxhraAnUr73BNqQxMiNeV2SwcjTLEVtb4Zl10mXutvOWm2otw5Sxz6TGTbdd6ncuYvVLXAXrvM+ruyBpy1S3JLGDfUQ1O6jn5vTsrJXvqSt4UNfj6vxTRPxBHER5QeSirhLGk/5rWN+ffB1XZuxjnDy1q87m7TS+xOGA+Iv4gfkbaw+nOMXHDHnITGEk0VfRFnn4Po4vNYm6RGukmggR0L08+l+e4HMeASo/i6AJUjLgAAAAAElFTkSuQmCC',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'default': '',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_ratio',
|
||||||
|
'label': 'Seed ratio',
|
||||||
|
'type': 'float',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_time',
|
||||||
|
'label': 'Seed time',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 40,
|
||||||
|
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 20,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import traceback
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup, SoupStrainer
|
||||||
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(TorrentProvider):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'test': 'https://www.bitsoup.me/',
|
||||||
|
'login': 'https://www.bitsoup.me/takelogin.php',
|
||||||
|
'login_check': 'https://www.bitsoup.me/my.php',
|
||||||
|
'search': 'https://www.bitsoup.me/browse.php?%s',
|
||||||
|
'baseurl': 'https://www.bitsoup.me/%s',
|
||||||
|
}
|
||||||
|
|
||||||
|
http_time_between_calls = 1 # Seconds
|
||||||
|
only_tables_tags = SoupStrainer('table')
|
||||||
|
|
||||||
|
def _searchOnTitle(self, title, movie, quality, results):
|
||||||
|
|
||||||
|
url = self.urls['search'] % self.buildUrl(title, movie, quality)
|
||||||
|
data = self.getHTMLData(url)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
html = BeautifulSoup(data, 'html.parser', parse_only = self.only_tables_tags)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result_table = html.find('table', attrs = {'class': 'koptekst'})
|
||||||
|
if not result_table or 'nothing found!' in data.lower():
|
||||||
|
return
|
||||||
|
|
||||||
|
entries = result_table.find_all('tr')
|
||||||
|
for result in entries[1:]:
|
||||||
|
|
||||||
|
all_cells = result.find_all('td')
|
||||||
|
|
||||||
|
torrent = all_cells[1].find('a')
|
||||||
|
download = all_cells[3].find('a')
|
||||||
|
|
||||||
|
torrent_id = torrent['href']
|
||||||
|
torrent_id = torrent_id.replace('details.php?id=', '')
|
||||||
|
torrent_id = torrent_id.replace('&hit=1', '')
|
||||||
|
|
||||||
|
torrent_name = torrent.getText()
|
||||||
|
|
||||||
|
torrent_size = self.parseSize(all_cells[7].getText())
|
||||||
|
torrent_seeders = tryInt(all_cells[9].getText())
|
||||||
|
torrent_leechers = tryInt(all_cells[10].getText())
|
||||||
|
torrent_url = self.urls['baseurl'] % download['href']
|
||||||
|
torrent_detail_url = self.urls['baseurl'] % torrent['href']
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'id': torrent_id,
|
||||||
|
'name': torrent_name,
|
||||||
|
'size': torrent_size,
|
||||||
|
'seeders': torrent_seeders,
|
||||||
|
'leechers': torrent_leechers,
|
||||||
|
'url': torrent_url,
|
||||||
|
'detail_url': torrent_detail_url,
|
||||||
|
})
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
|
def getLoginParams(self):
|
||||||
|
return {
|
||||||
|
'username': self.conf('username'),
|
||||||
|
'password': self.conf('password'),
|
||||||
|
'ssl': 'yes',
|
||||||
|
}
|
||||||
|
|
||||||
|
def loginSuccess(self, output):
|
||||||
|
return 'logout.php' in output.lower()
|
||||||
|
|
||||||
|
loginCheckSuccess = loginSuccess
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'bitsoup',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'Bitsoup',
|
||||||
|
'description': '<a href="https://bitsoup.me">Bitsoup</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAB8ElEQVR4AbWSS2sTURiGz3euk0mswaE37HhNhIrajQheFgF3rgR/lAt/gOBCXNZlo6AbqfUWRVCxi04wqUnTRibpJLaJzdzOOZ6WUumyC5/VHOb9eN/FA91uFx0FjI4IPfgiGLTWH73tn348GKmN7ijD0d2b41fO5qJEaX24AWNIUrVQCTTJ3Llx6vbV6Vtzk7Gi9+ebi996guFDDYAQAVj4FExP5qdOZB49W62t/zH3hECcwsPnbWeMXz6Xi2K1f0ApeK3hMCHHbP5gvvoriBgFAAQJEAxhjJ4u+YWTNsVI6b1JgtPWZkoIefKy4fcii2OTw2BABs7wj3bYDlLL4rvjGWOdTser1j5Xf7c3Q/MbHQYApxItvnm31mhQQ71eX2vUB76/vsWB2hg0QuogrMwLIG8P3InM2/eVGXeDViqVwWB79vRU2lgJYmdHcgXCTAXQFJTN5HguvDCR2Hxsxe8EvT54nlcul5vNpqDIEgwRQanAhAAABgRIyiQcjpIkkTOuWyqVoN/vSylX67XXH74uV1vHRUyxxFqbLBCSmBpiXSq6xcL5QrGYzWZ3XQIAwdlOJB+/aL764ucdmncYs0WsCI7kvTnn+qyDMEnTVCn1Tz5KsBFg6fvWcmsUAcnYNC/g2hnromvvqbHvxv+39S+MX+bWkFXwAgAAAABJRU5ErkJggg==',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'default': '',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_ratio',
|
||||||
|
'label': 'Seed ratio',
|
||||||
|
'type': 'float',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_time',
|
||||||
|
'label': 'Seed time',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 40,
|
||||||
|
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 20,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import re
|
||||||
|
import json
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from couchpotato.core.helpers.variable import tryInt, getIdentifier
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(TorrentProvider):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'test': 'https://hdbits.org/',
|
||||||
|
'detail': 'https://hdbits.org/details.php?id=%s',
|
||||||
|
'download': 'https://hdbits.org/download.php?id=%s&passkey=%s',
|
||||||
|
'api': 'https://hdbits.org/api/torrents'
|
||||||
|
}
|
||||||
|
|
||||||
|
http_time_between_calls = 1 # Seconds
|
||||||
|
|
||||||
|
def _post_query(self, **params):
|
||||||
|
|
||||||
|
post_data = {
|
||||||
|
'username': self.conf('username'),
|
||||||
|
'passkey': self.conf('passkey')
|
||||||
|
}
|
||||||
|
post_data.update(params)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = self.getJsonData(self.urls['api'], data = json.dumps(post_data))
|
||||||
|
|
||||||
|
if result:
|
||||||
|
if result['status'] != 0:
|
||||||
|
log.error('Error searching hdbits: %s' % result['message'])
|
||||||
|
else:
|
||||||
|
return result['data']
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _search(self, movie, quality, results):
|
||||||
|
|
||||||
|
match = re.match(r'tt(\d{7})', getIdentifier(movie))
|
||||||
|
|
||||||
|
data = self._post_query(imdb = {'id': match.group(1)})
|
||||||
|
|
||||||
|
if data:
|
||||||
|
try:
|
||||||
|
for result in data:
|
||||||
|
results.append({
|
||||||
|
'id': result['id'],
|
||||||
|
'name': result['name'],
|
||||||
|
'url': self.urls['download'] % (result['id'], self.conf('passkey')),
|
||||||
|
'detail_url': self.urls['detail'] % result['id'],
|
||||||
|
'size': tryInt(result['size']) / 1024 / 1024,
|
||||||
|
'seeders': tryInt(result['seeders']),
|
||||||
|
'leechers': tryInt(result['leechers'])
|
||||||
|
})
|
||||||
|
except:
|
||||||
|
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'hdbits',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'HDBits',
|
||||||
|
'wizard': True,
|
||||||
|
'description': '<a href="http://hdbits.org">HDBits</a>',
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABi0lEQVR4AZWSzUsbQRjGdyabTcvSNPTSHlpQQeMHJApC8CJRvHgQQU969+LJP8G7f4N3DwpeFRQvRr0EKaUl0ATSpkigUNFsMl/r9NmZLCEHA/nNO5PfvMPDm0DI6fV3ZxiolEICe1oZCBVCCmBPKwOh2ErKBHGE4KYEXBpSLkUlqO4LcM7f+6nVhRnOhSkOz/hexk+tL+YL0yPF2YmN4tynD++4gTLGkNNac9YFLoREBR1+cnF3dFY6v/m6PD+FaXiNJtgA4xYbABxiGrz6+6HWaI5/+Qh37YS0/3Znc8UxwNGBIIBX22z+/ZdJ+4wzyjpR4PEpODg8tgUXBv2iWUzSpa12B0IR6n6lvt8Aek2lZHb084+fdRNgrwY8z81PjhVy2d2ttUrtV/lbBa+JXGEpDMPnoF2tN1QYRqVUtf6nFbThb7wk7le395elcqhASLb39okDiHY00VCtCTEHwSiH4AI0lkOiT1dwMeSfT3SRxiQWNO7Zwj1egkoVIQFMKvSiC3bcjXq9Jf8DcDIRT3hh10kAAAAASUVORK5CYII=',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'passkey',
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_ratio',
|
||||||
|
'label': 'Seed ratio',
|
||||||
|
'type': 'float',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_time',
|
||||||
|
'label': 'Seed time',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 40,
|
||||||
|
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 0,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
+86
-19
@@ -1,19 +1,21 @@
|
|||||||
from bs4 import BeautifulSoup
|
|
||||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
|
||||||
from couchpotato.core.helpers.variable import tryInt
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
|
||||||
import re
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||||
|
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ILoveTorrents(TorrentProvider):
|
class Base(TorrentProvider):
|
||||||
|
|
||||||
urls = {
|
urls = {
|
||||||
'download': 'https://www.ilovetorrents.me/%s',
|
'download': 'https://www.ilovetorrents.me/%s',
|
||||||
'detail': 'https//www.ilovetorrents.me/%s',
|
'detail': 'https://www.ilovetorrents.me/%s',
|
||||||
'search': 'https://www.ilovetorrents.me/browse.php?search=%s&page=%s&cat=%s',
|
'search': 'https://www.ilovetorrents.me/browse.php?search=%s&page=%s&cat=%s',
|
||||||
'test': 'https://www.ilovetorrents.me/',
|
'test': 'https://www.ilovetorrents.me/',
|
||||||
'login': 'https://www.ilovetorrents.me/takelogin.php',
|
'login': 'https://www.ilovetorrents.me/takelogin.php',
|
||||||
@@ -21,9 +23,9 @@ class ILoveTorrents(TorrentProvider):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cat_ids = [
|
cat_ids = [
|
||||||
(['41'], ['720p', '1080p', 'brrip']),
|
(['41'], ['720p', '1080p', 'brrip']),
|
||||||
(['19'], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
|
(['19'], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
|
||||||
(['20'], ['dvdr'])
|
(['20'], ['dvdr'])
|
||||||
]
|
]
|
||||||
|
|
||||||
cat_backup_id = 200
|
cat_backup_id = 200
|
||||||
@@ -34,28 +36,35 @@ class ILoveTorrents(TorrentProvider):
|
|||||||
|
|
||||||
page = 0
|
page = 0
|
||||||
total_pages = 1
|
total_pages = 1
|
||||||
cats = self.getCatId(quality['identifier'])
|
cats = self.getCatId(quality)
|
||||||
|
|
||||||
while page < total_pages:
|
while page < total_pages:
|
||||||
|
|
||||||
movieTitle = tryUrlencode('"%s" %s' % (title, movie['library']['year']))
|
movieTitle = tryUrlencode('"%s" %s' % (title, movie['info']['year']))
|
||||||
search_url = self.urls['search'] % (movieTitle, page, cats[0])
|
search_url = self.urls['search'] % (movieTitle, page, cats[0])
|
||||||
page += 1
|
page += 1
|
||||||
|
|
||||||
data = self.getHTMLData(search_url)
|
data = self.getHTMLData(search_url)
|
||||||
if data:
|
if data:
|
||||||
try:
|
try:
|
||||||
soup = BeautifulSoup(data)
|
|
||||||
|
|
||||||
results_table = soup.find('table', attrs = {'class': 'koptekst'})
|
results_table = None
|
||||||
|
|
||||||
|
data_split = splitString(data, '<table')
|
||||||
|
soup = None
|
||||||
|
for x in data_split:
|
||||||
|
soup = BeautifulSoup(x)
|
||||||
|
results_table = soup.find('table', attrs = {'class': 'koptekst'})
|
||||||
|
if results_table:
|
||||||
|
break
|
||||||
|
|
||||||
if not results_table:
|
if not results_table:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pagelinks = soup.findAll(href = re.compile('page'))
|
pagelinks = soup.findAll(href = re.compile('page'))
|
||||||
pageNumbers = [int(re.search('page=(?P<pageNumber>.+'')', i['href']).group('pageNumber')) for i in pagelinks]
|
page_numbers = [int(re.search('page=(?P<page_number>.+'')', i['href']).group('page_number')) for i in pagelinks]
|
||||||
total_pages = max(pageNumbers)
|
total_pages = max(page_numbers)
|
||||||
|
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -77,7 +86,7 @@ class ILoveTorrents(TorrentProvider):
|
|||||||
return confirmed + trusted + vip + moderated
|
return confirmed + trusted + vip + moderated
|
||||||
|
|
||||||
id = re.search('id=(?P<id>\d+)&', link).group('id')
|
id = re.search('id=(?P<id>\d+)&', link).group('id')
|
||||||
url = self.urls['download'] % (download)
|
url = self.urls['download'] % download
|
||||||
|
|
||||||
fileSize = self.parseSize(result.select('td.rowhead')[5].text)
|
fileSize = self.parseSize(result.select('td.rowhead')[5].text)
|
||||||
results.append({
|
results.append({
|
||||||
@@ -111,7 +120,7 @@ class ILoveTorrents(TorrentProvider):
|
|||||||
try:
|
try:
|
||||||
full_description = self.getHTMLData(item['detail_url'])
|
full_description = self.getHTMLData(item['detail_url'])
|
||||||
html = BeautifulSoup(full_description)
|
html = BeautifulSoup(full_description)
|
||||||
nfo_pre = html.find('td', attrs = {'class':'main'}).findAll('table')[1]
|
nfo_pre = html.find('td', attrs = {'class': 'main'}).findAll('table')[1]
|
||||||
description = toUnicode(nfo_pre.text) if nfo_pre else ''
|
description = toUnicode(nfo_pre.text) if nfo_pre else ''
|
||||||
except:
|
except:
|
||||||
log.error('Failed getting more info for %s', item['name'])
|
log.error('Failed getting more info for %s', item['name'])
|
||||||
@@ -126,3 +135,61 @@ class ILoveTorrents(TorrentProvider):
|
|||||||
return 'logout.php' in output.lower()
|
return 'logout.php' in output.lower()
|
||||||
|
|
||||||
loginCheckSuccess = loginSuccess
|
loginCheckSuccess = loginSuccess
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'ilovetorrents',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'ILoveTorrents',
|
||||||
|
'description': 'Where the Love of Torrents is Born',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACPUlEQVR4AYWM0U9SbxjH3+v266I/oNvWZTfd2J1d0ZqbZEFwWrUImOKs4YwtumFKZvvlJJADR2TCQQlMPKg5NmpREgaekAPnBATKgmK1LqQlx6awHnZWF1Tr2Xfvvs+7z+dB0mlO7StpAh+M4S/2jbo3w8+xvJvlnSneEt+10zwer5ujNUOoChjALWFw5XOwdCAk/P57cGvPl+Oht0W7VJHN5NC1uW1BON4hGjXbwpVWMZhsy9v7sEIXAsDNYBXgdkEoIKyWD2CF8ut/aOXTZc/fBSgLWw1BgA4BDHOV0GkT90cBQpXahU5TFomsb38XhJC5/Tbh1P8c6rJlBeGfAeyMhUFwNVcs9lxV9Ot0dwmyd+mrNvRtbJ2fSPC6Z3Vsvub2z3sDFACAAYzk0+kUyxEkyfN7PopqNBro55A+P6yPKIrL5zF1HwjdeBJJCObIsZO79bo3sHhWhglo5WMV3mazuVPb4fLvSL8/FAkB1hK6rXQPwYhMyROK8VK5LAiH/jsMt0HQjxiN4/ePdoilllcqDyt3Mkg8mRBNbIhMb8RERkowQA/p76g0/UDDdCoNmDminM0qSK5vlpE5kugCHhNPxntwWmJPYTMZtYcFR6ABHQsVRlYLukVORaaULvqKI46keFSCv77kSPS6kxrPptLNDHgz16fWBtyxe6v5h08LUy+KI8ushqTPWWIX8Sg6b45IrGtyW6zXFb/hpQf9m3oqfWuB0fpSw0uZ4WB69En69uOk2rmO2V52PXj+A/mI4ESKpb2HAAAAAElFTkSuQmCC',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': False
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
'label': 'Username',
|
||||||
|
'type': 'string',
|
||||||
|
'default': '',
|
||||||
|
'description': 'The user name for your ILT account',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'label': 'Password',
|
||||||
|
'type': 'password',
|
||||||
|
'default': '',
|
||||||
|
'description': 'The password for your ILT account.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_ratio',
|
||||||
|
'label': 'Seed ratio',
|
||||||
|
'type': 'float',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_time',
|
||||||
|
'label': 'Seed time',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 40,
|
||||||
|
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 0,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
import traceback
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||||
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||||
|
import six
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(TorrentProvider):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'test': 'https://www.iptorrents.com/',
|
||||||
|
'base_url': 'https://www.iptorrents.com',
|
||||||
|
'login': 'https://www.iptorrents.com/torrents/',
|
||||||
|
'login_check': 'https://www.iptorrents.com/inbox.php',
|
||||||
|
'search': 'https://www.iptorrents.com/torrents/?%s%%s&q=%s&qf=ti&p=%%d',
|
||||||
|
}
|
||||||
|
|
||||||
|
http_time_between_calls = 1 # Seconds
|
||||||
|
cat_backup_id = None
|
||||||
|
|
||||||
|
def buildUrl(self, title, media, quality):
|
||||||
|
return self._buildUrl(title.replace(':', ''), quality)
|
||||||
|
|
||||||
|
def _buildUrl(self, query, quality):
|
||||||
|
|
||||||
|
cat_ids = self.getCatId(quality)
|
||||||
|
|
||||||
|
if not cat_ids:
|
||||||
|
log.warning('Unable to find category ids for identifier "%s"', quality.get('identifier'))
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self.urls['search'] % ("&".join(("l%d=" % x) for x in cat_ids), tryUrlencode(query).replace('%', '%%'))
|
||||||
|
|
||||||
|
def _searchOnTitle(self, title, media, quality, results):
|
||||||
|
|
||||||
|
freeleech = '' if not self.conf('freeleech') else '&free=on'
|
||||||
|
|
||||||
|
base_url = self.buildUrl(title, media, quality)
|
||||||
|
if not base_url: return
|
||||||
|
|
||||||
|
pages = 1
|
||||||
|
current_page = 1
|
||||||
|
while current_page <= pages and not self.shuttingDown():
|
||||||
|
data = self.getHTMLData(base_url % (freeleech, current_page))
|
||||||
|
|
||||||
|
if data:
|
||||||
|
html = BeautifulSoup(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
page_nav = html.find('span', attrs = {'class': 'page_nav'})
|
||||||
|
if page_nav:
|
||||||
|
next_link = page_nav.find("a", text = "Next")
|
||||||
|
if next_link:
|
||||||
|
final_page_link = next_link.previous_sibling.previous_sibling
|
||||||
|
pages = int(final_page_link.string)
|
||||||
|
|
||||||
|
result_table = html.find('table', attrs = {'class': 'torrents'})
|
||||||
|
|
||||||
|
if not result_table or 'nothing found!' in data.lower():
|
||||||
|
return
|
||||||
|
|
||||||
|
entries = result_table.find_all('tr')
|
||||||
|
|
||||||
|
for result in entries[1:]:
|
||||||
|
|
||||||
|
torrent = result.find_all('td')
|
||||||
|
if len(torrent) <= 1:
|
||||||
|
break
|
||||||
|
|
||||||
|
torrent = torrent[1].find('a')
|
||||||
|
|
||||||
|
torrent_id = torrent['href'].replace('/details.php?id=', '')
|
||||||
|
torrent_name = six.text_type(torrent.string)
|
||||||
|
torrent_download_url = self.urls['base_url'] + (result.find_all('td')[3].find('a'))['href'].replace(' ', '.')
|
||||||
|
torrent_details_url = self.urls['base_url'] + torrent['href']
|
||||||
|
torrent_size = self.parseSize(result.find_all('td')[5].string)
|
||||||
|
torrent_seeders = tryInt(result.find('td', attrs = {'class': 'ac t_seeders'}).string)
|
||||||
|
torrent_leechers = tryInt(result.find('td', attrs = {'class': 'ac t_leechers'}).string)
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'id': torrent_id,
|
||||||
|
'name': torrent_name,
|
||||||
|
'url': torrent_download_url,
|
||||||
|
'detail_url': torrent_details_url,
|
||||||
|
'size': torrent_size,
|
||||||
|
'seeders': torrent_seeders,
|
||||||
|
'leechers': torrent_leechers,
|
||||||
|
})
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
break
|
||||||
|
|
||||||
|
current_page += 1
|
||||||
|
|
||||||
|
def getLoginParams(self):
|
||||||
|
return {
|
||||||
|
'username': self.conf('username'),
|
||||||
|
'password': self.conf('password'),
|
||||||
|
'login': 'submit',
|
||||||
|
}
|
||||||
|
|
||||||
|
def loginSuccess(self, output):
|
||||||
|
return 'don\'t have an account' not in output.lower()
|
||||||
|
|
||||||
|
def loginCheckSuccess(self, output):
|
||||||
|
return '/logout.php' in output.lower()
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'iptorrents',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'IPTorrents',
|
||||||
|
'description': '<a href="http://www.iptorrents.com">IPTorrents</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABRklEQVR42qWQO0vDUBiG8zeKY3EqQUtNO7g0J6ZJ1+ifKIIFQXAqDYKCyaaYxM3udrZLHdRFhXrZ6liCW6mubfk874EESgqaeOCF7/Y8hEh41aq6yZi2nyZgBGya9XKtZs4No05pAkZV2YbEmyMMsoSxLQeC46wCTdPPY4HruPQyGIhF97qLWsS78Miydn4XdK46NJ9OsQAYBzMIMf8MQ9wtCnTdWCaIDx/u7uljOIQEe0hiIWPamSTLay3+RxOCSPI9+RJAo7Er9r2bnqjBFAqyK+VyK4f5/Cr5ni8OFKVCz49PFI5GdNvvU7ttE1M1zMU+8AMqFksEhrMnQsBDzqmDAwzx2ehRLwT7yyCI+vSC99c3mozH1NxrJgWWtR1BOECfEJSVCm6WCzJGCA7+IWhBsM4zywDPwEp4vCjx2DzBH2ODAfsDb33Ps6dQwJgAAAAASUVORK5CYII=',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'default': '',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'freeleech',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Only search for [FreeLeech] torrents.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_ratio',
|
||||||
|
'label': 'Seed ratio',
|
||||||
|
'type': 'float',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_time',
|
||||||
|
'label': 'Seed time',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 40,
|
||||||
|
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 0,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from couchpotato.core.helpers.variable import tryInt, getIdentifier
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(TorrentMagnetProvider):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'detail': '%s/%s',
|
||||||
|
'search': '%s/%s-i%s/',
|
||||||
|
}
|
||||||
|
|
||||||
|
cat_ids = [
|
||||||
|
(['cam'], ['cam']),
|
||||||
|
(['telesync'], ['ts', 'tc']),
|
||||||
|
(['screener', 'tvrip'], ['screener']),
|
||||||
|
(['x264', '720p', '1080p', 'blu-ray', 'hdrip'], ['bd50', '1080p', '720p', 'brrip']),
|
||||||
|
(['dvdrip'], ['dvdrip']),
|
||||||
|
(['dvd'], ['dvdr']),
|
||||||
|
]
|
||||||
|
|
||||||
|
http_time_between_calls = 1 # Seconds
|
||||||
|
cat_backup_id = None
|
||||||
|
|
||||||
|
proxy_list = [
|
||||||
|
'https://kickass.to',
|
||||||
|
'http://kickass.pw',
|
||||||
|
'http://kickassto.come.in',
|
||||||
|
'http://katproxy.ws',
|
||||||
|
'http://www.kickassunblock.info',
|
||||||
|
'http://www.kickassproxy.info',
|
||||||
|
'http://katph.eu',
|
||||||
|
'http://kickassto.come.in',
|
||||||
|
]
|
||||||
|
|
||||||
|
def _search(self, media, quality, results):
|
||||||
|
|
||||||
|
data = self.getHTMLData(self.urls['search'] % (self.getDomain(), 'm', getIdentifier(media).replace('tt', '')))
|
||||||
|
|
||||||
|
if data:
|
||||||
|
|
||||||
|
cat_ids = self.getCatId(quality)
|
||||||
|
table_order = ['name', 'size', None, 'age', 'seeds', 'leechers']
|
||||||
|
|
||||||
|
try:
|
||||||
|
html = BeautifulSoup(data)
|
||||||
|
resultdiv = html.find('div', attrs = {'class': 'tabs'})
|
||||||
|
for result in resultdiv.find_all('div', recursive = False):
|
||||||
|
if result.get('id').lower().strip('tab-') not in cat_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
for temp in result.find_all('tr'):
|
||||||
|
if temp['class'] is 'firstr' or not temp.get('id'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
new = {}
|
||||||
|
|
||||||
|
nr = 0
|
||||||
|
for td in temp.find_all('td'):
|
||||||
|
column_name = table_order[nr]
|
||||||
|
if column_name:
|
||||||
|
|
||||||
|
if column_name == 'name':
|
||||||
|
link = td.find('div', {'class': 'torrentname'}).find_all('a')[2]
|
||||||
|
new['id'] = temp.get('id')[-7:]
|
||||||
|
new['name'] = link.text
|
||||||
|
new['url'] = td.find('a', 'imagnet')['href']
|
||||||
|
new['detail_url'] = self.urls['detail'] % (self.getDomain(), link['href'][1:])
|
||||||
|
new['verified'] = True if td.find('a', 'iverify') else False
|
||||||
|
new['score'] = 100 if new['verified'] else 0
|
||||||
|
elif column_name is 'size':
|
||||||
|
new['size'] = self.parseSize(td.text)
|
||||||
|
elif column_name is 'age':
|
||||||
|
new['age'] = self.ageToDays(td.text)
|
||||||
|
elif column_name is 'seeds':
|
||||||
|
new['seeders'] = tryInt(td.text)
|
||||||
|
elif column_name is 'leechers':
|
||||||
|
new['leechers'] = tryInt(td.text)
|
||||||
|
|
||||||
|
nr += 1
|
||||||
|
|
||||||
|
# Only store verified torrents
|
||||||
|
if self.conf('only_verified') and not new['verified']:
|
||||||
|
continue
|
||||||
|
|
||||||
|
results.append(new)
|
||||||
|
except:
|
||||||
|
log.error('Failed parsing KickAssTorrents: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
except AttributeError:
|
||||||
|
log.debug('No search results found.')
|
||||||
|
|
||||||
|
def ageToDays(self, age_str):
|
||||||
|
age = 0
|
||||||
|
age_str = age_str.replace(' ', ' ')
|
||||||
|
|
||||||
|
regex = '(\d*.?\d+).(sec|hour|day|week|month|year)+'
|
||||||
|
matches = re.findall(regex, age_str)
|
||||||
|
for match in matches:
|
||||||
|
nr, size = match
|
||||||
|
mult = 1
|
||||||
|
if size == 'week':
|
||||||
|
mult = 7
|
||||||
|
elif size == 'month':
|
||||||
|
mult = 30.5
|
||||||
|
elif size == 'year':
|
||||||
|
mult = 365
|
||||||
|
|
||||||
|
age += tryInt(nr) * mult
|
||||||
|
|
||||||
|
return tryInt(age)
|
||||||
|
|
||||||
|
def isEnabled(self):
|
||||||
|
return super(Base, self).isEnabled() and self.getDomain()
|
||||||
|
|
||||||
|
def correctProxy(self, data):
|
||||||
|
return 'search query' in data.lower()
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'kickasstorrents',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'KickAssTorrents',
|
||||||
|
'description': '<a href="https://kat.ph/">KickAssTorrents</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACD0lEQVR42pXK20uTcRjA8d/fsJsuap0orBuFlm3hir3JJvQOVmuwllN20Lb2isI2nVHKjBqrCWYaNnNuBrkSWxglhDVJOkBdSWUOq5FgoiOrMdRJ2xPPxW+8OUf1ge/FcyCUSVe2qedK5U/OxNTTXRNXEQ52Glb4O6dNEfK1auJkvRY7+/zxnQbA/D596laXcY3OWOiaIX2393SGznUmxkUo/YkDgqHemuzobQ7+NV+reo5Q1mqp68GABdY3+/EloO+JeN4tEqiFU8f3CwhyWo9E7wfMgI0ELTDx0AvjIxcgvZoC9P7NMN7yMmrFeoKa68rfDfmrARsNN0Ihr55cx59ctZWSiwS5bLKpwW4dYJH+M/B6/CYszE0BFZ+egG+Ln+HRoBN/cpl1pV6COIMkOnBVA/w+fXgGKJVM4LxhumMleoL06hJ3wKcCfl+/TAKKx17gnFePRwkqxR4BQSpFkbCrrQJueI7mWpyfATQ9OQY43+uv/+PutBycJ3y2qn2x7jY50GJvnwLKZjOwspyE5I8F4N+1yr1uwqcs3ym63Hwo29EiAyzUWQVr6WVAS4lZCPutQG/2GtES2YiW3d3XflYKtL72kzAcdEDHeSa3czeIMyyz/TApRKvcFfE0isHbJMnrHCf6xTLb1ORvWNlWo91cvHrJUQo0o6ZoRi7dIiT/g2WEDi27Iyov21xMCvgNfXvtwIACfHwAAAAASUVORK5CYII=',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'domain',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Proxy server',
|
||||||
|
'description': 'Domain for requests, keep empty to let CouchPotato pick.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_ratio',
|
||||||
|
'label': 'Seed ratio',
|
||||||
|
'type': 'float',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_time',
|
||||||
|
'label': 'Seed time',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 40,
|
||||||
|
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'only_verified',
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'default': False,
|
||||||
|
'description': 'Only search for verified releases.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 0,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
+110
-49
@@ -1,19 +1,21 @@
|
|||||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
|
||||||
from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
|
||||||
from dateutil.parser import parse
|
|
||||||
import htmlentitydefs
|
import htmlentitydefs
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||||
|
from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts, getIdentifier
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||||
|
from dateutil.parser import parse
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PassThePopcorn(TorrentProvider):
|
class Base(TorrentProvider):
|
||||||
|
|
||||||
urls = {
|
urls = {
|
||||||
'domain': 'https://tls.passthepopcorn.me',
|
'domain': 'https://tls.passthepopcorn.me',
|
||||||
@@ -26,43 +28,15 @@ class PassThePopcorn(TorrentProvider):
|
|||||||
|
|
||||||
http_time_between_calls = 2
|
http_time_between_calls = 2
|
||||||
|
|
||||||
quality_search_params = {
|
def _search(self, media, quality, results):
|
||||||
'bd50': {'media': 'Blu-ray', 'format': 'BD50'},
|
|
||||||
'1080p': {'resolution': '1080p'},
|
|
||||||
'720p': {'resolution': '720p'},
|
|
||||||
'brrip': {'media': 'Blu-ray'},
|
|
||||||
'dvdr': {'resolution': 'anysd'},
|
|
||||||
'dvdrip': {'media': 'DVD'},
|
|
||||||
'scr': {'media': 'DVD-Screener'},
|
|
||||||
'r5': {'media': 'R5'},
|
|
||||||
'tc': {'media': 'TC'},
|
|
||||||
'ts': {'media': 'TS'},
|
|
||||||
'cam': {'media': 'CAM'}
|
|
||||||
}
|
|
||||||
|
|
||||||
post_search_filters = {
|
movie_title = getTitle(media)
|
||||||
'bd50': {'Codec': ['BD50']},
|
|
||||||
'1080p': {'Resolution': ['1080p']},
|
|
||||||
'720p': {'Resolution': ['720p']},
|
|
||||||
'brrip': {'Source': ['Blu-ray'], 'Quality': ['High Definition'], 'Container': ['!ISO']},
|
|
||||||
'dvdr': {'Codec': ['DVD5', 'DVD9']},
|
|
||||||
'dvdrip': {'Source': ['DVD'], 'Codec': ['!DVD5', '!DVD9']},
|
|
||||||
'scr': {'Source': ['DVD-Screener']},
|
|
||||||
'r5': {'Source': ['R5']},
|
|
||||||
'tc': {'Source': ['TC']},
|
|
||||||
'ts': {'Source': ['TS']},
|
|
||||||
'cam': {'Source': ['CAM']}
|
|
||||||
}
|
|
||||||
|
|
||||||
def _search(self, movie, quality, results):
|
|
||||||
|
|
||||||
movie_title = getTitle(movie['library'])
|
|
||||||
quality_id = quality['identifier']
|
quality_id = quality['identifier']
|
||||||
|
|
||||||
params = mergeDicts(self.quality_search_params[quality_id].copy(), {
|
params = mergeDicts(self.quality_search_params[quality_id].copy(), {
|
||||||
'order_by': 'relevance',
|
'order_by': 'relevance',
|
||||||
'order_way': 'descending',
|
'order_way': 'descending',
|
||||||
'searchstr': movie['library']['identifier']
|
'searchstr': getIdentifier(media)
|
||||||
})
|
})
|
||||||
|
|
||||||
url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params))
|
url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params))
|
||||||
@@ -162,23 +136,23 @@ class PassThePopcorn(TorrentProvider):
|
|||||||
|
|
||||||
def htmlToUnicode(self, text):
|
def htmlToUnicode(self, text):
|
||||||
def fixup(m):
|
def fixup(m):
|
||||||
text = m.group(0)
|
txt = m.group(0)
|
||||||
if text[:2] == "&#":
|
if txt[:2] == "&#":
|
||||||
# character reference
|
# character reference
|
||||||
try:
|
try:
|
||||||
if text[:3] == "&#x":
|
if txt[:3] == "&#x":
|
||||||
return unichr(int(text[3:-1], 16))
|
return unichr(int(txt[3:-1], 16))
|
||||||
else:
|
else:
|
||||||
return unichr(int(text[2:-1]))
|
return unichr(int(txt[2:-1]))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
# named entity
|
# named entity
|
||||||
try:
|
try:
|
||||||
text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
|
txt = unichr(htmlentitydefs.name2codepoint[txt[1:-1]])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
return text # leave as is
|
return txt # leave as is
|
||||||
return re.sub("&#?\w+;", fixup, six.u('%s') % text)
|
return re.sub("&#?\w+;", fixup, six.u('%s') % text)
|
||||||
|
|
||||||
def unicodeToASCII(self, text):
|
def unicodeToASCII(self, text):
|
||||||
@@ -190,11 +164,11 @@ class PassThePopcorn(TorrentProvider):
|
|||||||
|
|
||||||
def getLoginParams(self):
|
def getLoginParams(self):
|
||||||
return {
|
return {
|
||||||
'username': self.conf('username'),
|
'username': self.conf('username'),
|
||||||
'password': self.conf('password'),
|
'password': self.conf('password'),
|
||||||
'passkey': self.conf('passkey'),
|
'passkey': self.conf('passkey'),
|
||||||
'keeplogged': '1',
|
'keeplogged': '1',
|
||||||
'login': 'Login'
|
'login': 'Login'
|
||||||
}
|
}
|
||||||
|
|
||||||
def loginSuccess(self, output):
|
def loginSuccess(self, output):
|
||||||
@@ -204,3 +178,90 @@ class PassThePopcorn(TorrentProvider):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
loginCheckSuccess = loginSuccess
|
loginCheckSuccess = loginSuccess
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'passthepopcorn',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'PassThePopcorn',
|
||||||
|
'description': '<a href="https://passthepopcorn.me">PassThePopcorn.me</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAARklEQVQoz2NgIAP8BwMiGWRpIN1JNWn/t6T9f532+W8GkNt7vzz9UkfarZVpb68BuWlbnqW1nU7L2DMx7eCoBlpqGOppCQB83zIgIg+wWQAAAABJRU5ErkJggg==',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': False
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'domain',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Proxy server',
|
||||||
|
'description': 'Domain for requests (HTTPS only!), keep empty to use default (tls.passthepopcorn.me).',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'default': '',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'passkey',
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'prefer_golden',
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'label': 'Prefer golden',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Favors Golden Popcorn-releases over all other releases.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'prefer_scene',
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'label': 'Prefer scene',
|
||||||
|
'default': 0,
|
||||||
|
'description': 'Favors scene-releases over non-scene releases.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'require_approval',
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'label': 'Require approval',
|
||||||
|
'default': 0,
|
||||||
|
'description': 'Require staff-approval for releases to be accepted.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_ratio',
|
||||||
|
'label': 'Seed ratio',
|
||||||
|
'type': 'float',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_time',
|
||||||
|
'label': 'Seed time',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 40,
|
||||||
|
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 20,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import traceback
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(TorrentProvider):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'test': 'https://www.sceneaccess.eu/',
|
||||||
|
'login': 'https://www.sceneaccess.eu/login',
|
||||||
|
'login_check': 'https://www.sceneaccess.eu/inbox',
|
||||||
|
'detail': 'https://www.sceneaccess.eu/details?id=%s',
|
||||||
|
'search': 'https://www.sceneaccess.eu/browse?c%d=%d',
|
||||||
|
'archive': 'https://www.sceneaccess.eu/archive?&c%d=%d',
|
||||||
|
'download': 'https://www.sceneaccess.eu/%s',
|
||||||
|
}
|
||||||
|
|
||||||
|
http_time_between_calls = 1 # Seconds
|
||||||
|
|
||||||
|
def _searchOnTitle(self, title, media, quality, results):
|
||||||
|
|
||||||
|
url = self.buildUrl(title, media, quality)
|
||||||
|
data = self.getHTMLData(url)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
html = BeautifulSoup(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
resultsTable = html.find('table', attrs = {'id': 'torrents-table'})
|
||||||
|
if resultsTable is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
entries = resultsTable.find_all('tr', attrs = {'class': 'tt_row'})
|
||||||
|
for result in entries:
|
||||||
|
|
||||||
|
link = result.find('td', attrs = {'class': 'ttr_name'}).find('a')
|
||||||
|
url = result.find('td', attrs = {'class': 'td_dl'}).find('a')
|
||||||
|
leechers = result.find('td', attrs = {'class': 'ttr_leechers'}).find('a')
|
||||||
|
torrent_id = link['href'].replace('details?id=', '')
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'id': torrent_id,
|
||||||
|
'name': link['title'],
|
||||||
|
'url': self.urls['download'] % url['href'],
|
||||||
|
'detail_url': self.urls['detail'] % torrent_id,
|
||||||
|
'size': self.parseSize(result.find('td', attrs = {'class': 'ttr_size'}).contents[0]),
|
||||||
|
'seeders': tryInt(result.find('td', attrs = {'class': 'ttr_seeders'}).find('a').string),
|
||||||
|
'leechers': tryInt(leechers.string) if leechers else 0,
|
||||||
|
'get_more_info': self.getMoreInfo,
|
||||||
|
})
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
|
def getMoreInfo(self, item):
|
||||||
|
full_description = self.getCache('sceneaccess.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
||||||
|
html = BeautifulSoup(full_description)
|
||||||
|
nfo_pre = html.find('div', attrs = {'id': 'details_table'})
|
||||||
|
description = toUnicode(nfo_pre.text) if nfo_pre else ''
|
||||||
|
|
||||||
|
item['description'] = description
|
||||||
|
return item
|
||||||
|
|
||||||
|
# Login
|
||||||
|
def getLoginParams(self):
|
||||||
|
return {
|
||||||
|
'username': self.conf('username'),
|
||||||
|
'password': self.conf('password'),
|
||||||
|
'submit': 'come on in',
|
||||||
|
}
|
||||||
|
|
||||||
|
def loginSuccess(self, output):
|
||||||
|
return '/inbox' in output.lower()
|
||||||
|
|
||||||
|
loginCheckSuccess = loginSuccess
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'sceneaccess',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'SceneAccess',
|
||||||
|
'description': '<a href="https://sceneaccess.eu/">SceneAccess</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAACT0lEQVR4AYVQS0sbURidO3OTmajJ5FElTTOkPmZ01GhHrIq0aoWAj1Vc+A/cuRMXbl24V9SlCGqrLhVFCrooEhCp2BAx0mobTY2kaR7qmOm87EXL1EWxh29xL+c7nPMdgGHYO5bF/gdbefnr6WlbWRnxluMwAB4Z0uEgXa7nwaDL7+/RNPzxbYvb/XJ0FBYVfd/ayh0fQ4qCGEHcm0KLRZUk7Pb2YRJPRwcsKMidnKD3t9VVT3s7BDh+z5FOZ3Vfn3h+Hltfx00mRRSRWFcUmmVNhYVqPn8dj3va2oh+txvcQRVF9ebm1fi4k+dRFbosY5rm4Hk7xxULQnJnx93S4g0EIEEQRoDLo6PrWEw8Pc0eHLwYGopMTDirqlJ7eyhYYGHhfgfHCcKYksZGVB/NcXI2mw6HhZERqrjYTNPHi4tFPh8aJIYIhgPlcCRDoZLW1s75+Z/7+59nZ/OJhLWigqAoKZX6Mjf3dXkZ3pydGYLc4aEoCCkInzQ1fRobS2xuvllaonkedfArnY5OTdGVldBkOADgqq2Nr6z8CIWaJietDHOhKB+HhwFKC6Gnq4ukKJvP9zcSbjYDXbeVlkKzuZBhnnV3e3t6UOmaJO0ODibW1hB1GYkg8R/gup7Z3TVZLJ5AILW9LcZiVpYtYBhw16O3t7cauckyeF9Tgz0ATpL2+nopmWycmbnY2LiKRjFk6/d7+/vRJfl4HGzV1T0UIM43MGBvaIBWK/YvwM5w+IMgGH8tkyEgvIpE7M3Nt6qqZrNyOq1kMmouh455Ggz+BhKY4GEc2CfwAAAAAElFTkSuQmCC',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'default': '',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_ratio',
|
||||||
|
'label': 'Seed ratio',
|
||||||
|
'type': 'float',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_time',
|
||||||
|
'label': 'Seed time',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 40,
|
||||||
|
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 20,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
+84
-29
@@ -1,54 +1,55 @@
|
|||||||
from bs4 import BeautifulSoup
|
|
||||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
|
||||||
from couchpotato.core.helpers.variable import tryInt
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.providers.torrent.base import TorrentMagnetProvider
|
|
||||||
import re
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ThePirateBay(TorrentMagnetProvider):
|
class Base(TorrentMagnetProvider):
|
||||||
|
|
||||||
urls = {
|
urls = {
|
||||||
'detail': '%s/torrent/%s',
|
'detail': '%s/torrent/%s',
|
||||||
'search': '%s/search/%s/%s/7/%s'
|
'search': '%s/search/%%s/%%s/7/%%s'
|
||||||
}
|
}
|
||||||
|
|
||||||
cat_ids = [
|
|
||||||
([207], ['720p', '1080p']),
|
|
||||||
([201], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
|
|
||||||
([201, 207], ['brrip']),
|
|
||||||
([202], ['dvdr'])
|
|
||||||
]
|
|
||||||
|
|
||||||
cat_backup_id = 200
|
cat_backup_id = 200
|
||||||
disable_provider = False
|
disable_provider = False
|
||||||
http_time_between_calls = 0
|
http_time_between_calls = 0
|
||||||
|
|
||||||
proxy_list = [
|
proxy_list = [
|
||||||
'https://tpb.ipredator.se',
|
'https://nobay.net',
|
||||||
|
'https://thebay.al',
|
||||||
'https://thepiratebay.se',
|
'https://thepiratebay.se',
|
||||||
'http://pirateproxy.ca',
|
'http://thepiratebay.cd',
|
||||||
'http://tpb.al',
|
'http://thebootlegbay.com',
|
||||||
'http://www.tpb.gr',
|
'http://www.tpb.gr',
|
||||||
'http://nl.tpb.li',
|
'http://tpbproxy.co.uk',
|
||||||
'http://proxybay.eu',
|
'http://pirateproxy.in',
|
||||||
'https://www.getpirate.com',
|
'http://www.getpirate.com',
|
||||||
'http://piratebay.io',
|
'http://piratebay.io',
|
||||||
|
'http://bayproxy.li',
|
||||||
|
'http://proxybay.pw',
|
||||||
]
|
]
|
||||||
|
|
||||||
def _searchOnTitle(self, title, movie, quality, results):
|
def _search(self, media, quality, results):
|
||||||
|
|
||||||
page = 0
|
page = 0
|
||||||
total_pages = 1
|
total_pages = 1
|
||||||
cats = self.getCatId(quality['identifier'])
|
cats = self.getCatId(quality)
|
||||||
|
|
||||||
|
base_search_url = self.urls['search'] % self.getDomain()
|
||||||
|
|
||||||
while page < total_pages:
|
while page < total_pages:
|
||||||
|
|
||||||
search_url = self.urls['search'] % (self.getDomain(), tryUrlencode('"%s" %s' % (title, movie['library']['year'])), page, ','.join(str(x) for x in cats))
|
search_url = base_search_url % self.buildUrl(media, page, cats)
|
||||||
|
|
||||||
page += 1
|
page += 1
|
||||||
|
|
||||||
data = self.getHTMLData(search_url)
|
data = self.getHTMLData(search_url)
|
||||||
@@ -67,7 +68,7 @@ class ThePirateBay(TorrentMagnetProvider):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
entries = results_table.find_all('tr')
|
entries = results_table.find_all('tr')
|
||||||
for result in entries[2:]:
|
for result in entries[1:]:
|
||||||
link = result.find(href = re.compile('torrent\/\d+\/'))
|
link = result.find(href = re.compile('torrent\/\d+\/'))
|
||||||
download = result.find(href = re.compile('magnet:'))
|
download = result.find(href = re.compile('magnet:'))
|
||||||
|
|
||||||
@@ -88,7 +89,7 @@ class ThePirateBay(TorrentMagnetProvider):
|
|||||||
|
|
||||||
results.append({
|
results.append({
|
||||||
'id': re.search('/(?P<id>\d+)/', link['href']).group('id'),
|
'id': re.search('/(?P<id>\d+)/', link['href']).group('id'),
|
||||||
'name': link.string,
|
'name': six.text_type(link.string),
|
||||||
'url': download['href'],
|
'url': download['href'],
|
||||||
'detail_url': self.getDomain(link['href']),
|
'detail_url': self.getDomain(link['href']),
|
||||||
'size': self.parseSize(size),
|
'size': self.parseSize(size),
|
||||||
@@ -102,7 +103,7 @@ class ThePirateBay(TorrentMagnetProvider):
|
|||||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
def isEnabled(self):
|
def isEnabled(self):
|
||||||
return super(ThePirateBay, self).isEnabled() and self.getDomain()
|
return super(Base, self).isEnabled() and self.getDomain()
|
||||||
|
|
||||||
def correctProxy(self, data):
|
def correctProxy(self, data):
|
||||||
return 'title="Pirate Search"' in data
|
return 'title="Pirate Search"' in data
|
||||||
@@ -111,7 +112,61 @@ class ThePirateBay(TorrentMagnetProvider):
|
|||||||
full_description = self.getCache('tpb.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
full_description = self.getCache('tpb.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
|
||||||
html = BeautifulSoup(full_description)
|
html = BeautifulSoup(full_description)
|
||||||
nfo_pre = html.find('div', attrs = {'class': 'nfo'})
|
nfo_pre = html.find('div', attrs = {'class': 'nfo'})
|
||||||
description = toUnicode(nfo_pre.text) if nfo_pre else ''
|
description = ''
|
||||||
|
try:
|
||||||
|
description = toUnicode(nfo_pre.text)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
item['description'] = description
|
item['description'] = description
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'thepiratebay',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'ThePirateBay',
|
||||||
|
'description': 'The world\'s largest bittorrent tracker. <a href="http://fucktimkuik.org/">ThePirateBay</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAA3UlEQVQY02P4DwT/YADIZvj//7qnozMYODmtAAusZoCDELDAegYGViZhAWZmRoYoqIDupfhNN1M3dTBEggXWMZg9jZRXV77YxhAOFpjDwMAPMoCXmcHsF1SAQZ6bQY2VgUEbKHClcAYzg3mINEO8jSCD478/DPsZmvqWblu1bOmStes3Pp0ezVDF4Gif0Hfx9///74/ObRZ2YNiZ47C8XIRBxFJR0jbSSUud4f9zAQWn8NTuziAt2zy5xIMM/z8LFX0E+fD/x0MRDCeA1v7Z++Y/FDzyvAtyBxIA+h8A8ZKLeT+lJroAAAAASUVORK5CYII=',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': False
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'domain',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Proxy server',
|
||||||
|
'description': 'Domain for requests, keep empty to let CouchPotato pick.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_ratio',
|
||||||
|
'label': 'Seed ratio',
|
||||||
|
'type': 'float',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_time',
|
||||||
|
'label': 'Seed time',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 40,
|
||||||
|
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 0,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
+61
-7
@@ -1,14 +1,16 @@
|
|||||||
|
import traceback
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||||
from couchpotato.core.helpers.variable import tryInt
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||||
import traceback
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TorrentBytes(TorrentProvider):
|
class Base(TorrentProvider):
|
||||||
|
|
||||||
urls = {
|
urls = {
|
||||||
'test': 'https://www.torrentbytes.net/',
|
'test': 'https://www.torrentbytes.net/',
|
||||||
@@ -29,19 +31,19 @@ class TorrentBytes(TorrentProvider):
|
|||||||
([20], ['dvdr']),
|
([20], ['dvdr']),
|
||||||
]
|
]
|
||||||
|
|
||||||
http_time_between_calls = 1 #seconds
|
http_time_between_calls = 1 # Seconds
|
||||||
cat_backup_id = None
|
cat_backup_id = None
|
||||||
|
|
||||||
def _searchOnTitle(self, title, movie, quality, results):
|
def _searchOnTitle(self, title, movie, quality, results):
|
||||||
|
|
||||||
url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])), self.getCatId(quality['identifier'])[0])
|
url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['info']['year'])), self.getCatId(quality)[0])
|
||||||
data = self.getHTMLData(url)
|
data = self.getHTMLData(url)
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
html = BeautifulSoup(data)
|
html = BeautifulSoup(data)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result_table = html.find('table', attrs = {'border' : '1'})
|
result_table = html.find('table', attrs = {'border': '1'})
|
||||||
if not result_table:
|
if not result_table:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@ class TorrentBytes(TorrentProvider):
|
|||||||
for result in entries[1:]:
|
for result in entries[1:]:
|
||||||
cells = result.find_all('td')
|
cells = result.find_all('td')
|
||||||
|
|
||||||
link = cells[1].find('a', attrs = {'class' : 'index'})
|
link = cells[1].find('a', attrs = {'class': 'index'})
|
||||||
|
|
||||||
full_id = link['href'].replace('details.php?id=', '')
|
full_id = link['href'].replace('details.php?id=', '')
|
||||||
torrent_id = full_id[:6]
|
torrent_id = full_id[:6]
|
||||||
@@ -80,3 +82,55 @@ class TorrentBytes(TorrentProvider):
|
|||||||
|
|
||||||
loginCheckSuccess = loginSuccess
|
loginCheckSuccess = loginSuccess
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'torrentbytes',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'TorrentBytes',
|
||||||
|
'description': '<a href="http://torrentbytes.net">TorrentBytes</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAeFBMVEUAAAAAAEQAA1QAEmEAKnQALHYAMoEAOokAQpIASYsASZgAS5UATZwATosATpgAVJ0AWZwAYZ4AZKAAaZ8Ab7IAcbMAfccAgcQAgcsAhM4AiscAjMkAmt0AoOIApecAp/EAqvQAs+kAt+wA3P8A4f8A//8VAAAfDbiaAl08AAAAjUlEQVQYGQXBO04DQRAFwHqz7Z8sECIl5f73ISRD5GBs7UxTlWfg9vYXnvJRQJqOL88D6BAwJtMMumHUVCl60aa6H93IrIv0b+157f1lpk+fm87lMWrZH0vncKbXdRUQrRmrh9C6Iwkq6rg4PXZcyXmbizzeV/g+rDra0rGve8jPKLSOJNi2AQAwAGjwD7ApPkEHdtPQAAAAAElFTkSuQmCC',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'default': '',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_ratio',
|
||||||
|
'label': 'Seed ratio',
|
||||||
|
'type': 'float',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_time',
|
||||||
|
'label': 'Seed time',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 40,
|
||||||
|
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 20,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(TorrentProvider):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'test': 'http://www.td.af/',
|
||||||
|
'login': 'http://www.td.af/torrents/',
|
||||||
|
'login_check': 'http://www.torrentday.com/userdetails.php',
|
||||||
|
'detail': 'http://www.td.af/details.php?id=%s',
|
||||||
|
'search': 'http://www.td.af/V3/API/API.php',
|
||||||
|
'download': 'http://www.td.af/download.php/%s/%s',
|
||||||
|
}
|
||||||
|
|
||||||
|
http_time_between_calls = 1 # Seconds
|
||||||
|
|
||||||
|
def _searchOnTitle(self, title, media, quality, results):
|
||||||
|
|
||||||
|
query = '"%s" %s' % (title, media['info']['year'])
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'/browse.php?': None,
|
||||||
|
'cata': 'yes',
|
||||||
|
'jxt': 8,
|
||||||
|
'jxw': 'b',
|
||||||
|
'search': query,
|
||||||
|
}
|
||||||
|
|
||||||
|
data = self.getJsonData(self.urls['search'], data = data)
|
||||||
|
try: torrents = data.get('Fs', [])[0].get('Cn', {}).get('torrents', [])
|
||||||
|
except: return
|
||||||
|
|
||||||
|
for torrent in torrents:
|
||||||
|
results.append({
|
||||||
|
'id': torrent['id'],
|
||||||
|
'name': torrent['name'],
|
||||||
|
'url': self.urls['download'] % (torrent['id'], torrent['fname']),
|
||||||
|
'detail_url': self.urls['detail'] % torrent['id'],
|
||||||
|
'size': self.parseSize(torrent.get('size')),
|
||||||
|
'seeders': tryInt(torrent.get('seed')),
|
||||||
|
'leechers': tryInt(torrent.get('leech')),
|
||||||
|
})
|
||||||
|
|
||||||
|
def getLoginParams(self):
|
||||||
|
return {
|
||||||
|
'username': self.conf('username'),
|
||||||
|
'password': self.conf('password'),
|
||||||
|
'submit.x': 18,
|
||||||
|
'submit.y': 11,
|
||||||
|
'submit': 'submit',
|
||||||
|
}
|
||||||
|
|
||||||
|
def loginSuccess(self, output):
|
||||||
|
return 'Password not correct' not in output
|
||||||
|
|
||||||
|
def loginCheckSuccess(self, output):
|
||||||
|
return 'logout.php' in output.lower()
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'torrentday',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'TorrentDay',
|
||||||
|
'description': '<a href="http://www.td.af/">TorrentDay</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAC5ElEQVQ4y12TXUgUURTH//fO7Di7foeQJH6gEEEIZZllVohfSG/6UA+RSFAQQj74VA8+Bj30lmAlRVSEvZRfhNhaka5ZUG1paKaW39tq5O6Ou+PM3M4o6m6X+XPPzD3zm/+dcy574r515WfIW8CZBM4YAA5Gc/aQC3yd7oXYEONcsISE5dTDh91HS0t7FEWhBUAeN9ynV/d9qJAgE4AECURAcVsGlCCnly26LMA0IQwTa52dje3d3e3hcPi8qqrrMjcVYI3EHCQZlkFOHBwR2QHh2ASAAIJxWGAQEDxjePhs3527XjJwnb37OHBq0T+Tyyjh+9KnEzNJ7nouc1Q/3A3HGsOvnJy+PSUlj81w2Lny9WuJ6+3AmTjD4HOcrdR2dWXLRQePvyaSLfQOPMPC8mC9iHCsOxSyzJCelzdSXlNzD5ujpb25Wbfc/XXJemTXF4+nnCNq+AMLe50uFfEJTiw4GXSFtiHL0SnIq66+p0kSArqO+eH3RdsAv9+f5vW7L7GICq6rmM8XBCAXlBw90rOyxibn5yzfkg/L09M52/jxqdESaIrBXHYZZbB1GX8cEpySxKIB8S5XcOnvqpli1zuwmrTtoLjw5LOK/eeuWsE4JH5IRPaPZKiKigmPp+5pa+u1aEjIMhEgrRkmi9mgxGUhM7LNJSzOzsE3+cOeExovXOjdytE0LV4zqNZUtV0uZzAGoGkhDH/2YHZiErmv4uyWQnZZWc+hoqL3WzlTExN5hhA8IEwkZWZOxwB++30YG/9GkYCPvqAaHAW5uWPROW86OmqCprUR7z1yZDAGQNuCvkoB/baIKUBWMTYymv+gra3eJNvjXu+B562tFyXqTJ6YuHK8rKwvBmC3vR7cOCPQLWFz8LnfXWUrJo9U19BwMyUlJRjTSMJ2ENxUiGxq9KXQfwqYlnWstvbR5aamG9g0uzM8Q4OFt++3NNixQ2NgYmeN03FOTUv7XVpV9aKisvLl1vN/WVhNc/Fi1NEAAAAASUVORK5CYII=',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'default': '',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_ratio',
|
||||||
|
'label': 'Seed ratio',
|
||||||
|
'type': 'float',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_time',
|
||||||
|
'label': 'Seed time',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 40,
|
||||||
|
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 0,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import traceback
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||||
|
import six
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(TorrentProvider):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'test': 'http://www.torrentleech.org/',
|
||||||
|
'login': 'http://www.torrentleech.org/user/account/login/',
|
||||||
|
'login_check': 'http://torrentleech.org/user/messages',
|
||||||
|
'detail': 'http://www.torrentleech.org/torrent/%s',
|
||||||
|
'search': 'http://www.torrentleech.org/torrents/browse/index/query/%s/categories/%d',
|
||||||
|
'download': 'http://www.torrentleech.org%s',
|
||||||
|
}
|
||||||
|
|
||||||
|
http_time_between_calls = 1 # Seconds
|
||||||
|
cat_backup_id = None
|
||||||
|
|
||||||
|
def _searchOnTitle(self, title, media, quality, results):
|
||||||
|
|
||||||
|
url = self.urls['search'] % self.buildUrl(title, media, quality)
|
||||||
|
|
||||||
|
data = self.getHTMLData(url)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
html = BeautifulSoup(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result_table = html.find('table', attrs = {'id': 'torrenttable'})
|
||||||
|
if not result_table:
|
||||||
|
return
|
||||||
|
|
||||||
|
entries = result_table.find_all('tr')
|
||||||
|
|
||||||
|
for result in entries[1:]:
|
||||||
|
|
||||||
|
link = result.find('td', attrs = {'class': 'name'}).find('a')
|
||||||
|
url = result.find('td', attrs = {'class': 'quickdownload'}).find('a')
|
||||||
|
details = result.find('td', attrs = {'class': 'name'}).find('a')
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'id': link['href'].replace('/torrent/', ''),
|
||||||
|
'name': six.text_type(link.string),
|
||||||
|
'url': self.urls['download'] % url['href'],
|
||||||
|
'detail_url': self.urls['download'] % details['href'],
|
||||||
|
'size': self.parseSize(result.find_all('td')[4].string),
|
||||||
|
'seeders': tryInt(result.find('td', attrs = {'class': 'seeders'}).string),
|
||||||
|
'leechers': tryInt(result.find('td', attrs = {'class': 'leechers'}).string),
|
||||||
|
})
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
|
def getLoginParams(self):
|
||||||
|
return {
|
||||||
|
'username': self.conf('username'),
|
||||||
|
'password': self.conf('password'),
|
||||||
|
'remember_me': 'on',
|
||||||
|
'login': 'submit',
|
||||||
|
}
|
||||||
|
|
||||||
|
def loginSuccess(self, output):
|
||||||
|
return '/user/account/logout' in output.lower() or 'welcome back' in output.lower()
|
||||||
|
|
||||||
|
loginCheckSuccess = loginSuccess
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'torrentleech',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'TorrentLeech',
|
||||||
|
'description': '<a href="http://torrentleech.org">TorrentLeech</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACHUlEQVR4AZVSO48SYRSdGTCBEMKzILLAWiybkKAGMZRUUJEoDZX7B9zsbuQPYEEjNLTQkYgJDwsoSaxspEBsCITXjjNAIKi8AkzceXgmbHQ1NJ5iMufmO9/9zrmXlCSJ+B8o75J8Pp/NZj0eTzweBy0Wi4PBYD6f12o1r9ebTCZx+22HcrnMsuxms7m6urTZ7LPZDMVYLBZ8ZV3yo8aq9Pq0wzCMTqe77dDv9y8uLyAWBH6xWOyL0K/56fcb+rrPgPZ6PZfLRe1fsl6vCUmGKIqoqNXqdDr9Dbjps9znUV0uTqdTjuPkDoVCIfcuJ4gizjMMm8u9vW+1nr04czqdK56c37CbKY9j2+1WEARZ0Gq1RFHAz2q1qlQqXxoN69HRcDjUarW8ZD6QUigUOnY8uKYH8N1sNkul9yiGw+F6vS4Rxn8EsodEIqHRaOSnq9T7ajQazWQycEIR1AEBYDabSZJyHDucJyegwWBQr9ebTCaKvHd4cCQANUU9evwQ1Ofz4YvUKUI43GE8HouSiFiNRhOowWBIpVLyHITJkuW3PwgAEf3pgIwxF5r+OplMEsk3CPT5szCMnY7EwUdhwUh/CXiej0Qi3idPz89fdrpdbsfBzH7S3Q9K5pP4c0sAKpVKoVAQGO1ut+t0OoFAQHkH2Da/3/+but3uarWK0ZMQoNdyucRutdttmqZxMTzY7XaYxsrgtUjEZrNhkSwWyy/0NCatZumrNQAAAABJRU5ErkJggg==',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'default': '',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_ratio',
|
||||||
|
'label': 'Seed ratio',
|
||||||
|
'type': 'float',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_time',
|
||||||
|
'label': 'Seed time',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 40,
|
||||||
|
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 20,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
+81
-22
@@ -1,45 +1,40 @@
|
|||||||
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
|
|
||||||
from couchpotato.core.helpers.variable import splitString, tryInt, tryFloat
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.providers.base import ResultList
|
|
||||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
import re
|
import re
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
|
from couchpotato.core.helpers.variable import splitString, tryInt, tryFloat
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.base import ResultList
|
||||||
|
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TorrentPotato(TorrentProvider):
|
class Base(TorrentProvider):
|
||||||
|
|
||||||
urls = {}
|
urls = {}
|
||||||
limits_reached = {}
|
limits_reached = {}
|
||||||
|
|
||||||
http_time_between_calls = 1 # Seconds
|
http_time_between_calls = 1 # Seconds
|
||||||
|
|
||||||
def search(self, movie, quality):
|
def search(self, media, quality):
|
||||||
hosts = self.getHosts()
|
hosts = self.getHosts()
|
||||||
|
|
||||||
results = ResultList(self, movie, quality, imdb_results = True)
|
results = ResultList(self, media, quality, imdb_results = True)
|
||||||
|
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
if self.isDisabled(host):
|
if self.isDisabled(host):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self._searchOnHost(host, movie, quality, results)
|
self._searchOnHost(host, media, quality, results)
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def _searchOnHost(self, host, movie, quality, results):
|
def _searchOnHost(self, host, media, quality, results):
|
||||||
|
|
||||||
arguments = tryUrlencode({
|
torrents = self.getJsonData(self.buildUrl(media, host), cache_timeout = 1800)
|
||||||
'user': host['name'],
|
|
||||||
'passkey': host['pass_key'],
|
|
||||||
'imdbid': movie['library']['identifier']
|
|
||||||
})
|
|
||||||
url = '%s?%s' % (host['host'], arguments)
|
|
||||||
|
|
||||||
torrents = self.getJsonData(url, cache_timeout = 1800)
|
|
||||||
|
|
||||||
if torrents:
|
if torrents:
|
||||||
try:
|
try:
|
||||||
@@ -75,7 +70,7 @@ class TorrentPotato(TorrentProvider):
|
|||||||
pass_keys = splitString(self.conf('pass_key'), clean = False)
|
pass_keys = splitString(self.conf('pass_key'), clean = False)
|
||||||
extra_score = splitString(self.conf('extra_score'), clean = False)
|
extra_score = splitString(self.conf('extra_score'), clean = False)
|
||||||
|
|
||||||
list = []
|
host_list = []
|
||||||
for nr in range(len(hosts)):
|
for nr in range(len(hosts)):
|
||||||
|
|
||||||
try: key = pass_keys[nr]
|
try: key = pass_keys[nr]
|
||||||
@@ -93,7 +88,7 @@ class TorrentPotato(TorrentProvider):
|
|||||||
try: seed_time = seed_times[nr]
|
try: seed_time = seed_times[nr]
|
||||||
except: seed_time = ''
|
except: seed_time = ''
|
||||||
|
|
||||||
list.append({
|
host_list.append({
|
||||||
'use': uses[nr],
|
'use': uses[nr],
|
||||||
'host': host,
|
'host': host,
|
||||||
'name': name,
|
'name': name,
|
||||||
@@ -103,14 +98,14 @@ class TorrentPotato(TorrentProvider):
|
|||||||
'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0
|
'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0
|
||||||
})
|
})
|
||||||
|
|
||||||
return list
|
return host_list
|
||||||
|
|
||||||
def belongsTo(self, url, provider = None, host = None):
|
def belongsTo(self, url, provider = None, host = None):
|
||||||
|
|
||||||
hosts = self.getHosts()
|
hosts = self.getHosts()
|
||||||
|
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
result = super(TorrentPotato, self).belongsTo(url, host = host['host'], provider = provider)
|
result = super(Base, self).belongsTo(url, host = host['host'], provider = provider)
|
||||||
if result:
|
if result:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -127,3 +122,67 @@ class TorrentPotato(TorrentProvider):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
return TorrentProvider.isEnabled(self) and host['host'] and host['pass_key'] and int(host['use'])
|
return TorrentProvider.isEnabled(self) and host['host'] and host['pass_key'] and int(host['use'])
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'torrentpotato',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'TorrentPotato',
|
||||||
|
'order': 10,
|
||||||
|
'description': 'CouchPotato torrent provider. Checkout <a href="https://github.com/RuudBurger/CouchPotatoServer/wiki/CouchPotato-Torrent-Provider">the wiki page about this provider</a> for more info.',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAABnRSTlMAAAAAAABupgeRAAABSElEQVR4AZ2Nz0oCURTGv8t1YMpqUxt9ARFxoQ/gQtppgvUKcu/sxB5iBJkogspaBC6iVUplEC6kv+oiiKDNhAtt16roP0HQgdsMLgaxfvy4nHP4Pi48qE2g4v91JOqT1CH/UnA7w7icUlLawyEdj+ZI/7h6YluWbRiddHonHh9M70aj7VTKzuXuikUMci/EO/ACnAI15599oAk8AR/AgxBQNCzreD7bmpl+FOIVuAHqQDUcJo+AK+CZFKLt95/MpSmMt0TiW9POxse6UvYZ6zB2wFgjFiNpOGesR0rZ0PVPXf8KhUCl22CwClz4eN8weoZBb9c0bdPsOWvHx/cYu9Y0CoNoZTJrwAbn5DrnZc6XOV+igVbnsgo0IxEomlJuA1vUIYGyq3PZBChwmExCUSmVZgMBDIUCK4UCFIv5vHIhm/XUDeAf/ADbcpd5+aXSWQAAAABJRU5ErkJggg==',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'use',
|
||||||
|
'default': ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'host',
|
||||||
|
'default': '',
|
||||||
|
'description': 'The url path of your TorrentPotato provider.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'default': '0',
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'name',
|
||||||
|
'label': 'Username',
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_ratio',
|
||||||
|
'label': 'Seed ratio',
|
||||||
|
'default': '1',
|
||||||
|
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_time',
|
||||||
|
'label': 'Seed time',
|
||||||
|
'default': '40',
|
||||||
|
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'pass_key',
|
||||||
|
'default': ',',
|
||||||
|
'label': 'Pass Key',
|
||||||
|
'description': 'Can be found on your profile page',
|
||||||
|
'type': 'combined',
|
||||||
|
'combine': ['use', 'host', 'pass_key', 'name', 'seed_ratio', 'seed_time', 'extra_score'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import traceback
|
||||||
|
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.torrent.base import TorrentProvider
|
||||||
|
import six
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(TorrentProvider):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'test': 'https://torrentshack.net/',
|
||||||
|
'login': 'https://torrentshack.net/login.php',
|
||||||
|
'login_check': 'https://torrentshack.net/inbox.php',
|
||||||
|
'detail': 'https://torrentshack.net/torrent/%s',
|
||||||
|
'search': 'https://torrentshack.net/torrents.php?action=advanced&searchstr=%s&scene=%s&filter_cat[%d]=1',
|
||||||
|
'download': 'https://torrentshack.net/%s',
|
||||||
|
}
|
||||||
|
|
||||||
|
http_time_between_calls = 1 # Seconds
|
||||||
|
|
||||||
|
def _search(self, media, quality, results):
|
||||||
|
|
||||||
|
url = self.urls['search'] % self.buildUrl(media, quality)
|
||||||
|
data = self.getHTMLData(url)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
html = BeautifulSoup(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result_table = html.find('table', attrs = {'id': 'torrent_table'})
|
||||||
|
if not result_table:
|
||||||
|
return
|
||||||
|
|
||||||
|
entries = result_table.find_all('tr', attrs = {'class': 'torrent'})
|
||||||
|
|
||||||
|
for result in entries:
|
||||||
|
|
||||||
|
link = result.find('span', attrs = {'class': 'torrent_name_link'}).parent
|
||||||
|
url = result.find('td', attrs = {'class': 'torrent_td'}).find('a')
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'id': link['href'].replace('torrents.php?torrentid=', ''),
|
||||||
|
'name': six.text_type(link.span.string).translate({ord(six.u('\xad')): None}),
|
||||||
|
'url': self.urls['download'] % url['href'],
|
||||||
|
'detail_url': self.urls['download'] % link['href'],
|
||||||
|
'size': self.parseSize(result.find_all('td')[5].string),
|
||||||
|
'seeders': tryInt(result.find_all('td')[7].string),
|
||||||
|
'leechers': tryInt(result.find_all('td')[8].string),
|
||||||
|
})
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
|
def getLoginParams(self):
|
||||||
|
return {
|
||||||
|
'username': self.conf('username'),
|
||||||
|
'password': self.conf('password'),
|
||||||
|
'keeplogged': '1',
|
||||||
|
'login': 'Login',
|
||||||
|
}
|
||||||
|
|
||||||
|
def loginSuccess(self, output):
|
||||||
|
return 'logout.php' in output.lower()
|
||||||
|
|
||||||
|
loginCheckSuccess = loginSuccess
|
||||||
|
|
||||||
|
def getSceneOnly(self):
|
||||||
|
return '1' if self.conf('scene_only') else ''
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'torrentshack',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'TorrentShack',
|
||||||
|
'description': '<a href="https://www.torrentshack.net/">TorrentShack</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABmElEQVQoFQXBzY2cVRiE0afqvd84CQiAnxWWtyxsS6ThINBYg2Dc7mZBMEjE4mzs6e9WcY5+ePNuVFJJodQAoLo+SaWCy9rcV8cmjah3CI6iYu7oRU30kE5xxELRfamklY3k1NL19sSm7vPzP/ZdNZzKVDaY2sPZJBh9fv5ITrmG2+Vp4e1sPchVqTCQZJnVXi+/L4uuAJGly1+Pw8CprLbi8Om7tbT19/XRqJUk11JP9uHj9ulxhXbvJbI9qJvr5YkGXFG2IBT8tXczt+sfzDZCp3765f3t9tHEHGEDACma77+8o4oATKk+/PfW9YmHruRFjWoVSFsVsGu1YSKq6Oc37+n98unPZSRlY7vsKDqN+92X3yR9+PdXee3iJNKMStqdcZqoTJbUSi5JOkpfRlhSI0mSpEmCFKoU7FqSNOLAk54uGwCStMUCgLrVic62g7oDoFmmdI+P3S0pDe1xvDqb6XrZqbtzShWNoh9fv/XQHaDdM9OqrZi2M7M3UrB2vlkPS1IbdEBk7UiSoD6VlZ6aKWer4aH4f/AvKoHUTjuyAAAAAElFTkSuQmCC',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'default': '',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_ratio',
|
||||||
|
'label': 'Seed ratio',
|
||||||
|
'type': 'float',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_time',
|
||||||
|
'label': 'Seed time',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 40,
|
||||||
|
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'scene_only',
|
||||||
|
'type': 'bool',
|
||||||
|
'default': False,
|
||||||
|
'description': 'Only allow scene releases.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 0,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||||
|
from couchpotato.core.helpers.rss import RSS
|
||||||
|
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider
|
||||||
|
import six
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(TorrentMagnetProvider, RSS):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'detail': 'https://torrentz.eu/%s',
|
||||||
|
'search': 'https://torrentz.eu/feed?q=%s',
|
||||||
|
'verified_search': 'https://torrentz.eu/feed_verified?q=%s'
|
||||||
|
}
|
||||||
|
|
||||||
|
http_time_between_calls = 0
|
||||||
|
|
||||||
|
def _search(self, media, quality, results):
|
||||||
|
|
||||||
|
search_url = self.urls['verified_search'] if self.conf('verified_only') else self.urls['search']
|
||||||
|
|
||||||
|
# Create search parameters
|
||||||
|
search_params = self.buildUrl(media)
|
||||||
|
|
||||||
|
smin = quality.get('size_min')
|
||||||
|
smax = quality.get('size_max')
|
||||||
|
if smin and smax:
|
||||||
|
search_params += ' size %sm - %sm' % (smin, smax)
|
||||||
|
|
||||||
|
min_seeds = tryInt(self.conf('minimal_seeds'))
|
||||||
|
if min_seeds:
|
||||||
|
search_params += ' seed > %s' % (min_seeds - 1)
|
||||||
|
|
||||||
|
rss_data = self.getRSSData(search_url % search_params)
|
||||||
|
|
||||||
|
if rss_data:
|
||||||
|
try:
|
||||||
|
|
||||||
|
for result in rss_data:
|
||||||
|
|
||||||
|
name = self.getTextElement(result, 'title')
|
||||||
|
detail_url = self.getTextElement(result, 'link')
|
||||||
|
description = self.getTextElement(result, 'description')
|
||||||
|
|
||||||
|
magnet = splitString(detail_url, '/')[-1]
|
||||||
|
magnet_url = 'magnet:?xt=urn:btih:%s&dn=%s&tr=%s' % (magnet.upper(), tryUrlencode(name), tryUrlencode('udp://tracker.openbittorrent.com/announce'))
|
||||||
|
|
||||||
|
reg = re.search('Size: (?P<size>\d+) MB Seeds: (?P<seeds>[\d,]+) Peers: (?P<peers>[\d,]+)', six.text_type(description))
|
||||||
|
size = reg.group('size')
|
||||||
|
seeds = reg.group('seeds').replace(',', '')
|
||||||
|
peers = reg.group('peers').replace(',', '')
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'id': magnet,
|
||||||
|
'name': six.text_type(name),
|
||||||
|
'url': magnet_url,
|
||||||
|
'detail_url': detail_url,
|
||||||
|
'size': tryInt(size),
|
||||||
|
'seeders': tryInt(seeds),
|
||||||
|
'leechers': tryInt(peers),
|
||||||
|
})
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'torrentz',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'Torrentz',
|
||||||
|
'description': 'Torrentz is a free, fast and powerful meta-search engine. <a href="https://torrentz.eu/">Torrentz</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAQklEQVQ4y2NgAALjtJn/ycEMlGiGG0IVAxiwAKzOxaKGARcgxgC8YNSAwWoAzuRMjgsIugqfAUR5CZcBRIcHsWEAADSA96Ig020yAAAAAElFTkSuQmCC',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'verified_only',
|
||||||
|
'type': 'bool',
|
||||||
|
'default': True,
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Only search verified releases',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'minimal_seeds',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 1,
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Only return releases with minimal X seeds',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_ratio',
|
||||||
|
'label': 'Seed ratio',
|
||||||
|
'type': 'float',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_time',
|
||||||
|
'label': 'Seed time',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 40,
|
||||||
|
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 0,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import traceback
|
||||||
|
|
||||||
|
from couchpotato.core.helpers.variable import tryInt, getIdentifier
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.providers.torrent.base import TorrentMagnetProvider
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(TorrentMagnetProvider):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'test': '%s/api',
|
||||||
|
'search': '%s/api/list.json?keywords=%s&quality=%s',
|
||||||
|
'detail': '%s/api/movie.json?id=%s'
|
||||||
|
}
|
||||||
|
|
||||||
|
http_time_between_calls = 1 # seconds
|
||||||
|
|
||||||
|
proxy_list = [
|
||||||
|
'http://yify.unlocktorrent.com',
|
||||||
|
'http://yify-torrents.com.come.in',
|
||||||
|
'http://yts.re',
|
||||||
|
'http://yts.im'
|
||||||
|
'http://yify-torrents.im',
|
||||||
|
]
|
||||||
|
|
||||||
|
def search(self, movie, quality):
|
||||||
|
|
||||||
|
if not quality.get('hd', False):
|
||||||
|
return []
|
||||||
|
|
||||||
|
return super(Base, self).search(movie, quality)
|
||||||
|
|
||||||
|
def _search(self, movie, quality, results):
|
||||||
|
|
||||||
|
domain = self.getDomain()
|
||||||
|
if not domain:
|
||||||
|
return
|
||||||
|
|
||||||
|
search_url = self.urls['search'] % (domain, getIdentifier(movie), quality['identifier'])
|
||||||
|
|
||||||
|
data = self.getJsonData(search_url)
|
||||||
|
|
||||||
|
if data and data.get('MovieList'):
|
||||||
|
try:
|
||||||
|
for result in data.get('MovieList'):
|
||||||
|
|
||||||
|
if result['Quality'] and result['Quality'] not in result['MovieTitle']:
|
||||||
|
title = result['MovieTitle'] + ' BrRip ' + result['Quality']
|
||||||
|
else:
|
||||||
|
title = result['MovieTitle'] + ' BrRip'
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'id': result['MovieID'],
|
||||||
|
'name': title,
|
||||||
|
'url': result['TorrentMagnetUrl'],
|
||||||
|
'detail_url': self.urls['detail'] % (domain, result['MovieID']),
|
||||||
|
'size': self.parseSize(result['Size']),
|
||||||
|
'seeders': tryInt(result['TorrentSeeds']),
|
||||||
|
'leechers': tryInt(result['TorrentPeers']),
|
||||||
|
})
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
|
def correctProxy(self, data):
|
||||||
|
data = data.lower()
|
||||||
|
return 'yify' in data and 'yts' in data
|
||||||
|
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'yify',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'Yify',
|
||||||
|
'description': 'Free provider, less accurate. Small HD movies, encoded by <a href="https://yify-torrents.com/">Yify</a>.',
|
||||||
|
'wizard': False,
|
||||||
|
'icon': 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAACL0lEQVR4AS1SPW/UQBAd23fxne/Ld2dvzvHuzPocEBAKokCBqGiQ6IgACYmvUKRBFEQgKKGg4BAlUoggggYUEQpSHOI7CIEoQs/fYcbLaU/efTvvvZlnA1qydoxU5kcxX0CkgmQZtPy0hCUjvK+WgEByOZ5dns1O5bzna8fRVkgsxH8B0YouIvBhdD5T11NiVOoKrsttyUcpRW0InUrFnwe9HzuP2uaQZYhF2LQ76TTXw2RVMTK8mYYbjfh+zNquMVCrqn93aArLSixPxnafdGDLaz1tjY5rmNa8z5BczEQOxQfCl1GyoqoWxYRN1bkh7ELw3q/vhP6HIL4TG9KumpjgvwuyM7OsjSj98E/vszMfZ7xvPtMaWxGO5crwIumKCR5HxDtJ0AWKGG204RfUd/3smJYqwem/Q7BTS1ZGfM4LNpVwuKAz6cMeROst0S2EwNE7GjTehO2H3dxqIpdkydat15G3F8SXBi4GlpBNlSz012L/k2+W0CLLk/jbcf13rf41yJeMQ8QWUZiHCfCA9ad+81nEKPtoS9mJOf9v0NmMJHgUT6xayheK9EIK7JJeU/AF4scDF7Y5SPlJrRcxJ+um4ibNEdObxLiIwJim+eT2AL5D9CIcnZ5zvSJi9eIlNHVVtZ831dk5svPgvjPWTq+ktWkd/kD0qtm71x+sDQe3kt6DXnM7Ct+GajmTxKlkAokWljyAKSm5oWa2w+BH4P2UuVub7eTyiGOQYapY/wEztHduSDYz5gAAAABJRU5ErkJggg==',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': False
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'domain',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Proxy server',
|
||||||
|
'description': 'Domain for requests, keep empty to let CouchPotato pick.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_ratio',
|
||||||
|
'label': 'Seed ratio',
|
||||||
|
'type': 'float',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Will not be (re)moved until this seed ratio is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'seed_time',
|
||||||
|
'label': 'Seed time',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 40,
|
||||||
|
'description': 'Will not be (re)moved until this seed time (in hours) is met.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 0,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
+3
-1
@@ -1,9 +1,11 @@
|
|||||||
|
from urlparse import urlparse
|
||||||
|
|
||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
from couchpotato.core.helpers.encoding import simplifyString
|
from couchpotato.core.helpers.encoding import simplifyString
|
||||||
from couchpotato.core.helpers.variable import getImdb, md5
|
from couchpotato.core.helpers.variable import getImdb, md5
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from urlparse import urlparse
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
from .main import Search
|
from .main import Search
|
||||||
|
|
||||||
|
|
||||||
def start():
|
def autoload():
|
||||||
return Search()
|
return Search()
|
||||||
|
|
||||||
config = []
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import fireEvent, addEvent
|
from couchpotato.core.event import fireEvent, addEvent
|
||||||
from couchpotato.core.helpers.variable import mergeDicts
|
from couchpotato.core.helpers.variable import mergeDicts, getImdb
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
|
||||||
@@ -35,12 +35,21 @@ class Search(Plugin):
|
|||||||
elif isinstance(types, (list, tuple, set)):
|
elif isinstance(types, (list, tuple, set)):
|
||||||
types = list(types)
|
types = list(types)
|
||||||
|
|
||||||
|
imdb_identifier = getImdb(q)
|
||||||
|
|
||||||
if not types:
|
if not types:
|
||||||
result = fireEvent('info.search', q = q, merge = True)
|
if imdb_identifier:
|
||||||
|
result = fireEvent('movie.info', identifier = imdb_identifier, merge = True)
|
||||||
|
result = {result['type']: [result]}
|
||||||
|
else:
|
||||||
|
result = fireEvent('info.search', q = q, merge = True)
|
||||||
else:
|
else:
|
||||||
result = {}
|
result = {}
|
||||||
for media_type in types:
|
for media_type in types:
|
||||||
result[media_type] = fireEvent('%s.search' % media_type)
|
if imdb_identifier:
|
||||||
|
result[media_type] = fireEvent('%s.info' % media_type, identifier = imdb_identifier)
|
||||||
|
else:
|
||||||
|
result[media_type] = fireEvent('%s.search' % media_type, q = q)
|
||||||
|
|
||||||
return mergeDicts({
|
return mergeDicts({
|
||||||
'success': True,
|
'success': True,
|
||||||
|
|||||||
@@ -6,12 +6,10 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-bottom: 4px solid transparent;
|
|
||||||
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
||||||
position: absolute;
|
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
border: 1px solid transparent;
|
border: 0 solid transparent;
|
||||||
border-width: 0 0 4px;
|
border-bottom-width: 4px;
|
||||||
}
|
}
|
||||||
.search_form:hover {
|
.search_form:hover {
|
||||||
border-color: #047792;
|
border-color: #047792;
|
||||||
@@ -22,19 +20,19 @@
|
|||||||
right: 44px;
|
right: 44px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search_form.focused,
|
.search_form.focused,
|
||||||
.search_form.shown {
|
.search_form.shown {
|
||||||
border-color: #04bce6;
|
border-color: #04bce6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search_form .input {
|
.search_form .input {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 45px;
|
width: 45px;
|
||||||
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search_form.focused .input,
|
.search_form.focused .input,
|
||||||
.search_form.shown .input {
|
.search_form.shown .input {
|
||||||
width: 380px;
|
width: 380px;
|
||||||
@@ -49,7 +47,6 @@
|
|||||||
color: #FFF;
|
color: #FFF;
|
||||||
font-size: 25px;
|
font-size: 25px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 10px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
padding: 0 40px 0 10px;
|
padding: 0 40px 0 10px;
|
||||||
@@ -59,23 +56,23 @@
|
|||||||
.search_form.shown .input input {
|
.search_form.shown .input input {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search_form input::-ms-clear {
|
.search_form input::-ms-clear {
|
||||||
width : 0;
|
width : 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (max-width: 480px) {
|
@media all and (max-width: 480px) {
|
||||||
.search_form .input input {
|
.search_form .input input {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search_form.focused .input,
|
.search_form.focused .input,
|
||||||
.search_form.shown .input {
|
.search_form.shown .input {
|
||||||
width: 277px;
|
width: 277px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search_form .input a {
|
.search_form .input a {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -89,7 +86,7 @@
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: #FFF;
|
color: #FFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search_form .input a:after {
|
.search_form .input a:after {
|
||||||
content: "\e03e";
|
content: "\e03e";
|
||||||
}
|
}
|
||||||
@@ -97,7 +94,7 @@
|
|||||||
.search_form.shown.filled .input a:after {
|
.search_form.shown.filled .input a:after {
|
||||||
content: "\e04e";
|
content: "\e04e";
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (max-width: 480px) {
|
@media all and (max-width: 480px) {
|
||||||
.search_form .input a {
|
.search_form .input a {
|
||||||
line-height: 44px;
|
line-height: 44px;
|
||||||
@@ -167,13 +164,13 @@
|
|||||||
.media_result .options select[name=title] { width: 170px; }
|
.media_result .options select[name=title] { width: 170px; }
|
||||||
.media_result .options select[name=profile] { width: 90px; }
|
.media_result .options select[name=profile] { width: 90px; }
|
||||||
.media_result .options select[name=category] { width: 80px; }
|
.media_result .options select[name=category] { width: 80px; }
|
||||||
|
|
||||||
@media all and (max-width: 480px) {
|
@media all and (max-width: 480px) {
|
||||||
|
|
||||||
.media_result .options select[name=title] { width: 90px; }
|
.media_result .options select[name=title] { width: 90px; }
|
||||||
.media_result .options select[name=profile] { width: 50px; }
|
.media_result .options select[name=profile] { width: 50px; }
|
||||||
.media_result .options select[name=category] { width: 50px; }
|
.media_result .options select[name=category] { width: 50px; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.media_result .options .button {
|
.media_result .options .button {
|
||||||
@@ -227,14 +224,14 @@
|
|||||||
right: 7px;
|
right: 7px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media_result .info h2 {
|
.media_result .info h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search_form .info h2 {
|
.search_form .info h2 {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -247,12 +244,12 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search_form .info h2 .title {
|
.search_form .info h2 .title {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 88%;
|
width: 88%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media_result .info h2 .year {
|
.media_result .info h2 .year {
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -260,14 +257,14 @@
|
|||||||
width: 12%;
|
width: 12%;
|
||||||
right: 0;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media all and (max-width: 480px) {
|
@media all and (max-width: 480px) {
|
||||||
|
|
||||||
.search_form .info h2 .year {
|
.search_form .info h2 .year {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-top: 7px;
|
margin-top: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search_form .mask,
|
.search_form .mask,
|
||||||
@@ -277,4 +274,4 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ Block.Search = new Class({
|
|||||||
self.input = new Element('input', {
|
self.input = new Element('input', {
|
||||||
'placeholder': 'Search & add a new media',
|
'placeholder': 'Search & add a new media',
|
||||||
'events': {
|
'events': {
|
||||||
|
'input': self.keyup.bind(self),
|
||||||
|
'paste': self.keyup.bind(self),
|
||||||
|
'change': self.keyup.bind(self),
|
||||||
'keyup': self.keyup.bind(self),
|
'keyup': self.keyup.bind(self),
|
||||||
'focus': function(){
|
'focus': function(){
|
||||||
if(focus_timer) clearTimeout(focus_timer);
|
if(focus_timer) clearTimeout(focus_timer);
|
||||||
self.el.addClass('focused')
|
self.el.addClass('focused');
|
||||||
if(this.get('value'))
|
if(this.get('value'))
|
||||||
self.hideResults(false)
|
self.hideResults(false)
|
||||||
},
|
},
|
||||||
@@ -57,17 +60,17 @@ Block.Search = new Class({
|
|||||||
(e).preventDefault();
|
(e).preventDefault();
|
||||||
|
|
||||||
if(self.last_q === ''){
|
if(self.last_q === ''){
|
||||||
self.input.blur()
|
self.input.blur();
|
||||||
self.last_q = null;
|
self.last_q = null;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
||||||
self.last_q = '';
|
self.last_q = '';
|
||||||
self.input.set('value', '');
|
self.input.set('value', '');
|
||||||
self.input.focus()
|
self.input.focus();
|
||||||
|
|
||||||
self.media = {}
|
self.media = {};
|
||||||
self.results.empty()
|
self.results.empty();
|
||||||
self.el.removeClass('filled')
|
self.el.removeClass('filled')
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -92,16 +95,16 @@ Block.Search = new Class({
|
|||||||
self.hidden = bool;
|
self.hidden = bool;
|
||||||
},
|
},
|
||||||
|
|
||||||
keyup: function(e){
|
keyup: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.el[self.q() ? 'addClass' : 'removeClass']('filled')
|
self.el[self.q() ? 'addClass' : 'removeClass']('filled');
|
||||||
|
|
||||||
if(self.q() != self.last_q){
|
if(self.q() != self.last_q){
|
||||||
if(self.api_request && self.api_request.isRunning())
|
if(self.api_request && self.api_request.isRunning())
|
||||||
self.api_request.cancel();
|
self.api_request.cancel();
|
||||||
|
|
||||||
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
|
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer);
|
||||||
self.autocomplete_timer = self.autocomplete.delay(300, self)
|
self.autocomplete_timer = self.autocomplete.delay(300, self)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +114,7 @@ Block.Search = new Class({
|
|||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if(!self.q()){
|
if(!self.q()){
|
||||||
self.hideResults(true)
|
self.hideResults(true);
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +142,7 @@ Block.Search = new Class({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
self.fill(q, cache)
|
self.fill(q, cache);
|
||||||
|
|
||||||
self.last_q = q;
|
self.last_q = q;
|
||||||
|
|
||||||
@@ -148,31 +151,31 @@ Block.Search = new Class({
|
|||||||
fill: function(q, json){
|
fill: function(q, json){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.cache[q] = json
|
self.cache[q] = json;
|
||||||
|
|
||||||
self.media = {}
|
self.media = {};
|
||||||
self.results.empty()
|
self.results.empty();
|
||||||
|
|
||||||
Object.each(json, function(media, type){
|
Object.each(json, function(media){
|
||||||
if(typeOf(media) == 'array'){
|
if(typeOf(media) == 'array'){
|
||||||
Object.each(media, function(m){
|
Object.each(media, function(m){
|
||||||
|
|
||||||
var m = new Block.Search[m.type.capitalize() + 'Item'](m);
|
var m = new Block.Search[m.type.capitalize() + 'Item'](m);
|
||||||
$(m).inject(self.results)
|
$(m).inject(self.results);
|
||||||
self.media[m.imdb || 'r-'+Math.floor(Math.random()*10000)] = m
|
self.media[m.imdb || 'r-'+Math.floor(Math.random()*10000)] = m;
|
||||||
|
|
||||||
if(q == m.imdb)
|
if(q == m.imdb)
|
||||||
m.showOptions()
|
m.showOptions()
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// Calculate result heights
|
// Calculate result heights
|
||||||
var w = window.getSize(),
|
var w = window.getSize(),
|
||||||
rc = self.result_container.getCoordinates();
|
rc = self.result_container.getCoordinates();
|
||||||
|
|
||||||
self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px')
|
self.results.setStyle('max-height', (w.y - rc.top - 50) + 'px');
|
||||||
self.mask.fade('out')
|
self.mask.fade('out')
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -185,4 +188,4 @@ Block.Search = new Class({
|
|||||||
return this.input.get('value').trim();
|
return this.input.get('value').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user