Compare commits
442 Commits
build/2.0.
...
build/2.0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88579cd71a | ||
|
|
6c57316ce6 | ||
|
|
6702683da3 | ||
|
|
e54928720a | ||
|
|
f8f22cdef7 | ||
|
|
1ed58586a1 | ||
|
|
e694276a8d | ||
|
|
73b7bcc6ce | ||
|
|
f08ccd4fd8 | ||
|
|
312562a9f5 | ||
|
|
1cd8040692 | ||
|
|
7268e02386 | ||
|
|
805aa3ca9f | ||
|
|
29cb34551c | ||
|
|
d267be4455 | ||
|
|
92f4ade371 | ||
|
|
9235eda73b | ||
|
|
1fe23afd1b | ||
|
|
09637c3069 | ||
|
|
2cafd509fc | ||
|
|
62cc570ab2 | ||
|
|
1ec9370e68 | ||
|
|
5b4c60ecba | ||
|
|
7b7488ece8 | ||
|
|
4ba7ff9f27 | ||
|
|
df2d1aca4b | ||
|
|
4fcba70c9a | ||
|
|
d0fc20ca6e | ||
|
|
9402b54f9b | ||
|
|
f0e7795b9b | ||
|
|
bba18d8bc9 | ||
|
|
0494e5fc8f | ||
|
|
df1b46272d | ||
|
|
ed068f09b0 | ||
|
|
5e852d05ee | ||
|
|
d111393bd6 | ||
|
|
89bff73431 | ||
|
|
cd16dddf13 | ||
|
|
25605c45b9 | ||
|
|
b6d0d54609 | ||
|
|
98981dac27 | ||
|
|
ddf03cbcf2 | ||
|
|
1e1abf407c | ||
|
|
1267cdac4d | ||
|
|
05bcee12ae | ||
|
|
fc3f15e0cf | ||
|
|
0a7765f639 | ||
|
|
c214458770 | ||
|
|
bfe501c84a | ||
|
|
e034465df8 | ||
|
|
a7b78d4131 | ||
|
|
3eed34c710 | ||
|
|
9cb3bef156 | ||
|
|
46c7e3fbed | ||
|
|
eed0382b41 | ||
|
|
4e45c94fc3 | ||
|
|
0a11dc6673 | ||
|
|
4ede2c20a1 | ||
|
|
af0cf523e3 | ||
|
|
3908e00650 | ||
|
|
f9bdf6da1c | ||
|
|
87cdf9222d | ||
|
|
2ca2cc9597 | ||
|
|
edb232df60 | ||
|
|
af113c0ffd | ||
|
|
856b495995 | ||
|
|
a56bbf0b3b | ||
|
|
4b54113f08 | ||
|
|
52371b7705 | ||
|
|
629bead919 | ||
|
|
c7cd72787f | ||
|
|
9e260a89af | ||
|
|
d233e4d22e | ||
|
|
a60e9dc4c3 | ||
|
|
b168c1364d | ||
|
|
23893dbcb9 | ||
|
|
14fffda3ff | ||
|
|
51364a3c25 | ||
|
|
c6642ffeb7 | ||
|
|
9fe9ccf0ad | ||
|
|
cb92b00534 | ||
|
|
7d3780133f | ||
|
|
f23b9d7cb9 | ||
|
|
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 |
@@ -62,7 +62,6 @@ class Loader(object):
|
||||
self.log.logger.addHandler(hdlr)
|
||||
|
||||
def addSignals(self):
|
||||
|
||||
signal.signal(signal.SIGINT, self.onExit)
|
||||
signal.signal(signal.SIGTERM, lambda signum, stack_frame: sys.exit(1))
|
||||
|
||||
@@ -74,7 +73,7 @@ class Loader(object):
|
||||
|
||||
def onExit(self, signal, frame):
|
||||
from couchpotato.core.event import fireEvent
|
||||
fireEvent('app.crappy_shutdown', single = True)
|
||||
fireEvent('app.shutdown', single = True)
|
||||
|
||||
def run(self):
|
||||
|
||||
@@ -100,7 +99,7 @@ class Loader(object):
|
||||
logging.shutdown()
|
||||
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)
|
||||
except:
|
||||
self.log.critical(traceback.format_exc())
|
||||
|
||||
25
Desktop.py
25
Desktop.py
@@ -1,12 +1,12 @@
|
||||
from esky.util import appdir_from_executable #@UnresolvedImport
|
||||
from threading import Thread
|
||||
from version import VERSION
|
||||
from wx.lib.softwareupdate import SoftwareUpdate
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import webbrowser
|
||||
import wx
|
||||
import subprocess
|
||||
|
||||
|
||||
# Include proper dirs
|
||||
if hasattr(sys, 'frozen'):
|
||||
@@ -166,8 +166,8 @@ class CouchPotatoApp(wx.App, SoftwareUpdate):
|
||||
def OnInit(self):
|
||||
|
||||
# Updater
|
||||
base_url = 'http://couchpota.to/updates/'
|
||||
self.InitUpdates(base_url, base_url + 'changelog.html',
|
||||
base_url = 'https://couchpota.to/updates/%s'
|
||||
self.InitUpdates(base_url % VERSION + '/', base_url % 'changelog.html',
|
||||
icon = wx.Icon('icon.png'))
|
||||
|
||||
self.frame = MainFrame(self)
|
||||
@@ -214,5 +214,18 @@ if __name__ == '__main__':
|
||||
time.sleep(1)
|
||||
|
||||
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.
|
||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`.
|
||||
* 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:
|
||||
|
||||
@@ -26,6 +27,7 @@ OSx:
|
||||
* Go to your App folder `cd /Applications`
|
||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
||||
* Then do `python CouchPotatoServer/CouchPotato.py`
|
||||
* Your browser should open up, but if it doesn't go to: `http://localhost:5050/`
|
||||
|
||||
Linux (ubuntu / debian):
|
||||
|
||||
@@ -33,7 +35,8 @@ Linux (ubuntu / debian):
|
||||
* 'cd' to the folder of your choosing.
|
||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
||||
* Then do `python CouchPotatoServer/CouchPotato.py` to start
|
||||
* To run on boot copy the init script. `cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
|
||||
* Change the paths inside the init script. `nano /etc/init.d/couchpotato`
|
||||
* Make it executable. `chmod +x /etc/init.d/couchpotato`
|
||||
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/ubuntu /etc/init.d/couchpotato`
|
||||
* Change the paths inside the init script. `sudo nano /etc/init.d/couchpotato`
|
||||
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
|
||||
* Add it to defaults. `sudo update-rc.d couchpotato defaults`
|
||||
* Open your browser and go to: `http://localhost:5050/`
|
||||
|
||||
15
contributing.md
Normal file
15
contributing.md
Normal file
@@ -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 ;)**
|
||||
@@ -78,6 +78,7 @@ def page_not_found(error):
|
||||
r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url)
|
||||
return redirect(r)
|
||||
else:
|
||||
time.sleep(0.1)
|
||||
if not Env.get('dev'):
|
||||
time.sleep(0.1)
|
||||
return 'Wrong API key used', 404
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from flask.blueprints import Blueprint
|
||||
from flask.helpers import url_for
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.web import RequestHandler, asynchronous
|
||||
from werkzeug.utils import redirect
|
||||
|
||||
@@ -11,7 +10,11 @@ api_nonblock = {}
|
||||
|
||||
|
||||
class NonBlockHandler(RequestHandler):
|
||||
stoppers = []
|
||||
|
||||
def __init__(self, application, request, **kwargs):
|
||||
cls = NonBlockHandler
|
||||
cls.stoppers = []
|
||||
super(NonBlockHandler, self).__init__(application, request, **kwargs)
|
||||
|
||||
@asynchronous
|
||||
def get(self, route):
|
||||
|
||||
@@ -23,19 +23,22 @@ config = [{
|
||||
'default': '',
|
||||
'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',
|
||||
'default': 5050,
|
||||
'type': 'int',
|
||||
'description': 'The port I should listen to.',
|
||||
},
|
||||
{
|
||||
'name': 'ssl_cert',
|
||||
'description': 'Path to SSL server.crt',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'name': 'ssl_key',
|
||||
'description': 'Path to SSL server.key',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'name': 'launch_browser',
|
||||
'default': True,
|
||||
|
||||
@@ -9,6 +9,7 @@ from tornado.ioloop import IOLoop
|
||||
from uuid import uuid4
|
||||
import os
|
||||
import platform
|
||||
import signal
|
||||
import time
|
||||
import traceback
|
||||
import webbrowser
|
||||
@@ -51,6 +52,9 @@ class Core(Plugin):
|
||||
addEvent('setting.save.core.password', self.md5Password)
|
||||
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):
|
||||
return md5(value.encode(Env.get('encoding'))) if value else ''
|
||||
@@ -66,7 +70,7 @@ class Core(Plugin):
|
||||
|
||||
def available(self):
|
||||
return jsonified({
|
||||
'succes': True
|
||||
'success': True
|
||||
})
|
||||
|
||||
def shutdown(self):
|
||||
@@ -98,7 +102,7 @@ class Core(Plugin):
|
||||
|
||||
self.shutdown_started = True
|
||||
|
||||
fireEvent('app.shutdown')
|
||||
fireEvent('app.do_shutdown')
|
||||
log.debug('Every plugin got shutdown event')
|
||||
|
||||
loop = True
|
||||
@@ -148,7 +152,7 @@ class Core(Plugin):
|
||||
|
||||
def createBaseUrl(self):
|
||||
host = Env.setting('host')
|
||||
if host == '0.0.0.0':
|
||||
if host == '0.0.0.0' or host == '':
|
||||
host = 'localhost'
|
||||
port = Env.setting('port')
|
||||
|
||||
@@ -170,3 +174,12 @@ class Core(Plugin):
|
||||
return jsonified({
|
||||
'version': self.version()
|
||||
})
|
||||
|
||||
def signalHandler(self):
|
||||
if Env.get('daemonized'): return
|
||||
|
||||
def signal_handler(signal, frame):
|
||||
fireEvent('app.shutdown', single = True)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
@@ -1,15 +1,60 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
from minify.cssmin import cssmin
|
||||
from minify.jsmin import jsmin
|
||||
import os
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class ClientScript(Plugin):
|
||||
|
||||
urls = {
|
||||
'style': {},
|
||||
'script': {},
|
||||
core_static = {
|
||||
'style': [
|
||||
'style/main.css',
|
||||
'style/uniform.generic.css',
|
||||
'style/uniform.css',
|
||||
'style/settings.css',
|
||||
],
|
||||
'script': [
|
||||
'scripts/library/mootools.js',
|
||||
'scripts/library/mootools_more.js',
|
||||
'scripts/library/prefix_free.js',
|
||||
'scripts/library/uniform.js',
|
||||
'scripts/library/form_replacement/form_check.js',
|
||||
'scripts/library/form_replacement/form_radio.js',
|
||||
'scripts/library/form_replacement/form_dropdown.js',
|
||||
'scripts/library/form_replacement/form_selectoption.js',
|
||||
'scripts/library/question.js',
|
||||
'scripts/library/scrollspy.js',
|
||||
'scripts/library/spin.js',
|
||||
'scripts/couchpotato.js',
|
||||
'scripts/api.js',
|
||||
'scripts/library/history.js',
|
||||
'scripts/page.js',
|
||||
'scripts/block.js',
|
||||
'scripts/block/navigation.js',
|
||||
'scripts/block/footer.js',
|
||||
'scripts/block/menu.js',
|
||||
'scripts/page/home.js',
|
||||
'scripts/page/wanted.js',
|
||||
'scripts/page/settings.js',
|
||||
'scripts/page/about.js',
|
||||
'scripts/page/manage.js',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
urls = {'style': {}, 'script': {}, }
|
||||
minified = {'style': {}, 'script': {}, }
|
||||
paths = {'style': {}, 'script': {}, }
|
||||
comment = {
|
||||
'style': '/*** %s:%d ***/\n',
|
||||
'script': '// %s:%d\n'
|
||||
}
|
||||
|
||||
html = {
|
||||
@@ -24,6 +69,66 @@ class ClientScript(Plugin):
|
||||
addEvent('clientscript.get_styles', self.getStyles)
|
||||
addEvent('clientscript.get_scripts', self.getScripts)
|
||||
|
||||
addEvent('app.load', self.minify)
|
||||
|
||||
self.addCore()
|
||||
|
||||
def addCore(self):
|
||||
|
||||
for static_type in self.core_static:
|
||||
for rel_path in self.core_static.get(static_type):
|
||||
file_path = os.path.join(Env.get('app_dir'), 'couchpotato', 'static', rel_path)
|
||||
core_url = 'api/%s/static/%s?%s' % (Env.setting('api_key'), rel_path, tryInt(os.path.getmtime(file_path)))
|
||||
|
||||
if static_type == 'script':
|
||||
self.registerScript(core_url, file_path, position = 'front')
|
||||
else:
|
||||
self.registerStyle(core_url, file_path, position = 'front')
|
||||
|
||||
|
||||
def minify(self):
|
||||
|
||||
for file_type in ['style', 'script']:
|
||||
ext = 'js' if file_type is 'script' else 'css'
|
||||
positions = self.paths.get(file_type, {})
|
||||
for position in positions:
|
||||
files = positions.get(position)
|
||||
self._minify(file_type, files, position, position + '.' + ext)
|
||||
|
||||
def _minify(self, file_type, files, position, out):
|
||||
|
||||
cache = Env.get('cache_dir')
|
||||
out_name = 'minified_' + out
|
||||
out = os.path.join(cache, out_name)
|
||||
|
||||
raw = []
|
||||
for file_path in files:
|
||||
f = open(file_path, 'r').read()
|
||||
|
||||
if file_type == 'script':
|
||||
data = jsmin(f)
|
||||
else:
|
||||
data = cssmin(f)
|
||||
data = data.replace('../images/', '../static/images/')
|
||||
|
||||
raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data})
|
||||
|
||||
# Combine all files together with some comments
|
||||
data = ''
|
||||
for r in raw:
|
||||
data += self.comment.get(file_type) % (r.get('file'), r.get('date'))
|
||||
data += r.get('data') + '\n\n'
|
||||
|
||||
self.createFile(out, data.strip())
|
||||
|
||||
if not self.minified.get(file_type):
|
||||
self.minified[file_type] = {}
|
||||
if not self.minified[file_type].get(position):
|
||||
self.minified[file_type][position] = []
|
||||
|
||||
minified_url = 'api/%s/file.cache/%s?%s' % (Env.setting('api_key'), out_name, tryInt(os.path.getmtime(out)))
|
||||
self.minified[file_type][position].append(minified_url)
|
||||
|
||||
def getStyles(self, *args, **kwargs):
|
||||
return self.get('style', *args, **kwargs)
|
||||
|
||||
@@ -35,22 +140,30 @@ class ClientScript(Plugin):
|
||||
data = '' if as_html else []
|
||||
|
||||
try:
|
||||
try:
|
||||
if not Env.get('dev'):
|
||||
return self.minified[type][location]
|
||||
except:
|
||||
pass
|
||||
|
||||
return self.urls[type][location]
|
||||
except Exception, e:
|
||||
log.error(e)
|
||||
except:
|
||||
log.error('Error getting minified %s, %s: %s', (type, location, traceback.format_exc()))
|
||||
|
||||
return data
|
||||
|
||||
def registerStyle(self, path, position = 'head'):
|
||||
self.register(path, 'style', position)
|
||||
def registerStyle(self, api_path, file_path, position = 'head'):
|
||||
self.register(api_path, file_path, 'style', position)
|
||||
|
||||
def registerScript(self, path, position = 'head'):
|
||||
self.register(path, 'script', position)
|
||||
def registerScript(self, api_path, file_path, position = 'head'):
|
||||
self.register(api_path, file_path, 'script', position)
|
||||
|
||||
def register(self, filepath, type, location):
|
||||
def register(self, api_path, file_path, type, location):
|
||||
|
||||
if not self.urls[type].get(location):
|
||||
self.urls[type][location] = []
|
||||
self.urls[type][location].append(api_path)
|
||||
|
||||
filePath = filepath
|
||||
self.urls[type][location].append(filePath)
|
||||
if not self.paths[type].get(location):
|
||||
self.paths[type][location] = []
|
||||
self.paths[type][location].append(file_path)
|
||||
|
||||
@@ -2,7 +2,6 @@ from apscheduler.scheduler import Scheduler as Sched
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
import logging
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from .main import Updater
|
||||
from couchpotato.environment import Env
|
||||
import os
|
||||
|
||||
def start():
|
||||
return Updater()
|
||||
@@ -33,6 +35,7 @@ config = [{
|
||||
{
|
||||
'name': 'git_command',
|
||||
'default': 'git',
|
||||
'hidden': not os.path.isdir(os.path.join(Env.get('app_dir'), '.git')),
|
||||
'advanced': True
|
||||
},
|
||||
],
|
||||
|
||||
@@ -106,6 +106,10 @@ class Updater(Plugin):
|
||||
if success:
|
||||
fireEventAsync('app.restart')
|
||||
|
||||
# Assume the updater handles things
|
||||
if not success:
|
||||
success = True
|
||||
|
||||
return jsonified({
|
||||
'success': success
|
||||
})
|
||||
@@ -396,6 +400,7 @@ class DesktopUpdater(BaseUpdater):
|
||||
self.update_failed = True
|
||||
|
||||
self.desktop._esky.auto_update(callback = do_restart)
|
||||
return
|
||||
except:
|
||||
self.update_failed = True
|
||||
|
||||
@@ -406,7 +411,7 @@ class DesktopUpdater(BaseUpdater):
|
||||
'last_check': self.last_check,
|
||||
'update_version': self.update_version,
|
||||
'version': self.getVersion(),
|
||||
'branch': 'desktop_build',
|
||||
'branch': self.branch,
|
||||
}
|
||||
|
||||
def check(self):
|
||||
|
||||
@@ -90,17 +90,18 @@ var UpdaterBase = new Class({
|
||||
doUpdate: function(){
|
||||
var self = this;
|
||||
|
||||
App.blockPage('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
|
||||
Api.request('updater.update', {
|
||||
'onComplete': function(json){
|
||||
if(json.success){
|
||||
if(json.success)
|
||||
self.updating();
|
||||
}
|
||||
else
|
||||
App.unBlockPage()
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updating: function(){
|
||||
App.blockPage('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
|
||||
App.checkAvailable.delay(500, App, [1000, function(){
|
||||
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,76 @@
|
||||
from base64 import b32decode, b16encode
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toSafeString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
import os
|
||||
from couchpotato.core.providers.base import Provider
|
||||
import random
|
||||
import re
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Downloader(Plugin):
|
||||
class Downloader(Provider):
|
||||
|
||||
type = []
|
||||
http_time_between_calls = 0
|
||||
|
||||
torrent_sources = [
|
||||
'http://torrage.com/torrent/%s.torrent',
|
||||
'http://torrage.ws/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):
|
||||
addEvent('download', self.download)
|
||||
addEvent('download.status', self.getDownloadStatus)
|
||||
addEvent('download', self._download)
|
||||
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):
|
||||
pass
|
||||
def getEnabledDownloadType(self):
|
||||
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 getAllDownloadStatus(self):
|
||||
return
|
||||
|
||||
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
|
||||
|
||||
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 removeFailed(self, item):
|
||||
return
|
||||
|
||||
def isCorrectType(self, item_type):
|
||||
is_correct = item_type in self.type
|
||||
@@ -56,7 +81,7 @@ class Downloader(Plugin):
|
||||
return is_correct
|
||||
|
||||
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
|
||||
if len(torrent_hash) == 32:
|
||||
@@ -78,9 +103,16 @@ class Downloader(Plugin):
|
||||
log.error('Failed converting magnet url to torrent: %s', (torrent_hash))
|
||||
return False
|
||||
|
||||
def isDisabled(self, manual):
|
||||
return not self.isEnabled(manual)
|
||||
def isDisabled(self, manual, data):
|
||||
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)
|
||||
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 couchpotato.core.helpers.variable import getDownloadDir
|
||||
|
||||
def start():
|
||||
return Blackhole()
|
||||
@@ -9,6 +10,7 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'blackhole',
|
||||
'label': 'Black hole',
|
||||
'description': 'Download the NZB/Torrent to a specific folder.',
|
||||
@@ -16,7 +18,7 @@ config = [{
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'default': True,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'nzb,torrent',
|
||||
},
|
||||
@@ -24,6 +26,7 @@ config = [{
|
||||
'name': 'directory',
|
||||
'type': 'directory',
|
||||
'description': 'Directory where the .nzb (or .torrent) file is saved to.',
|
||||
'default': getDownloadDir()
|
||||
},
|
||||
{
|
||||
'name': 'use_for',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import with_statement
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
import os
|
||||
import traceback
|
||||
|
||||
@@ -10,11 +11,7 @@ class Blackhole(Downloader):
|
||||
|
||||
type = ['nzb', 'torrent', 'torrent_magnet']
|
||||
|
||||
def download(self, data = {}, movie = {}, manual = False, 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
|
||||
def download(self, data = {}, movie = {}, filedata = None):
|
||||
|
||||
directory = self.conf('directory')
|
||||
if not directory or not os.path.isdir(directory):
|
||||
@@ -40,6 +37,7 @@ class Blackhole(Downloader):
|
||||
log.info('Downloading %s to %s.', (data.get('type'), fullPath))
|
||||
with open(fullPath, 'wb') as f:
|
||||
f.write(filedata)
|
||||
os.chmod(fullPath, Env.getPermission('file'))
|
||||
return True
|
||||
else:
|
||||
log.info('File %s already exists.', fullPath)
|
||||
@@ -52,4 +50,23 @@ class Blackhole(Downloader):
|
||||
except:
|
||||
log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc()))
|
||||
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 = {}):
|
||||
for_type = ['both']
|
||||
if data and 'torrent' in data.get('type'):
|
||||
for_type.append('torrent')
|
||||
elif data:
|
||||
for_type.append(data.get('type'))
|
||||
|
||||
return super(Blackhole, self).isEnabled(manual, data) and \
|
||||
((self.conf('use_for') in for_type))
|
||||
|
||||
@@ -8,10 +8,10 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'nzbget',
|
||||
'label': 'NZBGet',
|
||||
'description': 'Send NZBs to your NZBGet installation.',
|
||||
'wizard': True,
|
||||
'description': 'Use <a href="http://nzbget.sourceforge.net/Main_Page" target="_blank">NZBGet</a> to download NZBs.',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
@@ -26,6 +26,7 @@ config = [{
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
'description': 'Default NZBGet password is <i>tegbzn6789</i>',
|
||||
},
|
||||
{
|
||||
@@ -33,6 +34,13 @@ config = [{
|
||||
'default': 'Movies',
|
||||
'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',
|
||||
'default': 0,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from base64 import standard_b64encode
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from inspect import isfunction
|
||||
import re
|
||||
import socket
|
||||
import traceback
|
||||
import xmlrpclib
|
||||
@@ -14,10 +16,7 @@ class NZBGet(Downloader):
|
||||
|
||||
url = 'http://nzbget:%(password)s@%(host)s/xmlrpc'
|
||||
|
||||
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
||||
|
||||
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
|
||||
return
|
||||
def download(self, data = {}, movie = {}, filedata = None):
|
||||
|
||||
if not filedata:
|
||||
log.error('Unable to get NZB file: %s', traceback.format_exc())
|
||||
@@ -26,7 +25,7 @@ class NZBGet(Downloader):
|
||||
log.info('Sending "%s" to NZBGet.', data.get('name'))
|
||||
|
||||
url = self.url % {'host': self.conf('host'), 'password': self.conf('password')}
|
||||
nzb_name = '%s.nzb' % self.createNzbName(data, movie)
|
||||
nzb_name = ss('%s.nzb' % self.createNzbName(data, movie))
|
||||
|
||||
rpc = xmlrpclib.ServerProxy(url)
|
||||
try:
|
||||
@@ -44,7 +43,12 @@ class NZBGet(Downloader):
|
||||
log.error('Protocol Error: %s', e)
|
||||
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')
|
||||
return True
|
||||
else:
|
||||
|
||||
47
couchpotato/core/downloaders/nzbvortex/__init__.py
Normal file
47
couchpotato/core/downloaders/nzbvortex/__init__.py
Normal file
@@ -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.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
170
couchpotato/core/downloaders/nzbvortex/main.py
Normal file
170
couchpotato/core/downloaders/nzbvortex/main.py
Normal file
@@ -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': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'pneumatic',
|
||||
'label': 'Pneumatic',
|
||||
'description': 'Download the .strm file to a specific folder.',
|
||||
'wizard': True,
|
||||
'description': 'Use <a href="http://forum.xbmc.org/showthread.php?tid=97657" target="_blank">Pneumatic</a> to download .strm files.',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
|
||||
@@ -11,9 +11,7 @@ class Pneumatic(Downloader):
|
||||
type = ['nzb']
|
||||
strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s'
|
||||
|
||||
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
||||
if self.isDisabled(manual) or (not self.isCorrectType(data.get('type'))):
|
||||
return
|
||||
def download(self, data = {}, movie = {}, filedata = None):
|
||||
|
||||
directory = self.conf('directory')
|
||||
if not directory or not os.path.isdir(directory):
|
||||
|
||||
@@ -8,9 +8,10 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': '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,
|
||||
'options': [
|
||||
{
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss
|
||||
from couchpotato.core.helpers.variable import cleanHost, mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
import traceback
|
||||
from couchpotato.environment import Env
|
||||
from urllib2 import URLError
|
||||
import json
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -11,10 +13,7 @@ class Sabnzbd(Downloader):
|
||||
|
||||
type = ['nzb']
|
||||
|
||||
def download(self, data = {}, movie = {}, manual = False, filedata = None):
|
||||
|
||||
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
|
||||
return
|
||||
def download(self, data = {}, movie = {}, filedata = None):
|
||||
|
||||
log.info('Sending "%s" to SABnzbd.', data.get('name'))
|
||||
|
||||
@@ -27,7 +26,7 @@ class Sabnzbd(Downloader):
|
||||
|
||||
if filedata:
|
||||
if len(filedata) < 50:
|
||||
log.error('No proper nzb available!')
|
||||
log.error('No proper nzb available: %s', (filedata))
|
||||
return False
|
||||
|
||||
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
|
||||
@@ -36,126 +35,119 @@ class Sabnzbd(Downloader):
|
||||
else:
|
||||
params['name'] = data.get('url')
|
||||
|
||||
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
|
||||
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(params)
|
||||
|
||||
try:
|
||||
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, headers = {'User-Agent': Env.getIdentifier()})
|
||||
else:
|
||||
sab = self.urlopen(url, timeout = 60, show_error = False)
|
||||
sab = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
|
||||
except URLError:
|
||||
log.error('Failed sending release, probably wrong HOST: %s', traceback.format_exc(0))
|
||||
return False
|
||||
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
|
||||
|
||||
result = sab.strip()
|
||||
if not result:
|
||||
log.error("SABnzbd didn't return anything.")
|
||||
log.error('SABnzbd didn\'t return anything.')
|
||||
return False
|
||||
|
||||
log.debug("Result text from SAB: " + result[:40])
|
||||
if result == "ok":
|
||||
log.info("NZB sent to SAB successfully.")
|
||||
log.debug('Result text from SAB: %s', result[:40])
|
||||
if result[:2] == 'ok':
|
||||
log.info('NZB sent to SAB successfully.')
|
||||
return True
|
||||
elif result == "Missing authentication":
|
||||
log.error("Incorrect username/password.")
|
||||
return False
|
||||
else:
|
||||
log.error("Unknown error: " + result[:40])
|
||||
log.error(result[:40])
|
||||
return False
|
||||
|
||||
def getDownloadStatus(self, data = {}, movie = {}):
|
||||
if self.isDisabled(manual = True) or not self.isCorrectType(data.get('type')):
|
||||
return
|
||||
def getAllDownloadStatus(self):
|
||||
|
||||
nzbname = self.createNzbName(data, movie)
|
||||
log.info('Checking download status of "%s" at SABnzbd.', nzbname)
|
||||
log.debug('Checking SABnzbd download status.')
|
||||
|
||||
# Go through Queue
|
||||
params = {
|
||||
'apikey': self.conf('api_key'),
|
||||
'mode': 'queue',
|
||||
'output': 'json'
|
||||
}
|
||||
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
|
||||
|
||||
try:
|
||||
sab = self.urlopen(url, timeout = 60, show_error = False)
|
||||
queue = self.call({
|
||||
'mode': 'queue',
|
||||
})
|
||||
except:
|
||||
log.error('Failed checking status: %s', traceback.format_exc())
|
||||
log.error('Failed getting queue: %s', traceback.format_exc(1))
|
||||
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
|
||||
params = {
|
||||
'apikey': self.conf('api_key'),
|
||||
'mode': 'history',
|
||||
'output': 'json'
|
||||
}
|
||||
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
|
||||
try:
|
||||
history = self.call({
|
||||
'mode': 'history',
|
||||
'limit': 15,
|
||||
})
|
||||
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:
|
||||
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:
|
||||
log.error('Failed getting history: %s', traceback.format_exc())
|
||||
return
|
||||
log.error('Failed deleting: %s', traceback.format_exc(0))
|
||||
return False
|
||||
|
||||
try:
|
||||
history = json.loads(sab)
|
||||
except:
|
||||
log.debug("Result text from SAB: " + sab[:40])
|
||||
log.error('Failed parsing history json: %s', traceback.format_exc())
|
||||
return
|
||||
return True
|
||||
|
||||
for slot in history['history']['slots']:
|
||||
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()):
|
||||
def call(self, params, use_json = True):
|
||||
|
||||
# Delete failed download
|
||||
if self.conf('delete_failed', default = True):
|
||||
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(params, {
|
||||
'apikey': self.conf('api_key'),
|
||||
'output': 'json'
|
||||
}))
|
||||
|
||||
log.info('%s failed downloading, deleting...', slot['name'])
|
||||
params = {
|
||||
'apikey': self.conf('api_key'),
|
||||
'mode': 'history',
|
||||
'name': 'delete',
|
||||
'del_files': '1',
|
||||
'value': slot['nzo_id']
|
||||
}
|
||||
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
|
||||
data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()})
|
||||
if use_json:
|
||||
d = json.loads(data)
|
||||
if d.get('error'):
|
||||
log.error('Error getting data from SABNZBd: %s', d.get('error'))
|
||||
return {}
|
||||
|
||||
try:
|
||||
sab = self.urlopen(url, timeout = 60, show_error = False)
|
||||
except:
|
||||
log.error('Failed deleting: %s', traceback.format_exc())
|
||||
return False
|
||||
return d[params['mode']]
|
||||
else:
|
||||
return data
|
||||
|
||||
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'
|
||||
|
||||
45
couchpotato/core/downloaders/synology/__init__.py
Normal file
45
couchpotato/core/downloaders/synology/__init__.py
Normal file
@@ -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.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
105
couchpotato/core/downloaders/synology/main.py
Normal file
105
couchpotato/core/downloaders/synology/main.py
Normal file
@@ -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': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': '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,
|
||||
'options': [
|
||||
{
|
||||
|
||||
@@ -16,10 +16,7 @@ class Transmission(Downloader):
|
||||
type = ['torrent', 'torrent_magnet']
|
||||
log = CPLog(__name__)
|
||||
|
||||
def download(self, data, movie, manual = False, filedata = None):
|
||||
|
||||
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
|
||||
return
|
||||
def download(self, data, movie, filedata = None):
|
||||
|
||||
log.debug('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
|
||||
|
||||
@@ -30,15 +27,23 @@ class Transmission(Downloader):
|
||||
return False
|
||||
|
||||
# Set parameters for Transmission
|
||||
folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1]
|
||||
folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep)
|
||||
|
||||
# Create the empty folder to download too
|
||||
self.makeDir(folder_path)
|
||||
|
||||
params = {
|
||||
'paused': self.conf('paused', default = 0),
|
||||
'download-dir': self.conf('directory', default = '').rstrip(os.path.sep)
|
||||
'download-dir': folder_path
|
||||
}
|
||||
|
||||
torrent_params = {
|
||||
'seedRatioLimit': self.conf('ratio'),
|
||||
'seedRatioMode': (0 if self.conf('ratio') else 1)
|
||||
}
|
||||
torrent_params = {}
|
||||
if self.conf('ratio'):
|
||||
torrent_params = {
|
||||
'seedRatioLimit': self.conf('ratio'),
|
||||
'seedRatioMode': self.conf('ratio')
|
||||
}
|
||||
|
||||
if not filedata and data.get('type') == 'torrent':
|
||||
log.error('Failed sending torrent, no data')
|
||||
@@ -49,11 +54,13 @@ class Transmission(Downloader):
|
||||
trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||
if data.get('type') == 'torrent_magnet':
|
||||
remote_torrent = trpc.add_torrent_uri(data.get('url'), arguments = params)
|
||||
torrent_params['trackerAdd'] = self.torrent_trackers
|
||||
else:
|
||||
remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params)
|
||||
|
||||
# Change settings of added torrents
|
||||
trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
|
||||
if torrent_params:
|
||||
trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
|
||||
|
||||
return True
|
||||
except Exception, err:
|
||||
|
||||
55
couchpotato/core/downloaders/utorrent/__init__.py
Normal file
55
couchpotato/core/downloaders/utorrent/__init__.py
Normal file
@@ -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.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
197
couchpotato/core/downloaders/utorrent/main.py
Normal file
197
couchpotato/core/downloaders/utorrent/main.py
Normal file
@@ -0,0 +1,197 @@
|
||||
from base64 import b16encode, b32decode
|
||||
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 json
|
||||
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)
|
||||
|
||||
# Convert base 32 to hex
|
||||
if len(torrent_hash) == 32:
|
||||
torrent_hash = b16encode(b32decode(torrent_hash))
|
||||
|
||||
# Send request to uTorrent
|
||||
try:
|
||||
if not self.utorrent_api:
|
||||
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||
|
||||
if data.get('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
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
|
||||
log.debug('Checking uTorrent download status.')
|
||||
|
||||
# Load host from config and split out port.
|
||||
host = self.conf('host').split(':')
|
||||
if not isInt(host[1]):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
return False
|
||||
|
||||
try:
|
||||
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||
except Exception, err:
|
||||
log.error('Failed to get uTorrent object: %s', err)
|
||||
return False
|
||||
|
||||
data = ''
|
||||
try:
|
||||
data = self.utorrent_api.get_status()
|
||||
queue = json.loads(data)
|
||||
if queue.get('error'):
|
||||
log.error('Error getting data from uTorrent: %s', queue.get('error'))
|
||||
return False
|
||||
|
||||
except Exception, err:
|
||||
log.error('Failed to get status from uTorrent: %s', err)
|
||||
return False
|
||||
|
||||
if queue.get('torrents', []) == []:
|
||||
log.debug('Nothing in queue')
|
||||
return False
|
||||
|
||||
statuses = []
|
||||
|
||||
# Get torrents
|
||||
for item in queue.get('torrents', []):
|
||||
|
||||
# item[21] = Paused | Downloading | Seeding | Finished
|
||||
status = 'busy'
|
||||
if item[21] == 'Finished' or item[21] == 'Seeding':
|
||||
status = 'completed'
|
||||
|
||||
statuses.append({
|
||||
'id': item[0],
|
||||
'name': item[2],
|
||||
'status': status,
|
||||
'original_status': item[1],
|
||||
'timeleft': item[10],
|
||||
})
|
||||
|
||||
return statuses
|
||||
|
||||
|
||||
|
||||
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()
|
||||
if response:
|
||||
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)
|
||||
|
||||
def get_status(self):
|
||||
action = "list=1"
|
||||
return self._request(action)
|
||||
@@ -12,14 +12,14 @@ def runHandler(name, handler, *args, **kwargs):
|
||||
return handler(*args, **kwargs)
|
||||
except:
|
||||
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):
|
||||
|
||||
if events.get(name):
|
||||
e = events[name]
|
||||
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):
|
||||
|
||||
@@ -46,49 +46,40 @@ def fireEvent(name, *args, **kwargs):
|
||||
#log.debug('Firing event %s', name)
|
||||
try:
|
||||
|
||||
# Fire after event
|
||||
is_after_event = False
|
||||
try:
|
||||
del kwargs['is_after_event']
|
||||
is_after_event = True
|
||||
except: pass
|
||||
options = {
|
||||
'is_after_event': False, # Fire after event
|
||||
'on_complete': False, # onComplete event
|
||||
'single': False, # Return single handler
|
||||
'merge': False, # Merge items
|
||||
'in_order': False, # Fire them in specific order, waits for the other to finish
|
||||
}
|
||||
|
||||
# onComplete event
|
||||
on_complete = False
|
||||
try:
|
||||
on_complete = kwargs['on_complete']
|
||||
del kwargs['on_complete']
|
||||
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
|
||||
# Do options
|
||||
for x in options:
|
||||
try:
|
||||
val = kwargs[x]
|
||||
del kwargs[x]
|
||||
options[x] = val
|
||||
except: pass
|
||||
|
||||
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
|
||||
|
||||
# Loop over results, stop when first not None result is found.
|
||||
@@ -112,19 +103,22 @@ def fireEvent(name, *args, **kwargs):
|
||||
errorHandler(r[1])
|
||||
|
||||
# Merge
|
||||
if merge and len(results) > 0:
|
||||
if options['merge'] and len(results) > 0:
|
||||
results.reverse() # Priority 1 is higher then 100
|
||||
|
||||
# Dict
|
||||
if type(results[0]) == dict:
|
||||
if isinstance(results[0], dict):
|
||||
merged = {}
|
||||
for result in results:
|
||||
merged = mergeDicts(merged, result)
|
||||
|
||||
results = merged
|
||||
# Lists
|
||||
elif type(results[0]) == list:
|
||||
elif isinstance(results[0], list):
|
||||
merged = []
|
||||
for result in results:
|
||||
merged += result
|
||||
if result not in merged:
|
||||
merged += result
|
||||
|
||||
results = merged
|
||||
|
||||
@@ -133,11 +127,11 @@ def fireEvent(name, *args, **kwargs):
|
||||
log.debug('Return modified results for %s', name)
|
||||
results = modified_results
|
||||
|
||||
if not is_after_event:
|
||||
if not options['is_after_event']:
|
||||
fireEvent('%s.after' % name, is_after_event = True)
|
||||
|
||||
if on_complete:
|
||||
on_complete()
|
||||
if options['on_complete']:
|
||||
options['on_complete']()
|
||||
|
||||
return results
|
||||
except KeyError, e:
|
||||
|
||||
@@ -68,7 +68,7 @@ def tryUrlencode(s):
|
||||
|
||||
return new[1:]
|
||||
else:
|
||||
for letter in toUnicode(s):
|
||||
for letter in ss(s):
|
||||
try:
|
||||
new += quote_plus(letter)
|
||||
except:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toSafeString
|
||||
from couchpotato.core.logger import CPLog
|
||||
import hashlib
|
||||
import os.path
|
||||
@@ -9,15 +10,34 @@ import sys
|
||||
|
||||
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():
|
||||
|
||||
# Windows
|
||||
if os.name == 'nt':
|
||||
return os.path.join(os.environ['APPDATA'], 'CouchPotato')
|
||||
|
||||
import pwd
|
||||
os.environ['HOME'] = pwd.getpwuid(os.geteuid()).pw_dir
|
||||
user_dir = os.path.expanduser('~')
|
||||
user_dir = getUserDir()
|
||||
|
||||
# OSX
|
||||
if 'darwin' in platform.platform().lower():
|
||||
@@ -84,7 +104,7 @@ def cleanHost(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):
|
||||
output = open(txt, 'r')
|
||||
@@ -92,8 +112,10 @@ def getImdb(txt, check_inside = True):
|
||||
output.close()
|
||||
|
||||
try:
|
||||
id = re.findall('(tt\d{7})', txt)[0]
|
||||
return id
|
||||
ids = re.findall('(tt\d{7})', txt)
|
||||
if multiple:
|
||||
return ids if len(ids) > 0 else []
|
||||
return ids[0]
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
@@ -118,12 +140,32 @@ def getTitle(library_dict):
|
||||
try:
|
||||
return library_dict['titles'][0]['title']
|
||||
except:
|
||||
log.error('Could not get title for %s', library_dict['identifier'])
|
||||
return None
|
||||
try:
|
||||
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:
|
||||
log.error('Could not get title for library item: %s', library_dict)
|
||||
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):
|
||||
return ''.join(random.choice(chars) for x in range(size))
|
||||
|
||||
def splitString(str, split_on = ','):
|
||||
return [x.strip() for x in str.split(split_on)] if str else []
|
||||
|
||||
@@ -67,6 +67,18 @@ class Loader(object):
|
||||
|
||||
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, '*')):
|
||||
name = os.path.basename(cur_file)
|
||||
if os.path.isdir(os.path.join(dir_name, name)):
|
||||
|
||||
@@ -5,7 +5,7 @@ import traceback
|
||||
class CPLog(object):
|
||||
|
||||
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 = ''):
|
||||
if context.endswith('.main'):
|
||||
@@ -17,6 +17,9 @@ class CPLog(object):
|
||||
def info(self, 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 = ()):
|
||||
self.logger.debug(self.addContext(msg, replace_tuple))
|
||||
|
||||
@@ -53,7 +56,8 @@ class CPLog(object):
|
||||
if not Env.get('dev'):
|
||||
|
||||
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
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from migrate.changeset.schema import create_column
|
||||
from sqlalchemy.schema import MetaData, Column, Table, Index
|
||||
from sqlalchemy.types import Integer
|
||||
|
||||
meta = MetaData()
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta.bind = migrate_engine
|
||||
|
||||
# Change release, add last_edit and index
|
||||
last_edit_column = Column('last_edit', Integer)
|
||||
release = Table('release', meta, last_edit_column)
|
||||
|
||||
create_column(last_edit_column, release)
|
||||
Index('ix_release_last_edit', release.c.last_edit).create()
|
||||
|
||||
# Change movie last_edit
|
||||
last_edit_column = Column('last_edit', Integer)
|
||||
movie = Table('movie', meta, last_edit_column)
|
||||
Index('ix_movie_last_edit', movie.c.last_edit).create()
|
||||
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
pass
|
||||
@@ -0,0 +1,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!'
|
||||
|
||||
listen_to = [
|
||||
'movie.downloaded', 'movie.snatched',
|
||||
'renamer.after', 'movie.snatched',
|
||||
'updater.available', 'updater.updated',
|
||||
]
|
||||
dont_listen_to = []
|
||||
@@ -30,10 +30,10 @@ class Notification(Plugin):
|
||||
addEvent(listener, self.createNotifyHandler(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':
|
||||
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
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'boxcar',
|
||||
'options': [
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ from couchpotato.api import addApiView, addNonBlockApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
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.notifications.base import Notification
|
||||
from couchpotato.core.settings.model import Notification as Notif
|
||||
@@ -22,7 +22,7 @@ class CoreNotifier(Notification):
|
||||
listeners = []
|
||||
|
||||
listen_to = [
|
||||
'movie.downloaded', 'movie.snatched',
|
||||
'renamer.after', 'movie.snatched',
|
||||
'updater.available', 'updater.updated',
|
||||
]
|
||||
|
||||
@@ -67,7 +67,7 @@ class CoreNotifier(Notification):
|
||||
|
||||
ids = None
|
||||
if getParam('ids'):
|
||||
ids = [x.strip() for x in getParam('ids').split(',')]
|
||||
ids = splitString(getParam('ids'))
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -79,7 +79,6 @@ class CoreNotifier(Notification):
|
||||
q.update({Notif.read: True})
|
||||
|
||||
db.commit()
|
||||
#db.close()
|
||||
|
||||
return jsonified({
|
||||
'success': True
|
||||
@@ -93,7 +92,7 @@ class CoreNotifier(Notification):
|
||||
q = db.query(Notif)
|
||||
|
||||
if limit_offset:
|
||||
splt = [x.strip() for x in limit_offset.split(',')]
|
||||
splt = splitString(limit_offset)
|
||||
limit = splt[0]
|
||||
offset = 0 if len(splt) is 1 else splt[1]
|
||||
q = q.limit(limit).offset(offset)
|
||||
@@ -107,7 +106,6 @@ class CoreNotifier(Notification):
|
||||
ndict['type'] = 'notification'
|
||||
notifications.append(ndict)
|
||||
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'empty': len(notifications) == 0,
|
||||
@@ -133,7 +131,6 @@ class CoreNotifier(Notification):
|
||||
|
||||
self.frontend(type = listener, data = data)
|
||||
|
||||
#db.close()
|
||||
return True
|
||||
|
||||
def frontend(self, type = 'notification', data = {}, message = None):
|
||||
|
||||
@@ -178,11 +178,14 @@ var NotificationBase = new Class({
|
||||
},
|
||||
|
||||
addTestButton: function(fieldset, plugin_name){
|
||||
var self = this;
|
||||
var self = this,
|
||||
button_name = self.testButtonName(fieldset);
|
||||
|
||||
if(button_name.contains('Notifications')) return;
|
||||
|
||||
new Element('.ctrlHolder.test_button').adopt(
|
||||
new Element('a.button', {
|
||||
'text': self.testButtonName(fieldset),
|
||||
'text': button_name,
|
||||
'events': {
|
||||
'click': function(){
|
||||
var button = fieldset.getElement('.test_button .button');
|
||||
@@ -191,7 +194,7 @@ var NotificationBase = new Class({
|
||||
Api.request('notify.'+plugin_name+'.test', {
|
||||
'onComplete': function(json){
|
||||
|
||||
button.set('text', self.testButtonName(fieldset));
|
||||
button.set('text', button_name);
|
||||
|
||||
if(json.success){
|
||||
var message = new Element('span.success', {
|
||||
|
||||
56
couchpotato/core/notifications/email/__init__.py
Normal file
56
couchpotato/core/notifications/email/__init__.py
Normal file
@@ -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.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
55
couchpotato/core/notifications/email/main.py
Normal file
55
couchpotato/core/notifications/email/main.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from email.mime.text import MIMEText
|
||||
import smtplib
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Email(Notification):
|
||||
|
||||
def notify(self, message = '', data = {}, 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, splitString(to_address), message.as_string())
|
||||
|
||||
# Close the SMTP connection
|
||||
mailserver.quit()
|
||||
|
||||
log.info('Email notification sent')
|
||||
|
||||
return True
|
||||
except:
|
||||
log.error('E-mail failed: %s', traceback.format_exc())
|
||||
return False
|
||||
|
||||
return False
|
||||
@@ -8,6 +8,7 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'growl',
|
||||
'description': 'Version 1.4+',
|
||||
'options': [
|
||||
|
||||
@@ -37,8 +37,11 @@ class Growl(Notification):
|
||||
)
|
||||
self.growl.register()
|
||||
self.registered = True
|
||||
except:
|
||||
log.error('Failed register of growl: %s', traceback.format_exc())
|
||||
except Exception, e:
|
||||
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):
|
||||
if self.isDisabled(): return
|
||||
|
||||
@@ -8,6 +8,7 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'nmj',
|
||||
'label': 'NMJ',
|
||||
'options': [
|
||||
|
||||
@@ -69,7 +69,7 @@ class NMJ(Notification):
|
||||
'mount': mount,
|
||||
})
|
||||
|
||||
def addToLibrary(self, group = {}):
|
||||
def addToLibrary(self, message = None, group = {}):
|
||||
if self.isDisabled(): return
|
||||
|
||||
host = self.conf('host')
|
||||
@@ -114,8 +114,8 @@ class NMJ(Notification):
|
||||
|
||||
def failed(self):
|
||||
return jsonified({'success': False})
|
||||
|
||||
|
||||
def test(self):
|
||||
return jsonified({'success': self.addToLibrary()})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'notifo',
|
||||
'description': 'Keep in mind that Notifo service will end soon.',
|
||||
'options': [
|
||||
|
||||
@@ -8,6 +8,7 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'notifymyandroid',
|
||||
'label': 'Notify My Android',
|
||||
'options': [
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import pynma
|
||||
@@ -11,7 +12,7 @@ class NotifyMyAndroid(Notification):
|
||||
if self.isDisabled(): return
|
||||
|
||||
nma = pynma.PyNMA()
|
||||
keys = [x.strip() for x in self.conf('api_key').split(',')]
|
||||
keys = splitString(self.conf('api_key'))
|
||||
nma.addkey(keys)
|
||||
nma.developerkey(self.conf('dev_key'))
|
||||
|
||||
|
||||
@@ -8,8 +8,9 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'notifymywp',
|
||||
'label': 'Notify My Windows Phone',
|
||||
'label': 'Windows Phone',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from pynmwp import PyNMWP
|
||||
@@ -10,7 +11,7 @@ class NotifyMyWP(Notification):
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
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'))
|
||||
|
||||
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': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'plex',
|
||||
'options': [
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ class Plex(Notification):
|
||||
super(Plex, self).__init__()
|
||||
addEvent('renamer.after', self.addToLibrary)
|
||||
|
||||
def addToLibrary(self, group = {}):
|
||||
def addToLibrary(self, message = None, group = {}):
|
||||
if self.isDisabled(): return
|
||||
|
||||
log.info('Sending notification to Plex')
|
||||
|
||||
@@ -8,6 +8,7 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'prowl',
|
||||
'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.notifications.base import Notification
|
||||
from httplib import HTTPSConnection
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Prowl(Notification):
|
||||
|
||||
urls = {
|
||||
'api': 'https://api.prowlapp.com/publicapi/add'
|
||||
}
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
http_handler = HTTPSConnection('api.prowlapp.com')
|
||||
|
||||
data = {
|
||||
'apikey': self.conf('api_key'),
|
||||
'application': self.default_title,
|
||||
'description': toUnicode(message),
|
||||
'priority': self.conf('priority'),
|
||||
}
|
||||
headers = {
|
||||
'Content-type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
|
||||
http_handler.request('POST',
|
||||
'/publicapi/add',
|
||||
headers = {'Content-type': 'application/x-www-form-urlencoded'},
|
||||
body = tryUrlencode(data)
|
||||
)
|
||||
response = http_handler.getresponse()
|
||||
request_status = response.status
|
||||
|
||||
if request_status == 200:
|
||||
try:
|
||||
self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False)
|
||||
log.info('Prowl notifications sent.')
|
||||
return True
|
||||
elif request_status == 401:
|
||||
log.error('Prowl auth failed: %s', response.reason)
|
||||
return False
|
||||
else:
|
||||
log.error('Prowl notification failed.')
|
||||
return False
|
||||
except:
|
||||
log.error('Prowl failed: %s', traceback.format_exc())
|
||||
|
||||
return False
|
||||
|
||||
48
couchpotato/core/notifications/pushalot/__init__.py
Normal file
48
couchpotato/core/notifications/pushalot/__init__.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from .main import Pushalot
|
||||
|
||||
def start():
|
||||
return Pushalot()
|
||||
|
||||
config = [{
|
||||
'name': 'pushalot',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'pushalot',
|
||||
'description': 'for Windows Phone and Windows 8',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'auth_token',
|
||||
'label': 'Auth Token',
|
||||
},
|
||||
{
|
||||
'name': 'silent',
|
||||
'label': 'Silent',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'description': 'Don\'t send Toast notifications. Only update Live Tile',
|
||||
},
|
||||
{
|
||||
'name': 'important',
|
||||
'label': 'High Priority',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'description': 'Send message with High priority.',
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also send message when movie is snatched.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
37
couchpotato/core/notifications/pushalot/main.py
Normal file
37
couchpotato/core/notifications/pushalot/main.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
class Pushalot(Notification):
|
||||
|
||||
urls = {
|
||||
'api': 'https://pushalot.com/api/sendmessage'
|
||||
}
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
data = {
|
||||
'AuthorizationToken': self.conf('auth_token'),
|
||||
'Title': self.default_title,
|
||||
'Body': toUnicode(message),
|
||||
'LinkTitle': toUnicode("CouchPotato"),
|
||||
'link': toUnicode("https://couchpota.to/"),
|
||||
'IsImportant': self.conf('important'),
|
||||
'IsSilent': self.conf('silent'),
|
||||
}
|
||||
|
||||
headers = {
|
||||
'Content-type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
|
||||
try:
|
||||
self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False)
|
||||
return True
|
||||
except:
|
||||
log.error('PushAlot failed: %s', traceback.format_exc())
|
||||
|
||||
return False
|
||||
@@ -8,6 +8,7 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'pushover',
|
||||
'options': [
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'synoindex',
|
||||
'description': 'Automaticly adds index to Synology Media Server.',
|
||||
'options': [
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -8,14 +10,17 @@ log = CPLog(__name__)
|
||||
|
||||
class Synoindex(Notification):
|
||||
|
||||
index_path = '/usr/syno/bin/synoindex'
|
||||
|
||||
def __init__(self):
|
||||
super(Synoindex, self).__init__()
|
||||
addEvent('renamer.after', self.addToLibrary)
|
||||
|
||||
def addToLibrary(self, group = {}):
|
||||
def addToLibrary(self, message = None, group = {}):
|
||||
if self.isDisabled(): return
|
||||
|
||||
command = ['/usr/syno/bin/synoindex', '-A', group.get('destination_dir')]
|
||||
log.info(u'Executing synoindex command: %s ', command)
|
||||
command = [self.index_path, '-A', group.get('destination_dir')]
|
||||
log.info('Executing synoindex command: %s ', command)
|
||||
try:
|
||||
p = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
|
||||
out = p.communicate()
|
||||
@@ -26,3 +31,6 @@ class Synoindex(Notification):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def test(self):
|
||||
return jsonified({'success': os.path.isfile(self.index_path)})
|
||||
|
||||
33
couchpotato/core/notifications/toasty/__init__.py
Normal file
33
couchpotato/core/notifications/toasty/__init__.py
Normal file
@@ -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.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
30
couchpotato/core/notifications/toasty/main.py
Normal file
30
couchpotato/core/notifications/toasty/main.py
Normal file
@@ -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': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'twitter',
|
||||
'options': [
|
||||
{
|
||||
|
||||
@@ -38,22 +38,33 @@ class Twitter(Notification):
|
||||
|
||||
direct_message = self.conf('direct_message')
|
||||
direct_message_users = self.conf('screen_name')
|
||||
|
||||
|
||||
mention = self.conf('mention')
|
||||
mention_tag = None
|
||||
if mention:
|
||||
if direct_message:
|
||||
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:
|
||||
message = '%s @%s' % (message, mention.lstrip('@'))
|
||||
mention_tag = '@%s' % mention.lstrip('@')
|
||||
message = '%s %s' % (message, mention_tag)
|
||||
|
||||
try:
|
||||
if direct_message:
|
||||
for user in direct_message_users.split():
|
||||
api.PostDirectMessage(user, '[%s] %s' % (self.default_title, message))
|
||||
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:
|
||||
log.error('Error sending tweet: %s', e)
|
||||
return False
|
||||
|
||||
@@ -8,8 +8,10 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'list': 'notification_providers',
|
||||
'name': 'xbmc',
|
||||
'label': 'XBMC',
|
||||
'description': 'v11 (Eden) and v12 (Frodo)',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
|
||||
186
couchpotato/core/notifications/xbmc/main.py
Normal file → Executable file
186
couchpotato/core/notifications/xbmc/main.py
Normal file → Executable file
@@ -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.notifications.base import Notification
|
||||
from flask.helpers import json
|
||||
import base64
|
||||
import traceback
|
||||
import urllib
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
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):
|
||||
if self.isDisabled(): return
|
||||
|
||||
hosts = [x.strip() for x in self.conf('host').split(",")]
|
||||
hosts = splitString(self.conf('host'))
|
||||
|
||||
successful = 0
|
||||
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'):
|
||||
headers = {
|
||||
'Authorization': "Basic %s" % base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password')))[:-1]
|
||||
}
|
||||
base64string = base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password'))).replace('\n', '')
|
||||
headers['Authorization'] = 'Basic %s' % base64string
|
||||
|
||||
try:
|
||||
self.urlopen(url, headers = headers, show_error = False)
|
||||
except:
|
||||
log.error("Couldn't sent command to XBMC")
|
||||
return False
|
||||
log.debug('Sending non-JSON-type request to %s: %s', (host, data))
|
||||
|
||||
# response wil either be 'OK':
|
||||
# <html>
|
||||
# <li>OK
|
||||
# </html>
|
||||
#
|
||||
# or 'Error':
|
||||
# <html>
|
||||
# <li>Error:<message>
|
||||
# </html>
|
||||
#
|
||||
response = self.urlopen(server, headers = headers)
|
||||
|
||||
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 = [{
|
||||
'name': 'automation',
|
||||
'order': 30,
|
||||
'order': 101,
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'automation',
|
||||
'name': 'automation',
|
||||
'label': 'Automation',
|
||||
'description': 'Minimal movie requirements',
|
||||
'label': 'Minimal movie requirements',
|
||||
'options': [
|
||||
{
|
||||
'name': 'year',
|
||||
|
||||
@@ -18,9 +18,17 @@ class Automation(Plugin):
|
||||
def addMovies(self):
|
||||
|
||||
movies = fireEvent('automation.get_movies', merge = True)
|
||||
movie_ids = []
|
||||
|
||||
for imdb_id in movies:
|
||||
prop_name = 'automation.added.%s' % imdb_id
|
||||
added = Env.prop(prop_name, default = False)
|
||||
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)
|
||||
|
||||
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 couchpotato import addView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, simplifyString, ss
|
||||
from couchpotato.core.helpers.variable import getExt
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString
|
||||
from couchpotato.core.helpers.variable import getExt, md5
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from flask.templating import render_template_string
|
||||
@@ -34,7 +34,7 @@ class Plugin(object):
|
||||
http_failed_disabled = {}
|
||||
|
||||
def registerPlugin(self):
|
||||
addEvent('app.shutdown', self.doShutdown)
|
||||
addEvent('app.do_shutdown', self.doShutdown)
|
||||
addEvent('plugin.running', self.isRunning)
|
||||
|
||||
def conf(self, attr, value = None, default = None):
|
||||
@@ -64,7 +64,7 @@ class Plugin(object):
|
||||
for f in glob.glob(os.path.join(self.plugin_path, 'static', '*')):
|
||||
ext = getExt(f)
|
||||
if ext in ['js', 'css']:
|
||||
fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f))
|
||||
fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f)
|
||||
|
||||
def showStatic(self, filename):
|
||||
d = os.path.join(self.plugin_path, 'static')
|
||||
@@ -78,7 +78,7 @@ class Plugin(object):
|
||||
self.makeDir(os.path.dirname(path))
|
||||
|
||||
try:
|
||||
f = open(path, 'w' if not binary else 'wb')
|
||||
f = open(path, 'w+' if not binary else 'w+b')
|
||||
f.write(content)
|
||||
f.close()
|
||||
os.chmod(path, Env.getPermission('file'))
|
||||
@@ -98,6 +98,7 @@ class Plugin(object):
|
||||
|
||||
# http request
|
||||
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 params: params = {}
|
||||
@@ -113,8 +114,11 @@ class Plugin(object):
|
||||
# Don't try for failed requests
|
||||
if self.http_failed_disabled.get(host, 0) > 0:
|
||||
if self.http_failed_disabled[host] > (time.time() - 900):
|
||||
log.info('Disabled calls to %s for 15 minutes because so many failed requests.', host)
|
||||
raise Exception
|
||||
log.info2('Disabled calls to %s for 15 minutes because so many failed requests.', host)
|
||||
if not show_error:
|
||||
raise
|
||||
else:
|
||||
return ''
|
||||
else:
|
||||
del self.http_failed_request[host]
|
||||
del self.http_failed_disabled[host]
|
||||
@@ -123,11 +127,14 @@ class Plugin(object):
|
||||
try:
|
||||
|
||||
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)
|
||||
|
||||
cookies = cookielib.CookieJar()
|
||||
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler)
|
||||
if opener:
|
||||
opener.add_handler(MultipartPostHandler())
|
||||
else:
|
||||
cookies = cookielib.CookieJar()
|
||||
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), MultipartPostHandler)
|
||||
|
||||
response = opener.open(request, timeout = timeout)
|
||||
else:
|
||||
@@ -218,7 +225,7 @@ class Plugin(object):
|
||||
|
||||
|
||||
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)
|
||||
if cache:
|
||||
if not Env.get('dev'): log.debug('Getting cache %s', cache_key)
|
||||
@@ -233,18 +240,36 @@ class Plugin(object):
|
||||
del kwargs['cache_timeout']
|
||||
|
||||
data = self.urlopen(url, **kwargs)
|
||||
|
||||
if data:
|
||||
self.setCache(cache_key, data, timeout = cache_timeout)
|
||||
return data
|
||||
except:
|
||||
pass
|
||||
if not kwargs.get('show_error', True):
|
||||
raise
|
||||
|
||||
return ''
|
||||
|
||||
def setCache(self, cache_key, value, timeout = 300):
|
||||
log.debug('Setting cache %s', cache_key)
|
||||
Env.get('cache').set(cache_key, value, timeout)
|
||||
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):
|
||||
return not self.isEnabled()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.helpers.request import getParam, jsonified
|
||||
from couchpotato.core.helpers.variable import getUserDir
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
import ctypes
|
||||
import os
|
||||
@@ -14,7 +15,7 @@ if os.name == 'nt':
|
||||
raise ImportError("Missing the win32file module, which is a part of the prerequisite \
|
||||
pywin32 package. You can get it from http://sourceforge.net/projects/pywin32/files/pywin32/");
|
||||
else:
|
||||
import win32file
|
||||
import win32file #@UnresolvedImport
|
||||
|
||||
class FileBrowser(Plugin):
|
||||
|
||||
@@ -27,6 +28,8 @@ class FileBrowser(Plugin):
|
||||
},
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'is_root': bool, //is top most folder
|
||||
'parent': string, //parent folder of requested path
|
||||
'home': string, //user home folder
|
||||
'empty': bool, //directory is empty
|
||||
'dirs': array, //directory names
|
||||
}"""}
|
||||
@@ -63,15 +66,28 @@ class FileBrowser(Plugin):
|
||||
def view(self):
|
||||
|
||||
path = getParam('path', '/')
|
||||
home = getUserDir()
|
||||
|
||||
if not path:
|
||||
path = home
|
||||
|
||||
try:
|
||||
dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True))
|
||||
except:
|
||||
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({
|
||||
'is_root': path == '/' or not path,
|
||||
'is_root': path == '/',
|
||||
'empty': len(dirs) == 0,
|
||||
'parent': parent,
|
||||
'home': home + os.path.sep,
|
||||
'platform': os.name,
|
||||
'dirs': dirs,
|
||||
})
|
||||
|
||||
@@ -82,7 +98,7 @@ class FileBrowser(Plugin):
|
||||
|
||||
def has_hidden_attribute(self, filepath):
|
||||
try:
|
||||
attrs = ctypes.windll.kernel32.GetFileAttributesW(unicode(filepath))
|
||||
attrs = ctypes.windll.kernel32.GetFileAttributesW(unicode(filepath)) #@UndefinedVariable
|
||||
assert attrs != -1
|
||||
result = bool(attrs & 2)
|
||||
except (AttributeError, AssertionError):
|
||||
|
||||
6
couchpotato/core/plugins/dashboard/__init__.py
Normal file
6
couchpotato/core/plugins/dashboard/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .main import Dashboard
|
||||
|
||||
def start():
|
||||
return Dashboard()
|
||||
|
||||
config = []
|
||||
134
couchpotato/core/plugins/dashboard/main.py
Normal file
134
couchpotato/core/plugins/dashboard/main.py
Normal file
@@ -0,0 +1,134 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.request import jsonified, getParams
|
||||
from couchpotato.core.helpers.variable import splitString, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Movie
|
||||
from sqlalchemy.orm import joinedload_all
|
||||
import random
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Dashboard(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
addApiView('dashboard.suggestions', self.suggestView)
|
||||
addApiView('dashboard.soon', self.getSoonView)
|
||||
|
||||
def newSuggestions(self):
|
||||
|
||||
movies = fireEvent('movie.list', status = ['active', 'done'], limit_offset = (20, 0), single = True)
|
||||
movie_identifiers = [m['library']['identifier'] for m in movies[1]]
|
||||
|
||||
ignored_movies = fireEvent('movie.list', status = ['ignored', 'deleted'], limit_offset = (100, 0), single = True)
|
||||
ignored_identifiers = [m['library']['identifier'] for m in ignored_movies[1]]
|
||||
|
||||
suggestions = fireEvent('movie.suggest', movies = movie_identifiers, ignore = ignored_identifiers, single = True)
|
||||
suggest_status = fireEvent('status.get', 'suggest', single = True)
|
||||
|
||||
for suggestion in suggestions:
|
||||
fireEvent('movie.add', params = {'identifier': suggestion}, force_readd = False, search_after = False, status_id = suggest_status.get('id'))
|
||||
|
||||
def suggestView(self):
|
||||
|
||||
db = get_session()
|
||||
|
||||
movies = db.query(Movie).limit(20).all()
|
||||
identifiers = [m.library.identifier for m in movies]
|
||||
|
||||
suggestions = fireEvent('movie.suggest', movies = identifiers, single = True)
|
||||
|
||||
return jsonified({
|
||||
'result': True,
|
||||
'suggestions': suggestions
|
||||
})
|
||||
|
||||
def getSoonView(self):
|
||||
|
||||
params = getParams()
|
||||
db = get_session()
|
||||
now = time.time()
|
||||
|
||||
# Get profiles first, determine pre or post theater
|
||||
profiles = fireEvent('profile.all', single = True)
|
||||
qualities = fireEvent('quality.all', single = True)
|
||||
pre_releases = fireEvent('quality.pre_releases', single = True)
|
||||
|
||||
id_pre = {}
|
||||
for quality in qualities:
|
||||
id_pre[quality.get('id')] = quality.get('identifier') in pre_releases
|
||||
|
||||
# See what the profile contain and cache it
|
||||
profile_pre = {}
|
||||
for profile in profiles:
|
||||
contains = {}
|
||||
for profile_type in profile.get('types', []):
|
||||
contains['theater' if id_pre.get(profile_type.get('quality_id')) else 'dvd'] = True
|
||||
|
||||
profile_pre[profile.get('id')] = contains
|
||||
|
||||
# Get all active movies
|
||||
active_status = fireEvent('status.get', 'active', single = True)
|
||||
subq = db.query(Movie).filter(Movie.status_id == active_status.get('id')).subquery()
|
||||
|
||||
q = db.query(Movie).join((subq, subq.c.id == Movie.id)) \
|
||||
.options(joinedload_all('releases')) \
|
||||
.options(joinedload_all('profile.types')) \
|
||||
.options(joinedload_all('library.titles')) \
|
||||
.options(joinedload_all('library.files')) \
|
||||
.options(joinedload_all('status')) \
|
||||
.options(joinedload_all('files'))
|
||||
|
||||
# Add limit
|
||||
limit_offset = params.get('limit_offset')
|
||||
limit = 12
|
||||
if limit_offset:
|
||||
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
|
||||
limit = tryInt(splt[0])
|
||||
|
||||
all_movies = q.all()
|
||||
|
||||
if params.get('random', False):
|
||||
random.shuffle(all_movies)
|
||||
|
||||
movies = []
|
||||
for movie in all_movies:
|
||||
pp = profile_pre.get(movie.profile.id)
|
||||
eta = movie.library.info.get('release_date', {}) or {}
|
||||
coming_soon = False
|
||||
|
||||
# Theater quality
|
||||
if pp.get('theater') and fireEvent('searcher.could_be_released', True, eta, single = True):
|
||||
coming_soon = True
|
||||
if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, single = True):
|
||||
coming_soon = True
|
||||
|
||||
|
||||
if coming_soon:
|
||||
temp = movie.to_dict({
|
||||
'profile': {'types': {}},
|
||||
'releases': {'files':{}, 'info': {}},
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {},
|
||||
})
|
||||
|
||||
# Don't list older movies
|
||||
if ((not params.get('late') and (not eta.get('dvd') or (eta.get('dvd') and eta.get('dvd') > (now - 2419200)))) or \
|
||||
(params.get('late') and eta.get('dvd') and eta.get('dvd') < (now - 2419200))):
|
||||
movies.append(temp)
|
||||
|
||||
if len(movies) >= limit:
|
||||
break
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'movies': movies,
|
||||
})
|
||||
|
||||
getLateView = getSoonView
|
||||
@@ -2,12 +2,15 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.helpers.variable import md5, getExt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.plugins.scanner.main import Scanner
|
||||
from couchpotato.core.settings.model import FileType, File
|
||||
from couchpotato.environment import Env
|
||||
import os.path
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -28,6 +31,54 @@ class FileManager(Plugin):
|
||||
'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 or 'minified' in filename: continue
|
||||
file_path = os.path.join(root, filename)
|
||||
f = db.query(File).filter(File.path == toUnicode(file_path)).first()
|
||||
if not f:
|
||||
os.remove(file_path)
|
||||
except:
|
||||
log.error('Failed removing unused file: %s', traceback.format_exc())
|
||||
|
||||
def showCacheFile(self, filename = ''):
|
||||
|
||||
cache_dir = Env.get('cache_dir')
|
||||
@@ -89,7 +140,6 @@ class FileManager(Plugin):
|
||||
db.commit()
|
||||
|
||||
type_dict = ft.to_dict()
|
||||
#db.close()
|
||||
return type_dict
|
||||
|
||||
def getTypes(self):
|
||||
@@ -102,5 +152,10 @@ class FileManager(Plugin):
|
||||
for type_object in results:
|
||||
types.append(type_object.to_dict())
|
||||
|
||||
#db.close()
|
||||
return types
|
||||
|
||||
def getTypesView(self):
|
||||
|
||||
return jsonified({
|
||||
'types': self.getTypes()
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ var File = new Class({
|
||||
var self = this;
|
||||
|
||||
if(!file){
|
||||
self.empty = true;
|
||||
self.el = new Element('div');
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||
from couchpotato.core.helpers.variable import mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Library, LibraryTitle, File
|
||||
@@ -37,7 +38,7 @@ class LibraryPlugin(Plugin):
|
||||
|
||||
title = LibraryTitle(
|
||||
title = toUnicode(attrs.get('title')),
|
||||
simple_title = self.simplifyTitle(attrs.get('title'))
|
||||
simple_title = self.simplifyTitle(attrs.get('title')),
|
||||
)
|
||||
|
||||
l.titles.append(title)
|
||||
@@ -52,7 +53,6 @@ class LibraryPlugin(Plugin):
|
||||
|
||||
library_dict = l.to_dict(self.default_dict)
|
||||
|
||||
#db.close()
|
||||
return library_dict
|
||||
|
||||
def update(self, identifier, default_title = '', force = False):
|
||||
@@ -96,6 +96,7 @@ class LibraryPlugin(Plugin):
|
||||
|
||||
titles = info.get('titles', [])
|
||||
log.debug('Adding titles: %s', titles)
|
||||
counter = 0
|
||||
for title in titles:
|
||||
if not title:
|
||||
continue
|
||||
@@ -103,26 +104,29 @@ class LibraryPlugin(Plugin):
|
||||
t = LibraryTitle(
|
||||
title = title,
|
||||
simple_title = self.simplifyTitle(title),
|
||||
default = title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title)
|
||||
default = (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title)
|
||||
)
|
||||
library.titles.append(t)
|
||||
counter += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
# Files
|
||||
images = info.get('images', [])
|
||||
for type in images:
|
||||
for image in images[type]:
|
||||
if not isinstance(image, str):
|
||||
for image_type in ['poster']:
|
||||
for image in images.get(image_type, []):
|
||||
if not isinstance(image, (str, unicode)):
|
||||
continue
|
||||
|
||||
file_path = fireEvent('file.download', url = image, single = True)
|
||||
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:
|
||||
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
|
||||
library.files.append(file_obj)
|
||||
db.commit()
|
||||
|
||||
break
|
||||
except:
|
||||
log.debug('Failed to attach to library: %s', traceback.format_exc())
|
||||
|
||||
@@ -136,26 +140,23 @@ class LibraryPlugin(Plugin):
|
||||
library = db.query(Library).filter_by(identifier = identifier).first()
|
||||
|
||||
if not library.info:
|
||||
library_dict = self.update(identifier)
|
||||
dates = library_dict.get('info', {}).get('release_dates')
|
||||
library_dict = self.update(identifier, force = True)
|
||||
dates = library_dict.get('info', {}).get('release_date')
|
||||
else:
|
||||
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)
|
||||
library.info['release_date'] = dates
|
||||
library.info = library.info
|
||||
library.info = mergeDicts(library.info, {'release_date': dates })
|
||||
db.commit()
|
||||
|
||||
dates = library.info.get('release_date', {})
|
||||
#db.close()
|
||||
|
||||
return dates
|
||||
|
||||
|
||||
def simplifyTitle(self, title):
|
||||
|
||||
title = toUnicode(title)
|
||||
|
||||
nr_prefix = '' if title[0] in ascii_letters else '#'
|
||||
title = simplifyString(title)
|
||||
|
||||
|
||||
@@ -2,22 +2,35 @@ from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import splitString, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
import ctypes
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
class Manage(Plugin):
|
||||
|
||||
in_progress = False
|
||||
|
||||
def __init__(self):
|
||||
|
||||
fireEvent('scheduler.interval', identifier = 'manage.update_library', handle = self.updateLibrary, hours = 2)
|
||||
|
||||
addEvent('manage.update', self.updateLibrary)
|
||||
addEvent('manage.diskspace', self.getDiskSpace)
|
||||
|
||||
# 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 = {
|
||||
'desc': 'Update the library by scanning for new movies',
|
||||
'params': {
|
||||
@@ -25,11 +38,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'):
|
||||
def updateLibrary():
|
||||
self.updateLibrary(full = False)
|
||||
addEvent('app.load', updateLibrary)
|
||||
|
||||
def getProgress(self):
|
||||
return jsonified({
|
||||
'progress': self.in_progress
|
||||
})
|
||||
|
||||
def updateLibraryView(self):
|
||||
|
||||
full = getParam('full', default = 1)
|
||||
@@ -43,49 +68,181 @@ class Manage(Plugin):
|
||||
def updateLibrary(self, full = True):
|
||||
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
|
||||
|
||||
directories = self.directories()
|
||||
added_identifiers = []
|
||||
self.in_progress = {}
|
||||
fireEvent('notify.frontend', type = 'manage.updating', data = True)
|
||||
|
||||
for directory in directories:
|
||||
try:
|
||||
|
||||
if not os.path.isdir(directory):
|
||||
if len(directory) > 0:
|
||||
log.error('Directory doesn\'t exist: %s', directory)
|
||||
continue
|
||||
directories = self.directories()
|
||||
added_identifiers = []
|
||||
|
||||
log.info('Updating manage library: %s', directory)
|
||||
identifiers = fireEvent('scanner.folder', folder = directory, newer_than = last_update if not full else 0, single = True)
|
||||
if identifiers:
|
||||
added_identifiers.extend(identifiers)
|
||||
# Add some progress
|
||||
self.in_progress = {}
|
||||
for directory in directories:
|
||||
self.in_progress[os.path.normpath(directory)] = {
|
||||
'total': None,
|
||||
'to_go': None,
|
||||
}
|
||||
|
||||
# Break if CP wants to shut down
|
||||
if self.shuttingDown():
|
||||
for directory in directories:
|
||||
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
|
||||
|
||||
# If cleanup option is enabled, remove offline files from database
|
||||
if self.conf('cleanup') and full and not self.shuttingDown():
|
||||
time.sleep(1)
|
||||
|
||||
# Get movies with done status
|
||||
total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True)
|
||||
fireEvent('notify.frontend', type = 'manage.updating', data = False)
|
||||
self.in_progress = False
|
||||
|
||||
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', []):
|
||||
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
|
||||
def createAddToLibrary(self, folder, added_identifiers = []):
|
||||
def addToLibrary(group, total_found, to_go):
|
||||
if self.in_progress[folder]['total'] is None:
|
||||
self.in_progress[folder] = {
|
||||
'total': total_found,
|
||||
'to_go': total_found,
|
||||
}
|
||||
|
||||
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):
|
||||
try:
|
||||
return [x.strip() for x in self.conf('library', default = '').split('::')]
|
||||
if self.conf('library', default = '').strip():
|
||||
return splitString(self.conf('library', default = ''), '::')
|
||||
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)
|
||||
|
||||
def getDiskSpace(self):
|
||||
|
||||
free_space = {}
|
||||
for folder in self.directories():
|
||||
|
||||
size = None
|
||||
if os.path.isdir(folder):
|
||||
if os.name == 'nt':
|
||||
_, total, free = ctypes.c_ulonglong(), ctypes.c_ulonglong(), \
|
||||
ctypes.c_ulonglong()
|
||||
if sys.version_info >= (3,) or isinstance(folder, unicode):
|
||||
fun = ctypes.windll.kernel32.GetDiskFreeSpaceExW #@UndefinedVariable
|
||||
else:
|
||||
fun = ctypes.windll.kernel32.GetDiskFreeSpaceExA #@UndefinedVariable
|
||||
ret = fun(folder, ctypes.byref(_), ctypes.byref(total), ctypes.byref(free))
|
||||
if ret == 0:
|
||||
raise ctypes.WinError()
|
||||
used = total.value - free.value
|
||||
return [total.value, used, free.value]
|
||||
else:
|
||||
s = os.statvfs(folder)
|
||||
size = [s.f_blocks * s.f_frsize / (1024 * 1024), (s.f_bavail * s.f_frsize) / (1024 * 1024)]
|
||||
|
||||
free_space[folder] = size
|
||||
|
||||
return free_space
|
||||
|
||||
|
||||
@@ -3,14 +3,16 @@ from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||
from couchpotato.core.helpers.request import getParams, jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import getImdb
|
||||
from couchpotato.core.helpers.variable import getImdb, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
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, \
|
||||
Release
|
||||
from couchpotato.environment import Env
|
||||
from sqlalchemy.orm import joinedload_all
|
||||
from sqlalchemy.sql.expression import or_, asc, not_
|
||||
from sqlalchemy.sql.expression import or_, asc, not_, desc
|
||||
from string import ascii_lowercase
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -41,6 +43,7 @@ class MoviePlugin(Plugin):
|
||||
'desc': 'List movies in wanted list',
|
||||
'params': {
|
||||
'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'},
|
||||
'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'},
|
||||
'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'},
|
||||
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
|
||||
'search': {'desc': 'Search movie title'},
|
||||
@@ -94,6 +97,34 @@ class MoviePlugin(Plugin):
|
||||
addEvent('movie.list', self.list)
|
||||
addEvent('movie.restatus', self.restatus)
|
||||
|
||||
# Clean releases that didn't have activity in the last week
|
||||
addEvent('app.load', self.cleanReleases)
|
||||
fireEvent('schedule.interval', 'movie.clean_releases', self.cleanReleases, hours = 4)
|
||||
|
||||
def cleanReleases(self):
|
||||
|
||||
log.debug('Removing releases from dashboard')
|
||||
|
||||
now = time.time()
|
||||
week = 262080
|
||||
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
||||
|
||||
db = get_session()
|
||||
|
||||
# get movies last_edit more than a week ago
|
||||
movies = db.query(Movie) \
|
||||
.filter(Movie.status_id == done_status.get('id'), Movie.last_edit < (now - week)) \
|
||||
.all()
|
||||
|
||||
#
|
||||
for movie in movies:
|
||||
for rel in movie.releases:
|
||||
if rel.status_id in [available_status.get('id'), snatched_status.get('id')]:
|
||||
fireEvent('release.delete', id = rel.id, single = True)
|
||||
|
||||
def getView(self):
|
||||
|
||||
movie_id = getParam('id')
|
||||
@@ -107,29 +138,43 @@ class MoviePlugin(Plugin):
|
||||
def get(self, movie_id):
|
||||
|
||||
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
|
||||
if m:
|
||||
results = m.to_dict(self.default_dict)
|
||||
|
||||
#db.close()
|
||||
return results
|
||||
|
||||
def list(self, status = ['active'], limit_offset = None, starts_with = None, search = None):
|
||||
def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
|
||||
|
||||
db = get_session()
|
||||
|
||||
# Make a list from string
|
||||
if not isinstance(status, (list, tuple)):
|
||||
if status and not isinstance(status, (list, tuple)):
|
||||
status = [status]
|
||||
if release_status and not isinstance(release_status, (list, tuple)):
|
||||
release_status = [release_status]
|
||||
|
||||
q = db.query(Movie) \
|
||||
.join(Movie.library, Library.titles) \
|
||||
.outerjoin(Movie.releases, Movie.library, Library.titles) \
|
||||
.filter(LibraryTitle.default == True) \
|
||||
.filter(or_(*[Movie.status.has(identifier = s) for s in status])) \
|
||||
.group_by(Movie.id)
|
||||
|
||||
# Filter on movie status
|
||||
if status and len(status) > 0:
|
||||
q = q.filter(or_(*[Movie.status.has(identifier = s) for s in status]))
|
||||
|
||||
# Filter on release status
|
||||
if release_status and len(release_status) > 0:
|
||||
q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status]))
|
||||
|
||||
total_count = q.count()
|
||||
|
||||
filter_or = []
|
||||
@@ -149,7 +194,10 @@ class MoviePlugin(Plugin):
|
||||
if filter_or:
|
||||
q = q.filter(or_(*filter_or))
|
||||
|
||||
q = q.order_by(asc(LibraryTitle.simple_title))
|
||||
if order == 'release_order':
|
||||
q = q.order_by(desc(Release.last_edit))
|
||||
else:
|
||||
q = q.order_by(asc(LibraryTitle.simple_title))
|
||||
|
||||
q = q.subquery()
|
||||
q2 = db.query(Movie).join((q, q.c.id == Movie.id)) \
|
||||
@@ -161,7 +209,7 @@ class MoviePlugin(Plugin):
|
||||
.options(joinedload_all('files'))
|
||||
|
||||
if limit_offset:
|
||||
splt = [x.strip() for x in limit_offset.split(',')]
|
||||
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
|
||||
limit = splt[0]
|
||||
offset = 0 if len(splt) is 1 else splt[1]
|
||||
q2 = q2.limit(limit).offset(offset)
|
||||
@@ -180,7 +228,7 @@ class MoviePlugin(Plugin):
|
||||
#db.close()
|
||||
return (total_count, movies)
|
||||
|
||||
def availableChars(self, status = ['active']):
|
||||
def availableChars(self, status = None, release_status = None):
|
||||
|
||||
chars = ''
|
||||
|
||||
@@ -189,11 +237,20 @@ class MoviePlugin(Plugin):
|
||||
# Make a list from string
|
||||
if not isinstance(status, (list, tuple)):
|
||||
status = [status]
|
||||
if release_status and not isinstance(release_status, (list, tuple)):
|
||||
release_status = [release_status]
|
||||
|
||||
q = db.query(Movie) \
|
||||
.join(Movie.library, Library.titles, Movie.status) \
|
||||
.options(joinedload_all('library.titles')) \
|
||||
.filter(or_(*[Movie.status.has(identifier = s) for s in status]))
|
||||
.outerjoin(Movie.releases, Movie.library, Library.titles, Movie.status) \
|
||||
.options(joinedload_all('library.titles'))
|
||||
|
||||
# Filter on movie status
|
||||
if status and len(status) > 0:
|
||||
q = q.filter(or_(*[Movie.status.has(identifier = s) for s in status]))
|
||||
|
||||
# Filter on release status
|
||||
if release_status and len(release_status) > 0:
|
||||
q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status]))
|
||||
|
||||
results = q.all()
|
||||
|
||||
@@ -201,20 +258,29 @@ class MoviePlugin(Plugin):
|
||||
char = movie.library.titles[0].simple_title[0]
|
||||
char = char if char in ascii_lowercase else '#'
|
||||
if char not in chars:
|
||||
chars += char
|
||||
chars += str(char)
|
||||
|
||||
#db.close()
|
||||
return chars
|
||||
return ''.join(sorted(chars, key = str.lower))
|
||||
|
||||
def listView(self):
|
||||
|
||||
params = getParams()
|
||||
status = params.get('status', ['active'])
|
||||
status = splitString(params.get('status', None))
|
||||
release_status = splitString(params.get('release_status', None))
|
||||
limit_offset = params.get('limit_offset', None)
|
||||
starts_with = params.get('starts_with', None)
|
||||
search = params.get('search', None)
|
||||
order = params.get('order', None)
|
||||
|
||||
total_movies, movies = self.list(status = status, limit_offset = limit_offset, starts_with = starts_with, search = search)
|
||||
total_movies, movies = self.list(
|
||||
status = status,
|
||||
release_status = release_status,
|
||||
limit_offset = limit_offset,
|
||||
starts_with = starts_with,
|
||||
search = search,
|
||||
order = order
|
||||
)
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
@@ -226,8 +292,9 @@ class MoviePlugin(Plugin):
|
||||
def charView(self):
|
||||
|
||||
params = getParams()
|
||||
status = params.get('status', ['active'])
|
||||
chars = self.availableChars(status)
|
||||
status = splitString(params.get('status', None))
|
||||
release_status = splitString(params.get('release_status', None))
|
||||
chars = self.availableChars(status, release_status)
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
@@ -239,7 +306,7 @@ class MoviePlugin(Plugin):
|
||||
|
||||
db = get_session()
|
||||
|
||||
for id in getParam('id').split(','):
|
||||
for id in splitString(getParam('id')):
|
||||
movie = db.query(Movie).filter_by(id = id).first()
|
||||
|
||||
if movie:
|
||||
@@ -278,7 +345,7 @@ class MoviePlugin(Plugin):
|
||||
'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, status_id = None):
|
||||
|
||||
if not params.get('identifier'):
|
||||
msg = 'Can\'t add movie without imdb identifier.'
|
||||
@@ -287,9 +354,8 @@ class MoviePlugin(Plugin):
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
url = 'http://thetvdb.com/api/GetSeriesByRemoteID.php?imdbid=%s' % params.get('identifier')
|
||||
tvdb = self.getCache('thetvdb.%s' % params.get('identifier'), url = url, show_error = False)
|
||||
if tvdb and 'series' in tvdb.lower():
|
||||
is_movie = fireEvent('movie.is_movie', identifier = params.get('identifier'), single = True)
|
||||
if not is_movie:
|
||||
msg = 'Can\'t add movie, seems to be a TV show.'
|
||||
log.error(msg)
|
||||
fireEvent('notify.frontend', type = 'movie.is_tvshow', message = msg)
|
||||
@@ -298,11 +364,13 @@ class MoviePlugin(Plugin):
|
||||
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_active = fireEvent('status.add', 'active', single = True)
|
||||
status_snatched = fireEvent('status.add', 'snatched', single = True)
|
||||
snatched_status = fireEvent('status.add', 'snatched', single = True)
|
||||
ignored_status = fireEvent('status.add', 'ignored', single = True)
|
||||
downloaded_status = fireEvent('status.add', 'downloaded', single = True)
|
||||
|
||||
default_profile = fireEvent('profile.default', single = True)
|
||||
|
||||
@@ -314,7 +382,7 @@ class MoviePlugin(Plugin):
|
||||
m = Movie(
|
||||
library_id = library.get('id'),
|
||||
profile_id = params.get('profile_id', default_profile.get('id')),
|
||||
status_id = status_active.get('id'),
|
||||
status_id = status_id if status_id else status_active.get('id'),
|
||||
)
|
||||
db.add(m)
|
||||
db.commit()
|
||||
@@ -326,10 +394,14 @@ class MoviePlugin(Plugin):
|
||||
fireEventAsync('library.update', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
|
||||
search_after = False
|
||||
elif force_readd:
|
||||
|
||||
# Clean snatched history
|
||||
for release in m.releases:
|
||||
if release.status_id == status_snatched.get('id'):
|
||||
release.delete()
|
||||
if release.status_id in [downloaded_status.get('id'), snatched_status.get('id')]:
|
||||
if params.get('ignore_previous', False):
|
||||
release.status_id = ignored_status.get('id')
|
||||
else:
|
||||
fireEvent('release.delete', release.id, single = True)
|
||||
|
||||
m.profile_id = params.get('profile_id', default_profile.get('id'))
|
||||
else:
|
||||
@@ -337,7 +409,8 @@ class MoviePlugin(Plugin):
|
||||
added = False
|
||||
|
||||
if force_readd:
|
||||
m.status_id = status_active.get('id')
|
||||
m.status_id = status_id if status_id else status_active.get('id')
|
||||
m.last_edit = int(time.time())
|
||||
do_search = True
|
||||
|
||||
db.commit()
|
||||
@@ -381,7 +454,7 @@ class MoviePlugin(Plugin):
|
||||
|
||||
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:
|
||||
|
||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||
@@ -417,7 +490,7 @@ class MoviePlugin(Plugin):
|
||||
|
||||
params = getParams()
|
||||
|
||||
ids = [x.strip() for x in params.get('id').split(',')]
|
||||
ids = splitString(params.get('id'))
|
||||
for movie_id in ids:
|
||||
self.delete(movie_id, delete_from = params.get('delete_from', 'all'))
|
||||
|
||||
@@ -431,9 +504,11 @@ class MoviePlugin(Plugin):
|
||||
|
||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
||||
if movie:
|
||||
deleted = False
|
||||
if delete_from == 'all':
|
||||
db.delete(movie)
|
||||
db.commit()
|
||||
deleted = True
|
||||
else:
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
|
||||
@@ -441,7 +516,7 @@ class MoviePlugin(Plugin):
|
||||
total_deleted = 0
|
||||
new_movie_status = None
|
||||
for release in movie.releases:
|
||||
if delete_from == 'wanted':
|
||||
if delete_from in ['wanted', 'snatched']:
|
||||
if release.status_id != done_status.get('id'):
|
||||
db.delete(release)
|
||||
total_deleted += 1
|
||||
@@ -456,6 +531,7 @@ class MoviePlugin(Plugin):
|
||||
if total_releases == total_deleted:
|
||||
db.delete(movie)
|
||||
db.commit()
|
||||
deleted = True
|
||||
elif new_movie_status:
|
||||
new_status = fireEvent('status.get', new_movie_status, single = True)
|
||||
movie.profile_id = None
|
||||
@@ -464,6 +540,9 @@ class MoviePlugin(Plugin):
|
||||
else:
|
||||
fireEvent('movie.restatus', movie.id, single = True)
|
||||
|
||||
if deleted:
|
||||
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
|
||||
|
||||
#db.close()
|
||||
return True
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ var MovieList = new Class({
|
||||
options: {
|
||||
navigation: true,
|
||||
limit: 50,
|
||||
load_more: true,
|
||||
menu: [],
|
||||
add_new: false
|
||||
},
|
||||
@@ -12,37 +13,67 @@ var MovieList = new Class({
|
||||
movies: [],
|
||||
movies_added: {},
|
||||
letters: {},
|
||||
filter: {
|
||||
'startswith': null,
|
||||
'search': null
|
||||
},
|
||||
filter: null,
|
||||
|
||||
initialize: function(options){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.offset = 0;
|
||||
self.filter = self.options.filter || {
|
||||
'startswith': null,
|
||||
'search': null
|
||||
}
|
||||
|
||||
self.el = new Element('div.movies').adopt(
|
||||
self.title = self.options.title ? new Element('h2', {
|
||||
'text': self.options.title,
|
||||
'styles': {'display': 'none'}
|
||||
}) : null,
|
||||
self.description = self.options.description ? new Element('div.description', {
|
||||
'html': self.options.description,
|
||||
'styles': {'display': 'none'}
|
||||
}) : null,
|
||||
self.movie_list = new Element('div'),
|
||||
self.load_more = new Element('a.load_more', {
|
||||
self.load_more = self.options.load_more ? new Element('a.load_more', {
|
||||
'events': {
|
||||
'click': self.loadMore.bind(self)
|
||||
}
|
||||
})
|
||||
}) : null
|
||||
);
|
||||
|
||||
self.changeView(self.getSavedView() || self.options.view || 'details');
|
||||
|
||||
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){
|
||||
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.checkIfEmpty();
|
||||
}
|
||||
},
|
||||
|
||||
create: function(){
|
||||
@@ -52,22 +83,14 @@ var MovieList = new Class({
|
||||
if(self.options.navigation)
|
||||
self.createNavigation();
|
||||
|
||||
self.movie_list.addEvents({
|
||||
'mouseenter:relay(.movie)': function(e, el){
|
||||
el.addClass('hover');
|
||||
},
|
||||
'mouseleave:relay(.movie)': function(e, el){
|
||||
el.removeClass('hover');
|
||||
}
|
||||
});
|
||||
|
||||
self.scrollspy = new ScrollSpy({
|
||||
min: function(){
|
||||
var c = self.load_more.getCoordinates()
|
||||
return c.top - window.document.getSize().y - 300
|
||||
},
|
||||
onEnter: self.loadMore.bind(self)
|
||||
});
|
||||
if(self.options.load_more)
|
||||
self.scrollspy = new ScrollSpy({
|
||||
min: function(){
|
||||
var c = self.load_more.getCoordinates()
|
||||
return c.top - window.document.getSize().y - 300
|
||||
},
|
||||
onEnter: self.loadMore.bind(self)
|
||||
});
|
||||
|
||||
self.created = true;
|
||||
},
|
||||
@@ -78,7 +101,7 @@ var MovieList = new Class({
|
||||
if(!self.created) self.create();
|
||||
|
||||
// do scrollspy
|
||||
if(movies.length < self.options.limit){
|
||||
if(movies.length < self.options.limit && self.scrollspy){
|
||||
self.load_more.hide();
|
||||
self.scrollspy.stop();
|
||||
}
|
||||
@@ -86,34 +109,31 @@ var MovieList = new Class({
|
||||
Object.each(movies, function(movie){
|
||||
self.createMovie(movie);
|
||||
});
|
||||
|
||||
|
||||
self.total_movies = total;
|
||||
self.setCounter(total);
|
||||
|
||||
},
|
||||
|
||||
|
||||
setCounter: function(count){
|
||||
var self = this;
|
||||
|
||||
|
||||
if(!self.navigation_counter) return;
|
||||
|
||||
|
||||
self.navigation_counter.set('text', (count || 0));
|
||||
|
||||
|
||||
},
|
||||
|
||||
createMovie: function(movie, inject_at){
|
||||
var self = this;
|
||||
|
||||
// Attach proper actions
|
||||
var a = self.options.actions,
|
||||
status = Status.get(movie.status_id);
|
||||
var actions = a[status.identifier.capitalize()] || a.Wanted || {};
|
||||
|
||||
var m = new Movie(self, {
|
||||
'actions': actions,
|
||||
'actions': self.options.actions,
|
||||
'view': self.current_view,
|
||||
'onSelect': self.calculateSelected.bind(self)
|
||||
}, movie);
|
||||
|
||||
$(m).inject(self.movie_list, inject_at || 'bottom');
|
||||
|
||||
m.fireEvent('injected');
|
||||
|
||||
self.movies.include(m)
|
||||
@@ -124,7 +144,7 @@ var MovieList = new Class({
|
||||
var self = this;
|
||||
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
self.current_view = self.getSavedView();
|
||||
self.current_view = self.getSavedView() || 'details';
|
||||
self.el.addClass(self.current_view+'_list')
|
||||
|
||||
self.navigation = new Element('div.alph_nav').adopt(
|
||||
@@ -197,7 +217,7 @@ var MovieList = new Class({
|
||||
});
|
||||
|
||||
// Actions
|
||||
['mass_edit', 'thumbs', 'list'].each(function(view){
|
||||
['mass_edit', 'details', 'list'].each(function(view){
|
||||
self.navigation_actions.adopt(
|
||||
new Element('li.'+view+(self.current_view == view ? '.active' : '')+'[data-view='+view+']', {
|
||||
'events': {
|
||||
@@ -281,10 +301,11 @@ var MovieList = new Class({
|
||||
},
|
||||
|
||||
deleteSelected: function(){
|
||||
var self = this;
|
||||
var ids = self.getSelectedMovies()
|
||||
var self = this,
|
||||
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'),
|
||||
'class': 'delete',
|
||||
'events': {
|
||||
@@ -309,6 +330,8 @@ var MovieList = new Class({
|
||||
|
||||
erase_movies.each(function(movie){
|
||||
self.movies.erase(movie);
|
||||
|
||||
movie.destroy()
|
||||
});
|
||||
|
||||
self.calculateSelected();
|
||||
@@ -376,11 +399,16 @@ var MovieList = new Class({
|
||||
var self = this;
|
||||
|
||||
self.movies = []
|
||||
self.calculateSelected()
|
||||
self.navigation_alpha.getElements('.active').removeClass('active')
|
||||
if(self.mass_edit_select)
|
||||
self.calculateSelected()
|
||||
if(self.navigation_alpha)
|
||||
self.navigation_alpha.getElements('.active').removeClass('active')
|
||||
|
||||
self.offset = 0;
|
||||
self.load_more.show();
|
||||
self.scrollspy.start();
|
||||
if(self.scrollspy){
|
||||
self.load_more.show();
|
||||
self.scrollspy.start();
|
||||
}
|
||||
},
|
||||
|
||||
activateLetter: function(letter){
|
||||
@@ -396,21 +424,17 @@ var MovieList = new Class({
|
||||
changeView: function(new_view){
|
||||
var self = this;
|
||||
|
||||
self.movies.each(function(movie){
|
||||
movie.changeView(new_view)
|
||||
});
|
||||
|
||||
self.el
|
||||
.removeClass(self.current_view+'_list')
|
||||
.addClass(new_view+'_list')
|
||||
|
||||
self.current_view = new_view;
|
||||
Cookie.write(self.options.identifier+'_view', new_view, {duration: 1000});
|
||||
Cookie.write(self.options.identifier+'_view2', new_view, {duration: 1000});
|
||||
},
|
||||
|
||||
getSavedView: function(){
|
||||
var self = this;
|
||||
return Cookie.read(self.options.identifier+'_view') || 'thumbs';
|
||||
return Cookie.read(self.options.identifier+'_view2');
|
||||
},
|
||||
|
||||
search: function(){
|
||||
@@ -446,9 +470,12 @@ var MovieList = new Class({
|
||||
getMovies: function(){
|
||||
var self = this;
|
||||
|
||||
if(self.scrollspy) self.scrollspy.stop();
|
||||
self.load_more.set('text', 'loading...');
|
||||
Api.request('movie.list', {
|
||||
if(self.scrollspy){
|
||||
self.scrollspy.stop();
|
||||
self.load_more.set('text', 'loading...');
|
||||
}
|
||||
|
||||
Api.request(self.options.api_call || 'movie.list', {
|
||||
'data': Object.merge({
|
||||
'status': self.options.status,
|
||||
'limit_offset': self.options.limit + ',' + self.offset
|
||||
@@ -456,8 +483,12 @@ var MovieList = new Class({
|
||||
'onComplete': function(json){
|
||||
self.store(json.movies);
|
||||
self.addMovies(json.movies, json.total);
|
||||
self.load_more.set('text', 'load more movies');
|
||||
if(self.scrollspy) self.scrollspy.start();
|
||||
if(self.scrollspy) {
|
||||
self.load_more.set('text', 'load more movies');
|
||||
self.scrollspy.start();
|
||||
}
|
||||
|
||||
self.checkIfEmpty()
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -475,6 +506,34 @@ var MovieList = new Class({
|
||||
|
||||
},
|
||||
|
||||
checkIfEmpty: function(){
|
||||
var self = this;
|
||||
|
||||
var is_empty = self.movies.length == 0 && (self.total_movies == 0 || self.total_movies === undefined);
|
||||
|
||||
if(self.title)
|
||||
self.title[is_empty ? 'hide' : 'show']()
|
||||
|
||||
if(self.description)
|
||||
self.description[is_empty ? 'hide' : 'show']()
|
||||
|
||||
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(){
|
||||
return this.el;
|
||||
}
|
||||
|
||||
699
couchpotato/core/plugins/movie/static/movie.actions.js
Normal file
699
couchpotato/core/plugins/movie/static/movie.actions.js
Normal file
@@ -0,0 +1,699 @@
|
||||
var MovieAction = new Class({
|
||||
|
||||
class_name: 'action icon',
|
||||
|
||||
initialize: function(movie){
|
||||
var self = this;
|
||||
self.movie = movie;
|
||||
|
||||
self.create();
|
||||
if(self.el)
|
||||
self.el.addClass(self.class_name)
|
||||
},
|
||||
|
||||
create: function(){},
|
||||
|
||||
disable: function(){
|
||||
this.el.addClass('disable')
|
||||
},
|
||||
|
||||
enable: function(){
|
||||
this.el.removeClass('disable')
|
||||
},
|
||||
|
||||
createMask: function(){
|
||||
var self = this;
|
||||
self.mask = new Element('div.mask', {
|
||||
'styles': {
|
||||
'z-index': '1'
|
||||
}
|
||||
}).inject(self.movie, 'top').fade('hide');
|
||||
//self.positionMask();
|
||||
},
|
||||
|
||||
positionMask: function(){
|
||||
var self = this,
|
||||
movie = $(self.movie),
|
||||
s = movie.getSize()
|
||||
|
||||
return;
|
||||
|
||||
return self.mask.setStyles({
|
||||
'width': s.x,
|
||||
'height': s.y
|
||||
}).position({
|
||||
'relativeTo': movie
|
||||
})
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el || null
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var MA = {};
|
||||
|
||||
MA.IMDB = new Class({
|
||||
|
||||
Extends: MovieAction,
|
||||
id: null,
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.id = self.movie.get('identifier');
|
||||
|
||||
self.el = new Element('a.imdb', {
|
||||
'title': 'Go to the IMDB page of ' + self.movie.getTitle(),
|
||||
'href': 'http://www.imdb.com/title/'+self.id+'/',
|
||||
'target': '_blank'
|
||||
});
|
||||
|
||||
if(!self.id) self.disable();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
MA.Release = new Class({
|
||||
|
||||
Extends: MovieAction,
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.releases.icon.download', {
|
||||
'title': 'Show the releases that are available for ' + self.movie.getTitle(),
|
||||
'events': {
|
||||
'click': self.show.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
if(self.movie.data.releases.length == 0){
|
||||
self.el.hide()
|
||||
}
|
||||
else {
|
||||
|
||||
var buttons_done = false;
|
||||
|
||||
self.movie.data.releases.sortBy('-info.score').each(function(release){
|
||||
if(buttons_done) return;
|
||||
|
||||
var status = Status.get(release.status_id);
|
||||
|
||||
if((self.next_release && (status.identifier == 'ignored' || status.identifier == 'failed')) || (!self.next_release && status.identifier == 'available')){
|
||||
self.hide_on_click = false;
|
||||
self.show();
|
||||
buttons_done = true;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
show: function(e){
|
||||
var self = this;
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
if(!self.options_container){
|
||||
self.options_container = new Element('div.options').adopt(
|
||||
self.release_container = new Element('div.releases.table').adopt(
|
||||
self.trynext_container = new Element('div.buttons.try_container')
|
||||
)
|
||||
).inject(self.movie, 'top');
|
||||
|
||||
// Header
|
||||
new Element('div.item.head').adopt(
|
||||
new Element('span.name', {'text': 'Release name'}),
|
||||
new Element('span.status', {'text': 'Status'}),
|
||||
new Element('span.quality', {'text': 'Quality'}),
|
||||
new Element('span.size', {'text': 'Size'}),
|
||||
new Element('span.age', {'text': 'Age'}),
|
||||
new Element('span.score', {'text': 'Score'}),
|
||||
new Element('span.provider', {'text': 'Provider'})
|
||||
).inject(self.release_container)
|
||||
|
||||
self.movie.data.releases.sortBy('-info.score').each(function(release){
|
||||
|
||||
var status = Status.get(release.status_id),
|
||||
quality = Quality.getProfile(release.quality_id) || {},
|
||||
info = release.info,
|
||||
provider = self.get(release, 'provider') + (release.info['provider_extra'] ? self.get(release, 'provider_extra') : '');
|
||||
release.status = status;
|
||||
|
||||
var release_name = self.get(release, 'name');
|
||||
if(release.files && release.files.length > 0){
|
||||
try {
|
||||
var movie_file = release.files.filter(function(file){
|
||||
var type = File.Type.get(file.type_id);
|
||||
return type && type.identifier == 'movie'
|
||||
}).pick();
|
||||
release_name = movie_file.path.split(Api.getOption('path_sep')).getLast();
|
||||
}
|
||||
catch(e){}
|
||||
}
|
||||
|
||||
// Create release
|
||||
new Element('div', {
|
||||
'class': 'item '+status.identifier,
|
||||
'id': 'release_'+release.id
|
||||
}).adopt(
|
||||
new Element('span.name', {'text': release_name, 'title': release_name}),
|
||||
new Element('span.status', {'text': status.identifier, 'class': 'release_status '+status.identifier}),
|
||||
new Element('span.quality', {'text': quality.get('label') || 'n/a'}),
|
||||
new Element('span.size', {'text': release.info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}),
|
||||
new Element('span.age', {'text': self.get(release, 'age')}),
|
||||
new Element('span.score', {'text': self.get(release, 'score')}),
|
||||
new Element('span.provider', { 'text': provider, 'title': provider }),
|
||||
release.info['detail_url'] ? new Element('a.info.icon', {
|
||||
'href': release.info['detail_url'],
|
||||
'target': '_blank'
|
||||
}) : null,
|
||||
new Element('a.download.icon', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
if(!this.hasClass('completed'))
|
||||
self.download(release);
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Element('a.delete.icon', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
self.ignore(release);
|
||||
this.getParent('.item').toggleClass('ignored')
|
||||
}
|
||||
}
|
||||
})
|
||||
).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;
|
||||
}
|
||||
});
|
||||
|
||||
if(self.last_release){
|
||||
self.release_container.getElement('#release_'+self.last_release.id).addClass('last_release');
|
||||
}
|
||||
|
||||
if(self.next_release){
|
||||
self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release');
|
||||
}
|
||||
|
||||
if(self.next_release || self.last_release){
|
||||
|
||||
self.trynext_container.adopt(
|
||||
new Element('span.or', {
|
||||
'text': 'This movie is snatched, if anything went wrong, download'
|
||||
}),
|
||||
self.last_release ? new Element('a.button.orange', {
|
||||
'text': 'the same release again',
|
||||
'events': {
|
||||
'click': self.trySameRelease.bind(self)
|
||||
}
|
||||
}) : null,
|
||||
self.next_release && self.last_release ? new Element('span.or', {
|
||||
'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
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
self.movie.slide('in', self.options_container);
|
||||
},
|
||||
|
||||
get: function(release, type){
|
||||
return release.info[type] || 'n/a'
|
||||
},
|
||||
|
||||
download: function(release){
|
||||
var self = this;
|
||||
|
||||
var release_el = self.release_container.getElement('#release_'+release.id),
|
||||
icon = release_el.getElement('.download.icon');
|
||||
|
||||
icon.addClass('spinner');
|
||||
|
||||
Api.request('release.download', {
|
||||
'data': {
|
||||
'id': release.id
|
||||
},
|
||||
'onComplete': function(json){
|
||||
icon.removeClass('spinner')
|
||||
if(json.success)
|
||||
icon.addClass('completed');
|
||||
else
|
||||
icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
ignore: function(release){
|
||||
var self = this;
|
||||
|
||||
Api.request('release.ignore', {
|
||||
'data': {
|
||||
'id': release.id
|
||||
}
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
tryNextRelease: function(movie_id){
|
||||
var self = this;
|
||||
|
||||
if(self.last_release)
|
||||
self.ignore(self.last_release);
|
||||
|
||||
if(self.next_release)
|
||||
self.download(self.next_release);
|
||||
|
||||
},
|
||||
|
||||
trySameRelease: function(movie_id){
|
||||
var self = this;
|
||||
|
||||
if(self.last_release)
|
||||
self.download(self.last_release);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
MA.Trailer = new Class({
|
||||
|
||||
Extends: MovieAction,
|
||||
id: null,
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.trailer', {
|
||||
'title': 'Watch the trailer of ' + self.movie.getTitle(),
|
||||
'events': {
|
||||
'click': self.watch.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
watch: function(offset){
|
||||
var self = this;
|
||||
|
||||
var data_url = 'http://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'
|
||||
var url = data_url.substitute({
|
||||
'title': encodeURI(self.movie.getTitle()),
|
||||
'year': self.movie.get('year'),
|
||||
'offset': offset || 1
|
||||
}),
|
||||
size = $(self.movie).getSize(),
|
||||
height = (size.x/16)*9,
|
||||
id = 'trailer-'+randomString();
|
||||
|
||||
self.player_container = new Element('div[id='+id+']');
|
||||
self.container = new Element('div.hide.trailer_container')
|
||||
.adopt(self.player_container)
|
||||
.inject($(self.movie), 'top');
|
||||
|
||||
self.container.setStyle('height', 0);
|
||||
self.container.removeClass('hide');
|
||||
|
||||
self.close_button = new Element('a.hide.hide_trailer', {
|
||||
'text': 'Hide trailer',
|
||||
'events': {
|
||||
'click': self.stop.bind(self)
|
||||
}
|
||||
}).inject(self.movie);
|
||||
|
||||
self.container.setStyle('height', height);
|
||||
$(self.movie).setStyle('height', height);
|
||||
|
||||
new Request.JSONP({
|
||||
'url': url,
|
||||
'onComplete': function(json){
|
||||
var video_url = json.feed.entry[0].id.$t.split('/'),
|
||||
video_id = video_url[video_url.length-1];
|
||||
|
||||
self.player = new YT.Player(id, {
|
||||
'height': height,
|
||||
'width': size.x,
|
||||
'videoId': video_id,
|
||||
'playerVars': {
|
||||
'autoplay': 1,
|
||||
'showsearch': 0,
|
||||
'wmode': 'transparent',
|
||||
'iv_load_policy': 3
|
||||
}
|
||||
});
|
||||
|
||||
self.close_button.removeClass('hide');
|
||||
|
||||
var quality_set = false;
|
||||
var change_quality = function(state){
|
||||
if(!quality_set && (state.data == 1 || state.data || 2)){
|
||||
try {
|
||||
self.player.setPlaybackQuality('hd720');
|
||||
quality_set = true;
|
||||
}
|
||||
catch(e){
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
self.player.addEventListener('onStateChange', change_quality);
|
||||
|
||||
}
|
||||
}).send()
|
||||
|
||||
},
|
||||
|
||||
stop: function(){
|
||||
var self = this;
|
||||
|
||||
self.player.stopVideo();
|
||||
self.container.addClass('hide');
|
||||
self.close_button.addClass('hide');
|
||||
$(self.movie).setStyle('height', null);
|
||||
|
||||
setTimeout(function(){
|
||||
self.container.destroy()
|
||||
self.close_button.destroy();
|
||||
}, 1800)
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
MA.Edit = new Class({
|
||||
|
||||
Extends: MovieAction,
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.edit', {
|
||||
'title': 'Change movie information, like title and quality.',
|
||||
'events': {
|
||||
'click': self.editMovie.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
editMovie: function(e){
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
|
||||
if(!self.options_container){
|
||||
self.options_container = new Element('div.options').adopt(
|
||||
new Element('div.form').adopt(
|
||||
self.title_select = new Element('select', {
|
||||
'name': 'title'
|
||||
}),
|
||||
self.profile_select = new Element('select', {
|
||||
'name': 'profile'
|
||||
}),
|
||||
new Element('a.button.edit', {
|
||||
'text': 'Save & Search',
|
||||
'events': {
|
||||
'click': self.save.bind(self)
|
||||
}
|
||||
})
|
||||
)
|
||||
).inject(self.movie, 'top');
|
||||
|
||||
Array.each(self.movie.data.library.titles, function(alt){
|
||||
new Element('option', {
|
||||
'text': alt.title
|
||||
}).inject(self.title_select);
|
||||
|
||||
if(alt['default'])
|
||||
self.title_select.set('value', alt.title);
|
||||
});
|
||||
|
||||
|
||||
Quality.getActiveProfiles().each(function(profile){
|
||||
|
||||
var profile_id = profile.id ? profile.id : profile.data.id;
|
||||
|
||||
new Element('option', {
|
||||
'value': profile_id,
|
||||
'text': profile.label ? profile.label : profile.data.label
|
||||
}).inject(self.profile_select);
|
||||
|
||||
if(self.movie.profile && self.movie.profile.data && self.movie.profile.data.id == profile_id)
|
||||
self.profile_select.set('value', profile_id);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
self.movie.slide('in', self.options_container);
|
||||
},
|
||||
|
||||
save: function(e){
|
||||
(e).preventDefault();
|
||||
var self = this;
|
||||
|
||||
Api.request('movie.edit', {
|
||||
'data': {
|
||||
'id': self.movie.get('id'),
|
||||
'default_title': self.title_select.get('value'),
|
||||
'profile_id': self.profile_select.get('value')
|
||||
},
|
||||
'useSpinner': true,
|
||||
'spinnerTarget': $(self.movie),
|
||||
'onComplete': function(){
|
||||
self.movie.quality.set('text', self.profile_select.getSelected()[0].get('text'));
|
||||
self.movie.title.set('text', self.title_select.getSelected()[0].get('text'));
|
||||
}
|
||||
});
|
||||
|
||||
self.movie.slide('out');
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
MA.Refresh = new Class({
|
||||
|
||||
Extends: MovieAction,
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.refresh', {
|
||||
'title': 'Refresh the movie info and do a forced search',
|
||||
'events': {
|
||||
'click': self.doRefresh.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
doRefresh: function(e){
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
|
||||
Api.request('movie.refresh', {
|
||||
'data': {
|
||||
'id': self.movie.get('id')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
MA.Readd = new Class({
|
||||
|
||||
Extends: MovieAction,
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
var movie_done = Status.get(self.movie.data.status_id).identifier == 'done';
|
||||
if(!movie_done)
|
||||
var snatched = self.movie.data.releases.filter(function(release){
|
||||
return release.status && (release.status.identifier == 'snatched' || release.status.identifier == 'downloaded' || release.status.identifier == 'done');
|
||||
}).length;
|
||||
|
||||
if(movie_done || snatched && snatched > 0)
|
||||
self.el = new Element('a.readd', {
|
||||
'title': 'Readd the movie and mark all previous snatched/downloaded as ignored',
|
||||
'events': {
|
||||
'click': self.doReadd.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
doReadd: function(e){
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
|
||||
Api.request('movie.add', {
|
||||
'data': {
|
||||
'identifier': self.movie.get('identifier'),
|
||||
'ignore_previous': 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
MA.Delete = new Class({
|
||||
|
||||
Extends: MovieAction,
|
||||
|
||||
Implements: [Chain],
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.delete', {
|
||||
'title': 'Remove the movie from this CP list',
|
||||
'events': {
|
||||
'click': self.showConfirm.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
showConfirm: function(e){
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
|
||||
if(!self.delete_container){
|
||||
self.delete_container = new Element('div.buttons.delete_container').adopt(
|
||||
new Element('a.cancel', {
|
||||
'text': 'Cancel',
|
||||
'events': {
|
||||
'click': self.hideConfirm.bind(self)
|
||||
}
|
||||
}),
|
||||
new Element('span.or', {
|
||||
'text': 'or'
|
||||
}),
|
||||
new Element('a.button.delete', {
|
||||
'text': 'Delete ' + self.movie.title.get('text'),
|
||||
'events': {
|
||||
'click': self.del.bind(self)
|
||||
}
|
||||
})
|
||||
).inject(self.movie, 'top');
|
||||
}
|
||||
|
||||
self.movie.slide('in', self.delete_container);
|
||||
|
||||
},
|
||||
|
||||
hideConfirm: function(e){
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
|
||||
self.movie.slide('out');
|
||||
},
|
||||
|
||||
del: function(e){
|
||||
(e).preventDefault();
|
||||
var self = this;
|
||||
|
||||
var movie = $(self.movie);
|
||||
|
||||
self.chain(
|
||||
function(){
|
||||
self.callChain();
|
||||
},
|
||||
function(){
|
||||
Api.request('movie.delete', {
|
||||
'data': {
|
||||
'id': self.movie.get('id'),
|
||||
'delete_from': self.movie.list.options.identifier
|
||||
},
|
||||
'onComplete': function(){
|
||||
movie.set('tween', {
|
||||
'duration': 300,
|
||||
'onComplete': function(){
|
||||
self.movie.destroy()
|
||||
}
|
||||
});
|
||||
movie.tween('height', 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
self.callChain();
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
MA.Files = new Class({
|
||||
|
||||
Extends: MovieAction,
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.directory', {
|
||||
'title': 'Available files',
|
||||
'events': {
|
||||
'click': self.showFiles.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
showFiles: function(e){
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
|
||||
if(!self.options_container){
|
||||
self.options_container = new Element('div.options').adopt(
|
||||
self.files_container = new Element('div.files.table')
|
||||
).inject(self.movie, 'top');
|
||||
|
||||
// Header
|
||||
new Element('div.item.head').adopt(
|
||||
new Element('span.name', {'text': 'File'}),
|
||||
new Element('span.type', {'text': 'Type'}),
|
||||
new Element('span.is_available', {'text': 'Available'})
|
||||
).inject(self.files_container)
|
||||
|
||||
Array.each(self.movie.data.releases, function(release){
|
||||
|
||||
var rel = new Element('div.release').inject(self.files_container);
|
||||
|
||||
Array.each(release.files, function(file){
|
||||
new Element('div.file.item').adopt(
|
||||
new Element('span.name', {'text': file.path}),
|
||||
new Element('span.type', {'text': File.Type.get(file.type_id).name}),
|
||||
new Element('span.available', {'text': file.available})
|
||||
).inject(rel)
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
self.movie.slide('in', self.options_container);
|
||||
},
|
||||
|
||||
});
|
||||
@@ -1,7 +1,33 @@
|
||||
.movies {
|
||||
padding: 60px 0 20px;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.movies h2 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.movies > .description {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 0;
|
||||
font-style: italic;
|
||||
text-shadow: none;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.movies:hover > .description {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.movies.thumbs_list {
|
||||
padding: 20px 0 20px;
|
||||
}
|
||||
|
||||
.home .movies {
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.movies.mass_edit_list {
|
||||
padding-top: 90px;
|
||||
}
|
||||
@@ -12,33 +38,58 @@
|
||||
margin: 10px 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
transition: all 0.2s linear;
|
||||
}
|
||||
.movies .movie.list_view, .movies .movie.mass_edit_view {
|
||||
|
||||
.movies.list_list .movie:not(.details_view),
|
||||
.movies.mass_edit_list .movie {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.movies.thumbs_list .movie {
|
||||
width: 153px;
|
||||
height: 230px;
|
||||
display: inline-block;
|
||||
margin: 0 8px 0 0;
|
||||
}
|
||||
.movies.thumbs_list .movie:nth-child(6n+6) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.movies .movie .mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.movies.list_list .movie:not(.details_view),
|
||||
.movies.mass_edit_list .movie {
|
||||
margin: 1px 0;
|
||||
border-radius: 0;
|
||||
background: no-repeat;
|
||||
box-shadow: none;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.05);
|
||||
}
|
||||
.movies .movie.list_view:hover, .movies .movie.mass_edit_view:hover {
|
||||
|
||||
.movies.list_list .movie:hover:not(.details_view),
|
||||
.movies.mass_edit_list .movie {
|
||||
background: rgba(255,255,255,0.03);
|
||||
}
|
||||
|
||||
.movies .movie_container {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.movies .data {
|
||||
padding: 20px;
|
||||
height: 180px;
|
||||
height: 100%;
|
||||
width: 840px;
|
||||
position: relative;
|
||||
float: right;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
border-radius: 0;
|
||||
transition: all 0.2s linear;
|
||||
transition: all .6s cubic-bezier(0.9,0,0.1,1);
|
||||
}
|
||||
.movies .list_view .data, .movies .mass_edit_view .data {
|
||||
.movies.list_list .movie:not(.details_view) .data,
|
||||
.movies.mass_edit_list .movie .data {
|
||||
height: 30px;
|
||||
padding: 3px 0 3px 10px;
|
||||
width: 938px;
|
||||
@@ -46,79 +97,148 @@
|
||||
border: 0;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.movies.thumbs_list .data {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
height: 100%;
|
||||
background: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.movies.thumbs_list .movie.no_thumbnail .data { background-image: linear-gradient(-30deg, rgba(255, 0, 85, .2) 0,rgba(125, 185, 235, .2) 100%);
|
||||
}
|
||||
.movies.thumbs_list .movie.no_thumbnail:nth-child(2n+6) .data { background-image: linear-gradient(-20deg, rgba(125, 0, 215, .2) 0, rgba(4, 55, 5, .7) 100%); }
|
||||
.movies.thumbs_list .movie.no_thumbnail:nth-child(3n+6) .data { background-image: linear-gradient(-30deg, rgba(155, 0, 85, .2) 0,rgba(25, 185, 235, .7) 100%); }
|
||||
.movies.thumbs_list .movie.no_thumbnail:nth-child(4n+6) .data { background-image: linear-gradient(-30deg, rgba(115, 5, 235, .2) 0, rgba(55, 180, 5, .7) 100%); }
|
||||
.movies.thumbs_list .movie.no_thumbnail:nth-child(5n+6) .data { background-image: linear-gradient(-30deg, rgba(35, 15, 215, .2) 0, rgba(135, 215, 115, .7) 100%); }
|
||||
.movies.thumbs_list .movie.no_thumbnail:nth-child(6n+6) .data { background-image: linear-gradient(-30deg, rgba(35, 15, 215, .2) 0, rgba(135, 15, 115, .7) 100%); }
|
||||
|
||||
.movies.thumbs_list .movie:hover .data {
|
||||
background: rgba(0,0,0,0.9);
|
||||
}
|
||||
|
||||
.movies .data.hide_right {
|
||||
right: -100%;
|
||||
}
|
||||
|
||||
.movies .movie .check {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.movies.mass_edit_list .movie .check {
|
||||
float: left;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
display: block;
|
||||
margin: 7px 0 0 5px;
|
||||
}
|
||||
|
||||
.movies .poster {
|
||||
float: left;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 120px;
|
||||
line-height: 0;
|
||||
overflow: hidden;
|
||||
height: 180px;
|
||||
height: 100%;
|
||||
border-radius: 4px 0 0 4px;
|
||||
transition: all 0.2s linear;
|
||||
transition: all .6s cubic-bezier(0.9,0,0.1,1);
|
||||
|
||||
}
|
||||
.movies .list_view .poster, .movies .mass_edit_view .poster {
|
||||
.movies.list_list .movie:not(.details_view) .poster,
|
||||
.movies.mass_edit_list .poster {
|
||||
width: 20px;
|
||||
height: 30px;
|
||||
border-radius: 1px 0 0 1px;
|
||||
}
|
||||
.movies.mass_edit_list .poster {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.movies.thumbs_list .poster {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.movies .poster img, .options .poster img {
|
||||
.movies .poster img,
|
||||
.options .poster img {
|
||||
width: 101%;
|
||||
height: 101%;
|
||||
}
|
||||
|
||||
.movies .info {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.movies .info .title {
|
||||
font-size: 30px;
|
||||
display: inline;
|
||||
position: absolute;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
float: left;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 90%;
|
||||
transition: all 0.2s linear;
|
||||
}
|
||||
.movies .list_view .info .title, .movies .mass_edit_view .info .title {
|
||||
.movies.list_list .movie:not(.details_view) .info .title,
|
||||
.movies.mass_edit_list .info .title {
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
text-overflow: ellipsis;
|
||||
width: auto;
|
||||
overflow: hidden;
|
||||
|
||||
}
|
||||
|
||||
.movies.thumbs_list .movie:not(.no_thumbnail) .info {
|
||||
display: none;
|
||||
}
|
||||
.movies.thumbs_list .movie:hover .info {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.movies.thumbs_list .info .title {
|
||||
font-size: 21px;
|
||||
text-shadow: 0 0 10px #000;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.movies .info .year {
|
||||
position: absolute;
|
||||
font-size: 30px;
|
||||
margin-bottom: 10px;
|
||||
float: right;
|
||||
color: #bbb;
|
||||
width: 10%;
|
||||
right: 0;
|
||||
top: 0;
|
||||
text-align: right;
|
||||
transition: all 0.2s linear;
|
||||
}
|
||||
.movies .list_view .info .year, .movies .mass_edit_view .info .year {
|
||||
.movies.list_list .movie:not(.details_view) .info .year,
|
||||
.movies.mass_edit_list .info .year {
|
||||
font-size: 16px;
|
||||
width: 6%;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.movies.thumbs_list .info .year {
|
||||
font-size: 23px;
|
||||
margin: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
top: auto;
|
||||
right: auto;
|
||||
color: #FFF;
|
||||
text-shadow: none;
|
||||
text-shadow: 0 0 6px #000;
|
||||
}
|
||||
|
||||
.movies .info .rating {
|
||||
font-size: 30px;
|
||||
margin-bottom: 10px;
|
||||
color: #444;
|
||||
float: left;
|
||||
width: 5%;
|
||||
padding: 0 0 0 3%;
|
||||
}
|
||||
|
||||
.movies .info .description {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
clear: both;
|
||||
height: 80px;
|
||||
overflow: hidden;
|
||||
@@ -126,63 +246,82 @@
|
||||
.movies .data:hover .description {
|
||||
overflow: auto;
|
||||
}
|
||||
.movies .list_view .info .description, .movies .mass_edit_view .info .description {
|
||||
.movies.list_list .movie:not(.details_view) .info .description,
|
||||
.movies.mass_edit_list .info .description,
|
||||
.movies.thumbs_list .info .description {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.movies .data .quality {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
display: block;
|
||||
min-height: 20px;
|
||||
vertical-align: mid;
|
||||
}
|
||||
|
||||
.movies .data .quality span {
|
||||
padding: 2px 3px;
|
||||
font-weight: bold;
|
||||
opacity: 0.5;
|
||||
font-size: 10px;
|
||||
height: 16px;
|
||||
line-height: 12px;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
text-transform: uppercase;
|
||||
text-shadow: none;
|
||||
font-weight: normal;
|
||||
margin: 0 2px;
|
||||
border-radius: 2px;
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
}
|
||||
.movies .list_view .data .quality, .movies .mass_edit_view .data .quality {
|
||||
text-align: right;
|
||||
float: right;
|
||||
|
||||
.movies .status_suggest .data .quality,
|
||||
.movies.thumbs_list .data .quality {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.movies .data .quality .available, .movies .data .quality .snatched {
|
||||
opacity: 1;
|
||||
box-shadow: 1px 1px 0 rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.movies .data .quality .available { background-color: #578bc3; }
|
||||
.movies .data .quality .snatched { background-color: #369545; }
|
||||
.movies .data .quality .done {
|
||||
background-color: #369545;
|
||||
opacity: 1;
|
||||
}
|
||||
.movies .data .quality .finish {
|
||||
background-image: url('../images/sprite.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 2px;
|
||||
padding-left: 14px;
|
||||
background-size: 14px
|
||||
}
|
||||
.movies .data .quality span {
|
||||
padding: 2px 3px;
|
||||
font-weight: bold;
|
||||
opacity: 0.5;
|
||||
font-size: 10px;
|
||||
height: 16px;
|
||||
line-height: 12px;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
text-transform: uppercase;
|
||||
text-shadow: none;
|
||||
font-weight: normal;
|
||||
margin: 0 2px;
|
||||
border-radius: 2px;
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
}
|
||||
.movies.list_list .data .quality,
|
||||
.movies.mass_edit_list .data .quality {
|
||||
text-align: right;
|
||||
right: 0;
|
||||
margin-right: 50px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.movies .data .quality .available,
|
||||
.movies .data .quality .snatched {
|
||||
opacity: 1;
|
||||
box-shadow: 1px 1px 0 rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.movies .data .quality .available { background-color: #578bc3; }
|
||||
.movies .data .quality .snatched { background-color: #369545; }
|
||||
.movies .data .quality .done {
|
||||
background-color: #369545;
|
||||
opacity: 1;
|
||||
}
|
||||
.movies .data .quality .finish {
|
||||
background-image: url('../images/sprite.png');
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 2px;
|
||||
padding-left: 14px;
|
||||
background-size: 14px
|
||||
}
|
||||
|
||||
.movies .data .actions {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
line-height: 0;
|
||||
clear: both;
|
||||
float: right;
|
||||
margin-top: -25px;
|
||||
}
|
||||
.movies.thumbs_list .data .actions {
|
||||
bottom: 8px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.movies .data:hover .action { opacity: 0.6; }
|
||||
.movies .data:hover .action:hover { opacity: 1; }
|
||||
.movies.mass_edit_list .data .actions {
|
||||
@@ -199,10 +338,14 @@
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.movies .list_view .data:hover .actions, .movies .mass_edit_view .data:hover .actions {
|
||||
margin: -34px 2px 0 0;
|
||||
.movies.list_list .movie:not(.details_view) .data:hover .actions,
|
||||
.movies.mass_edit_list .data:hover .actions {
|
||||
margin: 0;
|
||||
background: #4e5969;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
right: 5px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.movies .delete_container {
|
||||
@@ -284,6 +427,7 @@
|
||||
.movies .options .table .provider {
|
||||
width: 120px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.movies .options .table .name {
|
||||
width: 350px;
|
||||
@@ -313,7 +457,7 @@
|
||||
padding-bottom: 4px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie .trailer_container {
|
||||
width: 100%;
|
||||
background: #000;
|
||||
@@ -324,7 +468,7 @@
|
||||
.movies .movie .trailer_container.hide {
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie .hide_trailer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -335,17 +479,17 @@
|
||||
padding: 3px 10px;
|
||||
background: #4e5969;
|
||||
border-radius: 0 0 2px 2px;
|
||||
transition: all .6s cubic-bezier(0.9,0,0.1,1) .2s;
|
||||
transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s;
|
||||
}
|
||||
.movies .movie .hide_trailer.hide {
|
||||
top: -30px;
|
||||
}
|
||||
|
||||
.movies .movie .hide_trailer.hide {
|
||||
top: -30px;
|
||||
}
|
||||
|
||||
.movies .movie .try_container {
|
||||
padding: 5px 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie .try_container a {
|
||||
margin: 0 5px;
|
||||
padding: 2px 5px;
|
||||
@@ -354,15 +498,15 @@
|
||||
.movies .movie .releases .next_release {
|
||||
border-left: 6px solid #2aa300;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie .releases .next_release > :first-child {
|
||||
margin-left: -6px;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie .releases .last_release {
|
||||
border-left: 6px solid #ffa200;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie .releases .last_release > :first-child {
|
||||
margin-left: -6px;
|
||||
}
|
||||
@@ -380,7 +524,7 @@
|
||||
.movies .alph_nav {
|
||||
transition: box-shadow .4s linear;
|
||||
position: fixed;
|
||||
z-index: 2;
|
||||
z-index: 4;
|
||||
top: 0;
|
||||
padding: 100px 60px 7px;
|
||||
width: 1080px;
|
||||
@@ -394,8 +538,8 @@
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.movies .alph_nav ul.numbers,
|
||||
.movies .alph_nav .counter,
|
||||
.movies .alph_nav ul.numbers,
|
||||
.movies .alph_nav .counter,
|
||||
.movies .alph_nav ul.actions {
|
||||
list-style: none;
|
||||
padding: 0 0 1px;
|
||||
@@ -409,7 +553,8 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.movies .alph_nav .numbers li, .movies .alph_nav .actions li {
|
||||
.movies .alph_nav .numbers li,
|
||||
.movies .alph_nav .actions li {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
width: 20px;
|
||||
@@ -472,7 +617,7 @@
|
||||
background-position: 3px -95px;
|
||||
}
|
||||
|
||||
.movies .alph_nav .actions li.thumbs span {
|
||||
.movies .alph_nav .actions li.details span {
|
||||
background-position: 3px -74px;
|
||||
}
|
||||
|
||||
@@ -518,7 +663,7 @@
|
||||
padding: 3px 7px;
|
||||
}
|
||||
|
||||
.movies .alph_nav .mass_edit_form .refresh,
|
||||
.movies .alph_nav .mass_edit_form .refresh,
|
||||
.movies .alph_nav .mass_edit_form .delete {
|
||||
float: left;
|
||||
padding: 8px 0 0 8px;
|
||||
@@ -534,5 +679,67 @@
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ var Movie = new Class({
|
||||
var self = this;
|
||||
|
||||
self.data = data;
|
||||
self.view = options.view || 'thumbs';
|
||||
self.view = options.view || 'details';
|
||||
self.list = list;
|
||||
|
||||
self.el = new Element('div.movie.inlay');
|
||||
@@ -16,14 +16,41 @@ var Movie = new Class({
|
||||
self.profile = Quality.getProfile(data.profile_id) || {};
|
||||
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){
|
||||
App.addEvent(listener+'.'+data.id, function(notification){
|
||||
App.addEvent(listener+'.'+self.data.id, function(notification){
|
||||
if(notification.data)
|
||||
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){
|
||||
@@ -45,7 +72,6 @@ var Movie = new Class({
|
||||
else if(!self.spinner) {
|
||||
self.createMask();
|
||||
self.spinner = createSpinner(self.mask);
|
||||
self.positionMask();
|
||||
self.mask.fade('in');
|
||||
}
|
||||
},
|
||||
@@ -54,10 +80,9 @@ var Movie = new Class({
|
||||
var self = this;
|
||||
self.mask = new Element('div.mask', {
|
||||
'styles': {
|
||||
'z-index': '1'
|
||||
'z-index': 4
|
||||
}
|
||||
}).inject(self.el, 'top').fade('hide');
|
||||
self.positionMask();
|
||||
},
|
||||
|
||||
positionMask: function(){
|
||||
@@ -76,7 +101,7 @@ var Movie = new Class({
|
||||
var self = this;
|
||||
|
||||
self.data = notification.data;
|
||||
self.container.destroy();
|
||||
self.el.empty();
|
||||
|
||||
self.profile = Quality.getProfile(self.data.profile_id) || {};
|
||||
self.create();
|
||||
@@ -87,52 +112,50 @@ var Movie = new Class({
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
var s = Status.get(self.get('status_id'));
|
||||
self.el.addClass('status_'+s.identifier);
|
||||
|
||||
self.el.adopt(
|
||||
self.container = new Element('div.movie_container').adopt(
|
||||
self.select_checkbox = new Element('input[type=checkbox].inlay', {
|
||||
'events': {
|
||||
'change': function(){
|
||||
self.fireEvent('select')
|
||||
}
|
||||
self.select_checkbox = new Element('input[type=checkbox].inlay', {
|
||||
'events': {
|
||||
'change': function(){
|
||||
self.fireEvent('select')
|
||||
}
|
||||
}),
|
||||
self.thumbnail = File.Select.single('poster', self.data.library.files),
|
||||
self.data_container = new Element('div.data.inlay.light', {
|
||||
'tween': {
|
||||
duration: 400,
|
||||
transition: 'quint:in:out',
|
||||
onComplete: self.fireEvent.bind(self, 'slideEnd')
|
||||
}
|
||||
}).adopt(
|
||||
self.info_container = new Element('div.info').adopt(
|
||||
self.title = new Element('div.title', {
|
||||
'text': self.getTitle() || 'n/a'
|
||||
}),
|
||||
self.year = new Element('div.year', {
|
||||
'text': self.data.library.year || 'n/a'
|
||||
}),
|
||||
self.rating = new Element('div.rating.icon', {
|
||||
'text': self.data.library.rating
|
||||
}),
|
||||
self.description = new Element('div.description', {
|
||||
'text': self.data.library.plot
|
||||
}),
|
||||
self.quality = new Element('div.quality', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
var releases = self.el.getElement('.actions .releases');
|
||||
if(releases)
|
||||
releases.fireEvent('click', [e])
|
||||
}
|
||||
}
|
||||
}),
|
||||
self.thumbnail = File.Select.single('poster', self.data.library.files),
|
||||
self.data_container = new Element('div.data.inlay.light').adopt(
|
||||
self.info_container = new Element('div.info').adopt(
|
||||
self.title = new Element('div.title', {
|
||||
'text': self.getTitle() || 'n/a'
|
||||
}),
|
||||
self.year = new Element('div.year', {
|
||||
'text': self.data.library.year || 'n/a'
|
||||
}),
|
||||
self.rating = new Element('div.rating.icon', {
|
||||
'text': self.data.library.rating
|
||||
}),
|
||||
self.description = new Element('div.description', {
|
||||
'text': self.data.library.plot
|
||||
}),
|
||||
self.quality = new Element('div.quality', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
var releases = self.el.getElement('.actions .releases');
|
||||
if(releases)
|
||||
releases.fireEvent('click', [e])
|
||||
}
|
||||
})
|
||||
),
|
||||
self.actions = new Element('div.actions')
|
||||
)
|
||||
}
|
||||
})
|
||||
),
|
||||
self.actions = new Element('div.actions')
|
||||
)
|
||||
);
|
||||
|
||||
self.changeView(self.view);
|
||||
if(self.thumbnail.empty)
|
||||
self.el.addClass('no_thumbnail');
|
||||
|
||||
//self.changeView(self.view);
|
||||
self.select_checkbox_class = new Form.Check(self.select_checkbox);
|
||||
|
||||
// Add profile
|
||||
@@ -147,7 +170,7 @@ var Movie = new Class({
|
||||
|
||||
});
|
||||
|
||||
// Add done releases
|
||||
// Add releases
|
||||
self.data.releases.each(function(release){
|
||||
|
||||
var q = self.quality.getElement('.q_id'+ release.quality_id),
|
||||
@@ -214,23 +237,23 @@ var Movie = new Class({
|
||||
|
||||
if(direction == 'in'){
|
||||
self.temp_view = self.view;
|
||||
self.changeView('thumbs')
|
||||
self.changeView('details')
|
||||
|
||||
self.el.addEvent('outerClick', function(){
|
||||
self.changeView(self.temp_view)
|
||||
self.removeView()
|
||||
self.slide('out')
|
||||
})
|
||||
el.show();
|
||||
self.data_container.tween('right', 0, -840);
|
||||
self.data_container.addClass('hide_right');
|
||||
}
|
||||
else {
|
||||
self.el.removeEvents('outerClick')
|
||||
|
||||
self.addEvent('slideEnd:once', function(){
|
||||
setTimeout(function(){
|
||||
self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
|
||||
});
|
||||
}, 600);
|
||||
|
||||
self.data_container.tween('right', -840, 0);
|
||||
self.data_container.removeClass('hide_right');
|
||||
}
|
||||
},
|
||||
|
||||
@@ -244,6 +267,12 @@ var Movie = new Class({
|
||||
self.view = new_view;
|
||||
},
|
||||
|
||||
removeView: function(){
|
||||
var self = this;
|
||||
|
||||
self.el.removeClass(self.view+'_view')
|
||||
},
|
||||
|
||||
get: function(attr){
|
||||
return this.data[attr] || this.data.library[attr]
|
||||
},
|
||||
@@ -261,377 +290,4 @@ var Movie = new Class({
|
||||
return this.el;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var MovieAction = new Class({
|
||||
|
||||
class_name: 'action icon',
|
||||
|
||||
initialize: function(movie){
|
||||
var self = this;
|
||||
self.movie = movie;
|
||||
|
||||
self.create();
|
||||
if(self.el)
|
||||
self.el.addClass(self.class_name)
|
||||
},
|
||||
|
||||
create: function(){},
|
||||
|
||||
disable: function(){
|
||||
this.el.addClass('disable')
|
||||
},
|
||||
|
||||
enable: function(){
|
||||
this.el.removeClass('disable')
|
||||
},
|
||||
|
||||
createMask: function(){
|
||||
var self = this;
|
||||
self.mask = new Element('div.mask', {
|
||||
'styles': {
|
||||
'z-index': '1'
|
||||
}
|
||||
}).inject(self.movie, 'top').fade('hide');
|
||||
self.positionMask();
|
||||
},
|
||||
|
||||
positionMask: function(){
|
||||
var self = this,
|
||||
movie = $(self.movie),
|
||||
s = movie.getSize()
|
||||
|
||||
return;
|
||||
|
||||
return self.mask.setStyles({
|
||||
'width': s.x,
|
||||
'height': s.y
|
||||
}).position({
|
||||
'relativeTo': movie
|
||||
})
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el || null
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var IMDBAction = new Class({
|
||||
|
||||
Extends: MovieAction,
|
||||
id: null,
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.id = self.movie.get('identifier');
|
||||
|
||||
self.el = new Element('a.imdb', {
|
||||
'title': 'Go to the IMDB page of ' + self.movie.getTitle(),
|
||||
'href': 'http://www.imdb.com/title/'+self.id+'/',
|
||||
'target': '_blank'
|
||||
});
|
||||
|
||||
if(!self.id) self.disable();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var ReleaseAction = new Class({
|
||||
|
||||
Extends: MovieAction,
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.releases.icon.download', {
|
||||
'title': 'Show the releases that are available for ' + self.movie.getTitle(),
|
||||
'events': {
|
||||
'click': self.show.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
var buttons_done = false;
|
||||
|
||||
self.movie.data.releases.sortBy('-info.score').each(function(release){
|
||||
if(buttons_done) return;
|
||||
|
||||
var status = Status.get(release.status_id);
|
||||
|
||||
if((status.identifier == 'ignored' || status.identifier == 'failed') || (!self.next_release && status.identifier == 'available')){
|
||||
self.hide_on_click = false;
|
||||
self.show();
|
||||
buttons_done = true;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
show: function(e){
|
||||
var self = this;
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
if(!self.options_container){
|
||||
self.options_container = new Element('div.options').adopt(
|
||||
self.release_container = new Element('div.releases.table').adopt(
|
||||
self.trynext_container = new Element('div.buttons.try_container')
|
||||
)
|
||||
).inject(self.movie, 'top');
|
||||
|
||||
// Header
|
||||
new Element('div.item.head').adopt(
|
||||
new Element('span.name', {'text': 'Release name'}),
|
||||
new Element('span.status', {'text': 'Status'}),
|
||||
new Element('span.quality', {'text': 'Quality'}),
|
||||
new Element('span.size', {'text': 'Size'}),
|
||||
new Element('span.age', {'text': 'Age'}),
|
||||
new Element('span.score', {'text': 'Score'}),
|
||||
new Element('span.provider', {'text': 'Provider'})
|
||||
).inject(self.release_container)
|
||||
|
||||
self.movie.data.releases.sortBy('-info.score').each(function(release){
|
||||
|
||||
var status = Status.get(release.status_id),
|
||||
quality = Quality.getProfile(release.quality_id) || {},
|
||||
info = release.info;
|
||||
|
||||
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
|
||||
new Element('div', {
|
||||
'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
|
||||
}).adopt(
|
||||
new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}),
|
||||
new Element('span.status', {'text': status.identifier, 'class': 'release_status '+status.identifier}),
|
||||
new Element('span.quality', {'text': quality.get('label') || 'n/a'}),
|
||||
new Element('span.size', {'text': release.info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}),
|
||||
new Element('span.age', {'text': self.get(release, 'age')}),
|
||||
new Element('span.score', {'text': self.get(release, 'score')}),
|
||||
new Element('span.provider', {'text': self.get(release, 'provider')}),
|
||||
release.info['detail_url'] ? new Element('a.info.icon', {
|
||||
'href': release.info['detail_url'],
|
||||
'target': '_blank'
|
||||
}) : null,
|
||||
new Element('a.download.icon', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
if(!this.hasClass('completed'))
|
||||
self.download(release);
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Element('a.delete.icon', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
self.ignore(release);
|
||||
this.getParent('.item').toggleClass('ignored')
|
||||
}
|
||||
}
|
||||
})
|
||||
).inject(self.release_container)
|
||||
});
|
||||
|
||||
self.trynext_container.adopt(
|
||||
new Element('span.or', {
|
||||
'text': 'Download'
|
||||
}),
|
||||
self.last_release ? new Element('a.button.orange', {
|
||||
'text': 'the same release again',
|
||||
'events': {
|
||||
'click': self.trySameRelease.bind(self)
|
||||
}
|
||||
}) : null,
|
||||
self.next_release && self.last_release ? new Element('span.or', {
|
||||
'text': 'or'
|
||||
}) : 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
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
self.movie.slide('in', self.options_container);
|
||||
},
|
||||
|
||||
get: function(release, type){
|
||||
return release.info[type] || 'n/a'
|
||||
},
|
||||
|
||||
download: function(release){
|
||||
var self = this;
|
||||
|
||||
var release_el = self.release_container.getElement('#release_'+release.id),
|
||||
icon = release_el.getElement('.download.icon');
|
||||
|
||||
icon.addClass('spinner');
|
||||
|
||||
Api.request('release.download', {
|
||||
'data': {
|
||||
'id': release.id
|
||||
},
|
||||
'onComplete': function(json){
|
||||
icon.removeClass('spinner')
|
||||
if(json.success)
|
||||
icon.addClass('completed');
|
||||
else
|
||||
icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
ignore: function(release){
|
||||
var self = this;
|
||||
|
||||
Api.request('release.ignore', {
|
||||
'data': {
|
||||
'id': release.id
|
||||
}
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
tryNextRelease: function(movie_id){
|
||||
var self = this;
|
||||
|
||||
if(self.last_release)
|
||||
self.ignore(self.last_release);
|
||||
|
||||
if(self.next_release)
|
||||
self.download(self.next_release);
|
||||
|
||||
},
|
||||
|
||||
trySameRelease: function(movie_id){
|
||||
var self = this;
|
||||
|
||||
if(self.last_release)
|
||||
self.download(self.last_release);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var TrailerAction = new Class({
|
||||
|
||||
Extends: MovieAction,
|
||||
id: null,
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.trailer', {
|
||||
'title': 'Watch the trailer of ' + self.movie.getTitle(),
|
||||
'events': {
|
||||
'click': self.watch.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
watch: function(offset){
|
||||
var self = this;
|
||||
|
||||
var data_url = 'http://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'
|
||||
var url = data_url.substitute({
|
||||
'title': encodeURI(self.movie.getTitle()),
|
||||
'year': self.movie.get('year'),
|
||||
'offset': offset || 1
|
||||
}),
|
||||
size = $(self.movie).getSize(),
|
||||
height = (size.x/16)*9,
|
||||
id = 'trailer-'+randomString();
|
||||
|
||||
self.player_container = new Element('div[id='+id+']');
|
||||
self.container = new Element('div.hide.trailer_container')
|
||||
.adopt(self.player_container)
|
||||
.inject(self.movie.container, 'top');
|
||||
|
||||
self.container.setStyle('height', 0);
|
||||
self.container.removeClass('hide');
|
||||
|
||||
self.close_button = new Element('a.hide.hide_trailer', {
|
||||
'text': 'Hide trailer',
|
||||
'events': {
|
||||
'click': self.stop.bind(self)
|
||||
}
|
||||
}).inject(self.movie);
|
||||
|
||||
setTimeout(function(){
|
||||
$(self.movie).setStyle('max-height', height);
|
||||
self.container.setStyle('height', height);
|
||||
}, 100)
|
||||
|
||||
new Request.JSONP({
|
||||
'url': url,
|
||||
'onComplete': function(json){
|
||||
var video_url = json.feed.entry[0].id.$t.split('/'),
|
||||
video_id = video_url[video_url.length-1];
|
||||
|
||||
self.player = new YT.Player(id, {
|
||||
'height': height,
|
||||
'width': size.x,
|
||||
'videoId': video_id,
|
||||
'playerVars': {
|
||||
'autoplay': 1,
|
||||
'showsearch': 0,
|
||||
'wmode': 'transparent',
|
||||
'iv_load_policy': 3
|
||||
}
|
||||
});
|
||||
|
||||
self.close_button.removeClass('hide');
|
||||
|
||||
var quality_set = false;
|
||||
var change_quality = function(state){
|
||||
if(!quality_set && (state.data == 1 || state.data || 2)){
|
||||
try {
|
||||
self.player.setPlaybackQuality('hd720');
|
||||
quality_set = true;
|
||||
}
|
||||
catch(e){
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
self.player.addEventListener('onStateChange', change_quality);
|
||||
|
||||
}
|
||||
}).send()
|
||||
|
||||
},
|
||||
|
||||
stop: function(){
|
||||
var self = this;
|
||||
|
||||
self.player.stopVideo();
|
||||
self.container.addClass('hide');
|
||||
self.close_button.addClass('hide');
|
||||
|
||||
setTimeout(function(){
|
||||
self.container.destroy()
|
||||
self.close_button.destroy();
|
||||
}, 1800)
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
@@ -91,10 +91,15 @@
|
||||
.movie_result {
|
||||
overflow: hidden;
|
||||
height: 140px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.movie_result .options {
|
||||
height: 140px;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 1px solid transparent;
|
||||
border-width: 1px 0;
|
||||
border-radius: 0;
|
||||
@@ -133,17 +138,21 @@
|
||||
|
||||
.movie_result .data {
|
||||
padding: 0 15px;
|
||||
width: 470px;
|
||||
position: relative;
|
||||
height: 140px;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
margin: -140px 0 0 0;
|
||||
left: 0;
|
||||
background: #5c697b;
|
||||
cursor: pointer;
|
||||
|
||||
border-bottom: 1px solid #333;
|
||||
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; }
|
||||
|
||||
@@ -182,6 +191,8 @@
|
||||
|
||||
.movie_result .info h2 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.movie_result .info h2 span {
|
||||
@@ -191,6 +202,12 @@
|
||||
.movie_result .info h2 span:before { content: "("; }
|
||||
.movie_result .info h2 span:after { content: ")"; }
|
||||
|
||||
.search_form .mask {
|
||||
.search_form .mask,
|
||||
.movie_result .mask {
|
||||
border-radius: 3px;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
@@ -19,7 +19,9 @@ Block.Search = new Class({
|
||||
self.hideResults(false)
|
||||
},
|
||||
'blur': function(){
|
||||
self.el.removeClass('focused')
|
||||
(function(){
|
||||
self.el.removeClass('focused')
|
||||
}).delay(2000);
|
||||
}
|
||||
}
|
||||
}),
|
||||
@@ -117,7 +119,7 @@ Block.Search = new Class({
|
||||
self.hideResults(false);
|
||||
|
||||
if(!cache){
|
||||
self.positionMask().fade('in');
|
||||
self.mask.fade('in');
|
||||
|
||||
if(!self.spinner)
|
||||
self.spinner = createSpinner(self.mask);
|
||||
@@ -139,7 +141,6 @@ Block.Search = new Class({
|
||||
fill: function(q, json){
|
||||
var self = this;
|
||||
|
||||
self.positionMask()
|
||||
self.cache[q] = json
|
||||
|
||||
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){
|
||||
this.el[bool ? 'addClass' : 'removeClass']('loading')
|
||||
},
|
||||
@@ -193,7 +181,7 @@ Block.Search = new Class({
|
||||
|
||||
Block.Search.Item = new Class({
|
||||
|
||||
initialize: function(info){
|
||||
initialize: function(info, options){
|
||||
var self = this;
|
||||
|
||||
self.info = info;
|
||||
@@ -203,14 +191,13 @@ Block.Search.Item = new Class({
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
var info = self.info;
|
||||
var self = this,
|
||||
info = self.info;
|
||||
|
||||
self.el = new Element('div.movie_result', {
|
||||
'id': info.imdb
|
||||
}).adopt(
|
||||
self.options = new Element('div.options.inlay'),
|
||||
self.options_el = new Element('div.options.inlay'),
|
||||
self.data_container = new Element('div.data', {
|
||||
'tween': {
|
||||
duration: 400,
|
||||
@@ -273,11 +260,7 @@ Block.Search.Item = new Class({
|
||||
|
||||
self.createOptions();
|
||||
|
||||
if(!self.width)
|
||||
self.width = self.data_container.getCoordinates().width
|
||||
|
||||
self.data_container.tween('left', 0, self.width);
|
||||
|
||||
self.data_container.addClass('open');
|
||||
self.el.addEvent('outerClick', self.closeOptions.bind(self))
|
||||
|
||||
},
|
||||
@@ -285,7 +268,7 @@ Block.Search.Item = new Class({
|
||||
closeOptions: function(){
|
||||
var self = this;
|
||||
|
||||
self.data_container.tween('left', self.width, 0);
|
||||
self.data_container.removeClass('open');
|
||||
self.el.removeEvents('outerClick')
|
||||
},
|
||||
|
||||
@@ -302,29 +285,32 @@ Block.Search.Item = new Class({
|
||||
'profile_id': self.profile_select.get('value')
|
||||
},
|
||||
'onComplete': function(json){
|
||||
self.options.empty();
|
||||
self.options.adopt(
|
||||
self.options_el.empty();
|
||||
self.options_el.adopt(
|
||||
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(){
|
||||
self.options.empty();
|
||||
self.options.adopt(
|
||||
self.options_el.empty();
|
||||
self.options_el.adopt(
|
||||
new Element('div.message', {
|
||||
'text': 'Something went wrong, check the logs for more info.'
|
||||
})
|
||||
);
|
||||
self.mask.fade('out');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
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){
|
||||
var in_library = [];
|
||||
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(
|
||||
self.option_thumbnail = self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', {
|
||||
'src': self.info.images.poster[0],
|
||||
self.thumbnail = (info.images && info.images.poster.length > 0) ? new Element('img.thumbnail', {
|
||||
'src': info.images.poster[0],
|
||||
'height': null,
|
||||
'width': null
|
||||
}) : null,
|
||||
self.info.in_wanted ? 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
|
||||
}) : (in_library ? new Element('span.in_library', {
|
||||
'text': 'Already in library: ' + in_library.join(', ')
|
||||
@@ -372,7 +358,7 @@ Block.Search.Item = new Class({
|
||||
}).inject(self.profile_select)
|
||||
});
|
||||
|
||||
self.options.addClass('set');
|
||||
self.options_el.addClass('set');
|
||||
}
|
||||
|
||||
},
|
||||
@@ -380,17 +366,7 @@ Block.Search.Item = new Class({
|
||||
loadingMask: function(){
|
||||
var self = this;
|
||||
|
||||
var s = self.options.getSize();
|
||||
|
||||
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')
|
||||
self.mask = new Element('div.mask').inject(self.el).fade('hide')
|
||||
|
||||
createSpinner(self.mask)
|
||||
self.mask.fade('in')
|
||||
|
||||
@@ -47,7 +47,6 @@ class ProfilePlugin(Plugin):
|
||||
for profile in profiles:
|
||||
temp.append(profile.to_dict(self.to_dict))
|
||||
|
||||
#db.close()
|
||||
return temp
|
||||
|
||||
def save(self):
|
||||
@@ -84,7 +83,6 @@ class ProfilePlugin(Plugin):
|
||||
|
||||
profile_dict = p.to_dict(self.to_dict)
|
||||
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'profile': profile_dict
|
||||
@@ -95,7 +93,6 @@ class ProfilePlugin(Plugin):
|
||||
db = get_session()
|
||||
default = db.query(Profile).first()
|
||||
default_dict = default.to_dict(self.to_dict)
|
||||
#db.close()
|
||||
|
||||
return default_dict
|
||||
|
||||
@@ -113,7 +110,6 @@ class ProfilePlugin(Plugin):
|
||||
order += 1
|
||||
|
||||
db.commit()
|
||||
#db.close()
|
||||
|
||||
return jsonified({
|
||||
'success': True
|
||||
@@ -137,8 +133,6 @@ class ProfilePlugin(Plugin):
|
||||
except Exception, e:
|
||||
message = log.error('Failed deleting Profile: %s', e)
|
||||
|
||||
#db.close()
|
||||
|
||||
return jsonified({
|
||||
'success': success,
|
||||
'message': message
|
||||
@@ -181,10 +175,10 @@ class ProfilePlugin(Plugin):
|
||||
)
|
||||
p.types.append(profile_type)
|
||||
|
||||
db.commit()
|
||||
quality_order += 1
|
||||
|
||||
order += 1
|
||||
|
||||
#db.close()
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
|
||||
@@ -86,7 +86,10 @@ var Profile = new Class({
|
||||
},
|
||||
'onComplete': function(json){
|
||||
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(
|
||||
self.finish = new Element('input.inlay.finish[type=checkbox]', {
|
||||
'checked': data.finish,
|
||||
'checked': data.finish !== undefined ? data.finish : 1,
|
||||
'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');
|
||||
|
||||
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.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Quality, Profile, ProfileType
|
||||
from sqlalchemy.sql.expression import or_
|
||||
import os.path
|
||||
import re
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -17,15 +19,15 @@ class QualityPlugin(Plugin):
|
||||
|
||||
qualities = [
|
||||
{'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': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
|
||||
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']},
|
||||
{'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, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
|
||||
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']},
|
||||
{'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': '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': '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']}
|
||||
]
|
||||
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
|
||||
@@ -68,7 +70,6 @@ class QualityPlugin(Plugin):
|
||||
q = mergeDicts(self.getQuality(quality.identifier), quality.to_dict())
|
||||
temp.append(q)
|
||||
|
||||
#db.close()
|
||||
return temp
|
||||
|
||||
def single(self, identifier = ''):
|
||||
@@ -76,11 +77,10 @@ class QualityPlugin(Plugin):
|
||||
db = get_session()
|
||||
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:
|
||||
quality_dict = dict(self.getQuality(quality.identifier), **quality.to_dict())
|
||||
|
||||
#db.close()
|
||||
return quality_dict
|
||||
|
||||
def getQuality(self, identifier):
|
||||
@@ -100,7 +100,6 @@ class QualityPlugin(Plugin):
|
||||
setattr(quality, params.get('value_type'), params.get('value'))
|
||||
db.commit()
|
||||
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': True
|
||||
})
|
||||
@@ -113,46 +112,48 @@ class QualityPlugin(Plugin):
|
||||
for q in self.qualities:
|
||||
|
||||
# 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'))
|
||||
quality = Quality()
|
||||
db.add(quality)
|
||||
qual = 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
|
||||
quality.identifier = q.get('identifier')
|
||||
quality.label = toUnicode(q.get('label'))
|
||||
quality.size_min, quality.size_max = q.get('size')
|
||||
db.add(qual)
|
||||
|
||||
# Create single quality profile
|
||||
profile = db.query(Profile).filter(
|
||||
prof = db.query(Profile).filter(
|
||||
Profile.core == True
|
||||
).filter(
|
||||
Profile.types.any(quality = quality)
|
||||
Profile.types.any(quality = qual)
|
||||
).all()
|
||||
|
||||
if not profile:
|
||||
if not prof:
|
||||
log.info('Creating profile: %s', q.get('label'))
|
||||
profile = Profile(
|
||||
prof = Profile(
|
||||
core = True,
|
||||
label = toUnicode(quality.label),
|
||||
label = toUnicode(qual.label),
|
||||
order = order
|
||||
)
|
||||
db.add(profile)
|
||||
db.add(prof)
|
||||
|
||||
profile_type = ProfileType(
|
||||
quality = quality,
|
||||
profile = profile,
|
||||
quality = qual,
|
||||
profile = prof,
|
||||
finish = True,
|
||||
order = 0
|
||||
)
|
||||
profile.types.append(profile_type)
|
||||
prof.types.append(profile_type)
|
||||
|
||||
order += 1
|
||||
db.commit()
|
||||
|
||||
#db.close()
|
||||
db.commit()
|
||||
|
||||
time.sleep(0.3) # Wait a moment
|
||||
|
||||
return True
|
||||
|
||||
def guess(self, files, extra = {}):
|
||||
@@ -198,9 +199,14 @@ class QualityPlugin(Plugin):
|
||||
|
||||
for quality in self.all():
|
||||
|
||||
# Last check on resolution only
|
||||
if quality.get('width', 480) == extra.get('resolution_width', 0):
|
||||
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width', 480), extra.get('resolution_width', 0)))
|
||||
# Check width resolution, range 20
|
||||
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', 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)
|
||||
|
||||
if 480 <= extra.get('resolution_width', 0) <= 720:
|
||||
|
||||
@@ -88,8 +88,6 @@ class Release(Plugin):
|
||||
|
||||
fireEvent('movie.restatus', movie.id)
|
||||
|
||||
#db.close()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -108,7 +106,6 @@ class Release(Plugin):
|
||||
|
||||
release_id = getParam('id')
|
||||
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': self.delete(release_id)
|
||||
})
|
||||
@@ -136,6 +133,9 @@ class Release(Plugin):
|
||||
db.delete(release_file)
|
||||
db.commit()
|
||||
|
||||
if len(rel.files) == 0:
|
||||
self.delete(id)
|
||||
|
||||
return True
|
||||
|
||||
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')
|
||||
db.commit()
|
||||
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': True
|
||||
})
|
||||
@@ -161,6 +160,7 @@ class Release(Plugin):
|
||||
|
||||
db = get_session()
|
||||
id = getParam('id')
|
||||
status_snatched = fireEvent('status.add', 'snatched', single = True)
|
||||
|
||||
rel = db.query(Relea).filter_by(id = id).first()
|
||||
if rel:
|
||||
@@ -181,14 +181,16 @@ class Release(Plugin):
|
||||
'files': {}
|
||||
}), manual = True, single = True)
|
||||
|
||||
#db.close()
|
||||
if success:
|
||||
rel.status_id = status_snatched.get('id')
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
'success': success
|
||||
})
|
||||
else:
|
||||
log.error('Couldn\'t find release with id: %s', id)
|
||||
|
||||
#db.close()
|
||||
return jsonified({
|
||||
'success': False
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from couchpotato.core.plugins.renamer.main import Renamer
|
||||
import os
|
||||
|
||||
def start():
|
||||
return Renamer()
|
||||
@@ -82,6 +83,15 @@ config = [{
|
||||
'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',
|
||||
},
|
||||
{
|
||||
'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,
|
||||
'name': 'next_on_failed',
|
||||
@@ -102,6 +112,15 @@ config = [{
|
||||
'label': 'Separator',
|
||||
'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.',
|
||||
},
|
||||
{
|
||||
'advanced': True,
|
||||
'name': 'ntfs_permission',
|
||||
'label': 'NTFS Permission',
|
||||
'type': 'bool',
|
||||
'hidden': os.name != 'nt',
|
||||
'description': 'Set permission of moved files to that of destination folder (Windows NTFS only).',
|
||||
'default': False,
|
||||
},
|
||||
],
|
||||
}, {
|
||||
'tab': 'renamer',
|
||||
@@ -124,13 +143,6 @@ config = [{
|
||||
'type': 'choice',
|
||||
'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.helpers.encoding import toUnicode, ss
|
||||
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.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Library, File, Profile, Release
|
||||
@@ -12,6 +13,7 @@ import errno
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -20,6 +22,7 @@ log = CPLog(__name__)
|
||||
class Renamer(Plugin):
|
||||
|
||||
renaming_started = False
|
||||
checking_snatched = False
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -31,8 +34,13 @@ class Renamer(Plugin):
|
||||
addEvent('renamer.check_snatched', self.checkSnatched)
|
||||
|
||||
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):
|
||||
|
||||
@@ -48,7 +56,7 @@ class Renamer(Plugin):
|
||||
return
|
||||
|
||||
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
|
||||
|
||||
# Check to see if the "to" folder is inside the "from" folder.
|
||||
@@ -127,6 +135,8 @@ class Renamer(Plugin):
|
||||
'resolution_width': group['meta_data'].get('resolution_width'),
|
||||
'resolution_height': group['meta_data'].get('resolution_height'),
|
||||
'imdb_id': library['identifier'],
|
||||
'cd': '',
|
||||
'cd_nr': '',
|
||||
}
|
||||
|
||||
for file_type in group['files']:
|
||||
@@ -144,7 +154,7 @@ class Renamer(Plugin):
|
||||
continue
|
||||
|
||||
# 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
|
||||
|
||||
for current_file in sorted(list(group['files'][file_type])):
|
||||
@@ -157,23 +167,19 @@ class Renamer(Plugin):
|
||||
replacements['ext'] = getExt(current_file)
|
||||
|
||||
# cd #
|
||||
replacements['cd'] = ' cd%d' % cd if cd else ''
|
||||
replacements['cd_nr'] = cd
|
||||
replacements['cd'] = ' cd%d' % cd if multiple else ''
|
||||
replacements['cd_nr'] = cd if multiple else ''
|
||||
|
||||
# Naming
|
||||
final_folder_name = self.doReplace(folder_name, replacements)
|
||||
final_file_name = self.doReplace(file_name, replacements)
|
||||
final_folder_name = self.doReplace(folder_name, replacements).lstrip('. ')
|
||||
final_file_name = self.doReplace(file_name, replacements).lstrip('. ')
|
||||
replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
|
||||
|
||||
# Group filename without cd extension
|
||||
replacements['cd'] = ''
|
||||
replacements['cd_nr'] = ''
|
||||
|
||||
# Meta naming
|
||||
if file_type is 'trailer':
|
||||
final_file_name = self.doReplace(trailer_name, replacements)
|
||||
final_file_name = self.doReplace(trailer_name, replacements, remove_multiple = True).lstrip('. ')
|
||||
elif file_type is 'nfo':
|
||||
final_file_name = self.doReplace(nfo_name, replacements)
|
||||
final_file_name = self.doReplace(nfo_name, replacements, remove_multiple = True).lstrip('. ')
|
||||
|
||||
# Seperator replace
|
||||
if separator:
|
||||
@@ -204,10 +210,16 @@ class Renamer(Plugin):
|
||||
# Check for extra subtitle files
|
||||
if file_type is 'subtitle':
|
||||
|
||||
# rename subtitles with or without language
|
||||
rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
|
||||
remove_multiple = False
|
||||
if len(group['files']['movie']) == 1:
|
||||
remove_multiple = True
|
||||
|
||||
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(
|
||||
extra_type = 'subtitle_extra',
|
||||
replacements = replacements,
|
||||
@@ -215,20 +227,19 @@ class Renamer(Plugin):
|
||||
file_name = file_name,
|
||||
destination = destination,
|
||||
group = group,
|
||||
current_file = current_file
|
||||
current_file = current_file,
|
||||
remove_multiple = remove_multiple,
|
||||
)
|
||||
|
||||
# Don't add language if multiple languages in 1 file
|
||||
if len(sub_langs) > 1:
|
||||
rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
|
||||
elif len(sub_langs) == 1:
|
||||
# Don't add language if multiple languages in 1 subtitle file
|
||||
if len(sub_langs) == 1:
|
||||
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 = mergeDicts(rename_files, rename_extras)
|
||||
|
||||
# Filename without cd etc
|
||||
if file_type is 'movie':
|
||||
elif file_type is 'movie':
|
||||
rename_extras = self.getRenameExtras(
|
||||
extra_type = 'movie_extra',
|
||||
replacements = replacements,
|
||||
@@ -240,7 +251,7 @@ class Renamer(Plugin):
|
||||
)
|
||||
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)
|
||||
|
||||
if multiple:
|
||||
@@ -265,6 +276,7 @@ class Renamer(Plugin):
|
||||
for profile_type in movie.profile.types:
|
||||
if profile_type.quality_id == group['meta_data']['quality']['id'] and profile_type.finish:
|
||||
movie.status_id = done_status.get('id')
|
||||
movie.last_edit = int(time.time())
|
||||
db.commit()
|
||||
except Exception, e:
|
||||
log.error('Failed marking movie finished: %s %s', (e, traceback.format_exc()))
|
||||
@@ -304,7 +316,12 @@ class Renamer(Plugin):
|
||||
elif release.status_id is snatched_status.get('id'):
|
||||
if release.quality.id is group['meta_data']['quality']['id']:
|
||||
log.debug('Marking release as downloaded')
|
||||
release.status_id = downloaded_status.get('id')
|
||||
try:
|
||||
release.status_id = downloaded_status.get('id')
|
||||
release.last_edit = int(time.time())
|
||||
except Exception, e:
|
||||
log.error('Failed marking release as finished: %s %s', (e, traceback.format_exc()))
|
||||
|
||||
db.commit()
|
||||
|
||||
# Remove leftover files
|
||||
@@ -328,6 +345,7 @@ class Renamer(Plugin):
|
||||
|
||||
log.info('Removing "%s"', src)
|
||||
try:
|
||||
src = ss(src)
|
||||
if os.path.isfile(src):
|
||||
os.remove(src)
|
||||
|
||||
@@ -341,7 +359,10 @@ class Renamer(Plugin):
|
||||
|
||||
# Delete leftover folder from older releases
|
||||
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
|
||||
group['renamed_files'] = []
|
||||
@@ -375,22 +396,22 @@ class Renamer(Plugin):
|
||||
except:
|
||||
log.error('Failed removing %s: %s', (group['parentdir'], traceback.format_exc()))
|
||||
|
||||
# Search for trailers etc
|
||||
fireEventAsync('renamer.after', group)
|
||||
|
||||
# Notify on download
|
||||
# Notify on download, search for trailers etc
|
||||
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
|
||||
if self.shuttingDown():
|
||||
break
|
||||
|
||||
#db.close()
|
||||
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 = {}
|
||||
|
||||
def test(s):
|
||||
@@ -399,8 +420,8 @@ class Renamer(Plugin):
|
||||
for extra in set(filter(test, group['files'][extra_type])):
|
||||
replacements['ext'] = getExt(extra)
|
||||
|
||||
final_folder_name = self.doReplace(folder_name, replacements)
|
||||
final_file_name = self.doReplace(file_name, replacements)
|
||||
final_folder_name = self.doReplace(folder_name, replacements, remove_multiple = remove_multiple)
|
||||
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)
|
||||
|
||||
return rename_files
|
||||
@@ -438,6 +459,8 @@ class Renamer(Plugin):
|
||||
|
||||
try:
|
||||
os.chmod(dest, Env.getPermission('file'))
|
||||
if os.name == 'nt' and self.conf('ntfs_permission'):
|
||||
os.popen('icacls "' + dest + '"* /reset /T')
|
||||
except:
|
||||
log.error('Failed setting permissions for file: %s, %s', (dest, traceback.format_exc(1)))
|
||||
|
||||
@@ -451,15 +474,20 @@ class Renamer(Plugin):
|
||||
|
||||
except:
|
||||
log.error('Couldn\'t move file "%s" to "%s": %s', (old, dest, traceback.format_exc()))
|
||||
raise Exception
|
||||
raise
|
||||
|
||||
return True
|
||||
|
||||
def doReplace(self, string, replacements):
|
||||
def doReplace(self, string, replacements, remove_multiple = False):
|
||||
'''
|
||||
replace confignames with the real thing
|
||||
'''
|
||||
|
||||
replacements = replacements.copy()
|
||||
if remove_multiple:
|
||||
replacements['cd'] = ''
|
||||
replacements['cd_nr'] = ''
|
||||
|
||||
replaced = toUnicode(string)
|
||||
for x, r in replacements.iteritems():
|
||||
if r is not None:
|
||||
@@ -477,6 +505,7 @@ class Renamer(Plugin):
|
||||
return string.replace(' ', ' ').replace(' .', '.')
|
||||
|
||||
def deleteEmptyFolder(self, folder, show_error = True):
|
||||
folder = ss(folder)
|
||||
|
||||
loge = log.error if show_error else log.debug
|
||||
for root, dirs, files in os.walk(folder):
|
||||
@@ -495,6 +524,11 @@ class Renamer(Plugin):
|
||||
loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
|
||||
|
||||
def checkSnatched(self):
|
||||
if self.checking_snatched:
|
||||
log.debug('Already checking snatched')
|
||||
|
||||
self.checking_snatched = True
|
||||
|
||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
||||
ignored_status = fireEvent('status.get', 'ignored', single = True)
|
||||
failed_status = fireEvent('status.get', 'failed', single = True)
|
||||
@@ -504,57 +538,71 @@ class Renamer(Plugin):
|
||||
db = get_session()
|
||||
rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all()
|
||||
|
||||
if rels:
|
||||
log.debug('Checking status snatched releases...')
|
||||
|
||||
scan_required = False
|
||||
|
||||
for rel in rels:
|
||||
if rels:
|
||||
self.checking_snatched = True
|
||||
log.debug('Checking status snatched releases...')
|
||||
|
||||
# Get current selected title
|
||||
default_title = ''
|
||||
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:
|
||||
statuses = fireEvent('download.status', merge = True)
|
||||
if not statuses:
|
||||
log.debug('Download status functionality is not implemented for active downloaders.')
|
||||
scan_required = True
|
||||
else:
|
||||
log.debug('Download status: %s' , downloadstatus)
|
||||
try:
|
||||
for rel in rels:
|
||||
rel_dict = rel.to_dict({'info': {}})
|
||||
|
||||
if downloadstatus == 'failed':
|
||||
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()
|
||||
# Get current selected title
|
||||
default_title = getTitle(rel.movie.library)
|
||||
|
||||
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')
|
||||
rel.last_edit = int(time.time())
|
||||
db.commit()
|
||||
continue
|
||||
|
||||
elif downloadstatus == 'completed':
|
||||
log.info('Download of %s completed!', item['name'])
|
||||
scan_required = True
|
||||
movie_dict = fireEvent('movie.get', rel.movie_id, single = True)
|
||||
|
||||
elif downloadstatus == 'not_found':
|
||||
log.info('%s not found in downloaders', item['name'])
|
||||
rel.status_id = ignored_status.get('id')
|
||||
db.commit()
|
||||
# check status
|
||||
nzbname = self.createNzbName(rel_dict['info'], movie_dict)
|
||||
|
||||
found = False
|
||||
for item in statuses:
|
||||
if item['name'] == nzbname or rel_dict['info']['name'] in item['name'] 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')
|
||||
rel.last_edit = int(time.time())
|
||||
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:
|
||||
fireEvent('renamer.scan')
|
||||
|
||||
self.checking_snatched = False
|
||||
|
||||
return True
|
||||
|
||||
@@ -23,7 +23,7 @@ class Scanner(Plugin):
|
||||
'media': 314572800, # 300MB
|
||||
'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 = [os.path.sep + 'extracted' + os.path.sep, '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']
|
||||
extensions = {
|
||||
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts', 'm4v'],
|
||||
@@ -34,6 +34,7 @@ class Scanner(Plugin):
|
||||
'subtitle_extra': ['idx'],
|
||||
'trailer': ['mov', 'mp4', 'flv']
|
||||
}
|
||||
|
||||
file_types = {
|
||||
'subtitle': ('subtitle', 'subtitle'),
|
||||
'subtitle_extra': ('subtitle', 'subtitle_extra'),
|
||||
@@ -42,6 +43,8 @@ class Scanner(Plugin):
|
||||
'movie': ('video', 'movie'),
|
||||
'movie_extra': ('movie', 'movie_extra'),
|
||||
'backdrop': ('image', 'backdrop'),
|
||||
'poster': ('image', 'poster'),
|
||||
'thumbnail': ('image', 'thumbnail'),
|
||||
'leftover': ('leftover', 'leftover'),
|
||||
}
|
||||
|
||||
@@ -50,6 +53,20 @@ class Scanner(Plugin):
|
||||
'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 = {
|
||||
'bluray': ['bluray', 'blu-ray', 'brrip', 'br-rip'],
|
||||
'hddvd': ['hddvd', 'hd-dvd'],
|
||||
@@ -72,7 +89,7 @@ class Scanner(Plugin):
|
||||
'()([ab])(\.....?)$' #*a.mkv
|
||||
]
|
||||
|
||||
cp_imdb = '(\.cp\((?P<id>tt[0-9{7}]+)\))'
|
||||
cp_imdb = '(.cp.(?P<id>tt[0-9{7}]+).)'
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -80,55 +97,10 @@ class Scanner(Plugin):
|
||||
addEvent('scanner.remove_cptag', self.removeCPTag)
|
||||
|
||||
addEvent('scanner.scan', self.scan)
|
||||
addEvent('scanner.files', self.scanFilesToLibrary)
|
||||
addEvent('scanner.folder', self.scanFolderToLibrary)
|
||||
addEvent('scanner.name_year', self.getReleaseNameYear)
|
||||
addEvent('scanner.partnumber', self.getPartNumber)
|
||||
|
||||
def after_rename(group):
|
||||
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):
|
||||
def scan(self, folder = None, files = None, simple = False, newer_than = 0, on_found = None):
|
||||
|
||||
folder = ss(os.path.normpath(folder))
|
||||
|
||||
@@ -141,7 +113,8 @@ class Scanner(Plugin):
|
||||
leftovers = []
|
||||
|
||||
# Scan all files of the folder if no files are set
|
||||
if len(files) == 0:
|
||||
if not files:
|
||||
check_file_date = True
|
||||
try:
|
||||
files = []
|
||||
for root, dirs, walk_files in os.walk(folder):
|
||||
@@ -150,6 +123,7 @@ class Scanner(Plugin):
|
||||
except:
|
||||
log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
|
||||
else:
|
||||
check_file_date = False
|
||||
files = [ss(x) for x in files]
|
||||
|
||||
db = get_session()
|
||||
@@ -279,8 +253,8 @@ class Scanner(Plugin):
|
||||
del path_identifiers[identifier]
|
||||
del delete_identifiers
|
||||
|
||||
# Determine file types
|
||||
processed_movies = {}
|
||||
# Make sure we remove older / still extracting files
|
||||
valid_files = {}
|
||||
while True and not self.shuttingDown():
|
||||
try:
|
||||
identifier, group = movie_files.popitem()
|
||||
@@ -302,7 +276,7 @@ class Scanner(Plugin):
|
||||
if file_too_new:
|
||||
break
|
||||
|
||||
if file_too_new:
|
||||
if check_file_date and file_too_new:
|
||||
try:
|
||||
time_string = time.ctime(file_time[0])
|
||||
except:
|
||||
@@ -320,17 +294,33 @@ class Scanner(Plugin):
|
||||
|
||||
# Only process movies newer than x
|
||||
if newer_than and newer_than > 0:
|
||||
has_new_files = False
|
||||
for cur_file in group['unsorted_files']:
|
||||
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
|
||||
|
||||
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
|
||||
del group['unsorted_files']
|
||||
# Delete the unsorted list
|
||||
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
|
||||
# images = self.getImages(group['unsorted_files'])
|
||||
@@ -351,11 +341,11 @@ class Scanner(Plugin):
|
||||
group['files']['movie'] = self.getMediaFiles(group['unsorted_files'])
|
||||
|
||||
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
|
||||
|
||||
log.debug('Getting metadata for %s', identifier)
|
||||
group['meta_data'] = self.getMetaData(group)
|
||||
group['meta_data'] = self.getMetaData(group, folder = folder)
|
||||
|
||||
# Subtitle meta
|
||||
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()
|
||||
group['movie_id'] = None if not movie else movie.id
|
||||
|
||||
|
||||
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:
|
||||
log.info('Found %s movies in the folder %s', (len(processed_movies), folder))
|
||||
else:
|
||||
@@ -402,7 +395,7 @@ class Scanner(Plugin):
|
||||
|
||||
return processed_movies
|
||||
|
||||
def getMetaData(self, group):
|
||||
def getMetaData(self, group, folder = ''):
|
||||
|
||||
data = {}
|
||||
files = list(group['files']['movie'])
|
||||
@@ -428,10 +421,10 @@ class Scanner(Plugin):
|
||||
if not data['quality']:
|
||||
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])
|
||||
data['group'] = self.getGroup(filename)
|
||||
data['group'] = self.getGroup(filename[len(folder):])
|
||||
data['source'] = self.getSourceMedia(filename)
|
||||
|
||||
return data
|
||||
@@ -440,9 +433,18 @@ class Scanner(Plugin):
|
||||
|
||||
try:
|
||||
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 {
|
||||
'video': p.video[0].codec,
|
||||
'audio': p.audio[0].codec,
|
||||
'video': vc,
|
||||
'audio': ac,
|
||||
'resolution_width': tryInt(p.video[0].width),
|
||||
'resolution_height': tryInt(p.video[0].height),
|
||||
}
|
||||
@@ -539,7 +541,6 @@ class Scanner(Plugin):
|
||||
break
|
||||
except:
|
||||
pass
|
||||
#db.close()
|
||||
|
||||
# Search based on OpenSubtitleHash
|
||||
if not imdb_id and not group['is_dvd']:
|
||||
@@ -760,8 +761,8 @@ class Scanner(Plugin):
|
||||
|
||||
def getGroup(self, file):
|
||||
try:
|
||||
match = re.search('-(?P<group>[A-Z0-9]+).', file, re.I)
|
||||
return match.group('group') or ''
|
||||
match = re.findall('\-([A-Z0-9]+)[\.\/]', file, re.I)
|
||||
return match[-1] or ''
|
||||
except:
|
||||
return ''
|
||||
|
||||
@@ -774,7 +775,7 @@ class Scanner(Plugin):
|
||||
return None
|
||||
|
||||
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:
|
||||
return matches.group('year')
|
||||
|
||||
|
||||
@@ -27,9 +27,9 @@ class Score(Plugin):
|
||||
score += sizeScore(nzb['size'])
|
||||
|
||||
# Torrents only
|
||||
if nzb.get('seeds'):
|
||||
if nzb.get('seeders'):
|
||||
try:
|
||||
score += nzb.get('seeds') / 5
|
||||
score += nzb.get('seeders') / 5
|
||||
score += nzb.get('leechers') / 10
|
||||
except:
|
||||
pass
|
||||
|
||||
@@ -10,7 +10,7 @@ name_scores = [
|
||||
# Video
|
||||
'x264:1', 'h264:1',
|
||||
# Audio
|
||||
'DTS:4', 'AC3:2',
|
||||
'dts:4', 'ac3:2',
|
||||
# Quality
|
||||
'720p:10', '1080p:10', 'bluray:10', 'dvd:1', 'dvdrip:1', 'brrip:1', 'bdrip:1', 'bd50:1', 'bd25:1',
|
||||
# Language / Subs
|
||||
@@ -116,7 +116,7 @@ def sizeScore(size):
|
||||
|
||||
|
||||
def providerScore(provider):
|
||||
if provider in ['NZBMatrix', 'Nzbs', 'Newzbin']:
|
||||
if provider in ['OMGWTFNZBs', 'PassThePopcorn', 'SceneAccess', 'TorrentLeech']:
|
||||
return 20
|
||||
|
||||
if provider in ['Newznab']:
|
||||
|
||||
@@ -24,18 +24,19 @@ config = [{
|
||||
'name': 'required_words',
|
||||
'label': 'Required words',
|
||||
'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',
|
||||
'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',
|
||||
'label': 'First search',
|
||||
'description': 'Which of the methods do you prefer',
|
||||
'default': 'nzb',
|
||||
'default': 'both',
|
||||
'type': 'dropdown',
|
||||
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')],
|
||||
},
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from couchpotato import get_session
|
||||
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.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.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
||||
@@ -11,6 +12,7 @@ from couchpotato.environment import Env
|
||||
from inspect import ismethod, isfunction
|
||||
from sqlalchemy.exc import InterfaceError
|
||||
import datetime
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
@@ -28,6 +30,7 @@ class Searcher(Plugin):
|
||||
addEvent('searcher.correct_movie', self.correctMovie)
|
||||
addEvent('searcher.download', self.download)
|
||||
addEvent('searcher.try_next_release', self.tryNextRelease)
|
||||
addEvent('searcher.could_be_released', self.couldBeReleased)
|
||||
|
||||
addApiView('searcher.try_next', self.tryNextReleaseView, docs = {
|
||||
'desc': 'Marks the snatched results as ignored and try the next best release',
|
||||
@@ -36,9 +39,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
|
||||
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):
|
||||
|
||||
@@ -53,30 +85,51 @@ class Searcher(Plugin):
|
||||
movies = db.query(Movie).filter(
|
||||
Movie.status.has(identifier = 'active')
|
||||
).all()
|
||||
random.shuffle(movies)
|
||||
|
||||
for movie in movies:
|
||||
movie_dict = movie.to_dict({
|
||||
'profile': {'types': {'quality': {}}},
|
||||
'releases': {'status': {}, 'quality': {}},
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {}
|
||||
})
|
||||
self.in_progress = {
|
||||
'total': len(movies),
|
||||
'to_go': len(movies),
|
||||
}
|
||||
|
||||
try:
|
||||
self.single(movie_dict)
|
||||
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()))
|
||||
try:
|
||||
search_types = self.getSearchTypes()
|
||||
|
||||
# Break if CP wants to shut down
|
||||
if self.shuttingDown():
|
||||
break
|
||||
for movie in movies:
|
||||
movie_dict = movie.to_dict({
|
||||
'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
|
||||
|
||||
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)
|
||||
|
||||
@@ -91,6 +144,8 @@ class Searcher(Plugin):
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
ignored_status = fireEvent('status.get', 'ignored', single = True)
|
||||
|
||||
found_releases = []
|
||||
|
||||
default_title = getTitle(movie['library'])
|
||||
if not default_title:
|
||||
log.error('No proper info found for movie, removing it from library to cause it from having more issues.')
|
||||
@@ -99,9 +154,10 @@ class Searcher(Plugin):
|
||||
|
||||
fireEvent('notify.frontend', type = 'searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title)
|
||||
|
||||
|
||||
ret = False
|
||||
for quality_type in movie['profile']['types']:
|
||||
if not self.couldBeReleased(quality_type['quality']['identifier'], release_dates, pre_releases):
|
||||
if not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates):
|
||||
log.info('Too early to search for %s, %s', (quality_type['quality']['identifier'], default_title))
|
||||
continue
|
||||
|
||||
@@ -118,7 +174,11 @@ class Searcher(Plugin):
|
||||
log.info('Search for %s in %s', (default_title, quality_type['quality']['label']))
|
||||
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
|
||||
|
||||
results = 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)
|
||||
if len(sorted_results) == 0:
|
||||
@@ -135,19 +195,23 @@ class Searcher(Plugin):
|
||||
# Add them to this movie releases list
|
||||
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:
|
||||
rls = Release(
|
||||
identifier = md5(nzb['url']),
|
||||
identifier = nzb_identifier,
|
||||
movie_id = movie.get('id'),
|
||||
quality_id = quality_type.get('quality_id'),
|
||||
status_id = available_status.get('id')
|
||||
)
|
||||
db.add(rls)
|
||||
db.commit()
|
||||
else:
|
||||
[db.delete(info) for info in rls.info]
|
||||
db.commit()
|
||||
[db.delete(old_info) for old_info in rls.info]
|
||||
rls.last_edit = int(time.time())
|
||||
|
||||
db.commit()
|
||||
|
||||
for info in nzb:
|
||||
try:
|
||||
@@ -159,14 +223,19 @@ class Searcher(Plugin):
|
||||
value = toUnicode(nzb[info])
|
||||
)
|
||||
rls.info.append(rls_info)
|
||||
db.commit()
|
||||
except InterfaceError:
|
||||
log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
|
||||
|
||||
db.commit()
|
||||
|
||||
nzb['status_id'] = rls.status_id
|
||||
|
||||
|
||||
for nzb in sorted_results:
|
||||
if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and nzb.get('age') <= quality_type.get('wait_for', 0):
|
||||
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
|
||||
continue
|
||||
|
||||
if nzb['status_id'] == ignored_status.get('id'):
|
||||
log.info('Ignored: %s', nzb['name'])
|
||||
continue
|
||||
@@ -181,6 +250,12 @@ class Searcher(Plugin):
|
||||
break
|
||||
elif downloaded != 'try_next':
|
||||
break
|
||||
|
||||
# Remove releases that aren't found anymore
|
||||
for release in movie.get('releases', []):
|
||||
if release.get('status_id') == available_status.get('id') and release.get('identifier') not in found_releases:
|
||||
fireEvent('release.delete', release.get('id'), single = True)
|
||||
|
||||
else:
|
||||
log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title))
|
||||
fireEvent('movie.restatus', movie['id'])
|
||||
@@ -192,111 +267,149 @@ class Searcher(Plugin):
|
||||
|
||||
fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
|
||||
|
||||
#db.close()
|
||||
return ret
|
||||
|
||||
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
|
||||
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
|
||||
if downloader_enabled:
|
||||
|
||||
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
|
||||
db = get_session()
|
||||
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
|
||||
rls.status_id = snatched_status.get('id')
|
||||
db.commit()
|
||||
successful = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
|
||||
|
||||
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
|
||||
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
|
||||
log.info(snatch_message)
|
||||
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
|
||||
if successful:
|
||||
|
||||
|
||||
# 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:
|
||||
if movie['status_id'] == active_status.get('id'):
|
||||
for profile_type in movie['profile']['types']:
|
||||
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
|
||||
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
|
||||
# Mark release as snatched
|
||||
db = get_session()
|
||||
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
|
||||
if rls:
|
||||
renamer_enabled = Env.setting('enabled', 'renamer')
|
||||
|
||||
# Mark release done
|
||||
rls.status_id = done_status.get('id')
|
||||
db.commit()
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
rls.status_id = done_status.get('id') if not renamer_enabled else snatched_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 Exception, e:
|
||||
log.error('Failed marking movie finished: %s %s', (e, traceback.format_exc()))
|
||||
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
|
||||
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
|
||||
log.info(snatch_message)
|
||||
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
|
||||
|
||||
#db.close()
|
||||
return True
|
||||
# If renamer isn't used, mark movie done
|
||||
if not renamer_enabled:
|
||||
active_status = fireEvent('status.get', 'active', single = True)
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
try:
|
||||
if movie['status_id'] == active_status.get('id'):
|
||||
for profile_type in movie['profile']['types']:
|
||||
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
|
||||
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
|
||||
|
||||
# Mark release done
|
||||
rls.status_id = done_status.get('id')
|
||||
rls.last_edit = int(time.time())
|
||||
db.commit()
|
||||
|
||||
# Mark movie done
|
||||
mvie = db.query(Movie).filter_by(id = movie['id']).first()
|
||||
mvie.status_id = done_status.get('id')
|
||||
mvie.last_edit = int(time.time())
|
||||
db.commit()
|
||||
except:
|
||||
log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc())
|
||||
|
||||
except:
|
||||
log.error('Failed marking movie finished: %s', traceback.format_exc())
|
||||
|
||||
return True
|
||||
|
||||
log.info('Tried to download, but none of the "%s" downloaders are enabled', (data.get('type', '')))
|
||||
|
||||
log.info('Tried to download, but none of the downloaders are enabled')
|
||||
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):
|
||||
|
||||
imdb_results = kwargs.get('imdb_results', False)
|
||||
retention = Env.setting('retention', section = 'nzb')
|
||||
|
||||
if nzb.get('seeds') is None and retention < nzb.get('age', 0):
|
||||
log.info('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
|
||||
if nzb.get('seeders') is None and 0 < retention < nzb.get('age', 0):
|
||||
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
|
||||
return False
|
||||
|
||||
movie_name = getTitle(movie['library'])
|
||||
movie_words = re.split('\W+', simplifyString(movie_name))
|
||||
nzb_name = simplifyString(nzb['name'])
|
||||
nzb_words = re.split('\W+', nzb_name)
|
||||
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)):
|
||||
log.info("Wrong: Required word missing: %s" % nzb['name'])
|
||||
req_match = 0
|
||||
for req_set in required_words:
|
||||
req = splitString(req_set, '&')
|
||||
req_match += len(list(set(nzb_words) & set(req))) == len(req)
|
||||
|
||||
if self.conf('required_words') and req_match == 0:
|
||||
log.info2("Wrong: Required word missing: %s" % nzb['name'])
|
||||
return False
|
||||
|
||||
ignored_words = [x.strip().lower() for x in self.conf('ignored_words').split(',')]
|
||||
blacklisted = list(set(nzb_words) & set(ignored_words))
|
||||
ignored_words = splitString(self.conf('ignored_words').lower())
|
||||
blacklisted = list(set(nzb_words) & set(ignored_words) - set(movie_words))
|
||||
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
|
||||
|
||||
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic']
|
||||
for p_tag in pron_tags:
|
||||
if p_tag in nzb_words and p_tag not in movie_words:
|
||||
log.info('Wrong: %s, probably pr0n', (nzb['name']))
|
||||
return False
|
||||
pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words))
|
||||
if pron_words:
|
||||
log.info('Wrong: %s, probably pr0n', (nzb['name']))
|
||||
return False
|
||||
|
||||
#qualities = fireEvent('quality.all', single = True)
|
||||
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
|
||||
|
||||
# Contains lower quality string
|
||||
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
|
||||
|
||||
|
||||
# File to small
|
||||
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
|
||||
|
||||
# File to large
|
||||
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
|
||||
|
||||
|
||||
@@ -314,20 +427,21 @@ class Searcher(Plugin):
|
||||
return True
|
||||
|
||||
# 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
|
||||
|
||||
for movie_title in movie['library']['titles']:
|
||||
movie_words = re.split('\W+', simplifyString(movie_title['title']))
|
||||
for raw_title in movie['library']['titles']:
|
||||
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 no IMDB link, at least check year range 1
|
||||
if len(movie_words) > 2 and self.correctYear([nzb['name']], movie['library']['year'], 1):
|
||||
return True
|
||||
if self.correctName(nzb['name'], movie_title):
|
||||
# if no IMDB link, at least check year range 1
|
||||
if len(movie_words) > 2 and self.correctYear([nzb['name']], movie['library']['year'], 1):
|
||||
return True
|
||||
|
||||
# if no IMDB link, at least check year
|
||||
if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
|
||||
return True
|
||||
# if no IMDB link, at least check year
|
||||
if len(movie_words) <= 2 and self.correctYear([nzb['name']], movie['library']['year'], 0):
|
||||
return True
|
||||
|
||||
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie_name, movie['library']['year']))
|
||||
return False
|
||||
@@ -350,13 +464,20 @@ class Searcher(Plugin):
|
||||
if list(set(nzb_words) & set(quality['alternative'])):
|
||||
found[quality['identifier']] = True
|
||||
|
||||
# Try guessing via quality tags
|
||||
guess = fireEvent('quality.guess', [nzb.get('name')], single = True)
|
||||
if guess:
|
||||
found[guess['identifier']] = True
|
||||
|
||||
# Hack for older movies that don't contain quality tag
|
||||
year_name = fireEvent('scanner.name_year', name, single = True)
|
||||
if 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
|
||||
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
|
||||
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
|
||||
for allowed in preferred_quality.get('allow'):
|
||||
@@ -373,23 +494,32 @@ class Searcher(Plugin):
|
||||
|
||||
return False
|
||||
|
||||
def correctYear(self, haystack, year, range):
|
||||
def correctYear(self, haystack, year, year_range):
|
||||
|
||||
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
|
||||
|
||||
log.debug('Movie year doesn\'t matche range: %s looking for %s', (year_name.get('year'), year))
|
||||
return False
|
||||
|
||||
def correctName(self, check_name, movie_name):
|
||||
|
||||
check_names = [check_name]
|
||||
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)
|
||||
|
||||
try:
|
||||
@@ -403,14 +533,19 @@ class Searcher(Plugin):
|
||||
|
||||
return False
|
||||
|
||||
def couldBeReleased(self, wanted_quality, dates, pre_releases):
|
||||
def couldBeReleased(self, is_pre_release, dates):
|
||||
|
||||
now = int(time.time())
|
||||
|
||||
if not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0):
|
||||
return True
|
||||
else:
|
||||
if wanted_quality in pre_releases:
|
||||
|
||||
# For movies before 1972
|
||||
if dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
|
||||
return True
|
||||
|
||||
if is_pre_release:
|
||||
# Prerelease 1 week before theaters
|
||||
if dates.get('theater') - 604800 < now:
|
||||
return True
|
||||
@@ -465,3 +600,12 @@ class Searcher(Plugin):
|
||||
except:
|
||||
log.error('Failed searching for next release: %s', traceback.format_exc())
|
||||
return False
|
||||
|
||||
class SearchSetupError(Exception):
|
||||
pass
|
||||
|
||||
class NoDownloaders(SearchSetupError):
|
||||
pass
|
||||
|
||||
class NoProviders(SearchSetupError):
|
||||
pass
|
||||
|
||||
@@ -22,6 +22,8 @@ class StatusPlugin(Plugin):
|
||||
'failed': 'Failed',
|
||||
'deleted': 'Deleted',
|
||||
'ignored': 'Ignored',
|
||||
'available': 'Available',
|
||||
'suggest': 'Suggest',
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -8,9 +8,9 @@ config = [{
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'renamer',
|
||||
'subtab': 'subtitles',
|
||||
'name': 'subtitle',
|
||||
'label': 'Download subtitles after rename',
|
||||
'label': 'Download subtitles',
|
||||
'description': 'after rename',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Library, FileType
|
||||
@@ -13,7 +14,7 @@ log = CPLog(__name__)
|
||||
|
||||
class Subtitle(Plugin):
|
||||
|
||||
services = ['opensubtitles', 'thesubdb', 'subswiki']
|
||||
services = ['opensubtitles', 'thesubdb', 'subswiki', 'podnapisi']
|
||||
|
||||
def __init__(self):
|
||||
addEvent('renamer.before', self.searchSingle)
|
||||
@@ -40,8 +41,6 @@ class Subtitle(Plugin):
|
||||
# get subtitles for those files
|
||||
subliminal.list_subtitles(files, cache_dir = Env.get('cache_dir'), multi = True, languages = self.getLanguages(), services = self.services)
|
||||
|
||||
#db.close()
|
||||
|
||||
def searchSingle(self, group):
|
||||
|
||||
if self.isDisabled(): return
|
||||
@@ -50,6 +49,7 @@ class Subtitle(Plugin):
|
||||
available_languages = sum(group['subtitle_language'].itervalues(), [])
|
||||
downloaded = []
|
||||
files = [toUnicode(x) for x in group['files']['movie']]
|
||||
log.debug('Searching for subtitles for: %s', files)
|
||||
|
||||
for lang in self.getLanguages():
|
||||
if lang not in available_languages:
|
||||
@@ -58,6 +58,7 @@ class Subtitle(Plugin):
|
||||
downloaded.extend(download[subtitle])
|
||||
|
||||
for d_sub in downloaded:
|
||||
log.info('Found subtitle (%s): %s', (d_sub.language.alpha2, files))
|
||||
group['files']['subtitle'].add(d_sub.path)
|
||||
group['subtitle_language'][d_sub.path] = [d_sub.language.alpha2]
|
||||
|
||||
@@ -69,4 +70,4 @@ class Subtitle(Plugin):
|
||||
return False
|
||||
|
||||
def getLanguages(self):
|
||||
return [x.strip() for x in self.conf('languages').split(',')]
|
||||
return splitString(self.conf('languages'))
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
|
||||
class Suggestion(Plugin):
|
||||
|
||||
pass
|
||||
def __init__(self):
|
||||
|
||||
addApiView('suggestion.view', self.getView)
|
||||
|
||||
def getView(self):
|
||||
|
||||
limit_offset = getParam('limit_offset', None)
|
||||
total_movies, movies = fireEvent('movie.list', status = 'suggest', limit_offset = limit_offset, single = True)
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'total': total_movies,
|
||||
'movies': movies,
|
||||
})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user