Compare commits
449 Commits
build/2.0.
...
build/2.2.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d08c7c57a8 | ||
|
|
eeeb845ef3 | ||
|
|
651a063f94 | ||
|
|
f20aaa2d9d | ||
|
|
ba925ec191 | ||
|
|
f67c6fe8be | ||
|
|
8d38fa87a4 | ||
|
|
7c79c6d1f3 | ||
|
|
b0781b45f8 | ||
|
|
ee53539906 | ||
|
|
c8ab6a06fb | ||
|
|
c75ac51eb7 | ||
|
|
33d7d994d4 | ||
|
|
96291f63da | ||
|
|
6464bb065d | ||
|
|
8b45b6f1a0 | ||
|
|
70ba5d80cd | ||
|
|
ac30152930 | ||
|
|
ad01a3da4d | ||
|
|
5f5f17112a | ||
|
|
156da670e8 | ||
|
|
821c26f35b | ||
|
|
a092f394fa | ||
|
|
18e3194e27 | ||
|
|
08a1e1e582 | ||
|
|
074005ed02 | ||
|
|
7660a3d78f | ||
|
|
9211e60804 | ||
|
|
87f295be28 | ||
|
|
cfa89c8921 | ||
|
|
70f834d925 | ||
|
|
6b4e4fd440 | ||
|
|
b83b2453a0 | ||
|
|
82d31d996d | ||
|
|
4faa617039 | ||
|
|
a1d2276668 | ||
|
|
19c50f728e | ||
|
|
a94307c59f | ||
|
|
c6403e87f1 | ||
|
|
b56cd3439e | ||
|
|
25693d44eb | ||
|
|
43af25a30e | ||
|
|
023278e0c0 | ||
|
|
0634c79f74 | ||
|
|
31b3c2ef64 | ||
|
|
4a71f2c556 | ||
|
|
9783409756 | ||
|
|
c7e85c00ca | ||
|
|
94647bbb57 | ||
|
|
1aa26a5a6c | ||
|
|
df13a0edc2 | ||
|
|
52a0de3b59 | ||
|
|
38886b28f7 | ||
|
|
226cf6fc38 | ||
|
|
203a52bfd1 | ||
|
|
1b6bf13619 | ||
|
|
bc94e90994 | ||
|
|
347125365f | ||
|
|
59a718be20 | ||
|
|
c41b3a612a | ||
|
|
23f77df911 | ||
|
|
117b952455 | ||
|
|
7714504831 | ||
|
|
5c61c24c04 | ||
|
|
b11e1d48e0 | ||
|
|
a6ce114284 | ||
|
|
88d512eacc | ||
|
|
f4d5366c93 | ||
|
|
ac9aaec7b8 | ||
|
|
0c5b950c87 | ||
|
|
47141f8e4f | ||
|
|
ec302fe665 | ||
|
|
7f304b0c28 | ||
|
|
8f88f7d89b | ||
|
|
400fd461ab | ||
|
|
cd8d2d4808 | ||
|
|
4cfa79488f | ||
|
|
b5993bcc21 | ||
|
|
6af00bf026 | ||
|
|
97c456c9e1 | ||
|
|
08f44197f3 | ||
|
|
779c7d2942 | ||
|
|
7fd14e0283 | ||
|
|
7d32a8750d | ||
|
|
110e0b78fc | ||
|
|
bc77812488 | ||
|
|
3e28cd5c95 | ||
|
|
2715dbaaa5 | ||
|
|
3baf12d3e4 | ||
|
|
a428d36604 | ||
|
|
b5207bc88c | ||
|
|
910578a2ac | ||
|
|
88176997e7 | ||
|
|
233e6f9be0 | ||
|
|
1fd11fb547 | ||
|
|
8bfd206578 | ||
|
|
62c6fd2e40 | ||
|
|
ac2d2a0463 | ||
|
|
c1e4b47b99 | ||
|
|
32b479467a | ||
|
|
6cab2b34d6 | ||
|
|
9e744199fe | ||
|
|
b22021e7f0 | ||
|
|
68bdf47ea4 | ||
|
|
af2876bd71 | ||
|
|
1e5d6bad2a | ||
|
|
f6c836157d | ||
|
|
d10874f216 | ||
|
|
700713abcf | ||
|
|
5180426fc1 | ||
|
|
e1c8a08f2f | ||
|
|
16f0bcc3ac | ||
|
|
9c98a38604 | ||
|
|
1b03c7e474 | ||
|
|
689feb78d0 | ||
|
|
336b15b199 | ||
|
|
4a4bb819ec | ||
|
|
48be010f33 | ||
|
|
104e21b314 | ||
|
|
aaf5cab138 | ||
|
|
22b744340a | ||
|
|
2954558004 | ||
|
|
b797590a4e | ||
|
|
9d71fe1724 | ||
|
|
9ad0ed642d | ||
|
|
cbd217271d | ||
|
|
65896497fb | ||
|
|
54a37b577d | ||
|
|
f1948ffb6a | ||
|
|
7dd3b0ed15 | ||
|
|
11fcfa8202 | ||
|
|
199e61ea14 | ||
|
|
0daa6c8eff | ||
|
|
b1b5f97f03 | ||
|
|
32d5587669 | ||
|
|
c13c0f24e5 | ||
|
|
7eb1d72333 | ||
|
|
3d6ec1feba | ||
|
|
c267232160 | ||
|
|
48f4b008df | ||
|
|
ae1f181fbf | ||
|
|
cbfee72d51 | ||
|
|
ee709054f2 | ||
|
|
ee60ec962b | ||
|
|
e013e38c5e | ||
|
|
20aa78105f | ||
|
|
770590e4f2 | ||
|
|
8e9e7b49ea | ||
|
|
08554889fd | ||
|
|
8ac2869de3 | ||
|
|
bb8e8a0df5 | ||
|
|
e2bd6a91cd | ||
|
|
ed0e5ef497 | ||
|
|
e1e475e605 | ||
|
|
cef5b04eb1 | ||
|
|
7e44af936d | ||
|
|
6aec5a9a60 | ||
|
|
79c75c886b | ||
|
|
bf6bcaed72 | ||
|
|
70bc2a6656 | ||
|
|
695cdea447 | ||
|
|
d0735a6d58 | ||
|
|
175c26bea9 | ||
|
|
8a298edd4e | ||
|
|
9860a1c138 | ||
|
|
3dff598d03 | ||
|
|
62b571d5f1 | ||
|
|
3af6623a91 | ||
|
|
c73ed8a4c5 | ||
|
|
4d5ba65254 | ||
|
|
91856f1159 | ||
|
|
f7da408f83 | ||
|
|
2824c55231 | ||
|
|
874655846c | ||
|
|
1620acedb1 | ||
|
|
6395e5dbbb | ||
|
|
251d9cdb8a | ||
|
|
623571acbb | ||
|
|
250f07ffa7 | ||
|
|
8917d7c16c | ||
|
|
d759280c18 | ||
|
|
67bc3903d4 | ||
|
|
cf6f83a44b | ||
|
|
4b15563ba3 | ||
|
|
dc36e15448 | ||
|
|
0b6330e98b | ||
|
|
2e93687bb4 | ||
|
|
0f925a466a | ||
|
|
16eeeda787 | ||
|
|
52f1df98bb | ||
|
|
a0ccff23a3 | ||
|
|
b8bed627a8 | ||
|
|
8d058d9dc8 | ||
|
|
57e92ff8d3 | ||
|
|
6eff724f97 | ||
|
|
55c3fe503b | ||
|
|
7f1ac63c58 | ||
|
|
2bb2e28f91 | ||
|
|
0bdffc5036 | ||
|
|
7202fbf084 | ||
|
|
317c3afb7a | ||
|
|
577baeca59 | ||
|
|
7c680cac10 | ||
|
|
0fadbd52a3 | ||
|
|
38e204dfe8 | ||
|
|
bf62653531 | ||
|
|
d851be41d3 | ||
|
|
3bd1875321 | ||
|
|
448c1d69a7 | ||
|
|
c99a5cb535 | ||
|
|
0492e90d6f | ||
|
|
4ffda9f705 | ||
|
|
b32d4fc42d | ||
|
|
4330dc39bf | ||
|
|
da50b19b6b | ||
|
|
797018fb8a | ||
|
|
3a8f891c7d | ||
|
|
56a788286c | ||
|
|
fd95364d5f | ||
|
|
470fde0890 | ||
|
|
f12d878c0b | ||
|
|
e8993932c1 | ||
|
|
e3933e4ddc | ||
|
|
dd67239b6e | ||
|
|
1ea0d3bd8b | ||
|
|
8b952d4be6 | ||
|
|
9e8a3bc701 | ||
|
|
76807176fb | ||
|
|
3650624e4b | ||
|
|
585c509aba | ||
|
|
046c7e732f | ||
|
|
564a27461d | ||
|
|
4ebbc1a01d | ||
|
|
4ec32a6403 | ||
|
|
412627aab0 | ||
|
|
2584abda0e | ||
|
|
7692322fba | ||
|
|
954018fea2 | ||
|
|
ebf37f7310 | ||
|
|
f22b836ede | ||
|
|
1cea786d66 | ||
|
|
9be10f7b79 | ||
|
|
1f35d0ec2f | ||
|
|
9fcf36a2ff | ||
|
|
30f5a66487 | ||
|
|
60e0ad1f5d | ||
|
|
ed60b4670e | ||
|
|
318daaf083 | ||
|
|
182987218b | ||
|
|
5ff8c7302f | ||
|
|
398712403b | ||
|
|
63f72eb23b | ||
|
|
9dea6d7200 | ||
|
|
36f63bdf99 | ||
|
|
a09fc14625 | ||
|
|
71e280238d | ||
|
|
e20bb13649 | ||
|
|
ed8108a9d8 | ||
|
|
c0b3c9a330 | ||
|
|
8a252bff64 | ||
|
|
d3d3106fc9 | ||
|
|
1ebb09226d | ||
|
|
52163428e9 | ||
|
|
da9dda2c2b | ||
|
|
a4a14cae96 | ||
|
|
989d6c55c4 | ||
|
|
3b7376fd18 | ||
|
|
c31b10c798 | ||
|
|
1c3e6ba930 | ||
|
|
acda664686 | ||
|
|
99123ad1c3 | ||
|
|
cdf9cf5cf4 | ||
|
|
797dedfcbb | ||
|
|
b61de4866c | ||
|
|
931951ff37 | ||
|
|
6f42b4c316 | ||
|
|
58c446de2d | ||
|
|
74bf6bc411 | ||
|
|
ad3c24f950 | ||
|
|
998e487fe8 | ||
|
|
93346b0c63 | ||
|
|
7d9920691f | ||
|
|
b1942678b4 | ||
|
|
8c77d0d775 | ||
|
|
3e667ee39a | ||
|
|
52b2858ac2 | ||
|
|
6fcb4c2058 | ||
|
|
7411670e22 | ||
|
|
cfd23c395a | ||
|
|
2e8f670e94 | ||
|
|
18a88eab51 | ||
|
|
84e9f9794d | ||
|
|
628c0e5dcc | ||
|
|
cdee08bd36 | ||
|
|
7ed43da425 | ||
|
|
461a0b3645 | ||
|
|
bd56539103 | ||
|
|
9bcd3de69b | ||
|
|
d8f57963a1 | ||
|
|
5328f7fe69 | ||
|
|
9eea42b121 | ||
|
|
374f8ba1de | ||
|
|
74c984dec3 | ||
|
|
52ea0215f0 | ||
|
|
ea3d719b32 | ||
|
|
fd1e655075 | ||
|
|
9f8d439780 | ||
|
|
7e1bdc99eb | ||
|
|
dac36d7f55 | ||
|
|
9d495a10ec | ||
|
|
9bb99319ba | ||
|
|
bc8d8dcd04 | ||
|
|
b2d9a7675d | ||
|
|
2477197656 | ||
|
|
171083b2f1 | ||
|
|
e592eb969f | ||
|
|
db1493f138 | ||
|
|
57c270f8fa | ||
|
|
bfe8bc89c0 | ||
|
|
0a00862495 | ||
|
|
7dd53d93cd | ||
|
|
abe65d4064 | ||
|
|
4977b31ba6 | ||
|
|
c1beb85ba5 | ||
|
|
ca9a78eea4 | ||
|
|
9bf006f4d3 | ||
|
|
3bb2a082b7 | ||
|
|
92d11522d2 | ||
|
|
44cfdc1503 | ||
|
|
2fdcbedea8 | ||
|
|
787c7fd966 | ||
|
|
09b4ad6937 | ||
|
|
580d43aeaf | ||
|
|
a1a7fec15f | ||
|
|
6dcd74d116 | ||
|
|
187f5a8a93 | ||
|
|
2eb938147a | ||
|
|
deffb75c14 | ||
|
|
f91707bfbe | ||
|
|
8aba7825dc | ||
|
|
b8b5b2fef2 | ||
|
|
f4d6d69184 | ||
|
|
a5b1c685e1 | ||
|
|
609805b84d | ||
|
|
00d1da7c01 | ||
|
|
7335726c7d | ||
|
|
02779939f0 | ||
|
|
6c6f015f40 | ||
|
|
f087d38b86 | ||
|
|
c78957f55c | ||
|
|
9ce0c47cd4 | ||
|
|
60034f2c96 | ||
|
|
c9a4af218e | ||
|
|
c5c2e61e06 | ||
|
|
b2930dd6a7 | ||
|
|
4aa6700ceb | ||
|
|
267ecfacab | ||
|
|
007597239f | ||
|
|
5699abf1be | ||
|
|
a6ccd037e2 | ||
|
|
009991ce4c | ||
|
|
6ef788a8f4 | ||
|
|
fa37f7d40a | ||
|
|
b195cebac7 | ||
|
|
8aeea60888 | ||
|
|
6e0857c6c1 | ||
|
|
260fdbe3b3 | ||
|
|
2f30c6c781 | ||
|
|
d5b4da655a | ||
|
|
1694ed7758 | ||
|
|
ee6cc6d319 | ||
|
|
7670e320ba | ||
|
|
15ab745bd0 | ||
|
|
7468b33991 | ||
|
|
750e02f38a | ||
|
|
e2852407ea | ||
|
|
88e738c6cd | ||
|
|
eaae8bdb0b | ||
|
|
95d146fea2 | ||
|
|
dc20b68a37 | ||
|
|
563e3072a5 | ||
|
|
b3ba4db00b | ||
|
|
a4c1480a1a | ||
|
|
91e0452320 | ||
|
|
ad80ea7885 | ||
|
|
1c20cda389 | ||
|
|
631759d833 | ||
|
|
ca02c66f26 | ||
|
|
3ac095d359 | ||
|
|
e1bc223de0 | ||
|
|
e065ead9b3 | ||
|
|
f9471f9b9b | ||
|
|
2612b50d06 | ||
|
|
d9ce2906a0 | ||
|
|
b76397f98e | ||
|
|
fcad9e0be5 | ||
|
|
2934347865 | ||
|
|
315f1b0207 | ||
|
|
965bd79a86 | ||
|
|
c18563e34b | ||
|
|
161e0de8d5 | ||
|
|
40aeca0740 | ||
|
|
63dd7fa7c0 | ||
|
|
509b49caf1 | ||
|
|
38c51cf79c | ||
|
|
0b693bba4e | ||
|
|
1258f34c78 | ||
|
|
510c0d5f56 | ||
|
|
cdb630e580 | ||
|
|
65fbd38105 | ||
|
|
1570132a55 | ||
|
|
7b5b748d23 | ||
|
|
041601c4a5 | ||
|
|
f692fd0202 | ||
|
|
e7b4de56f2 | ||
|
|
4a616a0c04 | ||
|
|
0814675d2a | ||
|
|
13df35462b | ||
|
|
899868f51e | ||
|
|
ee466aebce | ||
|
|
687ef2662e | ||
|
|
5aa29acbd3 | ||
|
|
1c2b3d063b | ||
|
|
551a000893 | ||
|
|
0d82d425cc | ||
|
|
0e1cea1034 | ||
|
|
2b75153148 | ||
|
|
c170615fb3 | ||
|
|
f6e84b6a35 | ||
|
|
6144f09a1f | ||
|
|
de142e8050 | ||
|
|
d0c1a119fd | ||
|
|
8fd80d3185 | ||
|
|
ae28c82858 | ||
|
|
1766764c7d | ||
|
|
129f8d72bd | ||
|
|
7314b5ecae | ||
|
|
7b0806355f | ||
|
|
49cf72e058 | ||
|
|
a11cad619d | ||
|
|
c1d35e8a57 | ||
|
|
fede348fbd | ||
|
|
f3c60e8fa6 | ||
|
|
00e53439ed | ||
|
|
368fced0c4 | ||
|
|
666771fb0f | ||
|
|
9e3f978677 | ||
|
|
f467d1c4f7 | ||
|
|
d8fc9d937e |
@@ -81,7 +81,7 @@ class TaskBarIcon(wx.TaskBarIcon):
|
||||
webbrowser.open(url)
|
||||
|
||||
def onSettings(self, event):
|
||||
url = self.frame.parent.getSetting('base_url') + '/settings/'
|
||||
url = self.frame.parent.getSetting('base_url') + 'settings/about/'
|
||||
webbrowser.open(url)
|
||||
|
||||
def onTaskBarClose(self, evt):
|
||||
|
||||
20
README.md
20
README.md
@@ -40,3 +40,23 @@ Linux (ubuntu / debian):
|
||||
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
|
||||
* Add it to defaults. `sudo update-rc.d couchpotato defaults`
|
||||
* Open your browser and go to: `http://localhost:5050/`
|
||||
|
||||
|
||||
FreeBSD :
|
||||
|
||||
* Update your ports tree `sudo portsnap fetch update`
|
||||
* Install Python 2.6+ [lang/python](http://www.freshports.org/lang/python) with `cd /usr/ports/lang/python; sudo make install clean`
|
||||
* Install port [databases/py-sqlite3](http://www.freshports.org/databases/py-sqlite3) with `cd /usr/ports/databases/py-sqlite3; sudo make install clean`
|
||||
* Add a symlink to 'python2' `sudo ln -s /usr/local/bin/python /usr/local/bin/python2`
|
||||
* Install port [ftp/libcurl](http://www.freshports.org/ftp/libcurl) with `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean`
|
||||
* Install port [ftp/curl](http://www.freshports.org/ftp/bcurl), deselect 'Asynchronous DNS resolution via c-ares' when prompted as part of config `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean`
|
||||
* Install port [textproc/docbook-xml-450](http://www.freshports.org/textproc/docbook-xml-450) with `cd /usr/ports/textproc/docbook-xml-450; sudo make install clean`
|
||||
* Install port [GIT](http://git-scm.com/) with `cd /usr/ports/devel/git; sudo make install clean`
|
||||
* 'cd' to the folder of your choosing.
|
||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
||||
* Then run `sudo python CouchPotatoServer/CouchPotato.py` to start for the first time
|
||||
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/freebsd /etc/rc.d/couchpotato`
|
||||
* Change the paths inside the init script. `sudo vim /etc/init.d/couchpotato`
|
||||
* Make init script executable. `sudo chmod +x /etc/rc.d/couchpotato`
|
||||
* Add init to startup. `sudo echo 'couchpotato_enable="YES"' >> /etc/rc.conf`
|
||||
* Open your browser and go to: `http://server:5050/`
|
||||
|
||||
@@ -12,4 +12,4 @@
|
||||
* What hardware / OS are you using and what are the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example.
|
||||
* I will mark issues with the "can't reproduce" tag. Don't go asking me "why closed" if it clearly says the issue in the tag ;)
|
||||
|
||||
**If I don't get enough info, the change of the issue getting closed is a lot bigger ;)**
|
||||
**If I don't get enough info, the chance of the issue getting closed is a lot bigger ;)**
|
||||
|
||||
@@ -1,84 +1,139 @@
|
||||
from couchpotato.api import api_docs, api_docs_missing
|
||||
from couchpotato.core.auth import requires_auth
|
||||
from couchpotato.api import api_docs, api_docs_missing, api
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.request import getParams, jsonified
|
||||
from couchpotato.core.helpers.variable import md5
|
||||
from couchpotato.core.helpers.variable import md5, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from flask.app import Flask
|
||||
from flask.blueprints import Blueprint
|
||||
from flask.globals import request
|
||||
from flask.helpers import url_for
|
||||
from flask.templating import render_template
|
||||
from sqlalchemy.engine import create_engine
|
||||
from sqlalchemy.orm import scoped_session
|
||||
from sqlalchemy.orm.session import sessionmaker
|
||||
from werkzeug.utils import redirect
|
||||
from tornado import template
|
||||
from tornado.web import RequestHandler, authenticated
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
app = Flask(__name__, static_folder = 'nope')
|
||||
web = Blueprint('web', __name__)
|
||||
|
||||
views = {}
|
||||
template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates'))
|
||||
|
||||
|
||||
class BaseHandler(RequestHandler):
|
||||
|
||||
def get_current_user(self):
|
||||
username = Env.setting('username')
|
||||
password = Env.setting('password')
|
||||
|
||||
if username and password:
|
||||
return self.get_secure_cookie('user')
|
||||
else: # Login when no username or password are set
|
||||
return True
|
||||
|
||||
# Main web handler
|
||||
class WebHandler(BaseHandler):
|
||||
|
||||
@authenticated
|
||||
def get(self, route, *args, **kwargs):
|
||||
route = route.strip('/')
|
||||
if not views.get(route):
|
||||
page_not_found(self)
|
||||
return
|
||||
|
||||
try:
|
||||
self.write(views[route]())
|
||||
except:
|
||||
log.error("Failed doing web request '%s': %s", (route, traceback.format_exc()))
|
||||
self.write({'success': False, 'error': 'Failed returning results'})
|
||||
|
||||
def addView(route, func, static = False):
|
||||
views[route] = func
|
||||
|
||||
def get_session(engine = None):
|
||||
return Env.getSession(engine)
|
||||
|
||||
def addView(route, func, static = False):
|
||||
web.add_url_rule(route + ('' if static else '/'), endpoint = route if route else 'index', view_func = func)
|
||||
|
||||
""" Web view """
|
||||
@web.route('/')
|
||||
@requires_auth
|
||||
# Web view
|
||||
def index():
|
||||
return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env)
|
||||
return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env)
|
||||
addView('', index)
|
||||
|
||||
""" Api view """
|
||||
@web.route('docs/')
|
||||
@requires_auth
|
||||
# API docs
|
||||
def apiDocs():
|
||||
from couchpotato import app
|
||||
routes = []
|
||||
for route, x in sorted(app.view_functions.iteritems()):
|
||||
if route[0:4] == 'api.':
|
||||
routes += [route[4:].replace('::', '.')]
|
||||
|
||||
for route in api.iterkeys():
|
||||
routes.append(route)
|
||||
|
||||
if api_docs.get(''):
|
||||
del api_docs['']
|
||||
del api_docs_missing['']
|
||||
return render_template('api.html', fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing))
|
||||
|
||||
@web.route('getkey/')
|
||||
def getApiKey():
|
||||
return template_loader.load('api.html').generate(fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing), Env = Env)
|
||||
|
||||
api = None
|
||||
params = getParams()
|
||||
username = Env.setting('username')
|
||||
password = Env.setting('password')
|
||||
addView('docs', apiDocs)
|
||||
|
||||
if (params.get('u') == md5(username) or not username) and (params.get('p') == password or not password):
|
||||
api = Env.setting('api_key')
|
||||
# Make non basic auth option to get api key
|
||||
class KeyHandler(RequestHandler):
|
||||
def get(self, *args, **kwargs):
|
||||
api = None
|
||||
|
||||
return jsonified({
|
||||
'success': api is not None,
|
||||
'api_key': api
|
||||
})
|
||||
try:
|
||||
username = Env.setting('username')
|
||||
password = Env.setting('password')
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(error):
|
||||
index_url = url_for('web.index')
|
||||
url = request.path[len(index_url):]
|
||||
if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password):
|
||||
api = Env.setting('api_key')
|
||||
|
||||
self.write({
|
||||
'success': api is not None,
|
||||
'api_key': api
|
||||
})
|
||||
except:
|
||||
log.error('Failed doing key request: %s', (traceback.format_exc()))
|
||||
self.write({'success': False, 'error': 'Failed returning results'})
|
||||
|
||||
|
||||
class LoginHandler(BaseHandler):
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
|
||||
if self.get_current_user():
|
||||
self.redirect(Env.get('web_base'))
|
||||
else:
|
||||
self.write(template_loader.load('login.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env))
|
||||
|
||||
def post(self, *args, **kwargs):
|
||||
|
||||
api = None
|
||||
|
||||
username = Env.setting('username')
|
||||
password = Env.setting('password')
|
||||
|
||||
if (self.get_argument('username') == username or not username) and (md5(self.get_argument('password')) == password or not password):
|
||||
api = Env.setting('api_key')
|
||||
|
||||
if api:
|
||||
remember_me = tryInt(self.get_argument('remember_me', default = 0))
|
||||
self.set_secure_cookie('user', api, expires_days = 30 if remember_me > 0 else None)
|
||||
|
||||
self.redirect(Env.get('web_base'))
|
||||
|
||||
class LogoutHandler(BaseHandler):
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
self.clear_cookie('user')
|
||||
self.redirect('%slogin/' % Env.get('web_base'))
|
||||
|
||||
|
||||
def page_not_found(rh):
|
||||
index_url = Env.get('web_base')
|
||||
url = rh.request.uri[len(index_url):]
|
||||
|
||||
if url[:3] != 'api':
|
||||
if request.path != '/':
|
||||
r = request.url.replace(request.path, index_url + '#' + url)
|
||||
else:
|
||||
r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url)
|
||||
return redirect(r)
|
||||
r = index_url + '#' + url.lstrip('/')
|
||||
rh.redirect(r)
|
||||
else:
|
||||
if not Env.get('dev'):
|
||||
time.sleep(0.1)
|
||||
return 'Wrong API key used', 404
|
||||
|
||||
rh.set_status(404)
|
||||
rh.write('Wrong API key used')
|
||||
|
||||
|
||||
@@ -1,44 +1,64 @@
|
||||
from flask.blueprints import Blueprint
|
||||
from flask.helpers import url_for
|
||||
from couchpotato.core.helpers.request import getParams
|
||||
from couchpotato.core.logger import CPLog
|
||||
from functools import wraps
|
||||
from threading import Thread
|
||||
from tornado.gen import coroutine
|
||||
from tornado.web import RequestHandler, asynchronous
|
||||
from werkzeug.utils import redirect
|
||||
import json
|
||||
import threading
|
||||
import tornado
|
||||
import traceback
|
||||
import urllib
|
||||
|
||||
api = Blueprint('api', __name__)
|
||||
api_docs = {}
|
||||
api_docs_missing = []
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
api = {}
|
||||
api_locks = {}
|
||||
api_nonblock = {}
|
||||
|
||||
api_docs = {}
|
||||
api_docs_missing = []
|
||||
|
||||
def run_async(func):
|
||||
@wraps(func)
|
||||
def async_func(*args, **kwargs):
|
||||
func_hl = Thread(target = func, args = args, kwargs = kwargs)
|
||||
func_hl.start()
|
||||
return func_hl
|
||||
|
||||
return async_func
|
||||
|
||||
# NonBlock API handler
|
||||
class NonBlockHandler(RequestHandler):
|
||||
|
||||
stoppers = []
|
||||
stopper = None
|
||||
|
||||
@asynchronous
|
||||
def get(self, route):
|
||||
def get(self, route, *args, **kwargs):
|
||||
route = route.strip('/')
|
||||
start, stop = api_nonblock[route]
|
||||
self.stoppers.append(stop)
|
||||
self.stopper = stop
|
||||
|
||||
start(self.onNewMessage, last_id = self.get_argument("last_id", None))
|
||||
start(self.onNewMessage, last_id = self.get_argument('last_id', None))
|
||||
|
||||
def onNewMessage(self, response):
|
||||
if self.request.connection.stream.closed():
|
||||
return
|
||||
self.finish(response)
|
||||
|
||||
try:
|
||||
self.finish(response)
|
||||
except:
|
||||
log.error('Failed doing nonblock request: %s', (traceback.format_exc()))
|
||||
try: self.finish({'success': False, 'error': 'Failed returning results'})
|
||||
except: pass
|
||||
|
||||
def on_connection_close(self):
|
||||
|
||||
for stop in self.stoppers:
|
||||
stop(self.onNewMessage)
|
||||
if self.stopper:
|
||||
self.stopper(self.onNewMessage)
|
||||
|
||||
self.stoppers = []
|
||||
|
||||
|
||||
def addApiView(route, func, static = False, docs = None, **kwargs):
|
||||
api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func, **kwargs)
|
||||
if docs:
|
||||
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
|
||||
else:
|
||||
api_docs_missing.append(route)
|
||||
self.stopper = None
|
||||
|
||||
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
||||
api_nonblock[route] = func_tuple
|
||||
@@ -48,9 +68,66 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
||||
else:
|
||||
api_docs_missing.append(route)
|
||||
|
||||
""" Api view """
|
||||
def index():
|
||||
index_url = url_for('web.index')
|
||||
return redirect(index_url + 'docs/')
|
||||
# Blocking API handler
|
||||
class ApiHandler(RequestHandler):
|
||||
|
||||
addApiView('', index)
|
||||
@coroutine
|
||||
def get(self, route, *args, **kwargs):
|
||||
route = route.strip('/')
|
||||
if not api.get(route):
|
||||
self.write('API call doesn\'t seem to exist')
|
||||
return
|
||||
|
||||
api_locks[route].acquire()
|
||||
|
||||
try:
|
||||
|
||||
kwargs = {}
|
||||
for x in self.request.arguments:
|
||||
kwargs[x] = urllib.unquote(self.get_argument(x))
|
||||
|
||||
# Split array arguments
|
||||
kwargs = getParams(kwargs)
|
||||
|
||||
# Remove t random string
|
||||
try: del kwargs['t']
|
||||
except: pass
|
||||
|
||||
# Add async callback handler
|
||||
@run_async
|
||||
def run_handler(callback):
|
||||
try:
|
||||
result = api[route](**kwargs)
|
||||
callback(result)
|
||||
except:
|
||||
log.error('Failed doing api request "%s": %s', (route, traceback.format_exc()))
|
||||
callback({'success': False, 'error': 'Failed returning results'})
|
||||
result = yield tornado.gen.Task(run_handler)
|
||||
|
||||
# Check JSONP callback
|
||||
jsonp_callback = self.get_argument('callback_func', default = None)
|
||||
|
||||
if jsonp_callback:
|
||||
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
|
||||
elif isinstance(result, tuple) and result[0] == 'redirect':
|
||||
self.redirect(result[1])
|
||||
else:
|
||||
self.write(result)
|
||||
|
||||
except:
|
||||
log.error('Failed doing api request "%s": %s', (route, traceback.format_exc()))
|
||||
self.write({'success': False, 'error': 'Failed returning results'})
|
||||
|
||||
api_locks[route].release()
|
||||
|
||||
def addApiView(route, func, static = False, docs = None, **kwargs):
|
||||
|
||||
if static: func(route)
|
||||
else:
|
||||
api[route] = func
|
||||
api_locks[route] = threading.Lock()
|
||||
|
||||
if docs:
|
||||
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
|
||||
else:
|
||||
api_docs_missing.append(route)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.helpers.variable import cleanHost, md5
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -57,7 +56,7 @@ class Core(Plugin):
|
||||
self.signalHandler()
|
||||
|
||||
def md5Password(self, value):
|
||||
return md5(value.encode(Env.get('encoding'))) if value else ''
|
||||
return md5(value) if value else ''
|
||||
|
||||
def checkApikey(self, value):
|
||||
return value if value and len(value) > 3 else uuid4().hex
|
||||
@@ -68,12 +67,12 @@ class Core(Plugin):
|
||||
|
||||
return True
|
||||
|
||||
def available(self):
|
||||
return jsonified({
|
||||
def available(self, **kwargs):
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def shutdown(self):
|
||||
def shutdown(self, **kwargs):
|
||||
if self.shutdown_started:
|
||||
return False
|
||||
|
||||
@@ -83,7 +82,7 @@ class Core(Plugin):
|
||||
|
||||
return 'shutdown'
|
||||
|
||||
def restart(self):
|
||||
def restart(self, **kwargs):
|
||||
if self.shutdown_started:
|
||||
return False
|
||||
|
||||
@@ -125,7 +124,7 @@ class Core(Plugin):
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
log.debug('Save to shutdown/restart')
|
||||
log.debug('Safe to shutdown/restart')
|
||||
|
||||
try:
|
||||
IOLoop.current().stop()
|
||||
@@ -156,10 +155,10 @@ class Core(Plugin):
|
||||
host = 'localhost'
|
||||
port = Env.setting('port')
|
||||
|
||||
return '%s:%d%s' % (cleanHost(host).rstrip('/'), int(port), '/' + Env.setting('url_base').lstrip('/') if Env.setting('url_base') else '')
|
||||
return '%s:%d%s' % (cleanHost(host).rstrip('/'), int(port), Env.get('web_base'))
|
||||
|
||||
def createApiUrl(self):
|
||||
return '%s/api/%s' % (self.createBaseUrl(), Env.setting('api_key'))
|
||||
return '%sapi/%s' % (self.createBaseUrl(), Env.setting('api_key'))
|
||||
|
||||
def version(self):
|
||||
ver = fireEvent('updater.info', single = True)
|
||||
@@ -170,10 +169,10 @@ class Core(Plugin):
|
||||
|
||||
return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash'])
|
||||
|
||||
def versionView(self):
|
||||
return jsonified({
|
||||
def versionView(self, **kwargs):
|
||||
return {
|
||||
'version': self.version()
|
||||
})
|
||||
}
|
||||
|
||||
def signalHandler(self):
|
||||
if Env.get('daemonized'): return
|
||||
|
||||
@@ -4,9 +4,11 @@ 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 cssprefixer
|
||||
from tornado.web import StaticFileHandler
|
||||
import os
|
||||
import re
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -79,7 +81,7 @@ class ClientScript(Plugin):
|
||||
for static_type in self.core_static:
|
||||
for rel_path in self.core_static.get(static_type):
|
||||
file_path = os.path.join(Env.get('app_dir'), 'couchpotato', 'static', rel_path)
|
||||
core_url = 'api/%s/static/%s?%s' % (Env.setting('api_key'), rel_path, tryInt(os.path.getmtime(file_path)))
|
||||
core_url = 'static/%s' % rel_path
|
||||
|
||||
if static_type == 'script':
|
||||
self.registerScript(core_url, file_path, position = 'front')
|
||||
@@ -89,6 +91,13 @@ class ClientScript(Plugin):
|
||||
|
||||
def minify(self):
|
||||
|
||||
# Create cache dir
|
||||
cache = Env.get('cache_dir')
|
||||
parent_dir = os.path.join(cache, 'minified')
|
||||
self.makeDir(parent_dir)
|
||||
|
||||
Env.get('app').add_handlers(".*$", [(Env.get('web_base') + 'minified/(.*)', StaticFileHandler, {'path': parent_dir})])
|
||||
|
||||
for file_type in ['style', 'script']:
|
||||
ext = 'js' if file_type is 'script' else 'css'
|
||||
positions = self.paths.get(file_type, {})
|
||||
@@ -99,8 +108,8 @@ class ClientScript(Plugin):
|
||||
def _minify(self, file_type, files, position, out):
|
||||
|
||||
cache = Env.get('cache_dir')
|
||||
out_name = 'minified_' + out
|
||||
out = os.path.join(cache, out_name)
|
||||
out_name = out
|
||||
out = os.path.join(cache, 'minified', out_name)
|
||||
|
||||
raw = []
|
||||
for file_path in files:
|
||||
@@ -109,7 +118,8 @@ class ClientScript(Plugin):
|
||||
if file_type == 'script':
|
||||
data = jsmin(f)
|
||||
else:
|
||||
data = cssprefixer.process(f, debug = False, minify = True)
|
||||
data = self.prefix(f)
|
||||
data = cssmin(data)
|
||||
data = data.replace('../images/', '../static/images/')
|
||||
data = data.replace('../fonts/', '../static/fonts/')
|
||||
data = data.replace('../../static/', '../static/') # Replace inside plugins
|
||||
@@ -119,17 +129,17 @@ class ClientScript(Plugin):
|
||||
# Combine all files together with some comments
|
||||
data = ''
|
||||
for r in raw:
|
||||
data += self.comment.get(file_type) % (r.get('file'), r.get('date'))
|
||||
data += self.comment.get(file_type) % (ss(r.get('file')), r.get('date'))
|
||||
data += r.get('data') + '\n\n'
|
||||
|
||||
self.createFile(out, ss(data.strip()))
|
||||
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)))
|
||||
minified_url = 'minified/%s?%s' % (out_name, tryInt(os.path.getmtime(out)))
|
||||
self.minified[file_type][position].append(minified_url)
|
||||
|
||||
def getStyles(self, *args, **kwargs):
|
||||
@@ -163,6 +173,8 @@ class ClientScript(Plugin):
|
||||
|
||||
def register(self, api_path, file_path, type, location):
|
||||
|
||||
api_path = '%s?%s' % (api_path, tryInt(os.path.getmtime(file_path)))
|
||||
|
||||
if not self.urls[type].get(location):
|
||||
self.urls[type][location] = []
|
||||
self.urls[type][location].append(api_path)
|
||||
@@ -170,3 +182,28 @@ class ClientScript(Plugin):
|
||||
if not self.paths[type].get(location):
|
||||
self.paths[type][location] = []
|
||||
self.paths[type][location].append(file_path)
|
||||
|
||||
prefix_properties = ['border-radius', 'transform', 'transition', 'box-shadow']
|
||||
prefix_tags = ['ms', 'moz', 'webkit']
|
||||
def prefix(self, data):
|
||||
|
||||
trimmed_data = re.sub('(\t|\n|\r)+', '', data)
|
||||
|
||||
new_data = ''
|
||||
colon_split = trimmed_data.split(';')
|
||||
for splt in colon_split:
|
||||
curl_split = splt.strip().split('{')
|
||||
for curly in curl_split:
|
||||
curly = curly.strip()
|
||||
for prop in self.prefix_properties:
|
||||
if curly[:len(prop) + 1] == prop + ':':
|
||||
for tag in self.prefix_tags:
|
||||
new_data += ' -%s-%s; ' % (tag, curly)
|
||||
|
||||
new_data += curly + (' { ' if len(curl_split) > 1 else ' ')
|
||||
|
||||
new_data += '; '
|
||||
|
||||
new_data = new_data.replace('{ ;', '; ').replace('} ;', '} ')
|
||||
|
||||
return new_data
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
@@ -33,10 +32,10 @@ class Updater(Plugin):
|
||||
else:
|
||||
self.updater = SourceUpdater()
|
||||
|
||||
addEvent('app.load', self.autoUpdate)
|
||||
addEvent('app.load', self.setCrons)
|
||||
addEvent('updater.info', self.info)
|
||||
|
||||
addApiView('updater.info', self.getInfo, docs = {
|
||||
addApiView('updater.info', self.info, docs = {
|
||||
'desc': 'Get updater information',
|
||||
'return': {
|
||||
'type': 'object',
|
||||
@@ -62,7 +61,7 @@ class Updater(Plugin):
|
||||
self.autoUpdate() # Check after enabling
|
||||
|
||||
def autoUpdate(self):
|
||||
if self.check() and self.conf('automatic') and not self.updater.update_failed:
|
||||
if self.isEnabled() and self.check() and self.conf('automatic') and not self.updater.update_failed:
|
||||
if self.updater.doUpdate():
|
||||
|
||||
# Notify before restarting
|
||||
@@ -80,31 +79,30 @@ class Updater(Plugin):
|
||||
|
||||
return False
|
||||
|
||||
def check(self):
|
||||
if self.isDisabled():
|
||||
def check(self, force = False):
|
||||
if not force and self.isDisabled():
|
||||
return
|
||||
|
||||
if self.updater.check():
|
||||
if not self.available_notified and self.conf('notification') and not self.conf('automatic'):
|
||||
fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
|
||||
info = self.updater.info()
|
||||
version_date = datetime.fromtimestamp(info['update_version']['date'])
|
||||
fireEvent('updater.available', message = 'A new update with hash "%s" is available, this version is from %s' % (info['update_version']['hash'], version_date), data = info)
|
||||
self.available_notified = True
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def info(self):
|
||||
def info(self, **kwargs):
|
||||
return self.updater.info()
|
||||
|
||||
def getInfo(self):
|
||||
return jsonified(self.updater.info())
|
||||
|
||||
def checkView(self):
|
||||
return jsonified({
|
||||
'update_available': self.check(),
|
||||
def checkView(self, **kwargs):
|
||||
return {
|
||||
'update_available': self.check(force = True),
|
||||
'info': self.updater.info()
|
||||
})
|
||||
}
|
||||
|
||||
def doUpdateView(self):
|
||||
def doUpdateView(self, **kwargs):
|
||||
|
||||
self.check()
|
||||
if not self.updater.update_version:
|
||||
@@ -119,9 +117,9 @@ class Updater(Plugin):
|
||||
if not success:
|
||||
success = True
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': success
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
class BaseUpdater(Plugin):
|
||||
@@ -134,13 +132,11 @@ class BaseUpdater(Plugin):
|
||||
update_failed = False
|
||||
update_version = None
|
||||
last_check = 0
|
||||
auto_register_static = False
|
||||
|
||||
def doUpdate(self):
|
||||
pass
|
||||
|
||||
def getInfo(self):
|
||||
return jsonified(self.info())
|
||||
|
||||
def info(self):
|
||||
return {
|
||||
'last_check': self.last_check,
|
||||
@@ -279,6 +275,7 @@ class SourceUpdater(BaseUpdater):
|
||||
if download_data.get('type') == 'zip':
|
||||
zip = zipfile.ZipFile(destination)
|
||||
zip.extractall(extracted_path)
|
||||
zip.close()
|
||||
else:
|
||||
tar = tarfile.open(destination)
|
||||
tar.extractall(path = extracted_path)
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
from couchpotato.core.helpers.variable import md5
|
||||
from couchpotato.environment import Env
|
||||
from flask import request, Response
|
||||
from functools import wraps
|
||||
|
||||
def check_auth(username, password):
|
||||
return username == Env.setting('username') and password == Env.setting('password')
|
||||
|
||||
def authenticate():
|
||||
return Response(
|
||||
'This is not the page you are looking for. *waves hand*', 401,
|
||||
{'WWW-Authenticate': 'Basic realm="CouchPotato Login"'}
|
||||
)
|
||||
|
||||
def requires_auth(f):
|
||||
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
auth = getattr(request, 'authorization')
|
||||
if Env.setting('username') and Env.setting('password'):
|
||||
if (not auth or not check_auth(auth.username.decode('latin1'), md5(auth.password.decode('latin1').encode(Env.get('encoding'))))):
|
||||
return authenticate()
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated
|
||||
@@ -11,12 +11,12 @@ log = CPLog(__name__)
|
||||
|
||||
class Downloader(Provider):
|
||||
|
||||
type = []
|
||||
protocol = []
|
||||
http_time_between_calls = 0
|
||||
|
||||
torrent_sources = [
|
||||
'http://torrage.com/torrent/%s.torrent',
|
||||
'http://torcache.net/torrent/%s.torrent',
|
||||
'https://torcache.net/torrent/%s.torrent',
|
||||
]
|
||||
|
||||
torrent_trackers = [
|
||||
@@ -36,18 +36,23 @@ class Downloader(Provider):
|
||||
def __init__(self):
|
||||
addEvent('download', self._download)
|
||||
addEvent('download.enabled', self._isEnabled)
|
||||
addEvent('download.enabled_types', self.getEnabledDownloadType)
|
||||
addEvent('download.enabled_protocols', self.getEnabledProtocol)
|
||||
addEvent('download.status', self._getAllDownloadStatus)
|
||||
addEvent('download.remove_failed', self._removeFailed)
|
||||
addEvent('download.pause', self._pause)
|
||||
addEvent('download.process_complete', self._processComplete)
|
||||
|
||||
def getEnabledDownloadType(self):
|
||||
for download_type in self.type:
|
||||
if self.isEnabled(manual = True, data = {'type': download_type}):
|
||||
return self.type
|
||||
def getEnabledProtocol(self):
|
||||
for download_protocol in self.protocol:
|
||||
if self.isEnabled(manual = True, data = {'protocol': download_protocol}):
|
||||
return self.protocol
|
||||
|
||||
return []
|
||||
|
||||
def _download(self, data = {}, movie = {}, manual = False, filedata = None):
|
||||
def _download(self, data = None, movie = None, manual = False, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
if self.isDisabled(manual, data):
|
||||
return
|
||||
return self.download(data = data, movie = movie, filedata = filedata)
|
||||
@@ -65,19 +70,35 @@ class Downloader(Provider):
|
||||
if self.isDisabled(manual = True, data = {}):
|
||||
return
|
||||
|
||||
if self.conf('delete_failed', default = True):
|
||||
return self.removeFailed(item)
|
||||
if item and item.get('downloader') == self.getName():
|
||||
if self.conf('delete_failed'):
|
||||
return self.removeFailed(item)
|
||||
|
||||
return False
|
||||
return False
|
||||
return
|
||||
|
||||
def removeFailed(self, item):
|
||||
return
|
||||
|
||||
def isCorrectType(self, item_type):
|
||||
is_correct = item_type in self.type
|
||||
def _processComplete(self, item):
|
||||
if self.isDisabled(manual = True, data = {}):
|
||||
return
|
||||
|
||||
if item and item.get('downloader') == self.getName():
|
||||
if self.conf('remove_complete', default = False):
|
||||
return self.processComplete(item = item, delete_files = self.conf('delete_files', default = False))
|
||||
|
||||
return False
|
||||
return
|
||||
|
||||
def processComplete(self, item, delete_files):
|
||||
return
|
||||
|
||||
def isCorrectProtocol(self, item_protocol):
|
||||
is_correct = item_protocol in self.protocol
|
||||
|
||||
if not is_correct:
|
||||
log.debug("Downloader doesn't support this type")
|
||||
log.debug("Downloader doesn't support this protocol")
|
||||
|
||||
return is_correct
|
||||
|
||||
@@ -101,7 +122,7 @@ class Downloader(Provider):
|
||||
except:
|
||||
log.debug('Torrent hash "%s" wasn\'t found on: %s', (torrent_hash, source))
|
||||
|
||||
log.error('Failed converting magnet url to torrent: %s', (torrent_hash))
|
||||
log.error('Failed converting magnet url to torrent: %s', torrent_hash)
|
||||
return False
|
||||
|
||||
def downloadReturnId(self, download_id):
|
||||
@@ -110,20 +131,38 @@ class Downloader(Provider):
|
||||
'id': download_id
|
||||
}
|
||||
|
||||
def isDisabled(self, manual, data):
|
||||
def isDisabled(self, manual = False, data = None):
|
||||
if not data: data = {}
|
||||
|
||||
return not self.isEnabled(manual, data)
|
||||
|
||||
def _isEnabled(self, manual, data = {}):
|
||||
def _isEnabled(self, manual, data = None):
|
||||
if not data: data = {}
|
||||
|
||||
if not self.isEnabled(manual, data):
|
||||
return
|
||||
return True
|
||||
|
||||
def isEnabled(self, manual, data = {}):
|
||||
def isEnabled(self, manual = False, data = None):
|
||||
if not data: data = {}
|
||||
|
||||
d_manual = self.conf('manual', default = False)
|
||||
return super(Downloader, self).isEnabled() and \
|
||||
((d_manual and manual) or (d_manual is False)) and \
|
||||
(not data or self.isCorrectType(data.get('type')))
|
||||
(d_manual and manual or d_manual is False) and \
|
||||
(not data or self.isCorrectProtocol(data.get('protocol')))
|
||||
|
||||
def _pause(self, item, pause = True):
|
||||
if self.isDisabled(manual = True, data = {}):
|
||||
return
|
||||
|
||||
if item and item.get('downloader') == self.getName():
|
||||
self.pause(item, pause)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def pause(self, item, pause):
|
||||
return
|
||||
|
||||
class StatusList(list):
|
||||
|
||||
|
||||
@@ -7,22 +7,25 @@ import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Blackhole(Downloader):
|
||||
|
||||
type = ['nzb', 'torrent', 'torrent_magnet']
|
||||
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
||||
|
||||
def download(self, data = {}, movie = {}, filedata = None):
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
directory = self.conf('directory')
|
||||
if not directory or not os.path.isdir(directory):
|
||||
log.error('No directory set for blackhole %s download.', data.get('type'))
|
||||
log.error('No directory set for blackhole %s download.', data.get('protocol'))
|
||||
else:
|
||||
try:
|
||||
if not filedata or len(filedata) < 50:
|
||||
try:
|
||||
if data.get('type') == 'torrent_magnet':
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
filedata = self.magnetToTorrent(data.get('url'))
|
||||
data['type'] = 'torrent'
|
||||
data['protocol'] = 'torrent'
|
||||
except:
|
||||
log.error('Failed download torrent via magnet url: %s', traceback.format_exc())
|
||||
|
||||
@@ -34,7 +37,7 @@ class Blackhole(Downloader):
|
||||
|
||||
try:
|
||||
if not os.path.isfile(fullPath):
|
||||
log.info('Downloading %s to %s.', (data.get('type'), fullPath))
|
||||
log.info('Downloading %s to %s.', (data.get('protocol'), fullPath))
|
||||
with open(fullPath, 'wb') as f:
|
||||
f.write(filedata)
|
||||
os.chmod(fullPath, Env.getPermission('file'))
|
||||
@@ -53,20 +56,21 @@ class Blackhole(Downloader):
|
||||
|
||||
return False
|
||||
|
||||
def getEnabledDownloadType(self):
|
||||
def getEnabledProtocol(self):
|
||||
if self.conf('use_for') == 'both':
|
||||
return super(Blackhole, self).getEnabledDownloadType()
|
||||
return super(Blackhole, self).getEnabledProtocol()
|
||||
elif self.conf('use_for') == 'torrent':
|
||||
return ['torrent', 'torrent_magnet']
|
||||
else:
|
||||
return ['nzb']
|
||||
|
||||
def isEnabled(self, manual, data = {}):
|
||||
for_type = ['both']
|
||||
if data and 'torrent' in data.get('type'):
|
||||
for_type.append('torrent')
|
||||
def isEnabled(self, manual = False, data = None):
|
||||
if not data: data = {}
|
||||
for_protocol = ['both']
|
||||
if data and 'torrent' in data.get('protocol'):
|
||||
for_protocol.append('torrent')
|
||||
elif data:
|
||||
for_type.append(data.get('type'))
|
||||
for_protocol.append(data.get('protocol'))
|
||||
|
||||
return super(Blackhole, self).isEnabled(manual, data) and \
|
||||
((self.conf('use_for') in for_type))
|
||||
((self.conf('use_for') in for_protocol))
|
||||
|
||||
90
couchpotato/core/downloaders/deluge/__init__.py
Normal file
90
couchpotato/core/downloaders/deluge/__init__.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from .main import Deluge
|
||||
|
||||
def start():
|
||||
return Deluge()
|
||||
|
||||
config = [{
|
||||
'name': 'deluge',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'deluge',
|
||||
'label': 'Deluge',
|
||||
'description': 'Use <a href="http://www.deluge-torrent.org/" target="_blank">Deluge</a> to download torrents.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'torrent',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost:58846',
|
||||
'description': 'Hostname with port. Usually <strong>localhost:58846</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'directory',
|
||||
'type': 'directory',
|
||||
'description': 'Download to this directory. Keep empty for default Deluge download directory.',
|
||||
},
|
||||
{
|
||||
'name': 'completed_directory',
|
||||
'type': 'directory',
|
||||
'description': 'Move completed torrent to this directory. Keep empty for default Deluge options.',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'name': 'label',
|
||||
'description': 'Label to add to torrents in the Deluge UI.',
|
||||
},
|
||||
{
|
||||
'name': 'remove_complete',
|
||||
'label': 'Remove torrent',
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'description': 'Remove the torrent from Deluge after it has finished seeding.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_files',
|
||||
'label': 'Remove files',
|
||||
'default': True,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also remove the leftover files.',
|
||||
},
|
||||
{
|
||||
'name': 'paused',
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'default': False,
|
||||
'description': 'Add the torrent paused.',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
244
couchpotato/core/downloaders/deluge/main.py
Normal file
244
couchpotato/core/downloaders/deluge/main.py
Normal file
@@ -0,0 +1,244 @@
|
||||
from base64 import b64encode
|
||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||
from couchpotato.core.helpers.encoding import isInt, ss
|
||||
from couchpotato.core.helpers.variable import tryFloat
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from datetime import timedelta
|
||||
from synchronousdeluge import DelugeClient
|
||||
import os.path
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Deluge(Downloader):
|
||||
|
||||
protocol = ['torrent', 'torrent_magnet']
|
||||
log = CPLog(__name__)
|
||||
drpc = None
|
||||
|
||||
def connect(self):
|
||||
# Load host from config and split out port.
|
||||
host = self.conf('host').split(':')
|
||||
if not isInt(host[1]):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
return False
|
||||
|
||||
if not self.drpc:
|
||||
self.drpc = DelugeRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||
|
||||
return self.drpc
|
||||
|
||||
def download(self, data, movie, filedata = None):
|
||||
log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol')))
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
if not filedata and data.get('protocol') == 'torrent':
|
||||
log.error('Failed sending torrent, no data')
|
||||
return False
|
||||
|
||||
# Set parameters for Deluge
|
||||
options = {
|
||||
'add_paused': self.conf('paused', default = 0),
|
||||
'label': self.conf('label')
|
||||
}
|
||||
|
||||
if self.conf('directory'):
|
||||
if os.path.isdir(self.conf('directory')):
|
||||
options['download_location'] = self.conf('directory')
|
||||
else:
|
||||
log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory'))
|
||||
|
||||
if self.conf('completed_directory'):
|
||||
if os.path.isdir(self.conf('completed_directory')):
|
||||
options['move_completed'] = 1
|
||||
options['move_completed_path'] = self.conf('completed_directory')
|
||||
else:
|
||||
log.error('Download directory from Deluge settings: %s doesn\'t exist', self.conf('directory'))
|
||||
|
||||
if data.get('seed_ratio'):
|
||||
options['stop_at_ratio'] = 1
|
||||
options['stop_ratio'] = tryFloat(data.get('seed_ratio'))
|
||||
|
||||
# Deluge only has seed time as a global option. Might be added in
|
||||
# in a future API release.
|
||||
# if data.get('seed_time'):
|
||||
|
||||
# Send request to Deluge
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options)
|
||||
else:
|
||||
filename = self.createFileName(data, filedata, movie)
|
||||
remote_torrent = self.drpc.add_torrent_file(filename, b64encode(filedata), options)
|
||||
|
||||
if not remote_torrent:
|
||||
log.error('Failed sending torrent to Deluge')
|
||||
return False
|
||||
|
||||
log.info('Torrent sent to Deluge successfully.')
|
||||
return self.downloadReturnId(remote_torrent)
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
|
||||
log.debug('Checking Deluge download status.')
|
||||
|
||||
if not os.path.isdir(Env.setting('from', 'renamer')):
|
||||
log.error('Renamer "from" folder doesn\'t to exist.')
|
||||
return
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
statuses = StatusList(self)
|
||||
|
||||
queue = self.drpc.get_alltorrents()
|
||||
|
||||
if not queue:
|
||||
log.debug('Nothing in queue or error')
|
||||
return False
|
||||
|
||||
for torrent_id in queue:
|
||||
item = queue[torrent_id]
|
||||
log.debug('name=%s / id=%s / save_path=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (item['name'], item['hash'], item['save_path'], item['move_completed_path'], item['hash'], item['progress'], item['state'], item['eta'], item['ratio'], item['stop_ratio'], item['is_seed'], item['is_finished'], item['paused']))
|
||||
|
||||
# Deluge has no easy way to work out if a torrent is stalled or failing.
|
||||
#status = 'failed'
|
||||
status = 'busy'
|
||||
if item['is_seed'] and tryFloat(item['ratio']) < tryFloat(item['stop_ratio']):
|
||||
# We have item['seeding_time'] to work out what the seeding time is, but we do not
|
||||
# have access to the downloader seed_time, as with deluge we have no way to pass it
|
||||
# when the torrent is added. So Deluge will only look at the ratio.
|
||||
# See above comment in download().
|
||||
status = 'seeding'
|
||||
elif item['is_seed'] and item['is_finished'] and item['paused'] and item['state'] == 'Paused':
|
||||
status = 'completed'
|
||||
|
||||
download_dir = item['save_path']
|
||||
if item['move_on_completed']:
|
||||
download_dir = item['move_completed_path']
|
||||
|
||||
statuses.append({
|
||||
'id': item['hash'],
|
||||
'name': item['name'],
|
||||
'status': status,
|
||||
'original_status': item['state'],
|
||||
'seed_ratio': item['ratio'],
|
||||
'timeleft': str(timedelta(seconds = item['eta'])),
|
||||
'folder': ss(os.path.join(download_dir, item['name'])),
|
||||
})
|
||||
|
||||
return statuses
|
||||
|
||||
def pause(self, item, pause = True):
|
||||
if pause:
|
||||
return self.drpc.pause_torrent([item['id']])
|
||||
else:
|
||||
return self.drpc.resume_torrent([item['id']])
|
||||
|
||||
def removeFailed(self, item):
|
||||
log.info('%s failed downloading, deleting...', item['name'])
|
||||
return self.drpc.remove_torrent(item['id'], True)
|
||||
|
||||
def processComplete(self, item, delete_files = False):
|
||||
log.debug('Requesting Deluge to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||
return self.drpc.remove_torrent(item['id'], remove_local_data = delete_files)
|
||||
|
||||
class DelugeRPC(object):
|
||||
|
||||
host = 'localhost'
|
||||
port = 58846
|
||||
username = None
|
||||
password = None
|
||||
client = None
|
||||
|
||||
def __init__(self, host = 'localhost', port = 58846, username = None, password = None):
|
||||
super(DelugeRPC, self).__init__()
|
||||
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
def connect(self):
|
||||
self.client = DelugeClient()
|
||||
self.client.connect(self.host, int(self.port), self.username, self.password)
|
||||
|
||||
def add_torrent_magnet(self, torrent, options):
|
||||
torrent_id = False
|
||||
try:
|
||||
self.connect()
|
||||
torrent_id = self.client.core.add_torrent_magnet(torrent, options).get()
|
||||
if options['label']:
|
||||
self.client.label.set_torrent(torrent_id, options['label']).get()
|
||||
except Exception, err:
|
||||
log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc()))
|
||||
finally:
|
||||
if self.client:
|
||||
self.disconnect()
|
||||
|
||||
return torrent_id
|
||||
|
||||
def add_torrent_file(self, filename, torrent, options):
|
||||
torrent_id = False
|
||||
try:
|
||||
self.connect()
|
||||
torrent_id = self.client.core.add_torrent_file(filename, torrent, options).get()
|
||||
if options['label']:
|
||||
self.client.label.set_torrent(torrent_id, options['label']).get()
|
||||
except Exception, err:
|
||||
log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc()))
|
||||
finally:
|
||||
if self.client:
|
||||
self.disconnect()
|
||||
|
||||
return torrent_id
|
||||
|
||||
def get_alltorrents(self):
|
||||
ret = False
|
||||
try:
|
||||
self.connect()
|
||||
ret = self.client.core.get_torrents_status({}, {}).get()
|
||||
except Exception, err:
|
||||
log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc()))
|
||||
finally:
|
||||
if self.client:
|
||||
self.disconnect()
|
||||
return ret
|
||||
|
||||
def pause_torrent(self, torrent_ids):
|
||||
try:
|
||||
self.connect()
|
||||
self.client.core.pause_torrent(torrent_ids).get()
|
||||
except Exception, err:
|
||||
log.error('Failed to pause torrent: %s %s', (err, traceback.format_exc()))
|
||||
finally:
|
||||
if self.client:
|
||||
self.disconnect()
|
||||
|
||||
def resume_torrent(self, torrent_ids):
|
||||
try:
|
||||
self.connect()
|
||||
self.client.core.resume_torrent(torrent_ids).get()
|
||||
except Exception, err:
|
||||
log.error('Failed to resume torrent: %s %s', (err, traceback.format_exc()))
|
||||
finally:
|
||||
if self.client:
|
||||
self.disconnect()
|
||||
|
||||
def remove_torrent(self, torrent_id, remove_local_data):
|
||||
ret = False
|
||||
try:
|
||||
self.connect()
|
||||
ret = self.client.core.remove_torrent(torrent_id, remove_local_data).get()
|
||||
except Exception, err:
|
||||
log.error('Failed to remove torrent: %s %s', (err, traceback.format_exc()))
|
||||
finally:
|
||||
if self.client:
|
||||
self.disconnect()
|
||||
return ret
|
||||
|
||||
def disconnect(self):
|
||||
self.client.disconnect()
|
||||
@@ -42,6 +42,7 @@ config = [{
|
||||
},
|
||||
{
|
||||
'name': 'priority',
|
||||
'advanced': True,
|
||||
'default': '0',
|
||||
'type': 'dropdown',
|
||||
'values': [('Very Low', -100), ('Low', -50), ('Normal', 0), ('High', 50), ('Very High', 100)],
|
||||
@@ -57,6 +58,7 @@ config = [{
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
|
||||
@@ -12,13 +12,16 @@ import xmlrpclib
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class NZBGet(Downloader):
|
||||
|
||||
type = ['nzb']
|
||||
protocol = ['nzb']
|
||||
|
||||
url = 'http://%(username)s:%(password)s@%(host)s/xmlrpc'
|
||||
|
||||
def download(self, data = {}, movie = {}, filedata = None):
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
if not filedata:
|
||||
log.error('Unable to get NZB file: %s', traceback.format_exc())
|
||||
@@ -32,7 +35,7 @@ class NZBGet(Downloader):
|
||||
rpc = xmlrpclib.ServerProxy(url)
|
||||
try:
|
||||
if rpc.writelog('INFO', 'CouchPotato connected to drop off %s.' % nzb_name):
|
||||
log.info('Successfully connected to NZBGet')
|
||||
log.debug('Successfully connected to NZBGet')
|
||||
else:
|
||||
log.info('Successfully connected to NZBGet, but unable to send a message')
|
||||
except socket.error:
|
||||
@@ -73,7 +76,7 @@ class NZBGet(Downloader):
|
||||
rpc = xmlrpclib.ServerProxy(url)
|
||||
try:
|
||||
if rpc.writelog('INFO', 'CouchPotato connected to check status'):
|
||||
log.info('Successfully connected to NZBGet')
|
||||
log.debug('Successfully connected to NZBGet')
|
||||
else:
|
||||
log.info('Successfully connected to NZBGet, but unable to send a message')
|
||||
except socket.error:
|
||||
@@ -104,12 +107,21 @@ class NZBGet(Downloader):
|
||||
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
|
||||
except:
|
||||
nzb_id = item['NZBID']
|
||||
|
||||
|
||||
timeleft = -1
|
||||
try:
|
||||
if item['ActiveDownloads'] > 0 and item['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
|
||||
timeleft = str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
|
||||
except:
|
||||
pass
|
||||
|
||||
statuses.append({
|
||||
'id': nzb_id,
|
||||
'name': item['NZBFilename'],
|
||||
'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED',
|
||||
# Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
|
||||
'timeleft': str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20)) if item['ActiveDownloads'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']) else -1,
|
||||
'timeleft': timeleft,
|
||||
})
|
||||
|
||||
for item in queue: # 'Parameters' is not passed in rpc.postqueue
|
||||
@@ -130,10 +142,10 @@ class NZBGet(Downloader):
|
||||
statuses.append({
|
||||
'id': nzb_id,
|
||||
'name': item['NZBFilename'],
|
||||
'status': 'completed' if item['ParStatus'] == 'SUCCESS' and item['ScriptStatus'] == 'SUCCESS' else 'failed',
|
||||
'status': 'completed' if item['ParStatus'] in ['SUCCESS','NONE'] and item['ScriptStatus'] in ['SUCCESS','NONE'] else 'failed',
|
||||
'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': item['DestDir']
|
||||
'folder': ss(item['DestDir'])
|
||||
})
|
||||
|
||||
return statuses
|
||||
@@ -142,12 +154,12 @@ class NZBGet(Downloader):
|
||||
|
||||
log.info('%s failed downloading, deleting...', item['name'])
|
||||
|
||||
url = self.url % {'host': self.conf('host'), 'password': self.conf('password')}
|
||||
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
|
||||
|
||||
rpc = xmlrpclib.ServerProxy(url)
|
||||
try:
|
||||
if rpc.writelog('INFO', 'CouchPotato connected to delete some history'):
|
||||
log.info('Successfully connected to NZBGet')
|
||||
log.debug('Successfully connected to NZBGet')
|
||||
else:
|
||||
log.info('Successfully connected to NZBGet, but unable to send a message')
|
||||
except socket.error:
|
||||
@@ -162,11 +174,16 @@ class NZBGet(Downloader):
|
||||
|
||||
try:
|
||||
history = rpc.history()
|
||||
nzb_id = None
|
||||
path = None
|
||||
|
||||
for hist in history:
|
||||
if hist['Parameters'] and hist['Parameters']['couchpotato'] and hist['Parameters']['couchpotato'] == item['id']:
|
||||
nzb_id = hist['ID']
|
||||
path = hist['DestDir']
|
||||
if rpc.editqueue('HistoryDelete', 0, "", [tryInt(nzb_id)]):
|
||||
for param in hist['Parameters']:
|
||||
if param['Name'] == 'couchpotato' and param['Value'] == item['id']:
|
||||
nzb_id = hist['ID']
|
||||
path = hist['DestDir']
|
||||
|
||||
if nzb_id and path and rpc.editqueue('HistoryDelete', 0, "", [tryInt(nzb_id)]):
|
||||
shutil.rmtree(path, True)
|
||||
except:
|
||||
log.error('Failed deleting: %s', traceback.format_exc(0))
|
||||
|
||||
@@ -38,6 +38,7 @@ config = [{
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
|
||||
@@ -16,13 +16,16 @@ import urllib2
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class NZBVortex(Downloader):
|
||||
|
||||
type = ['nzb']
|
||||
protocol = ['nzb']
|
||||
api_level = None
|
||||
session_id = None
|
||||
|
||||
def download(self, data = {}, movie = {}, filedata = None):
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
# Send the nzb
|
||||
try:
|
||||
@@ -55,8 +58,8 @@ class NZBVortex(Downloader):
|
||||
'name': item['uiTitle'],
|
||||
'status': status,
|
||||
'original_status': item['state'],
|
||||
'timeleft': -1,
|
||||
'folder': item['destinationPath'],
|
||||
'timeleft':-1,
|
||||
'folder': ss(item['destinationPath']),
|
||||
})
|
||||
|
||||
return statuses
|
||||
@@ -96,9 +99,10 @@ class NZBVortex(Downloader):
|
||||
return False
|
||||
|
||||
|
||||
def call(self, call, parameters = {}, repeat = False, auth = True, *args, **kwargs):
|
||||
def call(self, call, parameters = None, repeat = False, auth = True, *args, **kwargs):
|
||||
|
||||
# Login first
|
||||
if not parameters: parameters = {}
|
||||
if not self.session_id and auth:
|
||||
self.login()
|
||||
|
||||
@@ -121,7 +125,7 @@ class NZBVortex(Downloader):
|
||||
# Try login and do again
|
||||
if not repeat:
|
||||
self.login()
|
||||
return self.call(call, parameters = parameters, repeat = True, *args, **kwargs)
|
||||
return self.call(call, parameters = parameters, repeat = True, **kwargs)
|
||||
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
except:
|
||||
@@ -147,7 +151,8 @@ class NZBVortex(Downloader):
|
||||
|
||||
return self.api_level
|
||||
|
||||
def isEnabled(self, manual, data):
|
||||
def isEnabled(self, manual = False, data = None):
|
||||
if not data: data = {}
|
||||
return super(NZBVortex, self).isEnabled(manual, data) and self.getApiLevel()
|
||||
|
||||
|
||||
|
||||
@@ -6,12 +6,15 @@ import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Pneumatic(Downloader):
|
||||
|
||||
type = ['nzb']
|
||||
protocol = ['nzb']
|
||||
strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s'
|
||||
|
||||
def download(self, data = {}, movie = {}, filedata = None):
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
directory = self.conf('directory')
|
||||
if not directory or not os.path.isdir(directory):
|
||||
@@ -26,7 +29,7 @@ class Pneumatic(Downloader):
|
||||
|
||||
try:
|
||||
if not os.path.isfile(fullPath):
|
||||
log.info('Downloading %s to %s.', (data.get('type'), fullPath))
|
||||
log.info('Downloading %s to %s.', (data.get('protocol'), fullPath))
|
||||
with open(fullPath, 'wb') as f:
|
||||
f.write(filedata)
|
||||
|
||||
|
||||
71
couchpotato/core/downloaders/rtorrent/__init__.py
Executable file
71
couchpotato/core/downloaders/rtorrent/__init__.py
Executable file
@@ -0,0 +1,71 @@
|
||||
from .main import rTorrent
|
||||
|
||||
def start():
|
||||
return rTorrent()
|
||||
|
||||
config = [{
|
||||
'name': 'rtorrent',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'list': 'download_providers',
|
||||
'name': 'rtorrent',
|
||||
'label': 'rTorrent',
|
||||
'description': '',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'torrent',
|
||||
},
|
||||
{
|
||||
'name': 'url',
|
||||
'default': 'http://localhost:80/RPC2',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'label',
|
||||
'description': 'Label to apply on added torrents.',
|
||||
},
|
||||
{
|
||||
'name': 'remove_complete',
|
||||
'label': 'Remove torrent',
|
||||
'default': False,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Remove the torrent after it finishes seeding.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_files',
|
||||
'label': 'Remove files',
|
||||
'default': True,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also remove the leftover files.',
|
||||
},
|
||||
{
|
||||
'name': 'paused',
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'default': False,
|
||||
'description': 'Add the torrent paused.',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
201
couchpotato/core/downloaders/rtorrent/main.py
Executable file
201
couchpotato/core/downloaders/rtorrent/main.py
Executable file
@@ -0,0 +1,201 @@
|
||||
from base64 import b16encode, b32decode
|
||||
from bencode import bencode, bdecode
|
||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.logger import CPLog
|
||||
from datetime import timedelta
|
||||
from hashlib import sha1
|
||||
from rtorrent import RTorrent
|
||||
from rtorrent.err import MethodError
|
||||
import shutil
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class rTorrent(Downloader):
|
||||
|
||||
protocol = ['torrent', 'torrent_magnet']
|
||||
rt = None
|
||||
|
||||
def connect(self):
|
||||
# Already connected?
|
||||
if self.rt is not None:
|
||||
return self.rt
|
||||
|
||||
# Ensure url is set
|
||||
if not self.conf('url'):
|
||||
log.error('Config properties are not filled in correctly, url is missing.')
|
||||
return False
|
||||
|
||||
if self.conf('username') and self.conf('password'):
|
||||
self.rt = RTorrent(
|
||||
self.conf('url'),
|
||||
self.conf('username'),
|
||||
self.conf('password')
|
||||
)
|
||||
else:
|
||||
self.rt = RTorrent(self.conf('url'))
|
||||
|
||||
return self.rt
|
||||
|
||||
def _update_provider_group(self, name, data):
|
||||
if data.get('seed_time'):
|
||||
log.info('seeding time ignored, not supported')
|
||||
|
||||
if not name:
|
||||
return False
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
views = self.rt.get_views()
|
||||
|
||||
if name not in views:
|
||||
self.rt.create_group(name)
|
||||
|
||||
group = self.rt.get_group(name)
|
||||
|
||||
try:
|
||||
if data.get('seed_ratio'):
|
||||
ratio = int(float(data.get('seed_ratio')) * 100)
|
||||
log.debug('Updating provider ratio to %s, group name: %s', (ratio, name))
|
||||
|
||||
# Explicitly set all group options to ensure it is setup correctly
|
||||
group.set_upload('1M')
|
||||
group.set_min(ratio)
|
||||
group.set_max(ratio)
|
||||
group.set_command('d.stop')
|
||||
group.enable()
|
||||
else:
|
||||
# Reset group action and disable it
|
||||
group.set_command()
|
||||
group.disable()
|
||||
except MethodError, err:
|
||||
log.error('Unable to set group options: %s', err.message)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def download(self, data, movie, filedata = None):
|
||||
log.debug('Sending "%s" to rTorrent.', (data.get('name')))
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
group_name = 'cp_' + data.get('provider').lower()
|
||||
if not self._update_provider_group(group_name, data):
|
||||
return False
|
||||
|
||||
torrent_params = {}
|
||||
if self.conf('label'):
|
||||
torrent_params['label'] = self.conf('label')
|
||||
|
||||
if not filedata and data.get('protocol') == 'torrent':
|
||||
log.error('Failed sending torrent, no data')
|
||||
return False
|
||||
|
||||
# Try download magnet torrents
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
filedata = self.magnetToTorrent(data.get('url'))
|
||||
|
||||
if filedata is False:
|
||||
return False
|
||||
|
||||
data['protocol'] = 'torrent'
|
||||
|
||||
info = bdecode(filedata)["info"]
|
||||
torrent_hash = sha1(bencode(info)).hexdigest().upper()
|
||||
|
||||
# Convert base 32 to hex
|
||||
if len(torrent_hash) == 32:
|
||||
torrent_hash = b16encode(b32decode(torrent_hash))
|
||||
|
||||
# Send request to rTorrent
|
||||
try:
|
||||
# Send torrent to rTorrent
|
||||
torrent = self.rt.load_torrent(filedata)
|
||||
|
||||
# Set label
|
||||
if self.conf('label'):
|
||||
torrent.set_custom(1, self.conf('label'))
|
||||
|
||||
# Set Ratio Group
|
||||
torrent.set_visible(group_name)
|
||||
|
||||
# Start torrent
|
||||
if not self.conf('paused', default = 0):
|
||||
torrent.start()
|
||||
|
||||
return self.downloadReturnId(torrent_hash)
|
||||
except Exception, err:
|
||||
log.error('Failed to send torrent to rTorrent: %s', err)
|
||||
return False
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
log.debug('Checking rTorrent download status.')
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
try:
|
||||
torrents = self.rt.get_torrents()
|
||||
|
||||
statuses = StatusList(self)
|
||||
|
||||
for item in torrents:
|
||||
status = 'busy'
|
||||
if item.complete:
|
||||
if item.active:
|
||||
status = 'seeding'
|
||||
else:
|
||||
status = 'completed'
|
||||
|
||||
statuses.append({
|
||||
'id': item.info_hash,
|
||||
'name': item.name,
|
||||
'status': status,
|
||||
'seed_ratio': item.ratio,
|
||||
'original_status': item.state,
|
||||
'timeleft': str(timedelta(seconds = float(item.left_bytes) / item.down_rate)) if item.down_rate > 0 else -1,
|
||||
'folder': ss(item.directory)
|
||||
})
|
||||
|
||||
return statuses
|
||||
|
||||
except Exception, err:
|
||||
log.error('Failed to get status from rTorrent: %s', err)
|
||||
return False
|
||||
|
||||
def pause(self, download_info, pause = True):
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
torrent = self.rt.find_torrent(download_info['id'])
|
||||
if torrent is None:
|
||||
return False
|
||||
|
||||
if pause:
|
||||
return torrent.pause()
|
||||
return torrent.resume()
|
||||
|
||||
def removeFailed(self, item):
|
||||
log.info('%s failed downloading, deleting...', item['name'])
|
||||
return self.processComplete(item, delete_files = True)
|
||||
|
||||
def processComplete(self, item, delete_files):
|
||||
log.debug('Requesting rTorrent to remove the torrent %s%s.',
|
||||
(item['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
torrent = self.rt.find_torrent(item['id'])
|
||||
if torrent is None:
|
||||
return False
|
||||
|
||||
torrent.erase() # just removes the torrent, doesn't delete data
|
||||
|
||||
if delete_files:
|
||||
shutil.rmtree(item['folder'], True)
|
||||
|
||||
return True
|
||||
@@ -11,7 +11,7 @@ config = [{
|
||||
'list': 'download_providers',
|
||||
'name': 'sabnzbd',
|
||||
'label': 'Sabnzbd',
|
||||
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> to download NZBs.',
|
||||
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> (0.7+) to download NZBs.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
@@ -34,6 +34,15 @@ config = [{
|
||||
'label': 'Category',
|
||||
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'priority',
|
||||
'label': 'Priority',
|
||||
'type': 'dropdown',
|
||||
'default': '0',
|
||||
'advanced': True,
|
||||
'values': [('Paused', -2), ('Low', -1), ('Normal', 0), ('High', 1), ('Forced', 2)],
|
||||
'description': 'Add to the queue with this priority.',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': False,
|
||||
@@ -41,9 +50,18 @@ config = [{
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'remove_complete',
|
||||
'advanced': True,
|
||||
'label': 'Remove NZB',
|
||||
'default': False,
|
||||
'type': 'bool',
|
||||
'description': 'Remove the NZB from history after it completed.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
|
||||
@@ -10,11 +10,14 @@ import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Sabnzbd(Downloader):
|
||||
|
||||
type = ['nzb']
|
||||
protocol = ['nzb']
|
||||
|
||||
def download(self, data = {}, movie = {}, filedata = None):
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.info('Sending "%s" to SABnzbd.', data.get('name'))
|
||||
|
||||
@@ -22,11 +25,13 @@ class Sabnzbd(Downloader):
|
||||
'cat': self.conf('category'),
|
||||
'mode': 'addurl',
|
||||
'nzbname': self.createNzbName(data, movie),
|
||||
'priority': self.conf('priority'),
|
||||
}
|
||||
|
||||
nzb_filename = None
|
||||
if filedata:
|
||||
if len(filedata) < 50:
|
||||
log.error('No proper nzb available: %s', (filedata))
|
||||
log.error('No proper nzb available: %s', filedata)
|
||||
return False
|
||||
|
||||
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
|
||||
@@ -36,7 +41,7 @@ class Sabnzbd(Downloader):
|
||||
req_params['name'] = data.get('url')
|
||||
|
||||
try:
|
||||
if req_params.get('mode') is 'addfile':
|
||||
if nzb_filename and req_params.get('mode') is 'addfile':
|
||||
sab_data = self.call(req_params, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True)
|
||||
else:
|
||||
sab_data = self.call(req_params)
|
||||
@@ -107,7 +112,7 @@ class Sabnzbd(Downloader):
|
||||
'status': status,
|
||||
'original_status': item['status'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': item['storage'],
|
||||
'folder': ss(item['storage']),
|
||||
})
|
||||
|
||||
return statuses
|
||||
@@ -129,6 +134,22 @@ class Sabnzbd(Downloader):
|
||||
|
||||
return True
|
||||
|
||||
def processComplete(self, item, delete_files = False):
|
||||
log.debug('Requesting SabNZBd to remove the NZB %s.', item['name'])
|
||||
|
||||
try:
|
||||
self.call({
|
||||
'mode': 'history',
|
||||
'name': 'delete',
|
||||
'del_files': '0',
|
||||
'value': item['id']
|
||||
}, use_json = False)
|
||||
except:
|
||||
log.error('Failed removing: %s', traceback.format_exc(0))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def call(self, request_params, use_json = True, **kwargs):
|
||||
|
||||
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(request_params, {
|
||||
|
||||
@@ -18,7 +18,7 @@ config = [{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'torrent',
|
||||
'radio_group': 'nzb,torrent',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
@@ -32,6 +32,13 @@ config = [{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'use_for',
|
||||
'label': 'Use for',
|
||||
'default': 'both',
|
||||
'type': 'dropdown',
|
||||
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.helpers.encoding import isInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
import httplib
|
||||
import json
|
||||
import urllib
|
||||
import urllib2
|
||||
|
||||
import requests
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Synology(Downloader):
|
||||
|
||||
type = ['torrent_magnet']
|
||||
protocol = ['nzb', 'torrent', 'torrent_magnet']
|
||||
log = CPLog(__name__)
|
||||
|
||||
def download(self, data, movie, filedata = None):
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.error('Sending "%s" (%s) to Synology.', (data.get('name'), data.get('type')))
|
||||
response = False
|
||||
log.error('Sending "%s" (%s) to Synology.', (data['name'], data['protocol']))
|
||||
|
||||
# Load host from config and split out port.
|
||||
host = self.conf('host').split(':')
|
||||
@@ -24,24 +25,47 @@ class Synology(Downloader):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
return False
|
||||
|
||||
if data.get('type') == 'torrent':
|
||||
log.error('Can\'t add binary torrent file')
|
||||
return False
|
||||
|
||||
try:
|
||||
# Send request to Transmission
|
||||
# Send request to Synology
|
||||
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
|
||||
remote_torrent = srpc.add_torrent_uri(data.get('url'))
|
||||
log.info('Response: %s', remote_torrent)
|
||||
return remote_torrent['success']
|
||||
if data['protocol'] == 'torrent_magnet':
|
||||
log.info('Adding torrent URL %s', data['url'])
|
||||
response = srpc.create_task(url = data['url'])
|
||||
elif data['protocol'] in ['nzb', 'torrent']:
|
||||
log.info('Adding %s' % data['protocol'])
|
||||
if not filedata:
|
||||
log.error('No %s data found' % data['protocol'])
|
||||
else:
|
||||
filename = data['name'] + '.' + data['protocol']
|
||||
response = srpc.create_task(filename = filename, filedata = filedata)
|
||||
except Exception, err:
|
||||
log.error('Exception while adding torrent: %s', err)
|
||||
return False
|
||||
finally:
|
||||
return response
|
||||
|
||||
def getEnabledProtocol(self):
|
||||
if self.conf('use_for') == 'both':
|
||||
return super(Synology, self).getEnabledProtocol()
|
||||
elif self.conf('use_for') == 'torrent':
|
||||
return ['torrent', 'torrent_magnet']
|
||||
else:
|
||||
return ['nzb']
|
||||
|
||||
def isEnabled(self, manual = False, data = None):
|
||||
if not data: data = {}
|
||||
|
||||
for_protocol = ['both']
|
||||
if data and 'torrent' in data.get('protocol'):
|
||||
for_protocol.append('torrent')
|
||||
elif data:
|
||||
for_protocol.append(data.get('protocol'))
|
||||
|
||||
return super(Synology, self).isEnabled(manual, data) and\
|
||||
((self.conf('use_for') in for_protocol))
|
||||
|
||||
class SynologyRPC(object):
|
||||
|
||||
'''SynologyRPC lite library'''
|
||||
"""SynologyRPC lite library"""
|
||||
|
||||
def __init__(self, host = 'localhost', port = 5000, username = None, password = None):
|
||||
|
||||
@@ -58,11 +82,13 @@ class SynologyRPC(object):
|
||||
args = {'api': 'SYNO.API.Auth', 'account': self.username, 'passwd': self.password, 'version': 2,
|
||||
'method': 'login', 'session': self.session_name, 'format': 'sid'}
|
||||
response = self._req(self.auth_url, args)
|
||||
if response['success'] == True:
|
||||
if response['success']:
|
||||
self.sid = response['data']['sid']
|
||||
log.debug('Sid=%s', self.sid)
|
||||
return response
|
||||
elif self.username or self.password:
|
||||
log.debug('sid=%s', self.sid)
|
||||
else:
|
||||
log.error('Couldn\'t login to Synology, %s', response)
|
||||
return response['success']
|
||||
else:
|
||||
log.error('User or password missing, not using authentication.')
|
||||
return False
|
||||
|
||||
@@ -70,36 +96,51 @@ class SynologyRPC(object):
|
||||
args = {'api':'SYNO.API.Auth', 'version':1, 'method':'logout', 'session':self.session_name, '_sid':self.sid}
|
||||
return self._req(self.auth_url, args)
|
||||
|
||||
def _req(self, url, args):
|
||||
req_url = url + '?' + urllib.urlencode(args)
|
||||
def _req(self, url, args, files = None):
|
||||
response = {'success': False}
|
||||
try:
|
||||
req_open = urllib2.urlopen(req_url)
|
||||
response = json.loads(req_open.read())
|
||||
if response['success'] == True:
|
||||
req = requests.post(url, data = args, files = files)
|
||||
req.raise_for_status()
|
||||
response = json.loads(req.text)
|
||||
if response['success']:
|
||||
log.info('Synology action successfull')
|
||||
return response
|
||||
except httplib.InvalidURL, err:
|
||||
log.error('Invalid Transmission host, check your config %s', err)
|
||||
return False
|
||||
except urllib2.HTTPError, err:
|
||||
except requests.ConnectionError, err:
|
||||
log.error('Synology connection error, check your config %s', err)
|
||||
except requests.HTTPError, err:
|
||||
log.error('SynologyRPC HTTPError: %s', err)
|
||||
return False
|
||||
except urllib2.URLError, err:
|
||||
log.error('Unable to connect to Synology %s', err)
|
||||
return False
|
||||
except Exception, err:
|
||||
log.error('Exception: %s', err)
|
||||
finally:
|
||||
return response
|
||||
|
||||
def add_torrent_uri(self, torrent):
|
||||
log.info('Adding torrent URL %s', torrent)
|
||||
response = {}
|
||||
def create_task(self, url = None, filename = None, filedata = None):
|
||||
""" Creates new download task in Synology DownloadStation. Either specify
|
||||
url or pair (filename, filedata).
|
||||
|
||||
Returns True if task was created, False otherwise
|
||||
"""
|
||||
result = False
|
||||
# login
|
||||
login = self._login()
|
||||
if len(login) > 0 and login['success'] == True:
|
||||
log.info('Login success, adding torrent')
|
||||
args = {'api':'SYNO.DownloadStation.Task', 'version':1, 'method':'create', 'uri':torrent, '_sid':self.sid}
|
||||
response = self._req(self.download_url, args)
|
||||
if self._login():
|
||||
args = {'api': 'SYNO.DownloadStation.Task',
|
||||
'version': '1',
|
||||
'method': 'create',
|
||||
'_sid': self.sid}
|
||||
if url:
|
||||
log.info('Login success, adding torrent URI')
|
||||
args['uri'] = url
|
||||
response = self._req(self.download_url, args = args)
|
||||
log.info('Response: %s', response)
|
||||
result = response['success']
|
||||
elif filename and filedata:
|
||||
log.info('Login success, adding torrent')
|
||||
files = {'file': (filename, filedata)}
|
||||
response = self._req(self.download_url, args = args, files = files)
|
||||
log.info('Response: %s', response)
|
||||
result = response['success']
|
||||
else:
|
||||
log.error('Invalid use of SynologyRPC.create_task: either url or filename+filedata must be specified')
|
||||
self._logout()
|
||||
else:
|
||||
log.error('Couldn\'t login to Synology, %s', login)
|
||||
return response
|
||||
|
||||
|
||||
return result
|
||||
|
||||
@@ -25,6 +25,13 @@ config = [{
|
||||
'default': 'localhost:9091',
|
||||
'description': 'Hostname with port. Usually <strong>localhost:9091</strong>',
|
||||
},
|
||||
{
|
||||
'name': 'rpc_url',
|
||||
'type': 'string',
|
||||
'default': 'transmission',
|
||||
'advanced': True,
|
||||
'description': 'Change if you don\'t run Transmission RPC at the default url.',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
},
|
||||
@@ -32,30 +39,33 @@ config = [{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'paused',
|
||||
'type': 'bool',
|
||||
'default': False,
|
||||
'description': 'Add the torrent paused.',
|
||||
},
|
||||
{
|
||||
'name': 'directory',
|
||||
'type': 'directory',
|
||||
'description': 'Download to this directory. Keep empty for default Transmission download directory.',
|
||||
},
|
||||
{
|
||||
'name': 'ratio',
|
||||
'default': 10,
|
||||
'type': 'float',
|
||||
'name': 'remove_complete',
|
||||
'label': 'Remove torrent',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'description': 'Stop transfer when reaching ratio',
|
||||
'type': 'bool',
|
||||
'description': 'Remove the torrent from Transmission after it finished seeding.',
|
||||
},
|
||||
{
|
||||
'name': 'ratiomode',
|
||||
'default': 0,
|
||||
'type': 'int',
|
||||
'name': 'delete_files',
|
||||
'label': 'Remove files',
|
||||
'default': True,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': '0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit.',
|
||||
'description': 'Also remove the leftover files.',
|
||||
},
|
||||
{
|
||||
'name': 'paused',
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'default': False,
|
||||
'description': 'Add the torrent paused.',
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
@@ -64,6 +74,20 @@ config = [{
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'stalled_as_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Consider a stalled torrent as failed',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from base64 import b64encode
|
||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||
from couchpotato.core.helpers.encoding import isInt
|
||||
from couchpotato.core.helpers.encoding import isInt, ss
|
||||
from couchpotato.core.helpers.variable import tryInt, tryFloat
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from datetime import timedelta
|
||||
@@ -8,8 +9,6 @@ import httplib
|
||||
import json
|
||||
import os.path
|
||||
import re
|
||||
import shutil
|
||||
import traceback
|
||||
import urllib2
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -17,154 +16,140 @@ log = CPLog(__name__)
|
||||
|
||||
class Transmission(Downloader):
|
||||
|
||||
type = ['torrent', 'torrent_magnet']
|
||||
protocol = ['torrent', 'torrent_magnet']
|
||||
log = CPLog(__name__)
|
||||
trpc = None
|
||||
|
||||
def download(self, data, movie, filedata = None):
|
||||
|
||||
log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
|
||||
|
||||
def connect(self):
|
||||
# Load host from config and split out port.
|
||||
host = self.conf('host').split(':')
|
||||
if not isInt(host[1]):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
return False
|
||||
|
||||
# Set parameters for Transmission
|
||||
params = {
|
||||
'paused': self.conf('paused', default = 0),
|
||||
}
|
||||
if not self.trpc:
|
||||
self.trpc = TransmissionRPC(host[0], port = host[1], rpc_url = self.conf('rpc_url'), username = self.conf('username'), password = self.conf('password'))
|
||||
|
||||
if len(self.conf('directory', default = '')) > 0:
|
||||
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)
|
||||
return self.trpc
|
||||
|
||||
# Create the empty folder to download too
|
||||
self.makeDir(folder_path)
|
||||
def download(self, data, movie, filedata = None):
|
||||
|
||||
params['download-dir'] = folder_path
|
||||
log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('protocol')))
|
||||
|
||||
torrent_params = {}
|
||||
if self.conf('ratio'):
|
||||
torrent_params = {
|
||||
'seedRatioLimit': self.conf('ratio'),
|
||||
'seedRatioMode': self.conf('ratiomode')
|
||||
}
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
if not filedata and data.get('type') == 'torrent':
|
||||
if not filedata and data.get('protocol') == 'torrent':
|
||||
log.error('Failed sending torrent, no data')
|
||||
return False
|
||||
|
||||
# Send request to Transmission
|
||||
try:
|
||||
trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||
if data.get('type') == 'torrent_magnet':
|
||||
remote_torrent = trpc.add_torrent_uri(data.get('url'), arguments = params)
|
||||
torrent_params['trackerAdd'] = self.torrent_trackers
|
||||
# Set parameters for adding torrent
|
||||
params = {
|
||||
'paused': self.conf('paused', default = False)
|
||||
}
|
||||
|
||||
if self.conf('directory'):
|
||||
if os.path.isdir(self.conf('directory')):
|
||||
params['download-dir'] = self.conf('directory')
|
||||
else:
|
||||
remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params)
|
||||
log.error('Download directory from Transmission settings: %s doesn\'t exist', self.conf('directory'))
|
||||
|
||||
if not remote_torrent:
|
||||
return False
|
||||
# Change parameters of torrent
|
||||
torrent_params = {}
|
||||
if data.get('seed_ratio'):
|
||||
torrent_params['seedRatioLimit'] = tryFloat(data.get('seed_ratio'))
|
||||
torrent_params['seedRatioMode'] = 1
|
||||
|
||||
# Change settings of added torrents
|
||||
elif torrent_params:
|
||||
trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
|
||||
if data.get('seed_time'):
|
||||
torrent_params['seedIdleLimit'] = tryInt(data.get('seed_time')) * 60
|
||||
torrent_params['seedIdleMode'] = 1
|
||||
|
||||
log.info('Torrent sent to Transmission successfully.')
|
||||
return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
|
||||
except:
|
||||
log.error('Failed to change settings for transfer: %s', traceback.format_exc())
|
||||
# Send request to Transmission
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
remote_torrent = self.trpc.add_torrent_uri(data.get('url'), arguments = params)
|
||||
torrent_params['trackerAdd'] = self.torrent_trackers
|
||||
else:
|
||||
remote_torrent = self.trpc.add_torrent_file(b64encode(filedata), arguments = params)
|
||||
|
||||
if not remote_torrent:
|
||||
log.error('Failed sending torrent to Transmission')
|
||||
return False
|
||||
|
||||
# Change settings of added torrents
|
||||
if torrent_params:
|
||||
self.trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
|
||||
|
||||
log.info('Torrent sent to Transmission successfully.')
|
||||
return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
|
||||
log.debug('Checking Transmission download status.')
|
||||
|
||||
# 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
|
||||
|
||||
# Go through Queue
|
||||
try:
|
||||
trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||
return_params = {
|
||||
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isFinished', 'downloadDir', 'uploadRatio']
|
||||
}
|
||||
queue = trpc.get_alltorrents(return_params)
|
||||
|
||||
except Exception, err:
|
||||
log.error('Failed getting queue: %s', err)
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
statuses = StatusList(self)
|
||||
|
||||
# Get torrents status
|
||||
# CouchPotato Status
|
||||
#status = 'busy'
|
||||
#status = 'failed'
|
||||
#status = 'completed'
|
||||
# Transmission Status
|
||||
#status = 0 => "Torrent is stopped"
|
||||
#status = 1 => "Queued to check files"
|
||||
#status = 2 => "Checking files"
|
||||
#status = 3 => "Queued to download"
|
||||
#status = 4 => "Downloading"
|
||||
#status = 4 => "Queued to seed"
|
||||
#status = 6 => "Seeding"
|
||||
#To do :
|
||||
# add checking file
|
||||
# manage no peer in a range time => fail
|
||||
return_params = {
|
||||
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isStalled', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit']
|
||||
}
|
||||
|
||||
queue = self.trpc.get_alltorrents(return_params)
|
||||
if not (queue and queue.get('torrents')):
|
||||
log.debug('Nothing in queue or error')
|
||||
return False
|
||||
|
||||
for item in queue['torrents']:
|
||||
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / confRatio=%s / isFinished=%s', (item['name'], item['id'], item['downloadDir'], item['hashString'], item['percentDone'], item['status'], item['eta'], item['uploadRatio'], self.conf('ratio'), item['isFinished']))
|
||||
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / isFinished=%s',
|
||||
(item['name'], item['id'], item['downloadDir'], item['hashString'], item['percentDone'], item['status'], item['eta'], item['uploadRatio'], item['isFinished']))
|
||||
|
||||
if not os.path.isdir(Env.setting('from', 'renamer')):
|
||||
log.error('Renamer "from" folder doesn\'t to exist.')
|
||||
return
|
||||
|
||||
if (item['percentDone'] * 100) >= 100 and (item['status'] == 6 or item['status'] == 0) and item['uploadRatio'] > self.conf('ratio'):
|
||||
try:
|
||||
trpc.stop_torrent(item['hashString'], {})
|
||||
statuses.append({
|
||||
'id': item['hashString'],
|
||||
'name': item['name'],
|
||||
'status': 'completed',
|
||||
'original_status': item['status'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
'folder': os.path.join(item['downloadDir'], item['name']),
|
||||
})
|
||||
except Exception, err:
|
||||
log.error('Failed to stop and remove torrent "%s" with error: %s', (item['name'], err))
|
||||
statuses.append({
|
||||
'id': item['hashString'],
|
||||
'name': item['name'],
|
||||
'status': 'failed',
|
||||
'original_status': item['status'],
|
||||
'timeleft': str(timedelta(seconds = 0)),
|
||||
})
|
||||
else:
|
||||
statuses.append({
|
||||
'id': item['hashString'],
|
||||
'name': item['name'],
|
||||
'status': 'busy',
|
||||
'original_status': item['status'],
|
||||
'timeleft': str(timedelta(seconds = item['eta'])), # Is ETA in seconds??
|
||||
})
|
||||
status = 'busy'
|
||||
if item['isStalled'] and self.conf('stalled_as_failed'):
|
||||
status = 'failed'
|
||||
elif item['status'] == 0 and item['percentDone'] == 1:
|
||||
status = 'completed'
|
||||
elif item['status'] in [5, 6]:
|
||||
status = 'seeding'
|
||||
|
||||
statuses.append({
|
||||
'id': item['hashString'],
|
||||
'name': item['name'],
|
||||
'status': status,
|
||||
'original_status': item['status'],
|
||||
'seed_ratio': item['uploadRatio'],
|
||||
'timeleft': str(timedelta(seconds = item['eta'])),
|
||||
'folder': ss(os.path.join(item['downloadDir'], item['name'])),
|
||||
})
|
||||
|
||||
return statuses
|
||||
|
||||
def pause(self, item, pause = True):
|
||||
if pause:
|
||||
return self.trpc.stop_torrent(item['id'])
|
||||
else:
|
||||
return self.trpc.start_torrent(item['id'])
|
||||
|
||||
def removeFailed(self, item):
|
||||
log.info('%s failed downloading, deleting...', item['name'])
|
||||
return self.trpc.remove_torrent(item['hashString'], True)
|
||||
|
||||
def processComplete(self, item, delete_files = False):
|
||||
log.debug('Requesting Transmission to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||
return self.trpc.remove_torrent(item['hashString'], delete_files)
|
||||
|
||||
class TransmissionRPC(object):
|
||||
|
||||
"""TransmissionRPC lite library"""
|
||||
|
||||
def __init__(self, host = 'localhost', port = 9091, username = None, password = None):
|
||||
def __init__(self, host = 'localhost', port = 9091, rpc_url = 'transmission', username = None, password = None):
|
||||
|
||||
super(TransmissionRPC, self).__init__()
|
||||
|
||||
self.url = 'http://' + host + ':' + str(port) + '/transmission/rpc'
|
||||
self.url = 'http://' + host + ':' + str(port) + '/' + rpc_url + '/rpc'
|
||||
self.tag = 0
|
||||
self.session_id = 0
|
||||
self.session = {}
|
||||
@@ -188,7 +173,7 @@ class TransmissionRPC(object):
|
||||
log.debug('request: %s', json.dumps(ojson))
|
||||
log.debug('response: %s', json.dumps(response))
|
||||
if response['result'] == 'success':
|
||||
log.debug('Transmission action successfull')
|
||||
log.debug('Transmission action successful')
|
||||
return response['arguments']
|
||||
else:
|
||||
log.debug('Unknown failure sending command to Transmission. Return text is: %s', response['result'])
|
||||
@@ -240,13 +225,15 @@ class TransmissionRPC(object):
|
||||
post_data = {'arguments': arguments, 'method': 'torrent-get', 'tag': self.tag}
|
||||
return self._request(post_data)
|
||||
|
||||
def stop_torrent(self, torrent_id, arguments):
|
||||
arguments['ids'] = torrent_id
|
||||
post_data = {'arguments': arguments, 'method': 'torrent-stop', 'tag': self.tag}
|
||||
def stop_torrent(self, torrent_id):
|
||||
post_data = {'arguments': {'ids': torrent_id}, 'method': 'torrent-stop', 'tag': self.tag}
|
||||
return self._request(post_data)
|
||||
|
||||
def remove_torrent(self, torrent_id, remove_local_data, arguments):
|
||||
arguments['ids'] = torrent_id
|
||||
arguments['delete-local-data'] = remove_local_data
|
||||
post_data = {'arguments': arguments, 'method': 'torrent-remove', 'tag': self.tag}
|
||||
def start_torrent(self, torrent_id):
|
||||
post_data = {'arguments': {'ids': torrent_id}, 'method': 'torrent-start', 'tag': self.tag}
|
||||
return self._request(post_data)
|
||||
|
||||
def remove_torrent(self, torrent_id, delete_local_data):
|
||||
post_data = {'arguments': {'ids': torrent_id, 'delete-local-data': delete_local_data}, 'method': 'torrent-remove', 'tag': self.tag}
|
||||
return self._request(post_data)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ config = [{
|
||||
'list': 'download_providers',
|
||||
'name': 'utorrent',
|
||||
'label': 'uTorrent',
|
||||
'description': 'Use <a href="http://www.utorrent.com/" target="_blank">uTorrent</a> to download torrents.',
|
||||
'description': 'Use <a href="http://www.utorrent.com/" target="_blank">uTorrent</a> (3.0+) to download torrents.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
@@ -36,9 +36,26 @@ config = [{
|
||||
'name': 'label',
|
||||
'description': 'Label to add torrent as.',
|
||||
},
|
||||
{
|
||||
'name': 'remove_complete',
|
||||
'label': 'Remove torrent',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Remove the torrent from uTorrent after it finished seeding.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_files',
|
||||
'label': 'Remove files',
|
||||
'default': True,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also remove the leftover files.',
|
||||
},
|
||||
{
|
||||
'name': 'paused',
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'default': False,
|
||||
'description': 'Add the torrent paused.',
|
||||
},
|
||||
@@ -49,6 +66,13 @@ config = [{
|
||||
'advanced': True,
|
||||
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
|
||||
},
|
||||
{
|
||||
'name': 'delete_failed',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'description': 'Delete a release after the download has failed.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,166 +1,212 @@
|
||||
from base64 import b16encode, b32decode
|
||||
from bencode import bencode, bdecode
|
||||
from bencode import bencode as benc, bdecode
|
||||
from couchpotato.core.downloaders.base import Downloader, StatusList
|
||||
from couchpotato.core.helpers.encoding import isInt, ss
|
||||
from couchpotato.core.helpers.variable import tryInt, tryFloat
|
||||
from couchpotato.core.logger import CPLog
|
||||
from datetime import timedelta
|
||||
from hashlib import sha1
|
||||
from multipartpost import MultipartPostHandler
|
||||
from datetime import timedelta
|
||||
import os
|
||||
import cookielib
|
||||
import httplib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import time
|
||||
import urllib
|
||||
import urllib2
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class uTorrent(Downloader):
|
||||
|
||||
type = ['torrent', 'torrent_magnet']
|
||||
protocol = ['torrent', 'torrent_magnet']
|
||||
utorrent_api = None
|
||||
|
||||
def download(self, data, movie, filedata = None):
|
||||
|
||||
log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('type')))
|
||||
|
||||
def connect(self):
|
||||
# Load host from config and split out port.
|
||||
host = self.conf('host').split(':')
|
||||
if not isInt(host[1]):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
return False
|
||||
|
||||
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||
|
||||
return self.utorrent_api
|
||||
|
||||
def download(self, data = None, movie = None, filedata = None):
|
||||
if not movie: movie = {}
|
||||
if not data: data = {}
|
||||
|
||||
log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('protocol')))
|
||||
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
settings = self.utorrent_api.get_settings()
|
||||
if not settings:
|
||||
return False
|
||||
|
||||
#Fix settings in case they are not set for CPS compatibility
|
||||
new_settings = {}
|
||||
if not (settings.get('seed_prio_limitul') == 0 and settings['seed_prio_limitul_flag']):
|
||||
new_settings['seed_prio_limitul'] = 0
|
||||
new_settings['seed_prio_limitul_flag'] = True
|
||||
log.info('Updated uTorrent settings to set a torrent to complete after it the seeding requirements are met.')
|
||||
|
||||
if settings.get('bt.read_only_on_complete'): #This doesn't work as this option seems to be not available through the api. Mitigated with removeReadOnly function
|
||||
new_settings['bt.read_only_on_complete'] = False
|
||||
log.info('Updated uTorrent settings to not set the files to read only after completing.')
|
||||
|
||||
if new_settings:
|
||||
self.utorrent_api.set_settings(new_settings)
|
||||
|
||||
torrent_params = {}
|
||||
if self.conf('label'):
|
||||
torrent_params['label'] = self.conf('label')
|
||||
|
||||
if not filedata and data.get('type') == 'torrent':
|
||||
if not filedata and data.get('protocol') == 'torrent':
|
||||
log.error('Failed sending torrent, no data')
|
||||
return False
|
||||
|
||||
if data.get('type') == 'torrent_magnet':
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper()
|
||||
torrent_params['trackers'] = '%0D%0A%0D%0A'.join(self.torrent_trackers)
|
||||
else:
|
||||
info = bdecode(filedata)["info"]
|
||||
torrent_hash = sha1(bencode(info)).hexdigest().upper()
|
||||
torrent_filename = self.createFileName(data, filedata, movie)
|
||||
torrent_hash = sha1(benc(info)).hexdigest().upper()
|
||||
torrent_filename = self.createFileName(data, filedata, movie)
|
||||
|
||||
if data.get('seed_ratio'):
|
||||
torrent_params['seed_override'] = 1
|
||||
torrent_params['seed_ratio'] = tryInt(tryFloat(data['seed_ratio']) * 1000)
|
||||
|
||||
if data.get('seed_time'):
|
||||
torrent_params['seed_override'] = 1
|
||||
torrent_params['seed_time'] = tryInt(data['seed_time']) * 3600
|
||||
|
||||
# Convert base 32 to hex
|
||||
if len(torrent_hash) == 32:
|
||||
torrent_hash = b16encode(b32decode(torrent_hash))
|
||||
|
||||
# Send request to uTorrent
|
||||
try:
|
||||
if not self.utorrent_api:
|
||||
self.utorrent_api = uTorrentAPI(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
self.utorrent_api.add_torrent_uri(torrent_filename, data.get('url'))
|
||||
else:
|
||||
self.utorrent_api.add_torrent_file(torrent_filename, filedata)
|
||||
|
||||
if data.get('type') == 'torrent_magnet':
|
||||
self.utorrent_api.add_torrent_uri(data.get('url'))
|
||||
# Change settings of added torrent
|
||||
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
|
||||
if self.conf('paused', default = 0):
|
||||
self.utorrent_api.pause_torrent(torrent_hash)
|
||||
|
||||
count = 0
|
||||
while True:
|
||||
|
||||
count += 1
|
||||
# Check if torrent is saved in subfolder of torrent name
|
||||
data = self.utorrent_api.get_files(torrent_hash)
|
||||
|
||||
torrent_files = json.loads(data)
|
||||
if torrent_files.get('error'):
|
||||
log.error('Error getting data from uTorrent: %s', torrent_files.get('error'))
|
||||
return False
|
||||
|
||||
if (torrent_files.get('files') and len(torrent_files['files'][1]) > 0) or count > 60:
|
||||
break
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
# Torrent has only one file, so uTorrent wont create a folder for it
|
||||
if len(torrent_files['files'][1]) == 1:
|
||||
# Remove torrent and try again
|
||||
self.utorrent_api.remove_torrent(torrent_hash, remove_data = True)
|
||||
|
||||
# Send request to uTorrent
|
||||
if data.get('protocol') == 'torrent_magnet':
|
||||
self.utorrent_api.add_torrent_uri(torrent_filename, data.get('url'), add_folder = True)
|
||||
else:
|
||||
self.utorrent_api.add_torrent_file(torrent_filename, filedata)
|
||||
self.utorrent_api.add_torrent_file(torrent_filename, filedata, add_folder = True)
|
||||
|
||||
# Change settings of added torrents
|
||||
# Change settings of added torrent
|
||||
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
|
||||
if self.conf('paused', default = 0):
|
||||
self.utorrent_api.pause_torrent(torrent_hash)
|
||||
return self.downloadReturnId(torrent_hash)
|
||||
except Exception, err:
|
||||
log.error('Failed to send torrent to uTorrent: %s', err)
|
||||
return False
|
||||
|
||||
return self.downloadReturnId(torrent_hash)
|
||||
|
||||
def getAllDownloadStatus(self):
|
||||
|
||||
log.debug('Checking uTorrent download status.')
|
||||
|
||||
# Load host from config and split out port.
|
||||
host = self.conf('host').split(':')
|
||||
if not isInt(host[1]):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
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')
|
||||
if not self.connect():
|
||||
return False
|
||||
|
||||
statuses = StatusList(self)
|
||||
download_folder = ''
|
||||
settings_dict = {}
|
||||
|
||||
try:
|
||||
data = self.utorrent_api.get_settings()
|
||||
utorrent_settings = json.loads(data)
|
||||
data = self.utorrent_api.get_status()
|
||||
if not data:
|
||||
log.error('Error getting data from uTorrent')
|
||||
return False
|
||||
|
||||
# Create settings dict
|
||||
for item in utorrent_settings['settings']:
|
||||
if item[1] == 0: # int
|
||||
settings_dict[item[0]] = int(item[2] if not item[2].strip() == '' else '0')
|
||||
elif item[1] == 1: # bool
|
||||
settings_dict[item[0]] = True if item[2] == 'true' else False
|
||||
elif item[1] == 2: # string
|
||||
settings_dict[item[0]] = item[2]
|
||||
log.debug('uTorrent settings: %s', settings_dict)
|
||||
queue = json.loads(data)
|
||||
if queue.get('error'):
|
||||
log.error('Error getting data from uTorrent: %s', queue.get('error'))
|
||||
return False
|
||||
|
||||
# Get the download path from the uTorrent settings
|
||||
if settings_dict['dir_completed_download_flag']:
|
||||
download_folder = settings_dict['dir_completed_download']
|
||||
elif settings_dict['dir_active_download_flag']:
|
||||
download_folder = settings_dict['dir_active_download']
|
||||
else:
|
||||
log.info('No download folder set in uTorrent. Please set a download folder')
|
||||
return False
|
||||
|
||||
except Exception, err:
|
||||
log.error('Failed to get settings from uTorrent: %s', err)
|
||||
return False
|
||||
if not queue.get('torrents'):
|
||||
log.debug('Nothing in queue')
|
||||
return False
|
||||
|
||||
# Get torrents
|
||||
for item in queue.get('torrents', []):
|
||||
for item in queue['torrents']:
|
||||
|
||||
# item[21] = Paused | Downloading | Seeding | Finished
|
||||
status = 'busy'
|
||||
if item[21] == 'Finished' or item[21] == 'Seeding':
|
||||
if 'Finished' in item[21]:
|
||||
status = 'completed'
|
||||
|
||||
if settings_dict['dir_add_label']:
|
||||
release_folder = os.path.join(download_folder, item[11], item[2])
|
||||
else:
|
||||
release_folder = os.path.join(download_folder, item[2])
|
||||
self.removeReadOnly(item[26])
|
||||
elif 'Seeding' in item[21]:
|
||||
status = 'seeding'
|
||||
self.removeReadOnly(item[26])
|
||||
|
||||
statuses.append({
|
||||
'id': item[0],
|
||||
'name': item[2],
|
||||
'status': status,
|
||||
'seed_ratio': float(item[7]) / 1000,
|
||||
'original_status': item[1],
|
||||
'timeleft': str(timedelta(seconds = item[10])),
|
||||
'folder': release_folder,
|
||||
'folder': ss(item[26]),
|
||||
})
|
||||
|
||||
return statuses
|
||||
|
||||
def pause(self, item, pause = True):
|
||||
if not self.connect():
|
||||
return False
|
||||
return self.utorrent_api.pause_torrent(item['id'], pause)
|
||||
|
||||
def removeFailed(self, item):
|
||||
log.info('%s failed downloading, deleting...', item['name'])
|
||||
if not self.connect():
|
||||
return False
|
||||
return self.utorrent_api.remove_torrent(item['id'], remove_data = True)
|
||||
|
||||
def processComplete(self, item, delete_files = False):
|
||||
log.debug('Requesting uTorrent to remove the torrent %s%s.', (item['name'], ' and cleanup the downloaded files' if delete_files else ''))
|
||||
if not self.connect():
|
||||
return False
|
||||
return self.utorrent_api.remove_torrent(item['id'], remove_data = delete_files)
|
||||
|
||||
def removeReadOnly(self, folder):
|
||||
#Removes all read-only flags in a folder
|
||||
if folder and os.path.isdir(folder):
|
||||
for root, folders, filenames in os.walk(folder):
|
||||
for filename in filenames:
|
||||
os.chmod(os.path.join(root, filename), stat.S_IWRITE)
|
||||
|
||||
class uTorrentAPI(object):
|
||||
|
||||
@@ -211,12 +257,16 @@ class uTorrentAPI(object):
|
||||
token = re.findall("<div.*?>(.*?)</", request.read())[0]
|
||||
return token
|
||||
|
||||
def add_torrent_uri(self, torrent):
|
||||
def add_torrent_uri(self, filename, torrent, add_folder = False):
|
||||
action = "action=add-url&s=%s" % urllib.quote(torrent)
|
||||
if add_folder:
|
||||
action += "&path=%s" % urllib.quote(filename)
|
||||
return self._request(action)
|
||||
|
||||
def add_torrent_file(self, filename, filedata):
|
||||
def add_torrent_file(self, filename, filedata, add_folder = False):
|
||||
action = "action=add-file"
|
||||
if add_folder:
|
||||
action += "&path=%s" % urllib.quote(filename)
|
||||
return self._request(action, {"torrent_file": (ss(filename), filedata)})
|
||||
|
||||
def set_torrent(self, hash, params):
|
||||
@@ -225,8 +275,22 @@ class uTorrentAPI(object):
|
||||
action += "&s=%s&v=%s" % (k, v)
|
||||
return self._request(action)
|
||||
|
||||
def pause_torrent(self, hash):
|
||||
action = "action=pause&hash=%s" % hash
|
||||
def pause_torrent(self, hash, pause = True):
|
||||
if pause:
|
||||
action = "action=pause&hash=%s" % hash
|
||||
else:
|
||||
action = "action=unpause&hash=%s" % hash
|
||||
return self._request(action)
|
||||
|
||||
def stop_torrent(self, hash):
|
||||
action = "action=stop&hash=%s" % hash
|
||||
return self._request(action)
|
||||
|
||||
def remove_torrent(self, hash, remove_data = False):
|
||||
if remove_data:
|
||||
action = "action=removedata&hash=%s" % hash
|
||||
else:
|
||||
action = "action=remove&hash=%s" % hash
|
||||
return self._request(action)
|
||||
|
||||
def get_status(self):
|
||||
@@ -235,4 +299,36 @@ class uTorrentAPI(object):
|
||||
|
||||
def get_settings(self):
|
||||
action = "action=getsettings"
|
||||
settings_dict = {}
|
||||
try:
|
||||
utorrent_settings = json.loads(self._request(action))
|
||||
|
||||
# Create settings dict
|
||||
for item in utorrent_settings['settings']:
|
||||
if item[1] == 0: # int
|
||||
settings_dict[item[0]] = int(item[2] if not item[2].strip() == '' else '0')
|
||||
elif item[1] == 1: # bool
|
||||
settings_dict[item[0]] = True if item[2] == 'true' else False
|
||||
elif item[1] == 2: # string
|
||||
settings_dict[item[0]] = item[2]
|
||||
|
||||
#log.debug('uTorrent settings: %s', settings_dict)
|
||||
|
||||
except Exception, err:
|
||||
log.error('Failed to get settings from uTorrent: %s', err)
|
||||
|
||||
return settings_dict
|
||||
|
||||
def set_settings(self, settings_dict = None):
|
||||
if not settings_dict: settings_dict = {}
|
||||
|
||||
for key in settings_dict:
|
||||
if isinstance(settings_dict[key], bool):
|
||||
settings_dict[key] = 1 if settings_dict[key] else 0
|
||||
|
||||
action = 'action=setsetting' + ''.join(['&s=%s&v=%s' % (key, value) for (key, value) in settings_dict.items()])
|
||||
return self._request(action)
|
||||
|
||||
def get_files(self, hash):
|
||||
action = "action=getfiles&hash=%s" % hash
|
||||
return self._request(action)
|
||||
|
||||
@@ -21,15 +21,25 @@ def addEvent(name, handler, priority = 100):
|
||||
|
||||
def createHandle(*args, **kwargs):
|
||||
|
||||
h = None
|
||||
try:
|
||||
parent = handler.im_self
|
||||
bc = hasattr(parent, 'beforeCall')
|
||||
if bc: parent.beforeCall(handler)
|
||||
# Open handler
|
||||
has_parent = hasattr(handler, 'im_self')
|
||||
parent = None
|
||||
if has_parent:
|
||||
parent = handler.im_self
|
||||
bc = hasattr(parent, 'beforeCall')
|
||||
if bc: parent.beforeCall(handler)
|
||||
|
||||
# Main event
|
||||
h = runHandler(name, handler, *args, **kwargs)
|
||||
ac = hasattr(parent, 'afterCall')
|
||||
if ac: parent.afterCall(handler)
|
||||
|
||||
# Close handler
|
||||
if parent and has_parent:
|
||||
ac = hasattr(parent, 'afterCall')
|
||||
if ac: parent.afterCall(handler)
|
||||
except:
|
||||
h = runHandler(name, handler, *args, **kwargs)
|
||||
log.error('Failed creating handler %s %s: %s', (name, handler, traceback.format_exc()))
|
||||
|
||||
return h
|
||||
|
||||
@@ -43,12 +53,7 @@ def removeEvent(name, handler):
|
||||
e -= handler
|
||||
|
||||
def fireEvent(name, *args, **kwargs):
|
||||
if not events.get(name): return
|
||||
|
||||
e = Event(name = name, threads = 10, asynch = kwargs.get('async', False), exc_info = True, traceback = True, lock = threading.RLock())
|
||||
|
||||
for event in events[name]:
|
||||
e.handle(event['handler'], priority = event['priority'])
|
||||
if not events.has_key(name): return
|
||||
|
||||
#log.debug('Firing event %s', name)
|
||||
try:
|
||||
@@ -59,7 +64,6 @@ def fireEvent(name, *args, **kwargs):
|
||||
'single': False, # Return single handler
|
||||
'merge': False, # Merge items
|
||||
'in_order': False, # Fire them in specific order, waits for the other to finish
|
||||
'async': False
|
||||
}
|
||||
|
||||
# Do options
|
||||
@@ -70,12 +74,32 @@ def fireEvent(name, *args, **kwargs):
|
||||
options[x] = val
|
||||
except: pass
|
||||
|
||||
# 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']
|
||||
if len(events[name]) == 1:
|
||||
|
||||
# Fire
|
||||
result = e(*args, **kwargs)
|
||||
single = None
|
||||
try:
|
||||
single = events[name][0]['handler'](*args, **kwargs)
|
||||
except:
|
||||
log.error('Failed running single event: %s', traceback.format_exc())
|
||||
|
||||
# Don't load thread for single event
|
||||
result = {
|
||||
'single': (single is not None, single),
|
||||
}
|
||||
|
||||
else:
|
||||
|
||||
e = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock())
|
||||
|
||||
for event in events[name]:
|
||||
e.handle(event['handler'], priority = event['priority'])
|
||||
|
||||
# Make sure only 1 event is fired at a time when order is wanted
|
||||
kwargs['event_order_lock'] = threading.RLock() if options['in_order'] or options['single'] else None
|
||||
kwargs['event_return_on_result'] = options['single']
|
||||
|
||||
# Fire
|
||||
result = e(*args, **kwargs)
|
||||
|
||||
if options['single'] and not options['merge']:
|
||||
results = None
|
||||
@@ -133,14 +157,17 @@ def fireEvent(name, *args, **kwargs):
|
||||
options['on_complete']()
|
||||
|
||||
return results
|
||||
except KeyError, e:
|
||||
pass
|
||||
except Exception:
|
||||
log.error('%s: %s', (name, traceback.format_exc()))
|
||||
|
||||
def fireEventAsync(*args, **kwargs):
|
||||
kwargs['async'] = True
|
||||
fireEvent(*args, **kwargs)
|
||||
try:
|
||||
t = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
return True
|
||||
except Exception, e:
|
||||
log.error('%s: %s', (args[0], e))
|
||||
|
||||
def errorHandler(error):
|
||||
etype, value, tb = error
|
||||
|
||||
@@ -11,7 +11,8 @@ log = CPLog(__name__)
|
||||
def toSafeString(original):
|
||||
valid_chars = "-_.() %s%s" % (ascii_letters, digits)
|
||||
cleanedFilename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
|
||||
return ''.join(c for c in cleanedFilename if c in valid_chars)
|
||||
valid_string = ''.join(c for c in cleanedFilename if c in valid_chars)
|
||||
return ' '.join(valid_string.split())
|
||||
|
||||
def simplifyString(original):
|
||||
string = stripAccents(original.lower())
|
||||
@@ -62,7 +63,7 @@ def stripAccents(s):
|
||||
|
||||
def tryUrlencode(s):
|
||||
new = u''
|
||||
if isinstance(s, (dict)):
|
||||
if isinstance(s, dict):
|
||||
for key, value in s.iteritems():
|
||||
new += u'&%s=%s' % (key, tryUrlencode(value))
|
||||
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import natcmp
|
||||
from flask.globals import current_app
|
||||
from flask.helpers import json, make_response
|
||||
from urllib import unquote
|
||||
from werkzeug.urls import url_decode
|
||||
import flask
|
||||
import re
|
||||
|
||||
def getParams():
|
||||
|
||||
params = url_decode(getattr(flask.request, 'environ').get('QUERY_STRING', ''))
|
||||
def getParams(params):
|
||||
|
||||
reg = re.compile('^[a-z0-9_\.]+$')
|
||||
|
||||
current = temp = {}
|
||||
temp = {}
|
||||
for param, value in sorted(params.iteritems()):
|
||||
|
||||
nest = re.split("([\[\]]+)", param)
|
||||
@@ -36,6 +32,8 @@ def getParams():
|
||||
current = current[item]
|
||||
else:
|
||||
temp[param] = toUnicode(unquote(value))
|
||||
if temp[param].lower() in ['true', 'false']:
|
||||
temp[param] = temp[param].lower() != 'false'
|
||||
|
||||
return dictToList(temp)
|
||||
|
||||
@@ -54,29 +52,3 @@ def dictToList(params):
|
||||
new = params
|
||||
|
||||
return new
|
||||
|
||||
def getParam(attr, default = None):
|
||||
try:
|
||||
return getParams().get(attr, default)
|
||||
except:
|
||||
return default
|
||||
|
||||
def padded_jsonify(callback, *args, **kwargs):
|
||||
content = str(callback) + '(' + json.dumps(dict(*args, **kwargs)) + ')'
|
||||
return getattr(current_app, 'response_class')(content, mimetype = 'text/javascript')
|
||||
|
||||
def jsonify(mimetype, *args, **kwargs):
|
||||
content = json.dumps(dict(*args, **kwargs))
|
||||
return getattr(current_app, 'response_class')(content, mimetype = mimetype)
|
||||
|
||||
def jsonified(*args, **kwargs):
|
||||
callback = getParam('callback_func', None)
|
||||
if callback:
|
||||
content = padded_jsonify(callback, *args, **kwargs)
|
||||
else:
|
||||
content = jsonify('application/json', *args, **kwargs)
|
||||
|
||||
response = make_response(content)
|
||||
response.cache_control.no_cache = True
|
||||
|
||||
return response
|
||||
|
||||
@@ -6,7 +6,7 @@ log = CPLog(__name__)
|
||||
class RSS(object):
|
||||
|
||||
def getTextElements(self, xml, path):
|
||||
''' Find elements and return tree'''
|
||||
""" Find elements and return tree"""
|
||||
|
||||
textelements = []
|
||||
try:
|
||||
@@ -28,7 +28,7 @@ class RSS(object):
|
||||
return elements
|
||||
|
||||
def getElement(self, xml, path):
|
||||
''' Find element and return text'''
|
||||
""" Find element and return text"""
|
||||
|
||||
try:
|
||||
return xml.find(path)
|
||||
@@ -36,7 +36,7 @@ class RSS(object):
|
||||
return
|
||||
|
||||
def getTextElement(self, xml, path):
|
||||
''' Find element and return text'''
|
||||
""" Find element and return text"""
|
||||
|
||||
try:
|
||||
return xml.find(path).text
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toSafeString
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss
|
||||
from couchpotato.core.logger import CPLog
|
||||
import hashlib
|
||||
import os.path
|
||||
@@ -101,11 +101,16 @@ def flattenList(l):
|
||||
return l
|
||||
|
||||
def md5(text):
|
||||
return hashlib.md5(text).hexdigest()
|
||||
return hashlib.md5(ss(text)).hexdigest()
|
||||
|
||||
def sha1(text):
|
||||
return hashlib.sha1(text).hexdigest()
|
||||
|
||||
def isLocalIP(ip):
|
||||
ip = ip.lstrip('htps:/')
|
||||
regex = '/(^127\.)|(^192\.168\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^::1)$/'
|
||||
return re.search(regex, ip) is not None or 'localhost' in ip or ip[:4] == '127.'
|
||||
|
||||
def getExt(filename):
|
||||
return os.path.splitext(filename)[1][1:]
|
||||
|
||||
@@ -113,12 +118,17 @@ def cleanHost(host):
|
||||
if not host.startswith(('http://', 'https://')):
|
||||
host = 'http://' + host
|
||||
|
||||
if not host.endswith('/'):
|
||||
host += '/'
|
||||
host = host.rstrip('/')
|
||||
host += '/'
|
||||
|
||||
return host
|
||||
|
||||
def getImdb(txt, check_inside = True, multiple = False):
|
||||
def getImdb(txt, check_inside = False, multiple = False):
|
||||
|
||||
if not check_inside:
|
||||
txt = simplifyString(txt)
|
||||
else:
|
||||
txt = ss(txt)
|
||||
|
||||
if check_inside and os.path.isfile(txt):
|
||||
output = open(txt, 'r')
|
||||
@@ -128,7 +138,7 @@ def getImdb(txt, check_inside = True, multiple = False):
|
||||
try:
|
||||
ids = re.findall('(tt\d{7})', txt)
|
||||
if multiple:
|
||||
return ids if len(ids) > 0 else []
|
||||
return list(set(ids)) if len(ids) > 0 else []
|
||||
return ids[0]
|
||||
except IndexError:
|
||||
pass
|
||||
@@ -140,7 +150,11 @@ def tryInt(s):
|
||||
except: return 0
|
||||
|
||||
def tryFloat(s):
|
||||
try: return float(s) if '.' in s else tryInt(s)
|
||||
try:
|
||||
if isinstance(s, str):
|
||||
return float(s) if '.' in s else tryInt(s)
|
||||
else:
|
||||
return float(s)
|
||||
except: return 0
|
||||
|
||||
def natsortKey(s):
|
||||
@@ -159,8 +173,11 @@ def getTitle(library_dict):
|
||||
if title.default:
|
||||
return title.title
|
||||
except:
|
||||
log.error('Could not get title for %s', library_dict.identifier)
|
||||
return None
|
||||
try:
|
||||
return library_dict['info']['titles'][0]
|
||||
except:
|
||||
log.error('Could not get title for %s', library_dict.identifier)
|
||||
return None
|
||||
|
||||
log.error('Could not get title for %s', library_dict['identifier'])
|
||||
return None
|
||||
@@ -170,16 +187,21 @@ def getTitle(library_dict):
|
||||
|
||||
def possibleTitles(raw_title):
|
||||
|
||||
titles = []
|
||||
titles = [
|
||||
toSafeString(raw_title).lower(),
|
||||
raw_title.lower(),
|
||||
simplifyString(raw_title)
|
||||
]
|
||||
|
||||
titles.append(toSafeString(raw_title).lower())
|
||||
titles.append(raw_title.lower())
|
||||
titles.append(simplifyString(raw_title))
|
||||
# replace some chars
|
||||
new_title = raw_title.replace('&', 'and')
|
||||
titles.append(simplifyString(new_title))
|
||||
|
||||
return list(set(titles))
|
||||
|
||||
def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
|
||||
return ''.join(random.choice(chars) for x in range(size))
|
||||
|
||||
def splitString(str, split_on = ','):
|
||||
return [x.strip() for x in str.split(split_on)] if str else []
|
||||
def splitString(str, split_on = ',', clean = True):
|
||||
list = [x.strip() for x in str.split(split_on)] if str else []
|
||||
return filter(None, list) if clean else list
|
||||
|
||||
@@ -6,15 +6,24 @@ import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
class Loader(object):
|
||||
|
||||
class Loader(object):
|
||||
plugins = {}
|
||||
providers = {}
|
||||
|
||||
modules = {}
|
||||
|
||||
def preload(self, root = ''):
|
||||
def addPath(self, root, base_path, priority, recursive = False):
|
||||
for filename in os.listdir(os.path.join(root, *base_path)):
|
||||
path = os.path.join(os.path.join(root, *base_path), filename)
|
||||
if os.path.isdir(path) and filename[:2] != '__':
|
||||
if u'__init__.py' in os.listdir(path):
|
||||
new_base_path = ''.join(s + '.' for s in base_path) + filename
|
||||
self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path)
|
||||
|
||||
if recursive:
|
||||
self.addPath(root, base_path + [filename], priority, recursive = True)
|
||||
|
||||
def preload(self, root = ''):
|
||||
core = os.path.join(root, 'couchpotato', 'core')
|
||||
|
||||
self.paths = {
|
||||
@@ -25,12 +34,10 @@ class Loader(object):
|
||||
}
|
||||
|
||||
# Add providers to loader
|
||||
provider_dir = os.path.join(root, 'couchpotato', 'core', 'providers')
|
||||
for provider in os.listdir(provider_dir):
|
||||
path = os.path.join(provider_dir, provider)
|
||||
if os.path.isdir(path):
|
||||
self.paths[provider + '_provider'] = (25, 'couchpotato.core.providers.' + provider, path)
|
||||
self.addPath(root, ['couchpotato', 'core', 'providers'], 25, recursive = False)
|
||||
|
||||
# Add media to loader
|
||||
self.addPath(root, ['couchpotato', 'core', 'media'], 25, recursive = True)
|
||||
|
||||
for plugin_type, plugin_tuple in self.paths.iteritems():
|
||||
priority, module, dir_name = plugin_tuple
|
||||
@@ -43,7 +50,13 @@ class Loader(object):
|
||||
for module_name, plugin in sorted(self.modules[priority].iteritems()):
|
||||
# Load module
|
||||
try:
|
||||
m = getattr(self.loadModule(module_name), plugin.get('name'))
|
||||
if plugin.get('name')[:2] == '__':
|
||||
continue
|
||||
|
||||
m = self.loadModule(module_name)
|
||||
if m is None:
|
||||
continue
|
||||
m = getattr(m, plugin.get('name'))
|
||||
|
||||
log.info('Loading %s: %s', (plugin['type'], plugin['name']))
|
||||
|
||||
@@ -53,7 +66,7 @@ class Loader(object):
|
||||
self.loadPlugins(m, plugin.get('name'))
|
||||
except ImportError as e:
|
||||
# todo:: subclass ImportError for missing requirements.
|
||||
if (e.message.lower().startswith("missing")):
|
||||
if e.message.lower().startswith("missing"):
|
||||
log.error(e.message)
|
||||
pass
|
||||
# todo:: this needs to be more descriptive.
|
||||
@@ -73,19 +86,21 @@ class Loader(object):
|
||||
splitted = module.split('.')
|
||||
for sub in splitted[1:]:
|
||||
m = getattr(m, sub)
|
||||
|
||||
if hasattr(m, 'config'):
|
||||
fireEvent('settings.options', splitted[-1] + '_config', getattr(m, 'config'))
|
||||
except:
|
||||
raise
|
||||
|
||||
for cur_file in glob.glob(os.path.join(dir_name, '*')):
|
||||
name = os.path.basename(cur_file)
|
||||
if os.path.isdir(os.path.join(dir_name, name)):
|
||||
if os.path.isdir(os.path.join(dir_name, name)) and name != 'static' and os.path.isfile(os.path.join(cur_file, '__init__.py')):
|
||||
module_name = '%s.%s' % (module, name)
|
||||
self.addModule(priority, plugin_type, module_name, name)
|
||||
|
||||
def loadSettings(self, module, name, save = True):
|
||||
|
||||
if not hasattr(module, 'config'):
|
||||
log.debug('Skip loading settings for plugin %s as it has no config section' % module.__file__)
|
||||
return False
|
||||
|
||||
try:
|
||||
for section in module.config:
|
||||
fireEvent('settings.options', section['name'], section)
|
||||
@@ -100,15 +115,14 @@ class Loader(object):
|
||||
return False
|
||||
|
||||
def loadPlugins(self, module, name):
|
||||
|
||||
if not hasattr(module, 'start'):
|
||||
log.debug('Skip startup for plugin %s as it has no start section' % module.__file__)
|
||||
return False
|
||||
try:
|
||||
klass = module.start()
|
||||
klass.registerPlugin()
|
||||
|
||||
if klass and getattr(klass, 'auto_register_static'):
|
||||
klass.registerStatic(module.__file__)
|
||||
|
||||
module.start()
|
||||
return True
|
||||
except Exception, e:
|
||||
except:
|
||||
log.error('Failed loading plugin "%s": %s', (module.__file__, traceback.format_exc()))
|
||||
return False
|
||||
|
||||
@@ -131,5 +145,8 @@ class Loader(object):
|
||||
for sub in splitted[1:-1]:
|
||||
m = getattr(m, sub)
|
||||
return m
|
||||
except ImportError:
|
||||
log.debug('Skip loading module plugin %s: %s', (name, traceback.format_exc()))
|
||||
return None
|
||||
except:
|
||||
raise
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import logging
|
||||
import re
|
||||
import traceback
|
||||
|
||||
class CPLog(object):
|
||||
|
||||
context = ''
|
||||
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key']
|
||||
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key', 'passkey']
|
||||
|
||||
def __init__(self, context = ''):
|
||||
if context.endswith('.main'):
|
||||
@@ -50,8 +49,8 @@ class CPLog(object):
|
||||
msg = msg % tuple([ss(x) for x in list(replace_tuple)])
|
||||
else:
|
||||
msg = msg % ss(replace_tuple)
|
||||
except:
|
||||
self.logger.error(u'Failed encoding stuff to log: %s' % traceback.format_exc())
|
||||
except Exception, e:
|
||||
self.logger.error(u'Failed encoding stuff to log "%s": %s' % (msg, e))
|
||||
|
||||
if not Env.get('dev'):
|
||||
|
||||
|
||||
13
couchpotato/core/media/__init__.py
Normal file
13
couchpotato/core/media/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
|
||||
|
||||
class MediaBase(Plugin):
|
||||
|
||||
_type = None
|
||||
|
||||
def initType(self):
|
||||
addEvent('media.types', self.getType)
|
||||
|
||||
def getType(self):
|
||||
return self._type
|
||||
13
couchpotato/core/media/_base/library/__init__.py
Normal file
13
couchpotato/core/media/_base/library/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
|
||||
|
||||
class LibraryBase(Plugin):
|
||||
|
||||
_type = None
|
||||
|
||||
def initType(self):
|
||||
addEvent('library.types', self.getType)
|
||||
|
||||
def getType(self):
|
||||
return self._type
|
||||
75
couchpotato/core/media/_base/searcher/__init__.py
Normal file
75
couchpotato/core/media/_base/searcher/__init__.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from .main import Searcher
|
||||
|
||||
def start():
|
||||
return Searcher()
|
||||
|
||||
config = [{
|
||||
'name': 'searcher',
|
||||
'order': 20,
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'name': 'searcher',
|
||||
'label': 'Basics',
|
||||
'description': 'General search options',
|
||||
'options': [
|
||||
{
|
||||
'name': 'preferred_method',
|
||||
'label': 'First search',
|
||||
'description': 'Which of the methods do you prefer',
|
||||
'default': 'both',
|
||||
'type': 'dropdown',
|
||||
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')],
|
||||
},
|
||||
],
|
||||
}, {
|
||||
'tab': 'searcher',
|
||||
'subtab': 'category',
|
||||
'subtab_label': 'Categories',
|
||||
'name': 'filter',
|
||||
'label': 'Global filters',
|
||||
'description': 'Prefer, ignore & required words in release names',
|
||||
'options': [
|
||||
{
|
||||
'name': 'preferred_words',
|
||||
'label': 'Preferred',
|
||||
'default': '',
|
||||
'placeholder': 'Example: CtrlHD, Amiable, Wiki',
|
||||
'description': 'Words that give the releases a higher score.'
|
||||
},
|
||||
{
|
||||
'name': 'required_words',
|
||||
'label': 'Required',
|
||||
'default': '',
|
||||
'placeholder': 'Example: DTS, AC3 & English',
|
||||
'description': 'Release should contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"'
|
||||
},
|
||||
{
|
||||
'name': 'ignored_words',
|
||||
'label': 'Ignored',
|
||||
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs',
|
||||
'description': 'Ignores releases that match any of these sets. (Works like explained above)'
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}, {
|
||||
'name': 'nzb',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'name': 'searcher',
|
||||
'label': 'NZB',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'retention',
|
||||
'label': 'Usenet Retention',
|
||||
'default': 1500,
|
||||
'type': 'int',
|
||||
'unit': 'days'
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
45
couchpotato/core/media/_base/searcher/base.py
Normal file
45
couchpotato/core/media/_base/searcher/base.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class SearcherBase(Plugin):
|
||||
|
||||
in_progress = False
|
||||
|
||||
def __init__(self):
|
||||
super(SearcherBase, self).__init__()
|
||||
|
||||
|
||||
addEvent('searcher.progress', self.getProgress)
|
||||
addEvent('%s.searcher.progress' % self.getType(), self.getProgress)
|
||||
|
||||
self.initCron()
|
||||
|
||||
def initCron(self):
|
||||
""" Set the searcher cronjob
|
||||
Make sure to reset cronjob after setting has changed
|
||||
"""
|
||||
|
||||
_type = self.getType()
|
||||
|
||||
def setCrons():
|
||||
|
||||
fireEvent('schedule.cron', '%s.searcher.all' % _type, self.searchAll,
|
||||
day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
|
||||
|
||||
addEvent('app.load', setCrons)
|
||||
addEvent('setting.save.%s_searcher.cron_day.after' % _type, setCrons)
|
||||
addEvent('setting.save.%s_searcher.cron_hour.after' % _type, setCrons)
|
||||
addEvent('setting.save.%s_searcher.cron_minute.after' % _type, setCrons)
|
||||
|
||||
def getProgress(self, **kwargs):
|
||||
""" Return progress of current searcher"""
|
||||
|
||||
progress = {
|
||||
self.getType(): self.in_progress
|
||||
}
|
||||
|
||||
return progress
|
||||
238
couchpotato/core/media/_base/searcher/main.py
Normal file
238
couchpotato/core/media/_base/searcher/main.py
Normal file
@@ -0,0 +1,238 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||
from couchpotato.core.helpers.variable import md5, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.searcher.base import SearcherBase
|
||||
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
||||
from couchpotato.environment import Env
|
||||
from inspect import ismethod, isfunction
|
||||
import datetime
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Searcher(SearcherBase):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('searcher.protocols', self.getSearchProtocols)
|
||||
addEvent('searcher.contains_other_quality', self.containsOtherQuality)
|
||||
addEvent('searcher.correct_year', self.correctYear)
|
||||
addEvent('searcher.correct_name', self.correctName)
|
||||
addEvent('searcher.download', self.download)
|
||||
|
||||
addApiView('searcher.full_search', self.searchAllView, docs = {
|
||||
'desc': 'Starts a full search for all media',
|
||||
})
|
||||
|
||||
addApiView('searcher.progress', self.getProgressForAll, docs = {
|
||||
'desc': 'Get the progress of all media searches',
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'movie': False || object, total & to_go,
|
||||
'show': False || object, total & to_go,
|
||||
}"""},
|
||||
})
|
||||
|
||||
def searchAllView(self):
|
||||
|
||||
results = {}
|
||||
for _type in fireEvent('media.types'):
|
||||
results[_type] = fireEvent('%s.searcher.all_view' % _type)
|
||||
|
||||
return results
|
||||
|
||||
def getProgressForAll(self):
|
||||
progress = fireEvent('searcher.progress', merge = True)
|
||||
return progress
|
||||
|
||||
def download(self, data, movie, manual = False):
|
||||
|
||||
if not data.get('protocol'):
|
||||
data['protocol'] = data['type']
|
||||
data['type'] = 'movie'
|
||||
|
||||
# Test to see if any downloaders are enabled for this type
|
||||
downloader_enabled = fireEvent('download.enabled', manual, data, single = True)
|
||||
|
||||
if downloader_enabled:
|
||||
|
||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
||||
|
||||
# Download movie to temp
|
||||
filedata = None
|
||||
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
|
||||
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
|
||||
if filedata == 'try_next':
|
||||
return filedata
|
||||
|
||||
download_result = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
|
||||
log.debug('Downloader result: %s', download_result)
|
||||
|
||||
if download_result:
|
||||
try:
|
||||
# Mark release as snatched
|
||||
db = get_session()
|
||||
rls = db.query(Release).filter_by(identifier = md5(data['url'])).first()
|
||||
if rls:
|
||||
renamer_enabled = Env.setting('enabled', 'renamer')
|
||||
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id')
|
||||
|
||||
# Save download-id info if returned
|
||||
if isinstance(download_result, dict):
|
||||
for key in download_result:
|
||||
rls_info = ReleaseInfo(
|
||||
identifier = 'download_%s' % key,
|
||||
value = toUnicode(download_result.get(key))
|
||||
)
|
||||
rls.info.append(rls_info)
|
||||
db.commit()
|
||||
|
||||
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
|
||||
snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie)
|
||||
log.info(snatch_message)
|
||||
fireEvent('movie.snatched', message = snatch_message, data = rls.to_dict())
|
||||
|
||||
# If renamer isn't used, mark movie done
|
||||
if not renamer_enabled:
|
||||
active_status = fireEvent('status.get', 'active', single = True)
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
try:
|
||||
if movie['status_id'] == active_status.get('id'):
|
||||
for profile_type in movie['profile']['types']:
|
||||
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
|
||||
log.info('Renamer disabled, marking movie as finished: %s', log_movie)
|
||||
|
||||
# Mark release done
|
||||
rls.status_id = done_status.get('id')
|
||||
rls.last_edit = int(time.time())
|
||||
db.commit()
|
||||
|
||||
# Mark movie done
|
||||
mvie = db.query(Movie).filter_by(id = movie['id']).first()
|
||||
mvie.status_id = done_status.get('id')
|
||||
mvie.last_edit = int(time.time())
|
||||
db.commit()
|
||||
except:
|
||||
log.error('Failed marking movie finished, renamer disabled: %s', traceback.format_exc())
|
||||
|
||||
except:
|
||||
log.error('Failed marking movie finished: %s', traceback.format_exc())
|
||||
|
||||
return True
|
||||
|
||||
log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', (data.get('protocol')))
|
||||
|
||||
return False
|
||||
|
||||
def getSearchProtocols(self):
|
||||
|
||||
download_protocols = fireEvent('download.enabled_protocols', merge = True)
|
||||
provider_protocols = fireEvent('provider.enabled_protocols', merge = True)
|
||||
|
||||
if download_protocols and len(list(set(provider_protocols) & set(download_protocols))) == 0:
|
||||
log.error('There aren\'t any providers enabled for your downloader (%s). Check your settings.', ','.join(download_protocols))
|
||||
return []
|
||||
|
||||
for useless_provider in list(set(provider_protocols) - set(download_protocols)):
|
||||
log.debug('Provider for "%s" enabled, but no downloader.', useless_provider)
|
||||
|
||||
search_protocols = download_protocols
|
||||
|
||||
if len(search_protocols) == 0:
|
||||
log.error('There aren\'t any downloaders enabled. Please pick one in settings.')
|
||||
return []
|
||||
|
||||
return search_protocols
|
||||
|
||||
def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = None):
|
||||
if not preferred_quality: preferred_quality = {}
|
||||
|
||||
name = nzb['name']
|
||||
size = nzb.get('size', 0)
|
||||
nzb_words = re.split('\W+', simplifyString(name))
|
||||
|
||||
qualities = fireEvent('quality.all', single = True)
|
||||
|
||||
found = {}
|
||||
for quality in qualities:
|
||||
# Main in words
|
||||
if quality['identifier'] in nzb_words:
|
||||
found[quality['identifier']] = True
|
||||
|
||||
# Alt in words
|
||||
if list(set(nzb_words) & set(quality['alternative'])):
|
||||
found[quality['identifier']] = True
|
||||
|
||||
# Try guessing via quality tags
|
||||
guess = fireEvent('quality.guess', [nzb.get('name')], single = True)
|
||||
if guess:
|
||||
found[guess['identifier']] = True
|
||||
|
||||
# Hack for older movies that don't contain quality tag
|
||||
year_name = fireEvent('scanner.name_year', name, single = True)
|
||||
if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None):
|
||||
if size > 3000: # Assume dvdr
|
||||
log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', size)
|
||||
found['dvdr'] = True
|
||||
else: # Assume dvdrip
|
||||
log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', size)
|
||||
found['dvdrip'] = True
|
||||
|
||||
# Allow other qualities
|
||||
for allowed in preferred_quality.get('allow'):
|
||||
if found.get(allowed):
|
||||
del found[allowed]
|
||||
|
||||
return not (found.get(preferred_quality['identifier']) and len(found) == 1)
|
||||
|
||||
def correctYear(self, haystack, year, year_range):
|
||||
|
||||
if not isinstance(haystack, (list, tuple, set)):
|
||||
haystack = [haystack]
|
||||
|
||||
year_name = {}
|
||||
for string in haystack:
|
||||
|
||||
year_name = fireEvent('scanner.name_year', string, single = True)
|
||||
|
||||
if year_name and ((year - year_range) <= year_name.get('year') <= (year + year_range)):
|
||||
log.debug('Movie year matches range: %s looking for %s', (year_name.get('year'), year))
|
||||
return True
|
||||
|
||||
log.debug('Movie year doesn\'t matche range: %s looking for %s', (year_name.get('year'), year))
|
||||
return False
|
||||
|
||||
def correctName(self, check_name, movie_name):
|
||||
|
||||
check_names = [check_name]
|
||||
|
||||
# Match names between "
|
||||
try: check_names.append(re.search(r'([\'"])[^\1]*\1', check_name).group(0))
|
||||
except: pass
|
||||
|
||||
# Match longest name between []
|
||||
try: check_names.append(max(check_name.split('['), key = len))
|
||||
except: pass
|
||||
|
||||
for check_name in list(set(check_names)):
|
||||
check_movie = fireEvent('scanner.name_year', check_name, single = True)
|
||||
|
||||
try:
|
||||
check_words = filter(None, re.split('\W+', check_movie.get('name', '')))
|
||||
movie_words = filter(None, re.split('\W+', simplifyString(movie_name)))
|
||||
|
||||
if len(check_words) > 0 and len(movie_words) > 0 and len(list(set(check_words) - set(movie_words))) == 0:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
class SearchSetupError(Exception):
|
||||
pass
|
||||
6
couchpotato/core/media/movie/__init__.py
Normal file
6
couchpotato/core/media/movie/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from couchpotato.core.media import MediaBase
|
||||
|
||||
|
||||
class MovieTypeBase(MediaBase):
|
||||
|
||||
_type = 'movie'
|
||||
6
couchpotato/core/media/movie/_base/__init__.py
Normal file
6
couchpotato/core/media/movie/_base/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .main import MovieBase
|
||||
|
||||
def start():
|
||||
return MovieBase()
|
||||
|
||||
config = []
|
||||
@@ -2,10 +2,10 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||
from couchpotato.core.helpers.request import getParams, jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import getImdb, splitString
|
||||
from couchpotato.core.helpers.variable import getImdb, splitString, tryInt, \
|
||||
mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.media.movie import MovieTypeBase
|
||||
from couchpotato.core.settings.model import Library, LibraryTitle, Movie, \
|
||||
Release
|
||||
from couchpotato.environment import Env
|
||||
@@ -17,17 +17,23 @@ import time
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class MoviePlugin(Plugin):
|
||||
class MovieBase(MovieTypeBase):
|
||||
|
||||
default_dict = {
|
||||
'profile': {'types': {'quality': {}}},
|
||||
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}},
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {},
|
||||
'status': {}
|
||||
'status': {},
|
||||
'category': {},
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
|
||||
# Initialize this type
|
||||
super(MovieBase, self).__init__()
|
||||
self.initType()
|
||||
|
||||
addApiView('movie.search', self.search, docs = {
|
||||
'desc': 'Search the movie providers for a movie',
|
||||
'params': {
|
||||
@@ -118,21 +124,21 @@ class MoviePlugin(Plugin):
|
||||
.filter(Movie.status_id == done_status.get('id'), Movie.last_edit < (now - week)) \
|
||||
.all()
|
||||
|
||||
#
|
||||
for movie in movies:
|
||||
for rel in movie.releases:
|
||||
if rel.status_id in [available_status.get('id'), snatched_status.get('id')]:
|
||||
fireEvent('release.delete', id = rel.id, single = True)
|
||||
|
||||
def getView(self):
|
||||
db.expire_all()
|
||||
|
||||
movie_id = getParam('id')
|
||||
movie = self.get(movie_id) if movie_id else None
|
||||
def getView(self, id = None, **kwargs):
|
||||
|
||||
return jsonified({
|
||||
movie = self.get(id) if id else None
|
||||
|
||||
return {
|
||||
'success': movie is not None,
|
||||
'movie': movie,
|
||||
})
|
||||
}
|
||||
|
||||
def get(self, movie_id):
|
||||
|
||||
@@ -140,7 +146,7 @@ class MoviePlugin(Plugin):
|
||||
|
||||
imdb_id = getImdb(str(movie_id))
|
||||
|
||||
if(imdb_id):
|
||||
if imdb_id:
|
||||
m = db.query(Movie).filter(Movie.library.has(identifier = imdb_id)).first()
|
||||
else:
|
||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||
@@ -149,6 +155,7 @@ class MoviePlugin(Plugin):
|
||||
if m:
|
||||
results = m.to_dict(self.default_dict)
|
||||
|
||||
db.expire_all()
|
||||
return results
|
||||
|
||||
def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
|
||||
@@ -161,21 +168,33 @@ class MoviePlugin(Plugin):
|
||||
if release_status and not isinstance(release_status, (list, tuple)):
|
||||
release_status = [release_status]
|
||||
|
||||
# query movie ids
|
||||
q = db.query(Movie) \
|
||||
.outerjoin(Movie.releases, Movie.library, Library.titles) \
|
||||
.filter(LibraryTitle.default == True) \
|
||||
.with_entities(Movie.id) \
|
||||
.group_by(Movie.id)
|
||||
|
||||
# Filter on movie status
|
||||
if status and len(status) > 0:
|
||||
q = q.filter(or_(*[Movie.status.has(identifier = s) for s in status]))
|
||||
statuses = fireEvent('status.get', status, single = len(status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Movie.status_id.in_(statuses))
|
||||
|
||||
# Filter on release status
|
||||
if release_status and len(release_status) > 0:
|
||||
q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status]))
|
||||
q = q.join(Movie.releases)
|
||||
|
||||
total_count = q.count()
|
||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Release.status_id.in_(statuses))
|
||||
|
||||
# Only join when searching / ordering
|
||||
if starts_with or search or order != 'release_order':
|
||||
q = q.join(Movie.library, Library.titles) \
|
||||
.filter(LibraryTitle.default == True)
|
||||
|
||||
# Add search filters
|
||||
filter_or = []
|
||||
if starts_with:
|
||||
starts_with = toUnicode(starts_with.lower())
|
||||
@@ -190,46 +209,79 @@ class MoviePlugin(Plugin):
|
||||
if search:
|
||||
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
|
||||
|
||||
if filter_or:
|
||||
if len(filter_or) > 0:
|
||||
q = q.filter(or_(*filter_or))
|
||||
|
||||
total_count = q.count()
|
||||
if total_count == 0:
|
||||
return 0, []
|
||||
|
||||
if order == 'release_order':
|
||||
q = q.order_by(desc(Release.last_edit))
|
||||
else:
|
||||
q = q.order_by(asc(LibraryTitle.simple_title))
|
||||
|
||||
q = q.subquery()
|
||||
q2 = db.query(Movie).join((q, q.c.id == Movie.id)) \
|
||||
.options(joinedload_all('releases')) \
|
||||
.options(joinedload_all('profile.types')) \
|
||||
if limit_offset:
|
||||
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
|
||||
limit = splt[0]
|
||||
offset = 0 if len(splt) is 1 else splt[1]
|
||||
q = q.limit(limit).offset(offset)
|
||||
|
||||
# Get all movie_ids in sorted order
|
||||
movie_ids = [m.id for m in q.all()]
|
||||
|
||||
# List release statuses
|
||||
releases = db.query(Release) \
|
||||
.filter(Release.movie_id.in_(movie_ids)) \
|
||||
.all()
|
||||
|
||||
release_statuses = dict((m, set()) for m in movie_ids)
|
||||
releases_count = dict((m, 0) for m in movie_ids)
|
||||
for release in releases:
|
||||
release_statuses[release.movie_id].add('%d,%d' % (release.status_id, release.quality_id))
|
||||
releases_count[release.movie_id] += 1
|
||||
|
||||
# Get main movie data
|
||||
q2 = db.query(Movie) \
|
||||
.options(joinedload_all('library.titles')) \
|
||||
.options(joinedload_all('library.files')) \
|
||||
.options(joinedload_all('status')) \
|
||||
.options(joinedload_all('files'))
|
||||
|
||||
if limit_offset:
|
||||
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
|
||||
limit = splt[0]
|
||||
offset = 0 if len(splt) is 1 else splt[1]
|
||||
q2 = q2.limit(limit).offset(offset)
|
||||
q2 = q2.filter(Movie.id.in_(movie_ids))
|
||||
|
||||
results = q2.all()
|
||||
movies = []
|
||||
|
||||
# Create dict by movie id
|
||||
movie_dict = {}
|
||||
for movie in results:
|
||||
temp = movie.to_dict({
|
||||
'profile': {'types': {}},
|
||||
'releases': {'files':{}, 'info': {}},
|
||||
movie_dict[movie.id] = movie
|
||||
|
||||
# List movies based on movie_ids order
|
||||
movies = []
|
||||
for movie_id in movie_ids:
|
||||
|
||||
releases = []
|
||||
for r in release_statuses.get(movie_id):
|
||||
x = splitString(r)
|
||||
releases.append({'status_id': x[0], 'quality_id': x[1]})
|
||||
|
||||
# Merge releases with movie dict
|
||||
movies.append(mergeDicts(movie_dict[movie_id].to_dict({
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {},
|
||||
})
|
||||
movies.append(temp)
|
||||
}), {
|
||||
'releases': releases,
|
||||
'releases_count': releases_count.get(movie_id),
|
||||
}))
|
||||
|
||||
#db.close()
|
||||
return (total_count, movies)
|
||||
db.expire_all()
|
||||
return total_count, movies
|
||||
|
||||
def availableChars(self, status = None, release_status = None):
|
||||
|
||||
chars = ''
|
||||
status = status or []
|
||||
release_status = release_status or []
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -239,38 +291,53 @@ class MoviePlugin(Plugin):
|
||||
if release_status and not isinstance(release_status, (list, tuple)):
|
||||
release_status = [release_status]
|
||||
|
||||
q = db.query(Movie) \
|
||||
.outerjoin(Movie.releases, Movie.library, Library.titles, Movie.status) \
|
||||
.options(joinedload_all('library.titles'))
|
||||
q = db.query(Movie)
|
||||
|
||||
# Filter on movie status
|
||||
if status and len(status) > 0:
|
||||
q = q.filter(or_(*[Movie.status.has(identifier = s) for s in status]))
|
||||
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
q = q.filter(Movie.status_id.in_(statuses))
|
||||
|
||||
# Filter on release status
|
||||
if release_status and len(release_status) > 0:
|
||||
q = q.filter(or_(*[Release.status.has(identifier = s) for s in release_status]))
|
||||
|
||||
results = q.all()
|
||||
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
|
||||
statuses = [s.get('id') for s in statuses]
|
||||
|
||||
for movie in results:
|
||||
char = movie.library.titles[0].simple_title[0]
|
||||
char = char if char in ascii_lowercase else '#'
|
||||
if char not in chars:
|
||||
chars += str(char)
|
||||
q = q.join(Movie.releases) \
|
||||
.filter(Release.status_id.in_(statuses))
|
||||
|
||||
#db.close()
|
||||
return ''.join(sorted(chars, key = str.lower))
|
||||
q = q.join(Library, LibraryTitle) \
|
||||
.with_entities(LibraryTitle.simple_title) \
|
||||
.filter(LibraryTitle.default == True)
|
||||
|
||||
def listView(self):
|
||||
titles = q.all()
|
||||
|
||||
params = getParams()
|
||||
status = splitString(params.get('status', None))
|
||||
release_status = splitString(params.get('release_status', None))
|
||||
limit_offset = params.get('limit_offset', None)
|
||||
starts_with = params.get('starts_with', None)
|
||||
search = params.get('search', None)
|
||||
order = params.get('order', None)
|
||||
chars = set()
|
||||
for title in titles:
|
||||
try:
|
||||
char = title[0][0]
|
||||
char = char if char in ascii_lowercase else '#'
|
||||
chars.add(str(char))
|
||||
except:
|
||||
log.error('Failed getting title for %s', title.libraries_id)
|
||||
|
||||
if len(chars) == 25:
|
||||
break
|
||||
|
||||
db.expire_all()
|
||||
return ''.join(sorted(chars))
|
||||
|
||||
def listView(self, **kwargs):
|
||||
|
||||
status = splitString(kwargs.get('status'))
|
||||
release_status = splitString(kwargs.get('release_status'))
|
||||
limit_offset = kwargs.get('limit_offset')
|
||||
starts_with = kwargs.get('starts_with')
|
||||
search = kwargs.get('search')
|
||||
order = kwargs.get('order')
|
||||
|
||||
total_movies, movies = self.list(
|
||||
status = status,
|
||||
@@ -281,32 +348,31 @@ class MoviePlugin(Plugin):
|
||||
order = order
|
||||
)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'total': total_movies,
|
||||
'movies': movies,
|
||||
})
|
||||
}
|
||||
|
||||
def charView(self):
|
||||
def charView(self, **kwargs):
|
||||
|
||||
params = getParams()
|
||||
status = splitString(params.get('status', None))
|
||||
release_status = splitString(params.get('release_status', None))
|
||||
status = splitString(kwargs.get('status', None))
|
||||
release_status = splitString(kwargs.get('release_status', None))
|
||||
chars = self.availableChars(status, release_status)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(chars) == 0,
|
||||
'chars': chars,
|
||||
})
|
||||
}
|
||||
|
||||
def refresh(self):
|
||||
def refresh(self, id = '', **kwargs):
|
||||
|
||||
db = get_session()
|
||||
|
||||
for id in splitString(getParam('id')):
|
||||
movie = db.query(Movie).filter_by(id = id).first()
|
||||
for x in splitString(id):
|
||||
movie = db.query(Movie).filter_by(id = x).first()
|
||||
|
||||
if movie:
|
||||
|
||||
@@ -315,18 +381,16 @@ class MoviePlugin(Plugin):
|
||||
for title in movie.library.titles:
|
||||
if title.default: default_title = title.title
|
||||
|
||||
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True)
|
||||
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id))
|
||||
fireEvent('notify.frontend', type = 'movie.busy.%s' % x, data = True)
|
||||
fireEventAsync('library.update.movie', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
|
||||
|
||||
|
||||
#db.close()
|
||||
return jsonified({
|
||||
db.expire_all()
|
||||
return {
|
||||
'success': True,
|
||||
})
|
||||
}
|
||||
|
||||
def search(self):
|
||||
def search(self, q = '', **kwargs):
|
||||
|
||||
q = getParam('q')
|
||||
cache_key = u'%s/%s' % (__name__, simplifyString(q))
|
||||
movies = Env.get('cache').get(cache_key)
|
||||
|
||||
@@ -338,13 +402,14 @@ class MoviePlugin(Plugin):
|
||||
movies = fireEvent('movie.search', q = q, merge = True)
|
||||
Env.get('cache').set(cache_key, movies)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0 if movies else 0,
|
||||
'movies': movies,
|
||||
})
|
||||
}
|
||||
|
||||
def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None):
|
||||
def add(self, params = None, force_readd = True, search_after = True, update_library = False, status_id = None):
|
||||
if not params: params = {}
|
||||
|
||||
if not params.get('identifier'):
|
||||
msg = 'Can\'t add movie without imdb identifier.'
|
||||
@@ -363,23 +428,26 @@ class MoviePlugin(Plugin):
|
||||
pass
|
||||
|
||||
|
||||
library = fireEvent('library.add', single = True, attrs = params, update_after = update_library)
|
||||
library = fireEvent('library.add.movie', single = True, attrs = params, update_after = update_library)
|
||||
|
||||
# Status
|
||||
status_active, snatched_status, ignored_status, done_status, downloaded_status = \
|
||||
fireEvent('status.get', ['active', 'snatched', 'ignored', 'done', 'downloaded'], single = True)
|
||||
|
||||
default_profile = fireEvent('profile.default', single = True)
|
||||
cat_id = params.get('category_id')
|
||||
|
||||
db = get_session()
|
||||
m = db.query(Movie).filter_by(library_id = library.get('id')).first()
|
||||
added = True
|
||||
do_search = False
|
||||
search_after = search_after and self.conf('search_on_add', section = 'moviesearcher')
|
||||
if not m:
|
||||
m = Movie(
|
||||
library_id = library.get('id'),
|
||||
profile_id = params.get('profile_id', default_profile.get('id')),
|
||||
status_id = status_id if status_id else status_active.get('id'),
|
||||
category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None,
|
||||
)
|
||||
db.add(m)
|
||||
db.commit()
|
||||
@@ -388,7 +456,7 @@ class MoviePlugin(Plugin):
|
||||
if search_after:
|
||||
onComplete = self.createOnComplete(m.id)
|
||||
|
||||
fireEventAsync('library.update', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
|
||||
fireEventAsync('library.update.movie', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
|
||||
search_after = False
|
||||
elif force_readd:
|
||||
|
||||
@@ -401,6 +469,7 @@ class MoviePlugin(Plugin):
|
||||
fireEvent('release.delete', release.id, single = True)
|
||||
|
||||
m.profile_id = params.get('profile_id', default_profile.get('id'))
|
||||
m.category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else (m.category_id or None)
|
||||
else:
|
||||
log.debug('Movie already exists, not updating: %s', params)
|
||||
added = False
|
||||
@@ -428,37 +497,38 @@ class MoviePlugin(Plugin):
|
||||
if added:
|
||||
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', ''))
|
||||
|
||||
#db.close()
|
||||
db.expire_all()
|
||||
return movie_dict
|
||||
|
||||
|
||||
def addView(self):
|
||||
def addView(self, **kwargs):
|
||||
|
||||
params = getParams()
|
||||
movie_dict = self.add(params = kwargs)
|
||||
|
||||
movie_dict = self.add(params)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'added': True if movie_dict else False,
|
||||
'movie': movie_dict,
|
||||
})
|
||||
}
|
||||
|
||||
def edit(self):
|
||||
def edit(self, id = '', **kwargs):
|
||||
|
||||
params = getParams()
|
||||
db = get_session()
|
||||
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
|
||||
ids = splitString(params.get('id'))
|
||||
ids = splitString(id)
|
||||
for movie_id in ids:
|
||||
|
||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||
if not m:
|
||||
continue
|
||||
|
||||
m.profile_id = params.get('profile_id')
|
||||
m.profile_id = kwargs.get('profile_id')
|
||||
|
||||
cat_id = kwargs.get('category_id')
|
||||
if cat_id is not None:
|
||||
m.category_id = tryInt(cat_id) if tryInt(cat_id) > 0 else None
|
||||
|
||||
# Remove releases
|
||||
for rel in m.releases:
|
||||
@@ -467,33 +537,31 @@ class MoviePlugin(Plugin):
|
||||
db.commit()
|
||||
|
||||
# Default title
|
||||
if params.get('default_title'):
|
||||
if kwargs.get('default_title'):
|
||||
for title in m.library.titles:
|
||||
title.default = toUnicode(params.get('default_title', '')).lower() == toUnicode(title.title).lower()
|
||||
title.default = toUnicode(kwargs.get('default_title', '')).lower() == toUnicode(title.title).lower()
|
||||
|
||||
db.commit()
|
||||
|
||||
fireEvent('movie.restatus', m.id)
|
||||
|
||||
movie_dict = m.to_dict(self.default_dict)
|
||||
fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
|
||||
fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
|
||||
|
||||
#db.close()
|
||||
return jsonified({
|
||||
db.expire_all()
|
||||
return {
|
||||
'success': True,
|
||||
})
|
||||
}
|
||||
|
||||
def deleteView(self):
|
||||
def deleteView(self, id = '', **kwargs):
|
||||
|
||||
params = getParams()
|
||||
|
||||
ids = splitString(params.get('id'))
|
||||
ids = splitString(id)
|
||||
for movie_id in ids:
|
||||
self.delete(movie_id, delete_from = params.get('delete_from', 'all'))
|
||||
self.delete(movie_id, delete_from = kwargs.get('delete_from', 'all'))
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
})
|
||||
}
|
||||
|
||||
def delete(self, movie_id, delete_from = None):
|
||||
|
||||
@@ -513,7 +581,7 @@ class MoviePlugin(Plugin):
|
||||
total_deleted = 0
|
||||
new_movie_status = None
|
||||
for release in movie.releases:
|
||||
if delete_from in ['wanted', 'snatched']:
|
||||
if delete_from in ['wanted', 'snatched', 'late']:
|
||||
if release.status_id != done_status.get('id'):
|
||||
db.delete(release)
|
||||
total_deleted += 1
|
||||
@@ -540,7 +608,7 @@ class MoviePlugin(Plugin):
|
||||
if deleted:
|
||||
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict())
|
||||
|
||||
#db.close()
|
||||
db.expire_all()
|
||||
return True
|
||||
|
||||
def restatus(self, movie_id):
|
||||
@@ -554,7 +622,7 @@ class MoviePlugin(Plugin):
|
||||
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
|
||||
return False
|
||||
|
||||
log.debug('Changing status for %s', (m.library.titles[0].title))
|
||||
log.debug('Changing status for %s', m.library.titles[0].title)
|
||||
if not m.profile:
|
||||
m.status_id = done_status.get('id')
|
||||
else:
|
||||
@@ -568,7 +636,6 @@ class MoviePlugin(Plugin):
|
||||
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
|
||||
|
||||
db.commit()
|
||||
#db.close()
|
||||
|
||||
return True
|
||||
|
||||
@@ -577,7 +644,8 @@ class MoviePlugin(Plugin):
|
||||
def onComplete():
|
||||
db = get_session()
|
||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
||||
fireEventAsync('searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id))
|
||||
fireEventAsync('movie.searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id))
|
||||
db.expire_all()
|
||||
|
||||
return onComplete
|
||||
|
||||
@@ -588,5 +656,6 @@ class MoviePlugin(Plugin):
|
||||
db = get_session()
|
||||
movie = db.query(Movie).filter_by(id = movie_id).first()
|
||||
fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
|
||||
db.expire_all()
|
||||
|
||||
return notifyFront
|
||||
@@ -14,6 +14,7 @@ var MovieList = new Class({
|
||||
|
||||
movies: [],
|
||||
movies_added: {},
|
||||
total_movies: 0,
|
||||
letters: {},
|
||||
filter: null,
|
||||
|
||||
@@ -23,7 +24,7 @@ var MovieList = new Class({
|
||||
|
||||
self.offset = 0;
|
||||
self.filter = self.options.filter || {
|
||||
'startswith': null,
|
||||
'starts_with': null,
|
||||
'search': null
|
||||
}
|
||||
|
||||
@@ -48,7 +49,7 @@ var MovieList = new Class({
|
||||
self.changeView('list');
|
||||
else
|
||||
self.changeView(self.getSavedView() || self.options.view || 'details');
|
||||
|
||||
|
||||
self.getMovies();
|
||||
|
||||
App.addEvent('movie.added', self.movieAdded.bind(self))
|
||||
@@ -62,7 +63,8 @@ var MovieList = new Class({
|
||||
self.movies.each(function(movie){
|
||||
if(movie.get('id') == notification.data.id){
|
||||
movie.destroy();
|
||||
delete self.movies_added[notification.data.id]
|
||||
delete self.movies_added[notification.data.id];
|
||||
self.setCounter(self.counter_count-1);
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -76,6 +78,7 @@ var MovieList = new Class({
|
||||
if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){
|
||||
window.scroll(0,0);
|
||||
self.createMovie(notification.data, 'top');
|
||||
self.setCounter(self.counter_count+1);
|
||||
|
||||
self.checkIfEmpty();
|
||||
}
|
||||
@@ -115,7 +118,7 @@ var MovieList = new Class({
|
||||
self.createMovie(movie);
|
||||
});
|
||||
|
||||
self.total_movies = total;
|
||||
self.total_movies += total;
|
||||
self.setCounter(total);
|
||||
|
||||
},
|
||||
@@ -125,8 +128,41 @@ var MovieList = new Class({
|
||||
|
||||
if(!self.navigation_counter) return;
|
||||
|
||||
self.counter_count = count;
|
||||
self.navigation_counter.set('text', (count || 0) + ' movies');
|
||||
|
||||
if (self.empty_message) {
|
||||
self.empty_message.destroy();
|
||||
self.empty_message = null;
|
||||
}
|
||||
|
||||
if(self.total_movies && count == 0 && !self.empty_message){
|
||||
var message = (self.filter.search ? 'for "'+self.filter.search+'"' : '') +
|
||||
(self.filter.starts_with ? ' in <strong>'+self.filter.starts_with+'</strong>' : '');
|
||||
|
||||
self.empty_message = new Element('.message', {
|
||||
'html': 'No movies found ' + message + '.<br/>'
|
||||
}).grab(
|
||||
new Element('a', {
|
||||
'text': 'Reset filter',
|
||||
'events': {
|
||||
'click': function(){
|
||||
self.filter = {
|
||||
'starts_with': null,
|
||||
'search': null
|
||||
};
|
||||
self.navigation_search_input.set('value', '');
|
||||
self.reset();
|
||||
self.activateLetter();
|
||||
self.getMovies(true);
|
||||
self.last_search_value = '';
|
||||
}
|
||||
}
|
||||
})
|
||||
).inject(self.movie_list);
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
createMovie: function(movie, inject_at){
|
||||
@@ -151,66 +187,69 @@ var MovieList = new Class({
|
||||
|
||||
self.el.addClass('with_navigation')
|
||||
|
||||
self.navigation = new Element('div.alph_nav').grab(
|
||||
new Element('div').adopt(
|
||||
self.navigation_alpha = new Element('ul.numbers', {
|
||||
self.navigation = new Element('div.alph_nav').adopt(
|
||||
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
|
||||
new Element('span.select').adopt(
|
||||
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
|
||||
'events': {
|
||||
'change': self.massEditToggleAll.bind(self)
|
||||
}
|
||||
}),
|
||||
self.mass_edit_selected = new Element('span.count', {'text': 0}),
|
||||
self.mass_edit_selected_label = new Element('span', {'text': 'selected'})
|
||||
),
|
||||
new Element('div.quality').adopt(
|
||||
self.mass_edit_quality = new Element('select'),
|
||||
new Element('a.button.orange', {
|
||||
'text': 'Change quality',
|
||||
'events': {
|
||||
'click': self.changeQualitySelected.bind(self)
|
||||
}
|
||||
})
|
||||
),
|
||||
new Element('div.delete').adopt(
|
||||
new Element('span[text=or]'),
|
||||
new Element('a.button.red', {
|
||||
'text': 'Delete',
|
||||
'events': {
|
||||
'click': self.deleteSelected.bind(self)
|
||||
}
|
||||
})
|
||||
),
|
||||
new Element('div.refresh').adopt(
|
||||
new Element('span[text=or]'),
|
||||
new Element('a.button.green', {
|
||||
'text': 'Refresh',
|
||||
'events': {
|
||||
'click': self.refreshSelected.bind(self)
|
||||
}
|
||||
})
|
||||
)
|
||||
),
|
||||
new Element('div.menus').adopt(
|
||||
self.navigation_counter = new Element('span.counter[title=Total]'),
|
||||
self.filter_menu = new Block.Menu(self, {
|
||||
'class': 'filter'
|
||||
}),
|
||||
self.navigation_actions = new Element('ul.actions', {
|
||||
'events': {
|
||||
'click:relay(li)': function(e, el){
|
||||
self.movie_list.empty()
|
||||
self.activateLetter(el.get('data-letter'))
|
||||
self.getMovies()
|
||||
var a = 'active';
|
||||
self.navigation_actions.getElements('.'+a).removeClass(a);
|
||||
self.changeView(el.get('data-view'));
|
||||
this.addClass(a);
|
||||
|
||||
el.inject(el.getParent(), 'top');
|
||||
el.getSiblings().hide()
|
||||
setTimeout(function(){
|
||||
el.getSiblings().setStyle('display', null);
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
}),
|
||||
self.navigation_counter = new Element('span.counter[title=Total]'),
|
||||
self.navigation_actions = new Element('ul.inlay.actions.reversed'),
|
||||
self.navigation_search_input = new Element('input.search.inlay', {
|
||||
'title': 'Search through ' + self.options.identifier,
|
||||
'placeholder': 'Search through ' + self.options.identifier,
|
||||
'events': {
|
||||
'keyup': self.search.bind(self),
|
||||
'change': self.search.bind(self)
|
||||
}
|
||||
}),
|
||||
self.navigation_menu = new Block.Menu(self),
|
||||
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
|
||||
new Element('span.select').adopt(
|
||||
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
|
||||
'events': {
|
||||
'change': self.massEditToggleAll.bind(self)
|
||||
}
|
||||
}),
|
||||
self.mass_edit_selected = new Element('span.count', {'text': 0}),
|
||||
self.mass_edit_selected_label = new Element('span', {'text': 'selected'})
|
||||
),
|
||||
new Element('div.quality').adopt(
|
||||
self.mass_edit_quality = new Element('select'),
|
||||
new Element('a.button.orange', {
|
||||
'text': 'Change quality',
|
||||
'events': {
|
||||
'click': self.changeQualitySelected.bind(self)
|
||||
}
|
||||
})
|
||||
),
|
||||
new Element('div.delete').adopt(
|
||||
new Element('span[text=or]'),
|
||||
new Element('a.button.red', {
|
||||
'text': 'Delete',
|
||||
'events': {
|
||||
'click': self.deleteSelected.bind(self)
|
||||
}
|
||||
})
|
||||
),
|
||||
new Element('div.refresh').adopt(
|
||||
new Element('span[text=or]'),
|
||||
new Element('a.button.green', {
|
||||
'text': 'Refresh',
|
||||
'events': {
|
||||
'click': self.refreshSelected.bind(self)
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
self.navigation_menu = new Block.Menu(self, {
|
||||
'class': 'extra'
|
||||
})
|
||||
)
|
||||
).inject(self.el, 'top');
|
||||
|
||||
@@ -223,20 +262,56 @@ var MovieList = new Class({
|
||||
}).inject(self.mass_edit_quality)
|
||||
});
|
||||
|
||||
self.filter_menu.addLink(
|
||||
self.navigation_search_input = new Element('input', {
|
||||
'title': 'Search through ' + self.options.identifier,
|
||||
'placeholder': 'Search through ' + self.options.identifier,
|
||||
'events': {
|
||||
'keyup': self.search.bind(self),
|
||||
'change': self.search.bind(self)
|
||||
}
|
||||
})
|
||||
).addClass('search');
|
||||
|
||||
var available_chars;
|
||||
self.filter_menu.addEvent('open', function(){
|
||||
self.navigation_search_input.focus();
|
||||
|
||||
// Get available chars and highlight
|
||||
if(!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible()))
|
||||
Api.request('movie.available_chars', {
|
||||
'data': Object.merge({
|
||||
'status': self.options.status
|
||||
}, self.filter),
|
||||
'onSuccess': function(json){
|
||||
available_chars = json.chars
|
||||
|
||||
json.chars.split('').each(function(c){
|
||||
self.letters[c.capitalize()].addClass('available')
|
||||
})
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
self.filter_menu.addLink(
|
||||
self.navigation_alpha = new Element('ul.numbers', {
|
||||
'events': {
|
||||
'click:relay(li.available)': function(e, el){
|
||||
self.activateLetter(el.get('data-letter'))
|
||||
self.getMovies(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Actions
|
||||
['mass_edit', 'details', 'list'].each(function(view){
|
||||
self.navigation_actions.adopt(
|
||||
new Element('li.'+view+(self.current_view == view ? '.active' : '')+'[data-view='+view+']', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
var a = 'active';
|
||||
self.navigation_actions.getElements('.'+a).removeClass(a);
|
||||
self.changeView(this.get('data-view'));
|
||||
this.addClass(a);
|
||||
}
|
||||
}
|
||||
}).adopt(new Element('span'))
|
||||
)
|
||||
var current = self.current_view == view;
|
||||
new Element('li', {
|
||||
'class': 'icon2 ' + view + (current ? ' active ' : ''),
|
||||
'data-view': view
|
||||
}).inject(self.navigation_actions, current ? 'top' : 'bottom');
|
||||
});
|
||||
|
||||
// All
|
||||
@@ -253,21 +328,6 @@ var MovieList = new Class({
|
||||
}).inject(self.navigation_alpha);
|
||||
});
|
||||
|
||||
// Get available chars and highlight
|
||||
if(self.navigation.isDisplayed() || self.navigation.isVisible())
|
||||
Api.request('movie.available_chars', {
|
||||
'data': Object.merge({
|
||||
'status': self.options.status
|
||||
}, self.filter),
|
||||
'onSuccess': function(json){
|
||||
|
||||
json.chars.split('').each(function(c){
|
||||
self.letters[c.capitalize()].addClass('available')
|
||||
})
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
// Add menu or hide
|
||||
if (self.options.menu.length > 0)
|
||||
self.options.menu.each(function(menu_item){
|
||||
@@ -322,14 +382,14 @@ var MovieList = new Class({
|
||||
self.movies.each(function(movie){
|
||||
if (movie.isSelected()){
|
||||
$(movie).destroy()
|
||||
erase_movies.include(movie)
|
||||
erase_movies.include(movie);
|
||||
}
|
||||
});
|
||||
|
||||
erase_movies.each(function(movie){
|
||||
self.movies.erase(movie);
|
||||
|
||||
movie.destroy()
|
||||
movie.destroy();
|
||||
self.setCounter(self.counter_count-1);
|
||||
});
|
||||
|
||||
self.calculateSelected();
|
||||
@@ -448,8 +508,7 @@ var MovieList = new Class({
|
||||
self.activateLetter();
|
||||
self.filter.search = search_value;
|
||||
|
||||
self.movie_list.empty();
|
||||
self.getMovies();
|
||||
self.getMovies(true);
|
||||
|
||||
self.last_search_value = search_value;
|
||||
|
||||
@@ -461,11 +520,10 @@ var MovieList = new Class({
|
||||
var self = this;
|
||||
|
||||
self.reset();
|
||||
self.movie_list.empty();
|
||||
self.getMovies();
|
||||
self.getMovies(true);
|
||||
},
|
||||
|
||||
getMovies: function(){
|
||||
getMovies: function(reset){
|
||||
var self = this;
|
||||
|
||||
if(self.scrollspy){
|
||||
@@ -492,10 +550,13 @@ var MovieList = new Class({
|
||||
Api.request(self.options.api_call || 'movie.list', {
|
||||
'data': Object.merge({
|
||||
'status': self.options.status,
|
||||
'limit_offset': self.options.limit + ',' + self.offset
|
||||
'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null
|
||||
}, self.filter),
|
||||
'onSuccess': function(json){
|
||||
|
||||
if(reset)
|
||||
self.movie_list.empty();
|
||||
|
||||
if(self.loader_first){
|
||||
var lf = self.loader_first;
|
||||
self.loader_first.addClass('hide')
|
||||
@@ -507,7 +568,7 @@ var MovieList = new Class({
|
||||
}
|
||||
|
||||
self.store(json.movies);
|
||||
self.addMovies(json.movies, json.total);
|
||||
self.addMovies(json.movies, json.total || json.movies.length);
|
||||
if(self.scrollspy) {
|
||||
self.load_more.set('text', 'load more movies');
|
||||
self.scrollspy.start();
|
||||
@@ -1,9 +1,13 @@
|
||||
var MovieAction = new Class({
|
||||
|
||||
class_name: 'action icon',
|
||||
Implements: [Options],
|
||||
|
||||
initialize: function(movie){
|
||||
class_name: 'action icon2',
|
||||
|
||||
initialize: function(movie, options){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.movie = movie;
|
||||
|
||||
self.create();
|
||||
@@ -21,6 +25,32 @@ var MovieAction = new Class({
|
||||
this.el.removeClass('disable')
|
||||
},
|
||||
|
||||
getTitle: function(){
|
||||
var self = this;
|
||||
|
||||
try {
|
||||
return self.movie.getTitle();
|
||||
}
|
||||
catch(e){
|
||||
try {
|
||||
return self.movie.original_title ? self.movie.original_title : self.movie.titles[0];
|
||||
}
|
||||
catch(e){
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
get: function(key){
|
||||
var self = this;
|
||||
try {
|
||||
return self.movie.get(key)
|
||||
}
|
||||
catch(e){
|
||||
return self.movie[key]
|
||||
}
|
||||
},
|
||||
|
||||
createMask: function(){
|
||||
var self = this;
|
||||
self.mask = new Element('div.mask', {
|
||||
@@ -62,10 +92,10 @@ MA.IMDB = new Class({
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.id = self.movie.get('identifier');
|
||||
self.id = self.movie.get('imdb') || self.movie.get('identifier');
|
||||
|
||||
self.el = new Element('a.imdb', {
|
||||
'title': 'Go to the IMDB page of ' + self.movie.getTitle(),
|
||||
'title': 'Go to the IMDB page of ' + self.getTitle(),
|
||||
'href': 'http://www.imdb.com/title/'+self.id+'/',
|
||||
'target': '_blank'
|
||||
});
|
||||
@@ -82,8 +112,8 @@ MA.Release = new Class({
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.releases.icon.download', {
|
||||
'title': 'Show the releases that are available for ' + self.movie.getTitle(),
|
||||
self.el = new Element('a.releases.download', {
|
||||
'title': 'Show the releases that are available for ' + self.getTitle(),
|
||||
'events': {
|
||||
'click': self.show.bind(self)
|
||||
}
|
||||
@@ -94,16 +124,54 @@ MA.Release = new Class({
|
||||
else
|
||||
self.showHelper();
|
||||
|
||||
App.addEvent('movie.searcher.ended.'+self.movie.data.id, function(notification){
|
||||
self.releases = null;
|
||||
if(self.options_container){
|
||||
self.options_container.destroy();
|
||||
self.options_container = null;
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
show: function(e){
|
||||
var self = this;
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
if(self.releases)
|
||||
self.createReleases();
|
||||
else {
|
||||
|
||||
self.movie.busy(true);
|
||||
|
||||
Api.request('release.for_movie', {
|
||||
'data': {
|
||||
'id': self.movie.data.id
|
||||
},
|
||||
'onComplete': function(json){
|
||||
self.movie.busy(false, 1);
|
||||
|
||||
if(json && json.releases){
|
||||
self.releases = json.releases;
|
||||
self.createReleases();
|
||||
}
|
||||
else
|
||||
alert('Something went wrong, check the logs.');
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
|
||||
createReleases: function(){
|
||||
var self = this;
|
||||
|
||||
if(!self.options_container){
|
||||
self.options_container = new Element('div.options').adopt(
|
||||
self.release_container = new Element('div.releases.table').adopt(
|
||||
self.trynext_container = new Element('div.buttons.try_container')
|
||||
)
|
||||
self.options_container = new Element('div.options').grab(
|
||||
self.release_container = new Element('div.releases.table')
|
||||
);
|
||||
|
||||
// Header
|
||||
@@ -117,7 +185,7 @@ MA.Release = new Class({
|
||||
new Element('span.provider', {'text': 'Provider'})
|
||||
).inject(self.release_container)
|
||||
|
||||
self.movie.data.releases.sortBy('-info.score').each(function(release){
|
||||
self.releases.each(function(release){
|
||||
|
||||
var status = Status.get(release.status_id),
|
||||
quality = Quality.getProfile(release.quality_id) || {},
|
||||
@@ -138,7 +206,7 @@ MA.Release = new Class({
|
||||
}
|
||||
|
||||
// Create release
|
||||
new Element('div', {
|
||||
var item = new Element('div', {
|
||||
'class': 'item '+status.identifier,
|
||||
'id': 'release_'+release.id
|
||||
}).adopt(
|
||||
@@ -149,11 +217,11 @@ MA.Release = new Class({
|
||||
new Element('span.age', {'text': self.get(release, 'age')}),
|
||||
new Element('span.score', {'text': self.get(release, 'score')}),
|
||||
new Element('span.provider', { 'text': provider, 'title': provider }),
|
||||
release.info['detail_url'] ? new Element('a.info.icon', {
|
||||
release.info['detail_url'] ? new Element('a.info.icon2', {
|
||||
'href': release.info['detail_url'],
|
||||
'target': '_blank'
|
||||
}) : null,
|
||||
new Element('a.download.icon', {
|
||||
}) : new Element('a'),
|
||||
new Element('a.download.icon2', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
@@ -162,16 +230,17 @@ MA.Release = new Class({
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Element('a.delete.icon', {
|
||||
new Element('a.delete.icon2', {
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
self.ignore(release);
|
||||
this.getParent('.item').toggleClass('ignored')
|
||||
}
|
||||
}
|
||||
})
|
||||
).inject(self.release_container)
|
||||
).inject(self.release_container);
|
||||
|
||||
release['el'] = item;
|
||||
|
||||
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
|
||||
if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched'))
|
||||
@@ -182,33 +251,40 @@ MA.Release = new Class({
|
||||
}
|
||||
});
|
||||
|
||||
if(self.last_release){
|
||||
if(self.last_release)
|
||||
self.release_container.getElement('#release_'+self.last_release.id).addClass('last_release');
|
||||
}
|
||||
|
||||
if(self.next_release){
|
||||
if(self.next_release)
|
||||
self.release_container.getElement('#release_'+self.next_release.id).addClass('next_release');
|
||||
}
|
||||
|
||||
if(self.next_release || self.last_release){
|
||||
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){
|
||||
|
||||
self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top');
|
||||
|
||||
var nr = self.next_release,
|
||||
lr = self.last_release;
|
||||
|
||||
self.trynext_container.adopt(
|
||||
new Element('span.or', {
|
||||
'text': 'This movie is snatched, if anything went wrong, download'
|
||||
}),
|
||||
self.last_release ? new Element('a.button.orange', {
|
||||
lr ? new Element('a.button.orange', {
|
||||
'text': 'the same release again',
|
||||
'events': {
|
||||
'click': self.trySameRelease.bind(self)
|
||||
'click': function(){
|
||||
self.download(lr);
|
||||
}
|
||||
}
|
||||
}) : null,
|
||||
self.next_release && self.last_release ? new Element('span.or', {
|
||||
nr && lr ? new Element('span.or', {
|
||||
'text': ','
|
||||
}) : null,
|
||||
self.next_release ? [new Element('a.button.green', {
|
||||
'text': self.last_release ? 'another release' : 'the best release',
|
||||
nr ? [new Element('a.button.green', {
|
||||
'text': lr ? 'another release' : 'the best release',
|
||||
'events': {
|
||||
'click': self.tryNextRelease.bind(self)
|
||||
'click': function(){
|
||||
self.download(nr);
|
||||
}
|
||||
}
|
||||
}),
|
||||
new Element('span.or', {
|
||||
@@ -217,18 +293,15 @@ MA.Release = new Class({
|
||||
)
|
||||
}
|
||||
|
||||
self.last_release = null;
|
||||
self.next_release = null;
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
show: function(e){
|
||||
var self = this;
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
self.createReleases();
|
||||
// Show it
|
||||
self.options_container.inject(self.movie, 'top');
|
||||
self.movie.slide('in', self.options_container);
|
||||
|
||||
},
|
||||
|
||||
showHelper: function(e){
|
||||
@@ -236,19 +309,34 @@ MA.Release = new Class({
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
self.createReleases();
|
||||
self.trynext_container = new Element('div.buttons.trynext').inject(self.movie.info_container);
|
||||
var has_available = false,
|
||||
has_snatched = false;
|
||||
|
||||
if(self.next_release || self.last_release){
|
||||
self.movie.data.releases.each(function(release){
|
||||
if(has_available && has_snatched) return;
|
||||
|
||||
var status = Status.get(release.status_id);
|
||||
|
||||
if(['snatched', 'downloaded', 'seeding'].contains(status.identifier))
|
||||
has_snatched = true;
|
||||
|
||||
if(['available'].contains(status.identifier))
|
||||
has_available = true;
|
||||
|
||||
});
|
||||
|
||||
if(has_available || has_snatched){
|
||||
|
||||
self.trynext_container = new Element('div.buttons.trynext').inject(self.movie.info_container);
|
||||
|
||||
self.trynext_container.adopt(
|
||||
self.next_release ? [new Element('a.icon.readd', {
|
||||
'text': self.last_release ? 'Download another release' : 'Download the best release',
|
||||
has_available ? [new Element('a.icon2.readd', {
|
||||
'text': has_snatched ? 'Download another release' : 'Download the best release',
|
||||
'events': {
|
||||
'click': self.tryNextRelease.bind(self)
|
||||
}
|
||||
}),
|
||||
new Element('a.icon.download', {
|
||||
new Element('a.icon2.download', {
|
||||
'text': 'pick one yourself',
|
||||
'events': {
|
||||
'click': function(){
|
||||
@@ -256,27 +344,10 @@ MA.Release = new Class({
|
||||
}
|
||||
}
|
||||
})] : null,
|
||||
new Element('a.icon.completed', {
|
||||
new Element('a.icon2.completed', {
|
||||
'text': 'mark this movie done',
|
||||
'events': {
|
||||
'click': function(){
|
||||
Api.request('movie.delete', {
|
||||
'data': {
|
||||
'id': self.movie.get('id'),
|
||||
'delete_from': 'wanted'
|
||||
},
|
||||
'onComplete': function(){
|
||||
var movie = $(self.movie);
|
||||
movie.set('tween', {
|
||||
'duration': 300,
|
||||
'onComplete': function(){
|
||||
self.movie.destroy()
|
||||
}
|
||||
});
|
||||
movie.tween('height', 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
'click': self.markMovieDone.bind(self)
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -292,19 +363,21 @@ MA.Release = new Class({
|
||||
var self = this;
|
||||
|
||||
var release_el = self.release_container.getElement('#release_'+release.id),
|
||||
icon = release_el.getElement('.download.icon');
|
||||
icon = release_el.getElement('.download.icon2');
|
||||
|
||||
self.movie.busy(true);
|
||||
icon.addClass('icon spinner').removeClass('download');
|
||||
|
||||
Api.request('release.download', {
|
||||
'data': {
|
||||
'id': release.id
|
||||
},
|
||||
'onComplete': function(json){
|
||||
self.movie.busy(false);
|
||||
icon.removeClass('icon spinner');
|
||||
|
||||
if(json.success)
|
||||
if(json.success){
|
||||
icon.addClass('completed');
|
||||
release_el.getElement('.release_status').set('text', 'snatched');
|
||||
}
|
||||
else
|
||||
icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.');
|
||||
}
|
||||
@@ -317,29 +390,52 @@ MA.Release = new Class({
|
||||
Api.request('release.ignore', {
|
||||
'data': {
|
||||
'id': release.id
|
||||
},
|
||||
'onComplete': function(){
|
||||
var el = release.el;
|
||||
if(el.hasClass('failed') || el.hasClass('ignored')){
|
||||
el.removeClass('failed').removeClass('ignored');
|
||||
el.getElement('.release_status').set('text', 'available');
|
||||
}
|
||||
else {
|
||||
el.addClass('ignored');
|
||||
el.getElement('.release_status').set('text', 'ignored');
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
markMovieDone: function(){
|
||||
var self = this;
|
||||
|
||||
Api.request('movie.delete', {
|
||||
'data': {
|
||||
'id': self.movie.get('id'),
|
||||
'delete_from': 'wanted'
|
||||
},
|
||||
'onComplete': function(){
|
||||
var movie = $(self.movie);
|
||||
movie.set('tween', {
|
||||
'duration': 300,
|
||||
'onComplete': function(){
|
||||
self.movie.destroy()
|
||||
}
|
||||
});
|
||||
movie.tween('height', 0);
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
tryNextRelease: function(movie_id){
|
||||
var self = this;
|
||||
|
||||
self.createReleases();
|
||||
|
||||
if(self.last_release)
|
||||
self.ignore(self.last_release);
|
||||
|
||||
if(self.next_release)
|
||||
self.download(self.next_release);
|
||||
|
||||
},
|
||||
|
||||
trySameRelease: function(movie_id){
|
||||
var self = this;
|
||||
|
||||
if(self.last_release)
|
||||
self.download(self.last_release);
|
||||
Api.request('movie.searcher.try_next', {
|
||||
'data': {
|
||||
'id': self.movie.get('id')
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -354,7 +450,7 @@ MA.Trailer = new Class({
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.trailer', {
|
||||
'title': 'Watch the trailer of ' + self.movie.getTitle(),
|
||||
'title': 'Watch the trailer of ' + self.getTitle(),
|
||||
'events': {
|
||||
'click': self.watch.bind(self)
|
||||
}
|
||||
@@ -365,14 +461,14 @@ MA.Trailer = new Class({
|
||||
watch: function(offset){
|
||||
var self = this;
|
||||
|
||||
var data_url = 'http://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'
|
||||
var data_url = 'https://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'
|
||||
var url = data_url.substitute({
|
||||
'title': encodeURI(self.movie.getTitle()),
|
||||
'year': self.movie.get('year'),
|
||||
'title': encodeURI(self.getTitle()),
|
||||
'year': self.get('year'),
|
||||
'offset': offset || 1
|
||||
}),
|
||||
size = $(self.movie).getSize(),
|
||||
height = (size.x/16)*9,
|
||||
height = self.options.height || (size.x/16)*9,
|
||||
id = 'trailer-'+randomString();
|
||||
|
||||
self.player_container = new Element('div[id='+id+']');
|
||||
@@ -478,6 +574,11 @@ MA.Edit = new Class({
|
||||
self.profile_select = new Element('select', {
|
||||
'name': 'profile'
|
||||
}),
|
||||
self.category_select = new Element('select', {
|
||||
'name': 'category'
|
||||
}).grab(
|
||||
new Element('option', {'value': -1, 'text': 'None'})
|
||||
),
|
||||
new Element('a.button.edit', {
|
||||
'text': 'Save & Search',
|
||||
'events': {
|
||||
@@ -497,7 +598,34 @@ MA.Edit = new Class({
|
||||
});
|
||||
|
||||
|
||||
Quality.getActiveProfiles().each(function(profile){
|
||||
// Fill categories
|
||||
var categories = CategoryList.getAll();
|
||||
|
||||
if(categories.length == 0)
|
||||
self.category_select.hide();
|
||||
else {
|
||||
self.category_select.show();
|
||||
categories.each(function(category){
|
||||
|
||||
var category_id = category.data.id;
|
||||
|
||||
new Element('option', {
|
||||
'value': category_id,
|
||||
'text': category.data.label
|
||||
}).inject(self.category_select);
|
||||
|
||||
if(self.movie.category && self.movie.category.data && self.movie.category.data.id == category_id)
|
||||
self.category_select.set('value', category_id);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
// Fill profiles
|
||||
var profiles = Quality.getActiveProfiles();
|
||||
if(profiles.length == 1)
|
||||
self.profile_select.hide();
|
||||
|
||||
profiles.each(function(profile){
|
||||
|
||||
var profile_id = profile.id ? profile.id : profile.data.id;
|
||||
|
||||
@@ -506,8 +634,9 @@ MA.Edit = new Class({
|
||||
'text': profile.label ? profile.label : profile.data.label
|
||||
}).inject(self.profile_select);
|
||||
|
||||
if(self.movie.profile && self.movie.profile.data && self.movie.profile.data.id == profile_id)
|
||||
if(self.movie.get('profile_id') == profile_id)
|
||||
self.profile_select.set('value', profile_id);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
@@ -523,7 +652,8 @@ MA.Edit = new Class({
|
||||
'data': {
|
||||
'id': self.movie.get('id'),
|
||||
'default_title': self.title_select.get('value'),
|
||||
'profile_id': self.profile_select.get('value')
|
||||
'profile_id': self.profile_select.get('value'),
|
||||
'category_id': self.category_select.get('value')
|
||||
},
|
||||
'useSpinner': true,
|
||||
'spinnerTarget': $(self.movie),
|
||||
@@ -654,6 +784,7 @@ MA.Delete = new Class({
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
|
||||
self.movie.removeView();
|
||||
self.movie.slide('out');
|
||||
},
|
||||
|
||||
@@ -702,16 +833,45 @@ MA.Files = new Class({
|
||||
self.el = new Element('a.directory', {
|
||||
'title': 'Available files',
|
||||
'events': {
|
||||
'click': self.showFiles.bind(self)
|
||||
'click': self.show.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
showFiles: function(e){
|
||||
show: function(e){
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
|
||||
if(self.releases)
|
||||
self.showFiles();
|
||||
else {
|
||||
|
||||
self.movie.busy(true);
|
||||
|
||||
Api.request('release.for_movie', {
|
||||
'data': {
|
||||
'id': self.movie.data.id
|
||||
},
|
||||
'onComplete': function(json){
|
||||
self.movie.busy(false, 1);
|
||||
|
||||
if(json && json.releases){
|
||||
self.releases = json.releases;
|
||||
self.showFiles();
|
||||
}
|
||||
else
|
||||
alert('Something went wrong, check the logs.');
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
showFiles: function(){
|
||||
var self = this;
|
||||
|
||||
if(!self.options_container){
|
||||
self.options_container = new Element('div.options').adopt(
|
||||
self.files_container = new Element('div.files.table')
|
||||
@@ -724,7 +884,7 @@ MA.Files = new Class({
|
||||
new Element('span.is_available', {'text': 'Available'})
|
||||
).inject(self.files_container)
|
||||
|
||||
Array.each(self.movie.data.releases, function(release){
|
||||
Array.each(self.releases, function(release){
|
||||
|
||||
var rel = new Element('div.release').inject(self.files_container);
|
||||
|
||||
@@ -8,12 +8,23 @@
|
||||
.movies > div {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
|
||||
.movies > div .message {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
.movies > div .message a {
|
||||
padding: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.movies.thumbs_list > div:not(.description) {
|
||||
margin-right: -4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.movies .loading {
|
||||
display: block;
|
||||
padding: 20px 0 0 0;
|
||||
@@ -32,26 +43,26 @@
|
||||
margin-top: -20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.movies .loading .spinner {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
.movies .loading .message {
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.movies h2 {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.movies h2 {
|
||||
font-size: 25px;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.movies > .description {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
@@ -62,41 +73,53 @@
|
||||
.movies:hover > .description {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 860px) {
|
||||
.movies > .description {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list {
|
||||
padding: 20px 0 20px;
|
||||
}
|
||||
|
||||
|
||||
.home .movies {
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.movies .movie {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
padding-left: 20px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
transition: all 0.6s cubic-bezier(0.9,0,0.1,1);
|
||||
transition-property: width, height;
|
||||
background: rgba(0,0,0,.2);
|
||||
}
|
||||
|
||||
|
||||
.movies.mass_edit_list .movie {
|
||||
padding-left: 22px;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.movies.details_list .movie {
|
||||
padding-left: 120px;
|
||||
}
|
||||
|
||||
.movies.list_list .movie:not(.details_view),
|
||||
.movies.mass_edit_list .movie {
|
||||
height: 32px;
|
||||
height: 30px;
|
||||
border-bottom: 1px solid rgba(255,255,255,.15);
|
||||
}
|
||||
|
||||
|
||||
.movies.list_list .movie:last-child,
|
||||
.movies.mass_edit_list .movie:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.movies.thumbs_list .movie {
|
||||
width: 16.66667%;
|
||||
height: auto;
|
||||
@@ -104,11 +127,8 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
vertical-align: top;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
.movies.thumbs_list .movie {
|
||||
width: 25%;
|
||||
@@ -125,16 +145,7 @@
|
||||
|
||||
.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.list_list .movie:hover:not(.details_view),
|
||||
.movies.mass_edit_list .movie {
|
||||
background: rgba(255,255,255,0.03);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.movies .data {
|
||||
@@ -142,19 +153,19 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
right: 0;
|
||||
border-radius: 0;
|
||||
transition: all .6s cubic-bezier(0.9,0,0.1,1);
|
||||
right: 0;
|
||||
}
|
||||
.movies.list_list .movie:not(.details_view) .data,
|
||||
.movies.mass_edit_list .movie .data {
|
||||
height: 30px;
|
||||
padding: 3px 0 3px 10px;
|
||||
box-shadow: none;
|
||||
padding: 0 0 0 10px;
|
||||
border: 0;
|
||||
background: #4e5969;
|
||||
}
|
||||
|
||||
.movies.mass_edit_list .movie .data {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.movies.thumbs_list .data {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@@ -165,15 +176,7 @@
|
||||
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);
|
||||
}
|
||||
@@ -201,28 +204,32 @@
|
||||
line-height: 0;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
border-radius: 4px 0 0 4px;
|
||||
transition: all .6s cubic-bezier(0.9,0,0.1,1);
|
||||
background: rgba(0,0,0,.1);
|
||||
}
|
||||
.movies.thumbs_list .poster {
|
||||
position: relative;
|
||||
border-radius: 0;
|
||||
}
|
||||
.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%;
|
||||
transition: none;
|
||||
background: no-repeat center;
|
||||
background-size: cover;
|
||||
}
|
||||
.movies.thumbs_list .no_thumbnail .empty_file {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.movies .poster img,
|
||||
.options .poster img {
|
||||
@@ -234,9 +241,9 @@
|
||||
width: 100%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
border-radius: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
.movies .info {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
@@ -248,31 +255,41 @@
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 2px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
padding-right: 80px;
|
||||
transition: all 0.2s linear;
|
||||
height: 35px;
|
||||
top: -5px;
|
||||
position: relative;
|
||||
}
|
||||
.movies.list_list .info .title,
|
||||
.movies.mass_edit_list .info .title {
|
||||
height: 100%;
|
||||
top: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.touch_enabled .movies.list_list .info .title {
|
||||
display: inline-block;
|
||||
padding-right: 55px;
|
||||
}
|
||||
|
||||
.movies .info .title span {
|
||||
display: block;
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
height: 100%;
|
||||
line-height: 30px;
|
||||
top: -5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list .info .title span {
|
||||
white-space: normal;
|
||||
overflow: auto;
|
||||
height: auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.movies.thumbs_list .movie .info .title span,
|
||||
.movies.thumbs_list .movie .info .year {
|
||||
@@ -281,32 +298,33 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.movies.list_list .movie:not(.details_view) .info .title,
|
||||
.movies.mass_edit_list .info .title {
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
||||
.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;
|
||||
word-wrap: break-word;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.movies .info .year {
|
||||
position: absolute;
|
||||
color: #bbb;
|
||||
right: 0;
|
||||
top: 1px;
|
||||
top: 6px;
|
||||
text-align: right;
|
||||
transition: all 0.2s linear;
|
||||
font-weight: normal;
|
||||
@@ -316,7 +334,7 @@
|
||||
font-size: 1.25em;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list .info .year {
|
||||
font-size: 23px;
|
||||
margin: 0;
|
||||
@@ -327,6 +345,10 @@
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.touch_enabled .movies.list_list .movie .info .year {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.movies .info .description {
|
||||
top: 30px;
|
||||
clear: both;
|
||||
@@ -349,13 +371,24 @@
|
||||
display: block;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
|
||||
.movies.list_list .movie:hover .data .quality {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.touch_enabled .movies.list_list .movie .data .quality {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
top: -4px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.movies .data .quality {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.movies .status_suggest .data .quality,
|
||||
.movies.thumbs_list .data .quality {
|
||||
display: none;
|
||||
@@ -382,17 +415,19 @@
|
||||
right: 0;
|
||||
margin-right: 60px;
|
||||
z-index: 1;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
|
||||
.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 .failed { background-color: #a43d34; }
|
||||
.movies .data .quality .snatched { background-color: #a2a232; }
|
||||
.movies .data .quality .seeding { background-color: #0a6819; }
|
||||
.movies .data .quality .done {
|
||||
background-color: #369545;
|
||||
opacity: 1;
|
||||
@@ -407,59 +442,71 @@
|
||||
|
||||
.movies .data .actions {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
bottom: 17px;
|
||||
right: 20px;
|
||||
line-height: 0;
|
||||
top: 0;
|
||||
display: block;
|
||||
width: auto;
|
||||
opacity: 0;
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
@media all and (max-width: 480px) {
|
||||
.movies .data .actions {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.movies .movie:hover .data .actions {
|
||||
|
||||
.movies .movie:hover .data .actions,
|
||||
.touch_enabled .movies .movie .data .actions {
|
||||
opacity: 1;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
||||
.movies.details_list .data .actions {
|
||||
top: auto;
|
||||
bottom: 18px;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie:hover .actions {
|
||||
opacity: 1;
|
||||
display: inline-block;
|
||||
}
|
||||
.movies.thumbs_list .data .actions {
|
||||
bottom: 2px;
|
||||
bottom: 12px;
|
||||
right: 10px;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie:hover .action { opacity: 0.6; }
|
||||
.movies .movie:hover .action:hover { opacity: 1; }
|
||||
|
||||
.movies .data .action {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
display: inline-block;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
padding: 3px;
|
||||
height: 22px;
|
||||
min-width: 33px;
|
||||
padding: 0 5px;
|
||||
line-height: 26px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
color: #FFF;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
.movies .data .action.trailer { color: #FFF; }
|
||||
.movies .data .action.download { color: #b9dec0; }
|
||||
.movies .data .action.edit { color: #c6b589; }
|
||||
.movies .data .action.refresh { color: #cbeecc; }
|
||||
.movies .data .action.delete { color: #e9b0b0; }
|
||||
.movies .data .action.directory { color: #ffed92; }
|
||||
.movies .data .action.readd { color: #c2fac5; }
|
||||
|
||||
.movies.mass_edit_list .movie .data .actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.movies.list_list .movie:not(.details_view):hover .actions,
|
||||
.movies.mass_edit_list .movie:hover .actions {
|
||||
.movies.mass_edit_list .movie:hover .actions,
|
||||
.touch_enabled .movies.list_list .movie:not(.details_view) .actions {
|
||||
margin: 0;
|
||||
background: #4e5969;
|
||||
top: 2px;
|
||||
@@ -473,12 +520,10 @@
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
position: absolute;
|
||||
padding: 70px 0 0;
|
||||
padding: 80px 0 0;
|
||||
left: 120px;
|
||||
right: 0;
|
||||
}
|
||||
.movies .delete_container .cancel {
|
||||
}
|
||||
.movies .delete_container .or {
|
||||
padding: 10px;
|
||||
}
|
||||
@@ -498,9 +543,9 @@
|
||||
}
|
||||
|
||||
.movies .options .form {
|
||||
margin: 70px 20px 0;
|
||||
float: left;
|
||||
margin: 80px 0 0;
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.movies .options .form select {
|
||||
@@ -510,16 +555,21 @@
|
||||
.movies .options .table {
|
||||
height: 180px;
|
||||
overflow: auto;
|
||||
line-height: 2em;
|
||||
}
|
||||
.movies .options .table .item {
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.movies .options .table .item.ignored span {
|
||||
.movies .options .table .item.ignored span,
|
||||
.movies .options .table .item.failed span {
|
||||
text-decoration: line-through;
|
||||
color: rgba(255,255,255,0.4);
|
||||
}
|
||||
.movies .options .table .item.ignored .delete {
|
||||
background-image: url('../images/icon.undo.png');
|
||||
.movies .options .table .item.ignored .delete:before,
|
||||
.movies .options .table .item.failed .delete:before {
|
||||
display: inline-block;
|
||||
content: "\e04b";
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
|
||||
.movies .options .table .item:last-child { border: 0; }
|
||||
@@ -565,10 +615,13 @@
|
||||
width: 30px !important;
|
||||
height: 20px;
|
||||
opacity: 0.8;
|
||||
line-height: 25px;
|
||||
}
|
||||
.movies .options .table a:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.movies .options .table a:hover { opacity: 1; }
|
||||
.movies .options .table a.download { color: #a7fbaf; }
|
||||
.movies .options .table a.delete { color: #fda3a3; }
|
||||
.movies .options .table .ignored a.delete,
|
||||
.movies .options .table .failed a.delete { color: #b5fda3; }
|
||||
|
||||
.movies .options .table .head > * {
|
||||
font-weight: bold;
|
||||
@@ -578,7 +631,7 @@
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.movies .movie .trailer_container {
|
||||
.trailer_container {
|
||||
width: 100%;
|
||||
background: #000;
|
||||
text-align: center;
|
||||
@@ -588,11 +641,17 @@
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
.movies .movie .trailer_container.hide {
|
||||
@media only screen and (device-width: 768px) {
|
||||
.trailer_container iframe {
|
||||
margin-top: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.trailer_container.hide {
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
.movies .movie .hide_trailer {
|
||||
.hide_trailer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
@@ -601,11 +660,10 @@
|
||||
text-align: center;
|
||||
padding: 3px 10px;
|
||||
background: #4e5969;
|
||||
border-radius: 0 0 2px 2px;
|
||||
transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s;
|
||||
z-index: 11;
|
||||
}
|
||||
.movies .movie .hide_trailer.hide {
|
||||
.hide_trailer.hide {
|
||||
top: -30px;
|
||||
}
|
||||
|
||||
@@ -637,17 +695,18 @@
|
||||
.movies .movie .trynext {
|
||||
display: inline;
|
||||
position: absolute;
|
||||
right: 135px;
|
||||
right: 180px;
|
||||
z-index: 2;
|
||||
opacity: 0;
|
||||
background: #4e5969;
|
||||
min-width: 300px;
|
||||
text-align: right;
|
||||
height: 100%;
|
||||
padding: 3px 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.touch_enabled .movies .movie .trynext {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.movies .movie .trynext {
|
||||
display: none;
|
||||
@@ -655,31 +714,42 @@
|
||||
}
|
||||
.movies.mass_edit_list .trynext { display: none; }
|
||||
.wanted .movies .movie .trynext {
|
||||
padding-right: 50px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
.movies .movie:hover .trynext {
|
||||
.movies .movie:hover .trynext,
|
||||
.touch_enabled .movies.details_list .movie .trynext {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
.movies.details_list .movie .trynext {
|
||||
background: #47515f;
|
||||
padding: 0;
|
||||
right: 0;
|
||||
bottom: 35px;
|
||||
height: auto;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie .trynext a {
|
||||
background-position: 5px center;
|
||||
padding: 0 5px 0 25px;
|
||||
margin-right: 10px;
|
||||
color: #FFF;
|
||||
border-radius: 2px;
|
||||
height: 100%;
|
||||
line-height: 27px;
|
||||
font-family: OpenSans, "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
|
||||
}
|
||||
.movies .movie .trynext a:last-child {
|
||||
margin: 0;
|
||||
.movies .movie .trynext a:before {
|
||||
margin: 2px 0 0 -20px;
|
||||
position: absolute;
|
||||
font-family: 'Elusive-Icons';
|
||||
}
|
||||
.movies .movie .trynext a:hover {
|
||||
.movies.details_list .movie .trynext a {
|
||||
line-height: 23px;
|
||||
}
|
||||
.movies .movie .trynext a:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
.movies .movie .trynext a:hover,
|
||||
.touch_enabled .movies .movie .trynext a {
|
||||
background-color: #369545;
|
||||
}
|
||||
|
||||
@@ -694,14 +764,7 @@
|
||||
}
|
||||
|
||||
.movies .alph_nav {
|
||||
transition: box-shadow .4s linear;
|
||||
position: relative;
|
||||
z-index: 4;
|
||||
top: 0px;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
padding: 10px 0;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
@@ -710,12 +773,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
.movies .alph_nav > div {
|
||||
position: relative;
|
||||
max-width: 980px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
min-height: 24px;
|
||||
.movies .alph_nav .menus {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.movies .alph_nav .numbers,
|
||||
@@ -724,118 +784,152 @@
|
||||
list-style: none;
|
||||
padding: 0 0 1px;
|
||||
margin: 0;
|
||||
float: left;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.movies .alph_nav .counter {
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
position: absolute;
|
||||
right: 270px;
|
||||
background: #4e5969;
|
||||
padding: 4px 10px;
|
||||
padding: 0 10px;
|
||||
height: 100%;
|
||||
line-height: 43px;
|
||||
border-right: 1px solid rgba(255,255,255,.07);
|
||||
}
|
||||
|
||||
.movies .alph_nav .numbers li,
|
||||
.movies .alph_nav .numbers li,
|
||||
.movies .alph_nav .actions li {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
height: 24px;
|
||||
line-height: 23px;
|
||||
height: 100%;
|
||||
line-height: 30px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: rgba(255,255,255,0.2);
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
@media all and (max-width: 900px) {
|
||||
.movies .alph_nav .numbers {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.movies .alph_nav .numbers li {
|
||||
width: auto;
|
||||
padding: 0 4px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.movies .alph_nav .numbers li.letter_all {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
.movies .alph_nav li.available {
|
||||
color: #FFF;
|
||||
font-weight: bolder;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
|
||||
}
|
||||
.movies .alph_nav li.active.available,
|
||||
.movies .alph_nav li.active.available,
|
||||
.movies .alph_nav li.available:hover {
|
||||
background: rgba(255,255,255,.1);
|
||||
background: rgba(0,0,0,.1);
|
||||
}
|
||||
|
||||
.movies .alph_nav .search {
|
||||
.movies .alph_nav .search input {
|
||||
padding: 6px 5px;
|
||||
margin: 0 0 0 20px;
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
width: 154px;
|
||||
height: 25px;
|
||||
transition: all 0.6s cubic-bezier(0.9,0,0.1,1);
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
display: inline-block;
|
||||
border: 0;
|
||||
background: none;
|
||||
color: #444;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
padding: 0 10px 0 30px;
|
||||
border-bottom: 1px solid rgba(0,0,0,.08);
|
||||
}
|
||||
.movies .alph_nav .search input:focus {
|
||||
background: rgba(0,0,0,.08);
|
||||
}
|
||||
|
||||
.movies .alph_nav .search input::-webkit-input-placeholder {
|
||||
color: #444;
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
.movies .alph_nav .search:before {
|
||||
font-family: 'Elusive-Icons';
|
||||
content: "\e03e";
|
||||
position: absolute;
|
||||
height: 20px;
|
||||
line-height: 45px;
|
||||
font-size: 12px;
|
||||
margin: 0 0 0 10px;
|
||||
opacity: .6;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.movies .alph_nav .actions {
|
||||
margin: 0 6px 0 0;
|
||||
-moz-user-select: none;
|
||||
position: absolute;
|
||||
right: 183px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
z-index: 200;
|
||||
position: relative;
|
||||
border: 1px solid rgba(255,255,255,.07);
|
||||
border-width: 0 1px;
|
||||
}
|
||||
.movies .alph_nav .actions:hover {
|
||||
box-shadow: 0 100px 20px -10px rgba(0,0,0,0.55);
|
||||
}
|
||||
.movies .alph_nav .actions li {
|
||||
border-radius: 1px;
|
||||
width: auto;
|
||||
width: 100%;
|
||||
height: 45px;
|
||||
line-height: 40px;
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.movies .alph_nav .actions li.active {
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.movies .alph_nav .actions li span {
|
||||
display: block;
|
||||
background: url('../images/sprite.png') no-repeat;
|
||||
width: 25px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.movies .alph_nav .actions li.mass_edit span {
|
||||
background-position: 3px 3px;
|
||||
.movies .alph_nav .actions:hover li:not(.active) {
|
||||
display: block;
|
||||
background: #FFF;
|
||||
color: #444;
|
||||
}
|
||||
.movies .alph_nav .actions li:hover:not(.active) {
|
||||
background: #ccc;
|
||||
}
|
||||
.movies .alph_nav .actions li.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.movies .alph_nav .actions li.list span {
|
||||
background-position: 3px -95px;
|
||||
.movies .alph_nav .actions li.mass_edit:before {
|
||||
content: "\e070";
|
||||
}
|
||||
|
||||
.movies .alph_nav .actions li.details span {
|
||||
background-position: 3px -74px;
|
||||
.movies .alph_nav .actions li.list:before {
|
||||
content: "\e0d8";
|
||||
}
|
||||
|
||||
.movies .alph_nav .actions li:first-child {
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
.movies .alph_nav .actions li:last-child {
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
.movies .alph_nav .actions li.details:before {
|
||||
content: "\e022";
|
||||
}
|
||||
|
||||
.movies .alph_nav .mass_edit_form {
|
||||
clear: both;
|
||||
text-align: center;
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
float: left;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
}
|
||||
.movies.mass_edit_list .mass_edit_form {
|
||||
display: block;
|
||||
display: inline-block;
|
||||
}
|
||||
.movies.mass_edit_list .mass_edit_form .select {
|
||||
float: left;
|
||||
margin: 5px 0 0 5px;
|
||||
font-size: 14px;
|
||||
display: inline-block;
|
||||
}
|
||||
.movies.mass_edit_list .mass_edit_form .select span {
|
||||
.movies.mass_edit_list .mass_edit_form .select .check {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: -4px 0 0 5px;
|
||||
}
|
||||
.movies.mass_edit_list .mass_edit_form .select span {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.movies.mass_edit_list .mass_edit_form .select .count {
|
||||
@@ -844,8 +938,7 @@
|
||||
}
|
||||
|
||||
.movies .alph_nav .mass_edit_form .quality {
|
||||
float: left;
|
||||
padding: 8px 0 0;
|
||||
display: inline-block;
|
||||
margin: 0 0 0 16px;
|
||||
}
|
||||
.movies .alph_nav .mass_edit_form .quality select {
|
||||
@@ -858,8 +951,8 @@
|
||||
|
||||
.movies .alph_nav .mass_edit_form .refresh,
|
||||
.movies .alph_nav .mass_edit_form .delete {
|
||||
float: left;
|
||||
padding: 8px 0 0 8px;
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.movies .alph_nav .mass_edit_form .refresh span,
|
||||
@@ -867,28 +960,48 @@
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
|
||||
.movies .alph_nav .more_menu {
|
||||
right: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.movies .alph_nav .more_menu > a {
|
||||
background-color: #4e5969;
|
||||
background-position: center -158px;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.movies .alph_nav .more_menu.extra > a:before {
|
||||
content: '...';
|
||||
font-size: 1.7em;
|
||||
line-height: 23px;
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.movies .alph_nav .more_menu.filter {
|
||||
}
|
||||
|
||||
.movies .alph_nav .more_menu.filter > a:before {
|
||||
content: "\e0e8";
|
||||
font-family: 'Elusive-Icons';
|
||||
line-height: 33px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.movies .alph_nav .more_menu.filter .wrapper {
|
||||
right: 88px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.movies .empty_wanted {
|
||||
background-image: url('../images/emptylist.png');
|
||||
background-position: 80% 0;
|
||||
height: 750px;
|
||||
width: 800px;
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
padding-top: 260px;
|
||||
margin-top: -50px;
|
||||
}
|
||||
|
||||
.movies .empty_manage {
|
||||
text-align: center;
|
||||
font-size: 25px;
|
||||
line-height: 150%;
|
||||
padding: 40px 0;
|
||||
}
|
||||
|
||||
.movies .empty_manage .after_manage {
|
||||
@@ -897,7 +1010,6 @@
|
||||
}
|
||||
|
||||
.movies .progress {
|
||||
border-radius: 2px;
|
||||
padding: 10px;
|
||||
margin: 5px 0;
|
||||
text-align: left;
|
||||
@@ -912,7 +1024,6 @@
|
||||
width: 49%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
margin: 2px 0.5%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.movies .progress > div .folder {
|
||||
@@ -11,9 +11,10 @@ var Movie = new Class({
|
||||
self.view = options.view || 'details';
|
||||
self.list = list;
|
||||
|
||||
self.el = new Element('div.movie.inlay');
|
||||
self.el = new Element('div.movie');
|
||||
|
||||
self.profile = Quality.getProfile(data.profile_id) || {};
|
||||
self.category = CategoryList.getCategory(data.category_id) || {};
|
||||
self.parent(self, options);
|
||||
|
||||
self.addEvents();
|
||||
@@ -23,18 +24,19 @@ var Movie = new Class({
|
||||
var self = this;
|
||||
|
||||
App.addEvent('movie.update.'+self.data.id, function(notification){
|
||||
self.busy(false)
|
||||
self.busy(false);
|
||||
self.removeView();
|
||||
self.update.delay(2000, self, notification);
|
||||
});
|
||||
|
||||
['movie.busy', 'searcher.started'].each(function(listener){
|
||||
['movie.busy', 'movie.searcher.started'].each(function(listener){
|
||||
App.addEvent(listener+'.'+self.data.id, function(notification){
|
||||
if(notification.data)
|
||||
self.busy(true)
|
||||
});
|
||||
})
|
||||
|
||||
App.addEvent('searcher.ended.'+self.data.id, function(notification){
|
||||
App.addEvent('movie.searcher.ended.'+self.data.id, function(notification){
|
||||
if(notification.data)
|
||||
self.busy(false)
|
||||
});
|
||||
@@ -51,12 +53,12 @@ var Movie = new Class({
|
||||
|
||||
// Remove events
|
||||
App.removeEvents('movie.update.'+self.data.id);
|
||||
['movie.busy', 'searcher.started'].each(function(listener){
|
||||
['movie.busy', 'movie.searcher.started'].each(function(listener){
|
||||
App.removeEvents(listener+'.'+self.data.id);
|
||||
})
|
||||
},
|
||||
|
||||
busy: function(set_busy){
|
||||
busy: function(set_busy, timeout){
|
||||
var self = this;
|
||||
|
||||
if(!set_busy){
|
||||
@@ -70,9 +72,9 @@ var Movie = new Class({
|
||||
self.spinner.el.destroy();
|
||||
self.spinner = null;
|
||||
self.mask = null;
|
||||
}, 400);
|
||||
}, timeout || 400);
|
||||
}
|
||||
}, 1000)
|
||||
}, timeout || 1000)
|
||||
}
|
||||
else if(!self.spinner) {
|
||||
self.createMask();
|
||||
@@ -107,8 +109,10 @@ var Movie = new Class({
|
||||
|
||||
self.data = notification.data;
|
||||
self.el.empty();
|
||||
self.removeView();
|
||||
|
||||
self.profile = Quality.getProfile(self.data.profile_id) || {};
|
||||
self.category = CategoryList.getCategory(self.data.category_id) || {};
|
||||
self.create();
|
||||
|
||||
self.busy(false);
|
||||
@@ -139,9 +143,6 @@ var Movie = new Class({
|
||||
'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
|
||||
}),
|
||||
@@ -149,8 +150,8 @@ var Movie = new Class({
|
||||
'events': {
|
||||
'click': function(e){
|
||||
var releases = self.el.getElement('.actions .releases');
|
||||
if(releases)
|
||||
releases.fireEvent('click', [e])
|
||||
if(releases.isVisible())
|
||||
releases.fireEvent('click', [e])
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -178,20 +179,21 @@ var Movie = new Class({
|
||||
});
|
||||
|
||||
// Add releases
|
||||
self.data.releases.each(function(release){
|
||||
|
||||
var q = self.quality.getElement('.q_id'+ release.quality_id),
|
||||
status = Status.get(release.status_id);
|
||||
|
||||
if(!q && (status.identifier == 'snatched' || status.identifier == 'done'))
|
||||
var q = self.addQuality(release.quality_id)
|
||||
|
||||
if (status && q && !q.hasClass(status.identifier)){
|
||||
q.addClass(status.identifier);
|
||||
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label)
|
||||
}
|
||||
|
||||
});
|
||||
if(self.data.releases)
|
||||
self.data.releases.each(function(release){
|
||||
|
||||
var q = self.quality.getElement('.q_id'+ release.quality_id),
|
||||
status = Status.get(release.status_id);
|
||||
|
||||
if(!q && (status.identifier == 'snatched' || status.identifier == 'done'))
|
||||
var q = self.addQuality(release.quality_id)
|
||||
|
||||
if (status && q && !q.hasClass(status.identifier)){
|
||||
q.addClass(status.identifier);
|
||||
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label)
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Object.each(self.options.actions, function(action, key){
|
||||
self.action[key.toLowerCase()] = action = new self.options.actions[key](self)
|
||||
@@ -199,9 +201,6 @@ var Movie = new Class({
|
||||
self.actions.adopt(action)
|
||||
});
|
||||
|
||||
if(!self.data.library.rating)
|
||||
self.rating.hide();
|
||||
|
||||
},
|
||||
|
||||
addQuality: function(quality_id){
|
||||
@@ -244,10 +243,10 @@ var Movie = new Class({
|
||||
|
||||
if(direction == 'in'){
|
||||
self.temp_view = self.view;
|
||||
self.changeView('details')
|
||||
self.changeView('details');
|
||||
|
||||
self.el.addEvent('outerClick', function(){
|
||||
self.removeView()
|
||||
self.removeView();
|
||||
self.slide('out')
|
||||
})
|
||||
el.show();
|
||||
@@ -44,9 +44,8 @@
|
||||
.search_form .input input {
|
||||
border-radius: 0;
|
||||
display: block;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: rgba(255,255,255,.08);
|
||||
background: none;
|
||||
color: #FFF;
|
||||
font-size: 25px;
|
||||
height: 100%;
|
||||
@@ -60,6 +59,11 @@
|
||||
.search_form.shown .input input {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.search_form input::-ms-clear {
|
||||
width : 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.search_form .input input {
|
||||
@@ -106,7 +110,7 @@
|
||||
background: #5c697b;
|
||||
margin: 4px 0 0;
|
||||
width: 470px;
|
||||
min-height: 140px;
|
||||
min-height: 50px;
|
||||
box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55);
|
||||
display: none;
|
||||
}
|
||||
@@ -160,13 +164,15 @@
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.movie_result .options select[name=title] { width: 180px; }
|
||||
.movie_result .options select[name=title] { width: 170px; }
|
||||
.movie_result .options select[name=profile] { width: 90px; }
|
||||
.movie_result .options select[name=category] { width: 80px; }
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
|
||||
.movie_result .options select[name=title] { width: 90px; }
|
||||
.movie_result .options select[name=profile] { width: 60px; }
|
||||
.movie_result .options select[name=profile] { width: 50px; }
|
||||
.movie_result .options select[name=category] { width: 50px; }
|
||||
|
||||
}
|
||||
|
||||
@@ -194,7 +200,7 @@
|
||||
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
||||
}
|
||||
.movie_result .data.open {
|
||||
left: 100%;
|
||||
left: 100% !important;
|
||||
}
|
||||
|
||||
.movie_result:last-child .data { border-bottom: 0; }
|
||||
@@ -218,26 +224,51 @@
|
||||
position: absolute;
|
||||
top: 20%;
|
||||
left: 15px;
|
||||
right: 60px;
|
||||
right: 7px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
.movie_result .info h2 {
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
font-size: 20px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search_form .info h2 {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.movie_result .info h2 .title {
|
||||
display: block;
|
||||
margin: 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.movie_result .info h2 span {
|
||||
padding: 0 5px;
|
||||
position: absolute;
|
||||
right: -60px;
|
||||
}
|
||||
|
||||
.search_form .info h2 .title {
|
||||
position: absolute;
|
||||
width: 88%;
|
||||
}
|
||||
|
||||
.movie_result .info h2 .year {
|
||||
padding: 0 5px;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
width: 12%;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
|
||||
.search_form .info h2 .year {
|
||||
font-size: 12px;
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.search_form .mask,
|
||||
.movie_result .mask {
|
||||
@@ -98,6 +98,9 @@ Block.Search = new Class({
|
||||
self.el[self.q() ? 'addClass' : 'removeClass']('filled')
|
||||
|
||||
if(self.q() != self.last_q){
|
||||
if(self.api_request && self.api_request.isRunning())
|
||||
self.api_request.cancel();
|
||||
|
||||
if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
|
||||
self.autocomplete_timer = self.autocomplete.delay(300, self)
|
||||
}
|
||||
@@ -116,12 +119,9 @@ Block.Search = new Class({
|
||||
},
|
||||
|
||||
list: function(){
|
||||
var self = this;
|
||||
|
||||
if(self.api_request && self.api_request.running) return
|
||||
|
||||
var q = self.q();
|
||||
var cache = self.cache[q];
|
||||
var self = this,
|
||||
q = self.q(),
|
||||
cache = self.cache[q];
|
||||
|
||||
self.hideResults(false);
|
||||
|
||||
@@ -164,9 +164,6 @@ Block.Search = new Class({
|
||||
|
||||
});
|
||||
|
||||
if(q != self.q())
|
||||
self.list()
|
||||
|
||||
// Calculate result heights
|
||||
var w = window.getSize(),
|
||||
rc = self.result_container.getCoordinates();
|
||||
@@ -188,8 +185,11 @@ Block.Search = new Class({
|
||||
|
||||
Block.Search.Item = new Class({
|
||||
|
||||
Implements: [Options, Events],
|
||||
|
||||
initialize: function(info, options){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.info = info;
|
||||
self.alternative_titles = [];
|
||||
@@ -211,18 +211,15 @@ Block.Search.Item = new Class({
|
||||
}) : null,
|
||||
self.options_el = new Element('div.options.inlay'),
|
||||
self.data_container = new Element('div.data', {
|
||||
'tween': {
|
||||
duration: 400,
|
||||
transition: 'quint:in:out'
|
||||
},
|
||||
'events': {
|
||||
'click': self.showOptions.bind(self)
|
||||
}
|
||||
}).adopt(
|
||||
new Element('div.info').adopt(
|
||||
self.title = new Element('h2', {
|
||||
'text': info.titles[0]
|
||||
}).adopt(
|
||||
self.info_container = new Element('div.info').adopt(
|
||||
new Element('h2').adopt(
|
||||
self.title = new Element('span.title', {
|
||||
'text': info.titles && info.titles.length > 0 ? info.titles[0] : 'Unknown'
|
||||
}),
|
||||
self.year = info.year ? new Element('span.year', {
|
||||
'text': info.year
|
||||
}) : null
|
||||
@@ -231,12 +228,12 @@ Block.Search.Item = new Class({
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
info.titles.each(function(title){
|
||||
self.alternativeTitle({
|
||||
'title': title
|
||||
});
|
||||
})
|
||||
if(info.titles)
|
||||
info.titles.each(function(title){
|
||||
self.alternativeTitle({
|
||||
'title': title
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
alternativeTitle: function(alternative){
|
||||
@@ -245,6 +242,20 @@ Block.Search.Item = new Class({
|
||||
self.alternative_titles.include(alternative);
|
||||
},
|
||||
|
||||
getTitle: function(){
|
||||
var self = this;
|
||||
try {
|
||||
return self.info.original_title ? self.info.original_title : self.info.titles[0];
|
||||
}
|
||||
catch(e){
|
||||
return 'Unknown';
|
||||
}
|
||||
},
|
||||
|
||||
get: function(key){
|
||||
return this.info[key]
|
||||
},
|
||||
|
||||
showOptions: function(){
|
||||
var self = this;
|
||||
|
||||
@@ -264,7 +275,9 @@ Block.Search.Item = new Class({
|
||||
|
||||
add: function(e){
|
||||
var self = this;
|
||||
(e).preventDefault();
|
||||
|
||||
if(e)
|
||||
(e).preventDefault();
|
||||
|
||||
self.loadingMask();
|
||||
|
||||
@@ -272,7 +285,8 @@ Block.Search.Item = new Class({
|
||||
'data': {
|
||||
'identifier': self.info.imdb,
|
||||
'title': self.title_select.get('value'),
|
||||
'profile_id': self.profile_select.get('value')
|
||||
'profile_id': self.profile_select.get('value'),
|
||||
'category_id': self.category_select.get('value')
|
||||
},
|
||||
'onComplete': function(json){
|
||||
self.options_el.empty();
|
||||
@@ -282,6 +296,8 @@ Block.Search.Item = new Class({
|
||||
})
|
||||
);
|
||||
self.mask.fade('out');
|
||||
|
||||
self.fireEvent('added');
|
||||
},
|
||||
'onFailure': function(){
|
||||
self.options_el.empty();
|
||||
@@ -310,10 +326,10 @@ Block.Search.Item = new Class({
|
||||
|
||||
self.options_el.grab(
|
||||
new Element('div', {
|
||||
'class': self.info.in_wanted && self.info.in_wanted.profile || in_library ? 'in_library_wanted' : ''
|
||||
'class': self.info.in_wanted && self.info.in_wanted.profile_id || in_library ? 'in_library_wanted' : ''
|
||||
}).adopt(
|
||||
self.info.in_wanted && self.info.in_wanted.profile ? new Element('span.in_wanted', {
|
||||
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
|
||||
self.info.in_wanted && self.info.in_wanted.profile_id ? new Element('span.in_wanted', {
|
||||
'text': 'Already in wanted list: ' + Quality.getProfile(self.info.in_wanted.profile_id).get('label')
|
||||
}) : (in_library ? new Element('span.in_library', {
|
||||
'text': 'Already in library: ' + in_library.join(', ')
|
||||
}) : null),
|
||||
@@ -323,7 +339,12 @@ Block.Search.Item = new Class({
|
||||
self.profile_select = new Element('select', {
|
||||
'name': 'profile'
|
||||
}),
|
||||
new Element('a.button', {
|
||||
self.category_select = new Element('select', {
|
||||
'name': 'category'
|
||||
}).grab(
|
||||
new Element('option', {'value': -1, 'text': 'None'})
|
||||
),
|
||||
self.add_button = new Element('a.button', {
|
||||
'text': 'Add',
|
||||
'events': {
|
||||
'click': self.add.bind(self)
|
||||
@@ -338,7 +359,28 @@ Block.Search.Item = new Class({
|
||||
}).inject(self.title_select)
|
||||
})
|
||||
|
||||
Quality.getActiveProfiles().each(function(profile){
|
||||
|
||||
// Fill categories
|
||||
var categories = CategoryList.getAll();
|
||||
|
||||
if(categories.length == 0)
|
||||
self.category_select.hide();
|
||||
else {
|
||||
self.category_select.show();
|
||||
categories.each(function(category){
|
||||
new Element('option', {
|
||||
'value': category.data.id,
|
||||
'text': category.data.label
|
||||
}).inject(self.category_select);
|
||||
});
|
||||
}
|
||||
|
||||
// Fill profiles
|
||||
var profiles = Quality.getActiveProfiles();
|
||||
if(profiles.length == 1)
|
||||
self.profile_select.hide();
|
||||
|
||||
profiles.each(function(profile){
|
||||
new Element('option', {
|
||||
'value': profile.id ? profile.id : profile.data.id,
|
||||
'text': profile.label ? profile.label : profile.data.label
|
||||
@@ -346,6 +388,11 @@ Block.Search.Item = new Class({
|
||||
});
|
||||
|
||||
self.options_el.addClass('set');
|
||||
|
||||
if(categories.length == 0 && self.title_select.getElements('option').length == 1 && profiles.length == 1 &&
|
||||
!(self.info.in_wanted && self.info.in_wanted.profile_id || in_library))
|
||||
self.add();
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
6
couchpotato/core/media/movie/library/movie/__init__.py
Normal file
6
couchpotato/core/media/movie/library/movie/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .main import MovieLibraryPlugin
|
||||
|
||||
def start():
|
||||
return MovieLibraryPlugin()
|
||||
|
||||
config = []
|
||||
@@ -1,9 +1,8 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||
from couchpotato.core.helpers.variable import mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.media._base.library import LibraryBase
|
||||
from couchpotato.core.settings.model import Library, LibraryTitle, File
|
||||
from string import ascii_letters
|
||||
import time
|
||||
@@ -11,17 +10,20 @@ import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
class LibraryPlugin(Plugin):
|
||||
|
||||
class MovieLibraryPlugin(LibraryBase):
|
||||
|
||||
default_dict = {'titles': {}, 'files':{}}
|
||||
|
||||
def __init__(self):
|
||||
addEvent('library.add', self.add)
|
||||
addEvent('library.update', self.update)
|
||||
addEvent('library.update_release_date', self.updateReleaseDate)
|
||||
addEvent('library.add.movie', self.add)
|
||||
addEvent('library.update.movie', self.update)
|
||||
addEvent('library.update.movie.release_date', self.updateReleaseDate)
|
||||
|
||||
def add(self, attrs = None, update_after = True):
|
||||
if not attrs: attrs = {}
|
||||
|
||||
def add(self, attrs = {}, update_after = True):
|
||||
primary_provider = attrs.get('primary_provider', 'imdb')
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -33,7 +35,8 @@ class LibraryPlugin(Plugin):
|
||||
identifier = attrs.get('identifier'),
|
||||
plot = toUnicode(attrs.get('plot')),
|
||||
tagline = toUnicode(attrs.get('tagline')),
|
||||
status_id = status.get('id')
|
||||
status_id = status.get('id'),
|
||||
info = {}
|
||||
)
|
||||
|
||||
title = LibraryTitle(
|
||||
@@ -49,37 +52,39 @@ class LibraryPlugin(Plugin):
|
||||
# Update library info
|
||||
if update_after is not False:
|
||||
handle = fireEventAsync if update_after is 'async' else fireEvent
|
||||
handle('library.update', identifier = l.identifier, default_title = toUnicode(attrs.get('title', '')))
|
||||
handle('library.update.movie', identifier = l.identifier, default_title = toUnicode(attrs.get('title', '')))
|
||||
|
||||
library_dict = l.to_dict(self.default_dict)
|
||||
|
||||
db.expire_all()
|
||||
return library_dict
|
||||
|
||||
def update(self, identifier, default_title = '', force = False):
|
||||
|
||||
if self.shuttingDown():
|
||||
return
|
||||
|
||||
db = get_session()
|
||||
library = db.query(Library).filter_by(identifier = identifier).first()
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
|
||||
library_dict = None
|
||||
if library:
|
||||
library_dict = library.to_dict(self.default_dict)
|
||||
|
||||
do_update = True
|
||||
|
||||
if library.status_id == done_status.get('id') and not force:
|
||||
do_update = False
|
||||
else:
|
||||
info = fireEvent('movie.info', merge = True, identifier = identifier)
|
||||
info = fireEvent('movie.info', merge = True, identifier = identifier)
|
||||
|
||||
# Don't need those here
|
||||
try: del info['in_wanted']
|
||||
except: pass
|
||||
try: del info['in_library']
|
||||
except: pass
|
||||
# Don't need those here
|
||||
try: del info['in_wanted']
|
||||
except: pass
|
||||
try: del info['in_library']
|
||||
except: pass
|
||||
|
||||
if not info or len(info) == 0:
|
||||
log.error('Could not update, no movie info to work with: %s', identifier)
|
||||
return False
|
||||
if not info or len(info) == 0:
|
||||
log.error('Could not update, no movie info to work with: %s', identifier)
|
||||
return False
|
||||
|
||||
# Main info
|
||||
if do_update:
|
||||
@@ -87,7 +92,7 @@ class LibraryPlugin(Plugin):
|
||||
library.tagline = toUnicode(info.get('tagline', ''))
|
||||
library.year = info.get('year', 0)
|
||||
library.status_id = done_status.get('id')
|
||||
library.info = info
|
||||
library.info.update(info)
|
||||
db.commit()
|
||||
|
||||
# Titles
|
||||
@@ -132,6 +137,7 @@ class LibraryPlugin(Plugin):
|
||||
|
||||
library_dict = library.to_dict(self.default_dict)
|
||||
|
||||
db.expire_all()
|
||||
return library_dict
|
||||
|
||||
def updateReleaseDate(self, identifier):
|
||||
@@ -147,9 +153,10 @@ class LibraryPlugin(Plugin):
|
||||
|
||||
if dates and dates.get('expires', 0) < time.time() or not dates:
|
||||
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
|
||||
library.info = mergeDicts(library.info, {'release_date': dates })
|
||||
library.info.update({'release_date': dates })
|
||||
db.commit()
|
||||
|
||||
db.expire_all()
|
||||
return dates
|
||||
|
||||
|
||||
73
couchpotato/core/media/movie/searcher/__init__.py
Normal file
73
couchpotato/core/media/movie/searcher/__init__.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from .main import MovieSearcher
|
||||
import random
|
||||
|
||||
def start():
|
||||
return MovieSearcher()
|
||||
|
||||
config = [{
|
||||
'name': 'moviesearcher',
|
||||
'order': 20,
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'name': 'movie_searcher',
|
||||
'label': 'Movie search',
|
||||
'description': 'Search options for movies',
|
||||
'advanced': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'always_search',
|
||||
'default': False,
|
||||
'migrate_from': 'searcher',
|
||||
'type': 'bool',
|
||||
'label': 'Always search',
|
||||
'description': 'Search for movies even before there is a ETA. Enabling this will probably get you a lot of fakes.',
|
||||
},
|
||||
{
|
||||
'name': 'run_on_launch',
|
||||
'migrate_from': 'searcher',
|
||||
'label': 'Run on launch',
|
||||
'advanced': True,
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'description': 'Force run the searcher after (re)start.',
|
||||
},
|
||||
{
|
||||
'name': 'search_on_add',
|
||||
'label': 'Search after add',
|
||||
'advanced': True,
|
||||
'default': 1,
|
||||
'type': 'bool',
|
||||
'description': 'Disable this to only search for movies on cron.',
|
||||
},
|
||||
{
|
||||
'name': 'cron_day',
|
||||
'migrate_from': 'searcher',
|
||||
'label': 'Day',
|
||||
'advanced': True,
|
||||
'default': '*',
|
||||
'type': 'string',
|
||||
'description': '<strong>*</strong>: Every day, <strong>*/2</strong>: Every 2 days, <strong>1</strong>: Every first of the month. See <a href="http://packages.python.org/APScheduler/cronschedule.html">APScheduler</a> for details.',
|
||||
},
|
||||
{
|
||||
'name': 'cron_hour',
|
||||
'migrate_from': 'searcher',
|
||||
'label': 'Hour',
|
||||
'advanced': True,
|
||||
'default': random.randint(0, 23),
|
||||
'type': 'string',
|
||||
'description': '<strong>*</strong>: Every hour, <strong>*/8</strong>: Every 8 hours, <strong>3</strong>: At 3, midnight.',
|
||||
},
|
||||
{
|
||||
'name': 'cron_minute',
|
||||
'migrate_from': 'searcher',
|
||||
'label': 'Minute',
|
||||
'advanced': True,
|
||||
'default': random.randint(0, 59),
|
||||
'type': 'string',
|
||||
'description': "Just keep it random, so the providers don't get DDOSed by every CP user on a 'full' hour."
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
438
couchpotato/core/media/movie/searcher/main.py
Normal file
438
couchpotato/core/media/movie/searcher/main.py
Normal file
@@ -0,0 +1,438 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode, ss
|
||||
from couchpotato.core.helpers.variable import md5, getTitle, splitString, \
|
||||
possibleTitles, getImdb
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.media._base.searcher.base import SearcherBase
|
||||
from couchpotato.core.media.movie import MovieTypeBase
|
||||
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
||||
from couchpotato.environment import Env
|
||||
from datetime import date
|
||||
from sqlalchemy.exc import InterfaceError
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class MovieSearcher(SearcherBase, MovieTypeBase):
|
||||
|
||||
in_progress = False
|
||||
|
||||
def __init__(self):
|
||||
super(MovieSearcher, self).__init__()
|
||||
|
||||
addEvent('movie.searcher.all', self.searchAll)
|
||||
addEvent('movie.searcher.all_view', self.searchAllView)
|
||||
addEvent('movie.searcher.single', self.single)
|
||||
addEvent('movie.searcher.correct_movie', self.correctMovie)
|
||||
addEvent('movie.searcher.try_next_release', self.tryNextRelease)
|
||||
addEvent('movie.searcher.could_be_released', self.couldBeReleased)
|
||||
|
||||
addApiView('movie.searcher.try_next', self.tryNextReleaseView, docs = {
|
||||
'desc': 'Marks the snatched results as ignored and try the next best release',
|
||||
'params': {
|
||||
'id': {'desc': 'The id of the movie'},
|
||||
},
|
||||
})
|
||||
|
||||
addApiView('movie.searcher.full_search', self.searchAllView, docs = {
|
||||
'desc': 'Starts a full search for all wanted movies',
|
||||
})
|
||||
|
||||
addApiView('movie.searcher.progress', self.getProgress, docs = {
|
||||
'desc': 'Get the progress of current full search',
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'progress': False || object, total & to_go,
|
||||
}"""},
|
||||
})
|
||||
|
||||
if self.conf('run_on_launch'):
|
||||
addEvent('app.load', self.searchAll)
|
||||
|
||||
def searchAllView(self, **kwargs):
|
||||
|
||||
fireEventAsync('movie.searcher.all')
|
||||
|
||||
return {
|
||||
'success': not self.in_progress
|
||||
}
|
||||
|
||||
def searchAll(self):
|
||||
|
||||
if self.in_progress:
|
||||
log.info('Search already in progress')
|
||||
fireEvent('notify.frontend', type = 'movie.searcher.already_started', data = True, message = 'Full search already in progress')
|
||||
return
|
||||
|
||||
self.in_progress = True
|
||||
fireEvent('notify.frontend', type = 'movie.searcher.started', data = True, message = 'Full search started')
|
||||
|
||||
db = get_session()
|
||||
|
||||
movies = db.query(Movie).filter(
|
||||
Movie.status.has(identifier = 'active')
|
||||
).all()
|
||||
random.shuffle(movies)
|
||||
|
||||
self.in_progress = {
|
||||
'total': len(movies),
|
||||
'to_go': len(movies),
|
||||
}
|
||||
|
||||
try:
|
||||
search_protocols = fireEvent('searcher.protocols', single = True)
|
||||
|
||||
for movie in movies:
|
||||
movie_dict = movie.to_dict({
|
||||
'category': {},
|
||||
'profile': {'types': {'quality': {}}},
|
||||
'releases': {'status': {}, 'quality': {}},
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {},
|
||||
})
|
||||
|
||||
try:
|
||||
self.single(movie_dict, search_protocols)
|
||||
except IndexError:
|
||||
log.error('Forcing library update for %s, if you see this often, please report: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
|
||||
fireEvent('library.update.movie', movie_dict['library']['identifier'], force = True)
|
||||
except:
|
||||
log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
|
||||
|
||||
self.in_progress['to_go'] -= 1
|
||||
|
||||
# Break if CP wants to shut down
|
||||
if self.shuttingDown():
|
||||
break
|
||||
|
||||
except SearchSetupError:
|
||||
pass
|
||||
|
||||
self.in_progress = False
|
||||
|
||||
def single(self, movie, search_protocols = None, manual = False):
|
||||
|
||||
# Find out search type
|
||||
try:
|
||||
if not search_protocols:
|
||||
search_protocols = fireEvent('searcher.protocols', single = True)
|
||||
except SearchSetupError:
|
||||
return
|
||||
|
||||
done_status = fireEvent('status.get', 'done', single = True)
|
||||
|
||||
if not movie['profile'] or (movie['status_id'] == done_status.get('id') and not manual):
|
||||
log.debug('Movie doesn\'t have a profile or already done, assuming in manage tab.')
|
||||
return
|
||||
|
||||
db = get_session()
|
||||
|
||||
pre_releases = fireEvent('quality.pre_releases', single = True)
|
||||
release_dates = fireEvent('library.update.movie.release_date', identifier = movie['library']['identifier'], merge = True)
|
||||
available_status, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], single = True)
|
||||
|
||||
found_releases = []
|
||||
too_early_to_search = []
|
||||
|
||||
default_title = getTitle(movie['library'])
|
||||
if not default_title:
|
||||
log.error('No proper info found for movie, removing it from library to cause it from having more issues.')
|
||||
fireEvent('movie.delete', movie['id'], single = True)
|
||||
return
|
||||
|
||||
fireEvent('notify.frontend', type = 'movie.searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title)
|
||||
|
||||
|
||||
ret = False
|
||||
for quality_type in movie['profile']['types']:
|
||||
if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']):
|
||||
too_early_to_search.append(quality_type['quality']['identifier'])
|
||||
continue
|
||||
|
||||
has_better_quality = 0
|
||||
|
||||
# See if better quality is available
|
||||
for release in movie['releases']:
|
||||
if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id'), failed_status.get('id')]:
|
||||
has_better_quality += 1
|
||||
|
||||
# Don't search for quality lower then already available.
|
||||
if has_better_quality is 0:
|
||||
|
||||
log.info('Search for %s in %s', (default_title, quality_type['quality']['label']))
|
||||
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
|
||||
|
||||
results = []
|
||||
for search_protocol in search_protocols:
|
||||
protocol_results = fireEvent('provider.search.%s.movie' % search_protocol, movie, quality, merge = True)
|
||||
if protocol_results:
|
||||
results += protocol_results
|
||||
|
||||
sorted_results = sorted(results, key = lambda k: k['score'], reverse = True)
|
||||
if len(sorted_results) == 0:
|
||||
log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label']))
|
||||
|
||||
download_preference = self.conf('preferred_method', section = 'searcher')
|
||||
if download_preference != 'both':
|
||||
sorted_results = sorted(sorted_results, key = lambda k: k['protocol'][:3], reverse = (download_preference == 'torrent'))
|
||||
|
||||
# Check if movie isn't deleted while searching
|
||||
if not db.query(Movie).filter_by(id = movie.get('id')).first():
|
||||
break
|
||||
|
||||
# Add them to this movie releases list
|
||||
for nzb in sorted_results:
|
||||
|
||||
nzb_identifier = md5(nzb['url'])
|
||||
found_releases.append(nzb_identifier)
|
||||
|
||||
rls = db.query(Release).filter_by(identifier = nzb_identifier).first()
|
||||
if not rls:
|
||||
rls = Release(
|
||||
identifier = nzb_identifier,
|
||||
movie_id = movie.get('id'),
|
||||
quality_id = quality_type.get('quality_id'),
|
||||
status_id = available_status.get('id')
|
||||
)
|
||||
db.add(rls)
|
||||
else:
|
||||
[db.delete(old_info) for old_info in rls.info]
|
||||
rls.last_edit = int(time.time())
|
||||
|
||||
db.commit()
|
||||
|
||||
for info in nzb:
|
||||
try:
|
||||
if not isinstance(nzb[info], (str, unicode, int, long, float)):
|
||||
continue
|
||||
|
||||
rls_info = ReleaseInfo(
|
||||
identifier = info,
|
||||
value = toUnicode(nzb[info])
|
||||
)
|
||||
rls.info.append(rls_info)
|
||||
except InterfaceError:
|
||||
log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
|
||||
|
||||
db.commit()
|
||||
|
||||
nzb['status_id'] = rls.status_id
|
||||
|
||||
|
||||
for nzb in sorted_results:
|
||||
if not quality_type.get('finish', False) and quality_type.get('wait_for', 0) > 0 and nzb.get('age') <= quality_type.get('wait_for', 0):
|
||||
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
|
||||
continue
|
||||
|
||||
if nzb['status_id'] in [ignored_status.get('id'), failed_status.get('id')]:
|
||||
log.info('Ignored: %s', nzb['name'])
|
||||
continue
|
||||
|
||||
if nzb['score'] <= 0:
|
||||
log.info('Ignored, score to low: %s', nzb['name'])
|
||||
continue
|
||||
|
||||
downloaded = fireEvent('searcher.download', data = nzb, movie = movie, manual = manual, single = True)
|
||||
if downloaded is True:
|
||||
ret = True
|
||||
break
|
||||
elif downloaded != 'try_next':
|
||||
break
|
||||
|
||||
# Remove releases that aren't found anymore
|
||||
for release in movie.get('releases', []):
|
||||
if release.get('status_id') == available_status.get('id') and release.get('identifier') not in found_releases:
|
||||
fireEvent('release.delete', release.get('id'), single = True)
|
||||
|
||||
else:
|
||||
log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title))
|
||||
fireEvent('movie.restatus', movie['id'])
|
||||
break
|
||||
|
||||
# Break if CP wants to shut down
|
||||
if self.shuttingDown() or ret:
|
||||
break
|
||||
|
||||
if len(too_early_to_search) > 0:
|
||||
log.info2('Too early to search for %s, %s', (too_early_to_search, default_title))
|
||||
|
||||
fireEvent('notify.frontend', type = 'movie.searcher.ended.%s' % movie['id'], data = True)
|
||||
|
||||
return ret
|
||||
|
||||
def correctMovie(self, nzb = None, movie = None, quality = None, **kwargs):
|
||||
|
||||
imdb_results = kwargs.get('imdb_results', False)
|
||||
retention = Env.setting('retention', section = 'nzb')
|
||||
|
||||
if nzb.get('seeders') is None and 0 < retention < nzb.get('age', 0):
|
||||
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
|
||||
return False
|
||||
|
||||
movie_name = getTitle(movie['library'])
|
||||
movie_words = re.split('\W+', simplifyString(movie_name))
|
||||
nzb_name = simplifyString(nzb['name'])
|
||||
nzb_words = re.split('\W+', nzb_name)
|
||||
|
||||
# Make sure it has required words
|
||||
required_words = splitString(self.conf('required_words', section = 'searcher').lower())
|
||||
try: required_words = list(set(required_words + splitString(movie['category']['required'].lower())))
|
||||
except: pass
|
||||
|
||||
req_match = 0
|
||||
for req_set in required_words:
|
||||
req = splitString(req_set, '&')
|
||||
req_match += len(list(set(nzb_words) & set(req))) == len(req)
|
||||
|
||||
if len(required_words) > 0 and req_match == 0:
|
||||
log.info2('Wrong: Required word missing: %s', nzb['name'])
|
||||
return False
|
||||
|
||||
# Ignore releases
|
||||
ignored_words = splitString(self.conf('ignored_words', section = 'searcher').lower())
|
||||
try: ignored_words = list(set(ignored_words + splitString(movie['category']['ignored'].lower())))
|
||||
except: pass
|
||||
|
||||
ignored_match = 0
|
||||
for ignored_set in ignored_words:
|
||||
ignored = splitString(ignored_set, '&')
|
||||
ignored_match += len(list(set(nzb_words) & set(ignored))) == len(ignored)
|
||||
|
||||
if len(ignored_words) > 0 and ignored_match:
|
||||
log.info2("Wrong: '%s' contains 'ignored words'", (nzb['name']))
|
||||
return False
|
||||
|
||||
# Ignore porn stuff
|
||||
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic', 'cock', 'dick']
|
||||
pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words))
|
||||
if pron_words:
|
||||
log.info('Wrong: %s, probably pr0n', (nzb['name']))
|
||||
return False
|
||||
|
||||
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
|
||||
|
||||
# Contains lower quality string
|
||||
if fireEvent('searcher.contains_other_quality', nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality, single = True):
|
||||
log.info2('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
|
||||
return False
|
||||
|
||||
|
||||
# File to small
|
||||
if nzb['size'] and preferred_quality['size_min'] > nzb['size']:
|
||||
log.info2('Wrong: "%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min']))
|
||||
return False
|
||||
|
||||
# File to large
|
||||
if nzb['size'] and preferred_quality.get('size_max') < nzb['size']:
|
||||
log.info2('Wrong: "%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max']))
|
||||
return False
|
||||
|
||||
|
||||
# Provider specific functions
|
||||
get_more = nzb.get('get_more_info')
|
||||
if get_more:
|
||||
get_more(nzb)
|
||||
|
||||
extra_check = nzb.get('extra_check')
|
||||
if extra_check and not extra_check(nzb):
|
||||
return False
|
||||
|
||||
|
||||
if imdb_results:
|
||||
return True
|
||||
|
||||
# Check if nzb contains imdb link
|
||||
if getImdb(nzb.get('description', '')) == movie['library']['identifier']:
|
||||
return True
|
||||
|
||||
for raw_title in movie['library']['titles']:
|
||||
for movie_title in possibleTitles(raw_title['title']):
|
||||
movie_words = re.split('\W+', simplifyString(movie_title))
|
||||
|
||||
if fireEvent('searcher.correct_name', nzb['name'], movie_title, single = True):
|
||||
# if no IMDB link, at least check year range 1
|
||||
if len(movie_words) > 2 and fireEvent('searcher.correct_year', nzb['name'], movie['library']['year'], 1, single = True):
|
||||
return True
|
||||
|
||||
# if no IMDB link, at least check year
|
||||
if len(movie_words) <= 2 and fireEvent('searcher.correct_year', nzb['name'], movie['library']['year'], 0, single = True):
|
||||
return True
|
||||
|
||||
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'", (nzb['name'], movie_name, movie['library']['year']))
|
||||
return False
|
||||
|
||||
def couldBeReleased(self, is_pre_release, dates, year = None):
|
||||
|
||||
now = int(time.time())
|
||||
now_year = date.today().year
|
||||
|
||||
if (year is None or year < now_year - 1) and (not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0)):
|
||||
return True
|
||||
else:
|
||||
|
||||
# For movies before 1972
|
||||
if not dates or dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
|
||||
return True
|
||||
|
||||
if is_pre_release:
|
||||
# Prerelease 1 week before theaters
|
||||
if dates.get('theater') - 604800 < now:
|
||||
return True
|
||||
else:
|
||||
# 12 weeks after theater release
|
||||
if dates.get('theater') > 0 and dates.get('theater') + 7257600 < now:
|
||||
return True
|
||||
|
||||
if dates.get('dvd') > 0:
|
||||
|
||||
# 4 weeks before dvd release
|
||||
if dates.get('dvd') - 2419200 < now:
|
||||
return True
|
||||
|
||||
# Dvd should be released
|
||||
if dates.get('dvd') < now:
|
||||
return True
|
||||
|
||||
|
||||
return False
|
||||
|
||||
def tryNextReleaseView(self, id = None, **kwargs):
|
||||
|
||||
trynext = self.tryNextRelease(id, manual = True)
|
||||
|
||||
return {
|
||||
'success': trynext
|
||||
}
|
||||
|
||||
def tryNextRelease(self, movie_id, manual = False):
|
||||
|
||||
snatched_status, done_status, ignored_status = fireEvent('status.get', ['snatched', 'done', 'ignored'], single = True)
|
||||
|
||||
try:
|
||||
db = get_session()
|
||||
rels = db.query(Release) \
|
||||
.filter_by(movie_id = movie_id) \
|
||||
.filter(Release.status_id.in_([snatched_status.get('id'), done_status.get('id')])) \
|
||||
.all()
|
||||
|
||||
for rel in rels:
|
||||
rel.status_id = ignored_status.get('id')
|
||||
db.commit()
|
||||
|
||||
movie_dict = fireEvent('movie.get', movie_id, single = True)
|
||||
log.info('Trying next release for: %s', getTitle(movie_dict['library']))
|
||||
fireEvent('movie.searcher.single', movie_dict, manual = manual)
|
||||
|
||||
return True
|
||||
|
||||
except:
|
||||
log.error('Failed searching for next release: %s', traceback.format_exc())
|
||||
return False
|
||||
|
||||
class SearchSetupError(Exception):
|
||||
pass
|
||||
17
couchpotato/core/migration/versions/002_Movie_category.py
Normal file
17
couchpotato/core/migration/versions/002_Movie_category.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from migrate.changeset.schema import create_column
|
||||
from sqlalchemy.schema import MetaData, Column, Table, Index
|
||||
from sqlalchemy.types import Integer
|
||||
|
||||
meta = MetaData()
|
||||
|
||||
|
||||
def upgrade(migrate_engine):
|
||||
meta.bind = migrate_engine
|
||||
|
||||
category_column = Column('category_id', Integer)
|
||||
movie = Table('movie', meta, category_column)
|
||||
create_column(category_column, movie)
|
||||
Index('ix_movie_category_id', movie.c.category_id).create()
|
||||
|
||||
def downgrade(migrate_engine):
|
||||
pass
|
||||
@@ -1,6 +1,5 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.base import Provider
|
||||
from couchpotato.environment import Env
|
||||
@@ -33,7 +32,9 @@ class Notification(Provider):
|
||||
addEvent(listener, self.createNotifyHandler(listener))
|
||||
|
||||
def createNotifyHandler(self, listener):
|
||||
def notify(message = None, group = {}, data = None):
|
||||
def notify(message = None, group = None, data = None):
|
||||
if not group: group = {}
|
||||
|
||||
if not self.conf('on_snatch', default = True) and listener == 'movie.snatched':
|
||||
return
|
||||
return self._notify(message = message, data = data if data else group, listener = listener)
|
||||
@@ -46,11 +47,12 @@ class Notification(Provider):
|
||||
def _notify(self, *args, **kwargs):
|
||||
if self.isEnabled():
|
||||
return self.notify(*args, **kwargs)
|
||||
return False
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
pass
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
def test(self):
|
||||
def test(self, **kwargs):
|
||||
|
||||
test_type = self.testNotifyName()
|
||||
|
||||
@@ -62,7 +64,9 @@ class Notification(Provider):
|
||||
listener = 'test'
|
||||
)
|
||||
|
||||
return jsonified({'success': success})
|
||||
return {
|
||||
'success': success
|
||||
}
|
||||
|
||||
def testNotifyName(self):
|
||||
return 'notify.%s.test' % self.getName().lower()
|
||||
|
||||
@@ -10,7 +10,8 @@ class Boxcar(Notification):
|
||||
|
||||
url = 'https://boxcar.io/devices/providers/7MNNXY3UIzVBwvzkKwkC/notifications'
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
try:
|
||||
message = message.strip()
|
||||
|
||||
@@ -2,15 +2,16 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView, addNonBlockApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from couchpotato.core.settings.model import Notification as Notif
|
||||
from couchpotato.environment import Env
|
||||
from operator import itemgetter
|
||||
from sqlalchemy.sql.expression import or_
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
import uuid
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -18,9 +19,7 @@ log = CPLog(__name__)
|
||||
|
||||
class CoreNotifier(Notification):
|
||||
|
||||
m_lock = threading.Lock()
|
||||
messages = []
|
||||
listeners = []
|
||||
m_lock = None
|
||||
|
||||
def __init__(self):
|
||||
super(CoreNotifier, self).__init__()
|
||||
@@ -51,10 +50,15 @@ class CoreNotifier(Notification):
|
||||
addApiView('notification.listener', self.listener)
|
||||
|
||||
fireEvent('schedule.interval', 'core.check_messages', self.checkMessages, hours = 12, single = True)
|
||||
fireEvent('schedule.interval', 'core.clean_messages', self.cleanMessages, seconds = 15, single = True)
|
||||
|
||||
addEvent('app.load', self.clean)
|
||||
addEvent('app.load', self.checkMessages)
|
||||
|
||||
self.messages = []
|
||||
self.listeners = []
|
||||
self.m_lock = threading.Lock()
|
||||
|
||||
def clean(self):
|
||||
|
||||
db = get_session()
|
||||
@@ -62,11 +66,9 @@ class CoreNotifier(Notification):
|
||||
db.commit()
|
||||
|
||||
|
||||
def markAsRead(self):
|
||||
def markAsRead(self, ids = None, **kwargs):
|
||||
|
||||
ids = None
|
||||
if getParam('ids'):
|
||||
ids = splitString(getParam('ids'))
|
||||
ids = splitString(ids) if ids else None
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -79,14 +81,13 @@ class CoreNotifier(Notification):
|
||||
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def listView(self):
|
||||
def listView(self, limit_offset = None, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
limit_offset = getParam('limit_offset', None)
|
||||
|
||||
q = db.query(Notif)
|
||||
|
||||
@@ -105,18 +106,18 @@ class CoreNotifier(Notification):
|
||||
ndict['type'] = 'notification'
|
||||
notifications.append(ndict)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(notifications) == 0,
|
||||
'notifications': notifications
|
||||
})
|
||||
}
|
||||
|
||||
def checkMessages(self):
|
||||
|
||||
prop_name = 'messages.last_check'
|
||||
last_check = tryInt(Env.prop(prop_name, default = 0))
|
||||
|
||||
messages = fireEvent('cp.messages', last_check = last_check, single = True)
|
||||
messages = fireEvent('cp.messages', last_check = last_check, single = True) or []
|
||||
|
||||
for message in messages:
|
||||
if message.get('time') > last_check:
|
||||
@@ -127,7 +128,8 @@ class CoreNotifier(Notification):
|
||||
|
||||
Env.prop(prop_name, value = last_check)
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -148,7 +150,10 @@ class CoreNotifier(Notification):
|
||||
|
||||
return True
|
||||
|
||||
def frontend(self, type = 'notification', data = {}, message = None):
|
||||
def frontend(self, type = 'notification', data = None, message = None):
|
||||
if not data: data = {}
|
||||
|
||||
log.debug('Notifying frontend')
|
||||
|
||||
self.m_lock.acquire()
|
||||
notification = {
|
||||
@@ -168,10 +173,12 @@ class CoreNotifier(Notification):
|
||||
'result': [notification],
|
||||
})
|
||||
except:
|
||||
break
|
||||
log.debug('Failed sending to listener: %s', traceback.format_exc())
|
||||
|
||||
self.listeners = []
|
||||
self.m_lock.release()
|
||||
self.cleanMessages()
|
||||
|
||||
log.debug('Done notifying frontend')
|
||||
|
||||
def addListener(self, callback, last_id = None):
|
||||
|
||||
@@ -183,59 +190,72 @@ class CoreNotifier(Notification):
|
||||
'result': messages,
|
||||
})
|
||||
|
||||
self.m_lock.acquire()
|
||||
self.listeners.append((callback, last_id))
|
||||
self.m_lock.release()
|
||||
|
||||
|
||||
def removeListener(self, callback):
|
||||
|
||||
self.m_lock.acquire()
|
||||
for list_tuple in self.listeners:
|
||||
try:
|
||||
listener, last_id = list_tuple
|
||||
if listener == callback:
|
||||
self.listeners.remove(list_tuple)
|
||||
except:
|
||||
pass
|
||||
|
||||
def cleanMessages(self):
|
||||
self.m_lock.acquire()
|
||||
|
||||
for message in self.messages:
|
||||
if message['time'] < (time.time() - 15):
|
||||
self.messages.remove(message)
|
||||
|
||||
log.debug('Failed removing listener: %s', traceback.format_exc())
|
||||
self.m_lock.release()
|
||||
|
||||
def cleanMessages(self):
|
||||
|
||||
if len(self.messages) == 0:
|
||||
return
|
||||
|
||||
log.debug('Cleaning messages')
|
||||
self.m_lock.acquire()
|
||||
|
||||
time_ago = (time.time() - 15)
|
||||
self.messages[:] = [m for m in self.messages if (m['time'] > time_ago)]
|
||||
|
||||
self.m_lock.release()
|
||||
log.debug('Done cleaning messages')
|
||||
|
||||
def getMessages(self, last_id):
|
||||
|
||||
log.debug('Getting messages with id: %s', last_id)
|
||||
self.m_lock.acquire()
|
||||
|
||||
recent = []
|
||||
index = 0
|
||||
for i in xrange(len(self.messages)):
|
||||
index = len(self.messages) - i - 1
|
||||
if self.messages[index]["message_id"] == last_id: break
|
||||
recent = self.messages[index:]
|
||||
try:
|
||||
index = map(itemgetter('message_id'), self.messages).index(last_id)
|
||||
recent = self.messages[index + 1:]
|
||||
except:
|
||||
pass
|
||||
|
||||
self.m_lock.release()
|
||||
log.debug('Returning for %s %s messages', (last_id, len(recent)))
|
||||
|
||||
return recent or []
|
||||
return recent
|
||||
|
||||
def listener(self):
|
||||
def listener(self, init = False, **kwargs):
|
||||
|
||||
messages = []
|
||||
|
||||
# Get unread
|
||||
if getParam('init'):
|
||||
if init:
|
||||
db = get_session()
|
||||
|
||||
notifications = db.query(Notif) \
|
||||
.filter(or_(Notif.read == False, Notif.added > (time.time() - 259200))) \
|
||||
.all()
|
||||
|
||||
for n in notifications:
|
||||
ndict = n.to_dict()
|
||||
ndict['type'] = 'notification'
|
||||
messages.append(ndict)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'result': messages,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ var NotificationBase = new Class({
|
||||
});
|
||||
|
||||
window.addEvent('load', function(){
|
||||
self.startInterval.delay($(window).getSize().x <= 480 ? 2000 : 300, self)
|
||||
});
|
||||
self.startInterval.delay($(window).getSize().x <= 480 ? 2000 : 100, self);
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
@@ -157,7 +157,7 @@ var NotificationBase = new Class({
|
||||
}
|
||||
|
||||
// Restart poll
|
||||
self.startPoll()
|
||||
self.startPoll.delay(1500, self);
|
||||
},
|
||||
|
||||
showMessage: function(message, sticky, data){
|
||||
|
||||
@@ -11,7 +11,8 @@ log = CPLog(__name__)
|
||||
|
||||
class Email(Notification):
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
# Extract all the settings from settings
|
||||
from_address = self.conf('from')
|
||||
|
||||
@@ -43,7 +43,8 @@ class Growl(Notification):
|
||||
else:
|
||||
log.error('Failed register of growl: %s', traceback.format_exc())
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
self.register()
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.request import getParams, jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import re
|
||||
@@ -22,21 +21,17 @@ class NMJ(Notification):
|
||||
addApiView(self.testNotifyName(), self.test)
|
||||
addApiView('notify.nmj.auto_config', self.autoConfig)
|
||||
|
||||
def autoConfig(self):
|
||||
def autoConfig(self, host = 'localhost', **kwargs):
|
||||
|
||||
params = getParams()
|
||||
host = params.get('host', 'localhost')
|
||||
|
||||
database = ''
|
||||
mount = ''
|
||||
|
||||
try:
|
||||
terminal = telnetlib.Telnet(host)
|
||||
except Exception:
|
||||
log.error('Warning: unable to get a telnet session to %s', (host))
|
||||
log.error('Warning: unable to get a telnet session to %s', host)
|
||||
return self.failed()
|
||||
|
||||
log.debug('Connected to %s via telnet', (host))
|
||||
log.debug('Connected to %s via telnet', host)
|
||||
terminal.read_until('sh-3.00# ')
|
||||
terminal.write('cat /tmp/source\n')
|
||||
terminal.write('cat /tmp/netshare\n')
|
||||
@@ -50,7 +45,7 @@ class NMJ(Notification):
|
||||
device = match.group(2)
|
||||
log.info('Found NMJ database %s on device %s', (database, device))
|
||||
else:
|
||||
log.error('Could not get current NMJ database on %s, NMJ is probably not running!', (host))
|
||||
log.error('Could not get current NMJ database on %s, NMJ is probably not running!', host)
|
||||
return self.failed()
|
||||
|
||||
if device.startswith('NETWORK_SHARE/'):
|
||||
@@ -58,28 +53,29 @@ class NMJ(Notification):
|
||||
|
||||
if match:
|
||||
mount = match.group().replace('127.0.0.1', host)
|
||||
log.info('Found mounting url on the Popcorn Hour in configuration: %s', (mount))
|
||||
log.info('Found mounting url on the Popcorn Hour in configuration: %s', mount)
|
||||
else:
|
||||
log.error('Detected a network share on the Popcorn Hour, but could not get the mounting url')
|
||||
return self.failed()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'database': database,
|
||||
'mount': mount,
|
||||
})
|
||||
}
|
||||
|
||||
def addToLibrary(self, message = None, group = {}):
|
||||
def addToLibrary(self, message = None, group = None):
|
||||
if self.isDisabled(): return
|
||||
if not group: group = {}
|
||||
|
||||
host = self.conf('host')
|
||||
mount = self.conf('mount')
|
||||
database = self.conf('database')
|
||||
|
||||
if mount:
|
||||
log.debug('Try to mount network drive via url: %s', (mount))
|
||||
log.debug('Try to mount network drive via url: %s', mount)
|
||||
try:
|
||||
data = self.urlopen(mount)
|
||||
self.urlopen(mount)
|
||||
except:
|
||||
return False
|
||||
|
||||
@@ -102,20 +98,24 @@ class NMJ(Notification):
|
||||
et = etree.fromstring(response)
|
||||
result = et.findtext('returnValue')
|
||||
except SyntaxError, e:
|
||||
log.error('Unable to parse XML returned from the Popcorn Hour: %s', (e))
|
||||
log.error('Unable to parse XML returned from the Popcorn Hour: %s', e)
|
||||
return False
|
||||
|
||||
if int(result) > 0:
|
||||
log.error('Popcorn Hour returned an errorcode: %s', (result))
|
||||
log.error('Popcorn Hour returned an errorcode: %s', result)
|
||||
return False
|
||||
else:
|
||||
log.info('NMJ started background scan')
|
||||
return True
|
||||
|
||||
def failed(self):
|
||||
return jsonified({'success': False})
|
||||
return {
|
||||
'success': False
|
||||
}
|
||||
|
||||
def test(self):
|
||||
return jsonified({'success': self.addToLibrary()})
|
||||
def test(self, **kwargs):
|
||||
return {
|
||||
'success': self.addToLibrary()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from flask.helpers import json
|
||||
import base64
|
||||
import json
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -12,7 +12,8 @@ class Notifo(Notification):
|
||||
|
||||
url = 'https://api.notifo.com/v1/send_notification'
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
try:
|
||||
params = {
|
||||
|
||||
@@ -8,19 +8,17 @@ log = CPLog(__name__)
|
||||
|
||||
class NotifyMyAndroid(Notification):
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
nma = pynma.PyNMA()
|
||||
keys = splitString(self.conf('api_key'))
|
||||
nma.addkey(keys)
|
||||
nma.developerkey(self.conf('dev_key'))
|
||||
|
||||
# hacky fix for the event type
|
||||
# as it seems to be part of the message now
|
||||
self.event = message.split(' ')[0]
|
||||
response = nma.push(
|
||||
application = self.default_title,
|
||||
event = self.event,
|
||||
event = message.split(' ')[0],
|
||||
description = message,
|
||||
priority = self.conf('priority'),
|
||||
batch_mode = len(keys) > 1
|
||||
|
||||
@@ -22,6 +22,13 @@ config = [{
|
||||
'description': 'Default should be on localhost',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also send message when movie is snatched.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.helpers.variable import cleanHost, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from urllib2 import URLError
|
||||
from urlparse import urlparse
|
||||
from xml.dom import minidom
|
||||
import traceback
|
||||
|
||||
@@ -17,16 +17,17 @@ class Plex(Notification):
|
||||
super(Plex, self).__init__()
|
||||
addEvent('renamer.after', self.addToLibrary)
|
||||
|
||||
def addToLibrary(self, message = None, group = {}):
|
||||
def addToLibrary(self, message = None, group = None):
|
||||
if self.isDisabled(): return
|
||||
if not group: group = {}
|
||||
|
||||
log.info('Sending notification to Plex')
|
||||
hosts = [cleanHost(x.strip() + ':32400') for x in self.conf('host').split(",")]
|
||||
hosts = self.getHosts(port = 32400)
|
||||
|
||||
for host in hosts:
|
||||
|
||||
source_type = ['movie']
|
||||
base_url = '%slibrary/sections' % host
|
||||
base_url = '%s/library/sections' % host
|
||||
refresh_url = '%s/%%s/refresh' % base_url
|
||||
|
||||
try:
|
||||
@@ -37,7 +38,7 @@ class Plex(Notification):
|
||||
for s in sections:
|
||||
if s.getAttribute('type') in source_type:
|
||||
url = refresh_url % s.getAttribute('key')
|
||||
x = self.urlopen(url)
|
||||
self.urlopen(url)
|
||||
|
||||
except:
|
||||
log.error('Plex library update failed for %s, Media Server not running: %s', (host, traceback.format_exc(1)))
|
||||
@@ -45,9 +46,10 @@ class Plex(Notification):
|
||||
|
||||
return True
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
hosts = [x.strip() + ':3000' for x in self.conf('host').split(",")]
|
||||
hosts = self.getHosts(port = 3000)
|
||||
successful = 0
|
||||
for host in hosts:
|
||||
if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host):
|
||||
@@ -57,8 +59,7 @@ class Plex(Notification):
|
||||
|
||||
def send(self, command, host):
|
||||
|
||||
url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command))
|
||||
|
||||
url = '%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command))
|
||||
headers = {}
|
||||
|
||||
try:
|
||||
@@ -73,7 +74,7 @@ class Plex(Notification):
|
||||
log.info('Plex notification to %s successful.', host)
|
||||
return True
|
||||
|
||||
def test(self):
|
||||
def test(self, **kwargs):
|
||||
|
||||
test_type = self.testNotifyName()
|
||||
|
||||
@@ -86,4 +87,21 @@ class Plex(Notification):
|
||||
)
|
||||
success2 = self.addToLibrary()
|
||||
|
||||
return jsonified({'success': success or success2})
|
||||
return {
|
||||
'success': success or success2
|
||||
}
|
||||
|
||||
def getHosts(self, port = None):
|
||||
|
||||
raw_hosts = splitString(self.conf('host'))
|
||||
hosts = []
|
||||
|
||||
for h in raw_hosts:
|
||||
h = cleanHost(h)
|
||||
p = urlparse(h)
|
||||
h = h.rstrip('/')
|
||||
if port and not p.port:
|
||||
h += ':%s' % port
|
||||
hosts.append(h)
|
||||
|
||||
return hosts
|
||||
|
||||
@@ -12,7 +12,8 @@ class Prowl(Notification):
|
||||
'api': 'https://api.prowlapp.com/publicapi/add'
|
||||
}
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
data = {
|
||||
'apikey': self.conf('api_key'),
|
||||
|
||||
@@ -11,7 +11,8 @@ class Pushalot(Notification):
|
||||
'api': 'https://pushalot.com/api/sendmessage'
|
||||
}
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
data = {
|
||||
'AuthorizationToken': self.conf('auth_token'),
|
||||
|
||||
@@ -11,7 +11,8 @@ class Pushover(Notification):
|
||||
|
||||
app_token = 'YkxHMYDZp285L265L3IwH3LmzkTaCy'
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
http_handler = HTTPSConnection("api.pushover.net:443")
|
||||
|
||||
@@ -23,7 +24,7 @@ class Pushover(Notification):
|
||||
}
|
||||
|
||||
if data and data.get('library'):
|
||||
api_data.extend({
|
||||
api_data.update({
|
||||
'url': toUnicode('http://www.imdb.com/title/%s/' % data['library']['identifier']),
|
||||
'url_title': toUnicode('%s on IMDb' % getTitle(data['library'])),
|
||||
})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import os
|
||||
@@ -16,8 +15,9 @@ class Synoindex(Notification):
|
||||
super(Synoindex, self).__init__()
|
||||
addEvent('renamer.after', self.addToLibrary)
|
||||
|
||||
def addToLibrary(self, message = None, group = {}):
|
||||
def addToLibrary(self, message = None, group = None):
|
||||
if self.isDisabled(): return
|
||||
if not group: group = {}
|
||||
|
||||
command = [self.index_path, '-A', group.get('destination_dir')]
|
||||
log.info('Executing synoindex command: %s ', command)
|
||||
@@ -28,9 +28,10 @@ class Synoindex(Notification):
|
||||
return True
|
||||
except OSError, e:
|
||||
log.error('Unable to run synoindex: %s', e)
|
||||
return False
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def test(self):
|
||||
return jsonified({'success': os.path.isfile(self.index_path)})
|
||||
def test(self, **kwargs):
|
||||
return {
|
||||
'success': os.path.isfile(self.index_path)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ class Toasty(Notification):
|
||||
'api': 'http://api.supertoasty.com/notify/%s?%s'
|
||||
}
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
data = {
|
||||
'title': self.default_title,
|
||||
|
||||
@@ -13,7 +13,8 @@ class Trakt(Notification):
|
||||
|
||||
listen_to = ['movie.downloaded']
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
post_data = {
|
||||
'username': self.conf('automation_username'),
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from flask.helpers import url_for
|
||||
from pytwitter import Api, parse_qsl
|
||||
from werkzeug.utils import redirect
|
||||
from couchpotato.environment import Env
|
||||
from pytwitter import Api
|
||||
from urlparse import parse_qsl
|
||||
import oauth2
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -31,7 +30,8 @@ class Twitter(Notification):
|
||||
addApiView('notify.%s.auth_url' % self.getName().lower(), self.getAuthorizationUrl)
|
||||
addApiView('notify.%s.credentials' % self.getName().lower(), self.getCredentials)
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
api = Api(self.consumer_key, self.consumer_secret, self.conf('access_token_key'), self.conf('access_token_secret'))
|
||||
|
||||
@@ -52,7 +52,7 @@ class Twitter(Notification):
|
||||
try:
|
||||
if direct_message:
|
||||
for user in direct_message_users.split():
|
||||
api.PostDirectMessage(user, '[%s] %s' % (self.default_title, message))
|
||||
api.PostDirectMessage('[%s] %s' % (self.default_title, message), screen_name = user)
|
||||
else:
|
||||
update_message = '[%s] %s' % (self.default_title, message)
|
||||
if len(update_message) > 140:
|
||||
@@ -70,10 +70,9 @@ class Twitter(Notification):
|
||||
|
||||
return True
|
||||
|
||||
def getAuthorizationUrl(self):
|
||||
def getAuthorizationUrl(self, host = None, **kwargs):
|
||||
|
||||
referer = getParam('host')
|
||||
callback_url = cleanHost(referer) + '%snotify.%s.credentials/' % (url_for('api.index').lstrip('/'), self.getName().lower())
|
||||
callback_url = cleanHost(host) + '%snotify.%s.credentials/' % (Env.get('api_base').lstrip('/'), self.getName().lower())
|
||||
|
||||
oauth_consumer = oauth2.Consumer(self.consumer_key, self.consumer_secret)
|
||||
oauth_client = oauth2.Client(oauth_consumer)
|
||||
@@ -82,31 +81,29 @@ class Twitter(Notification):
|
||||
|
||||
if resp['status'] != '200':
|
||||
log.error('Invalid response from Twitter requesting temp token: %s', resp['status'])
|
||||
return jsonified({
|
||||
return {
|
||||
'success': False,
|
||||
})
|
||||
}
|
||||
else:
|
||||
self.request_token = dict(parse_qsl(content))
|
||||
|
||||
auth_url = self.urls['authorize'] + ("?oauth_token=%s" % self.request_token['oauth_token'])
|
||||
|
||||
log.info('Redirecting to "%s"', auth_url)
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'url': auth_url,
|
||||
})
|
||||
}
|
||||
|
||||
def getCredentials(self):
|
||||
|
||||
key = getParam('oauth_verifier')
|
||||
def getCredentials(self, oauth_verifier, **kwargs):
|
||||
|
||||
token = oauth2.Token(self.request_token['oauth_token'], self.request_token['oauth_token_secret'])
|
||||
token.set_verifier(key)
|
||||
token.set_verifier(oauth_verifier)
|
||||
|
||||
oauth_consumer = oauth2.Consumer(key = self.consumer_key, secret = self.consumer_secret)
|
||||
oauth_client = oauth2.Client(oauth_consumer, token)
|
||||
|
||||
resp, content = oauth_client.request(self.urls['access'], method = 'POST', body = 'oauth_verifier=%s' % key)
|
||||
resp, content = oauth_client.request(self.urls['access'], method = 'POST', body = 'oauth_verifier=%s' % oauth_verifier)
|
||||
access_token = dict(parse_qsl(content))
|
||||
|
||||
if resp['status'] != '200':
|
||||
@@ -121,4 +118,4 @@ class Twitter(Notification):
|
||||
|
||||
self.request_token = None
|
||||
|
||||
return redirect(url_for('web.index') + 'settings/notifications/')
|
||||
return 'redirect', Env.get('web_base') + 'settings/notifications/'
|
||||
|
||||
@@ -31,6 +31,28 @@ config = [{
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'only_first',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Only update the first host when movie snatched, useful for synced XBMC',
|
||||
},
|
||||
{
|
||||
'name': 'remote_dir_scan',
|
||||
'label': 'Remote Folder Scan',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Only scan new movie folder at remote XBMC servers. Works if movie location is the same.',
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also send message when movie is snatched.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from flask.helpers import json
|
||||
from urllib2 import URLError
|
||||
import base64
|
||||
import json
|
||||
import socket
|
||||
import traceback
|
||||
import urllib
|
||||
|
||||
@@ -11,39 +13,56 @@ log = CPLog(__name__)
|
||||
|
||||
class XBMC(Notification):
|
||||
|
||||
listen_to = ['renamer.after']
|
||||
listen_to = ['renamer.after', 'movie.snatched']
|
||||
use_json_notifications = {}
|
||||
http_time_between_calls = 0
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
def notify(self, message = '', data = None, listener = None):
|
||||
if not data: data = {}
|
||||
|
||||
hosts = splitString(self.conf('host'))
|
||||
|
||||
successful = 0
|
||||
max_successful = 0
|
||||
for host in hosts:
|
||||
|
||||
if self.use_json_notifications.get(host) is None:
|
||||
self.getXBMCJSONversion(host, message = message)
|
||||
|
||||
if self.use_json_notifications.get(host):
|
||||
response = self.request(host, [
|
||||
calls = [
|
||||
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}),
|
||||
('VideoLibrary.Scan', {}),
|
||||
])
|
||||
]
|
||||
|
||||
if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0):
|
||||
param = {}
|
||||
if self.conf('remote_dir_scan') or socket.getfqdn('localhost') == socket.getfqdn(host.split(':')[0]):
|
||||
param = {'directory': data['destination_dir']}
|
||||
|
||||
calls.append(('VideoLibrary.Scan', param))
|
||||
|
||||
max_successful += len(calls)
|
||||
response = self.request(host, calls)
|
||||
else:
|
||||
response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
|
||||
response += self.request(host, [('VideoLibrary.Scan', {})])
|
||||
|
||||
if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0):
|
||||
response += self.request(host, [('VideoLibrary.Scan', {})])
|
||||
max_successful += 1
|
||||
|
||||
max_successful += 1
|
||||
|
||||
try:
|
||||
for result in response:
|
||||
if (result.get('result') and result['result'] == 'OK'):
|
||||
if result.get('result') and result['result'] == 'OK':
|
||||
successful += 1
|
||||
elif (result.get('error')):
|
||||
elif result.get('error'):
|
||||
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
|
||||
|
||||
except:
|
||||
log.error('Failed parsing results: %s', traceback.format_exc())
|
||||
|
||||
return successful == len(hosts) * 2
|
||||
return successful == max_successful
|
||||
|
||||
def getXBMCJSONversion(self, host, message = ''):
|
||||
|
||||
@@ -52,9 +71,9 @@ class XBMC(Notification):
|
||||
# XBMC JSON-RPC version request
|
||||
response = self.request(host, [
|
||||
('JSONRPC.Version', {})
|
||||
])
|
||||
])
|
||||
for result in response:
|
||||
if (result.get('result') and type(result['result']['version']).__name__ == 'int'):
|
||||
if result.get('result') and type(result['result']['version']).__name__ == 'int':
|
||||
# only v2 and v4 return an int object
|
||||
# v6 (as of XBMC v12(Frodo)) is required to send notifications
|
||||
xbmc_rpc_version = str(result['result']['version'])
|
||||
@@ -67,15 +86,15 @@ class XBMC(Notification):
|
||||
# send the text message
|
||||
resp = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
|
||||
for result in resp:
|
||||
if (result.get('result') and result['result'] == 'OK'):
|
||||
if result.get('result') and result['result'] == 'OK':
|
||||
log.debug('Message delivered successfully!')
|
||||
success = True
|
||||
break
|
||||
elif (result.get('error')):
|
||||
elif result.get('error'):
|
||||
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
|
||||
break
|
||||
|
||||
elif (result.get('result') and type(result['result']['version']).__name__ == 'dict'):
|
||||
elif result.get('result') and type(result['result']['version']).__name__ == 'dict':
|
||||
# XBMC JSON-RPC v6 returns an array object containing
|
||||
# major, minor and patch number
|
||||
xbmc_rpc_version = str(result['result']['version']['major'])
|
||||
@@ -90,16 +109,16 @@ class XBMC(Notification):
|
||||
# send the text message
|
||||
resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image': self.getNotificationImage('small')})])
|
||||
for result in resp:
|
||||
if (result.get('result') and result['result'] == 'OK'):
|
||||
if result.get('result') and result['result'] == 'OK':
|
||||
log.debug('Message delivered successfully!')
|
||||
success = True
|
||||
break
|
||||
elif (result.get('error')):
|
||||
elif result.get('error'):
|
||||
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
|
||||
break
|
||||
|
||||
# error getting version info (we do have contact with XBMC though)
|
||||
elif (result.get('error')):
|
||||
elif result.get('error'):
|
||||
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
|
||||
|
||||
log.debug('Use JSON notifications: %s ', self.use_json_notifications)
|
||||
@@ -137,7 +156,7 @@ class XBMC(Notification):
|
||||
# <li>Error:<message>
|
||||
# </html>
|
||||
#
|
||||
response = self.urlopen(server, headers = headers)
|
||||
response = self.urlopen(server, headers = headers, timeout = 3, show_error = False)
|
||||
|
||||
if 'OK' in response:
|
||||
log.debug('Returned from non-JSON-type request %s: %s', (host, response))
|
||||
@@ -148,6 +167,13 @@ class XBMC(Notification):
|
||||
# manually fake expected response array
|
||||
return [{'result': 'Error'}]
|
||||
|
||||
except URLError, e:
|
||||
if isinstance(e.reason, socket.timeout):
|
||||
log.info('Couldn\'t send request to XBMC, assuming it\'s turned off')
|
||||
return [{'result': 'Error'}]
|
||||
else:
|
||||
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
|
||||
return [{'result': 'Error'}]
|
||||
except:
|
||||
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
|
||||
return [{'result': 'Error'}]
|
||||
@@ -176,11 +202,17 @@ class XBMC(Notification):
|
||||
|
||||
try:
|
||||
log.debug('Sending request to %s: %s', (host, data))
|
||||
rdata = self.urlopen(server, headers = headers, params = data, multipart = True)
|
||||
response = json.loads(rdata)
|
||||
response = self.getJsonData(server, headers = headers, params = data, timeout = 3, show_error = False)
|
||||
log.debug('Returned from request %s: %s', (host, response))
|
||||
|
||||
return response
|
||||
except URLError, e:
|
||||
if isinstance(e.reason, socket.timeout):
|
||||
log.info('Couldn\'t send request to XBMC, assuming it\'s turned off')
|
||||
return []
|
||||
else:
|
||||
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
|
||||
return []
|
||||
except:
|
||||
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
|
||||
return []
|
||||
|
||||
@@ -26,6 +26,10 @@ class Automation(Plugin):
|
||||
movie_ids = []
|
||||
|
||||
for imdb_id in movies:
|
||||
|
||||
if self.shuttingDown():
|
||||
break
|
||||
|
||||
prop_name = 'automation.added.%s' % imdb_id
|
||||
added = Env.prop(prop_name, default = False)
|
||||
if not added:
|
||||
@@ -35,5 +39,11 @@ class Automation(Plugin):
|
||||
Env.prop(prop_name, True)
|
||||
|
||||
for movie_id in movie_ids:
|
||||
|
||||
if self.shuttingDown():
|
||||
break
|
||||
|
||||
movie_dict = fireEvent('movie.get', movie_id, single = True)
|
||||
fireEvent('searcher.single', movie_dict)
|
||||
fireEvent('movie.searcher.single', movie_dict)
|
||||
|
||||
return True
|
||||
@@ -1,16 +1,18 @@
|
||||
from StringIO import StringIO
|
||||
from couchpotato import addView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString
|
||||
from couchpotato.core.helpers.variable import getExt, md5
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString, \
|
||||
toUnicode
|
||||
from couchpotato.core.helpers.variable import getExt, md5, isLocalIP
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from flask.templating import render_template_string
|
||||
from multipartpost import MultipartPostHandler
|
||||
from tornado import template
|
||||
from tornado.web import StaticFileHandler
|
||||
from urlparse import urlparse
|
||||
import cookielib
|
||||
import glob
|
||||
import gzip
|
||||
import inspect
|
||||
import math
|
||||
import os.path
|
||||
import re
|
||||
@@ -23,55 +25,76 @@ log = CPLog(__name__)
|
||||
|
||||
class Plugin(object):
|
||||
|
||||
_class_name = None
|
||||
plugin_path = None
|
||||
|
||||
enabled_option = 'enabled'
|
||||
auto_register_static = True
|
||||
|
||||
_needs_shutdown = False
|
||||
_running = None
|
||||
|
||||
user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:24.0) Gecko/20130519 Firefox/24.0'
|
||||
http_last_use = {}
|
||||
http_time_between_calls = 0
|
||||
http_failed_request = {}
|
||||
http_failed_disabled = {}
|
||||
|
||||
def __new__(typ, *args, **kwargs):
|
||||
new_plugin = super(Plugin, typ).__new__(typ)
|
||||
new_plugin.registerPlugin()
|
||||
|
||||
return new_plugin
|
||||
|
||||
def registerPlugin(self):
|
||||
addEvent('app.do_shutdown', self.doShutdown)
|
||||
addEvent('plugin.running', self.isRunning)
|
||||
self._running = []
|
||||
|
||||
def conf(self, attr, value = None, default = None):
|
||||
return Env.setting(attr, self.getName().lower(), value = value, default = default)
|
||||
if self.auto_register_static:
|
||||
self.registerStatic(inspect.getfile(self.__class__))
|
||||
|
||||
def conf(self, attr, value = None, default = None, section = None):
|
||||
class_name = self.getName().lower().split(':')
|
||||
return Env.setting(attr, section = section if section else class_name[0].lower(), value = value, default = default)
|
||||
|
||||
def getName(self):
|
||||
return self.__class__.__name__
|
||||
return self._class_name or self.__class__.__name__
|
||||
|
||||
def renderTemplate(self, parent_file, template, **params):
|
||||
def setName(self, name):
|
||||
self._class_name = name
|
||||
|
||||
template = open(os.path.join(os.path.dirname(parent_file), template), 'r').read()
|
||||
return render_template_string(template, **params)
|
||||
def renderTemplate(self, parent_file, templ, **params):
|
||||
|
||||
t = template.Template(open(os.path.join(os.path.dirname(parent_file), templ), 'r').read())
|
||||
return t.generate(**params)
|
||||
|
||||
def registerStatic(self, plugin_file, add_to_head = True):
|
||||
|
||||
# Register plugin path
|
||||
self.plugin_path = os.path.dirname(plugin_file)
|
||||
static_folder = toUnicode(os.path.join(self.plugin_path, 'static'))
|
||||
|
||||
if not os.path.isdir(static_folder):
|
||||
return
|
||||
|
||||
# Get plugin_name from PluginName
|
||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', self.__class__.__name__)
|
||||
class_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
||||
|
||||
path = 'api/%s/static/%s/' % (Env.setting('api_key'), class_name)
|
||||
addView(path + '<path:filename>', self.showStatic, static = True)
|
||||
# View path
|
||||
path = 'static/plugin/%s/' % (class_name)
|
||||
|
||||
# Add handler to Tornado
|
||||
Env.get('app').add_handlers(".*$", [(Env.get('web_base') + path + '(.*)', StaticFileHandler, {'path': static_folder})])
|
||||
|
||||
# Register for HTML <HEAD>
|
||||
if add_to_head:
|
||||
for f in glob.glob(os.path.join(self.plugin_path, 'static', '*')):
|
||||
ext = getExt(f)
|
||||
if ext in ['js', 'css']:
|
||||
fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f)
|
||||
|
||||
def showStatic(self, filename):
|
||||
d = os.path.join(self.plugin_path, 'static')
|
||||
|
||||
from flask.helpers import send_from_directory
|
||||
return send_from_directory(d, filename)
|
||||
|
||||
def createFile(self, path, content, binary = False):
|
||||
path = ss(path)
|
||||
|
||||
@@ -104,19 +127,22 @@ class Plugin(object):
|
||||
if not params: params = {}
|
||||
|
||||
# Fill in some headers
|
||||
headers['Referer'] = headers.get('Referer', urlparse(url).hostname)
|
||||
headers['Host'] = headers.get('Host', urlparse(url).hostname)
|
||||
headers['User-Agent'] = headers.get('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:10.0.2) Gecko/20100101 Firefox/10.0.2')
|
||||
headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip')
|
||||
parsed_url = urlparse(url)
|
||||
host = '%s%s' % (parsed_url.hostname, (':' + str(parsed_url.port) if parsed_url.port else ''))
|
||||
|
||||
host = urlparse(url).hostname
|
||||
headers['Referer'] = headers.get('Referer', '%s://%s' % (parsed_url.scheme, host))
|
||||
headers['Host'] = headers.get('Host', host)
|
||||
headers['User-Agent'] = headers.get('User-Agent', self.user_agent)
|
||||
headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip')
|
||||
headers['Connection'] = headers.get('Connection', 'keep-alive')
|
||||
headers['Cache-Control'] = headers.get('Cache-Control', 'max-age=0')
|
||||
|
||||
# Don't try for failed requests
|
||||
if self.http_failed_disabled.get(host, 0) > 0:
|
||||
if self.http_failed_disabled[host] > (time.time() - 900):
|
||||
log.info2('Disabled calls to %s for 15 minutes because so many failed requests.', host)
|
||||
if not show_error:
|
||||
raise
|
||||
raise Exception('Disabled calls to %s for 15 minutes because so many failed requests')
|
||||
else:
|
||||
return ''
|
||||
else:
|
||||
@@ -126,6 +152,10 @@ class Plugin(object):
|
||||
self.wait(host)
|
||||
try:
|
||||
|
||||
# Make sure opener has the correct headers
|
||||
if opener:
|
||||
opener.add_headers = headers
|
||||
|
||||
if multipart:
|
||||
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
|
||||
request = urllib2.Request(url, params, headers)
|
||||
@@ -138,8 +168,13 @@ class Plugin(object):
|
||||
|
||||
response = opener.open(request, timeout = timeout)
|
||||
else:
|
||||
log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()]))
|
||||
data = tryUrlencode(params) if len(params) > 0 else None
|
||||
log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
|
||||
|
||||
if isinstance(params, (str, unicode)) and len(params) > 0:
|
||||
data = params
|
||||
else:
|
||||
data = tryUrlencode(params) if len(params) > 0 else None
|
||||
|
||||
request = urllib2.Request(url, data, headers)
|
||||
|
||||
if opener:
|
||||
@@ -152,8 +187,10 @@ class Plugin(object):
|
||||
buf = StringIO(response.read())
|
||||
f = gzip.GzipFile(fileobj = buf)
|
||||
data = f.read()
|
||||
f.close()
|
||||
else:
|
||||
data = response.read()
|
||||
response.close()
|
||||
|
||||
self.http_failed_request[host] = 0
|
||||
except IOError:
|
||||
@@ -168,7 +205,7 @@ class Plugin(object):
|
||||
self.http_failed_request[host] += 1
|
||||
|
||||
# Disable temporarily
|
||||
if self.http_failed_request[host] > 5:
|
||||
if self.http_failed_request[host] > 5 and not isLocalIP(host):
|
||||
self.http_failed_disabled[host] = time.time()
|
||||
|
||||
except:
|
||||
@@ -209,9 +246,6 @@ class Plugin(object):
|
||||
|
||||
def isRunning(self, value = None, boolean = True):
|
||||
|
||||
if not hasattr(self, '_running'):
|
||||
self._running = []
|
||||
|
||||
if value is None:
|
||||
return self._running
|
||||
|
||||
@@ -225,8 +259,8 @@ class Plugin(object):
|
||||
|
||||
|
||||
def getCache(self, cache_key, url = None, **kwargs):
|
||||
cache_key = md5(ss(cache_key))
|
||||
cache = Env.get('cache').get(cache_key)
|
||||
cache_key_md5 = md5(cache_key)
|
||||
cache = Env.get('cache').get(cache_key_md5)
|
||||
if cache:
|
||||
if not Env.get('dev'): log.debug('Getting cache %s', cache_key)
|
||||
return cache
|
||||
@@ -250,8 +284,9 @@ class Plugin(object):
|
||||
return ''
|
||||
|
||||
def setCache(self, cache_key, value, timeout = 300):
|
||||
cache_key_md5 = md5(cache_key)
|
||||
log.debug('Setting cache %s', cache_key)
|
||||
Env.get('cache').set(cache_key, value, timeout)
|
||||
Env.get('cache').set(cache_key_md5, value, timeout)
|
||||
return value
|
||||
|
||||
def createNzbName(self, data, movie):
|
||||
@@ -260,9 +295,9 @@ class Plugin(object):
|
||||
|
||||
def createFileName(self, data, filedata, movie):
|
||||
name = os.path.join(self.createNzbName(data, movie))
|
||||
if data.get('type') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
|
||||
if data.get('protocol') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
|
||||
return '%s.%s' % (name, 'rar')
|
||||
return '%s.%s' % (name, data.get('type'))
|
||||
return '%s.%s' % (name, data.get('protocol'))
|
||||
|
||||
def cpTag(self, movie):
|
||||
if Env.setting('enabled', 'renamer'):
|
||||
@@ -274,4 +309,4 @@ class Plugin(object):
|
||||
return not self.isEnabled()
|
||||
|
||||
def isEnabled(self):
|
||||
return self.conf(self.enabled_option) or self.conf(self.enabled_option) == None
|
||||
return self.conf(self.enabled_option) or self.conf(self.enabled_option) is None
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.helpers.request import getParam, jsonified
|
||||
from couchpotato.core.helpers.variable import getUserDir
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
import ctypes
|
||||
@@ -13,7 +12,7 @@ if os.name == 'nt':
|
||||
except:
|
||||
# todo:: subclass ImportError for missing dependencies, vs. broken plugins?
|
||||
raise ImportError("Missing the win32file module, which is a part of the prerequisite \
|
||||
pywin32 package. You can get it from http://sourceforge.net/projects/pywin32/files/pywin32/");
|
||||
pywin32 package. You can get it from http://sourceforge.net/projects/pywin32/files/pywin32/")
|
||||
else:
|
||||
import win32file #@UnresolvedImport
|
||||
|
||||
@@ -63,16 +62,15 @@ class FileBrowser(Plugin):
|
||||
|
||||
return driveletters
|
||||
|
||||
def view(self):
|
||||
def view(self, path = '/', show_hidden = True, **kwargs):
|
||||
|
||||
path = getParam('path', '/')
|
||||
home = getUserDir()
|
||||
|
||||
if not path:
|
||||
path = home
|
||||
|
||||
try:
|
||||
dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True))
|
||||
dirs = self.getDirectories(path = path, show_hidden = show_hidden)
|
||||
except:
|
||||
dirs = []
|
||||
|
||||
@@ -82,14 +80,14 @@ class FileBrowser(Plugin):
|
||||
elif parent != '/' and parent[-2:] != ':\\':
|
||||
parent += os.path.sep
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'is_root': path == '/',
|
||||
'empty': len(dirs) == 0,
|
||||
'parent': parent,
|
||||
'home': home + os.path.sep,
|
||||
'platform': os.name,
|
||||
'dirs': dirs,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
def is_hidden(self, filepath):
|
||||
|
||||
6
couchpotato/core/plugins/category/__init__.py
Normal file
6
couchpotato/core/plugins/category/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .main import CategoryPlugin
|
||||
|
||||
def start():
|
||||
return CategoryPlugin()
|
||||
|
||||
config = []
|
||||
121
couchpotato/core/plugins/category/main.py
Normal file
121
couchpotato/core/plugins/category/main.py
Normal file
@@ -0,0 +1,121 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Movie, Category
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class CategoryPlugin(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('category.all', self.all)
|
||||
|
||||
addApiView('category.save', self.save)
|
||||
addApiView('category.save_order', self.saveOrder)
|
||||
addApiView('category.delete', self.delete)
|
||||
addApiView('category.list', self.allView, docs = {
|
||||
'desc': 'List all available categories',
|
||||
'return': {'type': 'object', 'example': """{
|
||||
'success': True,
|
||||
'list': array, categories
|
||||
}"""}
|
||||
})
|
||||
|
||||
def allView(self, **kwargs):
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'list': self.all()
|
||||
}
|
||||
|
||||
def all(self):
|
||||
|
||||
db = get_session()
|
||||
categories = db.query(Category).all()
|
||||
|
||||
temp = []
|
||||
for category in categories:
|
||||
temp.append(category.to_dict())
|
||||
|
||||
db.expire_all()
|
||||
return temp
|
||||
|
||||
def save(self, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
|
||||
c = db.query(Category).filter_by(id = kwargs.get('id')).first()
|
||||
if not c:
|
||||
c = Category()
|
||||
db.add(c)
|
||||
|
||||
c.order = kwargs.get('order', c.order if c.order else 0)
|
||||
c.label = toUnicode(kwargs.get('label', ''))
|
||||
c.ignored = toUnicode(kwargs.get('ignored', ''))
|
||||
c.preferred = toUnicode(kwargs.get('preferred', ''))
|
||||
c.required = toUnicode(kwargs.get('required', ''))
|
||||
c.destination = toUnicode(kwargs.get('destination', ''))
|
||||
|
||||
db.commit()
|
||||
|
||||
category_dict = c.to_dict()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'category': category_dict
|
||||
}
|
||||
|
||||
def saveOrder(self, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
|
||||
order = 0
|
||||
for category_id in kwargs.get('ids', []):
|
||||
c = db.query(Category).filter_by(id = category_id).first()
|
||||
c.order = order
|
||||
|
||||
order += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
'success': True
|
||||
}
|
||||
|
||||
def delete(self, id = None, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
|
||||
success = False
|
||||
message = ''
|
||||
try:
|
||||
c = db.query(Category).filter_by(id = id).first()
|
||||
db.delete(c)
|
||||
db.commit()
|
||||
|
||||
# Force defaults on all empty category movies
|
||||
self.removeFromMovie(id)
|
||||
|
||||
success = True
|
||||
except Exception, e:
|
||||
message = log.error('Failed deleting category: %s', e)
|
||||
|
||||
db.expire_all()
|
||||
return {
|
||||
'success': success,
|
||||
'message': message
|
||||
}
|
||||
|
||||
def removeFromMovie(self, category_id):
|
||||
|
||||
db = get_session()
|
||||
movies = db.query(Movie).filter(Movie.category_id == category_id).all()
|
||||
|
||||
if len(movies) > 0:
|
||||
for movie in movies:
|
||||
movie.category_id = None
|
||||
db.commit()
|
||||
82
couchpotato/core/plugins/category/static/category.css
Normal file
82
couchpotato/core/plugins/category/static/category.css
Normal file
@@ -0,0 +1,82 @@
|
||||
.add_new_category {
|
||||
padding: 20px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.category {
|
||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.category > .delete {
|
||||
position: absolute;
|
||||
padding: 16px;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
color: #fd5353;
|
||||
}
|
||||
.category > .delete:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.category .ctrlHolder:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.category .formHint {
|
||||
width: 250px !important;
|
||||
margin: 0 !important;
|
||||
opacity: 0.1;
|
||||
}
|
||||
.category:hover .formHint {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#category_ordering {
|
||||
|
||||
}
|
||||
|
||||
#category_ordering ul {
|
||||
float: left;
|
||||
margin: 0;
|
||||
width: 275px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#category_ordering li {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
cursor: grab;
|
||||
border-bottom: 1px solid rgba(255,255,255,0.2);
|
||||
padding: 0 5px;
|
||||
}
|
||||
#category_ordering li:last-child { border: 0; }
|
||||
|
||||
#category_ordering li .check {
|
||||
margin: 2px 10px 0 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
#category_ordering li > span {
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
vertical-align: top;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#category_ordering li .handle {
|
||||
background: url('../../static/profile_plugin/handle.png') center;
|
||||
width: 20px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
#category_ordering .formHint {
|
||||
clear: none;
|
||||
float: right;
|
||||
width: 250px;
|
||||
margin: 0;
|
||||
}
|
||||
332
couchpotato/core/plugins/category/static/category.js
Normal file
332
couchpotato/core/plugins/category/static/category.js
Normal file
@@ -0,0 +1,332 @@
|
||||
var CategoryListBase = new Class({
|
||||
|
||||
initialize: function(){
|
||||
var self = this;
|
||||
|
||||
App.addEvent('load', self.addSettings.bind(self));
|
||||
},
|
||||
|
||||
setup: function(categories){
|
||||
var self = this;
|
||||
|
||||
self.categories = []
|
||||
Array.each(categories, self.createCategory.bind(self));
|
||||
|
||||
},
|
||||
|
||||
addSettings: function(){
|
||||
var self = this;
|
||||
|
||||
self.settings = App.getPage('Settings')
|
||||
self.settings.addEvent('create', function(){
|
||||
var tab = self.settings.createSubTab('category', {
|
||||
'label': 'Categories',
|
||||
'name': 'category',
|
||||
'subtab_label': 'Category & filtering'
|
||||
}, self.settings.tabs.searcher ,'searcher');
|
||||
|
||||
self.tab = tab.tab;
|
||||
self.content = tab.content;
|
||||
|
||||
self.createList();
|
||||
self.createOrdering();
|
||||
|
||||
})
|
||||
|
||||
// Add categories in renamer
|
||||
self.settings.addEvent('create', function(){
|
||||
var renamer_group = self.settings.tabs.renamer.groups.renamer;
|
||||
|
||||
self.categories.each(function(category){
|
||||
|
||||
var input = new Option.Directory('section_name', 'option.name', category.get('destination'), {
|
||||
'name': category.get('label')
|
||||
});
|
||||
input.inject(renamer_group.getElement('.renamer_to'));
|
||||
input.fireEvent('injected');
|
||||
|
||||
input.save = function(){
|
||||
category.data.destination = input.getValue();
|
||||
category.save();
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
createList: function(){
|
||||
var self = this;
|
||||
|
||||
var count = self.categories.length;
|
||||
|
||||
self.settings.createGroup({
|
||||
'label': 'Categories',
|
||||
'description': 'Create categories, each one extending global filters. (Needs refresh \'' +(App.isMac() ? 'CMD+R' : 'F5')+ '\' after editing)'
|
||||
}).inject(self.content).adopt(
|
||||
self.category_container = new Element('div.container'),
|
||||
new Element('a.add_new_category', {
|
||||
'text': count > 0 ? 'Create another category' : 'Click here to create a category.',
|
||||
'events': {
|
||||
'click': function(){
|
||||
var category = self.createCategory();
|
||||
$(category).inject(self.category_container)
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Add categories, that aren't part of the core (for editing)
|
||||
Array.each(self.categories, function(category){
|
||||
$(category).inject(self.category_container)
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
getCategory: function(id){
|
||||
return this.categories.filter(function(category){
|
||||
return category.data.id == id
|
||||
}).pick()
|
||||
},
|
||||
|
||||
getAll: function(){
|
||||
return this.categories;
|
||||
},
|
||||
|
||||
createCategory: function(data){
|
||||
var self = this;
|
||||
|
||||
var data = data || {'id': randomString()}
|
||||
var category = new Category(data)
|
||||
self.categories.include(category)
|
||||
|
||||
return category;
|
||||
},
|
||||
|
||||
createOrdering: function(){
|
||||
var self = this;
|
||||
|
||||
var category_list;
|
||||
var group = self.settings.createGroup({
|
||||
'label': 'Category ordering'
|
||||
}).adopt(
|
||||
new Element('.ctrlHolder#category_ordering').adopt(
|
||||
new Element('label[text=Order]'),
|
||||
category_list = new Element('ul'),
|
||||
new Element('p.formHint', {
|
||||
'html': 'Change the order the categories are in the dropdown list.<br />First one will be default.'
|
||||
})
|
||||
)
|
||||
).inject(self.content)
|
||||
|
||||
Array.each(self.categories, function(category){
|
||||
new Element('li', {'data-id': category.data.id}).adopt(
|
||||
new Element('span.category_label', {
|
||||
'text': category.data.label
|
||||
}),
|
||||
new Element('span.handle')
|
||||
).inject(category_list);
|
||||
|
||||
});
|
||||
|
||||
// Sortable
|
||||
self.category_sortable = new Sortables(category_list, {
|
||||
'revert': true,
|
||||
'handle': '',
|
||||
'opacity': 0.5,
|
||||
'onComplete': self.saveOrdering.bind(self)
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
saveOrdering: function(){
|
||||
var self = this;
|
||||
|
||||
var ids = [];
|
||||
|
||||
self.category_sortable.list.getElements('li').each(function(el, nr){
|
||||
ids.include(el.get('data-id'));
|
||||
});
|
||||
|
||||
Api.request('category.save_order', {
|
||||
'data': {
|
||||
'ids': ids
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
window.CategoryList = new CategoryListBase();
|
||||
|
||||
var Category = new Class({
|
||||
|
||||
data: {},
|
||||
|
||||
initialize: function(data){
|
||||
var self = this;
|
||||
|
||||
self.data = data;
|
||||
|
||||
self.create();
|
||||
|
||||
self.el.addEvents({
|
||||
'change:relay(select)': self.save.bind(self, 0),
|
||||
'keyup:relay(input[type=text])': self.save.bind(self, [300])
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
var data = self.data;
|
||||
|
||||
self.el = new Element('div.category').adopt(
|
||||
self.delete_button = new Element('span.delete.icon2', {
|
||||
'events': {
|
||||
'click': self.del.bind(self)
|
||||
}
|
||||
}),
|
||||
new Element('.category_label.ctrlHolder').adopt(
|
||||
new Element('label', {'text':'Name'}),
|
||||
new Element('input.inlay', {
|
||||
'type':'text',
|
||||
'value': data.label,
|
||||
'placeholder': 'Example: Kids, Horror or His'
|
||||
}),
|
||||
new Element('p.formHint', {'text': 'See global filters for explanation.'})
|
||||
),
|
||||
new Element('.category_preferred.ctrlHolder').adopt(
|
||||
new Element('label', {'text':'Preferred'}),
|
||||
new Element('input.inlay', {
|
||||
'type':'text',
|
||||
'value': data.preferred,
|
||||
'placeholder': 'Blu-ray, DTS'
|
||||
})
|
||||
),
|
||||
new Element('.category_required.ctrlHolder').adopt(
|
||||
new Element('label', {'text':'Required'}),
|
||||
new Element('input.inlay', {
|
||||
'type':'text',
|
||||
'value': data.required,
|
||||
'placeholder': 'Example: DTS, AC3 & English'
|
||||
})
|
||||
),
|
||||
new Element('.category_ignored.ctrlHolder').adopt(
|
||||
new Element('label', {'text':'Ignored'}),
|
||||
new Element('input.inlay', {
|
||||
'type':'text',
|
||||
'value': data.ignored,
|
||||
'placeholder': 'Example: dubbed, swesub, french'
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
self.makeSortable()
|
||||
|
||||
},
|
||||
|
||||
save: function(delay){
|
||||
var self = this;
|
||||
|
||||
if(self.save_timer) clearTimeout(self.save_timer);
|
||||
self.save_timer = (function(){
|
||||
|
||||
var data = self.getData();
|
||||
|
||||
Api.request('category.save', {
|
||||
'data': self.getData(),
|
||||
'useSpinner': true,
|
||||
'spinnerOptions': {
|
||||
'target': self.el
|
||||
},
|
||||
'onComplete': function(json){
|
||||
if(json.success){
|
||||
self.data = json.category;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}).delay(delay || 0, self)
|
||||
|
||||
},
|
||||
|
||||
getData: function(){
|
||||
var self = this;
|
||||
|
||||
var data = {
|
||||
'id' : self.data.id,
|
||||
'label' : self.el.getElement('.category_label input').get('value'),
|
||||
'required' : self.el.getElement('.category_required input').get('value'),
|
||||
'preferred' : self.el.getElement('.category_preferred input').get('value'),
|
||||
'ignored' : self.el.getElement('.category_ignored input').get('value'),
|
||||
'destination': self.data.destination
|
||||
}
|
||||
|
||||
return data
|
||||
},
|
||||
|
||||
del: function(){
|
||||
var self = this;
|
||||
|
||||
if(self.data.label == undefined){
|
||||
self.el.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
var label = self.el.getElement('.category_label input').get('value');
|
||||
var qObj = new Question('Are you sure you want to delete <strong>"'+label+'"</strong>?', '', [{
|
||||
'text': 'Delete "'+label+'"',
|
||||
'class': 'delete',
|
||||
'events': {
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
Api.request('category.delete', {
|
||||
'data': {
|
||||
'id': self.data.id
|
||||
},
|
||||
'useSpinner': true,
|
||||
'spinnerOptions': {
|
||||
'target': self.el
|
||||
},
|
||||
'onComplete': function(json){
|
||||
if(json.success) {
|
||||
qObj.close();
|
||||
self.el.destroy();
|
||||
} else {
|
||||
alert(json.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, {
|
||||
'text': 'Cancel',
|
||||
'cancel': true
|
||||
}]);
|
||||
|
||||
},
|
||||
|
||||
makeSortable: function(){
|
||||
var self = this;
|
||||
|
||||
self.sortable = new Sortables(self.category_container, {
|
||||
'revert': true,
|
||||
'handle': '.handle',
|
||||
'opacity': 0.5,
|
||||
'onComplete': self.save.bind(self, 300)
|
||||
});
|
||||
},
|
||||
|
||||
get: function(attr){
|
||||
return this.data[attr]
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el
|
||||
}
|
||||
|
||||
});
|
||||
BIN
couchpotato/core/plugins/category/static/handle.png
Normal file
BIN
couchpotato/core/plugins/category/static/handle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 160 B |
@@ -1,13 +1,13 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.request import jsonified, getParams
|
||||
from couchpotato.core.helpers.variable import splitString, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Movie
|
||||
from couchpotato.core.settings.model import Movie, Library, LibraryTitle
|
||||
from sqlalchemy.orm import joinedload_all
|
||||
import random
|
||||
from sqlalchemy.sql.expression import asc
|
||||
import random as rndm
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -16,41 +16,10 @@ log = CPLog(__name__)
|
||||
class Dashboard(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
addApiView('dashboard.suggestions', self.suggestView)
|
||||
addApiView('dashboard.soon', self.getSoonView)
|
||||
|
||||
def newSuggestions(self):
|
||||
def getSoonView(self, limit_offset = None, random = False, late = False, **kwargs):
|
||||
|
||||
movies = fireEvent('movie.list', status = ['active', 'done'], limit_offset = (20, 0), single = True)
|
||||
movie_identifiers = [m['library']['identifier'] for m in movies[1]]
|
||||
|
||||
ignored_movies = fireEvent('movie.list', status = ['ignored', 'deleted'], limit_offset = (100, 0), single = True)
|
||||
ignored_identifiers = [m['library']['identifier'] for m in ignored_movies[1]]
|
||||
|
||||
suggestions = fireEvent('movie.suggest', movies = movie_identifiers, ignore = ignored_identifiers, single = True)
|
||||
suggest_status = fireEvent('status.get', 'suggest', single = True)
|
||||
|
||||
for suggestion in suggestions:
|
||||
fireEvent('movie.add', params = {'identifier': suggestion}, force_readd = False, search_after = False, status_id = suggest_status.get('id'))
|
||||
|
||||
def suggestView(self):
|
||||
|
||||
db = get_session()
|
||||
|
||||
movies = db.query(Movie).limit(20).all()
|
||||
identifiers = [m.library.identifier for m in movies]
|
||||
|
||||
suggestions = fireEvent('movie.suggest', movies = identifiers, single = True)
|
||||
|
||||
return jsonified({
|
||||
'result': True,
|
||||
'suggestions': suggestions
|
||||
})
|
||||
|
||||
def getSoonView(self):
|
||||
|
||||
params = getParams()
|
||||
db = get_session()
|
||||
now = time.time()
|
||||
|
||||
@@ -72,63 +41,85 @@ class Dashboard(Plugin):
|
||||
|
||||
profile_pre[profile.get('id')] = contains
|
||||
|
||||
# Get all active movies
|
||||
active_status = fireEvent('status.get', 'active', single = True)
|
||||
subq = db.query(Movie).filter(Movie.status_id == active_status.get('id')).subquery()
|
||||
|
||||
q = db.query(Movie).join((subq, subq.c.id == Movie.id)) \
|
||||
.options(joinedload_all('releases')) \
|
||||
.options(joinedload_all('profile.types')) \
|
||||
.options(joinedload_all('library.titles')) \
|
||||
.options(joinedload_all('library.files')) \
|
||||
.options(joinedload_all('status')) \
|
||||
.options(joinedload_all('files'))
|
||||
|
||||
# Add limit
|
||||
limit_offset = params.get('limit_offset')
|
||||
limit = 12
|
||||
if limit_offset:
|
||||
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
|
||||
limit = tryInt(splt[0])
|
||||
|
||||
all_movies = q.all()
|
||||
# Get all active movies
|
||||
active_status = fireEvent('status.get', ['active'], single = True)
|
||||
q = db.query(Movie) \
|
||||
.join(Library) \
|
||||
.filter(Movie.status_id == active_status.get('id')) \
|
||||
.with_entities(Movie.id, Movie.profile_id, Library.info, Library.year) \
|
||||
.group_by(Movie.id)
|
||||
|
||||
if params.get('random', False):
|
||||
random.shuffle(all_movies)
|
||||
if not random:
|
||||
q = q.join(LibraryTitle) \
|
||||
.filter(LibraryTitle.default == True) \
|
||||
.order_by(asc(LibraryTitle.simple_title))
|
||||
|
||||
active = q.all()
|
||||
movies = []
|
||||
for movie in all_movies:
|
||||
pp = profile_pre.get(movie.profile.id)
|
||||
eta = movie.library.info.get('release_date', {}) or {}
|
||||
coming_soon = False
|
||||
|
||||
# Theater quality
|
||||
if pp.get('theater') and fireEvent('searcher.could_be_released', True, eta, single = True):
|
||||
coming_soon = True
|
||||
if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, single = True):
|
||||
coming_soon = True
|
||||
if len(active) > 0:
|
||||
|
||||
# Do the shuffle
|
||||
if random:
|
||||
rndm.shuffle(active)
|
||||
|
||||
if coming_soon:
|
||||
temp = movie.to_dict({
|
||||
'profile': {'types': {}},
|
||||
'releases': {'files':{}, 'info': {}},
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {},
|
||||
})
|
||||
movie_ids = []
|
||||
for movie in active:
|
||||
movie_id, profile_id, info, year = movie
|
||||
|
||||
# Don't list older movies
|
||||
if ((not params.get('late') and (not eta.get('dvd') or (eta.get('dvd') and eta.get('dvd') > (now - 2419200)))) or \
|
||||
(params.get('late') and eta.get('dvd') and eta.get('dvd') < (now - 2419200))):
|
||||
movies.append(temp)
|
||||
pp = profile_pre.get(profile_id)
|
||||
if not pp: continue
|
||||
|
||||
if len(movies) >= limit:
|
||||
break
|
||||
eta = info.get('release_date', {}) or {}
|
||||
coming_soon = False
|
||||
|
||||
return jsonified({
|
||||
# Theater quality
|
||||
if pp.get('theater') and fireEvent('movie.searcher.could_be_released', True, eta, year, single = True):
|
||||
coming_soon = True
|
||||
elif pp.get('dvd') and fireEvent('movie.searcher.could_be_released', False, eta, year, single = True):
|
||||
coming_soon = True
|
||||
|
||||
if coming_soon:
|
||||
|
||||
# Don't list older movies
|
||||
if ((not late and (not eta.get('dvd') and not eta.get('theater') or eta.get('dvd') and eta.get('dvd') > (now - 2419200))) or
|
||||
(late and (eta.get('dvd', 0) > 0 or eta.get('theater')) and eta.get('dvd') < (now - 2419200))):
|
||||
movie_ids.append(movie_id)
|
||||
|
||||
if len(movie_ids) >= limit:
|
||||
break
|
||||
|
||||
if len(movie_ids) > 0:
|
||||
|
||||
# Get all movie information
|
||||
movies_raw = db.query(Movie) \
|
||||
.options(joinedload_all('library.titles')) \
|
||||
.options(joinedload_all('library.files')) \
|
||||
.options(joinedload_all('files')) \
|
||||
.filter(Movie.id.in_(movie_ids)) \
|
||||
.all()
|
||||
|
||||
# Create dict by movie id
|
||||
movie_dict = {}
|
||||
for movie in movies_raw:
|
||||
movie_dict[movie.id] = movie
|
||||
|
||||
for movie_id in movie_ids:
|
||||
movies.append(movie_dict[movie_id].to_dict({
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {},
|
||||
}))
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'movies': movies,
|
||||
})
|
||||
}
|
||||
|
||||
getLateView = getSoonView
|
||||
|
||||
@@ -2,15 +2,13 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.helpers.variable import md5, getExt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.plugins.scanner.main import Scanner
|
||||
from couchpotato.core.settings.model import FileType, File
|
||||
from couchpotato.environment import Env
|
||||
from flask.helpers import send_file
|
||||
from werkzeug.exceptions import NotFound
|
||||
from tornado.web import StaticFileHandler
|
||||
import os.path
|
||||
import time
|
||||
import traceback
|
||||
@@ -25,7 +23,7 @@ class FileManager(Plugin):
|
||||
addEvent('file.download', self.download)
|
||||
addEvent('file.types', self.getTypes)
|
||||
|
||||
addApiView('file.cache/<path:filename>', self.showCacheFile, static = True, docs = {
|
||||
addApiView('file.cache/(.*)', self.showCacheFile, static = True, docs = {
|
||||
'desc': 'Return a file from the cp_data/cache directory',
|
||||
'params': {
|
||||
'filename': {'desc': 'path/filename of the wanted file'}
|
||||
@@ -73,25 +71,20 @@ class FileManager(Plugin):
|
||||
db = get_session()
|
||||
for root, dirs, walk_files in os.walk(Env.get('cache_dir')):
|
||||
for filename in walk_files:
|
||||
if root == python_cache or 'minified' in filename or 'version' in filename: continue
|
||||
file_path = os.path.join(root, filename)
|
||||
f = db.query(File).filter(File.path == toUnicode(file_path)).first()
|
||||
if not f:
|
||||
os.remove(file_path)
|
||||
if os.path.splitext(filename)[1] in ['.png', '.jpg', '.jpeg']:
|
||||
file_path = os.path.join(root, filename)
|
||||
f = db.query(File).filter(File.path == toUnicode(file_path)).first()
|
||||
if not f:
|
||||
os.remove(file_path)
|
||||
except:
|
||||
log.error('Failed removing unused file: %s', traceback.format_exc())
|
||||
|
||||
def showCacheFile(self, filename = ''):
|
||||
def showCacheFile(self, route, **kwargs):
|
||||
Env.get('app').add_handlers(".*$", [('%s%s' % (Env.get('api_base'), route), StaticFileHandler, {'path': Env.get('cache_dir')})])
|
||||
|
||||
file_path = os.path.join(Env.get('cache_dir'), os.path.basename(filename))
|
||||
|
||||
if not os.path.isfile(file_path):
|
||||
log.error('File "%s" not found', file_path)
|
||||
raise NotFound()
|
||||
|
||||
return send_file(file_path, conditional = True)
|
||||
|
||||
def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}):
|
||||
def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = None):
|
||||
if not urlopen_kwargs: urlopen_kwargs = {}
|
||||
|
||||
if not dest: # to Cache
|
||||
dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url)))
|
||||
@@ -108,7 +101,9 @@ class FileManager(Plugin):
|
||||
self.createFile(dest, filedata, binary = True)
|
||||
return dest
|
||||
|
||||
def add(self, path = '', part = 1, type_tuple = (), available = 1, properties = {}):
|
||||
def add(self, path = '', part = 1, type_tuple = (), available = 1, properties = None):
|
||||
if not properties: properties = {}
|
||||
|
||||
type_id = self.getType(type_tuple).get('id')
|
||||
db = get_session()
|
||||
|
||||
@@ -158,8 +153,8 @@ class FileManager(Plugin):
|
||||
|
||||
return types
|
||||
|
||||
def getTypesView(self):
|
||||
def getTypesView(self, **kwargs):
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'types': self.getTypes()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
var File = new Class({
|
||||
|
||||
initialize: function(file){
|
||||
initialize: function(type, file){
|
||||
var self = this;
|
||||
|
||||
if(!file){
|
||||
self.empty = true;
|
||||
self.el = new Element('div');
|
||||
self.el = new Element('div.empty_file.'+type);
|
||||
return
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ var File = new Class({
|
||||
var file_name = self.data.path.replace(/^.*[\\\/]/, '');
|
||||
|
||||
self.el = new Element('div', {
|
||||
'class': 'type_image ' + self.type.identifier
|
||||
'class': 'type_image ' + self.type.identifier,
|
||||
'styles': {
|
||||
'background-image': 'url('+Api.createUrl('file.cache') + file_name+')'
|
||||
}
|
||||
}).adopt(
|
||||
new Element('img', {
|
||||
'src': Api.createUrl('file.cache') + file_name
|
||||
@@ -45,7 +48,7 @@ var FileSelect = new Class({
|
||||
});
|
||||
|
||||
if(single)
|
||||
return new File(results.pop());
|
||||
return new File(type, results.pop());
|
||||
|
||||
return results;
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
from .main import LibraryPlugin
|
||||
|
||||
def start():
|
||||
return LibraryPlugin()
|
||||
|
||||
config = []
|
||||
@@ -1,6 +1,5 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified, getParam, getParams
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -47,9 +46,9 @@ class Logging(Plugin):
|
||||
}
|
||||
})
|
||||
|
||||
def get(self):
|
||||
def get(self, nr = 0, **kwargs):
|
||||
|
||||
nr = int(getParam('nr', 0))
|
||||
nr = tryInt(nr)
|
||||
current_path = None
|
||||
|
||||
total = 1
|
||||
@@ -71,16 +70,15 @@ class Logging(Plugin):
|
||||
f = open(current_path, 'r')
|
||||
log = f.read()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'log': toUnicode(log),
|
||||
'total': total,
|
||||
})
|
||||
}
|
||||
|
||||
def partial(self):
|
||||
def partial(self, type = 'all', lines = 30, **kwargs):
|
||||
|
||||
log_type = getParam('type', 'all')
|
||||
total_lines = tryInt(getParam('lines', 30))
|
||||
total_lines = tryInt(lines)
|
||||
|
||||
log_lines = []
|
||||
|
||||
@@ -92,7 +90,6 @@ class Logging(Plugin):
|
||||
if not os.path.isfile(path):
|
||||
break
|
||||
|
||||
reversed_lines = []
|
||||
f = open(path, 'r')
|
||||
reversed_lines = toUnicode(f.read()).split('[0m\n')
|
||||
reversed_lines.reverse()
|
||||
@@ -100,7 +97,7 @@ class Logging(Plugin):
|
||||
brk = False
|
||||
for line in reversed_lines:
|
||||
|
||||
if log_type == 'all' or '%s ' % log_type.upper() in line:
|
||||
if type == 'all' or '%s ' % type.upper() in line:
|
||||
log_lines.append(line)
|
||||
|
||||
if len(log_lines) >= total_lines:
|
||||
@@ -111,18 +108,18 @@ class Logging(Plugin):
|
||||
break
|
||||
|
||||
log_lines.reverse()
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'log': '[0m\n'.join(log_lines),
|
||||
})
|
||||
}
|
||||
|
||||
def clear(self):
|
||||
def clear(self, **kwargs):
|
||||
|
||||
for x in range(0, 50):
|
||||
path = '%s%s' % (Env.get('log_path'), '.%s' % x if x > 0 else '')
|
||||
|
||||
if not os.path.isfile(path):
|
||||
break
|
||||
continue
|
||||
|
||||
try:
|
||||
|
||||
@@ -135,24 +132,21 @@ class Logging(Plugin):
|
||||
except:
|
||||
log.error('Couldn\'t delete file "%s": %s', (path, traceback.format_exc()))
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def log(self):
|
||||
|
||||
params = getParams()
|
||||
def log(self, type = 'error', **kwargs):
|
||||
|
||||
try:
|
||||
log_message = 'API log: %s' % params
|
||||
log_message = 'API log: %s' % kwargs
|
||||
try:
|
||||
getattr(log, params.get('type', 'error'))(log_message)
|
||||
getattr(log, type)(log_message)
|
||||
except:
|
||||
log.error(log_message)
|
||||
except:
|
||||
log.error('Couldn\'t log via API: %s', params)
|
||||
log.error('Couldn\'t log via API: %s', kwargs)
|
||||
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,19 +6,6 @@ Page.Log = new Class({
|
||||
title: 'Show recent logs.',
|
||||
has_tab: false,
|
||||
|
||||
initialize: function(options){
|
||||
var self = this;
|
||||
self.parent(options)
|
||||
|
||||
|
||||
App.getBlock('more').addLink(new Element('a', {
|
||||
'href': App.createUrl(self.name),
|
||||
'text': self.name.capitalize(),
|
||||
'title': self.title
|
||||
}))
|
||||
|
||||
},
|
||||
|
||||
indexAction: function(){
|
||||
var self = this;
|
||||
|
||||
|
||||
@@ -28,6 +28,14 @@ config = [{
|
||||
'description': 'Remove movie from db if it can\'t be found after re-scan.',
|
||||
'default': True,
|
||||
},
|
||||
{
|
||||
'label': 'Scan at startup',
|
||||
'name': 'startup_scan',
|
||||
'type': 'bool',
|
||||
'default': True,
|
||||
'advanced': True,
|
||||
'description': 'Do a quick scan on startup. On slow systems better disable this.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import splitString, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -27,7 +26,8 @@ class Manage(Plugin):
|
||||
addEvent('manage.diskspace', self.getDiskSpace)
|
||||
|
||||
# Add files after renaming
|
||||
def after_rename(message = None, group = {}):
|
||||
def after_rename(message = None, group = None):
|
||||
if not group: group = {}
|
||||
return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'])
|
||||
addEvent('renamer.after', after_rename, priority = 110)
|
||||
|
||||
@@ -45,25 +45,24 @@ class Manage(Plugin):
|
||||
}"""},
|
||||
})
|
||||
|
||||
if not Env.get('dev'):
|
||||
def updateLibrary():
|
||||
self.updateLibrary(full = False)
|
||||
addEvent('app.load', updateLibrary)
|
||||
if not Env.get('dev') and self.conf('startup_scan'):
|
||||
addEvent('app.load', self.updateLibraryQuick)
|
||||
|
||||
def getProgress(self):
|
||||
return jsonified({
|
||||
def getProgress(self, **kwargs):
|
||||
return {
|
||||
'progress': self.in_progress
|
||||
})
|
||||
}
|
||||
|
||||
def updateLibraryView(self):
|
||||
def updateLibraryView(self, full = 1, **kwargs):
|
||||
|
||||
full = getParam('full', default = 1)
|
||||
fireEventAsync('manage.update', full = True if full == '1' else False)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def updateLibraryQuick(self):
|
||||
return self.updateLibrary(full = False)
|
||||
|
||||
def updateLibrary(self, full = True):
|
||||
last_update = float(Env.prop('manage.last_update', default = 0))
|
||||
@@ -119,7 +118,9 @@ class Manage(Plugin):
|
||||
fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
|
||||
else:
|
||||
|
||||
for release in done_movie.get('releases', []):
|
||||
releases = fireEvent('release.for_movie', id = done_movie.get('id'), single = True)
|
||||
|
||||
for release in releases:
|
||||
if len(release.get('files', [])) == 0:
|
||||
fireEvent('release.delete', release['id'])
|
||||
else:
|
||||
@@ -130,9 +131,9 @@ class Manage(Plugin):
|
||||
break
|
||||
|
||||
# Check if there are duplicate releases (different quality) use the last one, delete the rest
|
||||
if len(done_movie.get('releases', [])) > 1:
|
||||
if len(releases) > 1:
|
||||
used_files = {}
|
||||
for release in done_movie.get('releases', []):
|
||||
for release in releases:
|
||||
|
||||
for release_file in release.get('files', []):
|
||||
already_used = used_files.get(release_file['path'])
|
||||
@@ -171,6 +172,7 @@ class Manage(Plugin):
|
||||
self.in_progress = False
|
||||
|
||||
def createAddToLibrary(self, folder, added_identifiers = []):
|
||||
|
||||
def addToLibrary(group, total_found, to_go):
|
||||
if self.in_progress[folder]['total'] is None:
|
||||
self.in_progress[folder] = {
|
||||
@@ -184,9 +186,9 @@ class Manage(Plugin):
|
||||
|
||||
# Add it to release and update the info
|
||||
fireEvent('release.add', group = group)
|
||||
fireEventAsync('library.update', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier))
|
||||
fireEventAsync('library.update.movie', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier))
|
||||
else:
|
||||
self.in_progress[folder]['to_go'] = self.in_progress[folder]['to_go'] - 1
|
||||
self.in_progress[folder]['to_go'] -= 1
|
||||
|
||||
return addToLibrary
|
||||
|
||||
@@ -194,7 +196,10 @@ class Manage(Plugin):
|
||||
|
||||
# Notify frontend
|
||||
def afterUpdate():
|
||||
self.in_progress[folder]['to_go'] = self.in_progress[folder]['to_go'] - 1
|
||||
if not self.in_progress or self.shuttingDown():
|
||||
return
|
||||
|
||||
self.in_progress[folder]['to_go'] -= 1
|
||||
total = self.in_progress[folder]['total']
|
||||
movie_dict = fireEvent('movie.get', identifier, single = True)
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
from .main import MoviePlugin
|
||||
|
||||
def start():
|
||||
return MoviePlugin()
|
||||
|
||||
config = []
|
||||
@@ -2,10 +2,10 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified, getParams, getParam
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Profile, ProfileType, Movie
|
||||
from sqlalchemy.orm import joinedload_all
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -46,48 +46,49 @@ class ProfilePlugin(Plugin):
|
||||
movie.profile_id = default_profile.get('id')
|
||||
db.commit()
|
||||
|
||||
def allView(self):
|
||||
def allView(self, **kwargs):
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'list': self.all()
|
||||
})
|
||||
}
|
||||
|
||||
def all(self):
|
||||
|
||||
db = get_session()
|
||||
profiles = db.query(Profile).all()
|
||||
profiles = db.query(Profile) \
|
||||
.options(joinedload_all('types')) \
|
||||
.all()
|
||||
|
||||
temp = []
|
||||
for profile in profiles:
|
||||
temp.append(profile.to_dict(self.to_dict))
|
||||
|
||||
db.expire_all()
|
||||
return temp
|
||||
|
||||
def save(self):
|
||||
|
||||
params = getParams()
|
||||
def save(self, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
|
||||
p = db.query(Profile).filter_by(id = params.get('id')).first()
|
||||
p = db.query(Profile).filter_by(id = kwargs.get('id')).first()
|
||||
if not p:
|
||||
p = Profile()
|
||||
db.add(p)
|
||||
|
||||
p.label = toUnicode(params.get('label'))
|
||||
p.order = params.get('order', p.order if p.order else 0)
|
||||
p.core = params.get('core', False)
|
||||
p.label = toUnicode(kwargs.get('label'))
|
||||
p.order = kwargs.get('order', p.order if p.order else 0)
|
||||
p.core = kwargs.get('core', False)
|
||||
|
||||
#delete old types
|
||||
[db.delete(t) for t in p.types]
|
||||
|
||||
order = 0
|
||||
for type in params.get('types', []):
|
||||
for type in kwargs.get('types', []):
|
||||
t = ProfileType(
|
||||
order = order,
|
||||
finish = type.get('finish') if order > 0 else 1,
|
||||
wait_for = params.get('wait_for'),
|
||||
wait_for = kwargs.get('wait_for'),
|
||||
quality_id = type.get('quality_id')
|
||||
)
|
||||
p.types.append(t)
|
||||
@@ -98,41 +99,41 @@ class ProfilePlugin(Plugin):
|
||||
|
||||
profile_dict = p.to_dict(self.to_dict)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'profile': profile_dict
|
||||
})
|
||||
}
|
||||
|
||||
def default(self):
|
||||
|
||||
db = get_session()
|
||||
default = db.query(Profile).first()
|
||||
default = db.query(Profile) \
|
||||
.options(joinedload_all('types')) \
|
||||
.first()
|
||||
default_dict = default.to_dict(self.to_dict)
|
||||
|
||||
db.expire_all()
|
||||
return default_dict
|
||||
|
||||
def saveOrder(self):
|
||||
def saveOrder(self, **kwargs):
|
||||
|
||||
params = getParams()
|
||||
db = get_session()
|
||||
|
||||
order = 0
|
||||
for profile in params.get('ids', []):
|
||||
for profile in kwargs.get('ids', []):
|
||||
p = db.query(Profile).filter_by(id = profile).first()
|
||||
p.hide = params.get('hidden')[order]
|
||||
p.hide = kwargs.get('hidden')[order]
|
||||
p.order = order
|
||||
|
||||
order += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def delete(self):
|
||||
|
||||
id = getParam('id')
|
||||
def delete(self, id = None, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -151,14 +152,15 @@ class ProfilePlugin(Plugin):
|
||||
except Exception, e:
|
||||
message = log.error('Failed deleting Profile: %s', e)
|
||||
|
||||
return jsonified({
|
||||
db.expire_all()
|
||||
return {
|
||||
'success': success,
|
||||
'message': message
|
||||
})
|
||||
}
|
||||
|
||||
def fill(self):
|
||||
|
||||
db = get_session();
|
||||
db = get_session()
|
||||
|
||||
profiles = [{
|
||||
'label': 'Best',
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
|
||||
.profile > .delete {
|
||||
position: absolute;
|
||||
padding: 25px 20px;
|
||||
background-position: center;
|
||||
padding: 16px;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
opacity: 0.6;
|
||||
color: #fd5353;
|
||||
}
|
||||
.profile > .delete:hover {
|
||||
opacity: 1;
|
||||
@@ -29,6 +29,7 @@
|
||||
|
||||
.profile .qualities {
|
||||
min-height: 80px;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.profile .formHint {
|
||||
@@ -97,11 +98,13 @@
|
||||
}
|
||||
|
||||
.profile .types .type .delete {
|
||||
background-position: left center;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
line-height: 20px;
|
||||
visibility: hidden;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: #fd5353;
|
||||
}
|
||||
|
||||
.profile .types .type:hover:not(.is_empty) .delete {
|
||||
|
||||
@@ -24,7 +24,7 @@ var Profile = new Class({
|
||||
var data = self.data;
|
||||
|
||||
self.el = new Element('div.profile').adopt(
|
||||
self.delete_button = new Element('span.delete.icon', {
|
||||
self.delete_button = new Element('span.delete.icon2', {
|
||||
'events': {
|
||||
'click': self.del.bind(self)
|
||||
}
|
||||
@@ -256,7 +256,7 @@ Profile.Type = new Class({
|
||||
}
|
||||
})
|
||||
),
|
||||
new Element('span.delete.icon', {
|
||||
new Element('span.delete.icon2', {
|
||||
'events': {
|
||||
'click': self.del.bind(self)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,11 @@ 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, getParams
|
||||
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
|
||||
|
||||
@@ -19,13 +17,13 @@ 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, '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': '1080p', 'hd': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
|
||||
{'identifier': '720p', 'hd': True, 'size': (3000, 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'], 'tags': ['hdtv', 'hdrip', 'webdl', ('web', 'dl')]},
|
||||
{'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'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
|
||||
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
|
||||
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener', 'hdscr'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg'], 'tags': ['webrip', ('web', 'rip')]},
|
||||
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], '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', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
|
||||
@@ -52,12 +50,12 @@ class QualityPlugin(Plugin):
|
||||
def preReleases(self):
|
||||
return self.pre_releases
|
||||
|
||||
def allView(self):
|
||||
def allView(self, **kwargs):
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'list': self.all()
|
||||
})
|
||||
}
|
||||
|
||||
def all(self):
|
||||
|
||||
@@ -89,24 +87,22 @@ class QualityPlugin(Plugin):
|
||||
if identifier == q.get('identifier'):
|
||||
return q
|
||||
|
||||
def saveSize(self):
|
||||
|
||||
params = getParams()
|
||||
def saveSize(self, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
quality = db.query(Quality).filter_by(identifier = params.get('identifier')).first()
|
||||
quality = db.query(Quality).filter_by(identifier = kwargs.get('identifier')).first()
|
||||
|
||||
if quality:
|
||||
setattr(quality, params.get('value_type'), params.get('value'))
|
||||
setattr(quality, kwargs.get('value_type'), kwargs.get('value'))
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def fill(self):
|
||||
|
||||
db = get_session();
|
||||
db = get_session()
|
||||
|
||||
order = 0
|
||||
for q in self.qualities:
|
||||
@@ -156,61 +152,77 @@ class QualityPlugin(Plugin):
|
||||
|
||||
return True
|
||||
|
||||
def guess(self, files, extra = {}):
|
||||
def guess(self, files, extra = None):
|
||||
if not extra: extra = {}
|
||||
|
||||
# Create hash for cache
|
||||
hash = md5(str([f.replace('.' + getExt(f), '') for f in files]))
|
||||
cached = self.getCache(hash)
|
||||
if cached and extra is {}: return cached
|
||||
cache_key = md5(str([f.replace('.' + getExt(f), '') for f in files]))
|
||||
cached = self.getCache(cache_key)
|
||||
if cached and len(extra) == 0: return cached
|
||||
|
||||
qualities = self.all()
|
||||
for cur_file in files:
|
||||
size = (os.path.getsize(cur_file) / 1024 / 1024) if os.path.isfile(cur_file) else 0
|
||||
words = re.split('\W+', cur_file.lower())
|
||||
|
||||
for quality in self.all():
|
||||
found = {}
|
||||
for quality in qualities:
|
||||
contains = self.containsTag(quality, words, cur_file)
|
||||
if contains:
|
||||
found[quality['identifier']] = True
|
||||
|
||||
# Check tags
|
||||
for quality in qualities:
|
||||
|
||||
# Check identifier
|
||||
if quality['identifier'] in words:
|
||||
log.debug('Found via identifier "%s" in %s', (quality['identifier'], cur_file))
|
||||
return self.setCache(hash, quality)
|
||||
if len(found) == 0 or len(found) == 1 and found.get(quality['identifier']):
|
||||
log.debug('Found via identifier "%s" in %s', (quality['identifier'], cur_file))
|
||||
return self.setCache(cache_key, quality)
|
||||
|
||||
if list(set(quality.get('alternative', [])) & set(words)):
|
||||
log.debug('Found %s via alt %s in %s', (quality['identifier'], quality.get('alternative'), cur_file))
|
||||
return self.setCache(hash, quality)
|
||||
|
||||
for tag in quality.get('tags', []):
|
||||
if isinstance(tag, tuple) and '.'.join(tag) in '.'.join(words):
|
||||
log.debug('Found %s via tag %s in %s', (quality['identifier'], quality.get('tags'), cur_file))
|
||||
return self.setCache(hash, quality)
|
||||
|
||||
if list(set(quality.get('tags', [])) & set(words)):
|
||||
log.debug('Found %s via tag %s in %s', (quality['identifier'], quality.get('tags'), cur_file))
|
||||
return self.setCache(hash, quality)
|
||||
# Check alt and tags
|
||||
contains = self.containsTag(quality, words, cur_file)
|
||||
if contains:
|
||||
return self.setCache(cache_key, quality)
|
||||
|
||||
# Try again with loose testing
|
||||
quality = self.guessLoose(hash, extra = extra)
|
||||
quality = self.guessLoose(cache_key, files = files, extra = extra)
|
||||
if quality:
|
||||
return self.setCache(hash, quality)
|
||||
return self.setCache(cache_key, quality)
|
||||
|
||||
log.debug('Could not identify quality for: %s', files)
|
||||
return None
|
||||
|
||||
def guessLoose(self, hash, extra):
|
||||
def containsTag(self, quality, words, cur_file = ''):
|
||||
|
||||
for quality in self.all():
|
||||
# Check alt and tags
|
||||
for tag_type in ['alternative', 'tags']:
|
||||
for alt in quality.get(tag_type, []):
|
||||
if isinstance(alt, tuple) and '.'.join(alt) in '.'.join(words):
|
||||
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
|
||||
return True
|
||||
|
||||
# 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)
|
||||
if list(set(quality.get(tag_type, [])) & set(words)):
|
||||
log.debug('Found %s via %s %s in %s', (quality['identifier'], tag_type, quality.get(tag_type), cur_file))
|
||||
return True
|
||||
|
||||
# Check height resolution, range 20
|
||||
if (quality.get('height', 480) - 20) <= extra.get('resolution_height', 0) <= (quality.get('height', 480) + 20):
|
||||
log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height', 480), extra.get('resolution_height', 0)))
|
||||
return self.setCache(hash, quality)
|
||||
return
|
||||
|
||||
if 480 <= extra.get('resolution_width', 0) <= 720:
|
||||
log.debug('Found as dvdrip')
|
||||
return self.setCache(hash, self.single('dvdrip'))
|
||||
def guessLoose(self, cache_key, files = None, extra = None):
|
||||
|
||||
if extra:
|
||||
for quality in self.all():
|
||||
|
||||
# Check width resolution, range 20
|
||||
if quality.get('width') and (quality.get('width') - 20) <= extra.get('resolution_width', 0) <= (quality.get('width') + 20):
|
||||
log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width'), extra.get('resolution_width', 0)))
|
||||
return self.setCache(cache_key, quality)
|
||||
|
||||
# Check height resolution, range 20
|
||||
if quality.get('height') and (quality.get('height') - 20) <= extra.get('resolution_height', 0) <= (quality.get('height') + 20):
|
||||
log.debug('Found %s via resolution_height: %s == %s', (quality['identifier'], quality.get('height'), extra.get('resolution_height', 0)))
|
||||
return self.setCache(cache_key, quality)
|
||||
|
||||
if 480 <= extra.get('resolution_width', 0) <= 720:
|
||||
log.debug('Found as dvdrip')
|
||||
return self.setCache(cache_key, self.single('dvdrip'))
|
||||
|
||||
return None
|
||||
|
||||
@@ -41,7 +41,8 @@ var QualityBase = new Class({
|
||||
self.settings.addEvent('create', function(){
|
||||
var tab = self.settings.createSubTab('profile', {
|
||||
'label': 'Quality',
|
||||
'name': 'profile'
|
||||
'name': 'profile',
|
||||
'subtab_label': 'Qualities'
|
||||
}, self.settings.tabs.searcher ,'searcher');
|
||||
|
||||
self.tab = tab.tab;
|
||||
@@ -102,7 +103,8 @@ var QualityBase = new Class({
|
||||
|
||||
var profile_list;
|
||||
var group = self.settings.createGroup({
|
||||
'label': 'Profile Defaults'
|
||||
'label': 'Profile Defaults',
|
||||
'description': '(Needs refresh \'' +(App.isMac() ? 'CMD+R' : 'F5')+ '\' after editing)'
|
||||
}).adopt(
|
||||
new Element('.ctrlHolder#profile_ordering').adopt(
|
||||
new Element('label[text=Order]'),
|
||||
|
||||
@@ -2,13 +2,14 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.request import getParam, jsonified
|
||||
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 File, Release as Relea, Movie
|
||||
from sqlalchemy.orm import joinedload_all
|
||||
from sqlalchemy.sql.expression import and_, or_
|
||||
import os
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -36,7 +37,14 @@ class Release(Plugin):
|
||||
'id': {'type': 'id', 'desc': 'ID of the release object in release-table'}
|
||||
}
|
||||
})
|
||||
addApiView('release.for_movie', self.forMovieView, docs = {
|
||||
'desc': 'Returns all releases for a movie. Ordered by score(desc)',
|
||||
'params': {
|
||||
'id': {'type': 'id', 'desc': 'ID of the movie'}
|
||||
}
|
||||
})
|
||||
|
||||
addEvent('release.for_movie', self.forMovie)
|
||||
addEvent('release.delete', self.delete)
|
||||
addEvent('release.clean', self.clean)
|
||||
|
||||
@@ -89,8 +97,8 @@ class Release(Plugin):
|
||||
added_files = db.query(File).filter(or_(*[File.id == x for x in added_files])).all()
|
||||
rel.files.extend(added_files)
|
||||
db.commit()
|
||||
except Exception, e:
|
||||
log.debug('Failed to attach "%s" to release: %s', (cur_file, e))
|
||||
except:
|
||||
log.debug('Failed to attach "%s" to release: %s', (added_files, traceback.format_exc()))
|
||||
|
||||
fireEvent('movie.restatus', movie.id)
|
||||
|
||||
@@ -108,13 +116,11 @@ class Release(Plugin):
|
||||
# Check database and update/insert if necessary
|
||||
return fireEvent('file.add', path = filepath, part = fireEvent('scanner.partnumber', file, single = True), type_tuple = Scanner.file_types.get(type), properties = properties, single = True)
|
||||
|
||||
def deleteView(self):
|
||||
def deleteView(self, id = None, **kwargs):
|
||||
|
||||
release_id = getParam('id')
|
||||
|
||||
return jsonified({
|
||||
'success': self.delete(release_id)
|
||||
})
|
||||
return {
|
||||
'success': self.delete(id)
|
||||
}
|
||||
|
||||
def delete(self, id):
|
||||
|
||||
@@ -146,25 +152,23 @@ class Release(Plugin):
|
||||
|
||||
return False
|
||||
|
||||
def ignore(self):
|
||||
def ignore(self, id = None, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
id = getParam('id')
|
||||
|
||||
rel = db.query(Relea).filter_by(id = id).first()
|
||||
if rel:
|
||||
ignored_status, available_status = fireEvent('status.get', ['ignored', 'available'], single = True)
|
||||
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id')
|
||||
ignored_status, failed_status, available_status = fireEvent('status.get', ['ignored', 'failed', 'available'], single = True)
|
||||
rel.status_id = available_status.get('id') if rel.status_id in [ignored_status.get('id'), failed_status.get('id')] else ignored_status.get('id')
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def download(self):
|
||||
def download(self, id = None, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
id = getParam('id')
|
||||
|
||||
snatched_status, done_status = fireEvent('status.get', ['snatched', 'done'], single = True)
|
||||
|
||||
@@ -179,8 +183,12 @@ class Release(Plugin):
|
||||
# Get matching provider
|
||||
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
|
||||
|
||||
if item['type'] != 'torrent_magnet':
|
||||
item['download'] = provider.download
|
||||
if not item.get('protocol'):
|
||||
item['protocol'] = item['type']
|
||||
item['type'] = 'movie'
|
||||
|
||||
if item.get('protocol') != 'torrent_magnet':
|
||||
item['download'] = provider.loginDownload if provider.urls.get('login') else provider.download
|
||||
|
||||
success = fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({
|
||||
'profile': {'types': {'quality': {}}},
|
||||
@@ -199,12 +207,37 @@ class Release(Plugin):
|
||||
|
||||
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name'])
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': success
|
||||
})
|
||||
}
|
||||
else:
|
||||
log.error('Couldn\'t find release with id: %s', id)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': False
|
||||
})
|
||||
}
|
||||
|
||||
def forMovie(self, id = None):
|
||||
|
||||
db = get_session()
|
||||
|
||||
releases_raw = db.query(Relea) \
|
||||
.options(joinedload_all('info')) \
|
||||
.options(joinedload_all('files')) \
|
||||
.filter(Relea.movie_id == id) \
|
||||
.all()
|
||||
|
||||
releases = [r.to_dict({'info':{}, 'files':{}}) for r in releases_raw]
|
||||
releases = sorted(releases, key = lambda k: k['info'].get('score', 0), reverse = True)
|
||||
|
||||
return releases
|
||||
|
||||
def forMovieView(self, id = None, **kwargs):
|
||||
|
||||
releases = self.forMovie(id)
|
||||
|
||||
return {
|
||||
'releases': releases,
|
||||
'success': True
|
||||
}
|
||||
|
||||
|
||||
27
couchpotato/core/plugins/renamer/__init__.py
Normal file → Executable file
27
couchpotato/core/plugins/renamer/__init__.py
Normal file → Executable file
@@ -14,15 +14,20 @@ rename_options = {
|
||||
'year': 'Year (2011)',
|
||||
'first': 'First letter (M)',
|
||||
'quality': 'Quality (720p)',
|
||||
'quality_type': '(HD) or (SD)',
|
||||
'video': 'Video (x264)',
|
||||
'audio': 'Audio (DTS)',
|
||||
'group': 'Releasegroup name',
|
||||
'source': 'Source media (Bluray)',
|
||||
'resolution_width': 'resolution width (1280)',
|
||||
'resolution_height': 'resolution height (720)',
|
||||
'audio_channels': 'audio channels (7.1)',
|
||||
'original': 'Original filename',
|
||||
'original_folder': 'Original foldername',
|
||||
'imdb_id': 'IMDB id (tt0123456)',
|
||||
'cd': 'CD number (cd1)',
|
||||
'cd_nr': 'Just the cd nr. (1)',
|
||||
'mpaa': 'MPAA Rating',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -50,7 +55,7 @@ config = [{
|
||||
{
|
||||
'name': 'to',
|
||||
'type': 'directory',
|
||||
'description': 'Folder where the movies should be moved to.',
|
||||
'description': 'Default folder where the movies are moved to.',
|
||||
},
|
||||
{
|
||||
'name': 'folder_name',
|
||||
@@ -68,6 +73,12 @@ config = [{
|
||||
'type': 'choice',
|
||||
'options': rename_options
|
||||
},
|
||||
{
|
||||
'name': 'unrar',
|
||||
'type': 'bool',
|
||||
'description': 'Extract rar files if found.',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'cleanup',
|
||||
'type': 'bool',
|
||||
@@ -109,16 +120,22 @@ config = [{
|
||||
{
|
||||
'advanced': True,
|
||||
'name': 'separator',
|
||||
'label': 'Separator',
|
||||
'label': 'File-Separator',
|
||||
'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.',
|
||||
},
|
||||
{
|
||||
'advanced': True,
|
||||
'name': 'foldersep',
|
||||
'label': 'Folder-Separator',
|
||||
'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.',
|
||||
},
|
||||
{
|
||||
'name': 'file_action',
|
||||
'label': 'Torrent File Action',
|
||||
'default': 'move',
|
||||
'default': 'link',
|
||||
'type': 'dropdown',
|
||||
'values': [('Move', 'move'), ('Copy', 'copy'), ('Hard link', 'hardlink'), ('Sym link', 'symlink'), ('Move & Sym link', 'move_symlink')],
|
||||
'description': 'Define which kind of file operation you want to use for torrents. Before you start using <a href="http://en.wikipedia.org/wiki/Hard_link">hard links</a> or <a href="http://en.wikipedia.org/wiki/Sym_link">sym links</a>, PLEASE read about their possible drawbacks.',
|
||||
'values': [('Link', 'link'), ('Copy', 'copy'), ('Move', 'move')],
|
||||
'description': '<strong>Link</strong> or <strong>Copy</strong> after downloading completed (and allow for seeding), or <strong>Move</strong> after seeding completed. Link first tries <a href="http://en.wikipedia.org/wiki/Hard_link">hard link</a>, then <a href="http://en.wikipedia.org/wiki/Sym_link">sym link</a> and falls back to Copy.',
|
||||
'advanced': True,
|
||||
},
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user