Merge branch 'refs/heads/develop'
This commit is contained in:
20
README.md
20
README.md
@@ -40,3 +40,23 @@ Linux (ubuntu / debian):
|
||||
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
|
||||
* Add it to defaults. `sudo update-rc.d couchpotato defaults`
|
||||
* Open your browser and go to: `http://localhost:5050/`
|
||||
|
||||
|
||||
FreeBSD :
|
||||
|
||||
* Update your ports tree `sudo portsnap fetch update`
|
||||
* Install Python 2.6+ [lang/python](http://www.freshports.org/lang/python) with `cd /usr/ports/lang/python; sudo make install clean`
|
||||
* Install port [databases/py-sqlite3](http://www.freshports.org/databases/py-sqlite3) with `cd /usr/ports/databases/py-sqlite3; sudo make install clean`
|
||||
* Add a symlink to 'python2' `sudo ln -s /usr/local/bin/python /usr/local/bin/python2`
|
||||
* Install port [ftp/libcurl](http://www.freshports.org/ftp/libcurl) with `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean`
|
||||
* Install port [ftp/curl](http://www.freshports.org/ftp/bcurl), deselect 'Asynchronous DNS resolution via c-ares' when prompted as part of config `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean`
|
||||
* Install port [textproc/docbook-xml-450](http://www.freshports.org/textproc/docbook-xml-450) with `cd /usr/ports/textproc/docbook-xml-450; sudo make install clean`
|
||||
* Install port [GIT](http://git-scm.com/) with `cd /usr/ports/devel/git; sudo make install clean`
|
||||
* 'cd' to the folder of your choosing.
|
||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
||||
* Then run `sudo python CouchPotatoServer/CouchPotato.py` to start for the first time
|
||||
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/freebsd /etc/rc.d/couchpotato`
|
||||
* Change the paths inside the init script. `sudo vim /etc/init.d/couchpotato`
|
||||
* Make init script executable. `sudo chmod +x /etc/rc.d/couchpotato`
|
||||
* Add init to startup. `sudo echo 'couchpotato_enable="YES"' >> /etc/rc.conf`
|
||||
* Open your browser and go to: `http://server:5050/`
|
||||
|
||||
@@ -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.event import fireEvent
|
||||
from couchpotato.core.helpers.request import getParams, jsonified
|
||||
from couchpotato.core.helpers.variable import md5
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from flask.app import Flask
|
||||
from flask.blueprints import Blueprint
|
||||
from flask.globals import request
|
||||
from flask.helpers import url_for
|
||||
from flask.templating import render_template
|
||||
from sqlalchemy.engine import create_engine
|
||||
from sqlalchemy.orm import scoped_session
|
||||
from sqlalchemy.orm.session import sessionmaker
|
||||
from werkzeug.utils import redirect
|
||||
from tornado import template
|
||||
from tornado.web import RequestHandler
|
||||
import os
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
app = Flask(__name__, static_folder = 'nope')
|
||||
web = Blueprint('web', __name__)
|
||||
views = {}
|
||||
template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates'))
|
||||
|
||||
# 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):
|
||||
return Env.getSession(engine)
|
||||
|
||||
def addView(route, func, static = False):
|
||||
web.add_url_rule(route + ('' if static else '/'), endpoint = route if route else 'index', view_func = func)
|
||||
|
||||
""" Web view """
|
||||
@web.route('/')
|
||||
@requires_auth
|
||||
# Web view
|
||||
def index():
|
||||
return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env)
|
||||
return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env)
|
||||
addView('', index)
|
||||
|
||||
""" Api view """
|
||||
@web.route('docs/')
|
||||
@requires_auth
|
||||
# API docs
|
||||
def apiDocs():
|
||||
from couchpotato import app
|
||||
routes = []
|
||||
for route, x in sorted(app.view_functions.iteritems()):
|
||||
if route[0:4] == 'api.':
|
||||
routes += [route[4:].replace('::', '.')]
|
||||
|
||||
for route in api.iterkeys():
|
||||
routes.append(route)
|
||||
|
||||
if api_docs.get(''):
|
||||
del api_docs['']
|
||||
del api_docs_missing['']
|
||||
return render_template('api.html', fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing))
|
||||
|
||||
@web.route('getkey/')
|
||||
def getApiKey():
|
||||
return template_loader.load('api.html').generate(fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing), Env = Env)
|
||||
|
||||
api = None
|
||||
params = getParams()
|
||||
username = Env.setting('username')
|
||||
password = Env.setting('password')
|
||||
addView('docs', apiDocs)
|
||||
|
||||
if (params.get('u') == md5(username) or not username) and (params.get('p') == password or not password):
|
||||
api = Env.setting('api_key')
|
||||
# Make non basic auth option to get api key
|
||||
class KeyHandler(RequestHandler):
|
||||
def get(self):
|
||||
api = None
|
||||
username = Env.setting('username')
|
||||
password = Env.setting('password')
|
||||
|
||||
return jsonified({
|
||||
'success': api is not None,
|
||||
'api_key': api
|
||||
})
|
||||
if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password):
|
||||
api = Env.setting('api_key')
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(error):
|
||||
index_url = url_for('web.index')
|
||||
url = request.path[len(index_url):]
|
||||
self.write({
|
||||
'success': api is not None,
|
||||
'api_key': api
|
||||
})
|
||||
|
||||
def page_not_found(rh):
|
||||
index_url = Env.get('web_base')
|
||||
url = rh.request.uri[len(index_url):]
|
||||
|
||||
if url[:3] != 'api':
|
||||
if request.path != '/':
|
||||
r = request.url.replace(request.path, index_url + '#' + url)
|
||||
else:
|
||||
r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url)
|
||||
return redirect(r)
|
||||
r = index_url + '#' + url.lstrip('/')
|
||||
rh.redirect(r)
|
||||
else:
|
||||
if not Env.get('dev'):
|
||||
time.sleep(0.1)
|
||||
return 'Wrong API key used', 404
|
||||
|
||||
rh.set_status(404)
|
||||
rh.write('Wrong API key used')
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from flask.blueprints import Blueprint
|
||||
from flask.helpers import url_for
|
||||
from tornado.web import RequestHandler, asynchronous
|
||||
from werkzeug.utils import redirect
|
||||
import json
|
||||
import urllib
|
||||
|
||||
api = Blueprint('api', __name__)
|
||||
api_docs = {}
|
||||
api_docs_missing = []
|
||||
api = {}
|
||||
api_nonblock = {}
|
||||
|
||||
api_docs = {}
|
||||
api_docs_missing = []
|
||||
|
||||
# NonBlock API handler
|
||||
class NonBlockHandler(RequestHandler):
|
||||
|
||||
stoppers = []
|
||||
@@ -32,14 +32,6 @@ class NonBlockHandler(RequestHandler):
|
||||
|
||||
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):
|
||||
api_nonblock[route] = func_tuple
|
||||
|
||||
@@ -48,9 +40,39 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
||||
else:
|
||||
api_docs_missing.append(route)
|
||||
|
||||
""" Api view """
|
||||
def index():
|
||||
index_url = url_for('web.index')
|
||||
return redirect(index_url + 'docs/')
|
||||
# Blocking API handler
|
||||
class ApiHandler(RequestHandler):
|
||||
|
||||
addApiView('', index)
|
||||
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.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.helpers.variable import cleanHost, md5
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -69,9 +68,9 @@ class Core(Plugin):
|
||||
return True
|
||||
|
||||
def available(self):
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def shutdown(self):
|
||||
if self.shutdown_started:
|
||||
@@ -156,10 +155,10 @@ class Core(Plugin):
|
||||
host = 'localhost'
|
||||
port = Env.setting('port')
|
||||
|
||||
return '%s:%d%s' % (cleanHost(host).rstrip('/'), int(port), '/' + Env.setting('url_base').lstrip('/') if Env.setting('url_base') else '')
|
||||
return '%s:%d%s' % (cleanHost(host).rstrip('/'), int(port), Env.get('web_base'))
|
||||
|
||||
def createApiUrl(self):
|
||||
return '%s/api/%s' % (self.createBaseUrl(), Env.setting('api_key'))
|
||||
return '%sapi/%s' % (self.createBaseUrl(), Env.setting('api_key'))
|
||||
|
||||
def version(self):
|
||||
ver = fireEvent('updater.info', single = True)
|
||||
@@ -171,9 +170,9 @@ class Core(Plugin):
|
||||
return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash'])
|
||||
|
||||
def versionView(self):
|
||||
return jsonified({
|
||||
return {
|
||||
'version': self.version()
|
||||
})
|
||||
}
|
||||
|
||||
def signalHandler(self):
|
||||
if Env.get('daemonized'): return
|
||||
|
||||
@@ -8,7 +8,6 @@ from minify.cssmin import cssmin
|
||||
from minify.jsmin import jsmin
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -122,7 +121,7 @@ class ClientScript(Plugin):
|
||||
# Combine all files together with some comments
|
||||
data = ''
|
||||
for r in raw:
|
||||
data += self.comment.get(file_type) % (r.get('file'), r.get('date'))
|
||||
data += self.comment.get(file_type) % (ss(r.get('file')), r.get('date'))
|
||||
data += r.get('data') + '\n\n'
|
||||
|
||||
self.createFile(out, data.strip())
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
@@ -36,7 +35,7 @@ class Updater(Plugin):
|
||||
addEvent('app.load', self.setCrons)
|
||||
addEvent('updater.info', self.info)
|
||||
|
||||
addApiView('updater.info', self.getInfo, docs = {
|
||||
addApiView('updater.info', self.info, docs = {
|
||||
'desc': 'Get updater information',
|
||||
'return': {
|
||||
'type': 'object',
|
||||
@@ -92,17 +91,14 @@ class Updater(Plugin):
|
||||
|
||||
return False
|
||||
|
||||
def info(self):
|
||||
def info(self, **kwargs):
|
||||
return self.updater.info()
|
||||
|
||||
def getInfo(self):
|
||||
return jsonified(self.updater.info())
|
||||
|
||||
def checkView(self):
|
||||
return jsonified({
|
||||
return {
|
||||
'update_available': self.check(force = True),
|
||||
'info': self.updater.info()
|
||||
})
|
||||
}
|
||||
|
||||
def doUpdateView(self):
|
||||
|
||||
@@ -119,9 +115,9 @@ class Updater(Plugin):
|
||||
if not success:
|
||||
success = True
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': success
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
class BaseUpdater(Plugin):
|
||||
@@ -138,9 +134,6 @@ class BaseUpdater(Plugin):
|
||||
def doUpdate(self):
|
||||
pass
|
||||
|
||||
def getInfo(self):
|
||||
return jsonified(self.info())
|
||||
|
||||
def info(self):
|
||||
return {
|
||||
'last_check': self.last_check,
|
||||
@@ -279,6 +272,7 @@ class SourceUpdater(BaseUpdater):
|
||||
if download_data.get('type') == 'zip':
|
||||
zip = zipfile.ZipFile(destination)
|
||||
zip.extractall(extracted_path)
|
||||
zip.close()
|
||||
else:
|
||||
tar = tarfile.open(destination)
|
||||
tar.extractall(path = extracted_path)
|
||||
|
||||
@@ -1,26 +1,51 @@
|
||||
from couchpotato.core.helpers.variable import md5
|
||||
from couchpotato.environment import Env
|
||||
from flask import request, Response
|
||||
from functools import wraps
|
||||
import base64
|
||||
|
||||
def check_auth(username, password):
|
||||
return username == Env.setting('username') and password == Env.setting('password')
|
||||
|
||||
def authenticate():
|
||||
return Response(
|
||||
'This is not the page you are looking for. *waves hand*', 401,
|
||||
{'WWW-Authenticate': 'Basic realm="CouchPotato Login"'}
|
||||
)
|
||||
def requires_auth(handler_class):
|
||||
|
||||
def requires_auth(f):
|
||||
def wrap_execute(handler_execute):
|
||||
|
||||
@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()
|
||||
def require_basic_auth(handler, kwargs):
|
||||
if Env.setting('username') and Env.setting('password'):
|
||||
|
||||
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 = [
|
||||
'http://torrage.com/torrent/%s.torrent',
|
||||
'http://torcache.net/torrent/%s.torrent',
|
||||
'https://torcache.net/torrent/%s.torrent',
|
||||
]
|
||||
|
||||
torrent_trackers = [
|
||||
|
||||
@@ -104,12 +104,21 @@ class NZBGet(Downloader):
|
||||
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
|
||||
except:
|
||||
nzb_id = item['NZBID']
|
||||
|
||||
|
||||
timeleft = -1
|
||||
try:
|
||||
if item['ActiveDownloads'] > 0 and item['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
|
||||
timeleft = str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
|
||||
except:
|
||||
pass
|
||||
|
||||
statuses.append({
|
||||
'id': nzb_id,
|
||||
'name': item['NZBFilename'],
|
||||
'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED',
|
||||
# Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
|
||||
'timeleft': str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20)) if item['ActiveDownloads'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']) else -1,
|
||||
'timeleft': timeleft,
|
||||
})
|
||||
|
||||
for item in queue: # 'Parameters' is not passed in rpc.postqueue
|
||||
|
||||
@@ -11,7 +11,7 @@ config = [{
|
||||
'list': 'download_providers',
|
||||
'name': 'sabnzbd',
|
||||
'label': 'Sabnzbd',
|
||||
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> to download NZBs.',
|
||||
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> (0.7+) to download NZBs.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ config = [{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'torrent',
|
||||
'radio_group': 'nzb,torrent',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
@@ -32,6 +32,13 @@ config = [{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'use_for',
|
||||
'label': 'Use for',
|
||||
'default': 'both',
|
||||
'type': 'dropdown',
|
||||
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.helpers.encoding import isInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
import httplib
|
||||
import json
|
||||
import urllib
|
||||
import urllib2
|
||||
|
||||
import requests
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Synology(Downloader):
|
||||
|
||||
type = ['torrent_magnet']
|
||||
type = ['nzb', 'torrent', 'torrent_magnet']
|
||||
log = CPLog(__name__)
|
||||
|
||||
def download(self, data, movie, filedata = None):
|
||||
|
||||
log.error('Sending "%s" (%s) to Synology.', (data.get('name'), data.get('type')))
|
||||
response = False
|
||||
log.error('Sending "%s" (%s) to Synology.', (data['name'], data['type']))
|
||||
|
||||
# Load host from config and split out port.
|
||||
host = self.conf('host').split(':')
|
||||
@@ -24,20 +23,41 @@ class Synology(Downloader):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
return False
|
||||
|
||||
if data.get('type') == 'torrent':
|
||||
log.error('Can\'t add binary torrent file')
|
||||
return False
|
||||
|
||||
try:
|
||||
# Send request to Transmission
|
||||
# Send request to Synology
|
||||
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
|
||||
remote_torrent = srpc.add_torrent_uri(data.get('url'))
|
||||
log.info('Response: %s', remote_torrent)
|
||||
return remote_torrent['success']
|
||||
if data['type'] == 'torrent_magnet':
|
||||
log.info('Adding torrent URL %s', data['url'])
|
||||
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:
|
||||
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):
|
||||
|
||||
@@ -58,11 +78,13 @@ class SynologyRPC(object):
|
||||
args = {'api': 'SYNO.API.Auth', 'account': self.username, 'passwd': self.password, 'version': 2,
|
||||
'method': 'login', 'session': self.session_name, 'format': 'sid'}
|
||||
response = self._req(self.auth_url, args)
|
||||
if response['success'] == True:
|
||||
if response['success']:
|
||||
self.sid = response['data']['sid']
|
||||
log.debug('Sid=%s', self.sid)
|
||||
return response
|
||||
elif self.username or self.password:
|
||||
log.debug('sid=%s', self.sid)
|
||||
else:
|
||||
log.error('Couldn\'t login to Synology, %s', response)
|
||||
return response['success']
|
||||
else:
|
||||
log.error('User or password missing, not using authentication.')
|
||||
return False
|
||||
|
||||
@@ -70,36 +92,51 @@ class SynologyRPC(object):
|
||||
args = {'api':'SYNO.API.Auth', 'version':1, 'method':'logout', 'session':self.session_name, '_sid':self.sid}
|
||||
return self._req(self.auth_url, args)
|
||||
|
||||
def _req(self, url, args):
|
||||
req_url = url + '?' + urllib.urlencode(args)
|
||||
def _req(self, url, args, files = None):
|
||||
response = {'success': False}
|
||||
try:
|
||||
req_open = urllib2.urlopen(req_url)
|
||||
response = json.loads(req_open.read())
|
||||
req = requests.post(url, data = args, files = files)
|
||||
req.raise_for_status()
|
||||
response = json.loads(req.text)
|
||||
if response['success'] == True:
|
||||
log.info('Synology action successfull')
|
||||
return response
|
||||
except httplib.InvalidURL, err:
|
||||
log.error('Invalid Transmission host, check your config %s', err)
|
||||
return False
|
||||
except urllib2.HTTPError, err:
|
||||
except requests.ConnectionError, err:
|
||||
log.error('Synology connection error, check your config %s', err)
|
||||
except requests.HTTPError, err:
|
||||
log.error('SynologyRPC HTTPError: %s', err)
|
||||
return False
|
||||
except urllib2.URLError, err:
|
||||
log.error('Unable to connect to Synology %s', err)
|
||||
return False
|
||||
except Exception, err:
|
||||
log.error('Exception: %s', err)
|
||||
finally:
|
||||
return response
|
||||
|
||||
def add_torrent_uri(self, torrent):
|
||||
log.info('Adding torrent URL %s', torrent)
|
||||
response = {}
|
||||
def create_task(self, url = None, filename = None, filedata = None):
|
||||
''' Creates new download task in Synology DownloadStation. Either specify
|
||||
url or pair (filename, filedata).
|
||||
|
||||
Returns True if task was created, False otherwise
|
||||
'''
|
||||
result = False
|
||||
# login
|
||||
login = self._login()
|
||||
if len(login) > 0 and login['success'] == True:
|
||||
log.info('Login success, adding torrent')
|
||||
args = {'api':'SYNO.DownloadStation.Task', 'version':1, 'method':'create', 'uri':torrent, '_sid':self.sid}
|
||||
response = self._req(self.download_url, args)
|
||||
if self._login():
|
||||
args = {'api': 'SYNO.DownloadStation.Task',
|
||||
'version': '1',
|
||||
'method': 'create',
|
||||
'_sid': self.sid}
|
||||
if url:
|
||||
log.info('Login success, adding torrent URI')
|
||||
args['uri'] = url
|
||||
response = self._req(self.download_url, args = args)
|
||||
log.info('Response: %s', response)
|
||||
result = response['success']
|
||||
elif filename and filedata:
|
||||
log.info('Login success, adding torrent')
|
||||
files = {'file': (filename, filedata)}
|
||||
response = self._req(self.download_url, args = args, files = files)
|
||||
log.info('Response: %s', response)
|
||||
result = response['success']
|
||||
else:
|
||||
log.error('Invalid use of SynologyRPC.create_task: either url or filename+filedata must be specified')
|
||||
self._logout()
|
||||
else:
|
||||
log.error('Couldn\'t login to Synology, %s', login)
|
||||
return response
|
||||
|
||||
|
||||
return result
|
||||
|
||||
@@ -22,14 +22,22 @@ def addEvent(name, handler, priority = 100):
|
||||
def createHandle(*args, **kwargs):
|
||||
|
||||
try:
|
||||
parent = handler.im_self
|
||||
bc = hasattr(parent, 'beforeCall')
|
||||
if bc: parent.beforeCall(handler)
|
||||
# Open handler
|
||||
has_parent = hasattr(handler, 'im_self')
|
||||
if has_parent:
|
||||
parent = handler.im_self
|
||||
bc = hasattr(parent, 'beforeCall')
|
||||
if bc: parent.beforeCall(handler)
|
||||
|
||||
# Main event
|
||||
h = runHandler(name, handler, *args, **kwargs)
|
||||
ac = hasattr(parent, 'afterCall')
|
||||
if ac: parent.afterCall(handler)
|
||||
|
||||
# Close handler
|
||||
if has_parent:
|
||||
ac = hasattr(parent, 'afterCall')
|
||||
if ac: parent.afterCall(handler)
|
||||
except:
|
||||
h = runHandler(name, handler, *args, **kwargs)
|
||||
log.error('Failed creating handler %s %s: %s', (name, handler, traceback.format_exc()))
|
||||
|
||||
return h
|
||||
|
||||
@@ -43,7 +51,7 @@ def removeEvent(name, handler):
|
||||
e -= handler
|
||||
|
||||
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())
|
||||
|
||||
@@ -133,8 +141,6 @@ def fireEvent(name, *args, **kwargs):
|
||||
options['on_complete']()
|
||||
|
||||
return results
|
||||
except KeyError, e:
|
||||
pass
|
||||
except Exception:
|
||||
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):
|
||||
return ''.join(random.choice(chars) for x in range(size))
|
||||
|
||||
def splitString(str, split_on = ','):
|
||||
return [x.strip() for x in str.split(split_on)] if str else []
|
||||
def splitString(str, split_on = ',', clean = True):
|
||||
list = [x.strip() for x in str.split(split_on)] if str else []
|
||||
return filter(None, list) if clean else list
|
||||
|
||||
@@ -4,7 +4,7 @@ import re
|
||||
class CPLog(object):
|
||||
|
||||
context = ''
|
||||
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key']
|
||||
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key', 'passkey']
|
||||
|
||||
def __init__(self, context = ''):
|
||||
if context.endswith('.main'):
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.base import Provider
|
||||
from couchpotato.environment import Env
|
||||
@@ -62,7 +61,9 @@ class Notification(Provider):
|
||||
listener = 'test'
|
||||
)
|
||||
|
||||
return jsonified({'success': success})
|
||||
return {
|
||||
'success': success
|
||||
}
|
||||
|
||||
def testNotifyName(self):
|
||||
return 'notify.%s.test' % self.getName().lower()
|
||||
|
||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView, addNonBlockApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
@@ -11,6 +10,7 @@ from couchpotato.environment import Env
|
||||
from sqlalchemy.sql.expression import or_
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
import uuid
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -62,11 +62,9 @@ class CoreNotifier(Notification):
|
||||
db.commit()
|
||||
|
||||
|
||||
def markAsRead(self):
|
||||
def markAsRead(self, ids = None):
|
||||
|
||||
ids = None
|
||||
if getParam('ids'):
|
||||
ids = splitString(getParam('ids'))
|
||||
ids = splitString(ids) if ids else None
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -79,14 +77,13 @@ class CoreNotifier(Notification):
|
||||
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def listView(self):
|
||||
def listView(self, limit_offset = None, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
limit_offset = getParam('limit_offset', None)
|
||||
|
||||
q = db.query(Notif)
|
||||
|
||||
@@ -105,11 +102,11 @@ class CoreNotifier(Notification):
|
||||
ndict['type'] = 'notification'
|
||||
notifications.append(ndict)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(notifications) == 0,
|
||||
'notifications': notifications
|
||||
})
|
||||
}
|
||||
|
||||
def checkMessages(self):
|
||||
|
||||
@@ -150,6 +147,8 @@ class CoreNotifier(Notification):
|
||||
|
||||
def frontend(self, type = 'notification', data = {}, message = None):
|
||||
|
||||
log.debug('Notifying frontend')
|
||||
|
||||
self.m_lock.acquire()
|
||||
notification = {
|
||||
'message_id': str(uuid.uuid4()),
|
||||
@@ -168,11 +167,13 @@ class CoreNotifier(Notification):
|
||||
'result': [notification],
|
||||
})
|
||||
except:
|
||||
break
|
||||
log.debug('Failed sending to listener: %s', traceback.format_exc())
|
||||
|
||||
self.m_lock.release()
|
||||
self.cleanMessages()
|
||||
|
||||
log.debug('Done notifying frontend')
|
||||
|
||||
def addListener(self, callback, last_id = None):
|
||||
|
||||
if last_id:
|
||||
@@ -194,9 +195,11 @@ class CoreNotifier(Notification):
|
||||
if listener == callback:
|
||||
self.listeners.remove(list_tuple)
|
||||
except:
|
||||
pass
|
||||
log.debug('Failed removing listener: %s', traceback.format_exc())
|
||||
|
||||
def cleanMessages(self):
|
||||
|
||||
log.debug('Cleaning messages')
|
||||
self.m_lock.acquire()
|
||||
|
||||
for message in self.messages:
|
||||
@@ -204,8 +207,11 @@ class CoreNotifier(Notification):
|
||||
self.messages.remove(message)
|
||||
|
||||
self.m_lock.release()
|
||||
log.debug('Done cleaning messages')
|
||||
|
||||
def getMessages(self, last_id):
|
||||
|
||||
log.debug('Getting messages with id: %s', last_id)
|
||||
self.m_lock.acquire()
|
||||
|
||||
recent = []
|
||||
@@ -216,15 +222,16 @@ class CoreNotifier(Notification):
|
||||
recent = self.messages[index:]
|
||||
|
||||
self.m_lock.release()
|
||||
log.debug('Returning for %s %s messages', (last_id, len(recent or [])))
|
||||
|
||||
return recent or []
|
||||
|
||||
def listener(self):
|
||||
def listener(self, init = False, **kwargs):
|
||||
|
||||
messages = []
|
||||
|
||||
# Get unread
|
||||
if getParam('init'):
|
||||
if init:
|
||||
db = get_session()
|
||||
|
||||
notifications = db.query(Notif) \
|
||||
@@ -235,7 +242,7 @@ class CoreNotifier(Notification):
|
||||
ndict['type'] = 'notification'
|
||||
messages.append(ndict)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'result': messages,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.request import getParams, jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import re
|
||||
@@ -22,10 +21,7 @@ class NMJ(Notification):
|
||||
addApiView(self.testNotifyName(), self.test)
|
||||
addApiView('notify.nmj.auto_config', self.autoConfig)
|
||||
|
||||
def autoConfig(self):
|
||||
|
||||
params = getParams()
|
||||
host = params.get('host', 'localhost')
|
||||
def autoConfig(self, host = 'localhost', **kwargs):
|
||||
|
||||
database = ''
|
||||
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')
|
||||
return self.failed()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'database': database,
|
||||
'mount': mount,
|
||||
})
|
||||
}
|
||||
|
||||
def addToLibrary(self, message = None, group = {}):
|
||||
if self.isDisabled(): return
|
||||
@@ -113,9 +109,13 @@ class NMJ(Notification):
|
||||
return True
|
||||
|
||||
def failed(self):
|
||||
return jsonified({'success': False})
|
||||
return {
|
||||
'success': False
|
||||
}
|
||||
|
||||
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.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from flask.helpers import json
|
||||
import base64
|
||||
import json
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
@@ -86,4 +85,6 @@ class Plex(Notification):
|
||||
)
|
||||
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.helpers.request import jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import os
|
||||
@@ -33,4 +32,6 @@ class Synoindex(Notification):
|
||||
return True
|
||||
|
||||
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.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from flask.helpers import url_for
|
||||
from couchpotato.environment import Env
|
||||
from pytwitter import Api, parse_qsl
|
||||
from werkzeug.utils import redirect
|
||||
import oauth2
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -70,10 +68,9 @@ class Twitter(Notification):
|
||||
|
||||
return True
|
||||
|
||||
def getAuthorizationUrl(self):
|
||||
def getAuthorizationUrl(self, host = None, **kwargs):
|
||||
|
||||
referer = getParam('host')
|
||||
callback_url = cleanHost(referer) + '%snotify.%s.credentials/' % (url_for('api.index').lstrip('/'), self.getName().lower())
|
||||
callback_url = cleanHost(host) + '%snotify.%s.credentials/' % (Env.get('api_base').lstrip('/'), self.getName().lower())
|
||||
|
||||
oauth_consumer = oauth2.Consumer(self.consumer_key, self.consumer_secret)
|
||||
oauth_client = oauth2.Client(oauth_consumer)
|
||||
@@ -82,31 +79,29 @@ class Twitter(Notification):
|
||||
|
||||
if resp['status'] != '200':
|
||||
log.error('Invalid response from Twitter requesting temp token: %s', resp['status'])
|
||||
return jsonified({
|
||||
return {
|
||||
'success': False,
|
||||
})
|
||||
}
|
||||
else:
|
||||
self.request_token = dict(parse_qsl(content))
|
||||
|
||||
auth_url = self.urls['authorize'] + ("?oauth_token=%s" % self.request_token['oauth_token'])
|
||||
|
||||
log.info('Redirecting to "%s"', auth_url)
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'url': auth_url,
|
||||
})
|
||||
}
|
||||
|
||||
def getCredentials(self):
|
||||
|
||||
key = getParam('oauth_verifier')
|
||||
def getCredentials(self, oauth_verifier, **kwargs):
|
||||
|
||||
token = oauth2.Token(self.request_token['oauth_token'], self.request_token['oauth_token_secret'])
|
||||
token.set_verifier(key)
|
||||
token.set_verifier(oauth_verifier)
|
||||
|
||||
oauth_consumer = oauth2.Consumer(key = self.consumer_key, secret = self.consumer_secret)
|
||||
oauth_client = oauth2.Client(oauth_consumer, token)
|
||||
|
||||
resp, content = oauth_client.request(self.urls['access'], method = 'POST', body = 'oauth_verifier=%s' % key)
|
||||
resp, content = oauth_client.request(self.urls['access'], method = 'POST', body = 'oauth_verifier=%s' % oauth_verifier)
|
||||
access_token = dict(parse_qsl(content))
|
||||
|
||||
if resp['status'] != '200':
|
||||
@@ -121,4 +116,4 @@ class Twitter(Notification):
|
||||
|
||||
self.request_token = None
|
||||
|
||||
return redirect(url_for('web.index') + 'settings/notifications/')
|
||||
return 'redirect', Env.get('web_base') + 'settings/notifications/'
|
||||
|
||||
@@ -31,6 +31,13 @@ config = [{
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'only_first',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Only update the first host when movie snatched, useful for synced XBMC',
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
'default': 0,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from flask.helpers import json
|
||||
from urllib2 import URLError
|
||||
import base64
|
||||
import json
|
||||
import socket
|
||||
import traceback
|
||||
import urllib
|
||||
|
||||
@@ -20,19 +22,30 @@ class XBMC(Notification):
|
||||
hosts = splitString(self.conf('host'))
|
||||
|
||||
successful = 0
|
||||
max_successful = 0
|
||||
for host in hosts:
|
||||
|
||||
if self.use_json_notifications.get(host) is None:
|
||||
self.getXBMCJSONversion(host, message = message)
|
||||
|
||||
if self.use_json_notifications.get(host):
|
||||
response = self.request(host, [
|
||||
calls = [
|
||||
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}),
|
||||
('VideoLibrary.Scan', {}),
|
||||
])
|
||||
]
|
||||
|
||||
if not self.conf('only_first') or hosts.index(host) == 0:
|
||||
calls.append(('VideoLibrary.Scan', {}))
|
||||
|
||||
max_successful += len(calls)
|
||||
response = self.request(host, calls)
|
||||
else:
|
||||
response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
|
||||
response += self.request(host, [('VideoLibrary.Scan', {})])
|
||||
|
||||
if not self.conf('only_first') or hosts.index(host) == 0:
|
||||
response += self.request(host, [('VideoLibrary.Scan', {})])
|
||||
max_successful += 1
|
||||
|
||||
max_successful += 1
|
||||
|
||||
try:
|
||||
for result in response:
|
||||
@@ -44,7 +57,7 @@ class XBMC(Notification):
|
||||
except:
|
||||
log.error('Failed parsing results: %s', traceback.format_exc())
|
||||
|
||||
return successful == len(hosts) * 2
|
||||
return successful == max_successful
|
||||
|
||||
def getXBMCJSONversion(self, host, message = ''):
|
||||
|
||||
@@ -53,7 +66,7 @@ class XBMC(Notification):
|
||||
# XBMC JSON-RPC version request
|
||||
response = self.request(host, [
|
||||
('JSONRPC.Version', {})
|
||||
])
|
||||
])
|
||||
for result in response:
|
||||
if (result.get('result') and type(result['result']['version']).__name__ == 'int'):
|
||||
# only v2 and v4 return an int object
|
||||
@@ -138,7 +151,7 @@ class XBMC(Notification):
|
||||
# <li>Error:<message>
|
||||
# </html>
|
||||
#
|
||||
response = self.urlopen(server, headers = headers)
|
||||
response = self.urlopen(server, headers = headers, timeout = 3, show_error = False)
|
||||
|
||||
if 'OK' in response:
|
||||
log.debug('Returned from non-JSON-type request %s: %s', (host, response))
|
||||
@@ -149,6 +162,13 @@ class XBMC(Notification):
|
||||
# manually fake expected response array
|
||||
return [{'result': 'Error'}]
|
||||
|
||||
except URLError, e:
|
||||
if isinstance(e.reason, socket.timeout):
|
||||
log.info('Couldn\'t send request to XBMC, assuming it\'s turned off')
|
||||
return [{'result': 'Error'}]
|
||||
else:
|
||||
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
|
||||
return [{'result': 'Error'}]
|
||||
except:
|
||||
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
|
||||
return [{'result': 'Error'}]
|
||||
@@ -177,11 +197,17 @@ class XBMC(Notification):
|
||||
|
||||
try:
|
||||
log.debug('Sending request to %s: %s', (host, data))
|
||||
rdata = self.urlopen(server, headers = headers, params = data, multipart = True)
|
||||
response = json.loads(rdata)
|
||||
response = self.getJsonData(server, headers = headers, params = data, timeout = 3, show_error = False)
|
||||
log.debug('Returned from request %s: %s', (host, response))
|
||||
|
||||
return response
|
||||
except URLError, e:
|
||||
if isinstance(e.reason, socket.timeout):
|
||||
log.info('Couldn\'t send request to XBMC, assuming it\'s turned off')
|
||||
return []
|
||||
else:
|
||||
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
|
||||
return []
|
||||
except:
|
||||
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
|
||||
return []
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from StringIO import StringIO
|
||||
from couchpotato import addView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString, \
|
||||
toUnicode
|
||||
from couchpotato.core.helpers.variable import getExt, md5
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from flask.templating import render_template_string
|
||||
from multipartpost import MultipartPostHandler
|
||||
from tornado import template
|
||||
from tornado.web import StaticFileHandler
|
||||
from urlparse import urlparse
|
||||
import cookielib
|
||||
import glob
|
||||
@@ -37,6 +38,7 @@ class Plugin(object):
|
||||
def registerPlugin(self):
|
||||
addEvent('app.do_shutdown', self.doShutdown)
|
||||
addEvent('plugin.running', self.isRunning)
|
||||
self._running = []
|
||||
|
||||
def conf(self, attr, value = None, default = None):
|
||||
return Env.setting(attr, self.getName().lower(), value = value, default = default)
|
||||
@@ -44,35 +46,37 @@ class Plugin(object):
|
||||
def getName(self):
|
||||
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()
|
||||
return render_template_string(template, **params)
|
||||
t = template.Template(open(os.path.join(os.path.dirname(parent_file), templ), 'r').read())
|
||||
return t.generate(**params)
|
||||
|
||||
def registerStatic(self, plugin_file, add_to_head = True):
|
||||
|
||||
# Register plugin path
|
||||
self.plugin_path = os.path.dirname(plugin_file)
|
||||
static_folder = toUnicode(os.path.join(self.plugin_path, 'static'))
|
||||
|
||||
if not os.path.isdir(static_folder):
|
||||
return
|
||||
|
||||
# Get plugin_name from PluginName
|
||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', self.__class__.__name__)
|
||||
class_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
||||
|
||||
# View path
|
||||
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:
|
||||
for f in glob.glob(os.path.join(self.plugin_path, 'static', '*')):
|
||||
ext = getExt(f)
|
||||
if ext in ['js', 'css']:
|
||||
fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f)
|
||||
|
||||
def showStatic(self, filename):
|
||||
d = os.path.join(self.plugin_path, 'static')
|
||||
|
||||
from flask.helpers import send_from_directory
|
||||
return send_from_directory(d, filename)
|
||||
|
||||
def createFile(self, path, content, binary = False):
|
||||
path = ss(path)
|
||||
|
||||
@@ -106,12 +110,14 @@ class Plugin(object):
|
||||
|
||||
# Fill in some headers
|
||||
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['Host'] = headers.get('Host', host)
|
||||
headers['User-Agent'] = headers.get('User-Agent', self.user_agent)
|
||||
headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip')
|
||||
headers['Connection'] = headers.get('Connection', 'keep-alive')
|
||||
headers['Cache-Control'] = headers.get('Cache-Control', 'max-age=0')
|
||||
|
||||
# Don't try for failed requests
|
||||
if self.http_failed_disabled.get(host, 0) > 0:
|
||||
@@ -128,6 +134,10 @@ class Plugin(object):
|
||||
self.wait(host)
|
||||
try:
|
||||
|
||||
# Make sure opener has the correct headers
|
||||
if opener:
|
||||
opener.add_headers = headers
|
||||
|
||||
if multipart:
|
||||
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
|
||||
request = urllib2.Request(url, params, headers)
|
||||
@@ -141,7 +151,12 @@ class Plugin(object):
|
||||
response = opener.open(request, timeout = timeout)
|
||||
else:
|
||||
log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
|
||||
data = tryUrlencode(params) if len(params) > 0 else None
|
||||
|
||||
if isinstance(params, (str, unicode)) and len(params) > 0:
|
||||
data = params
|
||||
else:
|
||||
data = tryUrlencode(params) if len(params) > 0 else None
|
||||
|
||||
request = urllib2.Request(url, data, headers)
|
||||
|
||||
if opener:
|
||||
@@ -154,8 +169,10 @@ class Plugin(object):
|
||||
buf = StringIO(response.read())
|
||||
f = gzip.GzipFile(fileobj = buf)
|
||||
data = f.read()
|
||||
f.close()
|
||||
else:
|
||||
data = response.read()
|
||||
response.close()
|
||||
|
||||
self.http_failed_request[host] = 0
|
||||
except IOError:
|
||||
@@ -211,9 +228,6 @@ class Plugin(object):
|
||||
|
||||
def isRunning(self, value = None, boolean = True):
|
||||
|
||||
if not hasattr(self, '_running'):
|
||||
self._running = []
|
||||
|
||||
if value is None:
|
||||
return self._running
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.helpers.request import getParam, jsonified
|
||||
from couchpotato.core.helpers.variable import getUserDir
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
import ctypes
|
||||
@@ -63,16 +62,15 @@ class FileBrowser(Plugin):
|
||||
|
||||
return driveletters
|
||||
|
||||
def view(self):
|
||||
def view(self, path = '/', show_hidden = True, **kwargs):
|
||||
|
||||
path = getParam('path', '/')
|
||||
home = getUserDir()
|
||||
|
||||
if not path:
|
||||
path = home
|
||||
|
||||
try:
|
||||
dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True))
|
||||
dirs = self.getDirectories(path = path, show_hidden = show_hidden)
|
||||
except:
|
||||
dirs = []
|
||||
|
||||
@@ -82,14 +80,14 @@ class FileBrowser(Plugin):
|
||||
elif parent != '/' and parent[-2:] != ':\\':
|
||||
parent += os.path.sep
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'is_root': path == '/',
|
||||
'empty': len(dirs) == 0,
|
||||
'parent': parent,
|
||||
'home': home + os.path.sep,
|
||||
'platform': os.name,
|
||||
'dirs': dirs,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
def is_hidden(self, filepath):
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.request import jsonified, getParams
|
||||
from couchpotato.core.helpers.variable import splitString, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Movie
|
||||
from sqlalchemy.orm import joinedload_all
|
||||
import random
|
||||
import random as rndm
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -43,14 +42,13 @@ class Dashboard(Plugin):
|
||||
|
||||
suggestions = fireEvent('movie.suggest', movies = identifiers, single = True)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'result': True,
|
||||
'suggestions': suggestions
|
||||
})
|
||||
}
|
||||
|
||||
def getSoonView(self):
|
||||
def getSoonView(self, limit_offset = None, random = False, late = False, **kwargs):
|
||||
|
||||
params = getParams()
|
||||
db = get_session()
|
||||
now = time.time()
|
||||
|
||||
@@ -85,7 +83,6 @@ class Dashboard(Plugin):
|
||||
.options(joinedload_all('files'))
|
||||
|
||||
# Add limit
|
||||
limit_offset = params.get('limit_offset')
|
||||
limit = 12
|
||||
if limit_offset:
|
||||
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
|
||||
@@ -93,8 +90,8 @@ class Dashboard(Plugin):
|
||||
|
||||
all_movies = q.all()
|
||||
|
||||
if params.get('random', False):
|
||||
random.shuffle(all_movies)
|
||||
if random:
|
||||
rndm.shuffle(all_movies)
|
||||
|
||||
movies = []
|
||||
for movie in all_movies:
|
||||
@@ -126,18 +123,18 @@ class Dashboard(Plugin):
|
||||
})
|
||||
|
||||
# Don't list older movies
|
||||
if ((not params.get('late') and (not eta.get('dvd') or (eta.get('dvd') and eta.get('dvd') > (now - 2419200)))) or \
|
||||
(params.get('late') and eta.get('dvd') and eta.get('dvd') < (now - 2419200))):
|
||||
if ((not late and (not eta.get('dvd') or (eta.get('dvd') and eta.get('dvd') > (now - 2419200)))) or \
|
||||
(late and eta.get('dvd') and eta.get('dvd') < (now - 2419200))):
|
||||
movies.append(temp)
|
||||
|
||||
if len(movies) >= limit:
|
||||
break
|
||||
|
||||
db.expire_all()
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'movies': movies,
|
||||
})
|
||||
}
|
||||
|
||||
getLateView = getSoonView
|
||||
|
||||
@@ -2,15 +2,13 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.helpers.variable import md5, getExt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.plugins.scanner.main import Scanner
|
||||
from couchpotato.core.settings.model import FileType, File
|
||||
from couchpotato.environment import Env
|
||||
from flask.helpers import send_file
|
||||
from werkzeug.exceptions import NotFound
|
||||
from tornado.web import StaticFileHandler
|
||||
import os.path
|
||||
import time
|
||||
import traceback
|
||||
@@ -25,7 +23,7 @@ class FileManager(Plugin):
|
||||
addEvent('file.download', self.download)
|
||||
addEvent('file.types', self.getTypes)
|
||||
|
||||
addApiView('file.cache/<path:filename>', self.showCacheFile, static = True, docs = {
|
||||
addApiView('file.cache/(.*)', self.showCacheFile, static = True, docs = {
|
||||
'desc': 'Return a file from the cp_data/cache directory',
|
||||
'params': {
|
||||
'filename': {'desc': 'path/filename of the wanted file'}
|
||||
@@ -81,15 +79,9 @@ class FileManager(Plugin):
|
||||
except:
|
||||
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 = {}):
|
||||
|
||||
@@ -160,6 +152,6 @@ class FileManager(Plugin):
|
||||
|
||||
def getTypesView(self):
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'types': self.getTypes()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified, getParam, getParams
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -47,9 +46,9 @@ class Logging(Plugin):
|
||||
}
|
||||
})
|
||||
|
||||
def get(self):
|
||||
def get(self, nr = 0, **kwargs):
|
||||
|
||||
nr = int(getParam('nr', 0))
|
||||
nr = tryInt(nr)
|
||||
current_path = None
|
||||
|
||||
total = 1
|
||||
@@ -71,16 +70,15 @@ class Logging(Plugin):
|
||||
f = open(current_path, 'r')
|
||||
log = f.read()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'log': toUnicode(log),
|
||||
'total': total,
|
||||
})
|
||||
}
|
||||
|
||||
def partial(self):
|
||||
def partial(self, type = 'all', lines = 30, **kwargs):
|
||||
|
||||
log_type = getParam('type', 'all')
|
||||
total_lines = tryInt(getParam('lines', 30))
|
||||
total_lines = tryInt(lines)
|
||||
|
||||
log_lines = []
|
||||
|
||||
@@ -100,7 +98,7 @@ class Logging(Plugin):
|
||||
brk = False
|
||||
for line in reversed_lines:
|
||||
|
||||
if log_type == 'all' or '%s ' % log_type.upper() in line:
|
||||
if type == 'all' or '%s ' % type.upper() in line:
|
||||
log_lines.append(line)
|
||||
|
||||
if len(log_lines) >= total_lines:
|
||||
@@ -111,10 +109,10 @@ class Logging(Plugin):
|
||||
break
|
||||
|
||||
log_lines.reverse()
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'log': '[0m\n'.join(log_lines),
|
||||
})
|
||||
}
|
||||
|
||||
def clear(self):
|
||||
|
||||
@@ -135,24 +133,21 @@ class Logging(Plugin):
|
||||
except:
|
||||
log.error('Couldn\'t delete file "%s": %s', (path, traceback.format_exc()))
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def log(self):
|
||||
|
||||
params = getParams()
|
||||
def log(self, type = 'error', **kwargs):
|
||||
|
||||
try:
|
||||
log_message = 'API log: %s' % params
|
||||
log_message = 'API log: %s' % kwargs
|
||||
try:
|
||||
getattr(log, params.get('type', 'error'))(log_message)
|
||||
getattr(log, type)(log_message)
|
||||
except:
|
||||
log.error(log_message)
|
||||
except:
|
||||
log.error('Couldn\'t log via API: %s', params)
|
||||
log.error('Couldn\'t log via API: %s', kwargs)
|
||||
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import splitString, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -46,24 +45,23 @@ class Manage(Plugin):
|
||||
})
|
||||
|
||||
if not Env.get('dev'):
|
||||
def updateLibrary():
|
||||
self.updateLibrary(full = False)
|
||||
addEvent('app.load', updateLibrary)
|
||||
addEvent('app.load', self.updateLibraryQuick)
|
||||
|
||||
def getProgress(self):
|
||||
return jsonified({
|
||||
return {
|
||||
'progress': self.in_progress
|
||||
})
|
||||
}
|
||||
|
||||
def updateLibraryView(self):
|
||||
def updateLibraryView(self, full = 1, **kwargs):
|
||||
|
||||
full = getParam('full', default = 1)
|
||||
fireEventAsync('manage.update', full = True if full == '1' else False)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def updateLibraryQuick(self):
|
||||
return self.updateLibrary(full = False)
|
||||
|
||||
def updateLibrary(self, full = True):
|
||||
last_update = float(Env.prop('manage.last_update', default = 0))
|
||||
|
||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||
from couchpotato.core.helpers.request import getParams, jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import getImdb, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -125,15 +124,14 @@ class MoviePlugin(Plugin):
|
||||
|
||||
db.expire_all()
|
||||
|
||||
def getView(self):
|
||||
def getView(self, id = None):
|
||||
|
||||
movie_id = getParam('id')
|
||||
movie = self.get(movie_id) if movie_id else None
|
||||
movie = self.get(id) if id else None
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': movie is not None,
|
||||
'movie': movie,
|
||||
})
|
||||
}
|
||||
|
||||
def get(self, movie_id):
|
||||
|
||||
@@ -263,15 +261,14 @@ class MoviePlugin(Plugin):
|
||||
db.expire_all()
|
||||
return ''.join(sorted(chars, key = str.lower))
|
||||
|
||||
def listView(self):
|
||||
def listView(self, **kwargs):
|
||||
|
||||
params = getParams()
|
||||
status = splitString(params.get('status', None))
|
||||
release_status = splitString(params.get('release_status', None))
|
||||
limit_offset = params.get('limit_offset', None)
|
||||
starts_with = params.get('starts_with', None)
|
||||
search = params.get('search', None)
|
||||
order = params.get('order', None)
|
||||
status = splitString(kwargs.get('status', None))
|
||||
release_status = splitString(kwargs.get('release_status', None))
|
||||
limit_offset = kwargs.get('limit_offset', None)
|
||||
starts_with = kwargs.get('starts_with', None)
|
||||
search = kwargs.get('search', None)
|
||||
order = kwargs.get('order', None)
|
||||
|
||||
total_movies, movies = self.list(
|
||||
status = status,
|
||||
@@ -282,32 +279,31 @@ class MoviePlugin(Plugin):
|
||||
order = order
|
||||
)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'total': total_movies,
|
||||
'movies': movies,
|
||||
})
|
||||
}
|
||||
|
||||
def charView(self):
|
||||
def charView(self, **kwargs):
|
||||
|
||||
params = getParams()
|
||||
status = splitString(params.get('status', None))
|
||||
release_status = splitString(params.get('release_status', None))
|
||||
status = splitString(kwargs.get('status', None))
|
||||
release_status = splitString(kwargs.get('release_status', None))
|
||||
chars = self.availableChars(status, release_status)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(chars) == 0,
|
||||
'chars': chars,
|
||||
})
|
||||
}
|
||||
|
||||
def refresh(self):
|
||||
def refresh(self, id = ''):
|
||||
|
||||
db = get_session()
|
||||
|
||||
for id in splitString(getParam('id')):
|
||||
movie = db.query(Movie).filter_by(id = id).first()
|
||||
for x in splitString(id):
|
||||
movie = db.query(Movie).filter_by(id = x).first()
|
||||
|
||||
if movie:
|
||||
|
||||
@@ -316,17 +312,16 @@ class MoviePlugin(Plugin):
|
||||
for title in movie.library.titles:
|
||||
if title.default: default_title = title.title
|
||||
|
||||
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True)
|
||||
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id))
|
||||
fireEvent('notify.frontend', type = 'movie.busy.%s' % x, data = True)
|
||||
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
|
||||
|
||||
db.expire_all()
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
})
|
||||
}
|
||||
|
||||
def search(self):
|
||||
def search(self, q = ''):
|
||||
|
||||
q = getParam('q')
|
||||
cache_key = u'%s/%s' % (__name__, simplifyString(q))
|
||||
movies = Env.get('cache').get(cache_key)
|
||||
|
||||
@@ -338,11 +333,11 @@ class MoviePlugin(Plugin):
|
||||
movies = fireEvent('movie.search', q = q, merge = True)
|
||||
Env.get('cache').set(cache_key, movies)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0 if movies else 0,
|
||||
'movies': movies,
|
||||
})
|
||||
}
|
||||
|
||||
def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None):
|
||||
|
||||
@@ -432,33 +427,30 @@ class MoviePlugin(Plugin):
|
||||
return movie_dict
|
||||
|
||||
|
||||
def addView(self):
|
||||
def addView(self, **kwargs):
|
||||
|
||||
params = getParams()
|
||||
movie_dict = self.add(params = kwargs)
|
||||
|
||||
movie_dict = self.add(params)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'added': True if movie_dict else False,
|
||||
'movie': movie_dict,
|
||||
})
|
||||
}
|
||||
|
||||
def edit(self):
|
||||
def edit(self, id = '', **kwargs):
|
||||
|
||||
params = getParams()
|
||||
db = get_session()
|
||||
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
|
||||
ids = splitString(params.get('id'))
|
||||
ids = splitString(id)
|
||||
for movie_id in ids:
|
||||
|
||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||
if not m:
|
||||
continue
|
||||
|
||||
m.profile_id = params.get('profile_id')
|
||||
m.profile_id = kwargs.get('profile_id')
|
||||
|
||||
# Remove releases
|
||||
for rel in m.releases:
|
||||
@@ -467,9 +459,9 @@ class MoviePlugin(Plugin):
|
||||
db.commit()
|
||||
|
||||
# Default title
|
||||
if params.get('default_title'):
|
||||
if kwargs.get('default_title'):
|
||||
for title in m.library.titles:
|
||||
title.default = toUnicode(params.get('default_title', '')).lower() == toUnicode(title.title).lower()
|
||||
title.default = toUnicode(kwargs.get('default_title', '')).lower() == toUnicode(title.title).lower()
|
||||
|
||||
db.commit()
|
||||
|
||||
@@ -479,21 +471,19 @@ class MoviePlugin(Plugin):
|
||||
fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
|
||||
|
||||
db.expire_all()
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
})
|
||||
}
|
||||
|
||||
def deleteView(self):
|
||||
def deleteView(self, id = '', **kwargs):
|
||||
|
||||
params = getParams()
|
||||
|
||||
ids = splitString(params.get('id'))
|
||||
ids = splitString(id)
|
||||
for movie_id in ids:
|
||||
self.delete(movie_id, delete_from = params.get('delete_from', 'all'))
|
||||
self.delete(movie_id, delete_from = kwargs.get('delete_from', 'all'))
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
})
|
||||
}
|
||||
|
||||
def delete(self, movie_id, delete_from = None):
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ MA.Release = new Class({
|
||||
}
|
||||
|
||||
// Create release
|
||||
new Element('div', {
|
||||
var item = new Element('div', {
|
||||
'class': 'item '+status.identifier,
|
||||
'id': 'release_'+release.id
|
||||
}).adopt(
|
||||
@@ -165,11 +165,12 @@ MA.Release = new Class({
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
self.ignore(release);
|
||||
this.getParent('.item').toggleClass('ignored')
|
||||
}
|
||||
}
|
||||
})
|
||||
).inject(self.release_container)
|
||||
).inject(self.release_container);
|
||||
|
||||
release['el'] = item;
|
||||
|
||||
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
|
||||
if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched'))
|
||||
@@ -189,7 +190,7 @@ MA.Release = new Class({
|
||||
}
|
||||
|
||||
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){
|
||||
|
||||
|
||||
self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top');
|
||||
|
||||
self.trynext_container.adopt(
|
||||
@@ -318,6 +319,17 @@ MA.Release = new Class({
|
||||
Api.request('release.ignore', {
|
||||
'data': {
|
||||
'id': release.id
|
||||
},
|
||||
'onComplete': function(){
|
||||
var el = release.el;
|
||||
if(el.hasClass('failed') || el.hasClass('ignored')){
|
||||
el.removeClass('failed').removeClass('ignored');
|
||||
el.getElement('.release_status').set('text', 'available');
|
||||
}
|
||||
else {
|
||||
el.addClass('ignored');
|
||||
el.getElement('.release_status').set('text', 'ignored');
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
.movies > div {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
|
||||
.movies > div .message {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
@@ -20,11 +20,11 @@
|
||||
padding: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list > div:not(.description) {
|
||||
margin-right: -4px;
|
||||
}
|
||||
|
||||
|
||||
.movies .loading {
|
||||
display: block;
|
||||
padding: 20px 0 0 0;
|
||||
@@ -43,26 +43,26 @@
|
||||
margin-top: -20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.movies .loading .spinner {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
.movies .loading .message {
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.movies h2 {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.movies h2 {
|
||||
font-size: 25px;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.movies > .description {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
@@ -73,17 +73,17 @@
|
||||
.movies:hover > .description {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 860px) {
|
||||
.movies > .description {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list {
|
||||
padding: 20px 0 20px;
|
||||
}
|
||||
|
||||
|
||||
.home .movies {
|
||||
padding-top: 6px;
|
||||
}
|
||||
@@ -99,27 +99,27 @@
|
||||
transition-property: width, height;
|
||||
background: rgba(0,0,0,.2);
|
||||
}
|
||||
|
||||
|
||||
.movies.mass_edit_list .movie {
|
||||
padding-left: 22px;
|
||||
background: none;
|
||||
}
|
||||
|
||||
|
||||
.movies.details_list .movie {
|
||||
padding-left: 120px;
|
||||
}
|
||||
|
||||
|
||||
.movies.list_list .movie:not(.details_view),
|
||||
.movies.mass_edit_list .movie {
|
||||
height: 30px;
|
||||
border-bottom: 1px solid rgba(255,255,255,.15);
|
||||
}
|
||||
|
||||
|
||||
.movies.list_list .movie:last-child,
|
||||
.movies.mass_edit_list .movie:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list .movie {
|
||||
width: 16.66667%;
|
||||
height: auto;
|
||||
@@ -128,7 +128,7 @@
|
||||
padding: 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
.movies.thumbs_list .movie {
|
||||
width: 25%;
|
||||
@@ -165,7 +165,7 @@
|
||||
.movies.mass_edit_list .movie .data {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list .data {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@@ -176,7 +176,7 @@
|
||||
background: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list .movie:hover .data {
|
||||
background: rgba(0,0,0,0.9);
|
||||
}
|
||||
@@ -218,7 +218,7 @@
|
||||
.movies.mass_edit_list .poster {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list .poster {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -243,7 +243,7 @@
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
.movies .info {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
@@ -272,7 +272,7 @@
|
||||
display: inline-block;
|
||||
padding-right: 55px;
|
||||
}
|
||||
|
||||
|
||||
.movies .info .title span {
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
@@ -282,14 +282,14 @@
|
||||
height: 100%;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list .info .title span {
|
||||
white-space: normal;
|
||||
overflow: auto;
|
||||
height: auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.movies.thumbs_list .movie .info .title span,
|
||||
.movies.thumbs_list .movie .info .year {
|
||||
@@ -298,21 +298,21 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.movies.list_list .movie:not(.details_view) .info .title,
|
||||
.movies.mass_edit_list .info .title {
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list .movie:not(.no_thumbnail) .info {
|
||||
display: none;
|
||||
}
|
||||
.movies.thumbs_list .movie:hover .info {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list .info .title {
|
||||
font-size: 21px;
|
||||
word-wrap: break-word;
|
||||
@@ -334,7 +334,7 @@
|
||||
font-size: 1.25em;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list .info .year {
|
||||
font-size: 23px;
|
||||
margin: 0;
|
||||
@@ -344,7 +344,7 @@
|
||||
right: auto;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
|
||||
.touch_enabled .movies.list_list .movie .info .year {
|
||||
font-size: 1em;
|
||||
}
|
||||
@@ -371,24 +371,24 @@
|
||||
display: block;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
|
||||
.movies.list_list .movie:hover .data .quality {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.touch_enabled .movies.list_list .movie .data .quality {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
top: -4px;
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.movies .data .quality {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.movies .status_suggest .data .quality,
|
||||
.movies.thumbs_list .data .quality {
|
||||
display: none;
|
||||
@@ -417,13 +417,13 @@
|
||||
z-index: 1;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
|
||||
.movies .data .quality .available,
|
||||
.movies .data .quality .snatched {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.movies .data .quality .available { background-color: #578bc3; }
|
||||
.movies .data .quality .snatched { background-color: #369545; }
|
||||
.movies .data .quality .done {
|
||||
@@ -454,18 +454,18 @@
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.movies .movie:hover .data .actions,
|
||||
.touch_enabled .movies .movie .data .actions {
|
||||
opacity: 1;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
.movies.details_list .data .actions {
|
||||
top: auto;
|
||||
bottom: 18px;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie:hover .actions {
|
||||
opacity: 1;
|
||||
display: inline-block;
|
||||
@@ -475,7 +475,7 @@
|
||||
right: 10px;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie:hover .action { opacity: 0.6; }
|
||||
.movies .movie:hover .action:hover { opacity: 1; }
|
||||
|
||||
@@ -497,7 +497,7 @@
|
||||
.movies .data .action.delete { color: #e9b0b0; }
|
||||
.movies .data .action.directory { color: #ffed92; }
|
||||
.movies .data .action.readd { color: #c2fac5; }
|
||||
|
||||
|
||||
.movies.mass_edit_list .movie .data .actions {
|
||||
display: none;
|
||||
}
|
||||
@@ -558,13 +558,15 @@
|
||||
.movies .options .table .item {
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.movies .options .table .item.ignored span {
|
||||
.movies .options .table .item.ignored span,
|
||||
.movies .options .table .item.failed span {
|
||||
text-decoration: line-through;
|
||||
color: rgba(255,255,255,0.4);
|
||||
}
|
||||
.movies .options .table .item.ignored .delete:before {
|
||||
display: inline-block;
|
||||
content: "\e04b";
|
||||
.movies .options .table .item.ignored .delete:before,
|
||||
.movies .options .table .item.failed .delete:before {
|
||||
display: inline-block;
|
||||
content: "\e04b";
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
|
||||
@@ -616,7 +618,8 @@
|
||||
.movies .options .table a:hover { opacity: 1; }
|
||||
.movies .options .table a.download { color: #a7fbaf; }
|
||||
.movies .options .table a.delete { color: #fda3a3; }
|
||||
.movies .options .table .ignored a.delete { color: #b5fda3; }
|
||||
.movies .options .table .ignored a.delete,
|
||||
.movies .options .table .failed a.delete { color: #b5fda3; }
|
||||
|
||||
.movies .options .table .head > * {
|
||||
font-weight: bold;
|
||||
@@ -695,7 +698,7 @@
|
||||
.touch_enabled .movies .movie .trynext {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.movies .movie .trynext {
|
||||
display: none;
|
||||
@@ -709,14 +712,14 @@
|
||||
.touch_enabled .movies.details_list .movie .trynext {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
.movies.details_list .movie .trynext {
|
||||
background: #47515f;
|
||||
padding: 0;
|
||||
right: 0;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie .trynext a {
|
||||
background-position: 5px center;
|
||||
padding: 0 5px 0 25px;
|
||||
@@ -761,7 +764,7 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.movies .alph_nav .menus {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
@@ -785,7 +788,7 @@
|
||||
border-right: 1px solid rgba(255,255,255,.07);
|
||||
}
|
||||
|
||||
.movies .alph_nav .numbers li,
|
||||
.movies .alph_nav .numbers li,
|
||||
.movies .alph_nav .actions li {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
@@ -795,7 +798,7 @@
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
.movies .alph_nav .numbers li {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
@@ -804,14 +807,14 @@
|
||||
.movies .alph_nav .numbers li.letter_all {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
|
||||
.movies .alph_nav li.available {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
|
||||
}
|
||||
.movies .alph_nav li.active.available,
|
||||
.movies .alph_nav li.active.available,
|
||||
.movies .alph_nav li.available:hover {
|
||||
background: rgba(0,0,0,.1);
|
||||
}
|
||||
@@ -832,12 +835,12 @@
|
||||
.movies .alph_nav .search input:focus {
|
||||
background: rgba(0,0,0,.08);
|
||||
}
|
||||
|
||||
|
||||
.movies .alph_nav .search input::-webkit-input-placeholder {
|
||||
color: #444;
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
|
||||
.movies .alph_nav .search:before {
|
||||
font-family: 'Elusive-Icons';
|
||||
content: "\e03e";
|
||||
@@ -960,10 +963,10 @@
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.movies .alph_nav .more_menu.filter {
|
||||
}
|
||||
|
||||
|
||||
.movies .alph_nav .more_menu.filter > a:before {
|
||||
content: "\e0e8";
|
||||
font-family: 'Elusive-Icons';
|
||||
@@ -971,7 +974,7 @@
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.movies .alph_nav .more_menu.filter .wrapper {
|
||||
right: 88px;
|
||||
width: 300px;
|
||||
|
||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified, getParams, getParam
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Profile, ProfileType, Movie
|
||||
@@ -48,10 +47,10 @@ class ProfilePlugin(Plugin):
|
||||
|
||||
def allView(self):
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'list': self.all()
|
||||
})
|
||||
}
|
||||
|
||||
def all(self):
|
||||
|
||||
@@ -65,30 +64,28 @@ class ProfilePlugin(Plugin):
|
||||
db.expire_all()
|
||||
return temp
|
||||
|
||||
def save(self):
|
||||
|
||||
params = getParams()
|
||||
def save(self, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
|
||||
p = db.query(Profile).filter_by(id = params.get('id')).first()
|
||||
p = db.query(Profile).filter_by(id = kwargs.get('id')).first()
|
||||
if not p:
|
||||
p = Profile()
|
||||
db.add(p)
|
||||
|
||||
p.label = toUnicode(params.get('label'))
|
||||
p.order = params.get('order', p.order if p.order else 0)
|
||||
p.core = params.get('core', False)
|
||||
p.label = toUnicode(kwargs.get('label'))
|
||||
p.order = kwargs.get('order', p.order if p.order else 0)
|
||||
p.core = kwargs.get('core', False)
|
||||
|
||||
#delete old types
|
||||
[db.delete(t) for t in p.types]
|
||||
|
||||
order = 0
|
||||
for type in params.get('types', []):
|
||||
for type in kwargs.get('types', []):
|
||||
t = ProfileType(
|
||||
order = order,
|
||||
finish = type.get('finish') if order > 0 else 1,
|
||||
wait_for = params.get('wait_for'),
|
||||
wait_for = kwargs.get('wait_for'),
|
||||
quality_id = type.get('quality_id')
|
||||
)
|
||||
p.types.append(t)
|
||||
@@ -99,10 +96,10 @@ class ProfilePlugin(Plugin):
|
||||
|
||||
profile_dict = p.to_dict(self.to_dict)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'profile': profile_dict
|
||||
})
|
||||
}
|
||||
|
||||
def default(self):
|
||||
|
||||
@@ -113,28 +110,25 @@ class ProfilePlugin(Plugin):
|
||||
db.expire_all()
|
||||
return default_dict
|
||||
|
||||
def saveOrder(self):
|
||||
def saveOrder(self, **kwargs):
|
||||
|
||||
params = getParams()
|
||||
db = get_session()
|
||||
|
||||
order = 0
|
||||
for profile in params.get('ids', []):
|
||||
for profile in kwargs.get('ids', []):
|
||||
p = db.query(Profile).filter_by(id = profile).first()
|
||||
p.hide = params.get('hidden')[order]
|
||||
p.hide = kwargs.get('hidden')[order]
|
||||
p.order = order
|
||||
|
||||
order += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def delete(self):
|
||||
|
||||
id = getParam('id')
|
||||
def delete(self, id = None):
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -154,10 +148,10 @@ class ProfilePlugin(Plugin):
|
||||
message = log.error('Failed deleting Profile: %s', e)
|
||||
|
||||
db.expire_all()
|
||||
return jsonified({
|
||||
return {
|
||||
'success': success,
|
||||
'message': message
|
||||
})
|
||||
}
|
||||
|
||||
def fill(self):
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified, getParams
|
||||
from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -53,10 +52,10 @@ class QualityPlugin(Plugin):
|
||||
|
||||
def allView(self):
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'list': self.all()
|
||||
})
|
||||
}
|
||||
|
||||
def all(self):
|
||||
|
||||
@@ -88,20 +87,18 @@ class QualityPlugin(Plugin):
|
||||
if identifier == q.get('identifier'):
|
||||
return q
|
||||
|
||||
def saveSize(self):
|
||||
|
||||
params = getParams()
|
||||
def saveSize(self, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
quality = db.query(Quality).filter_by(identifier = params.get('identifier')).first()
|
||||
quality = db.query(Quality).filter_by(identifier = kwargs.get('identifier')).first()
|
||||
|
||||
if quality:
|
||||
setattr(quality, params.get('value_type'), params.get('value'))
|
||||
setattr(quality, kwargs.get('value_type'), kwargs.get('value'))
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def fill(self):
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.request import getParam, jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.plugins.scanner.main import Scanner
|
||||
@@ -108,13 +107,11 @@ class Release(Plugin):
|
||||
# Check database and update/insert if necessary
|
||||
return fireEvent('file.add', path = filepath, part = fireEvent('scanner.partnumber', file, single = True), type_tuple = Scanner.file_types.get(type), properties = properties, single = True)
|
||||
|
||||
def deleteView(self):
|
||||
def deleteView(self, id = None, **kwargs):
|
||||
|
||||
release_id = getParam('id')
|
||||
|
||||
return jsonified({
|
||||
'success': self.delete(release_id)
|
||||
})
|
||||
return {
|
||||
'success': self.delete(id)
|
||||
}
|
||||
|
||||
def delete(self, id):
|
||||
|
||||
@@ -146,25 +143,23 @@ class Release(Plugin):
|
||||
|
||||
return False
|
||||
|
||||
def ignore(self):
|
||||
def ignore(self, id = None, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
id = getParam('id')
|
||||
|
||||
rel = db.query(Relea).filter_by(id = id).first()
|
||||
if rel:
|
||||
ignored_status, available_status = fireEvent('status.get', ['ignored', 'available'], single = True)
|
||||
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id')
|
||||
ignored_status, failed_status, available_status = fireEvent('status.get', ['ignored', 'failed', 'available'], single = True)
|
||||
rel.status_id = available_status.get('id') if rel.status_id in [ignored_status.get('id'), failed_status.get('id')] else ignored_status.get('id')
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def download(self):
|
||||
def download(self, id = None, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
id = getParam('id')
|
||||
|
||||
snatched_status, done_status = fireEvent('status.get', ['snatched', 'done'], single = True)
|
||||
|
||||
@@ -199,12 +194,12 @@ class Release(Plugin):
|
||||
|
||||
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name'])
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': success
|
||||
})
|
||||
}
|
||||
else:
|
||||
log.error('Couldn\'t find release with id: %s', id)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': False
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,10 +14,14 @@ rename_options = {
|
||||
'year': 'Year (2011)',
|
||||
'first': 'First letter (M)',
|
||||
'quality': 'Quality (720p)',
|
||||
'quality_type': '(HD) or (SD)',
|
||||
'video': 'Video (x264)',
|
||||
'audio': 'Audio (DTS)',
|
||||
'group': 'Releasegroup name',
|
||||
'source': 'Source media (Bluray)',
|
||||
'resolution_width': 'resolution width (1280)',
|
||||
'resolution_height': 'resolution height (720)',
|
||||
'audio_channels': 'audio channels (7.1)',
|
||||
'original': 'Original filename',
|
||||
'original_folder': 'Original foldername',
|
||||
'imdb_id': 'IMDB id (tt0123456)',
|
||||
|
||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import toUnicode, ss
|
||||
from couchpotato.core.helpers.request import getParams, jsonified
|
||||
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
|
||||
getImdb, link, symlink, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
@@ -59,13 +58,12 @@ class Renamer(Plugin):
|
||||
|
||||
return True
|
||||
|
||||
def scanView(self):
|
||||
def scanView(self, **kwargs):
|
||||
|
||||
params = getParams()
|
||||
async = tryInt(params.get('async', None))
|
||||
movie_folder = params.get('movie_folder', None)
|
||||
downloader = params.get('downloader', None)
|
||||
download_id = params.get('download_id', None)
|
||||
async = tryInt(kwargs.get('async', None))
|
||||
movie_folder = kwargs.get('movie_folder', None)
|
||||
downloader = kwargs.get('downloader', None)
|
||||
download_id = kwargs.get('download_id', None)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def scan(self, movie_folder = None, download_info = None):
|
||||
|
||||
@@ -183,6 +181,7 @@ class Renamer(Plugin):
|
||||
'source': group['meta_data']['source'],
|
||||
'resolution_width': group['meta_data'].get('resolution_width'),
|
||||
'resolution_height': group['meta_data'].get('resolution_height'),
|
||||
'audio_channels': group['meta_data'].get('audio_channels'),
|
||||
'imdb_id': library['identifier'],
|
||||
'cd': '',
|
||||
'cd_nr': '',
|
||||
|
||||
@@ -432,6 +432,7 @@ class Scanner(Plugin):
|
||||
data['audio'] = meta.get('audio', self.getCodec(cur_file, self.codecs['audio']))
|
||||
data['resolution_width'] = meta.get('resolution_width', 720)
|
||||
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)
|
||||
except:
|
||||
log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc()))
|
||||
@@ -476,6 +477,7 @@ class Scanner(Plugin):
|
||||
'audio': ac,
|
||||
'resolution_width': tryInt(p.video[0].width),
|
||||
'resolution_height': tryInt(p.video[0].height),
|
||||
'audio_channels': p.audio[0].channels,
|
||||
}
|
||||
except ParseError:
|
||||
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)
|
||||
|
||||
if len(movie) > 0:
|
||||
imdb_id = movie[0]['imdb']
|
||||
imdb_id = movie[0].get('imdb')
|
||||
if imdb_id:
|
||||
log.debug('Found movie via OpenSubtitleHash: %s', cur_file)
|
||||
break
|
||||
@@ -600,7 +602,7 @@ class Scanner(Plugin):
|
||||
movie = fireEvent('movie.search', q = '%(name)s %(year)s' % name_year, merge = True, limit = 1)
|
||||
|
||||
if len(movie) > 0:
|
||||
imdb_id = movie[0]['imdb']
|
||||
imdb_id = movie[0].get('imdb')
|
||||
log.debug('Found movie via search: %s', cur_file)
|
||||
if imdb_id: break
|
||||
else:
|
||||
|
||||
@@ -18,7 +18,7 @@ class Score(Plugin):
|
||||
def calculate(self, nzb, movie):
|
||||
''' 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']:
|
||||
score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title['title']))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from couchpotato.core.event import fireEvent
|
||||
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
|
||||
import re
|
||||
|
||||
@@ -42,10 +42,8 @@ def nameScore(name, year):
|
||||
|
||||
# Contains preferred word
|
||||
nzb_words = re.split('\W+', simplifyString(name))
|
||||
preferred_words = [x.strip() for x in Env.setting('preferred_words', section = 'searcher').split(',')]
|
||||
for word in preferred_words:
|
||||
if word.strip() and word.strip().lower() in nzb_words:
|
||||
score = score + 100
|
||||
preferred_words = splitString(Env.setting('preferred_words', section = 'searcher'))
|
||||
score += 100 * len(list(set(nzb_words) & set(preferred_words)))
|
||||
|
||||
return score
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import md5, getTitle, splitString, \
|
||||
possibleTitles
|
||||
from couchpotato.core.logger import CPLog
|
||||
@@ -67,15 +66,15 @@ class Searcher(Plugin):
|
||||
else:
|
||||
fireEvent('notify.frontend', type = 'searcher.already_started', data = True, message = 'Full search already in progress')
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': not in_progress
|
||||
})
|
||||
}
|
||||
|
||||
def getProgress(self):
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'progress': self.in_progress
|
||||
})
|
||||
}
|
||||
|
||||
def allMovies(self):
|
||||
|
||||
@@ -146,9 +145,10 @@ class Searcher(Plugin):
|
||||
|
||||
pre_releases = fireEvent('quality.pre_releases', single = 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 = []
|
||||
too_early_to_search = []
|
||||
|
||||
default_title = getTitle(movie['library'])
|
||||
if not default_title:
|
||||
@@ -162,14 +162,14 @@ class Searcher(Plugin):
|
||||
ret = False
|
||||
for quality_type in movie['profile']['types']:
|
||||
if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates):
|
||||
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
|
||||
|
||||
has_better_quality = 0
|
||||
|
||||
# See if better quality is available
|
||||
for release in movie['releases']:
|
||||
if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id')]:
|
||||
if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id'), failed_status.get('id')]:
|
||||
has_better_quality += 1
|
||||
|
||||
# Don't search for quality lower then already available.
|
||||
@@ -240,7 +240,7 @@ class Searcher(Plugin):
|
||||
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
|
||||
continue
|
||||
|
||||
if nzb['status_id'] == ignored_status.get('id'):
|
||||
if nzb['status_id'] in [ignored_status.get('id'), failed_status.get('id')]:
|
||||
log.info('Ignored: %s', nzb['name'])
|
||||
continue
|
||||
|
||||
@@ -269,6 +269,9 @@ class Searcher(Plugin):
|
||||
if self.shuttingDown() or ret:
|
||||
break
|
||||
|
||||
if len(too_early_to_search) > 0:
|
||||
log.info2('Too early to search for %s, %s', (too_early_to_search, default_title))
|
||||
|
||||
fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
|
||||
|
||||
return ret
|
||||
@@ -586,18 +589,17 @@ class Searcher(Plugin):
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
def tryNextRelease(self, movie_id, manual = False):
|
||||
|
||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
||||
ignored_status = fireEvent('status.get', 'ignored', single = True)
|
||||
snatched_status, ignored_status = fireEvent('status.get', ['snatched', 'ignored'], single = True)
|
||||
|
||||
try:
|
||||
db = get_session()
|
||||
|
||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Status
|
||||
@@ -44,10 +43,10 @@ class StatusPlugin(Plugin):
|
||||
|
||||
def list(self):
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'list': self.all()
|
||||
})
|
||||
}
|
||||
|
||||
def getById(self, id):
|
||||
db = get_session()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
|
||||
class Suggestion(Plugin):
|
||||
@@ -9,14 +8,13 @@ class Suggestion(Plugin):
|
||||
|
||||
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)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'total': total_movies,
|
||||
'movies': movies,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
var includes = {{includes|tojson}};
|
||||
var excludes = {{excludes|tojson}};
|
||||
{% autoescape None %}
|
||||
|
||||
var includes = {{ json_encode(includes) }};
|
||||
var excludes = {{ json_encode(excludes) }};
|
||||
|
||||
var specialChars = '\\{}+.():-|^$';
|
||||
var makeRegex = function(pattern) {
|
||||
@@ -20,6 +22,8 @@ var makeRegex = function(pattern) {
|
||||
|
||||
var isCorrectUrl = function() {
|
||||
for(i in includes) {
|
||||
if(!includes.hasOwnProperty(i)) continue;
|
||||
|
||||
var reg = includes[i]
|
||||
if (makeRegex(reg).test(document.location.href))
|
||||
return true;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
from couchpotato import index
|
||||
from couchpotato.api import addApiView
|
||||
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.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
|
||||
from flask.templating import render_template
|
||||
from tornado.web import RequestHandler
|
||||
import os
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -18,7 +16,8 @@ class Userscript(Plugin):
|
||||
version = 3
|
||||
|
||||
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.add_via_url', self.getViaUrl)
|
||||
addApiView('userscript.includes', self.getIncludes)
|
||||
@@ -26,38 +25,46 @@ class Userscript(Plugin):
|
||||
|
||||
addEvent('userscript.get_version', self.getVersion)
|
||||
|
||||
def bookmark(self):
|
||||
def bookmark(self, host = None, **kwargs):
|
||||
|
||||
params = {
|
||||
'includes': fireEvent('userscript.get_includes', merge = True),
|
||||
'excludes': fireEvent('userscript.get_excludes', merge = True),
|
||||
'host': getParam('host', None),
|
||||
'host': host,
|
||||
}
|
||||
|
||||
return self.renderTemplate(__file__, 'bookmark.js', **params)
|
||||
|
||||
def getIncludes(self):
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'includes': fireEvent('userscript.get_includes', merge = True),
|
||||
'excludes': fireEvent('userscript.get_excludes', merge = True),
|
||||
})
|
||||
|
||||
def getUserScript(self, random = '', filename = ''):
|
||||
|
||||
params = {
|
||||
'includes': fireEvent('userscript.get_includes', merge = True),
|
||||
'excludes': fireEvent('userscript.get_excludes', merge = True),
|
||||
'version': self.getVersion(),
|
||||
'api': '%suserscript/' % url_for('api.index').lstrip('/'),
|
||||
'host': request.host_url,
|
||||
}
|
||||
|
||||
script = self.renderTemplate(__file__, 'template.js', **params)
|
||||
self.createFile(os.path.join(Env.get('cache_dir'), 'couchpotato.user.js'), script)
|
||||
def getUserScript(self, route):
|
||||
|
||||
klass = self
|
||||
|
||||
class UserscriptHandler(RequestHandler):
|
||||
|
||||
def get(self, random, route):
|
||||
|
||||
params = {
|
||||
'includes': fireEvent('userscript.get_includes', merge = True),
|
||||
'excludes': fireEvent('userscript.get_excludes', merge = True),
|
||||
'version': klass.getVersion(),
|
||||
'api': '%suserscript/' % Env.get('api_base'),
|
||||
'host': '%s://%s' % (self.request.protocol, self.request.host),
|
||||
}
|
||||
|
||||
script = klass.renderTemplate(__file__, 'template.js', **params)
|
||||
klass.createFile(os.path.join(Env.get('cache_dir'), 'couchpotato.user.js'), script)
|
||||
|
||||
self.redirect(Env.get('api_base') + 'file.cache/couchpotato.user.js')
|
||||
|
||||
Env.get('app').add_handlers(".*$", [('%s%s' % (Env.get('api_base'), route), UserscriptHandler)])
|
||||
|
||||
from flask.helpers import send_from_directory
|
||||
return send_from_directory(Env.get('cache_dir'), 'couchpotato.user.js')
|
||||
|
||||
def getVersion(self):
|
||||
|
||||
@@ -69,12 +76,12 @@ class Userscript(Plugin):
|
||||
|
||||
return version
|
||||
|
||||
def iFrame(self):
|
||||
return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env)
|
||||
def iFrame(self, **kwargs):
|
||||
return index()
|
||||
|
||||
def getViaUrl(self):
|
||||
def getViaUrl(self, url = None, **kwargs):
|
||||
|
||||
url = getParam('url')
|
||||
print url
|
||||
|
||||
params = {
|
||||
'url': url,
|
||||
@@ -84,4 +91,4 @@ class Userscript(Plugin):
|
||||
log.error('Failed adding movie via url: %s', url)
|
||||
params['error'] = params['movie'] if params['movie'] else 'Failed getting movie info'
|
||||
|
||||
return jsonified(params)
|
||||
return params
|
||||
|
||||
@@ -9,15 +9,16 @@
|
||||
// @grant none
|
||||
// @version {{version}}
|
||||
|
||||
// @match {{host}}*
|
||||
// @match {{host}}/*
|
||||
{% for include in includes %}
|
||||
// @match {{include}}{% endfor %}
|
||||
// @match {{include}}{% end %}
|
||||
{% for exclude in excludes %}
|
||||
// @exclude {{exclude}}{% endfor %}
|
||||
// @exclude {{exclude}}{% end %}
|
||||
// @exclude {{host}}{{api.rstrip('/')}}*
|
||||
|
||||
// ==/UserScript==
|
||||
|
||||
{% autoescape None %}
|
||||
if (window.top == window.self){ // Only run on top window
|
||||
|
||||
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):
|
||||
if not movie.get('rating'):
|
||||
log.info('ignoring %s as no rating is available for.', (movie['original_title']))
|
||||
return False
|
||||
|
||||
if movie['rating'] and movie['rating'].get('imdb'):
|
||||
|
||||
@@ -11,7 +11,7 @@ config = [{
|
||||
'list': 'watchlist_providers',
|
||||
'name': 'imdb_automation',
|
||||
'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': [
|
||||
{
|
||||
'name': 'automation_enabled',
|
||||
|
||||
@@ -86,6 +86,7 @@ class YarrProvider(Provider):
|
||||
sizeKb = ['kb', 'kib']
|
||||
|
||||
login_opener = None
|
||||
last_login_check = 0
|
||||
|
||||
def __init__(self):
|
||||
addEvent('provider.enabled_types', self.getEnabledProviderType)
|
||||
@@ -101,17 +102,29 @@ class YarrProvider(Provider):
|
||||
|
||||
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:
|
||||
cookiejar = cookielib.CookieJar()
|
||||
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))
|
||||
opener.addheaders = [('User-Agent', self.user_agent)]
|
||||
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()
|
||||
output = self.urlopen(self.urls['login'], params = self.getLoginParams(), opener = opener)
|
||||
|
||||
if self.loginSuccess(output):
|
||||
self.last_login_check = now
|
||||
self.login_opener = opener
|
||||
return True
|
||||
|
||||
@@ -119,15 +132,19 @@ class YarrProvider(Provider):
|
||||
except:
|
||||
error = traceback.format_exc()
|
||||
|
||||
self.login_opener = None
|
||||
log.error('Failed to login %s: %s', (self.getName(), error))
|
||||
return False
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return True
|
||||
|
||||
def loginCheckSuccess(self, output):
|
||||
return True
|
||||
|
||||
def loginDownload(self, url = '', nzb_id = ''):
|
||||
try:
|
||||
if not self.login_opener and not self.login():
|
||||
if not self.login():
|
||||
log.error('Failed downloading from %s', self.getName())
|
||||
return self.urlopen(url, opener = self.login_opener)
|
||||
except:
|
||||
@@ -150,7 +167,7 @@ class YarrProvider(Provider):
|
||||
return []
|
||||
|
||||
# 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())
|
||||
return []
|
||||
|
||||
@@ -258,7 +275,7 @@ class ResultList(list):
|
||||
'id': 0,
|
||||
'type': self.provider.type,
|
||||
'provider': self.provider.getName(),
|
||||
'download': self.provider.download,
|
||||
'download': self.provider.loginDownload if self.provider.urls.get('login') else self.provider.download,
|
||||
'url': '',
|
||||
'name': '',
|
||||
'age': 0,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.request import jsonified, getParams
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.movie.base import MovieProvider
|
||||
from couchpotato.core.settings.model import Movie
|
||||
@@ -50,8 +49,8 @@ class CouchPotatoApi(MovieProvider):
|
||||
'branch': branch,
|
||||
}), headers = self.getRequestHeaders())
|
||||
|
||||
def search(self, q, limit = 12):
|
||||
return self.getJsonData(self.urls['search'] % tryUrlencode(q), headers = self.getRequestHeaders())
|
||||
def search(self, q, limit = 5):
|
||||
return self.getJsonData(self.urls['search'] % tryUrlencode(q) + ('?limit=%s' % limit), headers = self.getRequestHeaders())
|
||||
|
||||
def isMovie(self, identifier = None):
|
||||
|
||||
@@ -86,17 +85,16 @@ class CouchPotatoApi(MovieProvider):
|
||||
def suggest(self, movies = [], ignore = []):
|
||||
suggestions = self.getJsonData(self.urls['suggest'], params = {
|
||||
'movies': ','.join(movies),
|
||||
#'ignore': ','.join(ignore),
|
||||
'ignore': ','.join(ignore),
|
||||
})
|
||||
log.info('Found Suggestions for %s', (suggestions))
|
||||
|
||||
return suggestions
|
||||
|
||||
def suggestView(self):
|
||||
def suggestView(self, **kwargs):
|
||||
|
||||
params = getParams()
|
||||
movies = params.get('movies')
|
||||
ignore = params.get('ignore', [])
|
||||
movies = kwargs.get('movies')
|
||||
ignore = kwargs.get('ignore', [])
|
||||
|
||||
if not movies:
|
||||
db = get_session()
|
||||
@@ -105,11 +103,11 @@ class CouchPotatoApi(MovieProvider):
|
||||
|
||||
suggestions = self.suggest(movies, ignore)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'count': len(suggestions),
|
||||
'suggestions': suggestions
|
||||
})
|
||||
}
|
||||
|
||||
def getRequestHeaders(self):
|
||||
return {
|
||||
|
||||
@@ -2,7 +2,7 @@ from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.movie.base import MovieProvider
|
||||
from libs.themoviedb import tmdb
|
||||
from themoviedb import tmdb
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -16,6 +16,7 @@ class FTDWorld(NZBProvider):
|
||||
'detail': 'http://ftdworld.net/spotinfo.php?id=%s',
|
||||
'download': 'http://ftdworld.net/cgi-bin/nzbdown.pl?fileID=%s',
|
||||
'login': 'http://ftdworld.net/api/login.php',
|
||||
'login_check': 'http://ftdworld.net/api/login.php',
|
||||
}
|
||||
|
||||
http_time_between_calls = 3 #seconds
|
||||
@@ -58,7 +59,6 @@ class FTDWorld(NZBProvider):
|
||||
'age': self.calculateAge(tryInt(item.get('Created'))),
|
||||
'size': item.get('Size', 0),
|
||||
'url': self.urls['download'] % nzb_id,
|
||||
'download': self.loginDownload,
|
||||
'detail_url': self.urls['detail'] % nzb_id,
|
||||
'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)
|
||||
except:
|
||||
return False
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
|
||||
@@ -53,11 +53,20 @@ class Newznab(NZBProvider, RSS):
|
||||
for nzb in nzbs:
|
||||
|
||||
date = None
|
||||
spotter = None
|
||||
for item in nzb:
|
||||
if date and spotter:
|
||||
break
|
||||
if item.attrib.get('name') == 'usenetdate':
|
||||
date = item.attrib.get('value')
|
||||
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:
|
||||
date = self.getTextElement(nzb, 'pubDate')
|
||||
|
||||
@@ -67,10 +76,15 @@ class Newznab(NZBProvider, RSS):
|
||||
if not name:
|
||||
continue
|
||||
|
||||
name_extra = ''
|
||||
if spotter:
|
||||
name_extra = spotter
|
||||
|
||||
results.append({
|
||||
'id': nzb_id,
|
||||
'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()))),
|
||||
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
|
||||
'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):
|
||||
|
||||
uses = splitString(str(self.conf('use')))
|
||||
hosts = splitString(self.conf('host'))
|
||||
api_keys = splitString(self.conf('api_key'))
|
||||
extra_score = splitString(self.conf('extra_score'))
|
||||
uses = splitString(str(self.conf('use')), clean = False)
|
||||
hosts = splitString(self.conf('host'), clean = False)
|
||||
api_keys = splitString(self.conf('api_key'), clean = False)
|
||||
extra_score = splitString(self.conf('extra_score'), clean = False)
|
||||
|
||||
list = []
|
||||
for nr in range(len(hosts)):
|
||||
|
||||
try: key = api_keys[nr]
|
||||
except: key = ''
|
||||
|
||||
try: host = hosts[nr]
|
||||
except: host = ''
|
||||
|
||||
list.append({
|
||||
'use': uses[nr],
|
||||
'host': hosts[nr],
|
||||
'api_key': api_keys[nr],
|
||||
'host': host,
|
||||
'api_key': key,
|
||||
'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0
|
||||
})
|
||||
|
||||
|
||||
59
couchpotato/core/providers/torrent/awesomehd/__init__.py
Normal file
59
couchpotato/core/providers/torrent/awesomehd/__init__.py
Normal file
@@ -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.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
|
||||
64
couchpotato/core/providers/torrent/awesomehd/main.py
Normal file
64
couchpotato/core/providers/torrent/awesomehd/main.py
Normal file
@@ -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',
|
||||
'search' : 'https://hdbits.org/json_search.php?imdb=%s',
|
||||
'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
|
||||
@@ -30,7 +31,7 @@ class HDBits(TorrentProvider):
|
||||
results.append({
|
||||
'id': result['id'],
|
||||
'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'],
|
||||
'size': self.parseSize(result['size']),
|
||||
'seeders': tryInt(result['seeder']),
|
||||
@@ -53,3 +54,5 @@ class HDBits(TorrentProvider):
|
||||
|
||||
def loginSuccess(self, output):
|
||||
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
|
||||
import traceback
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
@@ -15,7 +14,8 @@ class IPTorrents(TorrentProvider):
|
||||
'test' : 'http://www.iptorrents.com/',
|
||||
'base_url' : 'http://www.iptorrents.com',
|
||||
'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 = [
|
||||
@@ -32,48 +32,62 @@ class IPTorrents(TorrentProvider):
|
||||
|
||||
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'])))
|
||||
data = self.getHTMLData(url, opener = self.login_opener)
|
||||
pages = 1
|
||||
current_page = 1
|
||||
while current_page <= pages and not self.shuttingDown():
|
||||
|
||||
if data:
|
||||
html = BeautifulSoup(data)
|
||||
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)
|
||||
|
||||
try:
|
||||
result_table = html.find('table', attrs = {'class' : 'torrents'})
|
||||
if data:
|
||||
html = BeautifulSoup(data)
|
||||
|
||||
if not result_table or 'nothing found!' in data.lower():
|
||||
return
|
||||
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)
|
||||
|
||||
entries = result_table.find_all('tr')
|
||||
result_table = html.find('table', attrs = {'class' : 'torrents'})
|
||||
|
||||
for result in entries[1:]:
|
||||
if not result_table or 'nothing found!' in data.lower():
|
||||
return
|
||||
|
||||
torrent = result.find_all('td')[1].find('a')
|
||||
entries = result_table.find_all('tr')
|
||||
|
||||
torrent_id = torrent['href'].replace('/details.php?id=', '')
|
||||
torrent_name = torrent.string
|
||||
torrent_download_url = self.urls['base_url'] + (result.find_all('td')[3].find('a'))['href'].replace(' ', '.')
|
||||
torrent_details_url = self.urls['base_url'] + torrent['href']
|
||||
torrent_size = self.parseSize(result.find_all('td')[5].string)
|
||||
torrent_seeders = tryInt(result.find('td', attrs = {'class' : 'ac t_seeders'}).string)
|
||||
torrent_leechers = tryInt(result.find('td', attrs = {'class' : 'ac t_leechers'}).string)
|
||||
for result in entries[1:]:
|
||||
|
||||
results.append({
|
||||
'id': torrent_id,
|
||||
'name': torrent_name,
|
||||
'url': torrent_download_url,
|
||||
'detail_url': torrent_details_url,
|
||||
'download': self.loginDownload,
|
||||
'size': torrent_size,
|
||||
'seeders': torrent_seeders,
|
||||
'leechers': torrent_leechers,
|
||||
})
|
||||
torrent = result.find_all('td')
|
||||
if len(torrent) <= 1:
|
||||
break
|
||||
|
||||
except:
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
torrent = torrent[1].find('a')
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return 'don\'t have an account' not in output.lower()
|
||||
torrent_id = torrent['href'].replace('/details.php?id=', '')
|
||||
torrent_name = torrent.string
|
||||
torrent_download_url = self.urls['base_url'] + (result.find_all('td')[3].find('a'))['href'].replace(' ', '.')
|
||||
torrent_details_url = self.urls['base_url'] + torrent['href']
|
||||
torrent_size = self.parseSize(result.find_all('td')[5].string)
|
||||
torrent_seeders = tryInt(result.find('td', attrs = {'class' : 'ac t_seeders'}).string)
|
||||
torrent_leechers = tryInt(result.find('td', attrs = {'class' : 'ac t_leechers'}).string)
|
||||
|
||||
results.append({
|
||||
'id': torrent_id,
|
||||
'name': torrent_name,
|
||||
'url': torrent_download_url,
|
||||
'detail_url': torrent_details_url,
|
||||
'size': torrent_size,
|
||||
'seeders': torrent_seeders,
|
||||
'leechers': torrent_leechers,
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
break
|
||||
|
||||
current_page += 1
|
||||
|
||||
def getLoginParams(self):
|
||||
return tryUrlencode({
|
||||
@@ -81,3 +95,9 @@ class IPTorrents(TorrentProvider):
|
||||
'password': self.conf('password'),
|
||||
'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):
|
||||
|
||||
urls = {
|
||||
'test': 'https://kat.ph/',
|
||||
'detail': 'https://kat.ph/%s',
|
||||
'search': 'https://kat.ph/%s-i%s/',
|
||||
'test': 'https://kickass.to/',
|
||||
'detail': 'https://kickass.to/%s',
|
||||
'search': 'https://kickass.to/%s-i%s/',
|
||||
}
|
||||
|
||||
cat_ids = [
|
||||
|
||||
@@ -3,13 +3,11 @@ from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||
from dateutil.parser import parse
|
||||
import cookielib
|
||||
import htmlentitydefs
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
import urllib2
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -21,9 +19,12 @@ class PassThePopcorn(TorrentProvider):
|
||||
'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s',
|
||||
'torrent': 'https://tls.passthepopcorn.me/torrents.php',
|
||||
'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'
|
||||
}
|
||||
|
||||
http_time_between_calls = 2
|
||||
|
||||
quality_search_params = {
|
||||
'bd50': {'media': 'Blu-ray', 'format': 'BD50'},
|
||||
'1080p': {'resolution': '1080p'},
|
||||
@@ -52,18 +53,6 @@ class PassThePopcorn(TorrentProvider):
|
||||
'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):
|
||||
|
||||
movie_title = getTitle(movie['library'])
|
||||
@@ -75,17 +64,8 @@ class PassThePopcorn(TorrentProvider):
|
||||
'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))
|
||||
txt = self.urlopen(url, opener = self.login_opener)
|
||||
res = json.loads(txt)
|
||||
except:
|
||||
log.error('Search on PassThePopcorn.me (%s) failed (could not decode JSON)', params)
|
||||
return
|
||||
url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params))
|
||||
res = self.getJsonData(url, opener = self.login_opener)
|
||||
|
||||
try:
|
||||
if not 'Movies' in res:
|
||||
@@ -136,40 +116,11 @@ class PassThePopcorn(TorrentProvider):
|
||||
'leechers': tryInt(torrent['Leechers']),
|
||||
'score': torrentscore,
|
||||
'extra_check': extra_check,
|
||||
'download': self.loginDownload,
|
||||
})
|
||||
|
||||
except:
|
||||
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):
|
||||
|
||||
if not quality in self.post_search_filters:
|
||||
@@ -186,7 +137,7 @@ class PassThePopcorn(TorrentProvider):
|
||||
seen_one = False
|
||||
|
||||
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
|
||||
|
||||
for spec in specs:
|
||||
@@ -244,3 +195,11 @@ class PassThePopcorn(TorrentProvider):
|
||||
'keeplogged': '1',
|
||||
'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 = {
|
||||
'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',
|
||||
'search': 'https://www.sceneaccess.eu/browse?method=2&c%d=%d',
|
||||
'download': 'https://www.sceneaccess.eu/%s',
|
||||
@@ -39,9 +40,6 @@ class SceneAccess(TorrentProvider):
|
||||
})
|
||||
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)
|
||||
|
||||
@@ -69,7 +67,6 @@ class SceneAccess(TorrentProvider):
|
||||
'size': self.parseSize(result.find('td', attrs = {'class' : 'ttr_size'}).contents[0]),
|
||||
'seeders': tryInt(result.find('td', attrs = {'class' : 'ttr_seeders'}).find('a').string),
|
||||
'leechers': tryInt(leechers.string) if leechers else 0,
|
||||
'download': self.loginDownload,
|
||||
'get_more_info': self.getMoreInfo,
|
||||
})
|
||||
|
||||
@@ -91,3 +88,8 @@ class SceneAccess(TorrentProvider):
|
||||
|
||||
item['description'] = description
|
||||
return item
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return '/inbox' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
@@ -13,6 +13,7 @@ class SceneHD(TorrentProvider):
|
||||
urls = {
|
||||
'test': 'https://scenehd.org/',
|
||||
'login' : 'https://scenehd.org/takelogin.php',
|
||||
'login_check': 'https://scenehd.org/my.php',
|
||||
'detail': 'https://scenehd.org/details.php?id=%s',
|
||||
'search': 'https://scenehd.org/browse.php?ajax',
|
||||
'download': 'https://scenehd.org/download.php?id=%s',
|
||||
@@ -28,10 +29,6 @@ class SceneHD(TorrentProvider):
|
||||
})
|
||||
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)
|
||||
|
||||
if data:
|
||||
@@ -61,7 +58,6 @@ class SceneHD(TorrentProvider):
|
||||
'seeders': tryInt(all_cells[10].find('a').string),
|
||||
'leechers': tryInt(leechers),
|
||||
'url': self.urls['download'] % torrent_id,
|
||||
'download': self.loginDownload,
|
||||
'description': all_cells[1].find('a')['href'],
|
||||
})
|
||||
|
||||
@@ -75,3 +71,9 @@ class SceneHD(TorrentProvider):
|
||||
'password': self.conf('password'),
|
||||
'ssl': 'yes',
|
||||
})
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return 'logout.php' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
|
||||
42
couchpotato/core/providers/torrent/torrentbytes/__init__.py
Normal file
42
couchpotato/core/providers/torrent/torrentbytes/__init__.py
Normal file
@@ -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.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
82
couchpotato/core/providers/torrent/torrentbytes/main.py
Normal file
82
couchpotato/core/providers/torrent/torrentbytes/main.py
Normal file
@@ -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 = {
|
||||
'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',
|
||||
'search': 'http://www.td.af/V3/API/API.php',
|
||||
'download': 'http://www.td.af/download.php/%s/%s',
|
||||
@@ -50,7 +51,6 @@ class TorrentDay(TorrentProvider):
|
||||
'size': self.parseSize(torrent.get('size')),
|
||||
'seeders': tryInt(torrent.get('seed')),
|
||||
'leechers': tryInt(torrent.get('leech')),
|
||||
'download': self.loginDownload,
|
||||
})
|
||||
|
||||
def getLoginParams(self):
|
||||
@@ -62,3 +62,6 @@ class TorrentDay(TorrentProvider):
|
||||
|
||||
def loginSuccess(self, 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 = {
|
||||
'test' : 'http://www.torrentleech.org/',
|
||||
'login' : 'http://www.torrentleech.org/user/account/login/',
|
||||
'login_check': 'http://torrentleech.org/user/messages',
|
||||
'detail' : 'http://www.torrentleech.org/torrent/%s',
|
||||
'search' : 'http://www.torrentleech.org/torrents/browse/index/query/%s/categories/%d',
|
||||
'download' : 'http://www.torrentleech.org%s',
|
||||
@@ -58,7 +59,6 @@ class TorrentLeech(TorrentProvider):
|
||||
'name': link.string,
|
||||
'url': self.urls['download'] % url['href'],
|
||||
'detail_url': self.urls['download'] % details['href'],
|
||||
'download': self.loginDownload,
|
||||
'size': self.parseSize(result.find_all('td')[4].string),
|
||||
'seeders': tryInt(result.find('td', attrs = {'class' : 'seeders'}).string),
|
||||
'leechers': tryInt(result.find('td', attrs = {'class' : 'leechers'}).string),
|
||||
@@ -77,3 +77,5 @@ class TorrentLeech(TorrentProvider):
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return '/user/account/logout' in output.lower() or 'welcome back' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
@@ -13,6 +13,7 @@ class TorrentShack(TorrentProvider):
|
||||
urls = {
|
||||
'test' : 'http://www.torrentshack.net/',
|
||||
'login' : 'http://www.torrentshack.net/login.php',
|
||||
'login_check': 'http://www.torrentshack.net/inbox.php',
|
||||
'detail' : 'http://www.torrentshack.net/torrent/%s',
|
||||
'search' : 'http://www.torrentshack.net/torrents.php?searchstr=%s&filter_cat[%d]=1',
|
||||
'download' : 'http://www.torrentshack.net/%s',
|
||||
@@ -58,7 +59,6 @@ class TorrentShack(TorrentProvider):
|
||||
'name': unicode(link.span.string).translate({ord(u'\xad'): None}),
|
||||
'url': self.urls['download'] % url['href'],
|
||||
'detail_url': self.urls['download'] % link['href'],
|
||||
'download': self.loginDownload,
|
||||
'size': self.parseSize(result.find_all('td')[4].string),
|
||||
'seeders': tryInt(result.find_all('td')[6].string),
|
||||
'leechers': tryInt(result.find_all('td')[7].string),
|
||||
@@ -79,3 +79,5 @@ class TorrentShack(TorrentProvider):
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return 'logout.php' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
@@ -2,7 +2,6 @@ from __future__ import with_statement
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
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.settings.model import Properties
|
||||
import ConfigParser
|
||||
@@ -170,18 +169,16 @@ class Settings(object):
|
||||
|
||||
|
||||
def view(self):
|
||||
return jsonified({
|
||||
return {
|
||||
'options': self.getOptions(),
|
||||
'values': self.getValues()
|
||||
})
|
||||
}
|
||||
|
||||
def saveView(self):
|
||||
def saveView(self, **kwargs):
|
||||
|
||||
params = getParams()
|
||||
|
||||
section = params.get('section')
|
||||
option = params.get('name')
|
||||
value = params.get('value')
|
||||
section = kwargs.get('section')
|
||||
option = kwargs.get('name')
|
||||
value = kwargs.get('value')
|
||||
|
||||
# See if a value handler is attached, use that as value
|
||||
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)
|
||||
fireEvent('setting.save.%s.%s.after' % (section, option), single = True)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
})
|
||||
}
|
||||
|
||||
def getProperty(self, identifier):
|
||||
from couchpotato import get_session
|
||||
|
||||
@@ -11,6 +11,7 @@ class Env(object):
|
||||
_appname = 'CouchPotato'
|
||||
|
||||
''' Environment variables '''
|
||||
_app = None
|
||||
_encoding = 'UTF-8'
|
||||
_debug = False
|
||||
_dev = False
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
from argparse import ArgumentParser
|
||||
from couchpotato import web
|
||||
from couchpotato.api import api, NonBlockHandler
|
||||
from cache import FileSystemCache
|
||||
from couchpotato import KeyHandler
|
||||
from couchpotato.api import NonBlockHandler, ApiHandler
|
||||
from couchpotato.core.event import fireEventAsync, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import getDataDir, tryInt
|
||||
from logging import handlers
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.web import Application, FallbackHandler
|
||||
from tornado.wsgi import WSGIContainer
|
||||
from werkzeug.contrib.cache import FileSystemCache
|
||||
from tornado.web import Application, StaticFileHandler, RedirectHandler
|
||||
import locale
|
||||
import logging
|
||||
import os.path
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import warnings
|
||||
|
||||
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'):
|
||||
encoding = 'UTF-8'
|
||||
|
||||
Env.set('encoding', encoding)
|
||||
|
||||
# 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
|
||||
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
|
||||
if not os.path.isdir(new_backup): os.makedirs(new_backup)
|
||||
src_files = [options.config_file, db_path, db_path + '-shm', db_path + '-wal']
|
||||
for src_file in src_files:
|
||||
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
|
||||
backups = []
|
||||
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):
|
||||
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 tryInt(os.path.basename(backup)) < time.time() - 259200:
|
||||
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):
|
||||
os.remove(b_file)
|
||||
os.rmdir(backup)
|
||||
@@ -108,13 +111,12 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
|
||||
|
||||
# Register environment settings
|
||||
Env.set('encoding', encoding)
|
||||
Env.set('app_dir', base_path)
|
||||
Env.set('data_dir', data_dir)
|
||||
Env.set('log_path', os.path.join(log_dir, 'CouchPotato.log'))
|
||||
Env.set('db_path', 'sqlite:///' + db_path)
|
||||
Env.set('cache_dir', os.path.join(data_dir, 'cache'))
|
||||
Env.set('cache', FileSystemCache(os.path.join(Env.get('cache_dir'), 'python')))
|
||||
Env.set('app_dir', toUnicode(base_path))
|
||||
Env.set('data_dir', toUnicode(data_dir))
|
||||
Env.set('log_path', toUnicode(os.path.join(log_dir, 'CouchPotato.log')))
|
||||
Env.set('db_path', toUnicode('sqlite:///' + db_path))
|
||||
Env.set('cache_dir', toUnicode(os.path.join(data_dir, 'cache')))
|
||||
Env.set('cache', FileSystemCache(toUnicode(os.path.join(Env.get('cache_dir'), 'python'))))
|
||||
Env.set('console_log', options.console_log)
|
||||
Env.set('quiet', options.quiet)
|
||||
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
|
||||
db = Env.get('db_path')
|
||||
db_exists = os.path.isfile(db_path)
|
||||
|
||||
# Load configs & plugins
|
||||
loader = Env.get('loader')
|
||||
loader.preload(root = base_path)
|
||||
loader.run()
|
||||
db_exists = os.path.isfile(toUnicode(db_path))
|
||||
|
||||
# Load migrations
|
||||
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
|
||||
setup()
|
||||
|
||||
# Fill database with needed stuff
|
||||
if not db_exists:
|
||||
fireEvent('app.initialize', in_order = True)
|
||||
|
||||
# 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')
|
||||
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
|
||||
app.secret_key = api_key
|
||||
host = Env.setting('host', default = '0.0.0.0')
|
||||
# app.debug = development
|
||||
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),
|
||||
}
|
||||
|
||||
# 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
|
||||
app.register_blueprint(web, url_prefix = '%s/' % url_base)
|
||||
app.register_blueprint(api, url_prefix = '%s/api/%s/' % (url_base, api_key))
|
||||
# Load the app
|
||||
application = Application([],
|
||||
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
|
||||
try: log.info('Starting server on port %(port)s', config)
|
||||
except: pass
|
||||
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']:
|
||||
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'])
|
||||
loop.start()
|
||||
except Exception, e:
|
||||
log.error('Failed starting: %s', traceback.format_exc())
|
||||
try:
|
||||
nr, msg = e
|
||||
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)
|
||||
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>
|
||||
<html>
|
||||
<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>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>CouchPotato API Documentation</h1>
|
||||
<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.
|
||||
All the data that you see there are from the API.
|
||||
<br />
|
||||
<br />
|
||||
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 />
|
||||
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 />
|
||||
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.
|
||||
<br />
|
||||
</div>
|
||||
@@ -41,9 +42,9 @@
|
||||
<td class="type">{{ api_docs[route]['params'][param].get('type', 'string') }}</td>
|
||||
<td class="description">{{ api_docs[route]['params'][param]['desc'] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% end %}
|
||||
</table>
|
||||
{% endif %}
|
||||
{% end %}
|
||||
|
||||
{% if api_docs[route].get('return') %}
|
||||
<h3>Return</h3>
|
||||
@@ -52,14 +53,14 @@
|
||||
{% if api_docs[route]['return'].get('example') %}
|
||||
<div class="example">
|
||||
<h4>Example</h4>
|
||||
<pre>{{ api_docs[route]['return'].get('example', '')|safe }}</pre>
|
||||
<pre>{{ api_docs[route]['return'].get('example', '') }}</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% end %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% end %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% end %}
|
||||
{% end %}
|
||||
|
||||
<div class="missing">
|
||||
<h1>Missing documentation</h1>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% autoescape None %}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -5,38 +6,38 @@
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
|
||||
{% 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) %}
|
||||
<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) %}
|
||||
<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) %}
|
||||
<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 rel="apple-touch-icon" href="{{ url_for('web.static', filename='images/homescreen.png') }}" />
|
||||
<link href="{{ Env.get('static_path') }}images/favicon.ico" rel="icon" type="image/x-icon" />
|
||||
<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">
|
||||
|
||||
|
||||
if($(window).getSize().x <= 480)
|
||||
window.addEvent('load', function() {
|
||||
|
||||
|
||||
setTimeout(function(){
|
||||
window.scrollTo(0, 1);
|
||||
window.scrollTo(0, 0);
|
||||
}, 100);
|
||||
|
||||
|
||||
});
|
||||
|
||||
window.addEvent('domready', function() {
|
||||
new Uniform();
|
||||
|
||||
Api.setup({
|
||||
'url': {{ url_for('api.index')|tojson|safe }},
|
||||
'path_sep': {{ sep|tojson|safe }},
|
||||
'url': {{ json_encode(Env.get('api_base')) }},
|
||||
'path_sep': {{ json_encode(sep) }},
|
||||
'is_remote': false
|
||||
});
|
||||
|
||||
@@ -61,29 +62,29 @@
|
||||
}
|
||||
|
||||
Quality.setup({
|
||||
'profiles': {{ fireEvent('profile.all', single = True)|tojson|safe }},
|
||||
'qualities': {{ fireEvent('quality.all', single = True)|tojson|safe }}
|
||||
'profiles': {{ json_encode(fireEvent('profile.all', single = True)) }},
|
||||
'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({
|
||||
'base_url': {{ url_for('web.index')|tojson|safe }},
|
||||
'args': {{ env.get('args')|tojson|safe }},
|
||||
'options': {{ ('%s' % env.get('options'))|tojson|safe }},
|
||||
'app_dir': {{ env.get('app_dir')|tojson|safe }},
|
||||
'data_dir': {{ env.get('data_dir')|tojson|safe }},
|
||||
'pid': {{ env.getPid()|tojson|safe }},
|
||||
'userscript_version': {{ fireEvent('userscript.get_version', single = True)|tojson|safe }}
|
||||
'base_url': {{ json_encode(Env.get('web_base')) }},
|
||||
'args': {{ json_encode(Env.get('args')) }},
|
||||
'options': {{ json_encode(('%s' % Env.get('options'))) }},
|
||||
'app_dir': {{ json_encode(Env.get('app_dir')) }},
|
||||
'data_dir': {{ json_encode(Env.get('data_dir')) }},
|
||||
'pid': {{ json_encode(Env.getPid()) }},
|
||||
'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'))
|
||||
window.location = '{{ url_for('web.index') }}wizard/'
|
||||
{% endif %}
|
||||
window.location = '{{ Env.get('web_base') }}wizard/'
|
||||
{% end %}
|
||||
|
||||
</script>
|
||||
<title>CouchPotato</title>
|
||||
|
||||
@@ -45,6 +45,8 @@ test -x $CP_DAEMON || exit 0
|
||||
|
||||
set -e
|
||||
|
||||
. /lib/lsb/init-functions
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
echo "Starting $DESC"
|
||||
@@ -63,9 +65,13 @@ case "$1" in
|
||||
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
|
||||
;;
|
||||
|
||||
status)
|
||||
status_of_proc -p $CP_PID_FILE "$CP_DAEMON" "$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
|
||||
;;
|
||||
esac
|
||||
|
||||
262
libs/cache/__init__.py
vendored
Normal file
262
libs/cache/__init__.py
vendored
Normal file
@@ -0,0 +1,262 @@
|
||||
"""
|
||||
copied from
|
||||
werkzeug.contrib.cache
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from cache.posixemulation import rename
|
||||
from itertools import izip
|
||||
from time import time
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
try:
|
||||
from hashlib import md5
|
||||
except ImportError:
|
||||
from md5 import new as md5
|
||||
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except ImportError:
|
||||
import pickle
|
||||
|
||||
|
||||
def _items(mappingorseq):
|
||||
"""Wrapper for efficient iteration over mappings represented by dicts
|
||||
or sequences::
|
||||
|
||||
>>> for k, v in _items((i, i*i) for i in xrange(5)):
|
||||
... assert k*k == v
|
||||
|
||||
>>> for k, v in _items(dict((i, i*i) for i in xrange(5))):
|
||||
... assert k*k == v
|
||||
|
||||
"""
|
||||
return mappingorseq.iteritems() if hasattr(mappingorseq, 'iteritems') \
|
||||
else mappingorseq
|
||||
|
||||
|
||||
class BaseCache(object):
|
||||
"""Baseclass for the cache systems. All the cache systems implement this
|
||||
API or a superset of it.
|
||||
|
||||
:param default_timeout: the default timeout that is used if no timeout is
|
||||
specified on :meth:`set`.
|
||||
"""
|
||||
|
||||
def __init__(self, default_timeout = 300):
|
||||
self.default_timeout = default_timeout
|
||||
|
||||
def delete(self, key):
|
||||
"""Deletes `key` from the cache. If it does not exist in the cache
|
||||
nothing happens.
|
||||
|
||||
:param key: the key to delete.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_many(self, *keys):
|
||||
"""Returns a list of values for the given keys.
|
||||
For each key a item in the list is created. Example::
|
||||
|
||||
foo, bar = cache.get_many("foo", "bar")
|
||||
|
||||
If a key can't be looked up `None` is returned for that key
|
||||
instead.
|
||||
|
||||
:param keys: The function accepts multiple keys as positional
|
||||
arguments.
|
||||
"""
|
||||
return map(self.get, keys)
|
||||
|
||||
def get_dict(self, *keys):
|
||||
"""Works like :meth:`get_many` but returns a dict::
|
||||
|
||||
d = cache.get_dict("foo", "bar")
|
||||
foo = d["foo"]
|
||||
bar = d["bar"]
|
||||
|
||||
:param keys: The function accepts multiple keys as positional
|
||||
arguments.
|
||||
"""
|
||||
return dict(izip(keys, self.get_many(*keys)))
|
||||
|
||||
def set(self, key, value, timeout = None):
|
||||
"""Adds a new key/value to the cache (overwrites value, if key already
|
||||
exists in the cache).
|
||||
|
||||
:param key: the key to set
|
||||
:param value: the value for the key
|
||||
:param timeout: the cache timeout for the key (if not specified,
|
||||
it uses the default timeout).
|
||||
"""
|
||||
pass
|
||||
|
||||
def add(self, key, value, timeout = None):
|
||||
"""Works like :meth:`set` but does not overwrite the values of already
|
||||
existing keys.
|
||||
|
||||
:param key: the key to set
|
||||
:param value: the value for the key
|
||||
:param timeout: the cache timeout for the key or the default
|
||||
timeout if not specified.
|
||||
"""
|
||||
pass
|
||||
|
||||
def set_many(self, mapping, timeout = None):
|
||||
"""Sets multiple keys and values from a mapping.
|
||||
|
||||
:param mapping: a mapping with the keys/values to set.
|
||||
:param timeout: the cache timeout for the key (if not specified,
|
||||
it uses the default timeout).
|
||||
"""
|
||||
for key, value in _items(mapping):
|
||||
self.set(key, value, timeout)
|
||||
|
||||
def delete_many(self, *keys):
|
||||
"""Deletes multiple keys at once.
|
||||
|
||||
:param keys: The function accepts multiple keys as positional
|
||||
arguments.
|
||||
"""
|
||||
for key in keys:
|
||||
self.delete(key)
|
||||
|
||||
def clear(self):
|
||||
"""Clears the cache. Keep in mind that not all caches support
|
||||
completely clearing the cache.
|
||||
"""
|
||||
pass
|
||||
|
||||
def inc(self, key, delta = 1):
|
||||
"""Increments the value of a key by `delta`. If the key does
|
||||
not yet exist it is initialized with `delta`.
|
||||
|
||||
For supporting caches this is an atomic operation.
|
||||
|
||||
:param key: the key to increment.
|
||||
:param delta: the delta to add.
|
||||
"""
|
||||
self.set(key, (self.get(key) or 0) + delta)
|
||||
|
||||
def dec(self, key, delta = 1):
|
||||
"""Decrements the value of a key by `delta`. If the key does
|
||||
not yet exist it is initialized with `-delta`.
|
||||
|
||||
For supporting caches this is an atomic operation.
|
||||
|
||||
:param key: the key to increment.
|
||||
:param delta: the delta to subtract.
|
||||
"""
|
||||
self.set(key, (self.get(key) or 0) - delta)
|
||||
|
||||
|
||||
class FileSystemCache(BaseCache):
|
||||
"""A cache that stores the items on the file system. This cache depends
|
||||
on being the only user of the `cache_dir`. Make absolutely sure that
|
||||
nobody but this cache stores files there or otherwise the cache will
|
||||
randomly delete files therein.
|
||||
|
||||
:param cache_dir: the directory where cache files are stored.
|
||||
:param threshold: the maximum number of items the cache stores before
|
||||
it starts deleting some.
|
||||
:param default_timeout: the default timeout that is used if no timeout is
|
||||
specified on :meth:`~BaseCache.set`.
|
||||
:param mode: the file mode wanted for the cache files, default 0600
|
||||
"""
|
||||
|
||||
#: used for temporary files by the FileSystemCache
|
||||
_fs_transaction_suffix = '.__wz_cache'
|
||||
|
||||
def __init__(self, cache_dir, threshold = 500, default_timeout = 300, mode = 0600):
|
||||
BaseCache.__init__(self, default_timeout)
|
||||
self._path = cache_dir
|
||||
self._threshold = threshold
|
||||
self._mode = mode
|
||||
if not os.path.exists(self._path):
|
||||
os.makedirs(self._path)
|
||||
|
||||
def _list_dir(self):
|
||||
"""return a list of (fully qualified) cache filenames
|
||||
"""
|
||||
return [os.path.join(self._path, fn) for fn in os.listdir(self._path)
|
||||
if not fn.endswith(self._fs_transaction_suffix)]
|
||||
|
||||
def _prune(self):
|
||||
entries = self._list_dir()
|
||||
if len(entries) > self._threshold:
|
||||
now = time()
|
||||
for idx, fname in enumerate(entries):
|
||||
remove = False
|
||||
f = None
|
||||
try:
|
||||
try:
|
||||
f = open(fname, 'rb')
|
||||
expires = pickle.load(f)
|
||||
remove = expires <= now or idx % 3 == 0
|
||||
finally:
|
||||
if f is not None:
|
||||
f.close()
|
||||
except Exception:
|
||||
pass
|
||||
if remove:
|
||||
try:
|
||||
os.remove(fname)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
|
||||
def clear(self):
|
||||
for fname in self._list_dir():
|
||||
try:
|
||||
os.remove(fname)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
|
||||
def _get_filename(self, key):
|
||||
hash = md5(key).hexdigest()
|
||||
return os.path.join(self._path, hash)
|
||||
|
||||
def get(self, key):
|
||||
filename = self._get_filename(key)
|
||||
try:
|
||||
f = open(filename, 'rb')
|
||||
try:
|
||||
if pickle.load(f) >= time():
|
||||
return pickle.load(f)
|
||||
finally:
|
||||
f.close()
|
||||
os.remove(filename)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def add(self, key, value, timeout = None):
|
||||
filename = self._get_filename(key)
|
||||
if not os.path.exists(filename):
|
||||
self.set(key, value, timeout)
|
||||
|
||||
def set(self, key, value, timeout = None):
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
filename = self._get_filename(key)
|
||||
self._prune()
|
||||
try:
|
||||
fd, tmp = tempfile.mkstemp(suffix = self._fs_transaction_suffix,
|
||||
dir = self._path)
|
||||
f = os.fdopen(fd, 'wb')
|
||||
try:
|
||||
pickle.dump(int(time() + timeout), f, 1)
|
||||
pickle.dump(value, f, pickle.HIGHEST_PROTOCOL)
|
||||
finally:
|
||||
f.close()
|
||||
rename(tmp, filename)
|
||||
os.chmod(filename, self._mode)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
|
||||
def delete(self, key):
|
||||
try:
|
||||
os.remove(self._get_filename(key))
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
@@ -1,44 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask
|
||||
~~~~~
|
||||
|
||||
A microframework based on Werkzeug. It's extensively documented
|
||||
and follows best practice patterns.
|
||||
|
||||
:copyright: (c) 2011 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
__version__ = '0.9'
|
||||
|
||||
# utilities we import from Werkzeug and Jinja2 that are unused
|
||||
# in the module but are exported as public interface.
|
||||
from werkzeug.exceptions import abort
|
||||
from werkzeug.utils import redirect
|
||||
from jinja2 import Markup, escape
|
||||
|
||||
from .app import Flask, Request, Response
|
||||
from .config import Config
|
||||
from .helpers import url_for, jsonify, json_available, flash, \
|
||||
send_file, send_from_directory, get_flashed_messages, \
|
||||
get_template_attribute, make_response, safe_join, \
|
||||
stream_with_context
|
||||
from .globals import current_app, g, request, session, _request_ctx_stack, \
|
||||
_app_ctx_stack
|
||||
from .ctx import has_request_context, has_app_context, \
|
||||
after_this_request
|
||||
from .module import Module
|
||||
from .blueprints import Blueprint
|
||||
from .templating import render_template, render_template_string
|
||||
|
||||
# the signals
|
||||
from .signals import signals_available, template_rendered, request_started, \
|
||||
request_finished, got_request_exception, request_tearing_down
|
||||
|
||||
# only import json if it's available
|
||||
if json_available:
|
||||
from .helpers import json
|
||||
|
||||
# backwards compat, goes away in 1.0
|
||||
from .sessions import SecureCookieSession as Session
|
||||
1701
libs/flask/app.py
1701
libs/flask/app.py
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'
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user