Compare commits
97
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b7376fd18 | ||
|
|
c31b10c798 | ||
|
|
acda664686 | ||
|
|
99123ad1c3 | ||
|
|
cdf9cf5cf4 | ||
|
|
797dedfcbb | ||
|
|
b61de4866c | ||
|
|
931951ff37 | ||
|
|
6f42b4c316 | ||
|
|
58c446de2d | ||
|
|
74bf6bc411 | ||
|
|
ad3c24f950 | ||
|
|
93346b0c63 | ||
|
|
b1942678b4 | ||
|
|
8c77d0d775 | ||
|
|
3e667ee39a | ||
|
|
52b2858ac2 | ||
|
|
6fcb4c2058 | ||
|
|
2e8f670e94 | ||
|
|
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 | ||
|
|
c9a4af218e | ||
|
|
c5c2e61e06 | ||
|
|
b2930dd6a7 | ||
|
|
4aa6700ceb | ||
|
|
267ecfacab | ||
|
|
5699abf1be | ||
|
|
a6ccd037e2 | ||
|
|
009991ce4c | ||
|
|
6ef788a8f4 | ||
|
|
fa37f7d40a | ||
|
|
b195cebac7 | ||
|
|
8aeea60888 | ||
|
|
6e0857c6c1 | ||
|
|
260fdbe3b3 | ||
|
|
2f30c6c781 | ||
|
|
d5b4da655a | ||
|
|
1694ed7758 | ||
|
|
ee6cc6d319 | ||
|
|
7670e320ba | ||
|
|
15ab745bd0 | ||
|
|
7468b33991 | ||
|
|
750e02f38a | ||
|
|
563e3072a5 |
@@ -40,3 +40,23 @@ Linux (ubuntu / debian):
|
|||||||
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
|
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
|
||||||
* Add it to defaults. `sudo update-rc.d couchpotato defaults`
|
* Add it to defaults. `sudo update-rc.d couchpotato defaults`
|
||||||
* Open your browser and go to: `http://localhost:5050/`
|
* 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/`
|
||||||
|
|||||||
+47
-46
@@ -1,84 +1,85 @@
|
|||||||
from couchpotato.api import api_docs, api_docs_missing
|
from couchpotato.api import api_docs, api_docs_missing, api
|
||||||
from couchpotato.core.auth import requires_auth
|
from couchpotato.core.auth import requires_auth
|
||||||
from couchpotato.core.event import fireEvent
|
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
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.environment import Env
|
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.engine import create_engine
|
||||||
from sqlalchemy.orm import scoped_session
|
from sqlalchemy.orm import scoped_session
|
||||||
from sqlalchemy.orm.session import sessionmaker
|
from sqlalchemy.orm.session import sessionmaker
|
||||||
from werkzeug.utils import redirect
|
from tornado import template
|
||||||
|
from tornado.web import RequestHandler
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
app = Flask(__name__, static_folder = 'nope')
|
views = {}
|
||||||
web = Blueprint('web', __name__)
|
template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates'))
|
||||||
|
|
||||||
|
# Main web handler
|
||||||
|
@requires_auth
|
||||||
|
class WebHandler(RequestHandler):
|
||||||
|
def get(self, route, *args, **kwargs):
|
||||||
|
route = route.strip('/')
|
||||||
|
if not views.get(route):
|
||||||
|
page_not_found(self)
|
||||||
|
return
|
||||||
|
self.write(views[route]())
|
||||||
|
|
||||||
|
def addView(route, func, static = False):
|
||||||
|
views[route] = func
|
||||||
|
|
||||||
def get_session(engine = None):
|
def get_session(engine = None):
|
||||||
return Env.getSession(engine)
|
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 view
|
||||||
@web.route('/')
|
|
||||||
@requires_auth
|
|
||||||
def index():
|
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 """
|
# API docs
|
||||||
@web.route('docs/')
|
|
||||||
@requires_auth
|
|
||||||
def apiDocs():
|
def apiDocs():
|
||||||
from couchpotato import app
|
|
||||||
routes = []
|
routes = []
|
||||||
for route, x in sorted(app.view_functions.iteritems()):
|
|
||||||
if route[0:4] == 'api.':
|
for route in api.iterkeys():
|
||||||
routes += [route[4:].replace('::', '.')]
|
routes.append(route)
|
||||||
|
|
||||||
if api_docs.get(''):
|
if api_docs.get(''):
|
||||||
del api_docs['']
|
del api_docs['']
|
||||||
del api_docs_missing['']
|
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/')
|
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)
|
||||||
def getApiKey():
|
|
||||||
|
|
||||||
api = None
|
addView('docs', apiDocs)
|
||||||
params = getParams()
|
|
||||||
username = Env.setting('username')
|
|
||||||
password = Env.setting('password')
|
|
||||||
|
|
||||||
if (params.get('u') == md5(username) or not username) and (params.get('p') == password or not password):
|
# Make non basic auth option to get api key
|
||||||
api = Env.setting('api_key')
|
class KeyHandler(RequestHandler):
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
api = None
|
||||||
|
username = Env.setting('username')
|
||||||
|
password = Env.setting('password')
|
||||||
|
|
||||||
return jsonified({
|
if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password):
|
||||||
'success': api is not None,
|
api = Env.setting('api_key')
|
||||||
'api_key': api
|
|
||||||
})
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
self.write({
|
||||||
def page_not_found(error):
|
'success': api is not None,
|
||||||
index_url = url_for('web.index')
|
'api_key': api
|
||||||
url = request.path[len(index_url):]
|
})
|
||||||
|
|
||||||
|
def page_not_found(rh):
|
||||||
|
index_url = Env.get('web_base')
|
||||||
|
url = rh.request.uri[len(index_url):]
|
||||||
|
|
||||||
if url[:3] != 'api':
|
if url[:3] != 'api':
|
||||||
if request.path != '/':
|
r = index_url + '#' + url.lstrip('/')
|
||||||
r = request.url.replace(request.path, index_url + '#' + url)
|
rh.redirect(r)
|
||||||
else:
|
|
||||||
r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url)
|
|
||||||
return redirect(r)
|
|
||||||
else:
|
else:
|
||||||
if not Env.get('dev'):
|
if not Env.get('dev'):
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
return 'Wrong API key used', 404
|
|
||||||
|
rh.set_status(404)
|
||||||
|
rh.write('Wrong API key used')
|
||||||
|
|
||||||
|
|||||||
+48
-20
@@ -1,20 +1,22 @@
|
|||||||
from flask.blueprints import Blueprint
|
from couchpotato.core.helpers.request import getParams
|
||||||
from flask.helpers import url_for
|
|
||||||
from tornado.web import RequestHandler, asynchronous
|
from tornado.web import RequestHandler, asynchronous
|
||||||
from werkzeug.utils import redirect
|
import json
|
||||||
|
import urllib
|
||||||
|
|
||||||
api = Blueprint('api', __name__)
|
api = {}
|
||||||
api_docs = {}
|
|
||||||
api_docs_missing = []
|
|
||||||
api_nonblock = {}
|
api_nonblock = {}
|
||||||
|
|
||||||
|
api_docs = {}
|
||||||
|
api_docs_missing = []
|
||||||
|
|
||||||
|
# NonBlock API handler
|
||||||
class NonBlockHandler(RequestHandler):
|
class NonBlockHandler(RequestHandler):
|
||||||
|
|
||||||
stoppers = []
|
stoppers = []
|
||||||
|
|
||||||
@asynchronous
|
@asynchronous
|
||||||
def get(self, route):
|
def get(self, route, *args, **kwargs):
|
||||||
|
route = route.strip('/')
|
||||||
start, stop = api_nonblock[route]
|
start, stop = api_nonblock[route]
|
||||||
self.stoppers.append(stop)
|
self.stoppers.append(stop)
|
||||||
|
|
||||||
@@ -32,14 +34,6 @@ class NonBlockHandler(RequestHandler):
|
|||||||
|
|
||||||
self.stoppers = []
|
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)
|
|
||||||
|
|
||||||
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
||||||
api_nonblock[route] = func_tuple
|
api_nonblock[route] = func_tuple
|
||||||
|
|
||||||
@@ -48,9 +42,43 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
|||||||
else:
|
else:
|
||||||
api_docs_missing.append(route)
|
api_docs_missing.append(route)
|
||||||
|
|
||||||
""" Api view """
|
# Blocking API handler
|
||||||
def index():
|
class ApiHandler(RequestHandler):
|
||||||
index_url = url_for('web.index')
|
|
||||||
return redirect(index_url + 'docs/')
|
|
||||||
|
|
||||||
addApiView('', index)
|
def get(self, route, *args, **kwargs):
|
||||||
|
route = route.strip('/')
|
||||||
|
if not api.get(route):
|
||||||
|
self.write('API call doesn\'t seem to exist')
|
||||||
|
return
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Check JSONP callback
|
||||||
|
result = api[route](**kwargs)
|
||||||
|
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)
|
||||||
|
|
||||||
|
def addApiView(route, func, static = False, docs = None, **kwargs):
|
||||||
|
|
||||||
|
if static: func(route)
|
||||||
|
else: api[route] = func
|
||||||
|
|
||||||
|
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.api import addApiView
|
||||||
from couchpotato.core.event import fireEvent, addEvent
|
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.helpers.variable import cleanHost, md5
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
@@ -68,12 +67,12 @@ class Core(Plugin):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def available(self):
|
def available(self, **kwargs):
|
||||||
return jsonified({
|
return {
|
||||||
'success': True
|
'success': True
|
||||||
})
|
}
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self, **kwargs):
|
||||||
if self.shutdown_started:
|
if self.shutdown_started:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -83,7 +82,7 @@ class Core(Plugin):
|
|||||||
|
|
||||||
return 'shutdown'
|
return 'shutdown'
|
||||||
|
|
||||||
def restart(self):
|
def restart(self, **kwargs):
|
||||||
if self.shutdown_started:
|
if self.shutdown_started:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -156,10 +155,10 @@ class Core(Plugin):
|
|||||||
host = 'localhost'
|
host = 'localhost'
|
||||||
port = Env.setting('port')
|
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):
|
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):
|
def version(self):
|
||||||
ver = fireEvent('updater.info', single = True)
|
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'])
|
return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash'])
|
||||||
|
|
||||||
def versionView(self):
|
def versionView(self, **kwargs):
|
||||||
return jsonified({
|
return {
|
||||||
'version': self.version()
|
'version': self.version()
|
||||||
})
|
}
|
||||||
|
|
||||||
def signalHandler(self):
|
def signalHandler(self):
|
||||||
if Env.get('daemonized'): return
|
if Env.get('daemonized'): return
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from minify.cssmin import cssmin
|
|||||||
from minify.jsmin import jsmin
|
from minify.jsmin import jsmin
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -122,7 +121,7 @@ class ClientScript(Plugin):
|
|||||||
# Combine all files together with some comments
|
# Combine all files together with some comments
|
||||||
data = ''
|
data = ''
|
||||||
for r in raw:
|
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'
|
data += r.get('data') + '\n\n'
|
||||||
|
|
||||||
self.createFile(out, data.strip())
|
self.createFile(out, data.strip())
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||||
from couchpotato.core.helpers.encoding import ss
|
from couchpotato.core.helpers.encoding import ss
|
||||||
from couchpotato.core.helpers.request import jsonified
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
@@ -36,7 +35,7 @@ class Updater(Plugin):
|
|||||||
addEvent('app.load', self.setCrons)
|
addEvent('app.load', self.setCrons)
|
||||||
addEvent('updater.info', self.info)
|
addEvent('updater.info', self.info)
|
||||||
|
|
||||||
addApiView('updater.info', self.getInfo, docs = {
|
addApiView('updater.info', self.info, docs = {
|
||||||
'desc': 'Get updater information',
|
'desc': 'Get updater information',
|
||||||
'return': {
|
'return': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
@@ -86,25 +85,24 @@ class Updater(Plugin):
|
|||||||
|
|
||||||
if self.updater.check():
|
if self.updater.check():
|
||||||
if not self.available_notified and self.conf('notification') and not self.conf('automatic'):
|
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
|
self.available_notified = True
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def info(self):
|
def info(self, **kwargs):
|
||||||
return self.updater.info()
|
return self.updater.info()
|
||||||
|
|
||||||
def getInfo(self):
|
def checkView(self, **kwargs):
|
||||||
return jsonified(self.updater.info())
|
return {
|
||||||
|
|
||||||
def checkView(self):
|
|
||||||
return jsonified({
|
|
||||||
'update_available': self.check(force = True),
|
'update_available': self.check(force = True),
|
||||||
'info': self.updater.info()
|
'info': self.updater.info()
|
||||||
})
|
}
|
||||||
|
|
||||||
def doUpdateView(self):
|
def doUpdateView(self, **kwargs):
|
||||||
|
|
||||||
self.check()
|
self.check()
|
||||||
if not self.updater.update_version:
|
if not self.updater.update_version:
|
||||||
@@ -119,9 +117,9 @@ class Updater(Plugin):
|
|||||||
if not success:
|
if not success:
|
||||||
success = True
|
success = True
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': success
|
'success': success
|
||||||
})
|
}
|
||||||
|
|
||||||
|
|
||||||
class BaseUpdater(Plugin):
|
class BaseUpdater(Plugin):
|
||||||
@@ -138,9 +136,6 @@ class BaseUpdater(Plugin):
|
|||||||
def doUpdate(self):
|
def doUpdate(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def getInfo(self):
|
|
||||||
return jsonified(self.info())
|
|
||||||
|
|
||||||
def info(self):
|
def info(self):
|
||||||
return {
|
return {
|
||||||
'last_check': self.last_check,
|
'last_check': self.last_check,
|
||||||
@@ -279,6 +274,7 @@ class SourceUpdater(BaseUpdater):
|
|||||||
if download_data.get('type') == 'zip':
|
if download_data.get('type') == 'zip':
|
||||||
zip = zipfile.ZipFile(destination)
|
zip = zipfile.ZipFile(destination)
|
||||||
zip.extractall(extracted_path)
|
zip.extractall(extracted_path)
|
||||||
|
zip.close()
|
||||||
else:
|
else:
|
||||||
tar = tarfile.open(destination)
|
tar = tarfile.open(destination)
|
||||||
tar.extractall(path = extracted_path)
|
tar.extractall(path = extracted_path)
|
||||||
|
|||||||
+30
-16
@@ -1,26 +1,40 @@
|
|||||||
from couchpotato.core.helpers.variable import md5
|
from couchpotato.core.helpers.variable import md5
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from flask import request, Response
|
import base64
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
def check_auth(username, password):
|
def check_auth(username, password):
|
||||||
return username == Env.setting('username') and password == Env.setting('password')
|
return username == Env.setting('username') and password == Env.setting('password')
|
||||||
|
|
||||||
def authenticate():
|
def requires_auth(handler_class):
|
||||||
return Response(
|
|
||||||
'This is not the page you are looking for. *waves hand*', 401,
|
|
||||||
{'WWW-Authenticate': 'Basic realm="CouchPotato Login"'}
|
|
||||||
)
|
|
||||||
|
|
||||||
def requires_auth(f):
|
def wrap_execute(handler_execute):
|
||||||
|
|
||||||
@wraps(f)
|
def require_basic_auth(handler, kwargs):
|
||||||
def decorated(*args, **kwargs):
|
if Env.setting('username') and Env.setting('password'):
|
||||||
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)
|
auth_header = handler.request.headers.get('Authorization')
|
||||||
|
auth_decoded = base64.decodestring(auth_header[6:]) if auth_header else None
|
||||||
|
if auth_decoded:
|
||||||
|
username, password = auth_decoded.split(':', 2)
|
||||||
|
|
||||||
return decorated
|
if auth_header is None or not auth_header.startswith('Basic ') or (not check_auth(username.decode('latin'), md5(password.decode('latin')))):
|
||||||
|
handler.set_status(401)
|
||||||
|
handler.set_header('WWW-Authenticate', 'Basic realm="CouchPotato Login"')
|
||||||
|
handler._transforms = []
|
||||||
|
handler.finish()
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _execute(self, transforms, *args, **kwargs):
|
||||||
|
|
||||||
|
if not require_basic_auth(self, kwargs):
|
||||||
|
return False
|
||||||
|
return handler_execute(self, transforms, *args, **kwargs)
|
||||||
|
|
||||||
|
return _execute
|
||||||
|
|
||||||
|
handler_class._execute = wrap_execute(handler_class._execute)
|
||||||
|
|
||||||
|
return handler_class
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class Downloader(Provider):
|
|||||||
|
|
||||||
torrent_sources = [
|
torrent_sources = [
|
||||||
'http://torrage.com/torrent/%s.torrent',
|
'http://torrage.com/torrent/%s.torrent',
|
||||||
'http://torcache.net/torrent/%s.torrent',
|
'https://torcache.net/torrent/%s.torrent',
|
||||||
]
|
]
|
||||||
|
|
||||||
torrent_trackers = [
|
torrent_trackers = [
|
||||||
|
|||||||
@@ -104,12 +104,21 @@ class NZBGet(Downloader):
|
|||||||
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
|
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
|
||||||
except:
|
except:
|
||||||
nzb_id = item['NZBID']
|
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({
|
statuses.append({
|
||||||
'id': nzb_id,
|
'id': nzb_id,
|
||||||
'name': item['NZBFilename'],
|
'name': item['NZBFilename'],
|
||||||
'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED',
|
'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
|
# 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
|
for item in queue: # 'Parameters' is not passed in rpc.postqueue
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ config = [{
|
|||||||
'list': 'download_providers',
|
'list': 'download_providers',
|
||||||
'name': 'sabnzbd',
|
'name': 'sabnzbd',
|
||||||
'label': '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,
|
'wizard': True,
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ config = [{
|
|||||||
'name': 'enabled',
|
'name': 'enabled',
|
||||||
'default': 0,
|
'default': 0,
|
||||||
'type': 'enabler',
|
'type': 'enabler',
|
||||||
'radio_group': 'torrent',
|
'radio_group': 'nzb,torrent',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': 'host',
|
'name': 'host',
|
||||||
@@ -32,6 +32,13 @@ config = [{
|
|||||||
'name': 'password',
|
'name': 'password',
|
||||||
'type': 'password',
|
'type': 'password',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'name': 'use_for',
|
||||||
|
'label': 'Use for',
|
||||||
|
'default': 'both',
|
||||||
|
'type': 'dropdown',
|
||||||
|
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'manual',
|
'name': 'manual',
|
||||||
'default': 0,
|
'default': 0,
|
||||||
|
|||||||
@@ -1,22 +1,21 @@
|
|||||||
from couchpotato.core.downloaders.base import Downloader
|
from couchpotato.core.downloaders.base import Downloader
|
||||||
from couchpotato.core.helpers.encoding import isInt
|
from couchpotato.core.helpers.encoding import isInt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
import httplib
|
|
||||||
import json
|
import json
|
||||||
import urllib
|
import requests
|
||||||
import urllib2
|
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Synology(Downloader):
|
class Synology(Downloader):
|
||||||
|
|
||||||
type = ['torrent_magnet']
|
type = ['nzb', 'torrent', 'torrent_magnet']
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
def download(self, data, movie, filedata = None):
|
def download(self, data, movie, filedata = None):
|
||||||
|
|
||||||
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['type']))
|
||||||
|
|
||||||
# Load host from config and split out port.
|
# Load host from config and split out port.
|
||||||
host = self.conf('host').split(':')
|
host = self.conf('host').split(':')
|
||||||
@@ -24,20 +23,41 @@ class Synology(Downloader):
|
|||||||
log.error('Config properties are not filled in correctly, port is missing.')
|
log.error('Config properties are not filled in correctly, port is missing.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if data.get('type') == 'torrent':
|
|
||||||
log.error('Can\'t add binary torrent file')
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Send request to Transmission
|
# Send request to Synology
|
||||||
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
|
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
|
||||||
remote_torrent = srpc.add_torrent_uri(data.get('url'))
|
if data['type'] == 'torrent_magnet':
|
||||||
log.info('Response: %s', remote_torrent)
|
log.info('Adding torrent URL %s', data['url'])
|
||||||
return remote_torrent['success']
|
response = srpc.create_task(url = data['url'])
|
||||||
|
elif data['type'] in ['nzb', 'torrent']:
|
||||||
|
log.info('Adding %s' % data['type'])
|
||||||
|
if not filedata:
|
||||||
|
log.error('No %s data found' % data['type'])
|
||||||
|
else:
|
||||||
|
filename = data['name'] + '.' + data['type']
|
||||||
|
response = srpc.create_task(filename = filename, filedata = filedata)
|
||||||
except Exception, err:
|
except Exception, err:
|
||||||
log.error('Exception while adding torrent: %s', err)
|
log.error('Exception while adding torrent: %s', err)
|
||||||
return False
|
finally:
|
||||||
|
return response
|
||||||
|
|
||||||
|
def getEnabledDownloadType(self):
|
||||||
|
if self.conf('use_for') == 'both':
|
||||||
|
return super(Synology, self).getEnabledDownloadType()
|
||||||
|
elif self.conf('use_for') == 'torrent':
|
||||||
|
return ['torrent', 'torrent_magnet']
|
||||||
|
else:
|
||||||
|
return ['nzb']
|
||||||
|
|
||||||
|
def isEnabled(self, manual, data = {}):
|
||||||
|
for_type = ['both']
|
||||||
|
if data and 'torrent' in data.get('type'):
|
||||||
|
for_type.append('torrent')
|
||||||
|
elif data:
|
||||||
|
for_type.append(data.get('type'))
|
||||||
|
|
||||||
|
return super(Synology, self).isEnabled(manual, data) and\
|
||||||
|
((self.conf('use_for') in for_type))
|
||||||
|
|
||||||
class SynologyRPC(object):
|
class SynologyRPC(object):
|
||||||
|
|
||||||
@@ -58,11 +78,13 @@ class SynologyRPC(object):
|
|||||||
args = {'api': 'SYNO.API.Auth', 'account': self.username, 'passwd': self.password, 'version': 2,
|
args = {'api': 'SYNO.API.Auth', 'account': self.username, 'passwd': self.password, 'version': 2,
|
||||||
'method': 'login', 'session': self.session_name, 'format': 'sid'}
|
'method': 'login', 'session': self.session_name, 'format': 'sid'}
|
||||||
response = self._req(self.auth_url, args)
|
response = self._req(self.auth_url, args)
|
||||||
if response['success'] == True:
|
if response['success']:
|
||||||
self.sid = response['data']['sid']
|
self.sid = response['data']['sid']
|
||||||
log.debug('Sid=%s', self.sid)
|
log.debug('sid=%s', self.sid)
|
||||||
return response
|
else:
|
||||||
elif self.username or self.password:
|
log.error('Couldn\'t login to Synology, %s', response)
|
||||||
|
return response['success']
|
||||||
|
else:
|
||||||
log.error('User or password missing, not using authentication.')
|
log.error('User or password missing, not using authentication.')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -70,36 +92,51 @@ class SynologyRPC(object):
|
|||||||
args = {'api':'SYNO.API.Auth', 'version':1, 'method':'logout', 'session':self.session_name, '_sid':self.sid}
|
args = {'api':'SYNO.API.Auth', 'version':1, 'method':'logout', 'session':self.session_name, '_sid':self.sid}
|
||||||
return self._req(self.auth_url, args)
|
return self._req(self.auth_url, args)
|
||||||
|
|
||||||
def _req(self, url, args):
|
def _req(self, url, args, files = None):
|
||||||
req_url = url + '?' + urllib.urlencode(args)
|
response = {'success': False}
|
||||||
try:
|
try:
|
||||||
req_open = urllib2.urlopen(req_url)
|
req = requests.post(url, data = args, files = files)
|
||||||
response = json.loads(req_open.read())
|
req.raise_for_status()
|
||||||
|
response = json.loads(req.text)
|
||||||
if response['success'] == True:
|
if response['success'] == True:
|
||||||
log.info('Synology action successfull')
|
log.info('Synology action successfull')
|
||||||
return response
|
return response
|
||||||
except httplib.InvalidURL, err:
|
except requests.ConnectionError, err:
|
||||||
log.error('Invalid Transmission host, check your config %s', err)
|
log.error('Synology connection error, check your config %s', err)
|
||||||
return False
|
except requests.HTTPError, err:
|
||||||
except urllib2.HTTPError, err:
|
|
||||||
log.error('SynologyRPC HTTPError: %s', err)
|
log.error('SynologyRPC HTTPError: %s', err)
|
||||||
return False
|
except Exception, err:
|
||||||
except urllib2.URLError, err:
|
log.error('Exception: %s', err)
|
||||||
log.error('Unable to connect to Synology %s', err)
|
finally:
|
||||||
return False
|
return response
|
||||||
|
|
||||||
def add_torrent_uri(self, torrent):
|
def create_task(self, url = None, filename = None, filedata = None):
|
||||||
log.info('Adding torrent URL %s', torrent)
|
''' Creates new download task in Synology DownloadStation. Either specify
|
||||||
response = {}
|
url or pair (filename, filedata).
|
||||||
|
|
||||||
|
Returns True if task was created, False otherwise
|
||||||
|
'''
|
||||||
|
result = False
|
||||||
# login
|
# login
|
||||||
login = self._login()
|
if self._login():
|
||||||
if len(login) > 0 and login['success'] == True:
|
args = {'api': 'SYNO.DownloadStation.Task',
|
||||||
log.info('Login success, adding torrent')
|
'version': '1',
|
||||||
args = {'api':'SYNO.DownloadStation.Task', 'version':1, 'method':'create', 'uri':torrent, '_sid':self.sid}
|
'method': 'create',
|
||||||
response = self._req(self.download_url, args)
|
'_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()
|
self._logout()
|
||||||
else:
|
|
||||||
log.error('Couldn\'t login to Synology, %s', login)
|
|
||||||
return response
|
|
||||||
|
|
||||||
|
|
||||||
|
return result
|
||||||
|
|||||||
@@ -22,14 +22,22 @@ def addEvent(name, handler, priority = 100):
|
|||||||
def createHandle(*args, **kwargs):
|
def createHandle(*args, **kwargs):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
parent = handler.im_self
|
# Open handler
|
||||||
bc = hasattr(parent, 'beforeCall')
|
has_parent = hasattr(handler, 'im_self')
|
||||||
if bc: parent.beforeCall(handler)
|
if has_parent:
|
||||||
|
parent = handler.im_self
|
||||||
|
bc = hasattr(parent, 'beforeCall')
|
||||||
|
if bc: parent.beforeCall(handler)
|
||||||
|
|
||||||
|
# Main event
|
||||||
h = runHandler(name, handler, *args, **kwargs)
|
h = runHandler(name, handler, *args, **kwargs)
|
||||||
ac = hasattr(parent, 'afterCall')
|
|
||||||
if ac: parent.afterCall(handler)
|
# Close handler
|
||||||
|
if has_parent:
|
||||||
|
ac = hasattr(parent, 'afterCall')
|
||||||
|
if ac: parent.afterCall(handler)
|
||||||
except:
|
except:
|
||||||
h = runHandler(name, handler, *args, **kwargs)
|
log.error('Failed creating handler %s %s: %s', (name, handler, traceback.format_exc()))
|
||||||
|
|
||||||
return h
|
return h
|
||||||
|
|
||||||
@@ -43,7 +51,7 @@ def removeEvent(name, handler):
|
|||||||
e -= handler
|
e -= handler
|
||||||
|
|
||||||
def fireEvent(name, *args, **kwargs):
|
def fireEvent(name, *args, **kwargs):
|
||||||
if not events.get(name): return
|
if not events.has_key(name): return
|
||||||
|
|
||||||
e = Event(name = name, threads = 10, asynch = kwargs.get('async', False), exc_info = True, traceback = True, lock = threading.RLock())
|
e = Event(name = name, threads = 10, asynch = kwargs.get('async', False), exc_info = True, traceback = True, lock = threading.RLock())
|
||||||
|
|
||||||
@@ -133,8 +141,6 @@ def fireEvent(name, *args, **kwargs):
|
|||||||
options['on_complete']()
|
options['on_complete']()
|
||||||
|
|
||||||
return results
|
return results
|
||||||
except KeyError, e:
|
|
||||||
pass
|
|
||||||
except Exception:
|
except Exception:
|
||||||
log.error('%s: %s', (name, traceback.format_exc()))
|
log.error('%s: %s', (name, traceback.format_exc()))
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.helpers.variable import natcmp
|
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 urllib import unquote
|
||||||
from werkzeug.urls import url_decode
|
|
||||||
import flask
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
def getParams():
|
|
||||||
|
|
||||||
params = url_decode(getattr(flask.request, 'environ').get('QUERY_STRING', ''))
|
def getParams(params):
|
||||||
|
|
||||||
reg = re.compile('^[a-z0-9_\.]+$')
|
reg = re.compile('^[a-z0-9_\.]+$')
|
||||||
|
|
||||||
current = temp = {}
|
current = temp = {}
|
||||||
@@ -36,6 +32,8 @@ def getParams():
|
|||||||
current = current[item]
|
current = current[item]
|
||||||
else:
|
else:
|
||||||
temp[param] = toUnicode(unquote(value))
|
temp[param] = toUnicode(unquote(value))
|
||||||
|
if temp[param].lower() in ['true', 'false']:
|
||||||
|
temp[param] = temp[param].lower() != 'false'
|
||||||
|
|
||||||
return dictToList(temp)
|
return dictToList(temp)
|
||||||
|
|
||||||
@@ -54,29 +52,3 @@ def dictToList(params):
|
|||||||
new = params
|
new = params
|
||||||
|
|
||||||
return new
|
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
|
|
||||||
|
|||||||
@@ -181,5 +181,6 @@ def possibleTitles(raw_title):
|
|||||||
def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
|
def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
|
||||||
return ''.join(random.choice(chars) for x in range(size))
|
return ''.join(random.choice(chars) for x in range(size))
|
||||||
|
|
||||||
def splitString(str, split_on = ','):
|
def splitString(str, split_on = ',', clean = True):
|
||||||
return [x.strip() for x in str.split(split_on)] if str else []
|
list = [x.strip() for x in str.split(split_on)] if str else []
|
||||||
|
return filter(None, list) if clean else list
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import re
|
|||||||
class CPLog(object):
|
class CPLog(object):
|
||||||
|
|
||||||
context = ''
|
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 = ''):
|
def __init__(self, context = ''):
|
||||||
if context.endswith('.main'):
|
if context.endswith('.main'):
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.request import jsonified
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.base import Provider
|
from couchpotato.core.providers.base import Provider
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
@@ -50,7 +49,7 @@ class Notification(Provider):
|
|||||||
def notify(self, message = '', data = {}, listener = None):
|
def notify(self, message = '', data = {}, listener = None):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test(self):
|
def test(self, **kwargs):
|
||||||
|
|
||||||
test_type = self.testNotifyName()
|
test_type = self.testNotifyName()
|
||||||
|
|
||||||
@@ -62,7 +61,9 @@ class Notification(Provider):
|
|||||||
listener = 'test'
|
listener = 'test'
|
||||||
)
|
)
|
||||||
|
|
||||||
return jsonified({'success': success})
|
return {
|
||||||
|
'success': success
|
||||||
|
}
|
||||||
|
|
||||||
def testNotifyName(self):
|
def testNotifyName(self):
|
||||||
return 'notify.%s.test' % self.getName().lower()
|
return 'notify.%s.test' % self.getName().lower()
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
|||||||
from couchpotato.api import addApiView, addNonBlockApiView
|
from couchpotato.api import addApiView, addNonBlockApiView
|
||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.helpers.request import jsonified, getParam
|
|
||||||
from couchpotato.core.helpers.variable import tryInt, splitString
|
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
@@ -11,6 +10,7 @@ from couchpotato.environment import Env
|
|||||||
from sqlalchemy.sql.expression import or_
|
from sqlalchemy.sql.expression import or_
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -62,11 +62,9 @@ class CoreNotifier(Notification):
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
def markAsRead(self):
|
def markAsRead(self, ids = None, **kwargs):
|
||||||
|
|
||||||
ids = None
|
ids = splitString(ids) if ids else None
|
||||||
if getParam('ids'):
|
|
||||||
ids = splitString(getParam('ids'))
|
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
@@ -79,14 +77,13 @@ class CoreNotifier(Notification):
|
|||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True
|
'success': True
|
||||||
})
|
}
|
||||||
|
|
||||||
def listView(self):
|
def listView(self, limit_offset = None, **kwargs):
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
limit_offset = getParam('limit_offset', None)
|
|
||||||
|
|
||||||
q = db.query(Notif)
|
q = db.query(Notif)
|
||||||
|
|
||||||
@@ -105,11 +102,11 @@ class CoreNotifier(Notification):
|
|||||||
ndict['type'] = 'notification'
|
ndict['type'] = 'notification'
|
||||||
notifications.append(ndict)
|
notifications.append(ndict)
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'empty': len(notifications) == 0,
|
'empty': len(notifications) == 0,
|
||||||
'notifications': notifications
|
'notifications': notifications
|
||||||
})
|
}
|
||||||
|
|
||||||
def checkMessages(self):
|
def checkMessages(self):
|
||||||
|
|
||||||
@@ -150,6 +147,8 @@ class CoreNotifier(Notification):
|
|||||||
|
|
||||||
def frontend(self, type = 'notification', data = {}, message = None):
|
def frontend(self, type = 'notification', data = {}, message = None):
|
||||||
|
|
||||||
|
log.debug('Notifying frontend')
|
||||||
|
|
||||||
self.m_lock.acquire()
|
self.m_lock.acquire()
|
||||||
notification = {
|
notification = {
|
||||||
'message_id': str(uuid.uuid4()),
|
'message_id': str(uuid.uuid4()),
|
||||||
@@ -168,11 +167,13 @@ class CoreNotifier(Notification):
|
|||||||
'result': [notification],
|
'result': [notification],
|
||||||
})
|
})
|
||||||
except:
|
except:
|
||||||
break
|
log.debug('Failed sending to listener: %s', traceback.format_exc())
|
||||||
|
|
||||||
self.m_lock.release()
|
self.m_lock.release()
|
||||||
self.cleanMessages()
|
self.cleanMessages()
|
||||||
|
|
||||||
|
log.debug('Done notifying frontend')
|
||||||
|
|
||||||
def addListener(self, callback, last_id = None):
|
def addListener(self, callback, last_id = None):
|
||||||
|
|
||||||
if last_id:
|
if last_id:
|
||||||
@@ -194,9 +195,11 @@ class CoreNotifier(Notification):
|
|||||||
if listener == callback:
|
if listener == callback:
|
||||||
self.listeners.remove(list_tuple)
|
self.listeners.remove(list_tuple)
|
||||||
except:
|
except:
|
||||||
pass
|
log.debug('Failed removing listener: %s', traceback.format_exc())
|
||||||
|
|
||||||
def cleanMessages(self):
|
def cleanMessages(self):
|
||||||
|
|
||||||
|
log.debug('Cleaning messages')
|
||||||
self.m_lock.acquire()
|
self.m_lock.acquire()
|
||||||
|
|
||||||
for message in self.messages:
|
for message in self.messages:
|
||||||
@@ -204,8 +207,11 @@ class CoreNotifier(Notification):
|
|||||||
self.messages.remove(message)
|
self.messages.remove(message)
|
||||||
|
|
||||||
self.m_lock.release()
|
self.m_lock.release()
|
||||||
|
log.debug('Done cleaning messages')
|
||||||
|
|
||||||
def getMessages(self, last_id):
|
def getMessages(self, last_id):
|
||||||
|
|
||||||
|
log.debug('Getting messages with id: %s', last_id)
|
||||||
self.m_lock.acquire()
|
self.m_lock.acquire()
|
||||||
|
|
||||||
recent = []
|
recent = []
|
||||||
@@ -216,15 +222,16 @@ class CoreNotifier(Notification):
|
|||||||
recent = self.messages[index:]
|
recent = self.messages[index:]
|
||||||
|
|
||||||
self.m_lock.release()
|
self.m_lock.release()
|
||||||
|
log.debug('Returning for %s %s messages', (last_id, len(recent or [])))
|
||||||
|
|
||||||
return recent or []
|
return recent or []
|
||||||
|
|
||||||
def listener(self):
|
def listener(self, init = False, **kwargs):
|
||||||
|
|
||||||
messages = []
|
messages = []
|
||||||
|
|
||||||
# Get unread
|
# Get unread
|
||||||
if getParam('init'):
|
if init:
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
notifications = db.query(Notif) \
|
notifications = db.query(Notif) \
|
||||||
@@ -235,7 +242,7 @@ class CoreNotifier(Notification):
|
|||||||
ndict['type'] = 'notification'
|
ndict['type'] = 'notification'
|
||||||
messages.append(ndict)
|
messages.append(ndict)
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'result': messages,
|
'result': messages,
|
||||||
})
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||||
from couchpotato.core.helpers.request import getParams, jsonified
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
import re
|
import re
|
||||||
@@ -22,10 +21,7 @@ class NMJ(Notification):
|
|||||||
addApiView(self.testNotifyName(), self.test)
|
addApiView(self.testNotifyName(), self.test)
|
||||||
addApiView('notify.nmj.auto_config', self.autoConfig)
|
addApiView('notify.nmj.auto_config', self.autoConfig)
|
||||||
|
|
||||||
def autoConfig(self):
|
def autoConfig(self, host = 'localhost', **kwargs):
|
||||||
|
|
||||||
params = getParams()
|
|
||||||
host = params.get('host', 'localhost')
|
|
||||||
|
|
||||||
database = ''
|
database = ''
|
||||||
mount = ''
|
mount = ''
|
||||||
@@ -63,11 +59,11 @@ class NMJ(Notification):
|
|||||||
log.error('Detected a network share on the Popcorn Hour, but could not get the mounting url')
|
log.error('Detected a network share on the Popcorn Hour, but could not get the mounting url')
|
||||||
return self.failed()
|
return self.failed()
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'database': database,
|
'database': database,
|
||||||
'mount': mount,
|
'mount': mount,
|
||||||
})
|
}
|
||||||
|
|
||||||
def addToLibrary(self, message = None, group = {}):
|
def addToLibrary(self, message = None, group = {}):
|
||||||
if self.isDisabled(): return
|
if self.isDisabled(): return
|
||||||
@@ -113,9 +109,13 @@ class NMJ(Notification):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def failed(self):
|
def failed(self):
|
||||||
return jsonified({'success': False})
|
return {
|
||||||
|
'success': False
|
||||||
|
}
|
||||||
|
|
||||||
def test(self):
|
def test(self, **kwargs):
|
||||||
return jsonified({'success': self.addToLibrary()})
|
return {
|
||||||
|
'success': self.addToLibrary()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
from flask.helpers import json
|
|
||||||
import base64
|
import base64
|
||||||
|
import json
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
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
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
@@ -73,7 +72,7 @@ class Plex(Notification):
|
|||||||
log.info('Plex notification to %s successful.', host)
|
log.info('Plex notification to %s successful.', host)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def test(self):
|
def test(self, **kwargs):
|
||||||
|
|
||||||
test_type = self.testNotifyName()
|
test_type = self.testNotifyName()
|
||||||
|
|
||||||
@@ -86,4 +85,6 @@ class Plex(Notification):
|
|||||||
)
|
)
|
||||||
success2 = self.addToLibrary()
|
success2 = self.addToLibrary()
|
||||||
|
|
||||||
return jsonified({'success': success or success2})
|
return {
|
||||||
|
'success': success or success2
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.request import jsonified
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
import os
|
import os
|
||||||
@@ -32,5 +31,7 @@ class Synoindex(Notification):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def test(self):
|
def test(self, **kwargs):
|
||||||
return jsonified({'success': os.path.isfile(self.index_path)})
|
return {
|
||||||
|
'success': os.path.isfile(self.index_path)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
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.helpers.variable import cleanHost
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
from flask.helpers import url_for
|
from couchpotato.environment import Env
|
||||||
from pytwitter import Api, parse_qsl
|
from pytwitter import Api, parse_qsl
|
||||||
from werkzeug.utils import redirect
|
|
||||||
import oauth2
|
import oauth2
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -70,10 +68,9 @@ class Twitter(Notification):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def getAuthorizationUrl(self):
|
def getAuthorizationUrl(self, host = None, **kwargs):
|
||||||
|
|
||||||
referer = getParam('host')
|
callback_url = cleanHost(host) + '%snotify.%s.credentials/' % (Env.get('api_base').lstrip('/'), self.getName().lower())
|
||||||
callback_url = cleanHost(referer) + '%snotify.%s.credentials/' % (url_for('api.index').lstrip('/'), self.getName().lower())
|
|
||||||
|
|
||||||
oauth_consumer = oauth2.Consumer(self.consumer_key, self.consumer_secret)
|
oauth_consumer = oauth2.Consumer(self.consumer_key, self.consumer_secret)
|
||||||
oauth_client = oauth2.Client(oauth_consumer)
|
oauth_client = oauth2.Client(oauth_consumer)
|
||||||
@@ -82,31 +79,29 @@ class Twitter(Notification):
|
|||||||
|
|
||||||
if resp['status'] != '200':
|
if resp['status'] != '200':
|
||||||
log.error('Invalid response from Twitter requesting temp token: %s', resp['status'])
|
log.error('Invalid response from Twitter requesting temp token: %s', resp['status'])
|
||||||
return jsonified({
|
return {
|
||||||
'success': False,
|
'success': False,
|
||||||
})
|
}
|
||||||
else:
|
else:
|
||||||
self.request_token = dict(parse_qsl(content))
|
self.request_token = dict(parse_qsl(content))
|
||||||
|
|
||||||
auth_url = self.urls['authorize'] + ("?oauth_token=%s" % self.request_token['oauth_token'])
|
auth_url = self.urls['authorize'] + ("?oauth_token=%s" % self.request_token['oauth_token'])
|
||||||
|
|
||||||
log.info('Redirecting to "%s"', auth_url)
|
log.info('Redirecting to "%s"', auth_url)
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'url': auth_url,
|
'url': auth_url,
|
||||||
})
|
}
|
||||||
|
|
||||||
def getCredentials(self):
|
def getCredentials(self, oauth_verifier, **kwargs):
|
||||||
|
|
||||||
key = getParam('oauth_verifier')
|
|
||||||
|
|
||||||
token = oauth2.Token(self.request_token['oauth_token'], self.request_token['oauth_token_secret'])
|
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_consumer = oauth2.Consumer(key = self.consumer_key, secret = self.consumer_secret)
|
||||||
oauth_client = oauth2.Client(oauth_consumer, token)
|
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))
|
access_token = dict(parse_qsl(content))
|
||||||
|
|
||||||
if resp['status'] != '200':
|
if resp['status'] != '200':
|
||||||
@@ -121,4 +116,4 @@ class Twitter(Notification):
|
|||||||
|
|
||||||
self.request_token = None
|
self.request_token = None
|
||||||
|
|
||||||
return redirect(url_for('web.index') + 'settings/notifications/')
|
return 'redirect', Env.get('web_base') + 'settings/notifications/'
|
||||||
|
|||||||
@@ -31,6 +31,13 @@ config = [{
|
|||||||
'default': '',
|
'default': '',
|
||||||
'type': 'password',
|
'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': 'on_snatch',
|
'name': 'on_snatch',
|
||||||
'default': 0,
|
'default': 0,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from couchpotato.core.helpers.variable import splitString
|
from couchpotato.core.helpers.variable import splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.notifications.base import Notification
|
from couchpotato.core.notifications.base import Notification
|
||||||
from flask.helpers import json
|
from urllib2 import URLError
|
||||||
import base64
|
import base64
|
||||||
|
import json
|
||||||
|
import socket
|
||||||
import traceback
|
import traceback
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
@@ -20,19 +22,30 @@ class XBMC(Notification):
|
|||||||
hosts = splitString(self.conf('host'))
|
hosts = splitString(self.conf('host'))
|
||||||
|
|
||||||
successful = 0
|
successful = 0
|
||||||
|
max_successful = 0
|
||||||
for host in hosts:
|
for host in hosts:
|
||||||
|
|
||||||
if self.use_json_notifications.get(host) is None:
|
if self.use_json_notifications.get(host) is None:
|
||||||
self.getXBMCJSONversion(host, message = message)
|
self.getXBMCJSONversion(host, message = message)
|
||||||
|
|
||||||
if self.use_json_notifications.get(host):
|
if self.use_json_notifications.get(host):
|
||||||
response = self.request(host, [
|
calls = [
|
||||||
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}),
|
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}),
|
||||||
('VideoLibrary.Scan', {}),
|
]
|
||||||
])
|
|
||||||
|
if not self.conf('only_first') or hosts.index(host) == 0:
|
||||||
|
calls.append(('VideoLibrary.Scan', {}))
|
||||||
|
|
||||||
|
max_successful += len(calls)
|
||||||
|
response = self.request(host, calls)
|
||||||
else:
|
else:
|
||||||
response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
|
response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
|
||||||
response += self.request(host, [('VideoLibrary.Scan', {})])
|
|
||||||
|
if not self.conf('only_first') or hosts.index(host) == 0:
|
||||||
|
response += self.request(host, [('VideoLibrary.Scan', {})])
|
||||||
|
max_successful += 1
|
||||||
|
|
||||||
|
max_successful += 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for result in response:
|
for result in response:
|
||||||
@@ -44,7 +57,7 @@ class XBMC(Notification):
|
|||||||
except:
|
except:
|
||||||
log.error('Failed parsing results: %s', traceback.format_exc())
|
log.error('Failed parsing results: %s', traceback.format_exc())
|
||||||
|
|
||||||
return successful == len(hosts) * 2
|
return successful == max_successful
|
||||||
|
|
||||||
def getXBMCJSONversion(self, host, message = ''):
|
def getXBMCJSONversion(self, host, message = ''):
|
||||||
|
|
||||||
@@ -53,7 +66,7 @@ class XBMC(Notification):
|
|||||||
# XBMC JSON-RPC version request
|
# XBMC JSON-RPC version request
|
||||||
response = self.request(host, [
|
response = self.request(host, [
|
||||||
('JSONRPC.Version', {})
|
('JSONRPC.Version', {})
|
||||||
])
|
])
|
||||||
for result in response:
|
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
|
# only v2 and v4 return an int object
|
||||||
@@ -138,7 +151,7 @@ class XBMC(Notification):
|
|||||||
# <li>Error:<message>
|
# <li>Error:<message>
|
||||||
# </html>
|
# </html>
|
||||||
#
|
#
|
||||||
response = self.urlopen(server, headers = headers)
|
response = self.urlopen(server, headers = headers, timeout = 3, show_error = False)
|
||||||
|
|
||||||
if 'OK' in response:
|
if 'OK' in response:
|
||||||
log.debug('Returned from non-JSON-type request %s: %s', (host, response))
|
log.debug('Returned from non-JSON-type request %s: %s', (host, response))
|
||||||
@@ -149,6 +162,13 @@ class XBMC(Notification):
|
|||||||
# manually fake expected response array
|
# manually fake expected response array
|
||||||
return [{'result': 'Error'}]
|
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:
|
except:
|
||||||
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
|
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
|
||||||
return [{'result': 'Error'}]
|
return [{'result': 'Error'}]
|
||||||
@@ -177,11 +197,17 @@ class XBMC(Notification):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
log.debug('Sending request to %s: %s', (host, data))
|
log.debug('Sending request to %s: %s', (host, data))
|
||||||
rdata = self.urlopen(server, headers = headers, params = data, multipart = True)
|
response = self.getJsonData(server, headers = headers, params = data, timeout = 3, show_error = False)
|
||||||
response = json.loads(rdata)
|
|
||||||
log.debug('Returned from request %s: %s', (host, response))
|
log.debug('Returned from request %s: %s', (host, response))
|
||||||
|
|
||||||
return 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:
|
except:
|
||||||
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
|
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
from couchpotato import addView
|
|
||||||
from couchpotato.core.event import fireEvent, addEvent
|
from couchpotato.core.event import fireEvent, addEvent
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString
|
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString, \
|
||||||
|
toUnicode
|
||||||
from couchpotato.core.helpers.variable import getExt, md5
|
from couchpotato.core.helpers.variable import getExt, md5
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from flask.templating import render_template_string
|
|
||||||
from multipartpost import MultipartPostHandler
|
from multipartpost import MultipartPostHandler
|
||||||
|
from tornado import template
|
||||||
|
from tornado.web import StaticFileHandler
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
import cookielib
|
import cookielib
|
||||||
import glob
|
import glob
|
||||||
@@ -37,6 +38,7 @@ class Plugin(object):
|
|||||||
def registerPlugin(self):
|
def registerPlugin(self):
|
||||||
addEvent('app.do_shutdown', self.doShutdown)
|
addEvent('app.do_shutdown', self.doShutdown)
|
||||||
addEvent('plugin.running', self.isRunning)
|
addEvent('plugin.running', self.isRunning)
|
||||||
|
self._running = []
|
||||||
|
|
||||||
def conf(self, attr, value = None, default = None):
|
def conf(self, attr, value = None, default = None):
|
||||||
return Env.setting(attr, self.getName().lower(), value = value, default = default)
|
return Env.setting(attr, self.getName().lower(), value = value, default = default)
|
||||||
@@ -44,35 +46,37 @@ class Plugin(object):
|
|||||||
def getName(self):
|
def getName(self):
|
||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
|
||||||
def renderTemplate(self, parent_file, template, **params):
|
def renderTemplate(self, parent_file, templ, **params):
|
||||||
|
|
||||||
template = open(os.path.join(os.path.dirname(parent_file), template), 'r').read()
|
t = template.Template(open(os.path.join(os.path.dirname(parent_file), templ), 'r').read())
|
||||||
return render_template_string(template, **params)
|
return t.generate(**params)
|
||||||
|
|
||||||
def registerStatic(self, plugin_file, add_to_head = True):
|
def registerStatic(self, plugin_file, add_to_head = True):
|
||||||
|
|
||||||
# Register plugin path
|
# Register plugin path
|
||||||
self.plugin_path = os.path.dirname(plugin_file)
|
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
|
# Get plugin_name from PluginName
|
||||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', self.__class__.__name__)
|
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()
|
class_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
||||||
|
|
||||||
|
# View path
|
||||||
path = 'api/%s/static/%s/' % (Env.setting('api_key'), class_name)
|
path = 'api/%s/static/%s/' % (Env.setting('api_key'), class_name)
|
||||||
addView(path + '<path:filename>', self.showStatic, static = True)
|
|
||||||
|
|
||||||
|
# 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:
|
if add_to_head:
|
||||||
for f in glob.glob(os.path.join(self.plugin_path, 'static', '*')):
|
for f in glob.glob(os.path.join(self.plugin_path, 'static', '*')):
|
||||||
ext = getExt(f)
|
ext = getExt(f)
|
||||||
if ext in ['js', 'css']:
|
if ext in ['js', 'css']:
|
||||||
fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f)
|
fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f)
|
||||||
|
|
||||||
def showStatic(self, filename):
|
|
||||||
d = os.path.join(self.plugin_path, 'static')
|
|
||||||
|
|
||||||
from flask.helpers import send_from_directory
|
|
||||||
return send_from_directory(d, filename)
|
|
||||||
|
|
||||||
def createFile(self, path, content, binary = False):
|
def createFile(self, path, content, binary = False):
|
||||||
path = ss(path)
|
path = ss(path)
|
||||||
|
|
||||||
@@ -106,12 +110,14 @@ class Plugin(object):
|
|||||||
|
|
||||||
# Fill in some headers
|
# Fill in some headers
|
||||||
parsed_url = urlparse(url)
|
parsed_url = urlparse(url)
|
||||||
host = parsed_url.hostname
|
host = '%s%s' % (parsed_url.hostname, (':' + str(parsed_url.port) if parsed_url.port else ''))
|
||||||
|
|
||||||
headers['Referer'] = headers.get('Referer', '%s://%s' % (parsed_url.scheme, host))
|
headers['Referer'] = headers.get('Referer', '%s://%s' % (parsed_url.scheme, host))
|
||||||
headers['Host'] = headers.get('Host', host)
|
headers['Host'] = headers.get('Host', host)
|
||||||
headers['User-Agent'] = headers.get('User-Agent', self.user_agent)
|
headers['User-Agent'] = headers.get('User-Agent', self.user_agent)
|
||||||
headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip')
|
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
|
# Don't try for failed requests
|
||||||
if self.http_failed_disabled.get(host, 0) > 0:
|
if self.http_failed_disabled.get(host, 0) > 0:
|
||||||
@@ -128,6 +134,10 @@ class Plugin(object):
|
|||||||
self.wait(host)
|
self.wait(host)
|
||||||
try:
|
try:
|
||||||
|
|
||||||
|
# Make sure opener has the correct headers
|
||||||
|
if opener:
|
||||||
|
opener.add_headers = headers
|
||||||
|
|
||||||
if multipart:
|
if multipart:
|
||||||
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
|
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
|
||||||
request = urllib2.Request(url, params, headers)
|
request = urllib2.Request(url, params, headers)
|
||||||
@@ -141,7 +151,12 @@ class Plugin(object):
|
|||||||
response = opener.open(request, timeout = timeout)
|
response = opener.open(request, timeout = timeout)
|
||||||
else:
|
else:
|
||||||
log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
|
log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
|
||||||
data = tryUrlencode(params) if len(params) > 0 else None
|
|
||||||
|
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)
|
request = urllib2.Request(url, data, headers)
|
||||||
|
|
||||||
if opener:
|
if opener:
|
||||||
@@ -154,8 +169,10 @@ class Plugin(object):
|
|||||||
buf = StringIO(response.read())
|
buf = StringIO(response.read())
|
||||||
f = gzip.GzipFile(fileobj = buf)
|
f = gzip.GzipFile(fileobj = buf)
|
||||||
data = f.read()
|
data = f.read()
|
||||||
|
f.close()
|
||||||
else:
|
else:
|
||||||
data = response.read()
|
data = response.read()
|
||||||
|
response.close()
|
||||||
|
|
||||||
self.http_failed_request[host] = 0
|
self.http_failed_request[host] = 0
|
||||||
except IOError:
|
except IOError:
|
||||||
@@ -211,9 +228,6 @@ class Plugin(object):
|
|||||||
|
|
||||||
def isRunning(self, value = None, boolean = True):
|
def isRunning(self, value = None, boolean = True):
|
||||||
|
|
||||||
if not hasattr(self, '_running'):
|
|
||||||
self._running = []
|
|
||||||
|
|
||||||
if value is None:
|
if value is None:
|
||||||
return self._running
|
return self._running
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.helpers.request import getParam, jsonified
|
|
||||||
from couchpotato.core.helpers.variable import getUserDir
|
from couchpotato.core.helpers.variable import getUserDir
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
import ctypes
|
import ctypes
|
||||||
@@ -63,16 +62,15 @@ class FileBrowser(Plugin):
|
|||||||
|
|
||||||
return driveletters
|
return driveletters
|
||||||
|
|
||||||
def view(self):
|
def view(self, path = '/', show_hidden = True, **kwargs):
|
||||||
|
|
||||||
path = getParam('path', '/')
|
|
||||||
home = getUserDir()
|
home = getUserDir()
|
||||||
|
|
||||||
if not path:
|
if not path:
|
||||||
path = home
|
path = home
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True))
|
dirs = self.getDirectories(path = path, show_hidden = show_hidden)
|
||||||
except:
|
except:
|
||||||
dirs = []
|
dirs = []
|
||||||
|
|
||||||
@@ -82,14 +80,14 @@ class FileBrowser(Plugin):
|
|||||||
elif parent != '/' and parent[-2:] != ':\\':
|
elif parent != '/' and parent[-2:] != ':\\':
|
||||||
parent += os.path.sep
|
parent += os.path.sep
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'is_root': path == '/',
|
'is_root': path == '/',
|
||||||
'empty': len(dirs) == 0,
|
'empty': len(dirs) == 0,
|
||||||
'parent': parent,
|
'parent': parent,
|
||||||
'home': home + os.path.sep,
|
'home': home + os.path.sep,
|
||||||
'platform': os.name,
|
'platform': os.name,
|
||||||
'dirs': dirs,
|
'dirs': dirs,
|
||||||
})
|
}
|
||||||
|
|
||||||
|
|
||||||
def is_hidden(self, filepath):
|
def is_hidden(self, filepath):
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
from couchpotato import get_session
|
from couchpotato import get_session
|
||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import fireEvent
|
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.helpers.variable import splitString, tryInt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Movie
|
from couchpotato.core.settings.model import Movie
|
||||||
from sqlalchemy.orm import joinedload_all
|
from sqlalchemy.orm import joinedload_all
|
||||||
import random
|
import random as rndm
|
||||||
import time
|
import time
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -16,41 +15,10 @@ log = CPLog(__name__)
|
|||||||
class Dashboard(Plugin):
|
class Dashboard(Plugin):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
addApiView('dashboard.suggestions', self.suggestView)
|
|
||||||
addApiView('dashboard.soon', self.getSoonView)
|
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()
|
db = get_session()
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
@@ -85,7 +53,6 @@ class Dashboard(Plugin):
|
|||||||
.options(joinedload_all('files'))
|
.options(joinedload_all('files'))
|
||||||
|
|
||||||
# Add limit
|
# Add limit
|
||||||
limit_offset = params.get('limit_offset')
|
|
||||||
limit = 12
|
limit = 12
|
||||||
if limit_offset:
|
if limit_offset:
|
||||||
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
|
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
|
||||||
@@ -93,8 +60,8 @@ class Dashboard(Plugin):
|
|||||||
|
|
||||||
all_movies = q.all()
|
all_movies = q.all()
|
||||||
|
|
||||||
if params.get('random', False):
|
if random:
|
||||||
random.shuffle(all_movies)
|
rndm.shuffle(all_movies)
|
||||||
|
|
||||||
movies = []
|
movies = []
|
||||||
for movie in all_movies:
|
for movie in all_movies:
|
||||||
@@ -103,9 +70,9 @@ class Dashboard(Plugin):
|
|||||||
coming_soon = False
|
coming_soon = False
|
||||||
|
|
||||||
# Theater quality
|
# Theater quality
|
||||||
if pp.get('theater') and fireEvent('searcher.could_be_released', True, eta, single = True):
|
if pp.get('theater') and fireEvent('searcher.could_be_released', True, eta, movie.library.year, single = True):
|
||||||
coming_soon = True
|
coming_soon = True
|
||||||
if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, single = True):
|
if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, movie.library.year, single = True):
|
||||||
coming_soon = True
|
coming_soon = True
|
||||||
|
|
||||||
# Skip if movie is snatched/downloaded/available
|
# Skip if movie is snatched/downloaded/available
|
||||||
@@ -126,18 +93,18 @@ class Dashboard(Plugin):
|
|||||||
})
|
})
|
||||||
|
|
||||||
# Don't list older movies
|
# 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 \
|
if ((not late and ((not eta.get('dvd') and not eta.get('theater')) or (eta.get('dvd') and eta.get('dvd') > (now - 2419200)))) or \
|
||||||
(params.get('late') and eta.get('dvd') and eta.get('dvd') < (now - 2419200))):
|
(late and (eta.get('dvd', 0) > 0 or eta.get('theater')) and eta.get('dvd') < (now - 2419200))):
|
||||||
movies.append(temp)
|
movies.append(temp)
|
||||||
|
|
||||||
if len(movies) >= limit:
|
if len(movies) >= limit:
|
||||||
break
|
break
|
||||||
|
|
||||||
db.expire_all()
|
db.expire_all()
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'empty': len(movies) == 0,
|
'empty': len(movies) == 0,
|
||||||
'movies': movies,
|
'movies': movies,
|
||||||
})
|
}
|
||||||
|
|
||||||
getLateView = getSoonView
|
getLateView = getSoonView
|
||||||
|
|||||||
@@ -2,15 +2,13 @@ from couchpotato import get_session
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.helpers.request import jsonified
|
|
||||||
from couchpotato.core.helpers.variable import md5, getExt
|
from couchpotato.core.helpers.variable import md5, getExt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.plugins.scanner.main import Scanner
|
from couchpotato.core.plugins.scanner.main import Scanner
|
||||||
from couchpotato.core.settings.model import FileType, File
|
from couchpotato.core.settings.model import FileType, File
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from flask.helpers import send_file
|
from tornado.web import StaticFileHandler
|
||||||
from werkzeug.exceptions import NotFound
|
|
||||||
import os.path
|
import os.path
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
@@ -25,7 +23,7 @@ class FileManager(Plugin):
|
|||||||
addEvent('file.download', self.download)
|
addEvent('file.download', self.download)
|
||||||
addEvent('file.types', self.getTypes)
|
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',
|
'desc': 'Return a file from the cp_data/cache directory',
|
||||||
'params': {
|
'params': {
|
||||||
'filename': {'desc': 'path/filename of the wanted file'}
|
'filename': {'desc': 'path/filename of the wanted file'}
|
||||||
@@ -81,15 +79,9 @@ class FileManager(Plugin):
|
|||||||
except:
|
except:
|
||||||
log.error('Failed removing unused file: %s', traceback.format_exc())
|
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 = {}):
|
||||||
|
|
||||||
@@ -158,8 +150,8 @@ class FileManager(Plugin):
|
|||||||
|
|
||||||
return types
|
return types
|
||||||
|
|
||||||
def getTypesView(self):
|
def getTypesView(self, **kwargs):
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'types': self.getTypes()
|
'types': self.getTypes()
|
||||||
})
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from couchpotato import get_session
|
from couchpotato import get_session
|
||||||
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||||
from couchpotato.core.helpers.variable import mergeDicts
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Library, LibraryTitle, File
|
from couchpotato.core.settings.model import Library, LibraryTitle, File
|
||||||
@@ -32,7 +31,8 @@ class LibraryPlugin(Plugin):
|
|||||||
identifier = attrs.get('identifier'),
|
identifier = attrs.get('identifier'),
|
||||||
plot = toUnicode(attrs.get('plot')),
|
plot = toUnicode(attrs.get('plot')),
|
||||||
tagline = toUnicode(attrs.get('tagline')),
|
tagline = toUnicode(attrs.get('tagline')),
|
||||||
status_id = status.get('id')
|
status_id = status.get('id'),
|
||||||
|
info = {},
|
||||||
)
|
)
|
||||||
|
|
||||||
title = LibraryTitle(
|
title = LibraryTitle(
|
||||||
@@ -87,7 +87,7 @@ class LibraryPlugin(Plugin):
|
|||||||
library.tagline = toUnicode(info.get('tagline', ''))
|
library.tagline = toUnicode(info.get('tagline', ''))
|
||||||
library.year = info.get('year', 0)
|
library.year = info.get('year', 0)
|
||||||
library.status_id = done_status.get('id')
|
library.status_id = done_status.get('id')
|
||||||
library.info = info
|
library.info.update(info)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Titles
|
# Titles
|
||||||
@@ -148,7 +148,7 @@ class LibraryPlugin(Plugin):
|
|||||||
|
|
||||||
if dates and dates.get('expires', 0) < time.time() or not dates:
|
if dates and dates.get('expires', 0) < time.time() or not dates:
|
||||||
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
|
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
|
||||||
library.info = mergeDicts(library.info, {'release_date': dates })
|
library.info.update({'release_date': dates })
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
db.expire_all()
|
db.expire_all()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
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.helpers.variable import tryInt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
@@ -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
|
current_path = None
|
||||||
|
|
||||||
total = 1
|
total = 1
|
||||||
@@ -71,16 +70,15 @@ class Logging(Plugin):
|
|||||||
f = open(current_path, 'r')
|
f = open(current_path, 'r')
|
||||||
log = f.read()
|
log = f.read()
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'log': toUnicode(log),
|
'log': toUnicode(log),
|
||||||
'total': total,
|
'total': total,
|
||||||
})
|
}
|
||||||
|
|
||||||
def partial(self):
|
def partial(self, type = 'all', lines = 30, **kwargs):
|
||||||
|
|
||||||
log_type = getParam('type', 'all')
|
total_lines = tryInt(lines)
|
||||||
total_lines = tryInt(getParam('lines', 30))
|
|
||||||
|
|
||||||
log_lines = []
|
log_lines = []
|
||||||
|
|
||||||
@@ -100,7 +98,7 @@ class Logging(Plugin):
|
|||||||
brk = False
|
brk = False
|
||||||
for line in reversed_lines:
|
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)
|
log_lines.append(line)
|
||||||
|
|
||||||
if len(log_lines) >= total_lines:
|
if len(log_lines) >= total_lines:
|
||||||
@@ -111,12 +109,12 @@ class Logging(Plugin):
|
|||||||
break
|
break
|
||||||
|
|
||||||
log_lines.reverse()
|
log_lines.reverse()
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'log': '[0m\n'.join(log_lines),
|
'log': '[0m\n'.join(log_lines),
|
||||||
})
|
}
|
||||||
|
|
||||||
def clear(self):
|
def clear(self, **kwargs):
|
||||||
|
|
||||||
for x in range(0, 50):
|
for x in range(0, 50):
|
||||||
path = '%s%s' % (Env.get('log_path'), '.%s' % x if x > 0 else '')
|
path = '%s%s' % (Env.get('log_path'), '.%s' % x if x > 0 else '')
|
||||||
@@ -135,24 +133,21 @@ class Logging(Plugin):
|
|||||||
except:
|
except:
|
||||||
log.error('Couldn\'t delete file "%s": %s', (path, traceback.format_exc()))
|
log.error('Couldn\'t delete file "%s": %s', (path, traceback.format_exc()))
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True
|
'success': True
|
||||||
})
|
}
|
||||||
|
|
||||||
def log(self):
|
def log(self, type = 'error', **kwargs):
|
||||||
|
|
||||||
params = getParams()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
log_message = 'API log: %s' % params
|
log_message = 'API log: %s' % kwargs
|
||||||
try:
|
try:
|
||||||
getattr(log, params.get('type', 'error'))(log_message)
|
getattr(log, type)(log_message)
|
||||||
except:
|
except:
|
||||||
log.error(log_message)
|
log.error(log_message)
|
||||||
except:
|
except:
|
||||||
log.error('Couldn\'t log via API: %s', params)
|
log.error('Couldn\'t log via API: %s', kwargs)
|
||||||
|
|
||||||
|
return {
|
||||||
return jsonified({
|
|
||||||
'success': True
|
'success': True
|
||||||
})
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
|
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
|
||||||
from couchpotato.core.helpers.encoding import ss
|
from couchpotato.core.helpers.encoding import ss
|
||||||
from couchpotato.core.helpers.request import jsonified, getParam
|
|
||||||
from couchpotato.core.helpers.variable import splitString, getTitle
|
from couchpotato.core.helpers.variable import splitString, getTitle
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
@@ -46,24 +45,23 @@ class Manage(Plugin):
|
|||||||
})
|
})
|
||||||
|
|
||||||
if not Env.get('dev'):
|
if not Env.get('dev'):
|
||||||
def updateLibrary():
|
addEvent('app.load', self.updateLibraryQuick)
|
||||||
self.updateLibrary(full = False)
|
|
||||||
addEvent('app.load', updateLibrary)
|
|
||||||
|
|
||||||
def getProgress(self):
|
def getProgress(self, **kwargs):
|
||||||
return jsonified({
|
return {
|
||||||
'progress': self.in_progress
|
'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)
|
fireEventAsync('manage.update', full = True if full == '1' else False)
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True
|
'success': True
|
||||||
})
|
}
|
||||||
|
|
||||||
|
def updateLibraryQuick(self):
|
||||||
|
return self.updateLibrary(full = False)
|
||||||
|
|
||||||
def updateLibrary(self, full = True):
|
def updateLibrary(self, full = True):
|
||||||
last_update = float(Env.prop('manage.last_update', default = 0))
|
last_update = float(Env.prop('manage.last_update', default = 0))
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||||
from couchpotato.core.helpers.request import getParams, jsonified, getParam
|
|
||||||
from couchpotato.core.helpers.variable import getImdb, splitString
|
from couchpotato.core.helpers.variable import getImdb, splitString
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
@@ -125,15 +124,14 @@ class MoviePlugin(Plugin):
|
|||||||
|
|
||||||
db.expire_all()
|
db.expire_all()
|
||||||
|
|
||||||
def getView(self):
|
def getView(self, id = None, **kwargs):
|
||||||
|
|
||||||
movie_id = getParam('id')
|
movie = self.get(id) if id else None
|
||||||
movie = self.get(movie_id) if movie_id else None
|
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': movie is not None,
|
'success': movie is not None,
|
||||||
'movie': movie,
|
'movie': movie,
|
||||||
})
|
}
|
||||||
|
|
||||||
def get(self, movie_id):
|
def get(self, movie_id):
|
||||||
|
|
||||||
@@ -263,15 +261,14 @@ class MoviePlugin(Plugin):
|
|||||||
db.expire_all()
|
db.expire_all()
|
||||||
return ''.join(sorted(chars, key = str.lower))
|
return ''.join(sorted(chars, key = str.lower))
|
||||||
|
|
||||||
def listView(self):
|
def listView(self, **kwargs):
|
||||||
|
|
||||||
params = getParams()
|
status = splitString(kwargs.get('status', None))
|
||||||
status = splitString(params.get('status', None))
|
release_status = splitString(kwargs.get('release_status', None))
|
||||||
release_status = splitString(params.get('release_status', None))
|
limit_offset = kwargs.get('limit_offset', None)
|
||||||
limit_offset = params.get('limit_offset', None)
|
starts_with = kwargs.get('starts_with', None)
|
||||||
starts_with = params.get('starts_with', None)
|
search = kwargs.get('search', None)
|
||||||
search = params.get('search', None)
|
order = kwargs.get('order', None)
|
||||||
order = params.get('order', None)
|
|
||||||
|
|
||||||
total_movies, movies = self.list(
|
total_movies, movies = self.list(
|
||||||
status = status,
|
status = status,
|
||||||
@@ -282,32 +279,31 @@ class MoviePlugin(Plugin):
|
|||||||
order = order
|
order = order
|
||||||
)
|
)
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'empty': len(movies) == 0,
|
'empty': len(movies) == 0,
|
||||||
'total': total_movies,
|
'total': total_movies,
|
||||||
'movies': movies,
|
'movies': movies,
|
||||||
})
|
}
|
||||||
|
|
||||||
def charView(self):
|
def charView(self, **kwargs):
|
||||||
|
|
||||||
params = getParams()
|
status = splitString(kwargs.get('status', None))
|
||||||
status = splitString(params.get('status', None))
|
release_status = splitString(kwargs.get('release_status', None))
|
||||||
release_status = splitString(params.get('release_status', None))
|
|
||||||
chars = self.availableChars(status, release_status)
|
chars = self.availableChars(status, release_status)
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'empty': len(chars) == 0,
|
'empty': len(chars) == 0,
|
||||||
'chars': chars,
|
'chars': chars,
|
||||||
})
|
}
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self, id = '', **kwargs):
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
for id in splitString(getParam('id')):
|
for x in splitString(id):
|
||||||
movie = db.query(Movie).filter_by(id = id).first()
|
movie = db.query(Movie).filter_by(id = x).first()
|
||||||
|
|
||||||
if movie:
|
if movie:
|
||||||
|
|
||||||
@@ -316,17 +312,16 @@ class MoviePlugin(Plugin):
|
|||||||
for title in movie.library.titles:
|
for title in movie.library.titles:
|
||||||
if title.default: default_title = title.title
|
if title.default: default_title = title.title
|
||||||
|
|
||||||
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True)
|
fireEvent('notify.frontend', type = 'movie.busy.%s' % x, data = True)
|
||||||
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id))
|
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
|
||||||
|
|
||||||
db.expire_all()
|
db.expire_all()
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
})
|
}
|
||||||
|
|
||||||
def search(self):
|
def search(self, q = '', **kwargs):
|
||||||
|
|
||||||
q = getParam('q')
|
|
||||||
cache_key = u'%s/%s' % (__name__, simplifyString(q))
|
cache_key = u'%s/%s' % (__name__, simplifyString(q))
|
||||||
movies = Env.get('cache').get(cache_key)
|
movies = Env.get('cache').get(cache_key)
|
||||||
|
|
||||||
@@ -338,11 +333,11 @@ class MoviePlugin(Plugin):
|
|||||||
movies = fireEvent('movie.search', q = q, merge = True)
|
movies = fireEvent('movie.search', q = q, merge = True)
|
||||||
Env.get('cache').set(cache_key, movies)
|
Env.get('cache').set(cache_key, movies)
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'empty': len(movies) == 0 if movies else 0,
|
'empty': len(movies) == 0 if movies else 0,
|
||||||
'movies': movies,
|
'movies': movies,
|
||||||
})
|
}
|
||||||
|
|
||||||
def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None):
|
def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None):
|
||||||
|
|
||||||
@@ -432,33 +427,30 @@ class MoviePlugin(Plugin):
|
|||||||
return movie_dict
|
return movie_dict
|
||||||
|
|
||||||
|
|
||||||
def addView(self):
|
def addView(self, **kwargs):
|
||||||
|
|
||||||
params = getParams()
|
movie_dict = self.add(params = kwargs)
|
||||||
|
|
||||||
movie_dict = self.add(params)
|
return {
|
||||||
|
|
||||||
return jsonified({
|
|
||||||
'success': True,
|
'success': True,
|
||||||
'added': True if movie_dict else False,
|
'added': True if movie_dict else False,
|
||||||
'movie': movie_dict,
|
'movie': movie_dict,
|
||||||
})
|
}
|
||||||
|
|
||||||
def edit(self):
|
def edit(self, id = '', **kwargs):
|
||||||
|
|
||||||
params = getParams()
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
available_status = fireEvent('status.get', 'available', single = True)
|
available_status = fireEvent('status.get', 'available', single = True)
|
||||||
|
|
||||||
ids = splitString(params.get('id'))
|
ids = splitString(id)
|
||||||
for movie_id in ids:
|
for movie_id in ids:
|
||||||
|
|
||||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||||
if not m:
|
if not m:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
m.profile_id = params.get('profile_id')
|
m.profile_id = kwargs.get('profile_id')
|
||||||
|
|
||||||
# Remove releases
|
# Remove releases
|
||||||
for rel in m.releases:
|
for rel in m.releases:
|
||||||
@@ -467,9 +459,9 @@ class MoviePlugin(Plugin):
|
|||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
# Default title
|
# Default title
|
||||||
if params.get('default_title'):
|
if kwargs.get('default_title'):
|
||||||
for title in m.library.titles:
|
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()
|
db.commit()
|
||||||
|
|
||||||
@@ -479,21 +471,19 @@ class MoviePlugin(Plugin):
|
|||||||
fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
|
fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
|
||||||
|
|
||||||
db.expire_all()
|
db.expire_all()
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
})
|
}
|
||||||
|
|
||||||
def deleteView(self):
|
def deleteView(self, id = '', **kwargs):
|
||||||
|
|
||||||
params = getParams()
|
ids = splitString(id)
|
||||||
|
|
||||||
ids = splitString(params.get('id'))
|
|
||||||
for movie_id in ids:
|
for movie_id in ids:
|
||||||
self.delete(movie_id, delete_from = params.get('delete_from', 'all'))
|
self.delete(movie_id, delete_from = kwargs.get('delete_from', 'all'))
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
})
|
}
|
||||||
|
|
||||||
def delete(self, movie_id, delete_from = None):
|
def delete(self, movie_id, delete_from = None):
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
var MovieAction = new Class({
|
var MovieAction = new Class({
|
||||||
|
|
||||||
|
Implements: [Options],
|
||||||
|
|
||||||
class_name: 'action icon2',
|
class_name: 'action icon2',
|
||||||
|
|
||||||
initialize: function(movie){
|
initialize: function(movie, options){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
self.setOptions(options);
|
||||||
|
|
||||||
self.movie = movie;
|
self.movie = movie;
|
||||||
|
|
||||||
self.create();
|
self.create();
|
||||||
@@ -21,6 +25,32 @@ var MovieAction = new Class({
|
|||||||
this.el.removeClass('disable')
|
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(){
|
createMask: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
self.mask = new Element('div.mask', {
|
self.mask = new Element('div.mask', {
|
||||||
@@ -62,10 +92,10 @@ MA.IMDB = new Class({
|
|||||||
create: function(){
|
create: function(){
|
||||||
var self = this;
|
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', {
|
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+'/',
|
'href': 'http://www.imdb.com/title/'+self.id+'/',
|
||||||
'target': '_blank'
|
'target': '_blank'
|
||||||
});
|
});
|
||||||
@@ -83,7 +113,7 @@ MA.Release = new Class({
|
|||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.el = new Element('a.releases.download', {
|
self.el = new Element('a.releases.download', {
|
||||||
'title': 'Show the releases that are available for ' + self.movie.getTitle(),
|
'title': 'Show the releases that are available for ' + self.getTitle(),
|
||||||
'events': {
|
'events': {
|
||||||
'click': self.show.bind(self)
|
'click': self.show.bind(self)
|
||||||
}
|
}
|
||||||
@@ -136,7 +166,7 @@ MA.Release = new Class({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create release
|
// Create release
|
||||||
new Element('div', {
|
var item = new Element('div', {
|
||||||
'class': 'item '+status.identifier,
|
'class': 'item '+status.identifier,
|
||||||
'id': 'release_'+release.id
|
'id': 'release_'+release.id
|
||||||
}).adopt(
|
}).adopt(
|
||||||
@@ -165,11 +195,12 @@ MA.Release = new Class({
|
|||||||
'click': function(e){
|
'click': function(e){
|
||||||
(e).preventDefault();
|
(e).preventDefault();
|
||||||
self.ignore(release);
|
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(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'))
|
if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched'))
|
||||||
@@ -318,6 +349,17 @@ MA.Release = new Class({
|
|||||||
Api.request('release.ignore', {
|
Api.request('release.ignore', {
|
||||||
'data': {
|
'data': {
|
||||||
'id': release.id
|
'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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -355,7 +397,7 @@ MA.Trailer = new Class({
|
|||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
self.el = new Element('a.trailer', {
|
self.el = new Element('a.trailer', {
|
||||||
'title': 'Watch the trailer of ' + self.movie.getTitle(),
|
'title': 'Watch the trailer of ' + self.getTitle(),
|
||||||
'events': {
|
'events': {
|
||||||
'click': self.watch.bind(self)
|
'click': self.watch.bind(self)
|
||||||
}
|
}
|
||||||
@@ -368,12 +410,12 @@ MA.Trailer = new Class({
|
|||||||
|
|
||||||
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 = 'http://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'
|
||||||
var url = data_url.substitute({
|
var url = data_url.substitute({
|
||||||
'title': encodeURI(self.movie.getTitle()),
|
'title': encodeURI(self.getTitle()),
|
||||||
'year': self.movie.get('year'),
|
'year': self.get('year'),
|
||||||
'offset': offset || 1
|
'offset': offset || 1
|
||||||
}),
|
}),
|
||||||
size = $(self.movie).getSize(),
|
size = $(self.movie).getSize(),
|
||||||
height = (size.x/16)*9,
|
height = self.options.height || (size.x/16)*9,
|
||||||
id = 'trailer-'+randomString();
|
id = 'trailer-'+randomString();
|
||||||
|
|
||||||
self.player_container = new Element('div[id='+id+']');
|
self.player_container = new Element('div[id='+id+']');
|
||||||
|
|||||||
@@ -558,11 +558,13 @@
|
|||||||
.movies .options .table .item {
|
.movies .options .table .item {
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
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;
|
text-decoration: line-through;
|
||||||
color: rgba(255,255,255,0.4);
|
color: rgba(255,255,255,0.4);
|
||||||
}
|
}
|
||||||
.movies .options .table .item.ignored .delete:before {
|
.movies .options .table .item.ignored .delete:before,
|
||||||
|
.movies .options .table .item.failed .delete:before {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
content: "\e04b";
|
content: "\e04b";
|
||||||
transform: scale(-1, 1);
|
transform: scale(-1, 1);
|
||||||
@@ -616,7 +618,8 @@
|
|||||||
.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.download { color: #a7fbaf; }
|
||||||
.movies .options .table a.delete { color: #fda3a3; }
|
.movies .options .table a.delete { color: #fda3a3; }
|
||||||
.movies .options .table .ignored a.delete { color: #b5fda3; }
|
.movies .options .table .ignored a.delete,
|
||||||
|
.movies .options .table .failed a.delete { color: #b5fda3; }
|
||||||
|
|
||||||
.movies .options .table .head > * {
|
.movies .options .table .head > * {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -626,7 +629,7 @@
|
|||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movies .movie .trailer_container {
|
.trailer_container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: #000;
|
background: #000;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -636,11 +639,11 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
.movies .movie .trailer_container.hide {
|
.trailer_container.hide {
|
||||||
height: 0 !important;
|
height: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movies .movie .hide_trailer {
|
.hide_trailer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@@ -652,7 +655,7 @@
|
|||||||
transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s;
|
transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s;
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
}
|
}
|
||||||
.movies .movie .hide_trailer.hide {
|
.hide_trailer.hide {
|
||||||
top: -30px;
|
top: -30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -842,7 +845,7 @@
|
|||||||
font-family: 'Elusive-Icons';
|
font-family: 'Elusive-Icons';
|
||||||
content: "\e03e";
|
content: "\e03e";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 20px;
|
||||||
line-height: 45px;
|
line-height: 45px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin: 0 0 0 10px;
|
margin: 0 0 0 10px;
|
||||||
|
|||||||
@@ -193,7 +193,7 @@
|
|||||||
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
||||||
}
|
}
|
||||||
.movie_result .data.open {
|
.movie_result .data.open {
|
||||||
left: 100%;
|
left: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.movie_result:last-child .data { border-bottom: 0; }
|
.movie_result:last-child .data { border-bottom: 0; }
|
||||||
|
|||||||
@@ -185,8 +185,11 @@ Block.Search = new Class({
|
|||||||
|
|
||||||
Block.Search.Item = new Class({
|
Block.Search.Item = new Class({
|
||||||
|
|
||||||
|
Implements: [Options, Events],
|
||||||
|
|
||||||
initialize: function(info, options){
|
initialize: function(info, options){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
self.setOptions(options);
|
||||||
|
|
||||||
self.info = info;
|
self.info = info;
|
||||||
self.alternative_titles = [];
|
self.alternative_titles = [];
|
||||||
@@ -208,17 +211,13 @@ Block.Search.Item = new Class({
|
|||||||
}) : null,
|
}) : null,
|
||||||
self.options_el = new Element('div.options.inlay'),
|
self.options_el = new Element('div.options.inlay'),
|
||||||
self.data_container = new Element('div.data', {
|
self.data_container = new Element('div.data', {
|
||||||
'tween': {
|
|
||||||
duration: 400,
|
|
||||||
transition: 'quint:in:out'
|
|
||||||
},
|
|
||||||
'events': {
|
'events': {
|
||||||
'click': self.showOptions.bind(self)
|
'click': self.showOptions.bind(self)
|
||||||
}
|
}
|
||||||
}).adopt(
|
}).adopt(
|
||||||
new Element('div.info').adopt(
|
new Element('div.info').adopt(
|
||||||
self.title = new Element('h2', {
|
self.title = new Element('h2', {
|
||||||
'text': info.titles[0]
|
'text': info.titles && info.titles.length > 0 ? info.titles[0] : 'Unknown'
|
||||||
}).adopt(
|
}).adopt(
|
||||||
self.year = info.year ? new Element('span.year', {
|
self.year = info.year ? new Element('span.year', {
|
||||||
'text': info.year
|
'text': info.year
|
||||||
@@ -228,12 +227,12 @@ Block.Search.Item = new Class({
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if(info.titles)
|
||||||
info.titles.each(function(title){
|
info.titles.each(function(title){
|
||||||
self.alternativeTitle({
|
self.alternativeTitle({
|
||||||
'title': title
|
'title': title
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
alternativeTitle: function(alternative){
|
alternativeTitle: function(alternative){
|
||||||
@@ -242,6 +241,20 @@ Block.Search.Item = new Class({
|
|||||||
self.alternative_titles.include(alternative);
|
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(){
|
showOptions: function(){
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
@@ -279,6 +292,8 @@ Block.Search.Item = new Class({
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
self.mask.fade('out');
|
self.mask.fade('out');
|
||||||
|
|
||||||
|
self.fireEvent('added');
|
||||||
},
|
},
|
||||||
'onFailure': function(){
|
'onFailure': function(){
|
||||||
self.options_el.empty();
|
self.options_el.empty();
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
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.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Profile, ProfileType, Movie
|
from couchpotato.core.settings.model import Profile, ProfileType, Movie
|
||||||
@@ -46,12 +45,12 @@ class ProfilePlugin(Plugin):
|
|||||||
movie.profile_id = default_profile.get('id')
|
movie.profile_id = default_profile.get('id')
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
def allView(self):
|
def allView(self, **kwargs):
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'list': self.all()
|
'list': self.all()
|
||||||
})
|
}
|
||||||
|
|
||||||
def all(self):
|
def all(self):
|
||||||
|
|
||||||
@@ -65,30 +64,28 @@ class ProfilePlugin(Plugin):
|
|||||||
db.expire_all()
|
db.expire_all()
|
||||||
return temp
|
return temp
|
||||||
|
|
||||||
def save(self):
|
def save(self, **kwargs):
|
||||||
|
|
||||||
params = getParams()
|
|
||||||
|
|
||||||
db = get_session()
|
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:
|
if not p:
|
||||||
p = Profile()
|
p = Profile()
|
||||||
db.add(p)
|
db.add(p)
|
||||||
|
|
||||||
p.label = toUnicode(params.get('label'))
|
p.label = toUnicode(kwargs.get('label'))
|
||||||
p.order = params.get('order', p.order if p.order else 0)
|
p.order = kwargs.get('order', p.order if p.order else 0)
|
||||||
p.core = params.get('core', False)
|
p.core = kwargs.get('core', False)
|
||||||
|
|
||||||
#delete old types
|
#delete old types
|
||||||
[db.delete(t) for t in p.types]
|
[db.delete(t) for t in p.types]
|
||||||
|
|
||||||
order = 0
|
order = 0
|
||||||
for type in params.get('types', []):
|
for type in kwargs.get('types', []):
|
||||||
t = ProfileType(
|
t = ProfileType(
|
||||||
order = order,
|
order = order,
|
||||||
finish = type.get('finish') if order > 0 else 1,
|
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')
|
quality_id = type.get('quality_id')
|
||||||
)
|
)
|
||||||
p.types.append(t)
|
p.types.append(t)
|
||||||
@@ -99,10 +96,10 @@ class ProfilePlugin(Plugin):
|
|||||||
|
|
||||||
profile_dict = p.to_dict(self.to_dict)
|
profile_dict = p.to_dict(self.to_dict)
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'profile': profile_dict
|
'profile': profile_dict
|
||||||
})
|
}
|
||||||
|
|
||||||
def default(self):
|
def default(self):
|
||||||
|
|
||||||
@@ -113,28 +110,25 @@ class ProfilePlugin(Plugin):
|
|||||||
db.expire_all()
|
db.expire_all()
|
||||||
return default_dict
|
return default_dict
|
||||||
|
|
||||||
def saveOrder(self):
|
def saveOrder(self, **kwargs):
|
||||||
|
|
||||||
params = getParams()
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
order = 0
|
order = 0
|
||||||
for profile in params.get('ids', []):
|
for profile in kwargs.get('ids', []):
|
||||||
p = db.query(Profile).filter_by(id = profile).first()
|
p = db.query(Profile).filter_by(id = profile).first()
|
||||||
p.hide = params.get('hidden')[order]
|
p.hide = kwargs.get('hidden')[order]
|
||||||
p.order = order
|
p.order = order
|
||||||
|
|
||||||
order += 1
|
order += 1
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True
|
'success': True
|
||||||
})
|
}
|
||||||
|
|
||||||
def delete(self):
|
def delete(self, id = None, **kwargs):
|
||||||
|
|
||||||
id = getParam('id')
|
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|
||||||
@@ -154,10 +148,10 @@ class ProfilePlugin(Plugin):
|
|||||||
message = log.error('Failed deleting Profile: %s', e)
|
message = log.error('Failed deleting Profile: %s', e)
|
||||||
|
|
||||||
db.expire_all()
|
db.expire_all()
|
||||||
return jsonified({
|
return {
|
||||||
'success': success,
|
'success': success,
|
||||||
'message': message
|
'message': message
|
||||||
})
|
}
|
||||||
|
|
||||||
def fill(self):
|
def fill(self):
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.helpers.request import jsonified, getParams
|
|
||||||
from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
|
from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
@@ -18,8 +17,8 @@ class QualityPlugin(Plugin):
|
|||||||
|
|
||||||
qualities = [
|
qualities = [
|
||||||
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
|
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
|
||||||
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
|
{'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': (3500, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
|
{'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']},
|
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']},
|
||||||
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
|
{'identifier': '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': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
|
||||||
@@ -51,12 +50,12 @@ class QualityPlugin(Plugin):
|
|||||||
def preReleases(self):
|
def preReleases(self):
|
||||||
return self.pre_releases
|
return self.pre_releases
|
||||||
|
|
||||||
def allView(self):
|
def allView(self, **kwargs):
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'list': self.all()
|
'list': self.all()
|
||||||
})
|
}
|
||||||
|
|
||||||
def all(self):
|
def all(self):
|
||||||
|
|
||||||
@@ -88,20 +87,18 @@ class QualityPlugin(Plugin):
|
|||||||
if identifier == q.get('identifier'):
|
if identifier == q.get('identifier'):
|
||||||
return q
|
return q
|
||||||
|
|
||||||
def saveSize(self):
|
def saveSize(self, **kwargs):
|
||||||
|
|
||||||
params = getParams()
|
|
||||||
|
|
||||||
db = get_session()
|
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:
|
if quality:
|
||||||
setattr(quality, params.get('value_type'), params.get('value'))
|
setattr(quality, kwargs.get('value_type'), kwargs.get('value'))
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True
|
'success': True
|
||||||
})
|
}
|
||||||
|
|
||||||
def fill(self):
|
def fill(self):
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import fireEvent, addEvent
|
from couchpotato.core.event import fireEvent, addEvent
|
||||||
from couchpotato.core.helpers.encoding import ss
|
from couchpotato.core.helpers.encoding import ss
|
||||||
from couchpotato.core.helpers.request import getParam, jsonified
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.plugins.scanner.main import Scanner
|
from couchpotato.core.plugins.scanner.main import Scanner
|
||||||
@@ -108,13 +107,11 @@ class Release(Plugin):
|
|||||||
# Check database and update/insert if necessary
|
# 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)
|
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 {
|
||||||
|
'success': self.delete(id)
|
||||||
return jsonified({
|
}
|
||||||
'success': self.delete(release_id)
|
|
||||||
})
|
|
||||||
|
|
||||||
def delete(self, id):
|
def delete(self, id):
|
||||||
|
|
||||||
@@ -146,25 +143,23 @@ class Release(Plugin):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def ignore(self):
|
def ignore(self, id = None, **kwargs):
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
id = getParam('id')
|
|
||||||
|
|
||||||
rel = db.query(Relea).filter_by(id = id).first()
|
rel = db.query(Relea).filter_by(id = id).first()
|
||||||
if rel:
|
if rel:
|
||||||
ignored_status, available_status = fireEvent('status.get', ['ignored', 'available'], single = True)
|
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 is ignored_status.get('id') else ignored_status.get('id')
|
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()
|
db.commit()
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True
|
'success': True
|
||||||
})
|
}
|
||||||
|
|
||||||
def download(self):
|
def download(self, id = None, **kwargs):
|
||||||
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
id = getParam('id')
|
|
||||||
|
|
||||||
snatched_status, done_status = fireEvent('status.get', ['snatched', 'done'], single = True)
|
snatched_status, done_status = fireEvent('status.get', ['snatched', 'done'], single = True)
|
||||||
|
|
||||||
@@ -199,12 +194,12 @@ class Release(Plugin):
|
|||||||
|
|
||||||
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name'])
|
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name'])
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': success
|
'success': success
|
||||||
})
|
}
|
||||||
else:
|
else:
|
||||||
log.error('Couldn\'t find release with id: %s', id)
|
log.error('Couldn\'t find release with id: %s', id)
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': False
|
'success': False
|
||||||
})
|
}
|
||||||
|
|||||||
@@ -14,10 +14,14 @@ rename_options = {
|
|||||||
'year': 'Year (2011)',
|
'year': 'Year (2011)',
|
||||||
'first': 'First letter (M)',
|
'first': 'First letter (M)',
|
||||||
'quality': 'Quality (720p)',
|
'quality': 'Quality (720p)',
|
||||||
|
'quality_type': '(HD) or (SD)',
|
||||||
'video': 'Video (x264)',
|
'video': 'Video (x264)',
|
||||||
'audio': 'Audio (DTS)',
|
'audio': 'Audio (DTS)',
|
||||||
'group': 'Releasegroup name',
|
'group': 'Releasegroup name',
|
||||||
'source': 'Source media (Bluray)',
|
'source': 'Source media (Bluray)',
|
||||||
|
'resolution_width': 'resolution width (1280)',
|
||||||
|
'resolution_height': 'resolution height (720)',
|
||||||
|
'audio_channels': 'audio channels (7.1)',
|
||||||
'original': 'Original filename',
|
'original': 'Original filename',
|
||||||
'original_folder': 'Original foldername',
|
'original_folder': 'Original foldername',
|
||||||
'imdb_id': 'IMDB id (tt0123456)',
|
'imdb_id': 'IMDB id (tt0123456)',
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||||
from couchpotato.core.helpers.encoding import toUnicode, ss
|
from couchpotato.core.helpers.encoding import toUnicode, ss
|
||||||
from couchpotato.core.helpers.request import getParams, jsonified
|
|
||||||
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
|
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
|
||||||
getImdb, link, symlink, tryInt
|
getImdb, link, symlink, tryInt
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
@@ -59,13 +58,12 @@ class Renamer(Plugin):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def scanView(self):
|
def scanView(self, **kwargs):
|
||||||
|
|
||||||
params = getParams()
|
async = tryInt(kwargs.get('async', None))
|
||||||
async = tryInt(params.get('async', None))
|
movie_folder = kwargs.get('movie_folder', None)
|
||||||
movie_folder = params.get('movie_folder', None)
|
downloader = kwargs.get('downloader', None)
|
||||||
downloader = params.get('downloader', None)
|
download_id = kwargs.get('download_id', None)
|
||||||
download_id = params.get('download_id', None)
|
|
||||||
|
|
||||||
fire_handle = fireEvent if not async else fireEventAsync
|
fire_handle = fireEvent if not async else fireEventAsync
|
||||||
|
|
||||||
@@ -74,9 +72,9 @@ class Renamer(Plugin):
|
|||||||
download_info = {'id': download_id, 'downloader': downloader} if download_id else None
|
download_info = {'id': download_id, 'downloader': downloader} if download_id else None
|
||||||
)
|
)
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True
|
'success': True
|
||||||
})
|
}
|
||||||
|
|
||||||
def scan(self, movie_folder = None, download_info = None):
|
def scan(self, movie_folder = None, download_info = None):
|
||||||
|
|
||||||
@@ -183,6 +181,7 @@ class Renamer(Plugin):
|
|||||||
'source': group['meta_data']['source'],
|
'source': group['meta_data']['source'],
|
||||||
'resolution_width': group['meta_data'].get('resolution_width'),
|
'resolution_width': group['meta_data'].get('resolution_width'),
|
||||||
'resolution_height': group['meta_data'].get('resolution_height'),
|
'resolution_height': group['meta_data'].get('resolution_height'),
|
||||||
|
'audio_channels': group['meta_data'].get('audio_channels'),
|
||||||
'imdb_id': library['identifier'],
|
'imdb_id': library['identifier'],
|
||||||
'cd': '',
|
'cd': '',
|
||||||
'cd_nr': '',
|
'cd_nr': '',
|
||||||
@@ -221,15 +220,15 @@ class Renamer(Plugin):
|
|||||||
replacements['cd_nr'] = cd if multiple else ''
|
replacements['cd_nr'] = cd if multiple else ''
|
||||||
|
|
||||||
# Naming
|
# Naming
|
||||||
final_folder_name = self.doReplace(folder_name, replacements).lstrip('. ')
|
final_folder_name = self.doReplace(folder_name, replacements)
|
||||||
final_file_name = self.doReplace(file_name, replacements).lstrip('. ')
|
final_file_name = self.doReplace(file_name, replacements)
|
||||||
replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
|
replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
|
||||||
|
|
||||||
# Meta naming
|
# Meta naming
|
||||||
if file_type is 'trailer':
|
if file_type is 'trailer':
|
||||||
final_file_name = self.doReplace(trailer_name, replacements, remove_multiple = True).lstrip('. ')
|
final_file_name = self.doReplace(trailer_name, replacements, remove_multiple = True)
|
||||||
elif file_type is 'nfo':
|
elif file_type is 'nfo':
|
||||||
final_file_name = self.doReplace(nfo_name, replacements, remove_multiple = True).lstrip('. ')
|
final_file_name = self.doReplace(nfo_name, replacements, remove_multiple = True)
|
||||||
|
|
||||||
# Seperator replace
|
# Seperator replace
|
||||||
if separator:
|
if separator:
|
||||||
@@ -283,7 +282,7 @@ class Renamer(Plugin):
|
|||||||
|
|
||||||
# Don't add language if multiple languages in 1 subtitle file
|
# Don't add language if multiple languages in 1 subtitle file
|
||||||
if len(sub_langs) == 1:
|
if len(sub_langs) == 1:
|
||||||
sub_name = final_file_name.replace(replacements['ext'], '%s.%s' % (sub_langs[0], replacements['ext']))
|
sub_name = sub_name.replace(replacements['ext'], '%s.%s' % (sub_langs[0], replacements['ext']))
|
||||||
rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
|
rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
|
||||||
|
|
||||||
rename_files = mergeDicts(rename_files, rename_extras)
|
rename_files = mergeDicts(rename_files, rename_extras)
|
||||||
@@ -557,7 +556,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
|||||||
replaced = re.sub(r"[\x00:\*\?\"<>\|]", '', replaced)
|
replaced = re.sub(r"[\x00:\*\?\"<>\|]", '', replaced)
|
||||||
|
|
||||||
sep = self.conf('separator')
|
sep = self.conf('separator')
|
||||||
return self.replaceDoubles(replaced).replace(' ', ' ' if not sep else sep)
|
return self.replaceDoubles(replaced.lstrip('. ')).replace(' ', ' ' if not sep else sep)
|
||||||
|
|
||||||
def replaceDoubles(self, string):
|
def replaceDoubles(self, string):
|
||||||
return string.replace(' ', ' ').replace(' .', '.')
|
return string.replace(' ', ' ').replace(' .', '.')
|
||||||
|
|||||||
@@ -432,6 +432,7 @@ class Scanner(Plugin):
|
|||||||
data['audio'] = meta.get('audio', self.getCodec(cur_file, self.codecs['audio']))
|
data['audio'] = meta.get('audio', self.getCodec(cur_file, self.codecs['audio']))
|
||||||
data['resolution_width'] = meta.get('resolution_width', 720)
|
data['resolution_width'] = meta.get('resolution_width', 720)
|
||||||
data['resolution_height'] = meta.get('resolution_height', 480)
|
data['resolution_height'] = meta.get('resolution_height', 480)
|
||||||
|
data['audio_channels'] = meta.get('audio_channels', 2.0)
|
||||||
data['aspect'] = meta.get('resolution_width', 720) / meta.get('resolution_height', 480)
|
data['aspect'] = meta.get('resolution_width', 720) / meta.get('resolution_height', 480)
|
||||||
except:
|
except:
|
||||||
log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc()))
|
log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc()))
|
||||||
@@ -476,6 +477,7 @@ class Scanner(Plugin):
|
|||||||
'audio': ac,
|
'audio': ac,
|
||||||
'resolution_width': tryInt(p.video[0].width),
|
'resolution_width': tryInt(p.video[0].width),
|
||||||
'resolution_height': tryInt(p.video[0].height),
|
'resolution_height': tryInt(p.video[0].height),
|
||||||
|
'audio_channels': p.audio[0].channels,
|
||||||
}
|
}
|
||||||
except ParseError:
|
except ParseError:
|
||||||
log.debug('Failed to parse meta for %s', filename)
|
log.debug('Failed to parse meta for %s', filename)
|
||||||
@@ -582,7 +584,7 @@ class Scanner(Plugin):
|
|||||||
movie = fireEvent('movie.by_hash', file = cur_file, merge = True)
|
movie = fireEvent('movie.by_hash', file = cur_file, merge = True)
|
||||||
|
|
||||||
if len(movie) > 0:
|
if len(movie) > 0:
|
||||||
imdb_id = movie[0]['imdb']
|
imdb_id = movie[0].get('imdb')
|
||||||
if imdb_id:
|
if imdb_id:
|
||||||
log.debug('Found movie via OpenSubtitleHash: %s', cur_file)
|
log.debug('Found movie via OpenSubtitleHash: %s', cur_file)
|
||||||
break
|
break
|
||||||
@@ -600,7 +602,7 @@ class Scanner(Plugin):
|
|||||||
movie = fireEvent('movie.search', q = '%(name)s %(year)s' % name_year, merge = True, limit = 1)
|
movie = fireEvent('movie.search', q = '%(name)s %(year)s' % name_year, merge = True, limit = 1)
|
||||||
|
|
||||||
if len(movie) > 0:
|
if len(movie) > 0:
|
||||||
imdb_id = movie[0]['imdb']
|
imdb_id = movie[0].get('imdb')
|
||||||
log.debug('Found movie via search: %s', cur_file)
|
log.debug('Found movie via search: %s', cur_file)
|
||||||
if imdb_id: break
|
if imdb_id: break
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class Score(Plugin):
|
|||||||
def calculate(self, nzb, movie):
|
def calculate(self, nzb, movie):
|
||||||
''' Calculate the score of a NZB, used for sorting later '''
|
''' Calculate the score of a NZB, used for sorting later '''
|
||||||
|
|
||||||
score = nameScore(toUnicode(nzb['name']), movie['library']['year'])
|
score = nameScore(toUnicode(nzb['name'] + ' ' + nzb.get('name_extra', '')), movie['library']['year'])
|
||||||
|
|
||||||
for movie_title in movie['library']['titles']:
|
for movie_title in movie['library']['titles']:
|
||||||
score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title['title']))
|
score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title['title']))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from couchpotato.core.event import fireEvent
|
from couchpotato.core.event import fireEvent
|
||||||
from couchpotato.core.helpers.encoding import simplifyString
|
from couchpotato.core.helpers.encoding import simplifyString
|
||||||
from couchpotato.core.helpers.variable import tryInt
|
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -42,10 +42,8 @@ def nameScore(name, year):
|
|||||||
|
|
||||||
# Contains preferred word
|
# Contains preferred word
|
||||||
nzb_words = re.split('\W+', simplifyString(name))
|
nzb_words = re.split('\W+', simplifyString(name))
|
||||||
preferred_words = [x.strip() for x in Env.setting('preferred_words', section = 'searcher').split(',')]
|
preferred_words = splitString(Env.setting('preferred_words', section = 'searcher'))
|
||||||
for word in preferred_words:
|
score += 100 * len(list(set(nzb_words) & set(preferred_words)))
|
||||||
if word.strip() and word.strip().lower() in nzb_words:
|
|
||||||
score = score + 100
|
|
||||||
|
|
||||||
return score
|
return score
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,14 @@ config = [{
|
|||||||
'advanced': True,
|
'advanced': True,
|
||||||
'description': 'Cron settings for the searcher see: <a href="http://packages.python.org/APScheduler/cronschedule.html">APScheduler</a> for details.',
|
'description': 'Cron settings for the searcher see: <a href="http://packages.python.org/APScheduler/cronschedule.html">APScheduler</a> for details.',
|
||||||
'options': [
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'run_on_launch',
|
||||||
|
'label': 'Run on launch',
|
||||||
|
'advanced': True,
|
||||||
|
'default': 0,
|
||||||
|
'type': 'bool',
|
||||||
|
'description': 'Force run the searcher after (re)start.',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'name': 'cron_day',
|
'name': 'cron_day',
|
||||||
'label': 'Day',
|
'label': 'Day',
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ from couchpotato import get_session
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||||
from couchpotato.core.helpers.request import jsonified, getParam
|
|
||||||
from couchpotato.core.helpers.variable import md5, getTitle, splitString, \
|
from couchpotato.core.helpers.variable import md5, getTitle, splitString, \
|
||||||
possibleTitles
|
possibleTitles
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
|
from datetime import date
|
||||||
from inspect import ismethod, isfunction
|
from inspect import ismethod, isfunction
|
||||||
from sqlalchemy.exc import InterfaceError
|
from sqlalchemy.exc import InterfaceError
|
||||||
import datetime
|
import datetime
|
||||||
@@ -50,6 +50,9 @@ class Searcher(Plugin):
|
|||||||
}"""},
|
}"""},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if self.conf('run_on_launch'):
|
||||||
|
addEvent('app.load', self.allMovies)
|
||||||
|
|
||||||
addEvent('app.load', self.setCrons)
|
addEvent('app.load', self.setCrons)
|
||||||
addEvent('setting.save.searcher.cron_day.after', self.setCrons)
|
addEvent('setting.save.searcher.cron_day.after', self.setCrons)
|
||||||
addEvent('setting.save.searcher.cron_hour.after', self.setCrons)
|
addEvent('setting.save.searcher.cron_hour.after', self.setCrons)
|
||||||
@@ -58,7 +61,7 @@ class Searcher(Plugin):
|
|||||||
def setCrons(self):
|
def setCrons(self):
|
||||||
fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
|
fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
|
||||||
|
|
||||||
def allMoviesView(self):
|
def allMoviesView(self, **kwargs):
|
||||||
|
|
||||||
in_progress = self.in_progress
|
in_progress = self.in_progress
|
||||||
if not in_progress:
|
if not in_progress:
|
||||||
@@ -67,15 +70,15 @@ class Searcher(Plugin):
|
|||||||
else:
|
else:
|
||||||
fireEvent('notify.frontend', type = 'searcher.already_started', data = True, message = 'Full search already in progress')
|
fireEvent('notify.frontend', type = 'searcher.already_started', data = True, message = 'Full search already in progress')
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': not in_progress
|
'success': not in_progress
|
||||||
})
|
}
|
||||||
|
|
||||||
def getProgress(self):
|
def getProgress(self, **kwargs):
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'progress': self.in_progress
|
'progress': self.in_progress
|
||||||
})
|
}
|
||||||
|
|
||||||
def allMovies(self):
|
def allMovies(self):
|
||||||
|
|
||||||
@@ -146,9 +149,10 @@ class Searcher(Plugin):
|
|||||||
|
|
||||||
pre_releases = fireEvent('quality.pre_releases', single = True)
|
pre_releases = fireEvent('quality.pre_releases', single = True)
|
||||||
release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True)
|
release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True)
|
||||||
available_status, ignored_status = fireEvent('status.get', ['available', 'ignored'], single = True)
|
available_status, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], single = True)
|
||||||
|
|
||||||
found_releases = []
|
found_releases = []
|
||||||
|
too_early_to_search = []
|
||||||
|
|
||||||
default_title = getTitle(movie['library'])
|
default_title = getTitle(movie['library'])
|
||||||
if not default_title:
|
if not default_title:
|
||||||
@@ -161,15 +165,15 @@ class Searcher(Plugin):
|
|||||||
|
|
||||||
ret = False
|
ret = False
|
||||||
for quality_type in movie['profile']['types']:
|
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):
|
if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']):
|
||||||
log.info('Too early to search for %s, %s', (quality_type['quality']['identifier'], default_title))
|
too_early_to_search.append(quality_type['quality']['identifier'])
|
||||||
continue
|
continue
|
||||||
|
|
||||||
has_better_quality = 0
|
has_better_quality = 0
|
||||||
|
|
||||||
# See if better quality is available
|
# See if better quality is available
|
||||||
for release in movie['releases']:
|
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')]:
|
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
|
has_better_quality += 1
|
||||||
|
|
||||||
# Don't search for quality lower then already available.
|
# Don't search for quality lower then already available.
|
||||||
@@ -240,7 +244,7 @@ class Searcher(Plugin):
|
|||||||
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
|
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if nzb['status_id'] == ignored_status.get('id'):
|
if nzb['status_id'] in [ignored_status.get('id'), failed_status.get('id')]:
|
||||||
log.info('Ignored: %s', nzb['name'])
|
log.info('Ignored: %s', nzb['name'])
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -269,6 +273,9 @@ class Searcher(Plugin):
|
|||||||
if self.shuttingDown() or ret:
|
if self.shuttingDown() or ret:
|
||||||
break
|
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 = 'searcher.ended.%s' % movie['id'], data = True)
|
fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
@@ -552,11 +559,12 @@ class Searcher(Plugin):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def couldBeReleased(self, is_pre_release, dates):
|
def couldBeReleased(self, is_pre_release, dates, year = None):
|
||||||
|
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
|
now_year = date.today().year
|
||||||
|
|
||||||
if not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0):
|
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
|
return True
|
||||||
else:
|
else:
|
||||||
|
|
||||||
@@ -586,18 +594,17 @@ class Searcher(Plugin):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def tryNextReleaseView(self):
|
def tryNextReleaseView(self, id = None, **kwargs):
|
||||||
|
|
||||||
trynext = self.tryNextRelease(getParam('id'))
|
trynext = self.tryNextRelease(id)
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': trynext
|
'success': trynext
|
||||||
})
|
}
|
||||||
|
|
||||||
def tryNextRelease(self, movie_id, manual = False):
|
def tryNextRelease(self, movie_id, manual = False):
|
||||||
|
|
||||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
snatched_status, ignored_status = fireEvent('status.get', ['snatched', 'ignored'], single = True)
|
||||||
ignored_status = fireEvent('status.get', 'ignored', single = True)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent
|
from couchpotato.core.event import addEvent
|
||||||
from couchpotato.core.helpers.encoding import toUnicode
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.helpers.request import jsonified
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.core.settings.model import Status
|
from couchpotato.core.settings.model import Status
|
||||||
@@ -42,12 +41,12 @@ class StatusPlugin(Plugin):
|
|||||||
}"""}
|
}"""}
|
||||||
})
|
})
|
||||||
|
|
||||||
def list(self):
|
def list(self, **kwargs):
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'list': self.all()
|
'list': self.all()
|
||||||
})
|
}
|
||||||
|
|
||||||
def getById(self, id):
|
def getById(self, id):
|
||||||
db = get_session()
|
db = get_session()
|
||||||
|
|||||||
@@ -1,22 +1,91 @@
|
|||||||
|
from couchpotato import get_session
|
||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import fireEvent
|
from couchpotato.core.event import fireEvent
|
||||||
from couchpotato.core.helpers.request import jsonified, getParam
|
from couchpotato.core.helpers.encoding import ss
|
||||||
|
from couchpotato.core.helpers.variable import splitString, md5
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
from couchpotato.core.settings.model import Movie
|
||||||
|
from couchpotato.environment import Env
|
||||||
|
from sqlalchemy.sql.expression import or_
|
||||||
|
|
||||||
class Suggestion(Plugin):
|
class Suggestion(Plugin):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
||||||
addApiView('suggestion.view', self.getView)
|
addApiView('suggestion.view', self.suggestView)
|
||||||
|
addApiView('suggestion.ignore', self.ignoreView)
|
||||||
|
|
||||||
def getView(self):
|
def suggestView(self, **kwargs):
|
||||||
|
|
||||||
limit_offset = getParam('limit_offset', None)
|
movies = splitString(kwargs.get('movies', ''))
|
||||||
total_movies, movies = fireEvent('movie.list', status = 'suggest', limit_offset = limit_offset, single = True)
|
ignored = splitString(kwargs.get('ignored', ''))
|
||||||
|
limit = kwargs.get('limit', 6)
|
||||||
|
|
||||||
return jsonified({
|
if not movies or len(movies) == 0:
|
||||||
|
db = get_session()
|
||||||
|
active_movies = db.query(Movie) \
|
||||||
|
.filter(or_(*[Movie.status.has(identifier = s) for s in ['active', 'done']])).all()
|
||||||
|
movies = [x.library.identifier for x in active_movies]
|
||||||
|
|
||||||
|
if not ignored or len(ignored) == 0:
|
||||||
|
ignored = splitString(Env.prop('suggest_ignore', default = ''))
|
||||||
|
|
||||||
|
cached_suggestion = self.getCache('suggestion_cached')
|
||||||
|
if cached_suggestion:
|
||||||
|
suggestions = cached_suggestion
|
||||||
|
else:
|
||||||
|
suggestions = fireEvent('movie.suggest', movies = movies, ignore = ignored, single = True)
|
||||||
|
self.setCache(md5(ss('suggestion_cached')), suggestions, timeout = 6048000) # Cache for 10 weeks
|
||||||
|
|
||||||
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'empty': len(movies) == 0,
|
'count': len(suggestions),
|
||||||
'total': total_movies,
|
'suggestions': suggestions[:limit]
|
||||||
'movies': movies,
|
}
|
||||||
})
|
|
||||||
|
def ignoreView(self, imdb = None, limit = 6, remove_only = False, **kwargs):
|
||||||
|
|
||||||
|
ignored = splitString(Env.prop('suggest_ignore', default = ''))
|
||||||
|
|
||||||
|
if imdb:
|
||||||
|
if not remove_only:
|
||||||
|
ignored.append(imdb)
|
||||||
|
Env.prop('suggest_ignore', ','.join(set(ignored)))
|
||||||
|
|
||||||
|
new_suggestions = self.updateSuggestionCache(ignore_imdb = imdb, limit = limit, ignored = ignored)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'result': True,
|
||||||
|
'ignore_count': len(ignored),
|
||||||
|
'suggestions': new_suggestions[limit - 1:limit]
|
||||||
|
}
|
||||||
|
|
||||||
|
def updateSuggestionCache(self, ignore_imdb = None, limit = 6, ignored = None):
|
||||||
|
|
||||||
|
# Combine with previous suggestion_cache
|
||||||
|
cached_suggestion = self.getCache('suggestion_cached')
|
||||||
|
new_suggestions = []
|
||||||
|
ignored = [] if not ignored else ignored
|
||||||
|
|
||||||
|
if ignore_imdb:
|
||||||
|
for cs in cached_suggestion:
|
||||||
|
if cs.get('imdb') != ignore_imdb:
|
||||||
|
new_suggestions.append(cs)
|
||||||
|
|
||||||
|
# Get new results and add them
|
||||||
|
if len(new_suggestions) - 1 < limit:
|
||||||
|
|
||||||
|
db = get_session()
|
||||||
|
active_movies = db.query(Movie) \
|
||||||
|
.filter(or_(*[Movie.status.has(identifier = s) for s in ['active', 'done']])).all()
|
||||||
|
movies = [x.library.identifier for x in active_movies]
|
||||||
|
|
||||||
|
ignored.extend([x.get('imdb') for x in cached_suggestion])
|
||||||
|
suggestions = fireEvent('movie.suggest', movies = movies, ignore = list(set(ignored)), single = True)
|
||||||
|
|
||||||
|
if suggestions:
|
||||||
|
new_suggestions.extend(suggestions)
|
||||||
|
|
||||||
|
self.setCache(md5(ss('suggestion_cached')), new_suggestions, timeout = 6048000)
|
||||||
|
|
||||||
|
return new_suggestions
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
.suggestions {
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions > h2 {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .movie_result {
|
||||||
|
display: inline-block;
|
||||||
|
width: 33.333%;
|
||||||
|
height: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 960px) {
|
||||||
|
.suggestions .movie_result {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 600px) {
|
||||||
|
.suggestions .movie_result {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .movie_result .data {
|
||||||
|
left: 100px;
|
||||||
|
background: #4e5969;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .movie_result .data .info {
|
||||||
|
top: 15px;
|
||||||
|
left: 15px;
|
||||||
|
right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .movie_result .data .info h2 {
|
||||||
|
white-space: normal;
|
||||||
|
max-height: 120px;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .movie_result .data .info .year {
|
||||||
|
position: static;
|
||||||
|
display: block;
|
||||||
|
margin: 5px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
opacity: .6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .movie_result .data {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .movie_result .options {
|
||||||
|
left: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .movie_result .thumbnail {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .movie_result .actions {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
display: none;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
.suggestions .movie_result:hover .actions {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.suggestions .movie_result .data.open .actions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions .movie_result .actions a {
|
||||||
|
margin-left: 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
var SuggestList = new Class({
|
||||||
|
|
||||||
|
Implements: [Options, Events],
|
||||||
|
|
||||||
|
initialize: function(options){
|
||||||
|
var self = this;
|
||||||
|
self.setOptions(options);
|
||||||
|
|
||||||
|
self.create();
|
||||||
|
},
|
||||||
|
|
||||||
|
create: function(){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
self.el = new Element('div.suggestions', {
|
||||||
|
'events': {
|
||||||
|
'click:relay(a.delete)': function(e, el){
|
||||||
|
(e).stop();
|
||||||
|
|
||||||
|
$(el).getParent('.movie_result').destroy();
|
||||||
|
|
||||||
|
Api.request('suggestion.ignore', {
|
||||||
|
'data': {
|
||||||
|
'imdb': el.get('data-ignore')
|
||||||
|
},
|
||||||
|
'onComplete': self.fill.bind(self)
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).grab(
|
||||||
|
new Element('h2', {
|
||||||
|
'text': 'You might like these'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
self.api_request = Api.request('suggestion.view', {
|
||||||
|
'onComplete': self.fill.bind(self)
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
fill: function(json){
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
Object.each(json.suggestions, function(movie){
|
||||||
|
|
||||||
|
var m = new Block.Search.Item(movie, {
|
||||||
|
'onAdded': function(){
|
||||||
|
self.afterAdded(m, movie)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
m.data_container.grab(
|
||||||
|
new Element('div.actions').adopt(
|
||||||
|
new Element('a.add.icon2', {
|
||||||
|
'title': 'Add movie with your default quality',
|
||||||
|
'data-add': movie.imdb,
|
||||||
|
'events': {
|
||||||
|
'click': m.showOptions.bind(m)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
$(new MA.IMDB(m)),
|
||||||
|
$(new MA.Trailer(m, {
|
||||||
|
'height': 150
|
||||||
|
})),
|
||||||
|
new Element('a.delete.icon2', {
|
||||||
|
'title': 'Don\'t suggest this movie again',
|
||||||
|
'data-ignore': movie.imdb
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
m.data_container.removeEvents('click');
|
||||||
|
$(m).inject(self.el);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
afterAdded: function(m, movie){
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
setTimeout(function(){
|
||||||
|
$(m).destroy();
|
||||||
|
|
||||||
|
Api.request('suggestion.ignore', {
|
||||||
|
'data': {
|
||||||
|
'imdb': movie.imdb,
|
||||||
|
'remove_only': true
|
||||||
|
},
|
||||||
|
'onComplete': self.fill.bind(self)
|
||||||
|
});
|
||||||
|
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
toElement: function(){
|
||||||
|
return this.el;
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
var includes = {{includes|tojson}};
|
{% autoescape None %}
|
||||||
var excludes = {{excludes|tojson}};
|
|
||||||
|
var includes = {{ json_encode(includes) }};
|
||||||
|
var excludes = {{ json_encode(excludes) }};
|
||||||
|
|
||||||
var specialChars = '\\{}+.():-|^$';
|
var specialChars = '\\{}+.():-|^$';
|
||||||
var makeRegex = function(pattern) {
|
var makeRegex = function(pattern) {
|
||||||
@@ -20,6 +22,8 @@ var makeRegex = function(pattern) {
|
|||||||
|
|
||||||
var isCorrectUrl = function() {
|
var isCorrectUrl = function() {
|
||||||
for(i in includes) {
|
for(i in includes) {
|
||||||
|
if(!includes.hasOwnProperty(i)) continue;
|
||||||
|
|
||||||
var reg = includes[i]
|
var reg = includes[i]
|
||||||
if (makeRegex(reg).test(document.location.href))
|
if (makeRegex(reg).test(document.location.href))
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
|
from couchpotato import index
|
||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import fireEvent, addEvent
|
from couchpotato.core.event import fireEvent, addEvent
|
||||||
from couchpotato.core.helpers.request import getParam, jsonified
|
|
||||||
from couchpotato.core.helpers.variable import isDict
|
from couchpotato.core.helpers.variable import isDict
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
from flask.globals import request
|
from tornado.web import RequestHandler
|
||||||
from flask.helpers import url_for
|
|
||||||
from flask.templating import render_template
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
@@ -18,7 +16,8 @@ class Userscript(Plugin):
|
|||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
addApiView('userscript.get/<random>/<path:filename>', self.getUserScript, static = True)
|
addApiView('userscript.get/(.*)/(.*)', self.getUserScript, static = True)
|
||||||
|
|
||||||
addApiView('userscript', self.iFrame)
|
addApiView('userscript', self.iFrame)
|
||||||
addApiView('userscript.add_via_url', self.getViaUrl)
|
addApiView('userscript.add_via_url', self.getViaUrl)
|
||||||
addApiView('userscript.includes', self.getIncludes)
|
addApiView('userscript.includes', self.getIncludes)
|
||||||
@@ -26,38 +25,46 @@ class Userscript(Plugin):
|
|||||||
|
|
||||||
addEvent('userscript.get_version', self.getVersion)
|
addEvent('userscript.get_version', self.getVersion)
|
||||||
|
|
||||||
def bookmark(self):
|
def bookmark(self, host = None, **kwargs):
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
'includes': fireEvent('userscript.get_includes', merge = True),
|
'includes': fireEvent('userscript.get_includes', merge = True),
|
||||||
'excludes': fireEvent('userscript.get_excludes', merge = True),
|
'excludes': fireEvent('userscript.get_excludes', merge = True),
|
||||||
'host': getParam('host', None),
|
'host': host,
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.renderTemplate(__file__, 'bookmark.js', **params)
|
return self.renderTemplate(__file__, 'bookmark.js', **params)
|
||||||
|
|
||||||
def getIncludes(self):
|
def getIncludes(self, **kwargs):
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'includes': fireEvent('userscript.get_includes', merge = True),
|
'includes': fireEvent('userscript.get_includes', merge = True),
|
||||||
'excludes': fireEvent('userscript.get_excludes', merge = True),
|
'excludes': fireEvent('userscript.get_excludes', merge = True),
|
||||||
})
|
|
||||||
|
|
||||||
def getUserScript(self, random = '', filename = ''):
|
|
||||||
|
|
||||||
params = {
|
|
||||||
'includes': fireEvent('userscript.get_includes', merge = True),
|
|
||||||
'excludes': fireEvent('userscript.get_excludes', merge = True),
|
|
||||||
'version': self.getVersion(),
|
|
||||||
'api': '%suserscript/' % url_for('api.index').lstrip('/'),
|
|
||||||
'host': request.host_url,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
script = self.renderTemplate(__file__, 'template.js', **params)
|
def getUserScript(self, route, **kwargs):
|
||||||
self.createFile(os.path.join(Env.get('cache_dir'), 'couchpotato.user.js'), script)
|
|
||||||
|
klass = self
|
||||||
|
|
||||||
|
class UserscriptHandler(RequestHandler):
|
||||||
|
|
||||||
|
def get(self, random, route):
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'includes': fireEvent('userscript.get_includes', merge = True),
|
||||||
|
'excludes': fireEvent('userscript.get_excludes', merge = True),
|
||||||
|
'version': klass.getVersion(),
|
||||||
|
'api': '%suserscript/' % Env.get('api_base'),
|
||||||
|
'host': '%s://%s' % (self.request.protocol, self.request.host),
|
||||||
|
}
|
||||||
|
|
||||||
|
script = klass.renderTemplate(__file__, 'template.js', **params)
|
||||||
|
klass.createFile(os.path.join(Env.get('cache_dir'), 'couchpotato.user.js'), script)
|
||||||
|
|
||||||
|
self.redirect(Env.get('api_base') + 'file.cache/couchpotato.user.js')
|
||||||
|
|
||||||
|
Env.get('app').add_handlers(".*$", [('%s%s' % (Env.get('api_base'), route), UserscriptHandler)])
|
||||||
|
|
||||||
from flask.helpers import send_from_directory
|
|
||||||
return send_from_directory(Env.get('cache_dir'), 'couchpotato.user.js')
|
|
||||||
|
|
||||||
def getVersion(self):
|
def getVersion(self):
|
||||||
|
|
||||||
@@ -69,12 +76,12 @@ class Userscript(Plugin):
|
|||||||
|
|
||||||
return version
|
return version
|
||||||
|
|
||||||
def iFrame(self):
|
def iFrame(self, **kwargs):
|
||||||
return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env)
|
return index()
|
||||||
|
|
||||||
def getViaUrl(self):
|
def getViaUrl(self, url = None, **kwargs):
|
||||||
|
|
||||||
url = getParam('url')
|
print url
|
||||||
|
|
||||||
params = {
|
params = {
|
||||||
'url': url,
|
'url': url,
|
||||||
@@ -84,4 +91,4 @@ class Userscript(Plugin):
|
|||||||
log.error('Failed adding movie via url: %s', url)
|
log.error('Failed adding movie via url: %s', url)
|
||||||
params['error'] = params['movie'] if params['movie'] else 'Failed getting movie info'
|
params['error'] = params['movie'] if params['movie'] else 'Failed getting movie info'
|
||||||
|
|
||||||
return jsonified(params)
|
return params
|
||||||
|
|||||||
@@ -9,15 +9,16 @@
|
|||||||
// @grant none
|
// @grant none
|
||||||
// @version {{version}}
|
// @version {{version}}
|
||||||
|
|
||||||
// @match {{host}}*
|
// @match {{host}}/*
|
||||||
{% for include in includes %}
|
{% for include in includes %}
|
||||||
// @match {{include}}{% endfor %}
|
// @match {{include}}{% end %}
|
||||||
{% for exclude in excludes %}
|
{% for exclude in excludes %}
|
||||||
// @exclude {{exclude}}{% endfor %}
|
// @exclude {{exclude}}{% end %}
|
||||||
// @exclude {{host}}{{api.rstrip('/')}}*
|
// @exclude {{host}}{{api.rstrip('/')}}*
|
||||||
|
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
|
||||||
|
{% autoescape None %}
|
||||||
if (window.top == window.self){ // Only run on top window
|
if (window.top == window.self){ // Only run on top window
|
||||||
|
|
||||||
var version = {{version}},
|
var version = {{version}},
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
from .main import V1Importer
|
|
||||||
|
|
||||||
def start():
|
|
||||||
return V1Importer()
|
|
||||||
|
|
||||||
config = []
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/main.css') }}" type="text/css">
|
|
||||||
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/uniform.generic.css') }}" type="text/css">
|
|
||||||
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/uniform.css') }}" type="text/css">
|
|
||||||
|
|
||||||
<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/library/mootools.js') }}"></script>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
|
|
||||||
window.addEvent('domready', function(){
|
|
||||||
if($('old_db'))
|
|
||||||
$('old_db').addEvent('change', function(){
|
|
||||||
$('form').submit();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{% if message: %}
|
|
||||||
{{ message }}
|
|
||||||
{% else: %}
|
|
||||||
<form id="form" method="post" enctype="multipart/form-data">
|
|
||||||
<input type="file" name="old_db" id="old_db" />
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
from couchpotato.api import addApiView
|
|
||||||
from couchpotato.core.event import fireEventAsync
|
|
||||||
from couchpotato.core.helpers.variable import getImdb
|
|
||||||
from couchpotato.core.logger import CPLog
|
|
||||||
from couchpotato.core.plugins.base import Plugin
|
|
||||||
from couchpotato.environment import Env
|
|
||||||
from flask.globals import request
|
|
||||||
from flask.helpers import url_for
|
|
||||||
import os
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class V1Importer(Plugin):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
addApiView('v1.import', self.fromOld, methods = ['GET', 'POST'])
|
|
||||||
|
|
||||||
def fromOld(self):
|
|
||||||
|
|
||||||
if request.method != 'POST':
|
|
||||||
return self.renderTemplate(__file__, 'form.html', url_for = url_for)
|
|
||||||
|
|
||||||
file = request.files['old_db']
|
|
||||||
|
|
||||||
uploaded_file = os.path.join(Env.get('cache_dir'), 'v1_database.db')
|
|
||||||
|
|
||||||
if os.path.isfile(uploaded_file):
|
|
||||||
os.remove(uploaded_file)
|
|
||||||
|
|
||||||
file.save(uploaded_file)
|
|
||||||
|
|
||||||
try:
|
|
||||||
import sqlite3
|
|
||||||
conn = sqlite3.connect(uploaded_file)
|
|
||||||
|
|
||||||
wanted = []
|
|
||||||
|
|
||||||
t = ('want',)
|
|
||||||
cur = conn.execute('SELECT status, imdb FROM Movie WHERE status=?', t)
|
|
||||||
for row in cur:
|
|
||||||
status, imdb = row
|
|
||||||
if getImdb(imdb):
|
|
||||||
wanted.append(imdb)
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
wanted = set(wanted)
|
|
||||||
for imdb in wanted:
|
|
||||||
fireEventAsync('movie.add', {'identifier': imdb}, search_after = False)
|
|
||||||
|
|
||||||
message = 'Successfully imported %s movie(s)' % len(wanted)
|
|
||||||
except Exception, e:
|
|
||||||
message = 'Failed: %s' % e
|
|
||||||
|
|
||||||
return self.renderTemplate(__file__, 'form.html', url_for = url_for, message = message)
|
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ class Automation(Provider):
|
|||||||
enabled_option = 'automation_enabled'
|
enabled_option = 'automation_enabled'
|
||||||
http_time_between_calls = 2
|
http_time_between_calls = 2
|
||||||
|
|
||||||
interval = 86400
|
interval = 1800
|
||||||
last_checked = 0
|
last_checked = 0
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -51,6 +51,7 @@ class Automation(Provider):
|
|||||||
|
|
||||||
def isMinimalMovie(self, movie):
|
def isMinimalMovie(self, movie):
|
||||||
if not movie.get('rating'):
|
if not movie.get('rating'):
|
||||||
|
log.info('ignoring %s as no rating is available for.', (movie['original_title']))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if movie['rating'] and movie['rating'].get('imdb'):
|
if movie['rating'] and movie['rating'].get('imdb'):
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ class Goodfilms(Automation):
|
|||||||
|
|
||||||
url = 'http://goodfil.ms/%s/queue?page=%d&without_layout=1'
|
url = 'http://goodfil.ms/%s/queue?page=%d&without_layout=1'
|
||||||
|
|
||||||
|
interval = 1800
|
||||||
|
|
||||||
def getIMDBids(self):
|
def getIMDBids(self):
|
||||||
|
|
||||||
if not self.conf('automation_username'):
|
if not self.conf('automation_username'):
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ config = [{
|
|||||||
'list': 'watchlist_providers',
|
'list': 'watchlist_providers',
|
||||||
'name': 'imdb_automation',
|
'name': 'imdb_automation',
|
||||||
'label': 'IMDB',
|
'label': 'IMDB',
|
||||||
'description': 'From any <strong>public</strong> IMDB watchlists. Url should be the RSS link.',
|
'description': 'From any <strong>public</strong> IMDB watchlists. Url should be the CSV link.',
|
||||||
'options': [
|
'options': [
|
||||||
{
|
{
|
||||||
'name': 'automation_enabled',
|
'name': 'automation_enabled',
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class Letterboxd(Automation):
|
|||||||
url = 'http://letterboxd.com/%s/watchlist/'
|
url = 'http://letterboxd.com/%s/watchlist/'
|
||||||
pattern = re.compile(r'(.*)\((\d*)\)')
|
pattern = re.compile(r'(.*)\((\d*)\)')
|
||||||
|
|
||||||
|
interval = 1800
|
||||||
|
|
||||||
def getIMDBids(self):
|
def getIMDBids(self):
|
||||||
|
|
||||||
urls = splitString(self.conf('automation_urls'))
|
urls = splitString(self.conf('automation_urls'))
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ class YarrProvider(Provider):
|
|||||||
sizeKb = ['kb', 'kib']
|
sizeKb = ['kb', 'kib']
|
||||||
|
|
||||||
login_opener = None
|
login_opener = None
|
||||||
|
last_login_check = 0
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
addEvent('provider.enabled_types', self.getEnabledProviderType)
|
addEvent('provider.enabled_types', self.getEnabledProviderType)
|
||||||
@@ -101,17 +102,29 @@ class YarrProvider(Provider):
|
|||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
|
|
||||||
|
# Check if we are still logged in every hour
|
||||||
|
now = time.time()
|
||||||
|
if self.login_opener and self.last_login_check < (now - 3600):
|
||||||
|
try:
|
||||||
|
output = self.urlopen(self.urls['login_check'], opener = self.login_opener)
|
||||||
|
if self.loginCheckSuccess(output):
|
||||||
|
self.last_login_check = now
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.login_opener = None
|
||||||
|
except:
|
||||||
|
self.login_opener = None
|
||||||
|
|
||||||
|
if self.login_opener:
|
||||||
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cookiejar = cookielib.CookieJar()
|
cookiejar = cookielib.CookieJar()
|
||||||
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))
|
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))
|
||||||
opener.addheaders = [('User-Agent', self.user_agent)]
|
output = self.urlopen(self.urls['login'], params = self.getLoginParams(), opener = opener)
|
||||||
urllib2.install_opener(opener)
|
|
||||||
log.info2('Logging into %s', self.urls['login'])
|
|
||||||
f = opener.open(self.urls['login'], self.getLoginParams())
|
|
||||||
output = f.read()
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
if self.loginSuccess(output):
|
if self.loginSuccess(output):
|
||||||
|
self.last_login_check = now
|
||||||
self.login_opener = opener
|
self.login_opener = opener
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -119,15 +132,19 @@ class YarrProvider(Provider):
|
|||||||
except:
|
except:
|
||||||
error = traceback.format_exc()
|
error = traceback.format_exc()
|
||||||
|
|
||||||
|
self.login_opener = None
|
||||||
log.error('Failed to login %s: %s', (self.getName(), error))
|
log.error('Failed to login %s: %s', (self.getName(), error))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def loginSuccess(self, output):
|
def loginSuccess(self, output):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def loginCheckSuccess(self, output):
|
||||||
|
return True
|
||||||
|
|
||||||
def loginDownload(self, url = '', nzb_id = ''):
|
def loginDownload(self, url = '', nzb_id = ''):
|
||||||
try:
|
try:
|
||||||
if not self.login_opener and not self.login():
|
if not self.login():
|
||||||
log.error('Failed downloading from %s', self.getName())
|
log.error('Failed downloading from %s', self.getName())
|
||||||
return self.urlopen(url, opener = self.login_opener)
|
return self.urlopen(url, opener = self.login_opener)
|
||||||
except:
|
except:
|
||||||
@@ -150,7 +167,7 @@ class YarrProvider(Provider):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
# Login if needed
|
# Login if needed
|
||||||
if self.urls.get('login') and (not self.login_opener and not self.login()):
|
if self.urls.get('login') and not self.login():
|
||||||
log.error('Failed to login to: %s', self.getName())
|
log.error('Failed to login to: %s', self.getName())
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -258,7 +275,7 @@ class ResultList(list):
|
|||||||
'id': 0,
|
'id': 0,
|
||||||
'type': self.provider.type,
|
'type': self.provider.type,
|
||||||
'provider': self.provider.getName(),
|
'provider': self.provider.getName(),
|
||||||
'download': self.provider.download,
|
'download': self.provider.loginDownload if self.provider.urls.get('login') else self.provider.download,
|
||||||
'url': '',
|
'url': '',
|
||||||
'name': '',
|
'name': '',
|
||||||
'age': 0,
|
'age': 0,
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ class MovieResultModifier(Plugin):
|
|||||||
'tagline': '',
|
'tagline': '',
|
||||||
'imdb': '',
|
'imdb': '',
|
||||||
'genres': [],
|
'genres': [],
|
||||||
'release_date': {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
from couchpotato import get_session
|
|
||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||||
from couchpotato.core.helpers.request import jsonified, getParams
|
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.movie.base import MovieProvider
|
from couchpotato.core.providers.movie.base import MovieProvider
|
||||||
from couchpotato.core.settings.model import Movie
|
|
||||||
from couchpotato.environment import Env
|
from couchpotato.environment import Env
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -29,7 +26,7 @@ class CouchPotatoApi(MovieProvider):
|
|||||||
addEvent('movie.info', self.getInfo, priority = 1)
|
addEvent('movie.info', self.getInfo, priority = 1)
|
||||||
addEvent('movie.search', self.search, priority = 1)
|
addEvent('movie.search', self.search, priority = 1)
|
||||||
addEvent('movie.release_date', self.getReleaseDate)
|
addEvent('movie.release_date', self.getReleaseDate)
|
||||||
addEvent('movie.suggest', self.suggest)
|
addEvent('movie.suggest', self.getSuggestions)
|
||||||
addEvent('movie.is_movie', self.isMovie)
|
addEvent('movie.is_movie', self.isMovie)
|
||||||
|
|
||||||
addEvent('cp.source_url', self.getSourceUrl)
|
addEvent('cp.source_url', self.getSourceUrl)
|
||||||
@@ -50,8 +47,8 @@ class CouchPotatoApi(MovieProvider):
|
|||||||
'branch': branch,
|
'branch': branch,
|
||||||
}), headers = self.getRequestHeaders())
|
}), headers = self.getRequestHeaders())
|
||||||
|
|
||||||
def search(self, q, limit = 12):
|
def search(self, q, limit = 5):
|
||||||
return self.getJsonData(self.urls['search'] % tryUrlencode(q), headers = self.getRequestHeaders())
|
return self.getJsonData(self.urls['search'] % tryUrlencode(q) + ('?limit=%s' % limit), headers = self.getRequestHeaders())
|
||||||
|
|
||||||
def isMovie(self, identifier = None):
|
def isMovie(self, identifier = None):
|
||||||
|
|
||||||
@@ -83,34 +80,15 @@ class CouchPotatoApi(MovieProvider):
|
|||||||
|
|
||||||
return dates
|
return dates
|
||||||
|
|
||||||
def suggest(self, movies = [], ignore = []):
|
def getSuggestions(self, movies = [], ignore = []):
|
||||||
suggestions = self.getJsonData(self.urls['suggest'], params = {
|
suggestions = self.getJsonData(self.urls['suggest'], params = {
|
||||||
'movies': ','.join(movies),
|
'movies': ','.join(movies),
|
||||||
#'ignore': ','.join(ignore),
|
'ignore': ','.join(ignore),
|
||||||
})
|
}, headers = self.getRequestHeaders())
|
||||||
log.info('Found Suggestions for %s', (suggestions))
|
log.info('Found suggestions for %s movies, %s ignored', (len(movies), len(ignore)))
|
||||||
|
|
||||||
return suggestions
|
return suggestions
|
||||||
|
|
||||||
def suggestView(self):
|
|
||||||
|
|
||||||
params = getParams()
|
|
||||||
movies = params.get('movies')
|
|
||||||
ignore = params.get('ignore', [])
|
|
||||||
|
|
||||||
if not movies:
|
|
||||||
db = get_session()
|
|
||||||
active_movies = db.query(Movie).filter(Movie.status.has(identifier = 'active')).all()
|
|
||||||
movies = [x.library.identifier for x in active_movies]
|
|
||||||
|
|
||||||
suggestions = self.suggest(movies, ignore)
|
|
||||||
|
|
||||||
return jsonified({
|
|
||||||
'success': True,
|
|
||||||
'count': len(suggestions),
|
|
||||||
'suggestions': suggestions
|
|
||||||
})
|
|
||||||
|
|
||||||
def getRequestHeaders(self):
|
def getRequestHeaders(self):
|
||||||
return {
|
return {
|
||||||
'X-CP-Version': fireEvent('app.version', single = True),
|
'X-CP-Version': fireEvent('app.version', single = True),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from couchpotato.core.event import addEvent
|
|||||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.movie.base import MovieProvider
|
from couchpotato.core.providers.movie.base import MovieProvider
|
||||||
from libs.themoviedb import tmdb
|
from themoviedb import tmdb
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class FTDWorld(NZBProvider):
|
|||||||
'detail': 'http://ftdworld.net/spotinfo.php?id=%s',
|
'detail': 'http://ftdworld.net/spotinfo.php?id=%s',
|
||||||
'download': 'http://ftdworld.net/cgi-bin/nzbdown.pl?fileID=%s',
|
'download': 'http://ftdworld.net/cgi-bin/nzbdown.pl?fileID=%s',
|
||||||
'login': 'http://ftdworld.net/api/login.php',
|
'login': 'http://ftdworld.net/api/login.php',
|
||||||
|
'login_check': 'http://ftdworld.net/api/login.php',
|
||||||
}
|
}
|
||||||
|
|
||||||
http_time_between_calls = 3 #seconds
|
http_time_between_calls = 3 #seconds
|
||||||
@@ -58,7 +59,6 @@ class FTDWorld(NZBProvider):
|
|||||||
'age': self.calculateAge(tryInt(item.get('Created'))),
|
'age': self.calculateAge(tryInt(item.get('Created'))),
|
||||||
'size': item.get('Size', 0),
|
'size': item.get('Size', 0),
|
||||||
'url': self.urls['download'] % nzb_id,
|
'url': self.urls['download'] % nzb_id,
|
||||||
'download': self.loginDownload,
|
|
||||||
'detail_url': self.urls['detail'] % nzb_id,
|
'detail_url': self.urls['detail'] % nzb_id,
|
||||||
'score': (tryInt(item.get('webPlus', 0)) - tryInt(item.get('webMin', 0))) * 3,
|
'score': (tryInt(item.get('webPlus', 0)) - tryInt(item.get('webMin', 0))) * 3,
|
||||||
})
|
})
|
||||||
@@ -78,3 +78,6 @@ class FTDWorld(NZBProvider):
|
|||||||
return json.loads(output).get('goodToGo', False)
|
return json.loads(output).get('goodToGo', False)
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
loginCheckSuccess = loginSuccess
|
||||||
|
|
||||||
|
|||||||
@@ -53,11 +53,20 @@ class Newznab(NZBProvider, RSS):
|
|||||||
for nzb in nzbs:
|
for nzb in nzbs:
|
||||||
|
|
||||||
date = None
|
date = None
|
||||||
|
spotter = None
|
||||||
for item in nzb:
|
for item in nzb:
|
||||||
|
if date and spotter:
|
||||||
|
break
|
||||||
if item.attrib.get('name') == 'usenetdate':
|
if item.attrib.get('name') == 'usenetdate':
|
||||||
date = item.attrib.get('value')
|
date = item.attrib.get('value')
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Get the name of the person who posts the spot
|
||||||
|
if item.attrib.get('name') == 'poster':
|
||||||
|
if "@spot.net" in item.attrib.get('value'):
|
||||||
|
spotter = item.attrib.get('value').split("@")[0]
|
||||||
|
continue
|
||||||
|
|
||||||
if not date:
|
if not date:
|
||||||
date = self.getTextElement(nzb, 'pubDate')
|
date = self.getTextElement(nzb, 'pubDate')
|
||||||
|
|
||||||
@@ -67,10 +76,15 @@ class Newznab(NZBProvider, RSS):
|
|||||||
if not name:
|
if not name:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
name_extra = ''
|
||||||
|
if spotter:
|
||||||
|
name_extra = spotter
|
||||||
|
|
||||||
results.append({
|
results.append({
|
||||||
'id': nzb_id,
|
'id': nzb_id,
|
||||||
'provider_extra': urlparse(host['host']).hostname or host['host'],
|
'provider_extra': urlparse(host['host']).hostname or host['host'],
|
||||||
'name': self.getTextElement(nzb, 'title'),
|
'name': name,
|
||||||
|
'name_extra': name_extra,
|
||||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
||||||
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
|
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
|
||||||
'url': (self.getUrl(host['host'], self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host),
|
'url': (self.getUrl(host['host'], self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host),
|
||||||
@@ -81,17 +95,24 @@ class Newznab(NZBProvider, RSS):
|
|||||||
|
|
||||||
def getHosts(self):
|
def getHosts(self):
|
||||||
|
|
||||||
uses = splitString(str(self.conf('use')))
|
uses = splitString(str(self.conf('use')), clean = False)
|
||||||
hosts = splitString(self.conf('host'))
|
hosts = splitString(self.conf('host'), clean = False)
|
||||||
api_keys = splitString(self.conf('api_key'))
|
api_keys = splitString(self.conf('api_key'), clean = False)
|
||||||
extra_score = splitString(self.conf('extra_score'))
|
extra_score = splitString(self.conf('extra_score'), clean = False)
|
||||||
|
|
||||||
list = []
|
list = []
|
||||||
for nr in range(len(hosts)):
|
for nr in range(len(hosts)):
|
||||||
|
|
||||||
|
try: key = api_keys[nr]
|
||||||
|
except: key = ''
|
||||||
|
|
||||||
|
try: host = hosts[nr]
|
||||||
|
except: host = ''
|
||||||
|
|
||||||
list.append({
|
list.append({
|
||||||
'use': uses[nr],
|
'use': uses[nr],
|
||||||
'host': hosts[nr],
|
'host': host,
|
||||||
'api_key': api_keys[nr],
|
'api_key': key,
|
||||||
'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0
|
'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class NzbIndex(NZBProvider, RSS):
|
|||||||
|
|
||||||
def _searchOnTitle(self, title, movie, quality, results):
|
def _searchOnTitle(self, title, movie, quality, results):
|
||||||
|
|
||||||
q = '"%s %s"' % (title, movie['library']['year'])
|
q = '"%s %s" | "%s (%s)"' % (title, movie['library']['year'], title, movie['library']['year'])
|
||||||
arguments = tryUrlencode({
|
arguments = tryUrlencode({
|
||||||
'q': q,
|
'q': q,
|
||||||
'age': Env.setting('retention', 'nzb'),
|
'age': Env.setting('retention', 'nzb'),
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
from .main import AwesomeHD
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return AwesomeHD()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'awesomehd',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'subtab': 'providers',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'Awesome-HD',
|
||||||
|
'description': 'See <a href="https://awesome-hd.net">AHD</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'passkey',
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'only_internal',
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Only search for internal releases.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'prefer_internal',
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'bool',
|
||||||
|
'default': 1,
|
||||||
|
'description': 'Favors internal releases over non-internal releases.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'favor',
|
||||||
|
'advanced': True,
|
||||||
|
'default': 'both',
|
||||||
|
'type': 'dropdown',
|
||||||
|
'values': [('Encodes & Remuxes', 'both'), ('Encodes', 'encode'), ('Remuxes', 'remux'), ('None', 'none')],
|
||||||
|
'description': 'Give extra scoring to encodes or remuxes.'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'type': 'int',
|
||||||
|
'default': 20,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AwesomeHD(TorrentProvider):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'test' : 'https://awesome-hd.net/',
|
||||||
|
'detail' : 'https://awesome-hd.net/torrents.php?torrentid=%s',
|
||||||
|
'search' : 'https://awesome-hd.net/searchapi.php?action=imdbsearch&passkey=%s&imdb=%s&internal=%s',
|
||||||
|
'download' : 'https://awesome-hd.net/torrents.php?action=download&id=%s&authkey=%s&torrent_pass=%s',
|
||||||
|
}
|
||||||
|
http_time_between_calls = 1
|
||||||
|
|
||||||
|
def _search(self, movie, quality, results):
|
||||||
|
|
||||||
|
data = self.getHTMLData(self.urls['search'] % (self.conf('passkey'), movie['library']['identifier'], self.conf('only_internal')))
|
||||||
|
|
||||||
|
if data:
|
||||||
|
try:
|
||||||
|
soup = BeautifulSoup(data)
|
||||||
|
authkey = soup.find('authkey').get_text()
|
||||||
|
entries = soup.find_all('torrent')
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
|
||||||
|
torrentscore = 0
|
||||||
|
torrent_id = entry.find('id').get_text()
|
||||||
|
name = entry.find('name').get_text()
|
||||||
|
year = entry.find('year').get_text()
|
||||||
|
releasegroup = entry.find('releasegroup').get_text()
|
||||||
|
resolution = entry.find('resolution').get_text()
|
||||||
|
encoding = entry.find('encoding').get_text()
|
||||||
|
freeleech = entry.find('freeleech').get_text()
|
||||||
|
torrent_desc = '/ %s / %s / %s ' % (releasegroup, resolution, encoding)
|
||||||
|
|
||||||
|
if freeleech == '0.25' and self.conf('prefer_internal'):
|
||||||
|
torrent_desc += '/ Internal'
|
||||||
|
torrentscore += 200
|
||||||
|
|
||||||
|
if encoding == 'x264' and self.conf('favor') in ['encode', 'both']:
|
||||||
|
torrentscore += 300
|
||||||
|
if re.search('Remux', encoding) and self.conf('favor') in ['remux', 'both']:
|
||||||
|
torrentscore += 200
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'id': torrent_id,
|
||||||
|
'name': re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) %s' % (name, year, torrent_desc)),
|
||||||
|
'url': self.urls['download'] % (torrent_id, authkey, self.conf('passkey')),
|
||||||
|
'detail_url': self.urls['detail'] % torrent_id,
|
||||||
|
'size': self.parseSize(entry.find('size').get_text()),
|
||||||
|
'seeders': tryInt(entry.find('seeders').get_text()),
|
||||||
|
'leechers': tryInt(entry.find('leechers').get_text()),
|
||||||
|
'score': torrentscore
|
||||||
|
})
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
@@ -16,6 +16,7 @@ class HDBits(TorrentProvider):
|
|||||||
'detail' : 'https://hdbits.org/details.php?id=%s&source=browse',
|
'detail' : 'https://hdbits.org/details.php?id=%s&source=browse',
|
||||||
'search' : 'https://hdbits.org/json_search.php?imdb=%s',
|
'search' : 'https://hdbits.org/json_search.php?imdb=%s',
|
||||||
'download' : 'https://hdbits.org/download.php/%s.torrent?id=%s&passkey=%s&source=details.browse',
|
'download' : 'https://hdbits.org/download.php/%s.torrent?id=%s&passkey=%s&source=details.browse',
|
||||||
|
'login_check': 'http://hdbits.org/inbox.php',
|
||||||
}
|
}
|
||||||
|
|
||||||
http_time_between_calls = 1 #seconds
|
http_time_between_calls = 1 #seconds
|
||||||
@@ -30,7 +31,7 @@ class HDBits(TorrentProvider):
|
|||||||
results.append({
|
results.append({
|
||||||
'id': result['id'],
|
'id': result['id'],
|
||||||
'name': result['title'],
|
'name': result['title'],
|
||||||
'url': self.urls['download'] % (result['title'], result['id'], self.conf('passkey')),
|
'url': self.urls['download'] % (result['id'], result['id'], self.conf('passkey')),
|
||||||
'detail_url': self.urls['detail'] % result['id'],
|
'detail_url': self.urls['detail'] % result['id'],
|
||||||
'size': self.parseSize(result['size']),
|
'size': self.parseSize(result['size']),
|
||||||
'seeders': tryInt(result['seeder']),
|
'seeders': tryInt(result['seeder']),
|
||||||
@@ -53,3 +54,5 @@ class HDBits(TorrentProvider):
|
|||||||
|
|
||||||
def loginSuccess(self, output):
|
def loginSuccess(self, output):
|
||||||
return '/logout.php' in output.lower()
|
return '/logout.php' in output.lower()
|
||||||
|
|
||||||
|
loginCheckSuccess = loginSuccess
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from couchpotato.core.logger import CPLog
|
|||||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -15,7 +14,8 @@ class IPTorrents(TorrentProvider):
|
|||||||
'test' : 'http://www.iptorrents.com/',
|
'test' : 'http://www.iptorrents.com/',
|
||||||
'base_url' : 'http://www.iptorrents.com',
|
'base_url' : 'http://www.iptorrents.com',
|
||||||
'login' : 'http://www.iptorrents.com/torrents/',
|
'login' : 'http://www.iptorrents.com/torrents/',
|
||||||
'search' : 'http://www.iptorrents.com/torrents/?l%d=1%s&q=%s&qf=ti',
|
'login_check': 'http://www.iptorrents.com/inbox.php',
|
||||||
|
'search' : 'http://www.iptorrents.com/torrents/?l%d=1%s&q=%s&qf=ti&p=%d',
|
||||||
}
|
}
|
||||||
|
|
||||||
cat_ids = [
|
cat_ids = [
|
||||||
@@ -32,48 +32,62 @@ class IPTorrents(TorrentProvider):
|
|||||||
|
|
||||||
freeleech = '' if not self.conf('freeleech') else '&free=on'
|
freeleech = '' if not self.conf('freeleech') else '&free=on'
|
||||||
|
|
||||||
url = self.urls['search'] % (self.getCatId(quality['identifier'])[0], freeleech, tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])))
|
pages = 1
|
||||||
data = self.getHTMLData(url, opener = self.login_opener)
|
current_page = 1
|
||||||
|
while current_page <= pages and not self.shuttingDown():
|
||||||
|
|
||||||
if data:
|
url = self.urls['search'] % (self.getCatId(quality['identifier'])[0], freeleech, tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])), current_page)
|
||||||
html = BeautifulSoup(data)
|
data = self.getHTMLData(url, opener = self.login_opener)
|
||||||
|
|
||||||
try:
|
if data:
|
||||||
result_table = html.find('table', attrs = {'class' : 'torrents'})
|
html = BeautifulSoup(data)
|
||||||
|
|
||||||
if not result_table or 'nothing found!' in data.lower():
|
try:
|
||||||
return
|
page_nav = html.find('span', attrs = {'class' : 'page_nav'})
|
||||||
|
if page_nav:
|
||||||
|
next_link = page_nav.find("a", text = "Next")
|
||||||
|
if next_link:
|
||||||
|
final_page_link = next_link.previous_sibling.previous_sibling
|
||||||
|
pages = int(final_page_link.string)
|
||||||
|
|
||||||
entries = result_table.find_all('tr')
|
result_table = html.find('table', attrs = {'class' : 'torrents'})
|
||||||
|
|
||||||
for result in entries[1:]:
|
if not result_table or 'nothing found!' in data.lower():
|
||||||
|
return
|
||||||
|
|
||||||
torrent = result.find_all('td')[1].find('a')
|
entries = result_table.find_all('tr')
|
||||||
|
|
||||||
torrent_id = torrent['href'].replace('/details.php?id=', '')
|
for result in entries[1:]:
|
||||||
torrent_name = torrent.string
|
|
||||||
torrent_download_url = self.urls['base_url'] + (result.find_all('td')[3].find('a'))['href'].replace(' ', '.')
|
|
||||||
torrent_details_url = self.urls['base_url'] + torrent['href']
|
|
||||||
torrent_size = self.parseSize(result.find_all('td')[5].string)
|
|
||||||
torrent_seeders = tryInt(result.find('td', attrs = {'class' : 'ac t_seeders'}).string)
|
|
||||||
torrent_leechers = tryInt(result.find('td', attrs = {'class' : 'ac t_leechers'}).string)
|
|
||||||
|
|
||||||
results.append({
|
torrent = result.find_all('td')
|
||||||
'id': torrent_id,
|
if len(torrent) <= 1:
|
||||||
'name': torrent_name,
|
break
|
||||||
'url': torrent_download_url,
|
|
||||||
'detail_url': torrent_details_url,
|
|
||||||
'download': self.loginDownload,
|
|
||||||
'size': torrent_size,
|
|
||||||
'seeders': torrent_seeders,
|
|
||||||
'leechers': torrent_leechers,
|
|
||||||
})
|
|
||||||
|
|
||||||
except:
|
torrent = torrent[1].find('a')
|
||||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
|
||||||
|
|
||||||
def loginSuccess(self, output):
|
torrent_id = torrent['href'].replace('/details.php?id=', '')
|
||||||
return 'don\'t have an account' not in output.lower()
|
torrent_name = torrent.string
|
||||||
|
torrent_download_url = self.urls['base_url'] + (result.find_all('td')[3].find('a'))['href'].replace(' ', '.')
|
||||||
|
torrent_details_url = self.urls['base_url'] + torrent['href']
|
||||||
|
torrent_size = self.parseSize(result.find_all('td')[5].string)
|
||||||
|
torrent_seeders = tryInt(result.find('td', attrs = {'class' : 'ac t_seeders'}).string)
|
||||||
|
torrent_leechers = tryInt(result.find('td', attrs = {'class' : 'ac t_leechers'}).string)
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'id': torrent_id,
|
||||||
|
'name': torrent_name,
|
||||||
|
'url': torrent_download_url,
|
||||||
|
'detail_url': torrent_details_url,
|
||||||
|
'size': torrent_size,
|
||||||
|
'seeders': torrent_seeders,
|
||||||
|
'leechers': torrent_leechers,
|
||||||
|
})
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
break
|
||||||
|
|
||||||
|
current_page += 1
|
||||||
|
|
||||||
def getLoginParams(self):
|
def getLoginParams(self):
|
||||||
return tryUrlencode({
|
return tryUrlencode({
|
||||||
@@ -81,3 +95,9 @@ class IPTorrents(TorrentProvider):
|
|||||||
'password': self.conf('password'),
|
'password': self.conf('password'),
|
||||||
'login': 'submit',
|
'login': 'submit',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def loginSuccess(self, output):
|
||||||
|
return 'don\'t have an account' not in output.lower()
|
||||||
|
|
||||||
|
def loginCheckSuccess(self, output):
|
||||||
|
return '/logout.php' in output.lower()
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ log = CPLog(__name__)
|
|||||||
class KickAssTorrents(TorrentMagnetProvider):
|
class KickAssTorrents(TorrentMagnetProvider):
|
||||||
|
|
||||||
urls = {
|
urls = {
|
||||||
'test': 'https://kat.ph/',
|
'test': 'https://kickass.to/',
|
||||||
'detail': 'https://kat.ph/%s',
|
'detail': 'https://kickass.to/%s',
|
||||||
'search': 'https://kat.ph/%s-i%s/',
|
'search': 'https://kickass.to/%s-i%s/',
|
||||||
}
|
}
|
||||||
|
|
||||||
cat_ids = [
|
cat_ids = [
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts
|
|||||||
from couchpotato.core.logger import CPLog
|
from couchpotato.core.logger import CPLog
|
||||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||||
from dateutil.parser import parse
|
from dateutil.parser import parse
|
||||||
import cookielib
|
|
||||||
import htmlentitydefs
|
import htmlentitydefs
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import urllib2
|
|
||||||
|
|
||||||
log = CPLog(__name__)
|
log = CPLog(__name__)
|
||||||
|
|
||||||
@@ -21,9 +19,12 @@ class PassThePopcorn(TorrentProvider):
|
|||||||
'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s',
|
'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s',
|
||||||
'torrent': 'https://tls.passthepopcorn.me/torrents.php',
|
'torrent': 'https://tls.passthepopcorn.me/torrents.php',
|
||||||
'login': 'https://tls.passthepopcorn.me/ajax.php?action=login',
|
'login': 'https://tls.passthepopcorn.me/ajax.php?action=login',
|
||||||
|
'login_check': 'https://tls.passthepopcorn.me/ajax.php?action=login',
|
||||||
'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d'
|
'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
http_time_between_calls = 2
|
||||||
|
|
||||||
quality_search_params = {
|
quality_search_params = {
|
||||||
'bd50': {'media': 'Blu-ray', 'format': 'BD50'},
|
'bd50': {'media': 'Blu-ray', 'format': 'BD50'},
|
||||||
'1080p': {'resolution': '1080p'},
|
'1080p': {'resolution': '1080p'},
|
||||||
@@ -52,18 +53,6 @@ class PassThePopcorn(TorrentProvider):
|
|||||||
'cam': {'Source': ['CAM']}
|
'cam': {'Source': ['CAM']}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotLoggedInHTTPError(urllib2.HTTPError):
|
|
||||||
def __init__(self, url, code, msg, headers, fp):
|
|
||||||
urllib2.HTTPError.__init__(self, url, code, msg, headers, fp)
|
|
||||||
|
|
||||||
class PTPHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
|
|
||||||
def http_error_302(self, req, fp, code, msg, headers):
|
|
||||||
log.debug("302 detected; redirected to %s", headers['Location'])
|
|
||||||
if (headers['Location'] != 'login.php'):
|
|
||||||
return urllib2.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
|
|
||||||
else:
|
|
||||||
raise PassThePopcorn.NotLoggedInHTTPError(req.get_full_url(), code, msg, headers, fp)
|
|
||||||
|
|
||||||
def _search(self, movie, quality, results):
|
def _search(self, movie, quality, results):
|
||||||
|
|
||||||
movie_title = getTitle(movie['library'])
|
movie_title = getTitle(movie['library'])
|
||||||
@@ -75,17 +64,8 @@ class PassThePopcorn(TorrentProvider):
|
|||||||
'searchstr': movie['library']['identifier']
|
'searchstr': movie['library']['identifier']
|
||||||
})
|
})
|
||||||
|
|
||||||
# Do login for the cookies
|
url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params))
|
||||||
if not self.login_opener and not self.login():
|
res = self.getJsonData(url, opener = self.login_opener)
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params))
|
|
||||||
txt = self.urlopen(url, opener = self.login_opener)
|
|
||||||
res = json.loads(txt)
|
|
||||||
except:
|
|
||||||
log.error('Search on PassThePopcorn.me (%s) failed (could not decode JSON)', params)
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not 'Movies' in res:
|
if not 'Movies' in res:
|
||||||
@@ -136,40 +116,11 @@ class PassThePopcorn(TorrentProvider):
|
|||||||
'leechers': tryInt(torrent['Leechers']),
|
'leechers': tryInt(torrent['Leechers']),
|
||||||
'score': torrentscore,
|
'score': torrentscore,
|
||||||
'extra_check': extra_check,
|
'extra_check': extra_check,
|
||||||
'download': self.loginDownload,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
except:
|
except:
|
||||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
def login(self):
|
|
||||||
|
|
||||||
cookieprocessor = urllib2.HTTPCookieProcessor(cookielib.CookieJar())
|
|
||||||
opener = urllib2.build_opener(cookieprocessor, PassThePopcorn.PTPHTTPRedirectHandler())
|
|
||||||
opener.addheaders = [
|
|
||||||
('User-Agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.75 Safari/537.1'),
|
|
||||||
('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
|
|
||||||
('Accept-Language', 'en-gb,en;q=0.5'),
|
|
||||||
('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7'),
|
|
||||||
('Keep-Alive', '115'),
|
|
||||||
('Connection', 'keep-alive'),
|
|
||||||
('Cache-Control', 'max-age=0'),
|
|
||||||
]
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = opener.open(self.urls['login'], self.getLoginParams())
|
|
||||||
except urllib2.URLError as e:
|
|
||||||
log.error('Login to PassThePopcorn failed: %s', e)
|
|
||||||
return False
|
|
||||||
|
|
||||||
if response.getcode() == 200:
|
|
||||||
log.debug('Login HTTP status 200; seems successful')
|
|
||||||
self.login_opener = opener
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
log.error('Login to PassThePopcorn failed: returned code %d', response.getcode())
|
|
||||||
return False
|
|
||||||
|
|
||||||
def torrentMeetsQualitySpec(self, torrent, quality):
|
def torrentMeetsQualitySpec(self, torrent, quality):
|
||||||
|
|
||||||
if not quality in self.post_search_filters:
|
if not quality in self.post_search_filters:
|
||||||
@@ -186,7 +137,7 @@ class PassThePopcorn(TorrentProvider):
|
|||||||
seen_one = False
|
seen_one = False
|
||||||
|
|
||||||
if not field in torrent:
|
if not field in torrent:
|
||||||
log.debug('Torrent with ID %s has no field "%s"; cannot apply post-search-filter for quality "%s"', (torrent['Id'], field, quality))
|
log.debug('Torrent with ID %s has no field "%s"; cannot apply post-search-filter for quality "%s"', (torrent['id'], field, quality))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for spec in specs:
|
for spec in specs:
|
||||||
@@ -244,3 +195,11 @@ class PassThePopcorn(TorrentProvider):
|
|||||||
'keeplogged': '1',
|
'keeplogged': '1',
|
||||||
'login': 'Login'
|
'login': 'Login'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def loginSuccess(self, output):
|
||||||
|
try:
|
||||||
|
return json.loads(output).get('Result', '').lower() == 'ok'
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
loginCheckSuccess = loginSuccess
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ class SceneAccess(TorrentProvider):
|
|||||||
|
|
||||||
urls = {
|
urls = {
|
||||||
'test': 'https://www.sceneaccess.eu/',
|
'test': 'https://www.sceneaccess.eu/',
|
||||||
'login' : 'https://www.sceneaccess.eu/login',
|
'login': 'https://www.sceneaccess.eu/login',
|
||||||
|
'login_check': 'https://www.sceneaccess.eu/inbox',
|
||||||
'detail': 'https://www.sceneaccess.eu/details?id=%s',
|
'detail': 'https://www.sceneaccess.eu/details?id=%s',
|
||||||
'search': 'https://www.sceneaccess.eu/browse?method=2&c%d=%d',
|
'search': 'https://www.sceneaccess.eu/browse?method=2&c%d=%d',
|
||||||
'download': 'https://www.sceneaccess.eu/%s',
|
'download': 'https://www.sceneaccess.eu/%s',
|
||||||
@@ -39,9 +40,6 @@ class SceneAccess(TorrentProvider):
|
|||||||
})
|
})
|
||||||
url = "%s&%s" % (url, arguments)
|
url = "%s&%s" % (url, arguments)
|
||||||
|
|
||||||
# Do login for the cookies
|
|
||||||
if not self.login_opener and not self.login():
|
|
||||||
return
|
|
||||||
|
|
||||||
data = self.getHTMLData(url, opener = self.login_opener)
|
data = self.getHTMLData(url, opener = self.login_opener)
|
||||||
|
|
||||||
@@ -69,7 +67,6 @@ class SceneAccess(TorrentProvider):
|
|||||||
'size': self.parseSize(result.find('td', attrs = {'class' : 'ttr_size'}).contents[0]),
|
'size': self.parseSize(result.find('td', attrs = {'class' : 'ttr_size'}).contents[0]),
|
||||||
'seeders': tryInt(result.find('td', attrs = {'class' : 'ttr_seeders'}).find('a').string),
|
'seeders': tryInt(result.find('td', attrs = {'class' : 'ttr_seeders'}).find('a').string),
|
||||||
'leechers': tryInt(leechers.string) if leechers else 0,
|
'leechers': tryInt(leechers.string) if leechers else 0,
|
||||||
'download': self.loginDownload,
|
|
||||||
'get_more_info': self.getMoreInfo,
|
'get_more_info': self.getMoreInfo,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -91,3 +88,8 @@ class SceneAccess(TorrentProvider):
|
|||||||
|
|
||||||
item['description'] = description
|
item['description'] = description
|
||||||
return item
|
return item
|
||||||
|
|
||||||
|
def loginSuccess(self, output):
|
||||||
|
return '/inbox' in output.lower()
|
||||||
|
|
||||||
|
loginCheckSuccess = loginSuccess
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class SceneHD(TorrentProvider):
|
|||||||
urls = {
|
urls = {
|
||||||
'test': 'https://scenehd.org/',
|
'test': 'https://scenehd.org/',
|
||||||
'login' : 'https://scenehd.org/takelogin.php',
|
'login' : 'https://scenehd.org/takelogin.php',
|
||||||
|
'login_check': 'https://scenehd.org/my.php',
|
||||||
'detail': 'https://scenehd.org/details.php?id=%s',
|
'detail': 'https://scenehd.org/details.php?id=%s',
|
||||||
'search': 'https://scenehd.org/browse.php?ajax',
|
'search': 'https://scenehd.org/browse.php?ajax',
|
||||||
'download': 'https://scenehd.org/download.php?id=%s',
|
'download': 'https://scenehd.org/download.php?id=%s',
|
||||||
@@ -28,10 +29,6 @@ class SceneHD(TorrentProvider):
|
|||||||
})
|
})
|
||||||
url = "%s&%s" % (self.urls['search'], arguments)
|
url = "%s&%s" % (self.urls['search'], arguments)
|
||||||
|
|
||||||
# Cookie login
|
|
||||||
if not self.login_opener and not self.login():
|
|
||||||
return
|
|
||||||
|
|
||||||
data = self.getHTMLData(url, opener = self.login_opener)
|
data = self.getHTMLData(url, opener = self.login_opener)
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
@@ -61,7 +58,6 @@ class SceneHD(TorrentProvider):
|
|||||||
'seeders': tryInt(all_cells[10].find('a').string),
|
'seeders': tryInt(all_cells[10].find('a').string),
|
||||||
'leechers': tryInt(leechers),
|
'leechers': tryInt(leechers),
|
||||||
'url': self.urls['download'] % torrent_id,
|
'url': self.urls['download'] % torrent_id,
|
||||||
'download': self.loginDownload,
|
|
||||||
'description': all_cells[1].find('a')['href'],
|
'description': all_cells[1].find('a')['href'],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -75,3 +71,9 @@ class SceneHD(TorrentProvider):
|
|||||||
'password': self.conf('password'),
|
'password': self.conf('password'),
|
||||||
'ssl': 'yes',
|
'ssl': 'yes',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def loginSuccess(self, output):
|
||||||
|
return 'logout.php' in output.lower()
|
||||||
|
|
||||||
|
loginCheckSuccess = loginSuccess
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
from .main import TorrentBytes
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return TorrentBytes()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'torrentbytes',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'subtab': 'providers',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'TorrentBytes',
|
||||||
|
'description': 'See <a href="http://torrentbytes.net">TorrentBytes</a>',
|
||||||
|
'wizard': True,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'username',
|
||||||
|
'default': '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'password',
|
||||||
|
'default': '',
|
||||||
|
'type': 'password',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 20,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}]
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||||
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TorrentBytes(TorrentProvider):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'test' : 'https://www.torrentbytes.net/',
|
||||||
|
'login' : 'https://www.torrentbytes.net/takelogin.php',
|
||||||
|
'login_check' : 'https://www.torrentbytes.net/inbox.php',
|
||||||
|
'detail' : 'https://www.torrentbytes.net/details.php?id=%s',
|
||||||
|
'search' : 'https://www.torrentbytes.net/browse.php?search=%s&cat=%d',
|
||||||
|
'download' : 'https://www.torrentbytes.net/download.php?id=%s&name=%s',
|
||||||
|
}
|
||||||
|
|
||||||
|
cat_ids = [
|
||||||
|
([5], ['720p', '1080p']),
|
||||||
|
([19], ['cam']),
|
||||||
|
([19], ['ts', 'tc']),
|
||||||
|
([19], ['r5', 'scr']),
|
||||||
|
([19], ['dvdrip']),
|
||||||
|
([5], ['brrip']),
|
||||||
|
([20], ['dvdr']),
|
||||||
|
]
|
||||||
|
|
||||||
|
http_time_between_calls = 1 #seconds
|
||||||
|
cat_backup_id = None
|
||||||
|
|
||||||
|
def _searchOnTitle(self, title, movie, quality, results):
|
||||||
|
|
||||||
|
url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])), self.getCatId(quality['identifier'])[0])
|
||||||
|
data = self.getHTMLData(url, opener = self.login_opener)
|
||||||
|
|
||||||
|
if data:
|
||||||
|
html = BeautifulSoup(data)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result_table = html.find('table', attrs = {'border' : '1'})
|
||||||
|
if not result_table:
|
||||||
|
return
|
||||||
|
|
||||||
|
entries = result_table.find_all('tr')
|
||||||
|
|
||||||
|
for result in entries[1:]:
|
||||||
|
cells = result.find_all('td')
|
||||||
|
|
||||||
|
link = cells[1].find('a', attrs = {'class' : 'index'})
|
||||||
|
|
||||||
|
full_id = link['href'].replace('details.php?id=', '')
|
||||||
|
torrent_id = full_id[:6]
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'id': torrent_id,
|
||||||
|
'name': link.contents[0],
|
||||||
|
'url': self.urls['download'] % (torrent_id, link.contents[0]),
|
||||||
|
'detail_url': self.urls['detail'] % torrent_id,
|
||||||
|
'size': self.parseSize(cells[6].contents[0] + cells[6].contents[2]),
|
||||||
|
'seeders': tryInt(cells[8].find('span').contents[0]),
|
||||||
|
'leechers': tryInt(cells[9].find('span').contents[0]),
|
||||||
|
})
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
|
def getLoginParams(self):
|
||||||
|
return tryUrlencode({
|
||||||
|
'username': self.conf('username'),
|
||||||
|
'password': self.conf('password'),
|
||||||
|
'login': 'submit',
|
||||||
|
})
|
||||||
|
|
||||||
|
def loginSuccess(self, output):
|
||||||
|
return 'logout.php' in output.lower() or 'Welcome' in output.lower()
|
||||||
|
|
||||||
|
loginCheckSuccess = loginSuccess
|
||||||
|
|
||||||
@@ -10,7 +10,8 @@ class TorrentDay(TorrentProvider):
|
|||||||
|
|
||||||
urls = {
|
urls = {
|
||||||
'test': 'http://www.td.af/',
|
'test': 'http://www.td.af/',
|
||||||
'login' : 'http://www.td.af/torrents/',
|
'login': 'http://www.td.af/torrents/',
|
||||||
|
'login_check': 'http://www.torrentday.com/userdetails.php',
|
||||||
'detail': 'http://www.td.af/details.php?id=%s',
|
'detail': 'http://www.td.af/details.php?id=%s',
|
||||||
'search': 'http://www.td.af/V3/API/API.php',
|
'search': 'http://www.td.af/V3/API/API.php',
|
||||||
'download': 'http://www.td.af/download.php/%s/%s',
|
'download': 'http://www.td.af/download.php/%s/%s',
|
||||||
@@ -50,7 +51,6 @@ class TorrentDay(TorrentProvider):
|
|||||||
'size': self.parseSize(torrent.get('size')),
|
'size': self.parseSize(torrent.get('size')),
|
||||||
'seeders': tryInt(torrent.get('seed')),
|
'seeders': tryInt(torrent.get('seed')),
|
||||||
'leechers': tryInt(torrent.get('leech')),
|
'leechers': tryInt(torrent.get('leech')),
|
||||||
'download': self.loginDownload,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
def getLoginParams(self):
|
def getLoginParams(self):
|
||||||
@@ -62,3 +62,6 @@ class TorrentDay(TorrentProvider):
|
|||||||
|
|
||||||
def loginSuccess(self, output):
|
def loginSuccess(self, output):
|
||||||
return 'Password not correct' not in output
|
return 'Password not correct' not in output
|
||||||
|
|
||||||
|
def loginCheckSuccess(self, output):
|
||||||
|
return 'logout.php' in output.lower()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class TorrentLeech(TorrentProvider):
|
|||||||
urls = {
|
urls = {
|
||||||
'test' : 'http://www.torrentleech.org/',
|
'test' : 'http://www.torrentleech.org/',
|
||||||
'login' : 'http://www.torrentleech.org/user/account/login/',
|
'login' : 'http://www.torrentleech.org/user/account/login/',
|
||||||
|
'login_check': 'http://torrentleech.org/user/messages',
|
||||||
'detail' : 'http://www.torrentleech.org/torrent/%s',
|
'detail' : 'http://www.torrentleech.org/torrent/%s',
|
||||||
'search' : 'http://www.torrentleech.org/torrents/browse/index/query/%s/categories/%d',
|
'search' : 'http://www.torrentleech.org/torrents/browse/index/query/%s/categories/%d',
|
||||||
'download' : 'http://www.torrentleech.org%s',
|
'download' : 'http://www.torrentleech.org%s',
|
||||||
@@ -58,7 +59,6 @@ class TorrentLeech(TorrentProvider):
|
|||||||
'name': link.string,
|
'name': link.string,
|
||||||
'url': self.urls['download'] % url['href'],
|
'url': self.urls['download'] % url['href'],
|
||||||
'detail_url': self.urls['download'] % details['href'],
|
'detail_url': self.urls['download'] % details['href'],
|
||||||
'download': self.loginDownload,
|
|
||||||
'size': self.parseSize(result.find_all('td')[4].string),
|
'size': self.parseSize(result.find_all('td')[4].string),
|
||||||
'seeders': tryInt(result.find('td', attrs = {'class' : 'seeders'}).string),
|
'seeders': tryInt(result.find('td', attrs = {'class' : 'seeders'}).string),
|
||||||
'leechers': tryInt(result.find('td', attrs = {'class' : 'leechers'}).string),
|
'leechers': tryInt(result.find('td', attrs = {'class' : 'leechers'}).string),
|
||||||
@@ -77,3 +77,5 @@ class TorrentLeech(TorrentProvider):
|
|||||||
|
|
||||||
def loginSuccess(self, output):
|
def loginSuccess(self, output):
|
||||||
return '/user/account/logout' in output.lower() or 'welcome back' in output.lower()
|
return '/user/account/logout' in output.lower() or 'welcome back' in output.lower()
|
||||||
|
|
||||||
|
loginCheckSuccess = loginSuccess
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class TorrentShack(TorrentProvider):
|
|||||||
urls = {
|
urls = {
|
||||||
'test' : 'http://www.torrentshack.net/',
|
'test' : 'http://www.torrentshack.net/',
|
||||||
'login' : 'http://www.torrentshack.net/login.php',
|
'login' : 'http://www.torrentshack.net/login.php',
|
||||||
|
'login_check': 'http://www.torrentshack.net/inbox.php',
|
||||||
'detail' : 'http://www.torrentshack.net/torrent/%s',
|
'detail' : 'http://www.torrentshack.net/torrent/%s',
|
||||||
'search' : 'http://www.torrentshack.net/torrents.php?searchstr=%s&filter_cat[%d]=1',
|
'search' : 'http://www.torrentshack.net/torrents.php?searchstr=%s&filter_cat[%d]=1',
|
||||||
'download' : 'http://www.torrentshack.net/%s',
|
'download' : 'http://www.torrentshack.net/%s',
|
||||||
@@ -58,7 +59,6 @@ class TorrentShack(TorrentProvider):
|
|||||||
'name': unicode(link.span.string).translate({ord(u'\xad'): None}),
|
'name': unicode(link.span.string).translate({ord(u'\xad'): None}),
|
||||||
'url': self.urls['download'] % url['href'],
|
'url': self.urls['download'] % url['href'],
|
||||||
'detail_url': self.urls['download'] % link['href'],
|
'detail_url': self.urls['download'] % link['href'],
|
||||||
'download': self.loginDownload,
|
|
||||||
'size': self.parseSize(result.find_all('td')[4].string),
|
'size': self.parseSize(result.find_all('td')[4].string),
|
||||||
'seeders': tryInt(result.find_all('td')[6].string),
|
'seeders': tryInt(result.find_all('td')[6].string),
|
||||||
'leechers': tryInt(result.find_all('td')[7].string),
|
'leechers': tryInt(result.find_all('td')[7].string),
|
||||||
@@ -79,3 +79,5 @@ class TorrentShack(TorrentProvider):
|
|||||||
|
|
||||||
def loginSuccess(self, output):
|
def loginSuccess(self, output):
|
||||||
return 'logout.php' in output.lower()
|
return 'logout.php' in output.lower()
|
||||||
|
|
||||||
|
loginCheckSuccess = loginSuccess
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
from main import Yify
|
||||||
|
|
||||||
|
def start():
|
||||||
|
return Yify()
|
||||||
|
|
||||||
|
config = [{
|
||||||
|
'name': 'yify',
|
||||||
|
'groups': [
|
||||||
|
{
|
||||||
|
'tab': 'searcher',
|
||||||
|
'subtab': 'providers',
|
||||||
|
'list': 'torrent_providers',
|
||||||
|
'name': 'Yify',
|
||||||
|
'description': 'Free provider, less accurate. Small HD movies, encoded by <a href="https://yify-torrents.com/">Yify</a>.',
|
||||||
|
'wizard': False,
|
||||||
|
'options': [
|
||||||
|
{
|
||||||
|
'name': 'enabled',
|
||||||
|
'type': 'enabler',
|
||||||
|
'default': 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'extra_score',
|
||||||
|
'advanced': True,
|
||||||
|
'label': 'Extra Score',
|
||||||
|
'type': 'int',
|
||||||
|
'default': 0,
|
||||||
|
'description': 'Starting score for each release found via this provider.',
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}]
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
from couchpotato.core.helpers.variable import tryInt
|
||||||
|
from couchpotato.core.logger import CPLog
|
||||||
|
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
log = CPLog(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Yify(TorrentProvider):
|
||||||
|
|
||||||
|
urls = {
|
||||||
|
'test' : 'https://yify-torrents.com/api',
|
||||||
|
'search' : 'https://yify-torrents.com/api/list.json?keywords=%s&quality=%s',
|
||||||
|
'detail': 'https://yify-torrents.com/api/movie.json?id=%s'
|
||||||
|
}
|
||||||
|
|
||||||
|
http_time_between_calls = 1 #seconds
|
||||||
|
|
||||||
|
def search(self, movie, quality):
|
||||||
|
|
||||||
|
if not quality.get('hd', False):
|
||||||
|
return []
|
||||||
|
|
||||||
|
return super(Yify, self).search(movie, quality)
|
||||||
|
|
||||||
|
def _searchOnTitle(self, title, movie, quality, results):
|
||||||
|
|
||||||
|
data = self.getJsonData(self.urls['search'] % (title, quality['identifier']))
|
||||||
|
|
||||||
|
if data and data.get('MovieList'):
|
||||||
|
try:
|
||||||
|
for result in data.get('MovieList'):
|
||||||
|
|
||||||
|
try:
|
||||||
|
title = result['TorrentUrl'].split('/')[-1][:-8].replace('_', '.').strip('._')
|
||||||
|
title = title.replace('.-.', '-')
|
||||||
|
title = title.replace('..', '.')
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'id': result['MovieID'],
|
||||||
|
'name': title,
|
||||||
|
'url': result['TorrentUrl'],
|
||||||
|
'detail_url': self.urls['detail'] % result['MovieID'],
|
||||||
|
'size': self.parseSize(result['Size']),
|
||||||
|
'seeders': tryInt(result['TorrentSeeds']),
|
||||||
|
'leechers': tryInt(result['TorrentPeers'])
|
||||||
|
})
|
||||||
|
|
||||||
|
except:
|
||||||
|
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
|
||||||
@@ -2,7 +2,6 @@ from __future__ import with_statement
|
|||||||
from couchpotato.api import addApiView
|
from couchpotato.api import addApiView
|
||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
from couchpotato.core.helpers.encoding import isInt, toUnicode
|
from couchpotato.core.helpers.encoding import isInt, toUnicode
|
||||||
from couchpotato.core.helpers.request import getParams, jsonified
|
|
||||||
from couchpotato.core.helpers.variable import mergeDicts, tryInt
|
from couchpotato.core.helpers.variable import mergeDicts, tryInt
|
||||||
from couchpotato.core.settings.model import Properties
|
from couchpotato.core.settings.model import Properties
|
||||||
import ConfigParser
|
import ConfigParser
|
||||||
@@ -169,19 +168,17 @@ class Settings(object):
|
|||||||
return self.options
|
return self.options
|
||||||
|
|
||||||
|
|
||||||
def view(self):
|
def view(self, **kwargs):
|
||||||
return jsonified({
|
return {
|
||||||
'options': self.getOptions(),
|
'options': self.getOptions(),
|
||||||
'values': self.getValues()
|
'values': self.getValues()
|
||||||
})
|
}
|
||||||
|
|
||||||
def saveView(self):
|
def saveView(self, **kwargs):
|
||||||
|
|
||||||
params = getParams()
|
section = kwargs.get('section')
|
||||||
|
option = kwargs.get('name')
|
||||||
section = params.get('section')
|
value = kwargs.get('value')
|
||||||
option = params.get('name')
|
|
||||||
value = params.get('value')
|
|
||||||
|
|
||||||
# See if a value handler is attached, use that as value
|
# See if a value handler is attached, use that as value
|
||||||
new_value = fireEvent('setting.save.%s.%s' % (section, option), value, single = True)
|
new_value = fireEvent('setting.save.%s.%s' % (section, option), value, single = True)
|
||||||
@@ -192,9 +189,9 @@ class Settings(object):
|
|||||||
# After save (for re-interval etc)
|
# After save (for re-interval etc)
|
||||||
fireEvent('setting.save.%s.%s.after' % (section, option), single = True)
|
fireEvent('setting.save.%s.%s.after' % (section, option), single = True)
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
})
|
}
|
||||||
|
|
||||||
def getProperty(self, identifier):
|
def getProperty(self, identifier):
|
||||||
from couchpotato import get_session
|
from couchpotato import get_session
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from elixir.entity import Entity
|
|||||||
from elixir.fields import Field
|
from elixir.fields import Field
|
||||||
from elixir.options import options_defaults, using_options
|
from elixir.options import options_defaults, using_options
|
||||||
from elixir.relationships import ManyToMany, OneToMany, ManyToOne
|
from elixir.relationships import ManyToMany, OneToMany, ManyToOne
|
||||||
|
from sqlalchemy.ext.mutable import Mutable
|
||||||
from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, String, \
|
from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, String, \
|
||||||
TypeDecorator
|
TypeDecorator
|
||||||
import json
|
import json
|
||||||
@@ -39,6 +40,37 @@ class JsonType(TypeDecorator):
|
|||||||
def process_result_value(self, value, dialect):
|
def process_result_value(self, value, dialect):
|
||||||
return json.loads(value if value else '{}')
|
return json.loads(value if value else '{}')
|
||||||
|
|
||||||
|
class MutableDict(Mutable, dict):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def coerce(cls, key, value):
|
||||||
|
if not isinstance(value, MutableDict):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return MutableDict(value)
|
||||||
|
return Mutable.coerce(key, value)
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
def __delitem(self, key):
|
||||||
|
dict.__delitem__(self, key)
|
||||||
|
self.changed()
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
dict.__setitem__(self, key, value)
|
||||||
|
self.changed()
|
||||||
|
|
||||||
|
def __getstate__(self):
|
||||||
|
return dict(self)
|
||||||
|
|
||||||
|
def __setstate__(self, state):
|
||||||
|
self.update(self)
|
||||||
|
|
||||||
|
def update(self, *args, **kwargs):
|
||||||
|
super(MutableDict, self).update(*args, **kwargs)
|
||||||
|
self.changed()
|
||||||
|
|
||||||
|
MutableDict.associate_with(JsonType)
|
||||||
|
|
||||||
|
|
||||||
class Movie(Entity):
|
class Movie(Entity):
|
||||||
"""Movie Resource a movie could have multiple releases
|
"""Movie Resource a movie could have multiple releases
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class Env(object):
|
|||||||
_appname = 'CouchPotato'
|
_appname = 'CouchPotato'
|
||||||
|
|
||||||
''' Environment variables '''
|
''' Environment variables '''
|
||||||
|
_app = None
|
||||||
_encoding = 'UTF-8'
|
_encoding = 'UTF-8'
|
||||||
_debug = False
|
_debug = False
|
||||||
_dev = False
|
_dev = False
|
||||||
|
|||||||
+75
-54
@@ -1,19 +1,20 @@
|
|||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from couchpotato import web
|
from cache import FileSystemCache
|
||||||
from couchpotato.api import api, NonBlockHandler
|
from couchpotato import KeyHandler
|
||||||
|
from couchpotato.api import NonBlockHandler, ApiHandler
|
||||||
from couchpotato.core.event import fireEventAsync, fireEvent
|
from couchpotato.core.event import fireEventAsync, fireEvent
|
||||||
|
from couchpotato.core.helpers.encoding import toUnicode
|
||||||
from couchpotato.core.helpers.variable import getDataDir, tryInt
|
from couchpotato.core.helpers.variable import getDataDir, tryInt
|
||||||
from logging import handlers
|
from logging import handlers
|
||||||
from tornado.httpserver import HTTPServer
|
from tornado.httpserver import HTTPServer
|
||||||
from tornado.web import Application, FallbackHandler
|
from tornado.web import Application, StaticFileHandler, RedirectHandler
|
||||||
from tornado.wsgi import WSGIContainer
|
|
||||||
from werkzeug.contrib.cache import FileSystemCache
|
|
||||||
import locale
|
import locale
|
||||||
import logging
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
def getOptions(base_path, args):
|
def getOptions(base_path, args):
|
||||||
@@ -75,23 +76,25 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
|||||||
if not encoding or encoding in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
|
if not encoding or encoding in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
|
||||||
encoding = 'UTF-8'
|
encoding = 'UTF-8'
|
||||||
|
|
||||||
|
Env.set('encoding', encoding)
|
||||||
|
|
||||||
# Do db stuff
|
# Do db stuff
|
||||||
db_path = os.path.join(data_dir, 'couchpotato.db')
|
db_path = toUnicode(os.path.join(data_dir, 'couchpotato.db'))
|
||||||
|
|
||||||
# Backup before start and cleanup old databases
|
# Backup before start and cleanup old databases
|
||||||
new_backup = os.path.join(data_dir, 'db_backup', str(int(time.time())))
|
new_backup = toUnicode(os.path.join(data_dir, 'db_backup', str(int(time.time()))))
|
||||||
|
|
||||||
# Create path and copy
|
# Create path and copy
|
||||||
if not os.path.isdir(new_backup): os.makedirs(new_backup)
|
if not os.path.isdir(new_backup): os.makedirs(new_backup)
|
||||||
src_files = [options.config_file, db_path, db_path + '-shm', db_path + '-wal']
|
src_files = [options.config_file, db_path, db_path + '-shm', db_path + '-wal']
|
||||||
for src_file in src_files:
|
for src_file in src_files:
|
||||||
if os.path.isfile(src_file):
|
if os.path.isfile(src_file):
|
||||||
shutil.copy2(src_file, os.path.join(new_backup, os.path.basename(src_file)))
|
shutil.copy2(src_file, toUnicode(os.path.join(new_backup, os.path.basename(src_file))))
|
||||||
|
|
||||||
# Remove older backups, keep backups 3 days or at least 3
|
# Remove older backups, keep backups 3 days or at least 3
|
||||||
backups = []
|
backups = []
|
||||||
for directory in os.listdir(os.path.dirname(new_backup)):
|
for directory in os.listdir(os.path.dirname(new_backup)):
|
||||||
backup = os.path.join(os.path.dirname(new_backup), directory)
|
backup = toUnicode(os.path.join(os.path.dirname(new_backup), directory))
|
||||||
if os.path.isdir(backup):
|
if os.path.isdir(backup):
|
||||||
backups.append(backup)
|
backups.append(backup)
|
||||||
|
|
||||||
@@ -100,7 +103,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
|||||||
if total_backups > 3:
|
if total_backups > 3:
|
||||||
if tryInt(os.path.basename(backup)) < time.time() - 259200:
|
if tryInt(os.path.basename(backup)) < time.time() - 259200:
|
||||||
for src_file in src_files:
|
for src_file in src_files:
|
||||||
b_file = os.path.join(backup, os.path.basename(src_file))
|
b_file = toUnicode(os.path.join(backup, os.path.basename(src_file)))
|
||||||
if os.path.isfile(b_file):
|
if os.path.isfile(b_file):
|
||||||
os.remove(b_file)
|
os.remove(b_file)
|
||||||
os.rmdir(backup)
|
os.rmdir(backup)
|
||||||
@@ -108,13 +111,12 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
|||||||
|
|
||||||
|
|
||||||
# Register environment settings
|
# Register environment settings
|
||||||
Env.set('encoding', encoding)
|
Env.set('app_dir', toUnicode(base_path))
|
||||||
Env.set('app_dir', base_path)
|
Env.set('data_dir', toUnicode(data_dir))
|
||||||
Env.set('data_dir', data_dir)
|
Env.set('log_path', toUnicode(os.path.join(log_dir, 'CouchPotato.log')))
|
||||||
Env.set('log_path', os.path.join(log_dir, 'CouchPotato.log'))
|
Env.set('db_path', toUnicode('sqlite:///' + db_path))
|
||||||
Env.set('db_path', 'sqlite:///' + db_path)
|
Env.set('cache_dir', toUnicode(os.path.join(data_dir, 'cache')))
|
||||||
Env.set('cache_dir', os.path.join(data_dir, 'cache'))
|
Env.set('cache', FileSystemCache(toUnicode(os.path.join(Env.get('cache_dir'), 'python'))))
|
||||||
Env.set('cache', FileSystemCache(os.path.join(Env.get('cache_dir'), 'python')))
|
|
||||||
Env.set('console_log', options.console_log)
|
Env.set('console_log', options.console_log)
|
||||||
Env.set('quiet', options.quiet)
|
Env.set('quiet', options.quiet)
|
||||||
Env.set('desktop', desktop)
|
Env.set('desktop', desktop)
|
||||||
@@ -170,12 +172,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
|||||||
|
|
||||||
# Check if database exists
|
# Check if database exists
|
||||||
db = Env.get('db_path')
|
db = Env.get('db_path')
|
||||||
db_exists = os.path.isfile(db_path)
|
db_exists = os.path.isfile(toUnicode(db_path))
|
||||||
|
|
||||||
# Load configs & plugins
|
|
||||||
loader = Env.get('loader')
|
|
||||||
loader.preload(root = base_path)
|
|
||||||
loader.run()
|
|
||||||
|
|
||||||
# Load migrations
|
# Load migrations
|
||||||
if db_exists:
|
if db_exists:
|
||||||
@@ -201,17 +198,16 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
|||||||
from couchpotato.core.settings.model import setup
|
from couchpotato.core.settings.model import setup
|
||||||
setup()
|
setup()
|
||||||
|
|
||||||
# Fill database with needed stuff
|
|
||||||
if not db_exists:
|
|
||||||
fireEvent('app.initialize', in_order = True)
|
|
||||||
|
|
||||||
# Create app
|
# Create app
|
||||||
from couchpotato import app
|
from couchpotato import WebHandler
|
||||||
|
web_base = ('/' + Env.setting('url_base').lstrip('/') + '/') if Env.setting('url_base') else '/'
|
||||||
|
Env.set('web_base', web_base)
|
||||||
|
|
||||||
api_key = Env.setting('api_key')
|
api_key = Env.setting('api_key')
|
||||||
url_base = '/' + Env.setting('url_base').lstrip('/') if Env.setting('url_base') else ''
|
api_base = r'%sapi/%s/' % (web_base, api_key)
|
||||||
|
Env.set('api_base', api_base)
|
||||||
|
|
||||||
# Basic config
|
# Basic config
|
||||||
app.secret_key = api_key
|
|
||||||
host = Env.setting('host', default = '0.0.0.0')
|
host = Env.setting('host', default = '0.0.0.0')
|
||||||
# app.debug = development
|
# app.debug = development
|
||||||
config = {
|
config = {
|
||||||
@@ -222,36 +218,60 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
|||||||
'ssl_key': Env.setting('ssl_key', default = None),
|
'ssl_key': Env.setting('ssl_key', default = None),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Static path
|
|
||||||
app.static_folder = os.path.join(base_path, 'couchpotato', 'static')
|
|
||||||
web.add_url_rule('api/%s/static/<path:filename>' % api_key,
|
|
||||||
endpoint = 'static',
|
|
||||||
view_func = app.send_static_file)
|
|
||||||
|
|
||||||
# Register modules
|
# Load the app
|
||||||
app.register_blueprint(web, url_prefix = '%s/' % url_base)
|
application = Application([],
|
||||||
app.register_blueprint(api, url_prefix = '%s/api/%s/' % (url_base, api_key))
|
log_function = lambda x : None,
|
||||||
|
debug = config['use_reloader'],
|
||||||
|
gzip = True,
|
||||||
|
)
|
||||||
|
Env.set('app', application)
|
||||||
|
|
||||||
|
|
||||||
|
# Request handlers
|
||||||
|
application.add_handlers(".*$", [
|
||||||
|
(r'%snonblock/(.*)(/?)' % api_base, NonBlockHandler),
|
||||||
|
|
||||||
|
# API handlers
|
||||||
|
(r'%s(.*)(/?)' % api_base, ApiHandler), # Main API handler
|
||||||
|
(r'%sgetkey(/?)' % web_base, KeyHandler), # Get API key
|
||||||
|
(r'%s' % api_base, RedirectHandler, {"url": web_base + 'docs/'}), # API docs
|
||||||
|
|
||||||
|
# Catch all webhandlers
|
||||||
|
(r'%s(.*)(/?)' % web_base, WebHandler),
|
||||||
|
(r'(.*)', WebHandler),
|
||||||
|
])
|
||||||
|
|
||||||
|
# Static paths
|
||||||
|
static_path = '%sstatic/' % api_base
|
||||||
|
for dir_name in ['fonts', 'images', 'scripts', 'style']:
|
||||||
|
application.add_handlers(".*$", [
|
||||||
|
('%s%s/(.*)' % (static_path, dir_name), StaticFileHandler, {'path': toUnicode(os.path.join(base_path, 'couchpotato', 'static', dir_name))})
|
||||||
|
])
|
||||||
|
Env.set('static_path', static_path);
|
||||||
|
|
||||||
|
|
||||||
|
# Load configs & plugins
|
||||||
|
loader = Env.get('loader')
|
||||||
|
loader.preload(root = toUnicode(base_path))
|
||||||
|
loader.run()
|
||||||
|
|
||||||
|
|
||||||
|
# Fill database with needed stuff
|
||||||
|
if not db_exists:
|
||||||
|
fireEvent('app.initialize', in_order = True)
|
||||||
|
|
||||||
|
|
||||||
|
# Go go go!
|
||||||
|
from tornado.ioloop import IOLoop
|
||||||
|
loop = IOLoop.current()
|
||||||
|
|
||||||
|
|
||||||
# Some logging and fire load event
|
# Some logging and fire load event
|
||||||
try: log.info('Starting server on port %(port)s', config)
|
try: log.info('Starting server on port %(port)s', config)
|
||||||
except: pass
|
except: pass
|
||||||
fireEventAsync('app.load')
|
fireEventAsync('app.load')
|
||||||
|
|
||||||
# Go go go!
|
|
||||||
from tornado.ioloop import IOLoop
|
|
||||||
web_container = WSGIContainer(app)
|
|
||||||
web_container._log = _log
|
|
||||||
loop = IOLoop.current()
|
|
||||||
|
|
||||||
|
|
||||||
application = Application([
|
|
||||||
(r'%s/api/%s/nonblock/(.*)/' % (url_base, api_key), NonBlockHandler),
|
|
||||||
(r'.*', FallbackHandler, dict(fallback = web_container)),
|
|
||||||
],
|
|
||||||
log_function = lambda x : None,
|
|
||||||
debug = config['use_reloader'],
|
|
||||||
gzip = True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if config['ssl_cert'] and config['ssl_key']:
|
if config['ssl_cert'] and config['ssl_key']:
|
||||||
server = HTTPServer(application, no_keep_alive = True, ssl_options = {
|
server = HTTPServer(application, no_keep_alive = True, ssl_options = {
|
||||||
@@ -269,10 +289,11 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
|||||||
server.listen(config['port'], config['host'])
|
server.listen(config['port'], config['host'])
|
||||||
loop.start()
|
loop.start()
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
|
log.error('Failed starting: %s', traceback.format_exc())
|
||||||
try:
|
try:
|
||||||
nr, msg = e
|
nr, msg = e
|
||||||
if nr == 48:
|
if nr == 48:
|
||||||
log.info('Already in use, try %s more time after few seconds', restart_tries)
|
log.info('Port (%s) needed for CouchPotato is already in use, try %s more time after few seconds', (config.get('port'), restart_tries))
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
restart_tries -= 1
|
restart_tries -= 1
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 29 KiB |
@@ -101,6 +101,9 @@ Page.Home = new Class({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Suggest
|
||||||
|
self.suggestion_list = new SuggestList();
|
||||||
|
|
||||||
// Still not available
|
// Still not available
|
||||||
self.late_list = new MovieList({
|
self.late_list = new MovieList({
|
||||||
'navigation': false,
|
'navigation': false,
|
||||||
@@ -121,25 +124,10 @@ Page.Home = new Class({
|
|||||||
self.el.adopt(
|
self.el.adopt(
|
||||||
$(self.available_list),
|
$(self.available_list),
|
||||||
$(self.soon_list),
|
$(self.soon_list),
|
||||||
|
$(self.suggestion_list),
|
||||||
$(self.late_list)
|
$(self.late_list)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Suggest
|
|
||||||
// self.suggestion_list = new MovieList({
|
|
||||||
// 'navigation': false,
|
|
||||||
// 'identifier': 'suggestions',
|
|
||||||
// 'limit': 6,
|
|
||||||
// 'load_more': false,
|
|
||||||
// 'view': 'thumbs',
|
|
||||||
// 'api_call': 'suggestion.suggest'
|
|
||||||
// });
|
|
||||||
// self.el.adopt(
|
|
||||||
// new Element('h2', {
|
|
||||||
// 'text': 'You might like'
|
|
||||||
// }),
|
|
||||||
// $(self.suggestion_list)
|
|
||||||
// );
|
|
||||||
|
|
||||||
// Recent
|
// Recent
|
||||||
// Snatched
|
// Snatched
|
||||||
// Renamed
|
// Renamed
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ body > .spinner, .mask{
|
|||||||
color: #FFF;
|
color: #FFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon2.add:before { content: "\e05a"; color: #c2fac5; }
|
||||||
.icon2.cog:before { content: "\e109"; }
|
.icon2.cog:before { content: "\e109"; }
|
||||||
.icon2.eye-open:before { content: "\e09d"; }
|
.icon2.eye-open:before { content: "\e09d"; }
|
||||||
.icon2.search:before { content: "\e03e"; }
|
.icon2.search:before { content: "\e03e"; }
|
||||||
|
|||||||
@@ -1,27 +1,28 @@
|
|||||||
|
{% autoescape None %}
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/api.css') }}" type="text/css">
|
<link rel="stylesheet" href="{{ Env.get('static_path') }}style/api.css" type="text/css">
|
||||||
<title>API documentation</title>
|
<title>API documentation</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<h1>CouchPotato API Documentation</h1>
|
<h1>CouchPotato API Documentation</h1>
|
||||||
<div class="api">
|
<div class="api">
|
||||||
You can access the API via <pre>{{ fireEvent('app.api_url', single = True)|safe }}/</pre>
|
You can access the API via <pre>{{ Env.get('api_base') }}</pre>
|
||||||
To see it in action, have a look at the webinterface with Firebug (on firefox) or the development tools included in Chrome.
|
To see it in action, have a look at the webinterface with Firebug (on firefox) or the development tools included in Chrome.
|
||||||
All the data that you see there are from the API.
|
All the data that you see there are from the API.
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
A normal API call:
|
A normal API call:
|
||||||
<pre><a href="{{ fireEvent('app.api_url', single = True)|safe }}/updater.info/">{{ fireEvent('app.api_url', single = True)|safe }}/updater.info/</a></pre>
|
<pre><a href="{{ Env.get('api_base') }}updater.info/">{{ Env.get('api_base') }}updater.info/</a></pre>
|
||||||
<br />
|
<br />
|
||||||
You can also use the API over another domain using JSONP, the callback function should be in 'callback_func'
|
You can also use the API over another domain using JSONP, the callback function should be in 'callback_func'
|
||||||
<pre><a href="{{ fireEvent('app.api_url', single = True)|safe }}/updater.info/?callback_func=myfunction">{{ fireEvent('app.api_url', single = True)|safe }}/updater.info/?callback_func=myfunction</a></pre>
|
<pre><a href="{{ Env.get('api_base') }}updater.info/?callback_func=myfunction">{{ Env.get('api_base') }}updater.info/?callback_func=myfunction</a></pre>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
Get the API key:
|
Get the API key:
|
||||||
<pre><a href="{{ url_for('web.index') }}getkey/?p=md5(password)&u=md5(username)">{{ url_for('web.index') }}getkey/?p=md5(password)&u=md5(username)</a></pre>
|
<pre><a href="{{ Env.get('web_base') }}getkey/?p=md5(password)&u=md5(username)">{{ Env.get('web_base') }}getkey/?p=md5(password)&u=md5(username)</a></pre>
|
||||||
Will return {"api_key": "XXXXXXXXXX", "success": true}. When username or password is empty you don't need to md5 it.
|
Will return {"api_key": "XXXXXXXXXX", "success": true}. When username or password is empty you don't need to md5 it.
|
||||||
<br />
|
<br />
|
||||||
</div>
|
</div>
|
||||||
@@ -41,9 +42,9 @@
|
|||||||
<td class="type">{{ api_docs[route]['params'][param].get('type', 'string') }}</td>
|
<td class="type">{{ api_docs[route]['params'][param].get('type', 'string') }}</td>
|
||||||
<td class="description">{{ api_docs[route]['params'][param]['desc'] }}</td>
|
<td class="description">{{ api_docs[route]['params'][param]['desc'] }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% end %}
|
||||||
</table>
|
</table>
|
||||||
{% endif %}
|
{% end %}
|
||||||
|
|
||||||
{% if api_docs[route].get('return') %}
|
{% if api_docs[route].get('return') %}
|
||||||
<h3>Return</h3>
|
<h3>Return</h3>
|
||||||
@@ -52,14 +53,14 @@
|
|||||||
{% if api_docs[route]['return'].get('example') %}
|
{% if api_docs[route]['return'].get('example') %}
|
||||||
<div class="example">
|
<div class="example">
|
||||||
<h4>Example</h4>
|
<h4>Example</h4>
|
||||||
<pre>{{ api_docs[route]['return'].get('example', '')|safe }}</pre>
|
<pre>{{ api_docs[route]['return'].get('example', '') }}</pre>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% end %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% end %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% end %}
|
||||||
{% endfor %}
|
{% end %}
|
||||||
|
|
||||||
<div class="missing">
|
<div class="missing">
|
||||||
<h1>Missing documentation</h1>
|
<h1>Missing documentation</h1>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
{% autoescape None %}
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -5,17 +6,17 @@
|
|||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
|
||||||
{% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'front', single = True) %}
|
{% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'front', single = True) %}
|
||||||
<link rel="stylesheet" href="{{ url_for('web.index') }}{{ url }}" type="text/css">{% endfor %}
|
<link rel="stylesheet" href="{{ Env.get('web_base') }}{{ url }}" type="text/css">{% end %}
|
||||||
{% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'front', single = True) %}
|
{% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'front', single = True) %}
|
||||||
<script type="text/javascript" src="{{ url_for('web.index') }}{{ url }}"></script>{% endfor %}
|
<script type="text/javascript" src="{{ Env.get('web_base') }}{{ url }}"></script>{% end %}
|
||||||
|
|
||||||
{% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'head', single = True) %}
|
{% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'head', single = True) %}
|
||||||
<script type="text/javascript" src="{{ url_for('web.index') }}{{ url }}"></script>{% endfor %}
|
<script type="text/javascript" src="{{ Env.get('web_base') }}{{ url }}"></script>{% end %}
|
||||||
{% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'head', single = True) %}
|
{% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'head', single = True) %}
|
||||||
<link rel="stylesheet" href="{{ url_for('web.index') }}{{ url }}" type="text/css">{% endfor %}
|
<link rel="stylesheet" href="{{ Env.get('web_base') }}{{ url }}" type="text/css">{% end %}
|
||||||
|
|
||||||
<link href="{{ url_for('web.static', filename='images/favicon.ico') }}" rel="icon" type="image/x-icon" />
|
<link href="{{ Env.get('static_path') }}images/favicon.ico" rel="icon" type="image/x-icon" />
|
||||||
<link rel="apple-touch-icon" href="{{ url_for('web.static', filename='images/homescreen.png') }}" />
|
<link rel="apple-touch-icon" href="{{ Env.get('static_path') }}images/homescreen.png" />
|
||||||
|
|
||||||
<script type="text/javascript" src="https://www.youtube.com/player_api" defer="defer"></script>
|
<script type="text/javascript" src="https://www.youtube.com/player_api" defer="defer"></script>
|
||||||
|
|
||||||
@@ -35,8 +36,8 @@
|
|||||||
new Uniform();
|
new Uniform();
|
||||||
|
|
||||||
Api.setup({
|
Api.setup({
|
||||||
'url': {{ url_for('api.index')|tojson|safe }},
|
'url': {{ json_encode(Env.get('api_base')) }},
|
||||||
'path_sep': {{ sep|tojson|safe }},
|
'path_sep': {{ json_encode(sep) }},
|
||||||
'is_remote': false
|
'is_remote': false
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,29 +62,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
Quality.setup({
|
Quality.setup({
|
||||||
'profiles': {{ fireEvent('profile.all', single = True)|tojson|safe }},
|
'profiles': {{ json_encode(fireEvent('profile.all', single = True)) }},
|
||||||
'qualities': {{ fireEvent('quality.all', single = True)|tojson|safe }}
|
'qualities': {{ json_encode(fireEvent('quality.all', single = True)) }}
|
||||||
});
|
});
|
||||||
|
|
||||||
Status.setup({{ fireEvent('status.all', single = True)|tojson|safe }});
|
Status.setup({{ json_encode(fireEvent('status.all', single = True)) }});
|
||||||
|
|
||||||
File.Type.setup({{ fireEvent('file.types', single = True)|tojson|safe }});
|
File.Type.setup({{ json_encode(fireEvent('file.types', single = True)) }});
|
||||||
|
|
||||||
App.setup({
|
App.setup({
|
||||||
'base_url': {{ url_for('web.index')|tojson|safe }},
|
'base_url': {{ json_encode(Env.get('web_base')) }},
|
||||||
'args': {{ env.get('args')|tojson|safe }},
|
'args': {{ json_encode(Env.get('args')) }},
|
||||||
'options': {{ ('%s' % env.get('options'))|tojson|safe }},
|
'options': {{ json_encode(('%s' % Env.get('options'))) }},
|
||||||
'app_dir': {{ env.get('app_dir')|tojson|safe }},
|
'app_dir': {{ json_encode(Env.get('app_dir')) }},
|
||||||
'data_dir': {{ env.get('data_dir')|tojson|safe }},
|
'data_dir': {{ json_encode(Env.get('data_dir')) }},
|
||||||
'pid': {{ env.getPid()|tojson|safe }},
|
'pid': {{ json_encode(Env.getPid()) }},
|
||||||
'userscript_version': {{ fireEvent('userscript.get_version', single = True)|tojson|safe }}
|
'userscript_version': {{ json_encode(fireEvent('userscript.get_version', single = True)) }}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
{% if env.setting('show_wizard') %}
|
{% if Env.setting('show_wizard') %}
|
||||||
if(!window.location.href.contains('wizard'))
|
if(!window.location.href.contains('wizard'))
|
||||||
window.location = '{{ url_for('web.index') }}wizard/'
|
window.location = '{{ Env.get('web_base') }}wizard/'
|
||||||
{% endif %}
|
{% end %}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<title>CouchPotato</title>
|
<title>CouchPotato</title>
|
||||||
|
|||||||
+7
-1
@@ -45,6 +45,8 @@ test -x $CP_DAEMON || exit 0
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
. /lib/lsb/init-functions
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
start)
|
start)
|
||||||
echo "Starting $DESC"
|
echo "Starting $DESC"
|
||||||
@@ -63,9 +65,13 @@ case "$1" in
|
|||||||
start-stop-daemon --stop --pidfile $CP_PID_FILE --retry 15
|
start-stop-daemon --stop --pidfile $CP_PID_FILE --retry 15
|
||||||
start-stop-daemon -d $CP_APP_PATH -c $CP_RUN_AS --start --background --pidfile $CP_PID_FILE --exec $CP_DAEMON -- $CP_DAEMON_OPTS
|
start-stop-daemon -d $CP_APP_PATH -c $CP_RUN_AS --start --background --pidfile $CP_PID_FILE --exec $CP_DAEMON -- $CP_DAEMON_OPTS
|
||||||
;;
|
;;
|
||||||
|
|
||||||
|
status)
|
||||||
|
status_of_proc -p $CP_PID_FILE "$CP_DAEMON" "$NAME"
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
N=/etc/init.d/$NAME
|
N=/etc/init.d/$NAME
|
||||||
echo "Usage: $N {start|stop|restart|force-reload}" >&2
|
echo "Usage: $N {start|stop|restart|force-reload|status}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
#define MyAppName "CouchPotato"
|
#define MyAppName "CouchPotato"
|
||||||
#define MyAppVer "2.0.8.1"
|
#define MyAppVer "2.1.0"
|
||||||
|
|
||||||
[Setup]
|
[Setup]
|
||||||
AppName={#MyAppName}
|
AppName={#MyAppName}
|
||||||
|
|||||||
Vendored
+262
@@ -0,0 +1,262 @@
|
|||||||
|
"""
|
||||||
|
copied from
|
||||||
|
werkzeug.contrib.cache
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||||
|
:license: BSD, see LICENSE for more details.
|
||||||
|
"""
|
||||||
|
from cache.posixemulation import rename
|
||||||
|
from itertools import izip
|
||||||
|
from time import time
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import tempfile
|
||||||
|
try:
|
||||||
|
from hashlib import md5
|
||||||
|
except ImportError:
|
||||||
|
from md5 import new as md5
|
||||||
|
|
||||||
|
try:
|
||||||
|
import cPickle as pickle
|
||||||
|
except ImportError:
|
||||||
|
import pickle
|
||||||
|
|
||||||
|
|
||||||
|
def _items(mappingorseq):
|
||||||
|
"""Wrapper for efficient iteration over mappings represented by dicts
|
||||||
|
or sequences::
|
||||||
|
|
||||||
|
>>> for k, v in _items((i, i*i) for i in xrange(5)):
|
||||||
|
... assert k*k == v
|
||||||
|
|
||||||
|
>>> for k, v in _items(dict((i, i*i) for i in xrange(5))):
|
||||||
|
... assert k*k == v
|
||||||
|
|
||||||
|
"""
|
||||||
|
return mappingorseq.iteritems() if hasattr(mappingorseq, 'iteritems') \
|
||||||
|
else mappingorseq
|
||||||
|
|
||||||
|
|
||||||
|
class BaseCache(object):
|
||||||
|
"""Baseclass for the cache systems. All the cache systems implement this
|
||||||
|
API or a superset of it.
|
||||||
|
|
||||||
|
:param default_timeout: the default timeout that is used if no timeout is
|
||||||
|
specified on :meth:`set`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, default_timeout = 300):
|
||||||
|
self.default_timeout = default_timeout
|
||||||
|
|
||||||
|
def delete(self, key):
|
||||||
|
"""Deletes `key` from the cache. If it does not exist in the cache
|
||||||
|
nothing happens.
|
||||||
|
|
||||||
|
:param key: the key to delete.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_many(self, *keys):
|
||||||
|
"""Returns a list of values for the given keys.
|
||||||
|
For each key a item in the list is created. Example::
|
||||||
|
|
||||||
|
foo, bar = cache.get_many("foo", "bar")
|
||||||
|
|
||||||
|
If a key can't be looked up `None` is returned for that key
|
||||||
|
instead.
|
||||||
|
|
||||||
|
:param keys: The function accepts multiple keys as positional
|
||||||
|
arguments.
|
||||||
|
"""
|
||||||
|
return map(self.get, keys)
|
||||||
|
|
||||||
|
def get_dict(self, *keys):
|
||||||
|
"""Works like :meth:`get_many` but returns a dict::
|
||||||
|
|
||||||
|
d = cache.get_dict("foo", "bar")
|
||||||
|
foo = d["foo"]
|
||||||
|
bar = d["bar"]
|
||||||
|
|
||||||
|
:param keys: The function accepts multiple keys as positional
|
||||||
|
arguments.
|
||||||
|
"""
|
||||||
|
return dict(izip(keys, self.get_many(*keys)))
|
||||||
|
|
||||||
|
def set(self, key, value, timeout = None):
|
||||||
|
"""Adds a new key/value to the cache (overwrites value, if key already
|
||||||
|
exists in the cache).
|
||||||
|
|
||||||
|
:param key: the key to set
|
||||||
|
:param value: the value for the key
|
||||||
|
:param timeout: the cache timeout for the key (if not specified,
|
||||||
|
it uses the default timeout).
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def add(self, key, value, timeout = None):
|
||||||
|
"""Works like :meth:`set` but does not overwrite the values of already
|
||||||
|
existing keys.
|
||||||
|
|
||||||
|
:param key: the key to set
|
||||||
|
:param value: the value for the key
|
||||||
|
:param timeout: the cache timeout for the key or the default
|
||||||
|
timeout if not specified.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_many(self, mapping, timeout = None):
|
||||||
|
"""Sets multiple keys and values from a mapping.
|
||||||
|
|
||||||
|
:param mapping: a mapping with the keys/values to set.
|
||||||
|
:param timeout: the cache timeout for the key (if not specified,
|
||||||
|
it uses the default timeout).
|
||||||
|
"""
|
||||||
|
for key, value in _items(mapping):
|
||||||
|
self.set(key, value, timeout)
|
||||||
|
|
||||||
|
def delete_many(self, *keys):
|
||||||
|
"""Deletes multiple keys at once.
|
||||||
|
|
||||||
|
:param keys: The function accepts multiple keys as positional
|
||||||
|
arguments.
|
||||||
|
"""
|
||||||
|
for key in keys:
|
||||||
|
self.delete(key)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""Clears the cache. Keep in mind that not all caches support
|
||||||
|
completely clearing the cache.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def inc(self, key, delta = 1):
|
||||||
|
"""Increments the value of a key by `delta`. If the key does
|
||||||
|
not yet exist it is initialized with `delta`.
|
||||||
|
|
||||||
|
For supporting caches this is an atomic operation.
|
||||||
|
|
||||||
|
:param key: the key to increment.
|
||||||
|
:param delta: the delta to add.
|
||||||
|
"""
|
||||||
|
self.set(key, (self.get(key) or 0) + delta)
|
||||||
|
|
||||||
|
def dec(self, key, delta = 1):
|
||||||
|
"""Decrements the value of a key by `delta`. If the key does
|
||||||
|
not yet exist it is initialized with `-delta`.
|
||||||
|
|
||||||
|
For supporting caches this is an atomic operation.
|
||||||
|
|
||||||
|
:param key: the key to increment.
|
||||||
|
:param delta: the delta to subtract.
|
||||||
|
"""
|
||||||
|
self.set(key, (self.get(key) or 0) - delta)
|
||||||
|
|
||||||
|
|
||||||
|
class FileSystemCache(BaseCache):
|
||||||
|
"""A cache that stores the items on the file system. This cache depends
|
||||||
|
on being the only user of the `cache_dir`. Make absolutely sure that
|
||||||
|
nobody but this cache stores files there or otherwise the cache will
|
||||||
|
randomly delete files therein.
|
||||||
|
|
||||||
|
:param cache_dir: the directory where cache files are stored.
|
||||||
|
:param threshold: the maximum number of items the cache stores before
|
||||||
|
it starts deleting some.
|
||||||
|
:param default_timeout: the default timeout that is used if no timeout is
|
||||||
|
specified on :meth:`~BaseCache.set`.
|
||||||
|
:param mode: the file mode wanted for the cache files, default 0600
|
||||||
|
"""
|
||||||
|
|
||||||
|
#: used for temporary files by the FileSystemCache
|
||||||
|
_fs_transaction_suffix = '.__wz_cache'
|
||||||
|
|
||||||
|
def __init__(self, cache_dir, threshold = 500, default_timeout = 300, mode = 0600):
|
||||||
|
BaseCache.__init__(self, default_timeout)
|
||||||
|
self._path = cache_dir
|
||||||
|
self._threshold = threshold
|
||||||
|
self._mode = mode
|
||||||
|
if not os.path.exists(self._path):
|
||||||
|
os.makedirs(self._path)
|
||||||
|
|
||||||
|
def _list_dir(self):
|
||||||
|
"""return a list of (fully qualified) cache filenames
|
||||||
|
"""
|
||||||
|
return [os.path.join(self._path, fn) for fn in os.listdir(self._path)
|
||||||
|
if not fn.endswith(self._fs_transaction_suffix)]
|
||||||
|
|
||||||
|
def _prune(self):
|
||||||
|
entries = self._list_dir()
|
||||||
|
if len(entries) > self._threshold:
|
||||||
|
now = time()
|
||||||
|
for idx, fname in enumerate(entries):
|
||||||
|
remove = False
|
||||||
|
f = None
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
f = open(fname, 'rb')
|
||||||
|
expires = pickle.load(f)
|
||||||
|
remove = expires <= now or idx % 3 == 0
|
||||||
|
finally:
|
||||||
|
if f is not None:
|
||||||
|
f.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if remove:
|
||||||
|
try:
|
||||||
|
os.remove(fname)
|
||||||
|
except (IOError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
for fname in self._list_dir():
|
||||||
|
try:
|
||||||
|
os.remove(fname)
|
||||||
|
except (IOError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_filename(self, key):
|
||||||
|
hash = md5(key).hexdigest()
|
||||||
|
return os.path.join(self._path, hash)
|
||||||
|
|
||||||
|
def get(self, key):
|
||||||
|
filename = self._get_filename(key)
|
||||||
|
try:
|
||||||
|
f = open(filename, 'rb')
|
||||||
|
try:
|
||||||
|
if pickle.load(f) >= time():
|
||||||
|
return pickle.load(f)
|
||||||
|
finally:
|
||||||
|
f.close()
|
||||||
|
os.remove(filename)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add(self, key, value, timeout = None):
|
||||||
|
filename = self._get_filename(key)
|
||||||
|
if not os.path.exists(filename):
|
||||||
|
self.set(key, value, timeout)
|
||||||
|
|
||||||
|
def set(self, key, value, timeout = None):
|
||||||
|
if timeout is None:
|
||||||
|
timeout = self.default_timeout
|
||||||
|
filename = self._get_filename(key)
|
||||||
|
self._prune()
|
||||||
|
try:
|
||||||
|
fd, tmp = tempfile.mkstemp(suffix = self._fs_transaction_suffix,
|
||||||
|
dir = self._path)
|
||||||
|
f = os.fdopen(fd, 'wb')
|
||||||
|
try:
|
||||||
|
pickle.dump(int(time() + timeout), f, 1)
|
||||||
|
pickle.dump(value, f, pickle.HIGHEST_PROTOCOL)
|
||||||
|
finally:
|
||||||
|
f.close()
|
||||||
|
rename(tmp, filename)
|
||||||
|
os.chmod(filename, self._mode)
|
||||||
|
except (IOError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def delete(self, key):
|
||||||
|
try:
|
||||||
|
os.remove(self._get_filename(key))
|
||||||
|
except (IOError, OSError):
|
||||||
|
pass
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
A microframework based on Werkzeug. It's extensively documented
|
|
||||||
and follows best practice patterns.
|
|
||||||
|
|
||||||
:copyright: (c) 2011 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__version__ = '0.9'
|
|
||||||
|
|
||||||
# utilities we import from Werkzeug and Jinja2 that are unused
|
|
||||||
# in the module but are exported as public interface.
|
|
||||||
from werkzeug.exceptions import abort
|
|
||||||
from werkzeug.utils import redirect
|
|
||||||
from jinja2 import Markup, escape
|
|
||||||
|
|
||||||
from .app import Flask, Request, Response
|
|
||||||
from .config import Config
|
|
||||||
from .helpers import url_for, jsonify, json_available, flash, \
|
|
||||||
send_file, send_from_directory, get_flashed_messages, \
|
|
||||||
get_template_attribute, make_response, safe_join, \
|
|
||||||
stream_with_context
|
|
||||||
from .globals import current_app, g, request, session, _request_ctx_stack, \
|
|
||||||
_app_ctx_stack
|
|
||||||
from .ctx import has_request_context, has_app_context, \
|
|
||||||
after_this_request
|
|
||||||
from .module import Module
|
|
||||||
from .blueprints import Blueprint
|
|
||||||
from .templating import render_template, render_template_string
|
|
||||||
|
|
||||||
# the signals
|
|
||||||
from .signals import signals_available, template_rendered, request_started, \
|
|
||||||
request_finished, got_request_exception, request_tearing_down
|
|
||||||
|
|
||||||
# only import json if it's available
|
|
||||||
if json_available:
|
|
||||||
from .helpers import json
|
|
||||||
|
|
||||||
# backwards compat, goes away in 1.0
|
|
||||||
from .sessions import SecureCookieSession as Session
|
|
||||||
-1701
File diff suppressed because it is too large
Load Diff
@@ -1,345 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.blueprints
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Blueprints are the recommended way to implement larger or more
|
|
||||||
pluggable applications in Flask 0.7 and later.
|
|
||||||
|
|
||||||
:copyright: (c) 2011 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
from functools import update_wrapper
|
|
||||||
|
|
||||||
from .helpers import _PackageBoundObject, _endpoint_from_view_func
|
|
||||||
|
|
||||||
|
|
||||||
class BlueprintSetupState(object):
|
|
||||||
"""Temporary holder object for registering a blueprint with the
|
|
||||||
application. An instance of this class is created by the
|
|
||||||
:meth:`~flask.Blueprint.make_setup_state` method and later passed
|
|
||||||
to all register callback functions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, blueprint, app, options, first_registration):
|
|
||||||
#: a reference to the current application
|
|
||||||
self.app = app
|
|
||||||
|
|
||||||
#: a reference to the blueprint that created this setup state.
|
|
||||||
self.blueprint = blueprint
|
|
||||||
|
|
||||||
#: a dictionary with all options that were passed to the
|
|
||||||
#: :meth:`~flask.Flask.register_blueprint` method.
|
|
||||||
self.options = options
|
|
||||||
|
|
||||||
#: as blueprints can be registered multiple times with the
|
|
||||||
#: application and not everything wants to be registered
|
|
||||||
#: multiple times on it, this attribute can be used to figure
|
|
||||||
#: out if the blueprint was registered in the past already.
|
|
||||||
self.first_registration = first_registration
|
|
||||||
|
|
||||||
subdomain = self.options.get('subdomain')
|
|
||||||
if subdomain is None:
|
|
||||||
subdomain = self.blueprint.subdomain
|
|
||||||
|
|
||||||
#: The subdomain that the blueprint should be active for, `None`
|
|
||||||
#: otherwise.
|
|
||||||
self.subdomain = subdomain
|
|
||||||
|
|
||||||
url_prefix = self.options.get('url_prefix')
|
|
||||||
if url_prefix is None:
|
|
||||||
url_prefix = self.blueprint.url_prefix
|
|
||||||
|
|
||||||
#: The prefix that should be used for all URLs defined on the
|
|
||||||
#: blueprint.
|
|
||||||
self.url_prefix = url_prefix
|
|
||||||
|
|
||||||
#: A dictionary with URL defaults that is added to each and every
|
|
||||||
#: URL that was defined with the blueprint.
|
|
||||||
self.url_defaults = dict(self.blueprint.url_values_defaults)
|
|
||||||
self.url_defaults.update(self.options.get('url_defaults', ()))
|
|
||||||
|
|
||||||
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
|
|
||||||
"""A helper method to register a rule (and optionally a view function)
|
|
||||||
to the application. The endpoint is automatically prefixed with the
|
|
||||||
blueprint's name.
|
|
||||||
"""
|
|
||||||
if self.url_prefix:
|
|
||||||
rule = self.url_prefix + rule
|
|
||||||
options.setdefault('subdomain', self.subdomain)
|
|
||||||
if endpoint is None:
|
|
||||||
endpoint = _endpoint_from_view_func(view_func)
|
|
||||||
defaults = self.url_defaults
|
|
||||||
if 'defaults' in options:
|
|
||||||
defaults = dict(defaults, **options.pop('defaults'))
|
|
||||||
self.app.add_url_rule(rule, '%s.%s' % (self.blueprint.name, endpoint),
|
|
||||||
view_func, defaults=defaults, **options)
|
|
||||||
|
|
||||||
|
|
||||||
class Blueprint(_PackageBoundObject):
|
|
||||||
"""Represents a blueprint. A blueprint is an object that records
|
|
||||||
functions that will be called with the
|
|
||||||
:class:`~flask.blueprint.BlueprintSetupState` later to register functions
|
|
||||||
or other things on the main application. See :ref:`blueprints` for more
|
|
||||||
information.
|
|
||||||
|
|
||||||
.. versionadded:: 0.7
|
|
||||||
"""
|
|
||||||
|
|
||||||
warn_on_modifications = False
|
|
||||||
_got_registered_once = False
|
|
||||||
|
|
||||||
def __init__(self, name, import_name, static_folder=None,
|
|
||||||
static_url_path=None, template_folder=None,
|
|
||||||
url_prefix=None, subdomain=None, url_defaults=None):
|
|
||||||
_PackageBoundObject.__init__(self, import_name, template_folder)
|
|
||||||
self.name = name
|
|
||||||
self.url_prefix = url_prefix
|
|
||||||
self.subdomain = subdomain
|
|
||||||
self.static_folder = static_folder
|
|
||||||
self.static_url_path = static_url_path
|
|
||||||
self.deferred_functions = []
|
|
||||||
self.view_functions = {}
|
|
||||||
if url_defaults is None:
|
|
||||||
url_defaults = {}
|
|
||||||
self.url_values_defaults = url_defaults
|
|
||||||
|
|
||||||
def record(self, func):
|
|
||||||
"""Registers a function that is called when the blueprint is
|
|
||||||
registered on the application. This function is called with the
|
|
||||||
state as argument as returned by the :meth:`make_setup_state`
|
|
||||||
method.
|
|
||||||
"""
|
|
||||||
if self._got_registered_once and self.warn_on_modifications:
|
|
||||||
from warnings import warn
|
|
||||||
warn(Warning('The blueprint was already registered once '
|
|
||||||
'but is getting modified now. These changes '
|
|
||||||
'will not show up.'))
|
|
||||||
self.deferred_functions.append(func)
|
|
||||||
|
|
||||||
def record_once(self, func):
|
|
||||||
"""Works like :meth:`record` but wraps the function in another
|
|
||||||
function that will ensure the function is only called once. If the
|
|
||||||
blueprint is registered a second time on the application, the
|
|
||||||
function passed is not called.
|
|
||||||
"""
|
|
||||||
def wrapper(state):
|
|
||||||
if state.first_registration:
|
|
||||||
func(state)
|
|
||||||
return self.record(update_wrapper(wrapper, func))
|
|
||||||
|
|
||||||
def make_setup_state(self, app, options, first_registration=False):
|
|
||||||
"""Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState`
|
|
||||||
object that is later passed to the register callback functions.
|
|
||||||
Subclasses can override this to return a subclass of the setup state.
|
|
||||||
"""
|
|
||||||
return BlueprintSetupState(self, app, options, first_registration)
|
|
||||||
|
|
||||||
def register(self, app, options, first_registration=False):
|
|
||||||
"""Called by :meth:`Flask.register_blueprint` to register a blueprint
|
|
||||||
on the application. This can be overridden to customize the register
|
|
||||||
behavior. Keyword arguments from
|
|
||||||
:func:`~flask.Flask.register_blueprint` are directly forwarded to this
|
|
||||||
method in the `options` dictionary.
|
|
||||||
"""
|
|
||||||
self._got_registered_once = True
|
|
||||||
state = self.make_setup_state(app, options, first_registration)
|
|
||||||
if self.has_static_folder:
|
|
||||||
state.add_url_rule(self.static_url_path + '/<path:filename>',
|
|
||||||
view_func=self.send_static_file,
|
|
||||||
endpoint='static')
|
|
||||||
|
|
||||||
for deferred in self.deferred_functions:
|
|
||||||
deferred(state)
|
|
||||||
|
|
||||||
def route(self, rule, **options):
|
|
||||||
"""Like :meth:`Flask.route` but for a blueprint. The endpoint for the
|
|
||||||
:func:`url_for` function is prefixed with the name of the blueprint.
|
|
||||||
"""
|
|
||||||
def decorator(f):
|
|
||||||
endpoint = options.pop("endpoint", f.__name__)
|
|
||||||
self.add_url_rule(rule, endpoint, f, **options)
|
|
||||||
return f
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
|
|
||||||
"""Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for
|
|
||||||
the :func:`url_for` function is prefixed with the name of the blueprint.
|
|
||||||
"""
|
|
||||||
if endpoint:
|
|
||||||
assert '.' not in endpoint, "Blueprint endpoint's should not contain dot's"
|
|
||||||
self.record(lambda s:
|
|
||||||
s.add_url_rule(rule, endpoint, view_func, **options))
|
|
||||||
|
|
||||||
def endpoint(self, endpoint):
|
|
||||||
"""Like :meth:`Flask.endpoint` but for a blueprint. This does not
|
|
||||||
prefix the endpoint with the blueprint name, this has to be done
|
|
||||||
explicitly by the user of this method. If the endpoint is prefixed
|
|
||||||
with a `.` it will be registered to the current blueprint, otherwise
|
|
||||||
it's an application independent endpoint.
|
|
||||||
"""
|
|
||||||
def decorator(f):
|
|
||||||
def register_endpoint(state):
|
|
||||||
state.app.view_functions[endpoint] = f
|
|
||||||
self.record_once(register_endpoint)
|
|
||||||
return f
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def app_template_filter(self, name=None):
|
|
||||||
"""Register a custom template filter, available application wide. Like
|
|
||||||
:meth:`Flask.template_filter` but for a blueprint.
|
|
||||||
|
|
||||||
:param name: the optional name of the filter, otherwise the
|
|
||||||
function name will be used.
|
|
||||||
"""
|
|
||||||
def decorator(f):
|
|
||||||
self.add_app_template_filter(f, name=name)
|
|
||||||
return f
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def add_app_template_filter(self, f, name=None):
|
|
||||||
"""Register a custom template filter, available application wide. Like
|
|
||||||
:meth:`Flask.add_template_filter` but for a blueprint. Works exactly
|
|
||||||
like the :meth:`app_template_filter` decorator.
|
|
||||||
|
|
||||||
:param name: the optional name of the filter, otherwise the
|
|
||||||
function name will be used.
|
|
||||||
"""
|
|
||||||
def register_template(state):
|
|
||||||
state.app.jinja_env.filters[name or f.__name__] = f
|
|
||||||
self.record_once(register_template)
|
|
||||||
|
|
||||||
def before_request(self, f):
|
|
||||||
"""Like :meth:`Flask.before_request` but for a blueprint. This function
|
|
||||||
is only executed before each request that is handled by a function of
|
|
||||||
that blueprint.
|
|
||||||
"""
|
|
||||||
self.record_once(lambda s: s.app.before_request_funcs
|
|
||||||
.setdefault(self.name, []).append(f))
|
|
||||||
return f
|
|
||||||
|
|
||||||
def before_app_request(self, f):
|
|
||||||
"""Like :meth:`Flask.before_request`. Such a function is executed
|
|
||||||
before each request, even if outside of a blueprint.
|
|
||||||
"""
|
|
||||||
self.record_once(lambda s: s.app.before_request_funcs
|
|
||||||
.setdefault(None, []).append(f))
|
|
||||||
return f
|
|
||||||
|
|
||||||
def before_app_first_request(self, f):
|
|
||||||
"""Like :meth:`Flask.before_first_request`. Such a function is
|
|
||||||
executed before the first request to the application.
|
|
||||||
"""
|
|
||||||
self.record_once(lambda s: s.app.before_first_request_funcs.append(f))
|
|
||||||
return f
|
|
||||||
|
|
||||||
def after_request(self, f):
|
|
||||||
"""Like :meth:`Flask.after_request` but for a blueprint. This function
|
|
||||||
is only executed after each request that is handled by a function of
|
|
||||||
that blueprint.
|
|
||||||
"""
|
|
||||||
self.record_once(lambda s: s.app.after_request_funcs
|
|
||||||
.setdefault(self.name, []).append(f))
|
|
||||||
return f
|
|
||||||
|
|
||||||
def after_app_request(self, f):
|
|
||||||
"""Like :meth:`Flask.after_request` but for a blueprint. Such a function
|
|
||||||
is executed after each request, even if outside of the blueprint.
|
|
||||||
"""
|
|
||||||
self.record_once(lambda s: s.app.after_request_funcs
|
|
||||||
.setdefault(None, []).append(f))
|
|
||||||
return f
|
|
||||||
|
|
||||||
def teardown_request(self, f):
|
|
||||||
"""Like :meth:`Flask.teardown_request` but for a blueprint. This
|
|
||||||
function is only executed when tearing down requests handled by a
|
|
||||||
function of that blueprint. Teardown request functions are executed
|
|
||||||
when the request context is popped, even when no actual request was
|
|
||||||
performed.
|
|
||||||
"""
|
|
||||||
self.record_once(lambda s: s.app.teardown_request_funcs
|
|
||||||
.setdefault(self.name, []).append(f))
|
|
||||||
return f
|
|
||||||
|
|
||||||
def teardown_app_request(self, f):
|
|
||||||
"""Like :meth:`Flask.teardown_request` but for a blueprint. Such a
|
|
||||||
function is executed when tearing down each request, even if outside of
|
|
||||||
the blueprint.
|
|
||||||
"""
|
|
||||||
self.record_once(lambda s: s.app.teardown_request_funcs
|
|
||||||
.setdefault(None, []).append(f))
|
|
||||||
return f
|
|
||||||
|
|
||||||
def context_processor(self, f):
|
|
||||||
"""Like :meth:`Flask.context_processor` but for a blueprint. This
|
|
||||||
function is only executed for requests handled by a blueprint.
|
|
||||||
"""
|
|
||||||
self.record_once(lambda s: s.app.template_context_processors
|
|
||||||
.setdefault(self.name, []).append(f))
|
|
||||||
return f
|
|
||||||
|
|
||||||
def app_context_processor(self, f):
|
|
||||||
"""Like :meth:`Flask.context_processor` but for a blueprint. Such a
|
|
||||||
function is executed each request, even if outside of the blueprint.
|
|
||||||
"""
|
|
||||||
self.record_once(lambda s: s.app.template_context_processors
|
|
||||||
.setdefault(None, []).append(f))
|
|
||||||
return f
|
|
||||||
|
|
||||||
def app_errorhandler(self, code):
|
|
||||||
"""Like :meth:`Flask.errorhandler` but for a blueprint. This
|
|
||||||
handler is used for all requests, even if outside of the blueprint.
|
|
||||||
"""
|
|
||||||
def decorator(f):
|
|
||||||
self.record_once(lambda s: s.app.errorhandler(code)(f))
|
|
||||||
return f
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
def url_value_preprocessor(self, f):
|
|
||||||
"""Registers a function as URL value preprocessor for this
|
|
||||||
blueprint. It's called before the view functions are called and
|
|
||||||
can modify the url values provided.
|
|
||||||
"""
|
|
||||||
self.record_once(lambda s: s.app.url_value_preprocessors
|
|
||||||
.setdefault(self.name, []).append(f))
|
|
||||||
return f
|
|
||||||
|
|
||||||
def url_defaults(self, f):
|
|
||||||
"""Callback function for URL defaults for this blueprint. It's called
|
|
||||||
with the endpoint and values and should update the values passed
|
|
||||||
in place.
|
|
||||||
"""
|
|
||||||
self.record_once(lambda s: s.app.url_default_functions
|
|
||||||
.setdefault(self.name, []).append(f))
|
|
||||||
return f
|
|
||||||
|
|
||||||
def app_url_value_preprocessor(self, f):
|
|
||||||
"""Same as :meth:`url_value_preprocessor` but application wide.
|
|
||||||
"""
|
|
||||||
self.record_once(lambda s: s.app.url_value_preprocessors
|
|
||||||
.setdefault(None, []).append(f))
|
|
||||||
return f
|
|
||||||
|
|
||||||
def app_url_defaults(self, f):
|
|
||||||
"""Same as :meth:`url_defaults` but application wide.
|
|
||||||
"""
|
|
||||||
self.record_once(lambda s: s.app.url_default_functions
|
|
||||||
.setdefault(None, []).append(f))
|
|
||||||
return f
|
|
||||||
|
|
||||||
def errorhandler(self, code_or_exception):
|
|
||||||
"""Registers an error handler that becomes active for this blueprint
|
|
||||||
only. Please be aware that routing does not happen local to a
|
|
||||||
blueprint so an error handler for 404 usually is not handled by
|
|
||||||
a blueprint unless it is caused inside a view function. Another
|
|
||||||
special case is the 500 internal server error which is always looked
|
|
||||||
up from the application.
|
|
||||||
|
|
||||||
Otherwise works as the :meth:`~flask.Flask.errorhandler` decorator
|
|
||||||
of the :class:`~flask.Flask` object.
|
|
||||||
"""
|
|
||||||
def decorator(f):
|
|
||||||
self.record_once(lambda s: s.app._register_error_handler(
|
|
||||||
self.name, code_or_exception, f))
|
|
||||||
return f
|
|
||||||
return decorator
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.config
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Implements the configuration related objects.
|
|
||||||
|
|
||||||
:copyright: (c) 2011 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
import imp
|
|
||||||
import os
|
|
||||||
import errno
|
|
||||||
|
|
||||||
from werkzeug.utils import import_string
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigAttribute(object):
|
|
||||||
"""Makes an attribute forward to the config"""
|
|
||||||
|
|
||||||
def __init__(self, name, get_converter=None):
|
|
||||||
self.__name__ = name
|
|
||||||
self.get_converter = get_converter
|
|
||||||
|
|
||||||
def __get__(self, obj, type=None):
|
|
||||||
if obj is None:
|
|
||||||
return self
|
|
||||||
rv = obj.config[self.__name__]
|
|
||||||
if self.get_converter is not None:
|
|
||||||
rv = self.get_converter(rv)
|
|
||||||
return rv
|
|
||||||
|
|
||||||
def __set__(self, obj, value):
|
|
||||||
obj.config[self.__name__] = value
|
|
||||||
|
|
||||||
|
|
||||||
class Config(dict):
|
|
||||||
"""Works exactly like a dict but provides ways to fill it from files
|
|
||||||
or special dictionaries. There are two common patterns to populate the
|
|
||||||
config.
|
|
||||||
|
|
||||||
Either you can fill the config from a config file::
|
|
||||||
|
|
||||||
app.config.from_pyfile('yourconfig.cfg')
|
|
||||||
|
|
||||||
Or alternatively you can define the configuration options in the
|
|
||||||
module that calls :meth:`from_object` or provide an import path to
|
|
||||||
a module that should be loaded. It is also possible to tell it to
|
|
||||||
use the same module and with that provide the configuration values
|
|
||||||
just before the call::
|
|
||||||
|
|
||||||
DEBUG = True
|
|
||||||
SECRET_KEY = 'development key'
|
|
||||||
app.config.from_object(__name__)
|
|
||||||
|
|
||||||
In both cases (loading from any Python file or loading from modules),
|
|
||||||
only uppercase keys are added to the config. This makes it possible to use
|
|
||||||
lowercase values in the config file for temporary values that are not added
|
|
||||||
to the config or to define the config keys in the same file that implements
|
|
||||||
the application.
|
|
||||||
|
|
||||||
Probably the most interesting way to load configurations is from an
|
|
||||||
environment variable pointing to a file::
|
|
||||||
|
|
||||||
app.config.from_envvar('YOURAPPLICATION_SETTINGS')
|
|
||||||
|
|
||||||
In this case before launching the application you have to set this
|
|
||||||
environment variable to the file you want to use. On Linux and OS X
|
|
||||||
use the export statement::
|
|
||||||
|
|
||||||
export YOURAPPLICATION_SETTINGS='/path/to/config/file'
|
|
||||||
|
|
||||||
On windows use `set` instead.
|
|
||||||
|
|
||||||
:param root_path: path to which files are read relative from. When the
|
|
||||||
config object is created by the application, this is
|
|
||||||
the application's :attr:`~flask.Flask.root_path`.
|
|
||||||
:param defaults: an optional dictionary of default values
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, root_path, defaults=None):
|
|
||||||
dict.__init__(self, defaults or {})
|
|
||||||
self.root_path = root_path
|
|
||||||
|
|
||||||
def from_envvar(self, variable_name, silent=False):
|
|
||||||
"""Loads a configuration from an environment variable pointing to
|
|
||||||
a configuration file. This is basically just a shortcut with nicer
|
|
||||||
error messages for this line of code::
|
|
||||||
|
|
||||||
app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS'])
|
|
||||||
|
|
||||||
:param variable_name: name of the environment variable
|
|
||||||
:param silent: set to `True` if you want silent failure for missing
|
|
||||||
files.
|
|
||||||
:return: bool. `True` if able to load config, `False` otherwise.
|
|
||||||
"""
|
|
||||||
rv = os.environ.get(variable_name)
|
|
||||||
if not rv:
|
|
||||||
if silent:
|
|
||||||
return False
|
|
||||||
raise RuntimeError('The environment variable %r is not set '
|
|
||||||
'and as such configuration could not be '
|
|
||||||
'loaded. Set this variable and make it '
|
|
||||||
'point to a configuration file' %
|
|
||||||
variable_name)
|
|
||||||
return self.from_pyfile(rv, silent=silent)
|
|
||||||
|
|
||||||
def from_pyfile(self, filename, silent=False):
|
|
||||||
"""Updates the values in the config from a Python file. This function
|
|
||||||
behaves as if the file was imported as module with the
|
|
||||||
:meth:`from_object` function.
|
|
||||||
|
|
||||||
:param filename: the filename of the config. This can either be an
|
|
||||||
absolute filename or a filename relative to the
|
|
||||||
root path.
|
|
||||||
:param silent: set to `True` if you want silent failure for missing
|
|
||||||
files.
|
|
||||||
|
|
||||||
.. versionadded:: 0.7
|
|
||||||
`silent` parameter.
|
|
||||||
"""
|
|
||||||
filename = os.path.join(self.root_path, filename)
|
|
||||||
d = imp.new_module('config')
|
|
||||||
d.__file__ = filename
|
|
||||||
try:
|
|
||||||
execfile(filename, d.__dict__)
|
|
||||||
except IOError, e:
|
|
||||||
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
|
|
||||||
return False
|
|
||||||
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
|
|
||||||
raise
|
|
||||||
self.from_object(d)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def from_object(self, obj):
|
|
||||||
"""Updates the values from the given object. An object can be of one
|
|
||||||
of the following two types:
|
|
||||||
|
|
||||||
- a string: in this case the object with that name will be imported
|
|
||||||
- an actual object reference: that object is used directly
|
|
||||||
|
|
||||||
Objects are usually either modules or classes.
|
|
||||||
|
|
||||||
Just the uppercase variables in that object are stored in the config.
|
|
||||||
Example usage::
|
|
||||||
|
|
||||||
app.config.from_object('yourapplication.default_config')
|
|
||||||
from yourapplication import default_config
|
|
||||||
app.config.from_object(default_config)
|
|
||||||
|
|
||||||
You should not use this function to load the actual configuration but
|
|
||||||
rather configuration defaults. The actual config should be loaded
|
|
||||||
with :meth:`from_pyfile` and ideally from a location not within the
|
|
||||||
package because the package might be installed system wide.
|
|
||||||
|
|
||||||
:param obj: an import name or object
|
|
||||||
"""
|
|
||||||
if isinstance(obj, basestring):
|
|
||||||
obj = import_string(obj)
|
|
||||||
for key in dir(obj):
|
|
||||||
if key.isupper():
|
|
||||||
self[key] = getattr(obj, key)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self))
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user