Compare commits
411 Commits
build/2.3.
...
build/2.4.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3efda74b2 | ||
|
|
66b849cb29 | ||
|
|
b19f98ef5b | ||
|
|
ffe6b7dd70 | ||
|
|
75f22f44a1 | ||
|
|
d60a8a71b7 | ||
|
|
9e471ac389 | ||
|
|
3172a4d030 | ||
|
|
c58315e2ee | ||
|
|
dc0ea5b3f6 | ||
|
|
b50cf1cf4c | ||
|
|
b69898d624 | ||
|
|
2066625bf0 | ||
|
|
7af1d00ea2 | ||
|
|
5b279a48cb | ||
|
|
a5fa0681ed | ||
|
|
22e922e860 | ||
|
|
0126f5ae84 | ||
|
|
c389790cf2 | ||
|
|
cfb246fa84 | ||
|
|
651119b7dd | ||
|
|
d7445dfa80 | ||
|
|
f944a70a9c | ||
|
|
9056f5ae59 | ||
|
|
ed62c981cc | ||
|
|
36782768a4 | ||
|
|
2a7ba28903 | ||
|
|
e8ec2ef8d1 | ||
|
|
2c9d487614 | ||
|
|
864e8654c3 | ||
|
|
e11453aafb | ||
|
|
e57620f67c | ||
|
|
e481763967 | ||
|
|
2b3d755c64 | ||
|
|
fc2db36820 | ||
|
|
4fdea782f3 | ||
|
|
188a1a3b03 | ||
|
|
8e2014f2d4 | ||
|
|
fb95d7923f | ||
|
|
fe5ca69f36 | ||
|
|
1086b808dc | ||
|
|
0050e5cdfc | ||
|
|
6b357674d0 | ||
|
|
82c0592e49 | ||
|
|
28ab4576d5 | ||
|
|
2debd5598f | ||
|
|
d86d44e2d4 | ||
|
|
3d85460dc8 | ||
|
|
52ce85fbf2 | ||
|
|
6d70533e0b | ||
|
|
4d8338e829 | ||
|
|
a4e48e1f6b | ||
|
|
790a74f9e4 | ||
|
|
893dde9958 | ||
|
|
d448b8cd99 | ||
|
|
ca2c4a0b3e | ||
|
|
499b8193ab | ||
|
|
1f18d2b09c | ||
|
|
a92d6fd35c | ||
|
|
12adde8f80 | ||
|
|
8b747dff9b | ||
|
|
027ff43dfd | ||
|
|
f50c8504cf | ||
|
|
30f5a3944c | ||
|
|
a1c0b000a4 | ||
|
|
f22778aacb | ||
|
|
888ee07f65 | ||
|
|
aa5937c278 | ||
|
|
4831c80598 | ||
|
|
b9a724c8bb | ||
|
|
886a271d19 | ||
|
|
68d826ca1c | ||
|
|
8dfb0d1d5c | ||
|
|
d6921882e1 | ||
|
|
3380e20e3a | ||
|
|
a2c87e1b7d | ||
|
|
9098e44513 | ||
|
|
62524e01e1 | ||
|
|
78bf1d274e | ||
|
|
461e469f28 | ||
|
|
e4e7ae3621 | ||
|
|
9d55ecffe9 | ||
|
|
660e20dada | ||
|
|
18c8e803a4 | ||
|
|
15a19949b8 | ||
|
|
ebc5a66375 | ||
|
|
f91081e39c | ||
|
|
9e991e1595 | ||
|
|
afac06081c | ||
|
|
b773228719 | ||
|
|
7001ed476d | ||
|
|
31c39650a9 | ||
|
|
fbae706b0f | ||
|
|
88c328af8e | ||
|
|
cbd8981ee2 | ||
|
|
3101926e9b | ||
|
|
c9e0910c55 | ||
|
|
d65667ce16 | ||
|
|
7d7251862c | ||
|
|
4d02a969c2 | ||
|
|
e20c776364 | ||
|
|
c55404699e | ||
|
|
6240e4eba0 | ||
|
|
cf86719607 | ||
|
|
76943b6529 | ||
|
|
ca8bbdc293 | ||
|
|
8e6f12a897 | ||
|
|
52c64c1a6a | ||
|
|
ca94d48f8b | ||
|
|
d860680823 | ||
|
|
d1dbf3745a | ||
|
|
4b1151bda1 | ||
|
|
18c64e493b | ||
|
|
fc6839b441 | ||
|
|
405b63acdd | ||
|
|
f3dee50448 | ||
|
|
04e550ebe7 | ||
|
|
05b58819d6 | ||
|
|
63c72853f4 | ||
|
|
f20cce0176 | ||
|
|
723cbcd8bd | ||
|
|
dfbb84caae | ||
|
|
009d6cafaf | ||
|
|
bd9a4289d1 | ||
|
|
29a34fef8c | ||
|
|
08e2a3a883 | ||
|
|
2d37022525 | ||
|
|
bb3faaf2cd | ||
|
|
2c43b9a926 | ||
|
|
964ed5f497 | ||
|
|
b47a94852a | ||
|
|
f318524070 | ||
|
|
04539edb45 | ||
|
|
5cf21452c1 | ||
|
|
799299c7cc | ||
|
|
458330d325 | ||
|
|
973bec9e6a | ||
|
|
1f941a5105 | ||
|
|
8217fecb33 | ||
|
|
1dda7edf1c | ||
|
|
2cfff73486 | ||
|
|
3c03e400f0 | ||
|
|
6388d97c5c | ||
|
|
0c7dda8d44 | ||
|
|
161e3086fa | ||
|
|
b3f1f938be | ||
|
|
082da6e3a6 | ||
|
|
d9b9447242 | ||
|
|
dbaa377770 | ||
|
|
47d2b81d1c | ||
|
|
d743282578 | ||
|
|
7eee6f0b96 | ||
|
|
dda3fca4b4 | ||
|
|
8648b2f948 | ||
|
|
f52cbd24f8 | ||
|
|
5ea13eeffd | ||
|
|
6cc802952f | ||
|
|
190b9db645 | ||
|
|
81949b9cad | ||
|
|
894e419f40 | ||
|
|
cdc6c036aa | ||
|
|
1e9168f682 | ||
|
|
790415dd4f | ||
|
|
679e0ea2c3 | ||
|
|
bd167403c3 | ||
|
|
13abe62bed | ||
|
|
4147c5b870 | ||
|
|
37d4755aae | ||
|
|
a9f416c4c5 | ||
|
|
8a11f246b1 | ||
|
|
8d44577dca | ||
|
|
72457d8d10 | ||
|
|
3bb44f8d9f | ||
|
|
279297b8fa | ||
|
|
c71e661daf | ||
|
|
f8820c06fe | ||
|
|
907b40e3c6 | ||
|
|
d318e163bb | ||
|
|
6e9c36a503 | ||
|
|
c9e9fe86aa | ||
|
|
c4f4e2b524 | ||
|
|
95246b90f6 | ||
|
|
2fad29df51 | ||
|
|
a95320e162 | ||
|
|
31b8805b5e | ||
|
|
9e69d4e153 | ||
|
|
aa5ecd7b42 | ||
|
|
15f90aa503 | ||
|
|
ec86bc4a38 | ||
|
|
a3efc64901 | ||
|
|
c929ecbac0 | ||
|
|
cc32e49060 | ||
|
|
05c41460c2 | ||
|
|
794efaa209 | ||
|
|
b0e93ee18c | ||
|
|
0393b51db6 | ||
|
|
464c8ad71c | ||
|
|
9df0e01874 | ||
|
|
bf2beb2530 | ||
|
|
f0b096d41a | ||
|
|
c948f38469 | ||
|
|
516cbd73bd | ||
|
|
680ae53cf4 | ||
|
|
99b99a992d | ||
|
|
fb9d52c2b9 | ||
|
|
5cc471cc87 | ||
|
|
07c7171fbb | ||
|
|
c15dd2dec9 | ||
|
|
a408cc0246 | ||
|
|
c2568432e7 | ||
|
|
91f3cda995 | ||
|
|
28aa908513 | ||
|
|
5e24b11c21 | ||
|
|
4cdf71513f | ||
|
|
7e6d9c02f6 | ||
|
|
afc4f73e36 | ||
|
|
5ef0c52277 | ||
|
|
c23b014cff | ||
|
|
f13cddfb26 | ||
|
|
623f6f3ed0 | ||
|
|
a158716c8b | ||
|
|
9df7f7b22c | ||
|
|
8e5c24282e | ||
|
|
266429311b | ||
|
|
d74342adee | ||
|
|
4408d99524 | ||
|
|
0168b9cbea | ||
|
|
e69421226b | ||
|
|
f08d34b816 | ||
|
|
4a36c3b6a8 | ||
|
|
be0b708d32 | ||
|
|
1cea50bcfb | ||
|
|
55483cf736 | ||
|
|
16f8a1159f | ||
|
|
d4d03a846e | ||
|
|
7bccc46583 | ||
|
|
dc61e9916f | ||
|
|
cf2b5f72ae | ||
|
|
fe397caafc | ||
|
|
787405ae62 | ||
|
|
0b01bbc52e | ||
|
|
190e1d2c4f | ||
|
|
8a822e35e2 | ||
|
|
64a196f21d | ||
|
|
a24d4a9e3b | ||
|
|
dafa70b7e3 | ||
|
|
32b9bc3345 | ||
|
|
a7b8f992d3 | ||
|
|
0c66b8067e | ||
|
|
7b3645ea7c | ||
|
|
69569758d9 | ||
|
|
55777531d5 | ||
|
|
99ce8dacbf | ||
|
|
138a3b1f3c | ||
|
|
d49c663c64 | ||
|
|
e9a457e263 | ||
|
|
3b0e07100f | ||
|
|
74561500b5 | ||
|
|
3b519aeac9 | ||
|
|
9a55961786 | ||
|
|
ea5d274f4d | ||
|
|
f57f2444fe | ||
|
|
fd768df9e5 | ||
|
|
4db68e4887 | ||
|
|
6d4297a5fb | ||
|
|
ab413f2f3e | ||
|
|
574255c4b6 | ||
|
|
008ba39856 | ||
|
|
cff1b3abdb | ||
|
|
231c5b8ca1 | ||
|
|
640664494e | ||
|
|
951b7b8425 | ||
|
|
c9980539f0 | ||
|
|
7eb802b42a | ||
|
|
2f4f3ce0fe | ||
|
|
824ac86d18 | ||
|
|
4553726423 | ||
|
|
f0bde7316d | ||
|
|
4eaddadf8c | ||
|
|
9dd98b29be | ||
|
|
732946d38a | ||
|
|
966f8c36b1 | ||
|
|
ca070e67e7 | ||
|
|
b468048d95 | ||
|
|
50c5044fe8 | ||
|
|
46b2d6ba6e | ||
|
|
8aec5cf605 | ||
|
|
54af80d5ad | ||
|
|
8b2cd62211 | ||
|
|
2fc4809821 | ||
|
|
bde6de1789 | ||
|
|
029ae20573 | ||
|
|
c72cca4ea2 | ||
|
|
0f071be762 | ||
|
|
fdcddaaffc | ||
|
|
cddf47f113 | ||
|
|
76f3f5253a | ||
|
|
d833a04293 | ||
|
|
2e96860380 | ||
|
|
3e2e6385cf | ||
|
|
ccc2028690 | ||
|
|
81dbc1ca79 | ||
|
|
e9a3059be2 | ||
|
|
3d5b33856f | ||
|
|
8d2e3a1919 | ||
|
|
f3380c4fed | ||
|
|
8a58d7f973 | ||
|
|
37b98cb835 | ||
|
|
50262112b8 | ||
|
|
4b9f9862fc | ||
|
|
df60d70592 | ||
|
|
1b5bc1fa05 | ||
|
|
e4993eac24 | ||
|
|
bd1bb1ee91 | ||
|
|
2c1c57333c | ||
|
|
a466cbcf16 | ||
|
|
379f62a339 | ||
|
|
eaf2974f8d | ||
|
|
99e641a30d | ||
|
|
88d6148500 | ||
|
|
f53364eb6c | ||
|
|
b8f78e311d | ||
|
|
bb6e1e2909 | ||
|
|
c62c6664ce | ||
|
|
8ae4e3be18 | ||
|
|
0065ff5086 | ||
|
|
28d073f934 | ||
|
|
df1cb0ae08 | ||
|
|
31a1af43d5 | ||
|
|
8951e9fc90 | ||
|
|
357166414c | ||
|
|
e1a311de40 | ||
|
|
ab923cc592 | ||
|
|
99947fb135 | ||
|
|
185cb0196a | ||
|
|
309ec50691 | ||
|
|
f865484182 | ||
|
|
ed19fd0254 | ||
|
|
cec88319fe | ||
|
|
d31b7eb72d | ||
|
|
b7d93b84dd | ||
|
|
4008774908 | ||
|
|
b4275639f5 | ||
|
|
accce789ba | ||
|
|
091b1fefd2 | ||
|
|
899b1f9b96 | ||
|
|
0ce5c51c67 | ||
|
|
f79fcda27f | ||
|
|
cdbcad2238 | ||
|
|
d6709469f6 | ||
|
|
da760db340 | ||
|
|
4242a5cedb | ||
|
|
8c41046836 | ||
|
|
3e43e3fc4c | ||
|
|
c5e6ce0e48 | ||
|
|
e622e68701 | ||
|
|
a90a4d1bc2 | ||
|
|
165676407a | ||
|
|
e6d76db250 | ||
|
|
3b3288c53d | ||
|
|
16cf220741 | ||
|
|
5131cb0ae1 | ||
|
|
d984f11cbf | ||
|
|
ae666bd9b6 | ||
|
|
d023eb8f1f | ||
|
|
d6fa5c97db | ||
|
|
3717443e85 | ||
|
|
a1ba39b3d3 | ||
|
|
b4ad7b459f | ||
|
|
5af8fd0b21 | ||
|
|
83e7a8d765 | ||
|
|
4bdd4eab64 | ||
|
|
5e683b5a48 | ||
|
|
f178825d21 | ||
|
|
fe2290fccb | ||
|
|
e4d67645b7 | ||
|
|
324415be15 | ||
|
|
7c44f9ab13 | ||
|
|
628fda2097 | ||
|
|
da6d749072 | ||
|
|
bef2b28acc | ||
|
|
302f571837 | ||
|
|
41dde209d5 | ||
|
|
5d350ef5ac | ||
|
|
4fd1d986dd | ||
|
|
55d57bc07b | ||
|
|
a81a262fb6 | ||
|
|
c37360f848 | ||
|
|
d7700900db | ||
|
|
faa136a365 | ||
|
|
bd73b94ea4 | ||
|
|
d764d0f096 | ||
|
|
fc8db130e0 | ||
|
|
682d678f91 | ||
|
|
06a211a24a | ||
|
|
55af696b7c | ||
|
|
636e9514e8 | ||
|
|
47e649643f | ||
|
|
bf59d2f357 | ||
|
|
fb90f6591b | ||
|
|
d66722e737 | ||
|
|
47d37c2ec9 | ||
|
|
40324ee89f | ||
|
|
9db1f3430e | ||
|
|
ec19932eef | ||
|
|
daf31870f3 | ||
|
|
35d49f6a5e | ||
|
|
5c0d8a7fef | ||
|
|
b2ab114b6d | ||
|
|
a8523e6d01 | ||
|
|
f946389d60 |
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
from __future__ import print_function
|
||||
from logging import handlers
|
||||
from os.path import dirname
|
||||
import logging
|
||||
@@ -132,14 +133,15 @@ if __name__ == '__main__':
|
||||
pass
|
||||
except SystemExit:
|
||||
raise
|
||||
except socket.error as (nr, msg):
|
||||
except socket.error as e:
|
||||
# log when socket receives SIGINT, but continue.
|
||||
# previous code would have skipped over other types of IO errors too.
|
||||
nr, msg = e
|
||||
if nr != 4:
|
||||
try:
|
||||
l.log.critical(traceback.format_exc())
|
||||
except:
|
||||
print traceback.format_exc()
|
||||
print(traceback.format_exc())
|
||||
raise
|
||||
except:
|
||||
try:
|
||||
@@ -148,7 +150,7 @@ if __name__ == '__main__':
|
||||
if l:
|
||||
l.log.critical(traceback.format_exc())
|
||||
else:
|
||||
print traceback.format_exc()
|
||||
print(traceback.format_exc())
|
||||
except:
|
||||
print traceback.format_exc()
|
||||
print(traceback.format_exc())
|
||||
raise
|
||||
|
||||
@@ -166,8 +166,8 @@ class CouchPotatoApp(wx.App, SoftwareUpdate):
|
||||
def OnInit(self):
|
||||
|
||||
# Updater
|
||||
base_url = 'https://couchpota.to/updates/%s'
|
||||
self.InitUpdates(base_url % VERSION + '/', base_url % 'changelog.html',
|
||||
base_url = 'https://api.couchpota.to/updates/%s'
|
||||
self.InitUpdates(base_url % VERSION + '/', 'https://couchpota.to/updates/%s' % 'changelog.html',
|
||||
icon = wx.Icon('icon.png'))
|
||||
|
||||
self.frame = MainFrame(self)
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
#So you feel like posting a bug, sending me a pull request or just telling me how awesome I am. No problem!
|
||||
## Got a issue/feature request or submitting a pull request?
|
||||
|
||||
##Just make sure you think of the following things:
|
||||
Make sure you think of the following things:
|
||||
|
||||
* Search through the existing (and closed) issues first. See if you can get your answer there.
|
||||
## Issue
|
||||
* Search through the existing (and closed) issues first, see if you can get your answer there.
|
||||
* Double check the result manually, because it could be an external issue.
|
||||
* Post logs! Without seeing what is going on, I can't reproduce the error.
|
||||
* What is the movie + quality you are searching for.
|
||||
* What are you settings for the specific problem.
|
||||
* What providers are you using. (While your logs include these, scanning through hundred of lines of log isn't my hobby).
|
||||
* Give me a short step by step of how to reproduce.
|
||||
* Also check the logs before submitting, obvious errors like permission or http errors are often not related to CP.
|
||||
* What is the movie + quality you are searching for?
|
||||
* What are you're settings for the specific problem?
|
||||
* What providers are you using? (While you're logs include these, scanning through hundred of lines of log isn't our hobby)
|
||||
* Post the logs from config directory, please do not copy paste the UI. Use pastebin to store these logs!
|
||||
* Give a short step by step of how to reproduce the error.
|
||||
* What hardware / OS are you using and what are the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example.
|
||||
* I will mark issues with the "can't reproduce" tag. Don't go asking me "why closed" if it clearly says the issue in the tag ;)
|
||||
* I will mark issues with the "can't reproduce" tag. Don't go asking "why closed" if it clearly says the issue in the tag ;)
|
||||
* If you're running on a NAS (QNAP, Austor etc..) with pre-made packages, make sure these are setup to use our source repo (RuudBurger/CouchPotatoServer) and nothing else!!
|
||||
|
||||
**If I don't get enough info, the chance of the issue getting closed is a lot bigger ;)**
|
||||
## Pull Request
|
||||
* Make sure you're pull request is made for develop branch (or relevant feature branch)
|
||||
* Have you tested your PR? If not, why?
|
||||
* Are there any limitations of your PR we should know of?
|
||||
* Make sure to keep you're PR up-to-date with the branch you're trying to push into.
|
||||
|
||||
**If we don't get enough info, the chance of the issue getting closed is a lot bigger ;)**
|
||||
|
||||
@@ -9,13 +9,12 @@ import os
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
views = {}
|
||||
template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates'))
|
||||
|
||||
|
||||
class BaseHandler(RequestHandler):
|
||||
|
||||
def get_current_user(self):
|
||||
@@ -24,9 +23,10 @@ class BaseHandler(RequestHandler):
|
||||
|
||||
if username and password:
|
||||
return self.get_secure_cookie('user')
|
||||
else: # Login when no username or password are set
|
||||
else: # Login when no username or password are set
|
||||
return True
|
||||
|
||||
|
||||
# Main web handler
|
||||
class WebHandler(BaseHandler):
|
||||
|
||||
@@ -43,11 +43,13 @@ class WebHandler(BaseHandler):
|
||||
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 get_session():
|
||||
return Env.getSession()
|
||||
|
||||
|
||||
# Web view
|
||||
@@ -55,12 +57,10 @@ def index():
|
||||
return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env)
|
||||
addView('', index)
|
||||
|
||||
|
||||
# API docs
|
||||
def apiDocs():
|
||||
routes = []
|
||||
|
||||
for route in api.iterkeys():
|
||||
routes.append(route)
|
||||
routes = list(api.keys())
|
||||
|
||||
if api_docs.get(''):
|
||||
del api_docs['']
|
||||
@@ -70,21 +70,22 @@ def apiDocs():
|
||||
|
||||
addView('docs', apiDocs)
|
||||
|
||||
|
||||
# Make non basic auth option to get api key
|
||||
class KeyHandler(RequestHandler):
|
||||
def get(self, *args, **kwargs):
|
||||
api = None
|
||||
api_key = None
|
||||
|
||||
try:
|
||||
username = Env.setting('username')
|
||||
password = Env.setting('password')
|
||||
|
||||
if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password):
|
||||
api = Env.setting('api_key')
|
||||
api_key = Env.setting('api_key')
|
||||
|
||||
self.write({
|
||||
'success': api is not None,
|
||||
'api_key': api
|
||||
'success': api_key is not None,
|
||||
'api_key': api_key
|
||||
})
|
||||
except:
|
||||
log.error('Failed doing key request: %s', (traceback.format_exc()))
|
||||
@@ -102,20 +103,21 @@ class LoginHandler(BaseHandler):
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
|
||||
api = None
|
||||
api_key = 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')
|
||||
api_key = Env.setting('api_key')
|
||||
|
||||
if api:
|
||||
if api_key:
|
||||
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.set_secure_cookie('user', api_key, expires_days = 30 if remember_me > 0 else None)
|
||||
|
||||
self.redirect(Env.get('web_base'))
|
||||
|
||||
|
||||
class LogoutHandler(BaseHandler):
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
@@ -136,4 +138,3 @@ def page_not_found(rh):
|
||||
|
||||
rh.set_status(404)
|
||||
rh.write('Wrong API key used')
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ api_nonblock = {}
|
||||
api_docs = {}
|
||||
api_docs_missing = []
|
||||
|
||||
|
||||
def run_async(func):
|
||||
@wraps(func)
|
||||
def async_func(*args, **kwargs):
|
||||
@@ -29,6 +30,7 @@ def run_async(func):
|
||||
|
||||
return async_func
|
||||
|
||||
|
||||
# NonBlock API handler
|
||||
class NonBlockHandler(RequestHandler):
|
||||
|
||||
@@ -61,6 +63,7 @@ class NonBlockHandler(RequestHandler):
|
||||
|
||||
self.stopper = None
|
||||
|
||||
|
||||
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
||||
api_nonblock[route] = func_tuple
|
||||
|
||||
@@ -69,6 +72,7 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
||||
else:
|
||||
api_docs_missing.append(route)
|
||||
|
||||
|
||||
# Blocking API handler
|
||||
class ApiHandler(RequestHandler):
|
||||
|
||||
@@ -98,11 +102,12 @@ class ApiHandler(RequestHandler):
|
||||
@run_async
|
||||
def run_handler(callback):
|
||||
try:
|
||||
result = api[route](**kwargs)
|
||||
callback(result)
|
||||
res = api[route](**kwargs)
|
||||
callback(res)
|
||||
except:
|
||||
log.error('Failed doing api request "%s": %s', (route, traceback.format_exc()))
|
||||
callback({'success': False, 'error': 'Failed returning results'})
|
||||
|
||||
result = yield tornado.gen.Task(run_handler)
|
||||
|
||||
# Check JSONP callback
|
||||
@@ -122,6 +127,7 @@ class ApiHandler(RequestHandler):
|
||||
|
||||
api_locks[route].release()
|
||||
|
||||
|
||||
def addApiView(route, func, static = False, docs = None, **kwargs):
|
||||
|
||||
if static: func(route)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from .main import Core
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
def start():
|
||||
return Core()
|
||||
|
||||
|
||||
@@ -55,6 +55,10 @@ class Core(Plugin):
|
||||
if not Env.get('desktop'):
|
||||
self.signalHandler()
|
||||
|
||||
# Set default urlopen timeout
|
||||
import socket
|
||||
socket.setdefaulttimeout(30)
|
||||
|
||||
def md5Password(self, value):
|
||||
return md5(value) if value else ''
|
||||
|
||||
@@ -113,7 +117,7 @@ class Core(Plugin):
|
||||
|
||||
if len(still_running) == 0:
|
||||
break
|
||||
elif starttime < time.time() - 30: # Always force break after 30s wait
|
||||
elif starttime < time.time() - 30: # Always force break after 30s wait
|
||||
break
|
||||
|
||||
running = list(set(still_running) - set(self.ignore_restart))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import ClientScript
|
||||
|
||||
|
||||
def start():
|
||||
return ClientScript()
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ class ClientScript(Plugin):
|
||||
'scripts/library/question.js',
|
||||
'scripts/library/scrollspy.js',
|
||||
'scripts/library/spin.js',
|
||||
'scripts/library/Array.stableSort.js',
|
||||
'scripts/library/async.js',
|
||||
'scripts/couchpotato.js',
|
||||
'scripts/api.js',
|
||||
'scripts/library/history.js',
|
||||
@@ -47,13 +49,14 @@ class ClientScript(Plugin):
|
||||
'scripts/page/settings.js',
|
||||
'scripts/page/about.js',
|
||||
'scripts/page/manage.js',
|
||||
'scripts/misc/downloaders.js',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
urls = {'style': {}, 'script': {}, }
|
||||
minified = {'style': {}, 'script': {}, }
|
||||
paths = {'style': {}, 'script': {}, }
|
||||
urls = {'style': {}, 'script': {}}
|
||||
minified = {'style': {}, 'script': {}}
|
||||
paths = {'style': {}, 'script': {}}
|
||||
comment = {
|
||||
'style': '/*** %s:%d ***/\n',
|
||||
'script': '// %s:%d\n'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Desktop
|
||||
|
||||
|
||||
def start():
|
||||
return Desktop()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Scheduler
|
||||
|
||||
|
||||
def start():
|
||||
return Scheduler()
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ class Scheduler(Plugin):
|
||||
addEvent('schedule.cron', self.cron)
|
||||
addEvent('schedule.interval', self.interval)
|
||||
addEvent('schedule.remove', self.remove)
|
||||
addEvent('schedule.queue', self.queue)
|
||||
|
||||
self.sched = Sched(misfire_grace_time = 60)
|
||||
self.sched.start()
|
||||
@@ -31,13 +32,13 @@ class Scheduler(Plugin):
|
||||
pass
|
||||
|
||||
def doShutdown(self):
|
||||
super(Scheduler, self).doShutdown()
|
||||
self.stop()
|
||||
return super(Scheduler, self).doShutdown()
|
||||
|
||||
def stop(self):
|
||||
if self.started:
|
||||
log.debug('Stopping scheduler')
|
||||
self.sched.shutdown()
|
||||
self.sched.shutdown(wait = False)
|
||||
log.debug('Scheduler stopped')
|
||||
self.started = False
|
||||
|
||||
@@ -64,3 +65,14 @@ class Scheduler(Plugin):
|
||||
'seconds': seconds,
|
||||
'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds)
|
||||
}
|
||||
|
||||
def queue(self, handlers = None):
|
||||
if not handlers: handlers = []
|
||||
|
||||
for h in handlers:
|
||||
h()
|
||||
|
||||
if self.shuttingDown():
|
||||
break
|
||||
|
||||
return True
|
||||
|
||||
@@ -2,6 +2,7 @@ from .main import Updater
|
||||
from couchpotato.environment import Env
|
||||
import os
|
||||
|
||||
|
||||
def start():
|
||||
return Updater()
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import time
|
||||
import traceback
|
||||
import version
|
||||
import zipfile
|
||||
from six.moves import filter
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -32,6 +33,7 @@ class Updater(Plugin):
|
||||
else:
|
||||
self.updater = SourceUpdater()
|
||||
|
||||
addEvent('app.load', self.logVersion, priority = 10000)
|
||||
addEvent('app.load', self.setCrons)
|
||||
addEvent('updater.info', self.info)
|
||||
|
||||
@@ -53,12 +55,16 @@ class Updater(Plugin):
|
||||
|
||||
addEvent('setting.save.updater.enabled.after', self.setCrons)
|
||||
|
||||
def logVersion(self):
|
||||
info = self.info()
|
||||
log.info('=== VERSION %s, using %s ===', (info.get('version', {}).get('repr', 'UNKNOWN'), self.updater.getName()))
|
||||
|
||||
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
|
||||
self.autoUpdate() # Check after enabling
|
||||
|
||||
def autoUpdate(self):
|
||||
if self.isEnabled() and self.check() and self.conf('automatic') and not self.updater.update_failed:
|
||||
@@ -146,6 +152,9 @@ class BaseUpdater(Plugin):
|
||||
'branch': self.branch,
|
||||
}
|
||||
|
||||
def getVersion(self):
|
||||
pass
|
||||
|
||||
def check(self):
|
||||
pass
|
||||
|
||||
@@ -174,7 +183,6 @@ class BaseUpdater(Plugin):
|
||||
log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
|
||||
|
||||
|
||||
|
||||
class GitUpdater(BaseUpdater):
|
||||
|
||||
def __init__(self, git_command):
|
||||
@@ -183,9 +191,6 @@ class GitUpdater(BaseUpdater):
|
||||
def doUpdate(self):
|
||||
|
||||
try:
|
||||
log.debug('Stashing local changes')
|
||||
self.repo.saveStash()
|
||||
|
||||
log.info('Updating to latest version')
|
||||
self.repo.pull()
|
||||
|
||||
@@ -204,14 +209,15 @@ class GitUpdater(BaseUpdater):
|
||||
|
||||
if not self.version:
|
||||
try:
|
||||
output = self.repo.getHead() # Yes, please
|
||||
output = self.repo.getHead() # Yes, please
|
||||
log.debug('Git version output: %s', output.hash)
|
||||
self.version = {
|
||||
'repr': 'git:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.branch, output.hash[:8], datetime.fromtimestamp(output.getDate())),
|
||||
'hash': output.hash[:8],
|
||||
'date': output.getDate(),
|
||||
'type': 'git',
|
||||
}
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s', e)
|
||||
return 'No GIT'
|
||||
|
||||
@@ -234,7 +240,7 @@ class GitUpdater(BaseUpdater):
|
||||
local = self.repo.getHead()
|
||||
remote = branch.getHead()
|
||||
|
||||
log.info('Versions, local:%s, remote:%s', (local.hash[:8], remote.hash[:8]))
|
||||
log.debug('Versions, local:%s, remote:%s', (local.hash[:8], remote.hash[:8]))
|
||||
|
||||
if local.getDate() < remote.getDate():
|
||||
self.update_version = {
|
||||
@@ -247,7 +253,6 @@ class GitUpdater(BaseUpdater):
|
||||
return False
|
||||
|
||||
|
||||
|
||||
class SourceUpdater(BaseUpdater):
|
||||
|
||||
def __init__(self):
|
||||
@@ -273,9 +278,9 @@ class SourceUpdater(BaseUpdater):
|
||||
|
||||
# Extract
|
||||
if download_data.get('type') == 'zip':
|
||||
zip = zipfile.ZipFile(destination)
|
||||
zip.extractall(extracted_path)
|
||||
zip.close()
|
||||
zip_file = zipfile.ZipFile(destination)
|
||||
zip_file.extractall(extracted_path)
|
||||
zip_file.close()
|
||||
else:
|
||||
tar = tarfile.open(destination)
|
||||
tar.extractall(path = extracted_path)
|
||||
@@ -342,13 +347,12 @@ class SourceUpdater(BaseUpdater):
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def removeDir(self, path):
|
||||
try:
|
||||
if os.path.isdir(path):
|
||||
shutil.rmtree(path)
|
||||
except OSError, inst:
|
||||
os.chmod(inst.filename, 0777)
|
||||
except OSError as inst:
|
||||
os.chmod(inst.filename, 0o777)
|
||||
self.removeDir(path)
|
||||
|
||||
def getVersion(self):
|
||||
@@ -362,7 +366,8 @@ class SourceUpdater(BaseUpdater):
|
||||
log.debug('Source version output: %s', output)
|
||||
self.version = output
|
||||
self.version['type'] = 'source'
|
||||
except Exception, e:
|
||||
self.version['repr'] = 'source:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.branch, output.get('hash', '')[:8], datetime.fromtimestamp(output.get('date', 0)))
|
||||
except Exception as e:
|
||||
log.error('Failed using source updater. %s', e)
|
||||
return {}
|
||||
|
||||
@@ -392,7 +397,7 @@ class SourceUpdater(BaseUpdater):
|
||||
|
||||
return {
|
||||
'hash': commit['sha'],
|
||||
'date': int(time.mktime(parse(commit['commit']['committer']['date']).timetuple())),
|
||||
'date': int(time.mktime(parse(commit['commit']['committer']['date']).timetuple())),
|
||||
}
|
||||
except:
|
||||
log.error('Failed getting latest request from github: %s', traceback.format_exc())
|
||||
@@ -437,7 +442,7 @@ class DesktopUpdater(BaseUpdater):
|
||||
if latest and latest != current_version.get('hash'):
|
||||
self.update_version = {
|
||||
'hash': latest,
|
||||
'date': None,
|
||||
'date': None,
|
||||
'changelog': self.desktop._changelogURL,
|
||||
}
|
||||
|
||||
@@ -449,6 +454,7 @@ class DesktopUpdater(BaseUpdater):
|
||||
|
||||
def getVersion(self):
|
||||
return {
|
||||
'repr': 'desktop: %s' % self.desktop._esky.active_version,
|
||||
'hash': self.desktop._esky.active_version,
|
||||
'date': None,
|
||||
'type': 'desktop',
|
||||
|
||||
@@ -24,7 +24,7 @@ var UpdaterBase = new Class({
|
||||
self.doUpdate();
|
||||
else {
|
||||
App.unBlockPage();
|
||||
App.fireEvent('message', 'No updates available');
|
||||
App.trigger('message', ['No updates available']);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from base64 import b32decode, b16encode
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.variable import mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
@@ -13,6 +14,7 @@ class Downloader(Provider):
|
||||
|
||||
protocol = []
|
||||
http_time_between_calls = 0
|
||||
status_support = True
|
||||
|
||||
torrent_sources = [
|
||||
'http://torrage.com/torrent/%s.torrent',
|
||||
@@ -41,6 +43,7 @@ class Downloader(Provider):
|
||||
addEvent('download.remove_failed', self._removeFailed)
|
||||
addEvent('download.pause', self._pause)
|
||||
addEvent('download.process_complete', self._processComplete)
|
||||
addApiView('download.%s.test' % self.getName().lower(), self._test)
|
||||
|
||||
def getEnabledProtocol(self):
|
||||
for download_protocol in self.protocol:
|
||||
@@ -49,22 +52,27 @@ class Downloader(Provider):
|
||||
|
||||
return []
|
||||
|
||||
def _download(self, data = None, movie = None, manual = False, filedata = None):
|
||||
if not movie: movie = {}
|
||||
def _download(self, data = None, media = None, manual = False, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
if self.isDisabled(manual, data):
|
||||
return
|
||||
return self.download(data = data, movie = movie, filedata = filedata)
|
||||
return self.download(data = data, media = media, filedata = filedata)
|
||||
|
||||
def _getAllDownloadStatus(self):
|
||||
def _getAllDownloadStatus(self, download_ids):
|
||||
if self.isDisabled(manual = True, data = {}):
|
||||
return
|
||||
|
||||
return self.getAllDownloadStatus()
|
||||
ids = [download_id['id'] for download_id in download_ids if download_id['downloader'] == self.getName()]
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
return
|
||||
if ids:
|
||||
return self.getAllDownloadStatus(ids)
|
||||
else:
|
||||
return
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
return []
|
||||
|
||||
def _removeFailed(self, release_download):
|
||||
if self.isDisabled(manual = True, data = {}):
|
||||
@@ -128,6 +136,7 @@ class Downloader(Provider):
|
||||
def downloadReturnId(self, download_id):
|
||||
return {
|
||||
'downloader': self.getName(),
|
||||
'status_support': self.status_support,
|
||||
'id': download_id
|
||||
}
|
||||
|
||||
@@ -151,6 +160,15 @@ class Downloader(Provider):
|
||||
(d_manual and manual or d_manual is False) and \
|
||||
(not data or self.isCorrectProtocol(data.get('protocol')))
|
||||
|
||||
def _test(self):
|
||||
t = self.test()
|
||||
if isinstance(t, tuple):
|
||||
return {'success': t[0], 'msg': t[1]}
|
||||
return {'success': t}
|
||||
|
||||
def test(self):
|
||||
return False
|
||||
|
||||
def _pause(self, release_download, pause = True):
|
||||
if self.isDisabled(manual = True, data = {}):
|
||||
return
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from .main import Blackhole
|
||||
from couchpotato.core.helpers.variable import getDownloadDir
|
||||
|
||||
|
||||
def start():
|
||||
return Blackhole()
|
||||
|
||||
@@ -13,7 +14,7 @@ config = [{
|
||||
'list': 'download_providers',
|
||||
'name': 'blackhole',
|
||||
'label': 'Black hole',
|
||||
'description': 'Download the NZB/Torrent to a specific folder.',
|
||||
'description': 'Download the NZB/Torrent to a specific folder. <em>Note: Seeding and copying/linking features do <strong>not</strong> work with Black hole</em>.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import with_statement
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.helpers.encoding import sp
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
import os
|
||||
@@ -11,9 +12,10 @@ log = CPLog(__name__)
|
||||
class Blackhole(Downloader):
|
||||
|
||||
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
||||
status_support = False
|
||||
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
directory = self.conf('directory')
|
||||
@@ -33,7 +35,7 @@ class Blackhole(Downloader):
|
||||
log.error('No nzb/torrent available: %s', data.get('url'))
|
||||
return False
|
||||
|
||||
file_name = self.createFileName(data, filedata, movie)
|
||||
file_name = self.createFileName(data, filedata, media)
|
||||
full_path = os.path.join(directory, file_name)
|
||||
|
||||
if self.conf('create_subdir'):
|
||||
@@ -51,10 +53,10 @@ class Blackhole(Downloader):
|
||||
with open(full_path, 'wb') as f:
|
||||
f.write(filedata)
|
||||
os.chmod(full_path, Env.getPermission('file'))
|
||||
return True
|
||||
return self.downloadReturnId('')
|
||||
else:
|
||||
log.info('File %s already exists.', full_path)
|
||||
return True
|
||||
return self.downloadReturnId('')
|
||||
|
||||
except:
|
||||
log.error('Failed to download to blackhole %s', traceback.format_exc())
|
||||
@@ -66,6 +68,20 @@ class Blackhole(Downloader):
|
||||
|
||||
return False
|
||||
|
||||
def test(self):
|
||||
directory = self.conf('directory')
|
||||
if directory and os.path.isdir(directory):
|
||||
|
||||
test_file = sp(os.path.join(directory, 'couchpotato_test.txt'))
|
||||
|
||||
# Check if folder is writable
|
||||
self.createFile(test_file, 'This is a test file')
|
||||
if os.path.isfile(test_file):
|
||||
os.remove(test_file)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def getEnabledProtocol(self):
|
||||
if self.conf('use_for') == 'both':
|
||||
return super(Blackhole, self).getEnabledProtocol()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Deluge
|
||||
|
||||
|
||||
def start():
|
||||
return Deluge()
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from base64 import b64encode, b16encode, b32decode
|
||||
from bencode import bencode as benc, bdecode
|
||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import isInt, sp
|
||||
from couchpotato.core.helpers.variable import tryFloat
|
||||
from couchpotato.core.helpers.variable import tryFloat, cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
from datetime import timedelta
|
||||
from hashlib import sha1
|
||||
@@ -20,19 +20,22 @@ class Deluge(Downloader):
|
||||
log = CPLog(__name__)
|
||||
drpc = None
|
||||
|
||||
def connect(self):
|
||||
def connect(self, reconnect = False):
|
||||
# Load host from config and split out port.
|
||||
host = self.conf('host').split(':')
|
||||
host = cleanHost(self.conf('host'), protocol = False).split(':')
|
||||
if not isInt(host[1]):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
return False
|
||||
|
||||
if not self.drpc:
|
||||
if not self.drpc or reconnect:
|
||||
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):
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol')))
|
||||
|
||||
if not self.connect():
|
||||
@@ -73,7 +76,7 @@ class Deluge(Downloader):
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options)
|
||||
else:
|
||||
filename = self.createFileName(data, filedata, movie)
|
||||
filename = self.createFileName(data, filedata, media)
|
||||
remote_torrent = self.drpc.add_torrent_file(filename, filedata, options)
|
||||
|
||||
if not remote_torrent:
|
||||
@@ -83,24 +86,34 @@ class Deluge(Downloader):
|
||||
log.info('Torrent sent to Deluge successfully.')
|
||||
return self.downloadReturnId(remote_torrent)
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
def test(self):
|
||||
if self.connect(True) and self.drpc.test():
|
||||
return True
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
|
||||
log.debug('Checking Deluge download status.')
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
return []
|
||||
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
|
||||
queue = self.drpc.get_alltorrents()
|
||||
queue = self.drpc.get_alltorrents(ids)
|
||||
|
||||
if not queue:
|
||||
log.debug('Nothing in queue or error')
|
||||
return False
|
||||
return []
|
||||
|
||||
for torrent_id in queue:
|
||||
torrent = 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', (torrent['name'], torrent['hash'], torrent['save_path'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused']))
|
||||
|
||||
if not 'hash' in torrent:
|
||||
# When given a list of ids, deluge will return an empty item for a non-existant torrent.
|
||||
continue
|
||||
|
||||
log.debug('name=%s / id=%s / save_path=%s / move_on_completed=%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', (torrent['name'], torrent['hash'], torrent['save_path'], torrent['move_on_completed'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused']))
|
||||
|
||||
# Deluge has no easy way to work out if a torrent is stalled or failing.
|
||||
#status = 'failed'
|
||||
@@ -149,6 +162,7 @@ class Deluge(Downloader):
|
||||
log.debug('Requesting Deluge to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||
return self.drpc.remove_torrent(release_download['id'], remove_local_data = delete_files)
|
||||
|
||||
|
||||
class DelugeRPC(object):
|
||||
|
||||
host = 'localhost'
|
||||
@@ -169,6 +183,13 @@ class DelugeRPC(object):
|
||||
self.client = DelugeClient()
|
||||
self.client.connect(self.host, int(self.port), self.username, self.password)
|
||||
|
||||
def test(self):
|
||||
try:
|
||||
self.connect()
|
||||
except:
|
||||
return False
|
||||
return True
|
||||
|
||||
def add_torrent_magnet(self, torrent, options):
|
||||
torrent_id = False
|
||||
try:
|
||||
@@ -179,7 +200,7 @@ class DelugeRPC(object):
|
||||
|
||||
if torrent_id and options['label']:
|
||||
self.client.label.set_torrent(torrent_id, options['label']).get()
|
||||
except Exception, err:
|
||||
except Exception as err:
|
||||
log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc()))
|
||||
finally:
|
||||
if self.client:
|
||||
@@ -197,7 +218,7 @@ class DelugeRPC(object):
|
||||
|
||||
if torrent_id and options['label']:
|
||||
self.client.label.set_torrent(torrent_id, options['label']).get()
|
||||
except Exception, err:
|
||||
except Exception as err:
|
||||
log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc()))
|
||||
finally:
|
||||
if self.client:
|
||||
@@ -205,12 +226,12 @@ class DelugeRPC(object):
|
||||
|
||||
return torrent_id
|
||||
|
||||
def get_alltorrents(self):
|
||||
def get_alltorrents(self, ids):
|
||||
ret = False
|
||||
try:
|
||||
self.connect()
|
||||
ret = self.client.core.get_torrents_status({}, {}).get()
|
||||
except Exception, err:
|
||||
ret = self.client.core.get_torrents_status({'id': ids}, ('name', 'hash', 'save_path', 'move_completed_path', 'progress', 'state', 'eta', 'ratio', 'stop_ratio', 'is_seed', 'is_finished', 'paused', 'move_on_completed', 'files')).get()
|
||||
except Exception as err:
|
||||
log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc()))
|
||||
finally:
|
||||
if self.client:
|
||||
@@ -221,7 +242,7 @@ class DelugeRPC(object):
|
||||
try:
|
||||
self.connect()
|
||||
self.client.core.pause_torrent(torrent_ids).get()
|
||||
except Exception, err:
|
||||
except Exception as err:
|
||||
log.error('Failed to pause torrent: %s %s', (err, traceback.format_exc()))
|
||||
finally:
|
||||
if self.client:
|
||||
@@ -231,7 +252,7 @@ class DelugeRPC(object):
|
||||
try:
|
||||
self.connect()
|
||||
self.client.core.resume_torrent(torrent_ids).get()
|
||||
except Exception, err:
|
||||
except Exception as err:
|
||||
log.error('Failed to resume torrent: %s %s', (err, traceback.format_exc()))
|
||||
finally:
|
||||
if self.client:
|
||||
@@ -242,7 +263,7 @@ class DelugeRPC(object):
|
||||
try:
|
||||
self.connect()
|
||||
ret = self.client.core.remove_torrent(torrent_id, remove_local_data).get()
|
||||
except Exception, err:
|
||||
except Exception as err:
|
||||
log.error('Failed to remove torrent: %s %s', (err, traceback.format_exc()))
|
||||
finally:
|
||||
if self.client:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import NZBGet
|
||||
|
||||
|
||||
def start():
|
||||
return NZBGet()
|
||||
|
||||
@@ -25,6 +26,13 @@ config = [{
|
||||
'default': 'localhost:6789',
|
||||
'description': 'Hostname with port. Usually <strong>localhost:6789</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'ssl',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': 'nzbget',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from base64 import standard_b64encode
|
||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import ss, sp
|
||||
from couchpotato.core.helpers.variable import tryInt, md5
|
||||
from couchpotato.core.helpers.variable import tryInt, md5, cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
from datetime import timedelta
|
||||
import re
|
||||
@@ -16,11 +16,10 @@ log = CPLog(__name__)
|
||||
class NZBGet(Downloader):
|
||||
|
||||
protocol = ['nzb']
|
||||
rpc = 'xmlrpc'
|
||||
|
||||
url = 'http://%(username)s:%(password)s@%(host)s/xmlrpc'
|
||||
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
if not filedata:
|
||||
@@ -29,10 +28,10 @@ class NZBGet(Downloader):
|
||||
|
||||
log.info('Sending "%s" to NZBGet.', data.get('name'))
|
||||
|
||||
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
|
||||
nzb_name = ss('%s.nzb' % self.createNzbName(data, movie))
|
||||
nzb_name = ss('%s.nzb' % self.createNzbName(data, media))
|
||||
|
||||
rpc = self.getRPC()
|
||||
|
||||
rpc = xmlrpclib.ServerProxy(url)
|
||||
try:
|
||||
if rpc.writelog('INFO', 'CouchPotato connected to drop off %s.' % nzb_name):
|
||||
log.debug('Successfully connected to NZBGet')
|
||||
@@ -41,7 +40,7 @@ class NZBGet(Downloader):
|
||||
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:
|
||||
except xmlrpclib.ProtocolError as e:
|
||||
if e.errcode == 401:
|
||||
log.error('Password is incorrect.')
|
||||
else:
|
||||
@@ -55,7 +54,7 @@ class NZBGet(Downloader):
|
||||
|
||||
if xml_response:
|
||||
log.info('NZB sent successfully to NZBGet')
|
||||
nzb_id = md5(data['url']) # about as unique as they come ;)
|
||||
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]
|
||||
@@ -67,13 +66,32 @@ class NZBGet(Downloader):
|
||||
log.error('NZBGet could not add %s to the queue.', nzb_name)
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
def test(self):
|
||||
rpc = self.getRPC()
|
||||
|
||||
try:
|
||||
if rpc.writelog('INFO', 'CouchPotato connected to test connection'):
|
||||
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 as e:
|
||||
if e.errcode == 401:
|
||||
log.error('Password is incorrect.')
|
||||
else:
|
||||
log.error('Protocol Error: %s', e)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
|
||||
log.debug('Checking NZBGet download status.')
|
||||
|
||||
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
|
||||
rpc = self.getRPC()
|
||||
|
||||
rpc = xmlrpclib.ServerProxy(url)
|
||||
try:
|
||||
if rpc.writelog('INFO', 'CouchPotato connected to check status'):
|
||||
log.debug('Successfully connected to NZBGet')
|
||||
@@ -81,13 +99,13 @@ class NZBGet(Downloader):
|
||||
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:
|
||||
return []
|
||||
except xmlrpclib.ProtocolError as e:
|
||||
if e.errcode == 401:
|
||||
log.error('Password is incorrect.')
|
||||
else:
|
||||
log.error('Protocol Error: %s', e)
|
||||
return False
|
||||
return []
|
||||
|
||||
# Get NZBGet data
|
||||
try:
|
||||
@@ -97,56 +115,59 @@ class NZBGet(Downloader):
|
||||
history = rpc.history()
|
||||
except:
|
||||
log.error('Failed getting data: %s', traceback.format_exc(1))
|
||||
return False
|
||||
return []
|
||||
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
|
||||
for nzb in groups:
|
||||
log.debug('Found %s in NZBGet download queue', nzb['NZBFilename'])
|
||||
try:
|
||||
nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0]
|
||||
except:
|
||||
nzb_id = nzb['NZBID']
|
||||
|
||||
if nzb_id in ids:
|
||||
log.debug('Found %s in NZBGet download queue', nzb['NZBFilename'])
|
||||
timeleft = -1
|
||||
try:
|
||||
if nzb['ActiveDownloads'] > 0 and nzb['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
|
||||
timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
|
||||
except:
|
||||
pass
|
||||
|
||||
timeleft = -1
|
||||
try:
|
||||
if nzb['ActiveDownloads'] > 0 and nzb['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
|
||||
timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
|
||||
except:
|
||||
pass
|
||||
|
||||
release_downloads.append({
|
||||
'id': nzb_id,
|
||||
'name': nzb['NZBFilename'],
|
||||
'original_status': 'DOWNLOADING' if nzb['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,
|
||||
})
|
||||
release_downloads.append({
|
||||
'id': nzb_id,
|
||||
'name': nzb['NZBFilename'],
|
||||
'original_status': 'DOWNLOADING' if nzb['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 nzb in queue: # 'Parameters' is not passed in rpc.postqueue
|
||||
log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename'])
|
||||
release_downloads.append({
|
||||
'id': nzb['NZBID'],
|
||||
'name': nzb['NZBFilename'],
|
||||
'original_status': nzb['Stage'],
|
||||
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
|
||||
})
|
||||
if nzb['NZBID'] in ids:
|
||||
log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename'])
|
||||
release_downloads.append({
|
||||
'id': nzb['NZBID'],
|
||||
'name': nzb['NZBFilename'],
|
||||
'original_status': nzb['Stage'],
|
||||
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
|
||||
})
|
||||
|
||||
for nzb in history:
|
||||
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log']))
|
||||
try:
|
||||
nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0]
|
||||
except:
|
||||
nzb_id = nzb['NZBID']
|
||||
release_downloads.append({
|
||||
'id': nzb_id,
|
||||
'name': nzb['NZBFilename'],
|
||||
'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed',
|
||||
'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': sp(nzb['DestDir'])
|
||||
})
|
||||
|
||||
if nzb_id in ids:
|
||||
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log']))
|
||||
release_downloads.append({
|
||||
'id': nzb_id,
|
||||
'name': nzb['NZBFilename'],
|
||||
'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed',
|
||||
'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': sp(nzb['DestDir'])
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
|
||||
@@ -154,9 +175,8 @@ class NZBGet(Downloader):
|
||||
|
||||
log.info('%s failed downloading, deleting...', release_download['name'])
|
||||
|
||||
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
|
||||
rpc = self.getRPC()
|
||||
|
||||
rpc = xmlrpclib.ServerProxy(url)
|
||||
try:
|
||||
if rpc.writelog('INFO', 'CouchPotato connected to delete some history'):
|
||||
log.debug('Successfully connected to NZBGet')
|
||||
@@ -165,7 +185,7 @@ class NZBGet(Downloader):
|
||||
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:
|
||||
except xmlrpclib.ProtocolError as e:
|
||||
if e.errcode == 401:
|
||||
log.error('Password is incorrect.')
|
||||
else:
|
||||
@@ -190,3 +210,7 @@ class NZBGet(Downloader):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def getRPC(self):
|
||||
url = cleanHost(host = self.conf('host'), ssl = self.conf('ssl'), username = self.conf('username'), password = self.conf('password')) + self.rpc
|
||||
return xmlrpclib.ServerProxy(url)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import NZBVortex
|
||||
|
||||
|
||||
def start():
|
||||
return NZBVortex()
|
||||
|
||||
@@ -22,7 +23,15 @@ config = [{
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'https://localhost:4321',
|
||||
'default': 'localhost:4321',
|
||||
'description': 'Hostname with port. Usually <strong>localhost:4321</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'ssl',
|
||||
'default': 1,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'api_key',
|
||||
|
||||
@@ -8,9 +8,11 @@ from uuid import uuid4
|
||||
import hashlib
|
||||
import httplib
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import urllib2
|
||||
|
||||
@@ -23,44 +25,54 @@ class NZBVortex(Downloader):
|
||||
api_level = None
|
||||
session_id = None
|
||||
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
# Send the nzb
|
||||
try:
|
||||
nzb_filename = self.createFileName(data, filedata, movie)
|
||||
self.call('nzb/add', params = {'file': (nzb_filename, filedata)}, multipart = True)
|
||||
nzb_filename = self.createFileName(data, filedata, media)
|
||||
self.call('nzb/add', files = {'file': (nzb_filename, filedata)})
|
||||
|
||||
time.sleep(10)
|
||||
raw_statuses = self.call('nzb')
|
||||
nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if nzb['name'] == nzb_filename][0]
|
||||
nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if os.path.basename(nzb['nzbFileName']) == nzb_filename][0]
|
||||
return self.downloadReturnId(nzb_id)
|
||||
except:
|
||||
log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
def test(self):
|
||||
try:
|
||||
login_result = self.login()
|
||||
except:
|
||||
return False
|
||||
|
||||
return login_result
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
|
||||
raw_statuses = self.call('nzb')
|
||||
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
for nzb in raw_statuses.get('nzbs', []):
|
||||
if nzb['id'] in ids:
|
||||
|
||||
# Check status
|
||||
status = 'busy'
|
||||
if nzb['state'] == 20:
|
||||
status = 'completed'
|
||||
elif nzb['state'] in [21, 22, 24]:
|
||||
status = 'failed'
|
||||
# Check status
|
||||
status = 'busy'
|
||||
if nzb['state'] == 20:
|
||||
status = 'completed'
|
||||
elif nzb['state'] in [21, 22, 24]:
|
||||
status = 'failed'
|
||||
|
||||
release_downloads.append({
|
||||
'id': nzb['id'],
|
||||
'name': nzb['uiTitle'],
|
||||
'status': status,
|
||||
'original_status': nzb['state'],
|
||||
'timeleft':-1,
|
||||
'folder': sp(nzb['destinationPath']),
|
||||
})
|
||||
release_downloads.append({
|
||||
'id': nzb['id'],
|
||||
'name': nzb['uiTitle'],
|
||||
'status': status,
|
||||
'original_status': nzb['state'],
|
||||
'timeleft': -1,
|
||||
'folder': sp(nzb['destinationPath']),
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
|
||||
@@ -98,7 +110,6 @@ class NZBVortex(Downloader):
|
||||
log.error('Login failed, please check you api-key')
|
||||
return False
|
||||
|
||||
|
||||
def call(self, call, parameters = None, repeat = False, auth = True, *args, **kwargs):
|
||||
|
||||
# Login first
|
||||
@@ -112,15 +123,14 @@ class NZBVortex(Downloader):
|
||||
|
||||
params = tryUrlencode(parameters)
|
||||
|
||||
url = cleanHost(self.conf('host')) + 'api/' + call
|
||||
url_opener = urllib2.build_opener(HTTPSHandler())
|
||||
url = cleanHost(self.conf('host'), ssl = self.conf('ssl')) + 'api/' + call
|
||||
|
||||
try:
|
||||
data = self.urlopen('%s?%s' % (url, params), opener = url_opener, *args, **kwargs)
|
||||
data = self.urlopen('%s?%s' % (url, params), *args, **kwargs)
|
||||
|
||||
if data:
|
||||
return json.loads(data)
|
||||
except URLError, e:
|
||||
except URLError as e:
|
||||
if hasattr(e, 'code') and e.code == 403:
|
||||
# Try login and do again
|
||||
if not repeat:
|
||||
@@ -138,12 +148,11 @@ class NZBVortex(Downloader):
|
||||
if not self.api_level:
|
||||
|
||||
url = cleanHost(self.conf('host')) + 'api/app/apilevel'
|
||||
url_opener = urllib2.build_opener(HTTPSHandler())
|
||||
|
||||
try:
|
||||
data = self.urlopen(url, opener = url_opener, show_error = False)
|
||||
data = self.urlopen(url, show_error = False)
|
||||
self.api_level = float(json.loads(data).get('apilevel'))
|
||||
except URLError, e:
|
||||
except URLError as e:
|
||||
if hasattr(e, 'code') and e.code == 403:
|
||||
log.error('This version of NZBVortex isn\'t supported. Please update to 2.8.6 or higher')
|
||||
else:
|
||||
@@ -173,6 +182,7 @@ class HTTPSConnection(httplib.HTTPSConnection):
|
||||
|
||||
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version = ssl.PROTOCOL_TLSv1)
|
||||
|
||||
|
||||
class HTTPSHandler(urllib2.HTTPSHandler):
|
||||
def https_open(self, req):
|
||||
return self.do_open(HTTPSConnection, req)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Pneumatic
|
||||
|
||||
|
||||
def start():
|
||||
return Pneumatic()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import with_statement
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.helpers.encoding import sp
|
||||
from couchpotato.core.logger import CPLog
|
||||
import os
|
||||
import traceback
|
||||
@@ -11,9 +12,10 @@ class Pneumatic(Downloader):
|
||||
|
||||
protocol = ['nzb']
|
||||
strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s'
|
||||
status_support = False
|
||||
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
directory = self.conf('directory')
|
||||
@@ -25,27 +27,27 @@ class Pneumatic(Downloader):
|
||||
log.error('No nzb available!')
|
||||
return False
|
||||
|
||||
fullPath = os.path.join(directory, self.createFileName(data, filedata, movie))
|
||||
full_path = os.path.join(directory, self.createFileName(data, filedata, media))
|
||||
|
||||
try:
|
||||
if not os.path.isfile(fullPath):
|
||||
log.info('Downloading %s to %s.', (data.get('protocol'), fullPath))
|
||||
with open(fullPath, 'wb') as f:
|
||||
if not os.path.isfile(full_path):
|
||||
log.info('Downloading %s to %s.', (data.get('protocol'), full_path))
|
||||
with open(full_path, 'wb') as f:
|
||||
f.write(filedata)
|
||||
|
||||
nzb_name = self.createNzbName(data, movie)
|
||||
nzb_name = self.createNzbName(data, media)
|
||||
strm_path = os.path.join(directory, nzb_name)
|
||||
|
||||
strm_file = open(strm_path + '.strm', 'wb')
|
||||
strmContent = self.strm_syntax % (fullPath, nzb_name)
|
||||
strmContent = self.strm_syntax % (full_path, nzb_name)
|
||||
strm_file.write(strmContent)
|
||||
strm_file.close()
|
||||
|
||||
return True
|
||||
return self.downloadReturnId('')
|
||||
|
||||
else:
|
||||
log.info('File %s already exists.', fullPath)
|
||||
return True
|
||||
log.info('File %s already exists.', full_path)
|
||||
return self.downloadReturnId('')
|
||||
|
||||
except:
|
||||
log.error('Failed to download .strm: %s', traceback.format_exc())
|
||||
@@ -55,3 +57,17 @@ class Pneumatic(Downloader):
|
||||
log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc()))
|
||||
return False
|
||||
return False
|
||||
|
||||
def test(self):
|
||||
directory = self.conf('directory')
|
||||
if directory and os.path.isdir(directory):
|
||||
|
||||
test_file = sp(os.path.join(directory, 'couchpotato_test.txt'))
|
||||
|
||||
# Check if folder is writable
|
||||
self.createFile(test_file, 'This is a test file')
|
||||
if os.path.isfile(test_file):
|
||||
os.remove(test_file)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import rTorrent
|
||||
|
||||
|
||||
def start():
|
||||
return rTorrent()
|
||||
|
||||
@@ -20,11 +21,32 @@ config = [{
|
||||
'type': 'enabler',
|
||||
'radio_group': 'torrent',
|
||||
},
|
||||
# @RuudBurger: How do I migrate this?
|
||||
# {
|
||||
# 'name': 'url',
|
||||
# 'default': 'http://localhost:80/RPC2',
|
||||
# 'description': 'XML-RPC Endpoint URI. Usually <strong>scgi://localhost:5000</strong> '
|
||||
# 'or <strong>http://localhost:80/RPC2</strong>'
|
||||
# },
|
||||
{
|
||||
'name': 'url',
|
||||
'default': 'http://localhost:80/RPC2',
|
||||
'description': 'XML-RPC Endpoint URI. Usually <strong>scgi://localhost:5000</strong> '
|
||||
'or <strong>http://localhost:80/RPC2</strong>'
|
||||
'name': 'host',
|
||||
'default': 'localhost:80',
|
||||
'description': 'RPC Communication URI. Usually <strong>scgi://localhost:5000</strong>, '
|
||||
'<strong>httprpc://localhost/rutorrent</strong> or <strong>localhost:80</strong>'
|
||||
},
|
||||
{
|
||||
'name': 'ssl',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'rpc_url',
|
||||
'type': 'string',
|
||||
'default': 'RPC2',
|
||||
'advanced': True,
|
||||
'description': 'Change if your RPC mount is at a different path.',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
@@ -58,14 +80,6 @@ config = [{
|
||||
'advanced': True,
|
||||
'description': 'Also remove the leftover files.',
|
||||
},
|
||||
{
|
||||
'name': 'append_label',
|
||||
'label': 'Append Label',
|
||||
'default': False,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Append label to download location. Requires you to set the download location above.',
|
||||
},
|
||||
{
|
||||
'name': 'paused',
|
||||
'type': 'bool',
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.encoding import sp
|
||||
from couchpotato.core.helpers.variable import cleanHost, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from base64 import b16encode, b32decode
|
||||
from bencode import bencode, bdecode
|
||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import sp
|
||||
from couchpotato.core.logger import CPLog
|
||||
from datetime import timedelta
|
||||
from hashlib import sha1
|
||||
from rtorrent import RTorrent
|
||||
from rtorrent.err import MethodError
|
||||
from urlparse import urlparse
|
||||
import os
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -16,29 +19,75 @@ class rTorrent(Downloader):
|
||||
|
||||
protocol = ['torrent', 'torrent_magnet']
|
||||
rt = None
|
||||
error_msg = ''
|
||||
|
||||
def connect(self):
|
||||
# Migration url to host options
|
||||
def __init__(self):
|
||||
super(rTorrent, self).__init__()
|
||||
|
||||
addEvent('app.load', self.migrate)
|
||||
addEvent('setting.save.rtorrent.*.after', self.settingsChanged)
|
||||
|
||||
def migrate(self):
|
||||
|
||||
url = self.conf('url')
|
||||
if url:
|
||||
host_split = splitString(url.split('://')[-1], split_on = '/')
|
||||
|
||||
self.conf('ssl', value = url.startswith('https'))
|
||||
self.conf('host', value = host_split[0].strip())
|
||||
self.conf('rpc_url', value = '/'.join(host_split[1:]))
|
||||
|
||||
self.deleteConf('url')
|
||||
|
||||
def settingsChanged(self):
|
||||
# Reset active connection if settings have changed
|
||||
if self.rt:
|
||||
log.debug('Settings have changed, closing active connection')
|
||||
|
||||
self.rt = None
|
||||
return True
|
||||
|
||||
def connect(self, reconnect = False):
|
||||
# Already connected?
|
||||
if self.rt is not None:
|
||||
if not reconnect and 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
|
||||
url = cleanHost(self.conf('host'), protocol = True, ssl = self.conf('ssl'))
|
||||
parsed = urlparse(url)
|
||||
|
||||
# rpc_url is only used on http/https scgi pass-through
|
||||
if parsed.scheme in ['http', 'https']:
|
||||
url += self.conf('rpc_url')
|
||||
|
||||
if self.conf('username') and self.conf('password'):
|
||||
self.rt = RTorrent(
|
||||
self.conf('url'),
|
||||
url,
|
||||
self.conf('username'),
|
||||
self.conf('password')
|
||||
)
|
||||
else:
|
||||
self.rt = RTorrent(self.conf('url'))
|
||||
self.rt = RTorrent(url)
|
||||
|
||||
self.error_msg = ''
|
||||
try:
|
||||
self.rt._verify_conn()
|
||||
except AssertionError as e:
|
||||
self.error_msg = e.message
|
||||
self.rt = None
|
||||
|
||||
return self.rt
|
||||
|
||||
def _update_provider_group(self, name, data):
|
||||
def test(self):
|
||||
if self.connect(True):
|
||||
return True
|
||||
|
||||
if self.error_msg:
|
||||
return False, 'Connection failed: ' + self.error_msg
|
||||
|
||||
return False
|
||||
|
||||
def updateProviderGroup(self, name, data):
|
||||
if data.get('seed_time'):
|
||||
log.info('seeding time ignored, not supported')
|
||||
|
||||
@@ -70,28 +119,30 @@ class rTorrent(Downloader):
|
||||
# Reset group action and disable it
|
||||
group.set_command()
|
||||
group.disable()
|
||||
except MethodError, err:
|
||||
except MethodError as err:
|
||||
log.error('Unable to set group options: %s', err.msg)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def download(self, data, movie, filedata = None):
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
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):
|
||||
if not self.updateProviderGroup(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
|
||||
@@ -115,7 +166,7 @@ class rTorrent(Downloader):
|
||||
# Send request to rTorrent
|
||||
try:
|
||||
# Send torrent to rTorrent
|
||||
torrent = self.rt.load_torrent(filedata)
|
||||
torrent = self.rt.load_torrent(filedata, verify_retries=10)
|
||||
|
||||
if not torrent:
|
||||
log.error('Unable to find the torrent, did it fail to load?')
|
||||
@@ -125,9 +176,7 @@ class rTorrent(Downloader):
|
||||
if self.conf('label'):
|
||||
torrent.set_custom(1, self.conf('label'))
|
||||
|
||||
if self.conf('directory') and self.conf('append_label'):
|
||||
torrent.set_directory(os.path.join(self.conf('directory'), self.conf('label')))
|
||||
elif self.conf('directory'):
|
||||
if self.conf('directory'):
|
||||
torrent.set_directory(self.conf('directory'))
|
||||
|
||||
# Set Ratio Group
|
||||
@@ -138,15 +187,30 @@ class rTorrent(Downloader):
|
||||
torrent.start()
|
||||
|
||||
return self.downloadReturnId(torrent_hash)
|
||||
except Exception, err:
|
||||
except Exception as err:
|
||||
log.error('Failed to send torrent to rTorrent: %s', err)
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
def getTorrentStatus(self, torrent):
|
||||
if torrent.hashing or torrent.hash_checking or torrent.message:
|
||||
return 'busy'
|
||||
|
||||
if not torrent.complete:
|
||||
return 'busy'
|
||||
|
||||
if not torrent.open:
|
||||
return 'completed'
|
||||
|
||||
if torrent.state and torrent.active:
|
||||
return 'seeding'
|
||||
|
||||
return 'busy'
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
log.debug('Checking rTorrent download status.')
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
return []
|
||||
|
||||
try:
|
||||
torrents = self.rt.get_torrents()
|
||||
@@ -154,33 +218,34 @@ class rTorrent(Downloader):
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
|
||||
for torrent in torrents:
|
||||
torrent_files = []
|
||||
for file_item in torrent.get_files():
|
||||
torrent_files.append(sp(os.path.join(torrent.directory, file_item.path)))
|
||||
if torrent.info_hash in ids:
|
||||
torrent_directory = os.path.normpath(torrent.directory)
|
||||
torrent_files = []
|
||||
|
||||
status = 'busy'
|
||||
if torrent.complete:
|
||||
if torrent.active:
|
||||
status = 'seeding'
|
||||
else:
|
||||
status = 'completed'
|
||||
for file in torrent.get_files():
|
||||
if not os.path.normpath(file.path).startswith(torrent_directory):
|
||||
file_path = os.path.join(torrent_directory, file.path.lstrip('/'))
|
||||
else:
|
||||
file_path = file.path
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent.info_hash,
|
||||
'name': torrent.name,
|
||||
'status': status,
|
||||
'seed_ratio': torrent.ratio,
|
||||
'original_status': torrent.state,
|
||||
'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1,
|
||||
'folder': sp(torrent.directory),
|
||||
'files': '|'.join(torrent_files)
|
||||
})
|
||||
torrent_files.append(sp(file_path))
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent.info_hash,
|
||||
'name': torrent.name,
|
||||
'status': self.getTorrentStatus(torrent),
|
||||
'seed_ratio': torrent.ratio,
|
||||
'original_status': torrent.state,
|
||||
'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1,
|
||||
'folder': sp(torrent.directory),
|
||||
'files': '|'.join(torrent_files)
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
|
||||
except Exception, err:
|
||||
except Exception as err:
|
||||
log.error('Failed to get status from rTorrent: %s', err)
|
||||
return False
|
||||
return []
|
||||
|
||||
def pause(self, release_download, pause = True):
|
||||
if not self.connect():
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Sabnzbd
|
||||
|
||||
|
||||
def start():
|
||||
return Sabnzbd()
|
||||
|
||||
@@ -24,6 +25,13 @@ config = [{
|
||||
'name': 'host',
|
||||
'default': 'localhost:8080',
|
||||
},
|
||||
{
|
||||
'name': 'ssl',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'api_key',
|
||||
'label': 'Api Key',
|
||||
|
||||
@@ -16,8 +16,8 @@ class Sabnzbd(Downloader):
|
||||
|
||||
protocol = ['nzb']
|
||||
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.info('Sending "%s" to SABnzbd.', data.get('name'))
|
||||
@@ -25,7 +25,7 @@ class Sabnzbd(Downloader):
|
||||
req_params = {
|
||||
'cat': self.conf('category'),
|
||||
'mode': 'addurl',
|
||||
'nzbname': self.createNzbName(data, movie),
|
||||
'nzbname': self.createNzbName(data, media),
|
||||
'priority': self.conf('priority'),
|
||||
}
|
||||
|
||||
@@ -36,14 +36,14 @@ class Sabnzbd(Downloader):
|
||||
return False
|
||||
|
||||
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
|
||||
nzb_filename = self.createFileName(data, filedata, movie)
|
||||
nzb_filename = self.createFileName(data, filedata, media)
|
||||
req_params['mode'] = 'addfile'
|
||||
else:
|
||||
req_params['name'] = data.get('url')
|
||||
|
||||
try:
|
||||
if nzb_filename and req_params.get('mode') is 'addfile':
|
||||
sab_data = self.call(req_params, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True)
|
||||
sab_data = self.call(req_params, files = {'nzbfile': (ss(nzb_filename), filedata)})
|
||||
else:
|
||||
sab_data = self.call(req_params)
|
||||
except URLError:
|
||||
@@ -64,7 +64,27 @@ class Sabnzbd(Downloader):
|
||||
log.error('Error getting data from SABNZBd: %s', sab_data)
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
def test(self):
|
||||
try:
|
||||
sab_data = self.call({
|
||||
'mode': 'version',
|
||||
})
|
||||
v = sab_data.split('.')
|
||||
if int(v[0]) == 0 and int(v[1]) < 7:
|
||||
return False, 'Your Sabnzbd client is too old, please update to newest version.'
|
||||
|
||||
# the version check will work even with wrong api key, so we need the next check as well
|
||||
sab_data = self.call({
|
||||
'mode': 'qstatus',
|
||||
})
|
||||
if not sab_data:
|
||||
return False
|
||||
except:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
|
||||
log.debug('Checking SABnzbd download status.')
|
||||
|
||||
@@ -75,7 +95,7 @@ class Sabnzbd(Downloader):
|
||||
})
|
||||
except:
|
||||
log.error('Failed getting queue: %s', traceback.format_exc(1))
|
||||
return False
|
||||
return []
|
||||
|
||||
# Go through history items
|
||||
try:
|
||||
@@ -85,41 +105,42 @@ class Sabnzbd(Downloader):
|
||||
})
|
||||
except:
|
||||
log.error('Failed getting history json: %s', traceback.format_exc(1))
|
||||
return False
|
||||
return []
|
||||
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
|
||||
# Get busy releases
|
||||
for nzb in queue.get('slots', []):
|
||||
status = 'busy'
|
||||
if 'ENCRYPTED / ' in nzb['filename']:
|
||||
status = 'failed'
|
||||
if nzb['nzo_id'] in ids:
|
||||
status = 'busy'
|
||||
if 'ENCRYPTED / ' in nzb['filename']:
|
||||
status = 'failed'
|
||||
|
||||
release_downloads.append({
|
||||
'id': nzb['nzo_id'],
|
||||
'name': nzb['filename'],
|
||||
'status': status,
|
||||
'original_status': nzb['status'],
|
||||
'timeleft': nzb['timeleft'] if not queue['paused'] else -1,
|
||||
})
|
||||
release_downloads.append({
|
||||
'id': nzb['nzo_id'],
|
||||
'name': nzb['filename'],
|
||||
'status': status,
|
||||
'original_status': nzb['status'],
|
||||
'timeleft': nzb['timeleft'] if not queue['paused'] else -1,
|
||||
})
|
||||
|
||||
# Get old releases
|
||||
for nzb in history.get('slots', []):
|
||||
if nzb['nzo_id'] in ids:
|
||||
status = 'busy'
|
||||
if nzb['status'] == 'Failed' or (nzb['status'] == 'Completed' and nzb['fail_message'].strip()):
|
||||
status = 'failed'
|
||||
elif nzb['status'] == 'Completed':
|
||||
status = 'completed'
|
||||
|
||||
status = 'busy'
|
||||
if nzb['status'] == 'Failed' or (nzb['status'] == 'Completed' and nzb['fail_message'].strip()):
|
||||
status = 'failed'
|
||||
elif nzb['status'] == 'Completed':
|
||||
status = 'completed'
|
||||
|
||||
release_downloads.append({
|
||||
'id': nzb['nzo_id'],
|
||||
'name': nzb['name'],
|
||||
'status': status,
|
||||
'original_status': nzb['status'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': sp(os.path.dirname(nzb['storage']) if os.path.isfile(nzb['storage']) else nzb['storage']),
|
||||
})
|
||||
release_downloads.append({
|
||||
'id': nzb['nzo_id'],
|
||||
'name': nzb['name'],
|
||||
'status': status,
|
||||
'original_status': nzb['status'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': sp(os.path.dirname(nzb['storage']) if os.path.isfile(nzb['storage']) else nzb['storage']),
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
|
||||
@@ -164,9 +185,9 @@ class Sabnzbd(Downloader):
|
||||
|
||||
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'
|
||||
url = cleanHost(self.conf('host'), ssl = self.conf('ssl')) + '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()}, **kwargs)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Synology
|
||||
|
||||
|
||||
def start():
|
||||
return Synology()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.helpers.encoding import isInt
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
import json
|
||||
import requests
|
||||
@@ -11,17 +12,17 @@ log = CPLog(__name__)
|
||||
class Synology(Downloader):
|
||||
|
||||
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
||||
log = CPLog(__name__)
|
||||
status_support = False
|
||||
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
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(':')
|
||||
host = cleanHost(self.conf('host'), protocol = False).split(':')
|
||||
if not isInt(host[1]):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
return False
|
||||
@@ -42,7 +43,17 @@ class Synology(Downloader):
|
||||
except:
|
||||
log.error('Exception while adding torrent: %s', traceback.format_exc())
|
||||
finally:
|
||||
return response
|
||||
return self.downloadReturnId('') if response else False
|
||||
|
||||
def test(self):
|
||||
host = cleanHost(self.conf('host'), protocol = False).split(':')
|
||||
try:
|
||||
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
|
||||
test_result = srpc.test()
|
||||
except:
|
||||
return False
|
||||
|
||||
return test_result
|
||||
|
||||
def getEnabledProtocol(self):
|
||||
if self.conf('use_for') == 'both':
|
||||
@@ -64,6 +75,7 @@ class Synology(Downloader):
|
||||
return super(Synology, self).isEnabled(manual, data) and\
|
||||
((self.conf('use_for') in for_protocol))
|
||||
|
||||
|
||||
class SynologyRPC(object):
|
||||
|
||||
"""SynologyRPC lite library"""
|
||||
@@ -106,11 +118,11 @@ class SynologyRPC(object):
|
||||
if response['success']:
|
||||
log.info('Synology action successfull')
|
||||
return response
|
||||
except requests.ConnectionError, err:
|
||||
except requests.ConnectionError as err:
|
||||
log.error('Synology connection error, check your config %s', err)
|
||||
except requests.HTTPError, err:
|
||||
except requests.HTTPError as err:
|
||||
log.error('SynologyRPC HTTPError: %s', err)
|
||||
except Exception, err:
|
||||
except Exception as err:
|
||||
log.error('Exception: %s', err)
|
||||
finally:
|
||||
return response
|
||||
@@ -145,3 +157,6 @@ class SynologyRPC(object):
|
||||
self._logout()
|
||||
|
||||
return result
|
||||
|
||||
def test(self):
|
||||
return bool(self._login())
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Transmission
|
||||
|
||||
|
||||
def start():
|
||||
return Transmission()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from base64 import b64encode
|
||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import isInt, sp
|
||||
from couchpotato.core.helpers.variable import tryInt, tryFloat
|
||||
from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
from datetime import timedelta
|
||||
import httplib
|
||||
@@ -19,19 +19,21 @@ class Transmission(Downloader):
|
||||
log = CPLog(__name__)
|
||||
trpc = None
|
||||
|
||||
def connect(self):
|
||||
def connect(self, reconnect = False):
|
||||
# Load host from config and split out port.
|
||||
host = self.conf('host').split(':')
|
||||
host = cleanHost(self.conf('host'), protocol = False).split(':')
|
||||
if not isInt(host[1]):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
return False
|
||||
|
||||
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'))
|
||||
if not self.trpc or reconnect:
|
||||
self.trpc = TransmissionRPC(host[0], port = host[1], rpc_url = self.conf('rpc_url').strip('/ '), username = self.conf('username'), password = self.conf('password'))
|
||||
|
||||
return self.trpc
|
||||
|
||||
def download(self, data, movie, filedata = None):
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('protocol')))
|
||||
|
||||
@@ -81,12 +83,17 @@ class Transmission(Downloader):
|
||||
log.info('Torrent sent to Transmission successfully.')
|
||||
return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
def test(self):
|
||||
if self.connect(True) and self.trpc.get_session():
|
||||
return True
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
|
||||
log.debug('Checking Transmission download status.')
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
return []
|
||||
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
|
||||
@@ -94,37 +101,44 @@ class Transmission(Downloader):
|
||||
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isStalled', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit', 'files']
|
||||
}
|
||||
|
||||
session = self.trpc.get_session()
|
||||
queue = self.trpc.get_alltorrents(return_params)
|
||||
if not (queue and queue.get('torrents')):
|
||||
log.debug('Nothing in queue or error')
|
||||
return False
|
||||
return []
|
||||
|
||||
for torrent in queue['torrents']:
|
||||
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / isFinished=%s',
|
||||
(torrent['name'], torrent['id'], torrent['downloadDir'], torrent['hashString'], torrent['percentDone'], torrent['status'], torrent['eta'], torrent['uploadRatio'], torrent['isFinished']))
|
||||
if torrent['hashString'] in ids:
|
||||
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / isStalled=%s / eta=%s / uploadRatio=%s / isFinished=%s / incomplete-dir-enabled=%s / incomplete-dir=%s',
|
||||
(torrent['name'], torrent['id'], torrent['downloadDir'], torrent['hashString'], torrent['percentDone'], torrent['status'], torrent.get('isStalled', 'N/A'), torrent['eta'], torrent['uploadRatio'], torrent['isFinished'], session['incomplete-dir-enabled'], session['incomplete-dir']))
|
||||
|
||||
torrent_files = []
|
||||
for file_item in torrent['files']:
|
||||
torrent_files.append(sp(os.path.join(torrent['downloadDir'], file_item['name'])))
|
||||
status = 'busy'
|
||||
if torrent.get('isStalled') and not torrent['percentDone'] == 1 and self.conf('stalled_as_failed'):
|
||||
status = 'failed'
|
||||
elif torrent['status'] == 0 and torrent['percentDone'] == 1:
|
||||
status = 'completed'
|
||||
elif torrent['status'] in [5, 6]:
|
||||
status = 'seeding'
|
||||
|
||||
status = 'busy'
|
||||
if torrent.get('isStalled') and self.conf('stalled_as_failed'):
|
||||
status = 'failed'
|
||||
elif torrent['status'] == 0 and torrent['percentDone'] == 1:
|
||||
status = 'completed'
|
||||
elif torrent['status'] in [5, 6]:
|
||||
status = 'seeding'
|
||||
if session['incomplete-dir-enabled'] and status == 'busy':
|
||||
torrent_folder = session['incomplete-dir']
|
||||
else:
|
||||
torrent_folder = torrent['downloadDir']
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent['hashString'],
|
||||
'name': torrent['name'],
|
||||
'status': status,
|
||||
'original_status': torrent['status'],
|
||||
'seed_ratio': torrent['uploadRatio'],
|
||||
'timeleft': str(timedelta(seconds = torrent['eta'])),
|
||||
'folder': sp(torrent['downloadDir'] if len(torrent_files) == 1 else os.path.join(torrent['downloadDir'], torrent['name'])),
|
||||
'files': '|'.join(torrent_files)
|
||||
})
|
||||
torrent_files = []
|
||||
for file_item in torrent['files']:
|
||||
torrent_files.append(sp(os.path.join(torrent_folder, file_item['name'])))
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent['hashString'],
|
||||
'name': torrent['name'],
|
||||
'status': status,
|
||||
'original_status': torrent['status'],
|
||||
'seed_ratio': torrent['uploadRatio'],
|
||||
'timeleft': str(timedelta(seconds = torrent['eta'])),
|
||||
'folder': sp(torrent_folder if len(torrent_files) == 1 else os.path.join(torrent_folder, torrent['name'])),
|
||||
'files': '|'.join(torrent_files)
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
|
||||
@@ -178,10 +192,10 @@ class TransmissionRPC(object):
|
||||
else:
|
||||
log.debug('Unknown failure sending command to Transmission. Return text is: %s', response['result'])
|
||||
return False
|
||||
except httplib.InvalidURL, err:
|
||||
except httplib.InvalidURL as err:
|
||||
log.error('Invalid Transmission host, check your config %s', err)
|
||||
return False
|
||||
except urllib2.HTTPError, err:
|
||||
except urllib2.HTTPError as err:
|
||||
if err.code == 401:
|
||||
log.error('Invalid Transmission Username or Password, check your config')
|
||||
return False
|
||||
@@ -199,7 +213,7 @@ class TransmissionRPC(object):
|
||||
log.error('Unable to get Transmission Session-Id %s', err)
|
||||
else:
|
||||
log.error('TransmissionRPC HTTPError: %s', err)
|
||||
except urllib2.URLError, err:
|
||||
except urllib2.URLError as err:
|
||||
log.error('Unable to connect to Transmission %s', err)
|
||||
|
||||
def get_session(self):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import uTorrent
|
||||
|
||||
|
||||
def start():
|
||||
return uTorrent()
|
||||
|
||||
@@ -23,7 +24,7 @@ config = [{
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost:8000',
|
||||
'description': 'Hostname with port. Usually <strong>localhost:8000</strong>',
|
||||
'description': 'Port can be found in settings when enabling WebUI.',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
|
||||
@@ -2,7 +2,7 @@ from base64 import b16encode, b32decode
|
||||
from bencode import bencode as benc, bdecode
|
||||
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
|
||||
from couchpotato.core.helpers.encoding import isInt, ss, sp
|
||||
from couchpotato.core.helpers.variable import tryInt, tryFloat
|
||||
from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
from datetime import timedelta
|
||||
from hashlib import sha1
|
||||
@@ -24,10 +24,20 @@ class uTorrent(Downloader):
|
||||
|
||||
protocol = ['torrent', 'torrent_magnet']
|
||||
utorrent_api = None
|
||||
status_flags = {
|
||||
'STARTED' : 1,
|
||||
'CHECKING' : 2,
|
||||
'CHECK-START' : 4,
|
||||
'CHECKED' : 8,
|
||||
'ERROR' : 16,
|
||||
'PAUSED' : 32,
|
||||
'QUEUED' : 64,
|
||||
'LOADED' : 128
|
||||
}
|
||||
|
||||
def connect(self):
|
||||
# Load host from config and split out port.
|
||||
host = self.conf('host').split(':')
|
||||
host = cleanHost(self.conf('host'), protocol = False).split(':')
|
||||
if not isInt(host[1]):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
return False
|
||||
@@ -36,11 +46,11 @@ class uTorrent(Downloader):
|
||||
|
||||
return self.utorrent_api
|
||||
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
def download(self, data = None, media = None, filedata = None):
|
||||
if not media: media = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('protocol')))
|
||||
log.debug("Sending '%s' (%s) to uTorrent.", (data.get('name'), data.get('protocol')))
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
@@ -56,7 +66,7 @@ class uTorrent(Downloader):
|
||||
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
|
||||
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.')
|
||||
|
||||
@@ -75,9 +85,10 @@ class uTorrent(Downloader):
|
||||
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"]
|
||||
info = bdecode(filedata)['info']
|
||||
torrent_hash = sha1(benc(info)).hexdigest().upper()
|
||||
torrent_filename = self.createFileName(data, filedata, movie)
|
||||
|
||||
torrent_filename = self.createFileName(data, filedata, media)
|
||||
|
||||
if data.get('seed_ratio'):
|
||||
torrent_params['seed_override'] = 1
|
||||
@@ -104,72 +115,73 @@ class uTorrent(Downloader):
|
||||
|
||||
return self.downloadReturnId(torrent_hash)
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
def test(self):
|
||||
if self.connect():
|
||||
build_version = self.utorrent_api.get_build()
|
||||
if not build_version:
|
||||
return False
|
||||
if build_version < 25406: # This build corresponds to version 3.0.0 stable
|
||||
return False, 'Your uTorrent client is too old, please update to newest version.'
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self, ids):
|
||||
|
||||
log.debug('Checking uTorrent download status.')
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
return []
|
||||
|
||||
release_downloads = ReleaseDownloadList(self)
|
||||
|
||||
data = self.utorrent_api.get_status()
|
||||
if not data:
|
||||
log.error('Error getting data from uTorrent')
|
||||
return False
|
||||
return []
|
||||
|
||||
queue = json.loads(data)
|
||||
if queue.get('error'):
|
||||
log.error('Error getting data from uTorrent: %s', queue.get('error'))
|
||||
return False
|
||||
return []
|
||||
|
||||
if not queue.get('torrents'):
|
||||
log.debug('Nothing in queue')
|
||||
return False
|
||||
return []
|
||||
|
||||
# Get torrents
|
||||
for torrent in queue['torrents']:
|
||||
if torrent[0] in ids:
|
||||
|
||||
#Get files of the torrent
|
||||
torrent_files = []
|
||||
try:
|
||||
torrent_files = json.loads(self.utorrent_api.get_files(torrent[0]))
|
||||
torrent_files = [sp(os.path.join(torrent[26], torrent_file[0])) for torrent_file in torrent_files['files'][1]]
|
||||
except:
|
||||
log.debug('Failed getting files from torrent: %s', torrent[2])
|
||||
#Get files of the torrent
|
||||
torrent_files = []
|
||||
try:
|
||||
torrent_files = json.loads(self.utorrent_api.get_files(torrent[0]))
|
||||
torrent_files = [sp(os.path.join(torrent[26], torrent_file[0])) for torrent_file in torrent_files['files'][1]]
|
||||
except:
|
||||
log.debug('Failed getting files from torrent: %s', torrent[2])
|
||||
|
||||
status_flags = {
|
||||
"STARTED" : 1,
|
||||
"CHECKING" : 2,
|
||||
"CHECK-START" : 4,
|
||||
"CHECKED" : 8,
|
||||
"ERROR" : 16,
|
||||
"PAUSED" : 32,
|
||||
"QUEUED" : 64,
|
||||
"LOADED" : 128
|
||||
}
|
||||
status = 'busy'
|
||||
if (torrent[1] & self.status_flags['STARTED'] or torrent[1] & self.status_flags['QUEUED']) and torrent[4] == 1000:
|
||||
status = 'seeding'
|
||||
elif (torrent[1] & self.status_flags['ERROR']):
|
||||
status = 'failed'
|
||||
elif torrent[4] == 1000:
|
||||
status = 'completed'
|
||||
|
||||
status = 'busy'
|
||||
if (torrent[1] & status_flags["STARTED"] or torrent[1] & status_flags["QUEUED"]) and torrent[4] == 1000:
|
||||
status = 'seeding'
|
||||
elif (torrent[1] & status_flags["ERROR"]):
|
||||
status = 'failed'
|
||||
elif torrent[4] == 1000:
|
||||
status = 'completed'
|
||||
if not status == 'busy':
|
||||
self.removeReadOnly(torrent_files)
|
||||
|
||||
if not status == 'busy':
|
||||
self.removeReadOnly(torrent_files)
|
||||
|
||||
release_downloads.append({
|
||||
'id': torrent[0],
|
||||
'name': torrent[2],
|
||||
'status': status,
|
||||
'seed_ratio': float(torrent[7]) / 1000,
|
||||
'original_status': torrent[1],
|
||||
'timeleft': str(timedelta(seconds = torrent[10])),
|
||||
'folder': sp(torrent[26]),
|
||||
'files': '|'.join(torrent_files)
|
||||
})
|
||||
release_downloads.append({
|
||||
'id': torrent[0],
|
||||
'name': torrent[2],
|
||||
'status': status,
|
||||
'seed_ratio': float(torrent[7]) / 1000,
|
||||
'original_status': torrent[1],
|
||||
'timeleft': str(timedelta(seconds = torrent[10])),
|
||||
'folder': sp(torrent[26]),
|
||||
'files': '|'.join(torrent_files)
|
||||
})
|
||||
|
||||
return release_downloads
|
||||
|
||||
@@ -222,7 +234,7 @@ class uTorrentAPI(object):
|
||||
if time.time() > self.last_time + 1800:
|
||||
self.last_time = time.time()
|
||||
self.token = self.get_token()
|
||||
request = urllib2.Request(self.url + "?token=" + self.token + "&" + action, data)
|
||||
request = urllib2.Request(self.url + '?token=' + self.token + '&' + action, data)
|
||||
try:
|
||||
open_request = self.opener.open(request)
|
||||
response = open_request.read()
|
||||
@@ -230,64 +242,64 @@ class uTorrentAPI(object):
|
||||
return response
|
||||
else:
|
||||
log.debug('Unknown failure sending command to uTorrent. Return text is: %s', response)
|
||||
except httplib.InvalidURL, err:
|
||||
except httplib.InvalidURL as err:
|
||||
log.error('Invalid uTorrent host, check your config %s', err)
|
||||
except urllib2.HTTPError, err:
|
||||
except urllib2.HTTPError as err:
|
||||
if err.code == 401:
|
||||
log.error('Invalid uTorrent Username or Password, check your config')
|
||||
else:
|
||||
log.error('uTorrent HTTPError: %s', err)
|
||||
except urllib2.URLError, err:
|
||||
except urllib2.URLError as err:
|
||||
log.error('Unable to connect to uTorrent %s', err)
|
||||
return False
|
||||
|
||||
def get_token(self):
|
||||
request = self.opener.open(self.url + "token.html")
|
||||
token = re.findall("<div.*?>(.*?)</", request.read())[0]
|
||||
request = self.opener.open(self.url + 'token.html')
|
||||
token = re.findall('<div.*?>(.*?)</', request.read())[0]
|
||||
return token
|
||||
|
||||
def add_torrent_uri(self, filename, torrent, add_folder = False):
|
||||
action = "action=add-url&s=%s" % urllib.quote(torrent)
|
||||
action = 'action=add-url&s=%s' % urllib.quote(torrent)
|
||||
if add_folder:
|
||||
action += "&path=%s" % urllib.quote(filename)
|
||||
action += '&path=%s' % urllib.quote(filename)
|
||||
return self._request(action)
|
||||
|
||||
def add_torrent_file(self, filename, filedata, add_folder = False):
|
||||
action = "action=add-file"
|
||||
action = 'action=add-file'
|
||||
if add_folder:
|
||||
action += "&path=%s" % urllib.quote(filename)
|
||||
return self._request(action, {"torrent_file": (ss(filename), filedata)})
|
||||
action += '&path=%s' % urllib.quote(filename)
|
||||
return self._request(action, {'torrent_file': (ss(filename), filedata)})
|
||||
|
||||
def set_torrent(self, hash, params):
|
||||
action = "action=setprops&hash=%s" % hash
|
||||
for k, v in params.iteritems():
|
||||
action += "&s=%s&v=%s" % (k, v)
|
||||
action = 'action=setprops&hash=%s' % hash
|
||||
for k, v in params.items():
|
||||
action += '&s=%s&v=%s' % (k, v)
|
||||
return self._request(action)
|
||||
|
||||
def pause_torrent(self, hash, pause = True):
|
||||
if pause:
|
||||
action = "action=pause&hash=%s" % hash
|
||||
action = 'action=pause&hash=%s' % hash
|
||||
else:
|
||||
action = "action=unpause&hash=%s" % hash
|
||||
action = 'action=unpause&hash=%s' % hash
|
||||
return self._request(action)
|
||||
|
||||
def stop_torrent(self, hash):
|
||||
action = "action=stop&hash=%s" % 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
|
||||
action = 'action=removedata&hash=%s' % hash
|
||||
else:
|
||||
action = "action=remove&hash=%s" % hash
|
||||
action = 'action=remove&hash=%s' % hash
|
||||
return self._request(action)
|
||||
|
||||
def get_status(self):
|
||||
action = "list=1"
|
||||
action = 'list=1'
|
||||
return self._request(action)
|
||||
|
||||
def get_settings(self):
|
||||
action = "action=getsettings"
|
||||
action = 'action=getsettings'
|
||||
settings_dict = {}
|
||||
try:
|
||||
utorrent_settings = json.loads(self._request(action))
|
||||
@@ -303,7 +315,7 @@ class uTorrentAPI(object):
|
||||
|
||||
#log.debug('uTorrent settings: %s', settings_dict)
|
||||
|
||||
except Exception, err:
|
||||
except Exception as err:
|
||||
log.error('Failed to get settings from uTorrent: %s', err)
|
||||
|
||||
return settings_dict
|
||||
@@ -319,5 +331,12 @@ class uTorrentAPI(object):
|
||||
return self._request(action)
|
||||
|
||||
def get_files(self, hash):
|
||||
action = "action=getfiles&hash=%s" % hash
|
||||
action = 'action=getfiles&hash=%s' % hash
|
||||
return self._request(action)
|
||||
|
||||
def get_build(self):
|
||||
data = self._request('')
|
||||
if not data:
|
||||
return False
|
||||
response = json.loads(data)
|
||||
return int(response.get('build'))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from axl.axel import Event
|
||||
from couchpotato.core.helpers.variable import mergeDicts, natcmp
|
||||
from couchpotato.core.helpers.variable import mergeDicts, natsortKey
|
||||
from couchpotato.core.logger import CPLog
|
||||
import threading
|
||||
import traceback
|
||||
@@ -7,6 +7,7 @@ import traceback
|
||||
log = CPLog(__name__)
|
||||
events = {}
|
||||
|
||||
|
||||
def runHandler(name, handler, *args, **kwargs):
|
||||
try:
|
||||
return handler(*args, **kwargs)
|
||||
@@ -14,6 +15,7 @@ def runHandler(name, handler, *args, **kwargs):
|
||||
from couchpotato.environment import Env
|
||||
log.error('Error in event "%s", that wasn\'t caught: %s%s', (name, traceback.format_exc(), Env.all() if not Env.get('dev') else ''))
|
||||
|
||||
|
||||
def addEvent(name, handler, priority = 100):
|
||||
|
||||
if not events.get(name):
|
||||
@@ -27,7 +29,7 @@ def addEvent(name, handler, priority = 100):
|
||||
has_parent = hasattr(handler, 'im_self')
|
||||
parent = None
|
||||
if has_parent:
|
||||
parent = handler.im_self
|
||||
parent = handler.__self__
|
||||
bc = hasattr(parent, 'beforeCall')
|
||||
if bc: parent.beforeCall(handler)
|
||||
|
||||
@@ -48,22 +50,19 @@ def addEvent(name, handler, priority = 100):
|
||||
'priority': priority,
|
||||
})
|
||||
|
||||
def removeEvent(name, handler):
|
||||
e = events[name]
|
||||
e -= handler
|
||||
|
||||
def fireEvent(name, *args, **kwargs):
|
||||
if not events.has_key(name): return
|
||||
if name not in events: return
|
||||
|
||||
#log.debug('Firing event %s', name)
|
||||
try:
|
||||
|
||||
options = {
|
||||
'is_after_event': False, # Fire after event
|
||||
'on_complete': False, # onComplete event
|
||||
'single': False, # Return single handler
|
||||
'merge': False, # Merge items
|
||||
'in_order': False, # Fire them in specific order, waits for the other to finish
|
||||
'is_after_event': False, # Fire after event
|
||||
'on_complete': False, # onComplete event
|
||||
'single': False, # Return single handler
|
||||
'merge': False, # Merge items
|
||||
'in_order': False, # Fire them in specific order, waits for the other to finish
|
||||
}
|
||||
|
||||
# Do options
|
||||
@@ -101,11 +100,14 @@ def fireEvent(name, *args, **kwargs):
|
||||
# Fire
|
||||
result = e(*args, **kwargs)
|
||||
|
||||
result_keys = result.keys()
|
||||
result_keys.sort(key = natsortKey)
|
||||
|
||||
if options['single'] and not options['merge']:
|
||||
results = None
|
||||
|
||||
# Loop over results, stop when first not None result is found.
|
||||
for r_key in sorted(result.iterkeys(), cmp = natcmp):
|
||||
for r_key in result_keys:
|
||||
r = result[r_key]
|
||||
if r[0] is True and r[1] is not None:
|
||||
results = r[1]
|
||||
@@ -117,7 +119,7 @@ def fireEvent(name, *args, **kwargs):
|
||||
|
||||
else:
|
||||
results = []
|
||||
for r_key in sorted(result.iterkeys(), cmp = natcmp):
|
||||
for r_key in result_keys:
|
||||
r = result[r_key]
|
||||
if r[0] == True and r[1]:
|
||||
results.append(r[1])
|
||||
@@ -160,18 +162,21 @@ def fireEvent(name, *args, **kwargs):
|
||||
except Exception:
|
||||
log.error('%s: %s', (name, traceback.format_exc()))
|
||||
|
||||
|
||||
def fireEventAsync(*args, **kwargs):
|
||||
try:
|
||||
t = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
return True
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
log.error('%s: %s', (args[0], e))
|
||||
|
||||
|
||||
def errorHandler(error):
|
||||
etype, value, tb = error
|
||||
log.error(''.join(traceback.format_exception(etype, value, tb)))
|
||||
|
||||
|
||||
def getEvent(name):
|
||||
return events[name]
|
||||
|
||||
@@ -5,29 +5,32 @@ import os
|
||||
import re
|
||||
import traceback
|
||||
import unicodedata
|
||||
import six
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
def toSafeString(original):
|
||||
valid_chars = "-_.() %s%s" % (ascii_letters, digits)
|
||||
cleanedFilename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
|
||||
valid_string = ''.join(c for c in cleanedFilename if c in valid_chars)
|
||||
cleaned_filename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
|
||||
valid_string = ''.join(c for c in cleaned_filename if c in valid_chars)
|
||||
return ' '.join(valid_string.split())
|
||||
|
||||
|
||||
def simplifyString(original):
|
||||
string = stripAccents(original.lower())
|
||||
string = toSafeString(' '.join(re.split('\W+', string)))
|
||||
split = re.split('\W+|_', string.lower())
|
||||
return toUnicode(' '.join(split))
|
||||
|
||||
|
||||
def toUnicode(original, *args):
|
||||
try:
|
||||
if isinstance(original, unicode):
|
||||
return original
|
||||
else:
|
||||
try:
|
||||
return unicode(original, *args)
|
||||
return six.text_type(original, *args)
|
||||
except:
|
||||
try:
|
||||
return ek(original, *args)
|
||||
@@ -38,19 +41,43 @@ def toUnicode(original, *args):
|
||||
ascii_text = str(original).encode('string_escape')
|
||||
return toUnicode(ascii_text)
|
||||
|
||||
|
||||
def ss(original, *args):
|
||||
|
||||
u_original = toUnicode(original, *args)
|
||||
try:
|
||||
from couchpotato.environment import Env
|
||||
return u_original.encode(Env.get('encoding'))
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
log.debug('Failed ss encoding char, force UTF8: %s', e)
|
||||
return u_original.encode('UTF-8')
|
||||
|
||||
|
||||
def sp(path, *args):
|
||||
|
||||
# Standardise encoding, normalise case, path and strip trailing '/' or '\'
|
||||
return os.path.normcase(os.path.normpath(ss(path, *args))).rstrip(os.path.sep)
|
||||
if not path or len(path) == 0:
|
||||
return path
|
||||
|
||||
# convert windows path (from remote box) to *nix path
|
||||
if os.path.sep == '/' and '\\' in path:
|
||||
path = '/' + path.replace(':', '').replace('\\', '/')
|
||||
|
||||
path = os.path.normpath(ss(path, *args))
|
||||
|
||||
# Remove any trailing path separators
|
||||
if path != os.path.sep:
|
||||
path = path.rstrip(os.path.sep)
|
||||
|
||||
# Add a trailing separator in case it is a root folder on windows (crashes guessit)
|
||||
if len(path) == 2 and path[1] == ':':
|
||||
path = path + os.path.sep
|
||||
|
||||
# Replace *NIX ambiguous '//' at the beginning of a path with '/' (crashes guessit)
|
||||
path = re.sub('^//', '/', path)
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def ek(original, *args):
|
||||
if isinstance(original, (str, unicode)):
|
||||
@@ -62,6 +89,7 @@ def ek(original, *args):
|
||||
|
||||
return original
|
||||
|
||||
|
||||
def isInt(value):
|
||||
try:
|
||||
int(value)
|
||||
@@ -69,14 +97,16 @@ def isInt(value):
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def stripAccents(s):
|
||||
return ''.join((c for c in unicodedata.normalize('NFD', toUnicode(s)) if unicodedata.category(c) != 'Mn'))
|
||||
|
||||
|
||||
def tryUrlencode(s):
|
||||
new = u''
|
||||
new = six.u('')
|
||||
if isinstance(s, dict):
|
||||
for key, value in s.iteritems():
|
||||
new += u'&%s=%s' % (key, tryUrlencode(value))
|
||||
for key, value in s.items():
|
||||
new += six.u('&%s=%s') % (key, tryUrlencode(value))
|
||||
|
||||
return new[1:]
|
||||
else:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import natcmp
|
||||
from couchpotato.core.helpers.variable import natsortKey
|
||||
from urllib import unquote
|
||||
import re
|
||||
|
||||
@@ -8,8 +8,13 @@ def getParams(params):
|
||||
|
||||
reg = re.compile('^[a-z0-9_\.]+$')
|
||||
|
||||
# Sort keys
|
||||
param_keys = params.keys()
|
||||
param_keys.sort(key = natsortKey)
|
||||
|
||||
temp = {}
|
||||
for param, value in sorted(params.iteritems()):
|
||||
for param in param_keys:
|
||||
value = params[param]
|
||||
|
||||
nest = re.split("([\[\]]+)", param)
|
||||
if len(nest) > 1:
|
||||
@@ -37,13 +42,17 @@ def getParams(params):
|
||||
|
||||
return dictToList(temp)
|
||||
|
||||
|
||||
def dictToList(params):
|
||||
|
||||
if type(params) is dict:
|
||||
new = {}
|
||||
for x, value in params.iteritems():
|
||||
for x, value in params.items():
|
||||
try:
|
||||
new_value = [dictToList(value[k]) for k in sorted(value.iterkeys(), cmp = natcmp)]
|
||||
convert = lambda text: int(text) if text.isdigit() else text.lower()
|
||||
alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
|
||||
sorted_keys = sorted(value.keys(), key = alphanum_key)
|
||||
new_value = [dictToList(value[k]) for k in sorted_keys]
|
||||
except:
|
||||
new_value = value
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import xml.etree.ElementTree as XMLTree
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class RSS(object):
|
||||
|
||||
def getTextElements(self, xml, path):
|
||||
@@ -46,6 +47,6 @@ class RSS(object):
|
||||
def getItems(self, data, path = 'channel/item'):
|
||||
try:
|
||||
return XMLTree.parse(data).findall(path)
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
log.error('Error parsing RSS. %s', e)
|
||||
return []
|
||||
|
||||
@@ -2,29 +2,38 @@ from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss
|
||||
from couchpotato.core.logger import CPLog
|
||||
import collections
|
||||
import hashlib
|
||||
import os.path
|
||||
import os
|
||||
import platform
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import sys
|
||||
import six
|
||||
from six.moves import map, zip, filter
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
def fnEscape(pattern):
|
||||
return pattern.replace('[', '[[').replace(']', '[]]').replace('[[', '[[]')
|
||||
|
||||
|
||||
def link(src, dst):
|
||||
if os.name == 'nt':
|
||||
import ctypes
|
||||
if ctypes.windll.kernel32.CreateHardLinkW(unicode(dst), unicode(src), 0) == 0: raise ctypes.WinError()
|
||||
if ctypes.windll.kernel32.CreateHardLinkW(six.text_type(dst), six.text_type(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()
|
||||
if ctypes.windll.kernel32.CreateSymbolicLinkW(six.text_type(dst), six.text_type(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
|
||||
@@ -34,6 +43,7 @@ def getUserDir():
|
||||
|
||||
return os.path.expanduser('~')
|
||||
|
||||
|
||||
def getDownloadDir():
|
||||
user_dir = getUserDir()
|
||||
|
||||
@@ -46,6 +56,7 @@ def getDownloadDir():
|
||||
|
||||
return user_dir
|
||||
|
||||
|
||||
def getDataDir():
|
||||
|
||||
# Windows
|
||||
@@ -65,8 +76,10 @@ def getDataDir():
|
||||
# Linux
|
||||
return os.path.join(user_dir, '.couchpotato')
|
||||
|
||||
def isDict(object):
|
||||
return isinstance(object, dict)
|
||||
|
||||
def isDict(obj):
|
||||
return isinstance(obj, dict)
|
||||
|
||||
|
||||
def mergeDicts(a, b, prepend_list = False):
|
||||
assert isDict(a), isDict(b)
|
||||
@@ -88,6 +101,7 @@ def mergeDicts(a, b, prepend_list = False):
|
||||
current_dst[key] = current_src[key]
|
||||
return dst
|
||||
|
||||
|
||||
def removeListDuplicates(seq):
|
||||
checked = []
|
||||
for e in seq:
|
||||
@@ -95,35 +109,73 @@ def removeListDuplicates(seq):
|
||||
checked.append(e)
|
||||
return checked
|
||||
|
||||
|
||||
def flattenList(l):
|
||||
if isinstance(l, list):
|
||||
return sum(map(flattenList, l))
|
||||
else:
|
||||
return l
|
||||
|
||||
|
||||
def md5(text):
|
||||
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:]
|
||||
|
||||
def cleanHost(host):
|
||||
if not host.startswith(('http://', 'https://')):
|
||||
host = 'http://' + host
|
||||
|
||||
host = host.rstrip('/')
|
||||
host += '/'
|
||||
def cleanHost(host, protocol = True, ssl = False, username = None, password = None):
|
||||
"""Return a cleaned up host with given url options set
|
||||
|
||||
Changes protocol to https if ssl is set to True and http if ssl is set to false.
|
||||
>>> cleanHost("localhost:80", ssl=True)
|
||||
'https://localhost:80/'
|
||||
>>> cleanHost("localhost:80", ssl=False)
|
||||
'http://localhost:80/'
|
||||
|
||||
Username and password is managed with the username and password variables
|
||||
>>> cleanHost("localhost:80", username="user", password="passwd")
|
||||
'http://user:passwd@localhost:80/'
|
||||
|
||||
Output without scheme (protocol) can be forced with protocol=False
|
||||
>>> cleanHost("localhost:80", protocol=False)
|
||||
'localhost:80'
|
||||
"""
|
||||
|
||||
if not '://' in host and protocol:
|
||||
host = ('https://' if ssl else 'http://') + host
|
||||
|
||||
if not protocol:
|
||||
host = host.split('://', 1)[-1]
|
||||
|
||||
if protocol and username and password:
|
||||
try:
|
||||
auth = re.findall('^(?:.+?//)(.+?):(.+?)@(?:.+)$', host)
|
||||
if auth:
|
||||
log.error('Cleanhost error: auth already defined in url: %s, please remove BasicAuth from url.', host)
|
||||
else:
|
||||
host = host.replace('://', '://%s:%s@' % (username, password), 1)
|
||||
except:
|
||||
pass
|
||||
|
||||
host = host.rstrip('/ ')
|
||||
if protocol:
|
||||
host += '/'
|
||||
|
||||
return host
|
||||
|
||||
|
||||
def getImdb(txt, check_inside = False, multiple = False):
|
||||
|
||||
if not check_inside:
|
||||
@@ -140,7 +192,7 @@ def getImdb(txt, check_inside = False, multiple = False):
|
||||
ids = re.findall('(tt\d{4,7})', txt)
|
||||
|
||||
if multiple:
|
||||
return list(set(['tt%07d' % tryInt(x[2:]) for x in ids])) if len(ids) > 0 else []
|
||||
return removeDuplicate(['tt%07d' % tryInt(x[2:]) for x in ids]) if len(ids) > 0 else []
|
||||
|
||||
return 'tt%07d' % tryInt(ids[0][2:])
|
||||
except IndexError:
|
||||
@@ -148,10 +200,12 @@ def getImdb(txt, check_inside = False, multiple = False):
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def tryInt(s, default = 0):
|
||||
try: return int(s)
|
||||
except: return default
|
||||
|
||||
|
||||
def tryFloat(s):
|
||||
try:
|
||||
if isinstance(s, str):
|
||||
@@ -160,17 +214,17 @@ def tryFloat(s):
|
||||
return float(s)
|
||||
except: return 0
|
||||
|
||||
def natsortKey(s):
|
||||
return map(tryInt, re.findall(r'(\d+|\D+)', s))
|
||||
def natsortKey(string_):
|
||||
"""See http://www.codinghorror.com/blog/archives/001018.html"""
|
||||
return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string_)]
|
||||
|
||||
def natcmp(a, b):
|
||||
return cmp(natsortKey(a), natsortKey(b))
|
||||
|
||||
def toIterable(value):
|
||||
if isinstance(value, collections.Iterable):
|
||||
return value
|
||||
return [value]
|
||||
|
||||
|
||||
def getTitle(library_dict):
|
||||
try:
|
||||
try:
|
||||
@@ -193,6 +247,7 @@ def getTitle(library_dict):
|
||||
log.error('Could not get title for library item: %s', library_dict)
|
||||
return None
|
||||
|
||||
|
||||
def possibleTitles(raw_title):
|
||||
|
||||
titles = [
|
||||
@@ -205,14 +260,42 @@ def possibleTitles(raw_title):
|
||||
new_title = raw_title.replace('&', 'and')
|
||||
titles.append(simplifyString(new_title))
|
||||
|
||||
return list(set(titles))
|
||||
return removeDuplicate(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 = ',', clean = True):
|
||||
list = [x.strip() for x in str.split(split_on)] if str else []
|
||||
return filter(None, list) if clean else list
|
||||
l = [x.strip() for x in str.split(split_on)] if str else []
|
||||
return removeEmpty(l) if clean else l
|
||||
|
||||
|
||||
def removeEmpty(l):
|
||||
return list(filter(None, l))
|
||||
|
||||
|
||||
def removeDuplicate(l):
|
||||
seen = set()
|
||||
return [x for x in l if x not in seen and not seen.add(x)]
|
||||
|
||||
|
||||
def dictIsSubset(a, b):
|
||||
return all([k in b and b[k] == v for k, v in a.items()])
|
||||
|
||||
|
||||
def isSubFolder(sub_folder, base_folder):
|
||||
# Returns True if sub_folder is the same as or inside base_folder
|
||||
return base_folder and sub_folder and ss(os.path.normpath(base_folder).rstrip(os.path.sep) + os.path.sep) in ss(os.path.normpath(sub_folder).rstrip(os.path.sep) + os.path.sep)
|
||||
|
||||
# From SABNZBD
|
||||
re_password = [re.compile(r'([^/\\]+)[/\\](.+)'), re.compile(r'(.+){{([^{}]+)}}$'), re.compile(r'(.+)\s+password\s*=\s*(.+)$', re.I)]
|
||||
def scanForPassword(name):
|
||||
m = None
|
||||
for reg in re_password:
|
||||
m = reg.search(name)
|
||||
if m: break
|
||||
|
||||
if m:
|
||||
return m.group(1).strip('. '), m.group(2).strip()
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from importlib import import_module
|
||||
from importhelper import import_module
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
import six
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -37,7 +38,7 @@ class Loader(object):
|
||||
self.paths['custom_plugins'] = (30, '', custom_plugin_dir)
|
||||
|
||||
# Loop over all paths and add to module list
|
||||
for plugin_type, plugin_tuple in self.paths.iteritems():
|
||||
for plugin_type, plugin_tuple in self.paths.items():
|
||||
priority, module, dir_name = plugin_tuple
|
||||
self.addFromDir(plugin_type, priority, module, dir_name)
|
||||
|
||||
@@ -45,7 +46,7 @@ class Loader(object):
|
||||
did_save = 0
|
||||
|
||||
for priority in sorted(self.modules):
|
||||
for module_name, plugin in sorted(self.modules[priority].iteritems()):
|
||||
for module_name, plugin in sorted(self.modules[priority].items()):
|
||||
|
||||
# Load module
|
||||
try:
|
||||
@@ -81,7 +82,7 @@ class Loader(object):
|
||||
for filename in os.listdir(root_path):
|
||||
path = os.path.join(root_path, filename)
|
||||
if os.path.isdir(path) and filename[:2] != '__':
|
||||
if u'__init__.py' in os.listdir(path):
|
||||
if six.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)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
|
||||
class CPLog(object):
|
||||
|
||||
context = ''
|
||||
@@ -37,7 +38,7 @@ class CPLog(object):
|
||||
def safeMessage(self, msg, replace_tuple = ()):
|
||||
|
||||
from couchpotato.environment import Env
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.encoding import ss, toUnicode
|
||||
|
||||
msg = ss(msg)
|
||||
|
||||
@@ -49,8 +50,8 @@ class CPLog(object):
|
||||
msg = msg % tuple([ss(x) for x in list(replace_tuple)])
|
||||
else:
|
||||
msg = msg % ss(replace_tuple)
|
||||
except Exception, e:
|
||||
self.logger.error(u'Failed encoding stuff to log "%s": %s' % (msg, e))
|
||||
except Exception as e:
|
||||
self.logger.error('Failed encoding stuff to log "%s": %s' % (msg, e))
|
||||
|
||||
if not Env.get('dev'):
|
||||
|
||||
@@ -66,4 +67,4 @@ class CPLog(object):
|
||||
except:
|
||||
pass
|
||||
|
||||
return msg
|
||||
return toUnicode(msg)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
from couchpotato import get_session
|
||||
import traceback
|
||||
from couchpotato import get_session, CPLog
|
||||
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Media
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class MediaBase(Plugin):
|
||||
|
||||
@@ -10,8 +13,8 @@ class MediaBase(Plugin):
|
||||
|
||||
default_dict = {
|
||||
'profile': {'types': {'quality': {}}},
|
||||
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'releases': {'status': {}, 'quality': {}, 'files': {}, 'info': {}},
|
||||
'library': {'titles': {}, 'files': {}},
|
||||
'files': {},
|
||||
'status': {},
|
||||
'category': {},
|
||||
@@ -26,19 +29,33 @@ class MediaBase(Plugin):
|
||||
def createOnComplete(self, id):
|
||||
|
||||
def onComplete():
|
||||
db = get_session()
|
||||
media = db.query(Media).filter_by(id = id).first()
|
||||
fireEventAsync('%s.searcher.single' % media.type, media.to_dict(self.default_dict), on_complete = self.createNotifyFront(id))
|
||||
db.expire_all()
|
||||
try:
|
||||
db = get_session()
|
||||
media = db.query(Media).filter_by(id = id).first()
|
||||
media_dict = media.to_dict(self.default_dict)
|
||||
event_name = '%s.searcher.single' % media.type
|
||||
|
||||
fireEvent(event_name, media_dict, on_complete = self.createNotifyFront(id))
|
||||
except:
|
||||
log.error('Failed creating onComplete: %s', traceback.format_exc())
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
return onComplete
|
||||
|
||||
def createNotifyFront(self, media_id):
|
||||
|
||||
def notifyFront():
|
||||
db = get_session()
|
||||
media = db.query(Media).filter_by(id = media_id).first()
|
||||
fireEvent('notify.frontend', type = '%s.update.%s' % (media.type, media.id), data = media.to_dict(self.default_dict))
|
||||
db.expire_all()
|
||||
try:
|
||||
db = get_session()
|
||||
media = db.query(Media).filter_by(id = media_id).first()
|
||||
media_dict = media.to_dict(self.default_dict)
|
||||
event_name = '%s.update' % media.type
|
||||
|
||||
fireEvent('notify.frontend', type = event_name, data = media_dict)
|
||||
except:
|
||||
log.error('Failed creating onComplete: %s', traceback.format_exc())
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
return notifyFront
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import MediaPlugin
|
||||
|
||||
|
||||
def start():
|
||||
return MediaPlugin()
|
||||
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
from couchpotato import get_session
|
||||
import traceback
|
||||
from couchpotato import get_session, tryInt
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import mergeDicts, splitString, getImdb, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media import MediaBase
|
||||
from couchpotato.core.settings.model import Media
|
||||
from couchpotato.core.settings.model import Library, LibraryTitle, Release, \
|
||||
Media
|
||||
from sqlalchemy.orm import joinedload_all
|
||||
from sqlalchemy.sql.expression import or_, asc, not_, desc
|
||||
from string import ascii_lowercase
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -20,30 +26,455 @@ class MediaPlugin(MediaBase):
|
||||
}
|
||||
})
|
||||
|
||||
addEvent('app.load', self.addSingleRefresh)
|
||||
addApiView('media.list', self.listView, docs = {
|
||||
'desc': 'List media',
|
||||
'params': {
|
||||
'type': {'type': 'string', 'desc': 'Media type to filter on.'},
|
||||
'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'},
|
||||
'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'},
|
||||
'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'},
|
||||
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
|
||||
'search': {'desc': 'Search movie title'},
|
||||
},
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'success': True,
|
||||
'empty': bool, any movies returned or not,
|
||||
'media': array, media found,
|
||||
}"""}
|
||||
})
|
||||
|
||||
addApiView('media.get', self.getView, docs = {
|
||||
'desc': 'Get media by id',
|
||||
'params': {
|
||||
'id': {'desc': 'The id of the media'},
|
||||
}
|
||||
})
|
||||
|
||||
addApiView('media.delete', self.deleteView, docs = {
|
||||
'desc': 'Delete a media from the wanted list',
|
||||
'params': {
|
||||
'id': {'desc': 'Media ID(s) you want to delete.', 'type': 'int (comma separated)'},
|
||||
'delete_from': {'desc': 'Delete media from this page', 'type': 'string: all (default), wanted, manage'},
|
||||
}
|
||||
})
|
||||
|
||||
addApiView('media.available_chars', self.charView)
|
||||
|
||||
addEvent('app.load', self.addSingleRefreshView)
|
||||
addEvent('app.load', self.addSingleListView)
|
||||
addEvent('app.load', self.addSingleCharView)
|
||||
addEvent('app.load', self.addSingleDeleteView)
|
||||
|
||||
addEvent('media.get', self.get)
|
||||
addEvent('media.list', self.list)
|
||||
addEvent('media.delete', self.delete)
|
||||
addEvent('media.restatus', self.restatus)
|
||||
|
||||
def refresh(self, id = '', **kwargs):
|
||||
db = get_session()
|
||||
handlers = []
|
||||
ids = splitString(id)
|
||||
|
||||
for x in splitString(id):
|
||||
media = db.query(Media).filter_by(id = x).first()
|
||||
for x in ids:
|
||||
|
||||
if media:
|
||||
# Get current selected title
|
||||
default_title = ''
|
||||
for title in media.library.titles:
|
||||
if title.default: default_title = title.title
|
||||
refresh_handler = self.createRefreshHandler(x)
|
||||
if refresh_handler:
|
||||
handlers.append(refresh_handler)
|
||||
|
||||
fireEvent('notify.frontend', type = '%s.busy.%s' % (media.type, x), data = True)
|
||||
fireEventAsync('library.update.%s' % media.type, identifier = media.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
|
||||
|
||||
db.expire_all()
|
||||
fireEvent('notify.frontend', type = 'media.busy', data = {'id': [tryInt(x) for x in ids]})
|
||||
fireEventAsync('schedule.queue', handlers = handlers)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
}
|
||||
|
||||
def addSingleRefresh(self):
|
||||
def createRefreshHandler(self, id):
|
||||
db = get_session()
|
||||
|
||||
media = db.query(Media).filter_by(id = id).first()
|
||||
|
||||
if media:
|
||||
|
||||
default_title = getTitle(media.library)
|
||||
identifier = media.library.identifier
|
||||
event = 'library.update.%s' % media.type
|
||||
|
||||
def handler():
|
||||
fireEvent(event, identifier = identifier, default_title = default_title, on_complete = self.createOnComplete(id))
|
||||
|
||||
if handler:
|
||||
return handler
|
||||
|
||||
def addSingleRefreshView(self):
|
||||
|
||||
for media_type in fireEvent('media.types', merge = True):
|
||||
addApiView('%s.refresh' % media_type, self.refresh)
|
||||
|
||||
def get(self, media_id):
|
||||
|
||||
db = get_session()
|
||||
|
||||
imdb_id = getImdb(str(media_id))
|
||||
|
||||
if imdb_id:
|
||||
m = db.query(Media).filter(Media.library.has(identifier = imdb_id)).first()
|
||||
else:
|
||||
m = db.query(Media).filter_by(id = media_id).first()
|
||||
|
||||
results = None
|
||||
if m:
|
||||
results = m.to_dict(self.default_dict)
|
||||
|
||||
return results
|
||||
|
||||
def getView(self, id = None, **kwargs):
|
||||
|
||||
media = self.get(id) if id else None
|
||||
|
||||
return {
|
||||
'success': media is not None,
|
||||
'media': media,
|
||||
}
|
||||
|
||||
def list(self, types = None, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
|
||||
|
||||
db = get_session()
|
||||
|
||||
# Make a list from string
|
||||
if status and not isinstance(status, (list, tuple)):
|
||||
status = [status]
|
||||
if release_status and not isinstance(release_status, (list, tuple)):
|
||||
release_status = [release_status]
|
||||
if types and not isinstance(types, (list, tuple)):
|
||||
types = [types]
|
||||
|
||||
# query movie ids
|
||||
q = db.query(Media) \
|
||||
.with_entities(Media.id) \
|
||||
.group_by(Media.id)
|
||||
|
||||
# Filter on movie status
|
||||
if status and len(status) > 0:
|
||||
statuses = fireEvent('status.get', status, single = len(status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Media.status_id.in_(statuses))
|
||||
|
||||
# Filter on release status
|
||||
if release_status and len(release_status) > 0:
|
||||
q = q.join(Media.releases)
|
||||
|
||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Release.status_id.in_(statuses))
|
||||
|
||||
# Filter on type
|
||||
if types and len(types) > 0:
|
||||
try: q = q.filter(Media.type.in_(types))
|
||||
except: pass
|
||||
|
||||
# Only join when searching / ordering
|
||||
if starts_with or search or order != 'release_order':
|
||||
q = q.join(Media.library, Library.titles) \
|
||||
.filter(LibraryTitle.default == True)
|
||||
|
||||
# Add search filters
|
||||
filter_or = []
|
||||
if starts_with:
|
||||
starts_with = toUnicode(starts_with.lower())
|
||||
if starts_with in ascii_lowercase:
|
||||
filter_or.append(LibraryTitle.simple_title.startswith(starts_with))
|
||||
else:
|
||||
ignore = []
|
||||
for letter in ascii_lowercase:
|
||||
ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter)))
|
||||
filter_or.append(not_(or_(*ignore)))
|
||||
|
||||
if search:
|
||||
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
|
||||
|
||||
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))
|
||||
|
||||
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 media_ids in sorted order
|
||||
media_ids = [m.id for m in q.all()]
|
||||
|
||||
# List release statuses
|
||||
releases = db.query(Release) \
|
||||
.filter(Release.movie_id.in_(media_ids)) \
|
||||
.all()
|
||||
|
||||
release_statuses = dict((m, set()) for m in media_ids)
|
||||
releases_count = dict((m, 0) for m in media_ids)
|
||||
for release in releases:
|
||||
release_statuses[release.movie_id].add('%d,%d' % (release.status_id, release.quality_id))
|
||||
releases_count[release.movie_id] += 1
|
||||
|
||||
# Get main movie data
|
||||
q2 = db.query(Media) \
|
||||
.options(joinedload_all('library.titles')) \
|
||||
.options(joinedload_all('library.files')) \
|
||||
.options(joinedload_all('status')) \
|
||||
.options(joinedload_all('files'))
|
||||
|
||||
q2 = q2.filter(Media.id.in_(media_ids))
|
||||
|
||||
results = q2.all()
|
||||
|
||||
# Create dict by movie id
|
||||
movie_dict = {}
|
||||
for movie in results:
|
||||
movie_dict[movie.id] = movie
|
||||
|
||||
# List movies based on media_ids order
|
||||
movies = []
|
||||
for media_id in media_ids:
|
||||
|
||||
releases = []
|
||||
for r in release_statuses.get(media_id):
|
||||
x = splitString(r)
|
||||
releases.append({'status_id': x[0], 'quality_id': x[1]})
|
||||
|
||||
# Merge releases with movie dict
|
||||
movies.append(mergeDicts(movie_dict[media_id].to_dict({
|
||||
'library': {'titles': {}, 'files': {}},
|
||||
'files': {},
|
||||
}), {
|
||||
'releases': releases,
|
||||
'releases_count': releases_count.get(media_id),
|
||||
}))
|
||||
|
||||
return total_count, movies
|
||||
|
||||
def listView(self, **kwargs):
|
||||
|
||||
types = splitString(kwargs.get('types'))
|
||||
status = splitString(kwargs.get('status'))
|
||||
release_status = splitString(kwargs.get('release_status'))
|
||||
limit_offset = kwargs.get('limit_offset')
|
||||
starts_with = kwargs.get('starts_with')
|
||||
search = kwargs.get('search')
|
||||
order = kwargs.get('order')
|
||||
|
||||
total_movies, movies = self.list(
|
||||
types = types,
|
||||
status = status,
|
||||
release_status = release_status,
|
||||
limit_offset = limit_offset,
|
||||
starts_with = starts_with,
|
||||
search = search,
|
||||
order = order
|
||||
)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'total': total_movies,
|
||||
'movies': movies,
|
||||
}
|
||||
|
||||
def addSingleListView(self):
|
||||
|
||||
for media_type in fireEvent('media.types', merge = True):
|
||||
def tempList(*args, **kwargs):
|
||||
return self.listView(types = media_type, *args, **kwargs)
|
||||
addApiView('%s.list' % media_type, tempList)
|
||||
|
||||
def availableChars(self, types = None, status = None, release_status = None):
|
||||
|
||||
types = types or []
|
||||
status = status or []
|
||||
release_status = release_status or []
|
||||
|
||||
db = get_session()
|
||||
|
||||
# Make a list from string
|
||||
if not isinstance(status, (list, tuple)):
|
||||
status = [status]
|
||||
if release_status and not isinstance(release_status, (list, tuple)):
|
||||
release_status = [release_status]
|
||||
if types and not isinstance(types, (list, tuple)):
|
||||
types = [types]
|
||||
|
||||
q = db.query(Media)
|
||||
|
||||
# Filter on movie status
|
||||
if status and len(status) > 0:
|
||||
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Media.status_id.in_(statuses))
|
||||
|
||||
# Filter on release status
|
||||
if release_status and len(release_status) > 0:
|
||||
|
||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.join(Media.releases) \
|
||||
.filter(Release.status_id.in_(statuses))
|
||||
|
||||
# Filter on type
|
||||
if types and len(types) > 0:
|
||||
try: q = q.filter(Media.type.in_(types))
|
||||
except: pass
|
||||
|
||||
q = q.join(Library, LibraryTitle) \
|
||||
.with_entities(LibraryTitle.simple_title) \
|
||||
.filter(LibraryTitle.default == True)
|
||||
|
||||
titles = q.all()
|
||||
|
||||
chars = set()
|
||||
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
|
||||
|
||||
return ''.join(sorted(chars))
|
||||
|
||||
def charView(self, **kwargs):
|
||||
|
||||
type = splitString(kwargs.get('type', 'movie'))
|
||||
status = splitString(kwargs.get('status', None))
|
||||
release_status = splitString(kwargs.get('release_status', None))
|
||||
chars = self.availableChars(type, status, release_status)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(chars) == 0,
|
||||
'chars': chars,
|
||||
}
|
||||
|
||||
def addSingleCharView(self):
|
||||
|
||||
for media_type in fireEvent('media.types', merge = True):
|
||||
def tempChar(*args, **kwargs):
|
||||
return self.charView(types = media_type, *args, **kwargs)
|
||||
addApiView('%s.available_chars' % media_type, tempChar)
|
||||
|
||||
def delete(self, media_id, delete_from = None):
|
||||
|
||||
try:
|
||||
db = get_session()
|
||||
|
||||
media = db.query(Media).filter_by(id = media_id).first()
|
||||
if media:
|
||||
deleted = False
|
||||
if delete_from == 'all':
|
||||
db.delete(media)
|
||||
db.commit()
|
||||
deleted = True
|
||||
else:
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
|
||||
total_releases = len(media.releases)
|
||||
total_deleted = 0
|
||||
new_movie_status = None
|
||||
for release in media.releases:
|
||||
if delete_from in ['wanted', 'snatched', 'late']:
|
||||
if release.status_id != done_status.get('id'):
|
||||
db.delete(release)
|
||||
total_deleted += 1
|
||||
new_movie_status = 'done'
|
||||
elif delete_from == 'manage':
|
||||
if release.status_id == done_status.get('id'):
|
||||
db.delete(release)
|
||||
total_deleted += 1
|
||||
new_movie_status = 'active'
|
||||
db.commit()
|
||||
|
||||
if total_releases == total_deleted:
|
||||
db.delete(media)
|
||||
db.commit()
|
||||
deleted = True
|
||||
elif new_movie_status:
|
||||
new_status = fireEvent('status.get', new_movie_status, single = True)
|
||||
media.profile_id = None
|
||||
media.status_id = new_status.get('id')
|
||||
db.commit()
|
||||
else:
|
||||
fireEvent('media.restatus', media.id, single = True)
|
||||
|
||||
if deleted:
|
||||
fireEvent('notify.frontend', type = 'movie.deleted', data = media.to_dict())
|
||||
except:
|
||||
log.error('Failed deleting media: %s', traceback.format_exc())
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
return True
|
||||
|
||||
def deleteView(self, id = '', **kwargs):
|
||||
|
||||
ids = splitString(id)
|
||||
for media_id in ids:
|
||||
self.delete(media_id, delete_from = kwargs.get('delete_from', 'all'))
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
}
|
||||
|
||||
def addSingleDeleteView(self):
|
||||
|
||||
for media_type in fireEvent('media.types', merge = True):
|
||||
def tempDelete(*args, **kwargs):
|
||||
return self.deleteView(types = media_type, *args, **kwargs)
|
||||
addApiView('%s.delete' % media_type, tempDelete)
|
||||
|
||||
def restatus(self, media_id):
|
||||
|
||||
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
|
||||
|
||||
try:
|
||||
db = get_session()
|
||||
|
||||
m = db.query(Media).filter_by(id = media_id).first()
|
||||
if not m or len(m.library.titles) == 0:
|
||||
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
|
||||
return False
|
||||
|
||||
log.debug('Changing status for %s', m.library.titles[0].title)
|
||||
if not m.profile:
|
||||
m.status_id = done_status.get('id')
|
||||
else:
|
||||
move_to_wanted = True
|
||||
|
||||
for t in m.profile.types:
|
||||
for release in m.releases:
|
||||
if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish):
|
||||
move_to_wanted = False
|
||||
|
||||
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
|
||||
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
except:
|
||||
log.error('Failed restatus: %s', traceback.format_exc())
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Search
|
||||
|
||||
|
||||
def start():
|
||||
return Search()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Searcher
|
||||
|
||||
|
||||
def start():
|
||||
return Searcher()
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ class SearcherBase(Plugin):
|
||||
def __init__(self):
|
||||
super(SearcherBase, self).__init__()
|
||||
|
||||
|
||||
addEvent('searcher.progress', self.getProgress)
|
||||
addEvent('%s.searcher.progress' % self.getType(), self.getProgress)
|
||||
|
||||
@@ -26,9 +25,8 @@ class SearcherBase(Plugin):
|
||||
_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'))
|
||||
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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import simplifyString
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.helpers.variable import splitString, removeEmpty, removeDuplicate
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.searcher.base import SearcherBase
|
||||
import datetime
|
||||
@@ -107,10 +107,10 @@ class Searcher(SearcherBase):
|
||||
# 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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -150,12 +150,12 @@ class Searcher(SearcherBase):
|
||||
try: check_names.append(max(re.findall(r'[^[]*\[([^]]*)\]', check_name), key = len).strip())
|
||||
except: pass
|
||||
|
||||
for check_name in list(set(check_names)):
|
||||
for check_name in removeDuplicate(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)))
|
||||
check_words = removeEmpty(re.split('\W+', check_movie.get('name', '')))
|
||||
movie_words = removeEmpty(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
|
||||
@@ -173,7 +173,7 @@ class Searcher(SearcherBase):
|
||||
|
||||
# Make sure it has required words
|
||||
required_words = splitString(self.conf('required_words', section = 'searcher').lower())
|
||||
try: required_words = list(set(required_words + splitString(media['category']['required'].lower())))
|
||||
try: required_words = removeDuplicate(required_words + splitString(media['category']['required'].lower()))
|
||||
except: pass
|
||||
|
||||
req_match = 0
|
||||
@@ -187,7 +187,7 @@ class Searcher(SearcherBase):
|
||||
|
||||
# Ignore releases
|
||||
ignored_words = splitString(self.conf('ignored_words', section = 'searcher').lower())
|
||||
try: ignored_words = list(set(ignored_words + splitString(media['category']['ignored'].lower())))
|
||||
try: ignored_words = removeDuplicate(ignored_words + splitString(media['category']['ignored'].lower()))
|
||||
except: pass
|
||||
|
||||
ignored_match = 0
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import MovieBase
|
||||
|
||||
|
||||
def start():
|
||||
return MovieBase()
|
||||
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import traceback
|
||||
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
|
||||
from couchpotato.core.helpers.variable import getImdb, splitString, tryInt, \
|
||||
mergeDicts
|
||||
from couchpotato.core.helpers.variable import splitString, tryInt, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media.movie import MovieTypeBase
|
||||
from couchpotato.core.settings.model import Library, LibraryTitle, Media, \
|
||||
Release
|
||||
from sqlalchemy.orm import joinedload_all
|
||||
from sqlalchemy.sql.expression import or_, asc, not_, desc
|
||||
from string import ascii_lowercase
|
||||
from couchpotato.core.settings.model import Media
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -26,33 +22,12 @@ class MovieBase(MovieTypeBase):
|
||||
super(MovieBase, self).__init__()
|
||||
self.initType()
|
||||
|
||||
addApiView('movie.list', self.listView, docs = {
|
||||
'desc': 'List movies in wanted list',
|
||||
'params': {
|
||||
'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'},
|
||||
'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'},
|
||||
'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'},
|
||||
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
|
||||
'search': {'desc': 'Search movie title'},
|
||||
},
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'success': True,
|
||||
'empty': bool, any movies returned or not,
|
||||
'movies': array, movies found,
|
||||
}"""}
|
||||
})
|
||||
addApiView('movie.get', self.getView, docs = {
|
||||
'desc': 'Get a movie by id',
|
||||
'params': {
|
||||
'id': {'desc': 'The id of the movie'},
|
||||
}
|
||||
})
|
||||
addApiView('movie.available_chars', self.charView)
|
||||
addApiView('movie.add', self.addView, docs = {
|
||||
'desc': 'Add new movie to the wanted list',
|
||||
'params': {
|
||||
'identifier': {'desc': 'IMDB id of the movie your want to add.'},
|
||||
'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'},
|
||||
'category_id': {'desc': 'ID of category you want the add the movie in. If empty will use no category.'},
|
||||
'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'},
|
||||
}
|
||||
})
|
||||
@@ -61,258 +36,12 @@ class MovieBase(MovieTypeBase):
|
||||
'params': {
|
||||
'id': {'desc': 'Movie ID(s) you want to edit.', 'type': 'int (comma separated)'},
|
||||
'profile_id': {'desc': 'ID of quality profile you want the edit the movie to.'},
|
||||
'category_id': {'desc': 'ID of category you want the add the movie in. If empty will use no category.'},
|
||||
'default_title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'},
|
||||
}
|
||||
})
|
||||
addApiView('movie.delete', self.deleteView, docs = {
|
||||
'desc': 'Delete a movie from the wanted list',
|
||||
'params': {
|
||||
'id': {'desc': 'Movie ID(s) you want to delete.', 'type': 'int (comma separated)'},
|
||||
'delete_from': {'desc': 'Delete movie from this page', 'type': 'string: all (default), wanted, manage'},
|
||||
}
|
||||
})
|
||||
|
||||
addEvent('movie.add', self.add)
|
||||
addEvent('movie.delete', self.delete)
|
||||
addEvent('movie.get', self.get)
|
||||
addEvent('movie.list', self.list)
|
||||
addEvent('movie.restatus', self.restatus)
|
||||
|
||||
def getView(self, id = None, **kwargs):
|
||||
|
||||
movie = self.get(id) if id else None
|
||||
|
||||
return {
|
||||
'success': movie is not None,
|
||||
'movie': movie,
|
||||
}
|
||||
|
||||
def get(self, movie_id):
|
||||
|
||||
db = get_session()
|
||||
|
||||
imdb_id = getImdb(str(movie_id))
|
||||
|
||||
if imdb_id:
|
||||
m = db.query(Media).filter(Media.library.has(identifier = imdb_id)).first()
|
||||
else:
|
||||
m = db.query(Media).filter_by(id = movie_id).first()
|
||||
|
||||
results = None
|
||||
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):
|
||||
|
||||
db = get_session()
|
||||
|
||||
# Make a list from string
|
||||
if status and not isinstance(status, (list, tuple)):
|
||||
status = [status]
|
||||
if release_status and not isinstance(release_status, (list, tuple)):
|
||||
release_status = [release_status]
|
||||
|
||||
# query movie ids
|
||||
q = db.query(Media) \
|
||||
.with_entities(Media.id) \
|
||||
.group_by(Media.id)
|
||||
|
||||
# Filter on movie status
|
||||
if status and len(status) > 0:
|
||||
statuses = fireEvent('status.get', status, single = len(status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Media.status_id.in_(statuses))
|
||||
|
||||
# Filter on release status
|
||||
if release_status and len(release_status) > 0:
|
||||
q = q.join(Media.releases)
|
||||
|
||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Release.status_id.in_(statuses))
|
||||
|
||||
# Only join when searching / ordering
|
||||
if starts_with or search or order != 'release_order':
|
||||
q = q.join(Media.library, Library.titles) \
|
||||
.filter(LibraryTitle.default == True)
|
||||
|
||||
# Add search filters
|
||||
filter_or = []
|
||||
if starts_with:
|
||||
starts_with = toUnicode(starts_with.lower())
|
||||
if starts_with in ascii_lowercase:
|
||||
filter_or.append(LibraryTitle.simple_title.startswith(starts_with))
|
||||
else:
|
||||
ignore = []
|
||||
for letter in ascii_lowercase:
|
||||
ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter)))
|
||||
filter_or.append(not_(or_(*ignore)))
|
||||
|
||||
if search:
|
||||
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
|
||||
|
||||
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))
|
||||
|
||||
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(Media) \
|
||||
.options(joinedload_all('library.titles')) \
|
||||
.options(joinedload_all('library.files')) \
|
||||
.options(joinedload_all('status')) \
|
||||
.options(joinedload_all('files'))
|
||||
|
||||
q2 = q2.filter(Media.id.in_(movie_ids))
|
||||
|
||||
results = q2.all()
|
||||
|
||||
# Create dict by movie id
|
||||
movie_dict = {}
|
||||
for movie in results:
|
||||
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': {},
|
||||
}), {
|
||||
'releases': releases,
|
||||
'releases_count': releases_count.get(movie_id),
|
||||
}))
|
||||
|
||||
db.expire_all()
|
||||
return total_count, movies
|
||||
|
||||
def availableChars(self, status = None, release_status = None):
|
||||
|
||||
status = status or []
|
||||
release_status = release_status or []
|
||||
|
||||
db = get_session()
|
||||
|
||||
# Make a list from string
|
||||
if not isinstance(status, (list, tuple)):
|
||||
status = [status]
|
||||
if release_status and not isinstance(release_status, (list, tuple)):
|
||||
release_status = [release_status]
|
||||
|
||||
q = db.query(Media)
|
||||
|
||||
# Filter on movie status
|
||||
if status and len(status) > 0:
|
||||
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Media.status_id.in_(statuses))
|
||||
|
||||
# Filter on release status
|
||||
if release_status and len(release_status) > 0:
|
||||
|
||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.join(Media.releases) \
|
||||
.filter(Release.status_id.in_(statuses))
|
||||
|
||||
q = q.join(Library, LibraryTitle) \
|
||||
.with_entities(LibraryTitle.simple_title) \
|
||||
.filter(LibraryTitle.default == True)
|
||||
|
||||
titles = q.all()
|
||||
|
||||
chars = set()
|
||||
for title in titles:
|
||||
try:
|
||||
char = title[0][0]
|
||||
char = char if char in ascii_lowercase else '#'
|
||||
chars.add(str(char))
|
||||
except:
|
||||
log.error('Failed getting title for %s', title.libraries_id)
|
||||
|
||||
if len(chars) == 25:
|
||||
break
|
||||
|
||||
db.expire_all()
|
||||
return ''.join(sorted(chars))
|
||||
|
||||
def listView(self, **kwargs):
|
||||
|
||||
status = splitString(kwargs.get('status'))
|
||||
release_status = splitString(kwargs.get('release_status'))
|
||||
limit_offset = kwargs.get('limit_offset')
|
||||
starts_with = kwargs.get('starts_with')
|
||||
search = kwargs.get('search')
|
||||
order = kwargs.get('order')
|
||||
|
||||
total_movies, movies = self.list(
|
||||
status = status,
|
||||
release_status = release_status,
|
||||
limit_offset = limit_offset,
|
||||
starts_with = starts_with,
|
||||
search = search,
|
||||
order = order
|
||||
)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'total': total_movies,
|
||||
'movies': movies,
|
||||
}
|
||||
|
||||
def charView(self, **kwargs):
|
||||
|
||||
status = splitString(kwargs.get('status', None))
|
||||
release_status = splitString(kwargs.get('release_status', None))
|
||||
chars = self.availableChars(status, release_status)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(chars) == 0,
|
||||
'chars': chars,
|
||||
}
|
||||
|
||||
def add(self, params = None, force_readd = True, search_after = True, update_library = False, status_id = None):
|
||||
if not params: params = {}
|
||||
@@ -333,7 +62,6 @@ class MovieBase(MovieTypeBase):
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
library = fireEvent('library.add.movie', single = True, attrs = params, update_after = update_library)
|
||||
|
||||
# Status
|
||||
@@ -343,68 +71,81 @@ class MovieBase(MovieTypeBase):
|
||||
default_profile = fireEvent('profile.default', single = True)
|
||||
cat_id = params.get('category_id')
|
||||
|
||||
db = get_session()
|
||||
m = db.query(Media).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 = Media(
|
||||
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()
|
||||
|
||||
onComplete = None
|
||||
if search_after:
|
||||
onComplete = self.createOnComplete(m.id)
|
||||
|
||||
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'), 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
|
||||
|
||||
if force_readd:
|
||||
m.status_id = status_id if status_id else status_active.get('id')
|
||||
m.last_edit = int(time.time())
|
||||
do_search = True
|
||||
|
||||
db.commit()
|
||||
|
||||
# Remove releases
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
for rel in m.releases:
|
||||
if rel.status_id is available_status.get('id'):
|
||||
db.delete(rel)
|
||||
try:
|
||||
db = get_session()
|
||||
m = db.query(Media).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 = Media(
|
||||
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()
|
||||
|
||||
movie_dict = m.to_dict(self.default_dict)
|
||||
onComplete = None
|
||||
if search_after:
|
||||
onComplete = self.createOnComplete(m.id)
|
||||
|
||||
if do_search and search_after:
|
||||
onComplete = self.createOnComplete(m.id)
|
||||
onComplete()
|
||||
fireEventAsync('library.update.movie', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
|
||||
search_after = False
|
||||
elif force_readd:
|
||||
|
||||
if added:
|
||||
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', ''))
|
||||
# Clean snatched history
|
||||
for release in m.releases:
|
||||
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)
|
||||
|
||||
db.expire_all()
|
||||
return movie_dict
|
||||
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
|
||||
|
||||
if force_readd:
|
||||
m.status_id = status_id if status_id else status_active.get('id')
|
||||
m.last_edit = int(time.time())
|
||||
do_search = True
|
||||
|
||||
db.commit()
|
||||
|
||||
# Remove releases
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
for rel in m.releases:
|
||||
if rel.status_id is available_status.get('id'):
|
||||
db.delete(rel)
|
||||
db.commit()
|
||||
|
||||
movie_dict = m.to_dict(self.default_dict)
|
||||
|
||||
if do_search and search_after:
|
||||
onComplete = self.createOnComplete(m.id)
|
||||
onComplete()
|
||||
|
||||
if added:
|
||||
if params.get('title'):
|
||||
message = 'Successfully added "%s" to your wanted list.' % params.get('title', '')
|
||||
else:
|
||||
title = getTitle(m.library)
|
||||
if title:
|
||||
message = 'Successfully added "%s" to your wanted list.' % title
|
||||
else:
|
||||
message = 'Succesfully added to your wanted list.'
|
||||
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = message)
|
||||
|
||||
return movie_dict
|
||||
except:
|
||||
log.error('Failed deleting media: %s', traceback.format_exc())
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def addView(self, **kwargs):
|
||||
add_dict = self.add(params = kwargs)
|
||||
@@ -416,128 +157,51 @@ class MovieBase(MovieTypeBase):
|
||||
|
||||
def edit(self, id = '', **kwargs):
|
||||
|
||||
db = get_session()
|
||||
try:
|
||||
db = get_session()
|
||||
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
|
||||
ids = splitString(id)
|
||||
for movie_id in ids:
|
||||
ids = splitString(id)
|
||||
for media_id in ids:
|
||||
|
||||
m = db.query(Media).filter_by(id = movie_id).first()
|
||||
if not m:
|
||||
continue
|
||||
m = db.query(Media).filter_by(id = media_id).first()
|
||||
if not m:
|
||||
continue
|
||||
|
||||
m.profile_id = kwargs.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
|
||||
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:
|
||||
if rel.status_id is available_status.get('id'):
|
||||
db.delete(rel)
|
||||
db.commit()
|
||||
# Remove releases
|
||||
for rel in m.releases:
|
||||
if rel.status_id is available_status.get('id'):
|
||||
db.delete(rel)
|
||||
db.commit()
|
||||
|
||||
# Default title
|
||||
if kwargs.get('default_title'):
|
||||
for title in m.library.titles:
|
||||
title.default = toUnicode(kwargs.get('default_title', '')).lower() == toUnicode(title.title).lower()
|
||||
# Default title
|
||||
if kwargs.get('default_title'):
|
||||
for title in m.library.titles:
|
||||
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('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
|
||||
|
||||
db.expire_all()
|
||||
return {
|
||||
'success': True,
|
||||
}
|
||||
|
||||
def deleteView(self, id = '', **kwargs):
|
||||
|
||||
ids = splitString(id)
|
||||
for movie_id in ids:
|
||||
self.delete(movie_id, delete_from = kwargs.get('delete_from', 'all'))
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
}
|
||||
|
||||
def delete(self, movie_id, delete_from = None):
|
||||
|
||||
db = get_session()
|
||||
|
||||
movie = db.query(Media).filter_by(id = movie_id).first()
|
||||
if movie:
|
||||
deleted = False
|
||||
if delete_from == 'all':
|
||||
db.delete(movie)
|
||||
db.commit()
|
||||
deleted = True
|
||||
else:
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
|
||||
total_releases = len(movie.releases)
|
||||
total_deleted = 0
|
||||
new_movie_status = None
|
||||
for release in movie.releases:
|
||||
if delete_from in ['wanted', 'snatched', 'late']:
|
||||
if release.status_id != done_status.get('id'):
|
||||
db.delete(release)
|
||||
total_deleted += 1
|
||||
new_movie_status = 'done'
|
||||
elif delete_from == 'manage':
|
||||
if release.status_id == done_status.get('id'):
|
||||
db.delete(release)
|
||||
total_deleted += 1
|
||||
new_movie_status = 'active'
|
||||
db.commit()
|
||||
|
||||
if total_releases == total_deleted:
|
||||
db.delete(movie)
|
||||
db.commit()
|
||||
deleted = True
|
||||
elif new_movie_status:
|
||||
new_status = fireEvent('status.get', new_movie_status, single = True)
|
||||
movie.profile_id = None
|
||||
movie.status_id = new_status.get('id')
|
||||
db.commit()
|
||||
else:
|
||||
fireEvent('movie.restatus', movie.id, single = True)
|
||||
fireEvent('media.restatus', m.id)
|
||||
|
||||
if deleted:
|
||||
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
|
||||
movie_dict = m.to_dict(self.default_dict)
|
||||
fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id))
|
||||
|
||||
db.expire_all()
|
||||
return True
|
||||
return {
|
||||
'success': True,
|
||||
}
|
||||
except:
|
||||
log.error('Failed deleting media: %s', traceback.format_exc())
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def restatus(self, movie_id):
|
||||
|
||||
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
|
||||
|
||||
db = get_session()
|
||||
|
||||
m = db.query(Media).filter_by(id = movie_id).first()
|
||||
if not m or len(m.library.titles) == 0:
|
||||
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
|
||||
return False
|
||||
|
||||
log.debug('Changing status for %s', m.library.titles[0].title)
|
||||
if not m.profile:
|
||||
m.status_id = done_status.get('id')
|
||||
else:
|
||||
move_to_wanted = True
|
||||
|
||||
for t in m.profile.types:
|
||||
for release in m.releases:
|
||||
if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish):
|
||||
move_to_wanted = False
|
||||
|
||||
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
|
||||
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
return {
|
||||
'success': False,
|
||||
}
|
||||
|
||||
@@ -52,8 +52,8 @@ var MovieList = new Class({
|
||||
|
||||
self.getMovies();
|
||||
|
||||
App.addEvent('movie.added', self.movieAdded.bind(self))
|
||||
App.addEvent('movie.deleted', self.movieDeleted.bind(self))
|
||||
App.on('movie.added', self.movieAdded.bind(self))
|
||||
App.on('movie.deleted', self.movieDeleted.bind(self))
|
||||
},
|
||||
|
||||
movieDeleted: function(notification){
|
||||
@@ -65,6 +65,7 @@ var MovieList = new Class({
|
||||
movie.destroy();
|
||||
delete self.movies_added[notification.data.id];
|
||||
self.setCounter(self.counter_count-1);
|
||||
self.total_movies--;
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -75,6 +76,7 @@ var MovieList = new Class({
|
||||
movieAdded: function(notification){
|
||||
var self = this;
|
||||
|
||||
self.fireEvent('movieAdded', notification);
|
||||
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');
|
||||
@@ -279,7 +281,7 @@ var MovieList = new Class({
|
||||
|
||||
// Get available chars and highlight
|
||||
if(!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible()))
|
||||
Api.request('movie.available_chars', {
|
||||
Api.request('media.available_chars', {
|
||||
'data': Object.merge({
|
||||
'status': self.options.status
|
||||
}, self.filter),
|
||||
@@ -370,7 +372,7 @@ var MovieList = new Class({
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
this.set('text', 'Deleting..')
|
||||
Api.request('movie.delete', {
|
||||
Api.request('media.delete', {
|
||||
'data': {
|
||||
'id': ids.join(','),
|
||||
'delete_from': self.options.identifier
|
||||
@@ -390,6 +392,7 @@ var MovieList = new Class({
|
||||
self.movies.erase(movie);
|
||||
movie.destroy();
|
||||
self.setCounter(self.counter_count-1);
|
||||
self.total_movies--;
|
||||
});
|
||||
|
||||
self.calculateSelected();
|
||||
@@ -547,8 +550,9 @@ var MovieList = new Class({
|
||||
|
||||
}
|
||||
|
||||
Api.request(self.options.api_call || 'movie.list', {
|
||||
Api.request(self.options.api_call || 'media.list', {
|
||||
'data': Object.merge({
|
||||
'type': 'movie',
|
||||
'status': self.options.status,
|
||||
'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null
|
||||
}, self.filter),
|
||||
|
||||
@@ -126,7 +126,9 @@ MA.Release = new Class({
|
||||
else
|
||||
self.showHelper();
|
||||
|
||||
App.addEvent('movie.searcher.ended.'+self.movie.data.id, function(notification){
|
||||
App.on('movie.searcher.ended', function(notification){
|
||||
if(self.movie.data.id != notification.data.id) return;
|
||||
|
||||
self.releases = null;
|
||||
if(self.options_container){
|
||||
self.options_container.destroy();
|
||||
@@ -250,12 +252,14 @@ MA.Release = new Class({
|
||||
else if(!self.next_release && status.identifier == 'available'){
|
||||
self.next_release = release;
|
||||
}
|
||||
|
||||
|
||||
var update_handle = function(notification) {
|
||||
var q = self.movie.quality.getElement('.q_id' + release.quality_id),
|
||||
if(notification.data.id != release.id) return;
|
||||
|
||||
var q = self.movie.quality.getElement('.q_id' + release.quality_id),
|
||||
status = Status.get(release.status_id),
|
||||
new_status = Status.get(notification.data);
|
||||
|
||||
new_status = Status.get(notification.data.status_id);
|
||||
|
||||
release.status_id = new_status.id
|
||||
release.el.set('class', 'item ' + new_status.identifier);
|
||||
|
||||
@@ -272,7 +276,7 @@ MA.Release = new Class({
|
||||
}
|
||||
}
|
||||
|
||||
App.addEvent('release.update_status.' + release.id, update_handle);
|
||||
App.on('release.update_status', update_handle);
|
||||
|
||||
});
|
||||
|
||||
@@ -285,7 +289,7 @@ MA.Release = new Class({
|
||||
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;
|
||||
|
||||
@@ -427,7 +431,7 @@ MA.Release = new Class({
|
||||
markMovieDone: function(){
|
||||
var self = this;
|
||||
|
||||
Api.request('movie.delete', {
|
||||
Api.request('media.delete', {
|
||||
'data': {
|
||||
'id': self.movie.get('id'),
|
||||
'delete_from': 'wanted'
|
||||
@@ -446,7 +450,7 @@ MA.Release = new Class({
|
||||
|
||||
},
|
||||
|
||||
tryNextRelease: function(movie_id){
|
||||
tryNextRelease: function(){
|
||||
var self = this;
|
||||
|
||||
Api.request('movie.searcher.try_next', {
|
||||
@@ -817,7 +821,7 @@ MA.Delete = new Class({
|
||||
self.callChain();
|
||||
},
|
||||
function(){
|
||||
Api.request('movie.delete', {
|
||||
Api.request('media.delete', {
|
||||
'data': {
|
||||
'id': self.movie.get('id'),
|
||||
'delete_from': self.movie.list.options.identifier
|
||||
|
||||
@@ -1036,7 +1036,7 @@
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
width: 85%;
|
||||
direction: rtl;
|
||||
direction: ltr;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,23 +23,49 @@ var Movie = new Class({
|
||||
addEvents: function(){
|
||||
var self = this;
|
||||
|
||||
App.addEvent('movie.update.'+self.data.id, function(notification){
|
||||
self.global_events = {}
|
||||
|
||||
// Do refresh with new data
|
||||
self.global_events['movie.update'] = function(notification){
|
||||
if(self.data.id != notification.data.id) return;
|
||||
|
||||
self.busy(false);
|
||||
self.removeView();
|
||||
self.update.delay(2000, self, notification);
|
||||
});
|
||||
}
|
||||
App.on('movie.update', self.global_events['movie.update']);
|
||||
|
||||
['movie.busy', 'movie.searcher.started'].each(function(listener){
|
||||
App.addEvent(listener+'.'+self.data.id, function(notification){
|
||||
if(notification.data)
|
||||
self.busy(true)
|
||||
});
|
||||
// Add spinner on load / search
|
||||
['media.busy', 'movie.searcher.started'].each(function(listener){
|
||||
self.global_events[listener] = function(notification){
|
||||
if(notification.data && (self.data.id == notification.data.id || (typeOf(notification.data.id) == 'array' && notification.data.id.indexOf(self.data.id) > -1)))
|
||||
self.busy(true);
|
||||
}
|
||||
App.on(listener, self.global_events[listener]);
|
||||
})
|
||||
|
||||
App.addEvent('movie.searcher.ended.'+self.data.id, function(notification){
|
||||
if(notification.data)
|
||||
// Remove spinner
|
||||
self.global_events['movie.searcher.ended'] = function(notification){
|
||||
if(notification.data && self.data.id == notification.data.id)
|
||||
self.busy(false)
|
||||
});
|
||||
}
|
||||
App.on('movie.searcher.ended', self.global_events['movie.searcher.ended']);
|
||||
|
||||
// Reload when releases have updated
|
||||
self.global_events['release.update_status'] = function(notification){
|
||||
var data = notification.data
|
||||
if(data && self.data.id == data.movie_id){
|
||||
|
||||
if(!self.data.releases)
|
||||
self.data.releases = [];
|
||||
|
||||
self.data.releases.push({'quality_id': data.quality_id, 'status_id': data.status_id});
|
||||
self.updateReleases();
|
||||
}
|
||||
}
|
||||
|
||||
App.on('release.update_status', self.global_events['release.update_status']);
|
||||
|
||||
},
|
||||
|
||||
destroy: function(){
|
||||
@@ -52,10 +78,9 @@ var Movie = new Class({
|
||||
self.list.checkIfEmpty();
|
||||
|
||||
// Remove events
|
||||
App.removeEvents('movie.update.'+self.data.id);
|
||||
['movie.busy', 'movie.searcher.started'].each(function(listener){
|
||||
App.removeEvents(listener+'.'+self.data.id);
|
||||
})
|
||||
Object.each(self.global_events, function(handle, listener){
|
||||
App.off(listener, handle);
|
||||
});
|
||||
},
|
||||
|
||||
busy: function(set_busy, timeout){
|
||||
@@ -179,21 +204,7 @@ var Movie = new Class({
|
||||
});
|
||||
|
||||
// Add releases
|
||||
if(self.data.releases)
|
||||
self.data.releases.each(function(release){
|
||||
|
||||
var q = self.quality.getElement('.q_id'+ release.quality_id),
|
||||
status = Status.get(release.status_id);
|
||||
|
||||
if(!q && (status.identifier == 'snatched' || status.identifier == 'seeding' || 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)
|
||||
}
|
||||
|
||||
});
|
||||
self.updateReleases();
|
||||
|
||||
Object.each(self.options.actions, function(action, key){
|
||||
self.action[key.toLowerCase()] = action = new self.options.actions[key](self)
|
||||
@@ -203,6 +214,26 @@ var Movie = new Class({
|
||||
|
||||
},
|
||||
|
||||
updateReleases: function(){
|
||||
var self = this;
|
||||
if(!self.data.releases || self.data.releases.length == 0) return;
|
||||
|
||||
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 == 'seeding' || 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)
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
addQuality: function(quality_id){
|
||||
var self = this;
|
||||
|
||||
@@ -298,4 +329,4 @@ var Movie = new Class({
|
||||
return this.el;
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import MovieLibraryPlugin
|
||||
|
||||
|
||||
def start():
|
||||
return MovieLibraryPlugin()
|
||||
|
||||
|
||||
@@ -7,13 +7,14 @@ from couchpotato.core.settings.model import Library, LibraryTitle, File
|
||||
from string import ascii_letters
|
||||
import time
|
||||
import traceback
|
||||
import six
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class MovieLibraryPlugin(LibraryBase):
|
||||
|
||||
default_dict = {'titles': {}, 'files':{}}
|
||||
default_dict = {'titles': {}, 'files': {}}
|
||||
|
||||
def __init__(self):
|
||||
addEvent('library.add.movie', self.add)
|
||||
@@ -25,69 +26,70 @@ class MovieLibraryPlugin(LibraryBase):
|
||||
|
||||
primary_provider = attrs.get('primary_provider', 'imdb')
|
||||
|
||||
db = get_session()
|
||||
try:
|
||||
db = get_session()
|
||||
|
||||
l = db.query(Library).filter_by(identifier = attrs.get('identifier')).first()
|
||||
if not l:
|
||||
status = fireEvent('status.get', 'needs_update', single = True)
|
||||
l = Library(
|
||||
year = attrs.get('year'),
|
||||
identifier = attrs.get('identifier'),
|
||||
plot = toUnicode(attrs.get('plot')),
|
||||
tagline = toUnicode(attrs.get('tagline')),
|
||||
status_id = status.get('id'),
|
||||
info = {}
|
||||
)
|
||||
l = db.query(Library).filter_by(identifier = attrs.get('identifier')).first()
|
||||
if not l:
|
||||
status = fireEvent('status.get', 'needs_update', single = True)
|
||||
l = Library(
|
||||
year = attrs.get('year'),
|
||||
identifier = attrs.get('identifier'),
|
||||
plot = toUnicode(attrs.get('plot')),
|
||||
tagline = toUnicode(attrs.get('tagline')),
|
||||
status_id = status.get('id'),
|
||||
info = {}
|
||||
)
|
||||
|
||||
title = LibraryTitle(
|
||||
title = toUnicode(attrs.get('title')),
|
||||
simple_title = self.simplifyTitle(attrs.get('title')),
|
||||
)
|
||||
title = LibraryTitle(
|
||||
title = toUnicode(attrs.get('title')),
|
||||
simple_title = self.simplifyTitle(attrs.get('title')),
|
||||
)
|
||||
|
||||
l.titles.append(title)
|
||||
l.titles.append(title)
|
||||
|
||||
db.add(l)
|
||||
db.commit()
|
||||
db.add(l)
|
||||
db.commit()
|
||||
|
||||
# Update library info
|
||||
if update_after is not False:
|
||||
handle = fireEventAsync if update_after is 'async' else fireEvent
|
||||
handle('library.update.movie', identifier = l.identifier, default_title = toUnicode(attrs.get('title', '')))
|
||||
# Update library info
|
||||
if update_after is not False:
|
||||
handle = fireEventAsync if update_after is 'async' else fireEvent
|
||||
handle('library.update.movie', identifier = l.identifier, default_title = toUnicode(attrs.get('title', '')))
|
||||
|
||||
library_dict = l.to_dict(self.default_dict)
|
||||
library_dict = l.to_dict(self.default_dict)
|
||||
return library_dict
|
||||
except:
|
||||
log.error('Failed adding media: %s', traceback.format_exc())
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
db.expire_all()
|
||||
return library_dict
|
||||
return {}
|
||||
|
||||
def update(self, identifier, default_title = '', force = False):
|
||||
def update(self, identifier, default_title = '', extended = 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)
|
||||
try:
|
||||
db = get_session()
|
||||
|
||||
library_dict = None
|
||||
if library:
|
||||
library_dict = library.to_dict(self.default_dict)
|
||||
library = db.query(Library).filter_by(identifier = identifier).first()
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
|
||||
do_update = True
|
||||
info = fireEvent('movie.info', merge = True, extended = extended, 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:
|
||||
# Main info
|
||||
library.plot = toUnicode(info.get('plot', ''))
|
||||
library.tagline = toUnicode(info.get('tagline', ''))
|
||||
library.year = info.get('year', 0)
|
||||
@@ -102,6 +104,17 @@ class MovieLibraryPlugin(LibraryBase):
|
||||
titles = info.get('titles', [])
|
||||
log.debug('Adding titles: %s', titles)
|
||||
counter = 0
|
||||
|
||||
def_title = None
|
||||
for title in titles:
|
||||
if (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == six.u('') and toUnicode(titles[0]) == title):
|
||||
def_title = toUnicode(title)
|
||||
break
|
||||
counter += 1
|
||||
|
||||
if not def_title:
|
||||
def_title = toUnicode(titles[0])
|
||||
|
||||
for title in titles:
|
||||
if not title:
|
||||
continue
|
||||
@@ -109,10 +122,9 @@ class MovieLibraryPlugin(LibraryBase):
|
||||
t = LibraryTitle(
|
||||
title = title,
|
||||
simple_title = self.simplifyTitle(title),
|
||||
default = (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title)
|
||||
default = title == def_title
|
||||
)
|
||||
library.titles.append(t)
|
||||
counter += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
@@ -134,30 +146,43 @@ class MovieLibraryPlugin(LibraryBase):
|
||||
break
|
||||
except:
|
||||
log.debug('Failed to attach to library: %s', traceback.format_exc())
|
||||
db.rollback()
|
||||
|
||||
library_dict = library.to_dict(self.default_dict)
|
||||
return library_dict
|
||||
except:
|
||||
log.error('Failed update media: %s', traceback.format_exc())
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
db.expire_all()
|
||||
return library_dict
|
||||
return {}
|
||||
|
||||
def updateReleaseDate(self, identifier):
|
||||
|
||||
db = get_session()
|
||||
library = db.query(Library).filter_by(identifier = identifier).first()
|
||||
try:
|
||||
db = get_session()
|
||||
library = db.query(Library).filter_by(identifier = identifier).first()
|
||||
|
||||
if not library.info:
|
||||
library_dict = self.update(identifier, force = True)
|
||||
dates = library_dict.get('info', {}).get('release_date')
|
||||
else:
|
||||
dates = library.info.get('release_date')
|
||||
if not library.info:
|
||||
library_dict = self.update(identifier)
|
||||
dates = library_dict.get('info', {}).get('release_date')
|
||||
else:
|
||||
dates = library.info.get('release_date')
|
||||
|
||||
if dates and (dates.get('expires', 0) < time.time() or dates.get('expires', 0) > time.time() + (604800 * 4)) or not dates:
|
||||
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
|
||||
library.info.update({'release_date': dates })
|
||||
db.commit()
|
||||
if dates and (dates.get('expires', 0) < time.time() or dates.get('expires', 0) > time.time() + (604800 * 4)) or not dates:
|
||||
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
|
||||
library.info.update({'release_date': dates})
|
||||
db.commit()
|
||||
|
||||
db.expire_all()
|
||||
return dates
|
||||
return dates
|
||||
except:
|
||||
log.error('Failed updating release dates: %s', traceback.format_exc())
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def simplifyTitle(self, title):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from .main import MovieSearcher
|
||||
import random
|
||||
|
||||
|
||||
def start():
|
||||
return MovieSearcher()
|
||||
|
||||
|
||||
@@ -73,10 +73,21 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
|
||||
db = get_session()
|
||||
|
||||
movies = db.query(Media).filter(
|
||||
movies_raw = db.query(Media).filter(
|
||||
Media.status.has(identifier = 'active')
|
||||
).all()
|
||||
random.shuffle(movies)
|
||||
|
||||
random.shuffle(movies_raw)
|
||||
|
||||
movies = []
|
||||
for m in movies_raw:
|
||||
movies.append(m.to_dict({
|
||||
'category': {},
|
||||
'profile': {'types': {'quality': {}}},
|
||||
'releases': {'status': {}, 'quality': {}},
|
||||
'library': {'titles': {}, 'files': {}},
|
||||
'files': {},
|
||||
}))
|
||||
|
||||
self.in_progress = {
|
||||
'total': len(movies),
|
||||
@@ -87,21 +98,14 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
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)
|
||||
self.single(movie, 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)
|
||||
log.error('Forcing library update for %s, if you see this often, please report: %s', (movie['library']['identifier'], traceback.format_exc()))
|
||||
fireEvent('library.update.movie', movie['library']['identifier'])
|
||||
except:
|
||||
log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
|
||||
log.error('Search failed for %s: %s', (movie['library']['identifier'], traceback.format_exc()))
|
||||
|
||||
self.in_progress['to_go'] -= 1
|
||||
|
||||
@@ -117,7 +121,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
def single(self, movie, search_protocols = None, manual = False):
|
||||
|
||||
# movies don't contain 'type' yet, so just set to default here
|
||||
if not movie.has_key('type'):
|
||||
if 'type' not in movie:
|
||||
movie['type'] = 'movie'
|
||||
|
||||
# Find out search type
|
||||
@@ -133,8 +137,6 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
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)
|
||||
@@ -145,11 +147,12 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
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)
|
||||
fireEvent('media.delete', movie['id'], single = True)
|
||||
return
|
||||
|
||||
fireEvent('notify.frontend', type = 'movie.searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title)
|
||||
fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'id': movie['id']}, message = 'Searching for "%s"' % default_title)
|
||||
|
||||
db = get_session()
|
||||
|
||||
ret = False
|
||||
for quality_type in movie['profile']['types']:
|
||||
@@ -192,7 +195,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
|
||||
else:
|
||||
log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title))
|
||||
fireEvent('movie.restatus', movie['id'])
|
||||
fireEvent('media.restatus', movie['id'])
|
||||
break
|
||||
|
||||
# Break if CP wants to shut down
|
||||
@@ -202,7 +205,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
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)
|
||||
fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'id': movie['id']})
|
||||
|
||||
return ret
|
||||
|
||||
@@ -279,11 +282,17 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
|
||||
now = int(time.time())
|
||||
now_year = date.today().year
|
||||
now_month = date.today().month
|
||||
|
||||
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:
|
||||
|
||||
# Don't allow movies with years to far in the future
|
||||
add_year = 1 if now_month > 10 else 0 # Only allow +1 year if end of the year
|
||||
if year is not None and year > (now_year + add_year):
|
||||
return False
|
||||
|
||||
# For movies before 1972
|
||||
if not dates or dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
|
||||
return True
|
||||
@@ -318,14 +327,14 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
'success': trynext
|
||||
}
|
||||
|
||||
def tryNextRelease(self, movie_id, manual = False):
|
||||
def tryNextRelease(self, media_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_by(movie_id = media_id) \
|
||||
.filter(Release.status_id.in_([snatched_status.get('id'), done_status.get('id')])) \
|
||||
.all()
|
||||
|
||||
@@ -333,7 +342,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
rel.status_id = ignored_status.get('id')
|
||||
db.commit()
|
||||
|
||||
movie_dict = fireEvent('movie.get', movie_id, single = True)
|
||||
movie_dict = fireEvent('media.get', media_id = media_id, single = True)
|
||||
log.info('Trying next release for: %s', getTitle(movie_dict['library']))
|
||||
fireEvent('movie.searcher.single', movie_dict, manual = manual)
|
||||
|
||||
@@ -341,7 +350,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
|
||||
except:
|
||||
log.error('Failed searching for next release: %s', traceback.format_exc())
|
||||
db.rollback()
|
||||
return False
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def getSearchTitle(self, media):
|
||||
if media['type'] == 'movie':
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Suggestion
|
||||
|
||||
|
||||
def start():
|
||||
return Suggestion()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.helpers.variable import splitString, removeDuplicate
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Media, Library
|
||||
from couchpotato.environment import Env
|
||||
@@ -40,7 +40,7 @@ class Suggestion(Plugin):
|
||||
movies.extend(splitString(Env.prop('suggest_seen', default = '')))
|
||||
|
||||
suggestions = fireEvent('movie.suggest', movies = movies, ignore = ignored, single = True)
|
||||
self.setCache('suggestion_cached', suggestions, timeout = 6048000) # Cache for 10 weeks
|
||||
self.setCache('suggestion_cached', suggestions, timeout = 6048000) # Cache for 10 weeks
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
@@ -79,8 +79,10 @@ class Suggestion(Plugin):
|
||||
seen = [] if not seen else seen
|
||||
|
||||
if ignore_imdb:
|
||||
suggested_imdbs = []
|
||||
for cs in cached_suggestion:
|
||||
if cs.get('imdb') != ignore_imdb:
|
||||
if cs.get('imdb') != ignore_imdb and cs.get('imdb') not in suggested_imdbs:
|
||||
suggested_imdbs.append(cs.get('imdb'))
|
||||
new_suggestions.append(cs)
|
||||
|
||||
# Get new results and add them
|
||||
@@ -97,7 +99,7 @@ class Suggestion(Plugin):
|
||||
movies.extend(seen)
|
||||
|
||||
ignored.extend([x.get('imdb') for x in cached_suggestion])
|
||||
suggestions = fireEvent('movie.suggest', movies = movies, ignore = list(set(ignored)), single = True)
|
||||
suggestions = fireEvent('movie.suggest', movies = movies, ignore = removeDuplicate(ignored), single = True)
|
||||
|
||||
if suggestions:
|
||||
new_suggestions.extend(suggestions)
|
||||
|
||||
@@ -101,7 +101,7 @@ var SuggestList = new Class({
|
||||
|
||||
// Add rating
|
||||
m.info_container.adopt(
|
||||
m.rating = m.info.rating && m.info.rating.imdb.length == 2 && parseFloat(m.info.rating.imdb[0]) > 0 ? new Element('span.rating', {
|
||||
m.rating = m.info.rating && m.info.rating.imdb && m.info.rating.imdb.length == 2 && parseFloat(m.info.rating.imdb[0]) > 0 ? new Element('span.rating', {
|
||||
'text': parseFloat(m.info.rating.imdb[0]),
|
||||
'title': parseInt(m.info.rating.imdb[1]) + ' votes'
|
||||
}) : null,
|
||||
|
||||
@@ -13,5 +13,6 @@ def upgrade(migrate_engine):
|
||||
create_column(category_column, movie)
|
||||
Index('ix_movie_category_id', movie.c.category_id).create()
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
pass
|
||||
|
||||
@@ -17,7 +17,7 @@ class Notification(Provider):
|
||||
listen_to = [
|
||||
'renamer.after', 'movie.snatched',
|
||||
'updater.available', 'updater.updated',
|
||||
'core.message',
|
||||
'core.message.important',
|
||||
]
|
||||
dont_listen_to = []
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Boxcar
|
||||
|
||||
|
||||
def start():
|
||||
return Boxcar()
|
||||
|
||||
|
||||
@@ -16,14 +16,14 @@ class Boxcar(Notification):
|
||||
try:
|
||||
message = message.strip()
|
||||
|
||||
params = {
|
||||
data = {
|
||||
'email': self.conf('email'),
|
||||
'notification[from_screen_name]': self.default_title,
|
||||
'notification[message]': toUnicode(message),
|
||||
'notification[from_remote_service_id]': int(time.time()),
|
||||
}
|
||||
|
||||
self.urlopen(self.url, params = params)
|
||||
self.urlopen(self.url, data = data)
|
||||
except:
|
||||
log.error('Check your email and added services on boxcar.io')
|
||||
return False
|
||||
|
||||
34
couchpotato/core/notifications/boxcar2/__init__.py
Normal file
34
couchpotato/core/notifications/boxcar2/__init__.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from .main import Boxcar2
|
||||
|
||||
|
||||
def start():
|
||||
return Boxcar2()
|
||||
|
||||
config = [{
|
||||
'name': 'boxcar2',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'boxcar2',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'token',
|
||||
'description': ('Your Boxcar access token.', 'Can be found in the app under settings')
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also send message when movie is snatched.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
39
couchpotato/core/notifications/boxcar2/main.py
Normal file
39
couchpotato/core/notifications/boxcar2/main.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Boxcar2(Notification):
|
||||
|
||||
url = 'https://new.boxcar.io/api/notifications'
|
||||
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
try:
|
||||
message = message.strip()
|
||||
|
||||
long_message = ''
|
||||
if listener == 'test':
|
||||
long_message = 'This is a test message'
|
||||
elif data.get('identifier'):
|
||||
long_message = 'More movie info <a href="http://www.imdb.com/title/%s/">on IMDB</a>' % data['identifier']
|
||||
|
||||
data = {
|
||||
'user_credentials': self.conf('token'),
|
||||
'notification[title]': toUnicode(message),
|
||||
'notification[long_message]': toUnicode(long_message),
|
||||
}
|
||||
|
||||
self.urlopen(self.url, data = data)
|
||||
except:
|
||||
log.error('Make sure the token provided is for the correct device')
|
||||
return False
|
||||
|
||||
log.info('Boxcar notification successful.')
|
||||
return True
|
||||
|
||||
def isEnabled(self):
|
||||
return super(Boxcar2, self).isEnabled() and self.conf('token')
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import CoreNotifier
|
||||
|
||||
|
||||
def start():
|
||||
return CoreNotifier()
|
||||
|
||||
|
||||
@@ -21,6 +21,12 @@ class CoreNotifier(Notification):
|
||||
|
||||
m_lock = None
|
||||
|
||||
listen_to = [
|
||||
'renamer.after', 'movie.snatched',
|
||||
'updater.available', 'updater.updated',
|
||||
'core.message', 'core.message.important',
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super(CoreNotifier, self).__init__()
|
||||
|
||||
@@ -61,28 +67,42 @@ class CoreNotifier(Notification):
|
||||
|
||||
def clean(self):
|
||||
|
||||
db = get_session()
|
||||
db.query(Notif).filter(Notif.added <= (int(time.time()) - 2419200)).delete()
|
||||
db.commit()
|
||||
|
||||
try:
|
||||
db = get_session()
|
||||
db.query(Notif).filter(Notif.added <= (int(time.time()) - 2419200)).delete()
|
||||
db.commit()
|
||||
except:
|
||||
log.error('Failed cleaning notification: %s', traceback.format_exc())
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def markAsRead(self, ids = None, **kwargs):
|
||||
|
||||
ids = splitString(ids) if ids else None
|
||||
|
||||
db = get_session()
|
||||
try:
|
||||
db = get_session()
|
||||
|
||||
if ids:
|
||||
q = db.query(Notif).filter(or_(*[Notif.id == tryInt(s) for s in ids]))
|
||||
else:
|
||||
q = db.query(Notif).filter_by(read = False)
|
||||
if ids:
|
||||
q = db.query(Notif).filter(or_(*[Notif.id == tryInt(s) for s in ids]))
|
||||
else:
|
||||
q = db.query(Notif).filter_by(read = False)
|
||||
|
||||
q.update({Notif.read: True})
|
||||
q.update({Notif.read: True})
|
||||
db.commit()
|
||||
|
||||
db.commit()
|
||||
return {
|
||||
'success': True
|
||||
}
|
||||
except:
|
||||
log.error('Failed mark as read: %s', traceback.format_exc())
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
return {
|
||||
'success': True
|
||||
'success': False
|
||||
}
|
||||
|
||||
def listView(self, limit_offset = None, **kwargs):
|
||||
@@ -121,7 +141,10 @@ class CoreNotifier(Notification):
|
||||
|
||||
for message in messages:
|
||||
if message.get('time') > last_check:
|
||||
fireEvent('core.message', message = message.get('message'), data = message)
|
||||
message['sticky'] = True # Always sticky core messages
|
||||
|
||||
message_type = 'core.message.important' if message.get('important') else 'core.message'
|
||||
fireEvent(message_type, message = message.get('message'), data = message)
|
||||
|
||||
if last_check < message.get('time'):
|
||||
last_check = message.get('time')
|
||||
@@ -131,24 +154,30 @@ class CoreNotifier(Notification):
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
db = get_session()
|
||||
try:
|
||||
db = get_session()
|
||||
|
||||
data['notification_type'] = listener if listener else 'unknown'
|
||||
data['notification_type'] = listener if listener else 'unknown'
|
||||
|
||||
n = Notif(
|
||||
message = toUnicode(message),
|
||||
data = data
|
||||
)
|
||||
db.add(n)
|
||||
db.commit()
|
||||
n = Notif(
|
||||
message = toUnicode(message),
|
||||
data = data
|
||||
)
|
||||
db.add(n)
|
||||
db.commit()
|
||||
|
||||
ndict = n.to_dict()
|
||||
ndict['type'] = 'notification'
|
||||
ndict['time'] = time.time()
|
||||
ndict = n.to_dict()
|
||||
ndict['type'] = 'notification'
|
||||
ndict['time'] = time.time()
|
||||
|
||||
self.frontend(type = listener, data = data)
|
||||
self.frontend(type = listener, data = data)
|
||||
|
||||
return True
|
||||
return True
|
||||
except:
|
||||
log.error('Failed notify: %s', traceback.format_exc())
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def frontend(self, type = 'notification', data = None, message = None):
|
||||
if not data: data = {}
|
||||
|
||||
@@ -10,8 +10,8 @@ var NotificationBase = new Class({
|
||||
// Listener
|
||||
App.addEvent('unload', self.stopPoll.bind(self));
|
||||
App.addEvent('reload', self.startInterval.bind(self, [true]));
|
||||
App.addEvent('notification', self.notify.bind(self));
|
||||
App.addEvent('message', self.showMessage.bind(self));
|
||||
App.on('notification', self.notify.bind(self));
|
||||
App.on('message', self.showMessage.bind(self));
|
||||
|
||||
// Add test buttons to settings page
|
||||
App.addEvent('load', self.addTestButtons.bind(self));
|
||||
@@ -50,9 +50,9 @@ var NotificationBase = new Class({
|
||||
, 'top');
|
||||
self.notifications.include(result);
|
||||
|
||||
if(result.data.important !== undefined && !result.read){
|
||||
if((result.data.important !== undefined || result.data.sticky !== undefined) && !result.read){
|
||||
var sticky = true
|
||||
App.fireEvent('message', [result.message, sticky, result])
|
||||
App.trigger('message', [result.message, sticky, result])
|
||||
}
|
||||
else if(!result.read){
|
||||
self.setBadge(self.notifications.filter(function(n){ return !n.read}).length)
|
||||
@@ -147,7 +147,7 @@ var NotificationBase = new Class({
|
||||
// Process data
|
||||
if(json){
|
||||
Array.each(json.result, function(result){
|
||||
App.fireEvent(result.type, result);
|
||||
App.trigger(result.type, [result]);
|
||||
if(result.message && result.read === undefined)
|
||||
self.showMessage(result.message);
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Email
|
||||
|
||||
|
||||
def start():
|
||||
return Email()
|
||||
|
||||
@@ -30,7 +31,7 @@ config = [{
|
||||
},
|
||||
{ 'name': 'smtp_port',
|
||||
'label': 'SMTP server port',
|
||||
'default': '25',
|
||||
'default': '25',
|
||||
'type': 'int',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from couchpotato.environment import Env
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formatdate, make_msgid
|
||||
import smtplib
|
||||
import traceback
|
||||
|
||||
@@ -30,6 +31,8 @@ class Email(Notification):
|
||||
message['Subject'] = self.default_title
|
||||
message['From'] = from_address
|
||||
message['To'] = to_address
|
||||
message['Date'] = formatdate(localtime = 1)
|
||||
message['Message-ID'] = make_msgid()
|
||||
|
||||
try:
|
||||
# Open the SMTP connection, via SSL if requested
|
||||
@@ -37,7 +40,7 @@ class Email(Notification):
|
||||
log.debug("SMTP over SSL %s", ("enabled" if ssl == 1 else "disabled"))
|
||||
mailserver = smtplib.SMTP_SSL(smtp_server) if ssl == 1 else smtplib.SMTP(smtp_server)
|
||||
|
||||
if (starttls):
|
||||
if starttls:
|
||||
log.debug("Using StartTLS to initiate the connection with the SMTP server")
|
||||
mailserver.starttls()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Growl
|
||||
|
||||
|
||||
def start():
|
||||
return Growl()
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class Growl(Notification):
|
||||
)
|
||||
self.growl.register()
|
||||
self.registered = True
|
||||
except Exception, e:
|
||||
except Exception as e:
|
||||
if 'timed out' in str(e):
|
||||
self.registered = True
|
||||
else:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import NMJ
|
||||
|
||||
|
||||
def start():
|
||||
return NMJ()
|
||||
|
||||
|
||||
@@ -86,18 +86,17 @@ class NMJ(Notification):
|
||||
'arg3': '',
|
||||
}
|
||||
params = tryUrlencode(params)
|
||||
UPDATE_URL = 'http://%(host)s:8008/metadata_database?%(params)s'
|
||||
updateUrl = UPDATE_URL % {'host': host, 'params': params}
|
||||
update_url = 'http://%(host)s:8008/metadata_database?%(params)s' % {'host': host, 'params': params}
|
||||
|
||||
try:
|
||||
response = self.urlopen(updateUrl)
|
||||
response = self.urlopen(update_url)
|
||||
except:
|
||||
return False
|
||||
|
||||
try:
|
||||
et = etree.fromstring(response)
|
||||
result = et.findtext('returnValue')
|
||||
except SyntaxError, e:
|
||||
except SyntaxError as e:
|
||||
log.error('Unable to parse XML returned from the Popcorn Hour: %s', e)
|
||||
return False
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import NotifyMyAndroid
|
||||
|
||||
|
||||
def start():
|
||||
return NotifyMyAndroid()
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import pynma
|
||||
import six
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -26,7 +27,7 @@ class NotifyMyAndroid(Notification):
|
||||
|
||||
successful = 0
|
||||
for key in keys:
|
||||
if not response[str(key)]['code'] == u'200':
|
||||
if not response[str(key)]['code'] == six.u('200'):
|
||||
log.error('Could not send notification to NotifyMyAndroid (%s). %s', (key, response[key]['message']))
|
||||
else:
|
||||
successful += 1
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import NotifyMyWP
|
||||
|
||||
|
||||
def start():
|
||||
return NotifyMyWP()
|
||||
|
||||
|
||||
@@ -2,13 +2,15 @@ from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from pynmwp import PyNMWP
|
||||
import six
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class NotifyMyWP(Notification):
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
keys = splitString(self.conf('api_key'))
|
||||
p = PyNMWP(keys, self.conf('dev_key'))
|
||||
@@ -16,7 +18,7 @@ class NotifyMyWP(Notification):
|
||||
response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1)
|
||||
|
||||
for key in keys:
|
||||
if not response[key]['Code'] == u'200':
|
||||
if not response[key]['Code'] == six.u('200'):
|
||||
log.error('Could not send notification to NotifyMyWindowsPhone (%s). %s', (key, response[key]['message']))
|
||||
return False
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Plex
|
||||
|
||||
|
||||
def start():
|
||||
return Plex()
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class PlexClientHTTP(PlexClientProtocol):
|
||||
|
||||
try:
|
||||
self.plex.urlopen(url, headers = headers, timeout = 3, show_error = False)
|
||||
except Exception, err:
|
||||
except Exception as err:
|
||||
log.error("Couldn't sent command to Plex: %s", err)
|
||||
return False
|
||||
|
||||
@@ -68,7 +68,7 @@ class PlexClientJSON(PlexClientProtocol):
|
||||
|
||||
try:
|
||||
requests.post(url, headers = headers, timeout = 3, data = json.dumps(request))
|
||||
except Exception, err:
|
||||
except Exception as err:
|
||||
log.error("Couldn't sent command to Plex: %s", err)
|
||||
return False
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@ class Plex(Notification):
|
||||
|
||||
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 = {}
|
||||
|
||||
return self.server.refresh()
|
||||
|
||||
@@ -57,7 +57,8 @@ class Plex(Notification):
|
||||
|
||||
return success
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
return self.notifyClients(message, self.getClientNames())
|
||||
|
||||
def test(self, **kwargs):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Prowl
|
||||
|
||||
|
||||
def start():
|
||||
return Prowl()
|
||||
|
||||
|
||||
@@ -22,11 +22,11 @@ class Prowl(Notification):
|
||||
'priority': self.conf('priority'),
|
||||
}
|
||||
headers = {
|
||||
'Content-type': 'application/x-www-form-urlencoded'
|
||||
'Content-type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
|
||||
try:
|
||||
self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False)
|
||||
self.urlopen(self.urls['api'], headers = headers, data = data, show_error = False)
|
||||
log.info('Prowl notifications sent.')
|
||||
return True
|
||||
except:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Pushalot
|
||||
|
||||
|
||||
def start():
|
||||
return Pushalot()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Pushalot(Notification):
|
||||
|
||||
urls = {
|
||||
@@ -29,7 +30,7 @@ class Pushalot(Notification):
|
||||
}
|
||||
|
||||
try:
|
||||
self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False)
|
||||
self.urlopen(self.urls['api'], headers = headers, data = data, show_error = False)
|
||||
return True
|
||||
except:
|
||||
log.error('PushAlot failed: %s', traceback.format_exc())
|
||||
|
||||
40
couchpotato/core/notifications/pushbullet/__init__.py
Normal file
40
couchpotato/core/notifications/pushbullet/__init__.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from .main import Pushbullet
|
||||
|
||||
|
||||
def start():
|
||||
return Pushbullet()
|
||||
|
||||
config = [{
|
||||
'name': 'pushbullet',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'pushbullet',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'api_key',
|
||||
'label': 'User API Key'
|
||||
},
|
||||
{
|
||||
'name': 'devices',
|
||||
'default': '',
|
||||
'advanced': True,
|
||||
'description': 'IDs of devices to send notifications to, empty = all devices'
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also send message when movie is snatched.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
69
couchpotato/core/notifications/pushbullet/main.py
Normal file
69
couchpotato/core/notifications/pushbullet/main.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import base64
|
||||
import json
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Pushbullet(Notification):
|
||||
|
||||
url = 'https://api.pushbullet.com/api/%s'
|
||||
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
devices = self.getDevices()
|
||||
if devices is None:
|
||||
return False
|
||||
|
||||
# Get all the device IDs linked to this user
|
||||
if not len(devices):
|
||||
response = self.request('devices')
|
||||
if not response:
|
||||
return False
|
||||
|
||||
devices += [device.get('id') for device in response['devices']]
|
||||
|
||||
successful = 0
|
||||
for device in devices:
|
||||
response = self.request(
|
||||
'pushes',
|
||||
cache = False,
|
||||
device_iden = device,
|
||||
type = 'note',
|
||||
title = self.default_title,
|
||||
body = toUnicode(message)
|
||||
)
|
||||
|
||||
if response:
|
||||
successful += 1
|
||||
else:
|
||||
log.error('Unable to push notification to Pushbullet device with ID %s' % device)
|
||||
|
||||
return successful == len(devices)
|
||||
|
||||
def getDevices(self):
|
||||
return splitString(self.conf('devices'))
|
||||
|
||||
def request(self, method, cache = True, **kwargs):
|
||||
try:
|
||||
base64string = base64.encodestring('%s:' % self.conf('api_key'))[:-1]
|
||||
|
||||
headers = {
|
||||
"Authorization": "Basic %s" % base64string
|
||||
}
|
||||
|
||||
if cache:
|
||||
return self.getJsonData(self.url % method, headers = headers, data = kwargs)
|
||||
else:
|
||||
data = self.urlopen(self.url % method, headers = headers, data = kwargs)
|
||||
return json.loads(data)
|
||||
|
||||
except Exception as ex:
|
||||
log.error('Pushbullet request failed')
|
||||
log.debug(ex)
|
||||
|
||||
return None
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Pushover
|
||||
|
||||
|
||||
def start():
|
||||
return Pushover()
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ class Pushover(Notification):
|
||||
})
|
||||
|
||||
http_handler.request('POST',
|
||||
"/1/messages.json",
|
||||
headers = {'Content-type': 'application/x-www-form-urlencoded'},
|
||||
body = tryUrlencode(api_data)
|
||||
"/1/messages.json",
|
||||
headers = {'Content-type': 'application/x-www-form-urlencoded'},
|
||||
body = tryUrlencode(api_data)
|
||||
)
|
||||
|
||||
response = http_handler.getresponse()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Synoindex
|
||||
|
||||
|
||||
def start():
|
||||
return Synoindex()
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class Synoindex(Notification):
|
||||
out = p.communicate()
|
||||
log.info('Result from synoindex: %s', str(out))
|
||||
return True
|
||||
except OSError, e:
|
||||
except OSError as e:
|
||||
log.error('Unable to run synoindex: %s', e)
|
||||
|
||||
return False
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Toasty
|
||||
|
||||
|
||||
def start():
|
||||
return Toasty()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Toasty(Notification):
|
||||
|
||||
urls = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Trakt
|
||||
|
||||
|
||||
def start():
|
||||
return Trakt()
|
||||
|
||||
|
||||
@@ -3,12 +3,14 @@ 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',
|
||||
'test': 'account/test/%s',
|
||||
}
|
||||
|
||||
listen_to = ['movie.downloaded']
|
||||
@@ -16,26 +18,40 @@ class Trakt(Notification):
|
||||
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 []
|
||||
}
|
||||
if listener == 'test':
|
||||
|
||||
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)
|
||||
post_data = {
|
||||
'username': self.conf('automation_username'),
|
||||
'password': self.conf('automation_password'),
|
||||
}
|
||||
|
||||
return result
|
||||
result = self.call((self.urls['test'] % self.conf('automation_api_key')), post_data)
|
||||
|
||||
return result
|
||||
|
||||
else:
|
||||
|
||||
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)
|
||||
|
||||
response = self.getJsonData(self.urls['base'] % method_url, data = post_data, cache_timeout = 1)
|
||||
if response:
|
||||
if response.get('status') == "success":
|
||||
log.info('Successfully called Trakt')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from .main import Twitter
|
||||
|
||||
|
||||
def start():
|
||||
return Twitter()
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user