Compare commits
796
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d08c7c57a8 | ||
|
|
eeeb845ef3 | ||
|
|
651a063f94 | ||
|
|
f20aaa2d9d | ||
|
|
ba925ec191 | ||
|
|
f67c6fe8be | ||
|
|
8d38fa87a4 | ||
|
|
7c79c6d1f3 | ||
|
|
b0781b45f8 | ||
|
|
ee53539906 | ||
|
|
c8ab6a06fb | ||
|
|
c75ac51eb7 | ||
|
|
33d7d994d4 | ||
|
|
96291f63da | ||
|
|
6464bb065d | ||
|
|
8b45b6f1a0 | ||
|
|
70ba5d80cd | ||
|
|
ac30152930 | ||
|
|
ad01a3da4d | ||
|
|
5f5f17112a | ||
|
|
156da670e8 | ||
|
|
821c26f35b | ||
|
|
a092f394fa | ||
|
|
18e3194e27 | ||
|
|
08a1e1e582 | ||
|
|
074005ed02 | ||
|
|
7660a3d78f | ||
|
|
9211e60804 | ||
|
|
87f295be28 | ||
|
|
cfa89c8921 | ||
|
|
70f834d925 | ||
|
|
6b4e4fd440 | ||
|
|
b83b2453a0 | ||
|
|
82d31d996d | ||
|
|
4faa617039 | ||
|
|
a1d2276668 | ||
|
|
19c50f728e | ||
|
|
a94307c59f | ||
|
|
c6403e87f1 | ||
|
|
b56cd3439e | ||
|
|
25693d44eb | ||
|
|
43af25a30e | ||
|
|
023278e0c0 | ||
|
|
0634c79f74 | ||
|
|
31b3c2ef64 | ||
|
|
4a71f2c556 | ||
|
|
9783409756 | ||
|
|
c7e85c00ca | ||
|
|
94647bbb57 | ||
|
|
1aa26a5a6c | ||
|
|
df13a0edc2 | ||
|
|
52a0de3b59 | ||
|
|
38886b28f7 | ||
|
|
226cf6fc38 | ||
|
|
203a52bfd1 | ||
|
|
1b6bf13619 | ||
|
|
bc94e90994 | ||
|
|
347125365f | ||
|
|
59a718be20 | ||
|
|
c41b3a612a | ||
|
|
23f77df911 | ||
|
|
117b952455 | ||
|
|
7714504831 | ||
|
|
5c61c24c04 | ||
|
|
b11e1d48e0 | ||
|
|
a6ce114284 | ||
|
|
88d512eacc | ||
|
|
f4d5366c93 | ||
|
|
ac9aaec7b8 | ||
|
|
0c5b950c87 | ||
|
|
47141f8e4f | ||
|
|
ec302fe665 | ||
|
|
7f304b0c28 | ||
|
|
8f88f7d89b | ||
|
|
400fd461ab | ||
|
|
cd8d2d4808 | ||
|
|
4cfa79488f | ||
|
|
b5993bcc21 | ||
|
|
6af00bf026 | ||
|
|
97c456c9e1 | ||
|
|
08f44197f3 | ||
|
|
779c7d2942 | ||
|
|
7fd14e0283 | ||
|
|
7d32a8750d | ||
|
|
110e0b78fc | ||
|
|
bc77812488 | ||
|
|
3e28cd5c95 | ||
|
|
2715dbaaa5 | ||
|
|
3baf12d3e4 | ||
|
|
a428d36604 | ||
|
|
b5207bc88c | ||
|
|
910578a2ac | ||
|
|
88176997e7 | ||
|
|
233e6f9be0 | ||
|
|
1fd11fb547 | ||
|
|
8bfd206578 | ||
|
|
62c6fd2e40 | ||
|
|
ac2d2a0463 | ||
|
|
c1e4b47b99 | ||
|
|
32b479467a | ||
|
|
6cab2b34d6 | ||
|
|
9e744199fe | ||
|
|
b22021e7f0 | ||
|
|
68bdf47ea4 | ||
|
|
af2876bd71 | ||
|
|
1e5d6bad2a | ||
|
|
f6c836157d | ||
|
|
d10874f216 | ||
|
|
700713abcf | ||
|
|
5180426fc1 | ||
|
|
e1c8a08f2f | ||
|
|
16f0bcc3ac | ||
|
|
9c98a38604 | ||
|
|
1b03c7e474 | ||
|
|
689feb78d0 | ||
|
|
336b15b199 | ||
|
|
4a4bb819ec | ||
|
|
48be010f33 | ||
|
|
104e21b314 | ||
|
|
aaf5cab138 | ||
|
|
22b744340a | ||
|
|
2954558004 | ||
|
|
b797590a4e | ||
|
|
9d71fe1724 | ||
|
|
9ad0ed642d | ||
|
|
cbd217271d | ||
|
|
65896497fb | ||
|
|
54a37b577d | ||
|
|
f1948ffb6a | ||
|
|
7dd3b0ed15 | ||
|
|
11fcfa8202 | ||
|
|
199e61ea14 | ||
|
|
0daa6c8eff | ||
|
|
b1b5f97f03 | ||
|
|
32d5587669 | ||
|
|
c13c0f24e5 | ||
|
|
7eb1d72333 | ||
|
|
3d6ec1feba | ||
|
|
c267232160 | ||
|
|
48f4b008df | ||
|
|
ae1f181fbf | ||
|
|
cbfee72d51 | ||
|
|
ee709054f2 | ||
|
|
ee60ec962b | ||
|
|
e013e38c5e | ||
|
|
20aa78105f | ||
|
|
770590e4f2 | ||
|
|
8e9e7b49ea | ||
|
|
08554889fd | ||
|
|
8ac2869de3 | ||
|
|
bb8e8a0df5 | ||
|
|
e2bd6a91cd | ||
|
|
ed0e5ef497 | ||
|
|
e1e475e605 | ||
|
|
cef5b04eb1 | ||
|
|
7e44af936d | ||
|
|
6aec5a9a60 | ||
|
|
79c75c886b | ||
|
|
bf6bcaed72 | ||
|
|
70bc2a6656 | ||
|
|
695cdea447 | ||
|
|
d0735a6d58 | ||
|
|
175c26bea9 | ||
|
|
8a298edd4e | ||
|
|
9860a1c138 | ||
|
|
3dff598d03 | ||
|
|
62b571d5f1 | ||
|
|
3af6623a91 | ||
|
|
c73ed8a4c5 | ||
|
|
4d5ba65254 | ||
|
|
91856f1159 | ||
|
|
f7da408f83 | ||
|
|
2824c55231 | ||
|
|
874655846c | ||
|
|
1620acedb1 | ||
|
|
6395e5dbbb | ||
|
|
251d9cdb8a | ||
|
|
623571acbb | ||
|
|
250f07ffa7 | ||
|
|
8917d7c16c | ||
|
|
d759280c18 | ||
|
|
67bc3903d4 | ||
|
|
cf6f83a44b | ||
|
|
4b15563ba3 | ||
|
|
dc36e15448 | ||
|
|
0b6330e98b | ||
|
|
2e93687bb4 | ||
|
|
0f925a466a | ||
|
|
16eeeda787 | ||
|
|
52f1df98bb | ||
|
|
a0ccff23a3 | ||
|
|
b8bed627a8 | ||
|
|
8d058d9dc8 | ||
|
|
57e92ff8d3 | ||
|
|
6eff724f97 | ||
|
|
55c3fe503b | ||
|
|
7f1ac63c58 | ||
|
|
2bb2e28f91 | ||
|
|
0bdffc5036 | ||
|
|
7202fbf084 | ||
|
|
317c3afb7a | ||
|
|
577baeca59 | ||
|
|
7c680cac10 | ||
|
|
0fadbd52a3 | ||
|
|
38e204dfe8 | ||
|
|
bf62653531 | ||
|
|
d851be41d3 | ||
|
|
3bd1875321 | ||
|
|
448c1d69a7 | ||
|
|
c99a5cb535 | ||
|
|
0492e90d6f | ||
|
|
4ffda9f705 | ||
|
|
b32d4fc42d | ||
|
|
4330dc39bf | ||
|
|
da50b19b6b | ||
|
|
797018fb8a | ||
|
|
3a8f891c7d | ||
|
|
56a788286c | ||
|
|
fd95364d5f | ||
|
|
470fde0890 | ||
|
|
f12d878c0b | ||
|
|
e8993932c1 | ||
|
|
e3933e4ddc | ||
|
|
dd67239b6e | ||
|
|
1ea0d3bd8b | ||
|
|
8b952d4be6 | ||
|
|
9e8a3bc701 | ||
|
|
76807176fb | ||
|
|
3650624e4b | ||
|
|
585c509aba | ||
|
|
046c7e732f | ||
|
|
564a27461d | ||
|
|
4ebbc1a01d | ||
|
|
4ec32a6403 | ||
|
|
412627aab0 | ||
|
|
2584abda0e | ||
|
|
7692322fba | ||
|
|
954018fea2 | ||
|
|
ebf37f7310 | ||
|
|
f22b836ede | ||
|
|
1cea786d66 | ||
|
|
9be10f7b79 | ||
|
|
1f35d0ec2f | ||
|
|
9fcf36a2ff | ||
|
|
30f5a66487 | ||
|
|
60e0ad1f5d | ||
|
|
ed60b4670e | ||
|
|
318daaf083 | ||
|
|
182987218b | ||
|
|
5ff8c7302f | ||
|
|
398712403b | ||
|
|
63f72eb23b | ||
|
|
9dea6d7200 | ||
|
|
36f63bdf99 | ||
|
|
a09fc14625 | ||
|
|
71e280238d | ||
|
|
e20bb13649 | ||
|
|
ed8108a9d8 | ||
|
|
c0b3c9a330 | ||
|
|
8a252bff64 | ||
|
|
d3d3106fc9 | ||
|
|
1ebb09226d | ||
|
|
52163428e9 | ||
|
|
da9dda2c2b | ||
|
|
a4a14cae96 | ||
|
|
989d6c55c4 | ||
|
|
3b7376fd18 | ||
|
|
c31b10c798 | ||
|
|
1c3e6ba930 | ||
|
|
acda664686 | ||
|
|
99123ad1c3 | ||
|
|
cdf9cf5cf4 | ||
|
|
797dedfcbb | ||
|
|
b61de4866c | ||
|
|
931951ff37 | ||
|
|
6f42b4c316 | ||
|
|
58c446de2d | ||
|
|
74bf6bc411 | ||
|
|
ad3c24f950 | ||
|
|
998e487fe8 | ||
|
|
93346b0c63 | ||
|
|
7d9920691f | ||
|
|
b1942678b4 | ||
|
|
8c77d0d775 | ||
|
|
3e667ee39a | ||
|
|
52b2858ac2 | ||
|
|
6fcb4c2058 | ||
|
|
7411670e22 | ||
|
|
cfd23c395a | ||
|
|
2e8f670e94 | ||
|
|
18a88eab51 | ||
|
|
84e9f9794d | ||
|
|
628c0e5dcc | ||
|
|
cdee08bd36 | ||
|
|
7ed43da425 | ||
|
|
461a0b3645 | ||
|
|
bd56539103 | ||
|
|
9bcd3de69b | ||
|
|
d8f57963a1 | ||
|
|
5328f7fe69 | ||
|
|
9eea42b121 | ||
|
|
374f8ba1de | ||
|
|
74c984dec3 | ||
|
|
52ea0215f0 | ||
|
|
ea3d719b32 | ||
|
|
fd1e655075 | ||
|
|
9f8d439780 | ||
|
|
7e1bdc99eb | ||
|
|
dac36d7f55 | ||
|
|
9d495a10ec | ||
|
|
9bb99319ba | ||
|
|
bc8d8dcd04 | ||
|
|
b2d9a7675d | ||
|
|
2477197656 | ||
|
|
171083b2f1 | ||
|
|
e592eb969f | ||
|
|
db1493f138 | ||
|
|
57c270f8fa | ||
|
|
bfe8bc89c0 | ||
|
|
0a00862495 | ||
|
|
7dd53d93cd | ||
|
|
abe65d4064 | ||
|
|
4977b31ba6 | ||
|
|
c1beb85ba5 | ||
|
|
ca9a78eea4 | ||
|
|
9bf006f4d3 | ||
|
|
3bb2a082b7 | ||
|
|
92d11522d2 | ||
|
|
44cfdc1503 | ||
|
|
2fdcbedea8 | ||
|
|
787c7fd966 | ||
|
|
09b4ad6937 | ||
|
|
580d43aeaf | ||
|
|
a1a7fec15f | ||
|
|
6dcd74d116 | ||
|
|
187f5a8a93 | ||
|
|
2eb938147a | ||
|
|
deffb75c14 | ||
|
|
f91707bfbe | ||
|
|
8aba7825dc | ||
|
|
b8b5b2fef2 | ||
|
|
f4d6d69184 | ||
|
|
a5b1c685e1 | ||
|
|
609805b84d | ||
|
|
00d1da7c01 | ||
|
|
7335726c7d | ||
|
|
02779939f0 | ||
|
|
6c6f015f40 | ||
|
|
f087d38b86 | ||
|
|
c78957f55c | ||
|
|
9ce0c47cd4 | ||
|
|
60034f2c96 | ||
|
|
c9a4af218e | ||
|
|
c5c2e61e06 | ||
|
|
b2930dd6a7 | ||
|
|
4aa6700ceb | ||
|
|
267ecfacab | ||
|
|
007597239f | ||
|
|
5699abf1be | ||
|
|
a6ccd037e2 | ||
|
|
009991ce4c | ||
|
|
6ef788a8f4 | ||
|
|
fa37f7d40a | ||
|
|
b195cebac7 | ||
|
|
8aeea60888 | ||
|
|
6e0857c6c1 | ||
|
|
260fdbe3b3 | ||
|
|
2f30c6c781 | ||
|
|
d5b4da655a | ||
|
|
1694ed7758 | ||
|
|
ee6cc6d319 | ||
|
|
7670e320ba | ||
|
|
15ab745bd0 | ||
|
|
7468b33991 | ||
|
|
750e02f38a | ||
|
|
e2852407ea | ||
|
|
88e738c6cd | ||
|
|
eaae8bdb0b | ||
|
|
95d146fea2 | ||
|
|
dc20b68a37 | ||
|
|
563e3072a5 | ||
|
|
b3ba4db00b | ||
|
|
a4c1480a1a | ||
|
|
91e0452320 | ||
|
|
ad80ea7885 | ||
|
|
1c20cda389 | ||
|
|
631759d833 | ||
|
|
ca02c66f26 | ||
|
|
3ac095d359 | ||
|
|
e1bc223de0 | ||
|
|
e065ead9b3 | ||
|
|
f9471f9b9b | ||
|
|
2612b50d06 | ||
|
|
d9ce2906a0 | ||
|
|
b76397f98e | ||
|
|
fcad9e0be5 | ||
|
|
2934347865 | ||
|
|
315f1b0207 | ||
|
|
965bd79a86 | ||
|
|
c18563e34b | ||
|
|
161e0de8d5 | ||
|
|
40aeca0740 | ||
|
|
63dd7fa7c0 | ||
|
|
509b49caf1 | ||
|
|
38c51cf79c | ||
|
|
0b693bba4e | ||
|
|
1258f34c78 | ||
|
|
510c0d5f56 | ||
|
|
cdb630e580 | ||
|
|
65fbd38105 | ||
|
|
1570132a55 | ||
|
|
7b5b748d23 | ||
|
|
041601c4a5 | ||
|
|
f692fd0202 | ||
|
|
e7b4de56f2 | ||
|
|
4a616a0c04 | ||
|
|
0814675d2a | ||
|
|
13df35462b | ||
|
|
899868f51e | ||
|
|
ee466aebce | ||
|
|
687ef2662e | ||
|
|
5aa29acbd3 | ||
|
|
1c2b3d063b | ||
|
|
551a000893 | ||
|
|
0d82d425cc | ||
|
|
0e1cea1034 | ||
|
|
2b75153148 | ||
|
|
c170615fb3 | ||
|
|
f6e84b6a35 | ||
|
|
6144f09a1f | ||
|
|
de142e8050 | ||
|
|
d0c1a119fd | ||
|
|
8fd80d3185 | ||
|
|
ae28c82858 | ||
|
|
1766764c7d | ||
|
|
129f8d72bd | ||
|
|
7314b5ecae | ||
|
|
7b0806355f | ||
|
|
49cf72e058 | ||
|
|
a11cad619d | ||
|
|
c1d35e8a57 | ||
|
|
fede348fbd | ||
|
|
f3c60e8fa6 | ||
|
|
00e53439ed | ||
|
|
368fced0c4 | ||
|
|
666771fb0f | ||
|
|
9e3f978677 | ||
|
|
f467d1c4f7 | ||
|
|
d8fc9d937e | ||
|
|
821f68909d | ||
|
|
2b8dfed475 | ||
|
|
0a749ce913 | ||
|
|
e6db505cf7 | ||
|
|
9e8d6aaaa1 | ||
|
|
e814b551b4 | ||
|
|
080da48223 | ||
|
|
897330e646 | ||
|
|
c4c7b5b1a9 | ||
|
|
b90861bc63 | ||
|
|
6d1297a85f | ||
|
|
dfd2c33657 | ||
|
|
f5af551325 | ||
|
|
7aad27c3d2 | ||
|
|
60ff3b08d4 | ||
|
|
7a5588d5de | ||
|
|
56b6fbbe7f | ||
|
|
46c408befb | ||
|
|
6f808fc25a | ||
|
|
4cba44fbb1 | ||
|
|
91c45bad71 | ||
|
|
a30caefc04 | ||
|
|
eb20fda878 | ||
|
|
4a5aa02e6c | ||
|
|
25b37ad915 | ||
|
|
bbcceb982a | ||
|
|
c41f5eb84d | ||
|
|
89dc9e90b2 | ||
|
|
39b1dedf12 | ||
|
|
be28820fb2 | ||
|
|
0654c8cf07 | ||
|
|
2f5cb81029 | ||
|
|
067d6e8514 | ||
|
|
42e19e1e2b | ||
|
|
6b846b91b4 | ||
|
|
5838a41813 | ||
|
|
3cd5513c0c | ||
|
|
bfdc8d1053 | ||
|
|
30ec8216e1 | ||
|
|
12c3fc6ce3 | ||
|
|
7b3a1409d5 | ||
|
|
924bed06cb | ||
|
|
8b0aa7a6b3 | ||
|
|
367c385fff | ||
|
|
840efb1571 | ||
|
|
9ba19d27a6 | ||
|
|
1d603e1ec2 | ||
|
|
7818b43045 | ||
|
|
3936100000 | ||
|
|
1a846b04ee | ||
|
|
384a355a53 | ||
|
|
58ad5c3938 | ||
|
|
6ee68d1418 | ||
|
|
6e45c14ac5 | ||
|
|
e786c9c79a | ||
|
|
518ac16814 | ||
|
|
1d07eafa83 | ||
|
|
1600b6d0ea | ||
|
|
6de3a7246e | ||
|
|
cbd29df52a | ||
|
|
92998bafc8 | ||
|
|
1022753213 | ||
|
|
b85942989d | ||
|
|
f2f43a2231 | ||
|
|
2979a8edec | ||
|
|
0e90739786 | ||
|
|
185a530b59 | ||
|
|
4f6b31d14a | ||
|
|
f1dde5c925 | ||
|
|
64afa3701a | ||
|
|
cb0b6614c6 | ||
|
|
177063d39c | ||
|
|
be595aba91 | ||
|
|
66d9d853af | ||
|
|
95a68af795 | ||
|
|
c1937ea71f | ||
|
|
a7bd8c822a | ||
|
|
0eff4f0096 | ||
|
|
4d7fa08805 | ||
|
|
5fd4312ff8 | ||
|
|
a600430be4 | ||
|
|
f77b598899 | ||
|
|
ac045539d1 | ||
|
|
5b0fa9054b | ||
|
|
3c2a00b17b | ||
|
|
47ddf31f76 | ||
|
|
57ae06e139 | ||
|
|
7f4373e000 | ||
|
|
63609bb52c | ||
|
|
f0af184262 | ||
|
|
72cc3576d3 | ||
|
|
3fe7d2ea15 | ||
|
|
8eed54f1f7 | ||
|
|
c7ee8a0635 | ||
|
|
33a6a7d3a0 | ||
|
|
2851781a72 | ||
|
|
45b9919f67 | ||
|
|
207e846ae6 | ||
|
|
a83c276aa2 | ||
|
|
4cdb99a383 | ||
|
|
8fe60a893c | ||
|
|
0c44c48628 | ||
|
|
6a18e546ca | ||
|
|
4cedccb178 | ||
|
|
eab9a735a9 | ||
|
|
1df05cf344 | ||
|
|
843ff0eabc | ||
|
|
5a23be2224 | ||
|
|
45c8817c62 | ||
|
|
7f87b255f9 | ||
|
|
665c84c6de | ||
|
|
b91a077c91 | ||
|
|
59b924efe7 | ||
|
|
730718a396 | ||
|
|
3c0edc0d6a | ||
|
|
7c234ab7e9 | ||
|
|
b82319cb54 | ||
|
|
6685495400 | ||
|
|
b216589e88 | ||
|
|
744aa153f6 | ||
|
|
67612fce98 | ||
|
|
72ba1a173c | ||
|
|
989e217775 | ||
|
|
b0d556c8eb | ||
|
|
1a54d8fad9 | ||
|
|
f9ace29cab | ||
|
|
a97570027d | ||
|
|
de36faa0a7 | ||
|
|
19641bd897 | ||
|
|
2c64641a1b | ||
|
|
5ac1118db3 | ||
|
|
717b88b5fe | ||
|
|
158a7fc311 | ||
|
|
2c46279617 | ||
|
|
b843d5f13b | ||
|
|
4aff3f0495 | ||
|
|
4406f133b9 | ||
|
|
572dfd529e | ||
|
|
2cb6ddfe9a | ||
|
|
250236bd25 | ||
|
|
7f24563bba | ||
|
|
5d6a9ad2d0 | ||
|
|
0115bf254e | ||
|
|
607b5ea766 | ||
|
|
88579cd71a | ||
|
|
6c57316ce6 | ||
|
|
6702683da3 | ||
|
|
b9c2b42725 | ||
|
|
e54928720a | ||
|
|
f8f22cdef7 | ||
|
|
1ed58586a1 | ||
|
|
e694276a8d | ||
|
|
a8369b4e93 | ||
|
|
73b7bcc6ce | ||
|
|
f08ccd4fd8 | ||
|
|
312562a9f5 | ||
|
|
fab8e66fe1 | ||
|
|
1cd8040692 | ||
|
|
4db1b57c70 | ||
|
|
7268e02386 | ||
|
|
805aa3ca9f | ||
|
|
29cb34551c | ||
|
|
d267be4455 | ||
|
|
92f4ade371 | ||
|
|
9235eda73b | ||
|
|
1fe23afd1b | ||
|
|
09637c3069 | ||
|
|
2cafd509fc | ||
|
|
62cc570ab2 | ||
|
|
1ec9370e68 | ||
|
|
5b4c60ecba | ||
|
|
7b7488ece8 | ||
|
|
4ba7ff9f27 | ||
|
|
df2d1aca4b | ||
|
|
4fcba70c9a | ||
|
|
d0fc20ca6e | ||
|
|
9402b54f9b | ||
|
|
f0e7795b9b | ||
|
|
bba18d8bc9 | ||
|
|
0494e5fc8f | ||
|
|
df1b46272d | ||
|
|
b06dbd3069 | ||
|
|
ed068f09b0 | ||
|
|
5e852d05ee | ||
|
|
d111393bd6 | ||
|
|
f84aa8c638 | ||
|
|
89bff73431 | ||
|
|
8e07dfc730 | ||
|
|
cd16dddf13 | ||
|
|
25605c45b9 | ||
|
|
b6d0d54609 | ||
|
|
98981dac27 | ||
|
|
ddf03cbcf2 | ||
|
|
1e1abf407c | ||
|
|
1267cdac4d | ||
|
|
05bcee12ae | ||
|
|
fc3f15e0cf | ||
|
|
0a7765f639 | ||
|
|
c214458770 | ||
|
|
bfe501c84a | ||
|
|
e034465df8 | ||
|
|
a7b78d4131 | ||
|
|
3eed34c710 | ||
|
|
9cb3bef156 | ||
|
|
46c7e3fbed | ||
|
|
a49a00a25f | ||
|
|
eed0382b41 | ||
|
|
673843fb66 | ||
|
|
4e45c94fc3 | ||
|
|
0a11dc6673 | ||
|
|
4ede2c20a1 | ||
|
|
af0cf523e3 | ||
|
|
3908e00650 | ||
|
|
f9bdf6da1c | ||
|
|
811f35b028 | ||
|
|
87cdf9222d | ||
|
|
2ca2cc9597 | ||
|
|
edb232df60 | ||
|
|
af113c0ffd | ||
|
|
856b495995 | ||
|
|
a56bbf0b3b | ||
|
|
4b54113f08 | ||
|
|
52371b7705 | ||
|
|
629bead919 | ||
|
|
c7cd72787f | ||
|
|
ec6e2c240f | ||
|
|
9e260a89af | ||
|
|
d233e4d22e | ||
|
|
a60e9dc4c3 | ||
|
|
b168c1364d | ||
|
|
23893dbcb9 | ||
|
|
3187a0f820 | ||
|
|
14fffda3ff | ||
|
|
f86b9299c4 | ||
|
|
51364a3c25 | ||
|
|
c6642ffeb7 | ||
|
|
9fe9ccf0ad | ||
|
|
d27d0abeb0 | ||
|
|
cb92b00534 | ||
|
|
7d3780133f | ||
|
|
f23b9d7cb9 | ||
|
|
506871b506 | ||
|
|
6115917660 | ||
|
|
44b78f8d2f | ||
|
|
21df8819d3 | ||
|
|
7c59348138 | ||
|
|
cad9bfae9f | ||
|
|
749075b4cb | ||
|
|
0456a1e820 | ||
|
|
35a9739ec5 | ||
|
|
2a451c255e | ||
|
|
7c38ad1c00 | ||
|
|
ab53f44157 | ||
|
|
b35f325d94 | ||
|
|
647159e549 | ||
|
|
7cc55c21b6 | ||
|
|
89c38f5aa4 | ||
|
|
5f428649c3 | ||
|
|
8ed2a99830 | ||
|
|
1a89d551dc | ||
|
|
9d633910f6 | ||
|
|
54ea22e9b6 | ||
|
|
fb3f3e11f6 | ||
|
|
f84b23eecc | ||
|
|
6ea045ddd3 | ||
|
|
f8b4e75b74 | ||
|
|
faaf351662 | ||
|
|
f41fc794c1 | ||
|
|
0f789b5b40 | ||
|
|
d2496d768d | ||
|
|
b93488f025 | ||
|
|
d4de68ef86 | ||
|
|
61a0bb8ec6 | ||
|
|
fe52ac7203 | ||
|
|
4447b7611e | ||
|
|
178c8942c3 | ||
|
|
4fe9f9e42f | ||
|
|
71b22345bc | ||
|
|
a0dc5c075a | ||
|
|
a264c75f8c | ||
|
|
fcc8a71eae | ||
|
|
cdd681ad48 | ||
|
|
36e5c49147 | ||
|
|
300f4738a0 | ||
|
|
9447833653 | ||
|
|
df53d0c578 | ||
|
|
17eaba3e2a | ||
|
|
0f389f18cb | ||
|
|
28ce083f48 | ||
|
|
cfaffe2bcb | ||
|
|
432852cf5d | ||
|
|
3c728608e9 | ||
|
|
8892ace3c2 | ||
|
|
87574a1810 | ||
|
|
393c14de54 | ||
|
|
14e0219e62 | ||
|
|
51e747049d | ||
|
|
0582f7d694 | ||
|
|
fa7cac7538 | ||
|
|
bff17c0b95 | ||
|
|
ec857a9b3d | ||
|
|
4d32b0b16d | ||
|
|
ca08287cff | ||
|
|
36fee69843 | ||
|
|
c5cae5ab9b | ||
|
|
9bd5688fb9 | ||
|
|
1993c2b6cb | ||
|
|
acc8ed2092 | ||
|
|
7b4924dd7a | ||
|
|
3a2861f72a | ||
|
|
4779265b43 | ||
|
|
f8a46ebe6d | ||
|
|
383ec7e6f5 | ||
|
|
dd9118292d | ||
|
|
4d0f8eb4ac | ||
|
|
637b21cc68 | ||
|
|
da429f0cb8 | ||
|
|
41c2845328 | ||
|
|
c2453bb070 | ||
|
|
a3a2c8da8e | ||
|
|
d172828ac5 | ||
|
|
a1d4bab793 | ||
|
|
d314a9b5b3 | ||
|
|
9a60f6001a | ||
|
|
96a39dbf60 | ||
|
|
9500ac73fc | ||
|
|
e2cf7e4421 | ||
|
|
015675750c | ||
|
|
bf4dc62f54 | ||
|
|
c2382ade05 | ||
|
|
2f65545086 | ||
|
|
3aea2cd968 | ||
|
|
f30cb9185c | ||
|
|
615468e8e6 | ||
|
|
0cbee01024 | ||
|
|
c29cb39797 | ||
|
|
580ff38136 | ||
|
|
6b8bca5491 | ||
|
|
e92b5d95ca | ||
|
|
611a32d110 | ||
|
|
74e4b015a9 | ||
|
|
1e0267cdb5 | ||
|
|
041a206fb4 | ||
|
|
12a4d6a995 | ||
|
|
b14a6c1e63 | ||
|
|
7fa08ef9b6 | ||
|
|
c087a6b49b |
+1
-2
@@ -62,7 +62,6 @@ class Loader(object):
|
|||||||
self.log.logger.addHandler(hdlr)
|
self.log.logger.addHandler(hdlr)
|
||||||
|
|
||||||
def addSignals(self):
|
def addSignals(self):
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, self.onExit)
|
signal.signal(signal.SIGINT, self.onExit)
|
||||||
signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1))
|
signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1))
|
||||||
|
|
||||||
@@ -74,7 +73,7 @@ class Loader(object):
|
|||||||
|
|
||||||
def onExit(self, signal, frame):
|
def onExit(self, signal, frame):
|
||||||
from couchpotato.core.event import fireEvent
|
from couchpotato.core.event import fireEvent
|
||||||
fireEvent('app.crappy_shutdown', single = True)
|
fireEvent('app.shutdown', single = True)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -81,7 +81,7 @@ class TaskBarIcon(wx.TaskBarIcon):
|
|||||||
webbrowser.open(url)
|
webbrowser.open(url)
|
||||||
|
|
||||||
def onSettings(self, event):
|
def onSettings(self, event):
|
||||||
url = self.frame.parent.getSetting('base_url') + '/settings/'
|
url = self.frame.parent.getSetting('base_url') + 'settings/about/'
|
||||||
webbrowser.open(url)
|
webbrowser.open(url)
|
||||||
|
|
||||||
def onTaskBarClose(self, evt):
|
def onTaskBarClose(self, evt):
|
||||||
@@ -166,8 +166,8 @@ class CouchPotatoApp(wx.App, SoftwareUpdate):
|
|||||||
def OnInit(self):
|
def OnInit(self):
|
||||||
|
|
||||||
# Updater
|
# Updater
|
||||||
base_url = 'http://couchpota.to/updates/%s/' % VERSION
|
base_url = 'https://couchpota.to/updates/%s'
|
||||||
self.InitUpdates(base_url, base_url + 'changelog.html',
|
self.InitUpdates(base_url % VERSION + '/', base_url % 'changelog.html',
|
||||||
icon = wx.Icon('icon.png'))
|
icon = wx.Icon('icon.png'))
|
||||||
|
|
||||||
self.frame = MainFrame(self)
|
self.frame = MainFrame(self)
|
||||||
|
|||||||
@@ -40,3 +40,23 @@ Linux (ubuntu / debian):
|
|||||||
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
|
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
|
||||||
* Add it to defaults. `sudo update-rc.d couchpotato defaults`
|
* Add it to defaults. `sudo update-rc.d couchpotato defaults`
|
||||||
* Open your browser and go to: `http://localhost:5050/`
|
* Open your browser and go to: `http://localhost:5050/`
|
||||||
|
|
||||||
|
|
||||||
|
FreeBSD :
|
||||||
|
|
||||||
|
* Update your ports tree `sudo portsnap fetch update`
|
||||||
|
* Install Python 2.6+ [lang/python](http://www.freshports.org/lang/python) with `cd /usr/ports/lang/python; sudo make install clean`
|
||||||
|
* Install port [databases/py-sqlite3](http://www.freshports.org/databases/py-sqlite3) with `cd /usr/ports/databases/py-sqlite3; sudo make install clean`
|
||||||
|
* Add a symlink to 'python2' `sudo ln -s /usr/local/bin/python /usr/local/bin/python2`
|
||||||
|
* Install port [ftp/libcurl](http://www.freshports.org/ftp/libcurl) with `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean`
|
||||||
|
* Install port [ftp/curl](http://www.freshports.org/ftp/bcurl), deselect 'Asynchronous DNS resolution via c-ares' when prompted as part of config `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean`
|
||||||
|
* Install port [textproc/docbook-xml-450](http://www.freshports.org/textproc/docbook-xml-450) with `cd /usr/ports/textproc/docbook-xml-450; sudo make install clean`
|
||||||
|
* Install port [GIT](http://git-scm.com/) with `cd /usr/ports/devel/git; sudo make install clean`
|
||||||
|
* 'cd' to the folder of your choosing.
|
||||||
|
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
||||||
|
* 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`
|
||||||
|
* Change the paths inside the init script. `sudo vim /etc/init.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`
|
||||||
|
* Open your browser and go to: `http://server:5050/`
|
||||||
|
|||||||
+5
-4
@@ -5,10 +5,11 @@
|
|||||||
* Search through the existing (and closed) issues first. See if you can get your answer there.
|
* Search through the existing (and closed) issues first. See if you can get your answer there.
|
||||||
* Double check the result manually, because it could be an external issue.
|
* Double check the result manually, because it could be an external issue.
|
||||||
* Post logs! Without seeing what is going on, I can't reproduce the error.
|
* Post logs! Without seeing what is going on, I can't reproduce the error.
|
||||||
* What are you settings for the specific problem
|
* What is the movie + quality you are searching for.
|
||||||
* What providers are you using. (While your logs include these, scanning through hundred of lines of log isn't my hobby)
|
* What are you settings for the specific problem.
|
||||||
* Give me a short step by step of how to reproduce
|
* What providers are you using. (While your logs include these, scanning through hundred of lines of log isn't my hobby).
|
||||||
|
* Give me a short step by step of how to reproduce.
|
||||||
* 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 the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example.
|
||||||
* I will mark issues with the "can't reproduce" tag. Don't go asking me "why closed" if it clearly says the issue in the tag ;)
|
* I will mark issues with the "can't reproduce" tag. Don't go asking me "why closed" if it clearly says the issue in the tag ;)
|
||||||
|
|
||||||
**If I don't get enough info, the change of the issue getting closed is a lot bigger ;)**
|
**If I don't get enough info, the chance of the issue getting closed is a lot bigger ;)**
|
||||||
|
|||||||
+108
-52
@@ -1,83 +1,139 @@
|
|||||||
from couchpotato.api import api_docs, api_docs_missing
|
from couchpotato.api import api_docs, api_docs_missing, api
|
||||||
from couchpotato.core.auth import requires_auth
|
|
||||||
from couchpotato.core.event import fireEvent
|
from couchpotato.core.event import fireEvent
|
||||||
from couchpotato.core.helpers.request import getParams, jsonified
|
from couchpotato.core.helpers.variable import md5, tryInt
|
||||||
from couchpotato.core.helpers.variable import md5
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from flask.app import Flask
|
from tornado import template
|
||||||
from flask.blueprints import Blueprint
|
from tornado.web import RequestHandler, authenticated
|
||||||
from flask.globals import request
|
|
||||||
from flask.helpers import url_for
|
|
||||||
from flask.templating import render_template
|
|
||||||
from sqlalchemy.engine import create_engine
|
|
||||||
from sqlalchemy.orm import scoped_session
|
|
||||||
from sqlalchemy.orm.session import sessionmaker
|
|
||||||
from werkzeug.utils import redirect
|
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
app = Flask(__name__, static_folder = 'nope')
|
|
||||||
web = Blueprint('web', __name__)
|
|
||||||
|
|
||||||
|
views = {}
|
||||||
|
template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates'))
|
||||||
|
|
||||||
|
|
||||||
|
class BaseHandler(RequestHandler):
|
||||||
|
|
||||||
|
def get_current_user(self):
|
||||||
|
username = Env.setting('username')
|
||||||
|
password = Env.setting('password')
|
||||||
|
|
||||||
|
if username and password:
|
||||||
|
return self.get_secure_cookie('user')
|
||||||
|
else: # Login when no username or password are set
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Main web handler
|
||||||
|
class WebHandler(BaseHandler):
|
||||||
|
|
||||||
|
@authenticated
|
||||||
|
def get(self, route, *args, **kwargs):
|
||||||
|
route = route.strip('/')
|
||||||
|
if not views.get(route):
|
||||||
|
page_not_found(self)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.write(views[route]())
|
||||||
|
except:
|
||||||
|
log.error("Failed doing web request '%s': %s", (route, traceback.format_exc()))
|
||||||
|
self.write({'success': False, 'error': 'Failed returning results'})
|
||||||
|
|
||||||
|
def addView(route, func, static = False):
|
||||||
|
views[route] = func
|
||||||
|
|
||||||
def get_session(engine = None):
|
def get_session(engine = None):
|
||||||
return Env.getSession(engine)
|
return Env.getSession(engine)
|
||||||
|
|
||||||
def addView(route, func, static = False):
|
|
||||||
web.add_url_rule(route + ('' if static else '/'), endpoint = route if route else 'index', view_func = func)
|
|
||||||
|
|
||||||
""" Web view """
|
# Web view
|
||||||
@web.route('/')
|
|
||||||
@requires_auth
|
|
||||||
def index():
|
def index():
|
||||||
return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env)
|
return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env)
|
||||||
|
addView('', index)
|
||||||
|
|
||||||
""" Api view """
|
# API docs
|
||||||
@web.route('docs/')
|
|
||||||
@requires_auth
|
|
||||||
def apiDocs():
|
def apiDocs():
|
||||||
from couchpotato import app
|
|
||||||
routes = []
|
routes = []
|
||||||
for route, x in sorted(app.view_functions.iteritems()):
|
|
||||||
if route[0:4] == 'api.':
|
for route in api.iterkeys():
|
||||||
routes += [route[4:].replace('::', '.')]
|
routes.append(route)
|
||||||
|
|
||||||
if api_docs.get(''):
|
if api_docs.get(''):
|
||||||
del api_docs['']
|
del api_docs['']
|
||||||
del api_docs_missing['']
|
del api_docs_missing['']
|
||||||
return render_template('api.html', fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing))
|
|
||||||
|
|
||||||
@web.route('getkey/')
|
return template_loader.load('api.html').generate(fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing), Env = Env)
|
||||||
def getApiKey():
|
|
||||||
|
|
||||||
api = None
|
addView('docs', apiDocs)
|
||||||
params = getParams()
|
|
||||||
username = Env.setting('username')
|
|
||||||
password = Env.setting('password')
|
|
||||||
|
|
||||||
if (params.get('u') == md5(username) or not username) and (params.get('p') == password or not password):
|
# Make non basic auth option to get api key
|
||||||
api = Env.setting('api_key')
|
class KeyHandler(RequestHandler):
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
api = None
|
||||||
|
|
||||||
return jsonified({
|
try:
|
||||||
'success': api is not None,
|
username = Env.setting('username')
|
||||||
'api_key': api
|
password = Env.setting('password')
|
||||||
})
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password):
|
||||||
def page_not_found(error):
|
api = Env.setting('api_key')
|
||||||
index_url = url_for('web.index')
|
|
||||||
url = request.path[len(index_url):]
|
self.write({
|
||||||
|
'success': api is not None,
|
||||||
|
'api_key': api
|
||||||
|
})
|
||||||
|
except:
|
||||||
|
log.error('Failed doing key request: %s', (traceback.format_exc()))
|
||||||
|
self.write({'success': False, 'error': 'Failed returning results'})
|
||||||
|
|
||||||
|
|
||||||
|
class LoginHandler(BaseHandler):
|
||||||
|
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
|
||||||
|
if self.get_current_user():
|
||||||
|
self.redirect(Env.get('web_base'))
|
||||||
|
else:
|
||||||
|
self.write(template_loader.load('login.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env))
|
||||||
|
|
||||||
|
def post(self, *args, **kwargs):
|
||||||
|
|
||||||
|
api = None
|
||||||
|
|
||||||
|
username = Env.setting('username')
|
||||||
|
password = Env.setting('password')
|
||||||
|
|
||||||
|
if (self.get_argument('username') == username or not username) and (md5(self.get_argument('password')) == password or not password):
|
||||||
|
api = Env.setting('api_key')
|
||||||
|
|
||||||
|
if api:
|
||||||
|
remember_me = tryInt(self.get_argument('remember_me', default = 0))
|
||||||
|
self.set_secure_cookie('user', api, expires_days = 30 if remember_me > 0 else None)
|
||||||
|
|
||||||
|
self.redirect(Env.get('web_base'))
|
||||||
|
|
||||||
|
class LogoutHandler(BaseHandler):
|
||||||
|
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
self.clear_cookie('user')
|
||||||
|
self.redirect('%slogin/' % Env.get('web_base'))
|
||||||
|
|
||||||
|
|
||||||
|
def page_not_found(rh):
|
||||||
|
index_url = Env.get('web_base')
|
||||||
|
url = rh.request.uri[len(index_url):]
|
||||||
|
|
||||||
if url[:3] != 'api':
|
if url[:3] != 'api':
|
||||||
if request.path != '/':
|
r = index_url + '#' + url.lstrip('/')
|
||||||
r = request.url.replace(request.path, index_url + '#' + url)
|
rh.redirect(r)
|
||||||
else:
|
|
||||||
r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url)
|
|
||||||
return redirect(r)
|
|
||||||
else:
|
else:
|
||||||
time.sleep(0.1)
|
if not Env.get('dev'):
|
||||||
return 'Wrong API key used', 404
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
rh.set_status(404)
|
||||||
|
rh.write('Wrong API key used')
|
||||||
|
|
||||||
|
|||||||
+105
-30
@@ -1,46 +1,64 @@
|
|||||||
from flask.blueprints import Blueprint
|
from couchpotato.core.helpers.request import getParams
|
||||||
from flask.helpers import url_for
|
from couchpotato.core.logger import CPLog
|
||||||
from tornado.ioloop import IOLoop
|
from functools import wraps
|
||||||
|
from threading import Thread
|
||||||
|
from tornado.gen import coroutine
|
||||||
from tornado.web import RequestHandler, asynchronous
|
from tornado.web import RequestHandler, asynchronous
|
||||||
from werkzeug.utils import redirect
|
import json
|
||||||
|
import threading
|
||||||
|
import tornado
|
||||||
|
import traceback
|
||||||
|
import urllib
|
||||||
|
|
||||||
api = Blueprint('api', __name__)
|
log = CPLog(__name__)
|
||||||
api_docs = {}
|
|
||||||
api_docs_missing = []
|
|
||||||
|
api = {}
|
||||||
|
api_locks = {}
|
||||||
api_nonblock = {}
|
api_nonblock = {}
|
||||||
|
|
||||||
|
api_docs = {}
|
||||||
|
api_docs_missing = []
|
||||||
|
|
||||||
|
def run_async(func):
|
||||||
|
@wraps(func)
|
||||||
|
def async_func(*args, **kwargs):
|
||||||
|
func_hl = Thread(target = func, args = args, kwargs = kwargs)
|
||||||
|
func_hl.start()
|
||||||
|
return func_hl
|
||||||
|
|
||||||
|
return async_func
|
||||||
|
|
||||||
|
# NonBlock API handler
|
||||||
class NonBlockHandler(RequestHandler):
|
class NonBlockHandler(RequestHandler):
|
||||||
stoppers = []
|
|
||||||
|
stopper = None
|
||||||
|
|
||||||
@asynchronous
|
@asynchronous
|
||||||
def get(self, route):
|
def get(self, route, *args, **kwargs):
|
||||||
cls = NonBlockHandler
|
route = route.strip('/')
|
||||||
start, stop = api_nonblock[route]
|
start, stop = api_nonblock[route]
|
||||||
cls.stoppers.append(stop)
|
self.stopper = stop
|
||||||
|
|
||||||
start(self.onNewMessage, last_id = self.get_argument("last_id", None))
|
start(self.onNewMessage, last_id = self.get_argument('last_id', None))
|
||||||
|
|
||||||
def onNewMessage(self, response):
|
def onNewMessage(self, response):
|
||||||
if self.request.connection.stream.closed():
|
if self.request.connection.stream.closed():
|
||||||
return
|
return
|
||||||
self.finish(response)
|
|
||||||
|
try:
|
||||||
|
self.finish(response)
|
||||||
|
except:
|
||||||
|
log.error('Failed doing nonblock request: %s', (traceback.format_exc()))
|
||||||
|
try: self.finish({'success': False, 'error': 'Failed returning results'})
|
||||||
|
except: pass
|
||||||
|
|
||||||
def on_connection_close(self):
|
def on_connection_close(self):
|
||||||
cls = NonBlockHandler
|
|
||||||
|
|
||||||
for stop in cls.stoppers:
|
if self.stopper:
|
||||||
stop(self.onNewMessage)
|
self.stopper(self.onNewMessage)
|
||||||
|
|
||||||
cls.stoppers = []
|
self.stopper = None
|
||||||
|
|
||||||
|
|
||||||
def addApiView(route, func, static = False, docs = None, **kwargs):
|
|
||||||
api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func, **kwargs)
|
|
||||||
if docs:
|
|
||||||
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
|
|
||||||
else:
|
|
||||||
api_docs_missing.append(route)
|
|
||||||
|
|
||||||
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
||||||
api_nonblock[route] = func_tuple
|
api_nonblock[route] = func_tuple
|
||||||
@@ -50,9 +68,66 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
|||||||
else:
|
else:
|
||||||
api_docs_missing.append(route)
|
api_docs_missing.append(route)
|
||||||
|
|
||||||
""" Api view """
|
# Blocking API handler
|
||||||
def index():
|
class ApiHandler(RequestHandler):
|
||||||
index_url = url_for('web.index')
|
|
||||||
return redirect(index_url + 'docs/')
|
|
||||||
|
|
||||||
addApiView('', index)
|
@coroutine
|
||||||
|
def get(self, route, *args, **kwargs):
|
||||||
|
route = route.strip('/')
|
||||||
|
if not api.get(route):
|
||||||
|
self.write('API call doesn\'t seem to exist')
|
||||||
|
return
|
||||||
|
|
||||||
|
api_locks[route].acquire()
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
kwargs = {}
|
||||||
|
for x in self.request.arguments:
|
||||||
|
kwargs[x] = urllib.unquote(self.get_argument(x))
|
||||||
|
|
||||||
|
# Split array arguments
|
||||||
|
kwargs = getParams(kwargs)
|
||||||
|
|
||||||
|
# Remove t random string
|
||||||
|
try: del kwargs['t']
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
# Add async callback handler
|
||||||
|
@run_async
|
||||||
|
def run_handler(callback):
|
||||||
|
try:
|
||||||
|
result = api[route](**kwargs)
|
||||||
|
callback(result)
|
||||||
|
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) + ')')
|
||||||
|
elif isinstance(result, tuple) and result[0] == 'redirect':
|
||||||
|
self.redirect(result[1])
|
||||||
|
else:
|
||||||
|
self.write(result)
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed doing api request "%s": %s', (route, traceback.format_exc()))
|
||||||
|
self.write({'success': False, 'error': 'Failed returning results'})
|
||||||
|
|
||||||
|
api_locks[route].release()
|
||||||
|
|
||||||
|
def addApiView(route, func, static = False, docs = None, **kwargs):
|
||||||
|
|
||||||
|
if static: func(route)
|
||||||
|
else:
|
||||||
|
api[route] = func
|
||||||
|
api_locks[route] = threading.Lock()
|
||||||
|
|
||||||
|
if docs:
|
||||||
|
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
|
||||||
|
else:
|
||||||
|
api_docs_missing.append(route)
|
||||||
|
|||||||
@@ -23,20 +23,22 @@ config = [{
|
|||||||
'default': '',
|
'default': '',
|
||||||
'type': 'password',
|
'type': 'password',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
'name': 'host',
|
|
||||||
'advanced': True,
|
|
||||||
'default': '0.0.0.0',
|
|
||||||
'hidden': True,
|
|
||||||
'label': 'IP',
|
|
||||||
'description': 'Host that I should listen to. "0.0.0.0" listens to all ips.',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
'name': 'port',
|
'name': 'port',
|
||||||
'default': 5050,
|
'default': 5050,
|
||||||
'type': 'int',
|
'type': 'int',
|
||||||
'description': 'The port I should listen to.',
|
'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',
|
'name': 'launch_browser',
|
||||||
'default': True,
|
'default': True,
|
||||||
@@ -68,7 +70,7 @@ config = [{
|
|||||||
'name': 'development',
|
'name': 'development',
|
||||||
'default': 0,
|
'default': 0,
|
||||||
'type': 'bool',
|
'type': 'bool',
|
||||||
'description': 'Disables some checks/downloads for faster reloading.',
|
'description': 'Enable this if you\'re developing, and NOT in any other case, thanks.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'data_dir',
|
'name': 'data_dir',
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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.request import jsonified
|
|
||||||
from couchpotato.core.helpers.variable import cleanHost, md5
|
from couchpotato.core.helpers.variable import cleanHost, 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
|
||||||
@@ -57,7 +56,7 @@ class Core(Plugin):
|
|||||||
self.signalHandler()
|
self.signalHandler()
|
||||||
|
|
||||||
def md5Password(self, value):
|
def md5Password(self, value):
|
||||||
return md5(value.encode(Env.get('encoding'))) if value else ''
|
return md5(value) if value else ''
|
||||||
|
|
||||||
def checkApikey(self, value):
|
def checkApikey(self, value):
|
||||||
return value if value and len(value) > 3 else uuid4().hex
|
return value if value and len(value) > 3 else uuid4().hex
|
||||||
@@ -68,28 +67,28 @@ class Core(Plugin):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def available(self):
|
def available(self, **kwargs):
|
||||||
return jsonified({
|
return {
|
||||||
'success': True
|
'success': True
|
||||||
})
|
}
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self, **kwargs):
|
||||||
if self.shutdown_started:
|
if self.shutdown_started:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def shutdown():
|
def shutdown():
|
||||||
self.initShutdown()
|
self.initShutdown()
|
||||||
IOLoop.instance().add_callback(shutdown)
|
IOLoop.current().add_callback(shutdown)
|
||||||
|
|
||||||
return 'shutdown'
|
return 'shutdown'
|
||||||
|
|
||||||
def restart(self):
|
def restart(self, **kwargs):
|
||||||
if self.shutdown_started:
|
if self.shutdown_started:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def restart():
|
def restart():
|
||||||
self.initShutdown(restart = True)
|
self.initShutdown(restart = True)
|
||||||
IOLoop.instance().add_callback(restart)
|
IOLoop.current().add_callback(restart)
|
||||||
|
|
||||||
return 'restarting'
|
return 'restarting'
|
||||||
|
|
||||||
@@ -125,10 +124,10 @@ class Core(Plugin):
|
|||||||
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
log.debug('Save to shutdown/restart')
|
log.debug('Safe to shutdown/restart')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
IOLoop.instance().stop()
|
IOLoop.current().stop()
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass
|
pass
|
||||||
except:
|
except:
|
||||||
@@ -152,14 +151,14 @@ class Core(Plugin):
|
|||||||
|
|
||||||
def createBaseUrl(self):
|
def createBaseUrl(self):
|
||||||
host = Env.setting('host')
|
host = Env.setting('host')
|
||||||
if host == '0.0.0.0':
|
if host == '0.0.0.0' or host == '':
|
||||||
host = 'localhost'
|
host = 'localhost'
|
||||||
port = Env.setting('port')
|
port = Env.setting('port')
|
||||||
|
|
||||||
return '%s:%d%s' % (cleanHost(host).rstrip('/'), int(port), '/' + Env.setting('url_base').lstrip('/') if Env.setting('url_base') else '')
|
return '%s:%d%s' % (cleanHost(host).rstrip('/'), int(port), Env.get('web_base'))
|
||||||
|
|
||||||
def createApiUrl(self):
|
def createApiUrl(self):
|
||||||
return '%s/api/%s' % (self.createBaseUrl(), Env.setting('api_key'))
|
return '%sapi/%s' % (self.createBaseUrl(), Env.setting('api_key'))
|
||||||
|
|
||||||
def version(self):
|
def version(self):
|
||||||
ver = fireEvent('updater.info', single = True)
|
ver = fireEvent('updater.info', single = True)
|
||||||
@@ -170,14 +169,16 @@ class Core(Plugin):
|
|||||||
|
|
||||||
return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash'])
|
return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash'])
|
||||||
|
|
||||||
def versionView(self):
|
def versionView(self, **kwargs):
|
||||||
return jsonified({
|
return {
|
||||||
'version': self.version()
|
'version': self.version()
|
||||||
})
|
}
|
||||||
|
|
||||||
def signalHandler(self):
|
def signalHandler(self):
|
||||||
|
if Env.get('daemonized'): return
|
||||||
|
|
||||||
def signal_handler(signal, frame):
|
def signal_handler(signal, frame):
|
||||||
fireEvent('app.do_shutdown')
|
fireEvent('app.shutdown', single = True)
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|||||||
@@ -1,15 +1,62 @@
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
|
from couchpotato.core.helpers.encoding import ss
|
||||||
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
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 couchpotato.environment import Env
|
||||||
|
from minify.cssmin import cssmin
|
||||||
|
from minify.jsmin import jsmin
|
||||||
|
from tornado.web import StaticFileHandler
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ClientScript(Plugin):
|
class ClientScript(Plugin):
|
||||||
|
|
||||||
urls = {
|
core_static = {
|
||||||
'style': {},
|
'style': [
|
||||||
'script': {},
|
'style/main.css',
|
||||||
|
'style/uniform.generic.css',
|
||||||
|
'style/uniform.css',
|
||||||
|
'style/settings.css',
|
||||||
|
],
|
||||||
|
'script': [
|
||||||
|
'scripts/library/mootools.js',
|
||||||
|
'scripts/library/mootools_more.js',
|
||||||
|
'scripts/library/uniform.js',
|
||||||
|
'scripts/library/form_replacement/form_check.js',
|
||||||
|
'scripts/library/form_replacement/form_radio.js',
|
||||||
|
'scripts/library/form_replacement/form_dropdown.js',
|
||||||
|
'scripts/library/form_replacement/form_selectoption.js',
|
||||||
|
'scripts/library/question.js',
|
||||||
|
'scripts/library/scrollspy.js',
|
||||||
|
'scripts/library/spin.js',
|
||||||
|
'scripts/couchpotato.js',
|
||||||
|
'scripts/api.js',
|
||||||
|
'scripts/library/history.js',
|
||||||
|
'scripts/page.js',
|
||||||
|
'scripts/block.js',
|
||||||
|
'scripts/block/navigation.js',
|
||||||
|
'scripts/block/footer.js',
|
||||||
|
'scripts/block/menu.js',
|
||||||
|
'scripts/page/home.js',
|
||||||
|
'scripts/page/wanted.js',
|
||||||
|
'scripts/page/settings.js',
|
||||||
|
'scripts/page/about.js',
|
||||||
|
'scripts/page/manage.js',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
urls = {'style': {}, 'script': {}, }
|
||||||
|
minified = {'style': {}, 'script': {}, }
|
||||||
|
paths = {'style': {}, 'script': {}, }
|
||||||
|
comment = {
|
||||||
|
'style': '/*** %s:%d ***/\n',
|
||||||
|
'script': '// %s:%d\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
html = {
|
html = {
|
||||||
@@ -24,6 +71,77 @@ class ClientScript(Plugin):
|
|||||||
addEvent('clientscript.get_styles', self.getStyles)
|
addEvent('clientscript.get_styles', self.getStyles)
|
||||||
addEvent('clientscript.get_scripts', self.getScripts)
|
addEvent('clientscript.get_scripts', self.getScripts)
|
||||||
|
|
||||||
|
if not Env.get('dev'):
|
||||||
|
addEvent('app.load', self.minify)
|
||||||
|
|
||||||
|
self.addCore()
|
||||||
|
|
||||||
|
def addCore(self):
|
||||||
|
|
||||||
|
for static_type in self.core_static:
|
||||||
|
for rel_path in self.core_static.get(static_type):
|
||||||
|
file_path = os.path.join(Env.get('app_dir'), 'couchpotato', 'static', rel_path)
|
||||||
|
core_url = 'static/%s' % rel_path
|
||||||
|
|
||||||
|
if static_type == 'script':
|
||||||
|
self.registerScript(core_url, file_path, position = 'front')
|
||||||
|
else:
|
||||||
|
self.registerStyle(core_url, file_path, position = 'front')
|
||||||
|
|
||||||
|
|
||||||
|
def minify(self):
|
||||||
|
|
||||||
|
# Create cache dir
|
||||||
|
cache = Env.get('cache_dir')
|
||||||
|
parent_dir = os.path.join(cache, 'minified')
|
||||||
|
self.makeDir(parent_dir)
|
||||||
|
|
||||||
|
Env.get('app').add_handlers(".*$", [(Env.get('web_base') + 'minified/(.*)', StaticFileHandler, {'path': parent_dir})])
|
||||||
|
|
||||||
|
for file_type in ['style', 'script']:
|
||||||
|
ext = 'js' if file_type is 'script' else 'css'
|
||||||
|
positions = self.paths.get(file_type, {})
|
||||||
|
for position in positions:
|
||||||
|
files = positions.get(position)
|
||||||
|
self._minify(file_type, files, position, position + '.' + ext)
|
||||||
|
|
||||||
|
def _minify(self, file_type, files, position, out):
|
||||||
|
|
||||||
|
cache = Env.get('cache_dir')
|
||||||
|
out_name = out
|
||||||
|
out = os.path.join(cache, 'minified', out_name)
|
||||||
|
|
||||||
|
raw = []
|
||||||
|
for file_path in files:
|
||||||
|
f = open(file_path, 'r').read()
|
||||||
|
|
||||||
|
if file_type == 'script':
|
||||||
|
data = jsmin(f)
|
||||||
|
else:
|
||||||
|
data = self.prefix(f)
|
||||||
|
data = cssmin(data)
|
||||||
|
data = data.replace('../images/', '../static/images/')
|
||||||
|
data = data.replace('../fonts/', '../static/fonts/')
|
||||||
|
data = data.replace('../../static/', '../static/') # Replace inside plugins
|
||||||
|
|
||||||
|
raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data})
|
||||||
|
|
||||||
|
# Combine all files together with some comments
|
||||||
|
data = ''
|
||||||
|
for r in raw:
|
||||||
|
data += self.comment.get(file_type) % (ss(r.get('file')), r.get('date'))
|
||||||
|
data += r.get('data') + '\n\n'
|
||||||
|
|
||||||
|
self.createFile(out, data.strip())
|
||||||
|
|
||||||
|
if not self.minified.get(file_type):
|
||||||
|
self.minified[file_type] = {}
|
||||||
|
if not self.minified[file_type].get(position):
|
||||||
|
self.minified[file_type][position] = []
|
||||||
|
|
||||||
|
minified_url = 'minified/%s?%s' % (out_name, tryInt(os.path.getmtime(out)))
|
||||||
|
self.minified[file_type][position].append(minified_url)
|
||||||
|
|
||||||
def getStyles(self, *args, **kwargs):
|
def getStyles(self, *args, **kwargs):
|
||||||
return self.get('style', *args, **kwargs)
|
return self.get('style', *args, **kwargs)
|
||||||
|
|
||||||
@@ -35,22 +153,57 @@ class ClientScript(Plugin):
|
|||||||
data = '' if as_html else []
|
data = '' if as_html else []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
try:
|
||||||
|
if not Env.get('dev'):
|
||||||
|
return self.minified[type][location]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
return self.urls[type][location]
|
return self.urls[type][location]
|
||||||
except Exception, e:
|
except:
|
||||||
log.error(e)
|
log.error('Error getting minified %s, %s: %s', (type, location, traceback.format_exc()))
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def registerStyle(self, path, position = 'head'):
|
def registerStyle(self, api_path, file_path, position = 'head'):
|
||||||
self.register(path, 'style', position)
|
self.register(api_path, file_path, 'style', position)
|
||||||
|
|
||||||
def registerScript(self, path, position = 'head'):
|
def registerScript(self, api_path, file_path, position = 'head'):
|
||||||
self.register(path, 'script', position)
|
self.register(api_path, file_path, 'script', position)
|
||||||
|
|
||||||
def register(self, filepath, type, location):
|
def register(self, api_path, file_path, type, location):
|
||||||
|
|
||||||
|
api_path = '%s?%s' % (api_path, tryInt(os.path.getmtime(file_path)))
|
||||||
|
|
||||||
if not self.urls[type].get(location):
|
if not self.urls[type].get(location):
|
||||||
self.urls[type][location] = []
|
self.urls[type][location] = []
|
||||||
|
self.urls[type][location].append(api_path)
|
||||||
|
|
||||||
filePath = filepath
|
if not self.paths[type].get(location):
|
||||||
self.urls[type][location].append(filePath)
|
self.paths[type][location] = []
|
||||||
|
self.paths[type][location].append(file_path)
|
||||||
|
|
||||||
|
prefix_properties = ['border-radius', 'transform', 'transition', 'box-shadow']
|
||||||
|
prefix_tags = ['ms', 'moz', 'webkit']
|
||||||
|
def prefix(self, data):
|
||||||
|
|
||||||
|
trimmed_data = re.sub('(\t|\n|\r)+', '', data)
|
||||||
|
|
||||||
|
new_data = ''
|
||||||
|
colon_split = trimmed_data.split(';')
|
||||||
|
for splt in colon_split:
|
||||||
|
curl_split = splt.strip().split('{')
|
||||||
|
for curly in curl_split:
|
||||||
|
curly = curly.strip()
|
||||||
|
for prop in self.prefix_properties:
|
||||||
|
if curly[:len(prop) + 1] == prop + ':':
|
||||||
|
for tag in self.prefix_tags:
|
||||||
|
new_data += ' -%s-%s; ' % (tag, curly)
|
||||||
|
|
||||||
|
new_data += curly + (' { ' if len(curl_split) > 1 else ' ')
|
||||||
|
|
||||||
|
new_data += '; '
|
||||||
|
|
||||||
|
new_data = new_data.replace('{ ;', '; ').replace('} ;', '} ')
|
||||||
|
|
||||||
|
return new_data
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from apscheduler.scheduler import Scheduler as Sched
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
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
|
||||||
import logging
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -17,51 +16,19 @@ class Scheduler(Plugin):
|
|||||||
|
|
||||||
addEvent('schedule.cron', self.cron)
|
addEvent('schedule.cron', self.cron)
|
||||||
addEvent('schedule.interval', self.interval)
|
addEvent('schedule.interval', self.interval)
|
||||||
addEvent('schedule.start', self.start)
|
addEvent('schedule.remove', self.remove)
|
||||||
addEvent('schedule.restart', self.start)
|
|
||||||
|
|
||||||
addEvent('app.load', self.start)
|
|
||||||
|
|
||||||
self.sched = Sched(misfire_grace_time = 60)
|
self.sched = Sched(misfire_grace_time = 60)
|
||||||
|
|
||||||
def remove(self, identifier):
|
|
||||||
for type in ['interval', 'cron']:
|
|
||||||
try:
|
|
||||||
self.sched.unschedule_job(getattr(self, type)[identifier]['job'])
|
|
||||||
log.debug('%s unscheduled %s', (type.capitalize(), identifier))
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
|
|
||||||
# Stop all running
|
|
||||||
self.stop()
|
|
||||||
|
|
||||||
# Crons
|
|
||||||
for identifier in self.crons:
|
|
||||||
try:
|
|
||||||
self.remove(identifier)
|
|
||||||
cron = self.crons[identifier]
|
|
||||||
job = self.sched.add_cron_job(cron['handle'], day = cron['day'], hour = cron['hour'], minute = cron['minute'])
|
|
||||||
cron['job'] = job
|
|
||||||
except ValueError, e:
|
|
||||||
log.error('Failed adding cronjob: %s', e)
|
|
||||||
|
|
||||||
# Intervals
|
|
||||||
for identifier in self.intervals:
|
|
||||||
try:
|
|
||||||
self.remove(identifier)
|
|
||||||
interval = self.intervals[identifier]
|
|
||||||
job = self.sched.add_interval_job(interval['handle'], hours = interval['hours'], minutes = interval['minutes'], seconds = interval['seconds'])
|
|
||||||
interval['job'] = job
|
|
||||||
except ValueError, e:
|
|
||||||
log.error('Failed adding interval cronjob: %s', e)
|
|
||||||
|
|
||||||
# Start it
|
|
||||||
log.debug('Starting scheduler')
|
|
||||||
self.sched.start()
|
self.sched.start()
|
||||||
self.started = True
|
self.started = True
|
||||||
log.debug('Scheduler started')
|
|
||||||
|
def remove(self, identifier):
|
||||||
|
for cron_type in ['intervals', 'crons']:
|
||||||
|
try:
|
||||||
|
self.sched.unschedule_job(getattr(self, cron_type)[identifier]['job'])
|
||||||
|
log.debug('%s unscheduled %s', (cron_type.capitalize(), identifier))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
def doShutdown(self):
|
def doShutdown(self):
|
||||||
super(Scheduler, self).doShutdown()
|
super(Scheduler, self).doShutdown()
|
||||||
@@ -83,6 +50,7 @@ class Scheduler(Plugin):
|
|||||||
'day': day,
|
'day': day,
|
||||||
'hour': hour,
|
'hour': hour,
|
||||||
'minute': minute,
|
'minute': minute,
|
||||||
|
'job': self.sched.add_cron_job(handle, day = day, hour = hour, minute = minute)
|
||||||
}
|
}
|
||||||
|
|
||||||
def interval(self, identifier = '', handle = None, hours = 0, minutes = 0, seconds = 0):
|
def interval(self, identifier = '', handle = None, hours = 0, minutes = 0, seconds = 0):
|
||||||
@@ -94,4 +62,5 @@ class Scheduler(Plugin):
|
|||||||
'hours': hours,
|
'hours': hours,
|
||||||
'minutes': minutes,
|
'minutes': minutes,
|
||||||
'seconds': seconds,
|
'seconds': seconds,
|
||||||
|
'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||||
from couchpotato.core.helpers.encoding import ss
|
from couchpotato.core.helpers.encoding import ss
|
||||||
from couchpotato.core.helpers.request import jsonified
|
|
||||||
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 couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
@@ -15,6 +14,7 @@ import tarfile
|
|||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import version
|
import version
|
||||||
|
import zipfile
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -32,11 +32,10 @@ class Updater(Plugin):
|
|||||||
else:
|
else:
|
||||||
self.updater = SourceUpdater()
|
self.updater = SourceUpdater()
|
||||||
|
|
||||||
fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
|
addEvent('app.load', self.setCrons)
|
||||||
addEvent('app.load', self.autoUpdate)
|
|
||||||
addEvent('updater.info', self.info)
|
addEvent('updater.info', self.info)
|
||||||
|
|
||||||
addApiView('updater.info', self.getInfo, docs = {
|
addApiView('updater.info', self.info, docs = {
|
||||||
'desc': 'Get updater information',
|
'desc': 'Get updater information',
|
||||||
'return': {
|
'return': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
@@ -52,8 +51,17 @@ class Updater(Plugin):
|
|||||||
'return': {'type': 'see updater.info'}
|
'return': {'type': 'see updater.info'}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
addEvent('setting.save.updater.enabled.after', self.setCrons)
|
||||||
|
|
||||||
|
def setCrons(self):
|
||||||
|
|
||||||
|
fireEvent('schedule.remove', 'updater.check', single = True)
|
||||||
|
if self.isEnabled():
|
||||||
|
fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
|
||||||
|
self.autoUpdate() # Check after enabling
|
||||||
|
|
||||||
def autoUpdate(self):
|
def autoUpdate(self):
|
||||||
if self.check() and self.conf('automatic') and not self.updater.update_failed:
|
if self.isEnabled() and self.check() and self.conf('automatic') and not self.updater.update_failed:
|
||||||
if self.updater.doUpdate():
|
if self.updater.doUpdate():
|
||||||
|
|
||||||
# Notify before restarting
|
# Notify before restarting
|
||||||
@@ -71,31 +79,30 @@ class Updater(Plugin):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def check(self):
|
def check(self, force = False):
|
||||||
if self.isDisabled():
|
if not force and self.isDisabled():
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.updater.check():
|
if self.updater.check():
|
||||||
if not self.available_notified and self.conf('notification') and not self.conf('automatic'):
|
if not self.available_notified and self.conf('notification') and not self.conf('automatic'):
|
||||||
fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
|
info = self.updater.info()
|
||||||
|
version_date = datetime.fromtimestamp(info['update_version']['date'])
|
||||||
|
fireEvent('updater.available', message = 'A new update with hash "%s" is available, this version is from %s' % (info['update_version']['hash'], version_date), data = info)
|
||||||
self.available_notified = True
|
self.available_notified = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def info(self):
|
def info(self, **kwargs):
|
||||||
return self.updater.info()
|
return self.updater.info()
|
||||||
|
|
||||||
def getInfo(self):
|
def checkView(self, **kwargs):
|
||||||
return jsonified(self.updater.info())
|
return {
|
||||||
|
'update_available': self.check(force = True),
|
||||||
def checkView(self):
|
|
||||||
return jsonified({
|
|
||||||
'update_available': self.check(),
|
|
||||||
'info': self.updater.info()
|
'info': self.updater.info()
|
||||||
})
|
}
|
||||||
|
|
||||||
def doUpdateView(self):
|
def doUpdateView(self, **kwargs):
|
||||||
|
|
||||||
self.check()
|
self.check()
|
||||||
if not self.updater.update_version:
|
if not self.updater.update_version:
|
||||||
@@ -110,9 +117,9 @@ class Updater(Plugin):
|
|||||||
if not success:
|
if not success:
|
||||||
success = True
|
success = True
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': success
|
'success': success
|
||||||
})
|
}
|
||||||
|
|
||||||
|
|
||||||
class BaseUpdater(Plugin):
|
class BaseUpdater(Plugin):
|
||||||
@@ -125,13 +132,11 @@ class BaseUpdater(Plugin):
|
|||||||
update_failed = False
|
update_failed = False
|
||||||
update_version = None
|
update_version = None
|
||||||
last_check = 0
|
last_check = 0
|
||||||
|
auto_register_static = False
|
||||||
|
|
||||||
def doUpdate(self):
|
def doUpdate(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def getInfo(self):
|
|
||||||
return jsonified(self.info())
|
|
||||||
|
|
||||||
def info(self):
|
def info(self):
|
||||||
return {
|
return {
|
||||||
'last_check': self.last_check,
|
'last_check': self.last_check,
|
||||||
@@ -255,11 +260,11 @@ class SourceUpdater(BaseUpdater):
|
|||||||
def doUpdate(self):
|
def doUpdate(self):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
url = 'https://github.com/%s/%s/tarball/%s' % (self.repo_user, self.repo_name, self.branch)
|
download_data = fireEvent('cp.source_url', repo = self.repo_user, repo_name = self.repo_name, branch = self.branch, single = True)
|
||||||
destination = os.path.join(Env.get('cache_dir'), self.update_version.get('hash') + '.tar.gz')
|
destination = os.path.join(Env.get('cache_dir'), self.update_version.get('hash')) + '.' + download_data.get('type')
|
||||||
extracted_path = os.path.join(Env.get('cache_dir'), 'temp_updater')
|
|
||||||
|
|
||||||
destination = fireEvent('file.download', url = url, dest = destination, single = True)
|
extracted_path = os.path.join(Env.get('cache_dir'), 'temp_updater')
|
||||||
|
destination = fireEvent('file.download', url = download_data.get('url'), dest = destination, single = True)
|
||||||
|
|
||||||
# Cleanup leftover from last time
|
# Cleanup leftover from last time
|
||||||
if os.path.isdir(extracted_path):
|
if os.path.isdir(extracted_path):
|
||||||
@@ -267,9 +272,15 @@ class SourceUpdater(BaseUpdater):
|
|||||||
self.makeDir(extracted_path)
|
self.makeDir(extracted_path)
|
||||||
|
|
||||||
# Extract
|
# Extract
|
||||||
tar = tarfile.open(destination)
|
if download_data.get('type') == 'zip':
|
||||||
tar.extractall(path = extracted_path)
|
zip = zipfile.ZipFile(destination)
|
||||||
tar.close()
|
zip.extractall(extracted_path)
|
||||||
|
zip.close()
|
||||||
|
else:
|
||||||
|
tar = tarfile.open(destination)
|
||||||
|
tar.extractall(path = extracted_path)
|
||||||
|
tar.close()
|
||||||
|
|
||||||
os.remove(destination)
|
os.remove(destination)
|
||||||
|
|
||||||
if self.replaceWith(os.path.join(extracted_path, os.listdir(extracted_path)[0])):
|
if self.replaceWith(os.path.join(extracted_path, os.listdir(extracted_path)[0])):
|
||||||
|
|||||||
@@ -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, 1000))
|
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);
|
||||||
@@ -84,7 +84,7 @@ var UpdaterBase = new Class({
|
|||||||
'click': self.doUpdate.bind(self)
|
'click': self.doUpdate.bind(self)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
).inject($(document.body).getElement('.header'))
|
).inject(document.body)
|
||||||
},
|
},
|
||||||
|
|
||||||
doUpdate: function(){
|
doUpdate: function(){
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
from couchpotato.core.helpers.variable import md5
|
|
||||||
from couchpotato.environment import Env
|
|
||||||
from flask import request, Response
|
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
def check_auth(username, password):
|
|
||||||
return username == Env.setting('username') and password == Env.setting('password')
|
|
||||||
|
|
||||||
def authenticate():
|
|
||||||
return Response(
|
|
||||||
'This is not the page you are looking for. *waves hand*', 401,
|
|
||||||
{'WWW-Authenticate': 'Basic realm="CouchPotato Login"'}
|
|
||||||
)
|
|
||||||
|
|
||||||
def requires_auth(f):
|
|
||||||
|
|
||||||
@wraps(f)
|
|
||||||
def decorated(*args, **kwargs):
|
|
||||||
auth = getattr(request, 'authorization')
|
|
||||||
if Env.setting('username') and Env.setting('password'):
|
|
||||||
if (not auth or not check_auth(auth.username.decode('latin1'), md5(auth.password.decode('latin1').encode(Env.get('encoding'))))):
|
|
||||||
return authenticate()
|
|
||||||
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
|
|
||||||
return decorated
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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,20 +1,22 @@
|
|||||||
from base64 import b32decode, b16encode
|
from base64 import b32decode, b16encode
|
||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
|
from couchpotato.core.helpers.variable import mergeDicts
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.providers.base import Provider
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Downloader(Plugin):
|
class Downloader(Provider):
|
||||||
|
|
||||||
type = []
|
protocol = []
|
||||||
|
http_time_between_calls = 0
|
||||||
|
|
||||||
torrent_sources = [
|
torrent_sources = [
|
||||||
'http://torrage.com/torrent/%s.torrent',
|
'http://torrage.com/torrent/%s.torrent',
|
||||||
'http://torcache.net/torrent/%s.torrent',
|
'https://torcache.net/torrent/%s.torrent',
|
||||||
]
|
]
|
||||||
|
|
||||||
torrent_trackers = [
|
torrent_trackers = [
|
||||||
@@ -32,24 +34,71 @@ class Downloader(Plugin):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
addEvent('download', self.download)
|
addEvent('download', self._download)
|
||||||
addEvent('download.status', self.getAllDownloadStatus)
|
addEvent('download.enabled', self._isEnabled)
|
||||||
addEvent('download.remove_failed', self.removeFailed)
|
addEvent('download.enabled_protocols', self.getEnabledProtocol)
|
||||||
|
addEvent('download.status', self._getAllDownloadStatus)
|
||||||
|
addEvent('download.remove_failed', self._removeFailed)
|
||||||
|
addEvent('download.pause', self._pause)
|
||||||
|
addEvent('download.process_complete', self._processComplete)
|
||||||
|
|
||||||
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
def getEnabledProtocol(self):
|
||||||
pass
|
for download_protocol in self.protocol:
|
||||||
|
if self.isEnabled(manual = True, data = {'protocol': download_protocol}):
|
||||||
|
return self.protocol
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _download(self, data = None, movie = None, manual = False, filedata = None):
|
||||||
|
if not movie: movie = {}
|
||||||
|
if not data: data = {}
|
||||||
|
|
||||||
|
if self.isDisabled(manual, data):
|
||||||
|
return
|
||||||
|
return self.download(data = data, movie = movie, filedata = filedata)
|
||||||
|
|
||||||
|
def _getAllDownloadStatus(self):
|
||||||
|
if self.isDisabled(manual = True, data = {}):
|
||||||
|
return
|
||||||
|
|
||||||
|
return self.getAllDownloadStatus()
|
||||||
|
|
||||||
def getAllDownloadStatus(self):
|
def getAllDownloadStatus(self):
|
||||||
return False
|
return
|
||||||
|
|
||||||
def removeFailed(self, name = {}, nzo_id = {}):
|
def _removeFailed(self, item):
|
||||||
return False
|
if self.isDisabled(manual = True, data = {}):
|
||||||
|
return
|
||||||
|
|
||||||
def isCorrectType(self, item_type):
|
if item and item.get('downloader') == self.getName():
|
||||||
is_correct = item_type in self.type
|
if self.conf('delete_failed'):
|
||||||
|
return self.removeFailed(item)
|
||||||
|
|
||||||
|
return False
|
||||||
|
return
|
||||||
|
|
||||||
|
def removeFailed(self, item):
|
||||||
|
return
|
||||||
|
|
||||||
|
def _processComplete(self, item):
|
||||||
|
if self.isDisabled(manual = True, data = {}):
|
||||||
|
return
|
||||||
|
|
||||||
|
if item and item.get('downloader') == self.getName():
|
||||||
|
if self.conf('remove_complete', default = False):
|
||||||
|
return self.processComplete(item = item, delete_files = self.conf('delete_files', default = False))
|
||||||
|
|
||||||
|
return False
|
||||||
|
return
|
||||||
|
|
||||||
|
def processComplete(self, item, delete_files):
|
||||||
|
return
|
||||||
|
|
||||||
|
def isCorrectProtocol(self, item_protocol):
|
||||||
|
is_correct = item_protocol in self.protocol
|
||||||
|
|
||||||
if not is_correct:
|
if not is_correct:
|
||||||
log.debug("Downloader doesn't support this type")
|
log.debug("Downloader doesn't support this protocol")
|
||||||
|
|
||||||
return is_correct
|
return is_correct
|
||||||
|
|
||||||
@@ -73,12 +122,75 @@ class Downloader(Plugin):
|
|||||||
except:
|
except:
|
||||||
log.debug('Torrent hash "%s" wasn\'t found on: %s', (torrent_hash, source))
|
log.debug('Torrent hash "%s" wasn\'t found on: %s', (torrent_hash, source))
|
||||||
|
|
||||||
log.error('Failed converting magnet url to torrent: %s', (torrent_hash))
|
log.error('Failed converting magnet url to torrent: %s', torrent_hash)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def isDisabled(self, manual):
|
def downloadReturnId(self, download_id):
|
||||||
return not self.isEnabled(manual)
|
return {
|
||||||
|
'downloader': self.getName(),
|
||||||
|
'id': download_id
|
||||||
|
}
|
||||||
|
|
||||||
|
def isDisabled(self, manual = False, data = None):
|
||||||
|
if not data: data = {}
|
||||||
|
|
||||||
|
return not self.isEnabled(manual, data)
|
||||||
|
|
||||||
|
def _isEnabled(self, manual, data = None):
|
||||||
|
if not data: data = {}
|
||||||
|
|
||||||
|
if not self.isEnabled(manual, data):
|
||||||
|
return
|
||||||
|
return True
|
||||||
|
|
||||||
|
def isEnabled(self, manual = False, data = None):
|
||||||
|
if not data: data = {}
|
||||||
|
|
||||||
def isEnabled(self, manual):
|
|
||||||
d_manual = self.conf('manual', default = False)
|
d_manual = self.conf('manual', default = False)
|
||||||
return super(Downloader, self).isEnabled() and ((d_manual and manual) or (d_manual is False))
|
return super(Downloader, self).isEnabled() and \
|
||||||
|
(d_manual and manual or d_manual is False) and \
|
||||||
|
(not data or self.isCorrectProtocol(data.get('protocol')))
|
||||||
|
|
||||||
|
def _pause(self, item, pause = True):
|
||||||
|
if self.isDisabled(manual = True, data = {}):
|
||||||
|
return
|
||||||
|
|
||||||
|
if item and item.get('downloader') == self.getName():
|
||||||
|
self.pause(item, pause)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def pause(self, item, pause):
|
||||||
|
return
|
||||||
|
|
||||||
|
class StatusList(list):
|
||||||
|
|
||||||
|
provider = None
|
||||||
|
|
||||||
|
def __init__(self, provider, **kwargs):
|
||||||
|
|
||||||
|
self.provider = provider
|
||||||
|
self.kwargs = kwargs
|
||||||
|
|
||||||
|
super(StatusList, self).__init__()
|
||||||
|
|
||||||
|
def extend(self, results):
|
||||||
|
for r in results:
|
||||||
|
self.append(r)
|
||||||
|
|
||||||
|
def append(self, result):
|
||||||
|
new_result = self.fillResult(result)
|
||||||
|
super(StatusList, self).append(new_result)
|
||||||
|
|
||||||
|
def fillResult(self, result):
|
||||||
|
|
||||||
|
defaults = {
|
||||||
|
'id': 0,
|
||||||
|
'status': 'busy',
|
||||||
|
'downloader': self.provider.getName(),
|
||||||
|
'folder': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergeDicts(defaults, result)
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'downloaders',
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
'name': 'blackhole',
|
'name': 'blackhole',
|
||||||
'label': 'Black hole',
|
'label': 'Black hole',
|
||||||
'description': 'Download the NZB/Torrent to a specific folder.',
|
'description': 'Download the NZB/Torrent to a specific folder.',
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
from __future__ import with_statement
|
from __future__ import with_statement
|
||||||
from couchpotato.core.downloaders.base import Downloader
|
from couchpotato.core.downloaders.base import Downloader
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.environment import Env
|
||||||
import os
|
import os
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Blackhole(Downloader):
|
class Blackhole(Downloader):
|
||||||
|
|
||||||
type = ['nzb', 'torrent', 'torrent_magnet']
|
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
||||||
|
|
||||||
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
def download(self, data = None, movie = None, filedata = None):
|
||||||
if self.isDisabled(manual) or \
|
if not movie: movie = {}
|
||||||
(not self.isCorrectType(data.get('type')) or \
|
if not data: data = {}
|
||||||
(not self.conf('use_for') in ['both', 'torrent' if 'torrent' in data.get('type') else data.get('type')])):
|
|
||||||
return
|
|
||||||
|
|
||||||
directory = self.conf('directory')
|
directory = self.conf('directory')
|
||||||
if not directory or not os.path.isdir(directory):
|
if not directory or not os.path.isdir(directory):
|
||||||
log.error('No directory set for blackhole %s download.', data.get('type'))
|
log.error('No directory set for blackhole %s download.', data.get('protocol'))
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
if not filedata or len(filedata) < 50:
|
if not filedata or len(filedata) < 50:
|
||||||
try:
|
try:
|
||||||
if data.get('type') == 'torrent_magnet':
|
if data.get('protocol') == 'torrent_magnet':
|
||||||
filedata = self.magnetToTorrent(data.get('url'))
|
filedata = self.magnetToTorrent(data.get('url'))
|
||||||
data['type'] = 'torrent'
|
data['protocol'] = 'torrent'
|
||||||
except:
|
except:
|
||||||
log.error('Failed download torrent via magnet url: %s', traceback.format_exc())
|
log.error('Failed download torrent via magnet url: %s', traceback.format_exc())
|
||||||
|
|
||||||
@@ -37,9 +37,10 @@ class Blackhole(Downloader):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if not os.path.isfile(fullPath):
|
if not os.path.isfile(fullPath):
|
||||||
log.info('Downloading %s to %s.', (data.get('type'), fullPath))
|
log.info('Downloading %s to %s.', (data.get('protocol'), fullPath))
|
||||||
with open(fullPath, 'wb') as f:
|
with open(fullPath, 'wb') as f:
|
||||||
f.write(filedata)
|
f.write(filedata)
|
||||||
|
os.chmod(fullPath, Env.getPermission('file'))
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
log.info('File %s already exists.', fullPath)
|
log.info('File %s already exists.', fullPath)
|
||||||
@@ -52,4 +53,24 @@ class Blackhole(Downloader):
|
|||||||
except:
|
except:
|
||||||
log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc()))
|
log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc()))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def getEnabledProtocol(self):
|
||||||
|
if self.conf('use_for') == 'both':
|
||||||
|
return super(Blackhole, self).getEnabledProtocol()
|
||||||
|
elif self.conf('use_for') == 'torrent':
|
||||||
|
return ['torrent', 'torrent_magnet']
|
||||||
|
else:
|
||||||
|
return ['nzb']
|
||||||
|
|
||||||
|
def isEnabled(self, manual = False, data = None):
|
||||||
|
if not data: data = {}
|
||||||
|
for_protocol = ['both']
|
||||||
|
if data and 'torrent' in data.get('protocol'):
|
||||||
|
for_protocol.append('torrent')
|
||||||
|
elif data:
|
||||||
|
for_protocol.append(data.get('protocol'))
|
||||||
|
|
||||||
|
return super(Blackhole, self).isEnabled(manual, data) and \
|
||||||
|
((self.conf('use_for') in for_protocol))
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
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.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
from base64 import b64encode
|
||||||
|
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||||
|
from couchpotato.core.helpers.encoding import isInt, ss
|
||||||
|
from couchpotato.core.helpers.variable import tryFloat
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
from datetime import timedelta
|
||||||
|
from synchronousdeluge import DelugeClient
|
||||||
|
import os.path
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Deluge(Downloader):
|
||||||
|
|
||||||
|
protocol = ['torrent', 'torrent_magnet']
|
||||||
|
log = CPLog(__name__)
|
||||||
|
drpc = None
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
# Load host from config and split out port.
|
||||||
|
host = self.conf('host').split(':')
|
||||||
|
if not isInt(host[1]):
|
||||||
|
log.error('Config properties are not filled in correctly, port is missing.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not self.drpc:
|
||||||
|
self.drpc = DelugeRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||||
|
|
||||||
|
return self.drpc
|
||||||
|
|
||||||
|
def download(self, data, movie, filedata = None):
|
||||||
|
log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol')))
|
||||||
|
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not filedata and data.get('protocol') == 'torrent':
|
||||||
|
log.error('Failed sending torrent, no data')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Set parameters for Deluge
|
||||||
|
options = {
|
||||||
|
'add_paused': self.conf('paused', default = 0),
|
||||||
|
'label': self.conf('label')
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.conf('directory'):
|
||||||
|
if os.path.isdir(self.conf('directory')):
|
||||||
|
options['download_location'] = self.conf('directory')
|
||||||
|
else:
|
||||||
|
log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory'))
|
||||||
|
|
||||||
|
if self.conf('completed_directory'):
|
||||||
|
if os.path.isdir(self.conf('completed_directory')):
|
||||||
|
options['move_completed'] = 1
|
||||||
|
options['move_completed_path'] = self.conf('completed_directory')
|
||||||
|
else:
|
||||||
|
log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory'))
|
||||||
|
|
||||||
|
if data.get('seed_ratio'):
|
||||||
|
options['stop_at_ratio'] = 1
|
||||||
|
options['stop_ratio'] = tryFloat(data.get('seed_ratio'))
|
||||||
|
|
||||||
|
# Deluge only has seed time as a global option. Might be added in
|
||||||
|
# in a future API release.
|
||||||
|
# if data.get('seed_time'):
|
||||||
|
|
||||||
|
# Send request to Deluge
|
||||||
|
if data.get('protocol') == 'torrent_magnet':
|
||||||
|
remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options)
|
||||||
|
else:
|
||||||
|
filename = self.createFileName(data, filedata, movie)
|
||||||
|
remote_torrent = self.drpc.add_torrent_file(filename, b64encode(filedata), options)
|
||||||
|
|
||||||
|
if not remote_torrent:
|
||||||
|
log.error('Failed sending torrent to Deluge')
|
||||||
|
return False
|
||||||
|
|
||||||
|
log.info('Torrent sent to Deluge successfully.')
|
||||||
|
return self.downloadReturnId(remote_torrent)
|
||||||
|
|
||||||
|
def getAllDownloadStatus(self):
|
||||||
|
|
||||||
|
log.debug('Checking Deluge download status.')
|
||||||
|
|
||||||
|
if not os.path.isdir(Env.setting('from', 'renamer')):
|
||||||
|
log.error('Renamer "from" folder doesn\'t to exist.')
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
statuses = StatusList(self)
|
||||||
|
|
||||||
|
queue = self.drpc.get_alltorrents()
|
||||||
|
|
||||||
|
if not queue:
|
||||||
|
log.debug('Nothing in queue or error')
|
||||||
|
return False
|
||||||
|
|
||||||
|
for torrent_id in queue:
|
||||||
|
item = queue[torrent_id]
|
||||||
|
log.debug('name=%s / id=%s / save_path=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (item['name'], item['hash'], item['save_path'], item['move_completed_path'], item['hash'], item['progress'], item['state'], item['eta'], item['ratio'], item['stop_ratio'], item['is_seed'], item['is_finished'], item['paused']))
|
||||||
|
|
||||||
|
# Deluge has no easy way to work out if a torrent is stalled or failing.
|
||||||
|
#status = 'failed'
|
||||||
|
status = 'busy'
|
||||||
|
if item['is_seed'] and tryFloat(item['ratio']) < tryFloat(item['stop_ratio']):
|
||||||
|
# We have item['seeding_time'] to work out what the seeding time is, but we do not
|
||||||
|
# have access to the downloader seed_time, as with deluge we have no way to pass it
|
||||||
|
# when the torrent is added. So Deluge will only look at the ratio.
|
||||||
|
# See above comment in download().
|
||||||
|
status = 'seeding'
|
||||||
|
elif item['is_seed'] and item['is_finished'] and item['paused'] and item['state'] == 'Paused':
|
||||||
|
status = 'completed'
|
||||||
|
|
||||||
|
download_dir = item['save_path']
|
||||||
|
if item['move_on_completed']:
|
||||||
|
download_dir = item['move_completed_path']
|
||||||
|
|
||||||
|
statuses.append({
|
||||||
|
'id': item['hash'],
|
||||||
|
'name': item['name'],
|
||||||
|
'status': status,
|
||||||
|
'original_status': item['state'],
|
||||||
|
'seed_ratio': item['ratio'],
|
||||||
|
'timeleft': str(timedelta(seconds = item['eta'])),
|
||||||
|
'folder': ss(os.path.join(download_dir, item['name'])),
|
||||||
|
})
|
||||||
|
|
||||||
|
return statuses
|
||||||
|
|
||||||
|
def pause(self, item, pause = True):
|
||||||
|
if pause:
|
||||||
|
return self.drpc.pause_torrent([item['id']])
|
||||||
|
else:
|
||||||
|
return self.drpc.resume_torrent([item['id']])
|
||||||
|
|
||||||
|
def removeFailed(self, item):
|
||||||
|
log.info('%s failed downloading, deleting...', item['name'])
|
||||||
|
return self.drpc.remove_torrent(item['id'], True)
|
||||||
|
|
||||||
|
def processComplete(self, item, delete_files = False):
|
||||||
|
log.debug('Requesting Deluge to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||||
|
return self.drpc.remove_torrent(item['id'], remove_local_data = delete_files)
|
||||||
|
|
||||||
|
class DelugeRPC(object):
|
||||||
|
|
||||||
|
host = 'localhost'
|
||||||
|
port = 58846
|
||||||
|
username = None
|
||||||
|
password = None
|
||||||
|
client = None
|
||||||
|
|
||||||
|
def __init__(self, host = 'localhost', port = 58846, username = None, password = None):
|
||||||
|
super(DelugeRPC, self).__init__()
|
||||||
|
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
self.client = DelugeClient()
|
||||||
|
self.client.connect(self.host, int(self.port), self.username, self.password)
|
||||||
|
|
||||||
|
def add_torrent_magnet(self, torrent, options):
|
||||||
|
torrent_id = False
|
||||||
|
try:
|
||||||
|
self.connect()
|
||||||
|
torrent_id = self.client.core.add_torrent_magnet(torrent, options).get()
|
||||||
|
if options['label']:
|
||||||
|
self.client.label.set_torrent(torrent_id, options['label']).get()
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc()))
|
||||||
|
finally:
|
||||||
|
if self.client:
|
||||||
|
self.disconnect()
|
||||||
|
|
||||||
|
return torrent_id
|
||||||
|
|
||||||
|
def add_torrent_file(self, filename, torrent, options):
|
||||||
|
torrent_id = False
|
||||||
|
try:
|
||||||
|
self.connect()
|
||||||
|
torrent_id = self.client.core.add_torrent_file(filename, torrent, options).get()
|
||||||
|
if options['label']:
|
||||||
|
self.client.label.set_torrent(torrent_id, options['label']).get()
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc()))
|
||||||
|
finally:
|
||||||
|
if self.client:
|
||||||
|
self.disconnect()
|
||||||
|
|
||||||
|
return torrent_id
|
||||||
|
|
||||||
|
def get_alltorrents(self):
|
||||||
|
ret = False
|
||||||
|
try:
|
||||||
|
self.connect()
|
||||||
|
ret = self.client.core.get_torrents_status({}, {}).get()
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc()))
|
||||||
|
finally:
|
||||||
|
if self.client:
|
||||||
|
self.disconnect()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def pause_torrent(self, torrent_ids):
|
||||||
|
try:
|
||||||
|
self.connect()
|
||||||
|
self.client.core.pause_torrent(torrent_ids).get()
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to pause torrent: %s %s', (err, traceback.format_exc()))
|
||||||
|
finally:
|
||||||
|
if self.client:
|
||||||
|
self.disconnect()
|
||||||
|
|
||||||
|
def resume_torrent(self, torrent_ids):
|
||||||
|
try:
|
||||||
|
self.connect()
|
||||||
|
self.client.core.resume_torrent(torrent_ids).get()
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to resume torrent: %s %s', (err, traceback.format_exc()))
|
||||||
|
finally:
|
||||||
|
if self.client:
|
||||||
|
self.disconnect()
|
||||||
|
|
||||||
|
def remove_torrent(self, torrent_id, remove_local_data):
|
||||||
|
ret = False
|
||||||
|
try:
|
||||||
|
self.connect()
|
||||||
|
ret = self.client.core.remove_torrent(torrent_id, remove_local_data).get()
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to remove torrent: %s %s', (err, traceback.format_exc()))
|
||||||
|
finally:
|
||||||
|
if self.client:
|
||||||
|
self.disconnect()
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def disconnect(self):
|
||||||
|
self.client.disconnect()
|
||||||
@@ -8,9 +8,10 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'downloaders',
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
'name': 'nzbget',
|
'name': 'nzbget',
|
||||||
'label': 'NZBGet',
|
'label': 'NZBGet',
|
||||||
'description': 'Send NZBs to your NZBGet installation.',
|
'description': 'Use <a href="http://nzbget.sourceforge.net/Main_Page" target="_blank">NZBGet</a> to download NZBs.',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'enabled',
|
'name': 'enabled',
|
||||||
@@ -23,8 +24,15 @@ config = [{
|
|||||||
'default': 'localhost:6789',
|
'default': 'localhost:6789',
|
||||||
'description': 'Hostname with port. Usually <strong>localhost:6789</strong>',
|
'description': 'Hostname with port. Usually <strong>localhost:6789</strong>',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
'default': 'nzbget',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Set a different username to connect. Default: nzbget',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'password',
|
'name': 'password',
|
||||||
|
'type': 'password',
|
||||||
'description': 'Default NZBGet password is <i>tegbzn6789</i>',
|
'description': 'Default NZBGet password is <i>tegbzn6789</i>',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -32,6 +40,14 @@ config = [{
|
|||||||
'default': 'Movies',
|
'default': 'Movies',
|
||||||
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
|
'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',
|
'name': 'manual',
|
||||||
'default': 0,
|
'default': 0,
|
||||||
@@ -39,6 +55,13 @@ config = [{
|
|||||||
'advanced': True,
|
'advanced': True,
|
||||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
'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,23 +1,27 @@
|
|||||||
from base64 import standard_b64encode
|
from base64 import standard_b64encode
|
||||||
from couchpotato.core.downloaders.base import Downloader
|
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||||
|
from couchpotato.core.helpers.encoding import ss
|
||||||
|
from couchpotato.core.helpers.variable import tryInt, md5
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from inspect import isfunction
|
from datetime import timedelta
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
import traceback
|
import traceback
|
||||||
import xmlrpclib
|
import xmlrpclib
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class NZBGet(Downloader):
|
class NZBGet(Downloader):
|
||||||
|
|
||||||
type = ['nzb']
|
protocol = ['nzb']
|
||||||
|
|
||||||
url = 'http://nzbget:%(password)s@%(host)s/xmlrpc'
|
url = 'http://%(username)s:%(password)s@%(host)s/xmlrpc'
|
||||||
|
|
||||||
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
def download(self, data = None, movie = None, filedata = None):
|
||||||
|
if not movie: movie = {}
|
||||||
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
|
if not data: data = {}
|
||||||
return
|
|
||||||
|
|
||||||
if not filedata:
|
if not filedata:
|
||||||
log.error('Unable to get NZB file: %s', traceback.format_exc())
|
log.error('Unable to get NZB file: %s', traceback.format_exc())
|
||||||
@@ -25,13 +29,13 @@ class NZBGet(Downloader):
|
|||||||
|
|
||||||
log.info('Sending "%s" to NZBGet.', data.get('name'))
|
log.info('Sending "%s" to NZBGet.', data.get('name'))
|
||||||
|
|
||||||
url = self.url % {'host': self.conf('host'), 'password': self.conf('password')}
|
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
|
||||||
nzb_name = '%s.nzb' % self.createNzbName(data, movie)
|
nzb_name = ss('%s.nzb' % self.createNzbName(data, movie))
|
||||||
|
|
||||||
rpc = xmlrpclib.ServerProxy(url)
|
rpc = xmlrpclib.ServerProxy(url)
|
||||||
try:
|
try:
|
||||||
if rpc.writelog('INFO', 'CouchPotato connected to drop off %s.' % nzb_name):
|
if rpc.writelog('INFO', 'CouchPotato connected to drop off %s.' % nzb_name):
|
||||||
log.info('Successfully connected to NZBGet')
|
log.debug('Successfully connected to NZBGet')
|
||||||
else:
|
else:
|
||||||
log.info('Successfully connected to NZBGet, but unable to send a message')
|
log.info('Successfully connected to NZBGet, but unable to send a message')
|
||||||
except socket.error:
|
except socket.error:
|
||||||
@@ -44,9 +48,145 @@ class NZBGet(Downloader):
|
|||||||
log.error('Protocol Error: %s', e)
|
log.error('Protocol Error: %s', e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if rpc.append(nzb_name, self.conf('category'), False, standard_b64encode(filedata.strip())):
|
if re.search(r"^0", rpc.version()):
|
||||||
|
xml_response = rpc.append(nzb_name, self.conf('category'), False, standard_b64encode(filedata.strip()))
|
||||||
|
else:
|
||||||
|
xml_response = rpc.append(nzb_name, self.conf('category'), tryInt(self.conf('priority')), False, standard_b64encode(filedata.strip()))
|
||||||
|
|
||||||
|
if xml_response:
|
||||||
log.info('NZB sent successfully to NZBGet')
|
log.info('NZB sent successfully to NZBGet')
|
||||||
return True
|
nzb_id = md5(data['url']) # about as unique as they come ;)
|
||||||
|
couchpotato_id = "couchpotato=" + nzb_id
|
||||||
|
groups = rpc.listgroups()
|
||||||
|
file_id = [item['LastID'] for item in groups if item['NZBFilename'] == nzb_name]
|
||||||
|
confirmed = rpc.editqueue("GroupSetParameter", 0, couchpotato_id, file_id)
|
||||||
|
if confirmed:
|
||||||
|
log.debug('couchpotato parameter set in nzbget download')
|
||||||
|
return self.downloadReturnId(nzb_id)
|
||||||
else:
|
else:
|
||||||
log.error('NZBGet could not add %s to the queue.', nzb_name)
|
log.error('NZBGet could not add %s to the queue.', nzb_name)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def getAllDownloadStatus(self):
|
||||||
|
|
||||||
|
log.debug('Checking NZBGet download status.')
|
||||||
|
|
||||||
|
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
|
||||||
|
|
||||||
|
rpc = xmlrpclib.ServerProxy(url)
|
||||||
|
try:
|
||||||
|
if rpc.writelog('INFO', 'CouchPotato connected to check status'):
|
||||||
|
log.debug('Successfully connected to NZBGet')
|
||||||
|
else:
|
||||||
|
log.info('Successfully connected to NZBGet, but unable to send a message')
|
||||||
|
except socket.error:
|
||||||
|
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
|
||||||
|
return False
|
||||||
|
except xmlrpclib.ProtocolError, e:
|
||||||
|
if e.errcode == 401:
|
||||||
|
log.error('Password is incorrect.')
|
||||||
|
else:
|
||||||
|
log.error('Protocol Error: %s', e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get NZBGet data
|
||||||
|
try:
|
||||||
|
status = rpc.status()
|
||||||
|
groups = rpc.listgroups()
|
||||||
|
queue = rpc.postqueue(0)
|
||||||
|
history = rpc.history()
|
||||||
|
except:
|
||||||
|
log.error('Failed getting data: %s', traceback.format_exc(1))
|
||||||
|
return False
|
||||||
|
|
||||||
|
statuses = StatusList(self)
|
||||||
|
|
||||||
|
for item in groups:
|
||||||
|
log.debug('Found %s in NZBGet download queue', item['NZBFilename'])
|
||||||
|
try:
|
||||||
|
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
|
||||||
|
except:
|
||||||
|
nzb_id = item['NZBID']
|
||||||
|
|
||||||
|
|
||||||
|
timeleft = -1
|
||||||
|
try:
|
||||||
|
if item['ActiveDownloads'] > 0 and item['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
|
||||||
|
timeleft = str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
statuses.append({
|
||||||
|
'id': nzb_id,
|
||||||
|
'name': item['NZBFilename'],
|
||||||
|
'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED',
|
||||||
|
# Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
|
||||||
|
'timeleft': timeleft,
|
||||||
|
})
|
||||||
|
|
||||||
|
for item in queue: # 'Parameters' is not passed in rpc.postqueue
|
||||||
|
log.debug('Found %s in NZBGet postprocessing queue', item['NZBFilename'])
|
||||||
|
statuses.append({
|
||||||
|
'id': item['NZBID'],
|
||||||
|
'name': item['NZBFilename'],
|
||||||
|
'original_status': item['Stage'],
|
||||||
|
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
|
||||||
|
})
|
||||||
|
|
||||||
|
for item in history:
|
||||||
|
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (item['NZBFilename'] , item['ParStatus'], item['ScriptStatus'] , item['Log']))
|
||||||
|
try:
|
||||||
|
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
|
||||||
|
except:
|
||||||
|
nzb_id = item['NZBID']
|
||||||
|
statuses.append({
|
||||||
|
'id': nzb_id,
|
||||||
|
'name': item['NZBFilename'],
|
||||||
|
'status': 'completed' if item['ParStatus'] in ['SUCCESS','NONE'] and item['ScriptStatus'] in ['SUCCESS','NONE'] else 'failed',
|
||||||
|
'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'],
|
||||||
|
'timeleft': str(timedelta(seconds = 0)),
|
||||||
|
'folder': ss(item['DestDir'])
|
||||||
|
})
|
||||||
|
|
||||||
|
return statuses
|
||||||
|
|
||||||
|
def removeFailed(self, item):
|
||||||
|
|
||||||
|
log.info('%s failed downloading, deleting...', item['name'])
|
||||||
|
|
||||||
|
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
|
||||||
|
|
||||||
|
rpc = xmlrpclib.ServerProxy(url)
|
||||||
|
try:
|
||||||
|
if rpc.writelog('INFO', 'CouchPotato connected to delete some history'):
|
||||||
|
log.debug('Successfully connected to NZBGet')
|
||||||
|
else:
|
||||||
|
log.info('Successfully connected to NZBGet, but unable to send a message')
|
||||||
|
except socket.error:
|
||||||
|
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
|
||||||
|
return False
|
||||||
|
except xmlrpclib.ProtocolError, e:
|
||||||
|
if e.errcode == 401:
|
||||||
|
log.error('Password is incorrect.')
|
||||||
|
else:
|
||||||
|
log.error('Protocol Error: %s', e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
history = rpc.history()
|
||||||
|
nzb_id = None
|
||||||
|
path = None
|
||||||
|
|
||||||
|
for hist in history:
|
||||||
|
for param in hist['Parameters']:
|
||||||
|
if param['Name'] == 'couchpotato' and param['Value'] == item['id']:
|
||||||
|
nzb_id = hist['ID']
|
||||||
|
path = hist['DestDir']
|
||||||
|
|
||||||
|
if nzb_id and path and rpc.editqueue('HistoryDelete', 0, "", [tryInt(nzb_id)]):
|
||||||
|
shutil.rmtree(path, True)
|
||||||
|
except:
|
||||||
|
log.error('Failed deleting: %s', traceback.format_exc(0))
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
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': 'https://localhost:4321',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'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.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
from base64 import b64encode
|
||||||
|
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||||
|
from couchpotato.core.helpers.encoding import tryUrlencode, ss
|
||||||
|
from couchpotato.core.helpers.variable import cleanHost
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from urllib2 import URLError
|
||||||
|
from uuid import uuid4
|
||||||
|
import hashlib
|
||||||
|
import httplib
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
import urllib2
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class NZBVortex(Downloader):
|
||||||
|
|
||||||
|
protocol = ['nzb']
|
||||||
|
api_level = None
|
||||||
|
session_id = None
|
||||||
|
|
||||||
|
def download(self, data = None, movie = None, filedata = None):
|
||||||
|
if not movie: movie = {}
|
||||||
|
if not data: data = {}
|
||||||
|
|
||||||
|
# Send the nzb
|
||||||
|
try:
|
||||||
|
nzb_filename = self.createFileName(data, filedata, movie)
|
||||||
|
self.call('nzb/add', params = {'file': (ss(nzb_filename), filedata)}, multipart = True)
|
||||||
|
|
||||||
|
raw_statuses = self.call('nzb')
|
||||||
|
nzb_id = [item['id'] for item in raw_statuses.get('nzbs', []) if item['name'] == nzb_filename][0]
|
||||||
|
return self.downloadReturnId(nzb_id)
|
||||||
|
except:
|
||||||
|
log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())
|
||||||
|
return False
|
||||||
|
|
||||||
|
def getAllDownloadStatus(self):
|
||||||
|
|
||||||
|
raw_statuses = self.call('nzb')
|
||||||
|
|
||||||
|
statuses = StatusList(self)
|
||||||
|
for item in raw_statuses.get('nzbs', []):
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
status = 'busy'
|
||||||
|
if item['state'] == 20:
|
||||||
|
status = 'completed'
|
||||||
|
elif item['state'] in [21, 22, 24]:
|
||||||
|
status = 'failed'
|
||||||
|
|
||||||
|
statuses.append({
|
||||||
|
'id': item['id'],
|
||||||
|
'name': item['uiTitle'],
|
||||||
|
'status': status,
|
||||||
|
'original_status': item['state'],
|
||||||
|
'timeleft':-1,
|
||||||
|
'folder': ss(item['destinationPath']),
|
||||||
|
})
|
||||||
|
|
||||||
|
return statuses
|
||||||
|
|
||||||
|
def removeFailed(self, item):
|
||||||
|
|
||||||
|
log.info('%s failed downloading, deleting...', item['name'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.call('nzb/%s/cancel' % item['id'])
|
||||||
|
except:
|
||||||
|
log.error('Failed deleting: %s', traceback.format_exc(0))
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
|
||||||
|
nonce = self.call('auth/nonce', auth = False).get('authNonce')
|
||||||
|
cnonce = uuid4().hex
|
||||||
|
hashed = b64encode(hashlib.sha256('%s:%s:%s' % (nonce, cnonce, self.conf('api_key'))).digest())
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'nonce': nonce,
|
||||||
|
'cnonce': cnonce,
|
||||||
|
'hash': hashed
|
||||||
|
}
|
||||||
|
|
||||||
|
login_data = self.call('auth/login', parameters = params, auth = False)
|
||||||
|
|
||||||
|
# Save for later
|
||||||
|
if login_data.get('loginResult') == 'successful':
|
||||||
|
self.session_id = login_data.get('sessionID')
|
||||||
|
return True
|
||||||
|
|
||||||
|
log.error('Login failed, please check you api-key')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def call(self, call, parameters = None, repeat = False, auth = True, *args, **kwargs):
|
||||||
|
|
||||||
|
# Login first
|
||||||
|
if not parameters: parameters = {}
|
||||||
|
if not self.session_id and auth:
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
# Always add session id to request
|
||||||
|
if self.session_id:
|
||||||
|
parameters['sessionid'] = self.session_id
|
||||||
|
|
||||||
|
params = tryUrlencode(parameters)
|
||||||
|
|
||||||
|
url = cleanHost(self.conf('host')) + 'api/' + call
|
||||||
|
url_opener = urllib2.build_opener(HTTPSHandler())
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = self.urlopen('%s?%s' % (url, params), opener = url_opener, *args, **kwargs)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
return json.loads(data)
|
||||||
|
except URLError, e:
|
||||||
|
if hasattr(e, 'code') and e.code == 403:
|
||||||
|
# Try login and do again
|
||||||
|
if not repeat:
|
||||||
|
self.login()
|
||||||
|
return self.call(call, parameters = parameters, repeat = True, **kwargs)
|
||||||
|
|
||||||
|
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
except:
|
||||||
|
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def getApiLevel(self):
|
||||||
|
|
||||||
|
if not self.api_level:
|
||||||
|
|
||||||
|
url = cleanHost(self.conf('host')) + 'api/app/apilevel'
|
||||||
|
url_opener = urllib2.build_opener(HTTPSHandler())
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = self.urlopen(url, opener = url_opener, show_error = False)
|
||||||
|
self.api_level = float(json.loads(data).get('apilevel'))
|
||||||
|
except URLError, e:
|
||||||
|
if hasattr(e, 'code') and e.code == 403:
|
||||||
|
log.error('This version of NZBVortex isn\'t supported. Please update to 2.8.6 or higher')
|
||||||
|
else:
|
||||||
|
log.error('NZBVortex doesn\'t seem to be running or maybe the remote option isn\'t enabled yet: %s', traceback.format_exc(1))
|
||||||
|
|
||||||
|
return self.api_level
|
||||||
|
|
||||||
|
def isEnabled(self, manual = False, data = None):
|
||||||
|
if not data: data = {}
|
||||||
|
return super(NZBVortex, self).isEnabled(manual, data) and self.getApiLevel()
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPSConnection(httplib.HTTPSConnection):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
httplib.HTTPSConnection.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
sock = socket.create_connection((self.host, self.port), self.timeout)
|
||||||
|
if sys.version_info < (2, 6, 7):
|
||||||
|
if hasattr(self, '_tunnel_host'):
|
||||||
|
self.sock = sock
|
||||||
|
self._tunnel()
|
||||||
|
else:
|
||||||
|
if self._tunnel_host:
|
||||||
|
self.sock = sock
|
||||||
|
self._tunnel()
|
||||||
|
|
||||||
|
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version = ssl.PROTOCOL_TLSv1)
|
||||||
|
|
||||||
|
class HTTPSHandler(urllib2.HTTPSHandler):
|
||||||
|
def https_open(self, req):
|
||||||
|
return self.do_open(HTTPSConnection, req)
|
||||||
@@ -9,9 +9,10 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'downloaders',
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
'name': 'pneumatic',
|
'name': 'pneumatic',
|
||||||
'label': 'Pneumatic',
|
'label': 'Pneumatic',
|
||||||
'description': 'Download the .strm file to a specific folder.',
|
'description': 'Use <a href="http://forum.xbmc.org/showthread.php?tid=97657" target="_blank">Pneumatic</a> to download .strm files.',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'enabled',
|
'name': 'enabled',
|
||||||
|
|||||||
@@ -6,14 +6,15 @@ import traceback
|
|||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Pneumatic(Downloader):
|
class Pneumatic(Downloader):
|
||||||
|
|
||||||
type = ['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'
|
||||||
|
|
||||||
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
def download(self, data = None, movie = None, filedata = None):
|
||||||
if self.isDisabled(manual) or (not self.isCorrectType(data.get('type'))):
|
if not movie: movie = {}
|
||||||
return
|
if not data: data = {}
|
||||||
|
|
||||||
directory = self.conf('directory')
|
directory = self.conf('directory')
|
||||||
if not directory or not os.path.isdir(directory):
|
if not directory or not os.path.isdir(directory):
|
||||||
@@ -28,7 +29,7 @@ class Pneumatic(Downloader):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
if not os.path.isfile(fullPath):
|
if not os.path.isfile(fullPath):
|
||||||
log.info('Downloading %s to %s.', (data.get('type'), fullPath))
|
log.info('Downloading %s to %s.', (data.get('protocol'), fullPath))
|
||||||
with open(fullPath, 'wb') as f:
|
with open(fullPath, 'wb') as f:
|
||||||
f.write(filedata)
|
f.write(filedata)
|
||||||
|
|
||||||
|
|||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
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',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'url',
|
||||||
|
'default': 'http://localhost:80/RPC2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'label',
|
||||||
|
'description': 'Label to apply on added torrents.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'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.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
+201
@@ -0,0 +1,201 @@
|
|||||||
|
from base64 import b16encode, b32decode
|
||||||
|
from bencode import bencode, bdecode
|
||||||
|
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||||
|
from couchpotato.core.helpers.encoding import ss
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from datetime import timedelta
|
||||||
|
from hashlib import sha1
|
||||||
|
from rtorrent import RTorrent
|
||||||
|
from rtorrent.err import MethodError
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class rTorrent(Downloader):
|
||||||
|
|
||||||
|
protocol = ['torrent', 'torrent_magnet']
|
||||||
|
rt = None
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
# Already connected?
|
||||||
|
if self.rt is not None:
|
||||||
|
return self.rt
|
||||||
|
|
||||||
|
# Ensure url is set
|
||||||
|
if not self.conf('url'):
|
||||||
|
log.error('Config properties are not filled in correctly, url is missing.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.conf('username') and self.conf('password'):
|
||||||
|
self.rt = RTorrent(
|
||||||
|
self.conf('url'),
|
||||||
|
self.conf('username'),
|
||||||
|
self.conf('password')
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.rt = RTorrent(self.conf('url'))
|
||||||
|
|
||||||
|
return self.rt
|
||||||
|
|
||||||
|
def _update_provider_group(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, err:
|
||||||
|
log.error('Unable to set group options: %s', err.message)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def download(self, data, movie, filedata = None):
|
||||||
|
log.debug('Sending "%s" to rTorrent.', (data.get('name')))
|
||||||
|
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
group_name = 'cp_' + data.get('provider').lower()
|
||||||
|
if not self._update_provider_group(group_name, data):
|
||||||
|
return False
|
||||||
|
|
||||||
|
torrent_params = {}
|
||||||
|
if self.conf('label'):
|
||||||
|
torrent_params['label'] = self.conf('label')
|
||||||
|
|
||||||
|
if not filedata and data.get('protocol') == 'torrent':
|
||||||
|
log.error('Failed sending torrent, no data')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Try download magnet torrents
|
||||||
|
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().upper()
|
||||||
|
|
||||||
|
# Convert base 32 to hex
|
||||||
|
if len(torrent_hash) == 32:
|
||||||
|
torrent_hash = b16encode(b32decode(torrent_hash))
|
||||||
|
|
||||||
|
# Send request to rTorrent
|
||||||
|
try:
|
||||||
|
# Send torrent to rTorrent
|
||||||
|
torrent = self.rt.load_torrent(filedata)
|
||||||
|
|
||||||
|
# Set label
|
||||||
|
if self.conf('label'):
|
||||||
|
torrent.set_custom(1, self.conf('label'))
|
||||||
|
|
||||||
|
# Set Ratio Group
|
||||||
|
torrent.set_visible(group_name)
|
||||||
|
|
||||||
|
# Start torrent
|
||||||
|
if not self.conf('paused', default = 0):
|
||||||
|
torrent.start()
|
||||||
|
|
||||||
|
return self.downloadReturnId(torrent_hash)
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to send torrent to rTorrent: %s', err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def getAllDownloadStatus(self):
|
||||||
|
log.debug('Checking rTorrent download status.')
|
||||||
|
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
torrents = self.rt.get_torrents()
|
||||||
|
|
||||||
|
statuses = StatusList(self)
|
||||||
|
|
||||||
|
for item in torrents:
|
||||||
|
status = 'busy'
|
||||||
|
if item.complete:
|
||||||
|
if item.active:
|
||||||
|
status = 'seeding'
|
||||||
|
else:
|
||||||
|
status = 'completed'
|
||||||
|
|
||||||
|
statuses.append({
|
||||||
|
'id': item.info_hash,
|
||||||
|
'name': item.name,
|
||||||
|
'status': status,
|
||||||
|
'seed_ratio': item.ratio,
|
||||||
|
'original_status': item.state,
|
||||||
|
'timeleft': str(timedelta(seconds = float(item.left_bytes) / item.down_rate)) if item.down_rate > 0 else -1,
|
||||||
|
'folder': ss(item.directory)
|
||||||
|
})
|
||||||
|
|
||||||
|
return statuses
|
||||||
|
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to get status from rTorrent: %s', err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def pause(self, download_info, pause = True):
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
torrent = self.rt.find_torrent(download_info['id'])
|
||||||
|
if torrent is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if pause:
|
||||||
|
return torrent.pause()
|
||||||
|
return torrent.resume()
|
||||||
|
|
||||||
|
def removeFailed(self, item):
|
||||||
|
log.info('%s failed downloading, deleting...', item['name'])
|
||||||
|
return self.processComplete(item, delete_files = True)
|
||||||
|
|
||||||
|
def processComplete(self, item, delete_files):
|
||||||
|
log.debug('Requesting rTorrent to remove the torrent %s%s.',
|
||||||
|
(item['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
torrent = self.rt.find_torrent(item['id'])
|
||||||
|
if torrent is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
torrent.erase() # just removes the torrent, doesn't delete data
|
||||||
|
|
||||||
|
if delete_files:
|
||||||
|
shutil.rmtree(item['folder'], True)
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -8,9 +8,10 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'downloaders',
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
'name': 'sabnzbd',
|
'name': 'sabnzbd',
|
||||||
'label': 'Sabnzbd',
|
'label': 'Sabnzbd',
|
||||||
'description': 'Send NZBs to your Sabnzbd installation.',
|
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> (0.7+) to download NZBs.',
|
||||||
'wizard': True,
|
'wizard': True,
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
@@ -33,6 +34,15 @@ config = [{
|
|||||||
'label': 'Category',
|
'label': 'Category',
|
||||||
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
|
'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',
|
'name': 'manual',
|
||||||
'default': False,
|
'default': False,
|
||||||
@@ -40,9 +50,18 @@ config = [{
|
|||||||
'advanced': True,
|
'advanced': True,
|
||||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
'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',
|
'name': 'delete_failed',
|
||||||
'default': True,
|
'default': True,
|
||||||
|
'advanced': True,
|
||||||
'type': 'bool',
|
'type': 'bool',
|
||||||
'description': 'Delete a release after the download has failed.',
|
'description': 'Delete a release after the download has failed.',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,49 +1,50 @@
|
|||||||
from couchpotato.core.downloaders.base import Downloader
|
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
from couchpotato.core.helpers.encoding import tryUrlencode, ss
|
||||||
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
|
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
from datetime import timedelta
|
||||||
from urllib2 import URLError
|
from urllib2 import URLError
|
||||||
import json
|
import json
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Sabnzbd(Downloader):
|
class Sabnzbd(Downloader):
|
||||||
|
|
||||||
type = ['nzb']
|
protocol = ['nzb']
|
||||||
|
|
||||||
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
def download(self, data = None, movie = None, filedata = None):
|
||||||
|
if not movie: movie = {}
|
||||||
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
|
if not data: data = {}
|
||||||
return
|
|
||||||
|
|
||||||
log.info('Sending "%s" to SABnzbd.', data.get('name'))
|
log.info('Sending "%s" to SABnzbd.', data.get('name'))
|
||||||
|
|
||||||
params = {
|
req_params = {
|
||||||
'apikey': self.conf('api_key'),
|
|
||||||
'cat': self.conf('category'),
|
'cat': self.conf('category'),
|
||||||
'mode': 'addurl',
|
'mode': 'addurl',
|
||||||
'nzbname': self.createNzbName(data, movie),
|
'nzbname': self.createNzbName(data, movie),
|
||||||
|
'priority': self.conf('priority'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nzb_filename = None
|
||||||
if filedata:
|
if filedata:
|
||||||
if len(filedata) < 50:
|
if len(filedata) < 50:
|
||||||
log.error('No proper nzb available: %s', (filedata))
|
log.error('No proper nzb available: %s', filedata)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
|
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
|
||||||
nzb_filename = self.createFileName(data, filedata, movie)
|
nzb_filename = self.createFileName(data, filedata, movie)
|
||||||
params['mode'] = 'addfile'
|
req_params['mode'] = 'addfile'
|
||||||
else:
|
else:
|
||||||
params['name'] = data.get('url')
|
req_params['name'] = data.get('url')
|
||||||
|
|
||||||
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(params)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if params.get('mode') is 'addfile':
|
if nzb_filename and req_params.get('mode') is 'addfile':
|
||||||
sab = self.urlopen(url, timeout = 60, params = {'nzbfile': (nzb_filename, filedata)}, multipart = True, show_error = False)
|
sab_data = self.call(req_params, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True)
|
||||||
else:
|
else:
|
||||||
sab = self.urlopen(url, timeout = 60, show_error = False)
|
sab_data = self.call(req_params)
|
||||||
except URLError:
|
except URLError:
|
||||||
log.error('Failed sending release, probably wrong HOST: %s', traceback.format_exc(0))
|
log.error('Failed sending release, probably wrong HOST: %s', traceback.format_exc(0))
|
||||||
return False
|
return False
|
||||||
@@ -51,22 +52,18 @@ class Sabnzbd(Downloader):
|
|||||||
log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0))
|
log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
result = sab.strip()
|
log.debug('Result from SAB: %s', sab_data)
|
||||||
if not result:
|
if sab_data.get('status') and not sab_data.get('error'):
|
||||||
log.error('SABnzbd didn\'t return anything.')
|
|
||||||
return False
|
|
||||||
|
|
||||||
log.debug('Result text from SAB: %s', result[:40])
|
|
||||||
if result[:2] == 'ok':
|
|
||||||
log.info('NZB sent to SAB successfully.')
|
log.info('NZB sent to SAB successfully.')
|
||||||
return True
|
if filedata:
|
||||||
|
return self.downloadReturnId(sab_data.get('nzo_ids')[0])
|
||||||
|
else:
|
||||||
|
return True
|
||||||
else:
|
else:
|
||||||
log.error(result[:40])
|
log.error('Error getting data from SABNZBd: %s', sab_data)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def getAllDownloadStatus(self):
|
def getAllDownloadStatus(self):
|
||||||
if self.isDisabled(manual = False):
|
|
||||||
return False
|
|
||||||
|
|
||||||
log.debug('Checking SABnzbd download status.')
|
log.debug('Checking SABnzbd download status.')
|
||||||
|
|
||||||
@@ -89,14 +86,13 @@ class Sabnzbd(Downloader):
|
|||||||
log.error('Failed getting history json: %s', traceback.format_exc(1))
|
log.error('Failed getting history json: %s', traceback.format_exc(1))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
statuses = []
|
statuses = StatusList(self)
|
||||||
|
|
||||||
# Get busy releases
|
# Get busy releases
|
||||||
for item in queue.get('slots', []):
|
for item in queue.get('slots', []):
|
||||||
statuses.append({
|
statuses.append({
|
||||||
'id': item['nzo_id'],
|
'id': item['nzo_id'],
|
||||||
'name': item['filename'],
|
'name': item['filename'],
|
||||||
'status': 'busy',
|
|
||||||
'original_status': item['status'],
|
'original_status': item['status'],
|
||||||
'timeleft': item['timeleft'] if not queue['paused'] else -1,
|
'timeleft': item['timeleft'] if not queue['paused'] else -1,
|
||||||
})
|
})
|
||||||
@@ -115,16 +111,14 @@ class Sabnzbd(Downloader):
|
|||||||
'name': item['name'],
|
'name': item['name'],
|
||||||
'status': status,
|
'status': status,
|
||||||
'original_status': item['status'],
|
'original_status': item['status'],
|
||||||
'timeleft': 0,
|
'timeleft': str(timedelta(seconds = 0)),
|
||||||
|
'folder': ss(item['storage']),
|
||||||
})
|
})
|
||||||
|
|
||||||
return statuses
|
return statuses
|
||||||
|
|
||||||
def removeFailed(self, item):
|
def removeFailed(self, item):
|
||||||
|
|
||||||
if not self.conf('delete_failed', default = True):
|
|
||||||
return False
|
|
||||||
|
|
||||||
log.info('%s failed downloading, deleting...', item['name'])
|
log.info('%s failed downloading, deleting...', item['name'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -140,21 +134,37 @@ class Sabnzbd(Downloader):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def call(self, params, use_json = True):
|
def processComplete(self, item, delete_files = False):
|
||||||
|
log.debug('Requesting SabNZBd to remove the NZB %s.', item['name'])
|
||||||
|
|
||||||
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(params, {
|
try:
|
||||||
|
self.call({
|
||||||
|
'mode': 'history',
|
||||||
|
'name': 'delete',
|
||||||
|
'del_files': '0',
|
||||||
|
'value': item['id']
|
||||||
|
}, use_json = False)
|
||||||
|
except:
|
||||||
|
log.error('Failed removing: %s', traceback.format_exc(0))
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def call(self, request_params, use_json = True, **kwargs):
|
||||||
|
|
||||||
|
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(request_params, {
|
||||||
'apikey': self.conf('api_key'),
|
'apikey': self.conf('api_key'),
|
||||||
'output': 'json'
|
'output': 'json'
|
||||||
}))
|
}))
|
||||||
|
|
||||||
data = self.urlopen(url, timeout = 60, show_error = False)
|
data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}, **kwargs)
|
||||||
if use_json:
|
if use_json:
|
||||||
d = json.loads(data)
|
d = json.loads(data)
|
||||||
if d.get('error'):
|
if d.get('error'):
|
||||||
log.error('Error getting data from SABNZBd: %s', d.get('error'))
|
log.error('Error getting data from SABNZBd: %s', d.get('error'))
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
return d[params['mode']]
|
return d.get(request_params['mode']) or d
|
||||||
else:
|
else:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|||||||
@@ -8,16 +8,17 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'downloaders',
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
'name': 'synology',
|
'name': 'synology',
|
||||||
'label': 'Synology',
|
'label': 'Synology',
|
||||||
'description': 'Send torrents to Synology\'s Download Station.',
|
'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,
|
'wizard': True,
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'enabled',
|
'name': 'enabled',
|
||||||
'default': 0,
|
'default': 0,
|
||||||
'type': 'enabler',
|
'type': 'enabler',
|
||||||
'radio_group': 'torrent',
|
'radio_group': 'nzb,torrent',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'host',
|
'name': 'host',
|
||||||
@@ -31,6 +32,13 @@ config = [{
|
|||||||
'name': 'password',
|
'name': 'password',
|
||||||
'type': 'password',
|
'type': 'password',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'use_for',
|
||||||
|
'label': 'Use for',
|
||||||
|
'default': 'both',
|
||||||
|
'type': 'dropdown',
|
||||||
|
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'manual',
|
'name': 'manual',
|
||||||
'default': 0,
|
'default': 0,
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
from couchpotato.core.downloaders.base import Downloader
|
from couchpotato.core.downloaders.base import Downloader
|
||||||
from couchpotato.core.helpers.encoding import isInt
|
from couchpotato.core.helpers.encoding import isInt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
import httplib
|
|
||||||
import json
|
import json
|
||||||
import urllib
|
import requests
|
||||||
import urllib2
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Synology(Downloader):
|
class Synology(Downloader):
|
||||||
|
|
||||||
type = ['torrent_magnet']
|
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
def download(self, data, movie, manual = False, filedata = None):
|
def download(self, data = None, movie = None, filedata = None):
|
||||||
|
if not movie: movie = {}
|
||||||
|
if not data: data = {}
|
||||||
|
|
||||||
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
|
response = False
|
||||||
return
|
log.error('Sending "%s" (%s) to Synology.', (data['name'], data['protocol']))
|
||||||
|
|
||||||
log.error('Sending "%s" (%s) to Synology.', (data.get('name'), data.get('type')))
|
|
||||||
|
|
||||||
# Load host from config and split out port.
|
# Load host from config and split out port.
|
||||||
host = self.conf('host').split(':')
|
host = self.conf('host').split(':')
|
||||||
@@ -27,24 +25,47 @@ class Synology(Downloader):
|
|||||||
log.error('Config properties are not filled in correctly, port is missing.')
|
log.error('Config properties are not filled in correctly, port is missing.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if data.get('type') == 'torrent':
|
|
||||||
log.error('Can\'t add binary torrent file')
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Send request to Transmission
|
# 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'))
|
||||||
remote_torrent = srpc.add_torrent_uri(data.get('url'))
|
if data['protocol'] == 'torrent_magnet':
|
||||||
log.info('Response: %s', remote_torrent)
|
log.info('Adding torrent URL %s', data['url'])
|
||||||
return remote_torrent['success']
|
response = srpc.create_task(url = data['url'])
|
||||||
|
elif data['protocol'] in ['nzb', 'torrent']:
|
||||||
|
log.info('Adding %s' % data['protocol'])
|
||||||
|
if not filedata:
|
||||||
|
log.error('No %s data found' % data['protocol'])
|
||||||
|
else:
|
||||||
|
filename = data['name'] + '.' + data['protocol']
|
||||||
|
response = srpc.create_task(filename = filename, filedata = filedata)
|
||||||
except Exception, err:
|
except Exception, err:
|
||||||
log.error('Exception while adding torrent: %s', err)
|
log.error('Exception while adding torrent: %s', err)
|
||||||
return False
|
finally:
|
||||||
|
return response
|
||||||
|
|
||||||
|
def getEnabledProtocol(self):
|
||||||
|
if self.conf('use_for') == 'both':
|
||||||
|
return super(Synology, self).getEnabledProtocol()
|
||||||
|
elif self.conf('use_for') == 'torrent':
|
||||||
|
return ['torrent', 'torrent_magnet']
|
||||||
|
else:
|
||||||
|
return ['nzb']
|
||||||
|
|
||||||
|
def isEnabled(self, manual = False, data = None):
|
||||||
|
if not data: data = {}
|
||||||
|
|
||||||
|
for_protocol = ['both']
|
||||||
|
if data and 'torrent' in data.get('protocol'):
|
||||||
|
for_protocol.append('torrent')
|
||||||
|
elif data:
|
||||||
|
for_protocol.append(data.get('protocol'))
|
||||||
|
|
||||||
|
return super(Synology, self).isEnabled(manual, data) and\
|
||||||
|
((self.conf('use_for') in for_protocol))
|
||||||
|
|
||||||
class SynologyRPC(object):
|
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):
|
||||||
|
|
||||||
@@ -61,11 +82,13 @@ class SynologyRPC(object):
|
|||||||
args = {'api': 'SYNO.API.Auth', 'account': self.username, 'passwd': self.password, 'version': 2,
|
args = {'api': 'SYNO.API.Auth', 'account': self.username, 'passwd': self.password, 'version': 2,
|
||||||
'method': 'login', 'session': self.session_name, 'format': 'sid'}
|
'method': 'login', 'session': self.session_name, 'format': 'sid'}
|
||||||
response = self._req(self.auth_url, args)
|
response = self._req(self.auth_url, args)
|
||||||
if response['success'] == True:
|
if response['success']:
|
||||||
self.sid = response['data']['sid']
|
self.sid = response['data']['sid']
|
||||||
log.debug('Sid=%s', self.sid)
|
log.debug('sid=%s', self.sid)
|
||||||
return response
|
else:
|
||||||
elif self.username or self.password:
|
log.error('Couldn\'t login to Synology, %s', response)
|
||||||
|
return response['success']
|
||||||
|
else:
|
||||||
log.error('User or password missing, not using authentication.')
|
log.error('User or password missing, not using authentication.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -73,36 +96,51 @@ class SynologyRPC(object):
|
|||||||
args = {'api':'SYNO.API.Auth', 'version':1, 'method':'logout', 'session':self.session_name, '_sid':self.sid}
|
args = {'api':'SYNO.API.Auth', 'version':1, 'method':'logout', 'session':self.session_name, '_sid':self.sid}
|
||||||
return self._req(self.auth_url, args)
|
return self._req(self.auth_url, args)
|
||||||
|
|
||||||
def _req(self, url, args):
|
def _req(self, url, args, files = None):
|
||||||
req_url = url + '?' + urllib.urlencode(args)
|
response = {'success': False}
|
||||||
try:
|
try:
|
||||||
req_open = urllib2.urlopen(req_url)
|
req = requests.post(url, data = args, files = files)
|
||||||
response = json.loads(req_open.read())
|
req.raise_for_status()
|
||||||
if response['success'] == True:
|
response = json.loads(req.text)
|
||||||
|
if response['success']:
|
||||||
log.info('Synology action successfull')
|
log.info('Synology action successfull')
|
||||||
return response
|
return response
|
||||||
except httplib.InvalidURL, err:
|
except requests.ConnectionError, err:
|
||||||
log.error('Invalid Transmission host, check your config %s', err)
|
log.error('Synology connection error, check your config %s', err)
|
||||||
return False
|
except requests.HTTPError, err:
|
||||||
except urllib2.HTTPError, err:
|
|
||||||
log.error('SynologyRPC HTTPError: %s', err)
|
log.error('SynologyRPC HTTPError: %s', err)
|
||||||
return False
|
except Exception, err:
|
||||||
except urllib2.URLError, err:
|
log.error('Exception: %s', err)
|
||||||
log.error('Unable to connect to Synology %s', err)
|
finally:
|
||||||
return False
|
return response
|
||||||
|
|
||||||
def add_torrent_uri(self, torrent):
|
def create_task(self, url = None, filename = None, filedata = None):
|
||||||
log.info('Adding torrent URL %s', torrent)
|
""" Creates new download task in Synology DownloadStation. Either specify
|
||||||
response = {}
|
url or pair (filename, filedata).
|
||||||
|
|
||||||
|
Returns True if task was created, False otherwise
|
||||||
|
"""
|
||||||
|
result = False
|
||||||
# login
|
# login
|
||||||
login = self._login()
|
if self._login():
|
||||||
if len(login) > 0 and login['success'] == True:
|
args = {'api': 'SYNO.DownloadStation.Task',
|
||||||
log.info('Login success, adding torrent')
|
'version': '1',
|
||||||
args = {'api':'SYNO.DownloadStation.Task', 'version':1, 'method':'create', 'uri':torrent, '_sid':self.sid}
|
'method': 'create',
|
||||||
response = self._req(self.download_url, args)
|
'_sid': self.sid}
|
||||||
|
if url:
|
||||||
|
log.info('Login success, adding torrent URI')
|
||||||
|
args['uri'] = url
|
||||||
|
response = self._req(self.download_url, args = args)
|
||||||
|
log.info('Response: %s', response)
|
||||||
|
result = response['success']
|
||||||
|
elif filename and filedata:
|
||||||
|
log.info('Login success, adding torrent')
|
||||||
|
files = {'file': (filename, filedata)}
|
||||||
|
response = self._req(self.download_url, args = args, files = files)
|
||||||
|
log.info('Response: %s', response)
|
||||||
|
result = response['success']
|
||||||
|
else:
|
||||||
|
log.error('Invalid use of SynologyRPC.create_task: either url or filename+filedata must be specified')
|
||||||
self._logout()
|
self._logout()
|
||||||
else:
|
|
||||||
log.error('Couldn\'t login to Synology, %s', login)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
|
return result
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'downloaders',
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
'name': 'transmission',
|
'name': 'transmission',
|
||||||
'label': 'Transmission',
|
'label': 'Transmission',
|
||||||
'description': 'Send torrents to Transmission.',
|
'description': 'Use <a href="http://www.transmissionbt.com/" target="_blank">Transmission</a> to download torrents.',
|
||||||
'wizard': True,
|
'wizard': True,
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
@@ -24,6 +25,13 @@ config = [{
|
|||||||
'default': 'localhost:9091',
|
'default': 'localhost:9091',
|
||||||
'description': 'Hostname with port. Usually <strong>localhost:9091</strong>',
|
'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': 'username',
|
||||||
},
|
},
|
||||||
@@ -31,23 +39,33 @@ config = [{
|
|||||||
'name': 'password',
|
'name': 'password',
|
||||||
'type': 'password',
|
'type': 'password',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
'name': 'paused',
|
|
||||||
'type': 'bool',
|
|
||||||
'default': False,
|
|
||||||
'description': 'Add the torrent paused.',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
'name': 'directory',
|
'name': 'directory',
|
||||||
'type': 'directory',
|
'type': 'directory',
|
||||||
'description': 'Where should Transmission saved the downloaded files?',
|
'description': 'Download to this directory. Keep empty for default Transmission download directory.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'ratio',
|
'name': 'remove_complete',
|
||||||
'default': 10,
|
'label': 'Remove torrent',
|
||||||
'type': 'int',
|
'default': True,
|
||||||
'advanced': True,
|
'advanced': True,
|
||||||
'description': 'Stop transfer when reaching ratio',
|
'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',
|
'name': 'manual',
|
||||||
@@ -56,6 +74,20 @@ config = [{
|
|||||||
'advanced': True,
|
'advanced': True,
|
||||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
'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,7 +1,10 @@
|
|||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from couchpotato.core.downloaders.base import Downloader
|
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||||
from couchpotato.core.helpers.encoding import isInt
|
from couchpotato.core.helpers.encoding import isInt, ss
|
||||||
|
from couchpotato.core.helpers.variable import tryInt, tryFloat
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
from datetime import timedelta
|
||||||
import httplib
|
import httplib
|
||||||
import json
|
import json
|
||||||
import os.path
|
import os.path
|
||||||
@@ -13,70 +16,140 @@ log = CPLog(__name__)
|
|||||||
|
|
||||||
class Transmission(Downloader):
|
class Transmission(Downloader):
|
||||||
|
|
||||||
type = ['torrent', 'torrent_magnet']
|
protocol = ['torrent', 'torrent_magnet']
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
trpc = None
|
||||||
|
|
||||||
def download(self, data, movie, manual = False, filedata = None):
|
def connect(self):
|
||||||
|
|
||||||
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
|
|
||||||
return
|
|
||||||
|
|
||||||
log.debug('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
|
|
||||||
|
|
||||||
# Load host from config and split out port.
|
# Load host from config and split out port.
|
||||||
host = self.conf('host').split(':')
|
host = self.conf('host').split(':')
|
||||||
if not isInt(host[1]):
|
if not isInt(host[1]):
|
||||||
log.error('Config properties are not filled in correctly, port is missing.')
|
log.error('Config properties are not filled in correctly, port is missing.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Set parameters for Transmission
|
if not self.trpc:
|
||||||
folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1]
|
self.trpc = TransmissionRPC(host[0], port = host[1], rpc_url = self.conf('rpc_url'), username = self.conf('username'), password = self.conf('password'))
|
||||||
folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep)
|
|
||||||
|
|
||||||
# Create the empty folder to download too
|
return self.trpc
|
||||||
self.makeDir(folder_path)
|
|
||||||
|
|
||||||
params = {
|
def download(self, data, movie, filedata = None):
|
||||||
'paused': self.conf('paused', default = 0),
|
|
||||||
'download-dir': folder_path
|
|
||||||
}
|
|
||||||
|
|
||||||
torrent_params = {
|
log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('protocol')))
|
||||||
'seedRatioLimit': self.conf('ratio'),
|
|
||||||
'seedRatioMode': (0 if self.conf('ratio') else 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if not filedata and data.get('type') == 'torrent':
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not filedata and data.get('protocol') == 'torrent':
|
||||||
log.error('Failed sending torrent, no data')
|
log.error('Failed sending torrent, no data')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Send request to Transmission
|
# Set parameters for adding torrent
|
||||||
try:
|
params = {
|
||||||
trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
'paused': self.conf('paused', default = False)
|
||||||
if data.get('type') == 'torrent_magnet':
|
}
|
||||||
remote_torrent = trpc.add_torrent_uri(data.get('url'), arguments = params)
|
|
||||||
torrent_params['trackerAdd'] = self.torrent_trackers
|
if self.conf('directory'):
|
||||||
|
if os.path.isdir(self.conf('directory')):
|
||||||
|
params['download-dir'] = self.conf('directory')
|
||||||
else:
|
else:
|
||||||
remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params)
|
log.error('Download directory from Transmission settings: %s doesn\'t exist', self.conf('directory'))
|
||||||
|
|
||||||
# Change settings of added torrents
|
# Change parameters of torrent
|
||||||
trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
|
torrent_params = {}
|
||||||
|
if data.get('seed_ratio'):
|
||||||
|
torrent_params['seedRatioLimit'] = tryFloat(data.get('seed_ratio'))
|
||||||
|
torrent_params['seedRatioMode'] = 1
|
||||||
|
|
||||||
return True
|
if data.get('seed_time'):
|
||||||
except Exception, err:
|
torrent_params['seedIdleLimit'] = tryInt(data.get('seed_time')) * 60
|
||||||
log.error('Failed to change settings for transfer: %s', err)
|
torrent_params['seedIdleMode'] = 1
|
||||||
|
|
||||||
|
# Send request to Transmission
|
||||||
|
if data.get('protocol') == 'torrent_magnet':
|
||||||
|
remote_torrent = self.trpc.add_torrent_uri(data.get('url'), arguments = params)
|
||||||
|
torrent_params['trackerAdd'] = self.torrent_trackers
|
||||||
|
else:
|
||||||
|
remote_torrent = self.trpc.add_torrent_file(b64encode(filedata), arguments = params)
|
||||||
|
|
||||||
|
if not remote_torrent:
|
||||||
|
log.error('Failed sending torrent to Transmission')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Change settings of added torrents
|
||||||
|
if torrent_params:
|
||||||
|
self.trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
|
||||||
|
|
||||||
|
log.info('Torrent sent to Transmission successfully.')
|
||||||
|
return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
|
||||||
|
|
||||||
|
def getAllDownloadStatus(self):
|
||||||
|
|
||||||
|
log.debug('Checking Transmission download status.')
|
||||||
|
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
statuses = StatusList(self)
|
||||||
|
|
||||||
|
return_params = {
|
||||||
|
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isStalled', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit']
|
||||||
|
}
|
||||||
|
|
||||||
|
queue = self.trpc.get_alltorrents(return_params)
|
||||||
|
if not (queue and queue.get('torrents')):
|
||||||
|
log.debug('Nothing in queue or error')
|
||||||
|
return False
|
||||||
|
|
||||||
|
for item in queue['torrents']:
|
||||||
|
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / isFinished=%s',
|
||||||
|
(item['name'], item['id'], item['downloadDir'], item['hashString'], item['percentDone'], item['status'], item['eta'], item['uploadRatio'], item['isFinished']))
|
||||||
|
|
||||||
|
if not os.path.isdir(Env.setting('from', 'renamer')):
|
||||||
|
log.error('Renamer "from" folder doesn\'t to exist.')
|
||||||
|
return
|
||||||
|
|
||||||
|
status = 'busy'
|
||||||
|
if item['isStalled'] and self.conf('stalled_as_failed'):
|
||||||
|
status = 'failed'
|
||||||
|
elif item['status'] == 0 and item['percentDone'] == 1:
|
||||||
|
status = 'completed'
|
||||||
|
elif item['status'] in [5, 6]:
|
||||||
|
status = 'seeding'
|
||||||
|
|
||||||
|
statuses.append({
|
||||||
|
'id': item['hashString'],
|
||||||
|
'name': item['name'],
|
||||||
|
'status': status,
|
||||||
|
'original_status': item['status'],
|
||||||
|
'seed_ratio': item['uploadRatio'],
|
||||||
|
'timeleft': str(timedelta(seconds = item['eta'])),
|
||||||
|
'folder': ss(os.path.join(item['downloadDir'], item['name'])),
|
||||||
|
})
|
||||||
|
|
||||||
|
return statuses
|
||||||
|
|
||||||
|
def pause(self, item, pause = True):
|
||||||
|
if pause:
|
||||||
|
return self.trpc.stop_torrent(item['id'])
|
||||||
|
else:
|
||||||
|
return self.trpc.start_torrent(item['id'])
|
||||||
|
|
||||||
|
def removeFailed(self, item):
|
||||||
|
log.info('%s failed downloading, deleting...', item['name'])
|
||||||
|
return self.trpc.remove_torrent(item['hashString'], True)
|
||||||
|
|
||||||
|
def processComplete(self, item, delete_files = False):
|
||||||
|
log.debug('Requesting Transmission to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||||
|
return self.trpc.remove_torrent(item['hashString'], delete_files)
|
||||||
|
|
||||||
class TransmissionRPC(object):
|
class TransmissionRPC(object):
|
||||||
|
|
||||||
"""TransmissionRPC lite library"""
|
"""TransmissionRPC lite library"""
|
||||||
|
def __init__(self, host = 'localhost', port = 9091, rpc_url = 'transmission', username = None, password = None):
|
||||||
def __init__(self, host = 'localhost', port = 9091, username = None, password = None):
|
|
||||||
|
|
||||||
super(TransmissionRPC, self).__init__()
|
super(TransmissionRPC, self).__init__()
|
||||||
|
|
||||||
self.url = 'http://' + host + ':' + str(port) + '/transmission/rpc'
|
self.url = 'http://' + host + ':' + str(port) + '/' + rpc_url + '/rpc'
|
||||||
self.tag = 0
|
self.tag = 0
|
||||||
self.session_id = 0
|
self.session_id = 0
|
||||||
self.session = {}
|
self.session = {}
|
||||||
@@ -97,9 +170,10 @@ class TransmissionRPC(object):
|
|||||||
try:
|
try:
|
||||||
open_request = urllib2.urlopen(request)
|
open_request = urllib2.urlopen(request)
|
||||||
response = json.loads(open_request.read())
|
response = json.loads(open_request.read())
|
||||||
|
log.debug('request: %s', json.dumps(ojson))
|
||||||
log.debug('response: %s', json.dumps(response))
|
log.debug('response: %s', json.dumps(response))
|
||||||
if response['result'] == 'success':
|
if response['result'] == 'success':
|
||||||
log.debug('Transmission action successfull')
|
log.debug('Transmission action successful')
|
||||||
return response['arguments']
|
return response['arguments']
|
||||||
else:
|
else:
|
||||||
log.debug('Unknown failure sending command to Transmission. Return text is: %s', response['result'])
|
log.debug('Unknown failure sending command to Transmission. Return text is: %s', response['result'])
|
||||||
@@ -146,3 +220,20 @@ class TransmissionRPC(object):
|
|||||||
arguments['ids'] = torrent_id
|
arguments['ids'] = torrent_id
|
||||||
post_data = {'arguments': arguments, 'method': 'torrent-set', 'tag': self.tag}
|
post_data = {'arguments': arguments, 'method': 'torrent-set', 'tag': self.tag}
|
||||||
return self._request(post_data)
|
return self._request(post_data)
|
||||||
|
|
||||||
|
def get_alltorrents(self, arguments):
|
||||||
|
post_data = {'arguments': arguments, 'method': 'torrent-get', 'tag': self.tag}
|
||||||
|
return self._request(post_data)
|
||||||
|
|
||||||
|
def stop_torrent(self, torrent_id):
|
||||||
|
post_data = {'arguments': {'ids': torrent_id}, 'method': 'torrent-stop', 'tag': self.tag}
|
||||||
|
return self._request(post_data)
|
||||||
|
|
||||||
|
def start_torrent(self, torrent_id):
|
||||||
|
post_data = {'arguments': {'ids': torrent_id}, 'method': 'torrent-start', 'tag': self.tag}
|
||||||
|
return self._request(post_data)
|
||||||
|
|
||||||
|
def remove_torrent(self, torrent_id, delete_local_data):
|
||||||
|
post_data = {'arguments': {'ids': torrent_id, 'delete-local-data': delete_local_data}, 'method': 'torrent-remove', 'tag': self.tag}
|
||||||
|
return self._request(post_data)
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'downloaders',
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
'name': 'utorrent',
|
'name': 'utorrent',
|
||||||
'label': 'uTorrent',
|
'label': 'uTorrent',
|
||||||
'description': 'Send torrents to uTorrent.',
|
'description': 'Use <a href="http://www.utorrent.com/" target="_blank">uTorrent</a> (3.0+) to download torrents.',
|
||||||
'wizard': True,
|
'wizard': True,
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
@@ -35,9 +36,26 @@ config = [{
|
|||||||
'name': 'label',
|
'name': 'label',
|
||||||
'description': 'Label to add torrent as.',
|
'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',
|
'name': 'paused',
|
||||||
'type': 'bool',
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
'default': False,
|
'default': False,
|
||||||
'description': 'Add the torrent paused.',
|
'description': 'Add the torrent paused.',
|
||||||
},
|
},
|
||||||
@@ -48,6 +66,13 @@ config = [{
|
|||||||
'advanced': True,
|
'advanced': True,
|
||||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
'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,72 +1,212 @@
|
|||||||
from bencode import bencode, bdecode
|
from base64 import b16encode, b32decode
|
||||||
from couchpotato.core.downloaders.base import Downloader
|
from bencode import bencode as benc, bdecode
|
||||||
from couchpotato.core.helpers.encoding import isInt
|
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||||
|
from couchpotato.core.helpers.encoding import isInt, ss
|
||||||
|
from couchpotato.core.helpers.variable import tryInt, tryFloat
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
|
from datetime import timedelta
|
||||||
from hashlib import sha1
|
from hashlib import sha1
|
||||||
from multipartpost import MultipartPostHandler
|
from multipartpost import MultipartPostHandler
|
||||||
import cookielib
|
import cookielib
|
||||||
import httplib
|
import httplib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
|
import stat
|
||||||
import time
|
import time
|
||||||
import urllib
|
import urllib
|
||||||
import urllib2
|
import urllib2
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class uTorrent(Downloader):
|
class uTorrent(Downloader):
|
||||||
|
|
||||||
type = ['torrent', 'torrent_magnet']
|
protocol = ['torrent', 'torrent_magnet']
|
||||||
utorrent_api = None
|
utorrent_api = None
|
||||||
|
|
||||||
def download(self, data, movie, manual = False, filedata = None):
|
def connect(self):
|
||||||
|
|
||||||
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
|
|
||||||
return
|
|
||||||
|
|
||||||
log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('type')))
|
|
||||||
|
|
||||||
# Load host from config and split out port.
|
# Load host from config and split out port.
|
||||||
host = self.conf('host').split(':')
|
host = self.conf('host').split(':')
|
||||||
if not isInt(host[1]):
|
if not isInt(host[1]):
|
||||||
log.error('Config properties are not filled in correctly, port is missing.')
|
log.error('Config properties are not filled in correctly, port is missing.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||||
|
|
||||||
|
return self.utorrent_api
|
||||||
|
|
||||||
|
def download(self, data = None, movie = None, filedata = None):
|
||||||
|
if not movie: movie = {}
|
||||||
|
if not data: data = {}
|
||||||
|
|
||||||
|
log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('protocol')))
|
||||||
|
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
|
||||||
|
settings = self.utorrent_api.get_settings()
|
||||||
|
if not settings:
|
||||||
|
return False
|
||||||
|
|
||||||
|
#Fix settings in case they are not set for CPS compatibility
|
||||||
|
new_settings = {}
|
||||||
|
if not (settings.get('seed_prio_limitul') == 0 and settings['seed_prio_limitul_flag']):
|
||||||
|
new_settings['seed_prio_limitul'] = 0
|
||||||
|
new_settings['seed_prio_limitul_flag'] = True
|
||||||
|
log.info('Updated uTorrent settings to set a torrent to complete after it the seeding requirements are met.')
|
||||||
|
|
||||||
|
if settings.get('bt.read_only_on_complete'): #This doesn't work as this option seems to be not available through the api. Mitigated with removeReadOnly function
|
||||||
|
new_settings['bt.read_only_on_complete'] = False
|
||||||
|
log.info('Updated uTorrent settings to not set the files to read only after completing.')
|
||||||
|
|
||||||
|
if new_settings:
|
||||||
|
self.utorrent_api.set_settings(new_settings)
|
||||||
|
|
||||||
torrent_params = {}
|
torrent_params = {}
|
||||||
if self.conf('label'):
|
if self.conf('label'):
|
||||||
torrent_params['label'] = self.conf('label')
|
torrent_params['label'] = self.conf('label')
|
||||||
|
|
||||||
if not filedata and data.get('type') == 'torrent':
|
if not filedata and data.get('protocol') == 'torrent':
|
||||||
log.error('Failed sending torrent, no data')
|
log.error('Failed sending torrent, no data')
|
||||||
return False
|
return False
|
||||||
if data.get('type') == 'torrent_magnet':
|
|
||||||
|
if data.get('protocol') == 'torrent_magnet':
|
||||||
torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper()
|
torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper()
|
||||||
torrent_params['trackers'] = '%0D%0A%0D%0A'.join(self.torrent_trackers)
|
torrent_params['trackers'] = '%0D%0A%0D%0A'.join(self.torrent_trackers)
|
||||||
else:
|
else:
|
||||||
info = bdecode(filedata)["info"]
|
info = bdecode(filedata)["info"]
|
||||||
torrent_hash = sha1(bencode(info)).hexdigest().upper()
|
torrent_hash = sha1(benc(info)).hexdigest().upper()
|
||||||
torrent_filename = self.createFileName(data, filedata, movie)
|
torrent_filename = self.createFileName(data, filedata, movie)
|
||||||
|
|
||||||
|
if data.get('seed_ratio'):
|
||||||
|
torrent_params['seed_override'] = 1
|
||||||
|
torrent_params['seed_ratio'] = tryInt(tryFloat(data['seed_ratio']) * 1000)
|
||||||
|
|
||||||
|
if data.get('seed_time'):
|
||||||
|
torrent_params['seed_override'] = 1
|
||||||
|
torrent_params['seed_time'] = tryInt(data['seed_time']) * 3600
|
||||||
|
|
||||||
|
# Convert base 32 to hex
|
||||||
|
if len(torrent_hash) == 32:
|
||||||
|
torrent_hash = b16encode(b32decode(torrent_hash))
|
||||||
|
|
||||||
# Send request to uTorrent
|
# Send request to uTorrent
|
||||||
try:
|
if data.get('protocol') == 'torrent_magnet':
|
||||||
if not self.utorrent_api:
|
self.utorrent_api.add_torrent_uri(torrent_filename, data.get('url'))
|
||||||
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
else:
|
||||||
|
self.utorrent_api.add_torrent_file(torrent_filename, filedata)
|
||||||
|
|
||||||
if data.get('type') == 'torrent_magnet':
|
# Change settings of added torrent
|
||||||
self.utorrent_api.add_torrent_uri(data.get('url'))
|
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
|
||||||
|
if self.conf('paused', default = 0):
|
||||||
|
self.utorrent_api.pause_torrent(torrent_hash)
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
while True:
|
||||||
|
|
||||||
|
count += 1
|
||||||
|
# Check if torrent is saved in subfolder of torrent name
|
||||||
|
data = self.utorrent_api.get_files(torrent_hash)
|
||||||
|
|
||||||
|
torrent_files = json.loads(data)
|
||||||
|
if torrent_files.get('error'):
|
||||||
|
log.error('Error getting data from uTorrent: %s', torrent_files.get('error'))
|
||||||
|
return False
|
||||||
|
|
||||||
|
if (torrent_files.get('files') and len(torrent_files['files'][1]) > 0) or count > 60:
|
||||||
|
break
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Torrent has only one file, so uTorrent wont create a folder for it
|
||||||
|
if len(torrent_files['files'][1]) == 1:
|
||||||
|
# Remove torrent and try again
|
||||||
|
self.utorrent_api.remove_torrent(torrent_hash, remove_data = True)
|
||||||
|
|
||||||
|
# Send request to uTorrent
|
||||||
|
if data.get('protocol') == 'torrent_magnet':
|
||||||
|
self.utorrent_api.add_torrent_uri(torrent_filename, data.get('url'), add_folder = True)
|
||||||
else:
|
else:
|
||||||
self.utorrent_api.add_torrent_file(torrent_filename, filedata)
|
self.utorrent_api.add_torrent_file(torrent_filename, filedata, add_folder = True)
|
||||||
|
|
||||||
# Change settings of added torrents
|
# Change settings of added torrent
|
||||||
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
|
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
|
||||||
if self.conf('paused', default = 0):
|
if self.conf('paused', default = 0):
|
||||||
self.utorrent_api.pause_torrent(torrent_hash)
|
self.utorrent_api.pause_torrent(torrent_hash)
|
||||||
return True
|
|
||||||
except Exception, err:
|
return self.downloadReturnId(torrent_hash)
|
||||||
log.error('Failed to send torrent to uTorrent: %s', err)
|
|
||||||
|
def getAllDownloadStatus(self):
|
||||||
|
|
||||||
|
log.debug('Checking uTorrent download status.')
|
||||||
|
|
||||||
|
if not self.connect():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
statuses = StatusList(self)
|
||||||
|
|
||||||
|
data = self.utorrent_api.get_status()
|
||||||
|
if not data:
|
||||||
|
log.error('Error getting data from uTorrent')
|
||||||
|
return False
|
||||||
|
|
||||||
|
queue = json.loads(data)
|
||||||
|
if queue.get('error'):
|
||||||
|
log.error('Error getting data from uTorrent: %s', queue.get('error'))
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not queue.get('torrents'):
|
||||||
|
log.debug('Nothing in queue')
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Get torrents
|
||||||
|
for item in queue['torrents']:
|
||||||
|
|
||||||
|
# item[21] = Paused | Downloading | Seeding | Finished
|
||||||
|
status = 'busy'
|
||||||
|
if 'Finished' in item[21]:
|
||||||
|
status = 'completed'
|
||||||
|
self.removeReadOnly(item[26])
|
||||||
|
elif 'Seeding' in item[21]:
|
||||||
|
status = 'seeding'
|
||||||
|
self.removeReadOnly(item[26])
|
||||||
|
|
||||||
|
statuses.append({
|
||||||
|
'id': item[0],
|
||||||
|
'name': item[2],
|
||||||
|
'status': status,
|
||||||
|
'seed_ratio': float(item[7]) / 1000,
|
||||||
|
'original_status': item[1],
|
||||||
|
'timeleft': str(timedelta(seconds = item[10])),
|
||||||
|
'folder': ss(item[26]),
|
||||||
|
})
|
||||||
|
|
||||||
|
return statuses
|
||||||
|
|
||||||
|
def pause(self, item, pause = True):
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
return self.utorrent_api.pause_torrent(item['id'], pause)
|
||||||
|
|
||||||
|
def removeFailed(self, item):
|
||||||
|
log.info('%s failed downloading, deleting...', item['name'])
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
return self.utorrent_api.remove_torrent(item['id'], remove_data = True)
|
||||||
|
|
||||||
|
def processComplete(self, item, delete_files = False):
|
||||||
|
log.debug('Requesting uTorrent to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||||
|
if not self.connect():
|
||||||
|
return False
|
||||||
|
return self.utorrent_api.remove_torrent(item['id'], remove_data = delete_files)
|
||||||
|
|
||||||
|
def removeReadOnly(self, folder):
|
||||||
|
#Removes all read-only flags in a folder
|
||||||
|
if folder and os.path.isdir(folder):
|
||||||
|
for root, folders, filenames in os.walk(folder):
|
||||||
|
for filename in filenames:
|
||||||
|
os.chmod(os.path.join(root, filename), stat.S_IWRITE)
|
||||||
|
|
||||||
class uTorrentAPI(object):
|
class uTorrentAPI(object):
|
||||||
|
|
||||||
@@ -97,9 +237,7 @@ class uTorrentAPI(object):
|
|||||||
try:
|
try:
|
||||||
open_request = self.opener.open(request)
|
open_request = self.opener.open(request)
|
||||||
response = open_request.read()
|
response = open_request.read()
|
||||||
log.debug('response: %s', response)
|
|
||||||
if response:
|
if response:
|
||||||
log.debug('uTorrent action successfull')
|
|
||||||
return response
|
return response
|
||||||
else:
|
else:
|
||||||
log.debug('Unknown failure sending command to uTorrent. Return text is: %s', response)
|
log.debug('Unknown failure sending command to uTorrent. Return text is: %s', response)
|
||||||
@@ -119,13 +257,17 @@ class uTorrentAPI(object):
|
|||||||
token = re.findall("<div.*?>(.*?)</", request.read())[0]
|
token = re.findall("<div.*?>(.*?)</", request.read())[0]
|
||||||
return token
|
return token
|
||||||
|
|
||||||
def add_torrent_uri(self, torrent):
|
def add_torrent_uri(self, filename, torrent, add_folder = False):
|
||||||
action = "action=add-url&s=%s" % urllib.quote(torrent)
|
action = "action=add-url&s=%s" % urllib.quote(torrent)
|
||||||
|
if add_folder:
|
||||||
|
action += "&path=%s" % urllib.quote(filename)
|
||||||
return self._request(action)
|
return self._request(action)
|
||||||
|
|
||||||
def add_torrent_file(self, filename, filedata):
|
def add_torrent_file(self, filename, filedata, add_folder = False):
|
||||||
action = "action=add-file"
|
action = "action=add-file"
|
||||||
return self._request(action, {"torrent_file": (filename, filedata)})
|
if add_folder:
|
||||||
|
action += "&path=%s" % urllib.quote(filename)
|
||||||
|
return self._request(action, {"torrent_file": (ss(filename), filedata)})
|
||||||
|
|
||||||
def set_torrent(self, hash, params):
|
def set_torrent(self, hash, params):
|
||||||
action = "action=setprops&hash=%s" % hash
|
action = "action=setprops&hash=%s" % hash
|
||||||
@@ -133,6 +275,60 @@ class uTorrentAPI(object):
|
|||||||
action += "&s=%s&v=%s" % (k, v)
|
action += "&s=%s&v=%s" % (k, v)
|
||||||
return self._request(action)
|
return self._request(action)
|
||||||
|
|
||||||
def pause_torrent(self, hash):
|
def pause_torrent(self, hash, pause = True):
|
||||||
action = "action=pause&hash=%s" % hash
|
if pause:
|
||||||
|
action = "action=pause&hash=%s" % hash
|
||||||
|
else:
|
||||||
|
action = "action=unpause&hash=%s" % hash
|
||||||
|
return self._request(action)
|
||||||
|
|
||||||
|
def stop_torrent(self, hash):
|
||||||
|
action = "action=stop&hash=%s" % hash
|
||||||
|
return self._request(action)
|
||||||
|
|
||||||
|
def remove_torrent(self, hash, remove_data = False):
|
||||||
|
if remove_data:
|
||||||
|
action = "action=removedata&hash=%s" % hash
|
||||||
|
else:
|
||||||
|
action = "action=remove&hash=%s" % hash
|
||||||
|
return self._request(action)
|
||||||
|
|
||||||
|
def get_status(self):
|
||||||
|
action = "list=1"
|
||||||
|
return self._request(action)
|
||||||
|
|
||||||
|
def get_settings(self):
|
||||||
|
action = "action=getsettings"
|
||||||
|
settings_dict = {}
|
||||||
|
try:
|
||||||
|
utorrent_settings = json.loads(self._request(action))
|
||||||
|
|
||||||
|
# Create settings dict
|
||||||
|
for item in utorrent_settings['settings']:
|
||||||
|
if item[1] == 0: # int
|
||||||
|
settings_dict[item[0]] = int(item[2] if not item[2].strip() == '' else '0')
|
||||||
|
elif item[1] == 1: # bool
|
||||||
|
settings_dict[item[0]] = True if item[2] == 'true' else False
|
||||||
|
elif item[1] == 2: # string
|
||||||
|
settings_dict[item[0]] = item[2]
|
||||||
|
|
||||||
|
#log.debug('uTorrent settings: %s', settings_dict)
|
||||||
|
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to get settings from uTorrent: %s', err)
|
||||||
|
|
||||||
|
return settings_dict
|
||||||
|
|
||||||
|
def set_settings(self, settings_dict = None):
|
||||||
|
if not settings_dict: settings_dict = {}
|
||||||
|
|
||||||
|
for key in settings_dict:
|
||||||
|
if isinstance(settings_dict[key], bool):
|
||||||
|
settings_dict[key] = 1 if settings_dict[key] else 0
|
||||||
|
|
||||||
|
action = 'action=setsetting' + ''.join(['&s=%s&v=%s' % (key, value) for (key, value) in settings_dict.items()])
|
||||||
|
return self._request(action)
|
||||||
|
|
||||||
|
def get_files(self, hash):
|
||||||
|
action = "action=getfiles&hash=%s" % hash
|
||||||
return self._request(action)
|
return self._request(action)
|
||||||
|
|||||||
+54
-30
@@ -16,33 +16,45 @@ def runHandler(name, handler, *args, **kwargs):
|
|||||||
|
|
||||||
def addEvent(name, handler, priority = 100):
|
def addEvent(name, handler, priority = 100):
|
||||||
|
|
||||||
if events.get(name):
|
if not events.get(name):
|
||||||
e = events[name]
|
events[name] = []
|
||||||
else:
|
|
||||||
e = events[name] = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock())
|
|
||||||
|
|
||||||
def createHandle(*args, **kwargs):
|
def createHandle(*args, **kwargs):
|
||||||
|
|
||||||
|
h = None
|
||||||
try:
|
try:
|
||||||
parent = handler.im_self
|
# Open handler
|
||||||
bc = hasattr(parent, 'beforeCall')
|
has_parent = hasattr(handler, 'im_self')
|
||||||
if bc: parent.beforeCall(handler)
|
parent = None
|
||||||
|
if has_parent:
|
||||||
|
parent = handler.im_self
|
||||||
|
bc = hasattr(parent, 'beforeCall')
|
||||||
|
if bc: parent.beforeCall(handler)
|
||||||
|
|
||||||
|
# Main event
|
||||||
h = runHandler(name, handler, *args, **kwargs)
|
h = runHandler(name, handler, *args, **kwargs)
|
||||||
ac = hasattr(parent, 'afterCall')
|
|
||||||
if ac: parent.afterCall(handler)
|
# Close handler
|
||||||
|
if parent and has_parent:
|
||||||
|
ac = hasattr(parent, 'afterCall')
|
||||||
|
if ac: parent.afterCall(handler)
|
||||||
except:
|
except:
|
||||||
h = runHandler(name, handler, *args, **kwargs)
|
log.error('Failed creating handler %s %s: %s', (name, handler, traceback.format_exc()))
|
||||||
|
|
||||||
return h
|
return h
|
||||||
|
|
||||||
e.handle(createHandle, priority = priority)
|
events[name].append({
|
||||||
|
'handler': createHandle,
|
||||||
|
'priority': priority,
|
||||||
|
})
|
||||||
|
|
||||||
def removeEvent(name, handler):
|
def removeEvent(name, handler):
|
||||||
e = events[name]
|
e = events[name]
|
||||||
e -= handler
|
e -= handler
|
||||||
|
|
||||||
def fireEvent(name, *args, **kwargs):
|
def fireEvent(name, *args, **kwargs):
|
||||||
if not events.get(name): return
|
if not events.has_key(name): return
|
||||||
|
|
||||||
#log.debug('Firing event %s', name)
|
#log.debug('Firing event %s', name)
|
||||||
try:
|
try:
|
||||||
|
|
||||||
@@ -62,22 +74,32 @@ def fireEvent(name, *args, **kwargs):
|
|||||||
options[x] = val
|
options[x] = val
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
e = events[name]
|
if len(events[name]) == 1:
|
||||||
|
|
||||||
# Lock this event
|
single = None
|
||||||
e.lock.acquire()
|
try:
|
||||||
|
single = events[name][0]['handler'](*args, **kwargs)
|
||||||
|
except:
|
||||||
|
log.error('Failed running single event: %s', traceback.format_exc())
|
||||||
|
|
||||||
e.asynchronous = False
|
# Don't load thread for single event
|
||||||
|
result = {
|
||||||
|
'single': (single is not None, single),
|
||||||
|
}
|
||||||
|
|
||||||
# Make sure only 1 event is fired at a time when order is wanted
|
else:
|
||||||
kwargs['event_order_lock'] = threading.RLock() if options['in_order'] or options['single'] else None
|
|
||||||
kwargs['event_return_on_result'] = options['single']
|
|
||||||
|
|
||||||
# Fire
|
e = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock())
|
||||||
result = e(*args, **kwargs)
|
|
||||||
|
|
||||||
# Release lock for this event
|
for event in events[name]:
|
||||||
e.lock.release()
|
e.handle(event['handler'], priority = event['priority'])
|
||||||
|
|
||||||
|
# Make sure only 1 event is fired at a time when order is wanted
|
||||||
|
kwargs['event_order_lock'] = threading.RLock() if options['in_order'] or options['single'] else None
|
||||||
|
kwargs['event_return_on_result'] = options['single']
|
||||||
|
|
||||||
|
# Fire
|
||||||
|
result = e(*args, **kwargs)
|
||||||
|
|
||||||
if options['single'] and not options['merge']:
|
if options['single'] and not options['merge']:
|
||||||
results = None
|
results = None
|
||||||
@@ -104,18 +126,22 @@ def fireEvent(name, *args, **kwargs):
|
|||||||
|
|
||||||
# Merge
|
# Merge
|
||||||
if options['merge'] and len(results) > 0:
|
if options['merge'] and len(results) > 0:
|
||||||
|
|
||||||
# Dict
|
# Dict
|
||||||
if isinstance(results[0], dict):
|
if isinstance(results[0], dict):
|
||||||
|
results.reverse()
|
||||||
|
|
||||||
merged = {}
|
merged = {}
|
||||||
for result in results:
|
for result in results:
|
||||||
merged = mergeDicts(merged, result)
|
merged = mergeDicts(merged, result, prepend_list = True)
|
||||||
|
|
||||||
results = merged
|
results = merged
|
||||||
# Lists
|
# Lists
|
||||||
elif isinstance(results[0], list):
|
elif isinstance(results[0], list):
|
||||||
merged = []
|
merged = []
|
||||||
for result in results:
|
for result in results:
|
||||||
merged += result
|
if result not in merged:
|
||||||
|
merged += result
|
||||||
|
|
||||||
results = merged
|
results = merged
|
||||||
|
|
||||||
@@ -131,16 +157,14 @@ def fireEvent(name, *args, **kwargs):
|
|||||||
options['on_complete']()
|
options['on_complete']()
|
||||||
|
|
||||||
return results
|
return results
|
||||||
except KeyError, e:
|
|
||||||
pass
|
|
||||||
except Exception:
|
except Exception:
|
||||||
log.error('%s: %s', (name, traceback.format_exc()))
|
log.error('%s: %s', (name, traceback.format_exc()))
|
||||||
|
|
||||||
def fireEventAsync(*args, **kwargs):
|
def fireEventAsync(*args, **kwargs):
|
||||||
try:
|
try:
|
||||||
my_thread = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
|
t = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
|
||||||
my_thread.setDaemon(True)
|
t.setDaemon(True)
|
||||||
my_thread.start()
|
t.start()
|
||||||
return True
|
return True
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
log.error('%s: %s', (args[0], e))
|
log.error('%s: %s', (args[0], e))
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ log = CPLog(__name__)
|
|||||||
def toSafeString(original):
|
def toSafeString(original):
|
||||||
valid_chars = "-_.() %s%s" % (ascii_letters, digits)
|
valid_chars = "-_.() %s%s" % (ascii_letters, digits)
|
||||||
cleanedFilename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
|
cleanedFilename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
|
||||||
return ''.join(c for c in cleanedFilename if c in valid_chars)
|
valid_string = ''.join(c for c in cleanedFilename if c in valid_chars)
|
||||||
|
return ' '.join(valid_string.split())
|
||||||
|
|
||||||
def simplifyString(original):
|
def simplifyString(original):
|
||||||
string = stripAccents(original.lower())
|
string = stripAccents(original.lower())
|
||||||
@@ -62,7 +63,7 @@ def stripAccents(s):
|
|||||||
|
|
||||||
def tryUrlencode(s):
|
def tryUrlencode(s):
|
||||||
new = u''
|
new = u''
|
||||||
if isinstance(s, (dict)):
|
if isinstance(s, dict):
|
||||||
for key, value in s.iteritems():
|
for key, value in s.iteritems():
|
||||||
new += u'&%s=%s' % (key, tryUrlencode(value))
|
new += u'&%s=%s' % (key, tryUrlencode(value))
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.helpers.variable import natcmp
|
from couchpotato.core.helpers.variable import natcmp
|
||||||
from flask.globals import current_app
|
|
||||||
from flask.helpers import json, make_response
|
|
||||||
from urllib import unquote
|
from urllib import unquote
|
||||||
from werkzeug.urls import url_decode
|
|
||||||
import flask
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
def getParams():
|
|
||||||
|
|
||||||
params = url_decode(getattr(flask.request, 'environ').get('QUERY_STRING', ''))
|
def getParams(params):
|
||||||
|
|
||||||
reg = re.compile('^[a-z0-9_\.]+$')
|
reg = re.compile('^[a-z0-9_\.]+$')
|
||||||
|
|
||||||
current = temp = {}
|
temp = {}
|
||||||
for param, value in sorted(params.iteritems()):
|
for param, value in sorted(params.iteritems()):
|
||||||
|
|
||||||
nest = re.split("([\[\]]+)", param)
|
nest = re.split("([\[\]]+)", param)
|
||||||
@@ -36,6 +32,8 @@ def getParams():
|
|||||||
current = current[item]
|
current = current[item]
|
||||||
else:
|
else:
|
||||||
temp[param] = toUnicode(unquote(value))
|
temp[param] = toUnicode(unquote(value))
|
||||||
|
if temp[param].lower() in ['true', 'false']:
|
||||||
|
temp[param] = temp[param].lower() != 'false'
|
||||||
|
|
||||||
return dictToList(temp)
|
return dictToList(temp)
|
||||||
|
|
||||||
@@ -54,29 +52,3 @@ def dictToList(params):
|
|||||||
new = params
|
new = params
|
||||||
|
|
||||||
return new
|
return new
|
||||||
|
|
||||||
def getParam(attr, default = None):
|
|
||||||
try:
|
|
||||||
return getParams().get(attr, default)
|
|
||||||
except:
|
|
||||||
return default
|
|
||||||
|
|
||||||
def padded_jsonify(callback, *args, **kwargs):
|
|
||||||
content = str(callback) + '(' + json.dumps(dict(*args, **kwargs)) + ')'
|
|
||||||
return getattr(current_app, 'response_class')(content, mimetype = 'text/javascript')
|
|
||||||
|
|
||||||
def jsonify(mimetype, *args, **kwargs):
|
|
||||||
content = json.dumps(dict(*args, **kwargs))
|
|
||||||
return getattr(current_app, 'response_class')(content, mimetype = mimetype)
|
|
||||||
|
|
||||||
def jsonified(*args, **kwargs):
|
|
||||||
callback = getParam('callback_func', None)
|
|
||||||
if callback:
|
|
||||||
content = padded_jsonify(callback, *args, **kwargs)
|
|
||||||
else:
|
|
||||||
content = jsonify('application/json', *args, **kwargs)
|
|
||||||
|
|
||||||
response = make_response(content)
|
|
||||||
response.cache_control.no_cache = True
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ log = CPLog(__name__)
|
|||||||
class RSS(object):
|
class RSS(object):
|
||||||
|
|
||||||
def getTextElements(self, xml, path):
|
def getTextElements(self, xml, path):
|
||||||
''' Find elements and return tree'''
|
""" Find elements and return tree"""
|
||||||
|
|
||||||
textelements = []
|
textelements = []
|
||||||
try:
|
try:
|
||||||
@@ -28,7 +28,7 @@ class RSS(object):
|
|||||||
return elements
|
return elements
|
||||||
|
|
||||||
def getElement(self, xml, path):
|
def getElement(self, xml, path):
|
||||||
''' Find element and return text'''
|
""" Find element and return text"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return xml.find(path)
|
return xml.find(path)
|
||||||
@@ -36,7 +36,7 @@ class RSS(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def getTextElement(self, xml, path):
|
def getTextElement(self, xml, path):
|
||||||
''' Find element and return text'''
|
""" Find element and return text"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return xml.find(path).text
|
return xml.find(path).text
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from couchpotato.core.helpers.encoding import simplifyString, toSafeString
|
from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
import hashlib
|
import hashlib
|
||||||
import os.path
|
import os.path
|
||||||
@@ -10,6 +10,20 @@ import sys
|
|||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
def link(src, dst):
|
||||||
|
if os.name == 'nt':
|
||||||
|
import ctypes
|
||||||
|
if ctypes.windll.kernel32.CreateHardLinkW(unicode(dst), unicode(src), 0) == 0: raise ctypes.WinError()
|
||||||
|
else:
|
||||||
|
os.link(src, dst)
|
||||||
|
|
||||||
|
def symlink(src, dst):
|
||||||
|
if os.name == 'nt':
|
||||||
|
import ctypes
|
||||||
|
if ctypes.windll.kernel32.CreateSymbolicLinkW(unicode(dst), unicode(src), 1 if os.path.isdir(src) else 0) in [0, 1280]: raise ctypes.WinError()
|
||||||
|
else:
|
||||||
|
os.symlink(src, dst)
|
||||||
|
|
||||||
def getUserDir():
|
def getUserDir():
|
||||||
try:
|
try:
|
||||||
import pwd
|
import pwd
|
||||||
@@ -53,7 +67,7 @@ def getDataDir():
|
|||||||
def isDict(object):
|
def isDict(object):
|
||||||
return isinstance(object, dict)
|
return isinstance(object, dict)
|
||||||
|
|
||||||
def mergeDicts(a, b):
|
def mergeDicts(a, b, prepend_list = False):
|
||||||
assert isDict(a), isDict(b)
|
assert isDict(a), isDict(b)
|
||||||
dst = a.copy()
|
dst = a.copy()
|
||||||
|
|
||||||
@@ -67,7 +81,7 @@ def mergeDicts(a, b):
|
|||||||
if isDict(current_src[key]) and isDict(current_dst[key]):
|
if isDict(current_src[key]) and isDict(current_dst[key]):
|
||||||
stack.append((current_dst[key], current_src[key]))
|
stack.append((current_dst[key], current_src[key]))
|
||||||
elif isinstance(current_src[key], list) and isinstance(current_dst[key], list):
|
elif isinstance(current_src[key], list) and isinstance(current_dst[key], list):
|
||||||
current_dst[key].extend(current_src[key])
|
current_dst[key] = current_src[key] + current_dst[key] if prepend_list else current_dst[key] + current_src[key]
|
||||||
current_dst[key] = removeListDuplicates(current_dst[key])
|
current_dst[key] = removeListDuplicates(current_dst[key])
|
||||||
else:
|
else:
|
||||||
current_dst[key] = current_src[key]
|
current_dst[key] = current_src[key]
|
||||||
@@ -87,11 +101,16 @@ def flattenList(l):
|
|||||||
return l
|
return l
|
||||||
|
|
||||||
def md5(text):
|
def md5(text):
|
||||||
return hashlib.md5(text).hexdigest()
|
return hashlib.md5(ss(text)).hexdigest()
|
||||||
|
|
||||||
def sha1(text):
|
def sha1(text):
|
||||||
return hashlib.sha1(text).hexdigest()
|
return hashlib.sha1(text).hexdigest()
|
||||||
|
|
||||||
|
def isLocalIP(ip):
|
||||||
|
ip = ip.lstrip('htps:/')
|
||||||
|
regex = '/(^127\.)|(^192\.168\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^::1)$/'
|
||||||
|
return re.search(regex, ip) is not None or 'localhost' in ip or ip[:4] == '127.'
|
||||||
|
|
||||||
def getExt(filename):
|
def getExt(filename):
|
||||||
return os.path.splitext(filename)[1][1:]
|
return os.path.splitext(filename)[1][1:]
|
||||||
|
|
||||||
@@ -99,12 +118,17 @@ def cleanHost(host):
|
|||||||
if not host.startswith(('http://', 'https://')):
|
if not host.startswith(('http://', 'https://')):
|
||||||
host = 'http://' + host
|
host = 'http://' + host
|
||||||
|
|
||||||
if not host.endswith('/'):
|
host = host.rstrip('/')
|
||||||
host += '/'
|
host += '/'
|
||||||
|
|
||||||
return host
|
return host
|
||||||
|
|
||||||
def getImdb(txt, check_inside = True, multiple = False):
|
def getImdb(txt, check_inside = False, multiple = False):
|
||||||
|
|
||||||
|
if not check_inside:
|
||||||
|
txt = simplifyString(txt)
|
||||||
|
else:
|
||||||
|
txt = ss(txt)
|
||||||
|
|
||||||
if check_inside and os.path.isfile(txt):
|
if check_inside and os.path.isfile(txt):
|
||||||
output = open(txt, 'r')
|
output = open(txt, 'r')
|
||||||
@@ -114,7 +138,7 @@ def getImdb(txt, check_inside = True, multiple = False):
|
|||||||
try:
|
try:
|
||||||
ids = re.findall('(tt\d{7})', txt)
|
ids = re.findall('(tt\d{7})', txt)
|
||||||
if multiple:
|
if multiple:
|
||||||
return ids if len(ids) > 0 else []
|
return list(set(ids)) if len(ids) > 0 else []
|
||||||
return ids[0]
|
return ids[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
@@ -126,7 +150,11 @@ def tryInt(s):
|
|||||||
except: return 0
|
except: return 0
|
||||||
|
|
||||||
def tryFloat(s):
|
def tryFloat(s):
|
||||||
try: return float(s) if '.' in s else tryInt(s)
|
try:
|
||||||
|
if isinstance(s, str):
|
||||||
|
return float(s) if '.' in s else tryInt(s)
|
||||||
|
else:
|
||||||
|
return float(s)
|
||||||
except: return 0
|
except: return 0
|
||||||
|
|
||||||
def natsortKey(s):
|
def natsortKey(s):
|
||||||
@@ -145,8 +173,11 @@ def getTitle(library_dict):
|
|||||||
if title.default:
|
if title.default:
|
||||||
return title.title
|
return title.title
|
||||||
except:
|
except:
|
||||||
log.error('Could not get title for %s', library_dict.identifier)
|
try:
|
||||||
return None
|
return library_dict['info']['titles'][0]
|
||||||
|
except:
|
||||||
|
log.error('Could not get title for %s', library_dict.identifier)
|
||||||
|
return None
|
||||||
|
|
||||||
log.error('Could not get title for %s', library_dict['identifier'])
|
log.error('Could not get title for %s', library_dict['identifier'])
|
||||||
return None
|
return None
|
||||||
@@ -156,16 +187,21 @@ def getTitle(library_dict):
|
|||||||
|
|
||||||
def possibleTitles(raw_title):
|
def possibleTitles(raw_title):
|
||||||
|
|
||||||
titles = []
|
titles = [
|
||||||
|
toSafeString(raw_title).lower(),
|
||||||
|
raw_title.lower(),
|
||||||
|
simplifyString(raw_title)
|
||||||
|
]
|
||||||
|
|
||||||
titles.append(toSafeString(raw_title).lower())
|
# replace some chars
|
||||||
titles.append(raw_title.lower())
|
new_title = raw_title.replace('&', 'and')
|
||||||
titles.append(simplifyString(raw_title))
|
titles.append(simplifyString(new_title))
|
||||||
|
|
||||||
return list(set(titles))
|
return list(set(titles))
|
||||||
|
|
||||||
def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
|
def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
|
||||||
return ''.join(random.choice(chars) for x in range(size))
|
return ''.join(random.choice(chars) for x in range(size))
|
||||||
|
|
||||||
def splitString(str, split_on = ','):
|
def splitString(str, split_on = ',', clean = True):
|
||||||
return [x.strip() for x in str.split(split_on)]
|
list = [x.strip() for x in str.split(split_on)] if str else []
|
||||||
|
return filter(None, list) if clean else list
|
||||||
|
|||||||
+47
-18
@@ -6,15 +6,24 @@ import traceback
|
|||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
class Loader(object):
|
|
||||||
|
|
||||||
|
class Loader(object):
|
||||||
plugins = {}
|
plugins = {}
|
||||||
providers = {}
|
providers = {}
|
||||||
|
|
||||||
modules = {}
|
modules = {}
|
||||||
|
|
||||||
def preload(self, root = ''):
|
def addPath(self, root, base_path, priority, recursive = False):
|
||||||
|
for filename in os.listdir(os.path.join(root, *base_path)):
|
||||||
|
path = os.path.join(os.path.join(root, *base_path), filename)
|
||||||
|
if os.path.isdir(path) and filename[:2] != '__':
|
||||||
|
if u'__init__.py' in os.listdir(path):
|
||||||
|
new_base_path = ''.join(s + '.' for s in base_path) + filename
|
||||||
|
self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path)
|
||||||
|
|
||||||
|
if recursive:
|
||||||
|
self.addPath(root, base_path + [filename], priority, recursive = True)
|
||||||
|
|
||||||
|
def preload(self, root = ''):
|
||||||
core = os.path.join(root, 'couchpotato', 'core')
|
core = os.path.join(root, 'couchpotato', 'core')
|
||||||
|
|
||||||
self.paths = {
|
self.paths = {
|
||||||
@@ -25,12 +34,10 @@ class Loader(object):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Add providers to loader
|
# Add providers to loader
|
||||||
provider_dir = os.path.join(root, 'couchpotato', 'core', 'providers')
|
self.addPath(root, ['couchpotato', 'core', 'providers'], 25, recursive = False)
|
||||||
for provider in os.listdir(provider_dir):
|
|
||||||
path = os.path.join(provider_dir, provider)
|
|
||||||
if os.path.isdir(path):
|
|
||||||
self.paths[provider + '_provider'] = (25, 'couchpotato.core.providers.' + provider, path)
|
|
||||||
|
|
||||||
|
# Add media to loader
|
||||||
|
self.addPath(root, ['couchpotato', 'core', 'media'], 25, recursive = True)
|
||||||
|
|
||||||
for plugin_type, plugin_tuple in self.paths.iteritems():
|
for plugin_type, plugin_tuple in self.paths.iteritems():
|
||||||
priority, module, dir_name = plugin_tuple
|
priority, module, dir_name = plugin_tuple
|
||||||
@@ -43,7 +50,13 @@ class Loader(object):
|
|||||||
for module_name, plugin in sorted(self.modules[priority].iteritems()):
|
for module_name, plugin in sorted(self.modules[priority].iteritems()):
|
||||||
# Load module
|
# Load module
|
||||||
try:
|
try:
|
||||||
m = getattr(self.loadModule(module_name), plugin.get('name'))
|
if plugin.get('name')[:2] == '__':
|
||||||
|
continue
|
||||||
|
|
||||||
|
m = self.loadModule(module_name)
|
||||||
|
if m is None:
|
||||||
|
continue
|
||||||
|
m = getattr(m, plugin.get('name'))
|
||||||
|
|
||||||
log.info('Loading %s: %s', (plugin['type'], plugin['name']))
|
log.info('Loading %s: %s', (plugin['type'], plugin['name']))
|
||||||
|
|
||||||
@@ -53,7 +66,7 @@ class Loader(object):
|
|||||||
self.loadPlugins(m, plugin.get('name'))
|
self.loadPlugins(m, 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"):
|
||||||
log.error(e.message)
|
log.error(e.message)
|
||||||
pass
|
pass
|
||||||
# todo:: this needs to be more descriptive.
|
# todo:: this needs to be more descriptive.
|
||||||
@@ -67,13 +80,27 @@ class Loader(object):
|
|||||||
|
|
||||||
def addFromDir(self, plugin_type, priority, module, dir_name):
|
def addFromDir(self, plugin_type, priority, module, dir_name):
|
||||||
|
|
||||||
|
# Load dir module
|
||||||
|
try:
|
||||||
|
m = __import__(module)
|
||||||
|
splitted = module.split('.')
|
||||||
|
for sub in splitted[1:]:
|
||||||
|
m = getattr(m, sub)
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
|
||||||
for cur_file in glob.glob(os.path.join(dir_name, '*')):
|
for cur_file in glob.glob(os.path.join(dir_name, '*')):
|
||||||
name = os.path.basename(cur_file)
|
name = os.path.basename(cur_file)
|
||||||
if os.path.isdir(os.path.join(dir_name, name)):
|
if os.path.isdir(os.path.join(dir_name, name)) and name != 'static' and os.path.isfile(os.path.join(cur_file, '__init__.py')):
|
||||||
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'):
|
||||||
|
log.debug('Skip loading settings for plugin %s as it has no config section' % module.__file__)
|
||||||
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for section in module.config:
|
for section in module.config:
|
||||||
fireEvent('settings.options', section['name'], section)
|
fireEvent('settings.options', section['name'], section)
|
||||||
@@ -88,15 +115,14 @@ class Loader(object):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def loadPlugins(self, module, name):
|
def loadPlugins(self, module, name):
|
||||||
|
|
||||||
|
if not hasattr(module, 'start'):
|
||||||
|
log.debug('Skip startup for plugin %s as it has no start section' % module.__file__)
|
||||||
|
return False
|
||||||
try:
|
try:
|
||||||
klass = module.start()
|
module.start()
|
||||||
klass.registerPlugin()
|
|
||||||
|
|
||||||
if klass and getattr(klass, 'auto_register_static'):
|
|
||||||
klass.registerStatic(module.__file__)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
except Exception, e:
|
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()))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -119,5 +145,8 @@ class Loader(object):
|
|||||||
for sub in splitted[1:-1]:
|
for sub in splitted[1:-1]:
|
||||||
m = getattr(m, sub)
|
m = getattr(m, sub)
|
||||||
return m
|
return m
|
||||||
|
except ImportError:
|
||||||
|
log.debug('Skip loading module plugin %s: %s', (name, traceback.format_exc()))
|
||||||
|
return None
|
||||||
except:
|
except:
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import traceback
|
|
||||||
|
|
||||||
class CPLog(object):
|
class CPLog(object):
|
||||||
|
|
||||||
context = ''
|
context = ''
|
||||||
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key']
|
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key', 'passkey']
|
||||||
|
|
||||||
def __init__(self, context = ''):
|
def __init__(self, context = ''):
|
||||||
if context.endswith('.main'):
|
if context.endswith('.main'):
|
||||||
@@ -50,8 +49,8 @@ class CPLog(object):
|
|||||||
msg = msg % tuple([ss(x) for x in list(replace_tuple)])
|
msg = msg % tuple([ss(x) for x in list(replace_tuple)])
|
||||||
else:
|
else:
|
||||||
msg = msg % ss(replace_tuple)
|
msg = msg % ss(replace_tuple)
|
||||||
except:
|
except Exception, e:
|
||||||
self.logger.error(u'Failed encoding stuff to log: %s' % traceback.format_exc())
|
self.logger.error(u'Failed encoding stuff to log "%s": %s' % (msg, e))
|
||||||
|
|
||||||
if not Env.get('dev'):
|
if not Env.get('dev'):
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from couchpotato.core.event import addEvent
|
||||||
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
|
||||||
|
|
||||||
|
class MediaBase(Plugin):
|
||||||
|
|
||||||
|
_type = None
|
||||||
|
|
||||||
|
def initType(self):
|
||||||
|
addEvent('media.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,75 @@
|
|||||||
|
from .main import Searcher
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Searcher()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'searcher',
|
||||||
|
'order': 20,
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'name': 'searcher',
|
||||||
|
'label': 'Basics',
|
||||||
|
'description': 'General search options',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'preferred_method',
|
||||||
|
'label': 'First search',
|
||||||
|
'description': 'Which of the methods do you prefer',
|
||||||
|
'default': 'both',
|
||||||
|
'type': 'dropdown',
|
||||||
|
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}, {
|
||||||
|
'tab': 'searcher',
|
||||||
|
'subtab': 'category',
|
||||||
|
'subtab_label': 'Categories',
|
||||||
|
'name': 'filter',
|
||||||
|
'label': 'Global filters',
|
||||||
|
'description': 'Prefer, ignore & required words in release names',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'preferred_words',
|
||||||
|
'label': 'Preferred',
|
||||||
|
'default': '',
|
||||||
|
'placeholder': 'Example: CtrlHD, Amiable, Wiki',
|
||||||
|
'description': 'Words that give the releases a higher score.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'required_words',
|
||||||
|
'label': 'Required',
|
||||||
|
'default': '',
|
||||||
|
'placeholder': 'Example: DTS, AC3 & English',
|
||||||
|
'description': 'Release should contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'ignored_words',
|
||||||
|
'label': 'Ignored',
|
||||||
|
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs',
|
||||||
|
'description': 'Ignores releases that match any of these sets. (Works like explained above)'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}, {
|
||||||
|
'name': 'nzb',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'name': 'searcher',
|
||||||
|
'label': 'NZB',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'retention',
|
||||||
|
'label': 'Usenet Retention',
|
||||||
|
'default': 1500,
|
||||||
|
'type': 'int',
|
||||||
|
'unit': 'days'
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SearcherBase(Plugin):
|
||||||
|
|
||||||
|
in_progress = False
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(SearcherBase, self).__init__()
|
||||||
|
|
||||||
|
|
||||||
|
addEvent('searcher.progress', self.getProgress)
|
||||||
|
addEvent('%s.searcher.progress' % self.getType(), self.getProgress)
|
||||||
|
|
||||||
|
self.initCron()
|
||||||
|
|
||||||
|
def initCron(self):
|
||||||
|
""" Set the searcher cronjob
|
||||||
|
Make sure to reset cronjob after setting has changed
|
||||||
|
"""
|
||||||
|
|
||||||
|
_type = self.getType()
|
||||||
|
|
||||||
|
def setCrons():
|
||||||
|
|
||||||
|
fireEvent('schedule.cron', '%s.searcher.all' % _type, self.searchAll,
|
||||||
|
day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
|
||||||
|
|
||||||
|
addEvent('app.load', setCrons)
|
||||||
|
addEvent('setting.save.%s_searcher.cron_day.after' % _type, setCrons)
|
||||||
|
addEvent('setting.save.%s_searcher.cron_hour.after' % _type, setCrons)
|
||||||
|
addEvent('setting.save.%s_searcher.cron_minute.after' % _type, setCrons)
|
||||||
|
|
||||||
|
def getProgress(self, **kwargs):
|
||||||
|
""" Return progress of current searcher"""
|
||||||
|
|
||||||
|
progress = {
|
||||||
|
self.getType(): self.in_progress
|
||||||
|
}
|
||||||
|
|
||||||
|
return progress
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
from couchpotato import get_session
|
||||||
|
from couchpotato.api import addApiView
|
||||||
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
|
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||||
|
from couchpotato.core.helpers.variable import md5, getTitle
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.searcher.base import SearcherBase
|
||||||
|
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
from inspect import ismethod, isfunction
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Searcher(SearcherBase):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
addEvent('searcher.protocols', self.getSearchProtocols)
|
||||||
|
addEvent('searcher.contains_other_quality', self.containsOtherQuality)
|
||||||
|
addEvent('searcher.correct_year', self.correctYear)
|
||||||
|
addEvent('searcher.correct_name', self.correctName)
|
||||||
|
addEvent('searcher.download', self.download)
|
||||||
|
|
||||||
|
addApiView('searcher.full_search', self.searchAllView, docs = {
|
||||||
|
'desc': 'Starts a full search for all media',
|
||||||
|
})
|
||||||
|
|
||||||
|
addApiView('searcher.progress', self.getProgressForAll, docs = {
|
||||||
|
'desc': 'Get the progress of all media searches',
|
||||||
|
'return': {'type': 'object', 'example': """{
|
||||||
|
'movie': False || object, total & to_go,
|
||||||
|
'show': False || object, total & to_go,
|
||||||
|
}"""},
|
||||||
|
})
|
||||||
|
|
||||||
|
def searchAllView(self):
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
for _type in fireEvent('media.types'):
|
||||||
|
results[_type] = fireEvent('%s.searcher.all_view' % _type)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
def getProgressForAll(self):
|
||||||
|
progress = fireEvent('searcher.progress', merge = True)
|
||||||
|
return progress
|
||||||
|
|
||||||
|
def download(self, data, movie, manual = False):
|
||||||
|
|
||||||
|
if not data.get('protocol'):
|
||||||
|
data['protocol'] = data['type']
|
||||||
|
data['type'] = 'movie'
|
||||||
|
|
||||||
|
# Test to see if any downloaders are enabled for this type
|
||||||
|
downloader_enabled = fireEvent('download.enabled', manual, data, single = True)
|
||||||
|
|
||||||
|
if downloader_enabled:
|
||||||
|
|
||||||
|
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
||||||
|
|
||||||
|
# Download movie to temp
|
||||||
|
filedata = None
|
||||||
|
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
|
||||||
|
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
|
||||||
|
if filedata == 'try_next':
|
||||||
|
return filedata
|
||||||
|
|
||||||
|
download_result = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
|
||||||
|
log.debug('Downloader result: %s', download_result)
|
||||||
|
|
||||||
|
if download_result:
|
||||||
|
try:
|
||||||
|
# Mark release as snatched
|
||||||
|
db = get_session()
|
||||||
|
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
|
||||||
|
if rls:
|
||||||
|
renamer_enabled = Env.setting('enabled', 'renamer')
|
||||||
|
|
||||||
|
done_status = fireEvent('status.get', 'done', single = True)
|
||||||
|
rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id')
|
||||||
|
|
||||||
|
# Save download-id info if returned
|
||||||
|
if isinstance(download_result, dict):
|
||||||
|
for key in download_result:
|
||||||
|
rls_info = ReleaseInfo(
|
||||||
|
identifier = 'download_%s' % key,
|
||||||
|
value = toUnicode(download_result.get(key))
|
||||||
|
)
|
||||||
|
rls.info.append(rls_info)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
|
||||||
|
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
|
||||||
|
log.info(snatch_message)
|
||||||
|
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
|
||||||
|
|
||||||
|
# If renamer isn't used, mark movie done
|
||||||
|
if not renamer_enabled:
|
||||||
|
active_status = fireEvent('status.get', 'active', single = True)
|
||||||
|
done_status = fireEvent('status.get', 'done', single = True)
|
||||||
|
try:
|
||||||
|
if movie['status_id'] == active_status.get('id'):
|
||||||
|
for profile_type in movie['profile']['types']:
|
||||||
|
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
|
||||||
|
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
|
||||||
|
|
||||||
|
# Mark release done
|
||||||
|
rls.status_id = done_status.get('id')
|
||||||
|
rls.last_edit = int(time.time())
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Mark movie done
|
||||||
|
mvie = db.query(Movie).filter_by(id = movie['id']).first()
|
||||||
|
mvie.status_id = done_status.get('id')
|
||||||
|
mvie.last_edit = int(time.time())
|
||||||
|
db.commit()
|
||||||
|
except:
|
||||||
|
log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed marking movie finished: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', (data.get('protocol')))
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def getSearchProtocols(self):
|
||||||
|
|
||||||
|
download_protocols = fireEvent('download.enabled_protocols', merge = True)
|
||||||
|
provider_protocols = fireEvent('provider.enabled_protocols', merge = True)
|
||||||
|
|
||||||
|
if download_protocols and len(list(set(provider_protocols) & set(download_protocols))) == 0:
|
||||||
|
log.error('There aren\'t any providers enabled for your downloader (%s). Check your settings.', ','.join(download_protocols))
|
||||||
|
return []
|
||||||
|
|
||||||
|
for useless_provider in list(set(provider_protocols) - set(download_protocols)):
|
||||||
|
log.debug('Provider for "%s" enabled, but no downloader.', useless_provider)
|
||||||
|
|
||||||
|
search_protocols = download_protocols
|
||||||
|
|
||||||
|
if len(search_protocols) == 0:
|
||||||
|
log.error('There aren\'t any downloaders enabled. Please pick one in settings.')
|
||||||
|
return []
|
||||||
|
|
||||||
|
return search_protocols
|
||||||
|
|
||||||
|
def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = None):
|
||||||
|
if not preferred_quality: preferred_quality = {}
|
||||||
|
|
||||||
|
name = nzb['name']
|
||||||
|
size = nzb.get('size', 0)
|
||||||
|
nzb_words = re.split('\W+', simplifyString(name))
|
||||||
|
|
||||||
|
qualities = fireEvent('quality.all', single = True)
|
||||||
|
|
||||||
|
found = {}
|
||||||
|
for quality in qualities:
|
||||||
|
# Main in words
|
||||||
|
if quality['identifier'] in nzb_words:
|
||||||
|
found[quality['identifier']] = True
|
||||||
|
|
||||||
|
# Alt in words
|
||||||
|
if list(set(nzb_words) & set(quality['alternative'])):
|
||||||
|
found[quality['identifier']] = True
|
||||||
|
|
||||||
|
# Try guessing via quality tags
|
||||||
|
guess = fireEvent('quality.guess', [nzb.get('name')], single = True)
|
||||||
|
if guess:
|
||||||
|
found[guess['identifier']] = True
|
||||||
|
|
||||||
|
# Hack for older movies that don't contain quality tag
|
||||||
|
year_name = fireEvent('scanner.name_year', name, single = True)
|
||||||
|
if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None):
|
||||||
|
if size > 3000: # Assume dvdr
|
||||||
|
log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', size)
|
||||||
|
found['dvdr'] = True
|
||||||
|
else: # Assume dvdrip
|
||||||
|
log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', size)
|
||||||
|
found['dvdrip'] = True
|
||||||
|
|
||||||
|
# Allow other qualities
|
||||||
|
for allowed in preferred_quality.get('allow'):
|
||||||
|
if found.get(allowed):
|
||||||
|
del found[allowed]
|
||||||
|
|
||||||
|
return not (found.get(preferred_quality['identifier']) and len(found) == 1)
|
||||||
|
|
||||||
|
def correctYear(self, haystack, year, year_range):
|
||||||
|
|
||||||
|
if not isinstance(haystack, (list, tuple, set)):
|
||||||
|
haystack = [haystack]
|
||||||
|
|
||||||
|
year_name = {}
|
||||||
|
for string in haystack:
|
||||||
|
|
||||||
|
year_name = fireEvent('scanner.name_year', string, single = True)
|
||||||
|
|
||||||
|
if year_name and ((year - year_range) <= year_name.get('year') <= (year + year_range)):
|
||||||
|
log.debug('Movie year matches range: %s looking for %s', (year_name.get('year'), year))
|
||||||
|
return True
|
||||||
|
|
||||||
|
log.debug('Movie year doesn\'t matche range: %s looking for %s', (year_name.get('year'), year))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def correctName(self, check_name, movie_name):
|
||||||
|
|
||||||
|
check_names = [check_name]
|
||||||
|
|
||||||
|
# Match names between "
|
||||||
|
try: check_names.append(re.search(r'([\'"])[^\1]*\1', check_name).group(0))
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
# Match longest name between []
|
||||||
|
try: check_names.append(max(check_name.split('['), key = len))
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
for check_name in list(set(check_names)):
|
||||||
|
check_movie = fireEvent('scanner.name_year', check_name, single = True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
check_words = filter(None, re.split('\W+', check_movie.get('name', '')))
|
||||||
|
movie_words = filter(None, re.split('\W+', simplifyString(movie_name)))
|
||||||
|
|
||||||
|
if len(check_words) > 0 and len(movie_words) > 0 and len(list(set(check_words) - set(movie_words))) == 0:
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
class SearchSetupError(Exception):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from couchpotato.core.media import MediaBase
|
||||||
|
|
||||||
|
|
||||||
|
class MovieTypeBase(MediaBase):
|
||||||
|
|
||||||
|
_type = 'movie'
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from .main import MovieBase
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return MovieBase()
|
||||||
|
|
||||||
|
config = []
|
||||||
+256
-123
@@ -2,30 +2,38 @@ from couchpotato import get_session
|
|||||||
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, simplifyString
|
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||||
from couchpotato.core.helpers.request import getParams, jsonified, getParam
|
from couchpotato.core.helpers.variable import getImdb, splitString, tryInt, \
|
||||||
from couchpotato.core.helpers.variable import getImdb, splitString
|
mergeDicts
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.media.movie import MovieTypeBase
|
||||||
from couchpotato.core.settings.model import Library, LibraryTitle, Movie
|
from couchpotato.core.settings.model import Library, LibraryTitle, Movie, \
|
||||||
|
Release
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from sqlalchemy.orm import joinedload_all
|
from sqlalchemy.orm import joinedload_all
|
||||||
from sqlalchemy.sql.expression import or_, asc, not_
|
from sqlalchemy.sql.expression import or_, asc, not_, desc
|
||||||
from string import ascii_lowercase
|
from string import ascii_lowercase
|
||||||
|
import time
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class MoviePlugin(Plugin):
|
class MovieBase(MovieTypeBase):
|
||||||
|
|
||||||
default_dict = {
|
default_dict = {
|
||||||
'profile': {'types': {'quality': {}}},
|
'profile': {'types': {'quality': {}}},
|
||||||
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
|
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
|
||||||
'library': {'titles': {}, 'files':{}},
|
'library': {'titles': {}, 'files':{}},
|
||||||
'files': {},
|
'files': {},
|
||||||
'status': {}
|
'status': {},
|
||||||
|
'category': {},
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
|
# Initialize this type
|
||||||
|
super(MovieBase, self).__init__()
|
||||||
|
self.initType()
|
||||||
|
|
||||||
addApiView('movie.search', self.search, docs = {
|
addApiView('movie.search', self.search, docs = {
|
||||||
'desc': 'Search the movie providers for a movie',
|
'desc': 'Search the movie providers for a movie',
|
||||||
'params': {
|
'params': {
|
||||||
@@ -41,6 +49,7 @@ class MoviePlugin(Plugin):
|
|||||||
'desc': 'List movies in wanted list',
|
'desc': 'List movies in wanted list',
|
||||||
'params': {
|
'params': {
|
||||||
'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'},
|
'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'},
|
||||||
|
'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'},
|
||||||
'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'},
|
'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'},
|
||||||
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
|
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
|
||||||
'search': {'desc': 'Search movie title'},
|
'search': {'desc': 'Search movie title'},
|
||||||
@@ -94,15 +103,42 @@ class MoviePlugin(Plugin):
|
|||||||
addEvent('movie.list', self.list)
|
addEvent('movie.list', self.list)
|
||||||
addEvent('movie.restatus', self.restatus)
|
addEvent('movie.restatus', self.restatus)
|
||||||
|
|
||||||
def getView(self):
|
# Clean releases that didn't have activity in the last week
|
||||||
|
addEvent('app.load', self.cleanReleases)
|
||||||
|
fireEvent('schedule.interval', 'movie.clean_releases', self.cleanReleases, hours = 4)
|
||||||
|
|
||||||
movie_id = getParam('id')
|
def cleanReleases(self):
|
||||||
movie = self.get(movie_id) if movie_id else None
|
|
||||||
|
|
||||||
return jsonified({
|
log.debug('Removing releases from dashboard')
|
||||||
|
|
||||||
|
now = time.time()
|
||||||
|
week = 262080
|
||||||
|
|
||||||
|
done_status, available_status, snatched_status = \
|
||||||
|
fireEvent('status.get', ['done', 'available', 'snatched'], single = True)
|
||||||
|
|
||||||
|
db = get_session()
|
||||||
|
|
||||||
|
# get movies last_edit more than a week ago
|
||||||
|
movies = db.query(Movie) \
|
||||||
|
.filter(Movie.status_id == done_status.get('id'), Movie.last_edit < (now - week)) \
|
||||||
|
.all()
|
||||||
|
|
||||||
|
for movie in movies:
|
||||||
|
for rel in movie.releases:
|
||||||
|
if rel.status_id in [available_status.get('id'), snatched_status.get('id')]:
|
||||||
|
fireEvent('release.delete', id = rel.id, single = True)
|
||||||
|
|
||||||
|
db.expire_all()
|
||||||
|
|
||||||
|
def getView(self, id = None, **kwargs):
|
||||||
|
|
||||||
|
movie = self.get(id) if id else None
|
||||||
|
|
||||||
|
return {
|
||||||
'success': movie is not None,
|
'success': movie is not None,
|
||||||
'movie': movie,
|
'movie': movie,
|
||||||
})
|
}
|
||||||
|
|
||||||
def get(self, movie_id):
|
def get(self, movie_id):
|
||||||
|
|
||||||
@@ -110,7 +146,7 @@ class MoviePlugin(Plugin):
|
|||||||
|
|
||||||
imdb_id = getImdb(str(movie_id))
|
imdb_id = getImdb(str(movie_id))
|
||||||
|
|
||||||
if(imdb_id):
|
if imdb_id:
|
||||||
m = db.query(Movie).filter(Movie.library.has(identifier = imdb_id)).first()
|
m = db.query(Movie).filter(Movie.library.has(identifier = imdb_id)).first()
|
||||||
else:
|
else:
|
||||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||||
@@ -119,24 +155,46 @@ class MoviePlugin(Plugin):
|
|||||||
if m:
|
if m:
|
||||||
results = m.to_dict(self.default_dict)
|
results = m.to_dict(self.default_dict)
|
||||||
|
|
||||||
|
db.expire_all()
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def list(self, status = ['active'], limit_offset = None, starts_with = None, search = None):
|
def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
|
||||||
|
|
||||||
db = get_session()
|
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)):
|
||||||
|
release_status = [release_status]
|
||||||
|
|
||||||
|
# query movie ids
|
||||||
q = db.query(Movie) \
|
q = db.query(Movie) \
|
||||||
.join(Movie.library, Library.titles) \
|
.with_entities(Movie.id) \
|
||||||
.filter(LibraryTitle.default == True) \
|
|
||||||
.filter(or_(*[Movie.status.has(identifier = s) for s in status])) \
|
|
||||||
.group_by(Movie.id)
|
.group_by(Movie.id)
|
||||||
|
|
||||||
total_count = q.count()
|
# Filter on movie status
|
||||||
|
if status and len(status) > 0:
|
||||||
|
statuses = fireEvent('status.get', status, single = len(status) > 1)
|
||||||
|
statuses = [s.get('id') for s in statuses]
|
||||||
|
|
||||||
|
q = q.filter(Movie.status_id.in_(statuses))
|
||||||
|
|
||||||
|
# Filter on release status
|
||||||
|
if release_status and len(release_status) > 0:
|
||||||
|
q = q.join(Movie.releases)
|
||||||
|
|
||||||
|
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||||
|
statuses = [s.get('id') for s in statuses]
|
||||||
|
|
||||||
|
q = q.filter(Release.status_id.in_(statuses))
|
||||||
|
|
||||||
|
# Only join when searching / ordering
|
||||||
|
if starts_with or search or order != 'release_order':
|
||||||
|
q = q.join(Movie.library, Library.titles) \
|
||||||
|
.filter(LibraryTitle.default == True)
|
||||||
|
|
||||||
|
# Add search filters
|
||||||
filter_or = []
|
filter_or = []
|
||||||
if starts_with:
|
if starts_with:
|
||||||
starts_with = toUnicode(starts_with.lower())
|
starts_with = toUnicode(starts_with.lower())
|
||||||
@@ -151,101 +209,170 @@ class MoviePlugin(Plugin):
|
|||||||
if search:
|
if search:
|
||||||
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
|
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
|
||||||
|
|
||||||
if filter_or:
|
if len(filter_or) > 0:
|
||||||
q = q.filter(or_(*filter_or))
|
q = q.filter(or_(*filter_or))
|
||||||
|
|
||||||
q = q.order_by(asc(LibraryTitle.simple_title))
|
total_count = q.count()
|
||||||
|
if total_count == 0:
|
||||||
|
return 0, []
|
||||||
|
|
||||||
q = q.subquery()
|
if order == 'release_order':
|
||||||
q2 = db.query(Movie).join((q, q.c.id == Movie.id)) \
|
q = q.order_by(desc(Release.last_edit))
|
||||||
.options(joinedload_all('releases')) \
|
else:
|
||||||
.options(joinedload_all('profile.types')) \
|
q = q.order_by(asc(LibraryTitle.simple_title))
|
||||||
|
|
||||||
|
if limit_offset:
|
||||||
|
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
|
||||||
|
limit = splt[0]
|
||||||
|
offset = 0 if len(splt) is 1 else splt[1]
|
||||||
|
q = q.limit(limit).offset(offset)
|
||||||
|
|
||||||
|
# Get all movie_ids in sorted order
|
||||||
|
movie_ids = [m.id for m in q.all()]
|
||||||
|
|
||||||
|
# List release statuses
|
||||||
|
releases = db.query(Release) \
|
||||||
|
.filter(Release.movie_id.in_(movie_ids)) \
|
||||||
|
.all()
|
||||||
|
|
||||||
|
release_statuses = dict((m, set()) for m in movie_ids)
|
||||||
|
releases_count = dict((m, 0) for m in movie_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(Movie) \
|
||||||
.options(joinedload_all('library.titles')) \
|
.options(joinedload_all('library.titles')) \
|
||||||
.options(joinedload_all('library.files')) \
|
.options(joinedload_all('library.files')) \
|
||||||
.options(joinedload_all('status')) \
|
.options(joinedload_all('status')) \
|
||||||
.options(joinedload_all('files'))
|
.options(joinedload_all('files'))
|
||||||
|
|
||||||
if limit_offset:
|
q2 = q2.filter(Movie.id.in_(movie_ids))
|
||||||
splt = splitString(limit_offset)
|
|
||||||
limit = splt[0]
|
|
||||||
offset = 0 if len(splt) is 1 else splt[1]
|
|
||||||
q2 = q2.limit(limit).offset(offset)
|
|
||||||
|
|
||||||
results = q2.all()
|
results = q2.all()
|
||||||
movies = []
|
|
||||||
|
# Create dict by movie id
|
||||||
|
movie_dict = {}
|
||||||
for movie in results:
|
for movie in results:
|
||||||
temp = movie.to_dict({
|
movie_dict[movie.id] = movie
|
||||||
'profile': {'types': {}},
|
|
||||||
'releases': {'files':{}, 'info': {}},
|
# List movies based on movie_ids order
|
||||||
|
movies = []
|
||||||
|
for movie_id in movie_ids:
|
||||||
|
|
||||||
|
releases = []
|
||||||
|
for r in release_statuses.get(movie_id):
|
||||||
|
x = splitString(r)
|
||||||
|
releases.append({'status_id': x[0], 'quality_id': x[1]})
|
||||||
|
|
||||||
|
# Merge releases with movie dict
|
||||||
|
movies.append(mergeDicts(movie_dict[movie_id].to_dict({
|
||||||
'library': {'titles': {}, 'files':{}},
|
'library': {'titles': {}, 'files':{}},
|
||||||
'files': {},
|
'files': {},
|
||||||
})
|
}), {
|
||||||
movies.append(temp)
|
'releases': releases,
|
||||||
|
'releases_count': releases_count.get(movie_id),
|
||||||
|
}))
|
||||||
|
|
||||||
#db.close()
|
db.expire_all()
|
||||||
return (total_count, movies)
|
return total_count, movies
|
||||||
|
|
||||||
def availableChars(self, status = ['active']):
|
def availableChars(self, status = None, release_status = None):
|
||||||
|
|
||||||
chars = ''
|
status = status or []
|
||||||
|
release_status = release_status or []
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
# Make a list from string
|
# Make a list from string
|
||||||
if not isinstance(status, (list, tuple)):
|
if not isinstance(status, (list, tuple)):
|
||||||
status = [status]
|
status = [status]
|
||||||
|
if release_status and not isinstance(release_status, (list, tuple)):
|
||||||
|
release_status = [release_status]
|
||||||
|
|
||||||
q = db.query(Movie) \
|
q = db.query(Movie)
|
||||||
.join(Movie.library, Library.titles, Movie.status) \
|
|
||||||
.options(joinedload_all('library.titles')) \
|
|
||||||
.filter(or_(*[Movie.status.has(identifier = s) for s in status]))
|
|
||||||
|
|
||||||
results = q.all()
|
# Filter on movie status
|
||||||
|
if status and len(status) > 0:
|
||||||
|
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
|
||||||
|
statuses = [s.get('id') for s in statuses]
|
||||||
|
|
||||||
for movie in results:
|
q = q.filter(Movie.status_id.in_(statuses))
|
||||||
char = movie.library.titles[0].simple_title[0]
|
|
||||||
char = char if char in ascii_lowercase else '#'
|
|
||||||
if char not in chars:
|
|
||||||
chars += char
|
|
||||||
|
|
||||||
#db.close()
|
# Filter on release status
|
||||||
return chars
|
if release_status and len(release_status) > 0:
|
||||||
|
|
||||||
def listView(self):
|
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||||
|
statuses = [s.get('id') for s in statuses]
|
||||||
|
|
||||||
params = getParams()
|
q = q.join(Movie.releases) \
|
||||||
status = params.get('status', ['active'])
|
.filter(Release.status_id.in_(statuses))
|
||||||
limit_offset = params.get('limit_offset', None)
|
|
||||||
starts_with = params.get('starts_with', None)
|
|
||||||
search = params.get('search', None)
|
|
||||||
|
|
||||||
total_movies, movies = self.list(status = status, limit_offset = limit_offset, starts_with = starts_with, search = search)
|
q = q.join(Library, LibraryTitle) \
|
||||||
|
.with_entities(LibraryTitle.simple_title) \
|
||||||
|
.filter(LibraryTitle.default == True)
|
||||||
|
|
||||||
return jsonified({
|
titles = q.all()
|
||||||
|
|
||||||
|
chars = set()
|
||||||
|
for title in titles:
|
||||||
|
try:
|
||||||
|
char = title[0][0]
|
||||||
|
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:
|
||||||
|
break
|
||||||
|
|
||||||
|
db.expire_all()
|
||||||
|
return ''.join(sorted(chars))
|
||||||
|
|
||||||
|
def listView(self, **kwargs):
|
||||||
|
|
||||||
|
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(
|
||||||
|
status = status,
|
||||||
|
release_status = release_status,
|
||||||
|
limit_offset = limit_offset,
|
||||||
|
starts_with = starts_with,
|
||||||
|
search = search,
|
||||||
|
order = order
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'empty': len(movies) == 0,
|
'empty': len(movies) == 0,
|
||||||
'total': total_movies,
|
'total': total_movies,
|
||||||
'movies': movies,
|
'movies': movies,
|
||||||
})
|
}
|
||||||
|
|
||||||
def charView(self):
|
def charView(self, **kwargs):
|
||||||
|
|
||||||
params = getParams()
|
status = splitString(kwargs.get('status', None))
|
||||||
status = params.get('status', ['active'])
|
release_status = splitString(kwargs.get('release_status', None))
|
||||||
chars = self.availableChars(status)
|
chars = self.availableChars(status, release_status)
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'empty': len(chars) == 0,
|
'empty': len(chars) == 0,
|
||||||
'chars': chars,
|
'chars': chars,
|
||||||
})
|
}
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self, id = '', **kwargs):
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
for id in splitString(getParam('id')):
|
for x in splitString(id):
|
||||||
movie = db.query(Movie).filter_by(id = id).first()
|
movie = db.query(Movie).filter_by(id = x).first()
|
||||||
|
|
||||||
if movie:
|
if movie:
|
||||||
|
|
||||||
@@ -254,18 +381,16 @@ class MoviePlugin(Plugin):
|
|||||||
for title in movie.library.titles:
|
for title in movie.library.titles:
|
||||||
if title.default: default_title = title.title
|
if title.default: default_title = title.title
|
||||||
|
|
||||||
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True, message = 'Updating "%s"' % default_title)
|
fireEvent('notify.frontend', type = 'movie.busy.%s' % x, data = True)
|
||||||
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id))
|
fireEventAsync('library.update.movie', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
|
||||||
|
|
||||||
|
db.expire_all()
|
||||||
#db.close()
|
return {
|
||||||
return jsonified({
|
|
||||||
'success': True,
|
'success': True,
|
||||||
})
|
}
|
||||||
|
|
||||||
def search(self):
|
def search(self, q = '', **kwargs):
|
||||||
|
|
||||||
q = getParam('q')
|
|
||||||
cache_key = u'%s/%s' % (__name__, simplifyString(q))
|
cache_key = u'%s/%s' % (__name__, simplifyString(q))
|
||||||
movies = Env.get('cache').get(cache_key)
|
movies = Env.get('cache').get(cache_key)
|
||||||
|
|
||||||
@@ -277,13 +402,14 @@ class MoviePlugin(Plugin):
|
|||||||
movies = fireEvent('movie.search', q = q, merge = True)
|
movies = fireEvent('movie.search', q = q, merge = True)
|
||||||
Env.get('cache').set(cache_key, movies)
|
Env.get('cache').set(cache_key, movies)
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'empty': len(movies) == 0 if movies else 0,
|
'empty': len(movies) == 0 if movies else 0,
|
||||||
'movies': movies,
|
'movies': movies,
|
||||||
})
|
}
|
||||||
|
|
||||||
def add(self, params = {}, force_readd = True, search_after = True, update_library = False):
|
def add(self, params = None, force_readd = True, search_after = True, update_library = False, status_id = None):
|
||||||
|
if not params: params = {}
|
||||||
|
|
||||||
if not params.get('identifier'):
|
if not params.get('identifier'):
|
||||||
msg = 'Can\'t add movie without imdb identifier.'
|
msg = 'Can\'t add movie without imdb identifier.'
|
||||||
@@ -292,9 +418,8 @@ class MoviePlugin(Plugin):
|
|||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
url = 'http://thetvdb.com/api/GetSeriesByRemoteID.php?imdbid=%s' % params.get('identifier')
|
is_movie = fireEvent('movie.is_movie', identifier = params.get('identifier'), single = True)
|
||||||
tvdb = self.getCache('thetvdb.%s' % params.get('identifier'), url = url, show_error = False)
|
if not is_movie:
|
||||||
if tvdb and 'series' in tvdb.lower():
|
|
||||||
msg = 'Can\'t add movie, seems to be a TV show.'
|
msg = 'Can\'t add movie, seems to be a TV show.'
|
||||||
log.error(msg)
|
log.error(msg)
|
||||||
fireEvent('notify.frontend', type = 'movie.is_tvshow', message = msg)
|
fireEvent('notify.frontend', type = 'movie.is_tvshow', message = msg)
|
||||||
@@ -303,23 +428,26 @@ class MoviePlugin(Plugin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
library = fireEvent('library.add', single = True, attrs = params, update_after = update_library)
|
library = fireEvent('library.add.movie', single = True, attrs = params, update_after = update_library)
|
||||||
|
|
||||||
# Status
|
# Status
|
||||||
status_active = fireEvent('status.add', 'active', single = True)
|
status_active, snatched_status, ignored_status, done_status, downloaded_status = \
|
||||||
status_snatched = fireEvent('status.add', 'snatched', single = True)
|
fireEvent('status.get', ['active', 'snatched', 'ignored', 'done', 'downloaded'], single = True)
|
||||||
|
|
||||||
default_profile = fireEvent('profile.default', single = True)
|
default_profile = fireEvent('profile.default', single = True)
|
||||||
|
cat_id = params.get('category_id')
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
m = db.query(Movie).filter_by(library_id = library.get('id')).first()
|
m = db.query(Movie).filter_by(library_id = library.get('id')).first()
|
||||||
added = True
|
added = True
|
||||||
do_search = False
|
do_search = False
|
||||||
|
search_after = search_after and self.conf('search_on_add', section = 'moviesearcher')
|
||||||
if not m:
|
if not m:
|
||||||
m = Movie(
|
m = Movie(
|
||||||
library_id = library.get('id'),
|
library_id = library.get('id'),
|
||||||
profile_id = params.get('profile_id', default_profile.get('id')),
|
profile_id = params.get('profile_id', default_profile.get('id')),
|
||||||
status_id = status_active.get('id'),
|
status_id = status_id if status_id else status_active.get('id'),
|
||||||
|
category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None,
|
||||||
)
|
)
|
||||||
db.add(m)
|
db.add(m)
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -328,21 +456,27 @@ class MoviePlugin(Plugin):
|
|||||||
if search_after:
|
if search_after:
|
||||||
onComplete = self.createOnComplete(m.id)
|
onComplete = self.createOnComplete(m.id)
|
||||||
|
|
||||||
fireEventAsync('library.update', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
|
fireEventAsync('library.update.movie', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
|
||||||
search_after = False
|
search_after = False
|
||||||
elif force_readd:
|
elif force_readd:
|
||||||
|
|
||||||
# Clean snatched history
|
# Clean snatched history
|
||||||
for release in m.releases:
|
for release in m.releases:
|
||||||
if release.status_id == status_snatched.get('id'):
|
if release.status_id in [downloaded_status.get('id'), snatched_status.get('id'), done_status.get('id')]:
|
||||||
release.delete()
|
if params.get('ignore_previous', False):
|
||||||
|
release.status_id = ignored_status.get('id')
|
||||||
|
else:
|
||||||
|
fireEvent('release.delete', release.id, single = True)
|
||||||
|
|
||||||
m.profile_id = params.get('profile_id', default_profile.get('id'))
|
m.profile_id = params.get('profile_id', default_profile.get('id'))
|
||||||
|
m.category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else (m.category_id or None)
|
||||||
else:
|
else:
|
||||||
log.debug('Movie already exists, not updating: %s', params)
|
log.debug('Movie already exists, not updating: %s', params)
|
||||||
added = False
|
added = False
|
||||||
|
|
||||||
if force_readd:
|
if force_readd:
|
||||||
m.status_id = status_active.get('id')
|
m.status_id = status_id if status_id else status_active.get('id')
|
||||||
|
m.last_edit = int(time.time())
|
||||||
do_search = True
|
do_search = True
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
@@ -363,37 +497,38 @@ class MoviePlugin(Plugin):
|
|||||||
if added:
|
if added:
|
||||||
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', ''))
|
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', ''))
|
||||||
|
|
||||||
#db.close()
|
db.expire_all()
|
||||||
return movie_dict
|
return movie_dict
|
||||||
|
|
||||||
|
|
||||||
def addView(self):
|
def addView(self, **kwargs):
|
||||||
|
|
||||||
params = getParams()
|
movie_dict = self.add(params = kwargs)
|
||||||
|
|
||||||
movie_dict = self.add(params)
|
return {
|
||||||
|
|
||||||
return jsonified({
|
|
||||||
'success': True,
|
'success': True,
|
||||||
'added': True if movie_dict else False,
|
'added': True if movie_dict else False,
|
||||||
'movie': movie_dict,
|
'movie': movie_dict,
|
||||||
})
|
}
|
||||||
|
|
||||||
def edit(self):
|
def edit(self, id = '', **kwargs):
|
||||||
|
|
||||||
params = getParams()
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
available_status = fireEvent('status.get', 'available', single = True)
|
available_status = fireEvent('status.get', 'available', single = True)
|
||||||
|
|
||||||
ids = splitString(params.get('id'))
|
ids = splitString(id)
|
||||||
for movie_id in ids:
|
for movie_id in ids:
|
||||||
|
|
||||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||||
if not m:
|
if not m:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
m.profile_id = params.get('profile_id')
|
m.profile_id = kwargs.get('profile_id')
|
||||||
|
|
||||||
|
cat_id = kwargs.get('category_id')
|
||||||
|
if cat_id is not None:
|
||||||
|
m.category_id = tryInt(cat_id) if tryInt(cat_id) > 0 else None
|
||||||
|
|
||||||
# Remove releases
|
# Remove releases
|
||||||
for rel in m.releases:
|
for rel in m.releases:
|
||||||
@@ -402,33 +537,31 @@ class MoviePlugin(Plugin):
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Default title
|
# Default title
|
||||||
if params.get('default_title'):
|
if kwargs.get('default_title'):
|
||||||
for title in m.library.titles:
|
for title in m.library.titles:
|
||||||
title.default = toUnicode(params.get('default_title', '')).lower() == toUnicode(title.title).lower()
|
title.default = toUnicode(kwargs.get('default_title', '')).lower() == toUnicode(title.title).lower()
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
fireEvent('movie.restatus', m.id)
|
fireEvent('movie.restatus', m.id)
|
||||||
|
|
||||||
movie_dict = m.to_dict(self.default_dict)
|
movie_dict = m.to_dict(self.default_dict)
|
||||||
fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
|
fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
|
||||||
|
|
||||||
#db.close()
|
db.expire_all()
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
})
|
}
|
||||||
|
|
||||||
def deleteView(self):
|
def deleteView(self, id = '', **kwargs):
|
||||||
|
|
||||||
params = getParams()
|
ids = splitString(id)
|
||||||
|
|
||||||
ids = splitString(params.get('id'))
|
|
||||||
for movie_id in ids:
|
for movie_id in ids:
|
||||||
self.delete(movie_id, delete_from = params.get('delete_from', 'all'))
|
self.delete(movie_id, delete_from = kwargs.get('delete_from', 'all'))
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
})
|
}
|
||||||
|
|
||||||
def delete(self, movie_id, delete_from = None):
|
def delete(self, movie_id, delete_from = None):
|
||||||
|
|
||||||
@@ -448,7 +581,7 @@ class MoviePlugin(Plugin):
|
|||||||
total_deleted = 0
|
total_deleted = 0
|
||||||
new_movie_status = None
|
new_movie_status = None
|
||||||
for release in movie.releases:
|
for release in movie.releases:
|
||||||
if delete_from == 'wanted':
|
if delete_from in ['wanted', 'snatched', 'late']:
|
||||||
if release.status_id != done_status.get('id'):
|
if release.status_id != done_status.get('id'):
|
||||||
db.delete(release)
|
db.delete(release)
|
||||||
total_deleted += 1
|
total_deleted += 1
|
||||||
@@ -475,13 +608,12 @@ class MoviePlugin(Plugin):
|
|||||||
if deleted:
|
if deleted:
|
||||||
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
|
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
|
||||||
|
|
||||||
#db.close()
|
db.expire_all()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def restatus(self, movie_id):
|
def restatus(self, movie_id):
|
||||||
|
|
||||||
active_status = fireEvent('status.get', 'active', single = True)
|
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
|
||||||
done_status = fireEvent('status.get', 'done', single = True)
|
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
@@ -490,7 +622,7 @@ class MoviePlugin(Plugin):
|
|||||||
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
|
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
log.debug('Changing status for %s', (m.library.titles[0].title))
|
log.debug('Changing status for %s', m.library.titles[0].title)
|
||||||
if not m.profile:
|
if not m.profile:
|
||||||
m.status_id = done_status.get('id')
|
m.status_id = done_status.get('id')
|
||||||
else:
|
else:
|
||||||
@@ -504,7 +636,6 @@ class MoviePlugin(Plugin):
|
|||||||
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
|
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
#db.close()
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -513,7 +644,8 @@ class MoviePlugin(Plugin):
|
|||||||
def onComplete():
|
def onComplete():
|
||||||
db = get_session()
|
db = get_session()
|
||||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
movie = db.query(Movie).filter_by(id = movie_id).first()
|
||||||
fireEventAsync('searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id))
|
fireEventAsync('movie.searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id))
|
||||||
|
db.expire_all()
|
||||||
|
|
||||||
return onComplete
|
return onComplete
|
||||||
|
|
||||||
@@ -524,5 +656,6 @@ class MoviePlugin(Plugin):
|
|||||||
db = get_session()
|
db = get_session()
|
||||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
movie = db.query(Movie).filter_by(id = movie_id).first()
|
||||||
fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
|
fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
|
||||||
|
db.expire_all()
|
||||||
|
|
||||||
return notifyFront
|
return notifyFront
|
||||||
+224
-124
@@ -1,36 +1,55 @@
|
|||||||
var MovieList = new Class({
|
var MovieList = new Class({
|
||||||
|
|
||||||
Implements: [Options],
|
Implements: [Events, Options],
|
||||||
|
|
||||||
options: {
|
options: {
|
||||||
navigation: true,
|
navigation: true,
|
||||||
limit: 50,
|
limit: 50,
|
||||||
|
load_more: true,
|
||||||
|
loader: true,
|
||||||
menu: [],
|
menu: [],
|
||||||
add_new: false
|
add_new: false,
|
||||||
|
force_view: false
|
||||||
},
|
},
|
||||||
|
|
||||||
movies: [],
|
movies: [],
|
||||||
movies_added: {},
|
movies_added: {},
|
||||||
|
total_movies: 0,
|
||||||
letters: {},
|
letters: {},
|
||||||
filter: {
|
filter: null,
|
||||||
'startswith': null,
|
|
||||||
'search': null
|
|
||||||
},
|
|
||||||
|
|
||||||
initialize: function(options){
|
initialize: function(options){
|
||||||
var self = this;
|
var self = this;
|
||||||
self.setOptions(options);
|
self.setOptions(options);
|
||||||
|
|
||||||
self.offset = 0;
|
self.offset = 0;
|
||||||
|
self.filter = self.options.filter || {
|
||||||
|
'starts_with': null,
|
||||||
|
'search': null
|
||||||
|
}
|
||||||
|
|
||||||
self.el = new Element('div.movies').adopt(
|
self.el = new Element('div.movies').adopt(
|
||||||
|
self.title = self.options.title ? new Element('h2', {
|
||||||
|
'text': self.options.title,
|
||||||
|
'styles': {'display': 'none'}
|
||||||
|
}) : null,
|
||||||
|
self.description = self.options.description ? new Element('div.description', {
|
||||||
|
'html': self.options.description,
|
||||||
|
'styles': {'display': 'none'}
|
||||||
|
}) : null,
|
||||||
self.movie_list = new Element('div'),
|
self.movie_list = new Element('div'),
|
||||||
self.load_more = new Element('a.load_more', {
|
self.load_more = self.options.load_more ? new Element('a.load_more', {
|
||||||
'events': {
|
'events': {
|
||||||
'click': self.loadMore.bind(self)
|
'click': self.loadMore.bind(self)
|
||||||
}
|
}
|
||||||
})
|
}) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if($(window).getSize().x <= 480 && !self.options.force_view)
|
||||||
|
self.changeView('list');
|
||||||
|
else
|
||||||
|
self.changeView(self.getSavedView() || self.options.view || 'details');
|
||||||
|
|
||||||
self.getMovies();
|
self.getMovies();
|
||||||
|
|
||||||
App.addEvent('movie.added', self.movieAdded.bind(self))
|
App.addEvent('movie.added', self.movieAdded.bind(self))
|
||||||
@@ -44,7 +63,8 @@ var MovieList = new Class({
|
|||||||
self.movies.each(function(movie){
|
self.movies.each(function(movie){
|
||||||
if(movie.get('id') == notification.data.id){
|
if(movie.get('id') == notification.data.id){
|
||||||
movie.destroy();
|
movie.destroy();
|
||||||
delete self.movies_added[notification.data.id]
|
delete self.movies_added[notification.data.id];
|
||||||
|
self.setCounter(self.counter_count-1);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -58,6 +78,7 @@ var MovieList = new Class({
|
|||||||
if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){
|
if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){
|
||||||
window.scroll(0,0);
|
window.scroll(0,0);
|
||||||
self.createMovie(notification.data, 'top');
|
self.createMovie(notification.data, 'top');
|
||||||
|
self.setCounter(self.counter_count+1);
|
||||||
|
|
||||||
self.checkIfEmpty();
|
self.checkIfEmpty();
|
||||||
}
|
}
|
||||||
@@ -70,22 +91,14 @@ var MovieList = new Class({
|
|||||||
if(self.options.navigation)
|
if(self.options.navigation)
|
||||||
self.createNavigation();
|
self.createNavigation();
|
||||||
|
|
||||||
self.movie_list.addEvents({
|
if(self.options.load_more)
|
||||||
'mouseenter:relay(.movie)': function(e, el){
|
self.scrollspy = new ScrollSpy({
|
||||||
el.addClass('hover');
|
min: function(){
|
||||||
},
|
var c = self.load_more.getCoordinates()
|
||||||
'mouseleave:relay(.movie)': function(e, el){
|
return c.top - window.document.getSize().y - 300
|
||||||
el.removeClass('hover');
|
},
|
||||||
}
|
onEnter: self.loadMore.bind(self)
|
||||||
});
|
});
|
||||||
|
|
||||||
self.scrollspy = new ScrollSpy({
|
|
||||||
min: function(){
|
|
||||||
var c = self.load_more.getCoordinates()
|
|
||||||
return c.top - window.document.getSize().y - 300
|
|
||||||
},
|
|
||||||
onEnter: self.loadMore.bind(self)
|
|
||||||
});
|
|
||||||
|
|
||||||
self.created = true;
|
self.created = true;
|
||||||
},
|
},
|
||||||
@@ -96,7 +109,7 @@ var MovieList = new Class({
|
|||||||
if(!self.created) self.create();
|
if(!self.created) self.create();
|
||||||
|
|
||||||
// do scrollspy
|
// do scrollspy
|
||||||
if(movies.length < self.options.limit){
|
if(movies.length < self.options.limit && self.scrollspy){
|
||||||
self.load_more.hide();
|
self.load_more.hide();
|
||||||
self.scrollspy.stop();
|
self.scrollspy.stop();
|
||||||
}
|
}
|
||||||
@@ -105,7 +118,7 @@ var MovieList = new Class({
|
|||||||
self.createMovie(movie);
|
self.createMovie(movie);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.total_movies = total;
|
self.total_movies += total;
|
||||||
self.setCounter(total);
|
self.setCounter(total);
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -115,24 +128,53 @@ var MovieList = new Class({
|
|||||||
|
|
||||||
if(!self.navigation_counter) return;
|
if(!self.navigation_counter) return;
|
||||||
|
|
||||||
self.navigation_counter.set('text', (count || 0));
|
self.counter_count = count;
|
||||||
|
self.navigation_counter.set('text', (count || 0) + ' movies');
|
||||||
|
|
||||||
|
if (self.empty_message) {
|
||||||
|
self.empty_message.destroy();
|
||||||
|
self.empty_message = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(self.total_movies && count == 0 && !self.empty_message){
|
||||||
|
var message = (self.filter.search ? 'for "'+self.filter.search+'"' : '') +
|
||||||
|
(self.filter.starts_with ? ' in <strong>'+self.filter.starts_with+'</strong>' : '');
|
||||||
|
|
||||||
|
self.empty_message = new Element('.message', {
|
||||||
|
'html': 'No movies found ' + message + '.<br/>'
|
||||||
|
}).grab(
|
||||||
|
new Element('a', {
|
||||||
|
'text': 'Reset filter',
|
||||||
|
'events': {
|
||||||
|
'click': function(){
|
||||||
|
self.filter = {
|
||||||
|
'starts_with': null,
|
||||||
|
'search': null
|
||||||
|
};
|
||||||
|
self.navigation_search_input.set('value', '');
|
||||||
|
self.reset();
|
||||||
|
self.activateLetter();
|
||||||
|
self.getMovies(true);
|
||||||
|
self.last_search_value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).inject(self.movie_list);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
createMovie: function(movie, inject_at){
|
createMovie: function(movie, inject_at){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
// Attach proper actions
|
|
||||||
var a = self.options.actions,
|
|
||||||
status = Status.get(movie.status_id);
|
|
||||||
var actions = a[status.identifier.capitalize()] || a.Wanted || {};
|
|
||||||
|
|
||||||
var m = new Movie(self, {
|
var m = new Movie(self, {
|
||||||
'actions': actions,
|
'actions': self.options.actions,
|
||||||
'view': self.current_view,
|
'view': self.current_view,
|
||||||
'onSelect': self.calculateSelected.bind(self)
|
'onSelect': self.calculateSelected.bind(self)
|
||||||
}, movie);
|
}, movie);
|
||||||
|
|
||||||
$(m).inject(self.movie_list, inject_at || 'bottom');
|
$(m).inject(self.movie_list, inject_at || 'bottom');
|
||||||
|
|
||||||
m.fireEvent('injected');
|
m.fireEvent('injected');
|
||||||
|
|
||||||
self.movies.include(m)
|
self.movies.include(m)
|
||||||
@@ -143,29 +185,9 @@ var MovieList = new Class({
|
|||||||
var self = this;
|
var self = this;
|
||||||
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
|
||||||
self.current_view = self.getSavedView();
|
self.el.addClass('with_navigation')
|
||||||
self.el.addClass(self.current_view+'_list')
|
|
||||||
|
|
||||||
self.navigation = new Element('div.alph_nav').adopt(
|
self.navigation = new Element('div.alph_nav').adopt(
|
||||||
self.navigation_actions = new Element('ul.inlay.actions.reversed'),
|
|
||||||
self.navigation_counter = new Element('span.counter[title=Total]'),
|
|
||||||
self.navigation_alpha = new Element('ul.numbers', {
|
|
||||||
'events': {
|
|
||||||
'click:relay(li)': function(e, el){
|
|
||||||
self.movie_list.empty()
|
|
||||||
self.activateLetter(el.get('data-letter'))
|
|
||||||
self.getMovies()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
self.navigation_search_input = new Element('input.inlay', {
|
|
||||||
'placeholder': 'Search',
|
|
||||||
'events': {
|
|
||||||
'keyup': self.search.bind(self),
|
|
||||||
'change': self.search.bind(self)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
self.navigation_menu = new Block.Menu(self),
|
|
||||||
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
|
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
|
||||||
new Element('span.select').adopt(
|
new Element('span.select').adopt(
|
||||||
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
|
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
|
||||||
@@ -203,6 +225,31 @@ var MovieList = new Class({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
new Element('div.menus').adopt(
|
||||||
|
self.navigation_counter = new Element('span.counter[title=Total]'),
|
||||||
|
self.filter_menu = new Block.Menu(self, {
|
||||||
|
'class': 'filter'
|
||||||
|
}),
|
||||||
|
self.navigation_actions = new Element('ul.actions', {
|
||||||
|
'events': {
|
||||||
|
'click:relay(li)': function(e, el){
|
||||||
|
var a = 'active';
|
||||||
|
self.navigation_actions.getElements('.'+a).removeClass(a);
|
||||||
|
self.changeView(el.get('data-view'));
|
||||||
|
this.addClass(a);
|
||||||
|
|
||||||
|
el.inject(el.getParent(), 'top');
|
||||||
|
el.getSiblings().hide()
|
||||||
|
setTimeout(function(){
|
||||||
|
el.getSiblings().setStyle('display', null);
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
self.navigation_menu = new Block.Menu(self, {
|
||||||
|
'class': 'extra'
|
||||||
|
})
|
||||||
)
|
)
|
||||||
).inject(self.el, 'top');
|
).inject(self.el, 'top');
|
||||||
|
|
||||||
@@ -215,20 +262,56 @@ var MovieList = new Class({
|
|||||||
}).inject(self.mass_edit_quality)
|
}).inject(self.mass_edit_quality)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Actions
|
self.filter_menu.addLink(
|
||||||
['mass_edit', 'thumbs', 'list'].each(function(view){
|
self.navigation_search_input = new Element('input', {
|
||||||
self.navigation_actions.adopt(
|
'title': 'Search through ' + self.options.identifier,
|
||||||
new Element('li.'+view+(self.current_view == view ? '.active' : '')+'[data-view='+view+']', {
|
'placeholder': 'Search through ' + self.options.identifier,
|
||||||
'events': {
|
'events': {
|
||||||
'click': function(e){
|
'keyup': self.search.bind(self),
|
||||||
var a = 'active';
|
'change': self.search.bind(self)
|
||||||
self.navigation_actions.getElements('.'+a).removeClass(a);
|
}
|
||||||
self.changeView(this.get('data-view'));
|
})
|
||||||
this.addClass(a);
|
).addClass('search');
|
||||||
}
|
|
||||||
|
var available_chars;
|
||||||
|
self.filter_menu.addEvent('open', function(){
|
||||||
|
self.navigation_search_input.focus();
|
||||||
|
|
||||||
|
// Get available chars and highlight
|
||||||
|
if(!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible()))
|
||||||
|
Api.request('movie.available_chars', {
|
||||||
|
'data': Object.merge({
|
||||||
|
'status': self.options.status
|
||||||
|
}, self.filter),
|
||||||
|
'onSuccess': function(json){
|
||||||
|
available_chars = json.chars
|
||||||
|
|
||||||
|
json.chars.split('').each(function(c){
|
||||||
|
self.letters[c.capitalize()].addClass('available')
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
}).adopt(new Element('span'))
|
});
|
||||||
)
|
});
|
||||||
|
|
||||||
|
self.filter_menu.addLink(
|
||||||
|
self.navigation_alpha = new Element('ul.numbers', {
|
||||||
|
'events': {
|
||||||
|
'click:relay(li.available)': function(e, el){
|
||||||
|
self.activateLetter(el.get('data-letter'))
|
||||||
|
self.getMovies(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
['mass_edit', 'details', 'list'].each(function(view){
|
||||||
|
var current = self.current_view == view;
|
||||||
|
new Element('li', {
|
||||||
|
'class': 'icon2 ' + view + (current ? ' active ' : ''),
|
||||||
|
'data-view': view
|
||||||
|
}).inject(self.navigation_actions, current ? 'top' : 'bottom');
|
||||||
});
|
});
|
||||||
|
|
||||||
// All
|
// All
|
||||||
@@ -245,37 +328,13 @@ var MovieList = new Class({
|
|||||||
}).inject(self.navigation_alpha);
|
}).inject(self.navigation_alpha);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get available chars and highlight
|
|
||||||
Api.request('movie.available_chars', {
|
|
||||||
'data': Object.merge({
|
|
||||||
'status': self.options.status
|
|
||||||
}, self.filter),
|
|
||||||
'onComplete': function(json){
|
|
||||||
|
|
||||||
json.chars.split('').each(function(c){
|
|
||||||
self.letters[c.capitalize()].addClass('available')
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add menu or hide
|
// Add menu or hide
|
||||||
if (self.options.menu.length > 0)
|
if (self.options.menu.length > 0)
|
||||||
self.options.menu.each(function(menu_item){
|
self.options.menu.each(function(menu_item){
|
||||||
self.navigation_menu.addLink(menu_item);
|
self.navigation_menu.addLink(menu_item);
|
||||||
})
|
})
|
||||||
else
|
else
|
||||||
self.navigation_menu.hide()
|
self.navigation_menu.hide();
|
||||||
|
|
||||||
self.nav_scrollspy = new ScrollSpy({
|
|
||||||
min: 10,
|
|
||||||
onEnter: function(){
|
|
||||||
self.navigation.addClass('float')
|
|
||||||
},
|
|
||||||
onLeave: function(){
|
|
||||||
self.navigation.removeClass('float')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -323,14 +382,14 @@ var MovieList = new Class({
|
|||||||
self.movies.each(function(movie){
|
self.movies.each(function(movie){
|
||||||
if (movie.isSelected()){
|
if (movie.isSelected()){
|
||||||
$(movie).destroy()
|
$(movie).destroy()
|
||||||
erase_movies.include(movie)
|
erase_movies.include(movie);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
erase_movies.each(function(movie){
|
erase_movies.each(function(movie){
|
||||||
self.movies.erase(movie);
|
self.movies.erase(movie);
|
||||||
|
movie.destroy();
|
||||||
movie.destroy()
|
self.setCounter(self.counter_count-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.calculateSelected();
|
self.calculateSelected();
|
||||||
@@ -398,11 +457,16 @@ var MovieList = new Class({
|
|||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.movies = []
|
self.movies = []
|
||||||
self.calculateSelected()
|
if(self.mass_edit_select)
|
||||||
self.navigation_alpha.getElements('.active').removeClass('active')
|
self.calculateSelected()
|
||||||
|
if(self.navigation_alpha)
|
||||||
|
self.navigation_alpha.getElements('.active').removeClass('active')
|
||||||
|
|
||||||
self.offset = 0;
|
self.offset = 0;
|
||||||
self.load_more.show();
|
if(self.scrollspy){
|
||||||
self.scrollspy.start();
|
self.load_more.show();
|
||||||
|
self.scrollspy.start();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
activateLetter: function(letter){
|
activateLetter: function(letter){
|
||||||
@@ -418,21 +482,17 @@ var MovieList = new Class({
|
|||||||
changeView: function(new_view){
|
changeView: function(new_view){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.movies.each(function(movie){
|
|
||||||
movie.changeView(new_view)
|
|
||||||
});
|
|
||||||
|
|
||||||
self.el
|
self.el
|
||||||
.removeClass(self.current_view+'_list')
|
.removeClass(self.current_view+'_list')
|
||||||
.addClass(new_view+'_list')
|
.addClass(new_view+'_list')
|
||||||
|
|
||||||
self.current_view = new_view;
|
self.current_view = new_view;
|
||||||
Cookie.write(self.options.identifier+'_view', new_view, {duration: 1000});
|
Cookie.write(self.options.identifier+'_view2', new_view, {duration: 1000});
|
||||||
},
|
},
|
||||||
|
|
||||||
getSavedView: function(){
|
getSavedView: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
return Cookie.read(self.options.identifier+'_view') || 'thumbs';
|
return Cookie.read(self.options.identifier+'_view2');
|
||||||
},
|
},
|
||||||
|
|
||||||
search: function(){
|
search: function(){
|
||||||
@@ -448,8 +508,7 @@ var MovieList = new Class({
|
|||||||
self.activateLetter();
|
self.activateLetter();
|
||||||
self.filter.search = search_value;
|
self.filter.search = search_value;
|
||||||
|
|
||||||
self.movie_list.empty();
|
self.getMovies(true);
|
||||||
self.getMovies();
|
|
||||||
|
|
||||||
self.last_search_value = search_value;
|
self.last_search_value = search_value;
|
||||||
|
|
||||||
@@ -461,27 +520,62 @@ var MovieList = new Class({
|
|||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.reset();
|
self.reset();
|
||||||
self.movie_list.empty();
|
self.getMovies(true);
|
||||||
self.getMovies();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getMovies: function(){
|
getMovies: function(reset){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if(self.scrollspy) self.scrollspy.stop();
|
if(self.scrollspy){
|
||||||
self.load_more.set('text', 'loading...');
|
self.scrollspy.stop();
|
||||||
Api.request('movie.list', {
|
self.load_more.set('text', 'loading...');
|
||||||
|
}
|
||||||
|
|
||||||
|
if(self.movies.length == 0 && self.options.loader){
|
||||||
|
|
||||||
|
self.loader_first = new Element('div.loading').adopt(
|
||||||
|
new Element('div.message', {'text': self.options.title ? 'Loading \'' + self.options.title + '\'' : 'Loading...'})
|
||||||
|
).inject(self.el, 'top');
|
||||||
|
|
||||||
|
createSpinner(self.loader_first, {
|
||||||
|
radius: 4,
|
||||||
|
length: 4,
|
||||||
|
width: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
self.el.setStyle('min-height', 93);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Api.request(self.options.api_call || 'movie.list', {
|
||||||
'data': Object.merge({
|
'data': Object.merge({
|
||||||
'status': self.options.status,
|
'status': self.options.status,
|
||||||
'limit_offset': self.options.limit + ',' + self.offset
|
'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null
|
||||||
}, self.filter),
|
}, self.filter),
|
||||||
'onComplete': function(json){
|
'onSuccess': function(json){
|
||||||
self.store(json.movies);
|
|
||||||
self.addMovies(json.movies, json.total);
|
|
||||||
self.load_more.set('text', 'load more movies');
|
|
||||||
if(self.scrollspy) self.scrollspy.start();
|
|
||||||
|
|
||||||
self.checkIfEmpty()
|
if(reset)
|
||||||
|
self.movie_list.empty();
|
||||||
|
|
||||||
|
if(self.loader_first){
|
||||||
|
var lf = self.loader_first;
|
||||||
|
self.loader_first.addClass('hide')
|
||||||
|
self.loader_first = null;
|
||||||
|
setTimeout(function(){
|
||||||
|
lf.destroy();
|
||||||
|
}, 20000);
|
||||||
|
self.el.setStyle('min-height', null);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.store(json.movies);
|
||||||
|
self.addMovies(json.movies, json.total || json.movies.length);
|
||||||
|
if(self.scrollspy) {
|
||||||
|
self.load_more.set('text', 'load more movies');
|
||||||
|
self.scrollspy.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.checkIfEmpty();
|
||||||
|
self.fireEvent('loaded');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -502,10 +596,16 @@ var MovieList = new Class({
|
|||||||
checkIfEmpty: function(){
|
checkIfEmpty: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var is_empty = self.movies.length == 0 && self.total_movies == 0;
|
var is_empty = self.movies.length == 0 && (self.total_movies == 0 || self.total_movies === undefined);
|
||||||
|
|
||||||
|
if(self.title)
|
||||||
|
self.title[is_empty ? 'hide' : 'show']()
|
||||||
|
|
||||||
|
if(self.description)
|
||||||
|
self.description.setStyle('display', [is_empty ? 'none' : ''])
|
||||||
|
|
||||||
if(is_empty && self.options.on_empty_element){
|
if(is_empty && self.options.on_empty_element){
|
||||||
self.el.grab(self.options.on_empty_element);
|
self.options.on_empty_element.inject(self.loader_first || self.title || self.movie_list, 'after');
|
||||||
|
|
||||||
if(self.navigation)
|
if(self.navigation)
|
||||||
self.navigation.hide();
|
self.navigation.hide();
|
||||||
@@ -0,0 +1,905 @@
|
|||||||
|
var MovieAction = new Class({
|
||||||
|
|
||||||
|
Implements: [Options],
|
||||||
|
|
||||||
|
class_name: 'action icon2',
|
||||||
|
|
||||||
|
initialize: function(movie, options){
|
||||||
|
var self = this;
|
||||||
|
self.setOptions(options);
|
||||||
|
|
||||||
|
self.movie = movie;
|
||||||
|
|
||||||
|
self.create();
|
||||||
|
if(self.el)
|
||||||
|
self.el.addClass(self.class_name)
|
||||||
|
},
|
||||||
|
|
||||||
|
create: function(){},
|
||||||
|
|
||||||
|
disable: function(){
|
||||||
|
this.el.addClass('disable')
|
||||||
|
},
|
||||||
|
|
||||||
|
enable: function(){
|
||||||
|
this.el.removeClass('disable')
|
||||||
|
},
|
||||||
|
|
||||||
|
getTitle: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return self.movie.getTitle();
|
||||||
|
}
|
||||||
|
catch(e){
|
||||||
|
try {
|
||||||
|
return self.movie.original_title ? self.movie.original_title : self.movie.titles[0];
|
||||||
|
}
|
||||||
|
catch(e){
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get: function(key){
|
||||||
|
var self = this;
|
||||||
|
try {
|
||||||
|
return self.movie.get(key)
|
||||||
|
}
|
||||||
|
catch(e){
|
||||||
|
return self.movie[key]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createMask: function(){
|
||||||
|
var self = this;
|
||||||
|
self.mask = new Element('div.mask', {
|
||||||
|
'styles': {
|
||||||
|
'z-index': '1'
|
||||||
|
}
|
||||||
|
}).inject(self.movie, 'top').fade('hide');
|
||||||
|
//self.positionMask();
|
||||||
|
},
|
||||||
|
|
||||||
|
positionMask: function(){
|
||||||
|
var self = this,
|
||||||
|
movie = $(self.movie),
|
||||||
|
s = movie.getSize()
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
return self.mask.setStyles({
|
||||||
|
'width': s.x,
|
||||||
|
'height': s.y
|
||||||
|
}).position({
|
||||||
|
'relativeTo': movie
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
toElement: function(){
|
||||||
|
return this.el || null
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
var MA = {};
|
||||||
|
|
||||||
|
MA.IMDB = new Class({
|
||||||
|
|
||||||
|
Extends: MovieAction,
|
||||||
|
id: null,
|
||||||
|
|
||||||
|
create: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.id = self.movie.get('imdb') || self.movie.get('identifier');
|
||||||
|
|
||||||
|
self.el = new Element('a.imdb', {
|
||||||
|
'title': 'Go to the IMDB page of ' + self.getTitle(),
|
||||||
|
'href': 'http://www.imdb.com/title/'+self.id+'/',
|
||||||
|
'target': '_blank'
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!self.id) self.disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
MA.Release = new Class({
|
||||||
|
|
||||||
|
Extends: MovieAction,
|
||||||
|
|
||||||
|
create: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.el = new Element('a.releases.download', {
|
||||||
|
'title': 'Show the releases that are available for ' + self.getTitle(),
|
||||||
|
'events': {
|
||||||
|
'click': self.show.bind(self)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(self.movie.data.releases.length == 0)
|
||||||
|
self.el.hide()
|
||||||
|
else
|
||||||
|
self.showHelper();
|
||||||
|
|
||||||
|
App.addEvent('movie.searcher.ended.'+self.movie.data.id, function(notification){
|
||||||
|
self.releases = null;
|
||||||
|
if(self.options_container){
|
||||||
|
self.options_container.destroy();
|
||||||
|
self.options_container = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
show: function(e){
|
||||||
|
var self = this;
|
||||||
|
if(e)
|
||||||
|
(e).preventDefault();
|
||||||
|
|
||||||
|
if(self.releases)
|
||||||
|
self.createReleases();
|
||||||
|
else {
|
||||||
|
|
||||||
|
self.movie.busy(true);
|
||||||
|
|
||||||
|
Api.request('release.for_movie', {
|
||||||
|
'data': {
|
||||||
|
'id': self.movie.data.id
|
||||||
|
},
|
||||||
|
'onComplete': function(json){
|
||||||
|
self.movie.busy(false, 1);
|
||||||
|
|
||||||
|
if(json && json.releases){
|
||||||
|
self.releases = json.releases;
|
||||||
|
self.createReleases();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
alert('Something went wrong, check the logs.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
createReleases: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if(!self.options_container){
|
||||||
|
self.options_container = new Element('div.options').grab(
|
||||||
|
self.release_container = new Element('div.releases.table')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Header
|
||||||
|
new Element('div.item.head').adopt(
|
||||||
|
new Element('span.name', {'text': 'Release name'}),
|
||||||
|
new Element('span.status', {'text': 'Status'}),
|
||||||
|
new Element('span.quality', {'text': 'Quality'}),
|
||||||
|
new Element('span.size', {'text': 'Size'}),
|
||||||
|
new Element('span.age', {'text': 'Age'}),
|
||||||
|
new Element('span.score', {'text': 'Score'}),
|
||||||
|
new Element('span.provider', {'text': 'Provider'})
|
||||||
|
).inject(self.release_container)
|
||||||
|
|
||||||
|
self.releases.each(function(release){
|
||||||
|
|
||||||
|
var status = Status.get(release.status_id),
|
||||||
|
quality = Quality.getProfile(release.quality_id) || {},
|
||||||
|
info = release.info,
|
||||||
|
provider = self.get(release, 'provider') + (release.info['provider_extra'] ? self.get(release, 'provider_extra') : '');
|
||||||
|
release.status = status;
|
||||||
|
|
||||||
|
var release_name = self.get(release, 'name');
|
||||||
|
if(release.files && release.files.length > 0){
|
||||||
|
try {
|
||||||
|
var movie_file = release.files.filter(function(file){
|
||||||
|
var type = File.Type.get(file.type_id);
|
||||||
|
return type && type.identifier == 'movie'
|
||||||
|
}).pick();
|
||||||
|
release_name = movie_file.path.split(Api.getOption('path_sep')).getLast();
|
||||||
|
}
|
||||||
|
catch(e){}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create release
|
||||||
|
var item = new Element('div', {
|
||||||
|
'class': 'item '+status.identifier,
|
||||||
|
'id': 'release_'+release.id
|
||||||
|
}).adopt(
|
||||||
|
new Element('span.name', {'text': release_name, 'title': release_name}),
|
||||||
|
new Element('span.status', {'text': status.identifier, 'class': 'release_status '+status.identifier}),
|
||||||
|
new Element('span.quality', {'text': quality.get('label') || 'n/a'}),
|
||||||
|
new Element('span.size', {'text': release.info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}),
|
||||||
|
new Element('span.age', {'text': self.get(release, 'age')}),
|
||||||
|
new Element('span.score', {'text': self.get(release, 'score')}),
|
||||||
|
new Element('span.provider', { 'text': provider, 'title': provider }),
|
||||||
|
release.info['detail_url'] ? new Element('a.info.icon2', {
|
||||||
|
'href': release.info['detail_url'],
|
||||||
|
'target': '_blank'
|
||||||
|
}) : new Element('a'),
|
||||||
|
new Element('a.download.icon2', {
|
||||||
|
'events': {
|
||||||
|
'click': function(e){
|
||||||
|
(e).preventDefault();
|
||||||
|
if(!this.hasClass('completed'))
|
||||||
|
self.download(release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new Element('a.delete.icon2', {
|
||||||
|
'events': {
|
||||||
|
'click': function(e){
|
||||||
|
(e).preventDefault();
|
||||||
|
self.ignore(release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).inject(self.release_container);
|
||||||
|
|
||||||
|
release['el'] = item;
|
||||||
|
|
||||||
|
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
|
||||||
|
if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched'))
|
||||||
|
self.last_release = release;
|
||||||
|
}
|
||||||
|
else if(!self.next_release && status.identifier == 'available'){
|
||||||
|
self.next_release = release;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(self.last_release)
|
||||||
|
self.release_container.getElement('#release_'+self.last_release.id).addClass('last_release');
|
||||||
|
|
||||||
|
if(self.next_release)
|
||||||
|
self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release');
|
||||||
|
|
||||||
|
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){
|
||||||
|
|
||||||
|
self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top');
|
||||||
|
|
||||||
|
var nr = self.next_release,
|
||||||
|
lr = self.last_release;
|
||||||
|
|
||||||
|
self.trynext_container.adopt(
|
||||||
|
new Element('span.or', {
|
||||||
|
'text': 'This movie is snatched, if anything went wrong, download'
|
||||||
|
}),
|
||||||
|
lr ? new Element('a.button.orange', {
|
||||||
|
'text': 'the same release again',
|
||||||
|
'events': {
|
||||||
|
'click': function(){
|
||||||
|
self.download(lr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) : null,
|
||||||
|
nr && lr ? new Element('span.or', {
|
||||||
|
'text': ','
|
||||||
|
}) : null,
|
||||||
|
nr ? [new Element('a.button.green', {
|
||||||
|
'text': lr ? 'another release' : 'the best release',
|
||||||
|
'events': {
|
||||||
|
'click': function(){
|
||||||
|
self.download(nr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new Element('span.or', {
|
||||||
|
'text': 'or pick one below'
|
||||||
|
})] : null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.last_release = null;
|
||||||
|
self.next_release = null;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show it
|
||||||
|
self.options_container.inject(self.movie, 'top');
|
||||||
|
self.movie.slide('in', self.options_container);
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
showHelper: function(e){
|
||||||
|
var self = this;
|
||||||
|
if(e)
|
||||||
|
(e).preventDefault();
|
||||||
|
|
||||||
|
var has_available = false,
|
||||||
|
has_snatched = false;
|
||||||
|
|
||||||
|
self.movie.data.releases.each(function(release){
|
||||||
|
if(has_available && has_snatched) return;
|
||||||
|
|
||||||
|
var status = Status.get(release.status_id);
|
||||||
|
|
||||||
|
if(['snatched', 'downloaded', 'seeding'].contains(status.identifier))
|
||||||
|
has_snatched = true;
|
||||||
|
|
||||||
|
if(['available'].contains(status.identifier))
|
||||||
|
has_available = true;
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
if(has_available || has_snatched){
|
||||||
|
|
||||||
|
self.trynext_container = new Element('div.buttons.trynext').inject(self.movie.info_container);
|
||||||
|
|
||||||
|
self.trynext_container.adopt(
|
||||||
|
has_available ? [new Element('a.icon2.readd', {
|
||||||
|
'text': has_snatched ? 'Download another release' : 'Download the best release',
|
||||||
|
'events': {
|
||||||
|
'click': self.tryNextRelease.bind(self)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new Element('a.icon2.download', {
|
||||||
|
'text': 'pick one yourself',
|
||||||
|
'events': {
|
||||||
|
'click': function(){
|
||||||
|
self.movie.quality.fireEvent('click');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})] : null,
|
||||||
|
new Element('a.icon2.completed', {
|
||||||
|
'text': 'mark this movie done',
|
||||||
|
'events': {
|
||||||
|
'click': self.markMovieDone.bind(self)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
get: function(release, type){
|
||||||
|
return release.info[type] || 'n/a'
|
||||||
|
},
|
||||||
|
|
||||||
|
download: function(release){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var release_el = self.release_container.getElement('#release_'+release.id),
|
||||||
|
icon = release_el.getElement('.download.icon2');
|
||||||
|
|
||||||
|
icon.addClass('icon spinner').removeClass('download');
|
||||||
|
|
||||||
|
Api.request('release.download', {
|
||||||
|
'data': {
|
||||||
|
'id': release.id
|
||||||
|
},
|
||||||
|
'onComplete': function(json){
|
||||||
|
icon.removeClass('icon spinner');
|
||||||
|
|
||||||
|
if(json.success){
|
||||||
|
icon.addClass('completed');
|
||||||
|
release_el.getElement('.release_status').set('text', 'snatched');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
ignore: function(release){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
Api.request('release.ignore', {
|
||||||
|
'data': {
|
||||||
|
'id': release.id
|
||||||
|
},
|
||||||
|
'onComplete': function(){
|
||||||
|
var el = release.el;
|
||||||
|
if(el.hasClass('failed') || el.hasClass('ignored')){
|
||||||
|
el.removeClass('failed').removeClass('ignored');
|
||||||
|
el.getElement('.release_status').set('text', 'available');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
el.addClass('ignored');
|
||||||
|
el.getElement('.release_status').set('text', 'ignored');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
markMovieDone: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
Api.request('movie.delete', {
|
||||||
|
'data': {
|
||||||
|
'id': self.movie.get('id'),
|
||||||
|
'delete_from': 'wanted'
|
||||||
|
},
|
||||||
|
'onComplete': function(){
|
||||||
|
var movie = $(self.movie);
|
||||||
|
movie.set('tween', {
|
||||||
|
'duration': 300,
|
||||||
|
'onComplete': function(){
|
||||||
|
self.movie.destroy()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
movie.tween('height', 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
tryNextRelease: function(movie_id){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
Api.request('movie.searcher.try_next', {
|
||||||
|
'data': {
|
||||||
|
'id': self.movie.get('id')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
MA.Trailer = new Class({
|
||||||
|
|
||||||
|
Extends: MovieAction,
|
||||||
|
id: null,
|
||||||
|
|
||||||
|
create: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.el = new Element('a.trailer', {
|
||||||
|
'title': 'Watch the trailer of ' + self.getTitle(),
|
||||||
|
'events': {
|
||||||
|
'click': self.watch.bind(self)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: function(offset){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var data_url = 'https://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'
|
||||||
|
var url = data_url.substitute({
|
||||||
|
'title': encodeURI(self.getTitle()),
|
||||||
|
'year': self.get('year'),
|
||||||
|
'offset': offset || 1
|
||||||
|
}),
|
||||||
|
size = $(self.movie).getSize(),
|
||||||
|
height = self.options.height || (size.x/16)*9,
|
||||||
|
id = 'trailer-'+randomString();
|
||||||
|
|
||||||
|
self.player_container = new Element('div[id='+id+']');
|
||||||
|
self.container = new Element('div.hide.trailer_container')
|
||||||
|
.adopt(self.player_container)
|
||||||
|
.inject($(self.movie), 'top');
|
||||||
|
|
||||||
|
self.container.setStyle('height', 0);
|
||||||
|
self.container.removeClass('hide');
|
||||||
|
|
||||||
|
self.close_button = new Element('a.hide.hide_trailer', {
|
||||||
|
'text': 'Hide trailer',
|
||||||
|
'events': {
|
||||||
|
'click': self.stop.bind(self)
|
||||||
|
}
|
||||||
|
}).inject(self.movie);
|
||||||
|
|
||||||
|
self.container.setStyle('height', height);
|
||||||
|
$(self.movie).setStyle('height', height);
|
||||||
|
|
||||||
|
new Request.JSONP({
|
||||||
|
'url': url,
|
||||||
|
'onComplete': function(json){
|
||||||
|
var video_url = json.feed.entry[0].id.$t.split('/'),
|
||||||
|
video_id = video_url[video_url.length-1];
|
||||||
|
|
||||||
|
self.player = new YT.Player(id, {
|
||||||
|
'height': height,
|
||||||
|
'width': size.x,
|
||||||
|
'videoId': video_id,
|
||||||
|
'playerVars': {
|
||||||
|
'autoplay': 1,
|
||||||
|
'showsearch': 0,
|
||||||
|
'wmode': 'transparent',
|
||||||
|
'iv_load_policy': 3
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.close_button.removeClass('hide');
|
||||||
|
|
||||||
|
var quality_set = false;
|
||||||
|
var change_quality = function(state){
|
||||||
|
if(!quality_set && (state.data == 1 || state.data || 2)){
|
||||||
|
try {
|
||||||
|
self.player.setPlaybackQuality('hd720');
|
||||||
|
quality_set = true;
|
||||||
|
}
|
||||||
|
catch(e){
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.player.addEventListener('onStateChange', change_quality);
|
||||||
|
|
||||||
|
}
|
||||||
|
}).send()
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
stop: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.player.stopVideo();
|
||||||
|
self.container.addClass('hide');
|
||||||
|
self.close_button.addClass('hide');
|
||||||
|
$(self.movie).setStyle('height', null);
|
||||||
|
|
||||||
|
setTimeout(function(){
|
||||||
|
self.container.destroy()
|
||||||
|
self.close_button.destroy();
|
||||||
|
}, 1800)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
MA.Edit = new Class({
|
||||||
|
|
||||||
|
Extends: MovieAction,
|
||||||
|
|
||||||
|
create: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.el = new Element('a.edit', {
|
||||||
|
'title': 'Change movie information, like title and quality.',
|
||||||
|
'events': {
|
||||||
|
'click': self.editMovie.bind(self)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
editMovie: function(e){
|
||||||
|
var self = this;
|
||||||
|
(e).preventDefault();
|
||||||
|
|
||||||
|
if(!self.options_container){
|
||||||
|
self.options_container = new Element('div.options').adopt(
|
||||||
|
new Element('div.form').adopt(
|
||||||
|
self.title_select = new Element('select', {
|
||||||
|
'name': 'title'
|
||||||
|
}),
|
||||||
|
self.profile_select = new Element('select', {
|
||||||
|
'name': 'profile'
|
||||||
|
}),
|
||||||
|
self.category_select = new Element('select', {
|
||||||
|
'name': 'category'
|
||||||
|
}).grab(
|
||||||
|
new Element('option', {'value': -1, 'text': 'None'})
|
||||||
|
),
|
||||||
|
new Element('a.button.edit', {
|
||||||
|
'text': 'Save & Search',
|
||||||
|
'events': {
|
||||||
|
'click': self.save.bind(self)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).inject(self.movie, 'top');
|
||||||
|
|
||||||
|
Array.each(self.movie.data.library.titles, function(alt){
|
||||||
|
new Element('option', {
|
||||||
|
'text': alt.title
|
||||||
|
}).inject(self.title_select);
|
||||||
|
|
||||||
|
if(alt['default'])
|
||||||
|
self.title_select.set('value', alt.title);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Fill categories
|
||||||
|
var categories = CategoryList.getAll();
|
||||||
|
|
||||||
|
if(categories.length == 0)
|
||||||
|
self.category_select.hide();
|
||||||
|
else {
|
||||||
|
self.category_select.show();
|
||||||
|
categories.each(function(category){
|
||||||
|
|
||||||
|
var category_id = category.data.id;
|
||||||
|
|
||||||
|
new Element('option', {
|
||||||
|
'value': category_id,
|
||||||
|
'text': category.data.label
|
||||||
|
}).inject(self.category_select);
|
||||||
|
|
||||||
|
if(self.movie.category && self.movie.category.data && self.movie.category.data.id == category_id)
|
||||||
|
self.category_select.set('value', category_id);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill profiles
|
||||||
|
var profiles = Quality.getActiveProfiles();
|
||||||
|
if(profiles.length == 1)
|
||||||
|
self.profile_select.hide();
|
||||||
|
|
||||||
|
profiles.each(function(profile){
|
||||||
|
|
||||||
|
var profile_id = profile.id ? profile.id : profile.data.id;
|
||||||
|
|
||||||
|
new Element('option', {
|
||||||
|
'value': profile_id,
|
||||||
|
'text': profile.label ? profile.label : profile.data.label
|
||||||
|
}).inject(self.profile_select);
|
||||||
|
|
||||||
|
if(self.movie.get('profile_id') == profile_id)
|
||||||
|
self.profile_select.set('value', profile_id);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
self.movie.slide('in', self.options_container);
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function(e){
|
||||||
|
(e).preventDefault();
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
Api.request('movie.edit', {
|
||||||
|
'data': {
|
||||||
|
'id': self.movie.get('id'),
|
||||||
|
'default_title': self.title_select.get('value'),
|
||||||
|
'profile_id': self.profile_select.get('value'),
|
||||||
|
'category_id': self.category_select.get('value')
|
||||||
|
},
|
||||||
|
'useSpinner': true,
|
||||||
|
'spinnerTarget': $(self.movie),
|
||||||
|
'onComplete': function(){
|
||||||
|
self.movie.quality.set('text', self.profile_select.getSelected()[0].get('text'));
|
||||||
|
self.movie.title.set('text', self.title_select.getSelected()[0].get('text'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.movie.slide('out');
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
MA.Refresh = new Class({
|
||||||
|
|
||||||
|
Extends: MovieAction,
|
||||||
|
|
||||||
|
create: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.el = new Element('a.refresh', {
|
||||||
|
'title': 'Refresh the movie info and do a forced search',
|
||||||
|
'events': {
|
||||||
|
'click': self.doRefresh.bind(self)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
doRefresh: function(e){
|
||||||
|
var self = this;
|
||||||
|
(e).preventDefault();
|
||||||
|
|
||||||
|
Api.request('movie.refresh', {
|
||||||
|
'data': {
|
||||||
|
'id': self.movie.get('id')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
MA.Readd = new Class({
|
||||||
|
|
||||||
|
Extends: MovieAction,
|
||||||
|
|
||||||
|
create: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var movie_done = Status.get(self.movie.data.status_id).identifier == 'done';
|
||||||
|
if(!movie_done)
|
||||||
|
var snatched = self.movie.data.releases.filter(function(release){
|
||||||
|
return release.status && (release.status.identifier == 'snatched' || release.status.identifier == 'downloaded' || release.status.identifier == 'done');
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
if(movie_done || snatched && snatched > 0)
|
||||||
|
self.el = new Element('a.readd', {
|
||||||
|
'title': 'Readd the movie and mark all previous snatched/downloaded as ignored',
|
||||||
|
'events': {
|
||||||
|
'click': self.doReadd.bind(self)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
doReadd: function(e){
|
||||||
|
var self = this;
|
||||||
|
(e).preventDefault();
|
||||||
|
|
||||||
|
Api.request('movie.add', {
|
||||||
|
'data': {
|
||||||
|
'identifier': self.movie.get('identifier'),
|
||||||
|
'ignore_previous': 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
MA.Delete = new Class({
|
||||||
|
|
||||||
|
Extends: MovieAction,
|
||||||
|
|
||||||
|
Implements: [Chain],
|
||||||
|
|
||||||
|
create: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.el = new Element('a.delete', {
|
||||||
|
'title': 'Remove the movie from this CP list',
|
||||||
|
'events': {
|
||||||
|
'click': self.showConfirm.bind(self)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
showConfirm: function(e){
|
||||||
|
var self = this;
|
||||||
|
(e).preventDefault();
|
||||||
|
|
||||||
|
if(!self.delete_container){
|
||||||
|
self.delete_container = new Element('div.buttons.delete_container').adopt(
|
||||||
|
new Element('a.cancel', {
|
||||||
|
'text': 'Cancel',
|
||||||
|
'events': {
|
||||||
|
'click': self.hideConfirm.bind(self)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new Element('span.or', {
|
||||||
|
'text': 'or'
|
||||||
|
}),
|
||||||
|
new Element('a.button.delete', {
|
||||||
|
'text': 'Delete ' + self.movie.title.get('text'),
|
||||||
|
'events': {
|
||||||
|
'click': self.del.bind(self)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).inject(self.movie, 'top');
|
||||||
|
}
|
||||||
|
|
||||||
|
self.movie.slide('in', self.delete_container);
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
hideConfirm: function(e){
|
||||||
|
var self = this;
|
||||||
|
(e).preventDefault();
|
||||||
|
|
||||||
|
self.movie.removeView();
|
||||||
|
self.movie.slide('out');
|
||||||
|
},
|
||||||
|
|
||||||
|
del: function(e){
|
||||||
|
(e).preventDefault();
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var movie = $(self.movie);
|
||||||
|
|
||||||
|
self.chain(
|
||||||
|
function(){
|
||||||
|
self.callChain();
|
||||||
|
},
|
||||||
|
function(){
|
||||||
|
Api.request('movie.delete', {
|
||||||
|
'data': {
|
||||||
|
'id': self.movie.get('id'),
|
||||||
|
'delete_from': self.movie.list.options.identifier
|
||||||
|
},
|
||||||
|
'onComplete': function(){
|
||||||
|
movie.set('tween', {
|
||||||
|
'duration': 300,
|
||||||
|
'onComplete': function(){
|
||||||
|
self.movie.destroy()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
movie.tween('height', 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
self.callChain();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
MA.Files = new Class({
|
||||||
|
|
||||||
|
Extends: MovieAction,
|
||||||
|
|
||||||
|
create: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.el = new Element('a.directory', {
|
||||||
|
'title': 'Available files',
|
||||||
|
'events': {
|
||||||
|
'click': self.show.bind(self)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
show: function(e){
|
||||||
|
var self = this;
|
||||||
|
(e).preventDefault();
|
||||||
|
|
||||||
|
if(self.releases)
|
||||||
|
self.showFiles();
|
||||||
|
else {
|
||||||
|
|
||||||
|
self.movie.busy(true);
|
||||||
|
|
||||||
|
Api.request('release.for_movie', {
|
||||||
|
'data': {
|
||||||
|
'id': self.movie.data.id
|
||||||
|
},
|
||||||
|
'onComplete': function(json){
|
||||||
|
self.movie.busy(false, 1);
|
||||||
|
|
||||||
|
if(json && json.releases){
|
||||||
|
self.releases = json.releases;
|
||||||
|
self.showFiles();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
alert('Something went wrong, check the logs.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
showFiles: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if(!self.options_container){
|
||||||
|
self.options_container = new Element('div.options').adopt(
|
||||||
|
self.files_container = new Element('div.files.table')
|
||||||
|
).inject(self.movie, 'top');
|
||||||
|
|
||||||
|
// Header
|
||||||
|
new Element('div.item.head').adopt(
|
||||||
|
new Element('span.name', {'text': 'File'}),
|
||||||
|
new Element('span.type', {'text': 'Type'}),
|
||||||
|
new Element('span.is_available', {'text': 'Available'})
|
||||||
|
).inject(self.files_container)
|
||||||
|
|
||||||
|
Array.each(self.releases, function(release){
|
||||||
|
|
||||||
|
var rel = new Element('div.release').inject(self.files_container);
|
||||||
|
|
||||||
|
Array.each(release.files, function(file){
|
||||||
|
new Element('div.file.item').adopt(
|
||||||
|
new Element('span.name', {'text': file.path}),
|
||||||
|
new Element('span.type', {'text': File.Type.get(file.type_id).name}),
|
||||||
|
new Element('span.available', {'text': file.available})
|
||||||
|
).inject(rel)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
self.movie.slide('in', self.options_container);
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,299 @@
|
|||||||
|
var Movie = new Class({
|
||||||
|
|
||||||
|
Extends: BlockBase,
|
||||||
|
|
||||||
|
action: {},
|
||||||
|
|
||||||
|
initialize: function(list, options, data){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.data = data;
|
||||||
|
self.view = options.view || 'details';
|
||||||
|
self.list = list;
|
||||||
|
|
||||||
|
self.el = new Element('div.movie');
|
||||||
|
|
||||||
|
self.profile = Quality.getProfile(data.profile_id) || {};
|
||||||
|
self.category = CategoryList.getCategory(data.category_id) || {};
|
||||||
|
self.parent(self, options);
|
||||||
|
|
||||||
|
self.addEvents();
|
||||||
|
},
|
||||||
|
|
||||||
|
addEvents: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
App.addEvent('movie.update.'+self.data.id, function(notification){
|
||||||
|
self.busy(false);
|
||||||
|
self.removeView();
|
||||||
|
self.update.delay(2000, self, notification);
|
||||||
|
});
|
||||||
|
|
||||||
|
['movie.busy', 'movie.searcher.started'].each(function(listener){
|
||||||
|
App.addEvent(listener+'.'+self.data.id, function(notification){
|
||||||
|
if(notification.data)
|
||||||
|
self.busy(true)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
App.addEvent('movie.searcher.ended.'+self.data.id, function(notification){
|
||||||
|
if(notification.data)
|
||||||
|
self.busy(false)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.el.destroy();
|
||||||
|
delete self.list.movies_added[self.get('id')];
|
||||||
|
self.list.movies.erase(self)
|
||||||
|
|
||||||
|
self.list.checkIfEmpty();
|
||||||
|
|
||||||
|
// Remove events
|
||||||
|
App.removeEvents('movie.update.'+self.data.id);
|
||||||
|
['movie.busy', 'movie.searcher.started'].each(function(listener){
|
||||||
|
App.removeEvents(listener+'.'+self.data.id);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
busy: function(set_busy, timeout){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if(!set_busy){
|
||||||
|
setTimeout(function(){
|
||||||
|
if(self.spinner){
|
||||||
|
self.mask.fade('out');
|
||||||
|
setTimeout(function(){
|
||||||
|
if(self.mask)
|
||||||
|
self.mask.destroy();
|
||||||
|
if(self.spinner)
|
||||||
|
self.spinner.el.destroy();
|
||||||
|
self.spinner = null;
|
||||||
|
self.mask = null;
|
||||||
|
}, timeout || 400);
|
||||||
|
}
|
||||||
|
}, timeout || 1000)
|
||||||
|
}
|
||||||
|
else if(!self.spinner) {
|
||||||
|
self.createMask();
|
||||||
|
self.spinner = createSpinner(self.mask);
|
||||||
|
self.mask.fade('in');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
createMask: function(){
|
||||||
|
var self = this;
|
||||||
|
self.mask = new Element('div.mask', {
|
||||||
|
'styles': {
|
||||||
|
'z-index': 4
|
||||||
|
}
|
||||||
|
}).inject(self.el, 'top').fade('hide');
|
||||||
|
},
|
||||||
|
|
||||||
|
positionMask: function(){
|
||||||
|
var self = this,
|
||||||
|
s = self.el.getSize()
|
||||||
|
|
||||||
|
return self.mask.setStyles({
|
||||||
|
'width': s.x,
|
||||||
|
'height': s.y
|
||||||
|
}).position({
|
||||||
|
'relativeTo': self.el
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
update: function(notification){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.data = notification.data;
|
||||||
|
self.el.empty();
|
||||||
|
self.removeView();
|
||||||
|
|
||||||
|
self.profile = Quality.getProfile(self.data.profile_id) || {};
|
||||||
|
self.category = CategoryList.getCategory(self.data.category_id) || {};
|
||||||
|
self.create();
|
||||||
|
|
||||||
|
self.busy(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
create: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var s = Status.get(self.get('status_id'));
|
||||||
|
self.el.addClass('status_'+s.identifier);
|
||||||
|
|
||||||
|
self.el.adopt(
|
||||||
|
self.select_checkbox = new Element('input[type=checkbox].inlay', {
|
||||||
|
'events': {
|
||||||
|
'change': function(){
|
||||||
|
self.fireEvent('select')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
self.thumbnail = File.Select.single('poster', self.data.library.files),
|
||||||
|
self.data_container = new Element('div.data.inlay.light').adopt(
|
||||||
|
self.info_container = new Element('div.info').adopt(
|
||||||
|
new Element('div.title').adopt(
|
||||||
|
self.title = new Element('span', {
|
||||||
|
'text': self.getTitle() || 'n/a'
|
||||||
|
}),
|
||||||
|
self.year = new Element('div.year', {
|
||||||
|
'text': self.data.library.year || 'n/a'
|
||||||
|
})
|
||||||
|
),
|
||||||
|
self.description = new Element('div.description', {
|
||||||
|
'text': self.data.library.plot
|
||||||
|
}),
|
||||||
|
self.quality = new Element('div.quality', {
|
||||||
|
'events': {
|
||||||
|
'click': function(e){
|
||||||
|
var releases = self.el.getElement('.actions .releases');
|
||||||
|
if(releases.isVisible())
|
||||||
|
releases.fireEvent('click', [e])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
),
|
||||||
|
self.actions = new Element('div.actions')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if(self.thumbnail.empty)
|
||||||
|
self.el.addClass('no_thumbnail');
|
||||||
|
|
||||||
|
//self.changeView(self.view);
|
||||||
|
self.select_checkbox_class = new Form.Check(self.select_checkbox);
|
||||||
|
|
||||||
|
// Add profile
|
||||||
|
if(self.profile.data)
|
||||||
|
self.profile.getTypes().each(function(type){
|
||||||
|
|
||||||
|
var q = self.addQuality(type.quality_id || type.get('quality_id'));
|
||||||
|
if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){
|
||||||
|
q.addClass('finish');
|
||||||
|
q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.')
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add releases
|
||||||
|
if(self.data.releases)
|
||||||
|
self.data.releases.each(function(release){
|
||||||
|
|
||||||
|
var q = self.quality.getElement('.q_id'+ release.quality_id),
|
||||||
|
status = Status.get(release.status_id);
|
||||||
|
|
||||||
|
if(!q && (status.identifier == 'snatched' || status.identifier == 'done'))
|
||||||
|
var q = self.addQuality(release.quality_id)
|
||||||
|
|
||||||
|
if (status && q && !q.hasClass(status.identifier)){
|
||||||
|
q.addClass(status.identifier);
|
||||||
|
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label)
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.each(self.options.actions, function(action, key){
|
||||||
|
self.action[key.toLowerCase()] = action = new self.options.actions[key](self)
|
||||||
|
if(action.el)
|
||||||
|
self.actions.adopt(action)
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
addQuality: function(quality_id){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var q = Quality.getQuality(quality_id);
|
||||||
|
return new Element('span', {
|
||||||
|
'text': q.label,
|
||||||
|
'class': 'q_'+q.identifier + ' q_id' + q.id,
|
||||||
|
'title': ''
|
||||||
|
}).inject(self.quality);
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
getTitle: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var titles = self.data.library.titles;
|
||||||
|
|
||||||
|
var title = titles.filter(function(title){
|
||||||
|
return title['default']
|
||||||
|
}).pop()
|
||||||
|
|
||||||
|
if(title)
|
||||||
|
return self.getUnprefixedTitle(title.title)
|
||||||
|
else if(titles.length > 0)
|
||||||
|
return self.getUnprefixedTitle(titles[0].title)
|
||||||
|
|
||||||
|
return 'Unknown movie'
|
||||||
|
},
|
||||||
|
|
||||||
|
getUnprefixedTitle: function(t){
|
||||||
|
if(t.substr(0, 4).toLowerCase() == 'the ')
|
||||||
|
t = t.substr(4) + ', The';
|
||||||
|
return t;
|
||||||
|
},
|
||||||
|
|
||||||
|
slide: function(direction, el){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if(direction == 'in'){
|
||||||
|
self.temp_view = self.view;
|
||||||
|
self.changeView('details');
|
||||||
|
|
||||||
|
self.el.addEvent('outerClick', function(){
|
||||||
|
self.removeView();
|
||||||
|
self.slide('out')
|
||||||
|
})
|
||||||
|
el.show();
|
||||||
|
self.data_container.addClass('hide_right');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
self.el.removeEvents('outerClick')
|
||||||
|
|
||||||
|
setTimeout(function(){
|
||||||
|
self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
|
||||||
|
}, 600);
|
||||||
|
|
||||||
|
self.data_container.removeClass('hide_right');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
changeView: function(new_view){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.el
|
||||||
|
.removeClass(self.view+'_view')
|
||||||
|
.addClass(new_view+'_view')
|
||||||
|
|
||||||
|
self.view = new_view;
|
||||||
|
},
|
||||||
|
|
||||||
|
removeView: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.el.removeClass(self.view+'_view')
|
||||||
|
},
|
||||||
|
|
||||||
|
get: function(attr){
|
||||||
|
return this.data[attr] || this.data.library[attr]
|
||||||
|
},
|
||||||
|
|
||||||
|
select: function(bool){
|
||||||
|
var self = this;
|
||||||
|
self.select_checkbox_class[bool ? 'check' : 'uncheck']()
|
||||||
|
},
|
||||||
|
|
||||||
|
isSelected: function(){
|
||||||
|
return this.select_checkbox.get('checked');
|
||||||
|
},
|
||||||
|
|
||||||
|
toElement: function(){
|
||||||
|
return this.el;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
.search_form {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
position: absolute;
|
||||||
|
right: 105px;
|
||||||
|
top: 0;
|
||||||
|
text-align: right;
|
||||||
|
height: 100%;
|
||||||
|
border-bottom: 4px solid transparent;
|
||||||
|
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
||||||
|
position: absolute;
|
||||||
|
z-index: 20;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-width: 0 0 4px;
|
||||||
|
}
|
||||||
|
.search_form:hover {
|
||||||
|
border-color: #047792;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 480px) {
|
||||||
|
.search_form {
|
||||||
|
right: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search_form.focused,
|
||||||
|
.search_form.shown {
|
||||||
|
border-color: #04bce6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search_form .input {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 45px;
|
||||||
|
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search_form.focused .input,
|
||||||
|
.search_form.shown .input {
|
||||||
|
width: 380px;
|
||||||
|
background: #4e5969;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search_form .input input {
|
||||||
|
border-radius: 0;
|
||||||
|
display: block;
|
||||||
|
border: 0;
|
||||||
|
background: none;
|
||||||
|
color: #FFF;
|
||||||
|
font-size: 25px;
|
||||||
|
height: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
padding: 0 40px 0 10px;
|
||||||
|
transition: all .4s ease-in-out .2s;
|
||||||
|
}
|
||||||
|
.search_form.focused .input input,
|
||||||
|
.search_form.shown .input input {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search_form input::-ms-clear {
|
||||||
|
width : 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 480px) {
|
||||||
|
.search_form .input input {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search_form.focused .input,
|
||||||
|
.search_form.shown .input {
|
||||||
|
width: 277px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search_form .input a {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 44px;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 66px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search_form .input a:after {
|
||||||
|
content: "\e03e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.search_form.shown.filled .input a:after {
|
||||||
|
content: "\e04e";
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 480px) {
|
||||||
|
.search_form .input a {
|
||||||
|
line-height: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search_form .results_container {
|
||||||
|
text-align: left;
|
||||||
|
position: absolute;
|
||||||
|
background: #5c697b;
|
||||||
|
margin: 4px 0 0;
|
||||||
|
width: 470px;
|
||||||
|
min-height: 50px;
|
||||||
|
box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media all and (max-width: 480px) {
|
||||||
|
.search_form .results_container {
|
||||||
|
width: 320px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.search_form.focused.filled .results_container,
|
||||||
|
.search_form.shown.filled .results_container {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search_form .results {
|
||||||
|
max-height: 570px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie_result {
|
||||||
|
overflow: hidden;
|
||||||
|
height: 50px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie_result .options {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 30px;
|
||||||
|
right: 0;
|
||||||
|
padding: 13px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-width: 1px 0;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: inset 0 1px 8px rgba(0,0,0,0.25);
|
||||||
|
}
|
||||||
|
.movie_result .options > .in_library_wanted {
|
||||||
|
margin-top: -7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie_result .options > div {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie_result .options .thumbnail {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie_result .options select {
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.movie_result .options select[name=title] { width: 170px; }
|
||||||
|
.movie_result .options select[name=profile] { width: 90px; }
|
||||||
|
.movie_result .options select[name=category] { width: 80px; }
|
||||||
|
|
||||||
|
@media all and (max-width: 480px) {
|
||||||
|
|
||||||
|
.movie_result .options select[name=title] { width: 90px; }
|
||||||
|
.movie_result .options select[name=profile] { width: 50px; }
|
||||||
|
.movie_result .options select[name=category] { width: 50px; }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie_result .options .button {
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie_result .options .message {
|
||||||
|
height: 100%;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #fff;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie_result .data {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 30px;
|
||||||
|
right: 0;
|
||||||
|
background: #5c697b;
|
||||||
|
cursor: pointer;
|
||||||
|
border-top: 1px solid rgba(255,255,255, 0.08);
|
||||||
|
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
||||||
|
}
|
||||||
|
.movie_result .data.open {
|
||||||
|
left: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie_result:last-child .data { border-bottom: 0; }
|
||||||
|
|
||||||
|
.movie_result .in_wanted, .movie_result .in_library {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 2px;
|
||||||
|
left: 14px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie_result .thumbnail {
|
||||||
|
width: 34px;
|
||||||
|
min-height: 100%;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie_result .info {
|
||||||
|
position: absolute;
|
||||||
|
top: 20%;
|
||||||
|
left: 15px;
|
||||||
|
right: 7px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie_result .info h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 20px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search_form .info h2 {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie_result .info h2 .title {
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search_form .info h2 .title {
|
||||||
|
position: absolute;
|
||||||
|
width: 88%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie_result .info h2 .year {
|
||||||
|
padding: 0 5px;
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
width: 12%;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 480px) {
|
||||||
|
|
||||||
|
.search_form .info h2 .year {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.search_form .mask,
|
||||||
|
.movie_result .mask {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
+111
-77
@@ -7,33 +7,30 @@ Block.Search = new Class({
|
|||||||
create: function(){
|
create: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
var focus_timer = 0;
|
||||||
self.el = new Element('div.search_form').adopt(
|
self.el = new Element('div.search_form').adopt(
|
||||||
new Element('div.input').adopt(
|
new Element('div.input').adopt(
|
||||||
self.input = new Element('input.inlay', {
|
self.input = new Element('input', {
|
||||||
'placeholder': 'Search & add a new movie',
|
'placeholder': 'Search & add a new movie',
|
||||||
'events': {
|
'events': {
|
||||||
'keyup': self.keyup.bind(self),
|
'keyup': self.keyup.bind(self),
|
||||||
'focus': function(){
|
'focus': function(){
|
||||||
|
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)
|
||||||
},
|
},
|
||||||
'blur': function(){
|
'blur': function(){
|
||||||
(function(){
|
focus_timer = (function(){
|
||||||
self.el.removeClass('focused')
|
self.el.removeClass('focused')
|
||||||
}).delay(2000);
|
}).delay(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
new Element('span.enter', {
|
new Element('a.icon2', {
|
||||||
'events': {
|
'events': {
|
||||||
'click': self.keyup.bind(self)
|
'click': self.clear.bind(self),
|
||||||
},
|
'touchend': self.clear.bind(self)
|
||||||
'text':'Enter'
|
|
||||||
}),
|
|
||||||
new Element('a', {
|
|
||||||
'events': {
|
|
||||||
'click': self.clear.bind(self)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
@@ -59,13 +56,21 @@ Block.Search = new Class({
|
|||||||
var self = this;
|
var self = this;
|
||||||
(e).preventDefault();
|
(e).preventDefault();
|
||||||
|
|
||||||
self.last_q = '';
|
if(self.last_q === ''){
|
||||||
self.input.set('value', '');
|
self.input.blur()
|
||||||
self.input.focus()
|
self.last_q = null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
self.movies = []
|
self.last_q = '';
|
||||||
self.results.empty()
|
self.input.set('value', '');
|
||||||
self.el.removeClass('filled')
|
self.input.focus()
|
||||||
|
|
||||||
|
self.movies = []
|
||||||
|
self.results.empty()
|
||||||
|
self.el.removeClass('filled')
|
||||||
|
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
hideResults: function(bool){
|
hideResults: function(bool){
|
||||||
@@ -92,8 +97,13 @@ Block.Search = new Class({
|
|||||||
|
|
||||||
self.el[self.q() ? 'addClass' : 'removeClass']('filled')
|
self.el[self.q() ? 'addClass' : 'removeClass']('filled')
|
||||||
|
|
||||||
if(self.q() != self.last_q && (['enter'].indexOf(e.key) > -1 || e.type == 'click'))
|
if(self.q() != self.last_q){
|
||||||
self.autocomplete()
|
if(self.api_request && self.api_request.isRunning())
|
||||||
|
self.api_request.cancel();
|
||||||
|
|
||||||
|
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
|
||||||
|
self.autocomplete_timer = self.autocomplete.delay(300, self)
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -109,12 +119,9 @@ Block.Search = new Class({
|
|||||||
},
|
},
|
||||||
|
|
||||||
list: function(){
|
list: function(){
|
||||||
var self = this;
|
var self = this,
|
||||||
|
q = self.q(),
|
||||||
if(self.api_request && self.api_request.running) return
|
cache = self.cache[q];
|
||||||
|
|
||||||
var q = self.q();
|
|
||||||
var cache = self.cache[q];
|
|
||||||
|
|
||||||
self.hideResults(false);
|
self.hideResults(false);
|
||||||
|
|
||||||
@@ -157,9 +164,6 @@ Block.Search = new Class({
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if(q != self.q())
|
|
||||||
self.list()
|
|
||||||
|
|
||||||
// Calculate result heights
|
// Calculate result heights
|
||||||
var w = window.getSize(),
|
var w = window.getSize(),
|
||||||
rc = self.result_container.getCoordinates();
|
rc = self.result_container.getCoordinates();
|
||||||
@@ -181,8 +185,11 @@ Block.Search = new Class({
|
|||||||
|
|
||||||
Block.Search.Item = new Class({
|
Block.Search.Item = new Class({
|
||||||
|
|
||||||
|
Implements: [Options, Events],
|
||||||
|
|
||||||
initialize: function(info, options){
|
initialize: function(info, options){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
self.setOptions(options);
|
||||||
|
|
||||||
self.info = info;
|
self.info = info;
|
||||||
self.alternative_titles = [];
|
self.alternative_titles = [];
|
||||||
@@ -197,56 +204,36 @@ Block.Search.Item = new Class({
|
|||||||
self.el = new Element('div.movie_result', {
|
self.el = new Element('div.movie_result', {
|
||||||
'id': info.imdb
|
'id': info.imdb
|
||||||
}).adopt(
|
}).adopt(
|
||||||
|
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
|
||||||
|
'src': info.images.poster[0],
|
||||||
|
'height': null,
|
||||||
|
'width': null
|
||||||
|
}) : null,
|
||||||
self.options_el = new Element('div.options.inlay'),
|
self.options_el = new Element('div.options.inlay'),
|
||||||
self.data_container = new Element('div.data', {
|
self.data_container = new Element('div.data', {
|
||||||
'tween': {
|
|
||||||
duration: 400,
|
|
||||||
transition: 'quint:in:out'
|
|
||||||
},
|
|
||||||
'events': {
|
'events': {
|
||||||
'click': self.showOptions.bind(self)
|
'click': self.showOptions.bind(self)
|
||||||
}
|
}
|
||||||
}).adopt(
|
}).adopt(
|
||||||
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
|
self.info_container = new Element('div.info').adopt(
|
||||||
'src': info.images.poster[0],
|
new Element('h2').adopt(
|
||||||
'height': null,
|
self.title = new Element('span.title', {
|
||||||
'width': null
|
'text': info.titles && info.titles.length > 0 ? info.titles[0] : 'Unknown'
|
||||||
}) : null,
|
}),
|
||||||
new Element('div.info').adopt(
|
|
||||||
self.title = new Element('h2', {
|
|
||||||
'text': info.titles[0]
|
|
||||||
}).adopt(
|
|
||||||
self.year = info.year ? new Element('span.year', {
|
self.year = info.year ? new Element('span.year', {
|
||||||
'text': info.year
|
'text': info.year
|
||||||
}) : null
|
}) : null
|
||||||
),
|
)
|
||||||
self.tagline = new Element('span.tagline', {
|
|
||||||
'text': info.tagline ? info.tagline : info.plot,
|
|
||||||
'title': info.tagline ? info.tagline : info.plot
|
|
||||||
}),
|
|
||||||
self.director = self.info.director ? new Element('span.director', {
|
|
||||||
'text': 'Director:' + info.director
|
|
||||||
}) : null,
|
|
||||||
self.starring = info.actors ? new Element('span.actors', {
|
|
||||||
'text': 'Starring:'
|
|
||||||
}) : null
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if(info.actors){
|
if(info.titles)
|
||||||
Object.each(info.actors, function(actor){
|
info.titles.each(function(title){
|
||||||
new Element('span', {
|
self.alternativeTitle({
|
||||||
'text': actor
|
'title': title
|
||||||
}).inject(self.starring)
|
});
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
info.titles.each(function(title){
|
|
||||||
self.alternativeTitle({
|
|
||||||
'title': title
|
|
||||||
});
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
alternativeTitle: function(alternative){
|
alternativeTitle: function(alternative){
|
||||||
@@ -255,6 +242,20 @@ Block.Search.Item = new Class({
|
|||||||
self.alternative_titles.include(alternative);
|
self.alternative_titles.include(alternative);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getTitle: function(){
|
||||||
|
var self = this;
|
||||||
|
try {
|
||||||
|
return self.info.original_title ? self.info.original_title : self.info.titles[0];
|
||||||
|
}
|
||||||
|
catch(e){
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get: function(key){
|
||||||
|
return this.info[key]
|
||||||
|
},
|
||||||
|
|
||||||
showOptions: function(){
|
showOptions: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
@@ -274,7 +275,9 @@ Block.Search.Item = new Class({
|
|||||||
|
|
||||||
add: function(e){
|
add: function(e){
|
||||||
var self = this;
|
var self = this;
|
||||||
(e).preventDefault();
|
|
||||||
|
if(e)
|
||||||
|
(e).preventDefault();
|
||||||
|
|
||||||
self.loadingMask();
|
self.loadingMask();
|
||||||
|
|
||||||
@@ -282,7 +285,8 @@ Block.Search.Item = new Class({
|
|||||||
'data': {
|
'data': {
|
||||||
'identifier': self.info.imdb,
|
'identifier': self.info.imdb,
|
||||||
'title': self.title_select.get('value'),
|
'title': self.title_select.get('value'),
|
||||||
'profile_id': self.profile_select.get('value')
|
'profile_id': self.profile_select.get('value'),
|
||||||
|
'category_id': self.category_select.get('value')
|
||||||
},
|
},
|
||||||
'onComplete': function(json){
|
'onComplete': function(json){
|
||||||
self.options_el.empty();
|
self.options_el.empty();
|
||||||
@@ -292,6 +296,8 @@ Block.Search.Item = new Class({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
self.mask.fade('out');
|
self.mask.fade('out');
|
||||||
|
|
||||||
|
self.fireEvent('added');
|
||||||
},
|
},
|
||||||
'onFailure': function(){
|
'onFailure': function(){
|
||||||
self.options_el.empty();
|
self.options_el.empty();
|
||||||
@@ -319,14 +325,11 @@ Block.Search.Item = new Class({
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.options_el.grab(
|
self.options_el.grab(
|
||||||
new Element('div').adopt(
|
new Element('div', {
|
||||||
self.thumbnail = (info.images && info.images.poster.length > 0) ? new Element('img.thumbnail', {
|
'class': self.info.in_wanted && self.info.in_wanted.profile_id || in_library ? 'in_library_wanted' : ''
|
||||||
'src': info.images.poster[0],
|
}).adopt(
|
||||||
'height': null,
|
self.info.in_wanted && self.info.in_wanted.profile_id ? new Element('span.in_wanted', {
|
||||||
'width': null
|
'text': 'Already in wanted list: ' + Quality.getProfile(self.info.in_wanted.profile_id).get('label')
|
||||||
}) : null,
|
|
||||||
self.info.in_wanted && self.info.in_wanted.profile ? new Element('span.in_wanted', {
|
|
||||||
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
|
|
||||||
}) : (in_library ? new Element('span.in_library', {
|
}) : (in_library ? new Element('span.in_library', {
|
||||||
'text': 'Already in library: ' + in_library.join(', ')
|
'text': 'Already in library: ' + in_library.join(', ')
|
||||||
}) : null),
|
}) : null),
|
||||||
@@ -336,7 +339,12 @@ Block.Search.Item = new Class({
|
|||||||
self.profile_select = new Element('select', {
|
self.profile_select = new Element('select', {
|
||||||
'name': 'profile'
|
'name': 'profile'
|
||||||
}),
|
}),
|
||||||
new Element('a.button', {
|
self.category_select = new Element('select', {
|
||||||
|
'name': 'category'
|
||||||
|
}).grab(
|
||||||
|
new Element('option', {'value': -1, 'text': 'None'})
|
||||||
|
),
|
||||||
|
self.add_button = new Element('a.button', {
|
||||||
'text': 'Add',
|
'text': 'Add',
|
||||||
'events': {
|
'events': {
|
||||||
'click': self.add.bind(self)
|
'click': self.add.bind(self)
|
||||||
@@ -351,7 +359,28 @@ Block.Search.Item = new Class({
|
|||||||
}).inject(self.title_select)
|
}).inject(self.title_select)
|
||||||
})
|
})
|
||||||
|
|
||||||
Quality.getActiveProfiles().each(function(profile){
|
|
||||||
|
// Fill categories
|
||||||
|
var categories = CategoryList.getAll();
|
||||||
|
|
||||||
|
if(categories.length == 0)
|
||||||
|
self.category_select.hide();
|
||||||
|
else {
|
||||||
|
self.category_select.show();
|
||||||
|
categories.each(function(category){
|
||||||
|
new Element('option', {
|
||||||
|
'value': category.data.id,
|
||||||
|
'text': category.data.label
|
||||||
|
}).inject(self.category_select);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill profiles
|
||||||
|
var profiles = Quality.getActiveProfiles();
|
||||||
|
if(profiles.length == 1)
|
||||||
|
self.profile_select.hide();
|
||||||
|
|
||||||
|
profiles.each(function(profile){
|
||||||
new Element('option', {
|
new Element('option', {
|
||||||
'value': profile.id ? profile.id : profile.data.id,
|
'value': profile.id ? profile.id : profile.data.id,
|
||||||
'text': profile.label ? profile.label : profile.data.label
|
'text': profile.label ? profile.label : profile.data.label
|
||||||
@@ -359,6 +388,11 @@ Block.Search.Item = new Class({
|
|||||||
});
|
});
|
||||||
|
|
||||||
self.options_el.addClass('set');
|
self.options_el.addClass('set');
|
||||||
|
|
||||||
|
if(categories.length == 0 && self.title_select.getElements('option').length == 1 && profiles.length == 1 &&
|
||||||
|
!(self.info.in_wanted && self.info.in_wanted.profile_id || in_library))
|
||||||
|
self.add();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -366,7 +400,7 @@ Block.Search.Item = new Class({
|
|||||||
loadingMask: function(){
|
loadingMask: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.mask = new Element('span.mask').inject(self.el).fade('hide')
|
self.mask = new Element('div.mask').inject(self.el).fade('hide')
|
||||||
|
|
||||||
createSpinner(self.mask)
|
createSpinner(self.mask)
|
||||||
self.mask.fade('in')
|
self.mask.fade('in')
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
from .main import MovieLibraryPlugin
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return MovieLibraryPlugin()
|
||||||
|
|
||||||
|
config = []
|
||||||
+34
-25
@@ -1,9 +1,8 @@
|
|||||||
from couchpotato import get_session
|
from couchpotato import get_session
|
||||||
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||||
from couchpotato.core.helpers.variable import mergeDicts
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.media._base.library import LibraryBase
|
||||||
from couchpotato.core.settings.model import Library, LibraryTitle, File
|
from couchpotato.core.settings.model import Library, LibraryTitle, File
|
||||||
from string import ascii_letters
|
from string import ascii_letters
|
||||||
import time
|
import time
|
||||||
@@ -11,17 +10,20 @@ import traceback
|
|||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
class LibraryPlugin(Plugin):
|
|
||||||
|
class MovieLibraryPlugin(LibraryBase):
|
||||||
|
|
||||||
default_dict = {'titles': {}, 'files':{}}
|
default_dict = {'titles': {}, 'files':{}}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
addEvent('library.add', self.add)
|
addEvent('library.add.movie', self.add)
|
||||||
addEvent('library.update', self.update)
|
addEvent('library.update.movie', self.update)
|
||||||
addEvent('library.update_release_date', self.updateReleaseDate)
|
addEvent('library.update.movie.release_date', self.updateReleaseDate)
|
||||||
|
|
||||||
|
def add(self, attrs = None, update_after = True):
|
||||||
|
if not attrs: attrs = {}
|
||||||
|
|
||||||
def add(self, attrs = {}, update_after = True):
|
primary_provider = attrs.get('primary_provider', 'imdb')
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
@@ -33,12 +35,13 @@ class LibraryPlugin(Plugin):
|
|||||||
identifier = attrs.get('identifier'),
|
identifier = attrs.get('identifier'),
|
||||||
plot = toUnicode(attrs.get('plot')),
|
plot = toUnicode(attrs.get('plot')),
|
||||||
tagline = toUnicode(attrs.get('tagline')),
|
tagline = toUnicode(attrs.get('tagline')),
|
||||||
status_id = status.get('id')
|
status_id = status.get('id'),
|
||||||
|
info = {}
|
||||||
)
|
)
|
||||||
|
|
||||||
title = LibraryTitle(
|
title = LibraryTitle(
|
||||||
title = toUnicode(attrs.get('title')),
|
title = toUnicode(attrs.get('title')),
|
||||||
simple_title = self.simplifyTitle(attrs.get('title'))
|
simple_title = self.simplifyTitle(attrs.get('title')),
|
||||||
)
|
)
|
||||||
|
|
||||||
l.titles.append(title)
|
l.titles.append(title)
|
||||||
@@ -49,37 +52,39 @@ class LibraryPlugin(Plugin):
|
|||||||
# Update library info
|
# Update library info
|
||||||
if update_after is not False:
|
if update_after is not False:
|
||||||
handle = fireEventAsync if update_after is 'async' else fireEvent
|
handle = fireEventAsync if update_after is 'async' else fireEvent
|
||||||
handle('library.update', identifier = l.identifier, default_title = toUnicode(attrs.get('title', '')))
|
handle('library.update.movie', identifier = l.identifier, default_title = toUnicode(attrs.get('title', '')))
|
||||||
|
|
||||||
library_dict = l.to_dict(self.default_dict)
|
library_dict = l.to_dict(self.default_dict)
|
||||||
|
|
||||||
|
db.expire_all()
|
||||||
return library_dict
|
return library_dict
|
||||||
|
|
||||||
def update(self, identifier, default_title = '', force = False):
|
def update(self, identifier, default_title = '', force = False):
|
||||||
|
|
||||||
|
if self.shuttingDown():
|
||||||
|
return
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
library = db.query(Library).filter_by(identifier = identifier).first()
|
library = db.query(Library).filter_by(identifier = identifier).first()
|
||||||
done_status = fireEvent('status.get', 'done', single = True)
|
done_status = fireEvent('status.get', 'done', single = True)
|
||||||
|
|
||||||
|
library_dict = None
|
||||||
if library:
|
if library:
|
||||||
library_dict = library.to_dict(self.default_dict)
|
library_dict = library.to_dict(self.default_dict)
|
||||||
|
|
||||||
do_update = True
|
do_update = True
|
||||||
|
|
||||||
if library.status_id == done_status.get('id') and not force:
|
info = fireEvent('movie.info', merge = True, identifier = identifier)
|
||||||
do_update = False
|
|
||||||
else:
|
|
||||||
info = fireEvent('movie.info', merge = True, identifier = identifier)
|
|
||||||
|
|
||||||
# Don't need those here
|
# Don't need those here
|
||||||
try: del info['in_wanted']
|
try: del info['in_wanted']
|
||||||
except: pass
|
except: pass
|
||||||
try: del info['in_library']
|
try: del info['in_library']
|
||||||
except: pass
|
except: pass
|
||||||
|
|
||||||
if not info or len(info) == 0:
|
if not info or len(info) == 0:
|
||||||
log.error('Could not update, no movie info to work with: %s', identifier)
|
log.error('Could not update, no movie info to work with: %s', identifier)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Main info
|
# Main info
|
||||||
if do_update:
|
if do_update:
|
||||||
@@ -87,7 +92,7 @@ class LibraryPlugin(Plugin):
|
|||||||
library.tagline = toUnicode(info.get('tagline', ''))
|
library.tagline = toUnicode(info.get('tagline', ''))
|
||||||
library.year = info.get('year', 0)
|
library.year = info.get('year', 0)
|
||||||
library.status_id = done_status.get('id')
|
library.status_id = done_status.get('id')
|
||||||
library.info = info
|
library.info.update(info)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Titles
|
# Titles
|
||||||
@@ -96,6 +101,7 @@ class LibraryPlugin(Plugin):
|
|||||||
|
|
||||||
titles = info.get('titles', [])
|
titles = info.get('titles', [])
|
||||||
log.debug('Adding titles: %s', titles)
|
log.debug('Adding titles: %s', titles)
|
||||||
|
counter = 0
|
||||||
for title in titles:
|
for title in titles:
|
||||||
if not title:
|
if not title:
|
||||||
continue
|
continue
|
||||||
@@ -103,9 +109,10 @@ class LibraryPlugin(Plugin):
|
|||||||
t = LibraryTitle(
|
t = LibraryTitle(
|
||||||
title = title,
|
title = title,
|
||||||
simple_title = self.simplifyTitle(title),
|
simple_title = self.simplifyTitle(title),
|
||||||
default = title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title)
|
default = (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title)
|
||||||
)
|
)
|
||||||
library.titles.append(t)
|
library.titles.append(t)
|
||||||
|
counter += 1
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
@@ -130,6 +137,7 @@ class LibraryPlugin(Plugin):
|
|||||||
|
|
||||||
library_dict = library.to_dict(self.default_dict)
|
library_dict = library.to_dict(self.default_dict)
|
||||||
|
|
||||||
|
db.expire_all()
|
||||||
return library_dict
|
return library_dict
|
||||||
|
|
||||||
def updateReleaseDate(self, identifier):
|
def updateReleaseDate(self, identifier):
|
||||||
@@ -145,9 +153,10 @@ class LibraryPlugin(Plugin):
|
|||||||
|
|
||||||
if dates and dates.get('expires', 0) < time.time() or not dates:
|
if dates and dates.get('expires', 0) < time.time() or not dates:
|
||||||
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
|
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
|
||||||
library.info = mergeDicts(library.info, {'release_date': dates })
|
library.info.update({'release_date': dates })
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
db.expire_all()
|
||||||
return dates
|
return dates
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
from .main import MovieSearcher
|
||||||
|
import random
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return MovieSearcher()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'moviesearcher',
|
||||||
|
'order': 20,
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'name': 'movie_searcher',
|
||||||
|
'label': 'Movie search',
|
||||||
|
'description': 'Search options for movies',
|
||||||
|
'advanced': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'always_search',
|
||||||
|
'default': False,
|
||||||
|
'migrate_from': 'searcher',
|
||||||
|
'type': 'bool',
|
||||||
|
'label': 'Always search',
|
||||||
|
'description': 'Search for movies even before there is a ETA. Enabling this will probably get you a lot of fakes.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'run_on_launch',
|
||||||
|
'migrate_from': 'searcher',
|
||||||
|
'label': 'Run on launch',
|
||||||
|
'advanced': True,
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Force run the searcher after (re)start.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'search_on_add',
|
||||||
|
'label': 'Search after add',
|
||||||
|
'advanced': True,
|
||||||
|
'default': 1,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Disable this to only search for movies on cron.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'cron_day',
|
||||||
|
'migrate_from': 'searcher',
|
||||||
|
'label': 'Day',
|
||||||
|
'advanced': True,
|
||||||
|
'default': '*',
|
||||||
|
'type': 'string',
|
||||||
|
'description': '<strong>*</strong>: Every day, <strong>*/2</strong>: Every 2 days, <strong>1</strong>: Every first of the month. See <a href="http://packages.python.org/APScheduler/cronschedule.html">APScheduler</a> for details.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'cron_hour',
|
||||||
|
'migrate_from': 'searcher',
|
||||||
|
'label': 'Hour',
|
||||||
|
'advanced': True,
|
||||||
|
'default': random.randint(0, 23),
|
||||||
|
'type': 'string',
|
||||||
|
'description': '<strong>*</strong>: Every hour, <strong>*/8</strong>: Every 8 hours, <strong>3</strong>: At 3, midnight.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'cron_minute',
|
||||||
|
'migrate_from': 'searcher',
|
||||||
|
'label': 'Minute',
|
||||||
|
'advanced': True,
|
||||||
|
'default': random.randint(0, 59),
|
||||||
|
'type': 'string',
|
||||||
|
'description': "Just keep it random, so the providers don't get DDOSed by every CP user on a 'full' hour."
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,438 @@
|
|||||||
|
from couchpotato import get_session
|
||||||
|
from couchpotato.api import addApiView
|
||||||
|
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||||
|
from couchpotato.core.helpers.encoding import simplifyString, toUnicode, ss
|
||||||
|
from couchpotato.core.helpers.variable import md5, getTitle, splitString, \
|
||||||
|
possibleTitles, getImdb
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.media._base.searcher.base import SearcherBase
|
||||||
|
from couchpotato.core.media.movie import MovieTypeBase
|
||||||
|
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
from datetime import date
|
||||||
|
from sqlalchemy.exc import InterfaceError
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||||
|
|
||||||
|
in_progress = False
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(MovieSearcher, self).__init__()
|
||||||
|
|
||||||
|
addEvent('movie.searcher.all', self.searchAll)
|
||||||
|
addEvent('movie.searcher.all_view', self.searchAllView)
|
||||||
|
addEvent('movie.searcher.single', self.single)
|
||||||
|
addEvent('movie.searcher.correct_movie', self.correctMovie)
|
||||||
|
addEvent('movie.searcher.try_next_release', self.tryNextRelease)
|
||||||
|
addEvent('movie.searcher.could_be_released', self.couldBeReleased)
|
||||||
|
|
||||||
|
addApiView('movie.searcher.try_next', self.tryNextReleaseView, docs = {
|
||||||
|
'desc': 'Marks the snatched results as ignored and try the next best release',
|
||||||
|
'params': {
|
||||||
|
'id': {'desc': 'The id of the movie'},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
addApiView('movie.searcher.full_search', self.searchAllView, docs = {
|
||||||
|
'desc': 'Starts a full search for all wanted movies',
|
||||||
|
})
|
||||||
|
|
||||||
|
addApiView('movie.searcher.progress', self.getProgress, docs = {
|
||||||
|
'desc': 'Get the progress of current full search',
|
||||||
|
'return': {'type': 'object', 'example': """{
|
||||||
|
'progress': False || object, total & to_go,
|
||||||
|
}"""},
|
||||||
|
})
|
||||||
|
|
||||||
|
if self.conf('run_on_launch'):
|
||||||
|
addEvent('app.load', self.searchAll)
|
||||||
|
|
||||||
|
def searchAllView(self, **kwargs):
|
||||||
|
|
||||||
|
fireEventAsync('movie.searcher.all')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': not self.in_progress
|
||||||
|
}
|
||||||
|
|
||||||
|
def searchAll(self):
|
||||||
|
|
||||||
|
if self.in_progress:
|
||||||
|
log.info('Search already in progress')
|
||||||
|
fireEvent('notify.frontend', type = 'movie.searcher.already_started', data = True, message = 'Full search already in progress')
|
||||||
|
return
|
||||||
|
|
||||||
|
self.in_progress = True
|
||||||
|
fireEvent('notify.frontend', type = 'movie.searcher.started', data = True, message = 'Full search started')
|
||||||
|
|
||||||
|
db = get_session()
|
||||||
|
|
||||||
|
movies = db.query(Movie).filter(
|
||||||
|
Movie.status.has(identifier = 'active')
|
||||||
|
).all()
|
||||||
|
random.shuffle(movies)
|
||||||
|
|
||||||
|
self.in_progress = {
|
||||||
|
'total': len(movies),
|
||||||
|
'to_go': len(movies),
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
search_protocols = fireEvent('searcher.protocols', single = True)
|
||||||
|
|
||||||
|
for movie in movies:
|
||||||
|
movie_dict = movie.to_dict({
|
||||||
|
'category': {},
|
||||||
|
'profile': {'types': {'quality': {}}},
|
||||||
|
'releases': {'status': {}, 'quality': {}},
|
||||||
|
'library': {'titles': {}, 'files':{}},
|
||||||
|
'files': {},
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.single(movie_dict, search_protocols)
|
||||||
|
except IndexError:
|
||||||
|
log.error('Forcing library update for %s, if you see this often, please report: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
|
||||||
|
fireEvent('library.update.movie', movie_dict['library']['identifier'], force = True)
|
||||||
|
except:
|
||||||
|
log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
|
||||||
|
|
||||||
|
self.in_progress['to_go'] -= 1
|
||||||
|
|
||||||
|
# Break if CP wants to shut down
|
||||||
|
if self.shuttingDown():
|
||||||
|
break
|
||||||
|
|
||||||
|
except SearchSetupError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.in_progress = False
|
||||||
|
|
||||||
|
def single(self, movie, search_protocols = None, manual = False):
|
||||||
|
|
||||||
|
# Find out search type
|
||||||
|
try:
|
||||||
|
if not search_protocols:
|
||||||
|
search_protocols = fireEvent('searcher.protocols', single = True)
|
||||||
|
except SearchSetupError:
|
||||||
|
return
|
||||||
|
|
||||||
|
done_status = fireEvent('status.get', 'done', single = True)
|
||||||
|
|
||||||
|
if not movie['profile'] or (movie['status_id'] == done_status.get('id') and not manual):
|
||||||
|
log.debug('Movie doesn\'t have a profile or already done, assuming in manage tab.')
|
||||||
|
return
|
||||||
|
|
||||||
|
db = get_session()
|
||||||
|
|
||||||
|
pre_releases = fireEvent('quality.pre_releases', single = True)
|
||||||
|
release_dates = fireEvent('library.update.movie.release_date', identifier = movie['library']['identifier'], merge = True)
|
||||||
|
available_status, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], single = True)
|
||||||
|
|
||||||
|
found_releases = []
|
||||||
|
too_early_to_search = []
|
||||||
|
|
||||||
|
default_title = getTitle(movie['library'])
|
||||||
|
if not default_title:
|
||||||
|
log.error('No proper info found for movie, removing it from library to cause it from having more issues.')
|
||||||
|
fireEvent('movie.delete', movie['id'], single = True)
|
||||||
|
return
|
||||||
|
|
||||||
|
fireEvent('notify.frontend', type = 'movie.searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title)
|
||||||
|
|
||||||
|
|
||||||
|
ret = False
|
||||||
|
for quality_type in movie['profile']['types']:
|
||||||
|
if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']):
|
||||||
|
too_early_to_search.append(quality_type['quality']['identifier'])
|
||||||
|
continue
|
||||||
|
|
||||||
|
has_better_quality = 0
|
||||||
|
|
||||||
|
# See if better quality is available
|
||||||
|
for release in movie['releases']:
|
||||||
|
if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id'), failed_status.get('id')]:
|
||||||
|
has_better_quality += 1
|
||||||
|
|
||||||
|
# Don't search for quality lower then already available.
|
||||||
|
if has_better_quality is 0:
|
||||||
|
|
||||||
|
log.info('Search for %s in %s', (default_title, quality_type['quality']['label']))
|
||||||
|
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for search_protocol in search_protocols:
|
||||||
|
protocol_results = fireEvent('provider.search.%s.movie' % search_protocol, movie, quality, merge = True)
|
||||||
|
if protocol_results:
|
||||||
|
results += protocol_results
|
||||||
|
|
||||||
|
sorted_results = sorted(results, key = lambda k: k['score'], reverse = True)
|
||||||
|
if len(sorted_results) == 0:
|
||||||
|
log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label']))
|
||||||
|
|
||||||
|
download_preference = self.conf('preferred_method', section = 'searcher')
|
||||||
|
if download_preference != 'both':
|
||||||
|
sorted_results = sorted(sorted_results, key = lambda k: k['protocol'][:3], reverse = (download_preference == 'torrent'))
|
||||||
|
|
||||||
|
# Check if movie isn't deleted while searching
|
||||||
|
if not db.query(Movie).filter_by(id = movie.get('id')).first():
|
||||||
|
break
|
||||||
|
|
||||||
|
# Add them to this movie releases list
|
||||||
|
for nzb in sorted_results:
|
||||||
|
|
||||||
|
nzb_identifier = md5(nzb['url'])
|
||||||
|
found_releases.append(nzb_identifier)
|
||||||
|
|
||||||
|
rls = db.query(Release).filter_by(identifier = nzb_identifier).first()
|
||||||
|
if not rls:
|
||||||
|
rls = Release(
|
||||||
|
identifier = nzb_identifier,
|
||||||
|
movie_id = movie.get('id'),
|
||||||
|
quality_id = quality_type.get('quality_id'),
|
||||||
|
status_id = available_status.get('id')
|
||||||
|
)
|
||||||
|
db.add(rls)
|
||||||
|
else:
|
||||||
|
[db.delete(old_info) for old_info in rls.info]
|
||||||
|
rls.last_edit = int(time.time())
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
for info in nzb:
|
||||||
|
try:
|
||||||
|
if not isinstance(nzb[info], (str, unicode, int, long, float)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
rls_info = ReleaseInfo(
|
||||||
|
identifier = info,
|
||||||
|
value = toUnicode(nzb[info])
|
||||||
|
)
|
||||||
|
rls.info.append(rls_info)
|
||||||
|
except InterfaceError:
|
||||||
|
log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
nzb['status_id'] = rls.status_id
|
||||||
|
|
||||||
|
|
||||||
|
for nzb in sorted_results:
|
||||||
|
if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and nzb.get('age') <= quality_type.get('wait_for', 0):
|
||||||
|
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if nzb['status_id'] in [ignored_status.get('id'), failed_status.get('id')]:
|
||||||
|
log.info('Ignored: %s', nzb['name'])
|
||||||
|
continue
|
||||||
|
|
||||||
|
if nzb['score'] <= 0:
|
||||||
|
log.info('Ignored, score to low: %s', nzb['name'])
|
||||||
|
continue
|
||||||
|
|
||||||
|
downloaded = fireEvent('searcher.download', data = nzb, movie = movie, manual = manual, single = True)
|
||||||
|
if downloaded is True:
|
||||||
|
ret = True
|
||||||
|
break
|
||||||
|
elif downloaded != 'try_next':
|
||||||
|
break
|
||||||
|
|
||||||
|
# Remove releases that aren't found anymore
|
||||||
|
for release in movie.get('releases', []):
|
||||||
|
if release.get('status_id') == available_status.get('id') and release.get('identifier') not in found_releases:
|
||||||
|
fireEvent('release.delete', release.get('id'), single = True)
|
||||||
|
|
||||||
|
else:
|
||||||
|
log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title))
|
||||||
|
fireEvent('movie.restatus', movie['id'])
|
||||||
|
break
|
||||||
|
|
||||||
|
# Break if CP wants to shut down
|
||||||
|
if self.shuttingDown() or ret:
|
||||||
|
break
|
||||||
|
|
||||||
|
if len(too_early_to_search) > 0:
|
||||||
|
log.info2('Too early to search for %s, %s', (too_early_to_search, default_title))
|
||||||
|
|
||||||
|
fireEvent('notify.frontend', type = 'movie.searcher.ended.%s' % movie['id'], data = True)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def correctMovie(self, nzb = None, movie = None, quality = None, **kwargs):
|
||||||
|
|
||||||
|
imdb_results = kwargs.get('imdb_results', False)
|
||||||
|
retention = Env.setting('retention', section = 'nzb')
|
||||||
|
|
||||||
|
if nzb.get('seeders') is None and 0 < retention < nzb.get('age', 0):
|
||||||
|
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
|
||||||
|
return False
|
||||||
|
|
||||||
|
movie_name = getTitle(movie['library'])
|
||||||
|
movie_words = re.split('\W+', simplifyString(movie_name))
|
||||||
|
nzb_name = simplifyString(nzb['name'])
|
||||||
|
nzb_words = re.split('\W+', nzb_name)
|
||||||
|
|
||||||
|
# Make sure it has required words
|
||||||
|
required_words = splitString(self.conf('required_words', section = 'searcher').lower())
|
||||||
|
try: required_words = list(set(required_words + splitString(movie['category']['required'].lower())))
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
req_match = 0
|
||||||
|
for req_set in required_words:
|
||||||
|
req = splitString(req_set, '&')
|
||||||
|
req_match += len(list(set(nzb_words) & set(req))) == len(req)
|
||||||
|
|
||||||
|
if len(required_words) > 0 and req_match == 0:
|
||||||
|
log.info2('Wrong: Required word missing: %s', nzb['name'])
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Ignore releases
|
||||||
|
ignored_words = splitString(self.conf('ignored_words', section = 'searcher').lower())
|
||||||
|
try: ignored_words = list(set(ignored_words + splitString(movie['category']['ignored'].lower())))
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
ignored_match = 0
|
||||||
|
for ignored_set in ignored_words:
|
||||||
|
ignored = splitString(ignored_set, '&')
|
||||||
|
ignored_match += len(list(set(nzb_words) & set(ignored))) == len(ignored)
|
||||||
|
|
||||||
|
if len(ignored_words) > 0 and ignored_match:
|
||||||
|
log.info2("Wrong: '%s' contains 'ignored words'", (nzb['name']))
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Ignore porn stuff
|
||||||
|
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic', 'cock', 'dick']
|
||||||
|
pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words))
|
||||||
|
if pron_words:
|
||||||
|
log.info('Wrong: %s, probably pr0n', (nzb['name']))
|
||||||
|
return False
|
||||||
|
|
||||||
|
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
|
||||||
|
|
||||||
|
# Contains lower quality string
|
||||||
|
if fireEvent('searcher.contains_other_quality', nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality, single = True):
|
||||||
|
log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# File to small
|
||||||
|
if nzb['size'] and preferred_quality['size_min'] > nzb['size']:
|
||||||
|
log.info2('Wrong: "%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min']))
|
||||||
|
return False
|
||||||
|
|
||||||
|
# File to large
|
||||||
|
if nzb['size'] and preferred_quality.get('size_max') < nzb['size']:
|
||||||
|
log.info2('Wrong: "%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max']))
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Provider specific functions
|
||||||
|
get_more = nzb.get('get_more_info')
|
||||||
|
if get_more:
|
||||||
|
get_more(nzb)
|
||||||
|
|
||||||
|
extra_check = nzb.get('extra_check')
|
||||||
|
if extra_check and not extra_check(nzb):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
if imdb_results:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Check if nzb contains imdb link
|
||||||
|
if getImdb(nzb.get('description', '')) == movie['library']['identifier']:
|
||||||
|
return True
|
||||||
|
|
||||||
|
for raw_title in movie['library']['titles']:
|
||||||
|
for movie_title in possibleTitles(raw_title['title']):
|
||||||
|
movie_words = re.split('\W+', simplifyString(movie_title))
|
||||||
|
|
||||||
|
if fireEvent('searcher.correct_name', nzb['name'], movie_title, single = True):
|
||||||
|
# if no IMDB link, at least check year range 1
|
||||||
|
if len(movie_words) > 2 and fireEvent('searcher.correct_year', nzb['name'], movie['library']['year'], 1, single = True):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# if no IMDB link, at least check year
|
||||||
|
if len(movie_words) <= 2 and fireEvent('searcher.correct_year', nzb['name'], movie['library']['year'], 0, single = True):
|
||||||
|
return True
|
||||||
|
|
||||||
|
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], movie_name, movie['library']['year']))
|
||||||
|
return False
|
||||||
|
|
||||||
|
def couldBeReleased(self, is_pre_release, dates, year = None):
|
||||||
|
|
||||||
|
now = int(time.time())
|
||||||
|
now_year = date.today().year
|
||||||
|
|
||||||
|
if (year is None or year < now_year - 1) and (not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0)):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
|
||||||
|
# For movies before 1972
|
||||||
|
if not dates or dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if is_pre_release:
|
||||||
|
# Prerelease 1 week before theaters
|
||||||
|
if dates.get('theater') - 604800 < now:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# 12 weeks after theater release
|
||||||
|
if dates.get('theater') > 0 and dates.get('theater') + 7257600 < now:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if dates.get('dvd') > 0:
|
||||||
|
|
||||||
|
# 4 weeks before dvd release
|
||||||
|
if dates.get('dvd') - 2419200 < now:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Dvd should be released
|
||||||
|
if dates.get('dvd') < now:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def tryNextReleaseView(self, id = None, **kwargs):
|
||||||
|
|
||||||
|
trynext = self.tryNextRelease(id, manual = True)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': trynext
|
||||||
|
}
|
||||||
|
|
||||||
|
def tryNextRelease(self, movie_id, manual = False):
|
||||||
|
|
||||||
|
snatched_status, done_status, ignored_status = fireEvent('status.get', ['snatched', 'done', 'ignored'], single = True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db = get_session()
|
||||||
|
rels = db.query(Release) \
|
||||||
|
.filter_by(movie_id = movie_id) \
|
||||||
|
.filter(Release.status_id.in_([snatched_status.get('id'), done_status.get('id')])) \
|
||||||
|
.all()
|
||||||
|
|
||||||
|
for rel in rels:
|
||||||
|
rel.status_id = ignored_status.get('id')
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
movie_dict = fireEvent('movie.get', movie_id, single = True)
|
||||||
|
log.info('Trying next release for: %s', getTitle(movie_dict['library']))
|
||||||
|
fireEvent('movie.searcher.single', movie_dict, manual = manual)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed searching for next release: %s', traceback.format_exc())
|
||||||
|
return False
|
||||||
|
|
||||||
|
class SearchSetupError(Exception):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from migrate.changeset.schema import create_column
|
||||||
|
from sqlalchemy.schema import MetaData, Column, Table, Index
|
||||||
|
from sqlalchemy.types import Integer
|
||||||
|
|
||||||
|
meta = MetaData()
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(migrate_engine):
|
||||||
|
meta.bind = migrate_engine
|
||||||
|
|
||||||
|
# Change release, add last_edit and index
|
||||||
|
last_edit_column = Column('last_edit', Integer)
|
||||||
|
release = Table('release', meta, last_edit_column)
|
||||||
|
|
||||||
|
create_column(last_edit_column, release)
|
||||||
|
Index('ix_release_last_edit', release.c.last_edit).create()
|
||||||
|
|
||||||
|
# Change movie last_edit
|
||||||
|
last_edit_column = Column('last_edit', Integer)
|
||||||
|
movie = Table('movie', meta, last_edit_column)
|
||||||
|
Index('ix_movie_last_edit', movie.c.last_edit).create()
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade(migrate_engine):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from migrate.changeset.schema import create_column
|
||||||
|
from sqlalchemy.schema import MetaData, Column, Table, Index
|
||||||
|
from sqlalchemy.types import Integer
|
||||||
|
|
||||||
|
meta = MetaData()
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(migrate_engine):
|
||||||
|
meta.bind = migrate_engine
|
||||||
|
|
||||||
|
category_column = Column('category_id', Integer)
|
||||||
|
movie = Table('movie', meta, category_column)
|
||||||
|
create_column(category_column, movie)
|
||||||
|
Index('ix_movie_category_id', movie.c.category_id).create()
|
||||||
|
|
||||||
|
def downgrade(migrate_engine):
|
||||||
|
pass
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
config = {
|
||||||
|
'name': 'notification_providers',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'label': 'Notifications',
|
||||||
|
'description': 'Notify when movies are done or snatched',
|
||||||
|
'type': 'list',
|
||||||
|
'name': 'notification_providers',
|
||||||
|
'tab': 'notifications',
|
||||||
|
'options': [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
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.request import jsonified
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.providers.base import Provider
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Notification(Plugin):
|
class Notification(Provider):
|
||||||
|
|
||||||
|
type = 'notification'
|
||||||
|
|
||||||
default_title = Env.get('appname')
|
default_title = Env.get('appname')
|
||||||
test_message = 'ZOMG Lazors Pewpewpew!'
|
test_message = 'ZOMG Lazors Pewpewpew!'
|
||||||
@@ -16,11 +17,12 @@ class Notification(Plugin):
|
|||||||
listen_to = [
|
listen_to = [
|
||||||
'renamer.after', 'movie.snatched',
|
'renamer.after', 'movie.snatched',
|
||||||
'updater.available', 'updater.updated',
|
'updater.available', 'updater.updated',
|
||||||
|
'core.message',
|
||||||
]
|
]
|
||||||
dont_listen_to = []
|
dont_listen_to = []
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
addEvent('notify.%s' % self.getName().lower(), self.notify)
|
addEvent('notify.%s' % self.getName().lower(), self._notify)
|
||||||
|
|
||||||
addApiView(self.testNotifyName(), self.test)
|
addApiView(self.testNotifyName(), self.test)
|
||||||
|
|
||||||
@@ -30,29 +32,41 @@ class Notification(Plugin):
|
|||||||
addEvent(listener, self.createNotifyHandler(listener))
|
addEvent(listener, self.createNotifyHandler(listener))
|
||||||
|
|
||||||
def createNotifyHandler(self, listener):
|
def createNotifyHandler(self, listener):
|
||||||
def notify(message = None, group = {}, data = None):
|
def notify(message = None, group = None, data = None):
|
||||||
|
if not group: group = {}
|
||||||
|
|
||||||
if not self.conf('on_snatch', default = True) and listener == 'movie.snatched':
|
if not self.conf('on_snatch', default = True) and listener == 'movie.snatched':
|
||||||
return
|
return
|
||||||
return self.notify(message = message, data = data if data else group, listener = listener)
|
return self._notify(message = message, data = data if data else group, listener = listener)
|
||||||
|
|
||||||
return notify
|
return notify
|
||||||
|
|
||||||
def notify(self, message = '', data = {}, listener = None):
|
def getNotificationImage(self, size = 'small'):
|
||||||
pass
|
return 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/notify.couch.%s.png' % size
|
||||||
|
|
||||||
def test(self):
|
def _notify(self, *args, **kwargs):
|
||||||
|
if self.isEnabled():
|
||||||
|
return self.notify(*args, **kwargs)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def notify(self, message = '', data = None, listener = None):
|
||||||
|
if not data: data = {}
|
||||||
|
|
||||||
|
def test(self, **kwargs):
|
||||||
|
|
||||||
test_type = self.testNotifyName()
|
test_type = self.testNotifyName()
|
||||||
|
|
||||||
log.info('Sending test to %s', test_type)
|
log.info('Sending test to %s', test_type)
|
||||||
|
|
||||||
success = self.notify(
|
success = self._notify(
|
||||||
message = self.test_message,
|
message = self.test_message,
|
||||||
data = {},
|
data = {},
|
||||||
listener = 'test'
|
listener = 'test'
|
||||||
)
|
)
|
||||||
|
|
||||||
return jsonified({'success': success})
|
return {
|
||||||
|
'success': success
|
||||||
|
}
|
||||||
|
|
||||||
def testNotifyName(self):
|
def testNotifyName(self):
|
||||||
return 'notify.%s.test' % self.getName().lower()
|
return 'notify.%s.test' % self.getName().lower()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'boxcar',
|
'name': 'boxcar',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ class Boxcar(Notification):
|
|||||||
|
|
||||||
url = 'https://boxcar.io/devices/providers/7MNNXY3UIzVBwvzkKwkC/notifications'
|
url = 'https://boxcar.io/devices/providers/7MNNXY3UIzVBwvzkKwkC/notifications'
|
||||||
|
|
||||||
def notify(self, message = '', data = {}, listener = None):
|
def notify(self, message = '', data = None, listener = None):
|
||||||
if self.isDisabled(): return
|
if not data: data = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
message = message.strip()
|
message = message.strip()
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
from couchpotato import get_session
|
from couchpotato import get_session
|
||||||
from couchpotato.api import addApiView, addNonBlockApiView
|
from couchpotato.api import addApiView, addNonBlockApiView
|
||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.helpers.request import jsonified, getParam
|
|
||||||
from couchpotato.core.helpers.variable import tryInt, splitString
|
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
from couchpotato.core.settings.model import Notification as Notif
|
from couchpotato.core.settings.model import Notification as Notif
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
from operator import itemgetter
|
||||||
from sqlalchemy.sql.expression import or_
|
from sqlalchemy.sql.expression import or_
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -17,14 +19,7 @@ log = CPLog(__name__)
|
|||||||
|
|
||||||
class CoreNotifier(Notification):
|
class CoreNotifier(Notification):
|
||||||
|
|
||||||
m_lock = threading.Lock()
|
m_lock = None
|
||||||
messages = []
|
|
||||||
listeners = []
|
|
||||||
|
|
||||||
listen_to = [
|
|
||||||
'renamer.after', 'movie.snatched',
|
|
||||||
'updater.available', 'updater.updated',
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(CoreNotifier, self).__init__()
|
super(CoreNotifier, self).__init__()
|
||||||
@@ -54,7 +49,15 @@ class CoreNotifier(Notification):
|
|||||||
addNonBlockApiView('notification.listener', (self.addListener, self.removeListener))
|
addNonBlockApiView('notification.listener', (self.addListener, self.removeListener))
|
||||||
addApiView('notification.listener', self.listener)
|
addApiView('notification.listener', self.listener)
|
||||||
|
|
||||||
|
fireEvent('schedule.interval', 'core.check_messages', self.checkMessages, hours = 12, single = True)
|
||||||
|
fireEvent('schedule.interval', 'core.clean_messages', self.cleanMessages, seconds = 15, single = True)
|
||||||
|
|
||||||
addEvent('app.load', self.clean)
|
addEvent('app.load', self.clean)
|
||||||
|
addEvent('app.load', self.checkMessages)
|
||||||
|
|
||||||
|
self.messages = []
|
||||||
|
self.listeners = []
|
||||||
|
self.m_lock = threading.Lock()
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
|
||||||
@@ -63,11 +66,9 @@ class CoreNotifier(Notification):
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
def markAsRead(self):
|
def markAsRead(self, ids = None, **kwargs):
|
||||||
|
|
||||||
ids = None
|
ids = splitString(ids) if ids else None
|
||||||
if getParam('ids'):
|
|
||||||
ids = splitString(getParam('ids'))
|
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
@@ -80,14 +81,13 @@ class CoreNotifier(Notification):
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True
|
'success': True
|
||||||
})
|
}
|
||||||
|
|
||||||
def listView(self):
|
def listView(self, limit_offset = None, **kwargs):
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
limit_offset = getParam('limit_offset', None)
|
|
||||||
|
|
||||||
q = db.query(Notif)
|
q = db.query(Notif)
|
||||||
|
|
||||||
@@ -106,13 +106,30 @@ class CoreNotifier(Notification):
|
|||||||
ndict['type'] = 'notification'
|
ndict['type'] = 'notification'
|
||||||
notifications.append(ndict)
|
notifications.append(ndict)
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'empty': len(notifications) == 0,
|
'empty': len(notifications) == 0,
|
||||||
'notifications': notifications
|
'notifications': notifications
|
||||||
})
|
}
|
||||||
|
|
||||||
def notify(self, message = '', data = {}, listener = None):
|
def checkMessages(self):
|
||||||
|
|
||||||
|
prop_name = 'messages.last_check'
|
||||||
|
last_check = tryInt(Env.prop(prop_name, default = 0))
|
||||||
|
|
||||||
|
messages = fireEvent('cp.messages', last_check = last_check, single = True) or []
|
||||||
|
|
||||||
|
for message in messages:
|
||||||
|
if message.get('time') > last_check:
|
||||||
|
fireEvent('core.message', message = message.get('message'), data = message)
|
||||||
|
|
||||||
|
if last_check < message.get('time'):
|
||||||
|
last_check = message.get('time')
|
||||||
|
|
||||||
|
Env.prop(prop_name, value = last_check)
|
||||||
|
|
||||||
|
def notify(self, message = '', data = None, listener = None):
|
||||||
|
if not data: data = {}
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
@@ -133,7 +150,10 @@ class CoreNotifier(Notification):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def frontend(self, type = 'notification', data = {}, message = None):
|
def frontend(self, type = 'notification', data = None, message = None):
|
||||||
|
if not data: data = {}
|
||||||
|
|
||||||
|
log.debug('Notifying frontend')
|
||||||
|
|
||||||
self.m_lock.acquire()
|
self.m_lock.acquire()
|
||||||
notification = {
|
notification = {
|
||||||
@@ -153,10 +173,12 @@ class CoreNotifier(Notification):
|
|||||||
'result': [notification],
|
'result': [notification],
|
||||||
})
|
})
|
||||||
except:
|
except:
|
||||||
break
|
log.debug('Failed sending to listener: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
self.listeners = []
|
||||||
self.m_lock.release()
|
self.m_lock.release()
|
||||||
self.cleanMessages()
|
|
||||||
|
log.debug('Done notifying frontend')
|
||||||
|
|
||||||
def addListener(self, callback, last_id = None):
|
def addListener(self, callback, last_id = None):
|
||||||
|
|
||||||
@@ -168,59 +190,72 @@ class CoreNotifier(Notification):
|
|||||||
'result': messages,
|
'result': messages,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
self.m_lock.acquire()
|
||||||
self.listeners.append((callback, last_id))
|
self.listeners.append((callback, last_id))
|
||||||
|
self.m_lock.release()
|
||||||
|
|
||||||
|
|
||||||
def removeListener(self, callback):
|
def removeListener(self, callback):
|
||||||
|
|
||||||
|
self.m_lock.acquire()
|
||||||
for list_tuple in self.listeners:
|
for list_tuple in self.listeners:
|
||||||
try:
|
try:
|
||||||
listener, last_id = list_tuple
|
listener, last_id = list_tuple
|
||||||
if listener == callback:
|
if listener == callback:
|
||||||
self.listeners.remove(list_tuple)
|
self.listeners.remove(list_tuple)
|
||||||
except:
|
except:
|
||||||
pass
|
log.debug('Failed removing listener: %s', traceback.format_exc())
|
||||||
|
|
||||||
def cleanMessages(self):
|
|
||||||
self.m_lock.acquire()
|
|
||||||
|
|
||||||
for message in self.messages:
|
|
||||||
if message['time'] < (time.time() - 15):
|
|
||||||
self.messages.remove(message)
|
|
||||||
|
|
||||||
self.m_lock.release()
|
self.m_lock.release()
|
||||||
|
|
||||||
|
def cleanMessages(self):
|
||||||
|
|
||||||
|
if len(self.messages) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
log.debug('Cleaning messages')
|
||||||
|
self.m_lock.acquire()
|
||||||
|
|
||||||
|
time_ago = (time.time() - 15)
|
||||||
|
self.messages[:] = [m for m in self.messages if (m['time'] > time_ago)]
|
||||||
|
|
||||||
|
self.m_lock.release()
|
||||||
|
log.debug('Done cleaning messages')
|
||||||
|
|
||||||
def getMessages(self, last_id):
|
def getMessages(self, last_id):
|
||||||
|
|
||||||
|
log.debug('Getting messages with id: %s', last_id)
|
||||||
self.m_lock.acquire()
|
self.m_lock.acquire()
|
||||||
|
|
||||||
recent = []
|
recent = []
|
||||||
index = 0
|
try:
|
||||||
for i in xrange(len(self.messages)):
|
index = map(itemgetter('message_id'), self.messages).index(last_id)
|
||||||
index = len(self.messages) - i - 1
|
recent = self.messages[index + 1:]
|
||||||
if self.messages[index]["message_id"] == last_id: break
|
except:
|
||||||
recent = self.messages[index:]
|
pass
|
||||||
|
|
||||||
self.m_lock.release()
|
self.m_lock.release()
|
||||||
|
log.debug('Returning for %s %s messages', (last_id, len(recent)))
|
||||||
|
|
||||||
return recent or []
|
return recent
|
||||||
|
|
||||||
def listener(self):
|
def listener(self, init = False, **kwargs):
|
||||||
|
|
||||||
messages = []
|
messages = []
|
||||||
|
|
||||||
# Get unread
|
# Get unread
|
||||||
if getParam('init'):
|
if init:
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
notifications = db.query(Notif) \
|
notifications = db.query(Notif) \
|
||||||
.filter(or_(Notif.read == False, Notif.added > (time.time() - 259200))) \
|
.filter(or_(Notif.read == False, Notif.added > (time.time() - 259200))) \
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
for n in notifications:
|
for n in notifications:
|
||||||
ndict = n.to_dict()
|
ndict = n.to_dict()
|
||||||
ndict['type'] = 'notification'
|
ndict['type'] = 'notification'
|
||||||
messages.append(ndict)
|
messages.append(ndict)
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'result': messages,
|
'result': messages,
|
||||||
})
|
}
|
||||||
|
|||||||
@@ -21,21 +21,18 @@ var NotificationBase = new Class({
|
|||||||
App.addEvent('load', function(){
|
App.addEvent('load', function(){
|
||||||
|
|
||||||
App.block.notification = new Block.Menu(self, {
|
App.block.notification = new Block.Menu(self, {
|
||||||
|
'button_class': 'icon2.eye-open',
|
||||||
'class': 'notification_menu',
|
'class': 'notification_menu',
|
||||||
'onOpen': self.markAsRead.bind(self)
|
'onOpen': self.markAsRead.bind(self)
|
||||||
})
|
})
|
||||||
$(App.block.notification).inject(App.getBlock('search'), 'after');
|
$(App.block.notification).inject(App.getBlock('search'), 'after');
|
||||||
self.badge = new Element('div.badge').inject(App.block.notification, 'top').hide();
|
self.badge = new Element('div.badge').inject(App.block.notification, 'top').hide();
|
||||||
|
|
||||||
/* App.getBlock('notification').addLink(new Element('a.more', {
|
|
||||||
'href': App.createUrl('notifications'),
|
|
||||||
'text': 'Show older notifications'
|
|
||||||
})); */
|
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEvent('load', function(){
|
window.addEvent('load', function(){
|
||||||
self.startInterval.delay(Browser.safari ? 100 : 0, self)
|
self.startInterval.delay($(window).getSize().x <= 480 ? 2000 : 100, self);
|
||||||
});
|
})
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -47,14 +44,19 @@ var NotificationBase = new Class({
|
|||||||
|
|
||||||
result.el = App.getBlock('notification').addLink(
|
result.el = App.getBlock('notification').addLink(
|
||||||
new Element('span.'+(result.read ? 'read' : '' )).adopt(
|
new Element('span.'+(result.read ? 'read' : '' )).adopt(
|
||||||
new Element('span.message', {'text': result.message}),
|
new Element('span.message', {'html': result.message}),
|
||||||
new Element('span.added', {'text': added.timeDiffInWords(), 'title': added})
|
new Element('span.added', {'text': added.timeDiffInWords(), 'title': added})
|
||||||
)
|
)
|
||||||
, 'top');
|
, 'top');
|
||||||
self.notifications.include(result);
|
self.notifications.include(result);
|
||||||
|
|
||||||
if(!result.read)
|
if(result.data.important !== undefined && !result.read){
|
||||||
|
var sticky = true
|
||||||
|
App.fireEvent('message', [result.message, sticky, result])
|
||||||
|
}
|
||||||
|
else if(!result.read){
|
||||||
self.setBadge(self.notifications.filter(function(n){ return !n.read}).length)
|
self.setBadge(self.notifications.filter(function(n){ return !n.read}).length)
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -64,20 +66,26 @@ var NotificationBase = new Class({
|
|||||||
self.badge[value ? 'show' : 'hide']()
|
self.badge[value ? 'show' : 'hide']()
|
||||||
},
|
},
|
||||||
|
|
||||||
markAsRead: function(){
|
markAsRead: function(force_ids){
|
||||||
var self = this;
|
var self = this,
|
||||||
|
ids = force_ids;
|
||||||
|
|
||||||
var rn = self.notifications.filter(function(n){
|
if(!force_ids) {
|
||||||
return !n.read
|
var rn = self.notifications.filter(function(n){
|
||||||
})
|
return !n.read && n.data.important === undefined
|
||||||
|
})
|
||||||
|
|
||||||
var ids = []
|
var ids = []
|
||||||
rn.each(function(n){
|
rn.each(function(n){
|
||||||
ids.include(n.id)
|
ids.include(n.id)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if(ids.length > 0)
|
if(ids.length > 0)
|
||||||
Api.request('notification.markread', {
|
Api.request('notification.markread', {
|
||||||
|
'data': {
|
||||||
|
'ids': ids.join(',')
|
||||||
|
},
|
||||||
'onSuccess': function(){
|
'onSuccess': function(){
|
||||||
self.setBadge('')
|
self.setBadge('')
|
||||||
}
|
}
|
||||||
@@ -93,11 +101,20 @@ var NotificationBase = new Class({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Api.request('notification.listener', {
|
self.request = Api.request('notification.listener', {
|
||||||
'data': {'init':true},
|
'data': {'init':true},
|
||||||
'onSuccess': self.processData.bind(self)
|
'onSuccess': self.processData.bind(self)
|
||||||
}).send()
|
}).send()
|
||||||
|
|
||||||
|
setInterval(function(){
|
||||||
|
|
||||||
|
if(self.request && self.request.isRunning()){
|
||||||
|
self.request.cancel();
|
||||||
|
self.startPoll()
|
||||||
|
}
|
||||||
|
|
||||||
|
}, 120000);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
startPoll: function(){
|
startPoll: function(){
|
||||||
@@ -140,29 +157,44 @@ var NotificationBase = new Class({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Restart poll
|
// Restart poll
|
||||||
self.startPoll()
|
self.startPoll.delay(1500, self);
|
||||||
},
|
},
|
||||||
|
|
||||||
showMessage: function(message){
|
showMessage: function(message, sticky, data){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if(!self.message_container)
|
if(!self.message_container)
|
||||||
self.message_container = new Element('div.messages').inject(document.body);
|
self.message_container = new Element('div.messages').inject(document.body);
|
||||||
|
|
||||||
var new_message = new Element('div.message', {
|
var new_message = new Element('div', {
|
||||||
'text': message
|
'class': 'message' + (sticky ? ' sticky' : ''),
|
||||||
}).inject(self.message_container);
|
'html': message
|
||||||
|
}).inject(self.message_container, 'top');
|
||||||
|
|
||||||
setTimeout(function(){
|
setTimeout(function(){
|
||||||
new_message.addClass('show')
|
new_message.addClass('show')
|
||||||
}, 10);
|
}, 10);
|
||||||
|
|
||||||
setTimeout(function(){
|
var hide_message = function(){
|
||||||
new_message.addClass('hide')
|
new_message.addClass('hide')
|
||||||
setTimeout(function(){
|
setTimeout(function(){
|
||||||
new_message.destroy();
|
new_message.destroy();
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}, 4000);
|
}
|
||||||
|
|
||||||
|
if(sticky)
|
||||||
|
new_message.grab(
|
||||||
|
new Element('a.close.icon2', {
|
||||||
|
'events': {
|
||||||
|
'click': function(){
|
||||||
|
self.markAsRead([data.id]);
|
||||||
|
hide_message();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
else
|
||||||
|
setTimeout(hide_message, 4000);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -178,11 +210,14 @@ var NotificationBase = new Class({
|
|||||||
},
|
},
|
||||||
|
|
||||||
addTestButton: function(fieldset, plugin_name){
|
addTestButton: function(fieldset, plugin_name){
|
||||||
var self = this;
|
var self = this,
|
||||||
|
button_name = self.testButtonName(fieldset);
|
||||||
|
|
||||||
|
if(button_name.contains('Notifications')) return;
|
||||||
|
|
||||||
new Element('.ctrlHolder.test_button').adopt(
|
new Element('.ctrlHolder.test_button').adopt(
|
||||||
new Element('a.button', {
|
new Element('a.button', {
|
||||||
'text': self.testButtonName(fieldset),
|
'text': button_name,
|
||||||
'events': {
|
'events': {
|
||||||
'click': function(){
|
'click': function(){
|
||||||
var button = fieldset.getElement('.test_button .button');
|
var button = fieldset.getElement('.test_button .button');
|
||||||
@@ -191,7 +226,7 @@ var NotificationBase = new Class({
|
|||||||
Api.request('notify.'+plugin_name+'.test', {
|
Api.request('notify.'+plugin_name+'.test', {
|
||||||
'onComplete': function(json){
|
'onComplete': function(json){
|
||||||
|
|
||||||
button.set('text', self.testButtonName(fieldset));
|
button.set('text', button_name);
|
||||||
|
|
||||||
if(json.success){
|
if(json.success){
|
||||||
var message = new Element('span.success', {
|
var message = new Element('span.success', {
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
from .main import Email
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Email()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'email',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
|
'name': 'email',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'from',
|
||||||
|
'label': 'Send e-mail from',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'to',
|
||||||
|
'label': 'Send e-mail to',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'smtp_server',
|
||||||
|
'label': 'SMTP server',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'ssl',
|
||||||
|
'label': 'Enable SSL',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'smtp_user',
|
||||||
|
'label': 'SMTP user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'smtp_pass',
|
||||||
|
'label': 'SMTP password',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'on_snatch',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Also send message when movie is snatched.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
|
from couchpotato.core.helpers.variable import splitString
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.notifications.base import Notification
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
import smtplib
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Email(Notification):
|
||||||
|
|
||||||
|
def notify(self, message = '', data = None, listener = None):
|
||||||
|
if not data: data = {}
|
||||||
|
|
||||||
|
# Extract all the settings from settings
|
||||||
|
from_address = self.conf('from')
|
||||||
|
to_address = self.conf('to')
|
||||||
|
ssl = self.conf('ssl')
|
||||||
|
smtp_server = self.conf('smtp_server')
|
||||||
|
smtp_user = self.conf('smtp_user')
|
||||||
|
smtp_pass = self.conf('smtp_pass')
|
||||||
|
|
||||||
|
# Make the basic message
|
||||||
|
message = MIMEText(toUnicode(message))
|
||||||
|
message['Subject'] = self.default_title
|
||||||
|
message['From'] = from_address
|
||||||
|
message['To'] = to_address
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Open the SMTP connection, via SSL if requested
|
||||||
|
log.debug("SMTP over SSL %s", ("enabled" if ssl == 1 else "disabled"))
|
||||||
|
mailserver = smtplib.SMTP_SSL(smtp_server) if ssl == 1 else smtplib.SMTP(smtp_server)
|
||||||
|
|
||||||
|
# Check too see if an login attempt should be attempted
|
||||||
|
if len(smtp_user) > 0:
|
||||||
|
log.debug("Logging on to SMTP server using username \'%s\'%s", (smtp_user, " and a password" if len(smtp_pass) > 0 else ""))
|
||||||
|
mailserver.login(smtp_user, smtp_pass)
|
||||||
|
|
||||||
|
# Send the e-mail
|
||||||
|
log.debug("Sending the email")
|
||||||
|
mailserver.sendmail(from_address, splitString(to_address), message.as_string())
|
||||||
|
|
||||||
|
# Close the SMTP connection
|
||||||
|
mailserver.quit()
|
||||||
|
|
||||||
|
log.info('Email notification sent')
|
||||||
|
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
log.error('E-mail failed: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
return False
|
||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'growl',
|
'name': 'growl',
|
||||||
'description': 'Version 1.4+',
|
'description': 'Version 1.4+',
|
||||||
'options': [
|
'options': [
|
||||||
|
|||||||
@@ -37,11 +37,14 @@ class Growl(Notification):
|
|||||||
)
|
)
|
||||||
self.growl.register()
|
self.growl.register()
|
||||||
self.registered = True
|
self.registered = True
|
||||||
except:
|
except Exception, e:
|
||||||
log.error('Failed register of growl: %s', traceback.format_exc())
|
if 'timed out' in str(e):
|
||||||
|
self.registered = True
|
||||||
|
else:
|
||||||
|
log.error('Failed register of growl: %s', traceback.format_exc())
|
||||||
|
|
||||||
def notify(self, message = '', data = {}, listener = None):
|
def notify(self, message = '', data = None, listener = None):
|
||||||
if self.isDisabled(): return
|
if not data: data = {}
|
||||||
|
|
||||||
self.register()
|
self.register()
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'nmj',
|
'name': 'nmj',
|
||||||
'label': 'NMJ',
|
'label': 'NMJ',
|
||||||
'options': [
|
'options': [
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
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.encoding import tryUrlencode
|
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||||
from couchpotato.core.helpers.request import getParams, jsonified
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
import re
|
import re
|
||||||
@@ -22,21 +21,17 @@ class NMJ(Notification):
|
|||||||
addApiView(self.testNotifyName(), self.test)
|
addApiView(self.testNotifyName(), self.test)
|
||||||
addApiView('notify.nmj.auto_config', self.autoConfig)
|
addApiView('notify.nmj.auto_config', self.autoConfig)
|
||||||
|
|
||||||
def autoConfig(self):
|
def autoConfig(self, host = 'localhost', **kwargs):
|
||||||
|
|
||||||
params = getParams()
|
|
||||||
host = params.get('host', 'localhost')
|
|
||||||
|
|
||||||
database = ''
|
|
||||||
mount = ''
|
mount = ''
|
||||||
|
|
||||||
try:
|
try:
|
||||||
terminal = telnetlib.Telnet(host)
|
terminal = telnetlib.Telnet(host)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.error('Warning: unable to get a telnet session to %s', (host))
|
log.error('Warning: unable to get a telnet session to %s', host)
|
||||||
return self.failed()
|
return self.failed()
|
||||||
|
|
||||||
log.debug('Connected to %s via telnet', (host))
|
log.debug('Connected to %s via telnet', host)
|
||||||
terminal.read_until('sh-3.00# ')
|
terminal.read_until('sh-3.00# ')
|
||||||
terminal.write('cat /tmp/source\n')
|
terminal.write('cat /tmp/source\n')
|
||||||
terminal.write('cat /tmp/netshare\n')
|
terminal.write('cat /tmp/netshare\n')
|
||||||
@@ -50,7 +45,7 @@ class NMJ(Notification):
|
|||||||
device = match.group(2)
|
device = match.group(2)
|
||||||
log.info('Found NMJ database %s on device %s', (database, device))
|
log.info('Found NMJ database %s on device %s', (database, device))
|
||||||
else:
|
else:
|
||||||
log.error('Could not get current NMJ database on %s, NMJ is probably not running!', (host))
|
log.error('Could not get current NMJ database on %s, NMJ is probably not running!', host)
|
||||||
return self.failed()
|
return self.failed()
|
||||||
|
|
||||||
if device.startswith('NETWORK_SHARE/'):
|
if device.startswith('NETWORK_SHARE/'):
|
||||||
@@ -58,28 +53,29 @@ class NMJ(Notification):
|
|||||||
|
|
||||||
if match:
|
if match:
|
||||||
mount = match.group().replace('127.0.0.1', host)
|
mount = match.group().replace('127.0.0.1', host)
|
||||||
log.info('Found mounting url on the Popcorn Hour in configuration: %s', (mount))
|
log.info('Found mounting url on the Popcorn Hour in configuration: %s', mount)
|
||||||
else:
|
else:
|
||||||
log.error('Detected a network share on the Popcorn Hour, but could not get the mounting url')
|
log.error('Detected a network share on the Popcorn Hour, but could not get the mounting url')
|
||||||
return self.failed()
|
return self.failed()
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'database': database,
|
'database': database,
|
||||||
'mount': mount,
|
'mount': mount,
|
||||||
})
|
}
|
||||||
|
|
||||||
def addToLibrary(self, message = None, group = {}):
|
def addToLibrary(self, message = None, group = None):
|
||||||
if self.isDisabled(): return
|
if self.isDisabled(): return
|
||||||
|
if not group: group = {}
|
||||||
|
|
||||||
host = self.conf('host')
|
host = self.conf('host')
|
||||||
mount = self.conf('mount')
|
mount = self.conf('mount')
|
||||||
database = self.conf('database')
|
database = self.conf('database')
|
||||||
|
|
||||||
if mount:
|
if mount:
|
||||||
log.debug('Try to mount network drive via url: %s', (mount))
|
log.debug('Try to mount network drive via url: %s', mount)
|
||||||
try:
|
try:
|
||||||
data = self.urlopen(mount)
|
self.urlopen(mount)
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -102,20 +98,24 @@ class NMJ(Notification):
|
|||||||
et = etree.fromstring(response)
|
et = etree.fromstring(response)
|
||||||
result = et.findtext('returnValue')
|
result = et.findtext('returnValue')
|
||||||
except SyntaxError, e:
|
except SyntaxError, e:
|
||||||
log.error('Unable to parse XML returned from the Popcorn Hour: %s', (e))
|
log.error('Unable to parse XML returned from the Popcorn Hour: %s', e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if int(result) > 0:
|
if int(result) > 0:
|
||||||
log.error('Popcorn Hour returned an errorcode: %s', (result))
|
log.error('Popcorn Hour returned an errorcode: %s', result)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
log.info('NMJ started background scan')
|
log.info('NMJ started background scan')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def failed(self):
|
def failed(self):
|
||||||
return jsonified({'success': False})
|
return {
|
||||||
|
'success': False
|
||||||
|
}
|
||||||
|
|
||||||
def test(self):
|
def test(self, **kwargs):
|
||||||
return jsonified({'success': self.addToLibrary()})
|
return {
|
||||||
|
'success': self.addToLibrary()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'notifo',
|
'name': 'notifo',
|
||||||
'description': 'Keep in mind that Notifo service will end soon.',
|
'description': 'Keep in mind that Notifo service will end soon.',
|
||||||
'options': [
|
'options': [
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
from flask.helpers import json
|
|
||||||
import base64
|
import base64
|
||||||
|
import json
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -12,8 +12,8 @@ class Notifo(Notification):
|
|||||||
|
|
||||||
url = 'https://api.notifo.com/v1/send_notification'
|
url = 'https://api.notifo.com/v1/send_notification'
|
||||||
|
|
||||||
def notify(self, message = '', data = {}, listener = None):
|
def notify(self, message = '', data = None, listener = None):
|
||||||
if self.isDisabled(): return
|
if not data: data = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
params = {
|
params = {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'notifymyandroid',
|
'name': 'notifymyandroid',
|
||||||
'label': 'Notify My Android',
|
'label': 'Notify My Android',
|
||||||
'options': [
|
'options': [
|
||||||
|
|||||||
@@ -8,20 +8,17 @@ log = CPLog(__name__)
|
|||||||
|
|
||||||
class NotifyMyAndroid(Notification):
|
class NotifyMyAndroid(Notification):
|
||||||
|
|
||||||
def notify(self, message = '', data = {}, listener = None):
|
def notify(self, message = '', data = None, listener = None):
|
||||||
if self.isDisabled(): return
|
if not data: data = {}
|
||||||
|
|
||||||
nma = pynma.PyNMA()
|
nma = pynma.PyNMA()
|
||||||
keys = splitString(self.conf('api_key'))
|
keys = splitString(self.conf('api_key'))
|
||||||
nma.addkey(keys)
|
nma.addkey(keys)
|
||||||
nma.developerkey(self.conf('dev_key'))
|
nma.developerkey(self.conf('dev_key'))
|
||||||
|
|
||||||
# hacky fix for the event type
|
|
||||||
# as it seems to be part of the message now
|
|
||||||
self.event = message.split(' ')[0]
|
|
||||||
response = nma.push(
|
response = nma.push(
|
||||||
application = self.default_title,
|
application = self.default_title,
|
||||||
event = self.event,
|
event = message.split(' ')[0],
|
||||||
description = message,
|
description = message,
|
||||||
priority = self.conf('priority'),
|
priority = self.conf('priority'),
|
||||||
batch_mode = len(keys) > 1
|
batch_mode = len(keys) > 1
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'notifymywp',
|
'name': 'notifymywp',
|
||||||
'label': 'Notify My Windows Phone',
|
'label': 'Windows Phone',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'enabled',
|
'name': 'enabled',
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ log = CPLog(__name__)
|
|||||||
class NotifyMyWP(Notification):
|
class NotifyMyWP(Notification):
|
||||||
|
|
||||||
def notify(self, message = '', data = {}, listener = None):
|
def notify(self, message = '', data = {}, listener = None):
|
||||||
if self.isDisabled(): return
|
|
||||||
|
|
||||||
keys = splitString(self.conf('api_key'))
|
keys = splitString(self.conf('api_key'))
|
||||||
p = PyNMWP(keys, self.conf('dev_key'))
|
p = PyNMWP(keys, self.conf('dev_key'))
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'plex',
|
'name': 'plex',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
@@ -21,6 +22,13 @@ config = [{
|
|||||||
'description': 'Default should be on localhost',
|
'description': 'Default should be on localhost',
|
||||||
'advanced': True,
|
'advanced': True,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'on_snatch',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Also send message when movie is snatched.',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||||
from couchpotato.core.helpers.request import jsonified
|
from couchpotato.core.helpers.variable import cleanHost, splitString
|
||||||
from couchpotato.core.helpers.variable import cleanHost
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
from urllib2 import URLError
|
from urllib2 import URLError
|
||||||
|
from urlparse import urlparse
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
@@ -17,16 +17,17 @@ class Plex(Notification):
|
|||||||
super(Plex, self).__init__()
|
super(Plex, self).__init__()
|
||||||
addEvent('renamer.after', self.addToLibrary)
|
addEvent('renamer.after', self.addToLibrary)
|
||||||
|
|
||||||
def addToLibrary(self, message = None, group = {}):
|
def addToLibrary(self, message = None, group = None):
|
||||||
if self.isDisabled(): return
|
if self.isDisabled(): return
|
||||||
|
if not group: group = {}
|
||||||
|
|
||||||
log.info('Sending notification to Plex')
|
log.info('Sending notification to Plex')
|
||||||
hosts = [cleanHost(x.strip() + ':32400') for x in self.conf('host').split(",")]
|
hosts = self.getHosts(port = 32400)
|
||||||
|
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
|
|
||||||
source_type = ['movie']
|
source_type = ['movie']
|
||||||
base_url = '%slibrary/sections' % host
|
base_url = '%s/library/sections' % host
|
||||||
refresh_url = '%s/%%s/refresh' % base_url
|
refresh_url = '%s/%%s/refresh' % base_url
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -37,7 +38,7 @@ class Plex(Notification):
|
|||||||
for s in sections:
|
for s in sections:
|
||||||
if s.getAttribute('type') in source_type:
|
if s.getAttribute('type') in source_type:
|
||||||
url = refresh_url % s.getAttribute('key')
|
url = refresh_url % s.getAttribute('key')
|
||||||
x = self.urlopen(url)
|
self.urlopen(url)
|
||||||
|
|
||||||
except:
|
except:
|
||||||
log.error('Plex library update failed for %s, Media Server not running: %s', (host, traceback.format_exc(1)))
|
log.error('Plex library update failed for %s, Media Server not running: %s', (host, traceback.format_exc(1)))
|
||||||
@@ -45,10 +46,10 @@ class Plex(Notification):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def notify(self, message = '', data = {}, listener = None):
|
def notify(self, message = '', data = None, listener = None):
|
||||||
if self.isDisabled(): return
|
if not data: data = {}
|
||||||
|
|
||||||
hosts = [x.strip() + ':3000' for x in self.conf('host').split(",")]
|
hosts = self.getHosts(port = 3000)
|
||||||
successful = 0
|
successful = 0
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host):
|
if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host):
|
||||||
@@ -58,8 +59,7 @@ class Plex(Notification):
|
|||||||
|
|
||||||
def send(self, command, host):
|
def send(self, command, host):
|
||||||
|
|
||||||
url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command))
|
url = '%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command))
|
||||||
|
|
||||||
headers = {}
|
headers = {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -74,7 +74,7 @@ class Plex(Notification):
|
|||||||
log.info('Plex notification to %s successful.', host)
|
log.info('Plex notification to %s successful.', host)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def test(self):
|
def test(self, **kwargs):
|
||||||
|
|
||||||
test_type = self.testNotifyName()
|
test_type = self.testNotifyName()
|
||||||
|
|
||||||
@@ -87,4 +87,21 @@ class Plex(Notification):
|
|||||||
)
|
)
|
||||||
success2 = self.addToLibrary()
|
success2 = self.addToLibrary()
|
||||||
|
|
||||||
return jsonified({'success': success or success2})
|
return {
|
||||||
|
'success': success or success2
|
||||||
|
}
|
||||||
|
|
||||||
|
def getHosts(self, port = None):
|
||||||
|
|
||||||
|
raw_hosts = splitString(self.conf('host'))
|
||||||
|
hosts = []
|
||||||
|
|
||||||
|
for h in raw_hosts:
|
||||||
|
h = cleanHost(h)
|
||||||
|
p = urlparse(h)
|
||||||
|
h = h.rstrip('/')
|
||||||
|
if port and not p.port:
|
||||||
|
h += ':%s' % port
|
||||||
|
hosts.append(h)
|
||||||
|
|
||||||
|
return hosts
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'prowl',
|
'name': 'prowl',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ class Prowl(Notification):
|
|||||||
'api': 'https://api.prowlapp.com/publicapi/add'
|
'api': 'https://api.prowlapp.com/publicapi/add'
|
||||||
}
|
}
|
||||||
|
|
||||||
def notify(self, message = '', data = {}, listener = None):
|
def notify(self, message = '', data = None, listener = None):
|
||||||
if self.isDisabled(): return
|
if not data: data = {}
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'apikey': self.conf('api_key'),
|
'apikey': self.conf('api_key'),
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
from .main import Pushalot
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Pushalot()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'pushalot',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
|
'name': 'pushalot',
|
||||||
|
'description': 'for Windows Phone and Windows 8',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'auth_token',
|
||||||
|
'label': 'Auth Token',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'silent',
|
||||||
|
'label': 'Silent',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Don\'t send Toast notifications. Only update Live Tile',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'important',
|
||||||
|
'label': 'High Priority',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Send message with High priority.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'on_snatch',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Also send message when movie is snatched.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.notifications.base import Notification
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
class Pushalot(Notification):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'api': 'https://pushalot.com/api/sendmessage'
|
||||||
|
}
|
||||||
|
|
||||||
|
def notify(self, message = '', data = None, listener = None):
|
||||||
|
if not data: data = {}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'AuthorizationToken': self.conf('auth_token'),
|
||||||
|
'Title': self.default_title,
|
||||||
|
'Body': toUnicode(message),
|
||||||
|
'IsImportant': self.conf('important'),
|
||||||
|
'IsSilent': self.conf('silent'),
|
||||||
|
'Image': toUnicode(self.getNotificationImage('medium') + '?1'),
|
||||||
|
'Source': toUnicode(self.default_title)
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-type': 'application/x-www-form-urlencoded'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
log.error('PushAlot failed: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
return False
|
||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'pushover',
|
'name': 'pushover',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||||
|
from couchpotato.core.helpers.variable import getTitle
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
from httplib import HTTPSConnection
|
from httplib import HTTPSConnection
|
||||||
@@ -10,22 +11,28 @@ class Pushover(Notification):
|
|||||||
|
|
||||||
app_token = 'YkxHMYDZp285L265L3IwH3LmzkTaCy'
|
app_token = 'YkxHMYDZp285L265L3IwH3LmzkTaCy'
|
||||||
|
|
||||||
def notify(self, message = '', data = {}, listener = None):
|
def notify(self, message = '', data = None, listener = None):
|
||||||
if self.isDisabled(): return
|
if not data: data = {}
|
||||||
|
|
||||||
http_handler = HTTPSConnection("api.pushover.net:443")
|
http_handler = HTTPSConnection("api.pushover.net:443")
|
||||||
|
|
||||||
data = {
|
api_data = {
|
||||||
'user': self.conf('user_key'),
|
'user': self.conf('user_key'),
|
||||||
'token': self.app_token,
|
'token': self.app_token,
|
||||||
'message': toUnicode(message),
|
'message': toUnicode(message),
|
||||||
'priority': self.conf('priority')
|
'priority': self.conf('priority'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if data and data.get('library'):
|
||||||
|
api_data.update({
|
||||||
|
'url': toUnicode('http://www.imdb.com/title/%s/' % data['library']['identifier']),
|
||||||
|
'url_title': toUnicode('%s on IMDb' % getTitle(data['library'])),
|
||||||
|
})
|
||||||
|
|
||||||
http_handler.request('POST',
|
http_handler.request('POST',
|
||||||
"/1/messages.json",
|
"/1/messages.json",
|
||||||
headers = {'Content-type': 'application/x-www-form-urlencoded'},
|
headers = {'Content-type': 'application/x-www-form-urlencoded'},
|
||||||
body = tryUrlencode(data)
|
body = tryUrlencode(api_data)
|
||||||
)
|
)
|
||||||
|
|
||||||
response = http_handler.getresponse()
|
response = http_handler.getresponse()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'synoindex',
|
'name': 'synoindex',
|
||||||
'description': 'Automaticly adds index to Synology Media Server.',
|
'description': 'Automaticly adds index to Synology Media Server.',
|
||||||
'options': [
|
'options': [
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.request import jsonified
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
import os
|
import os
|
||||||
@@ -16,8 +15,9 @@ class Synoindex(Notification):
|
|||||||
super(Synoindex, self).__init__()
|
super(Synoindex, self).__init__()
|
||||||
addEvent('renamer.after', self.addToLibrary)
|
addEvent('renamer.after', self.addToLibrary)
|
||||||
|
|
||||||
def addToLibrary(self, message = None, group = {}):
|
def addToLibrary(self, message = None, group = None):
|
||||||
if self.isDisabled(): return
|
if self.isDisabled(): return
|
||||||
|
if not group: group = {}
|
||||||
|
|
||||||
command = [self.index_path, '-A', group.get('destination_dir')]
|
command = [self.index_path, '-A', group.get('destination_dir')]
|
||||||
log.info('Executing synoindex command: %s ', command)
|
log.info('Executing synoindex command: %s ', command)
|
||||||
@@ -28,9 +28,10 @@ class Synoindex(Notification):
|
|||||||
return True
|
return True
|
||||||
except OSError, e:
|
except OSError, e:
|
||||||
log.error('Unable to run synoindex: %s', e)
|
log.error('Unable to run synoindex: %s', e)
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
return False
|
||||||
|
|
||||||
def test(self):
|
def test(self, **kwargs):
|
||||||
return jsonified({'success': os.path.isfile(self.index_path)})
|
return {
|
||||||
|
'success': os.path.isfile(self.index_path)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
from .main import Toasty
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Toasty()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'toasty',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
|
'name': 'toasty',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'api_key',
|
||||||
|
'label': 'Device ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'on_snatch',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Also send message when movie is snatched.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.notifications.base import Notification
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
class Toasty(Notification):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'api': 'http://api.supertoasty.com/notify/%s?%s'
|
||||||
|
}
|
||||||
|
|
||||||
|
def notify(self, message = '', data = None, listener = None):
|
||||||
|
if not data: data = {}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'title': self.default_title,
|
||||||
|
'text': toUnicode(message),
|
||||||
|
'sender': toUnicode("CouchPotato"),
|
||||||
|
'image': 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/homescreen.png',
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.urlopen(self.urls['api'] % (self.conf('api_key'), tryUrlencode(data)), show_error = False)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
log.error('Toasty failed: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
return False
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from .main import Trakt
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Trakt()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'trakt',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
|
'name': 'trakt',
|
||||||
|
'label': 'Trakt',
|
||||||
|
'description': 'add movies to your collection once downloaded. Fill in your username and password in the <a href="../automation/">Automation Trakt settings</a>',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'notification_enabled',
|
||||||
|
'default': False,
|
||||||
|
'type': 'enabler',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'remove_watchlist_enabled',
|
||||||
|
'label': 'Remove from watchlist',
|
||||||
|
'default': False,
|
||||||
|
'type': 'bool',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.notifications.base import Notification
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
class Trakt(Notification):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'base': 'http://api.trakt.tv/%s',
|
||||||
|
'library': 'movie/library/%s',
|
||||||
|
'unwatchlist': 'movie/unwatchlist/%s',
|
||||||
|
}
|
||||||
|
|
||||||
|
listen_to = ['movie.downloaded']
|
||||||
|
|
||||||
|
def notify(self, message = '', data = None, listener = None):
|
||||||
|
if not data: data = {}
|
||||||
|
|
||||||
|
post_data = {
|
||||||
|
'username': self.conf('automation_username'),
|
||||||
|
'password' : self.conf('automation_password'),
|
||||||
|
'movies': [{
|
||||||
|
'imdb_id': data['library']['identifier'],
|
||||||
|
'title': data['library']['titles'][0]['title'],
|
||||||
|
'year': data['library']['year']
|
||||||
|
}] if data else []
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self.call((self.urls['library'] % self.conf('automation_api_key')), post_data)
|
||||||
|
if self.conf('remove_watchlist_enabled'):
|
||||||
|
result = result and self.call((self.urls['unwatchlist'] % self.conf('automation_api_key')), post_data)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def call(self, method_url, post_data):
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.getJsonData(self.urls['base'] % method_url, params = post_data, cache_timeout = 1)
|
||||||
|
if response:
|
||||||
|
if response.get('status') == "success":
|
||||||
|
log.info('Successfully called Trakt')
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
log.error('Failed to call trakt, check your login.')
|
||||||
|
return False
|
||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'twitter',
|
'name': 'twitter',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||||
from couchpotato.core.helpers.request import jsonified, getParam
|
|
||||||
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
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
from flask.helpers import url_for
|
from couchpotato.environment import Env
|
||||||
from pytwitter import Api, parse_qsl
|
from pytwitter import Api
|
||||||
from werkzeug.utils import redirect
|
from urlparse import parse_qsl
|
||||||
import oauth2
|
import oauth2
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -31,8 +30,8 @@ class Twitter(Notification):
|
|||||||
addApiView('notify.%s.auth_url' % self.getName().lower(), self.getAuthorizationUrl)
|
addApiView('notify.%s.auth_url' % self.getName().lower(), self.getAuthorizationUrl)
|
||||||
addApiView('notify.%s.credentials' % self.getName().lower(), self.getCredentials)
|
addApiView('notify.%s.credentials' % self.getName().lower(), self.getCredentials)
|
||||||
|
|
||||||
def notify(self, message = '', data = {}, listener = None):
|
def notify(self, message = '', data = None, listener = None):
|
||||||
if self.isDisabled(): return
|
if not data: data = {}
|
||||||
|
|
||||||
api = Api(self.consumer_key, self.consumer_secret, self.conf('access_token_key'), self.conf('access_token_secret'))
|
api = Api(self.consumer_key, self.consumer_secret, self.conf('access_token_key'), self.conf('access_token_secret'))
|
||||||
|
|
||||||
@@ -53,7 +52,7 @@ class Twitter(Notification):
|
|||||||
try:
|
try:
|
||||||
if direct_message:
|
if direct_message:
|
||||||
for user in direct_message_users.split():
|
for user in direct_message_users.split():
|
||||||
api.PostDirectMessage(user, '[%s] %s' % (self.default_title, message))
|
api.PostDirectMessage('[%s] %s' % (self.default_title, message), screen_name = user)
|
||||||
else:
|
else:
|
||||||
update_message = '[%s] %s' % (self.default_title, message)
|
update_message = '[%s] %s' % (self.default_title, message)
|
||||||
if len(update_message) > 140:
|
if len(update_message) > 140:
|
||||||
@@ -71,10 +70,9 @@ class Twitter(Notification):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def getAuthorizationUrl(self):
|
def getAuthorizationUrl(self, host = None, **kwargs):
|
||||||
|
|
||||||
referer = getParam('host')
|
callback_url = cleanHost(host) + '%snotify.%s.credentials/' % (Env.get('api_base').lstrip('/'), self.getName().lower())
|
||||||
callback_url = cleanHost(referer) + '%snotify.%s.credentials/' % (url_for('api.index').lstrip('/'), self.getName().lower())
|
|
||||||
|
|
||||||
oauth_consumer = oauth2.Consumer(self.consumer_key, self.consumer_secret)
|
oauth_consumer = oauth2.Consumer(self.consumer_key, self.consumer_secret)
|
||||||
oauth_client = oauth2.Client(oauth_consumer)
|
oauth_client = oauth2.Client(oauth_consumer)
|
||||||
@@ -83,31 +81,29 @@ class Twitter(Notification):
|
|||||||
|
|
||||||
if resp['status'] != '200':
|
if resp['status'] != '200':
|
||||||
log.error('Invalid response from Twitter requesting temp token: %s', resp['status'])
|
log.error('Invalid response from Twitter requesting temp token: %s', resp['status'])
|
||||||
return jsonified({
|
return {
|
||||||
'success': False,
|
'success': False,
|
||||||
})
|
}
|
||||||
else:
|
else:
|
||||||
self.request_token = dict(parse_qsl(content))
|
self.request_token = dict(parse_qsl(content))
|
||||||
|
|
||||||
auth_url = self.urls['authorize'] + ("?oauth_token=%s" % self.request_token['oauth_token'])
|
auth_url = self.urls['authorize'] + ("?oauth_token=%s" % self.request_token['oauth_token'])
|
||||||
|
|
||||||
log.info('Redirecting to "%s"', auth_url)
|
log.info('Redirecting to "%s"', auth_url)
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'url': auth_url,
|
'url': auth_url,
|
||||||
})
|
}
|
||||||
|
|
||||||
def getCredentials(self):
|
def getCredentials(self, oauth_verifier, **kwargs):
|
||||||
|
|
||||||
key = getParam('oauth_verifier')
|
|
||||||
|
|
||||||
token = oauth2.Token(self.request_token['oauth_token'], self.request_token['oauth_token_secret'])
|
token = oauth2.Token(self.request_token['oauth_token'], self.request_token['oauth_token_secret'])
|
||||||
token.set_verifier(key)
|
token.set_verifier(oauth_verifier)
|
||||||
|
|
||||||
oauth_consumer = oauth2.Consumer(key = self.consumer_key, secret = self.consumer_secret)
|
oauth_consumer = oauth2.Consumer(key = self.consumer_key, secret = self.consumer_secret)
|
||||||
oauth_client = oauth2.Client(oauth_consumer, token)
|
oauth_client = oauth2.Client(oauth_consumer, token)
|
||||||
|
|
||||||
resp, content = oauth_client.request(self.urls['access'], method = 'POST', body = 'oauth_verifier=%s' % key)
|
resp, content = oauth_client.request(self.urls['access'], method = 'POST', body = 'oauth_verifier=%s' % oauth_verifier)
|
||||||
access_token = dict(parse_qsl(content))
|
access_token = dict(parse_qsl(content))
|
||||||
|
|
||||||
if resp['status'] != '200':
|
if resp['status'] != '200':
|
||||||
@@ -122,4 +118,4 @@ class Twitter(Notification):
|
|||||||
|
|
||||||
self.request_token = None
|
self.request_token = None
|
||||||
|
|
||||||
return redirect(url_for('web.index') + 'settings/notifications/')
|
return 'redirect', Env.get('web_base') + 'settings/notifications/'
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'xbmc',
|
'name': 'xbmc',
|
||||||
'label': 'XBMC',
|
'label': 'XBMC',
|
||||||
|
'description': 'v11 (Eden) and v12 (Frodo)',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'enabled',
|
'name': 'enabled',
|
||||||
@@ -29,6 +31,28 @@ config = [{
|
|||||||
'default': '',
|
'default': '',
|
||||||
'type': 'password',
|
'type': 'password',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'only_first',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Only update the first host when movie snatched, useful for synced XBMC',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'remote_dir_scan',
|
||||||
|
'label': 'Remote Folder Scan',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Only scan new movie folder at remote XBMC servers. Works if movie location is the same.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'on_snatch',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Also send message when movie is snatched.',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,36 +1,182 @@
|
|||||||
from couchpotato.core.helpers.variable import splitString
|
from couchpotato.core.helpers.variable import splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
from flask.helpers import json
|
from urllib2 import URLError
|
||||||
import base64
|
import base64
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
import traceback
|
import traceback
|
||||||
|
import urllib
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class XBMC(Notification):
|
class XBMC(Notification):
|
||||||
|
|
||||||
listen_to = ['renamer.after']
|
listen_to = ['renamer.after', 'movie.snatched']
|
||||||
|
use_json_notifications = {}
|
||||||
|
http_time_between_calls = 0
|
||||||
|
|
||||||
def notify(self, message = '', data = {}, listener = None):
|
def notify(self, message = '', data = None, listener = None):
|
||||||
if self.isDisabled(): return
|
if not data: data = {}
|
||||||
|
|
||||||
hosts = splitString(self.conf('host'))
|
hosts = splitString(self.conf('host'))
|
||||||
|
|
||||||
successful = 0
|
successful = 0
|
||||||
|
max_successful = 0
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
response = self.request(host, [
|
|
||||||
('GUI.ShowNotification', {"title":"CouchPotato", "message":message}),
|
if self.use_json_notifications.get(host) is None:
|
||||||
('VideoLibrary.Scan', {}),
|
self.getXBMCJSONversion(host, message = message)
|
||||||
])
|
|
||||||
|
if self.use_json_notifications.get(host):
|
||||||
|
calls = [
|
||||||
|
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}),
|
||||||
|
]
|
||||||
|
|
||||||
|
if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0):
|
||||||
|
param = {}
|
||||||
|
if self.conf('remote_dir_scan') or socket.getfqdn('localhost') == socket.getfqdn(host.split(':')[0]):
|
||||||
|
param = {'directory': data['destination_dir']}
|
||||||
|
|
||||||
|
calls.append(('VideoLibrary.Scan', param))
|
||||||
|
|
||||||
|
max_successful += len(calls)
|
||||||
|
response = self.request(host, calls)
|
||||||
|
else:
|
||||||
|
response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
|
||||||
|
|
||||||
|
if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0):
|
||||||
|
response += self.request(host, [('VideoLibrary.Scan', {})])
|
||||||
|
max_successful += 1
|
||||||
|
|
||||||
|
max_successful += 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for result in response:
|
for result in response:
|
||||||
if result['result'] == "OK":
|
if result.get('result') and result['result'] == 'OK':
|
||||||
successful += 1
|
successful += 1
|
||||||
|
elif result.get('error'):
|
||||||
|
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
|
||||||
|
|
||||||
except:
|
except:
|
||||||
log.error('Failed parsing results: %s', traceback.format_exc())
|
log.error('Failed parsing results: %s', traceback.format_exc())
|
||||||
|
|
||||||
return successful == len(hosts) * 2
|
return successful == max_successful
|
||||||
|
|
||||||
|
def getXBMCJSONversion(self, host, message = ''):
|
||||||
|
|
||||||
|
success = False
|
||||||
|
|
||||||
|
# XBMC JSON-RPC version request
|
||||||
|
response = self.request(host, [
|
||||||
|
('JSONRPC.Version', {})
|
||||||
|
])
|
||||||
|
for result in response:
|
||||||
|
if result.get('result') and type(result['result']['version']).__name__ == 'int':
|
||||||
|
# only v2 and v4 return an int object
|
||||||
|
# v6 (as of XBMC v12(Frodo)) is required to send notifications
|
||||||
|
xbmc_rpc_version = str(result['result']['version'])
|
||||||
|
|
||||||
|
log.debug('XBMC JSON-RPC Version: %s ; Notifications by JSON-RPC only supported for v6 [as of XBMC v12(Frodo)]', xbmc_rpc_version)
|
||||||
|
|
||||||
|
# disable JSON use
|
||||||
|
self.use_json_notifications[host] = False
|
||||||
|
|
||||||
|
# send the text message
|
||||||
|
resp = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
|
||||||
|
for result in resp:
|
||||||
|
if result.get('result') and result['result'] == 'OK':
|
||||||
|
log.debug('Message delivered successfully!')
|
||||||
|
success = True
|
||||||
|
break
|
||||||
|
elif result.get('error'):
|
||||||
|
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
|
||||||
|
break
|
||||||
|
|
||||||
|
elif result.get('result') and type(result['result']['version']).__name__ == 'dict':
|
||||||
|
# XBMC JSON-RPC v6 returns an array object containing
|
||||||
|
# major, minor and patch number
|
||||||
|
xbmc_rpc_version = str(result['result']['version']['major'])
|
||||||
|
xbmc_rpc_version += '.' + str(result['result']['version']['minor'])
|
||||||
|
xbmc_rpc_version += '.' + str(result['result']['version']['patch'])
|
||||||
|
|
||||||
|
log.debug('XBMC JSON-RPC Version: %s', xbmc_rpc_version)
|
||||||
|
|
||||||
|
# ok, XBMC version is supported
|
||||||
|
self.use_json_notifications[host] = True
|
||||||
|
|
||||||
|
# send the text message
|
||||||
|
resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image': self.getNotificationImage('small')})])
|
||||||
|
for result in resp:
|
||||||
|
if result.get('result') and result['result'] == 'OK':
|
||||||
|
log.debug('Message delivered successfully!')
|
||||||
|
success = True
|
||||||
|
break
|
||||||
|
elif result.get('error'):
|
||||||
|
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
|
||||||
|
break
|
||||||
|
|
||||||
|
# error getting version info (we do have contact with XBMC though)
|
||||||
|
elif result.get('error'):
|
||||||
|
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
|
||||||
|
|
||||||
|
log.debug('Use JSON notifications: %s ', self.use_json_notifications)
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
def notifyXBMCnoJSON(self, host, data):
|
||||||
|
|
||||||
|
server = 'http://%s/xbmcCmds/' % host
|
||||||
|
|
||||||
|
# Notification(title, message [, timeout , image])
|
||||||
|
cmd = "xbmcHttp?command=ExecBuiltIn(Notification(%s,%s,'',%s))" % (urllib.quote(data['title']), urllib.quote(data['message']), urllib.quote(self.getNotificationImage('medium')))
|
||||||
|
server += cmd
|
||||||
|
|
||||||
|
# I have no idea what to set to, just tried text/plain and seems to be working :)
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
}
|
||||||
|
|
||||||
|
# authentication support
|
||||||
|
if self.conf('password'):
|
||||||
|
base64string = base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password'))).replace('\n', '')
|
||||||
|
headers['Authorization'] = 'Basic %s' % base64string
|
||||||
|
|
||||||
|
try:
|
||||||
|
log.debug('Sending non-JSON-type request to %s: %s', (host, data))
|
||||||
|
|
||||||
|
# response wil either be 'OK':
|
||||||
|
# <html>
|
||||||
|
# <li>OK
|
||||||
|
# </html>
|
||||||
|
#
|
||||||
|
# or 'Error':
|
||||||
|
# <html>
|
||||||
|
# <li>Error:<message>
|
||||||
|
# </html>
|
||||||
|
#
|
||||||
|
response = self.urlopen(server, headers = headers, timeout = 3, show_error = False)
|
||||||
|
|
||||||
|
if 'OK' in response:
|
||||||
|
log.debug('Returned from non-JSON-type request %s: %s', (host, response))
|
||||||
|
# manually fake expected response array
|
||||||
|
return [{'result': 'OK'}]
|
||||||
|
else:
|
||||||
|
log.error('Returned from non-JSON-type request %s: %s', (host, response))
|
||||||
|
# manually fake expected response array
|
||||||
|
return [{'result': 'Error'}]
|
||||||
|
|
||||||
|
except URLError, e:
|
||||||
|
if isinstance(e.reason, socket.timeout):
|
||||||
|
log.info('Couldn\'t send request to XBMC, assuming it\'s turned off')
|
||||||
|
return [{'result': 'Error'}]
|
||||||
|
else:
|
||||||
|
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
|
||||||
|
return [{'result': 'Error'}]
|
||||||
|
except:
|
||||||
|
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
|
||||||
|
return [{'result': 'Error'}]
|
||||||
|
|
||||||
def request(self, host, requests):
|
def request(self, host, requests):
|
||||||
server = 'http://%s/jsonrpc' % host
|
server = 'http://%s/jsonrpc' % host
|
||||||
@@ -56,11 +202,17 @@ class XBMC(Notification):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
log.debug('Sending request to %s: %s', (host, data))
|
log.debug('Sending request to %s: %s', (host, data))
|
||||||
rdata = self.urlopen(server, headers = headers, params = data, multipart = True)
|
response = self.getJsonData(server, headers = headers, params = data, timeout = 3, show_error = False)
|
||||||
response = json.loads(rdata)
|
|
||||||
log.debug('Returned from request %s: %s', (host, response))
|
log.debug('Returned from request %s: %s', (host, response))
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
except URLError, e:
|
||||||
|
if isinstance(e.reason, socket.timeout):
|
||||||
|
log.info('Couldn\'t send request to XBMC, assuming it\'s turned off')
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
|
||||||
|
return []
|
||||||
except:
|
except:
|
||||||
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
|
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
|
||||||
return []
|
return []
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user