Merge branch 'refs/heads/develop'

This commit is contained in:
Ruud committed 2013-06-23 12:24:01 +02:00
commit 47d37c2ec9
496 files changed
+17953 -60610

No files matched your search

+20
View File
@@ -40,3 +40,23 @@ Linux (ubuntu / debian):
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato` * Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
* Add it to defaults. `sudo update-rc.d couchpotato defaults` * Add it to defaults. `sudo update-rc.d couchpotato defaults`
* Open your browser and go to: `http://localhost:5050/` * Open your browser and go to: `http://localhost:5050/`
FreeBSD :
* Update your ports tree `sudo portsnap fetch update`
* Install Python 2.6+ [lang/python](http://www.freshports.org/lang/python) with `cd /usr/ports/lang/python; sudo make install clean`
* Install port [databases/py-sqlite3](http://www.freshports.org/databases/py-sqlite3) with `cd /usr/ports/databases/py-sqlite3; sudo make install clean`
* Add a symlink to 'python2' `sudo ln -s /usr/local/bin/python /usr/local/bin/python2`
* Install port [ftp/libcurl](http://www.freshports.org/ftp/libcurl) with `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean`
* Install port [ftp/curl](http://www.freshports.org/ftp/bcurl), deselect 'Asynchronous DNS resolution via c-ares' when prompted as part of config `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean`
* Install port [textproc/docbook-xml-450](http://www.freshports.org/textproc/docbook-xml-450) with `cd /usr/ports/textproc/docbook-xml-450; sudo make install clean`
* Install port [GIT](http://git-scm.com/) with `cd /usr/ports/devel/git; sudo make install clean`
* 'cd' to the folder of your choosing.
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
* Then run `sudo python CouchPotatoServer/CouchPotato.py` to start for the first time
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/freebsd /etc/rc.d/couchpotato`
* Change the paths inside the init script. `sudo vim /etc/init.d/couchpotato`
* Make init script executable. `sudo chmod +x /etc/rc.d/couchpotato`
* Add init to startup. `sudo echo 'couchpotato_enable="YES"' >> /etc/rc.conf`
* Open your browser and go to: `http://server:5050/`
+46 -46
View File
@@ -1,84 +1,84 @@
from couchpotato.api import api_docs, api_docs_missing from couchpotato.api import api_docs, api_docs_missing, api
from couchpotato.core.auth import requires_auth from couchpotato.core.auth import requires_auth
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.helpers.variable import md5 from couchpotato.core.helpers.variable import md5
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
from flask.app import Flask
from flask.blueprints import Blueprint
from flask.globals import request
from flask.helpers import url_for
from flask.templating import render_template
from sqlalchemy.engine import create_engine from sqlalchemy.engine import create_engine
from sqlalchemy.orm import scoped_session from sqlalchemy.orm import scoped_session
from sqlalchemy.orm.session import sessionmaker from sqlalchemy.orm.session import sessionmaker
from werkzeug.utils import redirect from tornado import template
from tornado.web import RequestHandler
import os import os
import time import time
log = CPLog(__name__) log = CPLog(__name__)
app = Flask(__name__, static_folder = 'nope') views = {}
web = Blueprint('web', __name__) template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates'))
# Main web handler
@requires_auth
class WebHandler(RequestHandler):
def get(self, route):
if not views.get(route):
page_not_found(self)
return
self.write(views[route]())
def addView(route, func, static = False):
views[route] = func
def get_session(engine = None): def get_session(engine = None):
return Env.getSession(engine) return Env.getSession(engine)
def addView(route, func, static = False):
web.add_url_rule(route + ('' if static else '/'), endpoint = route if route else 'index', view_func = func)
""" Web view """ # Web view
@web.route('/')
@requires_auth
def index(): def index():
return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env) return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env)
addView('', index)
""" Api view """ # API docs
@web.route('docs/')
@requires_auth
def apiDocs(): def apiDocs():
from couchpotato import app
routes = [] routes = []
for route, x in sorted(app.view_functions.iteritems()):
if route[0:4] == 'api.': for route in api.iterkeys():
routes += [route[4:].replace('::', '.')] routes.append(route)
if api_docs.get(''): if api_docs.get(''):
del api_docs[''] del api_docs['']
del api_docs_missing[''] del api_docs_missing['']
return render_template('api.html', fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing))
@web.route('getkey/') return template_loader.load('api.html').generate(fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing), Env = Env)
def getApiKey():
api = None addView('docs', apiDocs)
params = getParams()
username = Env.setting('username')
password = Env.setting('password')
if (params.get('u') == md5(username) or not username) and (params.get('p') == password or not password): # Make non basic auth option to get api key
api = Env.setting('api_key') class KeyHandler(RequestHandler):
def get(self):
api = None
username = Env.setting('username')
password = Env.setting('password')
return jsonified({ if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password):
'success': api is not None, api = Env.setting('api_key')
'api_key': api
})
@app.errorhandler(404) self.write({
def page_not_found(error): 'success': api is not None,
index_url = url_for('web.index') 'api_key': api
url = request.path[len(index_url):] })
def page_not_found(rh):
index_url = Env.get('web_base')
url = rh.request.uri[len(index_url):]
if url[:3] != 'api': if url[:3] != 'api':
if request.path != '/': r = index_url + '#' + url.lstrip('/')
r = request.url.replace(request.path, index_url + '#' + url) rh.redirect(r)
else:
r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url)
return redirect(r)
else: else:
if not Env.get('dev'): if not Env.get('dev'):
time.sleep(0.1) time.sleep(0.1)
return 'Wrong API key used', 404
rh.set_status(404)
rh.write('Wrong API key used')
+41 -19
View File
@@ -1,14 +1,14 @@
from flask.blueprints import Blueprint
from flask.helpers import url_for
from tornado.web import RequestHandler, asynchronous from tornado.web import RequestHandler, asynchronous
from werkzeug.utils import redirect import json
import urllib
api = Blueprint('api', __name__) api = {}
api_docs = {}
api_docs_missing = []
api_nonblock = {} api_nonblock = {}
api_docs = {}
api_docs_missing = []
# NonBlock API handler
class NonBlockHandler(RequestHandler): class NonBlockHandler(RequestHandler):
stoppers = [] stoppers = []
@@ -32,14 +32,6 @@ class NonBlockHandler(RequestHandler):
self.stoppers = [] self.stoppers = []
def addApiView(route, func, static = False, docs = None, **kwargs):
api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func, **kwargs)
if docs:
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
else:
api_docs_missing.append(route)
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs): def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
api_nonblock[route] = func_tuple api_nonblock[route] = func_tuple
@@ -48,9 +40,39 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
else: else:
api_docs_missing.append(route) api_docs_missing.append(route)
""" Api view """ # Blocking API handler
def index(): class ApiHandler(RequestHandler):
index_url = url_for('web.index')
return redirect(index_url + 'docs/')
addApiView('', index) def get(self, route):
if not api.get(route):
self.write('API call doesn\'t seem to exist')
return
kwargs = {}
for x in self.request.arguments:
kwargs[x] = urllib.unquote(self.get_argument(x))
# Remove t random string
try: del kwargs['t']
except: pass
# Check JSONP callback
result = api[route](**kwargs)
jsonp_callback = self.get_argument('callback_func', default = None)
if jsonp_callback:
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
elif isinstance(result, (tuple)) and result[0] == 'redirect':
self.redirect(result[1])
else:
self.write(result)
def addApiView(route, func, static = False, docs = None, **kwargs):
if static: func(route)
else: api[route] = func
if docs:
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
else:
api_docs_missing.append(route)
+6 -7
View File
@@ -1,6 +1,5 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.helpers.variable import cleanHost, md5 from couchpotato.core.helpers.variable import cleanHost, md5
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
@@ -69,9 +68,9 @@ class Core(Plugin):
return True return True
def available(self): def available(self):
return jsonified({ return {
'success': True 'success': True
}) }
def shutdown(self): def shutdown(self):
if self.shutdown_started: if self.shutdown_started:
@@ -156,10 +155,10 @@ class Core(Plugin):
host = 'localhost' host = 'localhost'
port = Env.setting('port') port = Env.setting('port')
return '%s:%d%s' % (cleanHost(host).rstrip('/'), int(port), '/' + Env.setting('url_base').lstrip('/') if Env.setting('url_base') else '') return '%s:%d%s' % (cleanHost(host).rstrip('/'), int(port), Env.get('web_base'))
def createApiUrl(self): def createApiUrl(self):
return '%s/api/%s' % (self.createBaseUrl(), Env.setting('api_key')) return '%sapi/%s' % (self.createBaseUrl(), Env.setting('api_key'))
def version(self): def version(self):
ver = fireEvent('updater.info', single = True) ver = fireEvent('updater.info', single = True)
@@ -171,9 +170,9 @@ class Core(Plugin):
return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash']) return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash'])
def versionView(self): def versionView(self):
return jsonified({ return {
'version': self.version() 'version': self.version()
}) }
def signalHandler(self): def signalHandler(self):
if Env.get('daemonized'): return if Env.get('daemonized'): return
+1 -2
View File
@@ -8,7 +8,6 @@ from minify.cssmin import cssmin
from minify.jsmin import jsmin from minify.jsmin import jsmin
import os import os
import re import re
import time
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -122,7 +121,7 @@ class ClientScript(Plugin):
# Combine all files together with some comments # Combine all files together with some comments
data = '' data = ''
for r in raw: for r in raw:
data += self.comment.get(file_type) % (r.get('file'), r.get('date')) data += self.comment.get(file_type) % (ss(r.get('file')), r.get('date'))
data += r.get('data') + '\n\n' data += r.get('data') + '\n\n'
self.createFile(out, data.strip()) self.createFile(out, data.strip())
+7 -13
View File
@@ -1,7 +1,6 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
@@ -36,7 +35,7 @@ class Updater(Plugin):
addEvent('app.load', self.setCrons) addEvent('app.load', self.setCrons)
addEvent('updater.info', self.info) addEvent('updater.info', self.info)
addApiView('updater.info', self.getInfo, docs = { addApiView('updater.info', self.info, docs = {
'desc': 'Get updater information', 'desc': 'Get updater information',
'return': { 'return': {
'type': 'object', 'type': 'object',
@@ -92,17 +91,14 @@ class Updater(Plugin):
return False return False
def info(self): def info(self, **kwargs):
return self.updater.info() return self.updater.info()
def getInfo(self):
return jsonified(self.updater.info())
def checkView(self): def checkView(self):
return jsonified({ return {
'update_available': self.check(force = True), 'update_available': self.check(force = True),
'info': self.updater.info() 'info': self.updater.info()
}) }
def doUpdateView(self): def doUpdateView(self):
@@ -119,9 +115,9 @@ class Updater(Plugin):
if not success: if not success:
success = True success = True
return jsonified({ return {
'success': success 'success': success
}) }
class BaseUpdater(Plugin): class BaseUpdater(Plugin):
@@ -138,9 +134,6 @@ class BaseUpdater(Plugin):
def doUpdate(self): def doUpdate(self):
pass pass
def getInfo(self):
return jsonified(self.info())
def info(self): def info(self):
return { return {
'last_check': self.last_check, 'last_check': self.last_check,
@@ -279,6 +272,7 @@ class SourceUpdater(BaseUpdater):
if download_data.get('type') == 'zip': if download_data.get('type') == 'zip':
zip = zipfile.ZipFile(destination) zip = zipfile.ZipFile(destination)
zip.extractall(extracted_path) zip.extractall(extracted_path)
zip.close()
else: else:
tar = tarfile.open(destination) tar = tarfile.open(destination)
tar.extractall(path = extracted_path) tar.extractall(path = extracted_path)
+41 -16
View File
@@ -1,26 +1,51 @@
from couchpotato.core.helpers.variable import md5 from couchpotato.core.helpers.variable import md5
from couchpotato.environment import Env from couchpotato.environment import Env
from flask import request, Response import base64
from functools import wraps
def check_auth(username, password): def check_auth(username, password):
return username == Env.setting('username') and password == Env.setting('password') return username == Env.setting('username') and password == Env.setting('password')
def authenticate(): def requires_auth(handler_class):
return Response(
'This is not the page you are looking for. *waves hand*', 401,
{'WWW-Authenticate': 'Basic realm="CouchPotato Login"'}
)
def requires_auth(f): def wrap_execute(handler_execute):
@wraps(f) def require_basic_auth(handler, kwargs):
def decorated(*args, **kwargs): if Env.setting('username') and Env.setting('password'):
auth = getattr(request, 'authorization')
if Env.setting('username') and Env.setting('password'):
if (not auth or not check_auth(auth.username.decode('latin1'), md5(auth.password.decode('latin1').encode(Env.get('encoding'))))):
return authenticate()
return f(*args, **kwargs) auth_header = handler.request.headers.get('Authorization')
auth_decoded = base64.decodestring(auth_header[6:]) if auth_header else None
if auth_decoded:
username, password = auth_decoded.split(':', 2)
return decorated if auth_header is None or not auth_header.startswith('Basic ') or (not check_auth(username.decode('latin'), md5(password.decode('latin')))):
handler.set_status(401)
handler.set_header('WWW-Authenticate', 'Basic realm="CouchPotato Login"')
handler._transforms = []
handler.finish()
return False
return True
def _execute(self, transforms, *args, **kwargs):
if not require_basic_auth(self, kwargs):
return False
return handler_execute(self, transforms, *args, **kwargs)
return _execute
handler_class._execute = wrap_execute(handler_class._execute)
return handler_class
# @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
+1 -1
View File
@@ -16,7 +16,7 @@ class Downloader(Provider):
torrent_sources = [ torrent_sources = [
'http://torrage.com/torrent/%s.torrent', 'http://torrage.com/torrent/%s.torrent',
'http://torcache.net/torrent/%s.torrent', 'https://torcache.net/torrent/%s.torrent',
] ]
torrent_trackers = [ torrent_trackers = [
+10 -1
View File
@@ -104,12 +104,21 @@ class NZBGet(Downloader):
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0] nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
except: except:
nzb_id = item['NZBID'] nzb_id = item['NZBID']
timeleft = -1
try:
if item['ActiveDownloads'] > 0 and item['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
timeleft = str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
except:
pass
statuses.append({ statuses.append({
'id': nzb_id, 'id': nzb_id,
'name': item['NZBFilename'], 'name': item['NZBFilename'],
'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED', 'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED',
# Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item # Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
'timeleft': str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20)) if item['ActiveDownloads'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']) else -1, 'timeleft': timeleft,
}) })
for item in queue: # 'Parameters' is not passed in rpc.postqueue for item in queue: # 'Parameters' is not passed in rpc.postqueue
@@ -11,7 +11,7 @@ config = [{
'list': 'download_providers', 'list': 'download_providers',
'name': 'sabnzbd', 'name': 'sabnzbd',
'label': 'Sabnzbd', 'label': 'Sabnzbd',
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> to download NZBs.', 'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> (0.7+) to download NZBs.',
'wizard': True, 'wizard': True,
'options': [ 'options': [
{ {
@@ -18,7 +18,7 @@ config = [{
'name': 'enabled', 'name': 'enabled',
'default': 0, 'default': 0,
'type': 'enabler', 'type': 'enabler',
'radio_group': 'torrent', 'radio_group': 'nzb,torrent',
}, },
{ {
'name': 'host', 'name': 'host',
@@ -32,6 +32,13 @@ config = [{
'name': 'password', 'name': 'password',
'type': 'password', 'type': 'password',
}, },
{
'name': 'use_for',
'label': 'Use for',
'default': 'both',
'type': 'dropdown',
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
},
{ {
'name': 'manual', 'name': 'manual',
'default': 0, 'default': 0,
+80 -43
View File
@@ -1,22 +1,21 @@
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import isInt from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import httplib
import json import json
import urllib import requests
import urllib2
log = CPLog(__name__) log = CPLog(__name__)
class Synology(Downloader): class Synology(Downloader):
type = ['torrent_magnet'] type = ['nzb', 'torrent', 'torrent_magnet']
log = CPLog(__name__) log = CPLog(__name__)
def download(self, data, movie, filedata = None): def download(self, data, movie, filedata = None):
log.error('Sending "%s" (%s) to Synology.', (data.get('name'), data.get('type'))) response = False
log.error('Sending "%s" (%s) to Synology.', (data['name'], data['type']))
# Load host from config and split out port. # Load host from config and split out port.
host = self.conf('host').split(':') host = self.conf('host').split(':')
@@ -24,20 +23,41 @@ class Synology(Downloader):
log.error('Config properties are not filled in correctly, port is missing.') log.error('Config properties are not filled in correctly, port is missing.')
return False return False
if data.get('type') == 'torrent':
log.error('Can\'t add binary torrent file')
return False
try: try:
# Send request to Transmission # Send request to Synology
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password')) srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
remote_torrent = srpc.add_torrent_uri(data.get('url')) if data['type'] == 'torrent_magnet':
log.info('Response: %s', remote_torrent) log.info('Adding torrent URL %s', data['url'])
return remote_torrent['success'] response = srpc.create_task(url = data['url'])
elif data['type'] in ['nzb', 'torrent']:
log.info('Adding %s' % data['type'])
if not filedata:
log.error('No %s data found' % data['type'])
else:
filename = data['name'] + '.' + data['type']
response = srpc.create_task(filename = filename, filedata = filedata)
except Exception, err: except Exception, err:
log.error('Exception while adding torrent: %s', err) log.error('Exception while adding torrent: %s', err)
return False finally:
return response
def getEnabledDownloadType(self):
if self.conf('use_for') == 'both':
return super(Synology, self).getEnabledDownloadType()
elif self.conf('use_for') == 'torrent':
return ['torrent', 'torrent_magnet']
else:
return ['nzb']
def isEnabled(self, manual, data = {}):
for_type = ['both']
if data and 'torrent' in data.get('type'):
for_type.append('torrent')
elif data:
for_type.append(data.get('type'))
return super(Synology, self).isEnabled(manual, data) and\
((self.conf('use_for') in for_type))
class SynologyRPC(object): class SynologyRPC(object):
@@ -58,11 +78,13 @@ class SynologyRPC(object):
args = {'api': 'SYNO.API.Auth', 'account': self.username, 'passwd': self.password, 'version': 2, args = {'api': 'SYNO.API.Auth', 'account': self.username, 'passwd': self.password, 'version': 2,
'method': 'login', 'session': self.session_name, 'format': 'sid'} 'method': 'login', 'session': self.session_name, 'format': 'sid'}
response = self._req(self.auth_url, args) response = self._req(self.auth_url, args)
if response['success'] == True: if response['success']:
self.sid = response['data']['sid'] self.sid = response['data']['sid']
log.debug('Sid=%s', self.sid) log.debug('sid=%s', self.sid)
return response else:
elif self.username or self.password: log.error('Couldn\'t login to Synology, %s', response)
return response['success']
else:
log.error('User or password missing, not using authentication.') log.error('User or password missing, not using authentication.')
return False return False
@@ -70,36 +92,51 @@ class SynologyRPC(object):
args = {'api':'SYNO.API.Auth', 'version':1, 'method':'logout', 'session':self.session_name, '_sid':self.sid} args = {'api':'SYNO.API.Auth', 'version':1, 'method':'logout', 'session':self.session_name, '_sid':self.sid}
return self._req(self.auth_url, args) return self._req(self.auth_url, args)
def _req(self, url, args): def _req(self, url, args, files = None):
req_url = url + '?' + urllib.urlencode(args) response = {'success': False}
try: try:
req_open = urllib2.urlopen(req_url) req = requests.post(url, data = args, files = files)
response = json.loads(req_open.read()) req.raise_for_status()
response = json.loads(req.text)
if response['success'] == True: if response['success'] == True:
log.info('Synology action successfull') log.info('Synology action successfull')
return response return response
except httplib.InvalidURL, err: except requests.ConnectionError, err:
log.error('Invalid Transmission host, check your config %s', err) log.error('Synology connection error, check your config %s', err)
return False except requests.HTTPError, err:
except urllib2.HTTPError, err:
log.error('SynologyRPC HTTPError: %s', err) log.error('SynologyRPC HTTPError: %s', err)
return False except Exception, err:
except urllib2.URLError, err: log.error('Exception: %s', err)
log.error('Unable to connect to Synology %s', err) finally:
return False return response
def add_torrent_uri(self, torrent): def create_task(self, url = None, filename = None, filedata = None):
log.info('Adding torrent URL %s', torrent) ''' Creates new download task in Synology DownloadStation. Either specify
response = {} url or pair (filename, filedata).
Returns True if task was created, False otherwise
'''
result = False
# login # login
login = self._login() if self._login():
if len(login) > 0 and login['success'] == True: args = {'api': 'SYNO.DownloadStation.Task',
log.info('Login success, adding torrent') 'version': '1',
args = {'api':'SYNO.DownloadStation.Task', 'version':1, 'method':'create', 'uri':torrent, '_sid':self.sid} 'method': 'create',
response = self._req(self.download_url, args) '_sid': self.sid}
if url:
log.info('Login success, adding torrent URI')
args['uri'] = url
response = self._req(self.download_url, args = args)
log.info('Response: %s', response)
result = response['success']
elif filename and filedata:
log.info('Login success, adding torrent')
files = {'file': (filename, filedata)}
response = self._req(self.download_url, args = args, files = files)
log.info('Response: %s', response)
result = response['success']
else:
log.error('Invalid use of SynologyRPC.create_task: either url or filename+filedata must be specified')
self._logout() self._logout()
else:
log.error('Couldn\'t login to Synology, %s', login)
return response
return result
+15 -9
View File
@@ -22,14 +22,22 @@ def addEvent(name, handler, priority = 100):
def createHandle(*args, **kwargs): def createHandle(*args, **kwargs):
try: try:
parent = handler.im_self # Open handler
bc = hasattr(parent, 'beforeCall') has_parent = hasattr(handler, 'im_self')
if bc: parent.beforeCall(handler) if has_parent:
parent = handler.im_self
bc = hasattr(parent, 'beforeCall')
if bc: parent.beforeCall(handler)
# Main event
h = runHandler(name, handler, *args, **kwargs) h = runHandler(name, handler, *args, **kwargs)
ac = hasattr(parent, 'afterCall')
if ac: parent.afterCall(handler) # Close handler
if has_parent:
ac = hasattr(parent, 'afterCall')
if ac: parent.afterCall(handler)
except: except:
h = runHandler(name, handler, *args, **kwargs) log.error('Failed creating handler %s %s: %s', (name, handler, traceback.format_exc()))
return h return h
@@ -43,7 +51,7 @@ def removeEvent(name, handler):
e -= handler e -= handler
def fireEvent(name, *args, **kwargs): def fireEvent(name, *args, **kwargs):
if not events.get(name): return if not events.has_key(name): return
e = Event(name = name, threads = 10, asynch = kwargs.get('async', False), exc_info = True, traceback = True, lock = threading.RLock()) e = Event(name = name, threads = 10, asynch = kwargs.get('async', False), exc_info = True, traceback = True, lock = threading.RLock())
@@ -133,8 +141,6 @@ def fireEvent(name, *args, **kwargs):
options['on_complete']() options['on_complete']()
return results return results
except KeyError, e:
pass
except Exception: except Exception:
log.error('%s: %s', (name, traceback.format_exc())) log.error('%s: %s', (name, traceback.format_exc()))
-82
View File
@@ -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
+3 -2
View File
@@ -181,5 +181,6 @@ def possibleTitles(raw_title):
def randomString(size = 8, chars = string.ascii_uppercase + string.digits): def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for x in range(size)) return ''.join(random.choice(chars) for x in range(size))
def splitString(str, split_on = ','): def splitString(str, split_on = ',', clean = True):
return [x.strip() for x in str.split(split_on)] if str else [] list = [x.strip() for x in str.split(split_on)] if str else []
return filter(None, list) if clean else list
+1 -1
View File
@@ -4,7 +4,7 @@ import re
class CPLog(object): class CPLog(object):
context = '' context = ''
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key'] replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key', 'passkey']
def __init__(self, context = ''): def __init__(self, context = ''):
if context.endswith('.main'): if context.endswith('.main'):
+3 -2
View File
@@ -1,6 +1,5 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import Provider from couchpotato.core.providers.base import Provider
from couchpotato.environment import Env from couchpotato.environment import Env
@@ -62,7 +61,9 @@ class Notification(Provider):
listener = 'test' listener = 'test'
) )
return jsonified({'success': success}) return {
'success': success
}
def testNotifyName(self): def testNotifyName(self):
return 'notify.%s.test' % self.getName().lower() return 'notify.%s.test' % self.getName().lower()
+24 -17
View File
@@ -2,7 +2,6 @@ from couchpotato import get_session
from couchpotato.api import addApiView, addNonBlockApiView from couchpotato.api import addApiView, addNonBlockApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import tryInt, splitString from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
@@ -11,6 +10,7 @@ from couchpotato.environment import Env
from sqlalchemy.sql.expression import or_ from sqlalchemy.sql.expression import or_
import threading import threading
import time import time
import traceback
import uuid import uuid
log = CPLog(__name__) log = CPLog(__name__)
@@ -62,11 +62,9 @@ class CoreNotifier(Notification):
db.commit() db.commit()
def markAsRead(self): def markAsRead(self, ids = None):
ids = None ids = splitString(ids) if ids else None
if getParam('ids'):
ids = splitString(getParam('ids'))
db = get_session() db = get_session()
@@ -79,14 +77,13 @@ class CoreNotifier(Notification):
db.commit() db.commit()
return jsonified({ return {
'success': True 'success': True
}) }
def listView(self): def listView(self, limit_offset = None, **kwargs):
db = get_session() db = get_session()
limit_offset = getParam('limit_offset', None)
q = db.query(Notif) q = db.query(Notif)
@@ -105,11 +102,11 @@ class CoreNotifier(Notification):
ndict['type'] = 'notification' ndict['type'] = 'notification'
notifications.append(ndict) notifications.append(ndict)
return jsonified({ return {
'success': True, 'success': True,
'empty': len(notifications) == 0, 'empty': len(notifications) == 0,
'notifications': notifications 'notifications': notifications
}) }
def checkMessages(self): def checkMessages(self):
@@ -150,6 +147,8 @@ class CoreNotifier(Notification):
def frontend(self, type = 'notification', data = {}, message = None): def frontend(self, type = 'notification', data = {}, message = None):
log.debug('Notifying frontend')
self.m_lock.acquire() self.m_lock.acquire()
notification = { notification = {
'message_id': str(uuid.uuid4()), 'message_id': str(uuid.uuid4()),
@@ -168,11 +167,13 @@ class CoreNotifier(Notification):
'result': [notification], 'result': [notification],
}) })
except: except:
break log.debug('Failed sending to listener: %s', traceback.format_exc())
self.m_lock.release() self.m_lock.release()
self.cleanMessages() self.cleanMessages()
log.debug('Done notifying frontend')
def addListener(self, callback, last_id = None): def addListener(self, callback, last_id = None):
if last_id: if last_id:
@@ -194,9 +195,11 @@ class CoreNotifier(Notification):
if listener == callback: if listener == callback:
self.listeners.remove(list_tuple) self.listeners.remove(list_tuple)
except: except:
pass log.debug('Failed removing listener: %s', traceback.format_exc())
def cleanMessages(self): def cleanMessages(self):
log.debug('Cleaning messages')
self.m_lock.acquire() self.m_lock.acquire()
for message in self.messages: for message in self.messages:
@@ -204,8 +207,11 @@ class CoreNotifier(Notification):
self.messages.remove(message) self.messages.remove(message)
self.m_lock.release() self.m_lock.release()
log.debug('Done cleaning messages')
def getMessages(self, last_id): def getMessages(self, last_id):
log.debug('Getting messages with id: %s', last_id)
self.m_lock.acquire() self.m_lock.acquire()
recent = [] recent = []
@@ -216,15 +222,16 @@ class CoreNotifier(Notification):
recent = self.messages[index:] recent = self.messages[index:]
self.m_lock.release() self.m_lock.release()
log.debug('Returning for %s %s messages', (last_id, len(recent or [])))
return recent or [] return recent or []
def listener(self): def listener(self, init = False, **kwargs):
messages = [] messages = []
# Get unread # Get unread
if getParam('init'): if init:
db = get_session() db = get_session()
notifications = db.query(Notif) \ notifications = db.query(Notif) \
@@ -235,7 +242,7 @@ class CoreNotifier(Notification):
ndict['type'] = 'notification' ndict['type'] = 'notification'
messages.append(ndict) messages.append(ndict)
return jsonified({ return {
'success': True, 'success': True,
'result': messages, 'result': messages,
}) }
+9 -9
View File
@@ -1,7 +1,6 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
import re import re
@@ -22,10 +21,7 @@ class NMJ(Notification):
addApiView(self.testNotifyName(), self.test) addApiView(self.testNotifyName(), self.test)
addApiView('notify.nmj.auto_config', self.autoConfig) addApiView('notify.nmj.auto_config', self.autoConfig)
def autoConfig(self): def autoConfig(self, host = 'localhost', **kwargs):
params = getParams()
host = params.get('host', 'localhost')
database = '' database = ''
mount = '' mount = ''
@@ -63,11 +59,11 @@ class NMJ(Notification):
log.error('Detected a network share on the Popcorn Hour, but could not get the mounting url') log.error('Detected a network share on the Popcorn Hour, but could not get the mounting url')
return self.failed() return self.failed()
return jsonified({ return {
'success': True, 'success': True,
'database': database, 'database': database,
'mount': mount, 'mount': mount,
}) }
def addToLibrary(self, message = None, group = {}): def addToLibrary(self, message = None, group = {}):
if self.isDisabled(): return if self.isDisabled(): return
@@ -113,9 +109,13 @@ class NMJ(Notification):
return True return True
def failed(self): def failed(self):
return jsonified({'success': False}) return {
'success': False
}
def test(self): def test(self):
return jsonified({'success': self.addToLibrary()}) return {
'success': self.addToLibrary()
}
@@ -1,8 +1,8 @@
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from flask.helpers import json
import base64 import base64
import json
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
+3 -2
View File
@@ -1,6 +1,5 @@
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
@@ -86,4 +85,6 @@ class Plex(Notification):
) )
success2 = self.addToLibrary() success2 = self.addToLibrary()
return jsonified({'success': success or success2}) return {
'success': success or success2
}
@@ -1,5 +1,4 @@
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
import os import os
@@ -33,4 +32,6 @@ class Synoindex(Notification):
return True return True
def test(self): def test(self):
return jsonified({'success': os.path.isfile(self.index_path)}) return {
'success': os.path.isfile(self.index_path)
}
+11 -16
View File
@@ -1,12 +1,10 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from flask.helpers import url_for from couchpotato.environment import Env
from pytwitter import Api, parse_qsl from pytwitter import Api, parse_qsl
from werkzeug.utils import redirect
import oauth2 import oauth2
log = CPLog(__name__) log = CPLog(__name__)
@@ -70,10 +68,9 @@ class Twitter(Notification):
return True return True
def getAuthorizationUrl(self): def getAuthorizationUrl(self, host = None, **kwargs):
referer = getParam('host') callback_url = cleanHost(host) + '%snotify.%s.credentials/' % (Env.get('api_base').lstrip('/'), self.getName().lower())
callback_url = cleanHost(referer) + '%snotify.%s.credentials/' % (url_for('api.index').lstrip('/'), self.getName().lower())
oauth_consumer = oauth2.Consumer(self.consumer_key, self.consumer_secret) oauth_consumer = oauth2.Consumer(self.consumer_key, self.consumer_secret)
oauth_client = oauth2.Client(oauth_consumer) oauth_client = oauth2.Client(oauth_consumer)
@@ -82,31 +79,29 @@ class Twitter(Notification):
if resp['status'] != '200': if resp['status'] != '200':
log.error('Invalid response from Twitter requesting temp token: %s', resp['status']) log.error('Invalid response from Twitter requesting temp token: %s', resp['status'])
return jsonified({ return {
'success': False, 'success': False,
}) }
else: else:
self.request_token = dict(parse_qsl(content)) self.request_token = dict(parse_qsl(content))
auth_url = self.urls['authorize'] + ("?oauth_token=%s" % self.request_token['oauth_token']) auth_url = self.urls['authorize'] + ("?oauth_token=%s" % self.request_token['oauth_token'])
log.info('Redirecting to "%s"', auth_url) log.info('Redirecting to "%s"', auth_url)
return jsonified({ return {
'success': True, 'success': True,
'url': auth_url, 'url': auth_url,
}) }
def getCredentials(self): def getCredentials(self, oauth_verifier, **kwargs):
key = getParam('oauth_verifier')
token = oauth2.Token(self.request_token['oauth_token'], self.request_token['oauth_token_secret']) token = oauth2.Token(self.request_token['oauth_token'], self.request_token['oauth_token_secret'])
token.set_verifier(key) token.set_verifier(oauth_verifier)
oauth_consumer = oauth2.Consumer(key = self.consumer_key, secret = self.consumer_secret) oauth_consumer = oauth2.Consumer(key = self.consumer_key, secret = self.consumer_secret)
oauth_client = oauth2.Client(oauth_consumer, token) oauth_client = oauth2.Client(oauth_consumer, token)
resp, content = oauth_client.request(self.urls['access'], method = 'POST', body = 'oauth_verifier=%s' % key) resp, content = oauth_client.request(self.urls['access'], method = 'POST', body = 'oauth_verifier=%s' % oauth_verifier)
access_token = dict(parse_qsl(content)) access_token = dict(parse_qsl(content))
if resp['status'] != '200': if resp['status'] != '200':
@@ -121,4 +116,4 @@ class Twitter(Notification):
self.request_token = None self.request_token = None
return redirect(url_for('web.index') + 'settings/notifications/') return 'redirect', Env.get('web_base') + 'settings/notifications/'
@@ -31,6 +31,13 @@ config = [{
'default': '', 'default': '',
'type': 'password', 'type': 'password',
}, },
{
'name': 'only_first',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Only update the first host when movie snatched, useful for synced XBMC',
},
{ {
'name': 'on_snatch', 'name': 'on_snatch',
'default': 0, 'default': 0,
+36 -10
View File
@@ -1,8 +1,10 @@
from couchpotato.core.helpers.variable import splitString from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from flask.helpers import json from urllib2 import URLError
import base64 import base64
import json
import socket
import traceback import traceback
import urllib import urllib
@@ -20,19 +22,30 @@ class XBMC(Notification):
hosts = splitString(self.conf('host')) hosts = splitString(self.conf('host'))
successful = 0 successful = 0
max_successful = 0
for host in hosts: for host in hosts:
if self.use_json_notifications.get(host) is None: if self.use_json_notifications.get(host) is None:
self.getXBMCJSONversion(host, message = message) self.getXBMCJSONversion(host, message = message)
if self.use_json_notifications.get(host): if self.use_json_notifications.get(host):
response = self.request(host, [ calls = [
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}), ('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}),
('VideoLibrary.Scan', {}), ]
])
if not self.conf('only_first') or hosts.index(host) == 0:
calls.append(('VideoLibrary.Scan', {}))
max_successful += len(calls)
response = self.request(host, calls)
else: else:
response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message}) response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
response += self.request(host, [('VideoLibrary.Scan', {})])
if not self.conf('only_first') or hosts.index(host) == 0:
response += self.request(host, [('VideoLibrary.Scan', {})])
max_successful += 1
max_successful += 1
try: try:
for result in response: for result in response:
@@ -44,7 +57,7 @@ class XBMC(Notification):
except: except:
log.error('Failed parsing results: %s', traceback.format_exc()) log.error('Failed parsing results: %s', traceback.format_exc())
return successful == len(hosts) * 2 return successful == max_successful
def getXBMCJSONversion(self, host, message = ''): def getXBMCJSONversion(self, host, message = ''):
@@ -53,7 +66,7 @@ class XBMC(Notification):
# XBMC JSON-RPC version request # XBMC JSON-RPC version request
response = self.request(host, [ response = self.request(host, [
('JSONRPC.Version', {}) ('JSONRPC.Version', {})
]) ])
for result in response: for result in response:
if (result.get('result') and type(result['result']['version']).__name__ == 'int'): if (result.get('result') and type(result['result']['version']).__name__ == 'int'):
# only v2 and v4 return an int object # only v2 and v4 return an int object
@@ -138,7 +151,7 @@ class XBMC(Notification):
# <li>Error:<message> # <li>Error:<message>
# </html> # </html>
# #
response = self.urlopen(server, headers = headers) response = self.urlopen(server, headers = headers, timeout = 3, show_error = False)
if 'OK' in response: if 'OK' in response:
log.debug('Returned from non-JSON-type request %s: %s', (host, response)) log.debug('Returned from non-JSON-type request %s: %s', (host, response))
@@ -149,6 +162,13 @@ class XBMC(Notification):
# manually fake expected response array # manually fake expected response array
return [{'result': 'Error'}] return [{'result': 'Error'}]
except URLError, e:
if isinstance(e.reason, socket.timeout):
log.info('Couldn\'t send request to XBMC, assuming it\'s turned off')
return [{'result': 'Error'}]
else:
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
return [{'result': 'Error'}]
except: except:
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc()) log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
return [{'result': 'Error'}] return [{'result': 'Error'}]
@@ -177,11 +197,17 @@ class XBMC(Notification):
try: try:
log.debug('Sending request to %s: %s', (host, data)) log.debug('Sending request to %s: %s', (host, data))
rdata = self.urlopen(server, headers = headers, params = data, multipart = True) response = self.getJsonData(server, headers = headers, params = data, timeout = 3, show_error = False)
response = json.loads(rdata)
log.debug('Returned from request %s: %s', (host, response)) log.debug('Returned from request %s: %s', (host, response))
return response return response
except URLError, e:
if isinstance(e.reason, socket.timeout):
log.info('Couldn\'t send request to XBMC, assuming it\'s turned off')
return []
else:
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
return []
except: except:
log.error('Failed sending request to XBMC: %s', traceback.format_exc()) log.error('Failed sending request to XBMC: %s', traceback.format_exc())
return [] return []
+32 -18
View File
@@ -1,12 +1,13 @@
from StringIO import StringIO from StringIO import StringIO
from couchpotato import addView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString, \
toUnicode
from couchpotato.core.helpers.variable import getExt, md5 from couchpotato.core.helpers.variable import getExt, md5
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
from flask.templating import render_template_string
from multipartpost import MultipartPostHandler from multipartpost import MultipartPostHandler
from tornado import template
from tornado.web import StaticFileHandler
from urlparse import urlparse from urlparse import urlparse
import cookielib import cookielib
import glob import glob
@@ -37,6 +38,7 @@ class Plugin(object):
def registerPlugin(self): def registerPlugin(self):
addEvent('app.do_shutdown', self.doShutdown) addEvent('app.do_shutdown', self.doShutdown)
addEvent('plugin.running', self.isRunning) addEvent('plugin.running', self.isRunning)
self._running = []
def conf(self, attr, value = None, default = None): def conf(self, attr, value = None, default = None):
return Env.setting(attr, self.getName().lower(), value = value, default = default) return Env.setting(attr, self.getName().lower(), value = value, default = default)
@@ -44,35 +46,37 @@ class Plugin(object):
def getName(self): def getName(self):
return self.__class__.__name__ return self.__class__.__name__
def renderTemplate(self, parent_file, template, **params): def renderTemplate(self, parent_file, templ, **params):
template = open(os.path.join(os.path.dirname(parent_file), template), 'r').read() t = template.Template(open(os.path.join(os.path.dirname(parent_file), templ), 'r').read())
return render_template_string(template, **params) return t.generate(**params)
def registerStatic(self, plugin_file, add_to_head = True): def registerStatic(self, plugin_file, add_to_head = True):
# Register plugin path # Register plugin path
self.plugin_path = os.path.dirname(plugin_file) self.plugin_path = os.path.dirname(plugin_file)
static_folder = toUnicode(os.path.join(self.plugin_path, 'static'))
if not os.path.isdir(static_folder):
return
# Get plugin_name from PluginName # Get plugin_name from PluginName
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', self.__class__.__name__) s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', self.__class__.__name__)
class_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() class_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
# View path
path = 'api/%s/static/%s/' % (Env.setting('api_key'), class_name) path = 'api/%s/static/%s/' % (Env.setting('api_key'), class_name)
addView(path + '<path:filename>', self.showStatic, static = True)
# Add handler to Tornado
Env.get('app').add_handlers(".*$", [(Env.get('web_base') + path + '(.*)', StaticFileHandler, {'path': static_folder})])
# Register for HTML <HEAD>
if add_to_head: if add_to_head:
for f in glob.glob(os.path.join(self.plugin_path, 'static', '*')): for f in glob.glob(os.path.join(self.plugin_path, 'static', '*')):
ext = getExt(f) ext = getExt(f)
if ext in ['js', 'css']: if ext in ['js', 'css']:
fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f) fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f)
def showStatic(self, filename):
d = os.path.join(self.plugin_path, 'static')
from flask.helpers import send_from_directory
return send_from_directory(d, filename)
def createFile(self, path, content, binary = False): def createFile(self, path, content, binary = False):
path = ss(path) path = ss(path)
@@ -106,12 +110,14 @@ class Plugin(object):
# Fill in some headers # Fill in some headers
parsed_url = urlparse(url) parsed_url = urlparse(url)
host = parsed_url.hostname host = '%s%s' % (parsed_url.hostname, (':' + str(parsed_url.port) if parsed_url.port else ''))
headers['Referer'] = headers.get('Referer', '%s://%s' % (parsed_url.scheme, host)) headers['Referer'] = headers.get('Referer', '%s://%s' % (parsed_url.scheme, host))
headers['Host'] = headers.get('Host', host) headers['Host'] = headers.get('Host', host)
headers['User-Agent'] = headers.get('User-Agent', self.user_agent) headers['User-Agent'] = headers.get('User-Agent', self.user_agent)
headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip') headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip')
headers['Connection'] = headers.get('Connection', 'keep-alive')
headers['Cache-Control'] = headers.get('Cache-Control', 'max-age=0')
# Don't try for failed requests # Don't try for failed requests
if self.http_failed_disabled.get(host, 0) > 0: if self.http_failed_disabled.get(host, 0) > 0:
@@ -128,6 +134,10 @@ class Plugin(object):
self.wait(host) self.wait(host)
try: try:
# Make sure opener has the correct headers
if opener:
opener.add_headers = headers
if multipart: if multipart:
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data')) log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
request = urllib2.Request(url, params, headers) request = urllib2.Request(url, params, headers)
@@ -141,7 +151,12 @@ class Plugin(object):
response = opener.open(request, timeout = timeout) response = opener.open(request, timeout = timeout)
else: else:
log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data')) log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
data = tryUrlencode(params) if len(params) > 0 else None
if isinstance(params, (str, unicode)) and len(params) > 0:
data = params
else:
data = tryUrlencode(params) if len(params) > 0 else None
request = urllib2.Request(url, data, headers) request = urllib2.Request(url, data, headers)
if opener: if opener:
@@ -154,8 +169,10 @@ class Plugin(object):
buf = StringIO(response.read()) buf = StringIO(response.read())
f = gzip.GzipFile(fileobj = buf) f = gzip.GzipFile(fileobj = buf)
data = f.read() data = f.read()
f.close()
else: else:
data = response.read() data = response.read()
response.close()
self.http_failed_request[host] = 0 self.http_failed_request[host] = 0
except IOError: except IOError:
@@ -211,9 +228,6 @@ class Plugin(object):
def isRunning(self, value = None, boolean = True): def isRunning(self, value = None, boolean = True):
if not hasattr(self, '_running'):
self._running = []
if value is None: if value is None:
return self._running return self._running
+4 -6
View File
@@ -1,5 +1,4 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.helpers.request import getParam, jsonified
from couchpotato.core.helpers.variable import getUserDir from couchpotato.core.helpers.variable import getUserDir
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
import ctypes import ctypes
@@ -63,16 +62,15 @@ class FileBrowser(Plugin):
return driveletters return driveletters
def view(self): def view(self, path = '/', show_hidden = True, **kwargs):
path = getParam('path', '/')
home = getUserDir() home = getUserDir()
if not path: if not path:
path = home path = home
try: try:
dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True)) dirs = self.getDirectories(path = path, show_hidden = show_hidden)
except: except:
dirs = [] dirs = []
@@ -82,14 +80,14 @@ class FileBrowser(Plugin):
elif parent != '/' and parent[-2:] != ':\\': elif parent != '/' and parent[-2:] != ':\\':
parent += os.path.sep parent += os.path.sep
return jsonified({ return {
'is_root': path == '/', 'is_root': path == '/',
'empty': len(dirs) == 0, 'empty': len(dirs) == 0,
'parent': parent, 'parent': parent,
'home': home + os.path.sep, 'home': home + os.path.sep,
'platform': os.name, 'platform': os.name,
'dirs': dirs, 'dirs': dirs,
}) }
def is_hidden(self, filepath): def is_hidden(self, filepath):
+10 -13
View File
@@ -1,13 +1,12 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.helpers.variable import splitString, tryInt from couchpotato.core.helpers.variable import splitString, tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie from couchpotato.core.settings.model import Movie
from sqlalchemy.orm import joinedload_all from sqlalchemy.orm import joinedload_all
import random import random as rndm
import time import time
log = CPLog(__name__) log = CPLog(__name__)
@@ -43,14 +42,13 @@ class Dashboard(Plugin):
suggestions = fireEvent('movie.suggest', movies = identifiers, single = True) suggestions = fireEvent('movie.suggest', movies = identifiers, single = True)
return jsonified({ return {
'result': True, 'result': True,
'suggestions': suggestions 'suggestions': suggestions
}) }
def getSoonView(self): def getSoonView(self, limit_offset = None, random = False, late = False, **kwargs):
params = getParams()
db = get_session() db = get_session()
now = time.time() now = time.time()
@@ -85,7 +83,6 @@ class Dashboard(Plugin):
.options(joinedload_all('files')) .options(joinedload_all('files'))
# Add limit # Add limit
limit_offset = params.get('limit_offset')
limit = 12 limit = 12
if limit_offset: if limit_offset:
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
@@ -93,8 +90,8 @@ class Dashboard(Plugin):
all_movies = q.all() all_movies = q.all()
if params.get('random', False): if random:
random.shuffle(all_movies) rndm.shuffle(all_movies)
movies = [] movies = []
for movie in all_movies: for movie in all_movies:
@@ -126,18 +123,18 @@ class Dashboard(Plugin):
}) })
# Don't list older movies # Don't list older movies
if ((not params.get('late') and (not eta.get('dvd') or (eta.get('dvd') and eta.get('dvd') > (now - 2419200)))) or \ if ((not late and (not eta.get('dvd') or (eta.get('dvd') and eta.get('dvd') > (now - 2419200)))) or \
(params.get('late') and eta.get('dvd') and eta.get('dvd') < (now - 2419200))): (late and eta.get('dvd') and eta.get('dvd') < (now - 2419200))):
movies.append(temp) movies.append(temp)
if len(movies) >= limit: if len(movies) >= limit:
break break
db.expire_all() db.expire_all()
return jsonified({ return {
'success': True, 'success': True,
'empty': len(movies) == 0, 'empty': len(movies) == 0,
'movies': movies, 'movies': movies,
}) }
getLateView = getSoonView getLateView = getSoonView
+6 -14
View File
@@ -2,15 +2,13 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.helpers.variable import md5, getExt from couchpotato.core.helpers.variable import md5, getExt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.plugins.scanner.main import Scanner from couchpotato.core.plugins.scanner.main import Scanner
from couchpotato.core.settings.model import FileType, File from couchpotato.core.settings.model import FileType, File
from couchpotato.environment import Env from couchpotato.environment import Env
from flask.helpers import send_file from tornado.web import StaticFileHandler
from werkzeug.exceptions import NotFound
import os.path import os.path
import time import time
import traceback import traceback
@@ -25,7 +23,7 @@ class FileManager(Plugin):
addEvent('file.download', self.download) addEvent('file.download', self.download)
addEvent('file.types', self.getTypes) addEvent('file.types', self.getTypes)
addApiView('file.cache/<path:filename>', self.showCacheFile, static = True, docs = { addApiView('file.cache/(.*)', self.showCacheFile, static = True, docs = {
'desc': 'Return a file from the cp_data/cache directory', 'desc': 'Return a file from the cp_data/cache directory',
'params': { 'params': {
'filename': {'desc': 'path/filename of the wanted file'} 'filename': {'desc': 'path/filename of the wanted file'}
@@ -81,15 +79,9 @@ class FileManager(Plugin):
except: except:
log.error('Failed removing unused file: %s', traceback.format_exc()) log.error('Failed removing unused file: %s', traceback.format_exc())
def showCacheFile(self, filename = ''): def showCacheFile(self, route):
Env.get('app').add_handlers(".*$", [('%s%s' % (Env.get('api_base'), route), StaticFileHandler, {'path': Env.get('cache_dir')})])
file_path = os.path.join(Env.get('cache_dir'), os.path.basename(filename))
if not os.path.isfile(file_path):
log.error('File "%s" not found', file_path)
raise NotFound()
return send_file(file_path, conditional = True)
def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}): def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}):
@@ -160,6 +152,6 @@ class FileManager(Plugin):
def getTypesView(self): def getTypesView(self):
return jsonified({ return {
'types': self.getTypes() 'types': self.getTypes()
}) }
+17 -22
View File
@@ -1,6 +1,5 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam, getParams
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
@@ -47,9 +46,9 @@ class Logging(Plugin):
} }
}) })
def get(self): def get(self, nr = 0, **kwargs):
nr = int(getParam('nr', 0)) nr = tryInt(nr)
current_path = None current_path = None
total = 1 total = 1
@@ -71,16 +70,15 @@ class Logging(Plugin):
f = open(current_path, 'r') f = open(current_path, 'r')
log = f.read() log = f.read()
return jsonified({ return {
'success': True, 'success': True,
'log': toUnicode(log), 'log': toUnicode(log),
'total': total, 'total': total,
}) }
def partial(self): def partial(self, type = 'all', lines = 30, **kwargs):
log_type = getParam('type', 'all') total_lines = tryInt(lines)
total_lines = tryInt(getParam('lines', 30))
log_lines = [] log_lines = []
@@ -100,7 +98,7 @@ class Logging(Plugin):
brk = False brk = False
for line in reversed_lines: for line in reversed_lines:
if log_type == 'all' or '%s ' % log_type.upper() in line: if type == 'all' or '%s ' % type.upper() in line:
log_lines.append(line) log_lines.append(line)
if len(log_lines) >= total_lines: if len(log_lines) >= total_lines:
@@ -111,10 +109,10 @@ class Logging(Plugin):
break break
log_lines.reverse() log_lines.reverse()
return jsonified({ return {
'success': True, 'success': True,
'log': '[0m\n'.join(log_lines), 'log': '[0m\n'.join(log_lines),
}) }
def clear(self): def clear(self):
@@ -135,24 +133,21 @@ class Logging(Plugin):
except: except:
log.error('Couldn\'t delete file "%s": %s', (path, traceback.format_exc())) log.error('Couldn\'t delete file "%s": %s', (path, traceback.format_exc()))
return jsonified({ return {
'success': True 'success': True
}) }
def log(self): def log(self, type = 'error', **kwargs):
params = getParams()
try: try:
log_message = 'API log: %s' % params log_message = 'API log: %s' % kwargs
try: try:
getattr(log, params.get('type', 'error'))(log_message) getattr(log, type)(log_message)
except: except:
log.error(log_message) log.error(log_message)
except: except:
log.error('Couldn\'t log via API: %s', params) log.error('Couldn\'t log via API: %s', kwargs)
return {
return jsonified({
'success': True 'success': True
}) }
+8 -10
View File
@@ -1,7 +1,6 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import splitString, getTitle from couchpotato.core.helpers.variable import splitString, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
@@ -46,24 +45,23 @@ class Manage(Plugin):
}) })
if not Env.get('dev'): if not Env.get('dev'):
def updateLibrary(): addEvent('app.load', self.updateLibraryQuick)
self.updateLibrary(full = False)
addEvent('app.load', updateLibrary)
def getProgress(self): def getProgress(self):
return jsonified({ return {
'progress': self.in_progress 'progress': self.in_progress
}) }
def updateLibraryView(self): def updateLibraryView(self, full = 1, **kwargs):
full = getParam('full', default = 1)
fireEventAsync('manage.update', full = True if full == '1' else False) fireEventAsync('manage.update', full = True if full == '1' else False)
return jsonified({ return {
'success': True 'success': True
}) }
def updateLibraryQuick(self):
return self.updateLibrary(full = False)
def updateLibrary(self, full = True): def updateLibrary(self, full = True):
last_update = float(Env.prop('manage.last_update', default = 0)) last_update = float(Env.prop('manage.last_update', default = 0))
+44 -54
View File
@@ -2,7 +2,6 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString from couchpotato.core.helpers.encoding import toUnicode, simplifyString
from couchpotato.core.helpers.request import getParams, jsonified, getParam
from couchpotato.core.helpers.variable import getImdb, splitString from couchpotato.core.helpers.variable import getImdb, splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
@@ -125,15 +124,14 @@ class MoviePlugin(Plugin):
db.expire_all() db.expire_all()
def getView(self): def getView(self, id = None):
movie_id = getParam('id') movie = self.get(id) if id else None
movie = self.get(movie_id) if movie_id else None
return jsonified({ return {
'success': movie is not None, 'success': movie is not None,
'movie': movie, 'movie': movie,
}) }
def get(self, movie_id): def get(self, movie_id):
@@ -263,15 +261,14 @@ class MoviePlugin(Plugin):
db.expire_all() db.expire_all()
return ''.join(sorted(chars, key = str.lower)) return ''.join(sorted(chars, key = str.lower))
def listView(self): def listView(self, **kwargs):
params = getParams() status = splitString(kwargs.get('status', None))
status = splitString(params.get('status', None)) release_status = splitString(kwargs.get('release_status', None))
release_status = splitString(params.get('release_status', None)) limit_offset = kwargs.get('limit_offset', None)
limit_offset = params.get('limit_offset', None) starts_with = kwargs.get('starts_with', None)
starts_with = params.get('starts_with', None) search = kwargs.get('search', None)
search = params.get('search', None) order = kwargs.get('order', None)
order = params.get('order', None)
total_movies, movies = self.list( total_movies, movies = self.list(
status = status, status = status,
@@ -282,32 +279,31 @@ class MoviePlugin(Plugin):
order = order order = order
) )
return jsonified({ return {
'success': True, 'success': True,
'empty': len(movies) == 0, 'empty': len(movies) == 0,
'total': total_movies, 'total': total_movies,
'movies': movies, 'movies': movies,
}) }
def charView(self): def charView(self, **kwargs):
params = getParams() status = splitString(kwargs.get('status', None))
status = splitString(params.get('status', None)) release_status = splitString(kwargs.get('release_status', None))
release_status = splitString(params.get('release_status', None))
chars = self.availableChars(status, release_status) chars = self.availableChars(status, release_status)
return jsonified({ return {
'success': True, 'success': True,
'empty': len(chars) == 0, 'empty': len(chars) == 0,
'chars': chars, 'chars': chars,
}) }
def refresh(self): def refresh(self, id = ''):
db = get_session() db = get_session()
for id in splitString(getParam('id')): for x in splitString(id):
movie = db.query(Movie).filter_by(id = id).first() movie = db.query(Movie).filter_by(id = x).first()
if movie: if movie:
@@ -316,17 +312,16 @@ class MoviePlugin(Plugin):
for title in movie.library.titles: for title in movie.library.titles:
if title.default: default_title = title.title if title.default: default_title = title.title
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True) fireEvent('notify.frontend', type = 'movie.busy.%s' % x, data = True)
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id)) fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
db.expire_all() db.expire_all()
return jsonified({ return {
'success': True, 'success': True,
}) }
def search(self): def search(self, q = ''):
q = getParam('q')
cache_key = u'%s/%s' % (__name__, simplifyString(q)) cache_key = u'%s/%s' % (__name__, simplifyString(q))
movies = Env.get('cache').get(cache_key) movies = Env.get('cache').get(cache_key)
@@ -338,11 +333,11 @@ class MoviePlugin(Plugin):
movies = fireEvent('movie.search', q = q, merge = True) movies = fireEvent('movie.search', q = q, merge = True)
Env.get('cache').set(cache_key, movies) Env.get('cache').set(cache_key, movies)
return jsonified({ return {
'success': True, 'success': True,
'empty': len(movies) == 0 if movies else 0, 'empty': len(movies) == 0 if movies else 0,
'movies': movies, 'movies': movies,
}) }
def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None): def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None):
@@ -432,33 +427,30 @@ class MoviePlugin(Plugin):
return movie_dict return movie_dict
def addView(self): def addView(self, **kwargs):
params = getParams() movie_dict = self.add(params = kwargs)
movie_dict = self.add(params) return {
return jsonified({
'success': True, 'success': True,
'added': True if movie_dict else False, 'added': True if movie_dict else False,
'movie': movie_dict, 'movie': movie_dict,
}) }
def edit(self): def edit(self, id = '', **kwargs):
params = getParams()
db = get_session() db = get_session()
available_status = fireEvent('status.get', 'available', single = True) available_status = fireEvent('status.get', 'available', single = True)
ids = splitString(params.get('id')) ids = splitString(id)
for movie_id in ids: for movie_id in ids:
m = db.query(Movie).filter_by(id = movie_id).first() m = db.query(Movie).filter_by(id = movie_id).first()
if not m: if not m:
continue continue
m.profile_id = params.get('profile_id') m.profile_id = kwargs.get('profile_id')
# Remove releases # Remove releases
for rel in m.releases: for rel in m.releases:
@@ -467,9 +459,9 @@ class MoviePlugin(Plugin):
db.commit() db.commit()
# Default title # Default title
if params.get('default_title'): if kwargs.get('default_title'):
for title in m.library.titles: for title in m.library.titles:
title.default = toUnicode(params.get('default_title', '')).lower() == toUnicode(title.title).lower() title.default = toUnicode(kwargs.get('default_title', '')).lower() == toUnicode(title.title).lower()
db.commit() db.commit()
@@ -479,21 +471,19 @@ class MoviePlugin(Plugin):
fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id)) fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
db.expire_all() db.expire_all()
return jsonified({ return {
'success': True, 'success': True,
}) }
def deleteView(self): def deleteView(self, id = '', **kwargs):
params = getParams() ids = splitString(id)
ids = splitString(params.get('id'))
for movie_id in ids: for movie_id in ids:
self.delete(movie_id, delete_from = params.get('delete_from', 'all')) self.delete(movie_id, delete_from = kwargs.get('delete_from', 'all'))
return jsonified({ return {
'success': True, 'success': True,
}) }
def delete(self, movie_id, delete_from = None): def delete(self, movie_id, delete_from = None):
@@ -136,7 +136,7 @@ MA.Release = new Class({
} }
// Create release // Create release
new Element('div', { var item = new Element('div', {
'class': 'item '+status.identifier, 'class': 'item '+status.identifier,
'id': 'release_'+release.id 'id': 'release_'+release.id
}).adopt( }).adopt(
@@ -165,11 +165,12 @@ MA.Release = new Class({
'click': function(e){ 'click': function(e){
(e).preventDefault(); (e).preventDefault();
self.ignore(release); self.ignore(release);
this.getParent('.item').toggleClass('ignored')
} }
} }
}) })
).inject(self.release_container) ).inject(self.release_container);
release['el'] = item;
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){ if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched')) if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched'))
@@ -189,7 +190,7 @@ MA.Release = new Class({
} }
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){ 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 = new Element('div.buttons.try_container').inject(self.release_container, 'top');
self.trynext_container.adopt( self.trynext_container.adopt(
@@ -318,6 +319,17 @@ MA.Release = new Class({
Api.request('release.ignore', { Api.request('release.ignore', {
'data': { 'data': {
'id': release.id 'id': release.id
},
'onComplete': function(){
var el = release.el;
if(el.hasClass('failed') || el.hasClass('ignored')){
el.removeClass('failed').removeClass('ignored');
el.getElement('.release_status').set('text', 'available');
}
else {
el.addClass('ignored');
el.getElement('.release_status').set('text', 'ignored');
}
} }
}) })
+62 -59
View File
@@ -8,7 +8,7 @@
.movies > div { .movies > div {
clear: both; clear: both;
} }
.movies > div .message { .movies > div .message {
display: block; display: block;
padding: 20px; padding: 20px;
@@ -20,11 +20,11 @@
padding: 20px; padding: 20px;
display: block; display: block;
} }
.movies.thumbs_list > div:not(.description) { .movies.thumbs_list > div:not(.description) {
margin-right: -4px; margin-right: -4px;
} }
.movies .loading { .movies .loading {
display: block; display: block;
padding: 20px 0 0 0; padding: 20px 0 0 0;
@@ -43,26 +43,26 @@
margin-top: -20px; margin-top: -20px;
overflow: hidden; overflow: hidden;
} }
.movies .loading .spinner { .movies .loading .spinner {
display: inline-block; display: inline-block;
} }
.movies .loading .message { .movies .loading .message {
margin: 0 20px; margin: 0 20px;
} }
.movies h2 { .movies h2 {
margin-bottom: 20px; margin-bottom: 20px;
} }
@media all and (max-width: 480px) { @media all and (max-width: 480px) {
.movies h2 { .movies h2 {
font-size: 25px; font-size: 25px;
margin-bottom: 10px; margin-bottom: 10px;
} }
} }
.movies > .description { .movies > .description {
position: absolute; position: absolute;
top: 30px; top: 30px;
@@ -73,17 +73,17 @@
.movies:hover > .description { .movies:hover > .description {
opacity: 1; opacity: 1;
} }
@media all and (max-width: 860px) { @media all and (max-width: 860px) {
.movies > .description { .movies > .description {
display: none; display: none;
} }
} }
.movies.thumbs_list { .movies.thumbs_list {
padding: 20px 0 20px; padding: 20px 0 20px;
} }
.home .movies { .home .movies {
padding-top: 6px; padding-top: 6px;
} }
@@ -99,27 +99,27 @@
transition-property: width, height; transition-property: width, height;
background: rgba(0,0,0,.2); background: rgba(0,0,0,.2);
} }
.movies.mass_edit_list .movie { .movies.mass_edit_list .movie {
padding-left: 22px; padding-left: 22px;
background: none; background: none;
} }
.movies.details_list .movie { .movies.details_list .movie {
padding-left: 120px; padding-left: 120px;
} }
.movies.list_list .movie:not(.details_view), .movies.list_list .movie:not(.details_view),
.movies.mass_edit_list .movie { .movies.mass_edit_list .movie {
height: 30px; height: 30px;
border-bottom: 1px solid rgba(255,255,255,.15); border-bottom: 1px solid rgba(255,255,255,.15);
} }
.movies.list_list .movie:last-child, .movies.list_list .movie:last-child,
.movies.mass_edit_list .movie:last-child { .movies.mass_edit_list .movie:last-child {
border: none; border: none;
} }
.movies.thumbs_list .movie { .movies.thumbs_list .movie {
width: 16.66667%; width: 16.66667%;
height: auto; height: auto;
@@ -128,7 +128,7 @@
padding: 0; padding: 0;
vertical-align: top; vertical-align: top;
} }
@media all and (max-width: 800px) { @media all and (max-width: 800px) {
.movies.thumbs_list .movie { .movies.thumbs_list .movie {
width: 25%; width: 25%;
@@ -165,7 +165,7 @@
.movies.mass_edit_list .movie .data { .movies.mass_edit_list .movie .data {
padding-left: 8px; padding-left: 8px;
} }
.movies.thumbs_list .data { .movies.thumbs_list .data {
position: absolute; position: absolute;
left: 0; left: 0;
@@ -176,7 +176,7 @@
background: none; background: none;
transition: none; transition: none;
} }
.movies.thumbs_list .movie:hover .data { .movies.thumbs_list .movie:hover .data {
background: rgba(0,0,0,0.9); background: rgba(0,0,0,0.9);
} }
@@ -218,7 +218,7 @@
.movies.mass_edit_list .poster { .movies.mass_edit_list .poster {
display: none; display: none;
} }
.movies.thumbs_list .poster { .movies.thumbs_list .poster {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -243,7 +243,7 @@
bottom: 0; bottom: 0;
opacity: 0; opacity: 0;
} }
.movies .info { .movies .info {
position: relative; position: relative;
height: 100%; height: 100%;
@@ -272,7 +272,7 @@
display: inline-block; display: inline-block;
padding-right: 55px; padding-right: 55px;
} }
.movies .info .title span { .movies .info .title span {
display: inline-block; display: inline-block;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -282,14 +282,14 @@
height: 100%; height: 100%;
line-height: 30px; line-height: 30px;
} }
.movies.thumbs_list .info .title span { .movies.thumbs_list .info .title span {
white-space: normal; white-space: normal;
overflow: auto; overflow: auto;
height: auto; height: auto;
text-align: left; text-align: left;
} }
@media all and (max-width: 480px) { @media all and (max-width: 480px) {
.movies.thumbs_list .movie .info .title span, .movies.thumbs_list .movie .info .title span,
.movies.thumbs_list .movie .info .year { .movies.thumbs_list .movie .info .year {
@@ -298,21 +298,21 @@
overflow: hidden; overflow: hidden;
} }
} }
.movies.list_list .movie:not(.details_view) .info .title, .movies.list_list .movie:not(.details_view) .info .title,
.movies.mass_edit_list .info .title { .movies.mass_edit_list .info .title {
font-size: 16px; font-size: 16px;
font-weight: normal; font-weight: normal;
width: auto; width: auto;
} }
.movies.thumbs_list .movie:not(.no_thumbnail) .info { .movies.thumbs_list .movie:not(.no_thumbnail) .info {
display: none; display: none;
} }
.movies.thumbs_list .movie:hover .info { .movies.thumbs_list .movie:hover .info {
display: block; display: block;
} }
.movies.thumbs_list .info .title { .movies.thumbs_list .info .title {
font-size: 21px; font-size: 21px;
word-wrap: break-word; word-wrap: break-word;
@@ -334,7 +334,7 @@
font-size: 1.25em; font-size: 1.25em;
right: 10px; right: 10px;
} }
.movies.thumbs_list .info .year { .movies.thumbs_list .info .year {
font-size: 23px; font-size: 23px;
margin: 0; margin: 0;
@@ -344,7 +344,7 @@
right: auto; right: auto;
color: #FFF; color: #FFF;
} }
.touch_enabled .movies.list_list .movie .info .year { .touch_enabled .movies.list_list .movie .info .year {
font-size: 1em; font-size: 1em;
} }
@@ -371,24 +371,24 @@
display: block; display: block;
min-height: 20px; min-height: 20px;
} }
.movies.list_list .movie:hover .data .quality { .movies.list_list .movie:hover .data .quality {
display: none; display: none;
} }
.touch_enabled .movies.list_list .movie .data .quality { .touch_enabled .movies.list_list .movie .data .quality {
position: relative; position: relative;
display: inline-block; display: inline-block;
margin: 0; margin: 0;
top: -4px; top: -4px;
} }
@media all and (max-width: 480px) { @media all and (max-width: 480px) {
.movies .data .quality { .movies .data .quality {
display: none; display: none;
} }
} }
.movies .status_suggest .data .quality, .movies .status_suggest .data .quality,
.movies.thumbs_list .data .quality { .movies.thumbs_list .data .quality {
display: none; display: none;
@@ -417,13 +417,13 @@
z-index: 1; z-index: 1;
top: 5px; top: 5px;
} }
.movies .data .quality .available, .movies .data .quality .available,
.movies .data .quality .snatched { .movies .data .quality .snatched {
opacity: 1; opacity: 1;
cursor: pointer; cursor: pointer;
} }
.movies .data .quality .available { background-color: #578bc3; } .movies .data .quality .available { background-color: #578bc3; }
.movies .data .quality .snatched { background-color: #369545; } .movies .data .quality .snatched { background-color: #369545; }
.movies .data .quality .done { .movies .data .quality .done {
@@ -454,18 +454,18 @@
display: none !important; display: none !important;
} }
} }
.movies .movie:hover .data .actions, .movies .movie:hover .data .actions,
.touch_enabled .movies .movie .data .actions { .touch_enabled .movies .movie .data .actions {
opacity: 1; opacity: 1;
display: inline-block; display: inline-block;
} }
.movies.details_list .data .actions { .movies.details_list .data .actions {
top: auto; top: auto;
bottom: 18px; bottom: 18px;
} }
.movies .movie:hover .actions { .movies .movie:hover .actions {
opacity: 1; opacity: 1;
display: inline-block; display: inline-block;
@@ -475,7 +475,7 @@
right: 10px; right: 10px;
top: auto; top: auto;
} }
.movies .movie:hover .action { opacity: 0.6; } .movies .movie:hover .action { opacity: 0.6; }
.movies .movie:hover .action:hover { opacity: 1; } .movies .movie:hover .action:hover { opacity: 1; }
@@ -497,7 +497,7 @@
.movies .data .action.delete { color: #e9b0b0; } .movies .data .action.delete { color: #e9b0b0; }
.movies .data .action.directory { color: #ffed92; } .movies .data .action.directory { color: #ffed92; }
.movies .data .action.readd { color: #c2fac5; } .movies .data .action.readd { color: #c2fac5; }
.movies.mass_edit_list .movie .data .actions { .movies.mass_edit_list .movie .data .actions {
display: none; display: none;
} }
@@ -558,13 +558,15 @@
.movies .options .table .item { .movies .options .table .item {
border-bottom: 1px solid rgba(255,255,255,0.1); border-bottom: 1px solid rgba(255,255,255,0.1);
} }
.movies .options .table .item.ignored span { .movies .options .table .item.ignored span,
.movies .options .table .item.failed span {
text-decoration: line-through; text-decoration: line-through;
color: rgba(255,255,255,0.4); color: rgba(255,255,255,0.4);
} }
.movies .options .table .item.ignored .delete:before { .movies .options .table .item.ignored .delete:before,
display: inline-block; .movies .options .table .item.failed .delete:before {
content: "\e04b"; display: inline-block;
content: "\e04b";
transform: scale(-1, 1); transform: scale(-1, 1);
} }
@@ -616,7 +618,8 @@
.movies .options .table a:hover { opacity: 1; } .movies .options .table a:hover { opacity: 1; }
.movies .options .table a.download { color: #a7fbaf; } .movies .options .table a.download { color: #a7fbaf; }
.movies .options .table a.delete { color: #fda3a3; } .movies .options .table a.delete { color: #fda3a3; }
.movies .options .table .ignored a.delete { color: #b5fda3; } .movies .options .table .ignored a.delete,
.movies .options .table .failed a.delete { color: #b5fda3; }
.movies .options .table .head > * { .movies .options .table .head > * {
font-weight: bold; font-weight: bold;
@@ -695,7 +698,7 @@
.touch_enabled .movies .movie .trynext { .touch_enabled .movies .movie .trynext {
display: none; display: none;
} }
@media all and (max-width: 480px) { @media all and (max-width: 480px) {
.movies .movie .trynext { .movies .movie .trynext {
display: none; display: none;
@@ -709,14 +712,14 @@
.touch_enabled .movies.details_list .movie .trynext { .touch_enabled .movies.details_list .movie .trynext {
opacity: 1; opacity: 1;
} }
.movies.details_list .movie .trynext { .movies.details_list .movie .trynext {
background: #47515f; background: #47515f;
padding: 0; padding: 0;
right: 0; right: 0;
height: 25px; height: 25px;
} }
.movies .movie .trynext a { .movies .movie .trynext a {
background-position: 5px center; background-position: 5px center;
padding: 0 5px 0 25px; padding: 0 5px 0 25px;
@@ -761,7 +764,7 @@
display: none; display: none;
} }
} }
.movies .alph_nav .menus { .movies .alph_nav .menus {
display: inline-block; display: inline-block;
float: right; float: right;
@@ -785,7 +788,7 @@
border-right: 1px solid rgba(255,255,255,.07); border-right: 1px solid rgba(255,255,255,.07);
} }
.movies .alph_nav .numbers li, .movies .alph_nav .numbers li,
.movies .alph_nav .actions li { .movies .alph_nav .actions li {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
@@ -795,7 +798,7 @@
border: 1px solid transparent; border: 1px solid transparent;
transition: all 0.1s ease-in-out; transition: all 0.1s ease-in-out;
} }
.movies .alph_nav .numbers li { .movies .alph_nav .numbers li {
width: 30px; width: 30px;
height: 30px; height: 30px;
@@ -804,14 +807,14 @@
.movies .alph_nav .numbers li.letter_all { .movies .alph_nav .numbers li.letter_all {
width: 60px; width: 60px;
} }
.movies .alph_nav li.available { .movies .alph_nav li.available {
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
opacity: 1; opacity: 1;
} }
.movies .alph_nav li.active.available, .movies .alph_nav li.active.available,
.movies .alph_nav li.available:hover { .movies .alph_nav li.available:hover {
background: rgba(0,0,0,.1); background: rgba(0,0,0,.1);
} }
@@ -832,12 +835,12 @@
.movies .alph_nav .search input:focus { .movies .alph_nav .search input:focus {
background: rgba(0,0,0,.08); background: rgba(0,0,0,.08);
} }
.movies .alph_nav .search input::-webkit-input-placeholder { .movies .alph_nav .search input::-webkit-input-placeholder {
color: #444; color: #444;
opacity: .6; opacity: .6;
} }
.movies .alph_nav .search:before { .movies .alph_nav .search:before {
font-family: 'Elusive-Icons'; font-family: 'Elusive-Icons';
content: "\e03e"; content: "\e03e";
@@ -960,10 +963,10 @@
text-align: center; text-align: center;
display: block; display: block;
} }
.movies .alph_nav .more_menu.filter { .movies .alph_nav .more_menu.filter {
} }
.movies .alph_nav .more_menu.filter > a:before { .movies .alph_nav .more_menu.filter > a:before {
content: "\e0e8"; content: "\e0e8";
font-family: 'Elusive-Icons'; font-family: 'Elusive-Icons';
@@ -971,7 +974,7 @@
display: block; display: block;
text-align: center; text-align: center;
} }
.movies .alph_nav .more_menu.filter .wrapper { .movies .alph_nav .more_menu.filter .wrapper {
right: 88px; right: 88px;
width: 300px; width: 300px;
+19 -25
View File
@@ -2,7 +2,6 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParams, getParam
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Profile, ProfileType, Movie from couchpotato.core.settings.model import Profile, ProfileType, Movie
@@ -48,10 +47,10 @@ class ProfilePlugin(Plugin):
def allView(self): def allView(self):
return jsonified({ return {
'success': True, 'success': True,
'list': self.all() 'list': self.all()
}) }
def all(self): def all(self):
@@ -65,30 +64,28 @@ class ProfilePlugin(Plugin):
db.expire_all() db.expire_all()
return temp return temp
def save(self): def save(self, **kwargs):
params = getParams()
db = get_session() db = get_session()
p = db.query(Profile).filter_by(id = params.get('id')).first() p = db.query(Profile).filter_by(id = kwargs.get('id')).first()
if not p: if not p:
p = Profile() p = Profile()
db.add(p) db.add(p)
p.label = toUnicode(params.get('label')) p.label = toUnicode(kwargs.get('label'))
p.order = params.get('order', p.order if p.order else 0) p.order = kwargs.get('order', p.order if p.order else 0)
p.core = params.get('core', False) p.core = kwargs.get('core', False)
#delete old types #delete old types
[db.delete(t) for t in p.types] [db.delete(t) for t in p.types]
order = 0 order = 0
for type in params.get('types', []): for type in kwargs.get('types', []):
t = ProfileType( t = ProfileType(
order = order, order = order,
finish = type.get('finish') if order > 0 else 1, finish = type.get('finish') if order > 0 else 1,
wait_for = params.get('wait_for'), wait_for = kwargs.get('wait_for'),
quality_id = type.get('quality_id') quality_id = type.get('quality_id')
) )
p.types.append(t) p.types.append(t)
@@ -99,10 +96,10 @@ class ProfilePlugin(Plugin):
profile_dict = p.to_dict(self.to_dict) profile_dict = p.to_dict(self.to_dict)
return jsonified({ return {
'success': True, 'success': True,
'profile': profile_dict 'profile': profile_dict
}) }
def default(self): def default(self):
@@ -113,28 +110,25 @@ class ProfilePlugin(Plugin):
db.expire_all() db.expire_all()
return default_dict return default_dict
def saveOrder(self): def saveOrder(self, **kwargs):
params = getParams()
db = get_session() db = get_session()
order = 0 order = 0
for profile in params.get('ids', []): for profile in kwargs.get('ids', []):
p = db.query(Profile).filter_by(id = profile).first() p = db.query(Profile).filter_by(id = profile).first()
p.hide = params.get('hidden')[order] p.hide = kwargs.get('hidden')[order]
p.order = order p.order = order
order += 1 order += 1
db.commit() db.commit()
return jsonified({ return {
'success': True 'success': True
}) }
def delete(self): def delete(self, id = None):
id = getParam('id')
db = get_session() db = get_session()
@@ -154,10 +148,10 @@ class ProfilePlugin(Plugin):
message = log.error('Failed deleting Profile: %s', e) message = log.error('Failed deleting Profile: %s', e)
db.expire_all() db.expire_all()
return jsonified({ return {
'success': success, 'success': success,
'message': message 'message': message
}) }
def fill(self): def fill(self):
+7 -10
View File
@@ -2,7 +2,6 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.helpers.variable import mergeDicts, md5, getExt from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
@@ -53,10 +52,10 @@ class QualityPlugin(Plugin):
def allView(self): def allView(self):
return jsonified({ return {
'success': True, 'success': True,
'list': self.all() 'list': self.all()
}) }
def all(self): def all(self):
@@ -88,20 +87,18 @@ class QualityPlugin(Plugin):
if identifier == q.get('identifier'): if identifier == q.get('identifier'):
return q return q
def saveSize(self): def saveSize(self, **kwargs):
params = getParams()
db = get_session() db = get_session()
quality = db.query(Quality).filter_by(identifier = params.get('identifier')).first() quality = db.query(Quality).filter_by(identifier = kwargs.get('identifier')).first()
if quality: if quality:
setattr(quality, params.get('value_type'), params.get('value')) setattr(quality, kwargs.get('value_type'), kwargs.get('value'))
db.commit() db.commit()
return jsonified({ return {
'success': True 'success': True
}) }
def fill(self): def fill(self):
+14 -19
View File
@@ -2,7 +2,6 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.request import getParam, jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.plugins.scanner.main import Scanner from couchpotato.core.plugins.scanner.main import Scanner
@@ -108,13 +107,11 @@ class Release(Plugin):
# Check database and update/insert if necessary # Check database and update/insert if necessary
return fireEvent('file.add', path = filepath, part = fireEvent('scanner.partnumber', file, single = True), type_tuple = Scanner.file_types.get(type), properties = properties, single = True) return fireEvent('file.add', path = filepath, part = fireEvent('scanner.partnumber', file, single = True), type_tuple = Scanner.file_types.get(type), properties = properties, single = True)
def deleteView(self): def deleteView(self, id = None, **kwargs):
release_id = getParam('id') return {
'success': self.delete(id)
return jsonified({ }
'success': self.delete(release_id)
})
def delete(self, id): def delete(self, id):
@@ -146,25 +143,23 @@ class Release(Plugin):
return False return False
def ignore(self): def ignore(self, id = None, **kwargs):
db = get_session() db = get_session()
id = getParam('id')
rel = db.query(Relea).filter_by(id = id).first() rel = db.query(Relea).filter_by(id = id).first()
if rel: if rel:
ignored_status, available_status = fireEvent('status.get', ['ignored', 'available'], single = True) ignored_status, failed_status, available_status = fireEvent('status.get', ['ignored', 'failed', 'available'], single = True)
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id') rel.status_id = available_status.get('id') if rel.status_id in [ignored_status.get('id'), failed_status.get('id')] else ignored_status.get('id')
db.commit() db.commit()
return jsonified({ return {
'success': True 'success': True
}) }
def download(self): def download(self, id = None, **kwargs):
db = get_session() db = get_session()
id = getParam('id')
snatched_status, done_status = fireEvent('status.get', ['snatched', 'done'], single = True) snatched_status, done_status = fireEvent('status.get', ['snatched', 'done'], single = True)
@@ -199,12 +194,12 @@ class Release(Plugin):
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name']) fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name'])
return jsonified({ return {
'success': success 'success': success
}) }
else: else:
log.error('Couldn\'t find release with id: %s', id) log.error('Couldn\'t find release with id: %s', id)
return jsonified({ return {
'success': False 'success': False
}) }
@@ -14,10 +14,14 @@ rename_options = {
'year': 'Year (2011)', 'year': 'Year (2011)',
'first': 'First letter (M)', 'first': 'First letter (M)',
'quality': 'Quality (720p)', 'quality': 'Quality (720p)',
'quality_type': '(HD) or (SD)',
'video': 'Video (x264)', 'video': 'Video (x264)',
'audio': 'Audio (DTS)', 'audio': 'Audio (DTS)',
'group': 'Releasegroup name', 'group': 'Releasegroup name',
'source': 'Source media (Bluray)', 'source': 'Source media (Bluray)',
'resolution_width': 'resolution width (1280)',
'resolution_height': 'resolution height (720)',
'audio_channels': 'audio channels (7.1)',
'original': 'Original filename', 'original': 'Original filename',
'original_folder': 'Original foldername', 'original_folder': 'Original foldername',
'imdb_id': 'IMDB id (tt0123456)', 'imdb_id': 'IMDB id (tt0123456)',
+8 -9
View File
@@ -2,7 +2,6 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode, ss from couchpotato.core.helpers.encoding import toUnicode, ss
from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \ from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
getImdb, link, symlink, tryInt getImdb, link, symlink, tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
@@ -59,13 +58,12 @@ class Renamer(Plugin):
return True return True
def scanView(self): def scanView(self, **kwargs):
params = getParams() async = tryInt(kwargs.get('async', None))
async = tryInt(params.get('async', None)) movie_folder = kwargs.get('movie_folder', None)
movie_folder = params.get('movie_folder', None) downloader = kwargs.get('downloader', None)
downloader = params.get('downloader', None) download_id = kwargs.get('download_id', None)
download_id = params.get('download_id', None)
fire_handle = fireEvent if not async else fireEventAsync fire_handle = fireEvent if not async else fireEventAsync
@@ -74,9 +72,9 @@ class Renamer(Plugin):
download_info = {'id': download_id, 'downloader': downloader} if download_id else None download_info = {'id': download_id, 'downloader': downloader} if download_id else None
) )
return jsonified({ return {
'success': True 'success': True
}) }
def scan(self, movie_folder = None, download_info = None): def scan(self, movie_folder = None, download_info = None):
@@ -183,6 +181,7 @@ class Renamer(Plugin):
'source': group['meta_data']['source'], 'source': group['meta_data']['source'],
'resolution_width': group['meta_data'].get('resolution_width'), 'resolution_width': group['meta_data'].get('resolution_width'),
'resolution_height': group['meta_data'].get('resolution_height'), 'resolution_height': group['meta_data'].get('resolution_height'),
'audio_channels': group['meta_data'].get('audio_channels'),
'imdb_id': library['identifier'], 'imdb_id': library['identifier'],
'cd': '', 'cd': '',
'cd_nr': '', 'cd_nr': '',
+4 -2
View File
@@ -432,6 +432,7 @@ class Scanner(Plugin):
data['audio'] = meta.get('audio', self.getCodec(cur_file, self.codecs['audio'])) data['audio'] = meta.get('audio', self.getCodec(cur_file, self.codecs['audio']))
data['resolution_width'] = meta.get('resolution_width', 720) data['resolution_width'] = meta.get('resolution_width', 720)
data['resolution_height'] = meta.get('resolution_height', 480) data['resolution_height'] = meta.get('resolution_height', 480)
data['audio_channels'] = meta.get('audio_channels', 2.0)
data['aspect'] = meta.get('resolution_width', 720) / meta.get('resolution_height', 480) data['aspect'] = meta.get('resolution_width', 720) / meta.get('resolution_height', 480)
except: except:
log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc())) log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc()))
@@ -476,6 +477,7 @@ class Scanner(Plugin):
'audio': ac, 'audio': ac,
'resolution_width': tryInt(p.video[0].width), 'resolution_width': tryInt(p.video[0].width),
'resolution_height': tryInt(p.video[0].height), 'resolution_height': tryInt(p.video[0].height),
'audio_channels': p.audio[0].channels,
} }
except ParseError: except ParseError:
log.debug('Failed to parse meta for %s', filename) log.debug('Failed to parse meta for %s', filename)
@@ -582,7 +584,7 @@ class Scanner(Plugin):
movie = fireEvent('movie.by_hash', file = cur_file, merge = True) movie = fireEvent('movie.by_hash', file = cur_file, merge = True)
if len(movie) > 0: if len(movie) > 0:
imdb_id = movie[0]['imdb'] imdb_id = movie[0].get('imdb')
if imdb_id: if imdb_id:
log.debug('Found movie via OpenSubtitleHash: %s', cur_file) log.debug('Found movie via OpenSubtitleHash: %s', cur_file)
break break
@@ -600,7 +602,7 @@ class Scanner(Plugin):
movie = fireEvent('movie.search', q = '%(name)s %(year)s' % name_year, merge = True, limit = 1) movie = fireEvent('movie.search', q = '%(name)s %(year)s' % name_year, merge = True, limit = 1)
if len(movie) > 0: if len(movie) > 0:
imdb_id = movie[0]['imdb'] imdb_id = movie[0].get('imdb')
log.debug('Found movie via search: %s', cur_file) log.debug('Found movie via search: %s', cur_file)
if imdb_id: break if imdb_id: break
else: else:
+1 -1
View File
@@ -18,7 +18,7 @@ class Score(Plugin):
def calculate(self, nzb, movie): def calculate(self, nzb, movie):
''' Calculate the score of a NZB, used for sorting later ''' ''' Calculate the score of a NZB, used for sorting later '''
score = nameScore(toUnicode(nzb['name']), movie['library']['year']) score = nameScore(toUnicode(nzb['name'] + ' ' + nzb.get('name_extra', '')), movie['library']['year'])
for movie_title in movie['library']['titles']: for movie_title in movie['library']['titles']:
score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title['title'])) score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title['title']))
+3 -5
View File
@@ -1,6 +1,6 @@
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import simplifyString from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.environment import Env from couchpotato.environment import Env
import re import re
@@ -42,10 +42,8 @@ def nameScore(name, year):
# Contains preferred word # Contains preferred word
nzb_words = re.split('\W+', simplifyString(name)) nzb_words = re.split('\W+', simplifyString(name))
preferred_words = [x.strip() for x in Env.setting('preferred_words', section = 'searcher').split(',')] preferred_words = splitString(Env.setting('preferred_words', section = 'searcher'))
for word in preferred_words: score += 100 * len(list(set(nzb_words) & set(preferred_words)))
if word.strip() and word.strip().lower() in nzb_words:
score = score + 100
return score return score
+17 -15
View File
@@ -2,7 +2,6 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import simplifyString, toUnicode from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import md5, getTitle, splitString, \ from couchpotato.core.helpers.variable import md5, getTitle, splitString, \
possibleTitles possibleTitles
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
@@ -67,15 +66,15 @@ class Searcher(Plugin):
else: else:
fireEvent('notify.frontend', type = 'searcher.already_started', data = True, message = 'Full search already in progress') fireEvent('notify.frontend', type = 'searcher.already_started', data = True, message = 'Full search already in progress')
return jsonified({ return {
'success': not in_progress 'success': not in_progress
}) }
def getProgress(self): def getProgress(self):
return jsonified({ return {
'progress': self.in_progress 'progress': self.in_progress
}) }
def allMovies(self): def allMovies(self):
@@ -146,9 +145,10 @@ class Searcher(Plugin):
pre_releases = fireEvent('quality.pre_releases', single = True) pre_releases = fireEvent('quality.pre_releases', single = True)
release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True) release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True)
available_status, ignored_status = fireEvent('status.get', ['available', 'ignored'], single = True) available_status, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], single = True)
found_releases = [] found_releases = []
too_early_to_search = []
default_title = getTitle(movie['library']) default_title = getTitle(movie['library'])
if not default_title: if not default_title:
@@ -162,14 +162,14 @@ class Searcher(Plugin):
ret = False ret = False
for quality_type in movie['profile']['types']: for quality_type in movie['profile']['types']:
if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates): if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates):
log.info('Too early to search for %s, %s', (quality_type['quality']['identifier'], default_title)) too_early_to_search.append(quality_type['quality']['identifier'])
continue continue
has_better_quality = 0 has_better_quality = 0
# See if better quality is available # See if better quality is available
for release in movie['releases']: for release in movie['releases']:
if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id')]: if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id'), failed_status.get('id')]:
has_better_quality += 1 has_better_quality += 1
# Don't search for quality lower then already available. # Don't search for quality lower then already available.
@@ -240,7 +240,7 @@ class Searcher(Plugin):
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name'])) log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
continue continue
if nzb['status_id'] == ignored_status.get('id'): if nzb['status_id'] in [ignored_status.get('id'), failed_status.get('id')]:
log.info('Ignored: %s', nzb['name']) log.info('Ignored: %s', nzb['name'])
continue continue
@@ -269,6 +269,9 @@ class Searcher(Plugin):
if self.shuttingDown() or ret: if self.shuttingDown() or ret:
break break
if len(too_early_to_search) > 0:
log.info2('Too early to search for %s, %s', (too_early_to_search, default_title))
fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True) fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
return ret return ret
@@ -586,18 +589,17 @@ class Searcher(Plugin):
return False return False
def tryNextReleaseView(self): def tryNextReleaseView(self, id = None, **kwargs):
trynext = self.tryNextRelease(getParam('id')) trynext = self.tryNextRelease(id)
return jsonified({ return {
'success': trynext 'success': trynext
}) }
def tryNextRelease(self, movie_id, manual = False): def tryNextRelease(self, movie_id, manual = False):
snatched_status = fireEvent('status.get', 'snatched', single = True) snatched_status, ignored_status = fireEvent('status.get', ['snatched', 'ignored'], single = True)
ignored_status = fireEvent('status.get', 'ignored', single = True)
try: try:
db = get_session() db = get_session()
+2 -3
View File
@@ -2,7 +2,6 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Status from couchpotato.core.settings.model import Status
@@ -44,10 +43,10 @@ class StatusPlugin(Plugin):
def list(self): def list(self):
return jsonified({ return {
'success': True, 'success': True,
'list': self.all() 'list': self.all()
}) }
def getById(self, id): def getById(self, id):
db = get_session() db = get_session()
+3 -5
View File
@@ -1,6 +1,5 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
class Suggestion(Plugin): class Suggestion(Plugin):
@@ -9,14 +8,13 @@ class Suggestion(Plugin):
addApiView('suggestion.view', self.getView) addApiView('suggestion.view', self.getView)
def getView(self): def getView(self, limit_offset = None, **kwargs):
limit_offset = getParam('limit_offset', None)
total_movies, movies = fireEvent('movie.list', status = 'suggest', limit_offset = limit_offset, single = True) total_movies, movies = fireEvent('movie.list', status = 'suggest', limit_offset = limit_offset, single = True)
return jsonified({ return {
'success': True, 'success': True,
'empty': len(movies) == 0, 'empty': len(movies) == 0,
'total': total_movies, 'total': total_movies,
'movies': movies, 'movies': movies,
}) }
@@ -1,5 +1,7 @@
var includes = {{includes|tojson}}; {% autoescape None %}
var excludes = {{excludes|tojson}};
var includes = {{ json_encode(includes) }};
var excludes = {{ json_encode(excludes) }};
var specialChars = '\\{}+.():-|^$'; var specialChars = '\\{}+.():-|^$';
var makeRegex = function(pattern) { var makeRegex = function(pattern) {
@@ -20,6 +22,8 @@ var makeRegex = function(pattern) {
var isCorrectUrl = function() { var isCorrectUrl = function() {
for(i in includes) { for(i in includes) {
if(!includes.hasOwnProperty(i)) continue;
var reg = includes[i] var reg = includes[i]
if (makeRegex(reg).test(document.location.href)) if (makeRegex(reg).test(document.location.href))
return true; return true;
+34 -27
View File
@@ -1,13 +1,11 @@
from couchpotato import index
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.request import getParam, jsonified
from couchpotato.core.helpers.variable import isDict from couchpotato.core.helpers.variable import isDict
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
from flask.globals import request from tornado.web import RequestHandler
from flask.helpers import url_for
from flask.templating import render_template
import os import os
log = CPLog(__name__) log = CPLog(__name__)
@@ -18,7 +16,8 @@ class Userscript(Plugin):
version = 3 version = 3
def __init__(self): def __init__(self):
addApiView('userscript.get/<random>/<path:filename>', self.getUserScript, static = True) addApiView('userscript.get/(.*)/(.*)', self.getUserScript, static = True)
addApiView('userscript', self.iFrame) addApiView('userscript', self.iFrame)
addApiView('userscript.add_via_url', self.getViaUrl) addApiView('userscript.add_via_url', self.getViaUrl)
addApiView('userscript.includes', self.getIncludes) addApiView('userscript.includes', self.getIncludes)
@@ -26,38 +25,46 @@ class Userscript(Plugin):
addEvent('userscript.get_version', self.getVersion) addEvent('userscript.get_version', self.getVersion)
def bookmark(self): def bookmark(self, host = None, **kwargs):
params = { params = {
'includes': fireEvent('userscript.get_includes', merge = True), 'includes': fireEvent('userscript.get_includes', merge = True),
'excludes': fireEvent('userscript.get_excludes', merge = True), 'excludes': fireEvent('userscript.get_excludes', merge = True),
'host': getParam('host', None), 'host': host,
} }
return self.renderTemplate(__file__, 'bookmark.js', **params) return self.renderTemplate(__file__, 'bookmark.js', **params)
def getIncludes(self): def getIncludes(self):
return jsonified({ return {
'includes': fireEvent('userscript.get_includes', merge = True), 'includes': fireEvent('userscript.get_includes', merge = True),
'excludes': fireEvent('userscript.get_excludes', merge = True), 'excludes': fireEvent('userscript.get_excludes', merge = True),
})
def getUserScript(self, random = '', filename = ''):
params = {
'includes': fireEvent('userscript.get_includes', merge = True),
'excludes': fireEvent('userscript.get_excludes', merge = True),
'version': self.getVersion(),
'api': '%suserscript/' % url_for('api.index').lstrip('/'),
'host': request.host_url,
} }
script = self.renderTemplate(__file__, 'template.js', **params) def getUserScript(self, route):
self.createFile(os.path.join(Env.get('cache_dir'), 'couchpotato.user.js'), script)
klass = self
class UserscriptHandler(RequestHandler):
def get(self, random, route):
params = {
'includes': fireEvent('userscript.get_includes', merge = True),
'excludes': fireEvent('userscript.get_excludes', merge = True),
'version': klass.getVersion(),
'api': '%suserscript/' % Env.get('api_base'),
'host': '%s://%s' % (self.request.protocol, self.request.host),
}
script = klass.renderTemplate(__file__, 'template.js', **params)
klass.createFile(os.path.join(Env.get('cache_dir'), 'couchpotato.user.js'), script)
self.redirect(Env.get('api_base') + 'file.cache/couchpotato.user.js')
Env.get('app').add_handlers(".*$", [('%s%s' % (Env.get('api_base'), route), UserscriptHandler)])
from flask.helpers import send_from_directory
return send_from_directory(Env.get('cache_dir'), 'couchpotato.user.js')
def getVersion(self): def getVersion(self):
@@ -69,12 +76,12 @@ class Userscript(Plugin):
return version return version
def iFrame(self): def iFrame(self, **kwargs):
return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env) return index()
def getViaUrl(self): def getViaUrl(self, url = None, **kwargs):
url = getParam('url') print url
params = { params = {
'url': url, 'url': url,
@@ -84,4 +91,4 @@ class Userscript(Plugin):
log.error('Failed adding movie via url: %s', url) log.error('Failed adding movie via url: %s', url)
params['error'] = params['movie'] if params['movie'] else 'Failed getting movie info' params['error'] = params['movie'] if params['movie'] else 'Failed getting movie info'
return jsonified(params) return params
@@ -9,15 +9,16 @@
// @grant none // @grant none
// @version {{version}} // @version {{version}}
// @match {{host}}* // @match {{host}}/*
{% for include in includes %} {% for include in includes %}
// @match {{include}}{% endfor %} // @match {{include}}{% end %}
{% for exclude in excludes %} {% for exclude in excludes %}
// @exclude {{exclude}}{% endfor %} // @exclude {{exclude}}{% end %}
// @exclude {{host}}{{api.rstrip('/')}}* // @exclude {{host}}{{api.rstrip('/')}}*
// ==/UserScript== // ==/UserScript==
{% autoescape None %}
if (window.top == window.self){ // Only run on top window if (window.top == window.self){ // Only run on top window
var version = {{version}}, var version = {{version}},
@@ -1,6 +0,0 @@
from .main import V1Importer
def start():
return V1Importer()
config = []
@@ -1,30 +0,0 @@
<html>
<head>
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/main.css') }}" type="text/css">
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/uniform.generic.css') }}" type="text/css">
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/uniform.css') }}" type="text/css">
<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/library/mootools.js') }}"></script>
<script type="text/javascript">
window.addEvent('domready', function(){
if($('old_db'))
$('old_db').addEvent('change', function(){
$('form').submit();
});
});
</script>
</head>
<body>
{% if message: %}
{{ message }}
{% else: %}
<form id="form" method="post" enctype="multipart/form-data">
<input type="file" name="old_db" id="old_db" />
</form>
{% endif %}
</body>
</html>
@@ -1,56 +0,0 @@
from couchpotato.api import addApiView
from couchpotato.core.event import fireEventAsync
from couchpotato.core.helpers.variable import getImdb
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
from flask.globals import request
from flask.helpers import url_for
import os
log = CPLog(__name__)
class V1Importer(Plugin):
def __init__(self):
addApiView('v1.import', self.fromOld, methods = ['GET', 'POST'])
def fromOld(self):
if request.method != 'POST':
return self.renderTemplate(__file__, 'form.html', url_for = url_for)
file = request.files['old_db']
uploaded_file = os.path.join(Env.get('cache_dir'), 'v1_database.db')
if os.path.isfile(uploaded_file):
os.remove(uploaded_file)
file.save(uploaded_file)
try:
import sqlite3
conn = sqlite3.connect(uploaded_file)
wanted = []
t = ('want',)
cur = conn.execute('SELECT status, imdb FROM Movie WHERE status=?', t)
for row in cur:
status, imdb = row
if getImdb(imdb):
wanted.append(imdb)
conn.close()
wanted = set(wanted)
for imdb in wanted:
fireEventAsync('movie.add', {'identifier': imdb}, search_after = False)
message = 'Successfully imported %s movie(s)' % len(wanted)
except Exception, e:
message = 'Failed: %s' % e
return self.renderTemplate(__file__, 'form.html', url_for = url_for, message = message)
@@ -51,6 +51,7 @@ class Automation(Provider):
def isMinimalMovie(self, movie): def isMinimalMovie(self, movie):
if not movie.get('rating'): if not movie.get('rating'):
log.info('ignoring %s as no rating is available for.', (movie['original_title']))
return False return False
if movie['rating'] and movie['rating'].get('imdb'): if movie['rating'] and movie['rating'].get('imdb'):
@@ -11,7 +11,7 @@ config = [{
'list': 'watchlist_providers', 'list': 'watchlist_providers',
'name': 'imdb_automation', 'name': 'imdb_automation',
'label': 'IMDB', 'label': 'IMDB',
'description': 'From any <strong>public</strong> IMDB watchlists. Url should be the RSS link.', 'description': 'From any <strong>public</strong> IMDB watchlists. Url should be the CSV link.',
'options': [ 'options': [
{ {
'name': 'automation_enabled', 'name': 'automation_enabled',
+26 -9
View File
@@ -86,6 +86,7 @@ class YarrProvider(Provider):
sizeKb = ['kb', 'kib'] sizeKb = ['kb', 'kib']
login_opener = None login_opener = None
last_login_check = 0
def __init__(self): def __init__(self):
addEvent('provider.enabled_types', self.getEnabledProviderType) addEvent('provider.enabled_types', self.getEnabledProviderType)
@@ -101,17 +102,29 @@ class YarrProvider(Provider):
def login(self): def login(self):
# Check if we are still logged in every hour
now = time.time()
if self.login_opener and self.last_login_check < (now - 3600):
try:
output = self.urlopen(self.urls['login_check'], opener = self.login_opener)
if self.loginCheckSuccess(output):
self.last_login_check = now
return True
else:
self.login_opener = None
except:
self.login_opener = None
if self.login_opener:
return True
try: try:
cookiejar = cookielib.CookieJar() cookiejar = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar)) opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))
opener.addheaders = [('User-Agent', self.user_agent)] output = self.urlopen(self.urls['login'], params = self.getLoginParams(), opener = opener)
urllib2.install_opener(opener)
log.info2('Logging into %s', self.urls['login'])
f = opener.open(self.urls['login'], self.getLoginParams())
output = f.read()
f.close()
if self.loginSuccess(output): if self.loginSuccess(output):
self.last_login_check = now
self.login_opener = opener self.login_opener = opener
return True return True
@@ -119,15 +132,19 @@ class YarrProvider(Provider):
except: except:
error = traceback.format_exc() error = traceback.format_exc()
self.login_opener = None
log.error('Failed to login %s: %s', (self.getName(), error)) log.error('Failed to login %s: %s', (self.getName(), error))
return False return False
def loginSuccess(self, output): def loginSuccess(self, output):
return True return True
def loginCheckSuccess(self, output):
return True
def loginDownload(self, url = '', nzb_id = ''): def loginDownload(self, url = '', nzb_id = ''):
try: try:
if not self.login_opener and not self.login(): if not self.login():
log.error('Failed downloading from %s', self.getName()) log.error('Failed downloading from %s', self.getName())
return self.urlopen(url, opener = self.login_opener) return self.urlopen(url, opener = self.login_opener)
except: except:
@@ -150,7 +167,7 @@ class YarrProvider(Provider):
return [] return []
# Login if needed # Login if needed
if self.urls.get('login') and (not self.login_opener and not self.login()): if self.urls.get('login') and not self.login():
log.error('Failed to login to: %s', self.getName()) log.error('Failed to login to: %s', self.getName())
return [] return []
@@ -258,7 +275,7 @@ class ResultList(list):
'id': 0, 'id': 0,
'type': self.provider.type, 'type': self.provider.type,
'provider': self.provider.getName(), 'provider': self.provider.getName(),
'download': self.provider.download, 'download': self.provider.loginDownload if self.provider.urls.get('login') else self.provider.download,
'url': '', 'url': '',
'name': '', 'name': '',
'age': 0, 'age': 0,
@@ -1,7 +1,6 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.movie.base import MovieProvider from couchpotato.core.providers.movie.base import MovieProvider
from couchpotato.core.settings.model import Movie from couchpotato.core.settings.model import Movie
@@ -50,8 +49,8 @@ class CouchPotatoApi(MovieProvider):
'branch': branch, 'branch': branch,
}), headers = self.getRequestHeaders()) }), headers = self.getRequestHeaders())
def search(self, q, limit = 12): def search(self, q, limit = 5):
return self.getJsonData(self.urls['search'] % tryUrlencode(q), headers = self.getRequestHeaders()) return self.getJsonData(self.urls['search'] % tryUrlencode(q) + ('?limit=%s' % limit), headers = self.getRequestHeaders())
def isMovie(self, identifier = None): def isMovie(self, identifier = None):
@@ -86,17 +85,16 @@ class CouchPotatoApi(MovieProvider):
def suggest(self, movies = [], ignore = []): def suggest(self, movies = [], ignore = []):
suggestions = self.getJsonData(self.urls['suggest'], params = { suggestions = self.getJsonData(self.urls['suggest'], params = {
'movies': ','.join(movies), 'movies': ','.join(movies),
#'ignore': ','.join(ignore), 'ignore': ','.join(ignore),
}) })
log.info('Found Suggestions for %s', (suggestions)) log.info('Found Suggestions for %s', (suggestions))
return suggestions return suggestions
def suggestView(self): def suggestView(self, **kwargs):
params = getParams() movies = kwargs.get('movies')
movies = params.get('movies') ignore = kwargs.get('ignore', [])
ignore = params.get('ignore', [])
if not movies: if not movies:
db = get_session() db = get_session()
@@ -105,11 +103,11 @@ class CouchPotatoApi(MovieProvider):
suggestions = self.suggest(movies, ignore) suggestions = self.suggest(movies, ignore)
return jsonified({ return {
'success': True, 'success': True,
'count': len(suggestions), 'count': len(suggestions),
'suggestions': suggestions 'suggestions': suggestions
}) }
def getRequestHeaders(self): def getRequestHeaders(self):
return { return {
@@ -2,7 +2,7 @@ from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import simplifyString, toUnicode from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.movie.base import MovieProvider from couchpotato.core.providers.movie.base import MovieProvider
from libs.themoviedb import tmdb from themoviedb import tmdb
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -16,6 +16,7 @@ class FTDWorld(NZBProvider):
'detail': 'http://ftdworld.net/spotinfo.php?id=%s', 'detail': 'http://ftdworld.net/spotinfo.php?id=%s',
'download': 'http://ftdworld.net/cgi-bin/nzbdown.pl?fileID=%s', 'download': 'http://ftdworld.net/cgi-bin/nzbdown.pl?fileID=%s',
'login': 'http://ftdworld.net/api/login.php', 'login': 'http://ftdworld.net/api/login.php',
'login_check': 'http://ftdworld.net/api/login.php',
} }
http_time_between_calls = 3 #seconds http_time_between_calls = 3 #seconds
@@ -58,7 +59,6 @@ class FTDWorld(NZBProvider):
'age': self.calculateAge(tryInt(item.get('Created'))), 'age': self.calculateAge(tryInt(item.get('Created'))),
'size': item.get('Size', 0), 'size': item.get('Size', 0),
'url': self.urls['download'] % nzb_id, 'url': self.urls['download'] % nzb_id,
'download': self.loginDownload,
'detail_url': self.urls['detail'] % nzb_id, 'detail_url': self.urls['detail'] % nzb_id,
'score': (tryInt(item.get('webPlus', 0)) - tryInt(item.get('webMin', 0))) * 3, 'score': (tryInt(item.get('webPlus', 0)) - tryInt(item.get('webMin', 0))) * 3,
}) })
@@ -78,3 +78,6 @@ class FTDWorld(NZBProvider):
return json.loads(output).get('goodToGo', False) return json.loads(output).get('goodToGo', False)
except: except:
return False return False
loginCheckSuccess = loginSuccess
+28 -7
View File
@@ -53,11 +53,20 @@ class Newznab(NZBProvider, RSS):
for nzb in nzbs: for nzb in nzbs:
date = None date = None
spotter = None
for item in nzb: for item in nzb:
if date and spotter:
break
if item.attrib.get('name') == 'usenetdate': if item.attrib.get('name') == 'usenetdate':
date = item.attrib.get('value') date = item.attrib.get('value')
break break
# Get the name of the person who posts the spot
if item.attrib.get('name') == 'poster':
if "@spot.net" in item.attrib.get('value'):
spotter = item.attrib.get('value').split("@")[0]
continue
if not date: if not date:
date = self.getTextElement(nzb, 'pubDate') date = self.getTextElement(nzb, 'pubDate')
@@ -67,10 +76,15 @@ class Newznab(NZBProvider, RSS):
if not name: if not name:
continue continue
name_extra = ''
if spotter:
name_extra = spotter
results.append({ results.append({
'id': nzb_id, 'id': nzb_id,
'provider_extra': urlparse(host['host']).hostname or host['host'], 'provider_extra': urlparse(host['host']).hostname or host['host'],
'name': self.getTextElement(nzb, 'title'), 'name': name,
'name_extra': name_extra,
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024, 'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
'url': (self.getUrl(host['host'], self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host), 'url': (self.getUrl(host['host'], self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host),
@@ -81,17 +95,24 @@ class Newznab(NZBProvider, RSS):
def getHosts(self): def getHosts(self):
uses = splitString(str(self.conf('use'))) uses = splitString(str(self.conf('use')), clean = False)
hosts = splitString(self.conf('host')) hosts = splitString(self.conf('host'), clean = False)
api_keys = splitString(self.conf('api_key')) api_keys = splitString(self.conf('api_key'), clean = False)
extra_score = splitString(self.conf('extra_score')) extra_score = splitString(self.conf('extra_score'), clean = False)
list = [] list = []
for nr in range(len(hosts)): for nr in range(len(hosts)):
try: key = api_keys[nr]
except: key = ''
try: host = hosts[nr]
except: host = ''
list.append({ list.append({
'use': uses[nr], 'use': uses[nr],
'host': hosts[nr], 'host': host,
'api_key': api_keys[nr], 'api_key': key,
'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0 'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0
}) })
@@ -0,0 +1,59 @@
from .main import AwesomeHD
def start():
return AwesomeHD()
config = [{
'name': 'awesomehd',
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'list': 'torrent_providers',
'name': 'Awesome-HD',
'description': 'See <a href="https://awesome-hd.net">AHD</a>',
'wizard': True,
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'passkey',
'default': '',
},
{
'name': 'only_internal',
'advanced': True,
'type': 'bool',
'default': 1,
'description': 'Only search for internal releases.'
},
{
'name': 'prefer_internal',
'advanced': True,
'type': 'bool',
'default': 1,
'description': 'Favors internal releases over non-internal releases.'
},
{
'name': 'favor',
'advanced': True,
'default': 'both',
'type': 'dropdown',
'values': [('Encodes & Remuxes', 'both'), ('Encodes', 'encode'), ('Remuxes', 'remux'), ('None', 'none')],
'description': 'Give extra scoring to encodes or remuxes.'
},
{
'name': 'extra_score',
'advanced': True,
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
},
],
},
],
}]
@@ -0,0 +1,64 @@
from bs4 import BeautifulSoup
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider
import re
import traceback
log = CPLog(__name__)
class AwesomeHD(TorrentProvider):
urls = {
'test' : 'https://awesome-hd.net/',
'detail' : 'https://awesome-hd.net/torrents.php?torrentid=%s',
'search' : 'https://awesome-hd.net/searchapi.php?action=imdbsearch&passkey=%s&imdb=%s&internal=%s',
'download' : 'https://awesome-hd.net/torrents.php?action=download&id=%s&authkey=%s&torrent_pass=%s',
}
http_time_between_calls = 1
def _search(self, movie, quality, results):
data = self.getHTMLData(self.urls['search'] % (self.conf('passkey'), movie['library']['identifier'], self.conf('only_internal')))
if data:
try:
soup = BeautifulSoup(data)
authkey = soup.find('authkey').get_text()
entries = soup.find_all('torrent')
for entry in entries:
torrentscore = 0
torrent_id = entry.find('id').get_text()
name = entry.find('name').get_text()
year = entry.find('year').get_text()
releasegroup = entry.find('releasegroup').get_text()
resolution = entry.find('resolution').get_text()
encoding = entry.find('encoding').get_text()
freeleech = entry.find('freeleech').get_text()
torrent_desc = '/ %s / %s / %s ' % (releasegroup, resolution, encoding)
if freeleech == '0.25' and self.conf('prefer_internal'):
torrent_desc += '/ Internal'
torrentscore += 200
if encoding == 'x264' and self.conf('favor') in ['encode', 'both']:
torrentscore += 300
if re.search('Remux', encoding) and self.conf('favor') in ['remux', 'both']:
torrentscore += 200
results.append({
'id': torrent_id,
'name': re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) %s' % (name, year, torrent_desc)),
'url': self.urls['download'] % (torrent_id, authkey, self.conf('passkey')),
'detail_url': self.urls['detail'] % torrent_id,
'size': self.parseSize(entry.find('size').get_text()),
'seeders': tryInt(entry.find('seeders').get_text()),
'leechers': tryInt(entry.find('leechers').get_text()),
'score': torrentscore
})
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
@@ -16,6 +16,7 @@ class HDBits(TorrentProvider):
'detail' : 'https://hdbits.org/details.php?id=%s&source=browse', 'detail' : 'https://hdbits.org/details.php?id=%s&source=browse',
'search' : 'https://hdbits.org/json_search.php?imdb=%s', 'search' : 'https://hdbits.org/json_search.php?imdb=%s',
'download' : 'https://hdbits.org/download.php/%s.torrent?id=%s&passkey=%s&source=details.browse', 'download' : 'https://hdbits.org/download.php/%s.torrent?id=%s&passkey=%s&source=details.browse',
'login_check': 'http://hdbits.org/inbox.php',
} }
http_time_between_calls = 1 #seconds http_time_between_calls = 1 #seconds
@@ -30,7 +31,7 @@ class HDBits(TorrentProvider):
results.append({ results.append({
'id': result['id'], 'id': result['id'],
'name': result['title'], 'name': result['title'],
'url': self.urls['download'] % (result['title'], result['id'], self.conf('passkey')), 'url': self.urls['download'] % (result['id'], result['id'], self.conf('passkey')),
'detail_url': self.urls['detail'] % result['id'], 'detail_url': self.urls['detail'] % result['id'],
'size': self.parseSize(result['size']), 'size': self.parseSize(result['size']),
'seeders': tryInt(result['seeder']), 'seeders': tryInt(result['seeder']),
@@ -53,3 +54,5 @@ class HDBits(TorrentProvider):
def loginSuccess(self, output): def loginSuccess(self, output):
return '/logout.php' in output.lower() return '/logout.php' in output.lower()
loginCheckSuccess = loginSuccess
@@ -5,7 +5,6 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider from couchpotato.core.providers.torrent.base import TorrentProvider
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -15,7 +14,8 @@ class IPTorrents(TorrentProvider):
'test' : 'http://www.iptorrents.com/', 'test' : 'http://www.iptorrents.com/',
'base_url' : 'http://www.iptorrents.com', 'base_url' : 'http://www.iptorrents.com',
'login' : 'http://www.iptorrents.com/torrents/', 'login' : 'http://www.iptorrents.com/torrents/',
'search' : 'http://www.iptorrents.com/torrents/?l%d=1%s&q=%s&qf=ti', 'login_check': 'http://www.iptorrents.com/inbox.php',
'search' : 'http://www.iptorrents.com/torrents/?l%d=1%s&q=%s&qf=ti&p=%d',
} }
cat_ids = [ cat_ids = [
@@ -32,48 +32,62 @@ class IPTorrents(TorrentProvider):
freeleech = '' if not self.conf('freeleech') else '&free=on' freeleech = '' if not self.conf('freeleech') else '&free=on'
url = self.urls['search'] % (self.getCatId(quality['identifier'])[0], freeleech, tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year']))) pages = 1
data = self.getHTMLData(url, opener = self.login_opener) current_page = 1
while current_page <= pages and not self.shuttingDown():
if data: url = self.urls['search'] % (self.getCatId(quality['identifier'])[0], freeleech, tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])), current_page)
html = BeautifulSoup(data) data = self.getHTMLData(url, opener = self.login_opener)
try: if data:
result_table = html.find('table', attrs = {'class' : 'torrents'}) html = BeautifulSoup(data)
if not result_table or 'nothing found!' in data.lower(): try:
return page_nav = html.find('span', attrs = {'class' : 'page_nav'})
if page_nav:
next_link = page_nav.find("a", text = "Next")
if next_link:
final_page_link = next_link.previous_sibling.previous_sibling
pages = int(final_page_link.string)
entries = result_table.find_all('tr') result_table = html.find('table', attrs = {'class' : 'torrents'})
for result in entries[1:]: if not result_table or 'nothing found!' in data.lower():
return
torrent = result.find_all('td')[1].find('a') entries = result_table.find_all('tr')
torrent_id = torrent['href'].replace('/details.php?id=', '') for result in entries[1:]:
torrent_name = torrent.string
torrent_download_url = self.urls['base_url'] + (result.find_all('td')[3].find('a'))['href'].replace(' ', '.')
torrent_details_url = self.urls['base_url'] + torrent['href']
torrent_size = self.parseSize(result.find_all('td')[5].string)
torrent_seeders = tryInt(result.find('td', attrs = {'class' : 'ac t_seeders'}).string)
torrent_leechers = tryInt(result.find('td', attrs = {'class' : 'ac t_leechers'}).string)
results.append({ torrent = result.find_all('td')
'id': torrent_id, if len(torrent) <= 1:
'name': torrent_name, break
'url': torrent_download_url,
'detail_url': torrent_details_url,
'download': self.loginDownload,
'size': torrent_size,
'seeders': torrent_seeders,
'leechers': torrent_leechers,
})
except: torrent = torrent[1].find('a')
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
def loginSuccess(self, output): torrent_id = torrent['href'].replace('/details.php?id=', '')
return 'don\'t have an account' not in output.lower() torrent_name = torrent.string
torrent_download_url = self.urls['base_url'] + (result.find_all('td')[3].find('a'))['href'].replace(' ', '.')
torrent_details_url = self.urls['base_url'] + torrent['href']
torrent_size = self.parseSize(result.find_all('td')[5].string)
torrent_seeders = tryInt(result.find('td', attrs = {'class' : 'ac t_seeders'}).string)
torrent_leechers = tryInt(result.find('td', attrs = {'class' : 'ac t_leechers'}).string)
results.append({
'id': torrent_id,
'name': torrent_name,
'url': torrent_download_url,
'detail_url': torrent_details_url,
'size': torrent_size,
'seeders': torrent_seeders,
'leechers': torrent_leechers,
})
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
break
current_page += 1
def getLoginParams(self): def getLoginParams(self):
return tryUrlencode({ return tryUrlencode({
@@ -81,3 +95,9 @@ class IPTorrents(TorrentProvider):
'password': self.conf('password'), 'password': self.conf('password'),
'login': 'submit', 'login': 'submit',
}) })
def loginSuccess(self, output):
return 'don\'t have an account' not in output.lower()
def loginCheckSuccess(self, output):
return '/logout.php' in output.lower()
@@ -11,9 +11,9 @@ log = CPLog(__name__)
class KickAssTorrents(TorrentMagnetProvider): class KickAssTorrents(TorrentMagnetProvider):
urls = { urls = {
'test': 'https://kat.ph/', 'test': 'https://kickass.to/',
'detail': 'https://kat.ph/%s', 'detail': 'https://kickass.to/%s',
'search': 'https://kat.ph/%s-i%s/', 'search': 'https://kickass.to/%s-i%s/',
} }
cat_ids = [ cat_ids = [
@@ -3,13 +3,11 @@ from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider from couchpotato.core.providers.torrent.base import TorrentProvider
from dateutil.parser import parse from dateutil.parser import parse
import cookielib
import htmlentitydefs import htmlentitydefs
import json import json
import re import re
import time import time
import traceback import traceback
import urllib2
log = CPLog(__name__) log = CPLog(__name__)
@@ -21,9 +19,12 @@ class PassThePopcorn(TorrentProvider):
'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s', 'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s',
'torrent': 'https://tls.passthepopcorn.me/torrents.php', 'torrent': 'https://tls.passthepopcorn.me/torrents.php',
'login': 'https://tls.passthepopcorn.me/ajax.php?action=login', 'login': 'https://tls.passthepopcorn.me/ajax.php?action=login',
'login_check': 'https://tls.passthepopcorn.me/ajax.php?action=login',
'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d' 'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d'
} }
http_time_between_calls = 2
quality_search_params = { quality_search_params = {
'bd50': {'media': 'Blu-ray', 'format': 'BD50'}, 'bd50': {'media': 'Blu-ray', 'format': 'BD50'},
'1080p': {'resolution': '1080p'}, '1080p': {'resolution': '1080p'},
@@ -52,18 +53,6 @@ class PassThePopcorn(TorrentProvider):
'cam': {'Source': ['CAM']} 'cam': {'Source': ['CAM']}
} }
class NotLoggedInHTTPError(urllib2.HTTPError):
def __init__(self, url, code, msg, headers, fp):
urllib2.HTTPError.__init__(self, url, code, msg, headers, fp)
class PTPHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
def http_error_302(self, req, fp, code, msg, headers):
log.debug("302 detected; redirected to %s", headers['Location'])
if (headers['Location'] != 'login.php'):
return urllib2.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
else:
raise PassThePopcorn.NotLoggedInHTTPError(req.get_full_url(), code, msg, headers, fp)
def _search(self, movie, quality, results): def _search(self, movie, quality, results):
movie_title = getTitle(movie['library']) movie_title = getTitle(movie['library'])
@@ -75,17 +64,8 @@ class PassThePopcorn(TorrentProvider):
'searchstr': movie['library']['identifier'] 'searchstr': movie['library']['identifier']
}) })
# Do login for the cookies url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params))
if not self.login_opener and not self.login(): res = self.getJsonData(url, opener = self.login_opener)
return
try:
url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params))
txt = self.urlopen(url, opener = self.login_opener)
res = json.loads(txt)
except:
log.error('Search on PassThePopcorn.me (%s) failed (could not decode JSON)', params)
return
try: try:
if not 'Movies' in res: if not 'Movies' in res:
@@ -136,40 +116,11 @@ class PassThePopcorn(TorrentProvider):
'leechers': tryInt(torrent['Leechers']), 'leechers': tryInt(torrent['Leechers']),
'score': torrentscore, 'score': torrentscore,
'extra_check': extra_check, 'extra_check': extra_check,
'download': self.loginDownload,
}) })
except: except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc())) log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
def login(self):
cookieprocessor = urllib2.HTTPCookieProcessor(cookielib.CookieJar())
opener = urllib2.build_opener(cookieprocessor, PassThePopcorn.PTPHTTPRedirectHandler())
opener.addheaders = [
('User-Agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.75 Safari/537.1'),
('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
('Accept-Language', 'en-gb,en;q=0.5'),
('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7'),
('Keep-Alive', '115'),
('Connection', 'keep-alive'),
('Cache-Control', 'max-age=0'),
]
try:
response = opener.open(self.urls['login'], self.getLoginParams())
except urllib2.URLError as e:
log.error('Login to PassThePopcorn failed: %s', e)
return False
if response.getcode() == 200:
log.debug('Login HTTP status 200; seems successful')
self.login_opener = opener
return True
else:
log.error('Login to PassThePopcorn failed: returned code %d', response.getcode())
return False
def torrentMeetsQualitySpec(self, torrent, quality): def torrentMeetsQualitySpec(self, torrent, quality):
if not quality in self.post_search_filters: if not quality in self.post_search_filters:
@@ -186,7 +137,7 @@ class PassThePopcorn(TorrentProvider):
seen_one = False seen_one = False
if not field in torrent: if not field in torrent:
log.debug('Torrent with ID %s has no field "%s"; cannot apply post-search-filter for quality "%s"', (torrent['Id'], field, quality)) log.debug('Torrent with ID %s has no field "%s"; cannot apply post-search-filter for quality "%s"', (torrent['id'], field, quality))
continue continue
for spec in specs: for spec in specs:
@@ -244,3 +195,11 @@ class PassThePopcorn(TorrentProvider):
'keeplogged': '1', 'keeplogged': '1',
'login': 'Login' 'login': 'Login'
}) })
def loginSuccess(self, output):
try:
return json.loads(output).get('Result', '').lower() == 'ok'
except:
return False
loginCheckSuccess = loginSuccess
@@ -12,7 +12,8 @@ class SceneAccess(TorrentProvider):
urls = { urls = {
'test': 'https://www.sceneaccess.eu/', 'test': 'https://www.sceneaccess.eu/',
'login' : 'https://www.sceneaccess.eu/login', 'login': 'https://www.sceneaccess.eu/login',
'login_check': 'https://www.sceneaccess.eu/inbox',
'detail': 'https://www.sceneaccess.eu/details?id=%s', 'detail': 'https://www.sceneaccess.eu/details?id=%s',
'search': 'https://www.sceneaccess.eu/browse?method=2&c%d=%d', 'search': 'https://www.sceneaccess.eu/browse?method=2&c%d=%d',
'download': 'https://www.sceneaccess.eu/%s', 'download': 'https://www.sceneaccess.eu/%s',
@@ -39,9 +40,6 @@ class SceneAccess(TorrentProvider):
}) })
url = "%s&%s" % (url, arguments) url = "%s&%s" % (url, arguments)
# Do login for the cookies
if not self.login_opener and not self.login():
return
data = self.getHTMLData(url, opener = self.login_opener) data = self.getHTMLData(url, opener = self.login_opener)
@@ -69,7 +67,6 @@ class SceneAccess(TorrentProvider):
'size': self.parseSize(result.find('td', attrs = {'class' : 'ttr_size'}).contents[0]), 'size': self.parseSize(result.find('td', attrs = {'class' : 'ttr_size'}).contents[0]),
'seeders': tryInt(result.find('td', attrs = {'class' : 'ttr_seeders'}).find('a').string), 'seeders': tryInt(result.find('td', attrs = {'class' : 'ttr_seeders'}).find('a').string),
'leechers': tryInt(leechers.string) if leechers else 0, 'leechers': tryInt(leechers.string) if leechers else 0,
'download': self.loginDownload,
'get_more_info': self.getMoreInfo, 'get_more_info': self.getMoreInfo,
}) })
@@ -91,3 +88,8 @@ class SceneAccess(TorrentProvider):
item['description'] = description item['description'] = description
return item return item
def loginSuccess(self, output):
return '/inbox' in output.lower()
loginCheckSuccess = loginSuccess
@@ -13,6 +13,7 @@ class SceneHD(TorrentProvider):
urls = { urls = {
'test': 'https://scenehd.org/', 'test': 'https://scenehd.org/',
'login' : 'https://scenehd.org/takelogin.php', 'login' : 'https://scenehd.org/takelogin.php',
'login_check': 'https://scenehd.org/my.php',
'detail': 'https://scenehd.org/details.php?id=%s', 'detail': 'https://scenehd.org/details.php?id=%s',
'search': 'https://scenehd.org/browse.php?ajax', 'search': 'https://scenehd.org/browse.php?ajax',
'download': 'https://scenehd.org/download.php?id=%s', 'download': 'https://scenehd.org/download.php?id=%s',
@@ -28,10 +29,6 @@ class SceneHD(TorrentProvider):
}) })
url = "%s&%s" % (self.urls['search'], arguments) url = "%s&%s" % (self.urls['search'], arguments)
# Cookie login
if not self.login_opener and not self.login():
return
data = self.getHTMLData(url, opener = self.login_opener) data = self.getHTMLData(url, opener = self.login_opener)
if data: if data:
@@ -61,7 +58,6 @@ class SceneHD(TorrentProvider):
'seeders': tryInt(all_cells[10].find('a').string), 'seeders': tryInt(all_cells[10].find('a').string),
'leechers': tryInt(leechers), 'leechers': tryInt(leechers),
'url': self.urls['download'] % torrent_id, 'url': self.urls['download'] % torrent_id,
'download': self.loginDownload,
'description': all_cells[1].find('a')['href'], 'description': all_cells[1].find('a')['href'],
}) })
@@ -75,3 +71,9 @@ class SceneHD(TorrentProvider):
'password': self.conf('password'), 'password': self.conf('password'),
'ssl': 'yes', 'ssl': 'yes',
}) })
def loginSuccess(self, output):
return 'logout.php' in output.lower()
loginCheckSuccess = loginSuccess
@@ -0,0 +1,42 @@
from .main import TorrentBytes
def start():
return TorrentBytes()
config = [{
'name': 'torrentbytes',
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'list': 'torrent_providers',
'name': 'TorrentBytes',
'description': 'See <a href="http://torrentbytes.net">TorrentBytes</a>',
'wizard': True,
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]
@@ -0,0 +1,82 @@
from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider
import traceback
log = CPLog(__name__)
class TorrentBytes(TorrentProvider):
urls = {
'test' : 'https://www.torrentbytes.net/',
'login' : 'https://www.torrentbytes.net/takelogin.php',
'login_check' : 'https://www.torrentbytes.net/inbox.php',
'detail' : 'https://www.torrentbytes.net/details.php?id=%s',
'search' : 'https://www.torrentbytes.net/browse.php?search=%s&cat=%d',
'download' : 'https://www.torrentbytes.net/download.php?id=%s&name=%s',
}
cat_ids = [
([5], ['720p', '1080p']),
([19], ['cam']),
([19], ['ts', 'tc']),
([19], ['r5', 'scr']),
([19], ['dvdrip']),
([5], ['brrip']),
([20], ['dvdr']),
]
http_time_between_calls = 1 #seconds
cat_backup_id = None
def _searchOnTitle(self, title, movie, quality, results):
url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])), self.getCatId(quality['identifier'])[0])
data = self.getHTMLData(url, opener = self.login_opener)
if data:
html = BeautifulSoup(data)
try:
result_table = html.find('table', attrs = {'border' : '1'})
if not result_table:
return
entries = result_table.find_all('tr')
for result in entries[1:]:
cells = result.find_all('td')
link = cells[1].find('a', attrs = {'class' : 'index'})
full_id = link['href'].replace('details.php?id=', '')
torrent_id = full_id[:6]
results.append({
'id': torrent_id,
'name': link.contents[0],
'url': self.urls['download'] % (torrent_id, link.contents[0]),
'detail_url': self.urls['detail'] % torrent_id,
'size': self.parseSize(cells[6].contents[0] + cells[6].contents[2]),
'seeders': tryInt(cells[8].find('span').contents[0]),
'leechers': tryInt(cells[9].find('span').contents[0]),
})
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self):
return tryUrlencode({
'username': self.conf('username'),
'password': self.conf('password'),
'login': 'submit',
})
def loginSuccess(self, output):
return 'logout.php' in output.lower() or 'Welcome' in output.lower()
loginCheckSuccess = loginSuccess
@@ -10,7 +10,8 @@ class TorrentDay(TorrentProvider):
urls = { urls = {
'test': 'http://www.td.af/', 'test': 'http://www.td.af/',
'login' : 'http://www.td.af/torrents/', 'login': 'http://www.td.af/torrents/',
'login_check': 'http://www.torrentday.com/userdetails.php',
'detail': 'http://www.td.af/details.php?id=%s', 'detail': 'http://www.td.af/details.php?id=%s',
'search': 'http://www.td.af/V3/API/API.php', 'search': 'http://www.td.af/V3/API/API.php',
'download': 'http://www.td.af/download.php/%s/%s', 'download': 'http://www.td.af/download.php/%s/%s',
@@ -50,7 +51,6 @@ class TorrentDay(TorrentProvider):
'size': self.parseSize(torrent.get('size')), 'size': self.parseSize(torrent.get('size')),
'seeders': tryInt(torrent.get('seed')), 'seeders': tryInt(torrent.get('seed')),
'leechers': tryInt(torrent.get('leech')), 'leechers': tryInt(torrent.get('leech')),
'download': self.loginDownload,
}) })
def getLoginParams(self): def getLoginParams(self):
@@ -62,3 +62,6 @@ class TorrentDay(TorrentProvider):
def loginSuccess(self, output): def loginSuccess(self, output):
return 'Password not correct' not in output return 'Password not correct' not in output
def loginCheckSuccess(self, output):
return 'logout.php' in output.lower()
@@ -14,6 +14,7 @@ class TorrentLeech(TorrentProvider):
urls = { urls = {
'test' : 'http://www.torrentleech.org/', 'test' : 'http://www.torrentleech.org/',
'login' : 'http://www.torrentleech.org/user/account/login/', 'login' : 'http://www.torrentleech.org/user/account/login/',
'login_check': 'http://torrentleech.org/user/messages',
'detail' : 'http://www.torrentleech.org/torrent/%s', 'detail' : 'http://www.torrentleech.org/torrent/%s',
'search' : 'http://www.torrentleech.org/torrents/browse/index/query/%s/categories/%d', 'search' : 'http://www.torrentleech.org/torrents/browse/index/query/%s/categories/%d',
'download' : 'http://www.torrentleech.org%s', 'download' : 'http://www.torrentleech.org%s',
@@ -58,7 +59,6 @@ class TorrentLeech(TorrentProvider):
'name': link.string, 'name': link.string,
'url': self.urls['download'] % url['href'], 'url': self.urls['download'] % url['href'],
'detail_url': self.urls['download'] % details['href'], 'detail_url': self.urls['download'] % details['href'],
'download': self.loginDownload,
'size': self.parseSize(result.find_all('td')[4].string), 'size': self.parseSize(result.find_all('td')[4].string),
'seeders': tryInt(result.find('td', attrs = {'class' : 'seeders'}).string), 'seeders': tryInt(result.find('td', attrs = {'class' : 'seeders'}).string),
'leechers': tryInt(result.find('td', attrs = {'class' : 'leechers'}).string), 'leechers': tryInt(result.find('td', attrs = {'class' : 'leechers'}).string),
@@ -77,3 +77,5 @@ class TorrentLeech(TorrentProvider):
def loginSuccess(self, output): def loginSuccess(self, output):
return '/user/account/logout' in output.lower() or 'welcome back' in output.lower() return '/user/account/logout' in output.lower() or 'welcome back' in output.lower()
loginCheckSuccess = loginSuccess
@@ -13,6 +13,7 @@ class TorrentShack(TorrentProvider):
urls = { urls = {
'test' : 'http://www.torrentshack.net/', 'test' : 'http://www.torrentshack.net/',
'login' : 'http://www.torrentshack.net/login.php', 'login' : 'http://www.torrentshack.net/login.php',
'login_check': 'http://www.torrentshack.net/inbox.php',
'detail' : 'http://www.torrentshack.net/torrent/%s', 'detail' : 'http://www.torrentshack.net/torrent/%s',
'search' : 'http://www.torrentshack.net/torrents.php?searchstr=%s&filter_cat[%d]=1', 'search' : 'http://www.torrentshack.net/torrents.php?searchstr=%s&filter_cat[%d]=1',
'download' : 'http://www.torrentshack.net/%s', 'download' : 'http://www.torrentshack.net/%s',
@@ -58,7 +59,6 @@ class TorrentShack(TorrentProvider):
'name': unicode(link.span.string).translate({ord(u'\xad'): None}), 'name': unicode(link.span.string).translate({ord(u'\xad'): None}),
'url': self.urls['download'] % url['href'], 'url': self.urls['download'] % url['href'],
'detail_url': self.urls['download'] % link['href'], 'detail_url': self.urls['download'] % link['href'],
'download': self.loginDownload,
'size': self.parseSize(result.find_all('td')[4].string), 'size': self.parseSize(result.find_all('td')[4].string),
'seeders': tryInt(result.find_all('td')[6].string), 'seeders': tryInt(result.find_all('td')[6].string),
'leechers': tryInt(result.find_all('td')[7].string), 'leechers': tryInt(result.find_all('td')[7].string),
@@ -79,3 +79,5 @@ class TorrentShack(TorrentProvider):
def loginSuccess(self, output): def loginSuccess(self, output):
return 'logout.php' in output.lower() return 'logout.php' in output.lower()
loginCheckSuccess = loginSuccess
+8 -11
View File
@@ -2,7 +2,6 @@ from __future__ import with_statement
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import isInt, toUnicode from couchpotato.core.helpers.encoding import isInt, toUnicode
from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.helpers.variable import mergeDicts, tryInt from couchpotato.core.helpers.variable import mergeDicts, tryInt
from couchpotato.core.settings.model import Properties from couchpotato.core.settings.model import Properties
import ConfigParser import ConfigParser
@@ -170,18 +169,16 @@ class Settings(object):
def view(self): def view(self):
return jsonified({ return {
'options': self.getOptions(), 'options': self.getOptions(),
'values': self.getValues() 'values': self.getValues()
}) }
def saveView(self): def saveView(self, **kwargs):
params = getParams() section = kwargs.get('section')
option = kwargs.get('name')
section = params.get('section') value = kwargs.get('value')
option = params.get('name')
value = params.get('value')
# See if a value handler is attached, use that as value # See if a value handler is attached, use that as value
new_value = fireEvent('setting.save.%s.%s' % (section, option), value, single = True) new_value = fireEvent('setting.save.%s.%s' % (section, option), value, single = True)
@@ -192,9 +189,9 @@ class Settings(object):
# After save (for re-interval etc) # After save (for re-interval etc)
fireEvent('setting.save.%s.%s.after' % (section, option), single = True) fireEvent('setting.save.%s.%s.after' % (section, option), single = True)
return jsonified({ return {
'success': True, 'success': True,
}) }
def getProperty(self, identifier): def getProperty(self, identifier):
from couchpotato import get_session from couchpotato import get_session
+1
View File
@@ -11,6 +11,7 @@ class Env(object):
_appname = 'CouchPotato' _appname = 'CouchPotato'
''' Environment variables ''' ''' Environment variables '''
_app = None
_encoding = 'UTF-8' _encoding = 'UTF-8'
_debug = False _debug = False
_dev = False _dev = False
+76 -54
View File
@@ -1,19 +1,20 @@
from argparse import ArgumentParser from argparse import ArgumentParser
from couchpotato import web from cache import FileSystemCache
from couchpotato.api import api, NonBlockHandler from couchpotato import KeyHandler
from couchpotato.api import NonBlockHandler, ApiHandler
from couchpotato.core.event import fireEventAsync, fireEvent from couchpotato.core.event import fireEventAsync, fireEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import getDataDir, tryInt from couchpotato.core.helpers.variable import getDataDir, tryInt
from logging import handlers from logging import handlers
from tornado.httpserver import HTTPServer from tornado.httpserver import HTTPServer
from tornado.web import Application, FallbackHandler from tornado.web import Application, StaticFileHandler, RedirectHandler
from tornado.wsgi import WSGIContainer
from werkzeug.contrib.cache import FileSystemCache
import locale import locale
import logging import logging
import os.path import os.path
import shutil import shutil
import sys import sys
import time import time
import traceback
import warnings import warnings
def getOptions(base_path, args): def getOptions(base_path, args):
@@ -75,23 +76,25 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
if not encoding or encoding in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'): if not encoding or encoding in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
encoding = 'UTF-8' encoding = 'UTF-8'
Env.set('encoding', encoding)
# Do db stuff # Do db stuff
db_path = os.path.join(data_dir, 'couchpotato.db') db_path = toUnicode(os.path.join(data_dir, 'couchpotato.db'))
# Backup before start and cleanup old databases # Backup before start and cleanup old databases
new_backup = os.path.join(data_dir, 'db_backup', str(int(time.time()))) new_backup = toUnicode(os.path.join(data_dir, 'db_backup', str(int(time.time()))))
# Create path and copy # Create path and copy
if not os.path.isdir(new_backup): os.makedirs(new_backup) if not os.path.isdir(new_backup): os.makedirs(new_backup)
src_files = [options.config_file, db_path, db_path + '-shm', db_path + '-wal'] src_files = [options.config_file, db_path, db_path + '-shm', db_path + '-wal']
for src_file in src_files: for src_file in src_files:
if os.path.isfile(src_file): if os.path.isfile(src_file):
shutil.copy2(src_file, os.path.join(new_backup, os.path.basename(src_file))) shutil.copy2(src_file, toUnicode(os.path.join(new_backup, os.path.basename(src_file))))
# Remove older backups, keep backups 3 days or at least 3 # Remove older backups, keep backups 3 days or at least 3
backups = [] backups = []
for directory in os.listdir(os.path.dirname(new_backup)): for directory in os.listdir(os.path.dirname(new_backup)):
backup = os.path.join(os.path.dirname(new_backup), directory) backup = toUnicode(os.path.join(os.path.dirname(new_backup), directory))
if os.path.isdir(backup): if os.path.isdir(backup):
backups.append(backup) backups.append(backup)
@@ -100,7 +103,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
if total_backups > 3: if total_backups > 3:
if tryInt(os.path.basename(backup)) < time.time() - 259200: if tryInt(os.path.basename(backup)) < time.time() - 259200:
for src_file in src_files: for src_file in src_files:
b_file = os.path.join(backup, os.path.basename(src_file)) b_file = toUnicode(os.path.join(backup, os.path.basename(src_file)))
if os.path.isfile(b_file): if os.path.isfile(b_file):
os.remove(b_file) os.remove(b_file)
os.rmdir(backup) os.rmdir(backup)
@@ -108,13 +111,12 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
# Register environment settings # Register environment settings
Env.set('encoding', encoding) Env.set('app_dir', toUnicode(base_path))
Env.set('app_dir', base_path) Env.set('data_dir', toUnicode(data_dir))
Env.set('data_dir', data_dir) Env.set('log_path', toUnicode(os.path.join(log_dir, 'CouchPotato.log')))
Env.set('log_path', os.path.join(log_dir, 'CouchPotato.log')) Env.set('db_path', toUnicode('sqlite:///' + db_path))
Env.set('db_path', 'sqlite:///' + db_path) Env.set('cache_dir', toUnicode(os.path.join(data_dir, 'cache')))
Env.set('cache_dir', os.path.join(data_dir, 'cache')) Env.set('cache', FileSystemCache(toUnicode(os.path.join(Env.get('cache_dir'), 'python'))))
Env.set('cache', FileSystemCache(os.path.join(Env.get('cache_dir'), 'python')))
Env.set('console_log', options.console_log) Env.set('console_log', options.console_log)
Env.set('quiet', options.quiet) Env.set('quiet', options.quiet)
Env.set('desktop', desktop) Env.set('desktop', desktop)
@@ -170,12 +172,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
# Check if database exists # Check if database exists
db = Env.get('db_path') db = Env.get('db_path')
db_exists = os.path.isfile(db_path) db_exists = os.path.isfile(toUnicode(db_path))
# Load configs & plugins
loader = Env.get('loader')
loader.preload(root = base_path)
loader.run()
# Load migrations # Load migrations
if db_exists: if db_exists:
@@ -201,17 +198,16 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
from couchpotato.core.settings.model import setup from couchpotato.core.settings.model import setup
setup() setup()
# Fill database with needed stuff
if not db_exists:
fireEvent('app.initialize', in_order = True)
# Create app # Create app
from couchpotato import app from couchpotato import WebHandler
web_base = ('/' + Env.setting('url_base').lstrip('/') + '/') if Env.setting('url_base') else '/'
Env.set('web_base', web_base)
api_key = Env.setting('api_key') api_key = Env.setting('api_key')
url_base = '/' + Env.setting('url_base').lstrip('/') if Env.setting('url_base') else '' api_base = r'%sapi/%s/' % (web_base, api_key)
Env.set('api_base', api_base)
# Basic config # Basic config
app.secret_key = api_key
host = Env.setting('host', default = '0.0.0.0') host = Env.setting('host', default = '0.0.0.0')
# app.debug = development # app.debug = development
config = { config = {
@@ -222,36 +218,61 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
'ssl_key': Env.setting('ssl_key', default = None), 'ssl_key': Env.setting('ssl_key', default = None),
} }
# Static path
app.static_folder = os.path.join(base_path, 'couchpotato', 'static')
web.add_url_rule('api/%s/static/<path:filename>' % api_key,
endpoint = 'static',
view_func = app.send_static_file)
# Register modules # Load the app
app.register_blueprint(web, url_prefix = '%s/' % url_base) application = Application([],
app.register_blueprint(api, url_prefix = '%s/api/%s/' % (url_base, api_key)) log_function = lambda x : None,
debug = config['use_reloader'],
gzip = True,
)
Env.set('app', application)
# Request handlers
application.add_handlers(".*$", [
(r'%snonblock/(.*)/' % api_base, NonBlockHandler),
# API handlers
(r'%s(.*)/' % api_base, ApiHandler), # Main API handler
(r'%sgetkey/' % web_base, KeyHandler), # Get API key
(r'%s' % api_base, RedirectHandler, {"url": web_base + 'docs/'}), # API docs
# Catch all webhandlers
(r'%s(.*)/' % web_base, WebHandler),
(r'%s(.*)' % web_base, WebHandler),
(r'(.*)', WebHandler),
])
# Static paths
static_path = '%sstatic/' % api_base
for dir_name in ['fonts', 'images', 'scripts', 'style']:
application.add_handlers(".*$", [
('%s%s/(.*)' % (static_path, dir_name), StaticFileHandler, {'path': toUnicode(os.path.join(base_path, 'couchpotato', 'static', dir_name))})
])
Env.set('static_path', static_path);
# Load configs & plugins
loader = Env.get('loader')
loader.preload(root = toUnicode(base_path))
loader.run()
# Fill database with needed stuff
if not db_exists:
fireEvent('app.initialize', in_order = True)
# Go go go!
from tornado.ioloop import IOLoop
loop = IOLoop.current()
# Some logging and fire load event # Some logging and fire load event
try: log.info('Starting server on port %(port)s', config) try: log.info('Starting server on port %(port)s', config)
except: pass except: pass
fireEventAsync('app.load') fireEventAsync('app.load')
# Go go go!
from tornado.ioloop import IOLoop
web_container = WSGIContainer(app)
web_container._log = _log
loop = IOLoop.current()
application = Application([
(r'%s/api/%s/nonblock/(.*)/' % (url_base, api_key), NonBlockHandler),
(r'.*', FallbackHandler, dict(fallback = web_container)),
],
log_function = lambda x : None,
debug = config['use_reloader'],
gzip = True,
)
if config['ssl_cert'] and config['ssl_key']: if config['ssl_cert'] and config['ssl_key']:
server = HTTPServer(application, no_keep_alive = True, ssl_options = { server = HTTPServer(application, no_keep_alive = True, ssl_options = {
@@ -269,10 +290,11 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
server.listen(config['port'], config['host']) server.listen(config['port'], config['host'])
loop.start() loop.start()
except Exception, e: except Exception, e:
log.error('Failed starting: %s', traceback.format_exc())
try: try:
nr, msg = e nr, msg = e
if nr == 48: if nr == 48:
log.info('Already in use, try %s more time after few seconds', restart_tries) log.info('Port (%s) needed for CouchPotato is already in use, try %s more time after few seconds', (config.get('port'), restart_tries))
time.sleep(1) time.sleep(1)
restart_tries -= 1 restart_tries -= 1
Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 29 KiB

+13 -12
View File
@@ -1,27 +1,28 @@
{% autoescape None %}
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/api.css') }}" type="text/css"> <link rel="stylesheet" href="{{ Env.get('static_path') }}style/api.css" type="text/css">
<title>API documentation</title> <title>API documentation</title>
</head> </head>
<body> <body>
<h1>CouchPotato API Documentation</h1> <h1>CouchPotato API Documentation</h1>
<div class="api"> <div class="api">
You can access the API via <pre>{{ fireEvent('app.api_url', single = True)|safe }}/</pre> You can access the API via <pre>{{ Env.get('api_base') }}</pre>
To see it in action, have a look at the webinterface with Firebug (on firefox) or the development tools included in Chrome. To see it in action, have a look at the webinterface with Firebug (on firefox) or the development tools included in Chrome.
All the data that you see there are from the API. All the data that you see there are from the API.
<br /> <br />
<br /> <br />
A normal API call: A normal API call:
<pre><a href="{{ fireEvent('app.api_url', single = True)|safe }}/updater.info/">{{ fireEvent('app.api_url', single = True)|safe }}/updater.info/</a></pre> <pre><a href="{{ Env.get('api_base') }}updater.info/">{{ Env.get('api_base') }}updater.info/</a></pre>
<br /> <br />
You can also use the API over another domain using JSONP, the callback function should be in 'callback_func' You can also use the API over another domain using JSONP, the callback function should be in 'callback_func'
<pre><a href="{{ fireEvent('app.api_url', single = True)|safe }}/updater.info/?callback_func=myfunction">{{ fireEvent('app.api_url', single = True)|safe }}/updater.info/?callback_func=myfunction</a></pre> <pre><a href="{{ Env.get('api_base') }}updater.info/?callback_func=myfunction">{{ Env.get('api_base') }}updater.info/?callback_func=myfunction</a></pre>
<br /> <br />
<br /> <br />
Get the API key: Get the API key:
<pre><a href="{{ url_for('web.index') }}getkey/?p=md5(password)&amp;u=md5(username)">{{ url_for('web.index') }}getkey/?p=md5(password)&amp;u=md5(username)</a></pre> <pre><a href="{{ Env.get('web_base') }}getkey/?p=md5(password)&amp;u=md5(username)">{{ Env.get('web_base') }}getkey/?p=md5(password)&amp;u=md5(username)</a></pre>
Will return {"api_key": "XXXXXXXXXX", "success": true}. When username or password is empty you don't need to md5 it. Will return {"api_key": "XXXXXXXXXX", "success": true}. When username or password is empty you don't need to md5 it.
<br /> <br />
</div> </div>
@@ -41,9 +42,9 @@
<td class="type">{{ api_docs[route]['params'][param].get('type', 'string') }}</td> <td class="type">{{ api_docs[route]['params'][param].get('type', 'string') }}</td>
<td class="description">{{ api_docs[route]['params'][param]['desc'] }}</td> <td class="description">{{ api_docs[route]['params'][param]['desc'] }}</td>
</tr> </tr>
{% endfor %} {% end %}
</table> </table>
{% endif %} {% end %}
{% if api_docs[route].get('return') %} {% if api_docs[route].get('return') %}
<h3>Return</h3> <h3>Return</h3>
@@ -52,14 +53,14 @@
{% if api_docs[route]['return'].get('example') %} {% if api_docs[route]['return'].get('example') %}
<div class="example"> <div class="example">
<h4>Example</h4> <h4>Example</h4>
<pre>{{ api_docs[route]['return'].get('example', '')|safe }}</pre> <pre>{{ api_docs[route]['return'].get('example', '') }}</pre>
</div> </div>
{% endif %} {% end %}
</div> </div>
{% endif %} {% end %}
</div> </div>
{% endif %} {% end %}
{% endfor %} {% end %}
<div class="missing"> <div class="missing">
<h1>Missing documentation</h1> <h1>Missing documentation</h1>
+26 -25
View File
@@ -1,3 +1,4 @@
{% autoescape None %}
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
@@ -5,38 +6,38 @@
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
{% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'front', single = True) %} {% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'front', single = True) %}
<link rel="stylesheet" href="{{ url_for('web.index') }}{{ url }}" type="text/css">{% endfor %} <link rel="stylesheet" href="{{ Env.get('web_base') }}{{ url }}" type="text/css">{% end %}
{% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'front', single = True) %} {% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'front', single = True) %}
<script type="text/javascript" src="{{ url_for('web.index') }}{{ url }}"></script>{% endfor %} <script type="text/javascript" src="{{ Env.get('web_base') }}{{ url }}"></script>{% end %}
{% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'head', single = True) %} {% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'head', single = True) %}
<script type="text/javascript" src="{{ url_for('web.index') }}{{ url }}"></script>{% endfor %} <script type="text/javascript" src="{{ Env.get('web_base') }}{{ url }}"></script>{% end %}
{% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'head', single = True) %} {% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'head', single = True) %}
<link rel="stylesheet" href="{{ url_for('web.index') }}{{ url }}" type="text/css">{% endfor %} <link rel="stylesheet" href="{{ Env.get('web_base') }}{{ url }}" type="text/css">{% end %}
<link href="{{ url_for('web.static', filename='images/favicon.ico') }}" rel="icon" type="image/x-icon" /> <link href="{{ Env.get('static_path') }}images/favicon.ico" rel="icon" type="image/x-icon" />
<link rel="apple-touch-icon" href="{{ url_for('web.static', filename='images/homescreen.png') }}" /> <link rel="apple-touch-icon" href="{{ Env.get('static_path') }}images/homescreen.png" />
<script type="text/javascript" src="https://www.youtube.com/player_api" defer="defer"></script> <script type="text/javascript" src="https://www.youtube.com/player_api" defer="defer"></script>
<script type="text/javascript"> <script type="text/javascript">
if($(window).getSize().x <= 480) if($(window).getSize().x <= 480)
window.addEvent('load', function() { window.addEvent('load', function() {
setTimeout(function(){ setTimeout(function(){
window.scrollTo(0, 1); window.scrollTo(0, 1);
window.scrollTo(0, 0); window.scrollTo(0, 0);
}, 100); }, 100);
}); });
window.addEvent('domready', function() { window.addEvent('domready', function() {
new Uniform(); new Uniform();
Api.setup({ Api.setup({
'url': {{ url_for('api.index')|tojson|safe }}, 'url': {{ json_encode(Env.get('api_base')) }},
'path_sep': {{ sep|tojson|safe }}, 'path_sep': {{ json_encode(sep) }},
'is_remote': false 'is_remote': false
}); });
@@ -61,29 +62,29 @@
} }
Quality.setup({ Quality.setup({
'profiles': {{ fireEvent('profile.all', single = True)|tojson|safe }}, 'profiles': {{ json_encode(fireEvent('profile.all', single = True)) }},
'qualities': {{ fireEvent('quality.all', single = True)|tojson|safe }} 'qualities': {{ json_encode(fireEvent('quality.all', single = True)) }}
}); });
Status.setup({{ fireEvent('status.all', single = True)|tojson|safe }}); Status.setup({{ json_encode(fireEvent('status.all', single = True)) }});
File.Type.setup({{ fireEvent('file.types', single = True)|tojson|safe }}); File.Type.setup({{ json_encode(fireEvent('file.types', single = True)) }});
App.setup({ App.setup({
'base_url': {{ url_for('web.index')|tojson|safe }}, 'base_url': {{ json_encode(Env.get('web_base')) }},
'args': {{ env.get('args')|tojson|safe }}, 'args': {{ json_encode(Env.get('args')) }},
'options': {{ ('%s' % env.get('options'))|tojson|safe }}, 'options': {{ json_encode(('%s' % Env.get('options'))) }},
'app_dir': {{ env.get('app_dir')|tojson|safe }}, 'app_dir': {{ json_encode(Env.get('app_dir')) }},
'data_dir': {{ env.get('data_dir')|tojson|safe }}, 'data_dir': {{ json_encode(Env.get('data_dir')) }},
'pid': {{ env.getPid()|tojson|safe }}, 'pid': {{ json_encode(Env.getPid()) }},
'userscript_version': {{ fireEvent('userscript.get_version', single = True)|tojson|safe }} 'userscript_version': {{ json_encode(fireEvent('userscript.get_version', single = True)) }}
}); });
}) })
{% if env.setting('show_wizard') %} {% if Env.setting('show_wizard') %}
if(!window.location.href.contains('wizard')) if(!window.location.href.contains('wizard'))
window.location = '{{ url_for('web.index') }}wizard/' window.location = '{{ Env.get('web_base') }}wizard/'
{% endif %} {% end %}
</script> </script>
<title>CouchPotato</title> <title>CouchPotato</title>
+7 -1
View File
@@ -45,6 +45,8 @@ test -x $CP_DAEMON || exit 0
set -e set -e
. /lib/lsb/init-functions
case "$1" in case "$1" in
start) start)
echo "Starting $DESC" echo "Starting $DESC"
@@ -63,9 +65,13 @@ case "$1" in
start-stop-daemon --stop --pidfile $CP_PID_FILE --retry 15 start-stop-daemon --stop --pidfile $CP_PID_FILE --retry 15
start-stop-daemon -d $CP_APP_PATH -c $CP_RUN_AS --start --background --pidfile $CP_PID_FILE --exec $CP_DAEMON -- $CP_DAEMON_OPTS start-stop-daemon -d $CP_APP_PATH -c $CP_RUN_AS --start --background --pidfile $CP_PID_FILE --exec $CP_DAEMON -- $CP_DAEMON_OPTS
;; ;;
status)
status_of_proc -p $CP_PID_FILE "$CP_DAEMON" "$NAME"
;;
*) *)
N=/etc/init.d/$NAME N=/etc/init.d/$NAME
echo "Usage: $N {start|stop|restart|force-reload}" >&2 echo "Usage: $N {start|stop|restart|force-reload|status}" >&2
exit 1 exit 1
;; ;;
esac esac
+262
View 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
File renamed without changes.
-44
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
-345
View File
@@ -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
-168
View File
@@ -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))
-295
View File
@@ -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
)
-79
View File
@@ -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
-49
View File
@@ -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.'
)
-29
View File
@@ -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
-119
View File
@@ -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
-36
View File
@@ -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'))
-893
View File
@@ -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)
-45
View File
@@ -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
-42
View File
@@ -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'
-19
View File
@@ -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
-205
View File
@@ -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)
-52
View File
@@ -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')
-140
View File
@@ -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)
-118
View File
@@ -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()
-150
View File
@@ -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)
-148
View File
@@ -1,148 +0,0 @@
# -*- coding: utf-8 -*-
"""
flask.wrappers
~~~~~~~~~~~~~~
Implements the WSGI wrappers (request and response).
:copyright: (c) 2011 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase
from werkzeug.utils import cached_property
from .exceptions import JSONBadRequest
from .debughelpers import attach_enctype_error_multidict
from .helpers import json, _assert_have_json
from .globals import _request_ctx_stack
class Request(RequestBase):
"""The request object used by default in Flask. Remembers the
matched endpoint and view arguments.
It is what ends up as :class:`~flask.request`. If you want to replace
the request object used you can subclass this and set
:attr:`~flask.Flask.request_class` to your subclass.
The request object is a :class:`~werkzeug.wrappers.Request` subclass and
provides all of the attributes Werkzeug defines plus a few Flask
specific ones.
"""
#: the internal URL rule that matched the request. This can be
#: useful to inspect which methods are allowed for the URL from
#: a before/after handler (``request.url_rule.methods``) etc.
#:
#: .. versionadded:: 0.6
url_rule = None
#: a dict of view arguments that matched the request. If an exception
#: happened when matching, this will be `None`.
view_args = None
#: if matching the URL failed, this is the exception that will be
#: raised / was raised as part of the request handling. This is
#: usually a :exc:`~werkzeug.exceptions.NotFound` exception or
#: something similar.
routing_exception = None
# switched by the request context until 1.0 to opt in deprecated
# module functionality
_is_old_module = False
@property
def max_content_length(self):
"""Read-only view of the `MAX_CONTENT_LENGTH` config key."""
ctx = _request_ctx_stack.top
if ctx is not None:
return ctx.app.config['MAX_CONTENT_LENGTH']
@property
def endpoint(self):
"""The endpoint that matched the request. This in combination with
:attr:`view_args` can be used to reconstruct the same or a
modified URL. If an exception happened when matching, this will
be `None`.
"""
if self.url_rule is not None:
return self.url_rule.endpoint
@property
def module(self):
"""The name of the current module if the request was dispatched
to an actual module. This is deprecated functionality, use blueprints
instead.
"""
from warnings import warn
warn(DeprecationWarning('modules were deprecated in favor of '
'blueprints. Use request.blueprint '
'instead.'), stacklevel=2)
if self._is_old_module:
return self.blueprint
@property
def blueprint(self):
"""The name of the current blueprint"""
if self.url_rule and '.' in self.url_rule.endpoint:
return self.url_rule.endpoint.rsplit('.', 1)[0]
@cached_property
def json(self):
"""If the mimetype is `application/json` this will contain the
parsed JSON data. Otherwise this will be `None`.
This requires Python 2.6 or an installed version of simplejson.
"""
if __debug__:
_assert_have_json()
if self.mimetype == 'application/json':
request_charset = self.mimetype_params.get('charset')
try:
if request_charset is not None:
return json.loads(self.data, encoding=request_charset)
return json.loads(self.data)
except ValueError, e:
return self.on_json_loading_failed(e)
def on_json_loading_failed(self, e):
"""Called if decoding of the JSON data failed. The return value of
this method is used by :attr:`json` when an error ocurred. The default
implementation raises a :class:`JSONBadRequest`, which is a subclass of
:class:`~werkzeug.exceptions.BadRequest` which sets the
``Content-Type`` to ``application/json`` and provides a JSON-formatted
error description::
{"description": "The browser (or proxy) sent a request that \
this server could not understand."}
.. versionchanged:: 0.9
Return a :class:`JSONBadRequest` instead of a
:class:`~werkzeug.exceptions.BadRequest` by default.
.. versionadded:: 0.8
"""
raise JSONBadRequest()
def _load_form_data(self):
RequestBase._load_form_data(self)
# in debug mode we're replacing the files multidict with an ad-hoc
# subclass that raises a different error for key errors.
ctx = _request_ctx_stack.top
if ctx is not None and ctx.app.debug and \
self.mimetype != 'multipart/form-data' and not self.files:
attach_enctype_error_multidict(self)
class Response(ResponseBase):
"""The response object that is used by default in Flask. Works like the
response object from Werkzeug but is set to have an HTML mimetype by
default. Quite often you don't have to create this object yourself because
:meth:`~flask.Flask.make_response` will take care of that for you.
If you want to replace the response object used you can subclass this and
set :attr:`~flask.Flask.response_class` to your subclass.
"""
default_mimetype = 'text/html'
Loaded 100 of 496 files, more files were not shown because too many files have changed in this diff. Show more