Merge branch 'refs/heads/develop'
This commit is contained in:
commit
47d37c2ec9
493 files changed
+10399
-53056
No files matched your search
@@ -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/`
|
||||||
+39
-39
@@ -1,84 +1,84 @@
|
|||||||
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):
|
||||||
|
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():
|
|
||||||
|
|
||||||
|
addView('docs', apiDocs)
|
||||||
|
|
||||||
|
# Make non basic auth option to get api key
|
||||||
|
class KeyHandler(RequestHandler):
|
||||||
|
def get(self):
|
||||||
api = None
|
api = None
|
||||||
params = getParams()
|
|
||||||
username = Env.setting('username')
|
username = Env.setting('username')
|
||||||
password = Env.setting('password')
|
password = Env.setting('password')
|
||||||
|
|
||||||
if (params.get('u') == md5(username) or not username) and (params.get('p') == password or not password):
|
if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password):
|
||||||
api = Env.setting('api_key')
|
api = Env.setting('api_key')
|
||||||
|
|
||||||
return jsonified({
|
self.write({
|
||||||
'success': api is not None,
|
'success': api is not None,
|
||||||
'api_key': api
|
'api_key': api
|
||||||
})
|
})
|
||||||
|
|
||||||
@app.errorhandler(404)
|
def page_not_found(rh):
|
||||||
def page_not_found(error):
|
index_url = Env.get('web_base')
|
||||||
index_url = url_for('web.index')
|
url = rh.request.uri[len(index_url):]
|
||||||
url = request.path[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')
|
||||||
|
|
||||||
+41
-19
@@ -1,14 +1,14 @@
|
|||||||
from flask.blueprints import Blueprint
|
|
||||||
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 = []
|
||||||
@@ -32,14 +32,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 +40,39 @@ 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):
|
||||||
|
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))
|
||||||
|
|
||||||
|
# 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
|
||||||
@@ -69,9 +68,9 @@ class Core(Plugin):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def available(self):
|
def available(self):
|
||||||
return jsonified({
|
return {
|
||||||
'success': True
|
'success': True
|
||||||
})
|
}
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
if self.shutdown_started:
|
if self.shutdown_started:
|
||||||
@@ -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)
|
||||||
@@ -171,9 +170,9 @@ 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):
|
||||||
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',
|
||||||
@@ -92,17 +91,14 @@ class Updater(Plugin):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def info(self):
|
def info(self, **kwargs):
|
||||||
return self.updater.info()
|
return self.updater.info()
|
||||||
|
|
||||||
def getInfo(self):
|
|
||||||
return jsonified(self.updater.info())
|
|
||||||
|
|
||||||
def checkView(self):
|
def checkView(self):
|
||||||
return jsonified({
|
return {
|
||||||
'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):
|
||||||
|
|
||||||
@@ -119,9 +115,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 +134,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 +272,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)
|
||||||
|
|||||||
+40
-15
@@ -1,26 +1,51 @@
|
|||||||
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):
|
|
||||||
auth = getattr(request, 'authorization')
|
|
||||||
if Env.setting('username') and Env.setting('password'):
|
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
|
||||||
|
|
||||||
|
# @wraps(f)
|
||||||
|
# def decorated(*args, **kwargs):
|
||||||
|
# auth = getattr(request, 'authorization')
|
||||||
|
# if Env.setting('username') and Env.setting('password'):
|
||||||
|
# if (not auth or not check_auth(auth.username.decode('latin1'), md5(auth.password.decode('latin1').encode(Env.get('encoding'))))):
|
||||||
|
# return authenticate()
|
||||||
|
#
|
||||||
|
# return f(*args, **kwargs)
|
||||||
|
#
|
||||||
|
# return decorated
|
||||||
@@ -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
|
|
||||||
|
|
||||||
def add_torrent_uri(self, torrent):
|
|
||||||
log.info('Adding torrent URL %s', torrent)
|
|
||||||
response = {}
|
|
||||||
# login
|
|
||||||
login = self._login()
|
|
||||||
if len(login) > 0 and login['success'] == True:
|
|
||||||
log.info('Login success, adding torrent')
|
|
||||||
args = {'api':'SYNO.DownloadStation.Task', 'version':1, 'method':'create', 'uri':torrent, '_sid':self.sid}
|
|
||||||
response = self._req(self.download_url, args)
|
|
||||||
self._logout()
|
|
||||||
else:
|
|
||||||
log.error('Couldn\'t login to Synology, %s', login)
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def create_task(self, url = None, filename = None, filedata = None):
|
||||||
|
''' Creates new download task in Synology DownloadStation. Either specify
|
||||||
|
url or pair (filename, filedata).
|
||||||
|
|
||||||
|
Returns True if task was created, False otherwise
|
||||||
|
'''
|
||||||
|
result = False
|
||||||
|
# login
|
||||||
|
if self._login():
|
||||||
|
args = {'api': 'SYNO.DownloadStation.Task',
|
||||||
|
'version': '1',
|
||||||
|
'method': 'create',
|
||||||
|
'_sid': self.sid}
|
||||||
|
if url:
|
||||||
|
log.info('Login success, adding torrent URI')
|
||||||
|
args['uri'] = url
|
||||||
|
response = self._req(self.download_url, args = args)
|
||||||
|
log.info('Response: %s', response)
|
||||||
|
result = response['success']
|
||||||
|
elif filename and filedata:
|
||||||
|
log.info('Login success, adding torrent')
|
||||||
|
files = {'file': (filename, filedata)}
|
||||||
|
response = self._req(self.download_url, args = args, files = files)
|
||||||
|
log.info('Response: %s', response)
|
||||||
|
result = response['success']
|
||||||
|
else:
|
||||||
|
log.error('Invalid use of SynologyRPC.create_task: either url or filename+filedata must be specified')
|
||||||
|
self._logout()
|
||||||
|
|
||||||
|
return result
|
||||||
@@ -22,14 +22,22 @@ def addEvent(name, handler, priority = 100):
|
|||||||
def createHandle(*args, **kwargs):
|
def createHandle(*args, **kwargs):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Open handler
|
||||||
|
has_parent = hasattr(handler, 'im_self')
|
||||||
|
if has_parent:
|
||||||
parent = handler.im_self
|
parent = handler.im_self
|
||||||
bc = hasattr(parent, 'beforeCall')
|
bc = hasattr(parent, 'beforeCall')
|
||||||
if bc: parent.beforeCall(handler)
|
if bc: parent.beforeCall(handler)
|
||||||
|
|
||||||
|
# Main event
|
||||||
h = runHandler(name, handler, *args, **kwargs)
|
h = runHandler(name, handler, *args, **kwargs)
|
||||||
|
|
||||||
|
# Close handler
|
||||||
|
if has_parent:
|
||||||
ac = hasattr(parent, 'afterCall')
|
ac = hasattr(parent, 'afterCall')
|
||||||
if ac: parent.afterCall(handler)
|
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,82 +0,0 @@
|
|||||||
from couchpotato.core.helpers.encoding import toUnicode
|
|
||||||
from couchpotato.core.helpers.variable import natcmp
|
|
||||||
from flask.globals import current_app
|
|
||||||
from flask.helpers import json, make_response
|
|
||||||
from urllib import unquote
|
|
||||||
from werkzeug.urls import url_decode
|
|
||||||
import flask
|
|
||||||
import re
|
|
||||||
|
|
||||||
def getParams():
|
|
||||||
|
|
||||||
params = url_decode(getattr(flask.request, 'environ').get('QUERY_STRING', ''))
|
|
||||||
reg = re.compile('^[a-z0-9_\.]+$')
|
|
||||||
|
|
||||||
current = temp = {}
|
|
||||||
for param, value in sorted(params.iteritems()):
|
|
||||||
|
|
||||||
nest = re.split("([\[\]]+)", param)
|
|
||||||
if len(nest) > 1:
|
|
||||||
nested = []
|
|
||||||
for key in nest:
|
|
||||||
if reg.match(key):
|
|
||||||
nested.append(key)
|
|
||||||
|
|
||||||
current = temp
|
|
||||||
|
|
||||||
for item in nested:
|
|
||||||
if item is nested[-1]:
|
|
||||||
current[item] = toUnicode(unquote(value))
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
current[item]
|
|
||||||
except:
|
|
||||||
current[item] = {}
|
|
||||||
|
|
||||||
current = current[item]
|
|
||||||
else:
|
|
||||||
temp[param] = toUnicode(unquote(value))
|
|
||||||
|
|
||||||
return dictToList(temp)
|
|
||||||
|
|
||||||
def dictToList(params):
|
|
||||||
|
|
||||||
if type(params) is dict:
|
|
||||||
new = {}
|
|
||||||
for x, value in params.iteritems():
|
|
||||||
try:
|
|
||||||
new_value = [dictToList(value[k]) for k in sorted(value.iterkeys(), cmp = natcmp)]
|
|
||||||
except:
|
|
||||||
new_value = value
|
|
||||||
|
|
||||||
new[x] = new_value
|
|
||||||
else:
|
|
||||||
new = params
|
|
||||||
|
|
||||||
return new
|
|
||||||
|
|
||||||
def getParam(attr, default = None):
|
|
||||||
try:
|
|
||||||
return getParams().get(attr, default)
|
|
||||||
except:
|
|
||||||
return default
|
|
||||||
|
|
||||||
def padded_jsonify(callback, *args, **kwargs):
|
|
||||||
content = str(callback) + '(' + json.dumps(dict(*args, **kwargs)) + ')'
|
|
||||||
return getattr(current_app, 'response_class')(content, mimetype = 'text/javascript')
|
|
||||||
|
|
||||||
def jsonify(mimetype, *args, **kwargs):
|
|
||||||
content = json.dumps(dict(*args, **kwargs))
|
|
||||||
return getattr(current_app, 'response_class')(content, mimetype = mimetype)
|
|
||||||
|
|
||||||
def jsonified(*args, **kwargs):
|
|
||||||
callback = getParam('callback_func', None)
|
|
||||||
if callback:
|
|
||||||
content = padded_jsonify(callback, *args, **kwargs)
|
|
||||||
else:
|
|
||||||
content = jsonify('application/json', *args, **kwargs)
|
|
||||||
|
|
||||||
response = make_response(content)
|
|
||||||
response.cache_control.no_cache = True
|
|
||||||
|
|
||||||
return response
|
|
||||||
@@ -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
|
||||||
@@ -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):
|
||||||
|
|
||||||
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):
|
||||||
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
|
||||||
@@ -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
|
||||||
@@ -33,4 +32,6 @@ class Synoindex(Notification):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def test(self):
|
def test(self):
|
||||||
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})
|
||||||
|
|
||||||
|
if not self.conf('only_first') or hosts.index(host) == 0:
|
||||||
response += self.request(host, [('VideoLibrary.Scan', {})])
|
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 = ''):
|
||||||
|
|
||||||
@@ -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'))
|
||||||
|
|
||||||
|
if isinstance(params, (str, unicode)) and len(params) > 0:
|
||||||
|
data = params
|
||||||
|
else:
|
||||||
data = tryUrlencode(params) if len(params) > 0 else None
|
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__)
|
||||||
@@ -43,14 +42,13 @@ class Dashboard(Plugin):
|
|||||||
|
|
||||||
suggestions = fireEvent('movie.suggest', movies = identifiers, single = True)
|
suggestions = fireEvent('movie.suggest', movies = identifiers, single = True)
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'result': True,
|
'result': True,
|
||||||
'suggestions': suggestions
|
'suggestions': suggestions
|
||||||
})
|
}
|
||||||
|
|
||||||
def getSoonView(self):
|
def getSoonView(self, limit_offset = None, random = False, late = False, **kwargs):
|
||||||
|
|
||||||
params = getParams()
|
|
||||||
db = get_session()
|
db = get_session()
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
@@ -85,7 +83,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 +90,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:
|
||||||
@@ -126,18 +123,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') 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') 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):
|
||||||
|
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 = {}):
|
||||||
|
|
||||||
@@ -160,6 +152,6 @@ class FileManager(Plugin):
|
|||||||
|
|
||||||
def getTypesView(self):
|
def getTypesView(self):
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'types': self.getTypes()
|
'types': self.getTypes()
|
||||||
})
|
}
|
||||||
@@ -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,10 +109,10 @@ 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):
|
||||||
|
|
||||||
@@ -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):
|
||||||
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):
|
||||||
|
|
||||||
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 = ''):
|
||||||
|
|
||||||
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 = ''):
|
||||||
|
|
||||||
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):
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,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 +165,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 +319,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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -48,10 +47,10 @@ class ProfilePlugin(Plugin):
|
|||||||
|
|
||||||
def allView(self):
|
def allView(self):
|
||||||
|
|
||||||
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):
|
||||||
|
|
||||||
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
|
||||||
@@ -53,10 +52,10 @@ class QualityPlugin(Plugin):
|
|||||||
|
|
||||||
def allView(self):
|
def allView(self):
|
||||||
|
|
||||||
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': '',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 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
|
||||||
@@ -67,15 +66,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):
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'progress': self.in_progress
|
'progress': self.in_progress
|
||||||
})
|
}
|
||||||
|
|
||||||
def allMovies(self):
|
def allMovies(self):
|
||||||
|
|
||||||
@@ -146,9 +145,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:
|
||||||
@@ -162,14 +162,14 @@ 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):
|
||||||
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 +240,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 +269,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
|
||||||
@@ -586,18 +589,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
|
||||||
@@ -44,10 +43,10 @@ class StatusPlugin(Plugin):
|
|||||||
|
|
||||||
def list(self):
|
def list(self):
|
||||||
|
|
||||||
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,6 +1,5 @@
|
|||||||
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.plugins.base import Plugin
|
from couchpotato.core.plugins.base import Plugin
|
||||||
|
|
||||||
class Suggestion(Plugin):
|
class Suggestion(Plugin):
|
||||||
@@ -9,14 +8,13 @@ class Suggestion(Plugin):
|
|||||||
|
|
||||||
addApiView('suggestion.view', self.getView)
|
addApiView('suggestion.view', self.getView)
|
||||||
|
|
||||||
def getView(self):
|
def getView(self, limit_offset = None, **kwargs):
|
||||||
|
|
||||||
limit_offset = getParam('limit_offset', None)
|
|
||||||
total_movies, movies = fireEvent('movie.list', status = 'suggest', limit_offset = limit_offset, single = True)
|
total_movies, movies = fireEvent('movie.list', status = 'suggest', limit_offset = limit_offset, single = True)
|
||||||
|
|
||||||
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,
|
||||||
})
|
}
|
||||||
@@ -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):
|
||||||
|
|
||||||
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 = ''):
|
def getUserScript(self, route):
|
||||||
|
|
||||||
|
klass = self
|
||||||
|
|
||||||
|
class UserscriptHandler(RequestHandler):
|
||||||
|
|
||||||
|
def get(self, random, route):
|
||||||
|
|
||||||
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),
|
||||||
'version': self.getVersion(),
|
'version': klass.getVersion(),
|
||||||
'api': '%suserscript/' % url_for('api.index').lstrip('/'),
|
'api': '%suserscript/' % Env.get('api_base'),
|
||||||
'host': request.host_url,
|
'host': '%s://%s' % (self.request.protocol, self.request.host),
|
||||||
}
|
}
|
||||||
|
|
||||||
script = self.renderTemplate(__file__, 'template.js', **params)
|
script = klass.renderTemplate(__file__, 'template.js', **params)
|
||||||
self.createFile(os.path.join(Env.get('cache_dir'), 'couchpotato.user.js'), script)
|
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)
|
|
||||||
|
|
||||||
@@ -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'):
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from couchpotato import get_session
|
from couchpotato import get_session
|
||||||
from couchpotato.core.event import addEvent, fireEvent
|
from couchpotato.core.event import addEvent, fireEvent
|
||||||
from couchpotato.core.helpers.encoding import 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.core.settings.model import Movie
|
||||||
@@ -50,8 +49,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):
|
||||||
|
|
||||||
@@ -86,17 +85,16 @@ class CouchPotatoApi(MovieProvider):
|
|||||||
def suggest(self, movies = [], ignore = []):
|
def suggest(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),
|
||||||
})
|
})
|
||||||
log.info('Found Suggestions for %s', (suggestions))
|
log.info('Found Suggestions for %s', (suggestions))
|
||||||
|
|
||||||
return suggestions
|
return suggestions
|
||||||
|
|
||||||
def suggestView(self):
|
def suggestView(self, **kwargs):
|
||||||
|
|
||||||
params = getParams()
|
movies = kwargs.get('movies')
|
||||||
movies = params.get('movies')
|
ignore = kwargs.get('ignore', [])
|
||||||
ignore = params.get('ignore', [])
|
|
||||||
|
|
||||||
if not movies:
|
if not movies:
|
||||||
db = get_session()
|
db = get_session()
|
||||||
@@ -105,11 +103,11 @@ class CouchPotatoApi(MovieProvider):
|
|||||||
|
|
||||||
suggestions = self.suggest(movies, ignore)
|
suggestions = self.suggest(movies, ignore)
|
||||||
|
|
||||||
return jsonified({
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'count': len(suggestions),
|
'count': len(suggestions),
|
||||||
'suggestions': suggestions
|
'suggestions': suggestions
|
||||||
})
|
}
|
||||||
|
|
||||||
def getRequestHeaders(self):
|
def getRequestHeaders(self):
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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,13 +32,24 @@ 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
|
||||||
|
current_page = 1
|
||||||
|
while current_page <= pages and not self.shuttingDown():
|
||||||
|
|
||||||
|
url = self.urls['search'] % (self.getCatId(quality['identifier'])[0], freeleech, tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])), current_page)
|
||||||
data = self.getHTMLData(url, opener = self.login_opener)
|
data = self.getHTMLData(url, opener = self.login_opener)
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
html = BeautifulSoup(data)
|
html = BeautifulSoup(data)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
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)
|
||||||
|
|
||||||
result_table = html.find('table', attrs = {'class' : 'torrents'})
|
result_table = html.find('table', attrs = {'class' : 'torrents'})
|
||||||
|
|
||||||
if not result_table or 'nothing found!' in data.lower():
|
if not result_table or 'nothing found!' in data.lower():
|
||||||
@@ -48,7 +59,11 @@ class IPTorrents(TorrentProvider):
|
|||||||
|
|
||||||
for result in entries[1:]:
|
for result in entries[1:]:
|
||||||
|
|
||||||
torrent = result.find_all('td')[1].find('a')
|
torrent = result.find_all('td')
|
||||||
|
if len(torrent) <= 1:
|
||||||
|
break
|
||||||
|
|
||||||
|
torrent = torrent[1].find('a')
|
||||||
|
|
||||||
torrent_id = torrent['href'].replace('/details.php?id=', '')
|
torrent_id = torrent['href'].replace('/details.php?id=', '')
|
||||||
torrent_name = torrent.string
|
torrent_name = torrent.string
|
||||||
@@ -63,7 +78,6 @@ class IPTorrents(TorrentProvider):
|
|||||||
'name': torrent_name,
|
'name': torrent_name,
|
||||||
'url': torrent_download_url,
|
'url': torrent_download_url,
|
||||||
'detail_url': torrent_details_url,
|
'detail_url': torrent_details_url,
|
||||||
'download': self.loginDownload,
|
|
||||||
'size': torrent_size,
|
'size': torrent_size,
|
||||||
'seeders': torrent_seeders,
|
'seeders': torrent_seeders,
|
||||||
'leechers': torrent_leechers,
|
'leechers': torrent_leechers,
|
||||||
@@ -71,9 +85,9 @@ class IPTorrents(TorrentProvider):
|
|||||||
|
|
||||||
except:
|
except:
|
||||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||||
|
break
|
||||||
|
|
||||||
def loginSuccess(self, output):
|
current_page += 1
|
||||||
return 'don\'t have an account' not in output.lower()
|
|
||||||
|
|
||||||
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
|
|
||||||
if not self.login_opener and not self.login():
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params))
|
url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params))
|
||||||
txt = self.urlopen(url, opener = self.login_opener)
|
res = self.getJsonData(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
|
||||||
@@ -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
|
||||||
@@ -170,18 +169,16 @@ class Settings(object):
|
|||||||
|
|
||||||
|
|
||||||
def view(self):
|
def view(self):
|
||||||
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+76
-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,61 @@ 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'%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 +290,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 |
@@ -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
|
||||||
|
|||||||
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
|
||||||
File renamed without changes.
@@ -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))
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.ctx
|
|
||||||
~~~~~~~~~
|
|
||||||
|
|
||||||
Implements the objects required to keep the context.
|
|
||||||
|
|
||||||
:copyright: (c) 2011 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from werkzeug.exceptions import HTTPException
|
|
||||||
|
|
||||||
from .globals import _request_ctx_stack, _app_ctx_stack
|
|
||||||
from .module import blueprint_is_module
|
|
||||||
|
|
||||||
|
|
||||||
class _RequestGlobals(object):
|
|
||||||
"""A plain object."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def after_this_request(f):
|
|
||||||
"""Executes a function after this request. This is useful to modify
|
|
||||||
response objects. The function is passed the response object and has
|
|
||||||
to return the same or a new one.
|
|
||||||
|
|
||||||
Example::
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
def index():
|
|
||||||
@after_this_request
|
|
||||||
def add_header(response):
|
|
||||||
response.headers['X-Foo'] = 'Parachute'
|
|
||||||
return response
|
|
||||||
return 'Hello World!'
|
|
||||||
|
|
||||||
This is more useful if a function other than the view function wants to
|
|
||||||
modify a response. For instance think of a decorator that wants to add
|
|
||||||
some headers without converting the return value into a response object.
|
|
||||||
|
|
||||||
.. versionadded:: 0.9
|
|
||||||
"""
|
|
||||||
_request_ctx_stack.top._after_request_functions.append(f)
|
|
||||||
return f
|
|
||||||
|
|
||||||
|
|
||||||
def has_request_context():
|
|
||||||
"""If you have code that wants to test if a request context is there or
|
|
||||||
not this function can be used. For instance, you may want to take advantage
|
|
||||||
of request information if the request object is available, but fail
|
|
||||||
silently if it is unavailable.
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
class User(db.Model):
|
|
||||||
|
|
||||||
def __init__(self, username, remote_addr=None):
|
|
||||||
self.username = username
|
|
||||||
if remote_addr is None and has_request_context():
|
|
||||||
remote_addr = request.remote_addr
|
|
||||||
self.remote_addr = remote_addr
|
|
||||||
|
|
||||||
Alternatively you can also just test any of the context bound objects
|
|
||||||
(such as :class:`request` or :class:`g` for truthness)::
|
|
||||||
|
|
||||||
class User(db.Model):
|
|
||||||
|
|
||||||
def __init__(self, username, remote_addr=None):
|
|
||||||
self.username = username
|
|
||||||
if remote_addr is None and request:
|
|
||||||
remote_addr = request.remote_addr
|
|
||||||
self.remote_addr = remote_addr
|
|
||||||
|
|
||||||
.. versionadded:: 0.7
|
|
||||||
"""
|
|
||||||
return _request_ctx_stack.top is not None
|
|
||||||
|
|
||||||
|
|
||||||
def has_app_context():
|
|
||||||
"""Works like :func:`has_request_context` but for the application
|
|
||||||
context. You can also just do a boolean check on the
|
|
||||||
:data:`current_app` object instead.
|
|
||||||
|
|
||||||
.. versionadded:: 0.9
|
|
||||||
"""
|
|
||||||
return _app_ctx_stack.top is not None
|
|
||||||
|
|
||||||
|
|
||||||
class AppContext(object):
|
|
||||||
"""The application context binds an application object implicitly
|
|
||||||
to the current thread or greenlet, similar to how the
|
|
||||||
:class:`RequestContext` binds request information. The application
|
|
||||||
context is also implicitly created if a request context is created
|
|
||||||
but the application is not on top of the individual application
|
|
||||||
context.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, app):
|
|
||||||
self.app = app
|
|
||||||
self.url_adapter = app.create_url_adapter(None)
|
|
||||||
|
|
||||||
# Like request context, app contexts can be pushed multiple times
|
|
||||||
# but there a basic "refcount" is enough to track them.
|
|
||||||
self._refcnt = 0
|
|
||||||
|
|
||||||
def push(self):
|
|
||||||
"""Binds the app context to the current context."""
|
|
||||||
self._refcnt += 1
|
|
||||||
_app_ctx_stack.push(self)
|
|
||||||
|
|
||||||
def pop(self, exc=None):
|
|
||||||
"""Pops the app context."""
|
|
||||||
self._refcnt -= 1
|
|
||||||
if self._refcnt <= 0:
|
|
||||||
if exc is None:
|
|
||||||
exc = sys.exc_info()[1]
|
|
||||||
self.app.do_teardown_appcontext(exc)
|
|
||||||
rv = _app_ctx_stack.pop()
|
|
||||||
assert rv is self, 'Popped wrong app context. (%r instead of %r)' \
|
|
||||||
% (rv, self)
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
self.push()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, tb):
|
|
||||||
self.pop(exc_value)
|
|
||||||
|
|
||||||
|
|
||||||
class RequestContext(object):
|
|
||||||
"""The request context contains all request relevant information. It is
|
|
||||||
created at the beginning of the request and pushed to the
|
|
||||||
`_request_ctx_stack` and removed at the end of it. It will create the
|
|
||||||
URL adapter and request object for the WSGI environment provided.
|
|
||||||
|
|
||||||
Do not attempt to use this class directly, instead use
|
|
||||||
:meth:`~flask.Flask.test_request_context` and
|
|
||||||
:meth:`~flask.Flask.request_context` to create this object.
|
|
||||||
|
|
||||||
When the request context is popped, it will evaluate all the
|
|
||||||
functions registered on the application for teardown execution
|
|
||||||
(:meth:`~flask.Flask.teardown_request`).
|
|
||||||
|
|
||||||
The request context is automatically popped at the end of the request
|
|
||||||
for you. In debug mode the request context is kept around if
|
|
||||||
exceptions happen so that interactive debuggers have a chance to
|
|
||||||
introspect the data. With 0.4 this can also be forced for requests
|
|
||||||
that did not fail and outside of `DEBUG` mode. By setting
|
|
||||||
``'flask._preserve_context'`` to `True` on the WSGI environment the
|
|
||||||
context will not pop itself at the end of the request. This is used by
|
|
||||||
the :meth:`~flask.Flask.test_client` for example to implement the
|
|
||||||
deferred cleanup functionality.
|
|
||||||
|
|
||||||
You might find this helpful for unittests where you need the
|
|
||||||
information from the context local around for a little longer. Make
|
|
||||||
sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in
|
|
||||||
that situation, otherwise your unittests will leak memory.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, app, environ):
|
|
||||||
self.app = app
|
|
||||||
self.request = app.request_class(environ)
|
|
||||||
self.url_adapter = app.create_url_adapter(self.request)
|
|
||||||
self.g = app.request_globals_class()
|
|
||||||
self.flashes = None
|
|
||||||
self.session = None
|
|
||||||
|
|
||||||
# Request contexts can be pushed multiple times and interleaved with
|
|
||||||
# other request contexts. Now only if the last level is popped we
|
|
||||||
# get rid of them. Additionally if an application context is missing
|
|
||||||
# one is created implicitly so for each level we add this information
|
|
||||||
self._implicit_app_ctx_stack = []
|
|
||||||
|
|
||||||
# indicator if the context was preserved. Next time another context
|
|
||||||
# is pushed the preserved context is popped.
|
|
||||||
self.preserved = False
|
|
||||||
|
|
||||||
# Functions that should be executed after the request on the response
|
|
||||||
# object. These will be called before the regular "after_request"
|
|
||||||
# functions.
|
|
||||||
self._after_request_functions = []
|
|
||||||
|
|
||||||
self.match_request()
|
|
||||||
|
|
||||||
# XXX: Support for deprecated functionality. This is going away with
|
|
||||||
# Flask 1.0
|
|
||||||
blueprint = self.request.blueprint
|
|
||||||
if blueprint is not None:
|
|
||||||
# better safe than sorry, we don't want to break code that
|
|
||||||
# already worked
|
|
||||||
bp = app.blueprints.get(blueprint)
|
|
||||||
if bp is not None and blueprint_is_module(bp):
|
|
||||||
self.request._is_old_module = True
|
|
||||||
|
|
||||||
def match_request(self):
|
|
||||||
"""Can be overridden by a subclass to hook into the matching
|
|
||||||
of the request.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
url_rule, self.request.view_args = \
|
|
||||||
self.url_adapter.match(return_rule=True)
|
|
||||||
self.request.url_rule = url_rule
|
|
||||||
except HTTPException, e:
|
|
||||||
self.request.routing_exception = e
|
|
||||||
|
|
||||||
def push(self):
|
|
||||||
"""Binds the request context to the current context."""
|
|
||||||
# If an exception ocurrs in debug mode or if context preservation is
|
|
||||||
# activated under exception situations exactly one context stays
|
|
||||||
# on the stack. The rationale is that you want to access that
|
|
||||||
# information under debug situations. However if someone forgets to
|
|
||||||
# pop that context again we want to make sure that on the next push
|
|
||||||
# it's invalidated otherwise we run at risk that something leaks
|
|
||||||
# memory. This is usually only a problem in testsuite since this
|
|
||||||
# functionality is not active in production environments.
|
|
||||||
top = _request_ctx_stack.top
|
|
||||||
if top is not None and top.preserved:
|
|
||||||
top.pop()
|
|
||||||
|
|
||||||
# Before we push the request context we have to ensure that there
|
|
||||||
# is an application context.
|
|
||||||
app_ctx = _app_ctx_stack.top
|
|
||||||
if app_ctx is None or app_ctx.app != self.app:
|
|
||||||
app_ctx = self.app.app_context()
|
|
||||||
app_ctx.push()
|
|
||||||
self._implicit_app_ctx_stack.append(app_ctx)
|
|
||||||
else:
|
|
||||||
self._implicit_app_ctx_stack.append(None)
|
|
||||||
|
|
||||||
_request_ctx_stack.push(self)
|
|
||||||
|
|
||||||
# Open the session at the moment that the request context is
|
|
||||||
# available. This allows a custom open_session method to use the
|
|
||||||
# request context (e.g. flask-sqlalchemy).
|
|
||||||
self.session = self.app.open_session(self.request)
|
|
||||||
if self.session is None:
|
|
||||||
self.session = self.app.make_null_session()
|
|
||||||
|
|
||||||
def pop(self, exc=None):
|
|
||||||
"""Pops the request context and unbinds it by doing that. This will
|
|
||||||
also trigger the execution of functions registered by the
|
|
||||||
:meth:`~flask.Flask.teardown_request` decorator.
|
|
||||||
|
|
||||||
.. versionchanged:: 0.9
|
|
||||||
Added the `exc` argument.
|
|
||||||
"""
|
|
||||||
app_ctx = self._implicit_app_ctx_stack.pop()
|
|
||||||
|
|
||||||
clear_request = False
|
|
||||||
if not self._implicit_app_ctx_stack:
|
|
||||||
self.preserved = False
|
|
||||||
if exc is None:
|
|
||||||
exc = sys.exc_info()[1]
|
|
||||||
self.app.do_teardown_request(exc)
|
|
||||||
clear_request = True
|
|
||||||
|
|
||||||
rv = _request_ctx_stack.pop()
|
|
||||||
assert rv is self, 'Popped wrong request context. (%r instead of %r)' \
|
|
||||||
% (rv, self)
|
|
||||||
|
|
||||||
# get rid of circular dependencies at the end of the request
|
|
||||||
# so that we don't require the GC to be active.
|
|
||||||
if clear_request:
|
|
||||||
rv.request.environ['werkzeug.request'] = None
|
|
||||||
|
|
||||||
# Get rid of the app as well if necessary.
|
|
||||||
if app_ctx is not None:
|
|
||||||
app_ctx.pop(exc)
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
self.push()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, tb):
|
|
||||||
# do not pop the request stack if we are in debug mode and an
|
|
||||||
# exception happened. This will allow the debugger to still
|
|
||||||
# access the request object in the interactive shell. Furthermore
|
|
||||||
# the context can be force kept alive for the test client.
|
|
||||||
# See flask.testing for how this works.
|
|
||||||
if self.request.environ.get('flask._preserve_context') or \
|
|
||||||
(tb is not None and self.app.preserve_context_on_exception):
|
|
||||||
self.preserved = True
|
|
||||||
else:
|
|
||||||
self.pop(exc_value)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s \'%s\' [%s] of %s>' % (
|
|
||||||
self.__class__.__name__,
|
|
||||||
self.request.url,
|
|
||||||
self.request.method,
|
|
||||||
self.app.name
|
|
||||||
)
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.debughelpers
|
|
||||||
~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Various helpers to make the development experience better.
|
|
||||||
|
|
||||||
:copyright: (c) 2011 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class DebugFilesKeyError(KeyError, AssertionError):
|
|
||||||
"""Raised from request.files during debugging. The idea is that it can
|
|
||||||
provide a better error message than just a generic KeyError/BadRequest.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, request, key):
|
|
||||||
form_matches = request.form.getlist(key)
|
|
||||||
buf = ['You tried to access the file "%s" in the request.files '
|
|
||||||
'dictionary but it does not exist. The mimetype for the request '
|
|
||||||
'is "%s" instead of "multipart/form-data" which means that no '
|
|
||||||
'file contents were transmitted. To fix this error you should '
|
|
||||||
'provide enctype="multipart/form-data" in your form.' %
|
|
||||||
(key, request.mimetype)]
|
|
||||||
if form_matches:
|
|
||||||
buf.append('\n\nThe browser instead transmitted some file names. '
|
|
||||||
'This was submitted: %s' % ', '.join('"%s"' % x
|
|
||||||
for x in form_matches))
|
|
||||||
self.msg = ''.join(buf).encode('utf-8')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.msg
|
|
||||||
|
|
||||||
|
|
||||||
class FormDataRoutingRedirect(AssertionError):
|
|
||||||
"""This exception is raised by Flask in debug mode if it detects a
|
|
||||||
redirect caused by the routing system when the request method is not
|
|
||||||
GET, HEAD or OPTIONS. Reasoning: form data will be dropped.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, request):
|
|
||||||
exc = request.routing_exception
|
|
||||||
buf = ['A request was sent to this URL (%s) but a redirect was '
|
|
||||||
'issued automatically by the routing system to "%s".'
|
|
||||||
% (request.url, exc.new_url)]
|
|
||||||
|
|
||||||
# In case just a slash was appended we can be extra helpful
|
|
||||||
if request.base_url + '/' == exc.new_url.split('?')[0]:
|
|
||||||
buf.append(' The URL was defined with a trailing slash so '
|
|
||||||
'Flask will automatically redirect to the URL '
|
|
||||||
'with the trailing slash if it was accessed '
|
|
||||||
'without one.')
|
|
||||||
|
|
||||||
buf.append(' Make sure to directly send your %s-request to this URL '
|
|
||||||
'since we can\'t make browsers or HTTP clients redirect '
|
|
||||||
'with form data reliably or without user interaction.' %
|
|
||||||
request.method)
|
|
||||||
buf.append('\n\nNote: this exception is only raised in debug mode')
|
|
||||||
AssertionError.__init__(self, ''.join(buf).encode('utf-8'))
|
|
||||||
|
|
||||||
|
|
||||||
def attach_enctype_error_multidict(request):
|
|
||||||
"""Since Flask 0.8 we're monkeypatching the files object in case a
|
|
||||||
request is detected that does not use multipart form data but the files
|
|
||||||
object is accessed.
|
|
||||||
"""
|
|
||||||
oldcls = request.files.__class__
|
|
||||||
class newcls(oldcls):
|
|
||||||
def __getitem__(self, key):
|
|
||||||
try:
|
|
||||||
return oldcls.__getitem__(self, key)
|
|
||||||
except KeyError, e:
|
|
||||||
if key not in request.form:
|
|
||||||
raise
|
|
||||||
raise DebugFilesKeyError(request, key)
|
|
||||||
newcls.__name__ = oldcls.__name__
|
|
||||||
newcls.__module__ = oldcls.__module__
|
|
||||||
request.files.__class__ = newcls
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.exceptions
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Flask specific additions to :class:`~werkzeug.exceptions.HTTPException`
|
|
||||||
|
|
||||||
:copyright: (c) 2011 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
from werkzeug.exceptions import HTTPException, BadRequest
|
|
||||||
from .helpers import json
|
|
||||||
|
|
||||||
|
|
||||||
class JSONHTTPException(HTTPException):
|
|
||||||
"""A base class for HTTP exceptions with ``Content-Type:
|
|
||||||
application/json``.
|
|
||||||
|
|
||||||
The ``description`` attribute of this class must set to a string (*not* an
|
|
||||||
HTML string) which describes the error.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_body(self, environ):
|
|
||||||
"""Overrides :meth:`werkzeug.exceptions.HTTPException.get_body` to
|
|
||||||
return the description of this error in JSON format instead of HTML.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return json.dumps(dict(description=self.get_description(environ)))
|
|
||||||
|
|
||||||
def get_headers(self, environ):
|
|
||||||
"""Returns a list of headers including ``Content-Type:
|
|
||||||
application/json``.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return [('Content-Type', 'application/json')]
|
|
||||||
|
|
||||||
|
|
||||||
class JSONBadRequest(JSONHTTPException, BadRequest):
|
|
||||||
"""Represents an HTTP ``400 Bad Request`` error whose body contains an
|
|
||||||
error message in JSON format instead of HTML format (as in the superclass).
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
#: The description of the error which occurred as a string.
|
|
||||||
description = (
|
|
||||||
'The browser (or proxy) sent a request that this server could not '
|
|
||||||
'understand.'
|
|
||||||
)
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.ext
|
|
||||||
~~~~~~~~~
|
|
||||||
|
|
||||||
Redirect imports for extensions. This module basically makes it possible
|
|
||||||
for us to transition from flaskext.foo to flask_foo without having to
|
|
||||||
force all extensions to upgrade at the same time.
|
|
||||||
|
|
||||||
When a user does ``from flask.ext.foo import bar`` it will attempt to
|
|
||||||
import ``from flask_foo import bar`` first and when that fails it will
|
|
||||||
try to import ``from flaskext.foo import bar``.
|
|
||||||
|
|
||||||
We're switching from namespace packages because it was just too painful for
|
|
||||||
everybody involved.
|
|
||||||
|
|
||||||
:copyright: (c) 2011 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def setup():
|
|
||||||
from ..exthook import ExtensionImporter
|
|
||||||
importer = ExtensionImporter(['flask_%s', 'flaskext.%s'], __name__)
|
|
||||||
importer.install()
|
|
||||||
|
|
||||||
|
|
||||||
setup()
|
|
||||||
del setup
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.exthook
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Redirect imports for extensions. This module basically makes it possible
|
|
||||||
for us to transition from flaskext.foo to flask_foo without having to
|
|
||||||
force all extensions to upgrade at the same time.
|
|
||||||
|
|
||||||
When a user does ``from flask.ext.foo import bar`` it will attempt to
|
|
||||||
import ``from flask_foo import bar`` first and when that fails it will
|
|
||||||
try to import ``from flaskext.foo import bar``.
|
|
||||||
|
|
||||||
We're switching from namespace packages because it was just too painful for
|
|
||||||
everybody involved.
|
|
||||||
|
|
||||||
This is used by `flask.ext`.
|
|
||||||
|
|
||||||
:copyright: (c) 2011 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
class ExtensionImporter(object):
|
|
||||||
"""This importer redirects imports from this submodule to other locations.
|
|
||||||
This makes it possible to transition from the old flaskext.name to the
|
|
||||||
newer flask_name without people having a hard time.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, module_choices, wrapper_module):
|
|
||||||
self.module_choices = module_choices
|
|
||||||
self.wrapper_module = wrapper_module
|
|
||||||
self.prefix = wrapper_module + '.'
|
|
||||||
self.prefix_cutoff = wrapper_module.count('.') + 1
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return self.__class__.__module__ == other.__class__.__module__ and \
|
|
||||||
self.__class__.__name__ == other.__class__.__name__ and \
|
|
||||||
self.wrapper_module == other.wrapper_module and \
|
|
||||||
self.module_choices == other.module_choices
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return not self.__eq__(other)
|
|
||||||
|
|
||||||
def install(self):
|
|
||||||
sys.meta_path[:] = [x for x in sys.meta_path if self != x] + [self]
|
|
||||||
|
|
||||||
def find_module(self, fullname, path=None):
|
|
||||||
if fullname.startswith(self.prefix):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def load_module(self, fullname):
|
|
||||||
if fullname in sys.modules:
|
|
||||||
return sys.modules[fullname]
|
|
||||||
modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff]
|
|
||||||
for path in self.module_choices:
|
|
||||||
realname = path % modname
|
|
||||||
try:
|
|
||||||
__import__(realname)
|
|
||||||
except ImportError:
|
|
||||||
exc_type, exc_value, tb = sys.exc_info()
|
|
||||||
# since we only establish the entry in sys.modules at the
|
|
||||||
# very this seems to be redundant, but if recursive imports
|
|
||||||
# happen we will call into the move import a second time.
|
|
||||||
# On the second invocation we still don't have an entry for
|
|
||||||
# fullname in sys.modules, but we will end up with the same
|
|
||||||
# fake module name and that import will succeed since this
|
|
||||||
# one already has a temporary entry in the modules dict.
|
|
||||||
# Since this one "succeeded" temporarily that second
|
|
||||||
# invocation now will have created a fullname entry in
|
|
||||||
# sys.modules which we have to kill.
|
|
||||||
sys.modules.pop(fullname, None)
|
|
||||||
|
|
||||||
# If it's an important traceback we reraise it, otherwise
|
|
||||||
# we swallow it and try the next choice. The skipped frame
|
|
||||||
# is the one from __import__ above which we don't care about
|
|
||||||
if self.is_important_traceback(realname, tb):
|
|
||||||
raise exc_type, exc_value, tb.tb_next
|
|
||||||
continue
|
|
||||||
module = sys.modules[fullname] = sys.modules[realname]
|
|
||||||
if '.' not in modname:
|
|
||||||
setattr(sys.modules[self.wrapper_module], modname, module)
|
|
||||||
return module
|
|
||||||
raise ImportError('No module named %s' % fullname)
|
|
||||||
|
|
||||||
def is_important_traceback(self, important_module, tb):
|
|
||||||
"""Walks a traceback's frames and checks if any of the frames
|
|
||||||
originated in the given important module. If that is the case then we
|
|
||||||
were able to import the module itself but apparently something went
|
|
||||||
wrong when the module was imported. (Eg: import of an import failed).
|
|
||||||
"""
|
|
||||||
while tb is not None:
|
|
||||||
if self.is_important_frame(important_module, tb):
|
|
||||||
return True
|
|
||||||
tb = tb.tb_next
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_important_frame(self, important_module, tb):
|
|
||||||
"""Checks a single frame if it's important."""
|
|
||||||
g = tb.tb_frame.f_globals
|
|
||||||
if '__name__' not in g:
|
|
||||||
return False
|
|
||||||
|
|
||||||
module_name = g['__name__']
|
|
||||||
|
|
||||||
# Python 2.7 Behavior. Modules are cleaned up late so the
|
|
||||||
# name shows up properly here. Success!
|
|
||||||
if module_name == important_module:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Some python verisons will will clean up modules so early that the
|
|
||||||
# module name at that point is no longer set. Try guessing from
|
|
||||||
# the filename then.
|
|
||||||
filename = os.path.abspath(tb.tb_frame.f_code.co_filename)
|
|
||||||
test_string = os.path.sep + important_module.replace('.', os.path.sep)
|
|
||||||
return test_string + '.py' in filename or \
|
|
||||||
test_string + os.path.sep + '__init__.py' in filename
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.globals
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Defines all the global objects that are proxies to the current
|
|
||||||
active context.
|
|
||||||
|
|
||||||
:copyright: (c) 2011 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from functools import partial
|
|
||||||
from werkzeug.local import LocalStack, LocalProxy
|
|
||||||
|
|
||||||
def _lookup_object(name):
|
|
||||||
top = _request_ctx_stack.top
|
|
||||||
if top is None:
|
|
||||||
raise RuntimeError('working outside of request context')
|
|
||||||
return getattr(top, name)
|
|
||||||
|
|
||||||
|
|
||||||
def _find_app():
|
|
||||||
top = _app_ctx_stack.top
|
|
||||||
if top is None:
|
|
||||||
raise RuntimeError('working outside of application context')
|
|
||||||
return top.app
|
|
||||||
|
|
||||||
|
|
||||||
# context locals
|
|
||||||
_request_ctx_stack = LocalStack()
|
|
||||||
_app_ctx_stack = LocalStack()
|
|
||||||
current_app = LocalProxy(_find_app)
|
|
||||||
request = LocalProxy(partial(_lookup_object, 'request'))
|
|
||||||
session = LocalProxy(partial(_lookup_object, 'session'))
|
|
||||||
g = LocalProxy(partial(_lookup_object, 'g'))
|
|
||||||
@@ -1,893 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.helpers
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Implements various helpers.
|
|
||||||
|
|
||||||
:copyright: (c) 2011 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import pkgutil
|
|
||||||
import posixpath
|
|
||||||
import mimetypes
|
|
||||||
from time import time
|
|
||||||
from zlib import adler32
|
|
||||||
from threading import RLock
|
|
||||||
from werkzeug.routing import BuildError
|
|
||||||
from werkzeug.urls import url_quote
|
|
||||||
from functools import update_wrapper
|
|
||||||
|
|
||||||
# try to load the best simplejson implementation available. If JSON
|
|
||||||
# is not installed, we add a failing class.
|
|
||||||
json_available = True
|
|
||||||
json = None
|
|
||||||
try:
|
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
import json
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
# Google Appengine offers simplejson via django
|
|
||||||
from django.utils import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
json_available = False
|
|
||||||
|
|
||||||
|
|
||||||
from werkzeug.datastructures import Headers
|
|
||||||
from werkzeug.exceptions import NotFound
|
|
||||||
|
|
||||||
# this was moved in 0.7
|
|
||||||
try:
|
|
||||||
from werkzeug.wsgi import wrap_file
|
|
||||||
except ImportError:
|
|
||||||
from werkzeug.utils import wrap_file
|
|
||||||
|
|
||||||
from jinja2 import FileSystemLoader
|
|
||||||
|
|
||||||
from .globals import session, _request_ctx_stack, _app_ctx_stack, \
|
|
||||||
current_app, request
|
|
||||||
|
|
||||||
|
|
||||||
def _assert_have_json():
|
|
||||||
"""Helper function that fails if JSON is unavailable."""
|
|
||||||
if not json_available:
|
|
||||||
raise RuntimeError('simplejson not installed')
|
|
||||||
|
|
||||||
|
|
||||||
# figure out if simplejson escapes slashes. This behavior was changed
|
|
||||||
# from one version to another without reason.
|
|
||||||
if not json_available or '\\/' not in json.dumps('/'):
|
|
||||||
|
|
||||||
def _tojson_filter(*args, **kwargs):
|
|
||||||
if __debug__:
|
|
||||||
_assert_have_json()
|
|
||||||
return json.dumps(*args, **kwargs).replace('/', '\\/')
|
|
||||||
else:
|
|
||||||
_tojson_filter = json.dumps
|
|
||||||
|
|
||||||
|
|
||||||
# sentinel
|
|
||||||
_missing = object()
|
|
||||||
|
|
||||||
|
|
||||||
# what separators does this operating system provide that are not a slash?
|
|
||||||
# this is used by the send_from_directory function to ensure that nobody is
|
|
||||||
# able to access files from outside the filesystem.
|
|
||||||
_os_alt_seps = list(sep for sep in [os.path.sep, os.path.altsep]
|
|
||||||
if sep not in (None, '/'))
|
|
||||||
|
|
||||||
|
|
||||||
def _endpoint_from_view_func(view_func):
|
|
||||||
"""Internal helper that returns the default endpoint for a given
|
|
||||||
function. This always is the function name.
|
|
||||||
"""
|
|
||||||
assert view_func is not None, 'expected view func if endpoint ' \
|
|
||||||
'is not provided.'
|
|
||||||
return view_func.__name__
|
|
||||||
|
|
||||||
|
|
||||||
def stream_with_context(generator_or_function):
|
|
||||||
"""Request contexts disappear when the response is started on the server.
|
|
||||||
This is done for efficiency reasons and to make it less likely to encounter
|
|
||||||
memory leaks with badly written WSGI middlewares. The downside is that if
|
|
||||||
you are using streamed responses, the generator cannot access request bound
|
|
||||||
information any more.
|
|
||||||
|
|
||||||
This function however can help you keep the context around for longer::
|
|
||||||
|
|
||||||
from flask import stream_with_context, request, Response
|
|
||||||
|
|
||||||
@app.route('/stream')
|
|
||||||
def streamed_response():
|
|
||||||
@stream_with_context
|
|
||||||
def generate():
|
|
||||||
yield 'Hello '
|
|
||||||
yield request.args['name']
|
|
||||||
yield '!'
|
|
||||||
return Response(generate())
|
|
||||||
|
|
||||||
Alternatively it can also be used around a specific generator:
|
|
||||||
|
|
||||||
from flask import stream_with_context, request, Response
|
|
||||||
|
|
||||||
@app.route('/stream')
|
|
||||||
def streamed_response():
|
|
||||||
def generate():
|
|
||||||
yield 'Hello '
|
|
||||||
yield request.args['name']
|
|
||||||
yield '!'
|
|
||||||
return Response(stream_with_context(generate()))
|
|
||||||
|
|
||||||
.. versionadded:: 0.9
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
gen = iter(generator_or_function)
|
|
||||||
except TypeError:
|
|
||||||
def decorator(*args, **kwargs):
|
|
||||||
gen = generator_or_function()
|
|
||||||
return stream_with_context(gen)
|
|
||||||
return update_wrapper(decorator, generator_or_function)
|
|
||||||
|
|
||||||
def generator():
|
|
||||||
ctx = _request_ctx_stack.top
|
|
||||||
if ctx is None:
|
|
||||||
raise RuntimeError('Attempted to stream with context but '
|
|
||||||
'there was no context in the first place to keep around.')
|
|
||||||
with ctx:
|
|
||||||
# Dummy sentinel. Has to be inside the context block or we're
|
|
||||||
# not actually keeping the context around.
|
|
||||||
yield None
|
|
||||||
|
|
||||||
# The try/finally is here so that if someone passes a WSGI level
|
|
||||||
# iterator in we're still running the cleanup logic. Generators
|
|
||||||
# don't need that because they are closed on their destruction
|
|
||||||
# automatically.
|
|
||||||
try:
|
|
||||||
for item in gen:
|
|
||||||
yield item
|
|
||||||
finally:
|
|
||||||
if hasattr(gen, 'close'):
|
|
||||||
gen.close()
|
|
||||||
|
|
||||||
# The trick is to start the generator. Then the code execution runs until
|
|
||||||
# the first dummy None is yielded at which point the context was already
|
|
||||||
# pushed. This item is discarded. Then when the iteration continues the
|
|
||||||
# real generator is executed.
|
|
||||||
wrapped_g = generator()
|
|
||||||
wrapped_g.next()
|
|
||||||
return wrapped_g
|
|
||||||
|
|
||||||
|
|
||||||
def jsonify(*args, **kwargs):
|
|
||||||
"""Creates a :class:`~flask.Response` with the JSON representation of
|
|
||||||
the given arguments with an `application/json` mimetype. The arguments
|
|
||||||
to this function are the same as to the :class:`dict` constructor.
|
|
||||||
|
|
||||||
Example usage::
|
|
||||||
|
|
||||||
@app.route('/_get_current_user')
|
|
||||||
def get_current_user():
|
|
||||||
return jsonify(username=g.user.username,
|
|
||||||
email=g.user.email,
|
|
||||||
id=g.user.id)
|
|
||||||
|
|
||||||
This will send a JSON response like this to the browser::
|
|
||||||
|
|
||||||
{
|
|
||||||
"username": "admin",
|
|
||||||
"email": "admin@localhost",
|
|
||||||
"id": 42
|
|
||||||
}
|
|
||||||
|
|
||||||
This requires Python 2.6 or an installed version of simplejson. For
|
|
||||||
security reasons only objects are supported toplevel. For more
|
|
||||||
information about this, have a look at :ref:`json-security`.
|
|
||||||
|
|
||||||
.. versionadded:: 0.2
|
|
||||||
"""
|
|
||||||
if __debug__:
|
|
||||||
_assert_have_json()
|
|
||||||
return current_app.response_class(json.dumps(dict(*args, **kwargs),
|
|
||||||
indent=None if request.is_xhr else 2), mimetype='application/json')
|
|
||||||
|
|
||||||
|
|
||||||
def make_response(*args):
|
|
||||||
"""Sometimes it is necessary to set additional headers in a view. Because
|
|
||||||
views do not have to return response objects but can return a value that
|
|
||||||
is converted into a response object by Flask itself, it becomes tricky to
|
|
||||||
add headers to it. This function can be called instead of using a return
|
|
||||||
and you will get a response object which you can use to attach headers.
|
|
||||||
|
|
||||||
If view looked like this and you want to add a new header::
|
|
||||||
|
|
||||||
def index():
|
|
||||||
return render_template('index.html', foo=42)
|
|
||||||
|
|
||||||
You can now do something like this::
|
|
||||||
|
|
||||||
def index():
|
|
||||||
response = make_response(render_template('index.html', foo=42))
|
|
||||||
response.headers['X-Parachutes'] = 'parachutes are cool'
|
|
||||||
return response
|
|
||||||
|
|
||||||
This function accepts the very same arguments you can return from a
|
|
||||||
view function. This for example creates a response with a 404 error
|
|
||||||
code::
|
|
||||||
|
|
||||||
response = make_response(render_template('not_found.html'), 404)
|
|
||||||
|
|
||||||
The other use case of this function is to force the return value of a
|
|
||||||
view function into a response which is helpful with view
|
|
||||||
decorators::
|
|
||||||
|
|
||||||
response = make_response(view_function())
|
|
||||||
response.headers['X-Parachutes'] = 'parachutes are cool'
|
|
||||||
|
|
||||||
Internally this function does the following things:
|
|
||||||
|
|
||||||
- if no arguments are passed, it creates a new response argument
|
|
||||||
- if one argument is passed, :meth:`flask.Flask.make_response`
|
|
||||||
is invoked with it.
|
|
||||||
- if more than one argument is passed, the arguments are passed
|
|
||||||
to the :meth:`flask.Flask.make_response` function as tuple.
|
|
||||||
|
|
||||||
.. versionadded:: 0.6
|
|
||||||
"""
|
|
||||||
if not args:
|
|
||||||
return current_app.response_class()
|
|
||||||
if len(args) == 1:
|
|
||||||
args = args[0]
|
|
||||||
return current_app.make_response(args)
|
|
||||||
|
|
||||||
|
|
||||||
def url_for(endpoint, **values):
|
|
||||||
"""Generates a URL to the given endpoint with the method provided.
|
|
||||||
|
|
||||||
Variable arguments that are unknown to the target endpoint are appended
|
|
||||||
to the generated URL as query arguments. If the value of a query argument
|
|
||||||
is `None`, the whole pair is skipped. In case blueprints are active
|
|
||||||
you can shortcut references to the same blueprint by prefixing the
|
|
||||||
local endpoint with a dot (``.``).
|
|
||||||
|
|
||||||
This will reference the index function local to the current blueprint::
|
|
||||||
|
|
||||||
url_for('.index')
|
|
||||||
|
|
||||||
For more information, head over to the :ref:`Quickstart <url-building>`.
|
|
||||||
|
|
||||||
To integrate applications, :class:`Flask` has a hook to intercept URL build
|
|
||||||
errors through :attr:`Flask.build_error_handler`. The `url_for` function
|
|
||||||
results in a :exc:`~werkzeug.routing.BuildError` when the current app does
|
|
||||||
not have a URL for the given endpoint and values. When it does, the
|
|
||||||
:data:`~flask.current_app` calls its :attr:`~Flask.build_error_handler` if
|
|
||||||
it is not `None`, which can return a string to use as the result of
|
|
||||||
`url_for` (instead of `url_for`'s default to raise the
|
|
||||||
:exc:`~werkzeug.routing.BuildError` exception) or re-raise the exception.
|
|
||||||
An example::
|
|
||||||
|
|
||||||
def external_url_handler(error, endpoint, **values):
|
|
||||||
"Looks up an external URL when `url_for` cannot build a URL."
|
|
||||||
# This is an example of hooking the build_error_handler.
|
|
||||||
# Here, lookup_url is some utility function you've built
|
|
||||||
# which looks up the endpoint in some external URL registry.
|
|
||||||
url = lookup_url(endpoint, **values)
|
|
||||||
if url is None:
|
|
||||||
# External lookup did not have a URL.
|
|
||||||
# Re-raise the BuildError, in context of original traceback.
|
|
||||||
exc_type, exc_value, tb = sys.exc_info()
|
|
||||||
if exc_value is error:
|
|
||||||
raise exc_type, exc_value, tb
|
|
||||||
else:
|
|
||||||
raise error
|
|
||||||
# url_for will use this result, instead of raising BuildError.
|
|
||||||
return url
|
|
||||||
|
|
||||||
app.build_error_handler = external_url_handler
|
|
||||||
|
|
||||||
Here, `error` is the instance of :exc:`~werkzeug.routing.BuildError`, and
|
|
||||||
`endpoint` and `**values` are the arguments passed into `url_for`. Note
|
|
||||||
that this is for building URLs outside the current application, and not for
|
|
||||||
handling 404 NotFound errors.
|
|
||||||
|
|
||||||
.. versionadded:: 0.9
|
|
||||||
The `_anchor` and `_method` parameters were added.
|
|
||||||
|
|
||||||
.. versionadded:: 0.9
|
|
||||||
Calls :meth:`Flask.handle_build_error` on
|
|
||||||
:exc:`~werkzeug.routing.BuildError`.
|
|
||||||
|
|
||||||
:param endpoint: the endpoint of the URL (name of the function)
|
|
||||||
:param values: the variable arguments of the URL rule
|
|
||||||
:param _external: if set to `True`, an absolute URL is generated.
|
|
||||||
:param _anchor: if provided this is added as anchor to the URL.
|
|
||||||
:param _method: if provided this explicitly specifies an HTTP method.
|
|
||||||
"""
|
|
||||||
appctx = _app_ctx_stack.top
|
|
||||||
reqctx = _request_ctx_stack.top
|
|
||||||
if appctx is None:
|
|
||||||
raise RuntimeError('Attempted to generate a URL with the application '
|
|
||||||
'context being pushed. This has to be executed ')
|
|
||||||
|
|
||||||
# If request specific information is available we have some extra
|
|
||||||
# features that support "relative" urls.
|
|
||||||
if reqctx is not None:
|
|
||||||
url_adapter = reqctx.url_adapter
|
|
||||||
blueprint_name = request.blueprint
|
|
||||||
if not reqctx.request._is_old_module:
|
|
||||||
if endpoint[:1] == '.':
|
|
||||||
if blueprint_name is not None:
|
|
||||||
endpoint = blueprint_name + endpoint
|
|
||||||
else:
|
|
||||||
endpoint = endpoint[1:]
|
|
||||||
else:
|
|
||||||
# TODO: get rid of this deprecated functionality in 1.0
|
|
||||||
if '.' not in endpoint:
|
|
||||||
if blueprint_name is not None:
|
|
||||||
endpoint = blueprint_name + '.' + endpoint
|
|
||||||
elif endpoint.startswith('.'):
|
|
||||||
endpoint = endpoint[1:]
|
|
||||||
external = values.pop('_external', False)
|
|
||||||
|
|
||||||
# Otherwise go with the url adapter from the appctx and make
|
|
||||||
# the urls external by default.
|
|
||||||
else:
|
|
||||||
url_adapter = appctx.url_adapter
|
|
||||||
if url_adapter is None:
|
|
||||||
raise RuntimeError('Application was not able to create a URL '
|
|
||||||
'adapter for request independent URL generation. '
|
|
||||||
'You might be able to fix this by setting '
|
|
||||||
'the SERVER_NAME config variable.')
|
|
||||||
external = values.pop('_external', True)
|
|
||||||
|
|
||||||
anchor = values.pop('_anchor', None)
|
|
||||||
method = values.pop('_method', None)
|
|
||||||
appctx.app.inject_url_defaults(endpoint, values)
|
|
||||||
try:
|
|
||||||
rv = url_adapter.build(endpoint, values, method=method,
|
|
||||||
force_external=external)
|
|
||||||
except BuildError, error:
|
|
||||||
# We need to inject the values again so that the app callback can
|
|
||||||
# deal with that sort of stuff.
|
|
||||||
values['_external'] = external
|
|
||||||
values['_anchor'] = anchor
|
|
||||||
values['_method'] = method
|
|
||||||
return appctx.app.handle_url_build_error(error, endpoint, values)
|
|
||||||
|
|
||||||
rv = url_adapter.build(endpoint, values, method=method,
|
|
||||||
force_external=external)
|
|
||||||
if anchor is not None:
|
|
||||||
rv += '#' + url_quote(anchor)
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
def get_template_attribute(template_name, attribute):
|
|
||||||
"""Loads a macro (or variable) a template exports. This can be used to
|
|
||||||
invoke a macro from within Python code. If you for example have a
|
|
||||||
template named `_cider.html` with the following contents:
|
|
||||||
|
|
||||||
.. sourcecode:: html+jinja
|
|
||||||
|
|
||||||
{% macro hello(name) %}Hello {{ name }}!{% endmacro %}
|
|
||||||
|
|
||||||
You can access this from Python code like this::
|
|
||||||
|
|
||||||
hello = get_template_attribute('_cider.html', 'hello')
|
|
||||||
return hello('World')
|
|
||||||
|
|
||||||
.. versionadded:: 0.2
|
|
||||||
|
|
||||||
:param template_name: the name of the template
|
|
||||||
:param attribute: the name of the variable of macro to acccess
|
|
||||||
"""
|
|
||||||
return getattr(current_app.jinja_env.get_template(template_name).module,
|
|
||||||
attribute)
|
|
||||||
|
|
||||||
|
|
||||||
def flash(message, category='message'):
|
|
||||||
"""Flashes a message to the next request. In order to remove the
|
|
||||||
flashed message from the session and to display it to the user,
|
|
||||||
the template has to call :func:`get_flashed_messages`.
|
|
||||||
|
|
||||||
.. versionchanged:: 0.3
|
|
||||||
`category` parameter added.
|
|
||||||
|
|
||||||
:param message: the message to be flashed.
|
|
||||||
:param category: the category for the message. The following values
|
|
||||||
are recommended: ``'message'`` for any kind of message,
|
|
||||||
``'error'`` for errors, ``'info'`` for information
|
|
||||||
messages and ``'warning'`` for warnings. However any
|
|
||||||
kind of string can be used as category.
|
|
||||||
"""
|
|
||||||
# Original implementation:
|
|
||||||
#
|
|
||||||
# session.setdefault('_flashes', []).append((category, message))
|
|
||||||
#
|
|
||||||
# This assumed that changes made to mutable structures in the session are
|
|
||||||
# are always in sync with the sess on object, which is not true for session
|
|
||||||
# implementations that use external storage for keeping their keys/values.
|
|
||||||
flashes = session.get('_flashes', [])
|
|
||||||
flashes.append((category, message))
|
|
||||||
session['_flashes'] = flashes
|
|
||||||
|
|
||||||
|
|
||||||
def get_flashed_messages(with_categories=False, category_filter=[]):
|
|
||||||
"""Pulls all flashed messages from the session and returns them.
|
|
||||||
Further calls in the same request to the function will return
|
|
||||||
the same messages. By default just the messages are returned,
|
|
||||||
but when `with_categories` is set to `True`, the return value will
|
|
||||||
be a list of tuples in the form ``(category, message)`` instead.
|
|
||||||
|
|
||||||
Filter the flashed messages to one or more categories by providing those
|
|
||||||
categories in `category_filter`. This allows rendering categories in
|
|
||||||
separate html blocks. The `with_categories` and `category_filter`
|
|
||||||
arguments are distinct:
|
|
||||||
|
|
||||||
* `with_categories` controls whether categories are returned with message
|
|
||||||
text (`True` gives a tuple, where `False` gives just the message text).
|
|
||||||
* `category_filter` filters the messages down to only those matching the
|
|
||||||
provided categories.
|
|
||||||
|
|
||||||
See :ref:`message-flashing-pattern` for examples.
|
|
||||||
|
|
||||||
.. versionchanged:: 0.3
|
|
||||||
`with_categories` parameter added.
|
|
||||||
|
|
||||||
.. versionchanged:: 0.9
|
|
||||||
`category_filter` parameter added.
|
|
||||||
|
|
||||||
:param with_categories: set to `True` to also receive categories.
|
|
||||||
:param category_filter: whitelist of categories to limit return values
|
|
||||||
"""
|
|
||||||
flashes = _request_ctx_stack.top.flashes
|
|
||||||
if flashes is None:
|
|
||||||
_request_ctx_stack.top.flashes = flashes = session.pop('_flashes') \
|
|
||||||
if '_flashes' in session else []
|
|
||||||
if category_filter:
|
|
||||||
flashes = filter(lambda f: f[0] in category_filter, flashes)
|
|
||||||
if not with_categories:
|
|
||||||
return [x[1] for x in flashes]
|
|
||||||
return flashes
|
|
||||||
|
|
||||||
|
|
||||||
def send_file(filename_or_fp, mimetype=None, as_attachment=False,
|
|
||||||
attachment_filename=None, add_etags=True,
|
|
||||||
cache_timeout=None, conditional=False):
|
|
||||||
"""Sends the contents of a file to the client. This will use the
|
|
||||||
most efficient method available and configured. By default it will
|
|
||||||
try to use the WSGI server's file_wrapper support. Alternatively
|
|
||||||
you can set the application's :attr:`~Flask.use_x_sendfile` attribute
|
|
||||||
to ``True`` to directly emit an `X-Sendfile` header. This however
|
|
||||||
requires support of the underlying webserver for `X-Sendfile`.
|
|
||||||
|
|
||||||
By default it will try to guess the mimetype for you, but you can
|
|
||||||
also explicitly provide one. For extra security you probably want
|
|
||||||
to send certain files as attachment (HTML for instance). The mimetype
|
|
||||||
guessing requires a `filename` or an `attachment_filename` to be
|
|
||||||
provided.
|
|
||||||
|
|
||||||
Please never pass filenames to this function from user sources without
|
|
||||||
checking them first. Something like this is usually sufficient to
|
|
||||||
avoid security problems::
|
|
||||||
|
|
||||||
if '..' in filename or filename.startswith('/'):
|
|
||||||
abort(404)
|
|
||||||
|
|
||||||
.. versionadded:: 0.2
|
|
||||||
|
|
||||||
.. versionadded:: 0.5
|
|
||||||
The `add_etags`, `cache_timeout` and `conditional` parameters were
|
|
||||||
added. The default behavior is now to attach etags.
|
|
||||||
|
|
||||||
.. versionchanged:: 0.7
|
|
||||||
mimetype guessing and etag support for file objects was
|
|
||||||
deprecated because it was unreliable. Pass a filename if you are
|
|
||||||
able to, otherwise attach an etag yourself. This functionality
|
|
||||||
will be removed in Flask 1.0
|
|
||||||
|
|
||||||
.. versionchanged:: 0.9
|
|
||||||
cache_timeout pulls its default from application config, when None.
|
|
||||||
|
|
||||||
:param filename_or_fp: the filename of the file to send. This is
|
|
||||||
relative to the :attr:`~Flask.root_path` if a
|
|
||||||
relative path is specified.
|
|
||||||
Alternatively a file object might be provided
|
|
||||||
in which case `X-Sendfile` might not work and
|
|
||||||
fall back to the traditional method. Make sure
|
|
||||||
that the file pointer is positioned at the start
|
|
||||||
of data to send before calling :func:`send_file`.
|
|
||||||
:param mimetype: the mimetype of the file if provided, otherwise
|
|
||||||
auto detection happens.
|
|
||||||
:param as_attachment: set to `True` if you want to send this file with
|
|
||||||
a ``Content-Disposition: attachment`` header.
|
|
||||||
:param attachment_filename: the filename for the attachment if it
|
|
||||||
differs from the file's filename.
|
|
||||||
:param add_etags: set to `False` to disable attaching of etags.
|
|
||||||
:param conditional: set to `True` to enable conditional responses.
|
|
||||||
|
|
||||||
:param cache_timeout: the timeout in seconds for the headers. When `None`
|
|
||||||
(default), this value is set by
|
|
||||||
:meth:`~Flask.get_send_file_max_age` of
|
|
||||||
:data:`~flask.current_app`.
|
|
||||||
"""
|
|
||||||
mtime = None
|
|
||||||
if isinstance(filename_or_fp, basestring):
|
|
||||||
filename = filename_or_fp
|
|
||||||
file = None
|
|
||||||
else:
|
|
||||||
from warnings import warn
|
|
||||||
file = filename_or_fp
|
|
||||||
filename = getattr(file, 'name', None)
|
|
||||||
|
|
||||||
# XXX: this behavior is now deprecated because it was unreliable.
|
|
||||||
# removed in Flask 1.0
|
|
||||||
if not attachment_filename and not mimetype \
|
|
||||||
and isinstance(filename, basestring):
|
|
||||||
warn(DeprecationWarning('The filename support for file objects '
|
|
||||||
'passed to send_file is now deprecated. Pass an '
|
|
||||||
'attach_filename if you want mimetypes to be guessed.'),
|
|
||||||
stacklevel=2)
|
|
||||||
if add_etags:
|
|
||||||
warn(DeprecationWarning('In future flask releases etags will no '
|
|
||||||
'longer be generated for file objects passed to the send_file '
|
|
||||||
'function because this behavior was unreliable. Pass '
|
|
||||||
'filenames instead if possible, otherwise attach an etag '
|
|
||||||
'yourself based on another value'), stacklevel=2)
|
|
||||||
|
|
||||||
if filename is not None:
|
|
||||||
if not os.path.isabs(filename):
|
|
||||||
filename = os.path.join(current_app.root_path, filename)
|
|
||||||
if mimetype is None and (filename or attachment_filename):
|
|
||||||
mimetype = mimetypes.guess_type(filename or attachment_filename)[0]
|
|
||||||
if mimetype is None:
|
|
||||||
mimetype = 'application/octet-stream'
|
|
||||||
|
|
||||||
headers = Headers()
|
|
||||||
if as_attachment:
|
|
||||||
if attachment_filename is None:
|
|
||||||
if filename is None:
|
|
||||||
raise TypeError('filename unavailable, required for '
|
|
||||||
'sending as attachment')
|
|
||||||
attachment_filename = os.path.basename(filename)
|
|
||||||
headers.add('Content-Disposition', 'attachment',
|
|
||||||
filename=attachment_filename)
|
|
||||||
|
|
||||||
if current_app.use_x_sendfile and filename:
|
|
||||||
if file is not None:
|
|
||||||
file.close()
|
|
||||||
headers['X-Sendfile'] = filename
|
|
||||||
data = None
|
|
||||||
else:
|
|
||||||
if file is None:
|
|
||||||
file = open(filename, 'rb')
|
|
||||||
mtime = os.path.getmtime(filename)
|
|
||||||
data = wrap_file(request.environ, file)
|
|
||||||
|
|
||||||
rv = current_app.response_class(data, mimetype=mimetype, headers=headers,
|
|
||||||
direct_passthrough=True)
|
|
||||||
|
|
||||||
# if we know the file modification date, we can store it as the
|
|
||||||
# the time of the last modification.
|
|
||||||
if mtime is not None:
|
|
||||||
rv.last_modified = int(mtime)
|
|
||||||
|
|
||||||
rv.cache_control.public = True
|
|
||||||
if cache_timeout is None:
|
|
||||||
cache_timeout = current_app.get_send_file_max_age(filename)
|
|
||||||
if cache_timeout is not None:
|
|
||||||
rv.cache_control.max_age = cache_timeout
|
|
||||||
rv.expires = int(time() + cache_timeout)
|
|
||||||
|
|
||||||
if add_etags and filename is not None:
|
|
||||||
rv.set_etag('flask-%s-%s-%s' % (
|
|
||||||
os.path.getmtime(filename),
|
|
||||||
os.path.getsize(filename),
|
|
||||||
adler32(
|
|
||||||
filename.encode('utf8') if isinstance(filename, unicode)
|
|
||||||
else filename
|
|
||||||
) & 0xffffffff
|
|
||||||
))
|
|
||||||
if conditional:
|
|
||||||
rv = rv.make_conditional(request)
|
|
||||||
# make sure we don't send x-sendfile for servers that
|
|
||||||
# ignore the 304 status code for x-sendfile.
|
|
||||||
if rv.status_code == 304:
|
|
||||||
rv.headers.pop('x-sendfile', None)
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
def safe_join(directory, filename):
|
|
||||||
"""Safely join `directory` and `filename`.
|
|
||||||
|
|
||||||
Example usage::
|
|
||||||
|
|
||||||
@app.route('/wiki/<path:filename>')
|
|
||||||
def wiki_page(filename):
|
|
||||||
filename = safe_join(app.config['WIKI_FOLDER'], filename)
|
|
||||||
with open(filename, 'rb') as fd:
|
|
||||||
content = fd.read() # Read and process the file content...
|
|
||||||
|
|
||||||
:param directory: the base directory.
|
|
||||||
:param filename: the untrusted filename relative to that directory.
|
|
||||||
:raises: :class:`~werkzeug.exceptions.NotFound` if the resulting path
|
|
||||||
would fall out of `directory`.
|
|
||||||
"""
|
|
||||||
filename = posixpath.normpath(filename)
|
|
||||||
for sep in _os_alt_seps:
|
|
||||||
if sep in filename:
|
|
||||||
raise NotFound()
|
|
||||||
if os.path.isabs(filename) or filename.startswith('../'):
|
|
||||||
raise NotFound()
|
|
||||||
return os.path.join(directory, filename)
|
|
||||||
|
|
||||||
|
|
||||||
def send_from_directory(directory, filename, **options):
|
|
||||||
"""Send a file from a given directory with :func:`send_file`. This
|
|
||||||
is a secure way to quickly expose static files from an upload folder
|
|
||||||
or something similar.
|
|
||||||
|
|
||||||
Example usage::
|
|
||||||
|
|
||||||
@app.route('/uploads/<path:filename>')
|
|
||||||
def download_file(filename):
|
|
||||||
return send_from_directory(app.config['UPLOAD_FOLDER'],
|
|
||||||
filename, as_attachment=True)
|
|
||||||
|
|
||||||
.. admonition:: Sending files and Performance
|
|
||||||
|
|
||||||
It is strongly recommended to activate either `X-Sendfile` support in
|
|
||||||
your webserver or (if no authentication happens) to tell the webserver
|
|
||||||
to serve files for the given path on its own without calling into the
|
|
||||||
web application for improved performance.
|
|
||||||
|
|
||||||
.. versionadded:: 0.5
|
|
||||||
|
|
||||||
:param directory: the directory where all the files are stored.
|
|
||||||
:param filename: the filename relative to that directory to
|
|
||||||
download.
|
|
||||||
:param options: optional keyword arguments that are directly
|
|
||||||
forwarded to :func:`send_file`.
|
|
||||||
"""
|
|
||||||
filename = safe_join(directory, filename)
|
|
||||||
if not os.path.isfile(filename):
|
|
||||||
raise NotFound()
|
|
||||||
options.setdefault('conditional', True)
|
|
||||||
return send_file(filename, **options)
|
|
||||||
|
|
||||||
|
|
||||||
def get_root_path(import_name):
|
|
||||||
"""Returns the path to a package or cwd if that cannot be found. This
|
|
||||||
returns the path of a package or the folder that contains a module.
|
|
||||||
|
|
||||||
Not to be confused with the package path returned by :func:`find_package`.
|
|
||||||
"""
|
|
||||||
# Module already imported and has a file attribute. Use that first.
|
|
||||||
mod = sys.modules.get(import_name)
|
|
||||||
if mod is not None and hasattr(mod, '__file__'):
|
|
||||||
return os.path.dirname(os.path.abspath(mod.__file__))
|
|
||||||
|
|
||||||
# Next attempt: check the loader.
|
|
||||||
loader = pkgutil.get_loader(import_name)
|
|
||||||
|
|
||||||
# Loader does not exist or we're referring to an unloaded main module
|
|
||||||
# or a main module without path (interactive sessions), go with the
|
|
||||||
# current working directory.
|
|
||||||
if loader is None or import_name == '__main__':
|
|
||||||
return os.getcwd()
|
|
||||||
|
|
||||||
# For .egg, zipimporter does not have get_filename until Python 2.7.
|
|
||||||
# Some other loaders might exhibit the same behavior.
|
|
||||||
if hasattr(loader, 'get_filename'):
|
|
||||||
filepath = loader.get_filename(import_name)
|
|
||||||
else:
|
|
||||||
# Fall back to imports.
|
|
||||||
__import__(import_name)
|
|
||||||
filepath = sys.modules[import_name].__file__
|
|
||||||
|
|
||||||
# filepath is import_name.py for a module, or __init__.py for a package.
|
|
||||||
return os.path.dirname(os.path.abspath(filepath))
|
|
||||||
|
|
||||||
|
|
||||||
def find_package(import_name):
|
|
||||||
"""Finds a package and returns the prefix (or None if the package is
|
|
||||||
not installed) as well as the folder that contains the package or
|
|
||||||
module as a tuple. The package path returned is the module that would
|
|
||||||
have to be added to the pythonpath in order to make it possible to
|
|
||||||
import the module. The prefix is the path below which a UNIX like
|
|
||||||
folder structure exists (lib, share etc.).
|
|
||||||
"""
|
|
||||||
root_mod_name = import_name.split('.')[0]
|
|
||||||
loader = pkgutil.get_loader(root_mod_name)
|
|
||||||
if loader is None or import_name == '__main__':
|
|
||||||
# import name is not found, or interactive/main module
|
|
||||||
package_path = os.getcwd()
|
|
||||||
else:
|
|
||||||
# For .egg, zipimporter does not have get_filename until Python 2.7.
|
|
||||||
if hasattr(loader, 'get_filename'):
|
|
||||||
filename = loader.get_filename(root_mod_name)
|
|
||||||
elif hasattr(loader, 'archive'):
|
|
||||||
# zipimporter's loader.archive points to the .egg or .zip
|
|
||||||
# archive filename is dropped in call to dirname below.
|
|
||||||
filename = loader.archive
|
|
||||||
else:
|
|
||||||
# At least one loader is missing both get_filename and archive:
|
|
||||||
# Google App Engine's HardenedModulesHook
|
|
||||||
#
|
|
||||||
# Fall back to imports.
|
|
||||||
__import__(import_name)
|
|
||||||
filename = sys.modules[import_name].__file__
|
|
||||||
package_path = os.path.abspath(os.path.dirname(filename))
|
|
||||||
# package_path ends with __init__.py for a package
|
|
||||||
if loader.is_package(root_mod_name):
|
|
||||||
package_path = os.path.dirname(package_path)
|
|
||||||
|
|
||||||
site_parent, site_folder = os.path.split(package_path)
|
|
||||||
py_prefix = os.path.abspath(sys.prefix)
|
|
||||||
if package_path.startswith(py_prefix):
|
|
||||||
return py_prefix, package_path
|
|
||||||
elif site_folder.lower() == 'site-packages':
|
|
||||||
parent, folder = os.path.split(site_parent)
|
|
||||||
# Windows like installations
|
|
||||||
if folder.lower() == 'lib':
|
|
||||||
base_dir = parent
|
|
||||||
# UNIX like installations
|
|
||||||
elif os.path.basename(parent).lower() == 'lib':
|
|
||||||
base_dir = os.path.dirname(parent)
|
|
||||||
else:
|
|
||||||
base_dir = site_parent
|
|
||||||
return base_dir, package_path
|
|
||||||
return None, package_path
|
|
||||||
|
|
||||||
|
|
||||||
class locked_cached_property(object):
|
|
||||||
"""A decorator that converts a function into a lazy property. The
|
|
||||||
function wrapped is called the first time to retrieve the result
|
|
||||||
and then that calculated result is used the next time you access
|
|
||||||
the value. Works like the one in Werkzeug but has a lock for
|
|
||||||
thread safety.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, func, name=None, doc=None):
|
|
||||||
self.__name__ = name or func.__name__
|
|
||||||
self.__module__ = func.__module__
|
|
||||||
self.__doc__ = doc or func.__doc__
|
|
||||||
self.func = func
|
|
||||||
self.lock = RLock()
|
|
||||||
|
|
||||||
def __get__(self, obj, type=None):
|
|
||||||
if obj is None:
|
|
||||||
return self
|
|
||||||
with self.lock:
|
|
||||||
value = obj.__dict__.get(self.__name__, _missing)
|
|
||||||
if value is _missing:
|
|
||||||
value = self.func(obj)
|
|
||||||
obj.__dict__[self.__name__] = value
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
class _PackageBoundObject(object):
|
|
||||||
|
|
||||||
def __init__(self, import_name, template_folder=None):
|
|
||||||
#: The name of the package or module. Do not change this once
|
|
||||||
#: it was set by the constructor.
|
|
||||||
self.import_name = import_name
|
|
||||||
|
|
||||||
#: location of the templates. `None` if templates should not be
|
|
||||||
#: exposed.
|
|
||||||
self.template_folder = template_folder
|
|
||||||
|
|
||||||
#: Where is the app root located?
|
|
||||||
self.root_path = get_root_path(self.import_name)
|
|
||||||
|
|
||||||
self._static_folder = None
|
|
||||||
self._static_url_path = None
|
|
||||||
|
|
||||||
def _get_static_folder(self):
|
|
||||||
if self._static_folder is not None:
|
|
||||||
return os.path.join(self.root_path, self._static_folder)
|
|
||||||
def _set_static_folder(self, value):
|
|
||||||
self._static_folder = value
|
|
||||||
static_folder = property(_get_static_folder, _set_static_folder)
|
|
||||||
del _get_static_folder, _set_static_folder
|
|
||||||
|
|
||||||
def _get_static_url_path(self):
|
|
||||||
if self._static_url_path is None:
|
|
||||||
if self.static_folder is None:
|
|
||||||
return None
|
|
||||||
return '/' + os.path.basename(self.static_folder)
|
|
||||||
return self._static_url_path
|
|
||||||
def _set_static_url_path(self, value):
|
|
||||||
self._static_url_path = value
|
|
||||||
static_url_path = property(_get_static_url_path, _set_static_url_path)
|
|
||||||
del _get_static_url_path, _set_static_url_path
|
|
||||||
|
|
||||||
@property
|
|
||||||
def has_static_folder(self):
|
|
||||||
"""This is `True` if the package bound object's container has a
|
|
||||||
folder named ``'static'``.
|
|
||||||
|
|
||||||
.. versionadded:: 0.5
|
|
||||||
"""
|
|
||||||
return self.static_folder is not None
|
|
||||||
|
|
||||||
@locked_cached_property
|
|
||||||
def jinja_loader(self):
|
|
||||||
"""The Jinja loader for this package bound object.
|
|
||||||
|
|
||||||
.. versionadded:: 0.5
|
|
||||||
"""
|
|
||||||
if self.template_folder is not None:
|
|
||||||
return FileSystemLoader(os.path.join(self.root_path,
|
|
||||||
self.template_folder))
|
|
||||||
|
|
||||||
def get_send_file_max_age(self, filename):
|
|
||||||
"""Provides default cache_timeout for the :func:`send_file` functions.
|
|
||||||
|
|
||||||
By default, this function returns ``SEND_FILE_MAX_AGE_DEFAULT`` from
|
|
||||||
the configuration of :data:`~flask.current_app`.
|
|
||||||
|
|
||||||
Static file functions such as :func:`send_from_directory` use this
|
|
||||||
function, and :func:`send_file` calls this function on
|
|
||||||
:data:`~flask.current_app` when the given cache_timeout is `None`. If a
|
|
||||||
cache_timeout is given in :func:`send_file`, that timeout is used;
|
|
||||||
otherwise, this method is called.
|
|
||||||
|
|
||||||
This allows subclasses to change the behavior when sending files based
|
|
||||||
on the filename. For example, to set the cache timeout for .js files
|
|
||||||
to 60 seconds::
|
|
||||||
|
|
||||||
class MyFlask(flask.Flask):
|
|
||||||
def get_send_file_max_age(self, name):
|
|
||||||
if name.lower().endswith('.js'):
|
|
||||||
return 60
|
|
||||||
return flask.Flask.get_send_file_max_age(self, name)
|
|
||||||
|
|
||||||
.. versionadded:: 0.9
|
|
||||||
"""
|
|
||||||
return current_app.config['SEND_FILE_MAX_AGE_DEFAULT']
|
|
||||||
|
|
||||||
def send_static_file(self, filename):
|
|
||||||
"""Function used internally to send static files from the static
|
|
||||||
folder to the browser.
|
|
||||||
|
|
||||||
.. versionadded:: 0.5
|
|
||||||
"""
|
|
||||||
if not self.has_static_folder:
|
|
||||||
raise RuntimeError('No static folder for this object')
|
|
||||||
# Ensure get_send_file_max_age is called in all cases.
|
|
||||||
# Here, we ensure get_send_file_max_age is called for Blueprints.
|
|
||||||
cache_timeout = self.get_send_file_max_age(filename)
|
|
||||||
return send_from_directory(self.static_folder, filename,
|
|
||||||
cache_timeout=cache_timeout)
|
|
||||||
|
|
||||||
def open_resource(self, resource, mode='rb'):
|
|
||||||
"""Opens a resource from the application's resource folder. To see
|
|
||||||
how this works, consider the following folder structure::
|
|
||||||
|
|
||||||
/myapplication.py
|
|
||||||
/schema.sql
|
|
||||||
/static
|
|
||||||
/style.css
|
|
||||||
/templates
|
|
||||||
/layout.html
|
|
||||||
/index.html
|
|
||||||
|
|
||||||
If you want to open the `schema.sql` file you would do the
|
|
||||||
following::
|
|
||||||
|
|
||||||
with app.open_resource('schema.sql') as f:
|
|
||||||
contents = f.read()
|
|
||||||
do_something_with(contents)
|
|
||||||
|
|
||||||
:param resource: the name of the resource. To access resources within
|
|
||||||
subfolders use forward slashes as separator.
|
|
||||||
"""
|
|
||||||
if mode not in ('r', 'rb'):
|
|
||||||
raise ValueError('Resources can only be opened for reading')
|
|
||||||
return open(os.path.join(self.root_path, resource), mode)
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.logging
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Implements the logging support for Flask.
|
|
||||||
|
|
||||||
:copyright: (c) 2011 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
from logging import getLogger, StreamHandler, Formatter, getLoggerClass, DEBUG
|
|
||||||
|
|
||||||
|
|
||||||
def create_logger(app):
|
|
||||||
"""Creates a logger for the given application. This logger works
|
|
||||||
similar to a regular Python logger but changes the effective logging
|
|
||||||
level based on the application's debug flag. Furthermore this
|
|
||||||
function also removes all attached handlers in case there was a
|
|
||||||
logger with the log name before.
|
|
||||||
"""
|
|
||||||
Logger = getLoggerClass()
|
|
||||||
|
|
||||||
class DebugLogger(Logger):
|
|
||||||
def getEffectiveLevel(x):
|
|
||||||
if x.level == 0 and app.debug:
|
|
||||||
return DEBUG
|
|
||||||
return Logger.getEffectiveLevel(x)
|
|
||||||
|
|
||||||
class DebugHandler(StreamHandler):
|
|
||||||
def emit(x, record):
|
|
||||||
StreamHandler.emit(x, record) if app.debug else None
|
|
||||||
|
|
||||||
handler = DebugHandler()
|
|
||||||
handler.setLevel(DEBUG)
|
|
||||||
handler.setFormatter(Formatter(app.debug_log_format))
|
|
||||||
logger = getLogger(app.logger_name)
|
|
||||||
# just in case that was not a new logger, get rid of all the handlers
|
|
||||||
# already attached to it.
|
|
||||||
del logger.handlers[:]
|
|
||||||
logger.__class__ = DebugLogger
|
|
||||||
logger.addHandler(handler)
|
|
||||||
return logger
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.module
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Implements a class that represents module blueprints.
|
|
||||||
|
|
||||||
:copyright: (c) 2011 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
from .blueprints import Blueprint
|
|
||||||
|
|
||||||
|
|
||||||
def blueprint_is_module(bp):
|
|
||||||
"""Used to figure out if something is actually a module"""
|
|
||||||
return isinstance(bp, Module)
|
|
||||||
|
|
||||||
|
|
||||||
class Module(Blueprint):
|
|
||||||
"""Deprecated module support. Until Flask 0.6 modules were a different
|
|
||||||
name of the concept now available as blueprints in Flask. They are
|
|
||||||
essentially doing the same but have some bad semantics for templates and
|
|
||||||
static files that were fixed with blueprints.
|
|
||||||
|
|
||||||
.. versionchanged:: 0.7
|
|
||||||
Modules were deprecated in favor for blueprints.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, import_name, name=None, url_prefix=None,
|
|
||||||
static_path=None, subdomain=None):
|
|
||||||
if name is None:
|
|
||||||
assert '.' in import_name, 'name required if package name ' \
|
|
||||||
'does not point to a submodule'
|
|
||||||
name = import_name.rsplit('.', 1)[1]
|
|
||||||
Blueprint.__init__(self, name, import_name, url_prefix=url_prefix,
|
|
||||||
subdomain=subdomain, template_folder='templates')
|
|
||||||
|
|
||||||
if os.path.isdir(os.path.join(self.root_path, 'static')):
|
|
||||||
self._static_folder = 'static'
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.session
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module used to flask with the session global so we moved it
|
|
||||||
over to flask.sessions
|
|
||||||
|
|
||||||
:copyright: (c) 2011 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from warnings import warn
|
|
||||||
warn(DeprecationWarning('please use flask.sessions instead'))
|
|
||||||
|
|
||||||
from .sessions import SecureCookieSession, NullSession
|
|
||||||
|
|
||||||
Session = SecureCookieSession
|
|
||||||
_NullSession = NullSession
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.sessions
|
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Implements cookie based sessions based on Werkzeug's secure cookie
|
|
||||||
system.
|
|
||||||
|
|
||||||
:copyright: (c) 2011 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
from werkzeug.contrib.securecookie import SecureCookie
|
|
||||||
|
|
||||||
|
|
||||||
class SessionMixin(object):
|
|
||||||
"""Expands a basic dictionary with an accessors that are expected
|
|
||||||
by Flask extensions and users for the session.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _get_permanent(self):
|
|
||||||
return self.get('_permanent', False)
|
|
||||||
|
|
||||||
def _set_permanent(self, value):
|
|
||||||
self['_permanent'] = bool(value)
|
|
||||||
|
|
||||||
#: this reflects the ``'_permanent'`` key in the dict.
|
|
||||||
permanent = property(_get_permanent, _set_permanent)
|
|
||||||
del _get_permanent, _set_permanent
|
|
||||||
|
|
||||||
#: some session backends can tell you if a session is new, but that is
|
|
||||||
#: not necessarily guaranteed. Use with caution. The default mixin
|
|
||||||
#: implementation just hardcodes `False` in.
|
|
||||||
new = False
|
|
||||||
|
|
||||||
#: for some backends this will always be `True`, but some backends will
|
|
||||||
#: default this to false and detect changes in the dictionary for as
|
|
||||||
#: long as changes do not happen on mutable structures in the session.
|
|
||||||
#: The default mixin implementation just hardcodes `True` in.
|
|
||||||
modified = True
|
|
||||||
|
|
||||||
|
|
||||||
class SecureCookieSession(SecureCookie, SessionMixin):
|
|
||||||
"""Expands the session with support for switching between permanent
|
|
||||||
and non-permanent sessions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class NullSession(SecureCookieSession):
|
|
||||||
"""Class used to generate nicer error messages if sessions are not
|
|
||||||
available. Will still allow read-only access to the empty session
|
|
||||||
but fail on setting.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _fail(self, *args, **kwargs):
|
|
||||||
raise RuntimeError('the session is unavailable because no secret '
|
|
||||||
'key was set. Set the secret_key on the '
|
|
||||||
'application to something unique and secret.')
|
|
||||||
__setitem__ = __delitem__ = clear = pop = popitem = \
|
|
||||||
update = setdefault = _fail
|
|
||||||
del _fail
|
|
||||||
|
|
||||||
|
|
||||||
class SessionInterface(object):
|
|
||||||
"""The basic interface you have to implement in order to replace the
|
|
||||||
default session interface which uses werkzeug's securecookie
|
|
||||||
implementation. The only methods you have to implement are
|
|
||||||
:meth:`open_session` and :meth:`save_session`, the others have
|
|
||||||
useful defaults which you don't need to change.
|
|
||||||
|
|
||||||
The session object returned by the :meth:`open_session` method has to
|
|
||||||
provide a dictionary like interface plus the properties and methods
|
|
||||||
from the :class:`SessionMixin`. We recommend just subclassing a dict
|
|
||||||
and adding that mixin::
|
|
||||||
|
|
||||||
class Session(dict, SessionMixin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
If :meth:`open_session` returns `None` Flask will call into
|
|
||||||
:meth:`make_null_session` to create a session that acts as replacement
|
|
||||||
if the session support cannot work because some requirement is not
|
|
||||||
fulfilled. The default :class:`NullSession` class that is created
|
|
||||||
will complain that the secret key was not set.
|
|
||||||
|
|
||||||
To replace the session interface on an application all you have to do
|
|
||||||
is to assign :attr:`flask.Flask.session_interface`::
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
|
||||||
app.session_interface = MySessionInterface()
|
|
||||||
|
|
||||||
.. versionadded:: 0.8
|
|
||||||
"""
|
|
||||||
|
|
||||||
#: :meth:`make_null_session` will look here for the class that should
|
|
||||||
#: be created when a null session is requested. Likewise the
|
|
||||||
#: :meth:`is_null_session` method will perform a typecheck against
|
|
||||||
#: this type.
|
|
||||||
null_session_class = NullSession
|
|
||||||
|
|
||||||
def make_null_session(self, app):
|
|
||||||
"""Creates a null session which acts as a replacement object if the
|
|
||||||
real session support could not be loaded due to a configuration
|
|
||||||
error. This mainly aids the user experience because the job of the
|
|
||||||
null session is to still support lookup without complaining but
|
|
||||||
modifications are answered with a helpful error message of what
|
|
||||||
failed.
|
|
||||||
|
|
||||||
This creates an instance of :attr:`null_session_class` by default.
|
|
||||||
"""
|
|
||||||
return self.null_session_class()
|
|
||||||
|
|
||||||
def is_null_session(self, obj):
|
|
||||||
"""Checks if a given object is a null session. Null sessions are
|
|
||||||
not asked to be saved.
|
|
||||||
|
|
||||||
This checks if the object is an instance of :attr:`null_session_class`
|
|
||||||
by default.
|
|
||||||
"""
|
|
||||||
return isinstance(obj, self.null_session_class)
|
|
||||||
|
|
||||||
def get_cookie_domain(self, app):
|
|
||||||
"""Helpful helper method that returns the cookie domain that should
|
|
||||||
be used for the session cookie if session cookies are used.
|
|
||||||
"""
|
|
||||||
if app.config['SESSION_COOKIE_DOMAIN'] is not None:
|
|
||||||
return app.config['SESSION_COOKIE_DOMAIN']
|
|
||||||
if app.config['SERVER_NAME'] is not None:
|
|
||||||
# chop of the port which is usually not supported by browsers
|
|
||||||
return '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0]
|
|
||||||
|
|
||||||
def get_cookie_path(self, app):
|
|
||||||
"""Returns the path for which the cookie should be valid. The
|
|
||||||
default implementation uses the value from the SESSION_COOKIE_PATH``
|
|
||||||
config var if it's set, and falls back to ``APPLICATION_ROOT`` or
|
|
||||||
uses ``/`` if it's `None`.
|
|
||||||
"""
|
|
||||||
return app.config['SESSION_COOKIE_PATH'] or \
|
|
||||||
app.config['APPLICATION_ROOT'] or '/'
|
|
||||||
|
|
||||||
def get_cookie_httponly(self, app):
|
|
||||||
"""Returns True if the session cookie should be httponly. This
|
|
||||||
currently just returns the value of the ``SESSION_COOKIE_HTTPONLY``
|
|
||||||
config var.
|
|
||||||
"""
|
|
||||||
return app.config['SESSION_COOKIE_HTTPONLY']
|
|
||||||
|
|
||||||
def get_cookie_secure(self, app):
|
|
||||||
"""Returns True if the cookie should be secure. This currently
|
|
||||||
just returns the value of the ``SESSION_COOKIE_SECURE`` setting.
|
|
||||||
"""
|
|
||||||
return app.config['SESSION_COOKIE_SECURE']
|
|
||||||
|
|
||||||
def get_expiration_time(self, app, session):
|
|
||||||
"""A helper method that returns an expiration date for the session
|
|
||||||
or `None` if the session is linked to the browser session. The
|
|
||||||
default implementation returns now + the permanent session
|
|
||||||
lifetime configured on the application.
|
|
||||||
"""
|
|
||||||
if session.permanent:
|
|
||||||
return datetime.utcnow() + app.permanent_session_lifetime
|
|
||||||
|
|
||||||
def open_session(self, app, request):
|
|
||||||
"""This method has to be implemented and must either return `None`
|
|
||||||
in case the loading failed because of a configuration error or an
|
|
||||||
instance of a session object which implements a dictionary like
|
|
||||||
interface + the methods and attributes on :class:`SessionMixin`.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def save_session(self, app, session, response):
|
|
||||||
"""This is called for actual sessions returned by :meth:`open_session`
|
|
||||||
at the end of the request. This is still called during a request
|
|
||||||
context so if you absolutely need access to the request you can do
|
|
||||||
that.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
|
|
||||||
class SecureCookieSessionInterface(SessionInterface):
|
|
||||||
"""The cookie session interface that uses the Werkzeug securecookie
|
|
||||||
as client side session backend.
|
|
||||||
"""
|
|
||||||
session_class = SecureCookieSession
|
|
||||||
|
|
||||||
def open_session(self, app, request):
|
|
||||||
key = app.secret_key
|
|
||||||
if key is not None:
|
|
||||||
return self.session_class.load_cookie(request,
|
|
||||||
app.session_cookie_name,
|
|
||||||
secret_key=key)
|
|
||||||
|
|
||||||
def save_session(self, app, session, response):
|
|
||||||
expires = self.get_expiration_time(app, session)
|
|
||||||
domain = self.get_cookie_domain(app)
|
|
||||||
path = self.get_cookie_path(app)
|
|
||||||
httponly = self.get_cookie_httponly(app)
|
|
||||||
secure = self.get_cookie_secure(app)
|
|
||||||
if session.modified and not session:
|
|
||||||
response.delete_cookie(app.session_cookie_name, path=path,
|
|
||||||
domain=domain)
|
|
||||||
else:
|
|
||||||
session.save_cookie(response, app.session_cookie_name, path=path,
|
|
||||||
expires=expires, httponly=httponly,
|
|
||||||
secure=secure, domain=domain)
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.signals
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Implements signals based on blinker if available, otherwise
|
|
||||||
falls silently back to a noop
|
|
||||||
|
|
||||||
:copyright: (c) 2011 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
signals_available = False
|
|
||||||
try:
|
|
||||||
from blinker import Namespace
|
|
||||||
signals_available = True
|
|
||||||
except ImportError:
|
|
||||||
class Namespace(object):
|
|
||||||
def signal(self, name, doc=None):
|
|
||||||
return _FakeSignal(name, doc)
|
|
||||||
|
|
||||||
class _FakeSignal(object):
|
|
||||||
"""If blinker is unavailable, create a fake class with the same
|
|
||||||
interface that allows sending of signals but will fail with an
|
|
||||||
error on anything else. Instead of doing anything on send, it
|
|
||||||
will just ignore the arguments and do nothing instead.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, name, doc=None):
|
|
||||||
self.name = name
|
|
||||||
self.__doc__ = doc
|
|
||||||
def _fail(self, *args, **kwargs):
|
|
||||||
raise RuntimeError('signalling support is unavailable '
|
|
||||||
'because the blinker library is '
|
|
||||||
'not installed.')
|
|
||||||
send = lambda *a, **kw: None
|
|
||||||
connect = disconnect = has_receivers_for = receivers_for = \
|
|
||||||
temporarily_connected_to = connected_to = _fail
|
|
||||||
del _fail
|
|
||||||
|
|
||||||
# the namespace for code signals. If you are not flask code, do
|
|
||||||
# not put signals in here. Create your own namespace instead.
|
|
||||||
_signals = Namespace()
|
|
||||||
|
|
||||||
|
|
||||||
# core signals. For usage examples grep the sourcecode or consult
|
|
||||||
# the API documentation in docs/api.rst as well as docs/signals.rst
|
|
||||||
template_rendered = _signals.signal('template-rendered')
|
|
||||||
request_started = _signals.signal('request-started')
|
|
||||||
request_finished = _signals.signal('request-finished')
|
|
||||||
request_tearing_down = _signals.signal('request-tearing-down')
|
|
||||||
got_request_exception = _signals.signal('got-request-exception')
|
|
||||||
appcontext_tearing_down = _signals.signal('appcontext-tearing-down')
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.templating
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Implements the bridge to Jinja2.
|
|
||||||
|
|
||||||
:copyright: (c) 2011 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
import posixpath
|
|
||||||
from jinja2 import BaseLoader, Environment as BaseEnvironment, \
|
|
||||||
TemplateNotFound
|
|
||||||
|
|
||||||
from .globals import _request_ctx_stack
|
|
||||||
from .signals import template_rendered
|
|
||||||
from .module import blueprint_is_module
|
|
||||||
|
|
||||||
|
|
||||||
def _default_template_ctx_processor():
|
|
||||||
"""Default template context processor. Injects `request`,
|
|
||||||
`session` and `g`.
|
|
||||||
"""
|
|
||||||
reqctx = _request_ctx_stack.top
|
|
||||||
return dict(
|
|
||||||
config=reqctx.app.config,
|
|
||||||
request=reqctx.request,
|
|
||||||
session=reqctx.session,
|
|
||||||
g=reqctx.g
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Environment(BaseEnvironment):
|
|
||||||
"""Works like a regular Jinja2 environment but has some additional
|
|
||||||
knowledge of how Flask's blueprint works so that it can prepend the
|
|
||||||
name of the blueprint to referenced templates if necessary.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, app, **options):
|
|
||||||
if 'loader' not in options:
|
|
||||||
options['loader'] = app.create_global_jinja_loader()
|
|
||||||
BaseEnvironment.__init__(self, **options)
|
|
||||||
self.app = app
|
|
||||||
|
|
||||||
|
|
||||||
class DispatchingJinjaLoader(BaseLoader):
|
|
||||||
"""A loader that looks for templates in the application and all
|
|
||||||
the blueprint folders.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, app):
|
|
||||||
self.app = app
|
|
||||||
|
|
||||||
def get_source(self, environment, template):
|
|
||||||
for loader, local_name in self._iter_loaders(template):
|
|
||||||
try:
|
|
||||||
return loader.get_source(environment, local_name)
|
|
||||||
except TemplateNotFound:
|
|
||||||
pass
|
|
||||||
|
|
||||||
raise TemplateNotFound(template)
|
|
||||||
|
|
||||||
def _iter_loaders(self, template):
|
|
||||||
loader = self.app.jinja_loader
|
|
||||||
if loader is not None:
|
|
||||||
yield loader, template
|
|
||||||
|
|
||||||
# old style module based loaders in case we are dealing with a
|
|
||||||
# blueprint that is an old style module
|
|
||||||
try:
|
|
||||||
module, local_name = posixpath.normpath(template).split('/', 1)
|
|
||||||
blueprint = self.app.blueprints[module]
|
|
||||||
if blueprint_is_module(blueprint):
|
|
||||||
loader = blueprint.jinja_loader
|
|
||||||
if loader is not None:
|
|
||||||
yield loader, local_name
|
|
||||||
except (ValueError, KeyError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
for blueprint in self.app.blueprints.itervalues():
|
|
||||||
if blueprint_is_module(blueprint):
|
|
||||||
continue
|
|
||||||
loader = blueprint.jinja_loader
|
|
||||||
if loader is not None:
|
|
||||||
yield loader, template
|
|
||||||
|
|
||||||
def list_templates(self):
|
|
||||||
result = set()
|
|
||||||
loader = self.app.jinja_loader
|
|
||||||
if loader is not None:
|
|
||||||
result.update(loader.list_templates())
|
|
||||||
|
|
||||||
for name, blueprint in self.app.blueprints.iteritems():
|
|
||||||
loader = blueprint.jinja_loader
|
|
||||||
if loader is not None:
|
|
||||||
for template in loader.list_templates():
|
|
||||||
prefix = ''
|
|
||||||
if blueprint_is_module(blueprint):
|
|
||||||
prefix = name + '/'
|
|
||||||
result.add(prefix + template)
|
|
||||||
|
|
||||||
return list(result)
|
|
||||||
|
|
||||||
|
|
||||||
def _render(template, context, app):
|
|
||||||
"""Renders the template and fires the signal"""
|
|
||||||
rv = template.render(context)
|
|
||||||
template_rendered.send(app, template=template, context=context)
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
def render_template(template_name_or_list, **context):
|
|
||||||
"""Renders a template from the template folder with the given
|
|
||||||
context.
|
|
||||||
|
|
||||||
:param template_name_or_list: the name of the template to be
|
|
||||||
rendered, or an iterable with template names
|
|
||||||
the first one existing will be rendered
|
|
||||||
:param context: the variables that should be available in the
|
|
||||||
context of the template.
|
|
||||||
"""
|
|
||||||
ctx = _request_ctx_stack.top
|
|
||||||
ctx.app.update_template_context(context)
|
|
||||||
return _render(ctx.app.jinja_env.get_or_select_template(template_name_or_list),
|
|
||||||
context, ctx.app)
|
|
||||||
|
|
||||||
|
|
||||||
def render_template_string(source, **context):
|
|
||||||
"""Renders a template from the given template source string
|
|
||||||
with the given context.
|
|
||||||
|
|
||||||
:param template_name: the sourcecode of the template to be
|
|
||||||
rendered
|
|
||||||
:param context: the variables that should be available in the
|
|
||||||
context of the template.
|
|
||||||
"""
|
|
||||||
ctx = _request_ctx_stack.top
|
|
||||||
ctx.app.update_template_context(context)
|
|
||||||
return _render(ctx.app.jinja_env.from_string(source),
|
|
||||||
context, ctx.app)
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.testing
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Implements test support helpers. This module is lazily imported
|
|
||||||
and usually not used in production environments.
|
|
||||||
|
|
||||||
:copyright: (c) 2011 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import with_statement
|
|
||||||
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from werkzeug.test import Client, EnvironBuilder
|
|
||||||
from flask import _request_ctx_stack
|
|
||||||
|
|
||||||
|
|
||||||
def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs):
|
|
||||||
"""Creates a new test builder with some application defaults thrown in."""
|
|
||||||
http_host = app.config.get('SERVER_NAME')
|
|
||||||
app_root = app.config.get('APPLICATION_ROOT')
|
|
||||||
if base_url is None:
|
|
||||||
base_url = 'http://%s/' % (http_host or 'localhost')
|
|
||||||
if app_root:
|
|
||||||
base_url += app_root.lstrip('/')
|
|
||||||
return EnvironBuilder(path, base_url, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class FlaskClient(Client):
|
|
||||||
"""Works like a regular Werkzeug test client but has some knowledge about
|
|
||||||
how Flask works to defer the cleanup of the request context stack to the
|
|
||||||
end of a with body when used in a with statement. For general information
|
|
||||||
about how to use this class refer to :class:`werkzeug.test.Client`.
|
|
||||||
|
|
||||||
Basic usage is outlined in the :ref:`testing` chapter.
|
|
||||||
"""
|
|
||||||
|
|
||||||
preserve_context = False
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def session_transaction(self, *args, **kwargs):
|
|
||||||
"""When used in combination with a with statement this opens a
|
|
||||||
session transaction. This can be used to modify the session that
|
|
||||||
the test client uses. Once the with block is left the session is
|
|
||||||
stored back.
|
|
||||||
|
|
||||||
with client.session_transaction() as session:
|
|
||||||
session['value'] = 42
|
|
||||||
|
|
||||||
Internally this is implemented by going through a temporary test
|
|
||||||
request context and since session handling could depend on
|
|
||||||
request variables this function accepts the same arguments as
|
|
||||||
:meth:`~flask.Flask.test_request_context` which are directly
|
|
||||||
passed through.
|
|
||||||
"""
|
|
||||||
if self.cookie_jar is None:
|
|
||||||
raise RuntimeError('Session transactions only make sense '
|
|
||||||
'with cookies enabled.')
|
|
||||||
app = self.application
|
|
||||||
environ_overrides = kwargs.setdefault('environ_overrides', {})
|
|
||||||
self.cookie_jar.inject_wsgi(environ_overrides)
|
|
||||||
outer_reqctx = _request_ctx_stack.top
|
|
||||||
with app.test_request_context(*args, **kwargs) as c:
|
|
||||||
sess = app.open_session(c.request)
|
|
||||||
if sess is None:
|
|
||||||
raise RuntimeError('Session backend did not open a session. '
|
|
||||||
'Check the configuration')
|
|
||||||
|
|
||||||
# Since we have to open a new request context for the session
|
|
||||||
# handling we want to make sure that we hide out own context
|
|
||||||
# from the caller. By pushing the original request context
|
|
||||||
# (or None) on top of this and popping it we get exactly that
|
|
||||||
# behavior. It's important to not use the push and pop
|
|
||||||
# methods of the actual request context object since that would
|
|
||||||
# mean that cleanup handlers are called
|
|
||||||
_request_ctx_stack.push(outer_reqctx)
|
|
||||||
try:
|
|
||||||
yield sess
|
|
||||||
finally:
|
|
||||||
_request_ctx_stack.pop()
|
|
||||||
|
|
||||||
resp = app.response_class()
|
|
||||||
if not app.session_interface.is_null_session(sess):
|
|
||||||
app.save_session(sess, resp)
|
|
||||||
headers = resp.get_wsgi_headers(c.request.environ)
|
|
||||||
self.cookie_jar.extract_wsgi(c.request.environ, headers)
|
|
||||||
|
|
||||||
def open(self, *args, **kwargs):
|
|
||||||
kwargs.setdefault('environ_overrides', {}) \
|
|
||||||
['flask._preserve_context'] = self.preserve_context
|
|
||||||
|
|
||||||
as_tuple = kwargs.pop('as_tuple', False)
|
|
||||||
buffered = kwargs.pop('buffered', False)
|
|
||||||
follow_redirects = kwargs.pop('follow_redirects', False)
|
|
||||||
builder = make_test_environ_builder(self.application, *args, **kwargs)
|
|
||||||
|
|
||||||
return Client.open(self, builder,
|
|
||||||
as_tuple=as_tuple,
|
|
||||||
buffered=buffered,
|
|
||||||
follow_redirects=follow_redirects)
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
if self.preserve_context:
|
|
||||||
raise RuntimeError('Cannot nest client invocations')
|
|
||||||
self.preserve_context = True
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, tb):
|
|
||||||
self.preserve_context = False
|
|
||||||
|
|
||||||
# on exit we want to clean up earlier. Normally the request context
|
|
||||||
# stays preserved until the next request in the same thread comes
|
|
||||||
# in. See RequestGlobals.push() for the general behavior.
|
|
||||||
top = _request_ctx_stack.top
|
|
||||||
if top is not None and top.preserved:
|
|
||||||
top.pop()
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.views
|
|
||||||
~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module provides class-based views inspired by the ones in Django.
|
|
||||||
|
|
||||||
:copyright: (c) 2011 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
from .globals import request
|
|
||||||
|
|
||||||
|
|
||||||
http_method_funcs = frozenset(['get', 'post', 'head', 'options',
|
|
||||||
'delete', 'put', 'trace', 'patch'])
|
|
||||||
|
|
||||||
|
|
||||||
class View(object):
|
|
||||||
"""Alternative way to use view functions. A subclass has to implement
|
|
||||||
:meth:`dispatch_request` which is called with the view arguments from
|
|
||||||
the URL routing system. If :attr:`methods` is provided the methods
|
|
||||||
do not have to be passed to the :meth:`~flask.Flask.add_url_rule`
|
|
||||||
method explicitly::
|
|
||||||
|
|
||||||
class MyView(View):
|
|
||||||
methods = ['GET']
|
|
||||||
|
|
||||||
def dispatch_request(self, name):
|
|
||||||
return 'Hello %s!' % name
|
|
||||||
|
|
||||||
app.add_url_rule('/hello/<name>', view_func=MyView.as_view('myview'))
|
|
||||||
|
|
||||||
When you want to decorate a pluggable view you will have to either do that
|
|
||||||
when the view function is created (by wrapping the return value of
|
|
||||||
:meth:`as_view`) or you can use the :attr:`decorators` attribute::
|
|
||||||
|
|
||||||
class SecretView(View):
|
|
||||||
methods = ['GET']
|
|
||||||
decorators = [superuser_required]
|
|
||||||
|
|
||||||
def dispatch_request(self):
|
|
||||||
...
|
|
||||||
|
|
||||||
The decorators stored in the decorators list are applied one after another
|
|
||||||
when the view function is created. Note that you can *not* use the class
|
|
||||||
based decorators since those would decorate the view class and not the
|
|
||||||
generated view function!
|
|
||||||
"""
|
|
||||||
|
|
||||||
#: A for which methods this pluggable view can handle.
|
|
||||||
methods = None
|
|
||||||
|
|
||||||
#: The canonical way to decorate class-based views is to decorate the
|
|
||||||
#: return value of as_view(). However since this moves parts of the
|
|
||||||
#: logic from the class declaration to the place where it's hooked
|
|
||||||
#: into the routing system.
|
|
||||||
#:
|
|
||||||
#: You can place one or more decorators in this list and whenever the
|
|
||||||
#: view function is created the result is automatically decorated.
|
|
||||||
#:
|
|
||||||
#: .. versionadded:: 0.8
|
|
||||||
decorators = []
|
|
||||||
|
|
||||||
def dispatch_request(self):
|
|
||||||
"""Subclasses have to override this method to implement the
|
|
||||||
actual view function code. This method is called with all
|
|
||||||
the arguments from the URL rule.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def as_view(cls, name, *class_args, **class_kwargs):
|
|
||||||
"""Converts the class into an actual view function that can be used
|
|
||||||
with the routing system. Internally this generates a function on the
|
|
||||||
fly which will instantiate the :class:`View` on each request and call
|
|
||||||
the :meth:`dispatch_request` method on it.
|
|
||||||
|
|
||||||
The arguments passed to :meth:`as_view` are forwarded to the
|
|
||||||
constructor of the class.
|
|
||||||
"""
|
|
||||||
def view(*args, **kwargs):
|
|
||||||
self = view.view_class(*class_args, **class_kwargs)
|
|
||||||
return self.dispatch_request(*args, **kwargs)
|
|
||||||
|
|
||||||
if cls.decorators:
|
|
||||||
view.__name__ = name
|
|
||||||
view.__module__ = cls.__module__
|
|
||||||
for decorator in cls.decorators:
|
|
||||||
view = decorator(view)
|
|
||||||
|
|
||||||
# we attach the view class to the view function for two reasons:
|
|
||||||
# first of all it allows us to easily figure out what class-based
|
|
||||||
# view this thing came from, secondly it's also used for instantiating
|
|
||||||
# the view class so you can actually replace it with something else
|
|
||||||
# for testing purposes and debugging.
|
|
||||||
view.view_class = cls
|
|
||||||
view.__name__ = name
|
|
||||||
view.__doc__ = cls.__doc__
|
|
||||||
view.__module__ = cls.__module__
|
|
||||||
view.methods = cls.methods
|
|
||||||
return view
|
|
||||||
|
|
||||||
|
|
||||||
class MethodViewType(type):
|
|
||||||
|
|
||||||
def __new__(cls, name, bases, d):
|
|
||||||
rv = type.__new__(cls, name, bases, d)
|
|
||||||
if 'methods' not in d:
|
|
||||||
methods = set(rv.methods or [])
|
|
||||||
for key in d:
|
|
||||||
if key in http_method_funcs:
|
|
||||||
methods.add(key.upper())
|
|
||||||
# if we have no method at all in there we don't want to
|
|
||||||
# add a method list. (This is for instance the case for
|
|
||||||
# the baseclass or another subclass of a base method view
|
|
||||||
# that does not introduce new methods).
|
|
||||||
if methods:
|
|
||||||
rv.methods = sorted(methods)
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
class MethodView(View):
|
|
||||||
"""Like a regular class-based view but that dispatches requests to
|
|
||||||
particular methods. For instance if you implement a method called
|
|
||||||
:meth:`get` it means you will response to ``'GET'`` requests and
|
|
||||||
the :meth:`dispatch_request` implementation will automatically
|
|
||||||
forward your request to that. Also :attr:`options` is set for you
|
|
||||||
automatically::
|
|
||||||
|
|
||||||
class CounterAPI(MethodView):
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
return session.get('counter', 0)
|
|
||||||
|
|
||||||
def post(self):
|
|
||||||
session['counter'] = session.get('counter', 0) + 1
|
|
||||||
return 'OK'
|
|
||||||
|
|
||||||
app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter'))
|
|
||||||
"""
|
|
||||||
__metaclass__ = MethodViewType
|
|
||||||
|
|
||||||
def dispatch_request(self, *args, **kwargs):
|
|
||||||
meth = getattr(self, request.method.lower(), None)
|
|
||||||
# if the request method is HEAD and we don't have a handler for it
|
|
||||||
# retry with GET
|
|
||||||
if meth is None and request.method == 'HEAD':
|
|
||||||
meth = getattr(self, 'get', None)
|
|
||||||
assert meth is not None, 'Unimplemented method %r' % request.method
|
|
||||||
return meth(*args, **kwargs)
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
flask.wrappers
|
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Implements the WSGI wrappers (request and response).
|
|
||||||
|
|
||||||
:copyright: (c) 2011 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase
|
|
||||||
from werkzeug.utils import cached_property
|
|
||||||
|
|
||||||
from .exceptions import JSONBadRequest
|
|
||||||
from .debughelpers import attach_enctype_error_multidict
|
|
||||||
from .helpers import json, _assert_have_json
|
|
||||||
from .globals import _request_ctx_stack
|
|
||||||
|
|
||||||
|
|
||||||
class Request(RequestBase):
|
|
||||||
"""The request object used by default in Flask. Remembers the
|
|
||||||
matched endpoint and view arguments.
|
|
||||||
|
|
||||||
It is what ends up as :class:`~flask.request`. If you want to replace
|
|
||||||
the request object used you can subclass this and set
|
|
||||||
:attr:`~flask.Flask.request_class` to your subclass.
|
|
||||||
|
|
||||||
The request object is a :class:`~werkzeug.wrappers.Request` subclass and
|
|
||||||
provides all of the attributes Werkzeug defines plus a few Flask
|
|
||||||
specific ones.
|
|
||||||
"""
|
|
||||||
|
|
||||||
#: the internal URL rule that matched the request. This can be
|
|
||||||
#: useful to inspect which methods are allowed for the URL from
|
|
||||||
#: a before/after handler (``request.url_rule.methods``) etc.
|
|
||||||
#:
|
|
||||||
#: .. versionadded:: 0.6
|
|
||||||
url_rule = None
|
|
||||||
|
|
||||||
#: a dict of view arguments that matched the request. If an exception
|
|
||||||
#: happened when matching, this will be `None`.
|
|
||||||
view_args = None
|
|
||||||
|
|
||||||
#: if matching the URL failed, this is the exception that will be
|
|
||||||
#: raised / was raised as part of the request handling. This is
|
|
||||||
#: usually a :exc:`~werkzeug.exceptions.NotFound` exception or
|
|
||||||
#: something similar.
|
|
||||||
routing_exception = None
|
|
||||||
|
|
||||||
# switched by the request context until 1.0 to opt in deprecated
|
|
||||||
# module functionality
|
|
||||||
_is_old_module = False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def max_content_length(self):
|
|
||||||
"""Read-only view of the `MAX_CONTENT_LENGTH` config key."""
|
|
||||||
ctx = _request_ctx_stack.top
|
|
||||||
if ctx is not None:
|
|
||||||
return ctx.app.config['MAX_CONTENT_LENGTH']
|
|
||||||
|
|
||||||
@property
|
|
||||||
def endpoint(self):
|
|
||||||
"""The endpoint that matched the request. This in combination with
|
|
||||||
:attr:`view_args` can be used to reconstruct the same or a
|
|
||||||
modified URL. If an exception happened when matching, this will
|
|
||||||
be `None`.
|
|
||||||
"""
|
|
||||||
if self.url_rule is not None:
|
|
||||||
return self.url_rule.endpoint
|
|
||||||
|
|
||||||
@property
|
|
||||||
def module(self):
|
|
||||||
"""The name of the current module if the request was dispatched
|
|
||||||
to an actual module. This is deprecated functionality, use blueprints
|
|
||||||
instead.
|
|
||||||
"""
|
|
||||||
from warnings import warn
|
|
||||||
warn(DeprecationWarning('modules were deprecated in favor of '
|
|
||||||
'blueprints. Use request.blueprint '
|
|
||||||
'instead.'), stacklevel=2)
|
|
||||||
if self._is_old_module:
|
|
||||||
return self.blueprint
|
|
||||||
|
|
||||||
@property
|
|
||||||
def blueprint(self):
|
|
||||||
"""The name of the current blueprint"""
|
|
||||||
if self.url_rule and '.' in self.url_rule.endpoint:
|
|
||||||
return self.url_rule.endpoint.rsplit('.', 1)[0]
|
|
||||||
|
|
||||||
@cached_property
|
|
||||||
def json(self):
|
|
||||||
"""If the mimetype is `application/json` this will contain the
|
|
||||||
parsed JSON data. Otherwise this will be `None`.
|
|
||||||
|
|
||||||
This requires Python 2.6 or an installed version of simplejson.
|
|
||||||
"""
|
|
||||||
if __debug__:
|
|
||||||
_assert_have_json()
|
|
||||||
if self.mimetype == 'application/json':
|
|
||||||
request_charset = self.mimetype_params.get('charset')
|
|
||||||
try:
|
|
||||||
if request_charset is not None:
|
|
||||||
return json.loads(self.data, encoding=request_charset)
|
|
||||||
return json.loads(self.data)
|
|
||||||
except ValueError, e:
|
|
||||||
return self.on_json_loading_failed(e)
|
|
||||||
|
|
||||||
def on_json_loading_failed(self, e):
|
|
||||||
"""Called if decoding of the JSON data failed. The return value of
|
|
||||||
this method is used by :attr:`json` when an error ocurred. The default
|
|
||||||
implementation raises a :class:`JSONBadRequest`, which is a subclass of
|
|
||||||
:class:`~werkzeug.exceptions.BadRequest` which sets the
|
|
||||||
``Content-Type`` to ``application/json`` and provides a JSON-formatted
|
|
||||||
error description::
|
|
||||||
|
|
||||||
{"description": "The browser (or proxy) sent a request that \
|
|
||||||
this server could not understand."}
|
|
||||||
|
|
||||||
.. versionchanged:: 0.9
|
|
||||||
Return a :class:`JSONBadRequest` instead of a
|
|
||||||
:class:`~werkzeug.exceptions.BadRequest` by default.
|
|
||||||
|
|
||||||
.. versionadded:: 0.8
|
|
||||||
"""
|
|
||||||
raise JSONBadRequest()
|
|
||||||
|
|
||||||
def _load_form_data(self):
|
|
||||||
RequestBase._load_form_data(self)
|
|
||||||
|
|
||||||
# in debug mode we're replacing the files multidict with an ad-hoc
|
|
||||||
# subclass that raises a different error for key errors.
|
|
||||||
ctx = _request_ctx_stack.top
|
|
||||||
if ctx is not None and ctx.app.debug and \
|
|
||||||
self.mimetype != 'multipart/form-data' and not self.files:
|
|
||||||
attach_enctype_error_multidict(self)
|
|
||||||
|
|
||||||
|
|
||||||
class Response(ResponseBase):
|
|
||||||
"""The response object that is used by default in Flask. Works like the
|
|
||||||
response object from Werkzeug but is set to have an HTML mimetype by
|
|
||||||
default. Quite often you don't have to create this object yourself because
|
|
||||||
:meth:`~flask.Flask.make_response` will take care of that for you.
|
|
||||||
|
|
||||||
If you want to replace the response object used you can subclass this and
|
|
||||||
set :attr:`~flask.Flask.response_class` to your subclass.
|
|
||||||
"""
|
|
||||||
default_mimetype = 'text/html'
|
|
||||||
Loaded 100 of 493 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user