Compare commits
359
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
506871b506 | ||
|
|
6115917660 | ||
|
|
44b78f8d2f | ||
|
|
21df8819d3 | ||
|
|
cad9bfae9f | ||
|
|
749075b4cb | ||
|
|
0456a1e820 | ||
|
|
35a9739ec5 | ||
|
|
2a451c255e | ||
|
|
7c38ad1c00 | ||
|
|
647159e549 | ||
|
|
7cc55c21b6 | ||
|
|
89c38f5aa4 | ||
|
|
5f428649c3 | ||
|
|
8ed2a99830 | ||
|
|
1a89d551dc | ||
|
|
9d633910f6 | ||
|
|
54ea22e9b6 | ||
|
|
fb3f3e11f6 | ||
|
|
f84b23eecc | ||
|
|
6ea045ddd3 | ||
|
|
f8b4e75b74 | ||
|
|
faaf351662 | ||
|
|
f41fc794c1 | ||
|
|
0f789b5b40 | ||
|
|
d2496d768d | ||
|
|
b93488f025 | ||
|
|
d4de68ef86 | ||
|
|
61a0bb8ec6 | ||
|
|
fe52ac7203 | ||
|
|
4447b7611e | ||
|
|
178c8942c3 | ||
|
|
4fe9f9e42f | ||
|
|
71b22345bc | ||
|
|
a0dc5c075a | ||
|
|
a264c75f8c | ||
|
|
fcc8a71eae | ||
|
|
cdd681ad48 | ||
|
|
36e5c49147 | ||
|
|
300f4738a0 | ||
|
|
9447833653 | ||
|
|
df53d0c578 | ||
|
|
17eaba3e2a | ||
|
|
0f389f18cb | ||
|
|
28ce083f48 | ||
|
|
cfaffe2bcb | ||
|
|
432852cf5d | ||
|
|
3c728608e9 | ||
|
|
8892ace3c2 | ||
|
|
87574a1810 | ||
|
|
14e0219e62 | ||
|
|
51e747049d | ||
|
|
0582f7d694 | ||
|
|
fa7cac7538 | ||
|
|
ec857a9b3d | ||
|
|
4d32b0b16d | ||
|
|
ca08287cff | ||
|
|
36fee69843 | ||
|
|
c5cae5ab9b | ||
|
|
9bd5688fb9 | ||
|
|
1993c2b6cb | ||
|
|
acc8ed2092 | ||
|
|
7b4924dd7a | ||
|
|
3a2861f72a | ||
|
|
4779265b43 | ||
|
|
f8a46ebe6d | ||
|
|
383ec7e6f5 | ||
|
|
dd9118292d | ||
|
|
4d0f8eb4ac | ||
|
|
637b21cc68 | ||
|
|
da429f0cb8 | ||
|
|
41c2845328 | ||
|
|
c2453bb070 | ||
|
|
a3a2c8da8e | ||
|
|
a1d4bab793 | ||
|
|
d314a9b5b3 | ||
|
|
9a60f6001a | ||
|
|
96a39dbf60 | ||
|
|
015675750c | ||
|
|
bf4dc62f54 | ||
|
|
c2382ade05 | ||
|
|
2f65545086 | ||
|
|
3aea2cd968 | ||
|
|
f30cb9185c | ||
|
|
615468e8e6 | ||
|
|
0cbee01024 | ||
|
|
c29cb39797 | ||
|
|
580ff38136 | ||
|
|
6b8bca5491 | ||
|
|
e92b5d95ca | ||
|
|
611a32d110 | ||
|
|
74e4b015a9 | ||
|
|
1e0267cdb5 | ||
|
|
041a206fb4 | ||
|
|
12a4d6a995 | ||
|
|
b14a6c1e63 | ||
|
|
7fa08ef9b6 | ||
|
|
9a314cfbc4 | ||
|
|
5941d0bf77 | ||
|
|
d326c1c25c | ||
|
|
7e6234298d | ||
|
|
5cf4b8b4d3 | ||
|
|
6e56072250 | ||
|
|
917c5552a4 | ||
|
|
73c5b90232 | ||
|
|
fd53ba0637 | ||
|
|
0ef3906b3d | ||
|
|
5ab0d7a97b | ||
|
|
dbbbbb2f84 | ||
|
|
1bfe948a45 | ||
|
|
0d2dcff7f0 | ||
|
|
d4da206f93 | ||
|
|
439cda8b63 | ||
|
|
bbe8362b08 | ||
|
|
985a168724 | ||
|
|
5e6aea97f7 | ||
|
|
6c7c4c7aba | ||
|
|
e2f59f5ff4 | ||
|
|
b225980ce7 | ||
|
|
b8e86b378f | ||
|
|
031a186d71 | ||
|
|
3c04eed218 | ||
|
|
17e01689d9 | ||
|
|
173c6194ed | ||
|
|
95c2e992b0 | ||
|
|
4bffb299af | ||
|
|
a2c4119508 | ||
|
|
4e9472f8ee | ||
|
|
f7911fe9f3 | ||
|
|
8ffa6a8392 | ||
|
|
382d49f895 | ||
|
|
570b79a67e | ||
|
|
e7aafc406f | ||
|
|
2dcc1e096e | ||
|
|
9f0746a668 | ||
|
|
d9c437bd7f | ||
|
|
7079647f87 | ||
|
|
65570ba479 | ||
|
|
a57ba9026d | ||
|
|
63246256ee | ||
|
|
1ac0dc3bbf | ||
|
|
bcd23ad10c | ||
|
|
342d31b48a | ||
|
|
ea7904ed9a | ||
|
|
ca37c2f018 | ||
|
|
5aa2146614 | ||
|
|
0fd49a2c67 | ||
|
|
b680d84cba | ||
|
|
898e6f487d | ||
|
|
96472a9a8f | ||
|
|
27252561e2 | ||
|
|
24b341005e | ||
|
|
749cf550ec | ||
|
|
650177803b | ||
|
|
bb7b4cbbed | ||
|
|
6618c3927c | ||
|
|
003db92c9b | ||
|
|
b2b396bf17 | ||
|
|
f1a1db8d5b | ||
|
|
f515cd2477 | ||
|
|
65bb1bec27 | ||
|
|
cc84532824 | ||
|
|
5530fbf792 | ||
|
|
5658a85f61 | ||
|
|
0c5206f01b | ||
|
|
4bffce637e | ||
|
|
9f2941a45c | ||
|
|
f452106bfc | ||
|
|
da3055be30 | ||
|
|
f9b65e7216 | ||
|
|
07e2c56095 | ||
|
|
9a6cfe3a21 | ||
|
|
802338a934 | ||
|
|
f0a3358561 | ||
|
|
1c4c69211b | ||
|
|
c9e732651f | ||
|
|
7849e7170d | ||
|
|
087894eb4e | ||
|
|
4b58b40226 | ||
|
|
77d57f5a09 | ||
|
|
618845a021 | ||
|
|
3aabcbf8f1 | ||
|
|
929c6fe3f9 | ||
|
|
c852949591 | ||
|
|
e36c8ec3ab | ||
|
|
afea12c7c0 | ||
|
|
c29a8b47d6 | ||
|
|
fdd0826b4f | ||
|
|
81b7ebaf51 | ||
|
|
eafc3db74d | ||
|
|
3464435a5c | ||
|
|
9f19902221 | ||
|
|
2ed72c9098 | ||
|
|
723f720280 | ||
|
|
daaa2154e5 | ||
|
|
95c5db2d17 | ||
|
|
e53a9ed30a | ||
|
|
2b49a4b5d6 | ||
|
|
4224a25e54 | ||
|
|
3635da1f59 | ||
|
|
71cca6b87f | ||
|
|
68c0496f8e | ||
|
|
6dc3c8d69d | ||
|
|
3ecc826629 | ||
|
|
b03012e4aa | ||
|
|
5a1f05df8e | ||
|
|
62a5909856 | ||
|
|
813c078db0 | ||
|
|
904d1ea4f7 | ||
|
|
20b773bc3b | ||
|
|
be56b96bd0 | ||
|
|
655e847aeb | ||
|
|
f3fd0afb42 | ||
|
|
3782ad7f98 | ||
|
|
6f5031fa7c | ||
|
|
93604a45e5 | ||
|
|
28f4169e44 | ||
|
|
2361057e4c | ||
|
|
5caa40bd81 | ||
|
|
a22bd4abd4 | ||
|
|
a32ba7a763 | ||
|
|
5fe645cc11 | ||
|
|
f333d85907 | ||
|
|
3ec2df5780 | ||
|
|
25f1b8c7a7 | ||
|
|
e71da1f14d | ||
|
|
212d64143c | ||
|
|
51f9b5c673 | ||
|
|
2215c000b7 | ||
|
|
14797249ff | ||
|
|
49e2607f5d | ||
|
|
938b14ba18 | ||
|
|
c893d5bbb8 | ||
|
|
c4adab69cb | ||
|
|
2c9af74f7f | ||
|
|
7eb15c1a53 | ||
|
|
a02257a906 | ||
|
|
667075a006 | ||
|
|
b0f6f9b2ea | ||
|
|
c0900cfe94 | ||
|
|
24a4810919 | ||
|
|
70b15a5696 | ||
|
|
d6522d8f38 | ||
|
|
78eab890e7 | ||
|
|
35c0356734 | ||
|
|
1a56191f83 | ||
|
|
41c0f34d95 | ||
|
|
37bf205d7a | ||
|
|
b66af0b6c6 | ||
|
|
d1e798323c | ||
|
|
32fe3796e4 | ||
|
|
7420785eaf | ||
|
|
a86522a810 | ||
|
|
359d1aaafa | ||
|
|
d636314971 | ||
|
|
fb5d336351 | ||
|
|
e918e6b12f | ||
|
|
b71f003ad8 | ||
|
|
a432ad4f5a | ||
|
|
ac04121dd3 | ||
|
|
61a3a0386e | ||
|
|
7b1f17c062 | ||
|
|
6f7b565103 | ||
|
|
4fb7467e97 | ||
|
|
bcdc633a5e | ||
|
|
19f74e398f | ||
|
|
09f723bda5 | ||
|
|
fbeadb8d9e | ||
|
|
5bda44d419 | ||
|
|
84eccbf9cf | ||
|
|
9ebc4dbf38 | ||
|
|
907f821e50 | ||
|
|
9dc1843f25 | ||
|
|
ad0a1b1efe | ||
|
|
8bfad087e1 | ||
|
|
67c87444de | ||
|
|
4dfd8b4cd5 | ||
|
|
3ffc6e122e | ||
|
|
f204309ed7 | ||
|
|
d3ebe531d5 | ||
|
|
6106fd4e82 | ||
|
|
981ba61458 | ||
|
|
a5534c4bd2 | ||
|
|
2cd887b70a | ||
|
|
19ddd03204 | ||
|
|
d0d9ac07a6 | ||
|
|
eb30dff986 | ||
|
|
0773d6e6ad | ||
|
|
1011e2e9b8 | ||
|
|
84f5dcc134 | ||
|
|
fef3eb1b84 | ||
|
|
ef6d0e04c0 | ||
|
|
269e98b049 | ||
|
|
378d1ccd1c | ||
|
|
f3e3632dd3 | ||
|
|
27635caa1d | ||
|
|
4836a9ffdc | ||
|
|
8874bd4e2b | ||
|
|
799b665f15 | ||
|
|
7f90135947 | ||
|
|
50a2bca459 | ||
|
|
a3b3b9c218 | ||
|
|
d38bd03422 | ||
|
|
9184a97fcd | ||
|
|
ce0bf7b51a | ||
|
|
151b100573 | ||
|
|
0e23413069 | ||
|
|
2ac2b0ff06 | ||
|
|
86bf08cbd4 | ||
|
|
ed0e54d64d | ||
|
|
3da0b1a804 | ||
|
|
39c2567d5a | ||
|
|
95c5d16991 | ||
|
|
08b450fc0a | ||
|
|
dc63796e48 | ||
|
|
c6cba2f6e5 | ||
|
|
ef945597d2 | ||
|
|
ba36c738c7 | ||
|
|
3a3a4fb1f3 | ||
|
|
3fa352e7c8 | ||
|
|
08ef153bbf | ||
|
|
7e3a6eeb83 | ||
|
|
24ad975917 | ||
|
|
952f29918e | ||
|
|
08ae51dbe6 | ||
|
|
d40ad1ddf2 | ||
|
|
0132012276 | ||
|
|
0f1e8eeff9 | ||
|
|
8eee2af49b | ||
|
|
3d26a53fbd | ||
|
|
d4600635e1 | ||
|
|
9312336962 | ||
|
|
6fc9d383de | ||
|
|
5776b2caad | ||
|
|
f82e2a3e6e | ||
|
|
a5fa503970 | ||
|
|
6f7d2caa9b | ||
|
|
c0012c9243 | ||
|
|
5cc7250528 | ||
|
|
aa1fa3eb9a | ||
|
|
f474962225 | ||
|
|
0e2f8a612c | ||
|
|
ad7de32e70 | ||
|
|
d295b881af | ||
|
|
ade4338ea6 | ||
|
|
20f81d06c0 | ||
|
|
8fb24bb101 | ||
|
|
ce3efd3a3c | ||
|
|
ef7fc62c66 | ||
|
|
4be8d02cbb | ||
|
|
7ce8a4fc45 | ||
|
|
55b20324c0 | ||
|
|
c0fb28301d | ||
|
|
f9c2503f81 | ||
|
|
5b4cdf05b1 | ||
|
|
6f25a6bdfd | ||
|
|
23427e95f7 | ||
|
|
90a09e573b | ||
|
|
e1d7440b9d |
+2
-3
@@ -62,7 +62,6 @@ class Loader(object):
|
|||||||
self.log.logger.addHandler(hdlr)
|
self.log.logger.addHandler(hdlr)
|
||||||
|
|
||||||
def addSignals(self):
|
def addSignals(self):
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, self.onExit)
|
signal.signal(signal.SIGINT, self.onExit)
|
||||||
signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1))
|
signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1))
|
||||||
|
|
||||||
@@ -74,7 +73,7 @@ class Loader(object):
|
|||||||
|
|
||||||
def onExit(self, signal, frame):
|
def onExit(self, signal, frame):
|
||||||
from couchpotato.core.event import fireEvent
|
from couchpotato.core.event import fireEvent
|
||||||
fireEvent('app.crappy_shutdown', single = True)
|
fireEvent('app.shutdown', single = True)
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
|
||||||
@@ -100,7 +99,7 @@ class Loader(object):
|
|||||||
logging.shutdown()
|
logging.shutdown()
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
|
|
||||||
args = [sys.executable] + [os.path.join(base_path, __file__)] + sys.argv[1:]
|
args = [sys.executable] + [os.path.join(base_path, os.path.basename(__file__))] + sys.argv[1:]
|
||||||
subprocess.Popen(args)
|
subprocess.Popen(args)
|
||||||
except:
|
except:
|
||||||
self.log.critical(traceback.format_exc())
|
self.log.critical(traceback.format_exc())
|
||||||
|
|||||||
+18
-5
@@ -1,12 +1,12 @@
|
|||||||
|
from esky.util import appdir_from_executable #@UnresolvedImport
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
from version import VERSION
|
||||||
from wx.lib.softwareupdate import SoftwareUpdate
|
from wx.lib.softwareupdate import SoftwareUpdate
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import webbrowser
|
import webbrowser
|
||||||
import wx
|
import wx
|
||||||
import subprocess
|
|
||||||
|
|
||||||
|
|
||||||
# Include proper dirs
|
# Include proper dirs
|
||||||
if hasattr(sys, 'frozen'):
|
if hasattr(sys, 'frozen'):
|
||||||
@@ -166,7 +166,7 @@ class CouchPotatoApp(wx.App, SoftwareUpdate):
|
|||||||
def OnInit(self):
|
def OnInit(self):
|
||||||
|
|
||||||
# Updater
|
# Updater
|
||||||
base_url = 'http://couchpota.to/updates/'
|
base_url = 'http://couchpota.to/updates/%s/' % VERSION
|
||||||
self.InitUpdates(base_url, base_url + 'changelog.html',
|
self.InitUpdates(base_url, base_url + 'changelog.html',
|
||||||
icon = wx.Icon('icon.png'))
|
icon = wx.Icon('icon.png'))
|
||||||
|
|
||||||
@@ -214,5 +214,18 @@ if __name__ == '__main__':
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
if app.restart:
|
if app.restart:
|
||||||
args = [sys.executable] + [os.path.join(base_path, 'Desktop.py')] + sys.argv[1:]
|
|
||||||
subprocess.Popen(args)
|
def appexe_from_executable(exepath):
|
||||||
|
appdir = appdir_from_executable(exepath)
|
||||||
|
exename = os.path.basename(exepath)
|
||||||
|
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
if os.path.isdir(os.path.join(appdir, "Contents", "MacOS")):
|
||||||
|
return os.path.join(appdir, "Contents", "MacOS", exename)
|
||||||
|
|
||||||
|
return os.path.join(appdir, exename)
|
||||||
|
|
||||||
|
exe = appexe_from_executable(sys.executable)
|
||||||
|
os.chdir(os.path.dirname(exe))
|
||||||
|
|
||||||
|
os.execv(exe, [exe] + sys.argv[1:])
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Windows, see [the CP forum](http://couchpota.to/forum/showthread.php?tid=14) for
|
|||||||
* Open up `Git Bash` (or CMD) and go to the folder you want to install CP. Something like Program Files.
|
* Open up `Git Bash` (or CMD) and go to the folder you want to install CP. Something like Program Files.
|
||||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`.
|
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`.
|
||||||
* You can now start CP via `CouchPotatoServer\CouchPotato.py` to start
|
* You can now start CP via `CouchPotatoServer\CouchPotato.py` to start
|
||||||
|
* Your browser should open up, but if it doesn't go to: `http://localhost:5050/`
|
||||||
|
|
||||||
OSx:
|
OSx:
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ OSx:
|
|||||||
* Go to your App folder `cd /Applications`
|
* Go to your App folder `cd /Applications`
|
||||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
||||||
* Then do `python CouchPotatoServer/CouchPotato.py`
|
* Then do `python CouchPotatoServer/CouchPotato.py`
|
||||||
|
* Your browser should open up, but if it doesn't go to: `http://localhost:5050/`
|
||||||
|
|
||||||
Linux (ubuntu / debian):
|
Linux (ubuntu / debian):
|
||||||
|
|
||||||
@@ -33,7 +35,8 @@ Linux (ubuntu / debian):
|
|||||||
* 'cd' to the folder of your choosing.
|
* 'cd' to the folder of your choosing.
|
||||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
||||||
* Then do `python CouchPotatoServer/CouchPotato.py` to start
|
* Then do `python CouchPotatoServer/CouchPotato.py` to start
|
||||||
* To run on boot copy the init script. `cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
|
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
|
||||||
* Change the paths inside the init script. `nano /etc/init.d/couchpotato`
|
* Change the paths inside the init script. `sudo nano /etc/init.d/couchpotato`
|
||||||
* Make it executable. `chmod +x /etc/init.d/couchpotato`
|
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
|
||||||
* Add it to defaults. `sudo update-rc.d couchpotato defaults`
|
* Add it to defaults. `sudo update-rc.d couchpotato defaults`
|
||||||
|
* Open your browser and go to: `http://localhost:5050/`
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#So you feel like posting a bug, sending me a pull request or just telling me how awesome I am. No problem!
|
||||||
|
|
||||||
|
##Just make sure you think of the following things:
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
* What hardware / OS are you using and what are the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example.
|
||||||
|
* I will mark issues with the "can't reproduce" tag. Don't go asking me "why closed" if it clearly says the issue in the tag ;)
|
||||||
|
|
||||||
|
**If I don't get enough info, the change of the issue getting closed is a lot bigger ;)**
|
||||||
+5
-2
@@ -1,6 +1,5 @@
|
|||||||
from flask.blueprints import Blueprint
|
from flask.blueprints import Blueprint
|
||||||
from flask.helpers import url_for
|
from flask.helpers import url_for
|
||||||
from tornado.ioloop import IOLoop
|
|
||||||
from tornado.web import RequestHandler, asynchronous
|
from tornado.web import RequestHandler, asynchronous
|
||||||
from werkzeug.utils import redirect
|
from werkzeug.utils import redirect
|
||||||
|
|
||||||
@@ -11,7 +10,11 @@ api_nonblock = {}
|
|||||||
|
|
||||||
|
|
||||||
class NonBlockHandler(RequestHandler):
|
class NonBlockHandler(RequestHandler):
|
||||||
stoppers = []
|
|
||||||
|
def __init__(self, application, request, **kwargs):
|
||||||
|
cls = NonBlockHandler
|
||||||
|
cls.stoppers = []
|
||||||
|
super(NonBlockHandler, self).__init__(application, request, **kwargs)
|
||||||
|
|
||||||
@asynchronous
|
@asynchronous
|
||||||
def get(self, route):
|
def get(self, route):
|
||||||
|
|||||||
@@ -23,19 +23,22 @@ config = [{
|
|||||||
'default': '',
|
'default': '',
|
||||||
'type': 'password',
|
'type': 'password',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
'name': 'host',
|
|
||||||
'advanced': True,
|
|
||||||
'default': '0.0.0.0',
|
|
||||||
'label': 'IP',
|
|
||||||
'description': 'Host that I should listen to. "0.0.0.0" listens to all ips.',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
'name': 'port',
|
'name': 'port',
|
||||||
'default': 5050,
|
'default': 5050,
|
||||||
'type': 'int',
|
'type': 'int',
|
||||||
'description': 'The port I should listen to.',
|
'description': 'The port I should listen to.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'ssl_cert',
|
||||||
|
'description': 'Path to SSL server.crt',
|
||||||
|
'advanced': True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'ssl_key',
|
||||||
|
'description': 'Path to SSL server.key',
|
||||||
|
'advanced': True,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'launch_browser',
|
'name': 'launch_browser',
|
||||||
'default': True,
|
'default': True,
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ from tornado.ioloop import IOLoop
|
|||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import webbrowser
|
import webbrowser
|
||||||
@@ -51,6 +53,9 @@ class Core(Plugin):
|
|||||||
addEvent('setting.save.core.password', self.md5Password)
|
addEvent('setting.save.core.password', self.md5Password)
|
||||||
addEvent('setting.save.core.api_key', self.checkApikey)
|
addEvent('setting.save.core.api_key', self.checkApikey)
|
||||||
|
|
||||||
|
# Make sure we can close-down with ctrl+c properly
|
||||||
|
if not Env.get('desktop'):
|
||||||
|
self.signalHandler()
|
||||||
|
|
||||||
def md5Password(self, value):
|
def md5Password(self, value):
|
||||||
return md5(value.encode(Env.get('encoding'))) if value else ''
|
return md5(value.encode(Env.get('encoding'))) if value else ''
|
||||||
@@ -66,7 +71,7 @@ class Core(Plugin):
|
|||||||
|
|
||||||
def available(self):
|
def available(self):
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'succes': True
|
'success': True
|
||||||
})
|
})
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
@@ -98,7 +103,7 @@ class Core(Plugin):
|
|||||||
|
|
||||||
self.shutdown_started = True
|
self.shutdown_started = True
|
||||||
|
|
||||||
fireEvent('app.shutdown')
|
fireEvent('app.do_shutdown')
|
||||||
log.debug('Every plugin got shutdown event')
|
log.debug('Every plugin got shutdown event')
|
||||||
|
|
||||||
loop = True
|
loop = True
|
||||||
@@ -170,3 +175,11 @@ class Core(Plugin):
|
|||||||
return jsonified({
|
return jsonified({
|
||||||
'version': self.version()
|
'version': self.version()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def signalHandler(self):
|
||||||
|
|
||||||
|
def signal_handler(signal, frame):
|
||||||
|
fireEvent('app.shutdown')
|
||||||
|
|
||||||
|
signal.signal(signal.SIGINT, signal_handler)
|
||||||
|
signal.signal(signal.SIGTERM, signal_handler)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from apscheduler.scheduler import Scheduler as Sched
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
import logging
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from .main import Updater
|
from .main import Updater
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
import os
|
||||||
|
|
||||||
def start():
|
def start():
|
||||||
return Updater()
|
return Updater()
|
||||||
@@ -33,6 +35,7 @@ config = [{
|
|||||||
{
|
{
|
||||||
'name': 'git_command',
|
'name': 'git_command',
|
||||||
'default': 'git',
|
'default': 'git',
|
||||||
|
'hidden': not os.path.isdir(os.path.join(Env.get('app_dir'), '.git')),
|
||||||
'advanced': True
|
'advanced': True
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -106,6 +106,10 @@ class Updater(Plugin):
|
|||||||
if success:
|
if success:
|
||||||
fireEventAsync('app.restart')
|
fireEventAsync('app.restart')
|
||||||
|
|
||||||
|
# Assume the updater handles things
|
||||||
|
if not success:
|
||||||
|
success = True
|
||||||
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'success': success
|
'success': success
|
||||||
})
|
})
|
||||||
@@ -396,6 +400,7 @@ class DesktopUpdater(BaseUpdater):
|
|||||||
self.update_failed = True
|
self.update_failed = True
|
||||||
|
|
||||||
self.desktop._esky.auto_update(callback = do_restart)
|
self.desktop._esky.auto_update(callback = do_restart)
|
||||||
|
return
|
||||||
except:
|
except:
|
||||||
self.update_failed = True
|
self.update_failed = True
|
||||||
|
|
||||||
@@ -406,7 +411,7 @@ class DesktopUpdater(BaseUpdater):
|
|||||||
'last_check': self.last_check,
|
'last_check': self.last_check,
|
||||||
'update_version': self.update_version,
|
'update_version': self.update_version,
|
||||||
'version': self.getVersion(),
|
'version': self.getVersion(),
|
||||||
'branch': 'desktop_build',
|
'branch': self.branch,
|
||||||
}
|
}
|
||||||
|
|
||||||
def check(self):
|
def check(self):
|
||||||
|
|||||||
@@ -90,17 +90,18 @@ var UpdaterBase = new Class({
|
|||||||
doUpdate: function(){
|
doUpdate: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
App.blockPage('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
|
||||||
Api.request('updater.update', {
|
Api.request('updater.update', {
|
||||||
'onComplete': function(json){
|
'onComplete': function(json){
|
||||||
if(json.success){
|
if(json.success)
|
||||||
self.updating();
|
self.updating();
|
||||||
}
|
else
|
||||||
|
App.unBlockPage()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updating: function(){
|
updating: function(){
|
||||||
App.blockPage('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
|
|
||||||
App.checkAvailable.delay(500, App, [1000, function(){
|
App.checkAvailable.delay(500, App, [1000, function(){
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}]);
|
}]);
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
config = {
|
||||||
|
'name': 'download_providers',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'label': 'Downloaders',
|
||||||
|
'description': 'You can select different downloaders for each type (usenet / torrent)',
|
||||||
|
'type': 'list',
|
||||||
|
'name': 'download_providers',
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'options': [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,51 +1,73 @@
|
|||||||
from base64 import b32decode, b16encode
|
from base64 import b32decode, b16encode
|
||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.encoding import toSafeString
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.providers.base import Provider
|
||||||
from couchpotato.environment import Env
|
|
||||||
import os
|
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Downloader(Plugin):
|
class Downloader(Provider):
|
||||||
|
|
||||||
type = []
|
type = []
|
||||||
|
http_time_between_calls = 0
|
||||||
|
|
||||||
torrent_sources = [
|
torrent_sources = [
|
||||||
'http://torrage.com/torrent/%s.torrent',
|
'http://torrage.com/torrent/%s.torrent',
|
||||||
'http://torrage.ws/torrent/%s.torrent',
|
|
||||||
'http://torcache.net/torrent/%s.torrent',
|
'http://torcache.net/torrent/%s.torrent',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
torrent_trackers = [
|
||||||
|
'http://tracker.publicbt.com/announce',
|
||||||
|
'udp://tracker.istole.it:80/announce',
|
||||||
|
'udp://fr33domtracker.h33t.com:3310/announce',
|
||||||
|
'http://tracker.istole.it/announce',
|
||||||
|
'http://tracker.ccc.de/announce',
|
||||||
|
'udp://tracker.publicbt.com:80/announce',
|
||||||
|
'udp://tracker.ccc.de:80/announce',
|
||||||
|
'http://exodus.desync.com/announce',
|
||||||
|
'http://exodus.desync.com:6969/announce',
|
||||||
|
'http://tracker.publichd.eu/announce',
|
||||||
|
'http://tracker.openbittorrent.com/announce',
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
addEvent('download', self.download)
|
addEvent('download', self._download)
|
||||||
addEvent('download.status', self.getDownloadStatus)
|
addEvent('download.enabled', self._isEnabled)
|
||||||
|
addEvent('download.enabled_types', self.getEnabledDownloadType)
|
||||||
|
addEvent('download.status', self._getAllDownloadStatus)
|
||||||
|
addEvent('download.remove_failed', self._removeFailed)
|
||||||
|
|
||||||
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
def getEnabledDownloadType(self):
|
||||||
pass
|
for download_type in self.type:
|
||||||
|
if self.isEnabled(manual = True, data = {'type': download_type}):
|
||||||
|
return self.type
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _download(self, data = {}, movie = {}, manual = False, filedata = None):
|
||||||
|
if self.isDisabled(manual, data):
|
||||||
|
return
|
||||||
|
return self.download(data = data, movie = movie, filedata = filedata)
|
||||||
|
|
||||||
|
def _getAllDownloadStatus(self):
|
||||||
|
if self.isDisabled(manual = True, data = {}):
|
||||||
|
return
|
||||||
|
|
||||||
|
return self.getAllDownloadStatus()
|
||||||
|
|
||||||
|
def _removeFailed(self, item):
|
||||||
|
if self.isDisabled(manual = True, data = {}):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.conf('delete_failed', default = True):
|
||||||
|
return self.removeFailed(item)
|
||||||
|
|
||||||
def getDownloadStatus(self, data = {}, movie = {}):
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def createNzbName(self, data, movie):
|
def removeFailed(self, item):
|
||||||
tag = self.cpTag(movie)
|
return
|
||||||
return '%s%s' % (toSafeString(data.get('name')[:127 - len(tag)]), tag)
|
|
||||||
|
|
||||||
def createFileName(self, data, filedata, movie):
|
|
||||||
name = os.path.join(self.createNzbName(data, movie))
|
|
||||||
if data.get('type') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
|
|
||||||
return '%s.%s' % (name, 'rar')
|
|
||||||
return '%s.%s' % (name, data.get('type'))
|
|
||||||
|
|
||||||
def cpTag(self, movie):
|
|
||||||
if Env.setting('enabled', 'renamer'):
|
|
||||||
return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
|
|
||||||
|
|
||||||
return ''
|
|
||||||
|
|
||||||
def isCorrectType(self, item_type):
|
def isCorrectType(self, item_type):
|
||||||
is_correct = item_type in self.type
|
is_correct = item_type in self.type
|
||||||
@@ -56,7 +78,7 @@ class Downloader(Plugin):
|
|||||||
return is_correct
|
return is_correct
|
||||||
|
|
||||||
def magnetToTorrent(self, magnet_link):
|
def magnetToTorrent(self, magnet_link):
|
||||||
torrent_hash = re.findall('urn:btih:([\w]{32,40})', magnet_link)[0]
|
torrent_hash = re.findall('urn:btih:([\w]{32,40})', magnet_link)[0].upper()
|
||||||
|
|
||||||
# Convert base 32 to hex
|
# Convert base 32 to hex
|
||||||
if len(torrent_hash) == 32:
|
if len(torrent_hash) == 32:
|
||||||
@@ -78,9 +100,16 @@ class Downloader(Plugin):
|
|||||||
log.error('Failed converting magnet url to torrent: %s', (torrent_hash))
|
log.error('Failed converting magnet url to torrent: %s', (torrent_hash))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def isDisabled(self, manual):
|
def isDisabled(self, manual, data):
|
||||||
return not self.isEnabled(manual)
|
return not self.isEnabled(manual, data)
|
||||||
|
|
||||||
def isEnabled(self, manual):
|
def _isEnabled(self, manual, data = {}):
|
||||||
|
if not self.isEnabled(manual, data):
|
||||||
|
return
|
||||||
|
return True
|
||||||
|
|
||||||
|
def isEnabled(self, manual, data = {}):
|
||||||
d_manual = self.conf('manual', default = False)
|
d_manual = self.conf('manual', default = False)
|
||||||
return super(Downloader, self).isEnabled() and ((d_manual and manual) or (d_manual is False))
|
return super(Downloader, self).isEnabled() and \
|
||||||
|
((d_manual and manual) or (d_manual is False)) and \
|
||||||
|
(not data or self.isCorrectType(data.get('type')))
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from .main import Blackhole
|
from .main import Blackhole
|
||||||
|
from couchpotato.core.helpers.variable import getDownloadDir
|
||||||
|
|
||||||
def start():
|
def start():
|
||||||
return Blackhole()
|
return Blackhole()
|
||||||
@@ -9,6 +10,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'downloaders',
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
'name': 'blackhole',
|
'name': 'blackhole',
|
||||||
'label': 'Black hole',
|
'label': 'Black hole',
|
||||||
'description': 'Download the NZB/Torrent to a specific folder.',
|
'description': 'Download the NZB/Torrent to a specific folder.',
|
||||||
@@ -16,7 +18,7 @@ config = [{
|
|||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'enabled',
|
'name': 'enabled',
|
||||||
'default': 0,
|
'default': True,
|
||||||
'type': 'enabler',
|
'type': 'enabler',
|
||||||
'radio_group': 'nzb,torrent',
|
'radio_group': 'nzb,torrent',
|
||||||
},
|
},
|
||||||
@@ -24,6 +26,7 @@ config = [{
|
|||||||
'name': 'directory',
|
'name': 'directory',
|
||||||
'type': 'directory',
|
'type': 'directory',
|
||||||
'description': 'Directory where the .nzb (or .torrent) file is saved to.',
|
'description': 'Directory where the .nzb (or .torrent) file is saved to.',
|
||||||
|
'default': getDownloadDir()
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'use_for',
|
'name': 'use_for',
|
||||||
|
|||||||
@@ -10,11 +10,7 @@ class Blackhole(Downloader):
|
|||||||
|
|
||||||
type = ['nzb', 'torrent', 'torrent_magnet']
|
type = ['nzb', 'torrent', 'torrent_magnet']
|
||||||
|
|
||||||
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
def download(self, data = {}, movie = {}, filedata = None):
|
||||||
if self.isDisabled(manual) or \
|
|
||||||
(not self.isCorrectType(data.get('type')) or \
|
|
||||||
(not self.conf('use_for') in ['both', 'torrent' if 'torrent' in data.get('type') else data.get('type')])):
|
|
||||||
return
|
|
||||||
|
|
||||||
directory = self.conf('directory')
|
directory = self.conf('directory')
|
||||||
if not directory or not os.path.isdir(directory):
|
if not directory or not os.path.isdir(directory):
|
||||||
@@ -52,4 +48,17 @@ class Blackhole(Downloader):
|
|||||||
except:
|
except:
|
||||||
log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc()))
|
log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc()))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def getEnabledDownloadType(self):
|
||||||
|
if self.conf('use_for') == 'both':
|
||||||
|
return super(Blackhole, self).getEnabledDownloadType()
|
||||||
|
elif self.conf('use_for') == 'torrent':
|
||||||
|
return ['torrent', 'torrent_magnet']
|
||||||
|
else:
|
||||||
|
return ['nzb']
|
||||||
|
|
||||||
|
def isEnabled(self, manual, data = {}):
|
||||||
|
return super(Blackhole, self).isEnabled(manual, data) and \
|
||||||
|
((self.conf('use_for') in ['both', 'torrent' if 'torrent' in data.get('type') else data.get('type')]))
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'downloaders',
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
'name': 'nzbget',
|
'name': 'nzbget',
|
||||||
'label': 'NZBGet',
|
'label': 'NZBGet',
|
||||||
'description': 'Send NZBs to your NZBGet installation.',
|
'description': 'Use <a href="http://nzbget.sourceforge.net/Main_Page" target="_blank">NZBGet</a> to download NZBs.',
|
||||||
'wizard': True,
|
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'enabled',
|
'name': 'enabled',
|
||||||
@@ -26,6 +26,7 @@ config = [{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'password',
|
'name': 'password',
|
||||||
|
'type': 'password',
|
||||||
'description': 'Default NZBGet password is <i>tegbzn6789</i>',
|
'description': 'Default NZBGet password is <i>tegbzn6789</i>',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -33,6 +34,13 @@ config = [{
|
|||||||
'default': 'Movies',
|
'default': 'Movies',
|
||||||
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
|
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'priority',
|
||||||
|
'default': '0',
|
||||||
|
'type': 'dropdown',
|
||||||
|
'values': [('Very Low', -100), ('Low', -50), ('Normal', 0), ('High', 50), ('Very High', 100)],
|
||||||
|
'description': 'Only change this if you are using NZBget 9.0 or higher',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'manual',
|
'name': 'manual',
|
||||||
'default': 0,
|
'default': 0,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from base64 import standard_b64encode
|
from base64 import standard_b64encode
|
||||||
from couchpotato.core.downloaders.base import Downloader
|
from couchpotato.core.downloaders.base import Downloader
|
||||||
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from inspect import isfunction
|
import re
|
||||||
import socket
|
import socket
|
||||||
import traceback
|
import traceback
|
||||||
import xmlrpclib
|
import xmlrpclib
|
||||||
@@ -14,10 +15,7 @@ class NZBGet(Downloader):
|
|||||||
|
|
||||||
url = 'http://nzbget:%(password)s@%(host)s/xmlrpc'
|
url = 'http://nzbget:%(password)s@%(host)s/xmlrpc'
|
||||||
|
|
||||||
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
def download(self, data = {}, movie = {}, filedata = None):
|
||||||
|
|
||||||
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
|
|
||||||
return
|
|
||||||
|
|
||||||
if not filedata:
|
if not filedata:
|
||||||
log.error('Unable to get NZB file: %s', traceback.format_exc())
|
log.error('Unable to get NZB file: %s', traceback.format_exc())
|
||||||
@@ -44,7 +42,12 @@ class NZBGet(Downloader):
|
|||||||
log.error('Protocol Error: %s', e)
|
log.error('Protocol Error: %s', e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if rpc.append(nzb_name, self.conf('category'), False, standard_b64encode(filedata.strip())):
|
if re.search(r"^0", rpc.version()):
|
||||||
|
xml_response = rpc.append(nzb_name, self.conf('category'), False, standard_b64encode(filedata.strip()))
|
||||||
|
else:
|
||||||
|
xml_response = rpc.append(nzb_name, self.conf('category'), tryInt(self.conf('priority')), False, standard_b64encode(filedata.strip()))
|
||||||
|
|
||||||
|
if xml_response:
|
||||||
log.info('NZB sent successfully to NZBGet')
|
log.info('NZB sent successfully to NZBGet')
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
from .main import NZBVortex
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return NZBVortex()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'nzbvortex',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
|
'name': 'nzbvortex',
|
||||||
|
'label': 'NZBVortex',
|
||||||
|
'description': 'Use <a href="http://www.nzbvortex.com/landing/" target="_blank">NZBVortex</a> to download NZBs.',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
'radio_group': 'nzb',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'host',
|
||||||
|
'default': 'https://localhost:4321',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'api_key',
|
||||||
|
'label': 'Api Key',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'manual',
|
||||||
|
'default': False,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'delete_failed',
|
||||||
|
'default': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Delete a release after the download has failed.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
from base64 import b64encode
|
||||||
|
from couchpotato.core.downloaders.base import Downloader
|
||||||
|
from couchpotato.core.helpers.encoding import tryUrlencode, ss
|
||||||
|
from couchpotato.core.helpers.variable import cleanHost
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from urllib2 import URLError
|
||||||
|
from uuid import uuid4
|
||||||
|
import hashlib
|
||||||
|
import httplib
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
import urllib2
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
class NZBVortex(Downloader):
|
||||||
|
|
||||||
|
type = ['nzb']
|
||||||
|
api_level = None
|
||||||
|
session_id = None
|
||||||
|
|
||||||
|
def download(self, data = {}, movie = {}, filedata = None):
|
||||||
|
|
||||||
|
# Send the nzb
|
||||||
|
try:
|
||||||
|
nzb_filename = self.createFileName(data, filedata, movie)
|
||||||
|
self.call('nzb/add', params = {'file': (ss(nzb_filename), filedata)}, multipart = True)
|
||||||
|
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())
|
||||||
|
return False
|
||||||
|
|
||||||
|
def getAllDownloadStatus(self):
|
||||||
|
|
||||||
|
raw_statuses = self.call('nzb')
|
||||||
|
|
||||||
|
statuses = []
|
||||||
|
for item in raw_statuses.get('nzbs', []):
|
||||||
|
|
||||||
|
# Check status
|
||||||
|
status = 'busy'
|
||||||
|
if item['state'] == 20:
|
||||||
|
status = 'completed'
|
||||||
|
elif item['state'] in [21, 22, 24]:
|
||||||
|
status = 'failed'
|
||||||
|
|
||||||
|
statuses.append({
|
||||||
|
'id': item['id'],
|
||||||
|
'name': item['uiTitle'],
|
||||||
|
'status': status,
|
||||||
|
'original_status': item['state'],
|
||||||
|
'timeleft':-1,
|
||||||
|
})
|
||||||
|
|
||||||
|
return statuses
|
||||||
|
|
||||||
|
def removeFailed(self, item):
|
||||||
|
|
||||||
|
log.info('%s failed downloading, deleting...', item['name'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.call('nzb/%s/cancel' % item['id'])
|
||||||
|
except:
|
||||||
|
log.error('Failed deleting: %s', traceback.format_exc(0))
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
|
||||||
|
nonce = self.call('auth/nonce', auth = False).get('authNonce')
|
||||||
|
cnonce = uuid4().hex
|
||||||
|
hashed = b64encode(hashlib.sha256('%s:%s:%s' % (nonce, cnonce, self.conf('api_key'))).digest())
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'nonce': nonce,
|
||||||
|
'cnonce': cnonce,
|
||||||
|
'hash': hashed
|
||||||
|
}
|
||||||
|
|
||||||
|
login_data = self.call('auth/login', parameters = params, auth = False)
|
||||||
|
|
||||||
|
# Save for later
|
||||||
|
if login_data.get('loginResult') == 'successful':
|
||||||
|
self.session_id = login_data.get('sessionID')
|
||||||
|
return True
|
||||||
|
|
||||||
|
log.error('Login failed, please check you api-key')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def call(self, call, parameters = {}, repeat = False, auth = True, *args, **kwargs):
|
||||||
|
|
||||||
|
# Login first
|
||||||
|
if not self.session_id and auth:
|
||||||
|
self.login()
|
||||||
|
|
||||||
|
# Always add session id to request
|
||||||
|
if self.session_id:
|
||||||
|
parameters['sessionid'] = self.session_id
|
||||||
|
|
||||||
|
params = tryUrlencode(parameters)
|
||||||
|
|
||||||
|
url = cleanHost(self.conf('host')) + 'api/' + call
|
||||||
|
url_opener = urllib2.build_opener(HTTPSHandler())
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = self.urlopen('%s?%s' % (url, params), opener = url_opener, *args, **kwargs)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
return json.loads(data)
|
||||||
|
except URLError, e:
|
||||||
|
if hasattr(e, 'code') and e.code == 403:
|
||||||
|
# Try login and do again
|
||||||
|
if not repeat:
|
||||||
|
self.login()
|
||||||
|
return self.call(call, parameters = parameters, repeat = True, *args, **kwargs)
|
||||||
|
|
||||||
|
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
except:
|
||||||
|
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def getApiLevel(self):
|
||||||
|
|
||||||
|
if not self.api_level:
|
||||||
|
|
||||||
|
url = cleanHost(self.conf('host')) + 'api/app/apilevel'
|
||||||
|
url_opener = urllib2.build_opener(HTTPSHandler())
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = self.urlopen(url, opener = url_opener, show_error = False)
|
||||||
|
self.api_level = float(json.loads(data).get('apilevel'))
|
||||||
|
except URLError, e:
|
||||||
|
if hasattr(e, 'code') and e.code == 403:
|
||||||
|
log.error('This version of NZBVortex isn\'t supported. Please update to 2.8.6 or higher')
|
||||||
|
else:
|
||||||
|
log.error('NZBVortex doesn\'t seem to be running or maybe the remote option isn\'t enabled yet: %s', traceback.format_exc(1))
|
||||||
|
|
||||||
|
return self.api_level
|
||||||
|
|
||||||
|
def isEnabled(self, manual, data):
|
||||||
|
return super(NZBVortex, self).isEnabled(manual, data) and self.getApiLevel()
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPSConnection(httplib.HTTPSConnection):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
httplib.HTTPSConnection.__init__(self, *args, **kwargs)
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
sock = socket.create_connection((self.host, self.port), self.timeout)
|
||||||
|
if sys.version_info < (2, 6, 7):
|
||||||
|
if hasattr(self, '_tunnel_host'):
|
||||||
|
self.sock = sock
|
||||||
|
self._tunnel()
|
||||||
|
else:
|
||||||
|
if self._tunnel_host:
|
||||||
|
self.sock = sock
|
||||||
|
self._tunnel()
|
||||||
|
|
||||||
|
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version = ssl.PROTOCOL_TLSv1)
|
||||||
|
|
||||||
|
class HTTPSHandler(urllib2.HTTPSHandler):
|
||||||
|
def https_open(self, req):
|
||||||
|
return self.do_open(HTTPSConnection, req)
|
||||||
@@ -9,10 +9,10 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'downloaders',
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
'name': 'pneumatic',
|
'name': 'pneumatic',
|
||||||
'label': 'Pneumatic',
|
'label': 'Pneumatic',
|
||||||
'description': 'Download the .strm file to a specific folder.',
|
'description': 'Use <a href="http://forum.xbmc.org/showthread.php?tid=97657" target="_blank">Pneumatic</a> to download .strm files.',
|
||||||
'wizard': True,
|
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'enabled',
|
'name': 'enabled',
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ class Pneumatic(Downloader):
|
|||||||
type = ['nzb']
|
type = ['nzb']
|
||||||
strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s'
|
strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s'
|
||||||
|
|
||||||
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
def download(self, data = {}, movie = {}, filedata = None):
|
||||||
if self.isDisabled(manual) or (not self.isCorrectType(data.get('type'))):
|
|
||||||
return
|
|
||||||
|
|
||||||
directory = self.conf('directory')
|
directory = self.conf('directory')
|
||||||
if not directory or not os.path.isdir(directory):
|
if not directory or not os.path.isdir(directory):
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'downloaders',
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
'name': 'sabnzbd',
|
'name': 'sabnzbd',
|
||||||
'label': 'Sabnzbd',
|
'label': 'Sabnzbd',
|
||||||
'description': 'Send NZBs to your Sabnzbd installation.',
|
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> to download NZBs.',
|
||||||
'wizard': True,
|
'wizard': True,
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from couchpotato.core.downloaders.base import Downloader
|
from couchpotato.core.downloaders.base import Downloader
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
from couchpotato.core.helpers.encoding import tryUrlencode, ss
|
||||||
from couchpotato.core.helpers.variable import cleanHost
|
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
import traceback
|
from urllib2 import URLError
|
||||||
import json
|
import json
|
||||||
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -11,10 +12,7 @@ class Sabnzbd(Downloader):
|
|||||||
|
|
||||||
type = ['nzb']
|
type = ['nzb']
|
||||||
|
|
||||||
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
def download(self, data = {}, movie = {}, filedata = None):
|
||||||
|
|
||||||
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
|
|
||||||
return
|
|
||||||
|
|
||||||
log.info('Sending "%s" to SABnzbd.', data.get('name'))
|
log.info('Sending "%s" to SABnzbd.', data.get('name'))
|
||||||
|
|
||||||
@@ -27,7 +25,7 @@ class Sabnzbd(Downloader):
|
|||||||
|
|
||||||
if filedata:
|
if filedata:
|
||||||
if len(filedata) < 50:
|
if len(filedata) < 50:
|
||||||
log.error('No proper nzb available!')
|
log.error('No proper nzb available: %s', (filedata))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
|
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
|
||||||
@@ -36,126 +34,119 @@ class Sabnzbd(Downloader):
|
|||||||
else:
|
else:
|
||||||
params['name'] = data.get('url')
|
params['name'] = data.get('url')
|
||||||
|
|
||||||
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
|
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(params)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if params.get('mode') is 'addfile':
|
if params.get('mode') is 'addfile':
|
||||||
sab = self.urlopen(url, timeout = 60, params = {"nzbfile": (nzb_filename, filedata)}, multipart = True, show_error = False)
|
sab = self.urlopen(url, timeout = 60, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True, show_error = False)
|
||||||
else:
|
else:
|
||||||
sab = self.urlopen(url, timeout = 60, show_error = False)
|
sab = self.urlopen(url, timeout = 60, show_error = False)
|
||||||
|
except URLError:
|
||||||
|
log.error('Failed sending release, probably wrong HOST: %s', traceback.format_exc(0))
|
||||||
|
return False
|
||||||
except:
|
except:
|
||||||
log.error('Failed sending release: %s', traceback.format_exc())
|
log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
result = sab.strip()
|
result = sab.strip()
|
||||||
if not result:
|
if not result:
|
||||||
log.error("SABnzbd didn't return anything.")
|
log.error('SABnzbd didn\'t return anything.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
log.debug("Result text from SAB: " + result[:40])
|
log.debug('Result text from SAB: %s', result[:40])
|
||||||
if result == "ok":
|
if result[:2] == 'ok':
|
||||||
log.info("NZB sent to SAB successfully.")
|
log.info('NZB sent to SAB successfully.')
|
||||||
return True
|
return True
|
||||||
elif result == "Missing authentication":
|
|
||||||
log.error("Incorrect username/password.")
|
|
||||||
return False
|
|
||||||
else:
|
else:
|
||||||
log.error("Unknown error: " + result[:40])
|
log.error(result[:40])
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def getDownloadStatus(self, data = {}, movie = {}):
|
def getAllDownloadStatus(self):
|
||||||
if self.isDisabled(manual = True) or not self.isCorrectType(data.get('type')):
|
|
||||||
return
|
|
||||||
|
|
||||||
nzbname = self.createNzbName(data, movie)
|
log.debug('Checking SABnzbd download status.')
|
||||||
log.info('Checking download status of "%s" at SABnzbd.', nzbname)
|
|
||||||
|
|
||||||
# Go through Queue
|
# Go through Queue
|
||||||
params = {
|
|
||||||
'apikey': self.conf('api_key'),
|
|
||||||
'mode': 'queue',
|
|
||||||
'output': 'json'
|
|
||||||
}
|
|
||||||
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sab = self.urlopen(url, timeout = 60, show_error = False)
|
queue = self.call({
|
||||||
|
'mode': 'queue',
|
||||||
|
})
|
||||||
except:
|
except:
|
||||||
log.error('Failed checking status: %s', traceback.format_exc())
|
log.error('Failed getting queue: %s', traceback.format_exc(1))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
|
||||||
history = json.loads(sab)
|
|
||||||
except:
|
|
||||||
log.debug("Result text from SAB: " + sab[:40])
|
|
||||||
log.error('Failed parsing json status: %s', traceback.format_exc())
|
|
||||||
return False
|
|
||||||
|
|
||||||
for slot in history['queue']['slots']:
|
|
||||||
log.debug('Found %s in SabNZBd queue, which is %s, with %s left', (slot['filename'], slot['status'], slot['timeleft']))
|
|
||||||
if slot['filename'] == nzbname:
|
|
||||||
return slot['status'].lower()
|
|
||||||
|
|
||||||
# Go through history items
|
# Go through history items
|
||||||
params = {
|
try:
|
||||||
'apikey': self.conf('api_key'),
|
history = self.call({
|
||||||
'mode': 'history',
|
'mode': 'history',
|
||||||
'output': 'json'
|
'limit': 15,
|
||||||
}
|
})
|
||||||
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
|
except:
|
||||||
|
log.error('Failed getting history json: %s', traceback.format_exc(1))
|
||||||
|
return False
|
||||||
|
|
||||||
|
statuses = []
|
||||||
|
|
||||||
|
# Get busy releases
|
||||||
|
for item in queue.get('slots', []):
|
||||||
|
statuses.append({
|
||||||
|
'id': item['nzo_id'],
|
||||||
|
'name': item['filename'],
|
||||||
|
'status': 'busy',
|
||||||
|
'original_status': item['status'],
|
||||||
|
'timeleft': item['timeleft'] if not queue['paused'] else -1,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Get old releases
|
||||||
|
for item in history.get('slots', []):
|
||||||
|
|
||||||
|
status = 'busy'
|
||||||
|
if item['status'] == 'Failed' or (item['status'] == 'Completed' and item['fail_message'].strip()):
|
||||||
|
status = 'failed'
|
||||||
|
elif item['status'] == 'Completed':
|
||||||
|
status = 'completed'
|
||||||
|
|
||||||
|
statuses.append({
|
||||||
|
'id': item['nzo_id'],
|
||||||
|
'name': item['name'],
|
||||||
|
'status': status,
|
||||||
|
'original_status': item['status'],
|
||||||
|
'timeleft': 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
return statuses
|
||||||
|
|
||||||
|
def removeFailed(self, item):
|
||||||
|
|
||||||
|
log.info('%s failed downloading, deleting...', item['name'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sab = self.urlopen(url, timeout = 60, show_error = False)
|
self.call({
|
||||||
|
'mode': 'history',
|
||||||
|
'name': 'delete',
|
||||||
|
'del_files': '1',
|
||||||
|
'value': item['id']
|
||||||
|
}, use_json = False)
|
||||||
except:
|
except:
|
||||||
log.error('Failed getting history: %s', traceback.format_exc())
|
log.error('Failed deleting: %s', traceback.format_exc(0))
|
||||||
return
|
return False
|
||||||
|
|
||||||
try:
|
return True
|
||||||
history = json.loads(sab)
|
|
||||||
except:
|
|
||||||
log.debug("Result text from SAB: " + sab[:40])
|
|
||||||
log.error('Failed parsing history json: %s', traceback.format_exc())
|
|
||||||
return
|
|
||||||
|
|
||||||
for slot in history['history']['slots']:
|
def call(self, params, use_json = True):
|
||||||
log.debug('Found %s in SabNZBd history, which has %s', (slot['name'], slot['status']))
|
|
||||||
if slot['name'] == nzbname:
|
|
||||||
# Note: if post process even if failed is on in SabNZBd, it will complete with a fail message
|
|
||||||
if slot['status'] == 'Failed' or (slot['status'] == 'Completed' and slot['fail_message'].strip()):
|
|
||||||
|
|
||||||
# Delete failed download
|
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(params, {
|
||||||
if self.conf('delete_failed', default = True):
|
'apikey': self.conf('api_key'),
|
||||||
|
'output': 'json'
|
||||||
|
}))
|
||||||
|
|
||||||
log.info('%s failed downloading, deleting...', slot['name'])
|
data = self.urlopen(url, timeout = 60, show_error = False)
|
||||||
params = {
|
if use_json:
|
||||||
'apikey': self.conf('api_key'),
|
d = json.loads(data)
|
||||||
'mode': 'history',
|
if d.get('error'):
|
||||||
'name': 'delete',
|
log.error('Error getting data from SABNZBd: %s', d.get('error'))
|
||||||
'del_files': '1',
|
return {}
|
||||||
'value': slot['nzo_id']
|
|
||||||
}
|
|
||||||
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
|
|
||||||
|
|
||||||
try:
|
return d[params['mode']]
|
||||||
sab = self.urlopen(url, timeout = 60, show_error = False)
|
else:
|
||||||
except:
|
return data
|
||||||
log.error('Failed deleting: %s', traceback.format_exc())
|
|
||||||
return False
|
|
||||||
|
|
||||||
result = sab.strip()
|
|
||||||
if not result:
|
|
||||||
log.error("SABnzbd didn't return anything.")
|
|
||||||
|
|
||||||
log.debug("Result text from SAB: " + result[:40])
|
|
||||||
if result == "ok":
|
|
||||||
log.info('SabNZBd deleted failed release %s successfully.', slot['name'])
|
|
||||||
elif result == "Missing authentication":
|
|
||||||
log.error("Incorrect username/password or API?.")
|
|
||||||
else:
|
|
||||||
log.error("Unknown error: " + result[:40])
|
|
||||||
|
|
||||||
return 'failed'
|
|
||||||
else:
|
|
||||||
return slot['status'].lower()
|
|
||||||
|
|
||||||
return 'not_found'
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
from .main import Synology
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Synology()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'synology',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
|
'name': 'synology',
|
||||||
|
'label': 'Synology',
|
||||||
|
'description': 'Use <a href="http://www.synology.com/dsm/home_home_applications_download_station.php" target="_blank">Synology Download Station</a> to download.',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
'radio_group': 'torrent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'host',
|
||||||
|
'default': 'localhost:5000',
|
||||||
|
'description': 'Hostname with port. Usually <strong>localhost:5000</strong>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'manual',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
from couchpotato.core.downloaders.base import Downloader
|
||||||
|
from couchpotato.core.helpers.encoding import isInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
import httplib
|
||||||
|
import json
|
||||||
|
import urllib
|
||||||
|
import urllib2
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
class Synology(Downloader):
|
||||||
|
|
||||||
|
type = ['torrent_magnet']
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
def download(self, data, movie, filedata = None):
|
||||||
|
|
||||||
|
log.error('Sending "%s" (%s) to Synology.', (data.get('name'), data.get('type')))
|
||||||
|
|
||||||
|
# Load host from config and split out port.
|
||||||
|
host = self.conf('host').split(':')
|
||||||
|
if not isInt(host[1]):
|
||||||
|
log.error('Config properties are not filled in correctly, port is missing.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
if data.get('type') == 'torrent':
|
||||||
|
log.error('Can\'t add binary torrent file')
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Send request to Transmission
|
||||||
|
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
|
||||||
|
remote_torrent = srpc.add_torrent_uri(data.get('url'))
|
||||||
|
log.info('Response: %s', remote_torrent)
|
||||||
|
return remote_torrent['success']
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Exception while adding torrent: %s', err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class SynologyRPC(object):
|
||||||
|
|
||||||
|
'''SynologyRPC lite library'''
|
||||||
|
|
||||||
|
def __init__(self, host = 'localhost', port = 5000, username = None, password = None):
|
||||||
|
|
||||||
|
super(SynologyRPC, self).__init__()
|
||||||
|
|
||||||
|
self.download_url = 'http://%s:%s/webapi/DownloadStation/task.cgi' % (host, port)
|
||||||
|
self.auth_url = 'http://%s:%s/webapi/auth.cgi' % (host, port)
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
self.session_name = 'DownloadStation'
|
||||||
|
|
||||||
|
def _login(self):
|
||||||
|
if self.username and self.password:
|
||||||
|
args = {'api': 'SYNO.API.Auth', 'account': self.username, 'passwd': self.password, 'version': 2,
|
||||||
|
'method': 'login', 'session': self.session_name, 'format': 'sid'}
|
||||||
|
response = self._req(self.auth_url, args)
|
||||||
|
if response['success'] == True:
|
||||||
|
self.sid = response['data']['sid']
|
||||||
|
log.debug('Sid=%s', self.sid)
|
||||||
|
return response
|
||||||
|
elif self.username or self.password:
|
||||||
|
log.error('User or password missing, not using authentication.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _logout(self):
|
||||||
|
args = {'api':'SYNO.API.Auth', 'version':1, 'method':'logout', 'session':self.session_name, '_sid':self.sid}
|
||||||
|
return self._req(self.auth_url, args)
|
||||||
|
|
||||||
|
def _req(self, url, args):
|
||||||
|
req_url = url + '?' + urllib.urlencode(args)
|
||||||
|
try:
|
||||||
|
req_open = urllib2.urlopen(req_url)
|
||||||
|
response = json.loads(req_open.read())
|
||||||
|
if response['success'] == True:
|
||||||
|
log.info('Synology action successfull')
|
||||||
|
return response
|
||||||
|
except httplib.InvalidURL, err:
|
||||||
|
log.error('Invalid Transmission host, check your config %s', err)
|
||||||
|
return False
|
||||||
|
except urllib2.HTTPError, err:
|
||||||
|
log.error('SynologyRPC HTTPError: %s', err)
|
||||||
|
return False
|
||||||
|
except urllib2.URLError, err:
|
||||||
|
log.error('Unable to connect to Synology %s', err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def add_torrent_uri(self, torrent):
|
||||||
|
log.info('Adding torrent URL %s', torrent)
|
||||||
|
response = {}
|
||||||
|
# login
|
||||||
|
login = self._login()
|
||||||
|
if len(login) > 0 and login['success'] == True:
|
||||||
|
log.info('Login success, adding torrent')
|
||||||
|
args = {'api':'SYNO.DownloadStation.Task', 'version':1, 'method':'create', 'uri':torrent, '_sid':self.sid}
|
||||||
|
response = self._req(self.download_url, args)
|
||||||
|
self._logout()
|
||||||
|
else:
|
||||||
|
log.error('Couldn\'t login to Synology, %s', login)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@@ -8,9 +8,10 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'downloaders',
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
'name': 'transmission',
|
'name': 'transmission',
|
||||||
'label': 'Transmission',
|
'label': 'Transmission',
|
||||||
'description': 'Send torrents to Transmission.',
|
'description': 'Use <a href="http://www.transmissionbt.com/" target="_blank">Transmission</a> to download torrents.',
|
||||||
'wizard': True,
|
'wizard': True,
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ class Transmission(Downloader):
|
|||||||
type = ['torrent', 'torrent_magnet']
|
type = ['torrent', 'torrent_magnet']
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
def download(self, data, movie, manual = False, filedata = None):
|
def download(self, data, movie, filedata = None):
|
||||||
|
|
||||||
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
|
|
||||||
return
|
|
||||||
|
|
||||||
log.debug('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
|
log.debug('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
|
||||||
|
|
||||||
@@ -30,15 +27,22 @@ class Transmission(Downloader):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# Set parameters for Transmission
|
# Set parameters for Transmission
|
||||||
|
folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1]
|
||||||
|
folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep)
|
||||||
|
|
||||||
|
# Create the empty folder to download too
|
||||||
|
self.makeDir(folder_path)
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
'paused': self.conf('paused', default = 0),
|
'paused': self.conf('paused', default = 0),
|
||||||
'download-dir': self.conf('directory', default = '').rstrip(os.path.sep)
|
'download-dir': folder_path
|
||||||
}
|
}
|
||||||
|
|
||||||
torrent_params = {
|
if self.conf('ratio'):
|
||||||
'seedRatioLimit': self.conf('ratio'),
|
torrent_params = {
|
||||||
'seedRatioMode': (0 if self.conf('ratio') else 1)
|
'seedRatioLimit': self.conf('ratio'),
|
||||||
}
|
'seedRatioMode': self.conf('ratio')
|
||||||
|
}
|
||||||
|
|
||||||
if not filedata and data.get('type') == 'torrent':
|
if not filedata and data.get('type') == 'torrent':
|
||||||
log.error('Failed sending torrent, no data')
|
log.error('Failed sending torrent, no data')
|
||||||
@@ -49,6 +53,7 @@ class Transmission(Downloader):
|
|||||||
trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||||
if data.get('type') == 'torrent_magnet':
|
if data.get('type') == 'torrent_magnet':
|
||||||
remote_torrent = trpc.add_torrent_uri(data.get('url'), arguments = params)
|
remote_torrent = trpc.add_torrent_uri(data.get('url'), arguments = params)
|
||||||
|
torrent_params['trackerAdd'] = self.torrent_trackers
|
||||||
else:
|
else:
|
||||||
remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params)
|
remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
from .main import uTorrent
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return uTorrent()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'utorrent',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'downloaders',
|
||||||
|
'list': 'download_providers',
|
||||||
|
'name': 'utorrent',
|
||||||
|
'label': 'uTorrent',
|
||||||
|
'description': 'Use <a href="http://www.utorrent.com/" target="_blank">uTorrent</a> to download torrents.',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
'radio_group': 'torrent',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'host',
|
||||||
|
'default': 'localhost:8000',
|
||||||
|
'description': 'Hostname with port. Usually <strong>localhost:8000</strong>',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'label',
|
||||||
|
'description': 'Label to add torrent as.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'paused',
|
||||||
|
'type': 'bool',
|
||||||
|
'default': False,
|
||||||
|
'description': 'Add the torrent paused.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'manual',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
from bencode import bencode, bdecode
|
||||||
|
from couchpotato.core.downloaders.base import Downloader
|
||||||
|
from couchpotato.core.helpers.encoding import isInt, ss
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from hashlib import sha1
|
||||||
|
from multipartpost import MultipartPostHandler
|
||||||
|
import cookielib
|
||||||
|
import httplib
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import urllib
|
||||||
|
import urllib2
|
||||||
|
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class uTorrent(Downloader):
|
||||||
|
|
||||||
|
type = ['torrent', 'torrent_magnet']
|
||||||
|
utorrent_api = None
|
||||||
|
|
||||||
|
def download(self, data, movie, filedata = None):
|
||||||
|
|
||||||
|
log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('type')))
|
||||||
|
|
||||||
|
# Load host from config and split out port.
|
||||||
|
host = self.conf('host').split(':')
|
||||||
|
if not isInt(host[1]):
|
||||||
|
log.error('Config properties are not filled in correctly, port is missing.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
torrent_params = {}
|
||||||
|
if self.conf('label'):
|
||||||
|
torrent_params['label'] = self.conf('label')
|
||||||
|
|
||||||
|
if not filedata and data.get('type') == 'torrent':
|
||||||
|
log.error('Failed sending torrent, no data')
|
||||||
|
return False
|
||||||
|
if data.get('type') == 'torrent_magnet':
|
||||||
|
torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper()
|
||||||
|
torrent_params['trackers'] = '%0D%0A%0D%0A'.join(self.torrent_trackers)
|
||||||
|
else:
|
||||||
|
info = bdecode(filedata)["info"]
|
||||||
|
torrent_hash = sha1(bencode(info)).hexdigest().upper()
|
||||||
|
torrent_filename = self.createFileName(data, filedata, movie)
|
||||||
|
|
||||||
|
# Send request to uTorrent
|
||||||
|
try:
|
||||||
|
if not self.utorrent_api:
|
||||||
|
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||||
|
|
||||||
|
if data.get('type') == 'torrent_magnet':
|
||||||
|
self.utorrent_api.add_torrent_uri(data.get('url'))
|
||||||
|
else:
|
||||||
|
self.utorrent_api.add_torrent_file(torrent_filename, filedata)
|
||||||
|
|
||||||
|
# Change settings of added torrents
|
||||||
|
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
|
||||||
|
if self.conf('paused', default = 0):
|
||||||
|
self.utorrent_api.pause_torrent(torrent_hash)
|
||||||
|
return True
|
||||||
|
except Exception, err:
|
||||||
|
log.error('Failed to send torrent to uTorrent: %s', err)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class uTorrentAPI(object):
|
||||||
|
|
||||||
|
def __init__(self, host = 'localhost', port = 8000, username = None, password = None):
|
||||||
|
|
||||||
|
super(uTorrentAPI, self).__init__()
|
||||||
|
|
||||||
|
self.url = 'http://' + str(host) + ':' + str(port) + '/gui/'
|
||||||
|
self.token = ''
|
||||||
|
self.last_time = time.time()
|
||||||
|
cookies = cookielib.CookieJar()
|
||||||
|
self.opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler)
|
||||||
|
self.opener.addheaders = [('User-agent', 'couchpotato-utorrent-client/1.0')]
|
||||||
|
if username and password:
|
||||||
|
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
||||||
|
password_manager.add_password(realm = None, uri = self.url, user = username, passwd = password)
|
||||||
|
self.opener.add_handler(urllib2.HTTPBasicAuthHandler(password_manager))
|
||||||
|
self.opener.add_handler(urllib2.HTTPDigestAuthHandler(password_manager))
|
||||||
|
elif username or password:
|
||||||
|
log.debug('User or password missing, not using authentication.')
|
||||||
|
self.token = self.get_token()
|
||||||
|
|
||||||
|
def _request(self, action, data = None):
|
||||||
|
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)
|
||||||
|
try:
|
||||||
|
open_request = self.opener.open(request)
|
||||||
|
response = open_request.read()
|
||||||
|
log.debug('response: %s', response)
|
||||||
|
if response:
|
||||||
|
log.debug('uTorrent action successfull')
|
||||||
|
return response
|
||||||
|
else:
|
||||||
|
log.debug('Unknown failure sending command to uTorrent. Return text is: %s', response)
|
||||||
|
except httplib.InvalidURL, err:
|
||||||
|
log.error('Invalid uTorrent host, check your config %s', err)
|
||||||
|
except urllib2.HTTPError, 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:
|
||||||
|
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]
|
||||||
|
return token
|
||||||
|
|
||||||
|
def add_torrent_uri(self, torrent):
|
||||||
|
action = "action=add-url&s=%s" % urllib.quote(torrent)
|
||||||
|
return self._request(action)
|
||||||
|
|
||||||
|
def add_torrent_file(self, filename, filedata):
|
||||||
|
action = "action=add-file"
|
||||||
|
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)
|
||||||
|
return self._request(action)
|
||||||
|
|
||||||
|
def pause_torrent(self, hash):
|
||||||
|
action = "action=pause&hash=%s" % hash
|
||||||
|
return self._request(action)
|
||||||
+40
-48
@@ -12,14 +12,14 @@ def runHandler(name, handler, *args, **kwargs):
|
|||||||
return handler(*args, **kwargs)
|
return handler(*args, **kwargs)
|
||||||
except:
|
except:
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
log.error('Error in event "%s", that wasn\'t caught: %s%s', (name, traceback.format_exc(), Env.all()))
|
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):
|
def addEvent(name, handler, priority = 100):
|
||||||
|
|
||||||
if events.get(name):
|
if events.get(name):
|
||||||
e = events[name]
|
e = events[name]
|
||||||
else:
|
else:
|
||||||
e = events[name] = Event(name = name, threads = 20, exc_info = True, traceback = True, lock = threading.RLock())
|
e = events[name] = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock())
|
||||||
|
|
||||||
def createHandle(*args, **kwargs):
|
def createHandle(*args, **kwargs):
|
||||||
|
|
||||||
@@ -46,49 +46,40 @@ def fireEvent(name, *args, **kwargs):
|
|||||||
#log.debug('Firing event %s', name)
|
#log.debug('Firing event %s', name)
|
||||||
try:
|
try:
|
||||||
|
|
||||||
# Fire after event
|
options = {
|
||||||
is_after_event = False
|
'is_after_event': False, # Fire after event
|
||||||
try:
|
'on_complete': False, # onComplete event
|
||||||
del kwargs['is_after_event']
|
'single': False, # Return single handler
|
||||||
is_after_event = True
|
'merge': False, # Merge items
|
||||||
except: pass
|
'in_order': False, # Fire them in specific order, waits for the other to finish
|
||||||
|
}
|
||||||
|
|
||||||
# onComplete event
|
# Do options
|
||||||
on_complete = False
|
for x in options:
|
||||||
try:
|
try:
|
||||||
on_complete = kwargs['on_complete']
|
val = kwargs[x]
|
||||||
del kwargs['on_complete']
|
del kwargs[x]
|
||||||
except: pass
|
options[x] = val
|
||||||
|
except: pass
|
||||||
# Return single handler
|
|
||||||
single = False
|
|
||||||
try:
|
|
||||||
del kwargs['single']
|
|
||||||
single = True
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
# Merge items
|
|
||||||
merge = False
|
|
||||||
try:
|
|
||||||
del kwargs['merge']
|
|
||||||
merge = True
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
# Merge items
|
|
||||||
in_order = False
|
|
||||||
try:
|
|
||||||
del kwargs['in_order']
|
|
||||||
in_order = True
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
e = events[name]
|
e = events[name]
|
||||||
if not in_order: e.lock.acquire()
|
|
||||||
e.asynchronous = False
|
|
||||||
e.in_order = in_order
|
|
||||||
result = e(*args, **kwargs)
|
|
||||||
if not in_order: e.lock.release()
|
|
||||||
|
|
||||||
if single and not merge:
|
# Lock this event
|
||||||
|
e.lock.acquire()
|
||||||
|
|
||||||
|
e.asynchronous = False
|
||||||
|
|
||||||
|
# Make sure only 1 event is fired at a time when order is wanted
|
||||||
|
kwargs['event_order_lock'] = threading.RLock() if options['in_order'] or options['single'] else None
|
||||||
|
kwargs['event_return_on_result'] = options['single']
|
||||||
|
|
||||||
|
# Fire
|
||||||
|
result = e(*args, **kwargs)
|
||||||
|
|
||||||
|
# Release lock for this event
|
||||||
|
e.lock.release()
|
||||||
|
|
||||||
|
if options['single'] and not options['merge']:
|
||||||
results = None
|
results = None
|
||||||
|
|
||||||
# Loop over results, stop when first not None result is found.
|
# Loop over results, stop when first not None result is found.
|
||||||
@@ -112,19 +103,20 @@ def fireEvent(name, *args, **kwargs):
|
|||||||
errorHandler(r[1])
|
errorHandler(r[1])
|
||||||
|
|
||||||
# Merge
|
# Merge
|
||||||
if merge and len(results) > 0:
|
if options['merge'] and len(results) > 0:
|
||||||
# Dict
|
# Dict
|
||||||
if type(results[0]) == dict:
|
if isinstance(results[0], dict):
|
||||||
merged = {}
|
merged = {}
|
||||||
for result in results:
|
for result in results:
|
||||||
merged = mergeDicts(merged, result)
|
merged = mergeDicts(merged, result)
|
||||||
|
|
||||||
results = merged
|
results = merged
|
||||||
# Lists
|
# Lists
|
||||||
elif type(results[0]) == list:
|
elif isinstance(results[0], list):
|
||||||
merged = []
|
merged = []
|
||||||
for result in results:
|
for result in results:
|
||||||
merged += result
|
if result not in merged:
|
||||||
|
merged += result
|
||||||
|
|
||||||
results = merged
|
results = merged
|
||||||
|
|
||||||
@@ -133,11 +125,11 @@ def fireEvent(name, *args, **kwargs):
|
|||||||
log.debug('Return modified results for %s', name)
|
log.debug('Return modified results for %s', name)
|
||||||
results = modified_results
|
results = modified_results
|
||||||
|
|
||||||
if not is_after_event:
|
if not options['is_after_event']:
|
||||||
fireEvent('%s.after' % name, is_after_event = True)
|
fireEvent('%s.after' % name, is_after_event = True)
|
||||||
|
|
||||||
if on_complete:
|
if options['on_complete']:
|
||||||
on_complete()
|
options['on_complete']()
|
||||||
|
|
||||||
return results
|
return results
|
||||||
except KeyError, e:
|
except KeyError, e:
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ def tryUrlencode(s):
|
|||||||
|
|
||||||
return new[1:]
|
return new[1:]
|
||||||
else:
|
else:
|
||||||
for letter in toUnicode(s):
|
for letter in ss(s):
|
||||||
try:
|
try:
|
||||||
new += quote_plus(letter)
|
new += quote_plus(letter)
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from couchpotato.core.helpers.encoding import simplifyString, toSafeString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
import hashlib
|
import hashlib
|
||||||
import os.path
|
import os.path
|
||||||
@@ -9,15 +10,34 @@ import sys
|
|||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
def getUserDir():
|
||||||
|
try:
|
||||||
|
import pwd
|
||||||
|
os.environ['HOME'] = pwd.getpwuid(os.geteuid()).pw_dir
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return os.path.expanduser('~')
|
||||||
|
|
||||||
|
def getDownloadDir():
|
||||||
|
user_dir = getUserDir()
|
||||||
|
|
||||||
|
# OSX
|
||||||
|
if 'darwin' in platform.platform().lower():
|
||||||
|
return os.path.join(user_dir, 'Downloads')
|
||||||
|
|
||||||
|
if os.name == 'nt':
|
||||||
|
return os.path.join(user_dir, 'Downloads')
|
||||||
|
|
||||||
|
return user_dir
|
||||||
|
|
||||||
def getDataDir():
|
def getDataDir():
|
||||||
|
|
||||||
# Windows
|
# Windows
|
||||||
if os.name == 'nt':
|
if os.name == 'nt':
|
||||||
return os.path.join(os.environ['APPDATA'], 'CouchPotato')
|
return os.path.join(os.environ['APPDATA'], 'CouchPotato')
|
||||||
|
|
||||||
import pwd
|
user_dir = getUserDir()
|
||||||
os.environ['HOME'] = pwd.getpwuid(os.geteuid()).pw_dir
|
|
||||||
user_dir = os.path.expanduser('~')
|
|
||||||
|
|
||||||
# OSX
|
# OSX
|
||||||
if 'darwin' in platform.platform().lower():
|
if 'darwin' in platform.platform().lower():
|
||||||
@@ -84,7 +104,7 @@ def cleanHost(host):
|
|||||||
|
|
||||||
return host
|
return host
|
||||||
|
|
||||||
def getImdb(txt, check_inside = True):
|
def getImdb(txt, check_inside = True, multiple = False):
|
||||||
|
|
||||||
if check_inside and os.path.isfile(txt):
|
if check_inside and os.path.isfile(txt):
|
||||||
output = open(txt, 'r')
|
output = open(txt, 'r')
|
||||||
@@ -92,8 +112,10 @@ def getImdb(txt, check_inside = True):
|
|||||||
output.close()
|
output.close()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
id = re.findall('(tt\d{7})', txt)[0]
|
ids = re.findall('(tt\d{7})', txt)
|
||||||
return id
|
if multiple:
|
||||||
|
return ids if len(ids) > 0 else []
|
||||||
|
return ids[0]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -118,12 +140,32 @@ def getTitle(library_dict):
|
|||||||
try:
|
try:
|
||||||
return library_dict['titles'][0]['title']
|
return library_dict['titles'][0]['title']
|
||||||
except:
|
except:
|
||||||
log.error('Could not get title for %s', library_dict['identifier'])
|
try:
|
||||||
return None
|
for title in library_dict.titles:
|
||||||
|
if title.default:
|
||||||
|
return title.title
|
||||||
|
except:
|
||||||
|
log.error('Could not get title for %s', library_dict.identifier)
|
||||||
|
return None
|
||||||
|
|
||||||
|
log.error('Could not get title for %s', library_dict['identifier'])
|
||||||
|
return None
|
||||||
except:
|
except:
|
||||||
log.error('Could not get title for library item: %s', library_dict)
|
log.error('Could not get title for library item: %s', library_dict)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def possibleTitles(raw_title):
|
||||||
|
|
||||||
|
titles = []
|
||||||
|
|
||||||
|
titles.append(toSafeString(raw_title).lower())
|
||||||
|
titles.append(raw_title.lower())
|
||||||
|
titles.append(simplifyString(raw_title))
|
||||||
|
|
||||||
|
return list(set(titles))
|
||||||
|
|
||||||
def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
|
def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
|
||||||
return ''.join(random.choice(chars) for x in range(size))
|
return ''.join(random.choice(chars) for x in range(size))
|
||||||
|
|
||||||
|
def splitString(str, split_on = ','):
|
||||||
|
return [x.strip() for x in str.split(split_on)]
|
||||||
|
|||||||
@@ -67,6 +67,18 @@ class Loader(object):
|
|||||||
|
|
||||||
def addFromDir(self, plugin_type, priority, module, dir_name):
|
def addFromDir(self, plugin_type, priority, module, dir_name):
|
||||||
|
|
||||||
|
# Load dir module
|
||||||
|
try:
|
||||||
|
m = __import__(module)
|
||||||
|
splitted = module.split('.')
|
||||||
|
for sub in splitted[1:]:
|
||||||
|
m = getattr(m, sub)
|
||||||
|
|
||||||
|
if hasattr(m, 'config'):
|
||||||
|
fireEvent('settings.options', splitted[-1] + '_config', getattr(m, 'config'))
|
||||||
|
except:
|
||||||
|
raise
|
||||||
|
|
||||||
for cur_file in glob.glob(os.path.join(dir_name, '*')):
|
for cur_file in glob.glob(os.path.join(dir_name, '*')):
|
||||||
name = os.path.basename(cur_file)
|
name = os.path.basename(cur_file)
|
||||||
if os.path.isdir(os.path.join(dir_name, name)):
|
if os.path.isdir(os.path.join(dir_name, name)):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import traceback
|
|||||||
class CPLog(object):
|
class CPLog(object):
|
||||||
|
|
||||||
context = ''
|
context = ''
|
||||||
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h']
|
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key']
|
||||||
|
|
||||||
def __init__(self, context = ''):
|
def __init__(self, context = ''):
|
||||||
if context.endswith('.main'):
|
if context.endswith('.main'):
|
||||||
@@ -17,6 +17,9 @@ class CPLog(object):
|
|||||||
def info(self, msg, replace_tuple = ()):
|
def info(self, msg, replace_tuple = ()):
|
||||||
self.logger.info(self.addContext(msg, replace_tuple))
|
self.logger.info(self.addContext(msg, replace_tuple))
|
||||||
|
|
||||||
|
def info2(self, msg, replace_tuple = ()):
|
||||||
|
self.logger.log(19, self.addContext(msg, replace_tuple))
|
||||||
|
|
||||||
def debug(self, msg, replace_tuple = ()):
|
def debug(self, msg, replace_tuple = ()):
|
||||||
self.logger.debug(self.addContext(msg, replace_tuple))
|
self.logger.debug(self.addContext(msg, replace_tuple))
|
||||||
|
|
||||||
@@ -53,7 +56,8 @@ class CPLog(object):
|
|||||||
if not Env.get('dev'):
|
if not Env.get('dev'):
|
||||||
|
|
||||||
for replace in self.replace_private:
|
for replace in self.replace_private:
|
||||||
msg = re.sub('(%s=)[^\&]+' % replace, '%s=xxx' % replace, msg)
|
msg = re.sub('(\?%s=)[^\&]+' % replace, '?%s=xxx' % replace, msg)
|
||||||
|
msg = re.sub('(&%s=)[^\&]+' % replace, '&%s=xxx' % replace, msg)
|
||||||
|
|
||||||
# Replace api key
|
# Replace api key
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
config = {
|
||||||
|
'name': 'notification_providers',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'label': 'Notifications',
|
||||||
|
'description': 'Notify when movies are done or snatched',
|
||||||
|
'type': 'list',
|
||||||
|
'name': 'notification_providers',
|
||||||
|
'tab': 'notifications',
|
||||||
|
'options': [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class Notification(Plugin):
|
|||||||
test_message = 'ZOMG Lazors Pewpewpew!'
|
test_message = 'ZOMG Lazors Pewpewpew!'
|
||||||
|
|
||||||
listen_to = [
|
listen_to = [
|
||||||
'movie.downloaded', 'movie.snatched',
|
'renamer.after', 'movie.snatched',
|
||||||
'updater.available', 'updater.updated',
|
'updater.available', 'updater.updated',
|
||||||
]
|
]
|
||||||
dont_listen_to = []
|
dont_listen_to = []
|
||||||
@@ -30,10 +30,10 @@ class Notification(Plugin):
|
|||||||
addEvent(listener, self.createNotifyHandler(listener))
|
addEvent(listener, self.createNotifyHandler(listener))
|
||||||
|
|
||||||
def createNotifyHandler(self, listener):
|
def createNotifyHandler(self, listener):
|
||||||
def notify(message, data):
|
def notify(message = None, group = {}, data = None):
|
||||||
if not self.conf('on_snatch', default = True) and listener == 'movie.snatched':
|
if not self.conf('on_snatch', default = True) and listener == 'movie.snatched':
|
||||||
return
|
return
|
||||||
return self.notify(message = message, data = data, listener = listener)
|
return self.notify(message = message, data = data if data else group, listener = listener)
|
||||||
|
|
||||||
return notify
|
return notify
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'boxcar',
|
'name': 'boxcar',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from couchpotato.api import addApiView, addNonBlockApiView
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.helpers.request import jsonified, getParam
|
from couchpotato.core.helpers.request import jsonified, getParam
|
||||||
from couchpotato.core.helpers.variable import tryInt
|
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
from couchpotato.core.settings.model import Notification as Notif
|
from couchpotato.core.settings.model import Notification as Notif
|
||||||
@@ -22,7 +22,7 @@ class CoreNotifier(Notification):
|
|||||||
listeners = []
|
listeners = []
|
||||||
|
|
||||||
listen_to = [
|
listen_to = [
|
||||||
'movie.downloaded', 'movie.snatched',
|
'renamer.after', 'movie.snatched',
|
||||||
'updater.available', 'updater.updated',
|
'updater.available', 'updater.updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ class CoreNotifier(Notification):
|
|||||||
|
|
||||||
ids = None
|
ids = None
|
||||||
if getParam('ids'):
|
if getParam('ids'):
|
||||||
ids = [x.strip() for x in getParam('ids').split(',')]
|
ids = splitString(getParam('ids'))
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
@@ -79,7 +79,6 @@ class CoreNotifier(Notification):
|
|||||||
q.update({Notif.read: True})
|
q.update({Notif.read: True})
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
#db.close()
|
|
||||||
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'success': True
|
'success': True
|
||||||
@@ -93,7 +92,7 @@ class CoreNotifier(Notification):
|
|||||||
q = db.query(Notif)
|
q = db.query(Notif)
|
||||||
|
|
||||||
if limit_offset:
|
if limit_offset:
|
||||||
splt = [x.strip() for x in limit_offset.split(',')]
|
splt = splitString(limit_offset)
|
||||||
limit = splt[0]
|
limit = splt[0]
|
||||||
offset = 0 if len(splt) is 1 else splt[1]
|
offset = 0 if len(splt) is 1 else splt[1]
|
||||||
q = q.limit(limit).offset(offset)
|
q = q.limit(limit).offset(offset)
|
||||||
@@ -107,7 +106,6 @@ class CoreNotifier(Notification):
|
|||||||
ndict['type'] = 'notification'
|
ndict['type'] = 'notification'
|
||||||
notifications.append(ndict)
|
notifications.append(ndict)
|
||||||
|
|
||||||
#db.close()
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'success': True,
|
'success': True,
|
||||||
'empty': len(notifications) == 0,
|
'empty': len(notifications) == 0,
|
||||||
@@ -133,7 +131,6 @@ class CoreNotifier(Notification):
|
|||||||
|
|
||||||
self.frontend(type = listener, data = data)
|
self.frontend(type = listener, data = data)
|
||||||
|
|
||||||
#db.close()
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def frontend(self, type = 'notification', data = {}, message = None):
|
def frontend(self, type = 'notification', data = {}, message = None):
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
from .main import Email
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Email()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'email',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
|
'name': 'email',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'from',
|
||||||
|
'label': 'Send e-mail from',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'to',
|
||||||
|
'label': 'Send e-mail to',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'smtp_server',
|
||||||
|
'label': 'SMTP server',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'ssl',
|
||||||
|
'label': 'Enable SSL',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'smtp_user',
|
||||||
|
'label': 'SMTP user',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'smtp_pass',
|
||||||
|
'label': 'SMTP password',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'on_snatch',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Also send message when movie is snatched.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.notifications.base import Notification
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
import smtplib
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Email(Notification):
|
||||||
|
|
||||||
|
def notify(self, message = '', data = {}, listener = None):
|
||||||
|
if self.isDisabled(): return
|
||||||
|
|
||||||
|
# Extract all the settings from settings
|
||||||
|
from_address = self.conf('from')
|
||||||
|
to_address = self.conf('to')
|
||||||
|
ssl = self.conf('ssl')
|
||||||
|
smtp_server = self.conf('smtp_server')
|
||||||
|
smtp_user = self.conf('smtp_user')
|
||||||
|
smtp_pass = self.conf('smtp_pass')
|
||||||
|
|
||||||
|
# Make the basic message
|
||||||
|
message = MIMEText(toUnicode(message))
|
||||||
|
message['Subject'] = self.default_title
|
||||||
|
message['From'] = from_address
|
||||||
|
message['To'] = to_address
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Open the SMTP connection, via SSL if requested
|
||||||
|
log.debug("SMTP over SSL %s", ("enabled" if ssl == 1 else "disabled"))
|
||||||
|
mailserver = smtplib.SMTP_SSL(smtp_server) if ssl == 1 else smtplib.SMTP(smtp_server)
|
||||||
|
|
||||||
|
# Check too see if an login attempt should be attempted
|
||||||
|
if len(smtp_user) > 0:
|
||||||
|
log.debug("Logging on to SMTP server using username \'%s\'%s", (smtp_user, " and a password" if len(smtp_pass) > 0 else ""))
|
||||||
|
mailserver.login(smtp_user, smtp_pass)
|
||||||
|
|
||||||
|
# Send the e-mail
|
||||||
|
log.debug("Sending the email")
|
||||||
|
mailserver.sendmail(from_address, to_address, message.as_string())
|
||||||
|
|
||||||
|
# Close the SMTP connection
|
||||||
|
mailserver.quit()
|
||||||
|
|
||||||
|
log.info('Email notification sent')
|
||||||
|
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
log.error('E-mail failed: %s', traceback.format_exc())
|
||||||
|
return False
|
||||||
|
|
||||||
|
return False
|
||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'growl',
|
'name': 'growl',
|
||||||
'description': 'Version 1.4+',
|
'description': 'Version 1.4+',
|
||||||
'options': [
|
'options': [
|
||||||
|
|||||||
@@ -37,8 +37,11 @@ class Growl(Notification):
|
|||||||
)
|
)
|
||||||
self.growl.register()
|
self.growl.register()
|
||||||
self.registered = True
|
self.registered = True
|
||||||
except:
|
except Exception, e:
|
||||||
log.error('Failed register of growl: %s', traceback.format_exc())
|
if 'timed out' in str(e):
|
||||||
|
self.registered = True
|
||||||
|
else:
|
||||||
|
log.error('Failed register of growl: %s', traceback.format_exc())
|
||||||
|
|
||||||
def notify(self, message = '', data = {}, listener = None):
|
def notify(self, message = '', data = {}, listener = None):
|
||||||
if self.isDisabled(): return
|
if self.isDisabled(): return
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'nmj',
|
'name': 'nmj',
|
||||||
'label': 'NMJ',
|
'label': 'NMJ',
|
||||||
'options': [
|
'options': [
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class NMJ(Notification):
|
|||||||
'mount': mount,
|
'mount': mount,
|
||||||
})
|
})
|
||||||
|
|
||||||
def addToLibrary(self, group = {}):
|
def addToLibrary(self, message = None, group = {}):
|
||||||
if self.isDisabled(): return
|
if self.isDisabled(): return
|
||||||
|
|
||||||
host = self.conf('host')
|
host = self.conf('host')
|
||||||
@@ -114,8 +114,8 @@ class NMJ(Notification):
|
|||||||
|
|
||||||
def failed(self):
|
def failed(self):
|
||||||
return jsonified({'success': False})
|
return jsonified({'success': False})
|
||||||
|
|
||||||
def test(self):
|
def test(self):
|
||||||
return jsonified({'success': self.addToLibrary()})
|
return jsonified({'success': self.addToLibrary()})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'notifo',
|
'name': 'notifo',
|
||||||
'description': 'Keep in mind that Notifo service will end soon.',
|
'description': 'Keep in mind that Notifo service will end soon.',
|
||||||
'options': [
|
'options': [
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'notifymyandroid',
|
'name': 'notifymyandroid',
|
||||||
'label': 'Notify My Android',
|
'label': 'Notify My Android',
|
||||||
'options': [
|
'options': [
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from couchpotato.core.helpers.variable import splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
import pynma
|
import pynma
|
||||||
@@ -11,7 +12,7 @@ class NotifyMyAndroid(Notification):
|
|||||||
if self.isDisabled(): return
|
if self.isDisabled(): return
|
||||||
|
|
||||||
nma = pynma.PyNMA()
|
nma = pynma.PyNMA()
|
||||||
keys = [x.strip() for x in self.conf('api_key').split(',')]
|
keys = splitString(self.conf('api_key'))
|
||||||
nma.addkey(keys)
|
nma.addkey(keys)
|
||||||
nma.developerkey(self.conf('dev_key'))
|
nma.developerkey(self.conf('dev_key'))
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'notifymywp',
|
'name': 'notifymywp',
|
||||||
'label': 'Notify My Windows Phone',
|
'label': 'Windows Phone',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'enabled',
|
'name': 'enabled',
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from couchpotato.core.helpers.variable import splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
from pynmwp import PyNMWP
|
from pynmwp import PyNMWP
|
||||||
@@ -10,7 +11,7 @@ class NotifyMyWP(Notification):
|
|||||||
def notify(self, message = '', data = {}, listener = None):
|
def notify(self, message = '', data = {}, listener = None):
|
||||||
if self.isDisabled(): return
|
if self.isDisabled(): return
|
||||||
|
|
||||||
keys = [x.strip() for x in self.conf('api_key').split(',')]
|
keys = splitString(self.conf('api_key'))
|
||||||
p = PyNMWP(keys, self.conf('dev_key'))
|
p = PyNMWP(keys, self.conf('dev_key'))
|
||||||
|
|
||||||
response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1)
|
response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'plex',
|
'name': 'plex',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class Plex(Notification):
|
|||||||
super(Plex, self).__init__()
|
super(Plex, self).__init__()
|
||||||
addEvent('renamer.after', self.addToLibrary)
|
addEvent('renamer.after', self.addToLibrary)
|
||||||
|
|
||||||
def addToLibrary(self, group = {}):
|
def addToLibrary(self, message = None, group = {}):
|
||||||
if self.isDisabled(): return
|
if self.isDisabled(): return
|
||||||
|
|
||||||
log.info('Sending notification to Plex')
|
log.info('Sending notification to Plex')
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'prowl',
|
'name': 'prowl',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,39 +1,35 @@
|
|||||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
from httplib import HTTPSConnection
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Prowl(Notification):
|
class Prowl(Notification):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'api': 'https://api.prowlapp.com/publicapi/add'
|
||||||
|
}
|
||||||
|
|
||||||
def notify(self, message = '', data = {}, listener = None):
|
def notify(self, message = '', data = {}, listener = None):
|
||||||
if self.isDisabled(): return
|
if self.isDisabled(): return
|
||||||
|
|
||||||
http_handler = HTTPSConnection('api.prowlapp.com')
|
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'apikey': self.conf('api_key'),
|
'apikey': self.conf('api_key'),
|
||||||
'application': self.default_title,
|
'application': self.default_title,
|
||||||
'description': toUnicode(message),
|
'description': toUnicode(message),
|
||||||
'priority': self.conf('priority'),
|
'priority': self.conf('priority'),
|
||||||
}
|
}
|
||||||
|
headers = {
|
||||||
|
'Content-type': 'application/x-www-form-urlencoded'
|
||||||
|
}
|
||||||
|
|
||||||
http_handler.request('POST',
|
try:
|
||||||
'/publicapi/add',
|
self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False)
|
||||||
headers = {'Content-type': 'application/x-www-form-urlencoded'},
|
|
||||||
body = tryUrlencode(data)
|
|
||||||
)
|
|
||||||
response = http_handler.getresponse()
|
|
||||||
request_status = response.status
|
|
||||||
|
|
||||||
if request_status == 200:
|
|
||||||
log.info('Prowl notifications sent.')
|
log.info('Prowl notifications sent.')
|
||||||
return True
|
return True
|
||||||
elif request_status == 401:
|
except:
|
||||||
log.error('Prowl auth failed: %s', response.reason)
|
log.error('Prowl failed: %s', traceback.format_exc())
|
||||||
return False
|
|
||||||
else:
|
return False
|
||||||
log.error('Prowl notification failed.')
|
|
||||||
return False
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'pushover',
|
'name': 'pushover',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'synoindex',
|
'name': 'synoindex',
|
||||||
'description': 'Automaticly adds index to Synology Media Server.',
|
'description': 'Automaticly adds index to Synology Media Server.',
|
||||||
'options': [
|
'options': [
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
|
from couchpotato.core.helpers.request import jsonified
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -8,14 +10,17 @@ log = CPLog(__name__)
|
|||||||
|
|
||||||
class Synoindex(Notification):
|
class Synoindex(Notification):
|
||||||
|
|
||||||
|
index_path = '/usr/syno/bin/synoindex'
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
super(Synoindex, self).__init__()
|
||||||
addEvent('renamer.after', self.addToLibrary)
|
addEvent('renamer.after', self.addToLibrary)
|
||||||
|
|
||||||
def addToLibrary(self, group = {}):
|
def addToLibrary(self, message = None, group = {}):
|
||||||
if self.isDisabled(): return
|
if self.isDisabled(): return
|
||||||
|
|
||||||
command = ['/usr/syno/bin/synoindex', '-A', group.get('destination_dir')]
|
command = [self.index_path, '-A', group.get('destination_dir')]
|
||||||
log.info(u'Executing synoindex command: %s ', command)
|
log.info('Executing synoindex command: %s ', command)
|
||||||
try:
|
try:
|
||||||
p = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
|
p = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
|
||||||
out = p.communicate()
|
out = p.communicate()
|
||||||
@@ -26,3 +31,6 @@ class Synoindex(Notification):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def test(self):
|
||||||
|
return jsonified({'success': os.path.isfile(self.index_path)})
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
from .main import Toasty
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Toasty()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'toasty',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
|
'name': 'toasty',
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'enabler',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'api_key',
|
||||||
|
'label': 'Device ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'on_snatch',
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'advanced': True,
|
||||||
|
'description': 'Also send message when movie is snatched.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.notifications.base import Notification
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
class Toasty(Notification):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'api': 'http://api.supertoasty.com/notify/%s?%s'
|
||||||
|
}
|
||||||
|
|
||||||
|
def notify(self, message = '', data = {}, listener = None):
|
||||||
|
if self.isDisabled(): return
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'title': self.default_title,
|
||||||
|
'text': toUnicode(message),
|
||||||
|
'sender': toUnicode("CouchPotato"),
|
||||||
|
'image': 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/homescreen.png',
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.urlopen(self.urls['api'] % (self.conf('api_key'), tryUrlencode(data)), show_error = False)
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
log.error('Toasty failed: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
return False
|
||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'twitter',
|
'name': 'twitter',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -38,22 +38,33 @@ class Twitter(Notification):
|
|||||||
|
|
||||||
direct_message = self.conf('direct_message')
|
direct_message = self.conf('direct_message')
|
||||||
direct_message_users = self.conf('screen_name')
|
direct_message_users = self.conf('screen_name')
|
||||||
|
|
||||||
mention = self.conf('mention')
|
mention = self.conf('mention')
|
||||||
|
mention_tag = None
|
||||||
if mention:
|
if mention:
|
||||||
if direct_message:
|
if direct_message:
|
||||||
direct_message_users = '%s %s' % (direct_message_users, mention)
|
direct_message_users = '%s %s' % (direct_message_users, mention)
|
||||||
direct_message_users = direct_message_users.replace('@',' ')
|
direct_message_users = direct_message_users.replace('@', ' ')
|
||||||
direct_message_users = direct_message_users.replace(',',' ')
|
direct_message_users = direct_message_users.replace(',', ' ')
|
||||||
else:
|
else:
|
||||||
message = '%s @%s' % (message, mention.lstrip('@'))
|
mention_tag = '@%s' % mention.lstrip('@')
|
||||||
|
message = '%s %s' % (message, mention_tag)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if direct_message:
|
if direct_message:
|
||||||
for user in direct_message_users.split():
|
for user in direct_message_users.split():
|
||||||
api.PostDirectMessage(user, '[%s] %s' % (self.default_title, message))
|
api.PostDirectMessage(user, '[%s] %s' % (self.default_title, message))
|
||||||
else:
|
else:
|
||||||
api.PostUpdate('[%s] %s' % (self.default_title, message))
|
update_message = '[%s] %s' % (self.default_title, message)
|
||||||
|
if len(update_message) > 140:
|
||||||
|
if mention_tag:
|
||||||
|
api.PostUpdate(update_message[:135 - len(mention_tag)] + ('%s 1/2 ' % mention_tag))
|
||||||
|
api.PostUpdate(update_message[135 - len(mention_tag):] + ('%s 2/2 ' % mention_tag))
|
||||||
|
else:
|
||||||
|
api.PostUpdate(update_message[:135] + ' 1/2')
|
||||||
|
api.PostUpdate(update_message[135:] + ' 2/2')
|
||||||
|
else:
|
||||||
|
api.PostUpdate(update_message)
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
log.error('Error sending tweet: %s', e)
|
log.error('Error sending tweet: %s', e)
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'notifications',
|
'tab': 'notifications',
|
||||||
|
'list': 'notification_providers',
|
||||||
'name': 'xbmc',
|
'name': 'xbmc',
|
||||||
'label': 'XBMC',
|
'label': 'XBMC',
|
||||||
|
'description': 'v11 (Eden) and v12 (Frodo)',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'enabled',
|
'name': 'enabled',
|
||||||
|
|||||||
Regular → Executable
+166
-20
@@ -1,43 +1,189 @@
|
|||||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
from couchpotato.core.helpers.variable import splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
|
from flask.helpers import json
|
||||||
import base64
|
import base64
|
||||||
|
import traceback
|
||||||
|
import urllib
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class XBMC(Notification):
|
class XBMC(Notification):
|
||||||
|
|
||||||
listen_to = ['movie.downloaded']
|
listen_to = ['renamer.after']
|
||||||
|
use_json_notifications = {}
|
||||||
|
couch_logo_url = 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/xbmc-notify.png'
|
||||||
|
|
||||||
def notify(self, message = '', data = {}, listener = None):
|
def notify(self, message = '', data = {}, listener = None):
|
||||||
if self.isDisabled(): return
|
if self.isDisabled(): return
|
||||||
|
|
||||||
hosts = [x.strip() for x in self.conf('host').split(",")]
|
hosts = splitString(self.conf('host'))
|
||||||
|
|
||||||
successful = 0
|
successful = 0
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host):
|
|
||||||
successful += 1
|
|
||||||
if self.send({'command': 'ExecBuiltIn', 'parameter': 'XBMC.updatelibrary(video)'}, host):
|
|
||||||
successful += 1
|
|
||||||
|
|
||||||
return successful == len(hosts)*2
|
if self.use_json_notifications.get(host) is None:
|
||||||
|
self.getXBMCJSONversion(host, message = message)
|
||||||
|
|
||||||
def send(self, command, host):
|
if self.use_json_notifications.get(host):
|
||||||
|
response = self.request(host, [
|
||||||
|
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.couch_logo_url}),
|
||||||
|
('VideoLibrary.Scan', {}),
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
|
||||||
|
response += self.request(host, [('VideoLibrary.Scan', {})])
|
||||||
|
|
||||||
url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command))
|
try:
|
||||||
|
for result in response:
|
||||||
|
if (result.get('result') and result['result'] == 'OK'):
|
||||||
|
successful += 1
|
||||||
|
elif (result.get('error')):
|
||||||
|
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
|
||||||
|
|
||||||
headers = {}
|
except:
|
||||||
|
log.error('Failed parsing results: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
return successful == len(hosts) * 2
|
||||||
|
|
||||||
|
def getXBMCJSONversion(self, host, message = ''):
|
||||||
|
|
||||||
|
success = False
|
||||||
|
|
||||||
|
# XBMC JSON-RPC version request
|
||||||
|
response = self.request(host, [
|
||||||
|
('JSONRPC.Version', {})
|
||||||
|
])
|
||||||
|
for result in response:
|
||||||
|
if (result.get('result') and type(result['result']['version']).__name__ == 'int'):
|
||||||
|
# only v2 and v4 return an int object
|
||||||
|
# v6 (as of XBMC v12(Frodo)) is required to send notifications
|
||||||
|
xbmc_rpc_version = str(result['result']['version'])
|
||||||
|
|
||||||
|
log.debug('XBMC JSON-RPC Version: %s ; Notifications by JSON-RPC only supported for v6 [as of XBMC v12(Frodo)]', xbmc_rpc_version)
|
||||||
|
|
||||||
|
# disable JSON use
|
||||||
|
self.use_json_notifications[host] = False
|
||||||
|
|
||||||
|
# send the text message
|
||||||
|
resp = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
|
||||||
|
for result in resp:
|
||||||
|
if (result.get('result') and result['result'] == 'OK'):
|
||||||
|
log.debug('Message delivered successfully!')
|
||||||
|
success = True
|
||||||
|
break
|
||||||
|
elif (result.get('error')):
|
||||||
|
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
|
||||||
|
break
|
||||||
|
|
||||||
|
elif (result.get('result') and type(result['result']['version']).__name__ == 'dict'):
|
||||||
|
# XBMC JSON-RPC v6 returns an array object containing
|
||||||
|
# major, minor and patch number
|
||||||
|
xbmc_rpc_version = str(result['result']['version']['major'])
|
||||||
|
xbmc_rpc_version += '.' + str(result['result']['version']['minor'])
|
||||||
|
xbmc_rpc_version += '.' + str(result['result']['version']['patch'])
|
||||||
|
|
||||||
|
log.debug('XBMC JSON-RPC Version: %s', xbmc_rpc_version)
|
||||||
|
|
||||||
|
# ok, XBMC version is supported
|
||||||
|
self.use_json_notifications[host] = True
|
||||||
|
|
||||||
|
# send the text message
|
||||||
|
resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image':self.couch_logo_url})])
|
||||||
|
for result in resp:
|
||||||
|
if (result.get('result') and result['result'] == 'OK'):
|
||||||
|
log.debug('Message delivered successfully!')
|
||||||
|
success = True
|
||||||
|
break
|
||||||
|
elif (result.get('error')):
|
||||||
|
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
|
||||||
|
break
|
||||||
|
|
||||||
|
# error getting version info (we do have contact with XBMC though)
|
||||||
|
elif (result.get('error')):
|
||||||
|
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
|
||||||
|
|
||||||
|
log.debug('Use JSON notifications: %s ', self.use_json_notifications)
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
def notifyXBMCnoJSON(self, host, data):
|
||||||
|
|
||||||
|
server = 'http://%s/xbmcCmds/' % host
|
||||||
|
|
||||||
|
# Notification(title, message [, timeout , image])
|
||||||
|
cmd = "xbmcHttp?command=ExecBuiltIn(Notification(%s,%s,'',%s))" % (urllib.quote(data['title']), urllib.quote(data['message']), urllib.quote(self.couch_logo_url))
|
||||||
|
server += cmd
|
||||||
|
|
||||||
|
# I have no idea what to set to, just tried text/plain and seems to be working :)
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'text/plain',
|
||||||
|
}
|
||||||
|
|
||||||
|
# authentication support
|
||||||
if self.conf('password'):
|
if self.conf('password'):
|
||||||
headers = {
|
base64string = base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password'))).replace('\n', '')
|
||||||
'Authorization': "Basic %s" % base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password')))[:-1]
|
headers['Authorization'] = 'Basic %s' % base64string
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.urlopen(url, headers = headers, show_error = False)
|
log.debug('Sending non-JSON-type request to %s: %s', (host, data))
|
||||||
except:
|
|
||||||
log.error("Couldn't sent command to XBMC")
|
# response wil either be 'OK':
|
||||||
return False
|
# <html>
|
||||||
|
# <li>OK
|
||||||
|
# </html>
|
||||||
|
#
|
||||||
|
# or 'Error':
|
||||||
|
# <html>
|
||||||
|
# <li>Error:<message>
|
||||||
|
# </html>
|
||||||
|
#
|
||||||
|
response = self.urlopen(server, headers = headers)
|
||||||
|
|
||||||
|
if 'OK' in response:
|
||||||
|
log.debug('Returned from non-JSON-type request %s: %s', (host, response))
|
||||||
|
# manually fake expected response array
|
||||||
|
return [{'result': 'OK'}]
|
||||||
|
else:
|
||||||
|
log.error('Returned from non-JSON-type request %s: %s', (host, response))
|
||||||
|
# manually fake expected response array
|
||||||
|
return [{'result': 'Error'}]
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
|
||||||
|
return [{'result': 'Error'}]
|
||||||
|
|
||||||
|
def request(self, host, requests):
|
||||||
|
server = 'http://%s/jsonrpc' % host
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for req in requests:
|
||||||
|
method, kwargs = req
|
||||||
|
data.append({
|
||||||
|
'method': method,
|
||||||
|
'params': kwargs,
|
||||||
|
'jsonrpc': '2.0',
|
||||||
|
'id': method,
|
||||||
|
})
|
||||||
|
data = json.dumps(data)
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.conf('password'):
|
||||||
|
base64string = base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password'))).replace('\n', '')
|
||||||
|
headers['Authorization'] = 'Basic %s' % base64string
|
||||||
|
|
||||||
|
try:
|
||||||
|
log.debug('Sending request to %s: %s', (host, data))
|
||||||
|
rdata = self.urlopen(server, headers = headers, params = data, multipart = True)
|
||||||
|
response = json.loads(rdata)
|
||||||
|
log.debug('Returned from request %s: %s', (host, response))
|
||||||
|
|
||||||
|
return response
|
||||||
|
except:
|
||||||
|
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
|
||||||
|
return []
|
||||||
|
|
||||||
log.info('XBMC notification to %s successful.', host)
|
|
||||||
return True
|
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ def start():
|
|||||||
|
|
||||||
config = [{
|
config = [{
|
||||||
'name': 'automation',
|
'name': 'automation',
|
||||||
'order': 30,
|
'order': 101,
|
||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'automation',
|
'tab': 'automation',
|
||||||
'name': 'automation',
|
'name': 'automation',
|
||||||
'label': 'Automation',
|
'label': 'Minimal movie requirements',
|
||||||
'description': 'Minimal movie requirements',
|
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'year',
|
'name': 'year',
|
||||||
|
|||||||
@@ -18,9 +18,17 @@ class Automation(Plugin):
|
|||||||
def addMovies(self):
|
def addMovies(self):
|
||||||
|
|
||||||
movies = fireEvent('automation.get_movies', merge = True)
|
movies = fireEvent('automation.get_movies', merge = True)
|
||||||
|
movie_ids = []
|
||||||
|
|
||||||
for imdb_id in movies:
|
for imdb_id in movies:
|
||||||
prop_name = 'automation.added.%s' % imdb_id
|
prop_name = 'automation.added.%s' % imdb_id
|
||||||
added = Env.prop(prop_name, default = False)
|
added = Env.prop(prop_name, default = False)
|
||||||
if not added:
|
if not added:
|
||||||
fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False)
|
added_movie = fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False, search_after = False, update_library = True, single = True)
|
||||||
|
if added_movie:
|
||||||
|
movie_ids.append(added_movie['id'])
|
||||||
Env.prop(prop_name, True)
|
Env.prop(prop_name, True)
|
||||||
|
|
||||||
|
for movie_id in movie_ids:
|
||||||
|
movie_dict = fireEvent('movie.get', movie_id, single = True)
|
||||||
|
fireEvent('searcher.single', movie_dict)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
from couchpotato import addView
|
from couchpotato import addView
|
||||||
from couchpotato.core.event import fireEvent, addEvent
|
from couchpotato.core.event import fireEvent, addEvent
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode, simplifyString, ss
|
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString
|
||||||
from couchpotato.core.helpers.variable import getExt
|
from couchpotato.core.helpers.variable import getExt, md5
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from flask.templating import render_template_string
|
from flask.templating import render_template_string
|
||||||
@@ -34,7 +34,7 @@ class Plugin(object):
|
|||||||
http_failed_disabled = {}
|
http_failed_disabled = {}
|
||||||
|
|
||||||
def registerPlugin(self):
|
def registerPlugin(self):
|
||||||
addEvent('app.shutdown', self.doShutdown)
|
addEvent('app.do_shutdown', self.doShutdown)
|
||||||
addEvent('plugin.running', self.isRunning)
|
addEvent('plugin.running', self.isRunning)
|
||||||
|
|
||||||
def conf(self, attr, value = None, default = None):
|
def conf(self, attr, value = None, default = None):
|
||||||
@@ -78,7 +78,7 @@ class Plugin(object):
|
|||||||
self.makeDir(os.path.dirname(path))
|
self.makeDir(os.path.dirname(path))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
f = open(path, 'w' if not binary else 'wb')
|
f = open(path, 'w+' if not binary else 'w+b')
|
||||||
f.write(content)
|
f.write(content)
|
||||||
f.close()
|
f.close()
|
||||||
os.chmod(path, Env.getPermission('file'))
|
os.chmod(path, Env.getPermission('file'))
|
||||||
@@ -98,6 +98,7 @@ class Plugin(object):
|
|||||||
|
|
||||||
# http request
|
# http request
|
||||||
def urlopen(self, url, timeout = 30, params = None, headers = None, opener = None, multipart = False, show_error = True):
|
def urlopen(self, url, timeout = 30, params = None, headers = None, opener = None, multipart = False, show_error = True):
|
||||||
|
url = ss(url)
|
||||||
|
|
||||||
if not headers: headers = {}
|
if not headers: headers = {}
|
||||||
if not params: params = {}
|
if not params: params = {}
|
||||||
@@ -113,8 +114,11 @@ class Plugin(object):
|
|||||||
# Don't try for failed requests
|
# Don't try for failed requests
|
||||||
if self.http_failed_disabled.get(host, 0) > 0:
|
if self.http_failed_disabled.get(host, 0) > 0:
|
||||||
if self.http_failed_disabled[host] > (time.time() - 900):
|
if self.http_failed_disabled[host] > (time.time() - 900):
|
||||||
log.info('Disabled calls to %s for 15 minutes because so many failed requests.', host)
|
log.info2('Disabled calls to %s for 15 minutes because so many failed requests.', host)
|
||||||
raise Exception
|
if not show_error:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
return ''
|
||||||
else:
|
else:
|
||||||
del self.http_failed_request[host]
|
del self.http_failed_request[host]
|
||||||
del self.http_failed_disabled[host]
|
del self.http_failed_disabled[host]
|
||||||
@@ -123,11 +127,14 @@ class Plugin(object):
|
|||||||
try:
|
try:
|
||||||
|
|
||||||
if multipart:
|
if multipart:
|
||||||
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()]))
|
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
|
||||||
request = urllib2.Request(url, params, headers)
|
request = urllib2.Request(url, params, headers)
|
||||||
|
|
||||||
cookies = cookielib.CookieJar()
|
if opener:
|
||||||
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler)
|
opener.add_handler(MultipartPostHandler())
|
||||||
|
else:
|
||||||
|
cookies = cookielib.CookieJar()
|
||||||
|
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler)
|
||||||
|
|
||||||
response = opener.open(request, timeout = timeout)
|
response = opener.open(request, timeout = timeout)
|
||||||
else:
|
else:
|
||||||
@@ -218,7 +225,7 @@ class Plugin(object):
|
|||||||
|
|
||||||
|
|
||||||
def getCache(self, cache_key, url = None, **kwargs):
|
def getCache(self, cache_key, url = None, **kwargs):
|
||||||
cache_key = simplifyString(cache_key)
|
cache_key = md5(ss(cache_key))
|
||||||
cache = Env.get('cache').get(cache_key)
|
cache = Env.get('cache').get(cache_key)
|
||||||
if cache:
|
if cache:
|
||||||
if not Env.get('dev'): log.debug('Getting cache %s', cache_key)
|
if not Env.get('dev'): log.debug('Getting cache %s', cache_key)
|
||||||
@@ -238,13 +245,32 @@ class Plugin(object):
|
|||||||
self.setCache(cache_key, data, timeout = cache_timeout)
|
self.setCache(cache_key, data, timeout = cache_timeout)
|
||||||
return data
|
return data
|
||||||
except:
|
except:
|
||||||
pass
|
if not kwargs.get('show_error', True):
|
||||||
|
raise
|
||||||
|
|
||||||
|
return ''
|
||||||
|
|
||||||
def setCache(self, cache_key, value, timeout = 300):
|
def setCache(self, cache_key, value, timeout = 300):
|
||||||
log.debug('Setting cache %s', cache_key)
|
log.debug('Setting cache %s', cache_key)
|
||||||
Env.get('cache').set(cache_key, value, timeout)
|
Env.get('cache').set(cache_key, value, timeout)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def createNzbName(self, data, movie):
|
||||||
|
tag = self.cpTag(movie)
|
||||||
|
return '%s%s' % (toSafeString(data.get('name')[:127 - len(tag)]), tag)
|
||||||
|
|
||||||
|
def createFileName(self, data, filedata, movie):
|
||||||
|
name = os.path.join(self.createNzbName(data, movie))
|
||||||
|
if data.get('type') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
|
||||||
|
return '%s.%s' % (name, 'rar')
|
||||||
|
return '%s.%s' % (name, data.get('type'))
|
||||||
|
|
||||||
|
def cpTag(self, movie):
|
||||||
|
if Env.setting('enabled', 'renamer'):
|
||||||
|
return '.cp(' + movie['library'].get('identifier') + ')' if movie['library'].get('identifier') else ''
|
||||||
|
|
||||||
|
return ''
|
||||||
|
|
||||||
def isDisabled(self):
|
def isDisabled(self):
|
||||||
return not self.isEnabled()
|
return not self.isEnabled()
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.helpers.request import getParam, jsonified
|
from couchpotato.core.helpers.request import getParam, jsonified
|
||||||
|
from couchpotato.core.helpers.variable import getUserDir
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
import ctypes
|
import ctypes
|
||||||
import os
|
import os
|
||||||
@@ -27,6 +28,8 @@ class FileBrowser(Plugin):
|
|||||||
},
|
},
|
||||||
'return': {'type': 'object', 'example': """{
|
'return': {'type': 'object', 'example': """{
|
||||||
'is_root': bool, //is top most folder
|
'is_root': bool, //is top most folder
|
||||||
|
'parent': string, //parent folder of requested path
|
||||||
|
'home': string, //user home folder
|
||||||
'empty': bool, //directory is empty
|
'empty': bool, //directory is empty
|
||||||
'dirs': array, //directory names
|
'dirs': array, //directory names
|
||||||
}"""}
|
}"""}
|
||||||
@@ -63,15 +66,28 @@ class FileBrowser(Plugin):
|
|||||||
def view(self):
|
def view(self):
|
||||||
|
|
||||||
path = getParam('path', '/')
|
path = getParam('path', '/')
|
||||||
|
home = getUserDir()
|
||||||
|
|
||||||
|
if not path:
|
||||||
|
path = home
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True))
|
dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True))
|
||||||
except:
|
except:
|
||||||
dirs = []
|
dirs = []
|
||||||
|
|
||||||
|
parent = os.path.dirname(path.rstrip(os.path.sep))
|
||||||
|
if parent == path.rstrip(os.path.sep):
|
||||||
|
parent = '/'
|
||||||
|
elif parent != '/' and parent[-2:] != ':\\':
|
||||||
|
parent += os.path.sep
|
||||||
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'is_root': path == '/' or not path,
|
'is_root': path == '/',
|
||||||
'empty': len(dirs) == 0,
|
'empty': len(dirs) == 0,
|
||||||
|
'parent': parent,
|
||||||
|
'home': home + os.path.sep,
|
||||||
|
'platform': os.name,
|
||||||
'dirs': dirs,
|
'dirs': dirs,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ from couchpotato import get_session
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
|
from couchpotato.core.helpers.request import jsonified
|
||||||
from couchpotato.core.helpers.variable import md5, getExt
|
from couchpotato.core.helpers.variable import md5, getExt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
from couchpotato.core.plugins.scanner.main import Scanner
|
||||||
from couchpotato.core.settings.model import FileType, File
|
from couchpotato.core.settings.model import FileType, File
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
import os.path
|
import os.path
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -28,6 +31,54 @@ class FileManager(Plugin):
|
|||||||
'return': {'type': 'file'}
|
'return': {'type': 'file'}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
addApiView('file.types', self.getTypesView, docs = {
|
||||||
|
'desc': 'Return a list of all the file types and their ids.',
|
||||||
|
'return': {'type': 'object', 'example': """{
|
||||||
|
'types': [
|
||||||
|
{
|
||||||
|
"identifier": "poster_original",
|
||||||
|
"type": "image",
|
||||||
|
"id": 1,
|
||||||
|
"name": "Poster_original"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identifier": "poster",
|
||||||
|
"type": "image",
|
||||||
|
"id": 2,
|
||||||
|
"name": "Poster"
|
||||||
|
},
|
||||||
|
etc
|
||||||
|
]
|
||||||
|
}"""}
|
||||||
|
})
|
||||||
|
|
||||||
|
addEvent('app.load', self.cleanup)
|
||||||
|
addEvent('app.load', self.init)
|
||||||
|
|
||||||
|
def init(self):
|
||||||
|
|
||||||
|
for type_tuple in Scanner.file_types.values():
|
||||||
|
self.getType(type_tuple)
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
|
||||||
|
# Wait a bit after starting before cleanup
|
||||||
|
time.sleep(3)
|
||||||
|
log.debug('Cleaning up unused files')
|
||||||
|
|
||||||
|
python_cache = Env.get('cache')._path
|
||||||
|
try:
|
||||||
|
db = get_session()
|
||||||
|
for root, dirs, walk_files in os.walk(Env.get('cache_dir')):
|
||||||
|
for filename in walk_files:
|
||||||
|
if root == python_cache: continue
|
||||||
|
file_path = os.path.join(root, filename)
|
||||||
|
f = db.query(File).filter(File.path == toUnicode(file_path)).first()
|
||||||
|
if not f:
|
||||||
|
os.remove(file_path)
|
||||||
|
except:
|
||||||
|
log.error('Failed removing unused file: %s', traceback.format_exc())
|
||||||
|
|
||||||
def showCacheFile(self, filename = ''):
|
def showCacheFile(self, filename = ''):
|
||||||
|
|
||||||
cache_dir = Env.get('cache_dir')
|
cache_dir = Env.get('cache_dir')
|
||||||
@@ -89,7 +140,6 @@ class FileManager(Plugin):
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
type_dict = ft.to_dict()
|
type_dict = ft.to_dict()
|
||||||
#db.close()
|
|
||||||
return type_dict
|
return type_dict
|
||||||
|
|
||||||
def getTypes(self):
|
def getTypes(self):
|
||||||
@@ -102,5 +152,10 @@ class FileManager(Plugin):
|
|||||||
for type_object in results:
|
for type_object in results:
|
||||||
types.append(type_object.to_dict())
|
types.append(type_object.to_dict())
|
||||||
|
|
||||||
#db.close()
|
|
||||||
return types
|
return types
|
||||||
|
|
||||||
|
def getTypesView(self):
|
||||||
|
|
||||||
|
return jsonified({
|
||||||
|
'types': self.getTypes()
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from couchpotato import get_session
|
from couchpotato import get_session
|
||||||
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||||
|
from couchpotato.core.helpers.variable import mergeDicts
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Library, LibraryTitle, File
|
from couchpotato.core.settings.model import Library, LibraryTitle, File
|
||||||
@@ -52,7 +53,6 @@ class LibraryPlugin(Plugin):
|
|||||||
|
|
||||||
library_dict = l.to_dict(self.default_dict)
|
library_dict = l.to_dict(self.default_dict)
|
||||||
|
|
||||||
#db.close()
|
|
||||||
return library_dict
|
return library_dict
|
||||||
|
|
||||||
def update(self, identifier, default_title = '', force = False):
|
def update(self, identifier, default_title = '', force = False):
|
||||||
@@ -111,18 +111,20 @@ class LibraryPlugin(Plugin):
|
|||||||
|
|
||||||
# Files
|
# Files
|
||||||
images = info.get('images', [])
|
images = info.get('images', [])
|
||||||
for type in images:
|
for image_type in ['poster']:
|
||||||
for image in images[type]:
|
for image in images.get(image_type, []):
|
||||||
if not isinstance(image, str):
|
if not isinstance(image, (str, unicode)):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
file_path = fireEvent('file.download', url = image, single = True)
|
file_path = fireEvent('file.download', url = image, single = True)
|
||||||
if file_path:
|
if file_path:
|
||||||
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', type), single = True)
|
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', image_type), single = True)
|
||||||
try:
|
try:
|
||||||
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
|
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
|
||||||
library.files.append(file_obj)
|
library.files.append(file_obj)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
break
|
||||||
except:
|
except:
|
||||||
log.debug('Failed to attach to library: %s', traceback.format_exc())
|
log.debug('Failed to attach to library: %s', traceback.format_exc())
|
||||||
|
|
||||||
@@ -136,26 +138,23 @@ class LibraryPlugin(Plugin):
|
|||||||
library = db.query(Library).filter_by(identifier = identifier).first()
|
library = db.query(Library).filter_by(identifier = identifier).first()
|
||||||
|
|
||||||
if not library.info:
|
if not library.info:
|
||||||
library_dict = self.update(identifier)
|
library_dict = self.update(identifier, force = True)
|
||||||
dates = library_dict.get('info', {}).get('release_dates')
|
dates = library_dict.get('info', {}).get('release_date')
|
||||||
else:
|
else:
|
||||||
dates = library.info.get('release_date')
|
dates = library.info.get('release_date')
|
||||||
|
|
||||||
if dates and dates.get('expires', 0) < time.time():
|
if dates and dates.get('expires', 0) < time.time() or not dates:
|
||||||
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
|
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
|
||||||
library.info['release_date'] = dates
|
library.info = mergeDicts(library.info, {'release_date': dates })
|
||||||
library.info = library.info
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
dates = library.info.get('release_date', {})
|
|
||||||
#db.close()
|
|
||||||
|
|
||||||
return dates
|
return dates
|
||||||
|
|
||||||
|
|
||||||
def simplifyTitle(self, title):
|
def simplifyTitle(self, title):
|
||||||
|
|
||||||
title = toUnicode(title)
|
title = toUnicode(title)
|
||||||
|
|
||||||
nr_prefix = '' if title[0] in ascii_letters else '#'
|
nr_prefix = '' if title[0] in ascii_letters else '#'
|
||||||
title = simplifyString(title)
|
title = simplifyString(title)
|
||||||
|
|
||||||
|
|||||||
@@ -2,22 +2,32 @@ from couchpotato.api import addApiView
|
|||||||
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
|
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
|
||||||
from couchpotato.core.helpers.encoding import ss
|
from couchpotato.core.helpers.encoding import ss
|
||||||
from couchpotato.core.helpers.request import jsonified, getParam
|
from couchpotato.core.helpers.request import jsonified, getParam
|
||||||
|
from couchpotato.core.helpers.variable import getTitle, splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
class Manage(Plugin):
|
class Manage(Plugin):
|
||||||
|
|
||||||
|
in_progress = False
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
fireEvent('scheduler.interval', identifier = 'manage.update_library', handle = self.updateLibrary, hours = 2)
|
fireEvent('scheduler.interval', identifier = 'manage.update_library', handle = self.updateLibrary, hours = 2)
|
||||||
|
|
||||||
addEvent('manage.update', self.updateLibrary)
|
addEvent('manage.update', self.updateLibrary)
|
||||||
|
|
||||||
|
# Add files after renaming
|
||||||
|
def after_rename(message = None, group = {}):
|
||||||
|
return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'])
|
||||||
|
addEvent('renamer.after', after_rename, priority = 110)
|
||||||
|
|
||||||
addApiView('manage.update', self.updateLibraryView, docs = {
|
addApiView('manage.update', self.updateLibraryView, docs = {
|
||||||
'desc': 'Update the library by scanning for new movies',
|
'desc': 'Update the library by scanning for new movies',
|
||||||
'params': {
|
'params': {
|
||||||
@@ -25,11 +35,23 @@ class Manage(Plugin):
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
addApiView('manage.progress', self.getProgress, docs = {
|
||||||
|
'desc': 'Get the progress of current manage update',
|
||||||
|
'return': {'type': 'object', 'example': """{
|
||||||
|
'progress': False || object, total & to_go,
|
||||||
|
}"""},
|
||||||
|
})
|
||||||
|
|
||||||
if not Env.get('dev'):
|
if not Env.get('dev'):
|
||||||
def updateLibrary():
|
def updateLibrary():
|
||||||
self.updateLibrary(full = False)
|
self.updateLibrary(full = False)
|
||||||
addEvent('app.load', updateLibrary)
|
addEvent('app.load', updateLibrary)
|
||||||
|
|
||||||
|
def getProgress(self):
|
||||||
|
return jsonified({
|
||||||
|
'progress': self.in_progress
|
||||||
|
})
|
||||||
|
|
||||||
def updateLibraryView(self):
|
def updateLibraryView(self):
|
||||||
|
|
||||||
full = getParam('full', default = 1)
|
full = getParam('full', default = 1)
|
||||||
@@ -43,49 +65,152 @@ class Manage(Plugin):
|
|||||||
def updateLibrary(self, full = True):
|
def updateLibrary(self, full = True):
|
||||||
last_update = float(Env.prop('manage.last_update', default = 0))
|
last_update = float(Env.prop('manage.last_update', default = 0))
|
||||||
|
|
||||||
if self.isDisabled() or (last_update > time.time() - 20):
|
if self.in_progress:
|
||||||
|
log.info('Already updating library: %s', self.in_progress)
|
||||||
|
return
|
||||||
|
elif self.isDisabled() or (last_update > time.time() - 20):
|
||||||
return
|
return
|
||||||
|
|
||||||
directories = self.directories()
|
self.in_progress = {}
|
||||||
added_identifiers = []
|
fireEvent('notify.frontend', type = 'manage.updating', data = True)
|
||||||
|
|
||||||
for directory in directories:
|
try:
|
||||||
|
|
||||||
if not os.path.isdir(directory):
|
directories = self.directories()
|
||||||
if len(directory) > 0:
|
added_identifiers = []
|
||||||
log.error('Directory doesn\'t exist: %s', directory)
|
|
||||||
continue
|
|
||||||
|
|
||||||
log.info('Updating manage library: %s', directory)
|
# Add some progress
|
||||||
identifiers = fireEvent('scanner.folder', folder = directory, newer_than = last_update if not full else 0, single = True)
|
self.in_progress = {}
|
||||||
if identifiers:
|
for directory in directories:
|
||||||
added_identifiers.extend(identifiers)
|
self.in_progress[os.path.normpath(directory)] = {
|
||||||
|
'total': None,
|
||||||
|
'to_go': None,
|
||||||
|
}
|
||||||
|
|
||||||
# Break if CP wants to shut down
|
for directory in directories:
|
||||||
if self.shuttingDown():
|
folder = os.path.normpath(directory)
|
||||||
|
|
||||||
|
if not os.path.isdir(folder):
|
||||||
|
if len(directory) > 0:
|
||||||
|
log.error('Directory doesn\'t exist: %s', folder)
|
||||||
|
continue
|
||||||
|
|
||||||
|
log.info('Updating manage library: %s', folder)
|
||||||
|
fireEvent('notify.frontend', type = 'manage.update', data = True, message = 'Scanning for movies in "%s"' % folder)
|
||||||
|
|
||||||
|
onFound = self.createAddToLibrary(folder, added_identifiers)
|
||||||
|
fireEvent('scanner.scan', folder = folder, simple = True, newer_than = last_update if not full else 0, on_found = onFound, single = True)
|
||||||
|
|
||||||
|
# Break if CP wants to shut down
|
||||||
|
if self.shuttingDown():
|
||||||
|
break
|
||||||
|
|
||||||
|
# If cleanup option is enabled, remove offline files from database
|
||||||
|
if self.conf('cleanup') and full and not self.shuttingDown():
|
||||||
|
|
||||||
|
# Get movies with done status
|
||||||
|
total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True)
|
||||||
|
|
||||||
|
for done_movie in done_movies:
|
||||||
|
if done_movie['library']['identifier'] not in added_identifiers:
|
||||||
|
fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
|
||||||
|
else:
|
||||||
|
|
||||||
|
for release in done_movie.get('releases', []):
|
||||||
|
if len(release.get('files', [])) == 0:
|
||||||
|
fireEvent('release.delete', release['id'])
|
||||||
|
else:
|
||||||
|
for release_file in release.get('files', []):
|
||||||
|
# Remove release not available anymore
|
||||||
|
if not os.path.isfile(ss(release_file['path'])):
|
||||||
|
fireEvent('release.clean', release['id'])
|
||||||
|
break
|
||||||
|
|
||||||
|
# Check if there are duplicate releases (different quality) use the last one, delete the rest
|
||||||
|
if len(done_movie.get('releases', [])) > 1:
|
||||||
|
used_files = {}
|
||||||
|
for release in done_movie.get('releases', []):
|
||||||
|
|
||||||
|
for release_file in release.get('files', []):
|
||||||
|
already_used = used_files.get(release_file['path'])
|
||||||
|
|
||||||
|
if already_used:
|
||||||
|
if already_used < release['id']:
|
||||||
|
fireEvent('release.delete', release['id'], single = True) # delete this one
|
||||||
|
else:
|
||||||
|
fireEvent('release.delete', already_used, single = True) # delete previous one
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
used_files[release_file['path']] = release.get('id')
|
||||||
|
del used_files
|
||||||
|
|
||||||
|
Env.prop('manage.last_update', time.time())
|
||||||
|
except:
|
||||||
|
log.error('Failed updating library: %s', (traceback.format_exc()))
|
||||||
|
|
||||||
|
while True and not self.shuttingDown():
|
||||||
|
|
||||||
|
delete_me = {}
|
||||||
|
|
||||||
|
for folder in self.in_progress:
|
||||||
|
if self.in_progress[folder]['to_go'] <= 0:
|
||||||
|
delete_me[folder] = True
|
||||||
|
|
||||||
|
for delete in delete_me:
|
||||||
|
del self.in_progress[delete]
|
||||||
|
|
||||||
|
if len(self.in_progress) == 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
# If cleanup option is enabled, remove offline files from database
|
time.sleep(1)
|
||||||
if self.conf('cleanup') and full and not self.shuttingDown():
|
|
||||||
|
|
||||||
# Get movies with done status
|
fireEvent('notify.frontend', type = 'manage.updating', data = False)
|
||||||
total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True)
|
self.in_progress = False
|
||||||
|
|
||||||
for done_movie in done_movies:
|
def createAddToLibrary(self, folder, added_identifiers = []):
|
||||||
if done_movie['library']['identifier'] not in added_identifiers:
|
def addToLibrary(group, total_found, to_go):
|
||||||
fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
|
if self.in_progress[folder]['total'] is None:
|
||||||
else:
|
self.in_progress[folder] = {
|
||||||
for release in done_movie.get('releases', []):
|
'total': total_found,
|
||||||
for release_file in release.get('files', []):
|
'to_go': total_found,
|
||||||
# Remove release not available anymore
|
}
|
||||||
if not os.path.isfile(ss(release_file['path'])):
|
|
||||||
fireEvent('release.clean', release['id'])
|
|
||||||
break
|
|
||||||
|
|
||||||
Env.prop('manage.last_update', time.time())
|
if group['library'] and group['library'].get('identifier'):
|
||||||
|
identifier = group['library'].get('identifier')
|
||||||
|
added_identifiers.append(identifier)
|
||||||
|
|
||||||
|
# Add it to release and update the info
|
||||||
|
fireEvent('release.add', group = group)
|
||||||
|
fireEventAsync('library.update', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier))
|
||||||
|
|
||||||
|
return addToLibrary
|
||||||
|
|
||||||
|
def createAfterUpdate(self, folder, identifier):
|
||||||
|
|
||||||
|
# Notify frontend
|
||||||
|
def afterUpdate():
|
||||||
|
self.in_progress[folder]['to_go'] = self.in_progress[folder]['to_go'] - 1
|
||||||
|
total = self.in_progress[folder]['total']
|
||||||
|
movie_dict = fireEvent('movie.get', identifier, single = True)
|
||||||
|
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = None if total > 5 else 'Added "%s" to manage.' % getTitle(movie_dict['library']))
|
||||||
|
|
||||||
|
return afterUpdate
|
||||||
|
|
||||||
def directories(self):
|
def directories(self):
|
||||||
try:
|
try:
|
||||||
return [x.strip() for x in self.conf('library', default = '').split('::')]
|
if self.conf('library', '').strip():
|
||||||
|
return splitString(self.conf('library', default = ''), '::')
|
||||||
except:
|
except:
|
||||||
return []
|
pass
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def scanFilesToLibrary(self, folder = None, files = None):
|
||||||
|
|
||||||
|
folder = os.path.normpath(folder)
|
||||||
|
|
||||||
|
groups = fireEvent('scanner.scan', folder = folder, files = files, single = True)
|
||||||
|
|
||||||
|
for group in groups.itervalues():
|
||||||
|
if group['library'] and group['library'].get('identifier'):
|
||||||
|
fireEvent('release.add', group = group)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from couchpotato.api import addApiView
|
|||||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||||
from couchpotato.core.helpers.request import getParams, jsonified, getParam
|
from couchpotato.core.helpers.request import getParams, jsonified, getParam
|
||||||
from couchpotato.core.helpers.variable import getImdb
|
from couchpotato.core.helpers.variable import getImdb, splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Library, LibraryTitle, Movie
|
from couchpotato.core.settings.model import Library, LibraryTitle, Movie
|
||||||
@@ -107,13 +107,18 @@ class MoviePlugin(Plugin):
|
|||||||
def get(self, movie_id):
|
def get(self, movie_id):
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
|
||||||
|
imdb_id = getImdb(str(movie_id))
|
||||||
|
|
||||||
|
if(imdb_id):
|
||||||
|
m = db.query(Movie).filter(Movie.library.has(identifier = imdb_id)).first()
|
||||||
|
else:
|
||||||
|
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||||
|
|
||||||
results = None
|
results = None
|
||||||
if m:
|
if m:
|
||||||
results = m.to_dict(self.default_dict)
|
results = m.to_dict(self.default_dict)
|
||||||
|
|
||||||
#db.close()
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def list(self, status = ['active'], limit_offset = None, starts_with = None, search = None):
|
def list(self, status = ['active'], limit_offset = None, starts_with = None, search = None):
|
||||||
@@ -161,7 +166,7 @@ class MoviePlugin(Plugin):
|
|||||||
.options(joinedload_all('files'))
|
.options(joinedload_all('files'))
|
||||||
|
|
||||||
if limit_offset:
|
if limit_offset:
|
||||||
splt = [x.strip() for x in limit_offset.split(',')]
|
splt = splitString(limit_offset)
|
||||||
limit = splt[0]
|
limit = splt[0]
|
||||||
offset = 0 if len(splt) is 1 else splt[1]
|
offset = 0 if len(splt) is 1 else splt[1]
|
||||||
q2 = q2.limit(limit).offset(offset)
|
q2 = q2.limit(limit).offset(offset)
|
||||||
@@ -239,7 +244,7 @@ class MoviePlugin(Plugin):
|
|||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
for id in getParam('id').split(','):
|
for id in splitString(getParam('id')):
|
||||||
movie = db.query(Movie).filter_by(id = id).first()
|
movie = db.query(Movie).filter_by(id = id).first()
|
||||||
|
|
||||||
if movie:
|
if movie:
|
||||||
@@ -278,7 +283,7 @@ class MoviePlugin(Plugin):
|
|||||||
'movies': movies,
|
'movies': movies,
|
||||||
})
|
})
|
||||||
|
|
||||||
def add(self, params = {}, force_readd = True, search_after = True):
|
def add(self, params = {}, force_readd = True, search_after = True, update_library = False):
|
||||||
|
|
||||||
if not params.get('identifier'):
|
if not params.get('identifier'):
|
||||||
msg = 'Can\'t add movie without imdb identifier.'
|
msg = 'Can\'t add movie without imdb identifier.'
|
||||||
@@ -298,7 +303,7 @@ class MoviePlugin(Plugin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
library = fireEvent('library.add', single = True, attrs = params, update_after = False)
|
library = fireEvent('library.add', single = True, attrs = params, update_after = update_library)
|
||||||
|
|
||||||
# Status
|
# Status
|
||||||
status_active = fireEvent('status.add', 'active', single = True)
|
status_active = fireEvent('status.add', 'active', single = True)
|
||||||
@@ -381,7 +386,7 @@ class MoviePlugin(Plugin):
|
|||||||
|
|
||||||
available_status = fireEvent('status.get', 'available', single = True)
|
available_status = fireEvent('status.get', 'available', single = True)
|
||||||
|
|
||||||
ids = [x.strip() for x in params.get('id').split(',')]
|
ids = splitString(params.get('id'))
|
||||||
for movie_id in ids:
|
for movie_id in ids:
|
||||||
|
|
||||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||||
@@ -417,7 +422,7 @@ class MoviePlugin(Plugin):
|
|||||||
|
|
||||||
params = getParams()
|
params = getParams()
|
||||||
|
|
||||||
ids = [x.strip() for x in params.get('id').split(',')]
|
ids = splitString(params.get('id'))
|
||||||
for movie_id in ids:
|
for movie_id in ids:
|
||||||
self.delete(movie_id, delete_from = params.get('delete_from', 'all'))
|
self.delete(movie_id, delete_from = params.get('delete_from', 'all'))
|
||||||
|
|
||||||
@@ -431,9 +436,11 @@ class MoviePlugin(Plugin):
|
|||||||
|
|
||||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
movie = db.query(Movie).filter_by(id = movie_id).first()
|
||||||
if movie:
|
if movie:
|
||||||
|
deleted = False
|
||||||
if delete_from == 'all':
|
if delete_from == 'all':
|
||||||
db.delete(movie)
|
db.delete(movie)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
deleted = True
|
||||||
else:
|
else:
|
||||||
done_status = fireEvent('status.get', 'done', single = True)
|
done_status = fireEvent('status.get', 'done', single = True)
|
||||||
|
|
||||||
@@ -456,6 +463,7 @@ class MoviePlugin(Plugin):
|
|||||||
if total_releases == total_deleted:
|
if total_releases == total_deleted:
|
||||||
db.delete(movie)
|
db.delete(movie)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
deleted = True
|
||||||
elif new_movie_status:
|
elif new_movie_status:
|
||||||
new_status = fireEvent('status.get', new_movie_status, single = True)
|
new_status = fireEvent('status.get', new_movie_status, single = True)
|
||||||
movie.profile_id = None
|
movie.profile_id = None
|
||||||
@@ -464,6 +472,9 @@ class MoviePlugin(Plugin):
|
|||||||
else:
|
else:
|
||||||
fireEvent('movie.restatus', movie.id, single = True)
|
fireEvent('movie.restatus', movie.id, single = True)
|
||||||
|
|
||||||
|
if deleted:
|
||||||
|
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
|
||||||
|
|
||||||
#db.close()
|
#db.close()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -33,16 +33,34 @@ var MovieList = new Class({
|
|||||||
);
|
);
|
||||||
self.getMovies();
|
self.getMovies();
|
||||||
|
|
||||||
if(options.add_new)
|
App.addEvent('movie.added', self.movieAdded.bind(self))
|
||||||
App.addEvent('movie.added', self.movieAdded.bind(self))
|
App.addEvent('movie.deleted', self.movieDeleted.bind(self))
|
||||||
|
},
|
||||||
|
|
||||||
|
movieDeleted: function(notification){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
if(self.movies_added[notification.data.id]){
|
||||||
|
self.movies.each(function(movie){
|
||||||
|
if(movie.get('id') == notification.data.id){
|
||||||
|
movie.destroy();
|
||||||
|
delete self.movies_added[notification.data.id]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
self.checkIfEmpty();
|
||||||
},
|
},
|
||||||
|
|
||||||
movieAdded: function(notification){
|
movieAdded: function(notification){
|
||||||
var self = this;
|
var self = this;
|
||||||
window.scroll(0,0);
|
|
||||||
|
|
||||||
if(!self.movies_added[notification.data.id])
|
if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){
|
||||||
|
window.scroll(0,0);
|
||||||
self.createMovie(notification.data, 'top');
|
self.createMovie(notification.data, 'top');
|
||||||
|
|
||||||
|
self.checkIfEmpty();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
create: function(){
|
create: function(){
|
||||||
@@ -86,18 +104,19 @@ var MovieList = new Class({
|
|||||||
Object.each(movies, function(movie){
|
Object.each(movies, function(movie){
|
||||||
self.createMovie(movie);
|
self.createMovie(movie);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.total_movies = total;
|
||||||
self.setCounter(total);
|
self.setCounter(total);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setCounter: function(count){
|
setCounter: function(count){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
if(!self.navigation_counter) return;
|
if(!self.navigation_counter) return;
|
||||||
|
|
||||||
self.navigation_counter.set('text', (count || 0));
|
self.navigation_counter.set('text', (count || 0));
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
createMovie: function(movie, inject_at){
|
createMovie: function(movie, inject_at){
|
||||||
@@ -281,10 +300,11 @@ var MovieList = new Class({
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteSelected: function(){
|
deleteSelected: function(){
|
||||||
var self = this;
|
var self = this,
|
||||||
var ids = self.getSelectedMovies()
|
ids = self.getSelectedMovies(),
|
||||||
|
help_msg = self.identifier == 'wanted' ? 'If you do, you won\'t be able to watch them, as they won\'t get downloaded!' : 'Your files will be safe, this will only delete the reference from the CouchPotato manage list';
|
||||||
|
|
||||||
var qObj = new Question('Are you sure you want to delete '+ids.length+' movie'+ (ids.length != 1 ? 's' : '') +'?', 'If you do, you won\'t be able to watch them, as they won\'t get downloaded!', [{
|
var qObj = new Question('Are you sure you want to delete '+ids.length+' movie'+ (ids.length != 1 ? 's' : '') +'?', help_msg, [{
|
||||||
'text': 'Yes, delete '+(ids.length != 1 ? 'them' : 'it'),
|
'text': 'Yes, delete '+(ids.length != 1 ? 'them' : 'it'),
|
||||||
'class': 'delete',
|
'class': 'delete',
|
||||||
'events': {
|
'events': {
|
||||||
@@ -309,6 +329,8 @@ var MovieList = new Class({
|
|||||||
|
|
||||||
erase_movies.each(function(movie){
|
erase_movies.each(function(movie){
|
||||||
self.movies.erase(movie);
|
self.movies.erase(movie);
|
||||||
|
|
||||||
|
movie.destroy()
|
||||||
});
|
});
|
||||||
|
|
||||||
self.calculateSelected();
|
self.calculateSelected();
|
||||||
@@ -458,6 +480,8 @@ var MovieList = new Class({
|
|||||||
self.addMovies(json.movies, json.total);
|
self.addMovies(json.movies, json.total);
|
||||||
self.load_more.set('text', 'load more movies');
|
self.load_more.set('text', 'load more movies');
|
||||||
if(self.scrollspy) self.scrollspy.start();
|
if(self.scrollspy) self.scrollspy.start();
|
||||||
|
|
||||||
|
self.checkIfEmpty()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -475,6 +499,28 @@ var MovieList = new Class({
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
checkIfEmpty: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var is_empty = self.movies.length == 0 && self.total_movies == 0;
|
||||||
|
|
||||||
|
if(is_empty && self.options.on_empty_element){
|
||||||
|
self.el.grab(self.options.on_empty_element);
|
||||||
|
|
||||||
|
if(self.navigation)
|
||||||
|
self.navigation.hide();
|
||||||
|
|
||||||
|
self.empty_element = self.options.on_empty_element;
|
||||||
|
}
|
||||||
|
else if(self.empty_element){
|
||||||
|
self.empty_element.destroy();
|
||||||
|
|
||||||
|
if(self.navigation)
|
||||||
|
self.navigation.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
toElement: function(){
|
toElement: function(){
|
||||||
return this.el;
|
return this.el;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
.movies .movie.list_view:hover, .movies .movie.mass_edit_view:hover {
|
.movies .movie.list_view:hover, .movies .movie.mass_edit_view:hover {
|
||||||
background: rgba(255,255,255,0.03);
|
background: rgba(255,255,255,0.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
.movies .movie_container {
|
.movies .movie_container {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
padding-bottom: 4px;
|
padding-bottom: 4px;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movies .movie .trailer_container {
|
.movies .movie .trailer_container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #000;
|
background: #000;
|
||||||
@@ -324,7 +324,7 @@
|
|||||||
.movies .movie .trailer_container.hide {
|
.movies .movie .trailer_container.hide {
|
||||||
height: 0 !important;
|
height: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movies .movie .hide_trailer {
|
.movies .movie .hide_trailer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -340,12 +340,12 @@
|
|||||||
.movies .movie .hide_trailer.hide {
|
.movies .movie .hide_trailer.hide {
|
||||||
top: -30px;
|
top: -30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movies .movie .try_container {
|
.movies .movie .try_container {
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movies .movie .try_container a {
|
.movies .movie .try_container a {
|
||||||
margin: 0 5px;
|
margin: 0 5px;
|
||||||
padding: 2px 5px;
|
padding: 2px 5px;
|
||||||
@@ -354,15 +354,15 @@
|
|||||||
.movies .movie .releases .next_release {
|
.movies .movie .releases .next_release {
|
||||||
border-left: 6px solid #2aa300;
|
border-left: 6px solid #2aa300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movies .movie .releases .next_release > :first-child {
|
.movies .movie .releases .next_release > :first-child {
|
||||||
margin-left: -6px;
|
margin-left: -6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movies .movie .releases .last_release {
|
.movies .movie .releases .last_release {
|
||||||
border-left: 6px solid #ffa200;
|
border-left: 6px solid #ffa200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movies .movie .releases .last_release > :first-child {
|
.movies .movie .releases .last_release > :first-child {
|
||||||
margin-left: -6px;
|
margin-left: -6px;
|
||||||
}
|
}
|
||||||
@@ -394,8 +394,8 @@
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movies .alph_nav ul.numbers,
|
.movies .alph_nav ul.numbers,
|
||||||
.movies .alph_nav .counter,
|
.movies .alph_nav .counter,
|
||||||
.movies .alph_nav ul.actions {
|
.movies .alph_nav ul.actions {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0 0 1px;
|
padding: 0 0 1px;
|
||||||
@@ -518,7 +518,7 @@
|
|||||||
padding: 3px 7px;
|
padding: 3px 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movies .alph_nav .mass_edit_form .refresh,
|
.movies .alph_nav .mass_edit_form .refresh,
|
||||||
.movies .alph_nav .mass_edit_form .delete {
|
.movies .alph_nav .mass_edit_form .delete {
|
||||||
float: left;
|
float: left;
|
||||||
padding: 8px 0 0 8px;
|
padding: 8px 0 0 8px;
|
||||||
@@ -534,5 +534,67 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.movies .alph_nav .more_menu > a {
|
.movies .alph_nav .more_menu > a {
|
||||||
background-position: center -157px;
|
background-position: center -158px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.movies .empty_wanted {
|
||||||
|
background-image: url('../images/emptylist.png');
|
||||||
|
height: 750px;
|
||||||
|
width: 800px;
|
||||||
|
padding-top: 260px;
|
||||||
|
margin-top: -50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movies .empty_manage {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 25px;
|
||||||
|
line-height: 150%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movies .empty_manage .after_manage {
|
||||||
|
margin-top: 30px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movies .progress {
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 5px 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movies .progress > div {
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 12px;
|
||||||
|
text-align: left;
|
||||||
|
display: inline-block;
|
||||||
|
width: 49%;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
margin: 2px 0.5%;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movies .progress > div .folder {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 20px 5px 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 85%;
|
||||||
|
direction: rtl;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.movies .progress > div .percentage {
|
||||||
|
font-weight: bold;
|
||||||
|
display: inline-block;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-shadow: none;
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 20px;
|
||||||
|
border-left: 1px solid rgba(255, 255, 255, .2);
|
||||||
|
width: 15%;
|
||||||
|
text-align: right;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,14 +16,41 @@ var Movie = new Class({
|
|||||||
self.profile = Quality.getProfile(data.profile_id) || {};
|
self.profile = Quality.getProfile(data.profile_id) || {};
|
||||||
self.parent(self, options);
|
self.parent(self, options);
|
||||||
|
|
||||||
App.addEvent('movie.update.'+data.id, self.update.bind(self));
|
self.addEvents();
|
||||||
|
},
|
||||||
|
|
||||||
|
addEvents: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
App.addEvent('movie.update.'+self.data.id, self.update.bind(self));
|
||||||
|
|
||||||
['movie.busy', 'searcher.started'].each(function(listener){
|
['movie.busy', 'searcher.started'].each(function(listener){
|
||||||
App.addEvent(listener+'.'+data.id, function(notification){
|
App.addEvent(listener+'.'+self.data.id, function(notification){
|
||||||
if(notification.data)
|
if(notification.data)
|
||||||
self.busy(true)
|
self.busy(true)
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
App.addEvent('searcher.ended.'+self.data.id, function(notification){
|
||||||
|
if(notification.data)
|
||||||
|
self.busy(false)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.el.destroy();
|
||||||
|
delete self.list.movies_added[self.get('id')];
|
||||||
|
self.list.movies.erase(self)
|
||||||
|
|
||||||
|
self.list.checkIfEmpty();
|
||||||
|
|
||||||
|
// Remove events
|
||||||
|
App.removeEvents('movie.update.'+self.data.id);
|
||||||
|
['movie.busy', 'searcher.started'].each(function(listener){
|
||||||
|
App.removeEvents(listener+'.'+self.data.id);
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
busy: function(set_busy){
|
busy: function(set_busy){
|
||||||
@@ -359,7 +386,7 @@ var ReleaseAction = new Class({
|
|||||||
|
|
||||||
var status = Status.get(release.status_id);
|
var status = Status.get(release.status_id);
|
||||||
|
|
||||||
if((status.identifier == 'ignored' || status.identifier == 'failed') || (!self.next_release && status.identifier == 'available')){
|
if((self.next_release && (status.identifier == 'ignored' || status.identifier == 'failed')) || (!self.next_release && status.identifier == 'available')){
|
||||||
self.hide_on_click = false;
|
self.hide_on_click = false;
|
||||||
self.show();
|
self.show();
|
||||||
buttons_done = true;
|
buttons_done = true;
|
||||||
@@ -397,19 +424,11 @@ var ReleaseAction = new Class({
|
|||||||
var status = Status.get(release.status_id),
|
var status = Status.get(release.status_id),
|
||||||
quality = Quality.getProfile(release.quality_id) || {},
|
quality = Quality.getProfile(release.quality_id) || {},
|
||||||
info = release.info;
|
info = release.info;
|
||||||
|
release.status = status;
|
||||||
if( status.identifier == 'ignored' || status.identifier == 'failed'){
|
|
||||||
self.last_release = release;
|
|
||||||
}
|
|
||||||
else if(!self.next_release && status.identifier == 'available'){
|
|
||||||
self.next_release = release;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create release
|
// Create release
|
||||||
new Element('div', {
|
new Element('div', {
|
||||||
'class': 'item '+status.identifier +
|
'class': 'item '+status.identifier,
|
||||||
(self.next_release && self.next_release.id == release.id ? ' next_release' : '') +
|
|
||||||
(self.last_release && self.last_release.id == release.id ? ' last_release' : ''),
|
|
||||||
'id': 'release_'+release.id
|
'id': 'release_'+release.id
|
||||||
}).adopt(
|
}).adopt(
|
||||||
new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}),
|
new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}),
|
||||||
@@ -442,31 +461,50 @@ var ReleaseAction = new Class({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
).inject(self.release_container)
|
).inject(self.release_container)
|
||||||
|
|
||||||
|
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
|
||||||
|
if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched'))
|
||||||
|
self.last_release = release;
|
||||||
|
}
|
||||||
|
else if(!self.next_release && status.identifier == 'available'){
|
||||||
|
self.next_release = release;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
self.trynext_container.adopt(
|
if(self.last_release){
|
||||||
new Element('span.or', {
|
self.release_container.getElement('#release_'+self.last_release.id).addClass('last_release');
|
||||||
'text': 'Download'
|
}
|
||||||
}),
|
|
||||||
self.last_release ? new Element('a.button.orange', {
|
if(self.next_release){
|
||||||
'text': 'the same release again',
|
self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release');
|
||||||
'events': {
|
}
|
||||||
'click': self.trySameRelease.bind(self)
|
|
||||||
}
|
if(self.next_release || self.last_release){
|
||||||
}) : null,
|
|
||||||
self.next_release && self.last_release ? new Element('span.or', {
|
self.trynext_container.adopt(
|
||||||
'text': 'or'
|
new Element('span.or', {
|
||||||
}) : null,
|
'text': 'This movie is snatched, if anything went wrong, download'
|
||||||
self.next_release ? [new Element('a.button.green', {
|
}),
|
||||||
'text': self.last_release ? 'another release' : 'the best release',
|
self.last_release ? new Element('a.button.orange', {
|
||||||
'events': {
|
'text': 'the same release again',
|
||||||
'click': self.tryNextRelease.bind(self)
|
'events': {
|
||||||
}
|
'click': self.trySameRelease.bind(self)
|
||||||
}),
|
}
|
||||||
new Element('span.or', {
|
}) : null,
|
||||||
'text': 'or pick one below'
|
self.next_release && self.last_release ? new Element('span.or', {
|
||||||
})] : null
|
'text': ','
|
||||||
)
|
}) : null,
|
||||||
|
self.next_release ? [new Element('a.button.green', {
|
||||||
|
'text': self.last_release ? 'another release' : 'the best release',
|
||||||
|
'events': {
|
||||||
|
'click': self.tryNextRelease.bind(self)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
new Element('span.or', {
|
||||||
|
'text': 'or pick one below'
|
||||||
|
})] : null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,10 +91,15 @@
|
|||||||
.movie_result {
|
.movie_result {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 140px;
|
height: 140px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result .options {
|
.movie_result .options {
|
||||||
height: 140px;
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-width: 1px 0;
|
border-width: 1px 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
@@ -133,17 +138,21 @@
|
|||||||
|
|
||||||
.movie_result .data {
|
.movie_result .data {
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
width: 470px;
|
position: absolute;
|
||||||
position: relative;
|
height: 100%;
|
||||||
height: 140px;
|
width: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
margin: -140px 0 0 0;
|
left: 0;
|
||||||
background: #5c697b;
|
background: #5c697b;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
border-bottom: 1px solid #333;
|
border-bottom: 1px solid #333;
|
||||||
border-top: 1px solid rgba(255,255,255, 0.15);
|
border-top: 1px solid rgba(255,255,255, 0.15);
|
||||||
|
transition: all .6s cubic-bezier(0.9,0,0.1,1);
|
||||||
}
|
}
|
||||||
|
.movie_result .data.open {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.movie_result:last-child .data { border-bottom: 0; }
|
.movie_result:last-child .data { border-bottom: 0; }
|
||||||
|
|
||||||
@@ -193,4 +202,9 @@
|
|||||||
|
|
||||||
.search_form .mask {
|
.search_form .mask {
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,9 @@ Block.Search = new Class({
|
|||||||
self.hideResults(false)
|
self.hideResults(false)
|
||||||
},
|
},
|
||||||
'blur': function(){
|
'blur': function(){
|
||||||
self.el.removeClass('focused')
|
(function(){
|
||||||
|
self.el.removeClass('focused')
|
||||||
|
}).delay(2000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -117,7 +119,7 @@ Block.Search = new Class({
|
|||||||
self.hideResults(false);
|
self.hideResults(false);
|
||||||
|
|
||||||
if(!cache){
|
if(!cache){
|
||||||
self.positionMask().fade('in');
|
self.mask.fade('in');
|
||||||
|
|
||||||
if(!self.spinner)
|
if(!self.spinner)
|
||||||
self.spinner = createSpinner(self.mask);
|
self.spinner = createSpinner(self.mask);
|
||||||
@@ -139,7 +141,6 @@ Block.Search = new Class({
|
|||||||
fill: function(q, json){
|
fill: function(q, json){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.positionMask()
|
|
||||||
self.cache[q] = json
|
self.cache[q] = json
|
||||||
|
|
||||||
self.movies = {}
|
self.movies = {}
|
||||||
@@ -168,19 +169,6 @@ Block.Search = new Class({
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
positionMask: function(){
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
var s = self.result_container.getSize()
|
|
||||||
|
|
||||||
return self.mask.setStyles({
|
|
||||||
'width': s.x,
|
|
||||||
'height': s.y
|
|
||||||
}).position({
|
|
||||||
'relativeTo': self.result_container
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
loading: function(bool){
|
loading: function(bool){
|
||||||
this.el[bool ? 'addClass' : 'removeClass']('loading')
|
this.el[bool ? 'addClass' : 'removeClass']('loading')
|
||||||
},
|
},
|
||||||
@@ -193,7 +181,7 @@ Block.Search = new Class({
|
|||||||
|
|
||||||
Block.Search.Item = new Class({
|
Block.Search.Item = new Class({
|
||||||
|
|
||||||
initialize: function(info){
|
initialize: function(info, options){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.info = info;
|
self.info = info;
|
||||||
@@ -203,14 +191,13 @@ Block.Search.Item = new Class({
|
|||||||
},
|
},
|
||||||
|
|
||||||
create: function(){
|
create: function(){
|
||||||
var self = this;
|
var self = this,
|
||||||
|
info = self.info;
|
||||||
var info = self.info;
|
|
||||||
|
|
||||||
self.el = new Element('div.movie_result', {
|
self.el = new Element('div.movie_result', {
|
||||||
'id': info.imdb
|
'id': info.imdb
|
||||||
}).adopt(
|
}).adopt(
|
||||||
self.options = new Element('div.options.inlay'),
|
self.options_el = new Element('div.options.inlay'),
|
||||||
self.data_container = new Element('div.data', {
|
self.data_container = new Element('div.data', {
|
||||||
'tween': {
|
'tween': {
|
||||||
duration: 400,
|
duration: 400,
|
||||||
@@ -273,11 +260,7 @@ Block.Search.Item = new Class({
|
|||||||
|
|
||||||
self.createOptions();
|
self.createOptions();
|
||||||
|
|
||||||
if(!self.width)
|
self.data_container.addClass('open');
|
||||||
self.width = self.data_container.getCoordinates().width
|
|
||||||
|
|
||||||
self.data_container.tween('left', 0, self.width);
|
|
||||||
|
|
||||||
self.el.addEvent('outerClick', self.closeOptions.bind(self))
|
self.el.addEvent('outerClick', self.closeOptions.bind(self))
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -285,7 +268,7 @@ Block.Search.Item = new Class({
|
|||||||
closeOptions: function(){
|
closeOptions: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.data_container.tween('left', self.width, 0);
|
self.data_container.removeClass('open');
|
||||||
self.el.removeEvents('outerClick')
|
self.el.removeEvents('outerClick')
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -302,29 +285,32 @@ Block.Search.Item = new Class({
|
|||||||
'profile_id': self.profile_select.get('value')
|
'profile_id': self.profile_select.get('value')
|
||||||
},
|
},
|
||||||
'onComplete': function(json){
|
'onComplete': function(json){
|
||||||
self.options.empty();
|
self.options_el.empty();
|
||||||
self.options.adopt(
|
self.options_el.adopt(
|
||||||
new Element('div.message', {
|
new Element('div.message', {
|
||||||
'text': json.added ? 'Movie succesfully added.' : 'Movie didn\'t add properly. Check logs'
|
'text': json.added ? 'Movie successfully added.' : 'Movie didn\'t add properly. Check logs'
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
self.mask.fade('out');
|
||||||
},
|
},
|
||||||
'onFailure': function(){
|
'onFailure': function(){
|
||||||
self.options.empty();
|
self.options_el.empty();
|
||||||
self.options.adopt(
|
self.options_el.adopt(
|
||||||
new Element('div.message', {
|
new Element('div.message', {
|
||||||
'text': 'Something went wrong, check the logs for more info.'
|
'text': 'Something went wrong, check the logs for more info.'
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
self.mask.fade('out');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
createOptions: function(){
|
createOptions: function(){
|
||||||
var self = this;
|
var self = this,
|
||||||
|
info = self.info;
|
||||||
|
|
||||||
|
if(!self.options_el.hasClass('set')){
|
||||||
|
|
||||||
if(!self.options.hasClass('set')){
|
|
||||||
|
|
||||||
if(self.info.in_library){
|
if(self.info.in_library){
|
||||||
var in_library = [];
|
var in_library = [];
|
||||||
self.info.in_library.releases.each(function(release){
|
self.info.in_library.releases.each(function(release){
|
||||||
@@ -332,14 +318,14 @@ Block.Search.Item = new Class({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
self.options.adopt(
|
self.options_el.grab(
|
||||||
new Element('div').adopt(
|
new Element('div').adopt(
|
||||||
self.option_thumbnail = self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', {
|
self.thumbnail = (info.images && info.images.poster.length > 0) ? new Element('img.thumbnail', {
|
||||||
'src': self.info.images.poster[0],
|
'src': info.images.poster[0],
|
||||||
'height': null,
|
'height': null,
|
||||||
'width': null
|
'width': null
|
||||||
}) : null,
|
}) : null,
|
||||||
self.info.in_wanted ? new Element('span.in_wanted', {
|
self.info.in_wanted && self.info.in_wanted.profile ? new Element('span.in_wanted', {
|
||||||
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
|
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
|
||||||
}) : (in_library ? new Element('span.in_library', {
|
}) : (in_library ? new Element('span.in_library', {
|
||||||
'text': 'Already in library: ' + in_library.join(', ')
|
'text': 'Already in library: ' + in_library.join(', ')
|
||||||
@@ -372,7 +358,7 @@ Block.Search.Item = new Class({
|
|||||||
}).inject(self.profile_select)
|
}).inject(self.profile_select)
|
||||||
});
|
});
|
||||||
|
|
||||||
self.options.addClass('set');
|
self.options_el.addClass('set');
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -380,17 +366,7 @@ Block.Search.Item = new Class({
|
|||||||
loadingMask: function(){
|
loadingMask: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
var s = self.options.getSize();
|
self.mask = new Element('span.mask').inject(self.el).fade('hide')
|
||||||
|
|
||||||
self.mask = new Element('span.mask', {
|
|
||||||
'styles': {
|
|
||||||
'position': 'relative',
|
|
||||||
'width': s.x,
|
|
||||||
'height': s.y,
|
|
||||||
'top': -s.y,
|
|
||||||
'display': 'block'
|
|
||||||
}
|
|
||||||
}).inject(self.options).fade('hide')
|
|
||||||
|
|
||||||
createSpinner(self.mask)
|
createSpinner(self.mask)
|
||||||
self.mask.fade('in')
|
self.mask.fade('in')
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ class ProfilePlugin(Plugin):
|
|||||||
for profile in profiles:
|
for profile in profiles:
|
||||||
temp.append(profile.to_dict(self.to_dict))
|
temp.append(profile.to_dict(self.to_dict))
|
||||||
|
|
||||||
#db.close()
|
|
||||||
return temp
|
return temp
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
@@ -84,7 +83,6 @@ class ProfilePlugin(Plugin):
|
|||||||
|
|
||||||
profile_dict = p.to_dict(self.to_dict)
|
profile_dict = p.to_dict(self.to_dict)
|
||||||
|
|
||||||
#db.close()
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'success': True,
|
'success': True,
|
||||||
'profile': profile_dict
|
'profile': profile_dict
|
||||||
@@ -95,7 +93,6 @@ class ProfilePlugin(Plugin):
|
|||||||
db = get_session()
|
db = get_session()
|
||||||
default = db.query(Profile).first()
|
default = db.query(Profile).first()
|
||||||
default_dict = default.to_dict(self.to_dict)
|
default_dict = default.to_dict(self.to_dict)
|
||||||
#db.close()
|
|
||||||
|
|
||||||
return default_dict
|
return default_dict
|
||||||
|
|
||||||
@@ -113,7 +110,6 @@ class ProfilePlugin(Plugin):
|
|||||||
order += 1
|
order += 1
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
#db.close()
|
|
||||||
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'success': True
|
'success': True
|
||||||
@@ -137,8 +133,6 @@ class ProfilePlugin(Plugin):
|
|||||||
except Exception, e:
|
except Exception, e:
|
||||||
message = log.error('Failed deleting Profile: %s', e)
|
message = log.error('Failed deleting Profile: %s', e)
|
||||||
|
|
||||||
#db.close()
|
|
||||||
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'success': success,
|
'success': success,
|
||||||
'message': message
|
'message': message
|
||||||
@@ -181,10 +175,10 @@ class ProfilePlugin(Plugin):
|
|||||||
)
|
)
|
||||||
p.types.append(profile_type)
|
p.types.append(profile_type)
|
||||||
|
|
||||||
db.commit()
|
|
||||||
quality_order += 1
|
quality_order += 1
|
||||||
|
|
||||||
order += 1
|
order += 1
|
||||||
|
|
||||||
#db.close()
|
db.commit()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -86,7 +86,10 @@ var Profile = new Class({
|
|||||||
},
|
},
|
||||||
'onComplete': function(json){
|
'onComplete': function(json){
|
||||||
if(json.success){
|
if(json.success){
|
||||||
self.data = json.profile
|
self.data = json.profile;
|
||||||
|
self.type_container.getElement('li:first-child input[type=checkbox]')
|
||||||
|
.set('checked', true)
|
||||||
|
.getParent().addClass('checked');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -239,9 +242,17 @@ Profile.Type = new Class({
|
|||||||
),
|
),
|
||||||
new Element('span.finish').adopt(
|
new Element('span.finish').adopt(
|
||||||
self.finish = new Element('input.inlay.finish[type=checkbox]', {
|
self.finish = new Element('input.inlay.finish[type=checkbox]', {
|
||||||
'checked': data.finish,
|
'checked': data.finish !== undefined ? data.finish : 1,
|
||||||
'events': {
|
'events': {
|
||||||
'change': self.fireEvent.bind(self, 'change')
|
'change': function(e){
|
||||||
|
if(self.el == self.el.getParent().getElement(':first-child')){
|
||||||
|
self.finish_class.check();
|
||||||
|
alert('Top quality always finishes the search')
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.fireEvent('change');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
@@ -255,7 +266,7 @@ Profile.Type = new Class({
|
|||||||
|
|
||||||
self.el[self.data.quality_id > 0 ? 'removeClass' : 'addClass']('is_empty');
|
self.el[self.data.quality_id > 0 ? 'removeClass' : 'addClass']('is_empty');
|
||||||
|
|
||||||
new Form.Check(self.finish);
|
self.finish_class = new Form.Check(self.finish);
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
|
|||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Quality, Profile, ProfileType
|
from couchpotato.core.settings.model import Quality, Profile, ProfileType
|
||||||
|
from sqlalchemy.sql.expression import or_
|
||||||
import os.path
|
import os.path
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -17,15 +19,15 @@ class QualityPlugin(Plugin):
|
|||||||
|
|
||||||
qualities = [
|
qualities = [
|
||||||
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
|
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
|
||||||
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
|
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
|
||||||
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
|
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
|
||||||
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']},
|
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']},
|
||||||
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
|
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
|
||||||
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
|
||||||
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']},
|
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||||
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
|
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||||
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||||
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||||
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
|
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
|
||||||
]
|
]
|
||||||
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
|
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
|
||||||
@@ -68,7 +70,6 @@ class QualityPlugin(Plugin):
|
|||||||
q = mergeDicts(self.getQuality(quality.identifier), quality.to_dict())
|
q = mergeDicts(self.getQuality(quality.identifier), quality.to_dict())
|
||||||
temp.append(q)
|
temp.append(q)
|
||||||
|
|
||||||
#db.close()
|
|
||||||
return temp
|
return temp
|
||||||
|
|
||||||
def single(self, identifier = ''):
|
def single(self, identifier = ''):
|
||||||
@@ -76,11 +77,10 @@ class QualityPlugin(Plugin):
|
|||||||
db = get_session()
|
db = get_session()
|
||||||
quality_dict = {}
|
quality_dict = {}
|
||||||
|
|
||||||
quality = db.query(Quality).filter_by(identifier = identifier).first()
|
quality = db.query(Quality).filter(or_(Quality.identifier == identifier, Quality.id == identifier)).first()
|
||||||
if quality:
|
if quality:
|
||||||
quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict())
|
quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict())
|
||||||
|
|
||||||
#db.close()
|
|
||||||
return quality_dict
|
return quality_dict
|
||||||
|
|
||||||
def getQuality(self, identifier):
|
def getQuality(self, identifier):
|
||||||
@@ -100,7 +100,6 @@ class QualityPlugin(Plugin):
|
|||||||
setattr(quality, params.get('value_type'), params.get('value'))
|
setattr(quality, params.get('value_type'), params.get('value'))
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
#db.close()
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'success': True
|
'success': True
|
||||||
})
|
})
|
||||||
@@ -113,46 +112,48 @@ class QualityPlugin(Plugin):
|
|||||||
for q in self.qualities:
|
for q in self.qualities:
|
||||||
|
|
||||||
# Create quality
|
# Create quality
|
||||||
quality = db.query(Quality).filter_by(identifier = q.get('identifier')).first()
|
qual = db.query(Quality).filter_by(identifier = q.get('identifier')).first()
|
||||||
|
|
||||||
if not quality:
|
if not qual:
|
||||||
log.info('Creating quality: %s', q.get('label'))
|
log.info('Creating quality: %s', q.get('label'))
|
||||||
quality = Quality()
|
qual = Quality()
|
||||||
db.add(quality)
|
qual.order = order
|
||||||
|
qual.identifier = q.get('identifier')
|
||||||
|
qual.label = toUnicode(q.get('label'))
|
||||||
|
qual.size_min, qual.size_max = q.get('size')
|
||||||
|
|
||||||
quality.order = order
|
db.add(qual)
|
||||||
quality.identifier = q.get('identifier')
|
|
||||||
quality.label = toUnicode(q.get('label'))
|
|
||||||
quality.size_min, quality.size_max = q.get('size')
|
|
||||||
|
|
||||||
# Create single quality profile
|
# Create single quality profile
|
||||||
profile = db.query(Profile).filter(
|
prof = db.query(Profile).filter(
|
||||||
Profile.core == True
|
Profile.core == True
|
||||||
).filter(
|
).filter(
|
||||||
Profile.types.any(quality = quality)
|
Profile.types.any(quality = qual)
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
if not profile:
|
if not prof:
|
||||||
log.info('Creating profile: %s', q.get('label'))
|
log.info('Creating profile: %s', q.get('label'))
|
||||||
profile = Profile(
|
prof = Profile(
|
||||||
core = True,
|
core = True,
|
||||||
label = toUnicode(quality.label),
|
label = toUnicode(qual.label),
|
||||||
order = order
|
order = order
|
||||||
)
|
)
|
||||||
db.add(profile)
|
db.add(prof)
|
||||||
|
|
||||||
profile_type = ProfileType(
|
profile_type = ProfileType(
|
||||||
quality = quality,
|
quality = qual,
|
||||||
profile = profile,
|
profile = prof,
|
||||||
finish = True,
|
finish = True,
|
||||||
order = 0
|
order = 0
|
||||||
)
|
)
|
||||||
profile.types.append(profile_type)
|
prof.types.append(profile_type)
|
||||||
|
|
||||||
order += 1
|
order += 1
|
||||||
db.commit()
|
|
||||||
|
|
||||||
#db.close()
|
db.commit()
|
||||||
|
|
||||||
|
time.sleep(0.3) # Wait a moment
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def guess(self, files, extra = {}):
|
def guess(self, files, extra = {}):
|
||||||
@@ -198,9 +199,14 @@ class QualityPlugin(Plugin):
|
|||||||
|
|
||||||
for quality in self.all():
|
for quality in self.all():
|
||||||
|
|
||||||
# Last check on resolution only
|
# Check width resolution, range 20
|
||||||
if quality.get('width', 480) == extra.get('resolution_width', 0):
|
if (quality.get('width', 720) - 20) <= extra.get('resolution_width', 0) <= (quality.get('width', 720) + 20):
|
||||||
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width', 480), extra.get('resolution_width', 0)))
|
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width', 720), extra.get('resolution_width', 0)))
|
||||||
|
return self.setCache(hash, quality)
|
||||||
|
|
||||||
|
# Check height resolution, range 20
|
||||||
|
if (quality.get('height', 480) - 20) <= extra.get('resolution_height', 0) <= (quality.get('height', 480) + 20):
|
||||||
|
log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height', 480), extra.get('resolution_height', 0)))
|
||||||
return self.setCache(hash, quality)
|
return self.setCache(hash, quality)
|
||||||
|
|
||||||
if 480 <= extra.get('resolution_width', 0) <= 720:
|
if 480 <= extra.get('resolution_width', 0) <= 720:
|
||||||
|
|||||||
@@ -88,8 +88,6 @@ class Release(Plugin):
|
|||||||
|
|
||||||
fireEvent('movie.restatus', movie.id)
|
fireEvent('movie.restatus', movie.id)
|
||||||
|
|
||||||
#db.close()
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -108,7 +106,6 @@ class Release(Plugin):
|
|||||||
|
|
||||||
release_id = getParam('id')
|
release_id = getParam('id')
|
||||||
|
|
||||||
#db.close()
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'success': self.delete(release_id)
|
'success': self.delete(release_id)
|
||||||
})
|
})
|
||||||
@@ -136,6 +133,9 @@ class Release(Plugin):
|
|||||||
db.delete(release_file)
|
db.delete(release_file)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
if len(rel.files) == 0:
|
||||||
|
self.delete(id)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
@@ -152,7 +152,6 @@ class Release(Plugin):
|
|||||||
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id')
|
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id')
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
#db.close()
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'success': True
|
'success': True
|
||||||
})
|
})
|
||||||
@@ -161,6 +160,7 @@ class Release(Plugin):
|
|||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
id = getParam('id')
|
id = getParam('id')
|
||||||
|
status_snatched = fireEvent('status.add', 'snatched', single = True)
|
||||||
|
|
||||||
rel = db.query(Relea).filter_by(id = id).first()
|
rel = db.query(Relea).filter_by(id = id).first()
|
||||||
if rel:
|
if rel:
|
||||||
@@ -181,14 +181,16 @@ class Release(Plugin):
|
|||||||
'files': {}
|
'files': {}
|
||||||
}), manual = True, single = True)
|
}), manual = True, single = True)
|
||||||
|
|
||||||
#db.close()
|
if success:
|
||||||
|
rel.status_id = status_snatched.get('id')
|
||||||
|
db.commit()
|
||||||
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'success': success
|
'success': success
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
log.error('Couldn\'t find release with id: %s', id)
|
log.error('Couldn\'t find release with id: %s', id)
|
||||||
|
|
||||||
#db.close()
|
|
||||||
return jsonified({
|
return jsonified({
|
||||||
'success': False
|
'success': False
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -82,6 +82,15 @@ config = [{
|
|||||||
'unit': 'min(s)',
|
'unit': 'min(s)',
|
||||||
'description': 'Detect movie status every X minutes. Will start the renamer if movie is <strong>completed</strong> or handle <strong>failed</strong> download if these options are enabled',
|
'description': 'Detect movie status every X minutes. Will start the renamer if movie is <strong>completed</strong> or handle <strong>failed</strong> download if these options are enabled',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'advanced': True,
|
||||||
|
'name': 'force_every',
|
||||||
|
'label': 'Force every',
|
||||||
|
'default': 2,
|
||||||
|
'type': 'int',
|
||||||
|
'unit': 'hour(s)',
|
||||||
|
'description': 'Forces the renamer to scan every X hours',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'advanced': True,
|
'advanced': True,
|
||||||
'name': 'next_on_failed',
|
'name': 'next_on_failed',
|
||||||
@@ -124,13 +133,6 @@ config = [{
|
|||||||
'type': 'choice',
|
'type': 'choice',
|
||||||
'options': rename_options
|
'options': rename_options
|
||||||
},
|
},
|
||||||
{
|
|
||||||
'name': 'trailer_name',
|
|
||||||
'label': 'Trailer naming',
|
|
||||||
'default': '<filename>-trailer.<ext>',
|
|
||||||
'type': 'choice',
|
|
||||||
'options': rename_options
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ from couchpotato.api import addApiView
|
|||||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||||
from couchpotato.core.helpers.encoding import toUnicode, ss
|
from couchpotato.core.helpers.encoding import toUnicode, ss
|
||||||
from couchpotato.core.helpers.request import jsonified
|
from couchpotato.core.helpers.request import jsonified
|
||||||
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle
|
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
|
||||||
|
getImdb
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Library, File, Profile, Release
|
from couchpotato.core.settings.model import Library, File, Profile, Release
|
||||||
@@ -20,6 +21,7 @@ log = CPLog(__name__)
|
|||||||
class Renamer(Plugin):
|
class Renamer(Plugin):
|
||||||
|
|
||||||
renaming_started = False
|
renaming_started = False
|
||||||
|
checking_snatched = False
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
@@ -31,8 +33,13 @@ class Renamer(Plugin):
|
|||||||
addEvent('renamer.check_snatched', self.checkSnatched)
|
addEvent('renamer.check_snatched', self.checkSnatched)
|
||||||
|
|
||||||
addEvent('app.load', self.scan)
|
addEvent('app.load', self.scan)
|
||||||
|
addEvent('app.load', self.checkSnatched)
|
||||||
|
|
||||||
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
|
if self.conf('run_every') > 0:
|
||||||
|
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
|
||||||
|
|
||||||
|
if self.conf('force_every') > 0:
|
||||||
|
fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every'))
|
||||||
|
|
||||||
def scanView(self):
|
def scanView(self):
|
||||||
|
|
||||||
@@ -48,7 +55,7 @@ class Renamer(Plugin):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.renaming_started is True:
|
if self.renaming_started is True:
|
||||||
log.info('Renamer is disabled to avoid infinite looping of the same error.')
|
log.info('Renamer is already running, if you see this often, check the logs above for errors.')
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check to see if the "to" folder is inside the "from" folder.
|
# Check to see if the "to" folder is inside the "from" folder.
|
||||||
@@ -127,6 +134,8 @@ class Renamer(Plugin):
|
|||||||
'resolution_width': group['meta_data'].get('resolution_width'),
|
'resolution_width': group['meta_data'].get('resolution_width'),
|
||||||
'resolution_height': group['meta_data'].get('resolution_height'),
|
'resolution_height': group['meta_data'].get('resolution_height'),
|
||||||
'imdb_id': library['identifier'],
|
'imdb_id': library['identifier'],
|
||||||
|
'cd': '',
|
||||||
|
'cd_nr': '',
|
||||||
}
|
}
|
||||||
|
|
||||||
for file_type in group['files']:
|
for file_type in group['files']:
|
||||||
@@ -144,7 +153,7 @@ class Renamer(Plugin):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Move other files
|
# Move other files
|
||||||
multiple = len(group['files']['movie']) > 1 and not group['is_dvd']
|
multiple = len(group['files'][file_type]) > 1 and not group['is_dvd']
|
||||||
cd = 1 if multiple else 0
|
cd = 1 if multiple else 0
|
||||||
|
|
||||||
for current_file in sorted(list(group['files'][file_type])):
|
for current_file in sorted(list(group['files'][file_type])):
|
||||||
@@ -157,23 +166,19 @@ class Renamer(Plugin):
|
|||||||
replacements['ext'] = getExt(current_file)
|
replacements['ext'] = getExt(current_file)
|
||||||
|
|
||||||
# cd #
|
# cd #
|
||||||
replacements['cd'] = ' cd%d' % cd if cd else ''
|
replacements['cd'] = ' cd%d' % cd if multiple else ''
|
||||||
replacements['cd_nr'] = cd
|
replacements['cd_nr'] = cd if multiple else ''
|
||||||
|
|
||||||
# Naming
|
# Naming
|
||||||
final_folder_name = self.doReplace(folder_name, replacements)
|
final_folder_name = self.doReplace(folder_name, replacements)
|
||||||
final_file_name = self.doReplace(file_name, replacements)
|
final_file_name = self.doReplace(file_name, replacements)
|
||||||
replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
|
replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
|
||||||
|
|
||||||
# Group filename without cd extension
|
|
||||||
replacements['cd'] = ''
|
|
||||||
replacements['cd_nr'] = ''
|
|
||||||
|
|
||||||
# Meta naming
|
# Meta naming
|
||||||
if file_type is 'trailer':
|
if file_type is 'trailer':
|
||||||
final_file_name = self.doReplace(trailer_name, replacements)
|
final_file_name = self.doReplace(trailer_name, replacements, remove_multiple = True)
|
||||||
elif file_type is 'nfo':
|
elif file_type is 'nfo':
|
||||||
final_file_name = self.doReplace(nfo_name, replacements)
|
final_file_name = self.doReplace(nfo_name, replacements, remove_multiple = True)
|
||||||
|
|
||||||
# Seperator replace
|
# Seperator replace
|
||||||
if separator:
|
if separator:
|
||||||
@@ -204,10 +209,16 @@ class Renamer(Plugin):
|
|||||||
# Check for extra subtitle files
|
# Check for extra subtitle files
|
||||||
if file_type is 'subtitle':
|
if file_type is 'subtitle':
|
||||||
|
|
||||||
# rename subtitles with or without language
|
remove_multiple = False
|
||||||
rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
|
if len(group['files']['movie']) == 1:
|
||||||
|
remove_multiple = True
|
||||||
|
|
||||||
sub_langs = group['subtitle_language'].get(current_file, [])
|
sub_langs = group['subtitle_language'].get(current_file, [])
|
||||||
|
|
||||||
|
# rename subtitles with or without language
|
||||||
|
sub_name = self.doReplace(file_name, replacements, remove_multiple = remove_multiple)
|
||||||
|
rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
|
||||||
|
|
||||||
rename_extras = self.getRenameExtras(
|
rename_extras = self.getRenameExtras(
|
||||||
extra_type = 'subtitle_extra',
|
extra_type = 'subtitle_extra',
|
||||||
replacements = replacements,
|
replacements = replacements,
|
||||||
@@ -215,20 +226,19 @@ class Renamer(Plugin):
|
|||||||
file_name = file_name,
|
file_name = file_name,
|
||||||
destination = destination,
|
destination = destination,
|
||||||
group = group,
|
group = group,
|
||||||
current_file = current_file
|
current_file = current_file,
|
||||||
|
remove_multiple = remove_multiple,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Don't add language if multiple languages in 1 file
|
# Don't add language if multiple languages in 1 subtitle file
|
||||||
if len(sub_langs) > 1:
|
if len(sub_langs) == 1:
|
||||||
rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
|
|
||||||
elif len(sub_langs) == 1:
|
|
||||||
sub_name = final_file_name.replace(replacements['ext'], '%s.%s' % (sub_langs[0], replacements['ext']))
|
sub_name = final_file_name.replace(replacements['ext'], '%s.%s' % (sub_langs[0], replacements['ext']))
|
||||||
rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
|
rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
|
||||||
|
|
||||||
rename_files = mergeDicts(rename_files, rename_extras)
|
rename_files = mergeDicts(rename_files, rename_extras)
|
||||||
|
|
||||||
# Filename without cd etc
|
# Filename without cd etc
|
||||||
if file_type is 'movie':
|
elif file_type is 'movie':
|
||||||
rename_extras = self.getRenameExtras(
|
rename_extras = self.getRenameExtras(
|
||||||
extra_type = 'movie_extra',
|
extra_type = 'movie_extra',
|
||||||
replacements = replacements,
|
replacements = replacements,
|
||||||
@@ -240,7 +250,7 @@ class Renamer(Plugin):
|
|||||||
)
|
)
|
||||||
rename_files = mergeDicts(rename_files, rename_extras)
|
rename_files = mergeDicts(rename_files, rename_extras)
|
||||||
|
|
||||||
group['filename'] = self.doReplace(file_name, replacements)[:-(len(getExt(final_file_name)) + 1)]
|
group['filename'] = self.doReplace(file_name, replacements, remove_multiple = True)[:-(len(getExt(final_file_name)) + 1)]
|
||||||
group['destination_dir'] = os.path.join(destination, final_folder_name)
|
group['destination_dir'] = os.path.join(destination, final_folder_name)
|
||||||
|
|
||||||
if multiple:
|
if multiple:
|
||||||
@@ -304,7 +314,10 @@ class Renamer(Plugin):
|
|||||||
elif release.status_id is snatched_status.get('id'):
|
elif release.status_id is snatched_status.get('id'):
|
||||||
if release.quality.id is group['meta_data']['quality']['id']:
|
if release.quality.id is group['meta_data']['quality']['id']:
|
||||||
log.debug('Marking release as downloaded')
|
log.debug('Marking release as downloaded')
|
||||||
release.status_id = downloaded_status.get('id')
|
try:
|
||||||
|
release.status_id = downloaded_status.get('id')
|
||||||
|
except Exception, e:
|
||||||
|
log.error('Failed marking release as finished: %s %s', (e, traceback.format_exc()))
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Remove leftover files
|
# Remove leftover files
|
||||||
@@ -328,6 +341,7 @@ class Renamer(Plugin):
|
|||||||
|
|
||||||
log.info('Removing "%s"', src)
|
log.info('Removing "%s"', src)
|
||||||
try:
|
try:
|
||||||
|
src = ss(src)
|
||||||
if os.path.isfile(src):
|
if os.path.isfile(src):
|
||||||
os.remove(src)
|
os.remove(src)
|
||||||
|
|
||||||
@@ -341,7 +355,10 @@ class Renamer(Plugin):
|
|||||||
|
|
||||||
# Delete leftover folder from older releases
|
# Delete leftover folder from older releases
|
||||||
for delete_folder in delete_folders:
|
for delete_folder in delete_folders:
|
||||||
self.deleteEmptyFolder(delete_folder, show_error = False)
|
try:
|
||||||
|
self.deleteEmptyFolder(delete_folder, show_error = False)
|
||||||
|
except Exception, e:
|
||||||
|
log.error('Failed to delete folder: %s %s', (e, traceback.format_exc()))
|
||||||
|
|
||||||
# Rename all files marked
|
# Rename all files marked
|
||||||
group['renamed_files'] = []
|
group['renamed_files'] = []
|
||||||
@@ -375,22 +392,22 @@ class Renamer(Plugin):
|
|||||||
except:
|
except:
|
||||||
log.error('Failed removing %s: %s', (group['parentdir'], traceback.format_exc()))
|
log.error('Failed removing %s: %s', (group['parentdir'], traceback.format_exc()))
|
||||||
|
|
||||||
# Search for trailers etc
|
# Notify on download, search for trailers etc
|
||||||
fireEventAsync('renamer.after', group)
|
|
||||||
|
|
||||||
# Notify on download
|
|
||||||
download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality'])
|
download_message = 'Downloaded %s (%s)' % (movie_title, replacements['quality'])
|
||||||
fireEventAsync('movie.downloaded', message = download_message, data = group)
|
try:
|
||||||
|
fireEvent('renamer.after', message = download_message, group = group, in_order = True)
|
||||||
|
except:
|
||||||
|
log.error('Failed firing (some) of the renamer.after events: %s', traceback.format_exc())
|
||||||
|
|
||||||
# Break if CP wants to shut down
|
# Break if CP wants to shut down
|
||||||
if self.shuttingDown():
|
if self.shuttingDown():
|
||||||
break
|
break
|
||||||
|
|
||||||
#db.close()
|
|
||||||
self.renaming_started = False
|
self.renaming_started = False
|
||||||
|
|
||||||
def getRenameExtras(self, extra_type = '', replacements = {}, folder_name = '', file_name = '', destination = '', group = {}, current_file = ''):
|
def getRenameExtras(self, extra_type = '', replacements = {}, folder_name = '', file_name = '', destination = '', group = {}, current_file = '', remove_multiple = False):
|
||||||
|
|
||||||
|
replacements = replacements.copy()
|
||||||
rename_files = {}
|
rename_files = {}
|
||||||
|
|
||||||
def test(s):
|
def test(s):
|
||||||
@@ -399,8 +416,8 @@ class Renamer(Plugin):
|
|||||||
for extra in set(filter(test, group['files'][extra_type])):
|
for extra in set(filter(test, group['files'][extra_type])):
|
||||||
replacements['ext'] = getExt(extra)
|
replacements['ext'] = getExt(extra)
|
||||||
|
|
||||||
final_folder_name = self.doReplace(folder_name, replacements)
|
final_folder_name = self.doReplace(folder_name, replacements, remove_multiple = remove_multiple)
|
||||||
final_file_name = self.doReplace(file_name, replacements)
|
final_file_name = self.doReplace(file_name, replacements, remove_multiple = remove_multiple)
|
||||||
rename_files[extra] = os.path.join(destination, final_folder_name, final_file_name)
|
rename_files[extra] = os.path.join(destination, final_folder_name, final_file_name)
|
||||||
|
|
||||||
return rename_files
|
return rename_files
|
||||||
@@ -455,11 +472,16 @@ class Renamer(Plugin):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def doReplace(self, string, replacements):
|
def doReplace(self, string, replacements, remove_multiple = False):
|
||||||
'''
|
'''
|
||||||
replace confignames with the real thing
|
replace confignames with the real thing
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
replacements = replacements.copy()
|
||||||
|
if remove_multiple:
|
||||||
|
replacements['cd'] = ''
|
||||||
|
replacements['cd_nr'] = ''
|
||||||
|
|
||||||
replaced = toUnicode(string)
|
replaced = toUnicode(string)
|
||||||
for x, r in replacements.iteritems():
|
for x, r in replacements.iteritems():
|
||||||
if r is not None:
|
if r is not None:
|
||||||
@@ -477,6 +499,7 @@ class Renamer(Plugin):
|
|||||||
return string.replace(' ', ' ').replace(' .', '.')
|
return string.replace(' ', ' ').replace(' .', '.')
|
||||||
|
|
||||||
def deleteEmptyFolder(self, folder, show_error = True):
|
def deleteEmptyFolder(self, folder, show_error = True):
|
||||||
|
folder = ss(folder)
|
||||||
|
|
||||||
loge = log.error if show_error else log.debug
|
loge = log.error if show_error else log.debug
|
||||||
for root, dirs, files in os.walk(folder):
|
for root, dirs, files in os.walk(folder):
|
||||||
@@ -495,6 +518,11 @@ class Renamer(Plugin):
|
|||||||
loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
|
loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
|
||||||
|
|
||||||
def checkSnatched(self):
|
def checkSnatched(self):
|
||||||
|
if self.checking_snatched:
|
||||||
|
log.debug('Already checking snatched')
|
||||||
|
|
||||||
|
self.checking_snatched = True
|
||||||
|
|
||||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
||||||
ignored_status = fireEvent('status.get', 'ignored', single = True)
|
ignored_status = fireEvent('status.get', 'ignored', single = True)
|
||||||
failed_status = fireEvent('status.get', 'failed', single = True)
|
failed_status = fireEvent('status.get', 'failed', single = True)
|
||||||
@@ -504,57 +532,69 @@ class Renamer(Plugin):
|
|||||||
db = get_session()
|
db = get_session()
|
||||||
rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all()
|
rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all()
|
||||||
|
|
||||||
if rels:
|
|
||||||
log.debug('Checking status snatched releases...')
|
|
||||||
|
|
||||||
scan_required = False
|
scan_required = False
|
||||||
|
|
||||||
for rel in rels:
|
if rels:
|
||||||
|
self.checking_snatched = True
|
||||||
|
log.debug('Checking status snatched releases...')
|
||||||
|
|
||||||
# Get current selected title
|
statuses = fireEvent('download.status', merge = True)
|
||||||
default_title = ''
|
if not statuses:
|
||||||
for title in rel.movie.library.titles:
|
|
||||||
if title.default: default_title = title.title
|
|
||||||
|
|
||||||
# Check if movie has already completed and is manage tab (legacy db correction)
|
|
||||||
if rel.movie.status_id == done_status.get('id'):
|
|
||||||
log.debug('Found a completed movie with a snatched release : %s. Setting release status to ignored...' , default_title)
|
|
||||||
rel.status_id = ignored_status.get('id')
|
|
||||||
db.commit()
|
|
||||||
continue
|
|
||||||
|
|
||||||
item = {}
|
|
||||||
for info in rel.info:
|
|
||||||
item[info.identifier] = info.value
|
|
||||||
|
|
||||||
movie_dict = fireEvent('movie.get', rel.movie_id, single = True)
|
|
||||||
|
|
||||||
# check status
|
|
||||||
downloadstatus = fireEvent('download.status', data = item, movie = movie_dict, single = True)
|
|
||||||
if not downloadstatus:
|
|
||||||
log.debug('Download status functionality is not implemented for active downloaders.')
|
log.debug('Download status functionality is not implemented for active downloaders.')
|
||||||
scan_required = True
|
scan_required = True
|
||||||
else:
|
else:
|
||||||
log.debug('Download status: %s' , downloadstatus)
|
try:
|
||||||
|
for rel in rels:
|
||||||
|
rel_dict = rel.to_dict({'info': {}})
|
||||||
|
|
||||||
if downloadstatus == 'failed':
|
# Get current selected title
|
||||||
if self.conf('next_on_failed'):
|
default_title = getTitle(rel.movie.library)
|
||||||
fireEvent('searcher.try_next_release', movie_id = rel.movie_id)
|
|
||||||
else:
|
|
||||||
rel.status_id = failed_status.get('id')
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
log.info('Download of %s failed.', item['name'])
|
# Check if movie has already completed and is manage tab (legacy db correction)
|
||||||
|
if rel.movie.status_id == done_status.get('id'):
|
||||||
|
log.debug('Found a completed movie with a snatched release : %s. Setting release status to ignored...' , default_title)
|
||||||
|
rel.status_id = ignored_status.get('id')
|
||||||
|
db.commit()
|
||||||
|
continue
|
||||||
|
|
||||||
elif downloadstatus == 'completed':
|
movie_dict = fireEvent('movie.get', rel.movie_id, single = True)
|
||||||
log.info('Download of %s completed!', item['name'])
|
|
||||||
scan_required = True
|
|
||||||
|
|
||||||
elif downloadstatus == 'not_found':
|
# check status
|
||||||
log.info('%s not found in downloaders', item['name'])
|
nzbname = self.createNzbName(rel_dict['info'], movie_dict)
|
||||||
rel.status_id = ignored_status.get('id')
|
|
||||||
db.commit()
|
found = False
|
||||||
|
for item in statuses:
|
||||||
|
if item['name'] == nzbname or getImdb(item['name']) == movie_dict['library']['identifier']:
|
||||||
|
|
||||||
|
timeleft = 'N/A' if item['timeleft'] == -1 else item['timeleft']
|
||||||
|
log.debug('Found %s: %s, time to go: %s', (item['name'], item['status'].upper(), timeleft))
|
||||||
|
|
||||||
|
if item['status'] == 'busy':
|
||||||
|
pass
|
||||||
|
elif item['status'] == 'failed':
|
||||||
|
fireEvent('download.remove_failed', item, single = True)
|
||||||
|
|
||||||
|
if self.conf('next_on_failed'):
|
||||||
|
fireEvent('searcher.try_next_release', movie_id = rel.movie_id)
|
||||||
|
else:
|
||||||
|
rel.status_id = failed_status.get('id')
|
||||||
|
db.commit()
|
||||||
|
elif item['status'] == 'completed':
|
||||||
|
log.info('Download of %s completed!', item['name'])
|
||||||
|
scan_required = True
|
||||||
|
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
log.info('%s not found in downloaders', nzbname)
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed checking for release in downloader: %s', traceback.format_exc())
|
||||||
|
|
||||||
# Note that Queued, Downloading, Paused, Repair and Unpackimg are also available as status for SabNZBd
|
|
||||||
if scan_required:
|
if scan_required:
|
||||||
fireEvent('renamer.scan')
|
fireEvent('renamer.scan')
|
||||||
|
|
||||||
|
self.checking_snatched = False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class Scanner(Plugin):
|
|||||||
'media': 314572800, # 300MB
|
'media': 314572800, # 300MB
|
||||||
'trailer': 1048576, # 1MB
|
'trailer': 1048576, # 1MB
|
||||||
}
|
}
|
||||||
ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '_failed_remove_', '_failed_rename_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
|
ignored_in_path = ['extracting', '_unpack', '_failed_', '_unknown_', '_exists_', '_failed_remove_', '_failed_rename_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
|
||||||
ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
|
ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
|
||||||
extensions = {
|
extensions = {
|
||||||
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts', 'm4v'],
|
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts', 'm4v'],
|
||||||
@@ -34,6 +34,7 @@ class Scanner(Plugin):
|
|||||||
'subtitle_extra': ['idx'],
|
'subtitle_extra': ['idx'],
|
||||||
'trailer': ['mov', 'mp4', 'flv']
|
'trailer': ['mov', 'mp4', 'flv']
|
||||||
}
|
}
|
||||||
|
|
||||||
file_types = {
|
file_types = {
|
||||||
'subtitle': ('subtitle', 'subtitle'),
|
'subtitle': ('subtitle', 'subtitle'),
|
||||||
'subtitle_extra': ('subtitle', 'subtitle_extra'),
|
'subtitle_extra': ('subtitle', 'subtitle_extra'),
|
||||||
@@ -42,6 +43,8 @@ class Scanner(Plugin):
|
|||||||
'movie': ('video', 'movie'),
|
'movie': ('video', 'movie'),
|
||||||
'movie_extra': ('movie', 'movie_extra'),
|
'movie_extra': ('movie', 'movie_extra'),
|
||||||
'backdrop': ('image', 'backdrop'),
|
'backdrop': ('image', 'backdrop'),
|
||||||
|
'poster': ('image', 'poster'),
|
||||||
|
'thumbnail': ('image', 'thumbnail'),
|
||||||
'leftover': ('leftover', 'leftover'),
|
'leftover': ('leftover', 'leftover'),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,6 +53,20 @@ class Scanner(Plugin):
|
|||||||
'video': ['x264', 'h264', 'divx', 'xvid']
|
'video': ['x264', 'h264', 'divx', 'xvid']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
audio_codec_map = {
|
||||||
|
0x2000: 'ac3',
|
||||||
|
0x2001: 'dts',
|
||||||
|
0x0055: 'mp3',
|
||||||
|
0x0050: 'mp2',
|
||||||
|
0x0001: 'pcm',
|
||||||
|
0x003: 'pcm',
|
||||||
|
0x77a1: 'tta1',
|
||||||
|
0x5756: 'wav',
|
||||||
|
0x6750: 'vorbis',
|
||||||
|
0xF1AC: 'flac',
|
||||||
|
0x00ff: 'aac',
|
||||||
|
}
|
||||||
|
|
||||||
source_media = {
|
source_media = {
|
||||||
'bluray': ['bluray', 'blu-ray', 'brrip', 'br-rip'],
|
'bluray': ['bluray', 'blu-ray', 'brrip', 'br-rip'],
|
||||||
'hddvd': ['hddvd', 'hd-dvd'],
|
'hddvd': ['hddvd', 'hd-dvd'],
|
||||||
@@ -72,7 +89,7 @@ class Scanner(Plugin):
|
|||||||
'()([ab])(\.....?)$' #*a.mkv
|
'()([ab])(\.....?)$' #*a.mkv
|
||||||
]
|
]
|
||||||
|
|
||||||
cp_imdb = '(\.cp\((?P<id>tt[0-9{7}]+)\))'
|
cp_imdb = '(.cp.(?P<id>tt[0-9{7}]+).)'
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
@@ -80,55 +97,10 @@ class Scanner(Plugin):
|
|||||||
addEvent('scanner.remove_cptag', self.removeCPTag)
|
addEvent('scanner.remove_cptag', self.removeCPTag)
|
||||||
|
|
||||||
addEvent('scanner.scan', self.scan)
|
addEvent('scanner.scan', self.scan)
|
||||||
addEvent('scanner.files', self.scanFilesToLibrary)
|
|
||||||
addEvent('scanner.folder', self.scanFolderToLibrary)
|
|
||||||
addEvent('scanner.name_year', self.getReleaseNameYear)
|
addEvent('scanner.name_year', self.getReleaseNameYear)
|
||||||
addEvent('scanner.partnumber', self.getPartNumber)
|
addEvent('scanner.partnumber', self.getPartNumber)
|
||||||
|
|
||||||
def after_rename(group):
|
def scan(self, folder = None, files = None, simple = False, newer_than = 0, on_found = None):
|
||||||
return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'])
|
|
||||||
|
|
||||||
addEvent('renamer.after', after_rename)
|
|
||||||
|
|
||||||
def scanFilesToLibrary(self, folder = None, files = None):
|
|
||||||
|
|
||||||
folder = os.path.normpath(folder)
|
|
||||||
|
|
||||||
groups = self.scan(folder = folder, files = files)
|
|
||||||
|
|
||||||
for group in groups.itervalues():
|
|
||||||
if group['library']:
|
|
||||||
fireEvent('release.add', group = group)
|
|
||||||
|
|
||||||
def scanFolderToLibrary(self, folder = None, newer_than = 0, simple = True):
|
|
||||||
|
|
||||||
folder = os.path.normpath(folder)
|
|
||||||
|
|
||||||
if not os.path.isdir(folder):
|
|
||||||
return
|
|
||||||
|
|
||||||
groups = self.scan(folder = folder, simple = simple, newer_than = newer_than)
|
|
||||||
|
|
||||||
added_identifier = []
|
|
||||||
while True and not self.shuttingDown():
|
|
||||||
try:
|
|
||||||
identifier, group = groups.popitem()
|
|
||||||
except:
|
|
||||||
break
|
|
||||||
|
|
||||||
# Save to DB
|
|
||||||
if group['library']:
|
|
||||||
|
|
||||||
# Add release
|
|
||||||
fireEvent('release.add', group = group)
|
|
||||||
library_item = fireEvent('library.update', identifier = group['library'].get('identifier'), single = True)
|
|
||||||
if library_item:
|
|
||||||
added_identifier.append(library_item['identifier'])
|
|
||||||
|
|
||||||
return added_identifier
|
|
||||||
|
|
||||||
|
|
||||||
def scan(self, folder = None, files = [], simple = False, newer_than = 0):
|
|
||||||
|
|
||||||
folder = ss(os.path.normpath(folder))
|
folder = ss(os.path.normpath(folder))
|
||||||
|
|
||||||
@@ -141,7 +113,8 @@ class Scanner(Plugin):
|
|||||||
leftovers = []
|
leftovers = []
|
||||||
|
|
||||||
# Scan all files of the folder if no files are set
|
# Scan all files of the folder if no files are set
|
||||||
if len(files) == 0:
|
if not files:
|
||||||
|
check_file_date = True
|
||||||
try:
|
try:
|
||||||
files = []
|
files = []
|
||||||
for root, dirs, walk_files in os.walk(folder):
|
for root, dirs, walk_files in os.walk(folder):
|
||||||
@@ -150,6 +123,7 @@ class Scanner(Plugin):
|
|||||||
except:
|
except:
|
||||||
log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
|
log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
|
||||||
else:
|
else:
|
||||||
|
check_file_date = False
|
||||||
files = [ss(x) for x in files]
|
files = [ss(x) for x in files]
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
@@ -279,8 +253,8 @@ class Scanner(Plugin):
|
|||||||
del path_identifiers[identifier]
|
del path_identifiers[identifier]
|
||||||
del delete_identifiers
|
del delete_identifiers
|
||||||
|
|
||||||
# Determine file types
|
# Make sure we remove older / still extracting files
|
||||||
processed_movies = {}
|
valid_files = {}
|
||||||
while True and not self.shuttingDown():
|
while True and not self.shuttingDown():
|
||||||
try:
|
try:
|
||||||
identifier, group = movie_files.popitem()
|
identifier, group = movie_files.popitem()
|
||||||
@@ -302,7 +276,7 @@ class Scanner(Plugin):
|
|||||||
if file_too_new:
|
if file_too_new:
|
||||||
break
|
break
|
||||||
|
|
||||||
if file_too_new:
|
if check_file_date and file_too_new:
|
||||||
try:
|
try:
|
||||||
time_string = time.ctime(file_time[0])
|
time_string = time.ctime(file_time[0])
|
||||||
except:
|
except:
|
||||||
@@ -320,17 +294,33 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
# Only process movies newer than x
|
# Only process movies newer than x
|
||||||
if newer_than and newer_than > 0:
|
if newer_than and newer_than > 0:
|
||||||
|
has_new_files = False
|
||||||
for cur_file in group['unsorted_files']:
|
for cur_file in group['unsorted_files']:
|
||||||
file_time = [os.path.getmtime(cur_file), os.path.getctime(cur_file)]
|
file_time = [os.path.getmtime(cur_file), os.path.getctime(cur_file)]
|
||||||
if file_time[0] > time.time() or file_time[1] > time.time():
|
if file_time[0] > newer_than or file_time[1] > newer_than:
|
||||||
|
has_new_files = True
|
||||||
break
|
break
|
||||||
|
|
||||||
log.debug('None of the files have changed since %s for %s, skipping.', (time.ctime(newer_than), identifier))
|
if not has_new_files:
|
||||||
|
log.debug('None of the files have changed since %s for %s, skipping.', (time.ctime(newer_than), identifier))
|
||||||
|
|
||||||
# Delete the unsorted list
|
# Delete the unsorted list
|
||||||
del group['unsorted_files']
|
del group['unsorted_files']
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
valid_files[identifier] = group
|
||||||
|
|
||||||
|
del movie_files
|
||||||
|
|
||||||
|
# Determine file types
|
||||||
|
processed_movies = {}
|
||||||
|
total_found = len(valid_files)
|
||||||
|
while True and not self.shuttingDown():
|
||||||
|
try:
|
||||||
|
identifier, group = valid_files.popitem()
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
|
||||||
# Group extra (and easy) files first
|
# Group extra (and easy) files first
|
||||||
# images = self.getImages(group['unsorted_files'])
|
# images = self.getImages(group['unsorted_files'])
|
||||||
@@ -351,11 +341,11 @@ class Scanner(Plugin):
|
|||||||
group['files']['movie'] = self.getMediaFiles(group['unsorted_files'])
|
group['files']['movie'] = self.getMediaFiles(group['unsorted_files'])
|
||||||
|
|
||||||
if len(group['files']['movie']) == 0:
|
if len(group['files']['movie']) == 0:
|
||||||
log.error('Couldn\t find any movie files for %s', identifier)
|
log.error('Couldn\'t find any movie files for %s', identifier)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
log.debug('Getting metadata for %s', identifier)
|
log.debug('Getting metadata for %s', identifier)
|
||||||
group['meta_data'] = self.getMetaData(group)
|
group['meta_data'] = self.getMetaData(group, folder = folder)
|
||||||
|
|
||||||
# Subtitle meta
|
# Subtitle meta
|
||||||
group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {}
|
group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {}
|
||||||
@@ -392,9 +382,12 @@ class Scanner(Plugin):
|
|||||||
movie = db.query(Movie).filter_by(library_id = group['library']['id']).first()
|
movie = db.query(Movie).filter_by(library_id = group['library']['id']).first()
|
||||||
group['movie_id'] = None if not movie else movie.id
|
group['movie_id'] = None if not movie else movie.id
|
||||||
|
|
||||||
|
|
||||||
processed_movies[identifier] = group
|
processed_movies[identifier] = group
|
||||||
|
|
||||||
|
# Notify parent & progress on something found
|
||||||
|
if on_found:
|
||||||
|
on_found(group, total_found, total_found - len(processed_movies))
|
||||||
|
|
||||||
if len(processed_movies) > 0:
|
if len(processed_movies) > 0:
|
||||||
log.info('Found %s movies in the folder %s', (len(processed_movies), folder))
|
log.info('Found %s movies in the folder %s', (len(processed_movies), folder))
|
||||||
else:
|
else:
|
||||||
@@ -402,7 +395,7 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
return processed_movies
|
return processed_movies
|
||||||
|
|
||||||
def getMetaData(self, group):
|
def getMetaData(self, group, folder = ''):
|
||||||
|
|
||||||
data = {}
|
data = {}
|
||||||
files = list(group['files']['movie'])
|
files = list(group['files']['movie'])
|
||||||
@@ -428,10 +421,10 @@ class Scanner(Plugin):
|
|||||||
if not data['quality']:
|
if not data['quality']:
|
||||||
data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
|
data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
|
||||||
|
|
||||||
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 else 'SD'
|
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD'
|
||||||
|
|
||||||
filename = re.sub('(.cp\(tt[0-9{7}]+\))', '', files[0])
|
filename = re.sub('(.cp\(tt[0-9{7}]+\))', '', files[0])
|
||||||
data['group'] = self.getGroup(filename)
|
data['group'] = self.getGroup(filename[len(folder):])
|
||||||
data['source'] = self.getSourceMedia(filename)
|
data['source'] = self.getSourceMedia(filename)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
@@ -440,9 +433,18 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
p = enzyme.parse(filename)
|
p = enzyme.parse(filename)
|
||||||
|
|
||||||
|
# Video codec
|
||||||
|
vc = ('h264' if p.video[0].codec == 'AVC1' else p.video[0].codec).lower()
|
||||||
|
|
||||||
|
# Audio codec
|
||||||
|
ac = p.audio[0].codec
|
||||||
|
try: ac = self.audio_codec_map.get(p.audio[0].codec)
|
||||||
|
except: pass
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'video': p.video[0].codec,
|
'video': vc,
|
||||||
'audio': p.audio[0].codec,
|
'audio': ac,
|
||||||
'resolution_width': tryInt(p.video[0].width),
|
'resolution_width': tryInt(p.video[0].width),
|
||||||
'resolution_height': tryInt(p.video[0].height),
|
'resolution_height': tryInt(p.video[0].height),
|
||||||
}
|
}
|
||||||
@@ -539,7 +541,6 @@ class Scanner(Plugin):
|
|||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
#db.close()
|
|
||||||
|
|
||||||
# Search based on OpenSubtitleHash
|
# Search based on OpenSubtitleHash
|
||||||
if not imdb_id and not group['is_dvd']:
|
if not imdb_id and not group['is_dvd']:
|
||||||
@@ -760,8 +761,8 @@ class Scanner(Plugin):
|
|||||||
|
|
||||||
def getGroup(self, file):
|
def getGroup(self, file):
|
||||||
try:
|
try:
|
||||||
match = re.search('-(?P<group>[A-Z0-9]+).', file, re.I)
|
match = re.findall('\-([A-Z0-9]+)[\.\/]', file, re.I)
|
||||||
return match.group('group') or ''
|
return match[-1] or ''
|
||||||
except:
|
except:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@@ -774,7 +775,7 @@ class Scanner(Plugin):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def findYear(self, text):
|
def findYear(self, text):
|
||||||
matches = re.search('(?P<year>[12]{1}[0-9]{3})', text)
|
matches = re.search('(?P<year>19[0-9]{2}|20[0-9]{2})', text)
|
||||||
if matches:
|
if matches:
|
||||||
return matches.group('year')
|
return matches.group('year')
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ class Score(Plugin):
|
|||||||
score += sizeScore(nzb['size'])
|
score += sizeScore(nzb['size'])
|
||||||
|
|
||||||
# Torrents only
|
# Torrents only
|
||||||
if nzb.get('seeds'):
|
if nzb.get('seeders'):
|
||||||
try:
|
try:
|
||||||
score += nzb.get('seeds') / 5
|
score += nzb.get('seeders') / 5
|
||||||
score += nzb.get('leechers') / 10
|
score += nzb.get('leechers') / 10
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ name_scores = [
|
|||||||
# Video
|
# Video
|
||||||
'x264:1', 'h264:1',
|
'x264:1', 'h264:1',
|
||||||
# Audio
|
# Audio
|
||||||
'DTS:4', 'AC3:2',
|
'dts:4', 'ac3:2',
|
||||||
# Quality
|
# Quality
|
||||||
'720p:10', '1080p:10', 'bluray:10', 'dvd:1', 'dvdrip:1', 'brrip:1', 'bdrip:1', 'bd50:1', 'bd25:1',
|
'720p:10', '1080p:10', 'bluray:10', 'dvd:1', 'dvdrip:1', 'brrip:1', 'bdrip:1', 'bd50:1', 'bd25:1',
|
||||||
# Language / Subs
|
# Language / Subs
|
||||||
@@ -116,7 +116,7 @@ def sizeScore(size):
|
|||||||
|
|
||||||
|
|
||||||
def providerScore(provider):
|
def providerScore(provider):
|
||||||
if provider in ['NZBMatrix', 'Nzbs', 'Newzbin']:
|
if provider in ['OMGWTFNZBs', 'PassThePopcorn', 'SceneAccess', 'TorrentLeech']:
|
||||||
return 20
|
return 20
|
||||||
|
|
||||||
if provider in ['Newznab']:
|
if provider in ['Newznab']:
|
||||||
|
|||||||
@@ -24,18 +24,19 @@ config = [{
|
|||||||
'name': 'required_words',
|
'name': 'required_words',
|
||||||
'label': 'Required words',
|
'label': 'Required words',
|
||||||
'default': '',
|
'default': '',
|
||||||
'description': 'Ignore releases that don\'t contain at least one of these words.'
|
'placeholder': 'Example: DTS, AC3 & English',
|
||||||
|
'description': 'Ignore releases that don\'t contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'ignored_words',
|
'name': 'ignored_words',
|
||||||
'label': 'Ignored words',
|
'label': 'Ignored words',
|
||||||
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub',
|
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'preferred_method',
|
'name': 'preferred_method',
|
||||||
'label': 'First search',
|
'label': 'First search',
|
||||||
'description': 'Which of the methods do you prefer',
|
'description': 'Which of the methods do you prefer',
|
||||||
'default': 'nzb',
|
'default': 'both',
|
||||||
'type': 'dropdown',
|
'type': 'dropdown',
|
||||||
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')],
|
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from couchpotato import get_session
|
from couchpotato import get_session
|
||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||||
from couchpotato.core.helpers.request import jsonified, getParam
|
from couchpotato.core.helpers.request import jsonified, getParam
|
||||||
from couchpotato.core.helpers.variable import md5, getTitle
|
from couchpotato.core.helpers.variable import md5, getTitle, splitString, \
|
||||||
|
possibleTitles
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
||||||
@@ -11,6 +12,7 @@ from couchpotato.environment import Env
|
|||||||
from inspect import ismethod, isfunction
|
from inspect import ismethod, isfunction
|
||||||
from sqlalchemy.exc import InterfaceError
|
from sqlalchemy.exc import InterfaceError
|
||||||
import datetime
|
import datetime
|
||||||
|
import random
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
@@ -36,9 +38,38 @@ class Searcher(Plugin):
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
addApiView('searcher.full_search', self.allMoviesView, docs = {
|
||||||
|
'desc': 'Starts a full search for all wanted movies',
|
||||||
|
})
|
||||||
|
|
||||||
|
addApiView('searcher.progress', self.getProgress, docs = {
|
||||||
|
'desc': 'Get the progress of current full search',
|
||||||
|
'return': {'type': 'object', 'example': """{
|
||||||
|
'progress': False || object, total & to_go,
|
||||||
|
}"""},
|
||||||
|
})
|
||||||
|
|
||||||
# Schedule cronjob
|
# Schedule cronjob
|
||||||
fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
|
fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
|
||||||
|
|
||||||
|
def allMoviesView(self):
|
||||||
|
|
||||||
|
in_progress = self.in_progress
|
||||||
|
if not in_progress:
|
||||||
|
fireEventAsync('searcher.all')
|
||||||
|
fireEvent('notify.frontend', type = 'searcher.started', data = True, message = 'Full search started')
|
||||||
|
else:
|
||||||
|
fireEvent('notify.frontend', type = 'searcher.already_started', data = True, message = 'Full search already in progress')
|
||||||
|
|
||||||
|
return jsonified({
|
||||||
|
'success': not in_progress
|
||||||
|
})
|
||||||
|
|
||||||
|
def getProgress(self):
|
||||||
|
|
||||||
|
return jsonified({
|
||||||
|
'progress': self.in_progress
|
||||||
|
})
|
||||||
|
|
||||||
def allMovies(self):
|
def allMovies(self):
|
||||||
|
|
||||||
@@ -53,30 +84,51 @@ class Searcher(Plugin):
|
|||||||
movies = db.query(Movie).filter(
|
movies = db.query(Movie).filter(
|
||||||
Movie.status.has(identifier = 'active')
|
Movie.status.has(identifier = 'active')
|
||||||
).all()
|
).all()
|
||||||
|
random.shuffle(movies)
|
||||||
|
|
||||||
for movie in movies:
|
self.in_progress = {
|
||||||
movie_dict = movie.to_dict({
|
'total': len(movies),
|
||||||
'profile': {'types': {'quality': {}}},
|
'to_go': len(movies),
|
||||||
'releases': {'status': {}, 'quality': {}},
|
}
|
||||||
'library': {'titles': {}, 'files':{}},
|
|
||||||
'files': {}
|
|
||||||
})
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.single(movie_dict)
|
search_types = self.getSearchTypes()
|
||||||
except IndexError:
|
|
||||||
fireEvent('library.update', movie_dict['library']['identifier'], force = True)
|
|
||||||
except:
|
|
||||||
log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
|
|
||||||
|
|
||||||
# Break if CP wants to shut down
|
for movie in movies:
|
||||||
if self.shuttingDown():
|
movie_dict = movie.to_dict({
|
||||||
break
|
'profile': {'types': {'quality': {}}},
|
||||||
|
'releases': {'status': {}, 'quality': {}},
|
||||||
|
'library': {'titles': {}, 'files':{}},
|
||||||
|
'files': {}
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.single(movie_dict, search_types)
|
||||||
|
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_dict['library']['identifier'], force = True)
|
||||||
|
except:
|
||||||
|
log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
|
||||||
|
|
||||||
|
self.in_progress['to_go'] -= 1
|
||||||
|
|
||||||
|
# Break if CP wants to shut down
|
||||||
|
if self.shuttingDown():
|
||||||
|
break
|
||||||
|
|
||||||
|
except SearchSetupError:
|
||||||
|
pass
|
||||||
|
|
||||||
#db.close()
|
|
||||||
self.in_progress = False
|
self.in_progress = False
|
||||||
|
|
||||||
def single(self, movie):
|
def single(self, movie, search_types = None):
|
||||||
|
|
||||||
|
# Find out search type
|
||||||
|
try:
|
||||||
|
if not search_types:
|
||||||
|
search_types = self.getSearchTypes()
|
||||||
|
except SearchSetupError:
|
||||||
|
return
|
||||||
|
|
||||||
done_status = fireEvent('status.get', 'done', single = True)
|
done_status = fireEvent('status.get', 'done', single = True)
|
||||||
|
|
||||||
@@ -91,6 +143,8 @@ class Searcher(Plugin):
|
|||||||
available_status = fireEvent('status.get', 'available', single = True)
|
available_status = fireEvent('status.get', 'available', single = True)
|
||||||
ignored_status = fireEvent('status.get', 'ignored', single = True)
|
ignored_status = fireEvent('status.get', 'ignored', single = True)
|
||||||
|
|
||||||
|
found_releases = []
|
||||||
|
|
||||||
default_title = getTitle(movie['library'])
|
default_title = getTitle(movie['library'])
|
||||||
if not default_title:
|
if not default_title:
|
||||||
log.error('No proper info found for movie, removing it from library to cause it from having more issues.')
|
log.error('No proper info found for movie, removing it from library to cause it from having more issues.')
|
||||||
@@ -99,6 +153,7 @@ class Searcher(Plugin):
|
|||||||
|
|
||||||
fireEvent('notify.frontend', type = 'searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title)
|
fireEvent('notify.frontend', type = 'searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title)
|
||||||
|
|
||||||
|
|
||||||
ret = False
|
ret = False
|
||||||
for quality_type in movie['profile']['types']:
|
for quality_type in movie['profile']['types']:
|
||||||
if not self.couldBeReleased(quality_type['quality']['identifier'], release_dates, pre_releases):
|
if not self.couldBeReleased(quality_type['quality']['identifier'], release_dates, pre_releases):
|
||||||
@@ -118,7 +173,11 @@ class Searcher(Plugin):
|
|||||||
log.info('Search for %s in %s', (default_title, quality_type['quality']['label']))
|
log.info('Search for %s in %s', (default_title, quality_type['quality']['label']))
|
||||||
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
|
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
|
||||||
|
|
||||||
results = fireEvent('yarr.search', movie, quality, merge = True)
|
results = []
|
||||||
|
for search_type in search_types:
|
||||||
|
type_results = fireEvent('%s.search' % search_type, movie, quality, merge = True)
|
||||||
|
if type_results:
|
||||||
|
results += type_results
|
||||||
|
|
||||||
sorted_results = sorted(results, key = lambda k: k['score'], reverse = True)
|
sorted_results = sorted(results, key = lambda k: k['score'], reverse = True)
|
||||||
if len(sorted_results) == 0:
|
if len(sorted_results) == 0:
|
||||||
@@ -135,19 +194,22 @@ class Searcher(Plugin):
|
|||||||
# Add them to this movie releases list
|
# Add them to this movie releases list
|
||||||
for nzb in sorted_results:
|
for nzb in sorted_results:
|
||||||
|
|
||||||
rls = db.query(Release).filter_by(identifier = md5(nzb['url'])).first()
|
nzb_identifier = md5(nzb['url'])
|
||||||
|
found_releases.append(nzb_identifier)
|
||||||
|
|
||||||
|
rls = db.query(Release).filter_by(identifier = nzb_identifier).first()
|
||||||
if not rls:
|
if not rls:
|
||||||
rls = Release(
|
rls = Release(
|
||||||
identifier = md5(nzb['url']),
|
identifier = nzb_identifier,
|
||||||
movie_id = movie.get('id'),
|
movie_id = movie.get('id'),
|
||||||
quality_id = quality_type.get('quality_id'),
|
quality_id = quality_type.get('quality_id'),
|
||||||
status_id = available_status.get('id')
|
status_id = available_status.get('id')
|
||||||
)
|
)
|
||||||
db.add(rls)
|
db.add(rls)
|
||||||
db.commit()
|
|
||||||
else:
|
else:
|
||||||
[db.delete(info) for info in rls.info]
|
[db.delete(old_info) for old_info in rls.info]
|
||||||
db.commit()
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
for info in nzb:
|
for info in nzb:
|
||||||
try:
|
try:
|
||||||
@@ -159,14 +221,19 @@ class Searcher(Plugin):
|
|||||||
value = toUnicode(nzb[info])
|
value = toUnicode(nzb[info])
|
||||||
)
|
)
|
||||||
rls.info.append(rls_info)
|
rls.info.append(rls_info)
|
||||||
db.commit()
|
|
||||||
except InterfaceError:
|
except InterfaceError:
|
||||||
log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
|
log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
|
||||||
nzb['status_id'] = rls.status_id
|
nzb['status_id'] = rls.status_id
|
||||||
|
|
||||||
|
|
||||||
for nzb in sorted_results:
|
for nzb in sorted_results:
|
||||||
|
if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and nzb.get('age') <= quality_type.get('wait_for', 0):
|
||||||
|
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
|
||||||
|
continue
|
||||||
|
|
||||||
if nzb['status_id'] == ignored_status.get('id'):
|
if nzb['status_id'] == ignored_status.get('id'):
|
||||||
log.info('Ignored: %s', nzb['name'])
|
log.info('Ignored: %s', nzb['name'])
|
||||||
continue
|
continue
|
||||||
@@ -181,6 +248,12 @@ class Searcher(Plugin):
|
|||||||
break
|
break
|
||||||
elif downloaded != 'try_next':
|
elif downloaded != 'try_next':
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Remove releases that aren't found anymore
|
||||||
|
for release in movie.get('releases', []):
|
||||||
|
if release.get('status_id') == available_status.get('id') and release.get('identifier') not in found_releases:
|
||||||
|
fireEvent('release.delete', release.get('id'), single = True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title))
|
log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title))
|
||||||
fireEvent('movie.restatus', movie['id'])
|
fireEvent('movie.restatus', movie['id'])
|
||||||
@@ -192,111 +265,144 @@ class Searcher(Plugin):
|
|||||||
|
|
||||||
fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
|
fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
|
||||||
|
|
||||||
#db.close()
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def download(self, data, movie, manual = False):
|
def download(self, data, movie, manual = False):
|
||||||
|
|
||||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
# Test to see if any downloaders are enabled for this type
|
||||||
|
downloader_enabled = fireEvent('download.enabled', manual, data, single = True)
|
||||||
|
|
||||||
# Download movie to temp
|
if downloader_enabled:
|
||||||
filedata = None
|
|
||||||
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
|
|
||||||
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
|
|
||||||
if filedata is 'try_next':
|
|
||||||
return filedata
|
|
||||||
|
|
||||||
successful = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
|
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
||||||
|
|
||||||
if successful:
|
# Download movie to temp
|
||||||
|
filedata = None
|
||||||
|
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
|
||||||
|
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
|
||||||
|
if filedata == 'try_next':
|
||||||
|
return filedata
|
||||||
|
|
||||||
# Mark release as snatched
|
successful = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
|
||||||
db = get_session()
|
|
||||||
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
|
|
||||||
rls.status_id = snatched_status.get('id')
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
|
if successful:
|
||||||
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
|
|
||||||
log.info(snatch_message)
|
|
||||||
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
|
|
||||||
|
|
||||||
|
|
||||||
# If renamer isn't used, mark movie done
|
|
||||||
if not Env.setting('enabled', 'renamer'):
|
|
||||||
active_status = fireEvent('status.get', 'active', single = True)
|
|
||||||
done_status = fireEvent('status.get', 'done', single = True)
|
|
||||||
try:
|
try:
|
||||||
if movie['status_id'] == active_status.get('id'):
|
# Mark release as snatched
|
||||||
for profile_type in movie['profile']['types']:
|
db = get_session()
|
||||||
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
|
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
|
||||||
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
|
if rls:
|
||||||
|
rls.status_id = snatched_status.get('id')
|
||||||
|
db.commit()
|
||||||
|
|
||||||
# Mark release done
|
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
|
||||||
rls.status_id = done_status.get('id')
|
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
|
||||||
db.commit()
|
log.info(snatch_message)
|
||||||
|
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
|
||||||
|
|
||||||
# Mark movie done
|
# If renamer isn't used, mark movie done
|
||||||
mvie = db.query(Movie).filter_by(id = movie['id']).first()
|
if not Env.setting('enabled', 'renamer'):
|
||||||
mvie.status_id = done_status.get('id')
|
active_status = fireEvent('status.get', 'active', single = True)
|
||||||
db.commit()
|
done_status = fireEvent('status.get', 'done', single = True)
|
||||||
except Exception, e:
|
try:
|
||||||
log.error('Failed marking movie finished: %s %s', (e, traceback.format_exc()))
|
if movie['status_id'] == active_status.get('id'):
|
||||||
|
for profile_type in movie['profile']['types']:
|
||||||
|
if rls and profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
|
||||||
|
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
|
||||||
|
|
||||||
#db.close()
|
# Mark release done
|
||||||
return True
|
rls.status_id = done_status.get('id')
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# Mark movie done
|
||||||
|
mvie = db.query(Movie).filter_by(id = movie['id']).first()
|
||||||
|
mvie.status_id = done_status.get('id')
|
||||||
|
db.commit()
|
||||||
|
except:
|
||||||
|
log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed marking movie finished: %s', traceback.format_exc())
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
log.info('Tried to download, but none of the "%s" downloaders are enabled', (data.get('type', '')))
|
||||||
|
|
||||||
log.info('Tried to download, but none of the downloaders are enabled')
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def getSearchTypes(self):
|
||||||
|
|
||||||
|
download_types = fireEvent('download.enabled_types', merge = True)
|
||||||
|
provider_types = fireEvent('provider.enabled_types', merge = True)
|
||||||
|
|
||||||
|
if download_types and len(list(set(provider_types) & set(download_types))) == 0:
|
||||||
|
log.error('There aren\'t any providers enabled for your downloader (%s). Check your settings.', ','.join(download_types))
|
||||||
|
raise NoProviders
|
||||||
|
|
||||||
|
for useless_provider in list(set(provider_types) - set(download_types)):
|
||||||
|
log.debug('Provider for "%s" enabled, but no downloader.', useless_provider)
|
||||||
|
|
||||||
|
search_types = download_types
|
||||||
|
|
||||||
|
if len(search_types) == 0:
|
||||||
|
log.error('There aren\'t any downloaders enabled. Please pick one in settings.')
|
||||||
|
raise NoDownloaders
|
||||||
|
|
||||||
|
return search_types
|
||||||
|
|
||||||
def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs):
|
def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs):
|
||||||
|
|
||||||
imdb_results = kwargs.get('imdb_results', False)
|
imdb_results = kwargs.get('imdb_results', False)
|
||||||
retention = Env.setting('retention', section = 'nzb')
|
retention = Env.setting('retention', section = 'nzb')
|
||||||
|
|
||||||
if nzb.get('seeds') is None and retention < nzb.get('age', 0):
|
if nzb.get('seeders') is None and 0 < retention < nzb.get('age', 0):
|
||||||
log.info('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
|
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
movie_name = getTitle(movie['library'])
|
movie_name = getTitle(movie['library'])
|
||||||
movie_words = re.split('\W+', simplifyString(movie_name))
|
movie_words = re.split('\W+', simplifyString(movie_name))
|
||||||
nzb_name = simplifyString(nzb['name'])
|
nzb_name = simplifyString(nzb['name'])
|
||||||
nzb_words = re.split('\W+', nzb_name)
|
nzb_words = re.split('\W+', nzb_name)
|
||||||
required_words = [x.strip().lower() for x in self.conf('required_words').lower().split(',')]
|
required_words = splitString(self.conf('required_words').lower())
|
||||||
|
|
||||||
if self.conf('required_words') and not list(set(nzb_words) & set(required_words)):
|
req_match = 0
|
||||||
log.info("Wrong: Required word missing: %s" % nzb['name'])
|
for req_set in required_words:
|
||||||
|
req = splitString(req_set, '&')
|
||||||
|
req_match += len(list(set(nzb_words) & set(req))) == len(req)
|
||||||
|
|
||||||
|
if self.conf('required_words') and req_match == 0:
|
||||||
|
log.info2("Wrong: Required word missing: %s" % nzb['name'])
|
||||||
return False
|
return False
|
||||||
|
|
||||||
ignored_words = [x.strip().lower() for x in self.conf('ignored_words').split(',')]
|
ignored_words = splitString(self.conf('ignored_words').lower())
|
||||||
blacklisted = list(set(nzb_words) & set(ignored_words))
|
blacklisted = list(set(nzb_words) & set(ignored_words) - set(movie_words))
|
||||||
if self.conf('ignored_words') and blacklisted:
|
if self.conf('ignored_words') and blacklisted:
|
||||||
log.info("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted)))
|
log.info2("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted)))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic']
|
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic']
|
||||||
for p_tag in pron_tags:
|
pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words))
|
||||||
if p_tag in nzb_words and p_tag not in movie_words:
|
if pron_words:
|
||||||
log.info('Wrong: %s, probably pr0n', (nzb['name']))
|
log.info('Wrong: %s, probably pr0n', (nzb['name']))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
#qualities = fireEvent('quality.all', single = True)
|
#qualities = fireEvent('quality.all', single = True)
|
||||||
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
|
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
|
||||||
|
|
||||||
# Contains lower quality string
|
# Contains lower quality string
|
||||||
if self.containsOtherQuality(nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality):
|
if self.containsOtherQuality(nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality):
|
||||||
log.info('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
|
log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# File to small
|
# File to small
|
||||||
if nzb['size'] and preferred_quality['size_min'] > nzb['size']:
|
if nzb['size'] and preferred_quality['size_min'] > nzb['size']:
|
||||||
log.info('"%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min']))
|
log.info2('Wrong: "%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min']))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# File to large
|
# File to large
|
||||||
if nzb['size'] and preferred_quality.get('size_max') < nzb['size']:
|
if nzb['size'] and preferred_quality.get('size_max') < nzb['size']:
|
||||||
log.info('"%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max']))
|
log.info2('Wrong: "%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max']))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -314,20 +420,21 @@ class Searcher(Plugin):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
# Check if nzb contains imdb link
|
# Check if nzb contains imdb link
|
||||||
if self.checkIMDB([nzb['description']], movie['library']['identifier']):
|
if self.checkIMDB([nzb.get('description', '')], movie['library']['identifier']):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
for movie_title in movie['library']['titles']:
|
for raw_title in movie['library']['titles']:
|
||||||
movie_words = re.split('\W+', simplifyString(movie_title['title']))
|
for movie_title in possibleTitles(raw_title['title']):
|
||||||
|
movie_words = re.split('\W+', simplifyString(movie_title))
|
||||||
|
|
||||||
if self.correctName(nzb['name'], movie_title['title']):
|
if self.correctName(nzb['name'], movie_title):
|
||||||
# if no IMDB link, at least check year range 1
|
# if no IMDB link, at least check year range 1
|
||||||
if len(movie_words) > 2 and self.correctYear([nzb['name']], movie['library']['year'], 1):
|
if len(movie_words) > 2 and self.correctYear([nzb['name']], movie['library']['year'], 1):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# if no IMDB link, at least check year
|
# if no IMDB link, at least check year
|
||||||
if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
|
if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie_name, movie['library']['year']))
|
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie_name, movie['library']['year']))
|
||||||
return False
|
return False
|
||||||
@@ -350,13 +457,20 @@ class Searcher(Plugin):
|
|||||||
if list(set(nzb_words) & set(quality['alternative'])):
|
if list(set(nzb_words) & set(quality['alternative'])):
|
||||||
found[quality['identifier']] = True
|
found[quality['identifier']] = True
|
||||||
|
|
||||||
|
# Try guessing via quality tags
|
||||||
|
guess = fireEvent('quality.guess', [nzb.get('name')], single = True)
|
||||||
|
if guess:
|
||||||
|
found[guess['identifier']] = True
|
||||||
|
|
||||||
# Hack for older movies that don't contain quality tag
|
# Hack for older movies that don't contain quality tag
|
||||||
year_name = fireEvent('scanner.name_year', name, single = True)
|
year_name = fireEvent('scanner.name_year', name, single = True)
|
||||||
if movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None):
|
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
|
||||||
return 'dvdr' == preferred_quality['identifier']
|
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
|
||||||
return 'dvdrip' == preferred_quality['identifier']
|
log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', (size))
|
||||||
|
found['dvdrip'] = True
|
||||||
|
|
||||||
# Allow other qualities
|
# Allow other qualities
|
||||||
for allowed in preferred_quality.get('allow'):
|
for allowed in preferred_quality.get('allow'):
|
||||||
@@ -373,23 +487,32 @@ class Searcher(Plugin):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def correctYear(self, haystack, year, range):
|
def correctYear(self, haystack, year, year_range):
|
||||||
|
|
||||||
for string in haystack:
|
for string in haystack:
|
||||||
if str(year) in string or str(int(year) + range) in string or str(int(year) - range) in string: # 1 year of is fine too
|
|
||||||
|
year_name = fireEvent('scanner.name_year', string, single = True)
|
||||||
|
|
||||||
|
if year_name and ((year - year_range) <= year_name.get('year') <= (year + year_range)):
|
||||||
|
log.debug('Movie year matches range: %s looking for %s', (year_name.get('year'), year))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
log.debug('Movie year doesn\'t matche range: %s looking for %s', (year_name.get('year'), year))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def correctName(self, check_name, movie_name):
|
def correctName(self, check_name, movie_name):
|
||||||
|
|
||||||
check_names = [check_name]
|
check_names = [check_name]
|
||||||
try:
|
|
||||||
check_names.append(re.search(r'([\'"])[^\1]*\1', check_name).group(0))
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
for check_name in check_names:
|
# Match names between "
|
||||||
|
try: check_names.append(re.search(r'([\'"])[^\1]*\1', check_name).group(0))
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
# Match longest name between []
|
||||||
|
try: check_names.append(max(check_name.split('['), key = len))
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
for check_name in list(set(check_names)):
|
||||||
check_movie = fireEvent('scanner.name_year', check_name, single = True)
|
check_movie = fireEvent('scanner.name_year', check_name, single = True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -410,6 +533,11 @@ class Searcher(Plugin):
|
|||||||
if not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0):
|
if not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
|
||||||
|
# For movies before 1972
|
||||||
|
if dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
|
||||||
|
return True
|
||||||
|
|
||||||
if wanted_quality in pre_releases:
|
if wanted_quality in pre_releases:
|
||||||
# Prerelease 1 week before theaters
|
# Prerelease 1 week before theaters
|
||||||
if dates.get('theater') - 604800 < now:
|
if dates.get('theater') - 604800 < now:
|
||||||
@@ -465,3 +593,12 @@ class Searcher(Plugin):
|
|||||||
except:
|
except:
|
||||||
log.error('Failed searching for next release: %s', traceback.format_exc())
|
log.error('Failed searching for next release: %s', traceback.format_exc())
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
class SearchSetupError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NoDownloaders(SearchSetupError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NoProviders(SearchSetupError):
|
||||||
|
pass
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class StatusPlugin(Plugin):
|
|||||||
'failed': 'Failed',
|
'failed': 'Failed',
|
||||||
'deleted': 'Deleted',
|
'deleted': 'Deleted',
|
||||||
'ignored': 'Ignored',
|
'ignored': 'Ignored',
|
||||||
|
'available': 'Available',
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'renamer',
|
'tab': 'renamer',
|
||||||
'subtab': 'subtitles',
|
|
||||||
'name': 'subtitle',
|
'name': 'subtitle',
|
||||||
'label': 'Download subtitles after rename',
|
'label': 'Download subtitles',
|
||||||
|
'description': 'after rename',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'enabled',
|
'name': 'enabled',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from couchpotato import get_session
|
from couchpotato import get_session
|
||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
|
from couchpotato.core.helpers.variable import splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Library, FileType
|
from couchpotato.core.settings.model import Library, FileType
|
||||||
@@ -13,7 +14,7 @@ log = CPLog(__name__)
|
|||||||
|
|
||||||
class Subtitle(Plugin):
|
class Subtitle(Plugin):
|
||||||
|
|
||||||
services = ['opensubtitles', 'thesubdb', 'subswiki']
|
services = ['opensubtitles', 'thesubdb', 'subswiki', 'podnapisi']
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
addEvent('renamer.before', self.searchSingle)
|
addEvent('renamer.before', self.searchSingle)
|
||||||
@@ -40,8 +41,6 @@ class Subtitle(Plugin):
|
|||||||
# get subtitles for those files
|
# get subtitles for those files
|
||||||
subliminal.list_subtitles(files, cache_dir = Env.get('cache_dir'), multi = True, languages = self.getLanguages(), services = self.services)
|
subliminal.list_subtitles(files, cache_dir = Env.get('cache_dir'), multi = True, languages = self.getLanguages(), services = self.services)
|
||||||
|
|
||||||
#db.close()
|
|
||||||
|
|
||||||
def searchSingle(self, group):
|
def searchSingle(self, group):
|
||||||
|
|
||||||
if self.isDisabled(): return
|
if self.isDisabled(): return
|
||||||
@@ -50,6 +49,7 @@ class Subtitle(Plugin):
|
|||||||
available_languages = sum(group['subtitle_language'].itervalues(), [])
|
available_languages = sum(group['subtitle_language'].itervalues(), [])
|
||||||
downloaded = []
|
downloaded = []
|
||||||
files = [toUnicode(x) for x in group['files']['movie']]
|
files = [toUnicode(x) for x in group['files']['movie']]
|
||||||
|
log.debug('Searching for subtitles for: %s', files)
|
||||||
|
|
||||||
for lang in self.getLanguages():
|
for lang in self.getLanguages():
|
||||||
if lang not in available_languages:
|
if lang not in available_languages:
|
||||||
@@ -58,6 +58,7 @@ class Subtitle(Plugin):
|
|||||||
downloaded.extend(download[subtitle])
|
downloaded.extend(download[subtitle])
|
||||||
|
|
||||||
for d_sub in downloaded:
|
for d_sub in downloaded:
|
||||||
|
log.info('Found subtitle (%s): %s', (d_sub.language.alpha2, files))
|
||||||
group['files']['subtitle'].add(d_sub.path)
|
group['files']['subtitle'].add(d_sub.path)
|
||||||
group['subtitle_language'][d_sub.path] = [d_sub.language.alpha2]
|
group['subtitle_language'][d_sub.path] = [d_sub.language.alpha2]
|
||||||
|
|
||||||
@@ -69,4 +70,4 @@ class Subtitle(Plugin):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def getLanguages(self):
|
def getLanguages(self):
|
||||||
return [x.strip() for x in self.conf('languages').split(',')]
|
return splitString(self.conf('languages'))
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'renamer',
|
'tab': 'renamer',
|
||||||
'subtab': 'trailer',
|
|
||||||
'name': 'trailer',
|
'name': 'trailer',
|
||||||
'label': 'Download trailer after rename',
|
'label': 'Download trailer',
|
||||||
|
'description': 'after rename',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'enabled',
|
'name': 'enabled',
|
||||||
@@ -25,10 +25,11 @@ config = [{
|
|||||||
'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')],
|
'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'automatic',
|
'name': 'name',
|
||||||
'default': False,
|
'label': 'Naming',
|
||||||
'type': 'bool',
|
'default': '<filename>-trailer',
|
||||||
'description': 'Automaticly search & download for movies in library',
|
'advanced': True,
|
||||||
|
'description': 'Use <filename> to use above settings.'
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,24 +12,27 @@ class Trailer(Plugin):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
addEvent('renamer.after', self.searchSingle)
|
addEvent('renamer.after', self.searchSingle)
|
||||||
|
|
||||||
def searchSingle(self, group):
|
def searchSingle(self, message = None, group = {}):
|
||||||
|
|
||||||
if self.isDisabled() or len(group['files']['trailer']) > 0: return
|
if self.isDisabled() or len(group['files']['trailer']) > 0: return
|
||||||
|
|
||||||
trailers = fireEvent('trailer.search', group = group, merge = True)
|
trailers = fireEvent('trailer.search', group = group, merge = True)
|
||||||
if not trailers or trailers == []:
|
if not trailers or trailers == []:
|
||||||
log.info('No trailers found for: %s', getTitle(group['library']))
|
log.info('No trailers found for: %s', getTitle(group['library']))
|
||||||
return
|
return False
|
||||||
|
|
||||||
for trailer in trailers.get(self.conf('quality'), []):
|
for trailer in trailers.get(self.conf('quality'), []):
|
||||||
destination = '%s-trailer.%s' % (self.getRootName(group), getExt(trailer))
|
filename = self.conf('name').replace('<filename>', group['filename']) + ('.%s' % getExt(trailer))
|
||||||
|
destination = os.path.join(group['destination_dir'], filename)
|
||||||
if not os.path.isfile(destination):
|
if not os.path.isfile(destination):
|
||||||
fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True)
|
fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True)
|
||||||
else:
|
else:
|
||||||
log.debug('Trailer already exists: %s', destination)
|
log.debug('Trailer already exists: %s', destination)
|
||||||
|
|
||||||
|
group['renamed_files'].append(destination)
|
||||||
|
|
||||||
# Download first and break
|
# Download first and break
|
||||||
break
|
break
|
||||||
|
|
||||||
def getRootName(self, data = {}):
|
return True
|
||||||
return os.path.join(data['destination_dir'], data['filename'])
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class Userscript(Plugin):
|
|||||||
addApiView('userscript.get/<random>/<path:filename>', self.getUserScript, static = True)
|
addApiView('userscript.get/<random>/<path:filename>', self.getUserScript, static = True)
|
||||||
addApiView('userscript', self.iFrame)
|
addApiView('userscript', self.iFrame)
|
||||||
addApiView('userscript.add_via_url', self.getViaUrl)
|
addApiView('userscript.add_via_url', self.getViaUrl)
|
||||||
|
addApiView('userscript.includes', self.getIncludes)
|
||||||
addApiView('userscript.bookmark', self.bookmark)
|
addApiView('userscript.bookmark', self.bookmark)
|
||||||
|
|
||||||
addEvent('userscript.get_version', self.getVersion)
|
addEvent('userscript.get_version', self.getVersion)
|
||||||
@@ -35,6 +36,13 @@ class Userscript(Plugin):
|
|||||||
|
|
||||||
return self.renderTemplate(__file__, 'bookmark.js', **params)
|
return self.renderTemplate(__file__, 'bookmark.js', **params)
|
||||||
|
|
||||||
|
def getIncludes(self):
|
||||||
|
|
||||||
|
return jsonified({
|
||||||
|
'includes': fireEvent('userscript.get_includes', merge = True),
|
||||||
|
'excludes': fireEvent('userscript.get_excludes', merge = True),
|
||||||
|
})
|
||||||
|
|
||||||
def getUserScript(self, random = '', filename = ''):
|
def getUserScript(self, random = '', filename = ''):
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
|
.page.wizard .uniForm {
|
||||||
|
width: 80%;
|
||||||
|
margin: 0 auto 30px;
|
||||||
|
}
|
||||||
|
|
||||||
.page.wizard h1 {
|
.page.wizard h1 {
|
||||||
padding: 10px 30px;
|
padding: 10px 30px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 40px;
|
font-size: 30px;
|
||||||
margin-top: 80px;
|
margin-top: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,12 +19,13 @@
|
|||||||
|
|
||||||
.page.wizard .tab_wrapper {
|
.page.wizard .tab_wrapper {
|
||||||
background: #5c697b;
|
background: #5c697b;
|
||||||
padding: 18px 0;
|
padding: 10px 0;
|
||||||
font-size: 23px;
|
font-size: 18px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-width: 960px;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
box-shadow: 0 0 50px rgba(0,0,0,0.55);
|
box-shadow: 0 0 50px rgba(0,0,0,0.55);
|
||||||
@@ -36,7 +42,7 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
.page.wizard .tabs li a {
|
.page.wizard .tabs li a {
|
||||||
padding: 20px 30px;
|
padding: 20px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page.wizard .tab_wrapper .pointer {
|
.page.wizard .tab_wrapper .pointer {
|
||||||
@@ -45,7 +51,7 @@
|
|||||||
border-top: 10px solid #5c697b;
|
border-top: 10px solid #5c697b;
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 60px;
|
top: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page.wizard .tab_content {
|
.page.wizard .tab_content {
|
||||||
@@ -58,11 +64,25 @@
|
|||||||
.page.wizard .wgroup_finish {
|
.page.wizard .wgroup_finish {
|
||||||
height: 300px;
|
height: 300px;
|
||||||
}
|
}
|
||||||
|
.page.wizard .wgroup_finish h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.page.wizard .wgroup_finish .wizard_support,
|
||||||
|
.page.wizard .wgroup_finish .description {
|
||||||
|
font-size: 25px;
|
||||||
|
line-height: 120%;
|
||||||
|
margin: 20px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.page.wizard .button.green {
|
.page.wizard .button.green {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
font-size: 25px;
|
font-size: 25px;
|
||||||
margin: 10px 30px;
|
margin: 10px 30px 80px;
|
||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page.wizard .tab_nzb_providers {
|
||||||
|
margin: 20px 0 0 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,214 +1,263 @@
|
|||||||
Page.Wizard = new Class({
|
Page.Wizard = new Class({
|
||||||
|
|
||||||
Extends: Page.Settings,
|
Extends: Page.Settings,
|
||||||
|
|
||||||
name: 'wizard',
|
name: 'wizard',
|
||||||
has_tab: false,
|
has_tab: false,
|
||||||
wizard_only: true,
|
wizard_only: true,
|
||||||
|
|
||||||
headers: {
|
headers: {
|
||||||
'welcome': {
|
'welcome': {
|
||||||
'title': 'Welcome to the new CouchPotato',
|
'title': 'Welcome to the new CouchPotato',
|
||||||
'description': 'To get started, fill in each of the following settings as much as your can. <br />Maybe first start with importing your movies from the previous CouchPotato',
|
'description': 'To get started, fill in each of the following settings as much as you can. <br />Maybe first start with importing your movies from the previous CouchPotato',
|
||||||
'content': new Element('div', {
|
'content': new Element('div', {
|
||||||
'styles': {
|
'styles': {
|
||||||
'margin': '0 0 0 30px'
|
'margin': '0 0 0 30px'
|
||||||
}
|
}
|
||||||
}).adopt(
|
}).adopt(
|
||||||
new Element('div', {
|
new Element('div', {
|
||||||
'html': 'Select the <strong>data.db</strong>. It should be in your CouchPotato root directory.'
|
'html': 'Select the <strong>data.db</strong>. It should be in your CouchPotato root directory.'
|
||||||
}),
|
}),
|
||||||
self.import_iframe = new Element('iframe', {
|
self.import_iframe = new Element('iframe', {
|
||||||
'styles': {
|
'styles': {
|
||||||
'height': 40,
|
'height': 40,
|
||||||
'width': 300,
|
'width': 300,
|
||||||
'border': 0,
|
'border': 0,
|
||||||
'overflow': 'hidden'
|
'overflow': 'hidden'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
'event': function(){
|
'event': function(){
|
||||||
self.import_iframe.set('src', Api.createUrl('v1.import'))
|
self.import_iframe.set('src', Api.createUrl('v1.import'))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'general': {
|
'general': {
|
||||||
'title': 'General',
|
'title': 'General',
|
||||||
'description': 'If you want to access CP from outside your local network, you better secure it a bit with a username & password.'
|
'description': 'If you want to access CP from outside your local network, you better secure it a bit with a username & password.'
|
||||||
},
|
},
|
||||||
'downloaders': {
|
'downloaders': {
|
||||||
'title': 'What download apps are you using?',
|
'title': 'What download apps are you using?',
|
||||||
'description': 'If you don\'t have any of these listed, you have to use Blackhole. Or drop me a line, maybe I\'ll support your download app.'
|
'description': 'CP needs an external download app to work with. Choose one below. For more downloaders check settings after you have filled in the wizard. If your download app isn\'t in the list, use the default Blackhole.'
|
||||||
},
|
},
|
||||||
'providers': {
|
'providers': {
|
||||||
'title': 'Are you registered at any of these sites?',
|
'title': 'Are you registered at any of these sites?',
|
||||||
'description': 'CP uses these sites to search for movies. A few free are enabled by default, but it\'s always better to have a few more.'
|
'description': 'CP uses these sites to search for movies. A few free are enabled by default, but it\'s always better to have a few more. Check settings for the full list of available providers.'
|
||||||
},
|
},
|
||||||
'renamer': {
|
'renamer': {
|
||||||
'title': 'Move & rename the movies after downloading?',
|
'title': 'Move & rename the movies after downloading?',
|
||||||
'description': ''
|
'description': 'The coolest part of CP is that it can move and organize your downloaded movies automagically. Check settings and you can even download trailers, subtitles and other data when it has finished downloading. It\'s awesome!'
|
||||||
},
|
},
|
||||||
'finish': {
|
'automation': {
|
||||||
'title': 'Finish Up',
|
'title': 'Easily add movies to your wanted list!',
|
||||||
'description': 'Are you done? Did you fill in everything as much as possible? Yes, ok gogogo!',
|
'description': 'You can easily add movies from your favorite movie site, like IMDB, Rotten Tomatoes, Apple Trailers and more. Just install the userscript or drag the bookmarklet to your browsers bookmarks.' +
|
||||||
'content': new Element('div').adopt(
|
'<br />Once installed, just click the bookmarklet on a movie page and watch the magic happen ;)',
|
||||||
new Element('a.button.green', {
|
'content': function(){
|
||||||
'text': 'I\'m ready to start the awesomeness, wow this button is big and green!',
|
return App.createUserscriptButtons().setStyles({
|
||||||
'events': {
|
'background-image': "url('"+Api.createUrl('static/userscript/userscript.png')+"')"
|
||||||
'click': function(e){
|
})
|
||||||
(e).preventDefault();
|
}
|
||||||
Api.request('settings.save', {
|
},
|
||||||
'data': {
|
'finish': {
|
||||||
'section': 'core',
|
'title': 'Finishing Up',
|
||||||
'name': 'show_wizard',
|
'description': 'Are you done? Did you fill in everything as much as possible?' +
|
||||||
'value': 0
|
'<br />Be sure to check the settings to see what more CP can do!<br /><br />' +
|
||||||
},
|
'<div class="wizard_support">After you\'ve used CP for a while, and you like it (which of course you will), consider supporting CP. Maybe even by writing some code. <br />Or by getting a subscription at <a href="https://usenetserver.com/partners/?a_aid=couchpotato&a_bid=3f357c6f">Usenet Server</a> or <a href="http://www.newshosting.com/partners/?a_aid=couchpotato&a_bid=a0b022df">Newshosting</a>.</div>',
|
||||||
'useSpinner': true,
|
'content': new Element('div').adopt(
|
||||||
'spinnerOptions': {
|
new Element('a.button.green', {
|
||||||
'target': self.el
|
'styles': {
|
||||||
},
|
'margin-top': 20
|
||||||
'onComplete': function(){
|
},
|
||||||
window.location = App.createUrl();
|
'text': 'I\'m ready to start the awesomeness, wow this button is big and green!',
|
||||||
}
|
'events': {
|
||||||
});
|
'click': function(e){
|
||||||
}
|
(e).preventDefault();
|
||||||
}
|
Api.request('settings.save', {
|
||||||
})
|
'data': {
|
||||||
)
|
'section': 'core',
|
||||||
}
|
'name': 'show_wizard',
|
||||||
},
|
'value': 0
|
||||||
groups: ['welcome', 'general', 'downloaders', 'searcher', 'providers', 'renamer', 'finish'],
|
},
|
||||||
|
'useSpinner': true,
|
||||||
open: function(action, params){
|
'spinnerOptions': {
|
||||||
var self = this;
|
'target': self.el
|
||||||
|
},
|
||||||
if(!self.initialized){
|
'onComplete': function(){
|
||||||
App.fireEvent('unload');
|
window.location = App.createUrl();
|
||||||
App.getBlock('header').hide();
|
}
|
||||||
|
});
|
||||||
self.parent(action, params);
|
}
|
||||||
|
}
|
||||||
self.addEvent('create', function(){
|
})
|
||||||
self.order();
|
)
|
||||||
});
|
}
|
||||||
|
},
|
||||||
self.initialized = true;
|
groups: ['welcome', 'general', 'downloaders', 'searcher', 'providers', 'renamer', 'automation', 'finish'],
|
||||||
|
|
||||||
self.scroll = new Fx.Scroll(document.body, {
|
open: function(action, params){
|
||||||
'transition': 'quint:in:out'
|
var self = this;
|
||||||
});
|
|
||||||
}
|
if(!self.initialized){
|
||||||
else
|
App.fireEvent('unload');
|
||||||
(function(){
|
App.getBlock('header').hide();
|
||||||
var sc = self.el.getElement('.wgroup_'+action);
|
|
||||||
self.scroll.start(0, sc.getCoordinates().top-80);
|
self.parent(action, params);
|
||||||
}).delay(1)
|
|
||||||
},
|
self.addEvent('create', function(){
|
||||||
|
self.order();
|
||||||
order: function(){
|
});
|
||||||
var self = this;
|
|
||||||
|
self.initialized = true;
|
||||||
var form = self.el.getElement('.uniForm');
|
|
||||||
var tabs = self.el.getElement('.tabs');
|
self.scroll = new Fx.Scroll(document.body, {
|
||||||
|
'transition': 'quint:in:out'
|
||||||
self.groups.each(function(group){
|
});
|
||||||
if(self.headers[group]){
|
}
|
||||||
group_container = new Element('.wgroup_'+group, {
|
else
|
||||||
'styles': {
|
(function(){
|
||||||
'opacity': 0.2
|
var sc = self.el.getElement('.wgroup_'+action);
|
||||||
},
|
self.scroll.start(0, sc.getCoordinates().top-80);
|
||||||
'tween': {
|
}).delay(1)
|
||||||
'duration': 350
|
},
|
||||||
}
|
|
||||||
});
|
order: function(){
|
||||||
group_container.adopt(
|
var self = this;
|
||||||
new Element('h1', {
|
|
||||||
'text': self.headers[group].title
|
var form = self.el.getElement('.uniForm');
|
||||||
}),
|
var tabs = self.el.getElement('.tabs');
|
||||||
self.headers[group].description ? new Element('span.description', {
|
|
||||||
'html': self.headers[group].description
|
self.groups.each(function(group, nr){
|
||||||
}) : null,
|
|
||||||
self.headers[group].content ? self.headers[group].content : null
|
if(self.headers[group]){
|
||||||
).inject(form);
|
group_container = new Element('.wgroup_'+group, {
|
||||||
}
|
'styles': {
|
||||||
|
'opacity': 0.2
|
||||||
var tab_navigation = tabs.getElement('.t_'+group);
|
},
|
||||||
if(tab_navigation && group_container){
|
'tween': {
|
||||||
tab_navigation.inject(tabs); // Tab navigation
|
'duration': 350
|
||||||
self.el.getElement('.tab_'+group).inject(group_container); // Tab content
|
}
|
||||||
if(self.headers[group]){
|
});
|
||||||
var a = tab_navigation.getElement('a');
|
|
||||||
a.set('text', (self.headers[group].label || group).capitalize());
|
if(self.headers[group].include){
|
||||||
var url_split = a.get('href').split('wizard')[1].split('/');
|
self.headers[group].include.each(function(inc){
|
||||||
if(url_split.length > 3)
|
group_container.addClass('wgroup_'+inc);
|
||||||
a.set('href', a.get('href').replace(url_split[url_split.length-3]+'/', ''));
|
})
|
||||||
|
}
|
||||||
}
|
|
||||||
}
|
var content = self.headers[group].content
|
||||||
else {
|
group_container.adopt(
|
||||||
new Element('li.t_'+group).adopt(
|
new Element('h1', {
|
||||||
new Element('a', {
|
'text': self.headers[group].title
|
||||||
'href': App.createUrl('wizard/'+group),
|
}),
|
||||||
'text': (self.headers[group].label || group).capitalize()
|
self.headers[group].description ? new Element('span.description', {
|
||||||
})
|
'html': self.headers[group].description
|
||||||
).inject(tabs);
|
}) : null,
|
||||||
}
|
content ? (typeOf(content) == 'function' ? content() : content) : null
|
||||||
|
).inject(form);
|
||||||
if(self.headers[group] && self.headers[group].event)
|
}
|
||||||
self.headers[group].event.call()
|
|
||||||
});
|
var tab_navigation = tabs.getElement('.t_'+group);
|
||||||
|
|
||||||
// Remove toggle
|
if(!tab_navigation && self.headers[group] && self.headers[group].include){
|
||||||
self.el.getElement('.advanced_toggle').destroy();
|
tab_navigation = []
|
||||||
|
self.headers[group].include.each(function(inc){
|
||||||
// Hide retention
|
tab_navigation.include(tabs.getElement('.t_'+inc));
|
||||||
self.el.getElement('.tab_searcher').hide();
|
})
|
||||||
self.el.getElement('.t_searcher').hide();
|
}
|
||||||
|
|
||||||
// Add pointer
|
if(tab_navigation && group_container){
|
||||||
new Element('.tab_wrapper').wraps(tabs).adopt(
|
tabs.adopt(tab_navigation); // Tab navigation
|
||||||
self.pointer = new Element('.pointer', {
|
|
||||||
'tween': {
|
if(self.headers[group] && self.headers[group].include){
|
||||||
'transition': 'quint:in:out'
|
|
||||||
}
|
self.headers[group].include.each(function(inc){
|
||||||
})
|
self.el.getElement('.tab_'+inc).inject(group_container);
|
||||||
);
|
})
|
||||||
|
|
||||||
// Add nav
|
new Element('li.t_'+group).adopt(
|
||||||
var minimum = self.el.getSize().y-window.getSize().y;
|
new Element('a', {
|
||||||
self.groups.each(function(group, nr){
|
'href': App.createUrl('wizard/'+group),
|
||||||
|
'text': (self.headers[group].label || group).capitalize()
|
||||||
var g = self.el.getElement('.wgroup_'+group);
|
})
|
||||||
if(!g || !g.isVisible()) return;
|
).inject(tabs);
|
||||||
var t = self.el.getElement('.t_'+group);
|
|
||||||
if(!t) return;
|
}
|
||||||
|
else
|
||||||
var func = function(){
|
self.el.getElement('.tab_'+group).inject(group_container); // Tab content
|
||||||
var ct = t.getCoordinates();
|
|
||||||
self.pointer.tween('left', ct.left+(ct.width/2)-(self.pointer.getWidth()/2));
|
if(tab_navigation.getElement && self.headers[group]){
|
||||||
g.tween('opacity', 1);
|
var a = tab_navigation.getElement('a');
|
||||||
}
|
a.set('text', (self.headers[group].label || group).capitalize());
|
||||||
|
var url_split = a.get('href').split('wizard')[1].split('/');
|
||||||
if(nr == 0)
|
if(url_split.length > 3)
|
||||||
func();
|
a.set('href', a.get('href').replace(url_split[url_split.length-3]+'/', ''));
|
||||||
|
|
||||||
|
}
|
||||||
var ss = new ScrollSpy( {
|
}
|
||||||
min: function(){
|
else {
|
||||||
var c = g.getCoordinates();
|
new Element('li.t_'+group).adopt(
|
||||||
var top = c.top-(window.getSize().y/2);
|
new Element('a', {
|
||||||
return top > minimum ? minimum : top
|
'href': App.createUrl('wizard/'+group),
|
||||||
},
|
'text': (self.headers[group].label || group).capitalize()
|
||||||
max: function(){
|
})
|
||||||
var c = g.getCoordinates();
|
).inject(tabs);
|
||||||
return c.top+(c.height/2)
|
}
|
||||||
},
|
|
||||||
onEnter: func,
|
if(self.headers[group] && self.headers[group].event)
|
||||||
onLeave: function(){
|
self.headers[group].event.call()
|
||||||
g.tween('opacity', 0.2)
|
});
|
||||||
}
|
|
||||||
});
|
// Remove toggle
|
||||||
});
|
self.el.getElement('.advanced_toggle').destroy();
|
||||||
|
|
||||||
}
|
// Hide retention
|
||||||
|
self.el.getElement('.tab_searcher').hide();
|
||||||
|
self.el.getElement('.t_searcher').hide();
|
||||||
|
|
||||||
|
// Add pointer
|
||||||
|
new Element('.tab_wrapper').wraps(tabs).adopt(
|
||||||
|
self.pointer = new Element('.pointer', {
|
||||||
|
'tween': {
|
||||||
|
'transition': 'quint:in:out'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add nav
|
||||||
|
var minimum = self.el.getSize().y-window.getSize().y;
|
||||||
|
self.groups.each(function(group, nr){
|
||||||
|
|
||||||
|
var g = self.el.getElement('.wgroup_'+group);
|
||||||
|
if(!g || !g.isVisible()) return;
|
||||||
|
var t = self.el.getElement('.t_'+group);
|
||||||
|
if(!t) return;
|
||||||
|
|
||||||
|
var func = function(){
|
||||||
|
var ct = t.getCoordinates();
|
||||||
|
self.pointer.tween('left', ct.left+(ct.width/2)-(self.pointer.getWidth()/2));
|
||||||
|
g.tween('opacity', 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(nr == 0)
|
||||||
|
func();
|
||||||
|
|
||||||
|
|
||||||
|
var ss = new ScrollSpy( {
|
||||||
|
min: function(){
|
||||||
|
var c = g.getCoordinates();
|
||||||
|
var top = c.top-(window.getSize().y/2);
|
||||||
|
return top > minimum ? minimum : top
|
||||||
|
},
|
||||||
|
max: function(){
|
||||||
|
var c = g.getCoordinates();
|
||||||
|
return c.top+(c.height/2)
|
||||||
|
},
|
||||||
|
onEnter: func,
|
||||||
|
onLeave: function(){
|
||||||
|
g.tween('opacity', 0.2)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
config = {
|
||||||
|
'name': 'automation_providers',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'label': 'Watchlists',
|
||||||
|
'description': 'Check watchlists for new movies',
|
||||||
|
'type': 'list',
|
||||||
|
'name': 'watchlist_providers',
|
||||||
|
'tab': 'automation',
|
||||||
|
'options': [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Automated',
|
||||||
|
'description': 'Uses minimal requirements',
|
||||||
|
'type': 'list',
|
||||||
|
'name': 'automation_providers',
|
||||||
|
'tab': 'automation',
|
||||||
|
'options': [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.providers.base import Provider
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
import time
|
import time
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Automation(Plugin):
|
class Automation(Provider):
|
||||||
|
|
||||||
enabled_option = 'automation_enabled'
|
enabled_option = 'automation_enabled'
|
||||||
|
|
||||||
@@ -19,6 +19,9 @@ class Automation(Plugin):
|
|||||||
|
|
||||||
def _getMovies(self):
|
def _getMovies(self):
|
||||||
|
|
||||||
|
if self.isDisabled():
|
||||||
|
return
|
||||||
|
|
||||||
if not self.canCheck():
|
if not self.canCheck():
|
||||||
log.debug('Just checked, skipping %s', self.getName())
|
log.debug('Just checked, skipping %s', self.getName())
|
||||||
return []
|
return []
|
||||||
@@ -28,9 +31,18 @@ class Automation(Plugin):
|
|||||||
return self.getIMDBids()
|
return self.getIMDBids()
|
||||||
|
|
||||||
def search(self, name, year = None, imdb_only = False):
|
def search(self, name, year = None, imdb_only = False):
|
||||||
|
|
||||||
|
prop_name = 'automation.cached.%s.%s' % (name, year)
|
||||||
|
cached_imdb = Env.prop(prop_name, default = False)
|
||||||
|
if cached_imdb and imdb_only:
|
||||||
|
return cached_imdb
|
||||||
|
|
||||||
result = fireEvent('movie.search', q = '%s %s' % (name, year if year else ''), limit = 1, merge = True)
|
result = fireEvent('movie.search', q = '%s %s' % (name, year if year else ''), limit = 1, merge = True)
|
||||||
|
|
||||||
if len(result) > 0:
|
if len(result) > 0:
|
||||||
|
if imdb_only and result[0].get('imdb'):
|
||||||
|
Env.prop(prop_name, result[0].get('imdb'))
|
||||||
|
|
||||||
return result[0].get('imdb') if imdb_only else result[0]
|
return result[0].get('imdb') if imdb_only else result[0]
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'automation',
|
'tab': 'automation',
|
||||||
|
'list': 'automation_providers',
|
||||||
'name': 'bluray_automation',
|
'name': 'bluray_automation',
|
||||||
'label': 'Blu-ray.com',
|
'label': 'Blu-ray.com',
|
||||||
'description': 'Imports movies from blu-ray.com. (uses minimal requirements)',
|
'description': 'Imports movies from blu-ray.com. (uses minimal requirements)',
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
from couchpotato.core.helpers.rss import RSS
|
from couchpotato.core.helpers.rss import RSS
|
||||||
from couchpotato.core.helpers.variable import md5, tryInt
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.automation.base import Automation
|
from couchpotato.core.providers.automation.base import Automation
|
||||||
from couchpotato.environment import Env
|
|
||||||
import xml.etree.ElementTree as XMLTree
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -15,32 +13,24 @@ class Bluray(Automation, RSS):
|
|||||||
|
|
||||||
def getIMDBids(self):
|
def getIMDBids(self):
|
||||||
|
|
||||||
if self.isDisabled():
|
|
||||||
return
|
|
||||||
|
|
||||||
movies = []
|
movies = []
|
||||||
|
|
||||||
cache_key = 'bluray.%s' % md5(self.rss_url)
|
rss_movies = self.getRSSData(self.rss_url)
|
||||||
rss_data = self.getCache(cache_key, self.rss_url)
|
|
||||||
data = XMLTree.fromstring(rss_data)
|
|
||||||
|
|
||||||
if data is not None:
|
for movie in rss_movies:
|
||||||
rss_movies = self.getElements(data, 'channel/item')
|
name = self.getTextElement(movie, 'title').lower().split('blu-ray')[0].strip('(').rstrip()
|
||||||
|
year = self.getTextElement(movie, 'description').split('|')[1].strip('(').strip()
|
||||||
|
|
||||||
for movie in rss_movies:
|
if not name.find('/') == -1: # make sure it is not a double movie release
|
||||||
name = self.getTextElement(movie, "title").lower().split("blu-ray")[0].strip("(").rstrip()
|
continue
|
||||||
year = self.getTextElement(movie, "description").split("|")[1].strip("(").strip()
|
|
||||||
|
|
||||||
if not name.find("/") == -1: # make sure it is not a double movie release
|
if tryInt(year) < self.getMinimal('year'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if tryInt(year) < self.getMinimal('year'):
|
imdb = self.search(name, year)
|
||||||
continue
|
|
||||||
|
|
||||||
imdb = self.search(name, year)
|
if imdb:
|
||||||
|
if self.isMinimalMovie(imdb):
|
||||||
if imdb:
|
movies.append(imdb['imdb'])
|
||||||
if self.isMinimalMovie(imdb):
|
|
||||||
movies.append(imdb['imdb'])
|
|
||||||
|
|
||||||
return movies
|
return movies
|
||||||
|
|||||||
@@ -8,7 +8,4 @@ class CP(Automation):
|
|||||||
|
|
||||||
def getMovies(self):
|
def getMovies(self):
|
||||||
|
|
||||||
if self.isDisabled():
|
|
||||||
return
|
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ config = [{
|
|||||||
'groups': [
|
'groups': [
|
||||||
{
|
{
|
||||||
'tab': 'automation',
|
'tab': 'automation',
|
||||||
|
'list': 'watchlist_providers',
|
||||||
'name': 'imdb_automation',
|
'name': 'imdb_automation',
|
||||||
'label': 'IMDB',
|
'label': 'IMDB',
|
||||||
'description': 'From any <strong>public</strong> IMDB watchlists. Url should be the RSS link.',
|
'description': 'From any <strong>public</strong> IMDB watchlists. Url should be the RSS link.',
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user