Compare commits
732 Commits
build/2.0.
...
build/2.2.
| 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 | ||
|
|
4db1b57c70 | ||
|
|
b06dbd3069 | ||
|
|
f84aa8c638 | ||
|
|
8e07dfc730 | ||
|
|
a49a00a25f | ||
|
|
673843fb66 | ||
|
|
811f35b028 | ||
|
|
ec6e2c240f | ||
|
|
9e260a89af | ||
|
|
d233e4d22e | ||
|
|
23893dbcb9 | ||
|
|
3187a0f820 | ||
|
|
f86b9299c4 | ||
|
|
d27d0abeb0 | ||
|
|
506871b506 | ||
|
|
6115917660 | ||
|
|
21df8819d3 | ||
|
|
7c59348138 | ||
|
|
ab53f44157 | ||
|
|
b35f325d94 | ||
|
|
fb3f3e11f6 | ||
|
|
178c8942c3 | ||
|
|
393c14de54 | ||
|
|
51e747049d | ||
|
|
0582f7d694 | ||
|
|
fa7cac7538 | ||
|
|
bff17c0b95 | ||
|
|
d172828ac5 | ||
|
|
9500ac73fc | ||
|
|
e2cf7e4421 | ||
|
|
9a314cfbc4 | ||
|
|
5941d0bf77 | ||
|
|
d326c1c25c | ||
|
|
7e6234298d | ||
|
|
d4da206f93 | ||
|
|
985a168724 | ||
|
|
173c6194ed | ||
|
|
bcd23ad10c | ||
|
|
898e6f487d | ||
|
|
96472a9a8f | ||
|
|
27252561e2 | ||
|
|
6618c3927c | ||
|
|
c9e732651f | ||
|
|
7849e7170d | ||
|
|
087894eb4e | ||
|
|
4b58b40226 | ||
|
|
3ecc826629 | ||
|
|
25f1b8c7a7 | ||
|
|
e71da1f14d | ||
|
|
938b14ba18 | ||
|
|
d6522d8f38 | ||
|
|
78eab890e7 | ||
|
|
1a56191f83 | ||
|
|
41c0f34d95 | ||
|
|
37bf205d7a | ||
|
|
32fe3796e4 | ||
|
|
359d1aaafa | ||
|
|
fb5d336351 | ||
|
|
eb30dff986 | ||
|
|
9312336962 | ||
|
|
aa1fa3eb9a | ||
|
|
0e2f8a612c | ||
|
|
c087a6b49b | ||
|
|
ade4338ea6 | ||
|
|
55b20324c0 | ||
|
|
465e7b2abc | ||
|
|
578fb45785 | ||
|
|
c0fb28301d | ||
|
|
96995bbbe5 | ||
|
|
4cfdafebbc | ||
|
|
f9c2503f81 | ||
|
|
b97acb8ef5 | ||
|
|
5b4cdf05b1 | ||
|
|
d68d2dfdb6 | ||
|
|
39b269a454 | ||
|
|
ac081d3e10 | ||
|
|
5d4efb60cf | ||
|
|
6f25a6bdfd | ||
|
|
23427e95f7 | ||
|
|
cc408b980c | ||
|
|
90a09e573b | ||
|
|
e1d7440b9d | ||
|
|
59590b3ac9 | ||
|
|
ff759dacf3 | ||
|
|
a328e44130 | ||
|
|
7924cac5f9 | ||
|
|
1cef3b0c93 | ||
|
|
3cd59edc8b | ||
|
|
0d624af01d | ||
|
|
a09132570c | ||
|
|
ee3fc38432 | ||
|
|
dbf0192c8e | ||
|
|
6962cfc3f5 | ||
|
|
e096ec3b5b | ||
|
|
b30a74ae0c | ||
|
|
978eeb16c9 | ||
|
|
e5c9d91657 | ||
|
|
fa81c3a07a | ||
|
|
9cdd520d41 | ||
|
|
55d7898771 | ||
|
|
b8256bef97 | ||
|
|
5be9dc0b4a | ||
|
|
7d0be0cefb | ||
|
|
f7ce1edb13 | ||
|
|
5ad9280b60 | ||
|
|
2b353f1b20 | ||
|
|
75ab90b87b | ||
|
|
0219296120 | ||
|
|
20032b3a31 | ||
|
|
ea9e9a8c90 | ||
|
|
f7b0ee145b | ||
|
|
cc866738ee | ||
|
|
eadccf6e33 | ||
|
|
b70b66e567 | ||
|
|
5b6792dc20 | ||
|
|
f498e7343a | ||
|
|
6962f441e6 | ||
|
|
1def62b1b1 | ||
|
|
a4a4a6a185 | ||
|
|
d4c9469c1a | ||
|
|
3e2d4c5d7b | ||
|
|
d03f711d69 | ||
|
|
44dd8d9b96 | ||
|
|
549a3be0d8 | ||
|
|
1bb2edf8ec | ||
|
|
84c6f36315 |
231
Desktop.py
Normal file
231
Desktop.py
Normal file
@@ -0,0 +1,231 @@
|
||||
from esky.util import appdir_from_executable #@UnresolvedImport
|
||||
from threading import Thread
|
||||
from version import VERSION
|
||||
from wx.lib.softwareupdate import SoftwareUpdate
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import webbrowser
|
||||
import wx
|
||||
|
||||
# Include proper dirs
|
||||
if hasattr(sys, 'frozen'):
|
||||
import libs
|
||||
base_path = os.path.dirname(os.path.dirname(os.path.abspath(libs.__file__)))
|
||||
else:
|
||||
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
lib_dir = os.path.join(base_path, 'libs')
|
||||
|
||||
sys.path.insert(0, base_path)
|
||||
sys.path.insert(0, lib_dir)
|
||||
|
||||
from couchpotato.environment import Env
|
||||
|
||||
class TaskBarIcon(wx.TaskBarIcon):
|
||||
|
||||
TBMENU_OPEN = wx.NewId()
|
||||
TBMENU_SETTINGS = wx.NewId()
|
||||
TBMENU_EXIT = wx.ID_EXIT
|
||||
|
||||
closed = False
|
||||
menu = False
|
||||
enabled = False
|
||||
|
||||
def __init__(self, frame):
|
||||
wx.TaskBarIcon.__init__(self)
|
||||
self.frame = frame
|
||||
|
||||
icon = wx.Icon('icon.png', wx.BITMAP_TYPE_PNG)
|
||||
self.SetIcon(icon)
|
||||
|
||||
self.Bind(wx.EVT_TASKBAR_LEFT_UP, self.OnTaskBarClick)
|
||||
self.Bind(wx.EVT_TASKBAR_RIGHT_UP, self.OnTaskBarClick)
|
||||
|
||||
self.Bind(wx.EVT_MENU, self.onOpen, id = self.TBMENU_OPEN)
|
||||
self.Bind(wx.EVT_MENU, self.onSettings, id = self.TBMENU_SETTINGS)
|
||||
self.Bind(wx.EVT_MENU, self.onTaskBarClose, id = self.TBMENU_EXIT)
|
||||
|
||||
def OnTaskBarClick(self, evt):
|
||||
menu = self.CreatePopupMenu()
|
||||
self.PopupMenu(menu)
|
||||
menu.Destroy()
|
||||
|
||||
def enable(self):
|
||||
self.enabled = True
|
||||
|
||||
if self.menu:
|
||||
self.open_menu.Enable(True)
|
||||
self.setting_menu.Enable(True)
|
||||
|
||||
self.open_menu.SetText('Open')
|
||||
|
||||
def CreatePopupMenu(self):
|
||||
|
||||
if not self.menu:
|
||||
self.menu = wx.Menu()
|
||||
self.open_menu = self.menu.Append(self.TBMENU_OPEN, 'Open')
|
||||
self.setting_menu = self.menu.Append(self.TBMENU_SETTINGS, 'About')
|
||||
self.exit_menu = self.menu.Append(self.TBMENU_EXIT, 'Quit')
|
||||
|
||||
if not self.enabled:
|
||||
self.open_menu.Enable(False)
|
||||
self.setting_menu.Enable(False)
|
||||
|
||||
self.open_menu.SetText('Loading...')
|
||||
|
||||
return self.menu
|
||||
|
||||
def onOpen(self, event):
|
||||
url = self.frame.parent.getSetting('base_url')
|
||||
webbrowser.open(url)
|
||||
|
||||
def onSettings(self, event):
|
||||
url = self.frame.parent.getSetting('base_url') + 'settings/about/'
|
||||
webbrowser.open(url)
|
||||
|
||||
def onTaskBarClose(self, evt):
|
||||
if self.closed:
|
||||
return
|
||||
|
||||
self.closed = True
|
||||
|
||||
self.RemoveIcon()
|
||||
wx.CallAfter(self.frame.Close)
|
||||
|
||||
|
||||
def makeIcon(self, img):
|
||||
if "wxMSW" in wx.PlatformInfo:
|
||||
img = img.Scale(16, 16)
|
||||
elif "wxGTK" in wx.PlatformInfo:
|
||||
img = img.Scale(22, 22)
|
||||
|
||||
icon = wx.IconFromBitmap(img.CopyFromBitmap())
|
||||
return icon
|
||||
|
||||
|
||||
class MainFrame(wx.Frame):
|
||||
|
||||
def __init__(self, parent):
|
||||
wx.Frame.__init__(self, None, style = wx.FRAME_NO_TASKBAR)
|
||||
|
||||
self.parent = parent
|
||||
self.tbicon = TaskBarIcon(self)
|
||||
|
||||
|
||||
class WorkerThread(Thread):
|
||||
|
||||
def __init__(self, desktop):
|
||||
Thread.__init__(self)
|
||||
self.daemon = True
|
||||
self._desktop = desktop
|
||||
|
||||
self.start()
|
||||
|
||||
def run(self):
|
||||
|
||||
# Get options via arg
|
||||
from couchpotato.runner import getOptions
|
||||
args = ['--quiet']
|
||||
self.options = getOptions(base_path, args)
|
||||
|
||||
# Load settings
|
||||
settings = Env.get('settings')
|
||||
settings.setFile(self.options.config_file)
|
||||
|
||||
# Create data dir if needed
|
||||
self.data_dir = os.path.expanduser(Env.setting('data_dir'))
|
||||
if self.data_dir == '':
|
||||
from couchpotato.core.helpers.variable import getDataDir
|
||||
self.data_dir = getDataDir()
|
||||
|
||||
if not os.path.isdir(self.data_dir):
|
||||
os.makedirs(self.data_dir)
|
||||
|
||||
# Create logging dir
|
||||
self.log_dir = os.path.join(self.data_dir, 'logs');
|
||||
if not os.path.isdir(self.log_dir):
|
||||
os.mkdir(self.log_dir)
|
||||
|
||||
try:
|
||||
from couchpotato.runner import runCouchPotato
|
||||
runCouchPotato(self.options, base_path, args, data_dir = self.data_dir, log_dir = self.log_dir, Env = Env, desktop = self._desktop)
|
||||
except:
|
||||
pass
|
||||
|
||||
self._desktop.frame.Close()
|
||||
|
||||
|
||||
class CouchPotatoApp(wx.App, SoftwareUpdate):
|
||||
|
||||
settings = {}
|
||||
events = {}
|
||||
restart = False
|
||||
closing = False
|
||||
|
||||
def OnInit(self):
|
||||
|
||||
# Updater
|
||||
base_url = 'https://couchpota.to/updates/%s'
|
||||
self.InitUpdates(base_url % VERSION + '/', base_url % 'changelog.html',
|
||||
icon = wx.Icon('icon.png'))
|
||||
|
||||
self.frame = MainFrame(self)
|
||||
self.frame.Bind(wx.EVT_CLOSE, self.onClose)
|
||||
|
||||
# CouchPotato thread
|
||||
self.worker = WorkerThread(self)
|
||||
|
||||
return True
|
||||
|
||||
def onAppLoad(self):
|
||||
self.frame.tbicon.enable()
|
||||
|
||||
def setSettings(self, settings = {}):
|
||||
self.settings = settings
|
||||
|
||||
def getSetting(self, name):
|
||||
return self.settings.get(name)
|
||||
|
||||
def addEvents(self, events = {}):
|
||||
for name in events.iterkeys():
|
||||
self.events[name] = events[name]
|
||||
|
||||
def onClose(self, event):
|
||||
|
||||
if not self.closing:
|
||||
self.closing = True
|
||||
self.frame.tbicon.onTaskBarClose(event)
|
||||
|
||||
onClose = self.events.get('onClose')
|
||||
onClose(event)
|
||||
|
||||
def afterShutdown(self, restart = False):
|
||||
self.frame.Destroy()
|
||||
self.restart = restart
|
||||
self.ExitMainLoop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
app = CouchPotatoApp(redirect = False)
|
||||
app.MainLoop()
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
if app.restart:
|
||||
|
||||
def appexe_from_executable(exepath):
|
||||
appdir = appdir_from_executable(exepath)
|
||||
exename = os.path.basename(exepath)
|
||||
|
||||
if sys.platform == "darwin":
|
||||
if os.path.isdir(os.path.join(appdir, "Contents", "MacOS")):
|
||||
return os.path.join(appdir, "Contents", "MacOS", exename)
|
||||
|
||||
return os.path.join(appdir, exename)
|
||||
|
||||
exe = appexe_from_executable(sys.executable)
|
||||
os.chdir(os.path.dirname(exe))
|
||||
|
||||
os.execv(exe, [exe] + sys.argv[1:])
|
||||
20
README.md
20
README.md
@@ -40,3 +40,23 @@ Linux (ubuntu / debian):
|
||||
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
|
||||
* Add it to defaults. `sudo update-rc.d couchpotato defaults`
|
||||
* 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/`
|
||||
|
||||
@@ -12,4 +12,4 @@
|
||||
* 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 ;)
|
||||
|
||||
**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 ;)**
|
||||
|
||||
@@ -1,84 +1,139 @@
|
||||
from couchpotato.api import api_docs, api_docs_missing
|
||||
from couchpotato.core.auth import requires_auth
|
||||
from couchpotato.api import api_docs, api_docs_missing, api
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.request import getParams, jsonified
|
||||
from couchpotato.core.helpers.variable import md5
|
||||
from couchpotato.core.helpers.variable import md5, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from flask.app import Flask
|
||||
from flask.blueprints import Blueprint
|
||||
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
|
||||
from tornado import template
|
||||
from tornado.web import RequestHandler, authenticated
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
|
||||
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):
|
||||
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.route('/')
|
||||
@requires_auth
|
||||
# Web view
|
||||
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 """
|
||||
@web.route('docs/')
|
||||
@requires_auth
|
||||
# API docs
|
||||
def apiDocs():
|
||||
from couchpotato import app
|
||||
routes = []
|
||||
for route, x in sorted(app.view_functions.iteritems()):
|
||||
if route[0:4] == 'api.':
|
||||
routes += [route[4:].replace('::', '.')]
|
||||
|
||||
for route in api.iterkeys():
|
||||
routes.append(route)
|
||||
|
||||
if api_docs.get(''):
|
||||
del api_docs['']
|
||||
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/')
|
||||
def getApiKey():
|
||||
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)
|
||||
|
||||
api = None
|
||||
params = getParams()
|
||||
username = Env.setting('username')
|
||||
password = Env.setting('password')
|
||||
addView('docs', apiDocs)
|
||||
|
||||
if (params.get('u') == md5(username) or not username) and (params.get('p') == password or not password):
|
||||
api = Env.setting('api_key')
|
||||
# Make non basic auth option to get api key
|
||||
class KeyHandler(RequestHandler):
|
||||
def get(self, *args, **kwargs):
|
||||
api = None
|
||||
|
||||
return jsonified({
|
||||
'success': api is not None,
|
||||
'api_key': api
|
||||
})
|
||||
try:
|
||||
username = Env.setting('username')
|
||||
password = Env.setting('password')
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(error):
|
||||
index_url = url_for('web.index')
|
||||
url = request.path[len(index_url):]
|
||||
if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password):
|
||||
api = Env.setting('api_key')
|
||||
|
||||
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 request.path != '/':
|
||||
r = request.url.replace(request.path, index_url + '#' + url)
|
||||
else:
|
||||
r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url)
|
||||
return redirect(r)
|
||||
r = index_url + '#' + url.lstrip('/')
|
||||
rh.redirect(r)
|
||||
else:
|
||||
if not Env.get('dev'):
|
||||
time.sleep(0.1)
|
||||
return 'Wrong API key used', 404
|
||||
|
||||
rh.set_status(404)
|
||||
rh.write('Wrong API key used')
|
||||
|
||||
|
||||
@@ -1,49 +1,64 @@
|
||||
from flask.blueprints import Blueprint
|
||||
from flask.helpers import url_for
|
||||
from couchpotato.core.helpers.request import getParams
|
||||
from couchpotato.core.logger import CPLog
|
||||
from functools import wraps
|
||||
from threading import Thread
|
||||
from tornado.gen import coroutine
|
||||
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__)
|
||||
api_docs = {}
|
||||
api_docs_missing = []
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
api = {}
|
||||
api_locks = {}
|
||||
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):
|
||||
|
||||
def __init__(self, application, request, **kwargs):
|
||||
cls = NonBlockHandler
|
||||
cls.stoppers = []
|
||||
super(NonBlockHandler, self).__init__(application, request, **kwargs)
|
||||
stopper = None
|
||||
|
||||
@asynchronous
|
||||
def get(self, route):
|
||||
cls = NonBlockHandler
|
||||
def get(self, route, *args, **kwargs):
|
||||
route = route.strip('/')
|
||||
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):
|
||||
if self.request.connection.stream.closed():
|
||||
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):
|
||||
cls = NonBlockHandler
|
||||
|
||||
for stop in cls.stoppers:
|
||||
stop(self.onNewMessage)
|
||||
if self.stopper:
|
||||
self.stopper(self.onNewMessage)
|
||||
|
||||
cls.stoppers = []
|
||||
|
||||
|
||||
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)
|
||||
self.stopper = None
|
||||
|
||||
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
||||
api_nonblock[route] = func_tuple
|
||||
@@ -53,9 +68,66 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
||||
else:
|
||||
api_docs_missing.append(route)
|
||||
|
||||
""" Api view """
|
||||
def index():
|
||||
index_url = url_for('web.index')
|
||||
return redirect(index_url + 'docs/')
|
||||
# Blocking API handler
|
||||
class ApiHandler(RequestHandler):
|
||||
|
||||
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)
|
||||
|
||||
@@ -70,7 +70,7 @@ config = [{
|
||||
'name': 'development',
|
||||
'default': 0,
|
||||
'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',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from couchpotato.api import addApiView
|
||||
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.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -57,7 +56,7 @@ class Core(Plugin):
|
||||
self.signalHandler()
|
||||
|
||||
def md5Password(self, value):
|
||||
return md5(value.encode(Env.get('encoding'))) if value else ''
|
||||
return md5(value) if value else ''
|
||||
|
||||
def checkApikey(self, value):
|
||||
return value if value and len(value) > 3 else uuid4().hex
|
||||
@@ -68,28 +67,28 @@ class Core(Plugin):
|
||||
|
||||
return True
|
||||
|
||||
def available(self):
|
||||
return jsonified({
|
||||
def available(self, **kwargs):
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def shutdown(self):
|
||||
def shutdown(self, **kwargs):
|
||||
if self.shutdown_started:
|
||||
return False
|
||||
|
||||
def shutdown():
|
||||
self.initShutdown()
|
||||
IOLoop.instance().add_callback(shutdown)
|
||||
IOLoop.current().add_callback(shutdown)
|
||||
|
||||
return 'shutdown'
|
||||
|
||||
def restart(self):
|
||||
def restart(self, **kwargs):
|
||||
if self.shutdown_started:
|
||||
return False
|
||||
|
||||
def restart():
|
||||
self.initShutdown(restart = True)
|
||||
IOLoop.instance().add_callback(restart)
|
||||
IOLoop.current().add_callback(restart)
|
||||
|
||||
return 'restarting'
|
||||
|
||||
@@ -125,10 +124,10 @@ class Core(Plugin):
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
log.debug('Save to shutdown/restart')
|
||||
log.debug('Safe to shutdown/restart')
|
||||
|
||||
try:
|
||||
IOLoop.instance().stop()
|
||||
IOLoop.current().stop()
|
||||
except RuntimeError:
|
||||
pass
|
||||
except:
|
||||
@@ -156,10 +155,10 @@ class Core(Plugin):
|
||||
host = 'localhost'
|
||||
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):
|
||||
return '%s/api/%s' % (self.createBaseUrl(), Env.setting('api_key'))
|
||||
return '%sapi/%s' % (self.createBaseUrl(), Env.setting('api_key'))
|
||||
|
||||
def version(self):
|
||||
ver = fireEvent('updater.info', single = True)
|
||||
@@ -170,10 +169,10 @@ class Core(Plugin):
|
||||
|
||||
return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash'])
|
||||
|
||||
def versionView(self):
|
||||
return jsonified({
|
||||
def versionView(self, **kwargs):
|
||||
return {
|
||||
'version': self.version()
|
||||
})
|
||||
}
|
||||
|
||||
def signalHandler(self):
|
||||
if Env.get('daemonized'): return
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
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.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__)
|
||||
@@ -23,7 +26,6 @@ class ClientScript(Plugin):
|
||||
'script': [
|
||||
'scripts/library/mootools.js',
|
||||
'scripts/library/mootools_more.js',
|
||||
'scripts/library/prefix_free.js',
|
||||
'scripts/library/uniform.js',
|
||||
'scripts/library/form_replacement/form_check.js',
|
||||
'scripts/library/form_replacement/form_radio.js',
|
||||
@@ -69,7 +71,8 @@ class ClientScript(Plugin):
|
||||
addEvent('clientscript.get_styles', self.getStyles)
|
||||
addEvent('clientscript.get_scripts', self.getScripts)
|
||||
|
||||
addEvent('app.load', self.minify)
|
||||
if not Env.get('dev'):
|
||||
addEvent('app.load', self.minify)
|
||||
|
||||
self.addCore()
|
||||
|
||||
@@ -78,7 +81,7 @@ class ClientScript(Plugin):
|
||||
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 = 'api/%s/static/%s?%s' % (Env.setting('api_key'), rel_path, tryInt(os.path.getmtime(file_path)))
|
||||
core_url = 'static/%s' % rel_path
|
||||
|
||||
if static_type == 'script':
|
||||
self.registerScript(core_url, file_path, position = 'front')
|
||||
@@ -88,6 +91,13 @@ class ClientScript(Plugin):
|
||||
|
||||
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, {})
|
||||
@@ -98,8 +108,8 @@ class ClientScript(Plugin):
|
||||
def _minify(self, file_type, files, position, out):
|
||||
|
||||
cache = Env.get('cache_dir')
|
||||
out_name = 'minified_' + out
|
||||
out = os.path.join(cache, out_name)
|
||||
out_name = out
|
||||
out = os.path.join(cache, 'minified', out_name)
|
||||
|
||||
raw = []
|
||||
for file_path in files:
|
||||
@@ -108,15 +118,18 @@ class ClientScript(Plugin):
|
||||
if file_type == 'script':
|
||||
data = jsmin(f)
|
||||
else:
|
||||
data = cssmin(f)
|
||||
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) % (r.get('file'), r.get('date'))
|
||||
data += self.comment.get(file_type) % (ss(r.get('file')), r.get('date'))
|
||||
data += r.get('data') + '\n\n'
|
||||
|
||||
self.createFile(out, data.strip())
|
||||
@@ -126,7 +139,7 @@ class ClientScript(Plugin):
|
||||
if not self.minified[file_type].get(position):
|
||||
self.minified[file_type][position] = []
|
||||
|
||||
minified_url = 'api/%s/file.cache/%s?%s' % (Env.setting('api_key'), out_name, tryInt(os.path.getmtime(out)))
|
||||
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):
|
||||
@@ -160,6 +173,8 @@ class ClientScript(Plugin):
|
||||
|
||||
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):
|
||||
self.urls[type][location] = []
|
||||
self.urls[type][location].append(api_path)
|
||||
@@ -167,3 +182,28 @@ class ClientScript(Plugin):
|
||||
if not self.paths[type].get(location):
|
||||
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
|
||||
|
||||
@@ -16,51 +16,19 @@ class Scheduler(Plugin):
|
||||
|
||||
addEvent('schedule.cron', self.cron)
|
||||
addEvent('schedule.interval', self.interval)
|
||||
addEvent('schedule.start', self.start)
|
||||
addEvent('schedule.restart', self.start)
|
||||
|
||||
addEvent('app.load', self.start)
|
||||
addEvent('schedule.remove', self.remove)
|
||||
|
||||
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.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):
|
||||
super(Scheduler, self).doShutdown()
|
||||
@@ -82,6 +50,7 @@ class Scheduler(Plugin):
|
||||
'day': day,
|
||||
'hour': hour,
|
||||
'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):
|
||||
@@ -93,4 +62,5 @@ class Scheduler(Plugin):
|
||||
'hours': hours,
|
||||
'minutes': minutes,
|
||||
'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.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
@@ -15,6 +14,7 @@ import tarfile
|
||||
import time
|
||||
import traceback
|
||||
import version
|
||||
import zipfile
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -32,11 +32,10 @@ class Updater(Plugin):
|
||||
else:
|
||||
self.updater = SourceUpdater()
|
||||
|
||||
fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
|
||||
addEvent('app.load', self.autoUpdate)
|
||||
addEvent('app.load', self.setCrons)
|
||||
addEvent('updater.info', self.info)
|
||||
|
||||
addApiView('updater.info', self.getInfo, docs = {
|
||||
addApiView('updater.info', self.info, docs = {
|
||||
'desc': 'Get updater information',
|
||||
'return': {
|
||||
'type': 'object',
|
||||
@@ -52,8 +51,17 @@ class Updater(Plugin):
|
||||
'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):
|
||||
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():
|
||||
|
||||
# Notify before restarting
|
||||
@@ -71,31 +79,30 @@ class Updater(Plugin):
|
||||
|
||||
return False
|
||||
|
||||
def check(self):
|
||||
if self.isDisabled():
|
||||
def check(self, force = False):
|
||||
if not force and self.isDisabled():
|
||||
return
|
||||
|
||||
if self.updater.check():
|
||||
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
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def info(self):
|
||||
def info(self, **kwargs):
|
||||
return self.updater.info()
|
||||
|
||||
def getInfo(self):
|
||||
return jsonified(self.updater.info())
|
||||
|
||||
def checkView(self):
|
||||
return jsonified({
|
||||
'update_available': self.check(),
|
||||
def checkView(self, **kwargs):
|
||||
return {
|
||||
'update_available': self.check(force = True),
|
||||
'info': self.updater.info()
|
||||
})
|
||||
}
|
||||
|
||||
def doUpdateView(self):
|
||||
def doUpdateView(self, **kwargs):
|
||||
|
||||
self.check()
|
||||
if not self.updater.update_version:
|
||||
@@ -110,9 +117,9 @@ class Updater(Plugin):
|
||||
if not success:
|
||||
success = True
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': success
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
class BaseUpdater(Plugin):
|
||||
@@ -125,13 +132,11 @@ class BaseUpdater(Plugin):
|
||||
update_failed = False
|
||||
update_version = None
|
||||
last_check = 0
|
||||
auto_register_static = False
|
||||
|
||||
def doUpdate(self):
|
||||
pass
|
||||
|
||||
def getInfo(self):
|
||||
return jsonified(self.info())
|
||||
|
||||
def info(self):
|
||||
return {
|
||||
'last_check': self.last_check,
|
||||
@@ -255,11 +260,11 @@ class SourceUpdater(BaseUpdater):
|
||||
def doUpdate(self):
|
||||
|
||||
try:
|
||||
url = 'https://github.com/%s/%s/tarball/%s' % (self.repo_user, self.repo_name, self.branch)
|
||||
destination = os.path.join(Env.get('cache_dir'), self.update_version.get('hash') + '.tar.gz')
|
||||
extracted_path = os.path.join(Env.get('cache_dir'), 'temp_updater')
|
||||
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')) + '.' + download_data.get('type')
|
||||
|
||||
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
|
||||
if os.path.isdir(extracted_path):
|
||||
@@ -267,9 +272,15 @@ class SourceUpdater(BaseUpdater):
|
||||
self.makeDir(extracted_path)
|
||||
|
||||
# Extract
|
||||
tar = tarfile.open(destination)
|
||||
tar.extractall(path = extracted_path)
|
||||
tar.close()
|
||||
if download_data.get('type') == 'zip':
|
||||
zip = zipfile.ZipFile(destination)
|
||||
zip.extractall(extracted_path)
|
||||
zip.close()
|
||||
else:
|
||||
tar = tarfile.open(destination)
|
||||
tar.extractall(path = extracted_path)
|
||||
tar.close()
|
||||
|
||||
os.remove(destination)
|
||||
|
||||
if self.replaceWith(os.path.join(extracted_path, os.listdir(extracted_path)[0])):
|
||||
|
||||
@@ -5,7 +5,7 @@ var UpdaterBase = new Class({
|
||||
initialize: function(){
|
||||
var self = this;
|
||||
|
||||
App.addEvent('load', self.info.bind(self, 1000))
|
||||
App.addEvent('load', self.info.bind(self, 2000))
|
||||
App.addEvent('unload', function(){
|
||||
if(self.timer)
|
||||
clearTimeout(self.timer);
|
||||
@@ -84,7 +84,7 @@ var UpdaterBase = new Class({
|
||||
'click': self.doUpdate.bind(self)
|
||||
}
|
||||
})
|
||||
).inject($(document.body).getElement('.header'))
|
||||
).inject(document.body)
|
||||
},
|
||||
|
||||
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
|
||||
@@ -1,5 +1,6 @@
|
||||
from base64 import b32decode, b16encode
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.variable import mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.base import Provider
|
||||
import random
|
||||
@@ -10,12 +11,12 @@ log = CPLog(__name__)
|
||||
|
||||
class Downloader(Provider):
|
||||
|
||||
type = []
|
||||
protocol = []
|
||||
http_time_between_calls = 0
|
||||
|
||||
torrent_sources = [
|
||||
'http://torrage.com/torrent/%s.torrent',
|
||||
'http://torcache.net/torrent/%s.torrent',
|
||||
'https://torcache.net/torrent/%s.torrent',
|
||||
]
|
||||
|
||||
torrent_trackers = [
|
||||
@@ -35,18 +36,23 @@ class Downloader(Provider):
|
||||
def __init__(self):
|
||||
addEvent('download', self._download)
|
||||
addEvent('download.enabled', self._isEnabled)
|
||||
addEvent('download.enabled_types', self.getEnabledDownloadType)
|
||||
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 getEnabledDownloadType(self):
|
||||
for download_type in self.type:
|
||||
if self.isEnabled(manual = True, data = {'type': download_type}):
|
||||
return self.type
|
||||
def getEnabledProtocol(self):
|
||||
for download_protocol in self.protocol:
|
||||
if self.isEnabled(manual = True, data = {'protocol': download_protocol}):
|
||||
return self.protocol
|
||||
|
||||
return []
|
||||
|
||||
def _download(self, data = {}, movie = {}, manual = False, filedata = None):
|
||||
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)
|
||||
@@ -64,19 +70,35 @@ class Downloader(Provider):
|
||||
if self.isDisabled(manual = True, data = {}):
|
||||
return
|
||||
|
||||
if self.conf('delete_failed', default = True):
|
||||
return self.removeFailed(item)
|
||||
if item and item.get('downloader') == self.getName():
|
||||
if self.conf('delete_failed'):
|
||||
return self.removeFailed(item)
|
||||
|
||||
return False
|
||||
return False
|
||||
return
|
||||
|
||||
def removeFailed(self, item):
|
||||
return
|
||||
|
||||
def isCorrectType(self, item_type):
|
||||
is_correct = item_type in self.type
|
||||
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:
|
||||
log.debug("Downloader doesn't support this type")
|
||||
log.debug("Downloader doesn't support this protocol")
|
||||
|
||||
return is_correct
|
||||
|
||||
@@ -100,19 +122,75 @@ class Downloader(Provider):
|
||||
except:
|
||||
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
|
||||
|
||||
def isDisabled(self, manual, data):
|
||||
def downloadReturnId(self, download_id):
|
||||
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 = {}):
|
||||
def _isEnabled(self, manual, data = None):
|
||||
if not data: data = {}
|
||||
|
||||
if not self.isEnabled(manual, data):
|
||||
return
|
||||
return True
|
||||
|
||||
def isEnabled(self, manual, data = {}):
|
||||
def isEnabled(self, manual = False, data = None):
|
||||
if not data: data = {}
|
||||
|
||||
d_manual = self.conf('manual', default = False)
|
||||
return super(Downloader, self).isEnabled() and \
|
||||
((d_manual and manual) or (d_manual is False)) and \
|
||||
(not data or self.isCorrectType(data.get('type')))
|
||||
(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)
|
||||
|
||||
|
||||
@@ -7,22 +7,25 @@ import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Blackhole(Downloader):
|
||||
|
||||
type = ['nzb', 'torrent', 'torrent_magnet']
|
||||
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
||||
|
||||
def download(self, data = {}, movie = {}, filedata = None):
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
directory = self.conf('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:
|
||||
try:
|
||||
if not filedata or len(filedata) < 50:
|
||||
try:
|
||||
if data.get('type') == 'torrent_magnet':
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
filedata = self.magnetToTorrent(data.get('url'))
|
||||
data['type'] = 'torrent'
|
||||
data['protocol'] = 'torrent'
|
||||
except:
|
||||
log.error('Failed download torrent via magnet url: %s', traceback.format_exc())
|
||||
|
||||
@@ -34,7 +37,7 @@ class Blackhole(Downloader):
|
||||
|
||||
try:
|
||||
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:
|
||||
f.write(filedata)
|
||||
os.chmod(fullPath, Env.getPermission('file'))
|
||||
@@ -53,20 +56,21 @@ class Blackhole(Downloader):
|
||||
|
||||
return False
|
||||
|
||||
def getEnabledDownloadType(self):
|
||||
def getEnabledProtocol(self):
|
||||
if self.conf('use_for') == 'both':
|
||||
return super(Blackhole, self).getEnabledDownloadType()
|
||||
return super(Blackhole, self).getEnabledProtocol()
|
||||
elif self.conf('use_for') == 'torrent':
|
||||
return ['torrent', 'torrent_magnet']
|
||||
else:
|
||||
return ['nzb']
|
||||
|
||||
def isEnabled(self, manual, data = {}):
|
||||
for_type = ['both']
|
||||
if data and 'torrent' in data.get('type'):
|
||||
for_type.append('torrent')
|
||||
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_type.append(data.get('type'))
|
||||
for_protocol.append(data.get('protocol'))
|
||||
|
||||
return super(Blackhole, self).isEnabled(manual, data) and \
|
||||
((self.conf('use_for') in for_type))
|
||||
((self.conf('use_for') in for_protocol))
|
||||
|
||||
90
couchpotato/core/downloaders/deluge/__init__.py
Normal file
90
couchpotato/core/downloaders/deluge/__init__.py
Normal file
@@ -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.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
244
couchpotato/core/downloaders/deluge/main.py
Normal file
244
couchpotato/core/downloaders/deluge/main.py
Normal file
@@ -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()
|
||||
@@ -24,6 +24,12 @@ config = [{
|
||||
'default': 'localhost:6789',
|
||||
'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',
|
||||
'type': 'password',
|
||||
@@ -36,6 +42,7 @@ config = [{
|
||||
},
|
||||
{
|
||||
'name': 'priority',
|
||||
'advanced': True,
|
||||
'default': '0',
|
||||
'type': 'dropdown',
|
||||
'values': [('Very Low', -100), ('Low', -50), ('Normal', 0), ('High', 50), ('Very High', 100)],
|
||||
@@ -48,6 +55,13 @@ config = [{
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
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
|
||||
from couchpotato.core.helpers.variable import tryInt, md5
|
||||
from couchpotato.core.logger import CPLog
|
||||
from datetime import timedelta
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import traceback
|
||||
import xmlrpclib
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
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 = {}, filedata = None):
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
if not filedata:
|
||||
log.error('Unable to get NZB file: %s', traceback.format_exc())
|
||||
@@ -24,13 +29,13 @@ class NZBGet(Downloader):
|
||||
|
||||
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 = ss('%s.nzb' % self.createNzbName(data, movie))
|
||||
|
||||
rpc = xmlrpclib.ServerProxy(url)
|
||||
try:
|
||||
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:
|
||||
log.info('Successfully connected to NZBGet, but unable to send a message')
|
||||
except socket.error:
|
||||
@@ -50,7 +55,138 @@ class NZBGet(Downloader):
|
||||
|
||||
if xml_response:
|
||||
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:
|
||||
log.error('NZBGet could not add %s to the queue.', nzb_name)
|
||||
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
|
||||
|
||||
@@ -38,6 +38,7 @@ config = [{
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 tryUrlencode, ss
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
@@ -16,20 +16,25 @@ import urllib2
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class NZBVortex(Downloader):
|
||||
|
||||
type = ['nzb']
|
||||
protocol = ['nzb']
|
||||
api_level = None
|
||||
session_id = None
|
||||
|
||||
def download(self, data = {}, movie = {}, filedata = 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)
|
||||
|
||||
return 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
|
||||
@@ -38,7 +43,7 @@ class NZBVortex(Downloader):
|
||||
|
||||
raw_statuses = self.call('nzb')
|
||||
|
||||
statuses = []
|
||||
statuses = StatusList(self)
|
||||
for item in raw_statuses.get('nzbs', []):
|
||||
|
||||
# Check status
|
||||
@@ -54,6 +59,7 @@ class NZBVortex(Downloader):
|
||||
'status': status,
|
||||
'original_status': item['state'],
|
||||
'timeleft':-1,
|
||||
'folder': ss(item['destinationPath']),
|
||||
})
|
||||
|
||||
return statuses
|
||||
@@ -93,9 +99,10 @@ class NZBVortex(Downloader):
|
||||
return False
|
||||
|
||||
|
||||
def call(self, call, parameters = {}, repeat = False, auth = True, *args, **kwargs):
|
||||
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()
|
||||
|
||||
@@ -118,7 +125,7 @@ class NZBVortex(Downloader):
|
||||
# Try login and do again
|
||||
if not repeat:
|
||||
self.login()
|
||||
return self.call(call, parameters = parameters, repeat = True, *args, **kwargs)
|
||||
return self.call(call, parameters = parameters, repeat = True, **kwargs)
|
||||
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
except:
|
||||
@@ -144,7 +151,8 @@ class NZBVortex(Downloader):
|
||||
|
||||
return self.api_level
|
||||
|
||||
def isEnabled(self, manual, data):
|
||||
def isEnabled(self, manual = False, data = None):
|
||||
if not data: data = {}
|
||||
return super(NZBVortex, self).isEnabled(manual, data) and self.getApiLevel()
|
||||
|
||||
|
||||
|
||||
@@ -6,12 +6,15 @@ import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Pneumatic(Downloader):
|
||||
|
||||
type = ['nzb']
|
||||
protocol = ['nzb']
|
||||
strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s'
|
||||
|
||||
def download(self, data = {}, movie = {}, filedata = None):
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
directory = self.conf('directory')
|
||||
if not directory or not os.path.isdir(directory):
|
||||
@@ -26,7 +29,7 @@ class Pneumatic(Downloader):
|
||||
|
||||
try:
|
||||
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:
|
||||
f.write(filedata)
|
||||
|
||||
|
||||
71
couchpotato/core/downloaders/rtorrent/__init__.py
Executable file
71
couchpotato/core/downloaders/rtorrent/__init__.py
Executable file
@@ -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
couchpotato/core/downloaders/rtorrent/main.py
Executable file
201
couchpotato/core/downloaders/rtorrent/main.py
Executable file
@@ -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
|
||||
@@ -11,7 +11,7 @@ config = [{
|
||||
'list': 'download_providers',
|
||||
'name': 'sabnzbd',
|
||||
'label': 'Sabnzbd',
|
||||
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> to download NZBs.',
|
||||
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> (0.7+) to download NZBs.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
@@ -34,6 +34,15 @@ config = [{
|
||||
'label': 'Category',
|
||||
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'priority',
|
||||
'label': 'Priority',
|
||||
'type': 'dropdown',
|
||||
'default': '0',
|
||||
'advanced': True,
|
||||
'values': [('Paused', -2), ('Low', -1), ('Normal', 0), ('High', 1), ('Forced', 2)],
|
||||
'description': 'Add to the queue with this priority.',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': False,
|
||||
@@ -41,9 +50,18 @@ config = [{
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'remove_complete',
|
||||
'advanced': True,
|
||||
'label': 'Remove NZB',
|
||||
'default': False,
|
||||
'type': 'bool',
|
||||
'description': 'Remove the NZB from history after it completed.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
|
||||
@@ -1,47 +1,50 @@
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss
|
||||
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from datetime import timedelta
|
||||
from urllib2 import URLError
|
||||
import json
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Sabnzbd(Downloader):
|
||||
|
||||
type = ['nzb']
|
||||
protocol = ['nzb']
|
||||
|
||||
def download(self, data = {}, movie = {}, filedata = None):
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.info('Sending "%s" to SABnzbd.', data.get('name'))
|
||||
|
||||
params = {
|
||||
'apikey': self.conf('api_key'),
|
||||
req_params = {
|
||||
'cat': self.conf('category'),
|
||||
'mode': 'addurl',
|
||||
'nzbname': self.createNzbName(data, movie),
|
||||
'priority': self.conf('priority'),
|
||||
}
|
||||
|
||||
nzb_filename = None
|
||||
if filedata:
|
||||
if len(filedata) < 50:
|
||||
log.error('No proper nzb available: %s', (filedata))
|
||||
log.error('No proper nzb available: %s', filedata)
|
||||
return False
|
||||
|
||||
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
|
||||
nzb_filename = self.createFileName(data, filedata, movie)
|
||||
params['mode'] = 'addfile'
|
||||
req_params['mode'] = 'addfile'
|
||||
else:
|
||||
params['name'] = data.get('url')
|
||||
|
||||
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(params)
|
||||
req_params['name'] = data.get('url')
|
||||
|
||||
try:
|
||||
if params.get('mode') is 'addfile':
|
||||
sab = self.urlopen(url, timeout = 60, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
|
||||
if nzb_filename and req_params.get('mode') is 'addfile':
|
||||
sab_data = self.call(req_params, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True)
|
||||
else:
|
||||
sab = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
|
||||
sab_data = self.call(req_params)
|
||||
except URLError:
|
||||
log.error('Failed sending release, probably wrong HOST: %s', traceback.format_exc(0))
|
||||
return False
|
||||
@@ -49,17 +52,15 @@ class Sabnzbd(Downloader):
|
||||
log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0))
|
||||
return False
|
||||
|
||||
result = sab.strip()
|
||||
if not result:
|
||||
log.error('SABnzbd didn\'t return anything.')
|
||||
return False
|
||||
|
||||
log.debug('Result text from SAB: %s', result[:40])
|
||||
if result[:2] == 'ok':
|
||||
log.debug('Result from SAB: %s', sab_data)
|
||||
if sab_data.get('status') and not sab_data.get('error'):
|
||||
log.info('NZB sent to SAB successfully.')
|
||||
return True
|
||||
if filedata:
|
||||
return self.downloadReturnId(sab_data.get('nzo_ids')[0])
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
log.error(result[:40])
|
||||
log.error('Error getting data from SABNZBd: %s', sab_data)
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
@@ -85,14 +86,13 @@ class Sabnzbd(Downloader):
|
||||
log.error('Failed getting history json: %s', traceback.format_exc(1))
|
||||
return False
|
||||
|
||||
statuses = []
|
||||
statuses = StatusList(self)
|
||||
|
||||
# Get busy releases
|
||||
for item in queue.get('slots', []):
|
||||
statuses.append({
|
||||
'id': item['nzo_id'],
|
||||
'name': item['filename'],
|
||||
'status': 'busy',
|
||||
'original_status': item['status'],
|
||||
'timeleft': item['timeleft'] if not queue['paused'] else -1,
|
||||
})
|
||||
@@ -111,7 +111,8 @@ class Sabnzbd(Downloader):
|
||||
'name': item['name'],
|
||||
'status': status,
|
||||
'original_status': item['status'],
|
||||
'timeleft': 0,
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': ss(item['storage']),
|
||||
})
|
||||
|
||||
return statuses
|
||||
@@ -133,21 +134,37 @@ class Sabnzbd(Downloader):
|
||||
|
||||
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'),
|
||||
'output': 'json'
|
||||
}))
|
||||
|
||||
data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
|
||||
data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}, **kwargs)
|
||||
if use_json:
|
||||
d = json.loads(data)
|
||||
if d.get('error'):
|
||||
log.error('Error getting data from SABNZBd: %s', d.get('error'))
|
||||
return {}
|
||||
|
||||
return d[params['mode']]
|
||||
return d.get(request_params['mode']) or d
|
||||
else:
|
||||
return data
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ config = [{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'torrent',
|
||||
'radio_group': 'nzb,torrent',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
@@ -32,6 +32,13 @@ config = [{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'use_for',
|
||||
'label': 'Use for',
|
||||
'default': 'both',
|
||||
'type': 'dropdown',
|
||||
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.helpers.encoding import isInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
import httplib
|
||||
import json
|
||||
import urllib
|
||||
import urllib2
|
||||
|
||||
import requests
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Synology(Downloader):
|
||||
|
||||
type = ['torrent_magnet']
|
||||
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
||||
log = CPLog(__name__)
|
||||
|
||||
def download(self, data, movie, filedata = None):
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.error('Sending "%s" (%s) to Synology.', (data.get('name'), data.get('type')))
|
||||
response = False
|
||||
log.error('Sending "%s" (%s) to Synology.', (data['name'], data['protocol']))
|
||||
|
||||
# Load host from config and split out port.
|
||||
host = self.conf('host').split(':')
|
||||
@@ -24,24 +25,47 @@ class Synology(Downloader):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
return False
|
||||
|
||||
if data.get('type') == 'torrent':
|
||||
log.error('Can\'t add binary torrent file')
|
||||
return False
|
||||
|
||||
try:
|
||||
# Send request to Transmission
|
||||
# Send request to Synology
|
||||
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
|
||||
remote_torrent = srpc.add_torrent_uri(data.get('url'))
|
||||
log.info('Response: %s', remote_torrent)
|
||||
return remote_torrent['success']
|
||||
if data['protocol'] == 'torrent_magnet':
|
||||
log.info('Adding torrent URL %s', data['url'])
|
||||
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:
|
||||
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):
|
||||
|
||||
'''SynologyRPC lite library'''
|
||||
"""SynologyRPC lite library"""
|
||||
|
||||
def __init__(self, host = 'localhost', port = 5000, username = None, password = None):
|
||||
|
||||
@@ -58,11 +82,13 @@ class SynologyRPC(object):
|
||||
args = {'api': 'SYNO.API.Auth', 'account': self.username, 'passwd': self.password, 'version': 2,
|
||||
'method': 'login', 'session': self.session_name, 'format': 'sid'}
|
||||
response = self._req(self.auth_url, args)
|
||||
if response['success'] == True:
|
||||
if response['success']:
|
||||
self.sid = response['data']['sid']
|
||||
log.debug('Sid=%s', self.sid)
|
||||
return response
|
||||
elif self.username or self.password:
|
||||
log.debug('sid=%s', self.sid)
|
||||
else:
|
||||
log.error('Couldn\'t login to Synology, %s', response)
|
||||
return response['success']
|
||||
else:
|
||||
log.error('User or password missing, not using authentication.')
|
||||
return False
|
||||
|
||||
@@ -70,36 +96,51 @@ class SynologyRPC(object):
|
||||
args = {'api':'SYNO.API.Auth', 'version':1, 'method':'logout', 'session':self.session_name, '_sid':self.sid}
|
||||
return self._req(self.auth_url, args)
|
||||
|
||||
def _req(self, url, args):
|
||||
req_url = url + '?' + urllib.urlencode(args)
|
||||
def _req(self, url, args, files = None):
|
||||
response = {'success': False}
|
||||
try:
|
||||
req_open = urllib2.urlopen(req_url)
|
||||
response = json.loads(req_open.read())
|
||||
if response['success'] == True:
|
||||
req = requests.post(url, data = args, files = files)
|
||||
req.raise_for_status()
|
||||
response = json.loads(req.text)
|
||||
if response['success']:
|
||||
log.info('Synology action successfull')
|
||||
return response
|
||||
except httplib.InvalidURL, err:
|
||||
log.error('Invalid Transmission host, check your config %s', err)
|
||||
return False
|
||||
except urllib2.HTTPError, err:
|
||||
except requests.ConnectionError, err:
|
||||
log.error('Synology connection error, check your config %s', err)
|
||||
except requests.HTTPError, err:
|
||||
log.error('SynologyRPC HTTPError: %s', err)
|
||||
return False
|
||||
except urllib2.URLError, err:
|
||||
log.error('Unable to connect to Synology %s', err)
|
||||
return False
|
||||
except Exception, err:
|
||||
log.error('Exception: %s', err)
|
||||
finally:
|
||||
return response
|
||||
|
||||
def add_torrent_uri(self, torrent):
|
||||
log.info('Adding torrent URL %s', torrent)
|
||||
response = {}
|
||||
def create_task(self, url = None, filename = None, filedata = None):
|
||||
""" Creates new download task in Synology DownloadStation. Either specify
|
||||
url or pair (filename, filedata).
|
||||
|
||||
Returns True if task was created, False otherwise
|
||||
"""
|
||||
result = False
|
||||
# login
|
||||
login = self._login()
|
||||
if len(login) > 0 and login['success'] == True:
|
||||
log.info('Login success, adding torrent')
|
||||
args = {'api':'SYNO.DownloadStation.Task', 'version':1, 'method':'create', 'uri':torrent, '_sid':self.sid}
|
||||
response = self._req(self.download_url, args)
|
||||
if self._login():
|
||||
args = {'api': 'SYNO.DownloadStation.Task',
|
||||
'version': '1',
|
||||
'method': 'create',
|
||||
'_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()
|
||||
else:
|
||||
log.error('Couldn\'t login to Synology, %s', login)
|
||||
return response
|
||||
|
||||
|
||||
return result
|
||||
|
||||
@@ -25,6 +25,13 @@ config = [{
|
||||
'default': 'localhost:9091',
|
||||
'description': 'Hostname with port. Usually <strong>localhost:9091</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'rpc_url',
|
||||
'type': 'string',
|
||||
'default': 'transmission',
|
||||
'advanced': True,
|
||||
'description': 'Change if you don\'t run Transmission RPC at the default url.',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
},
|
||||
@@ -32,23 +39,33 @@ config = [{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'paused',
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
'description': 'Add the torrent paused.',
|
||||
},
|
||||
{
|
||||
'name': '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',
|
||||
'default': 10,
|
||||
'type': 'int',
|
||||
'name': 'remove_complete',
|
||||
'label': 'Remove torrent',
|
||||
'default': 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',
|
||||
@@ -57,6 +74,20 @@ config = [{
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'stalled_as_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Consider a stalled torrent as failed',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from base64 import b64encode
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
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.environment import Env
|
||||
from datetime import timedelta
|
||||
import httplib
|
||||
import json
|
||||
import os.path
|
||||
@@ -13,70 +16,140 @@ log = CPLog(__name__)
|
||||
|
||||
class Transmission(Downloader):
|
||||
|
||||
type = ['torrent', 'torrent_magnet']
|
||||
protocol = ['torrent', 'torrent_magnet']
|
||||
log = CPLog(__name__)
|
||||
trpc = None
|
||||
|
||||
def download(self, data, movie, filedata = None):
|
||||
|
||||
log.debug('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
|
||||
|
||||
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
|
||||
|
||||
# Set parameters for Transmission
|
||||
folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1]
|
||||
folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep)
|
||||
if not self.trpc:
|
||||
self.trpc = TransmissionRPC(host[0], port = host[1], rpc_url = self.conf('rpc_url'), username = self.conf('username'), password = self.conf('password'))
|
||||
|
||||
# Create the empty folder to download too
|
||||
self.makeDir(folder_path)
|
||||
return self.trpc
|
||||
|
||||
params = {
|
||||
'paused': self.conf('paused', default = 0),
|
||||
'download-dir': folder_path
|
||||
}
|
||||
def download(self, data, movie, filedata = None):
|
||||
|
||||
torrent_params = {}
|
||||
if self.conf('ratio'):
|
||||
torrent_params = {
|
||||
'seedRatioLimit': self.conf('ratio'),
|
||||
'seedRatioMode': self.conf('ratio')
|
||||
}
|
||||
log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('protocol')))
|
||||
|
||||
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')
|
||||
return False
|
||||
|
||||
# Send request to Transmission
|
||||
try:
|
||||
trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||
if data.get('type') == 'torrent_magnet':
|
||||
remote_torrent = trpc.add_torrent_uri(data.get('url'), arguments = params)
|
||||
torrent_params['trackerAdd'] = self.torrent_trackers
|
||||
# Set parameters for adding torrent
|
||||
params = {
|
||||
'paused': self.conf('paused', default = False)
|
||||
}
|
||||
|
||||
if self.conf('directory'):
|
||||
if os.path.isdir(self.conf('directory')):
|
||||
params['download-dir'] = self.conf('directory')
|
||||
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
|
||||
if torrent_params:
|
||||
trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
|
||||
# Change parameters of torrent
|
||||
torrent_params = {}
|
||||
if data.get('seed_ratio'):
|
||||
torrent_params['seedRatioLimit'] = tryFloat(data.get('seed_ratio'))
|
||||
torrent_params['seedRatioMode'] = 1
|
||||
|
||||
return True
|
||||
except Exception, err:
|
||||
log.error('Failed to change settings for transfer: %s', err)
|
||||
if data.get('seed_time'):
|
||||
torrent_params['seedIdleLimit'] = tryInt(data.get('seed_time')) * 60
|
||||
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
|
||||
|
||||
# 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):
|
||||
|
||||
"""TransmissionRPC lite library"""
|
||||
|
||||
def __init__(self, host = 'localhost', port = 9091, username = None, password = None):
|
||||
def __init__(self, host = 'localhost', port = 9091, rpc_url = 'transmission', username = None, password = None):
|
||||
|
||||
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.session_id = 0
|
||||
self.session = {}
|
||||
@@ -97,9 +170,10 @@ class TransmissionRPC(object):
|
||||
try:
|
||||
open_request = urllib2.urlopen(request)
|
||||
response = json.loads(open_request.read())
|
||||
log.debug('request: %s', json.dumps(ojson))
|
||||
log.debug('response: %s', json.dumps(response))
|
||||
if response['result'] == 'success':
|
||||
log.debug('Transmission action successfull')
|
||||
log.debug('Transmission action successful')
|
||||
return response['arguments']
|
||||
else:
|
||||
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
|
||||
post_data = {'arguments': arguments, 'method': 'torrent-set', 'tag': self.tag}
|
||||
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)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ config = [{
|
||||
'list': 'download_providers',
|
||||
'name': 'utorrent',
|
||||
'label': 'uTorrent',
|
||||
'description': 'Use <a href="http://www.utorrent.com/" target="_blank">uTorrent</a> to download torrents.',
|
||||
'description': 'Use <a href="http://www.utorrent.com/" target="_blank">uTorrent</a> (3.0+) to download torrents.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
@@ -36,9 +36,26 @@ config = [{
|
||||
'name': 'label',
|
||||
'description': 'Label to add torrent as.',
|
||||
},
|
||||
{
|
||||
'name': 'remove_complete',
|
||||
'label': 'Remove torrent',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Remove the torrent from uTorrent after it finished seeding.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_files',
|
||||
'label': 'Remove files',
|
||||
'default': True,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also remove the leftover files.',
|
||||
},
|
||||
{
|
||||
'name': 'paused',
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'default': False,
|
||||
'description': 'Add the torrent paused.',
|
||||
},
|
||||
@@ -49,6 +66,13 @@ config = [{
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,129 +1,212 @@
|
||||
from base64 import b16encode, b32decode
|
||||
from bencode import bencode, bdecode
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from bencode import bencode as benc, bdecode
|
||||
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 datetime import timedelta
|
||||
from hashlib import sha1
|
||||
from multipartpost import MultipartPostHandler
|
||||
import cookielib
|
||||
import httplib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import time
|
||||
import urllib
|
||||
import urllib2
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class uTorrent(Downloader):
|
||||
|
||||
type = ['torrent', 'torrent_magnet']
|
||||
protocol = ['torrent', 'torrent_magnet']
|
||||
utorrent_api = None
|
||||
|
||||
def download(self, data, movie, filedata = None):
|
||||
|
||||
log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('type')))
|
||||
|
||||
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
|
||||
|
||||
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 = {}
|
||||
if 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')
|
||||
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_params['trackers'] = '%0D%0A%0D%0A'.join(self.torrent_trackers)
|
||||
else:
|
||||
info = bdecode(filedata)["info"]
|
||||
torrent_hash = sha1(bencode(info)).hexdigest().upper()
|
||||
torrent_filename = self.createFileName(data, filedata, movie)
|
||||
torrent_hash = sha1(benc(info)).hexdigest().upper()
|
||||
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
|
||||
try:
|
||||
if not self.utorrent_api:
|
||||
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
self.utorrent_api.add_torrent_uri(torrent_filename, data.get('url'))
|
||||
else:
|
||||
self.utorrent_api.add_torrent_file(torrent_filename, filedata)
|
||||
|
||||
if data.get('type') == 'torrent_magnet':
|
||||
self.utorrent_api.add_torrent_uri(data.get('url'))
|
||||
# Change settings of added torrent
|
||||
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:
|
||||
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)
|
||||
if self.conf('paused', default = 0):
|
||||
self.utorrent_api.pause_torrent(torrent_hash)
|
||||
return True
|
||||
except Exception, err:
|
||||
log.error('Failed to send torrent to uTorrent: %s', err)
|
||||
return False
|
||||
|
||||
return self.downloadReturnId(torrent_hash)
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
|
||||
log.debug('Checking uTorrent download status.')
|
||||
|
||||
# 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.')
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
try:
|
||||
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||
except Exception, err:
|
||||
log.error('Failed to get uTorrent object: %s', err)
|
||||
statuses = StatusList(self)
|
||||
|
||||
data = self.utorrent_api.get_status()
|
||||
if not data:
|
||||
log.error('Error getting data from uTorrent')
|
||||
return False
|
||||
|
||||
data = ''
|
||||
try:
|
||||
data = self.utorrent_api.get_status()
|
||||
queue = json.loads(data)
|
||||
if queue.get('error'):
|
||||
log.error('Error getting data from uTorrent: %s', queue.get('error'))
|
||||
return False
|
||||
|
||||
except Exception, err:
|
||||
log.error('Failed to get status from uTorrent: %s', err)
|
||||
queue = json.loads(data)
|
||||
if queue.get('error'):
|
||||
log.error('Error getting data from uTorrent: %s', queue.get('error'))
|
||||
return False
|
||||
|
||||
if queue.get('torrents', []) == []:
|
||||
if not queue.get('torrents'):
|
||||
log.debug('Nothing in queue')
|
||||
return False
|
||||
|
||||
statuses = []
|
||||
|
||||
# Get torrents
|
||||
for item in queue.get('torrents', []):
|
||||
for item in queue['torrents']:
|
||||
|
||||
# item[21] = Paused | Downloading | Seeding | Finished
|
||||
status = 'busy'
|
||||
if item[21] == 'Finished' or item[21] == 'Seeding':
|
||||
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': item[10],
|
||||
'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):
|
||||
|
||||
@@ -174,12 +257,16 @@ class uTorrentAPI(object):
|
||||
token = re.findall("<div.*?>(.*?)</", request.read())[0]
|
||||
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)
|
||||
if add_folder:
|
||||
action += "&path=%s" % urllib.quote(filename)
|
||||
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"
|
||||
if add_folder:
|
||||
action += "&path=%s" % urllib.quote(filename)
|
||||
return self._request(action, {"torrent_file": (ss(filename), filedata)})
|
||||
|
||||
def set_torrent(self, hash, params):
|
||||
@@ -188,10 +275,60 @@ class uTorrentAPI(object):
|
||||
action += "&s=%s&v=%s" % (k, v)
|
||||
return self._request(action)
|
||||
|
||||
def pause_torrent(self, hash):
|
||||
action = "action=pause&hash=%s" % hash
|
||||
def pause_torrent(self, hash, pause = True):
|
||||
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)
|
||||
|
||||
@@ -16,33 +16,45 @@ def runHandler(name, handler, *args, **kwargs):
|
||||
|
||||
def addEvent(name, handler, priority = 100):
|
||||
|
||||
if events.get(name):
|
||||
e = events[name]
|
||||
else:
|
||||
e = events[name] = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock())
|
||||
if not events.get(name):
|
||||
events[name] = []
|
||||
|
||||
def createHandle(*args, **kwargs):
|
||||
|
||||
h = None
|
||||
try:
|
||||
parent = handler.im_self
|
||||
bc = hasattr(parent, 'beforeCall')
|
||||
if bc: parent.beforeCall(handler)
|
||||
# Open handler
|
||||
has_parent = hasattr(handler, 'im_self')
|
||||
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)
|
||||
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:
|
||||
h = runHandler(name, handler, *args, **kwargs)
|
||||
log.error('Failed creating handler %s %s: %s', (name, handler, traceback.format_exc()))
|
||||
|
||||
return h
|
||||
|
||||
e.handle(createHandle, priority = priority)
|
||||
events[name].append({
|
||||
'handler': createHandle,
|
||||
'priority': priority,
|
||||
})
|
||||
|
||||
def removeEvent(name, handler):
|
||||
e = events[name]
|
||||
e -= handler
|
||||
|
||||
def fireEvent(name, *args, **kwargs):
|
||||
if not events.get(name): return
|
||||
if not events.has_key(name): return
|
||||
|
||||
#log.debug('Firing event %s', name)
|
||||
try:
|
||||
|
||||
@@ -62,22 +74,32 @@ def fireEvent(name, *args, **kwargs):
|
||||
options[x] = val
|
||||
except: pass
|
||||
|
||||
e = events[name]
|
||||
if len(events[name]) == 1:
|
||||
|
||||
# Lock this event
|
||||
e.lock.acquire()
|
||||
single = None
|
||||
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
|
||||
kwargs['event_order_lock'] = threading.RLock() if options['in_order'] or options['single'] else None
|
||||
kwargs['event_return_on_result'] = options['single']
|
||||
else:
|
||||
|
||||
# Fire
|
||||
result = e(*args, **kwargs)
|
||||
e = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock())
|
||||
|
||||
# Release lock for this event
|
||||
e.lock.release()
|
||||
for event in events[name]:
|
||||
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']:
|
||||
results = None
|
||||
@@ -104,13 +126,14 @@ def fireEvent(name, *args, **kwargs):
|
||||
|
||||
# Merge
|
||||
if options['merge'] and len(results) > 0:
|
||||
results.reverse() # Priority 1 is higher then 100
|
||||
|
||||
# Dict
|
||||
if isinstance(results[0], dict):
|
||||
results.reverse()
|
||||
|
||||
merged = {}
|
||||
for result in results:
|
||||
merged = mergeDicts(merged, result)
|
||||
merged = mergeDicts(merged, result, prepend_list = True)
|
||||
|
||||
results = merged
|
||||
# Lists
|
||||
@@ -134,16 +157,14 @@ def fireEvent(name, *args, **kwargs):
|
||||
options['on_complete']()
|
||||
|
||||
return results
|
||||
except KeyError, e:
|
||||
pass
|
||||
except Exception:
|
||||
log.error('%s: %s', (name, traceback.format_exc()))
|
||||
|
||||
def fireEventAsync(*args, **kwargs):
|
||||
try:
|
||||
my_thread = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
|
||||
my_thread.setDaemon(True)
|
||||
my_thread.start()
|
||||
t = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
return True
|
||||
except Exception, e:
|
||||
log.error('%s: %s', (args[0], e))
|
||||
|
||||
@@ -11,7 +11,8 @@ log = CPLog(__name__)
|
||||
def toSafeString(original):
|
||||
valid_chars = "-_.() %s%s" % (ascii_letters, digits)
|
||||
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):
|
||||
string = stripAccents(original.lower())
|
||||
@@ -62,7 +63,7 @@ def stripAccents(s):
|
||||
|
||||
def tryUrlencode(s):
|
||||
new = u''
|
||||
if isinstance(s, (dict)):
|
||||
if isinstance(s, dict):
|
||||
for key, value in s.iteritems():
|
||||
new += u'&%s=%s' % (key, tryUrlencode(value))
|
||||
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
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 werkzeug.urls import url_decode
|
||||
import flask
|
||||
import re
|
||||
|
||||
def getParams():
|
||||
|
||||
params = url_decode(getattr(flask.request, 'environ').get('QUERY_STRING', ''))
|
||||
def getParams(params):
|
||||
|
||||
reg = re.compile('^[a-z0-9_\.]+$')
|
||||
|
||||
current = temp = {}
|
||||
temp = {}
|
||||
for param, value in sorted(params.iteritems()):
|
||||
|
||||
nest = re.split("([\[\]]+)", param)
|
||||
@@ -36,6 +32,8 @@ def getParams():
|
||||
current = current[item]
|
||||
else:
|
||||
temp[param] = toUnicode(unquote(value))
|
||||
if temp[param].lower() in ['true', 'false']:
|
||||
temp[param] = temp[param].lower() != 'false'
|
||||
|
||||
return dictToList(temp)
|
||||
|
||||
@@ -54,29 +52,3 @@ def dictToList(params):
|
||||
new = params
|
||||
|
||||
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):
|
||||
|
||||
def getTextElements(self, xml, path):
|
||||
''' Find elements and return tree'''
|
||||
""" Find elements and return tree"""
|
||||
|
||||
textelements = []
|
||||
try:
|
||||
@@ -28,7 +28,7 @@ class RSS(object):
|
||||
return elements
|
||||
|
||||
def getElement(self, xml, path):
|
||||
''' Find element and return text'''
|
||||
""" Find element and return text"""
|
||||
|
||||
try:
|
||||
return xml.find(path)
|
||||
@@ -36,7 +36,7 @@ class RSS(object):
|
||||
return
|
||||
|
||||
def getTextElement(self, xml, path):
|
||||
''' Find element and return text'''
|
||||
""" Find element and return text"""
|
||||
|
||||
try:
|
||||
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
|
||||
import hashlib
|
||||
import os.path
|
||||
@@ -10,6 +10,20 @@ import sys
|
||||
|
||||
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():
|
||||
try:
|
||||
import pwd
|
||||
@@ -53,7 +67,7 @@ def getDataDir():
|
||||
def isDict(object):
|
||||
return isinstance(object, dict)
|
||||
|
||||
def mergeDicts(a, b):
|
||||
def mergeDicts(a, b, prepend_list = False):
|
||||
assert isDict(a), isDict(b)
|
||||
dst = a.copy()
|
||||
|
||||
@@ -67,7 +81,7 @@ def mergeDicts(a, b):
|
||||
if isDict(current_src[key]) and isDict(current_dst[key]):
|
||||
stack.append((current_dst[key], current_src[key]))
|
||||
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])
|
||||
else:
|
||||
current_dst[key] = current_src[key]
|
||||
@@ -87,11 +101,16 @@ def flattenList(l):
|
||||
return l
|
||||
|
||||
def md5(text):
|
||||
return hashlib.md5(text).hexdigest()
|
||||
return hashlib.md5(ss(text)).hexdigest()
|
||||
|
||||
def sha1(text):
|
||||
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):
|
||||
return os.path.splitext(filename)[1][1:]
|
||||
|
||||
@@ -99,12 +118,17 @@ def cleanHost(host):
|
||||
if not host.startswith(('http://', 'https://')):
|
||||
host = 'http://' + host
|
||||
|
||||
if not host.endswith('/'):
|
||||
host += '/'
|
||||
host = host.rstrip('/')
|
||||
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):
|
||||
output = open(txt, 'r')
|
||||
@@ -114,7 +138,7 @@ def getImdb(txt, check_inside = True, multiple = False):
|
||||
try:
|
||||
ids = re.findall('(tt\d{7})', txt)
|
||||
if multiple:
|
||||
return ids if len(ids) > 0 else []
|
||||
return list(set(ids)) if len(ids) > 0 else []
|
||||
return ids[0]
|
||||
except IndexError:
|
||||
pass
|
||||
@@ -126,7 +150,11 @@ def tryInt(s):
|
||||
except: return 0
|
||||
|
||||
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
|
||||
|
||||
def natsortKey(s):
|
||||
@@ -145,8 +173,11 @@ def getTitle(library_dict):
|
||||
if title.default:
|
||||
return title.title
|
||||
except:
|
||||
log.error('Could not get title for %s', library_dict.identifier)
|
||||
return None
|
||||
try:
|
||||
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'])
|
||||
return None
|
||||
@@ -156,16 +187,21 @@ def getTitle(library_dict):
|
||||
|
||||
def possibleTitles(raw_title):
|
||||
|
||||
titles = []
|
||||
titles = [
|
||||
toSafeString(raw_title).lower(),
|
||||
raw_title.lower(),
|
||||
simplifyString(raw_title)
|
||||
]
|
||||
|
||||
titles.append(toSafeString(raw_title).lower())
|
||||
titles.append(raw_title.lower())
|
||||
titles.append(simplifyString(raw_title))
|
||||
# replace some chars
|
||||
new_title = raw_title.replace('&', 'and')
|
||||
titles.append(simplifyString(new_title))
|
||||
|
||||
return list(set(titles))
|
||||
|
||||
def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
|
||||
return ''.join(random.choice(chars) for x in range(size))
|
||||
|
||||
def splitString(str, split_on = ','):
|
||||
return [x.strip() for x in str.split(split_on)] if str else []
|
||||
def splitString(str, split_on = ',', clean = True):
|
||||
list = [x.strip() for x in str.split(split_on)] if str else []
|
||||
return filter(None, list) if clean else list
|
||||
|
||||
@@ -6,15 +6,24 @@ import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
class Loader(object):
|
||||
|
||||
class Loader(object):
|
||||
plugins = {}
|
||||
providers = {}
|
||||
|
||||
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')
|
||||
|
||||
self.paths = {
|
||||
@@ -25,12 +34,10 @@ class Loader(object):
|
||||
}
|
||||
|
||||
# Add providers to loader
|
||||
provider_dir = os.path.join(root, 'couchpotato', 'core', 'providers')
|
||||
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)
|
||||
self.addPath(root, ['couchpotato', 'core', 'providers'], 25, recursive = False)
|
||||
|
||||
# Add media to loader
|
||||
self.addPath(root, ['couchpotato', 'core', 'media'], 25, recursive = True)
|
||||
|
||||
for plugin_type, plugin_tuple in self.paths.iteritems():
|
||||
priority, module, dir_name = plugin_tuple
|
||||
@@ -43,7 +50,13 @@ class Loader(object):
|
||||
for module_name, plugin in sorted(self.modules[priority].iteritems()):
|
||||
# Load module
|
||||
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']))
|
||||
|
||||
@@ -53,7 +66,7 @@ class Loader(object):
|
||||
self.loadPlugins(m, plugin.get('name'))
|
||||
except ImportError as e:
|
||||
# todo:: subclass ImportError for missing requirements.
|
||||
if (e.message.lower().startswith("missing")):
|
||||
if e.message.lower().startswith("missing"):
|
||||
log.error(e.message)
|
||||
pass
|
||||
# todo:: this needs to be more descriptive.
|
||||
@@ -73,19 +86,21 @@ class Loader(object):
|
||||
splitted = module.split('.')
|
||||
for sub in splitted[1:]:
|
||||
m = getattr(m, sub)
|
||||
|
||||
if hasattr(m, 'config'):
|
||||
fireEvent('settings.options', splitted[-1] + '_config', getattr(m, 'config'))
|
||||
except:
|
||||
raise
|
||||
|
||||
for cur_file in glob.glob(os.path.join(dir_name, '*')):
|
||||
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)
|
||||
self.addModule(priority, plugin_type, module_name, name)
|
||||
|
||||
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:
|
||||
for section in module.config:
|
||||
fireEvent('settings.options', section['name'], section)
|
||||
@@ -100,15 +115,14 @@ class Loader(object):
|
||||
return False
|
||||
|
||||
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:
|
||||
klass = module.start()
|
||||
klass.registerPlugin()
|
||||
|
||||
if klass and getattr(klass, 'auto_register_static'):
|
||||
klass.registerStatic(module.__file__)
|
||||
|
||||
module.start()
|
||||
return True
|
||||
except Exception, e:
|
||||
except:
|
||||
log.error('Failed loading plugin "%s": %s', (module.__file__, traceback.format_exc()))
|
||||
return False
|
||||
|
||||
@@ -131,5 +145,8 @@ class Loader(object):
|
||||
for sub in splitted[1:-1]:
|
||||
m = getattr(m, sub)
|
||||
return m
|
||||
except ImportError:
|
||||
log.debug('Skip loading module plugin %s: %s', (name, traceback.format_exc()))
|
||||
return None
|
||||
except:
|
||||
raise
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import logging
|
||||
import re
|
||||
import traceback
|
||||
|
||||
class CPLog(object):
|
||||
|
||||
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 = ''):
|
||||
if context.endswith('.main'):
|
||||
@@ -50,8 +49,8 @@ class CPLog(object):
|
||||
msg = msg % tuple([ss(x) for x in list(replace_tuple)])
|
||||
else:
|
||||
msg = msg % ss(replace_tuple)
|
||||
except:
|
||||
self.logger.error(u'Failed encoding stuff to log: %s' % traceback.format_exc())
|
||||
except Exception, e:
|
||||
self.logger.error(u'Failed encoding stuff to log "%s": %s' % (msg, e))
|
||||
|
||||
if not Env.get('dev'):
|
||||
|
||||
|
||||
13
couchpotato/core/media/__init__.py
Normal file
13
couchpotato/core/media/__init__.py
Normal file
@@ -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
|
||||
13
couchpotato/core/media/_base/library/__init__.py
Normal file
13
couchpotato/core/media/_base/library/__init__.py
Normal file
@@ -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
|
||||
75
couchpotato/core/media/_base/searcher/__init__.py
Normal file
75
couchpotato/core/media/_base/searcher/__init__.py
Normal file
@@ -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'
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
45
couchpotato/core/media/_base/searcher/base.py
Normal file
45
couchpotato/core/media/_base/searcher/base.py
Normal file
@@ -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
|
||||
238
couchpotato/core/media/_base/searcher/main.py
Normal file
238
couchpotato/core/media/_base/searcher/main.py
Normal file
@@ -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
|
||||
6
couchpotato/core/media/movie/__init__.py
Normal file
6
couchpotato/core/media/movie/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from couchpotato.core.media import MediaBase
|
||||
|
||||
|
||||
class MovieTypeBase(MediaBase):
|
||||
|
||||
_type = 'movie'
|
||||
6
couchpotato/core/media/movie/_base/__init__.py
Normal file
6
couchpotato/core/media/movie/_base/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .main import MovieBase
|
||||
|
||||
def start():
|
||||
return MovieBase()
|
||||
|
||||
config = []
|
||||
@@ -2,10 +2,10 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||
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
|
||||
from couchpotato.core.helpers.variable import getImdb, splitString, tryInt, \
|
||||
mergeDicts
|
||||
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, \
|
||||
Release
|
||||
from couchpotato.environment import Env
|
||||
@@ -17,17 +17,23 @@ import time
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class MoviePlugin(Plugin):
|
||||
class MovieBase(MovieTypeBase):
|
||||
|
||||
default_dict = {
|
||||
'profile': {'types': {'quality': {}}},
|
||||
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {},
|
||||
'status': {}
|
||||
'status': {},
|
||||
'category': {},
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
|
||||
# Initialize this type
|
||||
super(MovieBase, self).__init__()
|
||||
self.initType()
|
||||
|
||||
addApiView('movie.search', self.search, docs = {
|
||||
'desc': 'Search the movie providers for a movie',
|
||||
'params': {
|
||||
@@ -108,9 +114,8 @@ class MoviePlugin(Plugin):
|
||||
now = time.time()
|
||||
week = 262080
|
||||
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
||||
done_status, available_status, snatched_status = \
|
||||
fireEvent('status.get', ['done', 'available', 'snatched'], single = True)
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -119,21 +124,21 @@ class MoviePlugin(Plugin):
|
||||
.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)
|
||||
|
||||
def getView(self):
|
||||
db.expire_all()
|
||||
|
||||
movie_id = getParam('id')
|
||||
movie = self.get(movie_id) if movie_id else None
|
||||
def getView(self, id = None, **kwargs):
|
||||
|
||||
return jsonified({
|
||||
movie = self.get(id) if id else None
|
||||
|
||||
return {
|
||||
'success': movie is not None,
|
||||
'movie': movie,
|
||||
})
|
||||
}
|
||||
|
||||
def get(self, movie_id):
|
||||
|
||||
@@ -141,7 +146,7 @@ class MoviePlugin(Plugin):
|
||||
|
||||
imdb_id = getImdb(str(movie_id))
|
||||
|
||||
if(imdb_id):
|
||||
if imdb_id:
|
||||
m = db.query(Movie).filter(Movie.library.has(identifier = imdb_id)).first()
|
||||
else:
|
||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||
@@ -150,6 +155,7 @@ class MoviePlugin(Plugin):
|
||||
if m:
|
||||
results = m.to_dict(self.default_dict)
|
||||
|
||||
db.expire_all()
|
||||
return results
|
||||
|
||||
def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
|
||||
@@ -162,21 +168,33 @@ class MoviePlugin(Plugin):
|
||||
if release_status and not isinstance(release_status, (list, tuple)):
|
||||
release_status = [release_status]
|
||||
|
||||
# query movie ids
|
||||
q = db.query(Movie) \
|
||||
.outerjoin(Movie.releases, Movie.library, Library.titles) \
|
||||
.filter(LibraryTitle.default == True) \
|
||||
.with_entities(Movie.id) \
|
||||
.group_by(Movie.id)
|
||||
|
||||
# Filter on movie status
|
||||
if status and len(status) > 0:
|
||||
q = q.filter(or_(*[Movie.status.has(identifier = s) for s in status]))
|
||||
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.filter(or_(*[Release.status.has(identifier = s) for s in release_status]))
|
||||
q = q.join(Movie.releases)
|
||||
|
||||
total_count = q.count()
|
||||
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 = []
|
||||
if starts_with:
|
||||
starts_with = toUnicode(starts_with.lower())
|
||||
@@ -191,46 +209,79 @@ class MoviePlugin(Plugin):
|
||||
if search:
|
||||
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
|
||||
|
||||
if filter_or:
|
||||
if len(filter_or) > 0:
|
||||
q = q.filter(or_(*filter_or))
|
||||
|
||||
total_count = q.count()
|
||||
if total_count == 0:
|
||||
return 0, []
|
||||
|
||||
if order == 'release_order':
|
||||
q = q.order_by(desc(Release.last_edit))
|
||||
else:
|
||||
q = q.order_by(asc(LibraryTitle.simple_title))
|
||||
|
||||
q = q.subquery()
|
||||
q2 = db.query(Movie).join((q, q.c.id == Movie.id)) \
|
||||
.options(joinedload_all('releases')) \
|
||||
.options(joinedload_all('profile.types')) \
|
||||
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.files')) \
|
||||
.options(joinedload_all('status')) \
|
||||
.options(joinedload_all('files'))
|
||||
|
||||
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]
|
||||
q2 = q2.limit(limit).offset(offset)
|
||||
q2 = q2.filter(Movie.id.in_(movie_ids))
|
||||
|
||||
results = q2.all()
|
||||
movies = []
|
||||
|
||||
# Create dict by movie id
|
||||
movie_dict = {}
|
||||
for movie in results:
|
||||
temp = movie.to_dict({
|
||||
'profile': {'types': {}},
|
||||
'releases': {'files':{}, 'info': {}},
|
||||
movie_dict[movie.id] = movie
|
||||
|
||||
# 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':{}},
|
||||
'files': {},
|
||||
})
|
||||
movies.append(temp)
|
||||
}), {
|
||||
'releases': releases,
|
||||
'releases_count': releases_count.get(movie_id),
|
||||
}))
|
||||
|
||||
#db.close()
|
||||
return (total_count, movies)
|
||||
db.expire_all()
|
||||
return total_count, movies
|
||||
|
||||
def availableChars(self, status = None, release_status = None):
|
||||
|
||||
chars = ''
|
||||
status = status or []
|
||||
release_status = release_status or []
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -240,38 +291,53 @@ class MoviePlugin(Plugin):
|
||||
if release_status and not isinstance(release_status, (list, tuple)):
|
||||
release_status = [release_status]
|
||||
|
||||
q = db.query(Movie) \
|
||||
.outerjoin(Movie.releases, Movie.library, Library.titles, Movie.status) \
|
||||
.options(joinedload_all('library.titles'))
|
||||
q = db.query(Movie)
|
||||
|
||||
# Filter on movie status
|
||||
if status and len(status) > 0:
|
||||
q = q.filter(or_(*[Movie.status.has(identifier = s) for s in status]))
|
||||
statuses = fireEvent('status.get', status, single = len(release_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.filter(or_(*[Release.status.has(identifier = s) for s in release_status]))
|
||||
|
||||
results = q.all()
|
||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
for movie in results:
|
||||
char = movie.library.titles[0].simple_title[0]
|
||||
char = char if char in ascii_lowercase else '#'
|
||||
if char not in chars:
|
||||
chars += str(char)
|
||||
q = q.join(Movie.releases) \
|
||||
.filter(Release.status_id.in_(statuses))
|
||||
|
||||
#db.close()
|
||||
return ''.join(sorted(chars, key = str.lower))
|
||||
q = q.join(Library, LibraryTitle) \
|
||||
.with_entities(LibraryTitle.simple_title) \
|
||||
.filter(LibraryTitle.default == True)
|
||||
|
||||
def listView(self):
|
||||
titles = q.all()
|
||||
|
||||
params = getParams()
|
||||
status = splitString(params.get('status', None))
|
||||
release_status = splitString(params.get('release_status', None))
|
||||
limit_offset = params.get('limit_offset', None)
|
||||
starts_with = params.get('starts_with', None)
|
||||
search = params.get('search', None)
|
||||
order = params.get('order', None)
|
||||
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,
|
||||
@@ -282,32 +348,31 @@ class MoviePlugin(Plugin):
|
||||
order = order
|
||||
)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'total': total_movies,
|
||||
'movies': movies,
|
||||
})
|
||||
}
|
||||
|
||||
def charView(self):
|
||||
def charView(self, **kwargs):
|
||||
|
||||
params = getParams()
|
||||
status = splitString(params.get('status', None))
|
||||
release_status = splitString(params.get('release_status', None))
|
||||
status = splitString(kwargs.get('status', None))
|
||||
release_status = splitString(kwargs.get('release_status', None))
|
||||
chars = self.availableChars(status, release_status)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(chars) == 0,
|
||||
'chars': chars,
|
||||
})
|
||||
}
|
||||
|
||||
def refresh(self):
|
||||
def refresh(self, id = '', **kwargs):
|
||||
|
||||
db = get_session()
|
||||
|
||||
for id in splitString(getParam('id')):
|
||||
movie = db.query(Movie).filter_by(id = id).first()
|
||||
for x in splitString(id):
|
||||
movie = db.query(Movie).filter_by(id = x).first()
|
||||
|
||||
if movie:
|
||||
|
||||
@@ -316,18 +381,16 @@ class MoviePlugin(Plugin):
|
||||
for title in movie.library.titles:
|
||||
if title.default: default_title = title.title
|
||||
|
||||
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True, message = 'Updating "%s"' % default_title)
|
||||
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id))
|
||||
fireEvent('notify.frontend', type = 'movie.busy.%s' % x, data = True)
|
||||
fireEventAsync('library.update.movie', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
|
||||
|
||||
|
||||
#db.close()
|
||||
return jsonified({
|
||||
db.expire_all()
|
||||
return {
|
||||
'success': True,
|
||||
})
|
||||
}
|
||||
|
||||
def search(self):
|
||||
def search(self, q = '', **kwargs):
|
||||
|
||||
q = getParam('q')
|
||||
cache_key = u'%s/%s' % (__name__, simplifyString(q))
|
||||
movies = Env.get('cache').get(cache_key)
|
||||
|
||||
@@ -339,13 +402,14 @@ class MoviePlugin(Plugin):
|
||||
movies = fireEvent('movie.search', q = q, merge = True)
|
||||
Env.get('cache').set(cache_key, movies)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0 if movies else 0,
|
||||
'movies': movies,
|
||||
})
|
||||
}
|
||||
|
||||
def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None):
|
||||
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'):
|
||||
msg = 'Can\'t add movie without imdb identifier.'
|
||||
@@ -364,25 +428,26 @@ class MoviePlugin(Plugin):
|
||||
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_active = fireEvent('status.add', 'active', single = True)
|
||||
snatched_status = fireEvent('status.add', 'snatched', single = True)
|
||||
ignored_status = fireEvent('status.add', 'ignored', single = True)
|
||||
downloaded_status = fireEvent('status.add', 'downloaded', single = True)
|
||||
status_active, snatched_status, ignored_status, done_status, downloaded_status = \
|
||||
fireEvent('status.get', ['active', 'snatched', 'ignored', 'done', 'downloaded'], single = True)
|
||||
|
||||
default_profile = fireEvent('profile.default', single = True)
|
||||
cat_id = params.get('category_id')
|
||||
|
||||
db = get_session()
|
||||
m = db.query(Movie).filter_by(library_id = library.get('id')).first()
|
||||
added = True
|
||||
do_search = False
|
||||
search_after = search_after and self.conf('search_on_add', section = 'moviesearcher')
|
||||
if not m:
|
||||
m = Movie(
|
||||
library_id = library.get('id'),
|
||||
profile_id = params.get('profile_id', default_profile.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.commit()
|
||||
@@ -391,19 +456,20 @@ class MoviePlugin(Plugin):
|
||||
if search_after:
|
||||
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
|
||||
elif force_readd:
|
||||
|
||||
# Clean snatched history
|
||||
for release in m.releases:
|
||||
if release.status_id in [downloaded_status.get('id'), snatched_status.get('id')]:
|
||||
if release.status_id in [downloaded_status.get('id'), snatched_status.get('id'), done_status.get('id')]:
|
||||
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.category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else (m.category_id or None)
|
||||
else:
|
||||
log.debug('Movie already exists, not updating: %s', params)
|
||||
added = False
|
||||
@@ -431,37 +497,38 @@ class MoviePlugin(Plugin):
|
||||
if added:
|
||||
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
|
||||
|
||||
|
||||
def addView(self):
|
||||
def addView(self, **kwargs):
|
||||
|
||||
params = getParams()
|
||||
movie_dict = self.add(params = kwargs)
|
||||
|
||||
movie_dict = self.add(params)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'added': True if movie_dict else False,
|
||||
'movie': movie_dict,
|
||||
})
|
||||
}
|
||||
|
||||
def edit(self):
|
||||
def edit(self, id = '', **kwargs):
|
||||
|
||||
params = getParams()
|
||||
db = get_session()
|
||||
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
|
||||
ids = splitString(params.get('id'))
|
||||
ids = splitString(id)
|
||||
for movie_id in ids:
|
||||
|
||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||
if not m:
|
||||
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
|
||||
for rel in m.releases:
|
||||
@@ -470,33 +537,31 @@ class MoviePlugin(Plugin):
|
||||
db.commit()
|
||||
|
||||
# Default title
|
||||
if params.get('default_title'):
|
||||
if kwargs.get('default_title'):
|
||||
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()
|
||||
|
||||
fireEvent('movie.restatus', m.id)
|
||||
|
||||
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()
|
||||
return jsonified({
|
||||
db.expire_all()
|
||||
return {
|
||||
'success': True,
|
||||
})
|
||||
}
|
||||
|
||||
def deleteView(self):
|
||||
def deleteView(self, id = '', **kwargs):
|
||||
|
||||
params = getParams()
|
||||
|
||||
ids = splitString(params.get('id'))
|
||||
ids = splitString(id)
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
def delete(self, movie_id, delete_from = None):
|
||||
|
||||
@@ -516,7 +581,7 @@ class MoviePlugin(Plugin):
|
||||
total_deleted = 0
|
||||
new_movie_status = None
|
||||
for release in movie.releases:
|
||||
if delete_from in ['wanted', 'snatched']:
|
||||
if delete_from in ['wanted', 'snatched', 'late']:
|
||||
if release.status_id != done_status.get('id'):
|
||||
db.delete(release)
|
||||
total_deleted += 1
|
||||
@@ -543,13 +608,12 @@ class MoviePlugin(Plugin):
|
||||
if deleted:
|
||||
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
|
||||
|
||||
#db.close()
|
||||
db.expire_all()
|
||||
return True
|
||||
|
||||
def restatus(self, movie_id):
|
||||
|
||||
active_status = fireEvent('status.get', 'active', single = True)
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -558,7 +622,7 @@ class MoviePlugin(Plugin):
|
||||
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
|
||||
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:
|
||||
m.status_id = done_status.get('id')
|
||||
else:
|
||||
@@ -572,7 +636,6 @@ class MoviePlugin(Plugin):
|
||||
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
|
||||
|
||||
db.commit()
|
||||
#db.close()
|
||||
|
||||
return True
|
||||
|
||||
@@ -581,7 +644,8 @@ class MoviePlugin(Plugin):
|
||||
def onComplete():
|
||||
db = get_session()
|
||||
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
|
||||
|
||||
@@ -592,5 +656,6 @@ class MoviePlugin(Plugin):
|
||||
db = get_session()
|
||||
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))
|
||||
db.expire_all()
|
||||
|
||||
return notifyFront
|
||||
@@ -1,17 +1,20 @@
|
||||
var MovieList = new Class({
|
||||
|
||||
Implements: [Options],
|
||||
Implements: [Events, Options],
|
||||
|
||||
options: {
|
||||
navigation: true,
|
||||
limit: 50,
|
||||
load_more: true,
|
||||
loader: true,
|
||||
menu: [],
|
||||
add_new: false
|
||||
add_new: false,
|
||||
force_view: false
|
||||
},
|
||||
|
||||
movies: [],
|
||||
movies_added: {},
|
||||
total_movies: 0,
|
||||
letters: {},
|
||||
filter: null,
|
||||
|
||||
@@ -21,7 +24,7 @@ var MovieList = new Class({
|
||||
|
||||
self.offset = 0;
|
||||
self.filter = self.options.filter || {
|
||||
'startswith': null,
|
||||
'starts_with': null,
|
||||
'search': null
|
||||
}
|
||||
|
||||
@@ -42,7 +45,10 @@ var MovieList = new Class({
|
||||
}) : null
|
||||
);
|
||||
|
||||
self.changeView(self.getSavedView() || self.options.view || 'details');
|
||||
if($(window).getSize().x <= 480 && !self.options.force_view)
|
||||
self.changeView('list');
|
||||
else
|
||||
self.changeView(self.getSavedView() || self.options.view || 'details');
|
||||
|
||||
self.getMovies();
|
||||
|
||||
@@ -57,7 +63,8 @@ var MovieList = new Class({
|
||||
self.movies.each(function(movie){
|
||||
if(movie.get('id') == notification.data.id){
|
||||
movie.destroy();
|
||||
delete self.movies_added[notification.data.id]
|
||||
delete self.movies_added[notification.data.id];
|
||||
self.setCounter(self.counter_count-1);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -71,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){
|
||||
window.scroll(0,0);
|
||||
self.createMovie(notification.data, 'top');
|
||||
self.setCounter(self.counter_count+1);
|
||||
|
||||
self.checkIfEmpty();
|
||||
}
|
||||
@@ -110,7 +118,7 @@ var MovieList = new Class({
|
||||
self.createMovie(movie);
|
||||
});
|
||||
|
||||
self.total_movies = total;
|
||||
self.total_movies += total;
|
||||
self.setCounter(total);
|
||||
|
||||
},
|
||||
@@ -120,7 +128,40 @@ var MovieList = new Class({
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
@@ -144,29 +185,9 @@ var MovieList = new Class({
|
||||
var self = this;
|
||||
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
self.current_view = self.getSavedView();
|
||||
self.el.addClass(self.current_view+'_list')
|
||||
self.el.addClass('with_navigation')
|
||||
|
||||
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(
|
||||
new Element('span.select').adopt(
|
||||
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
|
||||
@@ -204,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');
|
||||
|
||||
@@ -216,20 +262,56 @@ var MovieList = new Class({
|
||||
}).inject(self.mass_edit_quality)
|
||||
});
|
||||
|
||||
self.filter_menu.addLink(
|
||||
self.navigation_search_input = new Element('input', {
|
||||
'title': 'Search through ' + self.options.identifier,
|
||||
'placeholder': 'Search through ' + self.options.identifier,
|
||||
'events': {
|
||||
'keyup': self.search.bind(self),
|
||||
'change': self.search.bind(self)
|
||||
}
|
||||
})
|
||||
).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')
|
||||
})
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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){
|
||||
self.navigation_actions.adopt(
|
||||
new Element('li.'+view+(self.current_view == view ? '.active' : '')+'[data-view='+view+']', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
var a = 'active';
|
||||
self.navigation_actions.getElements('.'+a).removeClass(a);
|
||||
self.changeView(this.get('data-view'));
|
||||
this.addClass(a);
|
||||
}
|
||||
}
|
||||
}).adopt(new Element('span'))
|
||||
)
|
||||
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
|
||||
@@ -246,37 +328,13 @@ var MovieList = new Class({
|
||||
}).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
|
||||
if (self.options.menu.length > 0)
|
||||
self.options.menu.each(function(menu_item){
|
||||
self.navigation_menu.addLink(menu_item);
|
||||
})
|
||||
else
|
||||
self.navigation_menu.hide()
|
||||
|
||||
self.nav_scrollspy = new ScrollSpy({
|
||||
min: 10,
|
||||
onEnter: function(){
|
||||
self.navigation.addClass('float')
|
||||
},
|
||||
onLeave: function(){
|
||||
self.navigation.removeClass('float')
|
||||
}
|
||||
});
|
||||
self.navigation_menu.hide();
|
||||
|
||||
},
|
||||
|
||||
@@ -324,14 +382,14 @@ var MovieList = new Class({
|
||||
self.movies.each(function(movie){
|
||||
if (movie.isSelected()){
|
||||
$(movie).destroy()
|
||||
erase_movies.include(movie)
|
||||
erase_movies.include(movie);
|
||||
}
|
||||
});
|
||||
|
||||
erase_movies.each(function(movie){
|
||||
self.movies.erase(movie);
|
||||
|
||||
movie.destroy()
|
||||
movie.destroy();
|
||||
self.setCounter(self.counter_count-1);
|
||||
});
|
||||
|
||||
self.calculateSelected();
|
||||
@@ -429,12 +487,12 @@ var MovieList = new Class({
|
||||
.addClass(new_view+'_list')
|
||||
|
||||
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(){
|
||||
var self = this;
|
||||
return Cookie.read(self.options.identifier+'_view') || 'details';
|
||||
return Cookie.read(self.options.identifier+'_view2');
|
||||
},
|
||||
|
||||
search: function(){
|
||||
@@ -450,8 +508,7 @@ var MovieList = new Class({
|
||||
self.activateLetter();
|
||||
self.filter.search = search_value;
|
||||
|
||||
self.movie_list.empty();
|
||||
self.getMovies();
|
||||
self.getMovies(true);
|
||||
|
||||
self.last_search_value = search_value;
|
||||
|
||||
@@ -463,11 +520,10 @@ var MovieList = new Class({
|
||||
var self = this;
|
||||
|
||||
self.reset();
|
||||
self.movie_list.empty();
|
||||
self.getMovies();
|
||||
self.getMovies(true);
|
||||
},
|
||||
|
||||
getMovies: function(){
|
||||
getMovies: function(reset){
|
||||
var self = this;
|
||||
|
||||
if(self.scrollspy){
|
||||
@@ -475,20 +531,51 @@ var MovieList = new Class({
|
||||
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({
|
||||
'status': self.options.status,
|
||||
'limit_offset': self.options.limit + ',' + self.offset
|
||||
'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null
|
||||
}, self.filter),
|
||||
'onComplete': function(json){
|
||||
'onSuccess': function(json){
|
||||
|
||||
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);
|
||||
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.checkIfEmpty();
|
||||
self.fireEvent('loaded');
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -515,10 +602,10 @@ var MovieList = new Class({
|
||||
self.title[is_empty ? 'hide' : 'show']()
|
||||
|
||||
if(self.description)
|
||||
self.description[is_empty ? 'hide' : 'show']()
|
||||
self.description.setStyle('display', [is_empty ? 'none' : ''])
|
||||
|
||||
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)
|
||||
self.navigation.hide();
|
||||
@@ -1,9 +1,13 @@
|
||||
var MovieAction = new Class({
|
||||
|
||||
class_name: 'action icon',
|
||||
Implements: [Options],
|
||||
|
||||
initialize: function(movie){
|
||||
class_name: 'action icon2',
|
||||
|
||||
initialize: function(movie, options){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.movie = movie;
|
||||
|
||||
self.create();
|
||||
@@ -21,6 +25,32 @@ var MovieAction = new Class({
|
||||
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', {
|
||||
@@ -62,10 +92,10 @@ MA.IMDB = new Class({
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.id = self.movie.get('identifier');
|
||||
self.id = self.movie.get('imdb') || self.movie.get('identifier');
|
||||
|
||||
self.el = new Element('a.imdb', {
|
||||
'title': 'Go to the IMDB page of ' + self.movie.getTitle(),
|
||||
'title': 'Go to the IMDB page of ' + self.getTitle(),
|
||||
'href': 'http://www.imdb.com/title/'+self.id+'/',
|
||||
'target': '_blank'
|
||||
});
|
||||
@@ -82,34 +112,25 @@ MA.Release = new Class({
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.releases.icon.download', {
|
||||
'title': 'Show the releases that are available for ' + self.movie.getTitle(),
|
||||
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){
|
||||
if(self.movie.data.releases.length == 0)
|
||||
self.el.hide()
|
||||
}
|
||||
else {
|
||||
else
|
||||
self.showHelper();
|
||||
|
||||
var buttons_done = false;
|
||||
|
||||
self.movie.data.releases.sortBy('-info.score').each(function(release){
|
||||
if(buttons_done) return;
|
||||
|
||||
var status = Status.get(release.status_id);
|
||||
|
||||
if((self.next_release && (status.identifier == 'ignored' || status.identifier == 'failed')) || (!self.next_release && status.identifier == 'available')){
|
||||
self.hide_on_click = false;
|
||||
self.show();
|
||||
buttons_done = true;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
@@ -118,12 +139,40 @@ MA.Release = new Class({
|
||||
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').adopt(
|
||||
self.release_container = new Element('div.releases.table').adopt(
|
||||
self.trynext_container = new Element('div.buttons.try_container')
|
||||
)
|
||||
).inject(self.movie, 'top');
|
||||
self.options_container = new Element('div.options').grab(
|
||||
self.release_container = new Element('div.releases.table')
|
||||
);
|
||||
|
||||
// Header
|
||||
new Element('div.item.head').adopt(
|
||||
@@ -136,7 +185,7 @@ MA.Release = new Class({
|
||||
new Element('span.provider', {'text': 'Provider'})
|
||||
).inject(self.release_container)
|
||||
|
||||
self.movie.data.releases.sortBy('-info.score').each(function(release){
|
||||
self.releases.each(function(release){
|
||||
|
||||
var status = Status.get(release.status_id),
|
||||
quality = Quality.getProfile(release.quality_id) || {},
|
||||
@@ -157,7 +206,7 @@ MA.Release = new Class({
|
||||
}
|
||||
|
||||
// Create release
|
||||
new Element('div', {
|
||||
var item = new Element('div', {
|
||||
'class': 'item '+status.identifier,
|
||||
'id': 'release_'+release.id
|
||||
}).adopt(
|
||||
@@ -168,11 +217,11 @@ MA.Release = new Class({
|
||||
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.icon', {
|
||||
release.info['detail_url'] ? new Element('a.info.icon2', {
|
||||
'href': release.info['detail_url'],
|
||||
'target': '_blank'
|
||||
}) : null,
|
||||
new Element('a.download.icon', {
|
||||
}) : new Element('a'),
|
||||
new Element('a.download.icon2', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
@@ -181,16 +230,17 @@ MA.Release = new Class({
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Element('a.delete.icon', {
|
||||
new Element('a.delete.icon2', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
self.ignore(release);
|
||||
this.getParent('.item').toggleClass('ignored')
|
||||
}
|
||||
}
|
||||
})
|
||||
).inject(self.release_container)
|
||||
).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'))
|
||||
@@ -201,33 +251,40 @@ MA.Release = new Class({
|
||||
}
|
||||
});
|
||||
|
||||
if(self.last_release){
|
||||
if(self.last_release)
|
||||
self.release_container.getElement('#release_'+self.last_release.id).addClass('last_release');
|
||||
}
|
||||
|
||||
if(self.next_release){
|
||||
if(self.next_release)
|
||||
self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release');
|
||||
}
|
||||
|
||||
if(self.next_release || self.last_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'
|
||||
}),
|
||||
self.last_release ? new Element('a.button.orange', {
|
||||
lr ? new Element('a.button.orange', {
|
||||
'text': 'the same release again',
|
||||
'events': {
|
||||
'click': self.trySameRelease.bind(self)
|
||||
'click': function(){
|
||||
self.download(lr);
|
||||
}
|
||||
}
|
||||
}) : null,
|
||||
self.next_release && self.last_release ? new Element('span.or', {
|
||||
nr && lr ? new Element('span.or', {
|
||||
'text': ','
|
||||
}) : null,
|
||||
self.next_release ? [new Element('a.button.green', {
|
||||
'text': self.last_release ? 'another release' : 'the best release',
|
||||
nr ? [new Element('a.button.green', {
|
||||
'text': lr ? 'another release' : 'the best release',
|
||||
'events': {
|
||||
'click': self.tryNextRelease.bind(self)
|
||||
'click': function(){
|
||||
self.download(nr);
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Element('span.or', {
|
||||
@@ -236,9 +293,66 @@ MA.Release = new Class({
|
||||
)
|
||||
}
|
||||
|
||||
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){
|
||||
@@ -249,18 +363,21 @@ MA.Release = new Class({
|
||||
var self = this;
|
||||
|
||||
var release_el = self.release_container.getElement('#release_'+release.id),
|
||||
icon = release_el.getElement('.download.icon');
|
||||
icon = release_el.getElement('.download.icon2');
|
||||
|
||||
icon.addClass('spinner');
|
||||
icon.addClass('icon spinner').removeClass('download');
|
||||
|
||||
Api.request('release.download', {
|
||||
'data': {
|
||||
'id': release.id
|
||||
},
|
||||
'onComplete': function(json){
|
||||
icon.removeClass('spinner')
|
||||
if(json.success)
|
||||
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.');
|
||||
}
|
||||
@@ -273,27 +390,52 @@ MA.Release = new Class({
|
||||
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;
|
||||
|
||||
if(self.last_release)
|
||||
self.ignore(self.last_release);
|
||||
|
||||
if(self.next_release)
|
||||
self.download(self.next_release);
|
||||
|
||||
},
|
||||
|
||||
trySameRelease: function(movie_id){
|
||||
var self = this;
|
||||
|
||||
if(self.last_release)
|
||||
self.download(self.last_release);
|
||||
Api.request('movie.searcher.try_next', {
|
||||
'data': {
|
||||
'id': self.movie.get('id')
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -308,7 +450,7 @@ MA.Trailer = new Class({
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.trailer', {
|
||||
'title': 'Watch the trailer of ' + self.movie.getTitle(),
|
||||
'title': 'Watch the trailer of ' + self.getTitle(),
|
||||
'events': {
|
||||
'click': self.watch.bind(self)
|
||||
}
|
||||
@@ -319,14 +461,14 @@ MA.Trailer = new Class({
|
||||
watch: function(offset){
|
||||
var self = this;
|
||||
|
||||
var data_url = 'http://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 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.movie.getTitle()),
|
||||
'year': self.movie.get('year'),
|
||||
'title': encodeURI(self.getTitle()),
|
||||
'year': self.get('year'),
|
||||
'offset': offset || 1
|
||||
}),
|
||||
size = $(self.movie).getSize(),
|
||||
height = (size.x/16)*9,
|
||||
height = self.options.height || (size.x/16)*9,
|
||||
id = 'trailer-'+randomString();
|
||||
|
||||
self.player_container = new Element('div[id='+id+']');
|
||||
@@ -432,6 +574,11 @@ MA.Edit = new Class({
|
||||
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': {
|
||||
@@ -451,7 +598,34 @@ MA.Edit = new Class({
|
||||
});
|
||||
|
||||
|
||||
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){
|
||||
|
||||
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;
|
||||
|
||||
@@ -460,8 +634,9 @@ MA.Edit = new Class({
|
||||
'text': profile.label ? profile.label : profile.data.label
|
||||
}).inject(self.profile_select);
|
||||
|
||||
if(self.movie.profile && self.movie.profile.data && self.movie.profile.data.id == profile_id)
|
||||
if(self.movie.get('profile_id') == profile_id)
|
||||
self.profile_select.set('value', profile_id);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
@@ -477,7 +652,8 @@ MA.Edit = new Class({
|
||||
'data': {
|
||||
'id': self.movie.get('id'),
|
||||
'default_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')
|
||||
},
|
||||
'useSpinner': true,
|
||||
'spinnerTarget': $(self.movie),
|
||||
@@ -608,6 +784,7 @@ MA.Delete = new Class({
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
|
||||
self.movie.removeView();
|
||||
self.movie.slide('out');
|
||||
},
|
||||
|
||||
@@ -656,16 +833,45 @@ MA.Files = new Class({
|
||||
self.el = new Element('a.directory', {
|
||||
'title': 'Available files',
|
||||
'events': {
|
||||
'click': self.showFiles.bind(self)
|
||||
'click': self.show.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
showFiles: function(e){
|
||||
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')
|
||||
@@ -678,7 +884,7 @@ MA.Files = new Class({
|
||||
new Element('span.is_available', {'text': 'Available'})
|
||||
).inject(self.files_container)
|
||||
|
||||
Array.each(self.movie.data.releases, function(release){
|
||||
Array.each(self.releases, function(release){
|
||||
|
||||
var rel = new Element('div.release').inject(self.files_container);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,9 +11,10 @@ var Movie = new Class({
|
||||
self.view = options.view || 'details';
|
||||
self.list = list;
|
||||
|
||||
self.el = new Element('div.movie.inlay');
|
||||
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();
|
||||
@@ -22,16 +23,20 @@ var Movie = new Class({
|
||||
addEvents: function(){
|
||||
var self = this;
|
||||
|
||||
App.addEvent('movie.update.'+self.data.id, self.update.bind(self));
|
||||
App.addEvent('movie.update.'+self.data.id, function(notification){
|
||||
self.busy(false);
|
||||
self.removeView();
|
||||
self.update.delay(2000, self, notification);
|
||||
});
|
||||
|
||||
['movie.busy', 'searcher.started'].each(function(listener){
|
||||
['movie.busy', 'movie.searcher.started'].each(function(listener){
|
||||
App.addEvent(listener+'.'+self.data.id, function(notification){
|
||||
if(notification.data)
|
||||
self.busy(true)
|
||||
});
|
||||
})
|
||||
|
||||
App.addEvent('searcher.ended.'+self.data.id, function(notification){
|
||||
App.addEvent('movie.searcher.ended.'+self.data.id, function(notification){
|
||||
if(notification.data)
|
||||
self.busy(false)
|
||||
});
|
||||
@@ -48,26 +53,28 @@ var Movie = new Class({
|
||||
|
||||
// Remove events
|
||||
App.removeEvents('movie.update.'+self.data.id);
|
||||
['movie.busy', 'searcher.started'].each(function(listener){
|
||||
['movie.busy', 'movie.searcher.started'].each(function(listener){
|
||||
App.removeEvents(listener+'.'+self.data.id);
|
||||
})
|
||||
},
|
||||
|
||||
busy: function(set_busy){
|
||||
busy: function(set_busy, timeout){
|
||||
var self = this;
|
||||
|
||||
if(!set_busy){
|
||||
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;
|
||||
}, 400);
|
||||
}
|
||||
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();
|
||||
@@ -102,8 +109,10 @@ var Movie = new Class({
|
||||
|
||||
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);
|
||||
@@ -126,15 +135,14 @@ var Movie = new Class({
|
||||
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(
|
||||
self.title = new Element('div.title', {
|
||||
'text': self.getTitle() || 'n/a'
|
||||
}),
|
||||
self.year = new Element('div.year', {
|
||||
'text': self.data.library.year || 'n/a'
|
||||
}),
|
||||
self.rating = new Element('div.rating.icon', {
|
||||
'text': self.data.library.rating
|
||||
}),
|
||||
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
|
||||
}),
|
||||
@@ -142,8 +150,8 @@ var Movie = new Class({
|
||||
'events': {
|
||||
'click': function(e){
|
||||
var releases = self.el.getElement('.actions .releases');
|
||||
if(releases)
|
||||
releases.fireEvent('click', [e])
|
||||
if(releases.isVisible())
|
||||
releases.fireEvent('click', [e])
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -171,20 +179,21 @@ var Movie = new Class({
|
||||
});
|
||||
|
||||
// Add 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)
|
||||
}
|
||||
|
||||
});
|
||||
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)
|
||||
@@ -192,9 +201,6 @@ var Movie = new Class({
|
||||
self.actions.adopt(action)
|
||||
});
|
||||
|
||||
if(!self.data.library.rating)
|
||||
self.rating.hide();
|
||||
|
||||
},
|
||||
|
||||
addQuality: function(quality_id){
|
||||
@@ -237,10 +243,10 @@ var Movie = new Class({
|
||||
|
||||
if(direction == 'in'){
|
||||
self.temp_view = self.view;
|
||||
self.changeView('details')
|
||||
self.changeView('details');
|
||||
|
||||
self.el.addEvent('outerClick', function(){
|
||||
self.removeView()
|
||||
self.removeView();
|
||||
self.slide('out')
|
||||
})
|
||||
el.show();
|
||||
280
couchpotato/core/media/movie/_base/static/search.css
Normal file
280
couchpotato/core/media/movie/_base/static/search.css
Normal file
@@ -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;
|
||||
}
|
||||
@@ -7,33 +7,30 @@ Block.Search = new Class({
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
var focus_timer = 0;
|
||||
self.el = new Element('div.search_form').adopt(
|
||||
new Element('div.input').adopt(
|
||||
self.input = new Element('input.inlay', {
|
||||
self.input = new Element('input', {
|
||||
'placeholder': 'Search & add a new movie',
|
||||
'events': {
|
||||
'keyup': self.keyup.bind(self),
|
||||
'focus': function(){
|
||||
if(focus_timer) clearTimeout(focus_timer);
|
||||
self.el.addClass('focused')
|
||||
if(this.get('value'))
|
||||
self.hideResults(false)
|
||||
},
|
||||
'blur': function(){
|
||||
(function(){
|
||||
focus_timer = (function(){
|
||||
self.el.removeClass('focused')
|
||||
}).delay(2000);
|
||||
}).delay(100);
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Element('span.enter', {
|
||||
new Element('a.icon2', {
|
||||
'events': {
|
||||
'click': self.keyup.bind(self)
|
||||
},
|
||||
'text':'Enter'
|
||||
}),
|
||||
new Element('a', {
|
||||
'events': {
|
||||
'click': self.clear.bind(self)
|
||||
'click': self.clear.bind(self),
|
||||
'touchend': self.clear.bind(self)
|
||||
}
|
||||
})
|
||||
),
|
||||
@@ -59,13 +56,21 @@ Block.Search = new Class({
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
|
||||
self.last_q = '';
|
||||
self.input.set('value', '');
|
||||
self.input.focus()
|
||||
if(self.last_q === ''){
|
||||
self.input.blur()
|
||||
self.last_q = null;
|
||||
}
|
||||
else {
|
||||
|
||||
self.movies = []
|
||||
self.results.empty()
|
||||
self.el.removeClass('filled')
|
||||
self.last_q = '';
|
||||
self.input.set('value', '');
|
||||
self.input.focus()
|
||||
|
||||
self.movies = []
|
||||
self.results.empty()
|
||||
self.el.removeClass('filled')
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
hideResults: function(bool){
|
||||
@@ -92,8 +97,13 @@ Block.Search = new Class({
|
||||
|
||||
self.el[self.q() ? 'addClass' : 'removeClass']('filled')
|
||||
|
||||
if(self.q() != self.last_q && (['enter'].indexOf(e.key) > -1 || e.type == 'click'))
|
||||
self.autocomplete()
|
||||
if(self.q() != self.last_q){
|
||||
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(){
|
||||
var self = this;
|
||||
|
||||
if(self.api_request && self.api_request.running) return
|
||||
|
||||
var q = self.q();
|
||||
var cache = self.cache[q];
|
||||
var self = this,
|
||||
q = self.q(),
|
||||
cache = self.cache[q];
|
||||
|
||||
self.hideResults(false);
|
||||
|
||||
@@ -157,9 +164,6 @@ Block.Search = new Class({
|
||||
|
||||
});
|
||||
|
||||
if(q != self.q())
|
||||
self.list()
|
||||
|
||||
// Calculate result heights
|
||||
var w = window.getSize(),
|
||||
rc = self.result_container.getCoordinates();
|
||||
@@ -181,8 +185,11 @@ Block.Search = new Class({
|
||||
|
||||
Block.Search.Item = new Class({
|
||||
|
||||
Implements: [Options, Events],
|
||||
|
||||
initialize: function(info, options){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.info = info;
|
||||
self.alternative_titles = [];
|
||||
@@ -197,56 +204,36 @@ Block.Search.Item = new Class({
|
||||
self.el = new Element('div.movie_result', {
|
||||
'id': info.imdb
|
||||
}).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.data_container = new Element('div.data', {
|
||||
'tween': {
|
||||
duration: 400,
|
||||
transition: 'quint:in:out'
|
||||
},
|
||||
'events': {
|
||||
'click': self.showOptions.bind(self)
|
||||
}
|
||||
}).adopt(
|
||||
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
|
||||
'src': info.images.poster[0],
|
||||
'height': null,
|
||||
'width': null
|
||||
}) : null,
|
||||
new Element('div.info').adopt(
|
||||
self.title = new Element('h2', {
|
||||
'text': info.titles[0]
|
||||
}).adopt(
|
||||
self.info_container = new Element('div.info').adopt(
|
||||
new Element('h2').adopt(
|
||||
self.title = new Element('span.title', {
|
||||
'text': info.titles && info.titles.length > 0 ? info.titles[0] : 'Unknown'
|
||||
}),
|
||||
self.year = info.year ? new Element('span.year', {
|
||||
'text': info.year
|
||||
}) : 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){
|
||||
Object.each(info.actors, function(actor){
|
||||
new Element('span', {
|
||||
'text': actor
|
||||
}).inject(self.starring)
|
||||
if(info.titles)
|
||||
info.titles.each(function(title){
|
||||
self.alternativeTitle({
|
||||
'title': title
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
info.titles.each(function(title){
|
||||
self.alternativeTitle({
|
||||
'title': title
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
alternativeTitle: function(alternative){
|
||||
@@ -255,6 +242,20 @@ Block.Search.Item = new Class({
|
||||
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(){
|
||||
var self = this;
|
||||
|
||||
@@ -274,7 +275,9 @@ Block.Search.Item = new Class({
|
||||
|
||||
add: function(e){
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
self.loadingMask();
|
||||
|
||||
@@ -282,7 +285,8 @@ Block.Search.Item = new Class({
|
||||
'data': {
|
||||
'identifier': self.info.imdb,
|
||||
'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){
|
||||
self.options_el.empty();
|
||||
@@ -292,6 +296,8 @@ Block.Search.Item = new Class({
|
||||
})
|
||||
);
|
||||
self.mask.fade('out');
|
||||
|
||||
self.fireEvent('added');
|
||||
},
|
||||
'onFailure': function(){
|
||||
self.options_el.empty();
|
||||
@@ -319,14 +325,11 @@ Block.Search.Item = new Class({
|
||||
}
|
||||
|
||||
self.options_el.grab(
|
||||
new Element('div').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.info.in_wanted && self.info.in_wanted.profile ? new Element('span.in_wanted', {
|
||||
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
|
||||
new Element('div', {
|
||||
'class': self.info.in_wanted && self.info.in_wanted.profile_id || in_library ? 'in_library_wanted' : ''
|
||||
}).adopt(
|
||||
self.info.in_wanted && self.info.in_wanted.profile_id ? new Element('span.in_wanted', {
|
||||
'text': 'Already in wanted list: ' + Quality.getProfile(self.info.in_wanted.profile_id).get('label')
|
||||
}) : (in_library ? new Element('span.in_library', {
|
||||
'text': 'Already in library: ' + in_library.join(', ')
|
||||
}) : null),
|
||||
@@ -336,7 +339,12 @@ Block.Search.Item = new Class({
|
||||
self.profile_select = new Element('select', {
|
||||
'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',
|
||||
'events': {
|
||||
'click': self.add.bind(self)
|
||||
@@ -351,7 +359,28 @@ Block.Search.Item = new Class({
|
||||
}).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', {
|
||||
'value': profile.id ? profile.id : profile.data.id,
|
||||
'text': profile.label ? profile.label : profile.data.label
|
||||
@@ -359,6 +388,11 @@ Block.Search.Item = new Class({
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
6
couchpotato/core/media/movie/library/movie/__init__.py
Normal file
6
couchpotato/core/media/movie/library/movie/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .main import MovieLibraryPlugin
|
||||
|
||||
def start():
|
||||
return MovieLibraryPlugin()
|
||||
|
||||
config = []
|
||||
@@ -1,9 +1,8 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||
from couchpotato.core.helpers.variable import mergeDicts
|
||||
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 string import ascii_letters
|
||||
import time
|
||||
@@ -11,17 +10,20 @@ import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
class LibraryPlugin(Plugin):
|
||||
|
||||
class MovieLibraryPlugin(LibraryBase):
|
||||
|
||||
default_dict = {'titles': {}, 'files':{}}
|
||||
|
||||
def __init__(self):
|
||||
addEvent('library.add', self.add)
|
||||
addEvent('library.update', self.update)
|
||||
addEvent('library.update_release_date', self.updateReleaseDate)
|
||||
addEvent('library.add.movie', self.add)
|
||||
addEvent('library.update.movie', self.update)
|
||||
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()
|
||||
|
||||
@@ -33,7 +35,8 @@ class LibraryPlugin(Plugin):
|
||||
identifier = attrs.get('identifier'),
|
||||
plot = toUnicode(attrs.get('plot')),
|
||||
tagline = toUnicode(attrs.get('tagline')),
|
||||
status_id = status.get('id')
|
||||
status_id = status.get('id'),
|
||||
info = {}
|
||||
)
|
||||
|
||||
title = LibraryTitle(
|
||||
@@ -49,37 +52,39 @@ class LibraryPlugin(Plugin):
|
||||
# Update library info
|
||||
if update_after is not False:
|
||||
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)
|
||||
|
||||
db.expire_all()
|
||||
return library_dict
|
||||
|
||||
def update(self, identifier, default_title = '', force = False):
|
||||
|
||||
if self.shuttingDown():
|
||||
return
|
||||
|
||||
db = get_session()
|
||||
library = db.query(Library).filter_by(identifier = identifier).first()
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
|
||||
library_dict = None
|
||||
if library:
|
||||
library_dict = library.to_dict(self.default_dict)
|
||||
|
||||
do_update = True
|
||||
|
||||
if library.status_id == done_status.get('id') and not force:
|
||||
do_update = False
|
||||
else:
|
||||
info = fireEvent('movie.info', merge = True, identifier = identifier)
|
||||
info = fireEvent('movie.info', merge = True, identifier = identifier)
|
||||
|
||||
# Don't need those here
|
||||
try: del info['in_wanted']
|
||||
except: pass
|
||||
try: del info['in_library']
|
||||
except: pass
|
||||
# Don't need those here
|
||||
try: del info['in_wanted']
|
||||
except: pass
|
||||
try: del info['in_library']
|
||||
except: pass
|
||||
|
||||
if not info or len(info) == 0:
|
||||
log.error('Could not update, no movie info to work with: %s', identifier)
|
||||
return False
|
||||
if not info or len(info) == 0:
|
||||
log.error('Could not update, no movie info to work with: %s', identifier)
|
||||
return False
|
||||
|
||||
# Main info
|
||||
if do_update:
|
||||
@@ -87,7 +92,7 @@ class LibraryPlugin(Plugin):
|
||||
library.tagline = toUnicode(info.get('tagline', ''))
|
||||
library.year = info.get('year', 0)
|
||||
library.status_id = done_status.get('id')
|
||||
library.info = info
|
||||
library.info.update(info)
|
||||
db.commit()
|
||||
|
||||
# Titles
|
||||
@@ -132,6 +137,7 @@ class LibraryPlugin(Plugin):
|
||||
|
||||
library_dict = library.to_dict(self.default_dict)
|
||||
|
||||
db.expire_all()
|
||||
return library_dict
|
||||
|
||||
def updateReleaseDate(self, identifier):
|
||||
@@ -147,9 +153,10 @@ class LibraryPlugin(Plugin):
|
||||
|
||||
if dates and dates.get('expires', 0) < time.time() or not dates:
|
||||
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.expire_all()
|
||||
return dates
|
||||
|
||||
|
||||
73
couchpotato/core/media/movie/searcher/__init__.py
Normal file
73
couchpotato/core/media/movie/searcher/__init__.py
Normal file
@@ -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."
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
438
couchpotato/core/media/movie/searcher/main.py
Normal file
438
couchpotato/core/media/movie/searcher/main.py
Normal file
@@ -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
|
||||
17
couchpotato/core/migration/versions/002_Movie_category.py
Normal file
17
couchpotato/core/migration/versions/002_Movie_category.py
Normal file
@@ -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
|
||||
@@ -1,14 +1,15 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
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
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Notification(Plugin):
|
||||
class Notification(Provider):
|
||||
|
||||
type = 'notification'
|
||||
|
||||
default_title = Env.get('appname')
|
||||
test_message = 'ZOMG Lazors Pewpewpew!'
|
||||
@@ -16,11 +17,12 @@ class Notification(Plugin):
|
||||
listen_to = [
|
||||
'renamer.after', 'movie.snatched',
|
||||
'updater.available', 'updater.updated',
|
||||
'core.message',
|
||||
]
|
||||
dont_listen_to = []
|
||||
|
||||
def __init__(self):
|
||||
addEvent('notify.%s' % self.getName().lower(), self.notify)
|
||||
addEvent('notify.%s' % self.getName().lower(), self._notify)
|
||||
|
||||
addApiView(self.testNotifyName(), self.test)
|
||||
|
||||
@@ -30,29 +32,41 @@ class Notification(Plugin):
|
||||
addEvent(listener, self.createNotifyHandler(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':
|
||||
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
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
pass
|
||||
def getNotificationImage(self, size = 'small'):
|
||||
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()
|
||||
|
||||
log.info('Sending test to %s', test_type)
|
||||
|
||||
success = self.notify(
|
||||
success = self._notify(
|
||||
message = self.test_message,
|
||||
data = {},
|
||||
listener = 'test'
|
||||
)
|
||||
|
||||
return jsonified({'success': success})
|
||||
return {
|
||||
'success': success
|
||||
}
|
||||
|
||||
def testNotifyName(self):
|
||||
return 'notify.%s.test' % self.getName().lower()
|
||||
|
||||
@@ -10,8 +10,8 @@ class Boxcar(Notification):
|
||||
|
||||
url = 'https://boxcar.io/devices/providers/7MNNXY3UIzVBwvzkKwkC/notifications'
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
try:
|
||||
message = message.strip()
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
from couchpotato import get_session
|
||||
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.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from couchpotato.core.settings.model import Notification as Notif
|
||||
from couchpotato.environment import Env
|
||||
from operator import itemgetter
|
||||
from sqlalchemy.sql.expression import or_
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
import uuid
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -17,14 +19,7 @@ log = CPLog(__name__)
|
||||
|
||||
class CoreNotifier(Notification):
|
||||
|
||||
m_lock = threading.Lock()
|
||||
messages = []
|
||||
listeners = []
|
||||
|
||||
listen_to = [
|
||||
'renamer.after', 'movie.snatched',
|
||||
'updater.available', 'updater.updated',
|
||||
]
|
||||
m_lock = None
|
||||
|
||||
def __init__(self):
|
||||
super(CoreNotifier, self).__init__()
|
||||
@@ -54,7 +49,15 @@ class CoreNotifier(Notification):
|
||||
addNonBlockApiView('notification.listener', (self.addListener, self.removeListener))
|
||||
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.checkMessages)
|
||||
|
||||
self.messages = []
|
||||
self.listeners = []
|
||||
self.m_lock = threading.Lock()
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -63,11 +66,9 @@ class CoreNotifier(Notification):
|
||||
db.commit()
|
||||
|
||||
|
||||
def markAsRead(self):
|
||||
def markAsRead(self, ids = None, **kwargs):
|
||||
|
||||
ids = None
|
||||
if getParam('ids'):
|
||||
ids = splitString(getParam('ids'))
|
||||
ids = splitString(ids) if ids else None
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -80,14 +81,13 @@ class CoreNotifier(Notification):
|
||||
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def listView(self):
|
||||
def listView(self, limit_offset = None, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
limit_offset = getParam('limit_offset', None)
|
||||
|
||||
q = db.query(Notif)
|
||||
|
||||
@@ -106,13 +106,30 @@ class CoreNotifier(Notification):
|
||||
ndict['type'] = 'notification'
|
||||
notifications.append(ndict)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(notifications) == 0,
|
||||
'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()
|
||||
|
||||
@@ -133,7 +150,10 @@ class CoreNotifier(Notification):
|
||||
|
||||
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()
|
||||
notification = {
|
||||
@@ -153,10 +173,12 @@ class CoreNotifier(Notification):
|
||||
'result': [notification],
|
||||
})
|
||||
except:
|
||||
break
|
||||
log.debug('Failed sending to listener: %s', traceback.format_exc())
|
||||
|
||||
self.listeners = []
|
||||
self.m_lock.release()
|
||||
self.cleanMessages()
|
||||
|
||||
log.debug('Done notifying frontend')
|
||||
|
||||
def addListener(self, callback, last_id = None):
|
||||
|
||||
@@ -168,59 +190,72 @@ class CoreNotifier(Notification):
|
||||
'result': messages,
|
||||
})
|
||||
|
||||
self.m_lock.acquire()
|
||||
self.listeners.append((callback, last_id))
|
||||
self.m_lock.release()
|
||||
|
||||
|
||||
def removeListener(self, callback):
|
||||
|
||||
self.m_lock.acquire()
|
||||
for list_tuple in self.listeners:
|
||||
try:
|
||||
listener, last_id = list_tuple
|
||||
if listener == callback:
|
||||
self.listeners.remove(list_tuple)
|
||||
except:
|
||||
pass
|
||||
|
||||
def cleanMessages(self):
|
||||
self.m_lock.acquire()
|
||||
|
||||
for message in self.messages:
|
||||
if message['time'] < (time.time() - 15):
|
||||
self.messages.remove(message)
|
||||
|
||||
log.debug('Failed removing listener: %s', traceback.format_exc())
|
||||
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):
|
||||
|
||||
log.debug('Getting messages with id: %s', last_id)
|
||||
self.m_lock.acquire()
|
||||
|
||||
recent = []
|
||||
index = 0
|
||||
for i in xrange(len(self.messages)):
|
||||
index = len(self.messages) - i - 1
|
||||
if self.messages[index]["message_id"] == last_id: break
|
||||
recent = self.messages[index:]
|
||||
try:
|
||||
index = map(itemgetter('message_id'), self.messages).index(last_id)
|
||||
recent = self.messages[index + 1:]
|
||||
except:
|
||||
pass
|
||||
|
||||
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 = []
|
||||
|
||||
# Get unread
|
||||
if getParam('init'):
|
||||
if init:
|
||||
db = get_session()
|
||||
|
||||
notifications = db.query(Notif) \
|
||||
.filter(or_(Notif.read == False, Notif.added > (time.time() - 259200))) \
|
||||
.all()
|
||||
|
||||
for n in notifications:
|
||||
ndict = n.to_dict()
|
||||
ndict['type'] = 'notification'
|
||||
messages.append(ndict)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'result': messages,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -21,21 +21,18 @@ var NotificationBase = new Class({
|
||||
App.addEvent('load', function(){
|
||||
|
||||
App.block.notification = new Block.Menu(self, {
|
||||
'button_class': 'icon2.eye-open',
|
||||
'class': 'notification_menu',
|
||||
'onOpen': self.markAsRead.bind(self)
|
||||
})
|
||||
$(App.block.notification).inject(App.getBlock('search'), 'after');
|
||||
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(){
|
||||
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(
|
||||
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})
|
||||
)
|
||||
, 'top');
|
||||
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)
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
@@ -64,20 +66,26 @@ var NotificationBase = new Class({
|
||||
self.badge[value ? 'show' : 'hide']()
|
||||
},
|
||||
|
||||
markAsRead: function(){
|
||||
var self = this;
|
||||
markAsRead: function(force_ids){
|
||||
var self = this,
|
||||
ids = force_ids;
|
||||
|
||||
var rn = self.notifications.filter(function(n){
|
||||
return !n.read
|
||||
})
|
||||
if(!force_ids) {
|
||||
var rn = self.notifications.filter(function(n){
|
||||
return !n.read && n.data.important === undefined
|
||||
})
|
||||
|
||||
var ids = []
|
||||
rn.each(function(n){
|
||||
ids.include(n.id)
|
||||
})
|
||||
var ids = []
|
||||
rn.each(function(n){
|
||||
ids.include(n.id)
|
||||
})
|
||||
}
|
||||
|
||||
if(ids.length > 0)
|
||||
Api.request('notification.markread', {
|
||||
'data': {
|
||||
'ids': ids.join(',')
|
||||
},
|
||||
'onSuccess': function(){
|
||||
self.setBadge('')
|
||||
}
|
||||
@@ -93,11 +101,20 @@ var NotificationBase = new Class({
|
||||
return;
|
||||
}
|
||||
|
||||
Api.request('notification.listener', {
|
||||
self.request = Api.request('notification.listener', {
|
||||
'data': {'init':true},
|
||||
'onSuccess': self.processData.bind(self)
|
||||
}).send()
|
||||
|
||||
setInterval(function(){
|
||||
|
||||
if(self.request && self.request.isRunning()){
|
||||
self.request.cancel();
|
||||
self.startPoll()
|
||||
}
|
||||
|
||||
}, 120000);
|
||||
|
||||
},
|
||||
|
||||
startPoll: function(){
|
||||
@@ -140,29 +157,44 @@ var NotificationBase = new Class({
|
||||
}
|
||||
|
||||
// Restart poll
|
||||
self.startPoll()
|
||||
self.startPoll.delay(1500, self);
|
||||
},
|
||||
|
||||
showMessage: function(message){
|
||||
showMessage: function(message, sticky, data){
|
||||
var self = this;
|
||||
|
||||
if(!self.message_container)
|
||||
self.message_container = new Element('div.messages').inject(document.body);
|
||||
|
||||
var new_message = new Element('div.message', {
|
||||
'text': message
|
||||
}).inject(self.message_container);
|
||||
var new_message = new Element('div', {
|
||||
'class': 'message' + (sticky ? ' sticky' : ''),
|
||||
'html': message
|
||||
}).inject(self.message_container, 'top');
|
||||
|
||||
setTimeout(function(){
|
||||
new_message.addClass('show')
|
||||
}, 10);
|
||||
|
||||
setTimeout(function(){
|
||||
var hide_message = function(){
|
||||
new_message.addClass('hide')
|
||||
setTimeout(function(){
|
||||
new_message.destroy();
|
||||
}, 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);
|
||||
|
||||
},
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ log = CPLog(__name__)
|
||||
|
||||
class Email(Notification):
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
# Extract all the settings from settings
|
||||
from_address = self.conf('from')
|
||||
@@ -50,6 +50,5 @@ class Email(Notification):
|
||||
return True
|
||||
except:
|
||||
log.error('E-mail failed: %s', traceback.format_exc())
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
@@ -43,8 +43,8 @@ class Growl(Notification):
|
||||
else:
|
||||
log.error('Failed register of growl: %s', traceback.format_exc())
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
self.register()
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.request import getParams, jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import re
|
||||
@@ -22,21 +21,17 @@ class NMJ(Notification):
|
||||
addApiView(self.testNotifyName(), self.test)
|
||||
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 = ''
|
||||
|
||||
try:
|
||||
terminal = telnetlib.Telnet(host)
|
||||
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()
|
||||
|
||||
log.debug('Connected to %s via telnet', (host))
|
||||
log.debug('Connected to %s via telnet', host)
|
||||
terminal.read_until('sh-3.00# ')
|
||||
terminal.write('cat /tmp/source\n')
|
||||
terminal.write('cat /tmp/netshare\n')
|
||||
@@ -50,7 +45,7 @@ class NMJ(Notification):
|
||||
device = match.group(2)
|
||||
log.info('Found NMJ database %s on device %s', (database, device))
|
||||
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()
|
||||
|
||||
if device.startswith('NETWORK_SHARE/'):
|
||||
@@ -58,28 +53,29 @@ class NMJ(Notification):
|
||||
|
||||
if match:
|
||||
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:
|
||||
log.error('Detected a network share on the Popcorn Hour, but could not get the mounting url')
|
||||
return self.failed()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'database': database,
|
||||
'mount': mount,
|
||||
})
|
||||
}
|
||||
|
||||
def addToLibrary(self, message = None, group = {}):
|
||||
def addToLibrary(self, message = None, group = None):
|
||||
if self.isDisabled(): return
|
||||
if not group: group = {}
|
||||
|
||||
host = self.conf('host')
|
||||
mount = self.conf('mount')
|
||||
database = self.conf('database')
|
||||
|
||||
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:
|
||||
data = self.urlopen(mount)
|
||||
self.urlopen(mount)
|
||||
except:
|
||||
return False
|
||||
|
||||
@@ -102,20 +98,24 @@ class NMJ(Notification):
|
||||
et = etree.fromstring(response)
|
||||
result = et.findtext('returnValue')
|
||||
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
|
||||
|
||||
if int(result) > 0:
|
||||
log.error('Popcorn Hour returned an errorcode: %s', (result))
|
||||
log.error('Popcorn Hour returned an errorcode: %s', result)
|
||||
return False
|
||||
else:
|
||||
log.info('NMJ started background scan')
|
||||
return True
|
||||
|
||||
def failed(self):
|
||||
return jsonified({'success': False})
|
||||
return {
|
||||
'success': False
|
||||
}
|
||||
|
||||
def test(self):
|
||||
return jsonified({'success': self.addToLibrary()})
|
||||
def test(self, **kwargs):
|
||||
return {
|
||||
'success': self.addToLibrary()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from flask.helpers import json
|
||||
import base64
|
||||
import json
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -12,8 +12,8 @@ class Notifo(Notification):
|
||||
|
||||
url = 'https://api.notifo.com/v1/send_notification'
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
try:
|
||||
params = {
|
||||
|
||||
@@ -8,20 +8,17 @@ log = CPLog(__name__)
|
||||
|
||||
class NotifyMyAndroid(Notification):
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
nma = pynma.PyNMA()
|
||||
keys = splitString(self.conf('api_key'))
|
||||
nma.addkey(keys)
|
||||
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(
|
||||
application = self.default_title,
|
||||
event = self.event,
|
||||
event = message.split(' ')[0],
|
||||
description = message,
|
||||
priority = self.conf('priority'),
|
||||
batch_mode = len(keys) > 1
|
||||
|
||||
@@ -9,7 +9,6 @@ log = CPLog(__name__)
|
||||
class NotifyMyWP(Notification):
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
keys = splitString(self.conf('api_key'))
|
||||
p = PyNMWP(keys, self.conf('dev_key'))
|
||||
|
||||
@@ -22,6 +22,13 @@ config = [{
|
||||
'description': 'Default should be on localhost',
|
||||
'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.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.helpers.variable import cleanHost, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from urllib2 import URLError
|
||||
from urlparse import urlparse
|
||||
from xml.dom import minidom
|
||||
import traceback
|
||||
|
||||
@@ -17,16 +17,17 @@ class Plex(Notification):
|
||||
super(Plex, self).__init__()
|
||||
addEvent('renamer.after', self.addToLibrary)
|
||||
|
||||
def addToLibrary(self, message = None, group = {}):
|
||||
def addToLibrary(self, message = None, group = None):
|
||||
if self.isDisabled(): return
|
||||
if not group: group = {}
|
||||
|
||||
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:
|
||||
|
||||
source_type = ['movie']
|
||||
base_url = '%slibrary/sections' % host
|
||||
base_url = '%s/library/sections' % host
|
||||
refresh_url = '%s/%%s/refresh' % base_url
|
||||
|
||||
try:
|
||||
@@ -37,7 +38,7 @@ class Plex(Notification):
|
||||
for s in sections:
|
||||
if s.getAttribute('type') in source_type:
|
||||
url = refresh_url % s.getAttribute('key')
|
||||
x = self.urlopen(url)
|
||||
self.urlopen(url)
|
||||
|
||||
except:
|
||||
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
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
hosts = [x.strip() + ':3000' for x in self.conf('host').split(",")]
|
||||
hosts = self.getHosts(port = 3000)
|
||||
successful = 0
|
||||
for host in hosts:
|
||||
if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host):
|
||||
@@ -58,8 +59,7 @@ class Plex(Notification):
|
||||
|
||||
def send(self, command, host):
|
||||
|
||||
url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command))
|
||||
|
||||
url = '%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command))
|
||||
headers = {}
|
||||
|
||||
try:
|
||||
@@ -74,7 +74,7 @@ class Plex(Notification):
|
||||
log.info('Plex notification to %s successful.', host)
|
||||
return True
|
||||
|
||||
def test(self):
|
||||
def test(self, **kwargs):
|
||||
|
||||
test_type = self.testNotifyName()
|
||||
|
||||
@@ -87,4 +87,21 @@ class Plex(Notification):
|
||||
)
|
||||
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
|
||||
|
||||
@@ -12,8 +12,8 @@ class Prowl(Notification):
|
||||
'api': 'https://api.prowlapp.com/publicapi/add'
|
||||
}
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
data = {
|
||||
'apikey': self.conf('api_key'),
|
||||
|
||||
@@ -11,17 +11,17 @@ class Pushalot(Notification):
|
||||
'api': 'https://pushalot.com/api/sendmessage'
|
||||
}
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
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),
|
||||
'LinkTitle': toUnicode("CouchPotato"),
|
||||
'link': toUnicode("https://couchpota.to/"),
|
||||
'IsImportant': self.conf('important'),
|
||||
'IsSilent': self.conf('silent'),
|
||||
'Image': toUnicode(self.getNotificationImage('medium') + '?1'),
|
||||
'Source': toUnicode(self.default_title)
|
||||
}
|
||||
|
||||
headers = {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||
from couchpotato.core.helpers.variable import getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from httplib import HTTPSConnection
|
||||
@@ -10,22 +11,28 @@ class Pushover(Notification):
|
||||
|
||||
app_token = 'YkxHMYDZp285L265L3IwH3LmzkTaCy'
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
http_handler = HTTPSConnection("api.pushover.net:443")
|
||||
|
||||
data = {
|
||||
api_data = {
|
||||
'user': self.conf('user_key'),
|
||||
'token': self.app_token,
|
||||
'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',
|
||||
"/1/messages.json",
|
||||
headers = {'Content-type': 'application/x-www-form-urlencoded'},
|
||||
body = tryUrlencode(data)
|
||||
body = tryUrlencode(api_data)
|
||||
)
|
||||
|
||||
response = http_handler.getresponse()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import os
|
||||
@@ -16,8 +15,9 @@ class Synoindex(Notification):
|
||||
super(Synoindex, self).__init__()
|
||||
addEvent('renamer.after', self.addToLibrary)
|
||||
|
||||
def addToLibrary(self, message = None, group = {}):
|
||||
def addToLibrary(self, message = None, group = None):
|
||||
if self.isDisabled(): return
|
||||
if not group: group = {}
|
||||
|
||||
command = [self.index_path, '-A', group.get('destination_dir')]
|
||||
log.info('Executing synoindex command: %s ', command)
|
||||
@@ -28,9 +28,10 @@ class Synoindex(Notification):
|
||||
return True
|
||||
except OSError, e:
|
||||
log.error('Unable to run synoindex: %s', e)
|
||||
return False
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def test(self):
|
||||
return jsonified({'success': os.path.isfile(self.index_path)})
|
||||
def test(self, **kwargs):
|
||||
return {
|
||||
'success': os.path.isfile(self.index_path)
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ class Toasty(Notification):
|
||||
'api': 'http://api.supertoasty.com/notify/%s?%s'
|
||||
}
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
data = {
|
||||
'title': self.default_title,
|
||||
|
||||
30
couchpotato/core/notifications/trakt/__init__.py
Normal file
30
couchpotato/core/notifications/trakt/__init__.py
Normal file
@@ -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',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
47
couchpotato/core/notifications/trakt/main.py
Normal file
47
couchpotato/core/notifications/trakt/main.py
Normal file
@@ -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
|
||||
@@ -1,12 +1,11 @@
|
||||
from couchpotato.api import addApiView
|
||||
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.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from flask.helpers import url_for
|
||||
from pytwitter import Api, parse_qsl
|
||||
from werkzeug.utils import redirect
|
||||
from couchpotato.environment import Env
|
||||
from pytwitter import Api
|
||||
from urlparse import parse_qsl
|
||||
import oauth2
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -31,8 +30,8 @@ class Twitter(Notification):
|
||||
addApiView('notify.%s.auth_url' % self.getName().lower(), self.getAuthorizationUrl)
|
||||
addApiView('notify.%s.credentials' % self.getName().lower(), self.getCredentials)
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
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:
|
||||
if direct_message:
|
||||
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:
|
||||
update_message = '[%s] %s' % (self.default_title, message)
|
||||
if len(update_message) > 140:
|
||||
@@ -71,10 +70,9 @@ class Twitter(Notification):
|
||||
|
||||
return True
|
||||
|
||||
def getAuthorizationUrl(self):
|
||||
def getAuthorizationUrl(self, host = None, **kwargs):
|
||||
|
||||
referer = getParam('host')
|
||||
callback_url = cleanHost(referer) + '%snotify.%s.credentials/' % (url_for('api.index').lstrip('/'), self.getName().lower())
|
||||
callback_url = cleanHost(host) + '%snotify.%s.credentials/' % (Env.get('api_base').lstrip('/'), self.getName().lower())
|
||||
|
||||
oauth_consumer = oauth2.Consumer(self.consumer_key, self.consumer_secret)
|
||||
oauth_client = oauth2.Client(oauth_consumer)
|
||||
@@ -83,31 +81,29 @@ class Twitter(Notification):
|
||||
|
||||
if resp['status'] != '200':
|
||||
log.error('Invalid response from Twitter requesting temp token: %s', resp['status'])
|
||||
return jsonified({
|
||||
return {
|
||||
'success': False,
|
||||
})
|
||||
}
|
||||
else:
|
||||
self.request_token = dict(parse_qsl(content))
|
||||
|
||||
auth_url = self.urls['authorize'] + ("?oauth_token=%s" % self.request_token['oauth_token'])
|
||||
|
||||
log.info('Redirecting to "%s"', auth_url)
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'url': auth_url,
|
||||
})
|
||||
}
|
||||
|
||||
def getCredentials(self):
|
||||
|
||||
key = getParam('oauth_verifier')
|
||||
def getCredentials(self, oauth_verifier, **kwargs):
|
||||
|
||||
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_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))
|
||||
|
||||
if resp['status'] != '200':
|
||||
@@ -122,4 +118,4 @@ class Twitter(Notification):
|
||||
|
||||
self.request_token = None
|
||||
|
||||
return redirect(url_for('web.index') + 'settings/notifications/')
|
||||
return 'redirect', Env.get('web_base') + 'settings/notifications/'
|
||||
|
||||
@@ -31,6 +31,28 @@ config = [{
|
||||
'default': '',
|
||||
'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,8 +1,10 @@
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from flask.helpers import json
|
||||
from urllib2 import URLError
|
||||
import base64
|
||||
import json
|
||||
import socket
|
||||
import traceback
|
||||
import urllib
|
||||
|
||||
@@ -11,41 +13,56 @@ log = CPLog(__name__)
|
||||
|
||||
class XBMC(Notification):
|
||||
|
||||
listen_to = ['renamer.after']
|
||||
listen_to = ['renamer.after', 'movie.snatched']
|
||||
use_json_notifications = {}
|
||||
couch_logo_url = 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/xbmc-notify.png'
|
||||
http_time_between_calls = 0
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
hosts = splitString(self.conf('host'))
|
||||
|
||||
successful = 0
|
||||
max_successful = 0
|
||||
for host in hosts:
|
||||
|
||||
if self.use_json_notifications.get(host) is None:
|
||||
self.getXBMCJSONversion(host, message = message)
|
||||
|
||||
if self.use_json_notifications.get(host):
|
||||
response = self.request(host, [
|
||||
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.couch_logo_url}),
|
||||
('VideoLibrary.Scan', {}),
|
||||
])
|
||||
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})
|
||||
response += self.request(host, [('VideoLibrary.Scan', {})])
|
||||
|
||||
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:
|
||||
for result in response:
|
||||
if (result.get('result') and result['result'] == 'OK'):
|
||||
if result.get('result') and result['result'] == 'OK':
|
||||
successful += 1
|
||||
elif (result.get('error')):
|
||||
elif result.get('error'):
|
||||
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
|
||||
|
||||
except:
|
||||
log.error('Failed parsing results: %s', traceback.format_exc())
|
||||
|
||||
return successful == len(hosts) * 2
|
||||
return successful == max_successful
|
||||
|
||||
def getXBMCJSONversion(self, host, message = ''):
|
||||
|
||||
@@ -54,9 +71,9 @@ class XBMC(Notification):
|
||||
# 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'):
|
||||
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'])
|
||||
@@ -69,15 +86,15 @@ class XBMC(Notification):
|
||||
# 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'):
|
||||
if result.get('result') and result['result'] == 'OK':
|
||||
log.debug('Message delivered successfully!')
|
||||
success = True
|
||||
break
|
||||
elif (result.get('error')):
|
||||
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'):
|
||||
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'])
|
||||
@@ -90,18 +107,18 @@ class XBMC(Notification):
|
||||
self.use_json_notifications[host] = True
|
||||
|
||||
# send the text message
|
||||
resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image':self.couch_logo_url})])
|
||||
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'):
|
||||
if result.get('result') and result['result'] == 'OK':
|
||||
log.debug('Message delivered successfully!')
|
||||
success = True
|
||||
break
|
||||
elif (result.get('error')):
|
||||
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')):
|
||||
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)
|
||||
@@ -113,7 +130,7 @@ class XBMC(Notification):
|
||||
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.couch_logo_url))
|
||||
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 :)
|
||||
@@ -139,7 +156,7 @@ class XBMC(Notification):
|
||||
# <li>Error:<message>
|
||||
# </html>
|
||||
#
|
||||
response = self.urlopen(server, headers = headers)
|
||||
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))
|
||||
@@ -150,6 +167,13 @@ class XBMC(Notification):
|
||||
# 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'}]
|
||||
@@ -178,11 +202,17 @@ class XBMC(Notification):
|
||||
|
||||
try:
|
||||
log.debug('Sending request to %s: %s', (host, data))
|
||||
rdata = self.urlopen(server, headers = headers, params = data, multipart = True)
|
||||
response = json.loads(rdata)
|
||||
response = self.getJsonData(server, headers = headers, params = data, timeout = 3, show_error = False)
|
||||
log.debug('Returned from request %s: %s', (host, 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:
|
||||
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
|
||||
return []
|
||||
|
||||
@@ -36,6 +36,20 @@ config = [{
|
||||
'unit': 'hours',
|
||||
'description': 'hours',
|
||||
},
|
||||
{
|
||||
'name': 'required_genres',
|
||||
'label': 'Required Genres',
|
||||
'default': '',
|
||||
'placeholder': 'Example: Action, Crime & Drama',
|
||||
'description': 'Ignore movies that don\'t contain at least one set of genres. Sets are separated by "," and each word within a set must be separated with "&"'
|
||||
},
|
||||
{
|
||||
'name': 'ignored_genres',
|
||||
'label': 'Ignored Genres',
|
||||
'default': '',
|
||||
'placeholder': 'Example: Horror, Comedy & Drama & Romance',
|
||||
'description': 'Ignore movies that contain at least one set of genres. Sets work the same as above.'
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -10,17 +10,26 @@ class Automation(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12))
|
||||
addEvent('app.load', self.setCrons)
|
||||
|
||||
if not Env.get('dev'):
|
||||
addEvent('app.load', self.addMovies)
|
||||
|
||||
addEvent('setting.save.automation.hour.after', self.setCrons)
|
||||
|
||||
def setCrons(self):
|
||||
fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12))
|
||||
|
||||
def addMovies(self):
|
||||
|
||||
movies = fireEvent('automation.get_movies', merge = True)
|
||||
movie_ids = []
|
||||
|
||||
for imdb_id in movies:
|
||||
|
||||
if self.shuttingDown():
|
||||
break
|
||||
|
||||
prop_name = 'automation.added.%s' % imdb_id
|
||||
added = Env.prop(prop_name, default = False)
|
||||
if not added:
|
||||
@@ -30,5 +39,11 @@ class Automation(Plugin):
|
||||
Env.prop(prop_name, True)
|
||||
|
||||
for movie_id in movie_ids:
|
||||
|
||||
if self.shuttingDown():
|
||||
break
|
||||
|
||||
movie_dict = fireEvent('movie.get', movie_id, single = True)
|
||||
fireEvent('searcher.single', movie_dict)
|
||||
fireEvent('movie.searcher.single', movie_dict)
|
||||
|
||||
return True
|
||||
@@ -1,16 +1,18 @@
|
||||
from StringIO import StringIO
|
||||
from couchpotato import addView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString
|
||||
from couchpotato.core.helpers.variable import getExt, md5
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString, \
|
||||
toUnicode
|
||||
from couchpotato.core.helpers.variable import getExt, md5, isLocalIP
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from flask.templating import render_template_string
|
||||
from multipartpost import MultipartPostHandler
|
||||
from tornado import template
|
||||
from tornado.web import StaticFileHandler
|
||||
from urlparse import urlparse
|
||||
import cookielib
|
||||
import glob
|
||||
import gzip
|
||||
import inspect
|
||||
import math
|
||||
import os.path
|
||||
import re
|
||||
@@ -23,55 +25,76 @@ log = CPLog(__name__)
|
||||
|
||||
class Plugin(object):
|
||||
|
||||
_class_name = None
|
||||
plugin_path = None
|
||||
|
||||
enabled_option = 'enabled'
|
||||
auto_register_static = True
|
||||
|
||||
_needs_shutdown = False
|
||||
_running = None
|
||||
|
||||
user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20130519 Firefox/24.0'
|
||||
http_last_use = {}
|
||||
http_time_between_calls = 0
|
||||
http_failed_request = {}
|
||||
http_failed_disabled = {}
|
||||
|
||||
def __new__(typ, *args, **kwargs):
|
||||
new_plugin = super(Plugin, typ).__new__(typ)
|
||||
new_plugin.registerPlugin()
|
||||
|
||||
return new_plugin
|
||||
|
||||
def registerPlugin(self):
|
||||
addEvent('app.do_shutdown', self.doShutdown)
|
||||
addEvent('plugin.running', self.isRunning)
|
||||
self._running = []
|
||||
|
||||
def conf(self, attr, value = None, default = None):
|
||||
return Env.setting(attr, self.getName().lower(), value = value, default = default)
|
||||
if self.auto_register_static:
|
||||
self.registerStatic(inspect.getfile(self.__class__))
|
||||
|
||||
def conf(self, attr, value = None, default = None, section = None):
|
||||
class_name = self.getName().lower().split(':')
|
||||
return Env.setting(attr, section = section if section else class_name[0].lower(), value = value, default = default)
|
||||
|
||||
def getName(self):
|
||||
return self.__class__.__name__
|
||||
return self._class_name or self.__class__.__name__
|
||||
|
||||
def renderTemplate(self, parent_file, template, **params):
|
||||
def setName(self, name):
|
||||
self._class_name = name
|
||||
|
||||
template = open(os.path.join(os.path.dirname(parent_file), template), 'r').read()
|
||||
return render_template_string(template, **params)
|
||||
def renderTemplate(self, parent_file, templ, **params):
|
||||
|
||||
t = template.Template(open(os.path.join(os.path.dirname(parent_file), templ), 'r').read())
|
||||
return t.generate(**params)
|
||||
|
||||
def registerStatic(self, plugin_file, add_to_head = True):
|
||||
|
||||
# Register plugin path
|
||||
self.plugin_path = os.path.dirname(plugin_file)
|
||||
static_folder = toUnicode(os.path.join(self.plugin_path, 'static'))
|
||||
|
||||
if not os.path.isdir(static_folder):
|
||||
return
|
||||
|
||||
# Get plugin_name from PluginName
|
||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', self.__class__.__name__)
|
||||
class_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
||||
|
||||
path = 'api/%s/static/%s/' % (Env.setting('api_key'), class_name)
|
||||
addView(path + '<path:filename>', self.showStatic, static = True)
|
||||
# View path
|
||||
path = 'static/plugin/%s/' % (class_name)
|
||||
|
||||
# Add handler to Tornado
|
||||
Env.get('app').add_handlers(".*$", [(Env.get('web_base') + path + '(.*)', StaticFileHandler, {'path': static_folder})])
|
||||
|
||||
# Register for HTML <HEAD>
|
||||
if add_to_head:
|
||||
for f in glob.glob(os.path.join(self.plugin_path, 'static', '*')):
|
||||
ext = getExt(f)
|
||||
if ext in ['js', 'css']:
|
||||
fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f)
|
||||
|
||||
def showStatic(self, filename):
|
||||
d = os.path.join(self.plugin_path, 'static')
|
||||
|
||||
from flask.helpers import send_from_directory
|
||||
return send_from_directory(d, filename)
|
||||
|
||||
def createFile(self, path, content, binary = False):
|
||||
path = ss(path)
|
||||
|
||||
@@ -104,19 +127,22 @@ class Plugin(object):
|
||||
if not params: params = {}
|
||||
|
||||
# Fill in some headers
|
||||
headers['Referer'] = headers.get('Referer', urlparse(url).hostname)
|
||||
headers['Host'] = headers.get('Host', urlparse(url).hostname)
|
||||
headers['User-Agent'] = headers.get('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:10.0.2) Gecko/20100101 Firefox/10.0.2')
|
||||
headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip')
|
||||
parsed_url = urlparse(url)
|
||||
host = '%s%s' % (parsed_url.hostname, (':' + str(parsed_url.port) if parsed_url.port else ''))
|
||||
|
||||
host = urlparse(url).hostname
|
||||
headers['Referer'] = headers.get('Referer', '%s://%s' % (parsed_url.scheme, host))
|
||||
headers['Host'] = headers.get('Host', host)
|
||||
headers['User-Agent'] = headers.get('User-Agent', self.user_agent)
|
||||
headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip')
|
||||
headers['Connection'] = headers.get('Connection', 'keep-alive')
|
||||
headers['Cache-Control'] = headers.get('Cache-Control', 'max-age=0')
|
||||
|
||||
# Don't try for failed requests
|
||||
if self.http_failed_disabled.get(host, 0) > 0:
|
||||
if self.http_failed_disabled[host] > (time.time() - 900):
|
||||
log.info2('Disabled calls to %s for 15 minutes because so many failed requests.', host)
|
||||
if not show_error:
|
||||
raise
|
||||
raise Exception('Disabled calls to %s for 15 minutes because so many failed requests')
|
||||
else:
|
||||
return ''
|
||||
else:
|
||||
@@ -126,6 +152,10 @@ class Plugin(object):
|
||||
self.wait(host)
|
||||
try:
|
||||
|
||||
# Make sure opener has the correct headers
|
||||
if opener:
|
||||
opener.add_headers = headers
|
||||
|
||||
if multipart:
|
||||
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
|
||||
request = urllib2.Request(url, params, headers)
|
||||
@@ -138,8 +168,13 @@ class Plugin(object):
|
||||
|
||||
response = opener.open(request, timeout = timeout)
|
||||
else:
|
||||
log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()]))
|
||||
data = tryUrlencode(params) if len(params) > 0 else None
|
||||
log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
|
||||
|
||||
if isinstance(params, (str, unicode)) and len(params) > 0:
|
||||
data = params
|
||||
else:
|
||||
data = tryUrlencode(params) if len(params) > 0 else None
|
||||
|
||||
request = urllib2.Request(url, data, headers)
|
||||
|
||||
if opener:
|
||||
@@ -152,8 +187,10 @@ class Plugin(object):
|
||||
buf = StringIO(response.read())
|
||||
f = gzip.GzipFile(fileobj = buf)
|
||||
data = f.read()
|
||||
f.close()
|
||||
else:
|
||||
data = response.read()
|
||||
response.close()
|
||||
|
||||
self.http_failed_request[host] = 0
|
||||
except IOError:
|
||||
@@ -168,7 +205,7 @@ class Plugin(object):
|
||||
self.http_failed_request[host] += 1
|
||||
|
||||
# Disable temporarily
|
||||
if self.http_failed_request[host] > 5:
|
||||
if self.http_failed_request[host] > 5 and not isLocalIP(host):
|
||||
self.http_failed_disabled[host] = time.time()
|
||||
|
||||
except:
|
||||
@@ -209,9 +246,6 @@ class Plugin(object):
|
||||
|
||||
def isRunning(self, value = None, boolean = True):
|
||||
|
||||
if not hasattr(self, '_running'):
|
||||
self._running = []
|
||||
|
||||
if value is None:
|
||||
return self._running
|
||||
|
||||
@@ -225,8 +259,8 @@ class Plugin(object):
|
||||
|
||||
|
||||
def getCache(self, cache_key, url = None, **kwargs):
|
||||
cache_key = md5(ss(cache_key))
|
||||
cache = Env.get('cache').get(cache_key)
|
||||
cache_key_md5 = md5(cache_key)
|
||||
cache = Env.get('cache').get(cache_key_md5)
|
||||
if cache:
|
||||
if not Env.get('dev'): log.debug('Getting cache %s', cache_key)
|
||||
return cache
|
||||
@@ -250,8 +284,9 @@ class Plugin(object):
|
||||
return ''
|
||||
|
||||
def setCache(self, cache_key, value, timeout = 300):
|
||||
cache_key_md5 = md5(cache_key)
|
||||
log.debug('Setting cache %s', cache_key)
|
||||
Env.get('cache').set(cache_key, value, timeout)
|
||||
Env.get('cache').set(cache_key_md5, value, timeout)
|
||||
return value
|
||||
|
||||
def createNzbName(self, data, movie):
|
||||
@@ -260,9 +295,9 @@ class Plugin(object):
|
||||
|
||||
def createFileName(self, data, filedata, movie):
|
||||
name = os.path.join(self.createNzbName(data, movie))
|
||||
if data.get('type') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
|
||||
if data.get('protocol') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
|
||||
return '%s.%s' % (name, 'rar')
|
||||
return '%s.%s' % (name, data.get('type'))
|
||||
return '%s.%s' % (name, data.get('protocol'))
|
||||
|
||||
def cpTag(self, movie):
|
||||
if Env.setting('enabled', 'renamer'):
|
||||
@@ -274,4 +309,4 @@ class Plugin(object):
|
||||
return not self.isEnabled()
|
||||
|
||||
def isEnabled(self):
|
||||
return self.conf(self.enabled_option) or self.conf(self.enabled_option) == None
|
||||
return self.conf(self.enabled_option) or self.conf(self.enabled_option) is None
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.helpers.request import getParam, jsonified
|
||||
from couchpotato.core.helpers.variable import getUserDir
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
import ctypes
|
||||
@@ -13,7 +12,7 @@ if os.name == 'nt':
|
||||
except:
|
||||
# todo:: subclass ImportError for missing dependencies, vs. broken plugins?
|
||||
raise ImportError("Missing the win32file module, which is a part of the prerequisite \
|
||||
pywin32 package. You can get it from http://sourceforge.net/projects/pywin32/files/pywin32/");
|
||||
pywin32 package. You can get it from http://sourceforge.net/projects/pywin32/files/pywin32/")
|
||||
else:
|
||||
import win32file #@UnresolvedImport
|
||||
|
||||
@@ -63,16 +62,15 @@ class FileBrowser(Plugin):
|
||||
|
||||
return driveletters
|
||||
|
||||
def view(self):
|
||||
def view(self, path = '/', show_hidden = True, **kwargs):
|
||||
|
||||
path = getParam('path', '/')
|
||||
home = getUserDir()
|
||||
|
||||
if not path:
|
||||
path = home
|
||||
|
||||
try:
|
||||
dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True))
|
||||
dirs = self.getDirectories(path = path, show_hidden = show_hidden)
|
||||
except:
|
||||
dirs = []
|
||||
|
||||
@@ -82,14 +80,14 @@ class FileBrowser(Plugin):
|
||||
elif parent != '/' and parent[-2:] != ':\\':
|
||||
parent += os.path.sep
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'is_root': path == '/',
|
||||
'empty': len(dirs) == 0,
|
||||
'parent': parent,
|
||||
'home': home + os.path.sep,
|
||||
'platform': os.name,
|
||||
'dirs': dirs,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
def is_hidden(self, filepath):
|
||||
|
||||
6
couchpotato/core/plugins/category/__init__.py
Normal file
6
couchpotato/core/plugins/category/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .main import CategoryPlugin
|
||||
|
||||
def start():
|
||||
return CategoryPlugin()
|
||||
|
||||
config = []
|
||||
121
couchpotato/core/plugins/category/main.py
Normal file
121
couchpotato/core/plugins/category/main.py
Normal file
@@ -0,0 +1,121 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Movie, Category
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class CategoryPlugin(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('category.all', self.all)
|
||||
|
||||
addApiView('category.save', self.save)
|
||||
addApiView('category.save_order', self.saveOrder)
|
||||
addApiView('category.delete', self.delete)
|
||||
addApiView('category.list', self.allView, docs = {
|
||||
'desc': 'List all available categories',
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'success': True,
|
||||
'list': array, categories
|
||||
}"""}
|
||||
})
|
||||
|
||||
def allView(self, **kwargs):
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'list': self.all()
|
||||
}
|
||||
|
||||
def all(self):
|
||||
|
||||
db = get_session()
|
||||
categories = db.query(Category).all()
|
||||
|
||||
temp = []
|
||||
for category in categories:
|
||||
temp.append(category.to_dict())
|
||||
|
||||
db.expire_all()
|
||||
return temp
|
||||
|
||||
def save(self, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
|
||||
c = db.query(Category).filter_by(id = kwargs.get('id')).first()
|
||||
if not c:
|
||||
c = Category()
|
||||
db.add(c)
|
||||
|
||||
c.order = kwargs.get('order', c.order if c.order else 0)
|
||||
c.label = toUnicode(kwargs.get('label', ''))
|
||||
c.ignored = toUnicode(kwargs.get('ignored', ''))
|
||||
c.preferred = toUnicode(kwargs.get('preferred', ''))
|
||||
c.required = toUnicode(kwargs.get('required', ''))
|
||||
c.destination = toUnicode(kwargs.get('destination', ''))
|
||||
|
||||
db.commit()
|
||||
|
||||
category_dict = c.to_dict()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'category': category_dict
|
||||
}
|
||||
|
||||
def saveOrder(self, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
|
||||
order = 0
|
||||
for category_id in kwargs.get('ids', []):
|
||||
c = db.query(Category).filter_by(id = category_id).first()
|
||||
c.order = order
|
||||
|
||||
order += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
'success': True
|
||||
}
|
||||
|
||||
def delete(self, id = None, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
|
||||
success = False
|
||||
message = ''
|
||||
try:
|
||||
c = db.query(Category).filter_by(id = id).first()
|
||||
db.delete(c)
|
||||
db.commit()
|
||||
|
||||
# Force defaults on all empty category movies
|
||||
self.removeFromMovie(id)
|
||||
|
||||
success = True
|
||||
except Exception, e:
|
||||
message = log.error('Failed deleting category: %s', e)
|
||||
|
||||
db.expire_all()
|
||||
return {
|
||||
'success': success,
|
||||
'message': message
|
||||
}
|
||||
|
||||
def removeFromMovie(self, category_id):
|
||||
|
||||
db = get_session()
|
||||
movies = db.query(Movie).filter(Movie.category_id == category_id).all()
|
||||
|
||||
if len(movies) > 0:
|
||||
for movie in movies:
|
||||
movie.category_id = None
|
||||
db.commit()
|
||||
82
couchpotato/core/plugins/category/static/category.css
Normal file
82
couchpotato/core/plugins/category/static/category.css
Normal file
@@ -0,0 +1,82 @@
|
||||
.add_new_category {
|
||||
padding: 20px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.category {
|
||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.category > .delete {
|
||||
position: absolute;
|
||||
padding: 16px;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
color: #fd5353;
|
||||
}
|
||||
.category > .delete:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.category .ctrlHolder:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.category .formHint {
|
||||
width: 250px !important;
|
||||
margin: 0 !important;
|
||||
opacity: 0.1;
|
||||
}
|
||||
.category:hover .formHint {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#category_ordering {
|
||||
|
||||
}
|
||||
|
||||
#category_ordering ul {
|
||||
float: left;
|
||||
margin: 0;
|
||||
width: 275px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#category_ordering li {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||
padding: 0 5px;
|
||||
}
|
||||
#category_ordering li:last-child { border: 0; }
|
||||
|
||||
#category_ordering li .check {
|
||||
margin: 2px 10px 0 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#category_ordering li > span {
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
vertical-align: top;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#category_ordering li .handle {
|
||||
background: url('../../static/profile_plugin/handle.png') center;
|
||||
width: 20px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
#category_ordering .formHint {
|
||||
clear: none;
|
||||
float: right;
|
||||
width: 250px;
|
||||
margin: 0;
|
||||
}
|
||||
332
couchpotato/core/plugins/category/static/category.js
Normal file
332
couchpotato/core/plugins/category/static/category.js
Normal file
@@ -0,0 +1,332 @@
|
||||
var CategoryListBase = new Class({
|
||||
|
||||
initialize: function(){
|
||||
var self = this;
|
||||
|
||||
App.addEvent('load', self.addSettings.bind(self));
|
||||
},
|
||||
|
||||
setup: function(categories){
|
||||
var self = this;
|
||||
|
||||
self.categories = []
|
||||
Array.each(categories, self.createCategory.bind(self));
|
||||
|
||||
},
|
||||
|
||||
addSettings: function(){
|
||||
var self = this;
|
||||
|
||||
self.settings = App.getPage('Settings')
|
||||
self.settings.addEvent('create', function(){
|
||||
var tab = self.settings.createSubTab('category', {
|
||||
'label': 'Categories',
|
||||
'name': 'category',
|
||||
'subtab_label': 'Category & filtering'
|
||||
}, self.settings.tabs.searcher ,'searcher');
|
||||
|
||||
self.tab = tab.tab;
|
||||
self.content = tab.content;
|
||||
|
||||
self.createList();
|
||||
self.createOrdering();
|
||||
|
||||
})
|
||||
|
||||
// Add categories in renamer
|
||||
self.settings.addEvent('create', function(){
|
||||
var renamer_group = self.settings.tabs.renamer.groups.renamer;
|
||||
|
||||
self.categories.each(function(category){
|
||||
|
||||
var input = new Option.Directory('section_name', 'option.name', category.get('destination'), {
|
||||
'name': category.get('label')
|
||||
});
|
||||
input.inject(renamer_group.getElement('.renamer_to'));
|
||||
input.fireEvent('injected');
|
||||
|
||||
input.save = function(){
|
||||
category.data.destination = input.getValue();
|
||||
category.save();
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
createList: function(){
|
||||
var self = this;
|
||||
|
||||
var count = self.categories.length;
|
||||
|
||||
self.settings.createGroup({
|
||||
'label': 'Categories',
|
||||
'description': 'Create categories, each one extending global filters. (Needs refresh \'' +(App.isMac() ? 'CMD+R' : 'F5')+ '\' after editing)'
|
||||
}).inject(self.content).adopt(
|
||||
self.category_container = new Element('div.container'),
|
||||
new Element('a.add_new_category', {
|
||||
'text': count > 0 ? 'Create another category' : 'Click here to create a category.',
|
||||
'events': {
|
||||
'click': function(){
|
||||
var category = self.createCategory();
|
||||
$(category).inject(self.category_container)
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Add categories, that aren't part of the core (for editing)
|
||||
Array.each(self.categories, function(category){
|
||||
$(category).inject(self.category_container)
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
getCategory: function(id){
|
||||
return this.categories.filter(function(category){
|
||||
return category.data.id == id
|
||||
}).pick()
|
||||
},
|
||||
|
||||
getAll: function(){
|
||||
return this.categories;
|
||||
},
|
||||
|
||||
createCategory: function(data){
|
||||
var self = this;
|
||||
|
||||
var data = data || {'id': randomString()}
|
||||
var category = new Category(data)
|
||||
self.categories.include(category)
|
||||
|
||||
return category;
|
||||
},
|
||||
|
||||
createOrdering: function(){
|
||||
var self = this;
|
||||
|
||||
var category_list;
|
||||
var group = self.settings.createGroup({
|
||||
'label': 'Category ordering'
|
||||
}).adopt(
|
||||
new Element('.ctrlHolder#category_ordering').adopt(
|
||||
new Element('label[text=Order]'),
|
||||
category_list = new Element('ul'),
|
||||
new Element('p.formHint', {
|
||||
'html': 'Change the order the categories are in the dropdown list.<br />First one will be default.'
|
||||
})
|
||||
)
|
||||
).inject(self.content)
|
||||
|
||||
Array.each(self.categories, function(category){
|
||||
new Element('li', {'data-id': category.data.id}).adopt(
|
||||
new Element('span.category_label', {
|
||||
'text': category.data.label
|
||||
}),
|
||||
new Element('span.handle')
|
||||
).inject(category_list);
|
||||
|
||||
});
|
||||
|
||||
// Sortable
|
||||
self.category_sortable = new Sortables(category_list, {
|
||||
'revert': true,
|
||||
'handle': '',
|
||||
'opacity': 0.5,
|
||||
'onComplete': self.saveOrdering.bind(self)
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
saveOrdering: function(){
|
||||
var self = this;
|
||||
|
||||
var ids = [];
|
||||
|
||||
self.category_sortable.list.getElements('li').each(function(el, nr){
|
||||
ids.include(el.get('data-id'));
|
||||
});
|
||||
|
||||
Api.request('category.save_order', {
|
||||
'data': {
|
||||
'ids': ids
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
window.CategoryList = new CategoryListBase();
|
||||
|
||||
var Category = new Class({
|
||||
|
||||
data: {},
|
||||
|
||||
initialize: function(data){
|
||||
var self = this;
|
||||
|
||||
self.data = data;
|
||||
|
||||
self.create();
|
||||
|
||||
self.el.addEvents({
|
||||
'change:relay(select)': self.save.bind(self, 0),
|
||||
'keyup:relay(input[type=text])': self.save.bind(self, [300])
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
var data = self.data;
|
||||
|
||||
self.el = new Element('div.category').adopt(
|
||||
self.delete_button = new Element('span.delete.icon2', {
|
||||
'events': {
|
||||
'click': self.del.bind(self)
|
||||
}
|
||||
}),
|
||||
new Element('.category_label.ctrlHolder').adopt(
|
||||
new Element('label', {'text':'Name'}),
|
||||
new Element('input.inlay', {
|
||||
'type':'text',
|
||||
'value': data.label,
|
||||
'placeholder': 'Example: Kids, Horror or His'
|
||||
}),
|
||||
new Element('p.formHint', {'text': 'See global filters for explanation.'})
|
||||
),
|
||||
new Element('.category_preferred.ctrlHolder').adopt(
|
||||
new Element('label', {'text':'Preferred'}),
|
||||
new Element('input.inlay', {
|
||||
'type':'text',
|
||||
'value': data.preferred,
|
||||
'placeholder': 'Blu-ray, DTS'
|
||||
})
|
||||
),
|
||||
new Element('.category_required.ctrlHolder').adopt(
|
||||
new Element('label', {'text':'Required'}),
|
||||
new Element('input.inlay', {
|
||||
'type':'text',
|
||||
'value': data.required,
|
||||
'placeholder': 'Example: DTS, AC3 & English'
|
||||
})
|
||||
),
|
||||
new Element('.category_ignored.ctrlHolder').adopt(
|
||||
new Element('label', {'text':'Ignored'}),
|
||||
new Element('input.inlay', {
|
||||
'type':'text',
|
||||
'value': data.ignored,
|
||||
'placeholder': 'Example: dubbed, swesub, french'
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
self.makeSortable()
|
||||
|
||||
},
|
||||
|
||||
save: function(delay){
|
||||
var self = this;
|
||||
|
||||
if(self.save_timer) clearTimeout(self.save_timer);
|
||||
self.save_timer = (function(){
|
||||
|
||||
var data = self.getData();
|
||||
|
||||
Api.request('category.save', {
|
||||
'data': self.getData(),
|
||||
'useSpinner': true,
|
||||
'spinnerOptions': {
|
||||
'target': self.el
|
||||
},
|
||||
'onComplete': function(json){
|
||||
if(json.success){
|
||||
self.data = json.category;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}).delay(delay || 0, self)
|
||||
|
||||
},
|
||||
|
||||
getData: function(){
|
||||
var self = this;
|
||||
|
||||
var data = {
|
||||
'id' : self.data.id,
|
||||
'label' : self.el.getElement('.category_label input').get('value'),
|
||||
'required' : self.el.getElement('.category_required input').get('value'),
|
||||
'preferred' : self.el.getElement('.category_preferred input').get('value'),
|
||||
'ignored' : self.el.getElement('.category_ignored input').get('value'),
|
||||
'destination': self.data.destination
|
||||
}
|
||||
|
||||
return data
|
||||
},
|
||||
|
||||
del: function(){
|
||||
var self = this;
|
||||
|
||||
if(self.data.label == undefined){
|
||||
self.el.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
var label = self.el.getElement('.category_label input').get('value');
|
||||
var qObj = new Question('Are you sure you want to delete <strong>"'+label+'"</strong>?', '', [{
|
||||
'text': 'Delete "'+label+'"',
|
||||
'class': 'delete',
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
Api.request('category.delete', {
|
||||
'data': {
|
||||
'id': self.data.id
|
||||
},
|
||||
'useSpinner': true,
|
||||
'spinnerOptions': {
|
||||
'target': self.el
|
||||
},
|
||||
'onComplete': function(json){
|
||||
if(json.success) {
|
||||
qObj.close();
|
||||
self.el.destroy();
|
||||
} else {
|
||||
alert(json.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, {
|
||||
'text': 'Cancel',
|
||||
'cancel': true
|
||||
}]);
|
||||
|
||||
},
|
||||
|
||||
makeSortable: function(){
|
||||
var self = this;
|
||||
|
||||
self.sortable = new Sortables(self.category_container, {
|
||||
'revert': true,
|
||||
'handle': '.handle',
|
||||
'opacity': 0.5,
|
||||
'onComplete': self.save.bind(self, 300)
|
||||
});
|
||||
},
|
||||
|
||||
get: function(attr){
|
||||
return this.data[attr]
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el
|
||||
}
|
||||
|
||||
});
|
||||
BIN
couchpotato/core/plugins/category/static/handle.png
Normal file
BIN
couchpotato/core/plugins/category/static/handle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 160 B |
@@ -1,13 +1,13 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.request import jsonified, getParams
|
||||
from couchpotato.core.helpers.variable import splitString, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Movie
|
||||
from couchpotato.core.settings.model import Movie, Library, LibraryTitle
|
||||
from sqlalchemy.orm import joinedload_all
|
||||
import random
|
||||
from sqlalchemy.sql.expression import asc
|
||||
import random as rndm
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -16,41 +16,10 @@ log = CPLog(__name__)
|
||||
class Dashboard(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
addApiView('dashboard.suggestions', self.suggestView)
|
||||
addApiView('dashboard.soon', self.getSoonView)
|
||||
|
||||
def newSuggestions(self):
|
||||
def getSoonView(self, limit_offset = None, random = False, late = False, **kwargs):
|
||||
|
||||
movies = fireEvent('movie.list', status = ['active', 'done'], limit_offset = (20, 0), single = True)
|
||||
movie_identifiers = [m['library']['identifier'] for m in movies[1]]
|
||||
|
||||
ignored_movies = fireEvent('movie.list', status = ['ignored', 'deleted'], limit_offset = (100, 0), single = True)
|
||||
ignored_identifiers = [m['library']['identifier'] for m in ignored_movies[1]]
|
||||
|
||||
suggestions = fireEvent('movie.suggest', movies = movie_identifiers, ignore = ignored_identifiers, single = True)
|
||||
suggest_status = fireEvent('status.get', 'suggest', single = True)
|
||||
|
||||
for suggestion in suggestions:
|
||||
fireEvent('movie.add', params = {'identifier': suggestion}, force_readd = False, search_after = False, status_id = suggest_status.get('id'))
|
||||
|
||||
def suggestView(self):
|
||||
|
||||
db = get_session()
|
||||
|
||||
movies = db.query(Movie).limit(20).all()
|
||||
identifiers = [m.library.identifier for m in movies]
|
||||
|
||||
suggestions = fireEvent('movie.suggest', movies = identifiers, single = True)
|
||||
|
||||
return jsonified({
|
||||
'result': True,
|
||||
'suggestions': suggestions
|
||||
})
|
||||
|
||||
def getSoonView(self):
|
||||
|
||||
params = getParams()
|
||||
db = get_session()
|
||||
now = time.time()
|
||||
|
||||
@@ -72,63 +41,85 @@ class Dashboard(Plugin):
|
||||
|
||||
profile_pre[profile.get('id')] = contains
|
||||
|
||||
# Get all active movies
|
||||
active_status = fireEvent('status.get', 'active', single = True)
|
||||
subq = db.query(Movie).filter(Movie.status_id == active_status.get('id')).subquery()
|
||||
|
||||
q = db.query(Movie).join((subq, subq.c.id == Movie.id)) \
|
||||
.options(joinedload_all('releases')) \
|
||||
.options(joinedload_all('profile.types')) \
|
||||
.options(joinedload_all('library.titles')) \
|
||||
.options(joinedload_all('library.files')) \
|
||||
.options(joinedload_all('status')) \
|
||||
.options(joinedload_all('files'))
|
||||
|
||||
# Add limit
|
||||
limit_offset = params.get('limit_offset')
|
||||
limit = 12
|
||||
if limit_offset:
|
||||
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
|
||||
limit = tryInt(splt[0])
|
||||
|
||||
all_movies = q.all()
|
||||
# Get all active movies
|
||||
active_status = fireEvent('status.get', ['active'], single = True)
|
||||
q = db.query(Movie) \
|
||||
.join(Library) \
|
||||
.filter(Movie.status_id == active_status.get('id')) \
|
||||
.with_entities(Movie.id, Movie.profile_id, Library.info, Library.year) \
|
||||
.group_by(Movie.id)
|
||||
|
||||
if params.get('random', False):
|
||||
random.shuffle(all_movies)
|
||||
if not random:
|
||||
q = q.join(LibraryTitle) \
|
||||
.filter(LibraryTitle.default == True) \
|
||||
.order_by(asc(LibraryTitle.simple_title))
|
||||
|
||||
active = q.all()
|
||||
movies = []
|
||||
for movie in all_movies:
|
||||
pp = profile_pre.get(movie.profile.id)
|
||||
eta = movie.library.info.get('release_date', {}) or {}
|
||||
coming_soon = False
|
||||
|
||||
# Theater quality
|
||||
if pp.get('theater') and fireEvent('searcher.could_be_released', True, eta, single = True):
|
||||
coming_soon = True
|
||||
if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, single = True):
|
||||
coming_soon = True
|
||||
if len(active) > 0:
|
||||
|
||||
# Do the shuffle
|
||||
if random:
|
||||
rndm.shuffle(active)
|
||||
|
||||
if coming_soon:
|
||||
temp = movie.to_dict({
|
||||
'profile': {'types': {}},
|
||||
'releases': {'files':{}, 'info': {}},
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {},
|
||||
})
|
||||
movie_ids = []
|
||||
for movie in active:
|
||||
movie_id, profile_id, info, year = movie
|
||||
|
||||
# Don't list older movies
|
||||
if ((not params.get('late') and (not eta.get('dvd') or (eta.get('dvd') and eta.get('dvd') > (now - 2419200)))) or \
|
||||
(params.get('late') and eta.get('dvd') and eta.get('dvd') < (now - 2419200))):
|
||||
movies.append(temp)
|
||||
pp = profile_pre.get(profile_id)
|
||||
if not pp: continue
|
||||
|
||||
if len(movies) >= limit:
|
||||
break
|
||||
eta = info.get('release_date', {}) or {}
|
||||
coming_soon = False
|
||||
|
||||
return jsonified({
|
||||
# Theater quality
|
||||
if pp.get('theater') and fireEvent('movie.searcher.could_be_released', True, eta, year, single = True):
|
||||
coming_soon = True
|
||||
elif pp.get('dvd') and fireEvent('movie.searcher.could_be_released', False, eta, year, single = True):
|
||||
coming_soon = True
|
||||
|
||||
if coming_soon:
|
||||
|
||||
# Don't list older movies
|
||||
if ((not late and (not eta.get('dvd') and not eta.get('theater') or eta.get('dvd') and eta.get('dvd') > (now - 2419200))) or
|
||||
(late and (eta.get('dvd', 0) > 0 or eta.get('theater')) and eta.get('dvd') < (now - 2419200))):
|
||||
movie_ids.append(movie_id)
|
||||
|
||||
if len(movie_ids) >= limit:
|
||||
break
|
||||
|
||||
if len(movie_ids) > 0:
|
||||
|
||||
# Get all movie information
|
||||
movies_raw = db.query(Movie) \
|
||||
.options(joinedload_all('library.titles')) \
|
||||
.options(joinedload_all('library.files')) \
|
||||
.options(joinedload_all('files')) \
|
||||
.filter(Movie.id.in_(movie_ids)) \
|
||||
.all()
|
||||
|
||||
# Create dict by movie id
|
||||
movie_dict = {}
|
||||
for movie in movies_raw:
|
||||
movie_dict[movie.id] = movie
|
||||
|
||||
for movie_id in movie_ids:
|
||||
movies.append(movie_dict[movie_id].to_dict({
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {},
|
||||
}))
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'movies': movies,
|
||||
})
|
||||
}
|
||||
|
||||
getLateView = getSoonView
|
||||
|
||||
@@ -2,13 +2,13 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.helpers.variable import md5, getExt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.plugins.scanner.main import Scanner
|
||||
from couchpotato.core.settings.model import FileType, File
|
||||
from couchpotato.environment import Env
|
||||
from tornado.web import StaticFileHandler
|
||||
import os.path
|
||||
import time
|
||||
import traceback
|
||||
@@ -23,7 +23,7 @@ class FileManager(Plugin):
|
||||
addEvent('file.download', self.download)
|
||||
addEvent('file.types', self.getTypes)
|
||||
|
||||
addApiView('file.cache/<path:filename>', self.showCacheFile, static = True, docs = {
|
||||
addApiView('file.cache/(.*)', self.showCacheFile, static = True, docs = {
|
||||
'desc': 'Return a file from the cp_data/cache directory',
|
||||
'params': {
|
||||
'filename': {'desc': 'path/filename of the wanted file'}
|
||||
@@ -71,23 +71,20 @@ class FileManager(Plugin):
|
||||
db = get_session()
|
||||
for root, dirs, walk_files in os.walk(Env.get('cache_dir')):
|
||||
for filename in walk_files:
|
||||
if root == python_cache or 'minified' in filename: continue
|
||||
file_path = os.path.join(root, filename)
|
||||
f = db.query(File).filter(File.path == toUnicode(file_path)).first()
|
||||
if not f:
|
||||
os.remove(file_path)
|
||||
if os.path.splitext(filename)[1] in ['.png', '.jpg', '.jpeg']:
|
||||
file_path = os.path.join(root, filename)
|
||||
f = db.query(File).filter(File.path == toUnicode(file_path)).first()
|
||||
if not f:
|
||||
os.remove(file_path)
|
||||
except:
|
||||
log.error('Failed removing unused file: %s', traceback.format_exc())
|
||||
|
||||
def showCacheFile(self, filename = ''):
|
||||
def showCacheFile(self, route, **kwargs):
|
||||
Env.get('app').add_handlers(".*$", [('%s%s' % (Env.get('api_base'), route), StaticFileHandler, {'path': Env.get('cache_dir')})])
|
||||
|
||||
cache_dir = Env.get('cache_dir')
|
||||
filename = os.path.basename(filename)
|
||||
|
||||
from flask.helpers import send_from_directory
|
||||
return send_from_directory(cache_dir, filename)
|
||||
|
||||
def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}):
|
||||
def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = None):
|
||||
if not urlopen_kwargs: urlopen_kwargs = {}
|
||||
|
||||
if not dest: # to Cache
|
||||
dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url)))
|
||||
@@ -104,7 +101,9 @@ class FileManager(Plugin):
|
||||
self.createFile(dest, filedata, binary = True)
|
||||
return dest
|
||||
|
||||
def add(self, path = '', part = 1, type_tuple = (), available = 1, properties = {}):
|
||||
def add(self, path = '', part = 1, type_tuple = (), available = 1, properties = None):
|
||||
if not properties: properties = {}
|
||||
|
||||
type_id = self.getType(type_tuple).get('id')
|
||||
db = get_session()
|
||||
|
||||
@@ -154,8 +153,8 @@ class FileManager(Plugin):
|
||||
|
||||
return types
|
||||
|
||||
def getTypesView(self):
|
||||
def getTypesView(self, **kwargs):
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'types': self.getTypes()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
var File = new Class({
|
||||
|
||||
initialize: function(file){
|
||||
initialize: function(type, file){
|
||||
var self = this;
|
||||
|
||||
if(!file){
|
||||
self.empty = true;
|
||||
self.el = new Element('div');
|
||||
self.el = new Element('div.empty_file.'+type);
|
||||
return
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ var File = new Class({
|
||||
var file_name = self.data.path.replace(/^.*[\\\/]/, '');
|
||||
|
||||
self.el = new Element('div', {
|
||||
'class': 'type_image ' + self.type.identifier
|
||||
'class': 'type_image ' + self.type.identifier,
|
||||
'styles': {
|
||||
'background-image': 'url('+Api.createUrl('file.cache') + file_name+')'
|
||||
}
|
||||
}).adopt(
|
||||
new Element('img', {
|
||||
'src': Api.createUrl('file.cache') + file_name
|
||||
@@ -45,7 +48,7 @@ var FileSelect = new Class({
|
||||
});
|
||||
|
||||
if(single)
|
||||
return new File(results.pop());
|
||||
return new File(type, results.pop());
|
||||
|
||||
return results;
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
from .main import LibraryPlugin
|
||||
|
||||
def start():
|
||||
return LibraryPlugin()
|
||||
|
||||
config = []
|
||||
@@ -1,6 +1,5 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified, getParam, getParams
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -47,9 +46,9 @@ class Logging(Plugin):
|
||||
}
|
||||
})
|
||||
|
||||
def get(self):
|
||||
def get(self, nr = 0, **kwargs):
|
||||
|
||||
nr = int(getParam('nr', 0))
|
||||
nr = tryInt(nr)
|
||||
current_path = None
|
||||
|
||||
total = 1
|
||||
@@ -71,16 +70,15 @@ class Logging(Plugin):
|
||||
f = open(current_path, 'r')
|
||||
log = f.read()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'log': toUnicode(log),
|
||||
'total': total,
|
||||
})
|
||||
}
|
||||
|
||||
def partial(self):
|
||||
def partial(self, type = 'all', lines = 30, **kwargs):
|
||||
|
||||
log_type = getParam('type', 'all')
|
||||
total_lines = tryInt(getParam('lines', 30))
|
||||
total_lines = tryInt(lines)
|
||||
|
||||
log_lines = []
|
||||
|
||||
@@ -92,7 +90,6 @@ class Logging(Plugin):
|
||||
if not os.path.isfile(path):
|
||||
break
|
||||
|
||||
reversed_lines = []
|
||||
f = open(path, 'r')
|
||||
reversed_lines = toUnicode(f.read()).split('[0m\n')
|
||||
reversed_lines.reverse()
|
||||
@@ -100,7 +97,7 @@ class Logging(Plugin):
|
||||
brk = False
|
||||
for line in reversed_lines:
|
||||
|
||||
if log_type == 'all' or '%s ' % log_type.upper() in line:
|
||||
if type == 'all' or '%s ' % type.upper() in line:
|
||||
log_lines.append(line)
|
||||
|
||||
if len(log_lines) >= total_lines:
|
||||
@@ -111,18 +108,18 @@ class Logging(Plugin):
|
||||
break
|
||||
|
||||
log_lines.reverse()
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'log': '[0m\n'.join(log_lines),
|
||||
})
|
||||
}
|
||||
|
||||
def clear(self):
|
||||
def clear(self, **kwargs):
|
||||
|
||||
for x in range(0, 50):
|
||||
path = '%s%s' % (Env.get('log_path'), '.%s' % x if x > 0 else '')
|
||||
|
||||
if not os.path.isfile(path):
|
||||
break
|
||||
continue
|
||||
|
||||
try:
|
||||
|
||||
@@ -135,24 +132,21 @@ class Logging(Plugin):
|
||||
except:
|
||||
log.error('Couldn\'t delete file "%s": %s', (path, traceback.format_exc()))
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def log(self):
|
||||
|
||||
params = getParams()
|
||||
def log(self, type = 'error', **kwargs):
|
||||
|
||||
try:
|
||||
log_message = 'API log: %s' % params
|
||||
log_message = 'API log: %s' % kwargs
|
||||
try:
|
||||
getattr(log, params.get('type', 'error'))(log_message)
|
||||
getattr(log, type)(log_message)
|
||||
except:
|
||||
log.error(log_message)
|
||||
except:
|
||||
log.error('Couldn\'t log via API: %s', params)
|
||||
log.error('Couldn\'t log via API: %s', kwargs)
|
||||
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
.page.log .nav {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
padding: 0 0 30px;
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
position: fixed;
|
||||
width: 960px;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: #4E5969;
|
||||
}
|
||||
|
||||
.page.log .nav li {
|
||||
display: inline;
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
@@ -24,7 +25,17 @@
|
||||
.page.log .nav li.active {
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
font-size: 30px;
|
||||
background: rgba(255,255,255,.1);
|
||||
}
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.page.log .nav {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.page.log .nav li {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.page.log .loading {
|
||||
|
||||
@@ -6,19 +6,6 @@ Page.Log = new Class({
|
||||
title: 'Show recent logs.',
|
||||
has_tab: false,
|
||||
|
||||
initialize: function(options){
|
||||
var self = this;
|
||||
self.parent(options)
|
||||
|
||||
|
||||
App.getBlock('more').addLink(new Element('a', {
|
||||
'href': App.createUrl(self.name),
|
||||
'text': self.name.capitalize(),
|
||||
'title': self.title
|
||||
}))
|
||||
|
||||
},
|
||||
|
||||
indexAction: function(){
|
||||
var self = this;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'manage',
|
||||
'label': 'movie library manager',
|
||||
'label': 'Movie Library Manager',
|
||||
'description': 'Add your existing movie folders.',
|
||||
'options': [
|
||||
{
|
||||
@@ -28,6 +28,14 @@ config = [{
|
||||
'description': 'Remove movie from db if it can\'t be found after re-scan.',
|
||||
'default': True,
|
||||
},
|
||||
{
|
||||
'label': 'Scan at startup',
|
||||
'name': 'startup_scan',
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'description': 'Do a quick scan on startup. On slow systems better disable this.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import splitString, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -27,7 +26,8 @@ class Manage(Plugin):
|
||||
addEvent('manage.diskspace', self.getDiskSpace)
|
||||
|
||||
# Add files after renaming
|
||||
def after_rename(message = None, group = {}):
|
||||
def after_rename(message = None, group = None):
|
||||
if not group: group = {}
|
||||
return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'])
|
||||
addEvent('renamer.after', after_rename, priority = 110)
|
||||
|
||||
@@ -45,25 +45,24 @@ class Manage(Plugin):
|
||||
}"""},
|
||||
})
|
||||
|
||||
if not Env.get('dev'):
|
||||
def updateLibrary():
|
||||
self.updateLibrary(full = False)
|
||||
addEvent('app.load', updateLibrary)
|
||||
if not Env.get('dev') and self.conf('startup_scan'):
|
||||
addEvent('app.load', self.updateLibraryQuick)
|
||||
|
||||
def getProgress(self):
|
||||
return jsonified({
|
||||
def getProgress(self, **kwargs):
|
||||
return {
|
||||
'progress': self.in_progress
|
||||
})
|
||||
}
|
||||
|
||||
def updateLibraryView(self):
|
||||
def updateLibraryView(self, full = 1, **kwargs):
|
||||
|
||||
full = getParam('full', default = 1)
|
||||
fireEventAsync('manage.update', full = True if full == '1' else False)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def updateLibraryQuick(self):
|
||||
return self.updateLibrary(full = False)
|
||||
|
||||
def updateLibrary(self, full = True):
|
||||
last_update = float(Env.prop('manage.last_update', default = 0))
|
||||
@@ -119,7 +118,9 @@ class Manage(Plugin):
|
||||
fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
|
||||
else:
|
||||
|
||||
for release in done_movie.get('releases', []):
|
||||
releases = fireEvent('release.for_movie', id = done_movie.get('id'), single = True)
|
||||
|
||||
for release in releases:
|
||||
if len(release.get('files', [])) == 0:
|
||||
fireEvent('release.delete', release['id'])
|
||||
else:
|
||||
@@ -130,9 +131,9 @@ class Manage(Plugin):
|
||||
break
|
||||
|
||||
# Check if there are duplicate releases (different quality) use the last one, delete the rest
|
||||
if len(done_movie.get('releases', [])) > 1:
|
||||
if len(releases) > 1:
|
||||
used_files = {}
|
||||
for release in done_movie.get('releases', []):
|
||||
for release in releases:
|
||||
|
||||
for release_file in release.get('files', []):
|
||||
already_used = used_files.get(release_file['path'])
|
||||
@@ -171,6 +172,7 @@ class Manage(Plugin):
|
||||
self.in_progress = False
|
||||
|
||||
def createAddToLibrary(self, folder, added_identifiers = []):
|
||||
|
||||
def addToLibrary(group, total_found, to_go):
|
||||
if self.in_progress[folder]['total'] is None:
|
||||
self.in_progress[folder] = {
|
||||
@@ -184,7 +186,9 @@ class Manage(Plugin):
|
||||
|
||||
# Add it to release and update the info
|
||||
fireEvent('release.add', group = group)
|
||||
fireEventAsync('library.update', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier))
|
||||
fireEventAsync('library.update.movie', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier))
|
||||
else:
|
||||
self.in_progress[folder]['to_go'] -= 1
|
||||
|
||||
return addToLibrary
|
||||
|
||||
@@ -192,7 +196,10 @@ class Manage(Plugin):
|
||||
|
||||
# Notify frontend
|
||||
def afterUpdate():
|
||||
self.in_progress[folder]['to_go'] = self.in_progress[folder]['to_go'] - 1
|
||||
if not self.in_progress or self.shuttingDown():
|
||||
return
|
||||
|
||||
self.in_progress[folder]['to_go'] -= 1
|
||||
total = self.in_progress[folder]['total']
|
||||
movie_dict = fireEvent('movie.get', identifier, single = True)
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
from .main import MoviePlugin
|
||||
|
||||
def start():
|
||||
return MoviePlugin()
|
||||
|
||||
config = []
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user