Merge branch 'refs/heads/develop' into desktop
Conflicts: version.py
This commit is contained in:
20
README.md
20
README.md
@@ -40,3 +40,23 @@ Linux (ubuntu / debian):
|
||||
* Make it executable. `sudo chmod +x /etc/init.d/couchpotato`
|
||||
* Add it to defaults. `sudo update-rc.d couchpotato defaults`
|
||||
* Open your browser and go to: `http://localhost:5050/`
|
||||
|
||||
|
||||
FreeBSD :
|
||||
|
||||
* Update your ports tree `sudo portsnap fetch update`
|
||||
* Install Python 2.6+ [lang/python](http://www.freshports.org/lang/python) with `cd /usr/ports/lang/python; sudo make install clean`
|
||||
* Install port [databases/py-sqlite3](http://www.freshports.org/databases/py-sqlite3) with `cd /usr/ports/databases/py-sqlite3; sudo make install clean`
|
||||
* Add a symlink to 'python2' `sudo ln -s /usr/local/bin/python /usr/local/bin/python2`
|
||||
* Install port [ftp/libcurl](http://www.freshports.org/ftp/libcurl) with `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean`
|
||||
* Install port [ftp/curl](http://www.freshports.org/ftp/bcurl), deselect 'Asynchronous DNS resolution via c-ares' when prompted as part of config `cd /usr/ports/ftp/fpc-libcurl; sudo make install clean`
|
||||
* Install port [textproc/docbook-xml-450](http://www.freshports.org/textproc/docbook-xml-450) with `cd /usr/ports/textproc/docbook-xml-450; sudo make install clean`
|
||||
* Install port [GIT](http://git-scm.com/) with `cd /usr/ports/devel/git; sudo make install clean`
|
||||
* 'cd' to the folder of your choosing.
|
||||
* Run `git clone https://github.com/RuudBurger/CouchPotatoServer.git`
|
||||
* Then run `sudo python CouchPotatoServer/CouchPotato.py` to start for the first time
|
||||
* To run on boot copy the init script. `sudo cp CouchPotatoServer/init/freebsd /etc/rc.d/couchpotato`
|
||||
* Change the paths inside the init script. `sudo vim /etc/init.d/couchpotato`
|
||||
* Make init script executable. `sudo chmod +x /etc/rc.d/couchpotato`
|
||||
* Add init to startup. `sudo echo 'couchpotato_enable="YES"' >> /etc/rc.conf`
|
||||
* Open your browser and go to: `http://server:5050/`
|
||||
|
||||
@@ -1,84 +1,85 @@
|
||||
from couchpotato.api import api_docs, api_docs_missing
|
||||
from couchpotato.api import api_docs, api_docs_missing, api
|
||||
from couchpotato.core.auth import requires_auth
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.request import getParams, jsonified
|
||||
from couchpotato.core.helpers.variable import md5
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from flask.app import Flask
|
||||
from flask.blueprints import Blueprint
|
||||
from flask.globals import request
|
||||
from flask.helpers import url_for
|
||||
from flask.templating import render_template
|
||||
from sqlalchemy.engine import create_engine
|
||||
from sqlalchemy.orm import scoped_session
|
||||
from sqlalchemy.orm.session import sessionmaker
|
||||
from werkzeug.utils import redirect
|
||||
from tornado import template
|
||||
from tornado.web import RequestHandler
|
||||
import os
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
app = Flask(__name__, static_folder = 'nope')
|
||||
web = Blueprint('web', __name__)
|
||||
views = {}
|
||||
template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates'))
|
||||
|
||||
# Main web handler
|
||||
@requires_auth
|
||||
class WebHandler(RequestHandler):
|
||||
def get(self, route, *args, **kwargs):
|
||||
route = route.strip('/')
|
||||
if not views.get(route):
|
||||
page_not_found(self)
|
||||
return
|
||||
self.write(views[route]())
|
||||
|
||||
def addView(route, func, static = False):
|
||||
views[route] = func
|
||||
|
||||
def get_session(engine = None):
|
||||
return Env.getSession(engine)
|
||||
|
||||
def addView(route, func, static = False):
|
||||
web.add_url_rule(route + ('' if static else '/'), endpoint = route if route else 'index', view_func = func)
|
||||
|
||||
""" Web view """
|
||||
@web.route('/')
|
||||
@requires_auth
|
||||
# Web view
|
||||
def index():
|
||||
return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env)
|
||||
return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env)
|
||||
addView('', index)
|
||||
|
||||
""" Api view """
|
||||
@web.route('docs/')
|
||||
@requires_auth
|
||||
# API docs
|
||||
def apiDocs():
|
||||
from couchpotato import app
|
||||
routes = []
|
||||
for route, x in sorted(app.view_functions.iteritems()):
|
||||
if route[0:4] == 'api.':
|
||||
routes += [route[4:].replace('::', '.')]
|
||||
|
||||
for route in api.iterkeys():
|
||||
routes.append(route)
|
||||
|
||||
if api_docs.get(''):
|
||||
del api_docs['']
|
||||
del api_docs_missing['']
|
||||
return render_template('api.html', fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing))
|
||||
|
||||
@web.route('getkey/')
|
||||
def getApiKey():
|
||||
return template_loader.load('api.html').generate(fireEvent = fireEvent, routes = sorted(routes), api_docs = api_docs, api_docs_missing = sorted(api_docs_missing), Env = Env)
|
||||
|
||||
api = None
|
||||
params = getParams()
|
||||
username = Env.setting('username')
|
||||
password = Env.setting('password')
|
||||
addView('docs', apiDocs)
|
||||
|
||||
if (params.get('u') == md5(username) or not username) and (params.get('p') == password or not password):
|
||||
api = Env.setting('api_key')
|
||||
# Make non basic auth option to get api key
|
||||
class KeyHandler(RequestHandler):
|
||||
def get(self, *args, **kwargs):
|
||||
api = None
|
||||
username = Env.setting('username')
|
||||
password = Env.setting('password')
|
||||
|
||||
return jsonified({
|
||||
'success': api is not None,
|
||||
'api_key': api
|
||||
})
|
||||
if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password):
|
||||
api = Env.setting('api_key')
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(error):
|
||||
index_url = url_for('web.index')
|
||||
url = request.path[len(index_url):]
|
||||
self.write({
|
||||
'success': api is not None,
|
||||
'api_key': api
|
||||
})
|
||||
|
||||
def page_not_found(rh):
|
||||
index_url = Env.get('web_base')
|
||||
url = rh.request.uri[len(index_url):]
|
||||
|
||||
if url[:3] != 'api':
|
||||
if request.path != '/':
|
||||
r = request.url.replace(request.path, index_url + '#' + url)
|
||||
else:
|
||||
r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url)
|
||||
return redirect(r)
|
||||
r = index_url + '#' + url.lstrip('/')
|
||||
rh.redirect(r)
|
||||
else:
|
||||
if not Env.get('dev'):
|
||||
time.sleep(0.1)
|
||||
return 'Wrong API key used', 404
|
||||
|
||||
rh.set_status(404)
|
||||
rh.write('Wrong API key used')
|
||||
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
from flask.blueprints import Blueprint
|
||||
from flask.helpers import url_for
|
||||
from couchpotato.core.helpers.request import getParams
|
||||
from tornado.web import RequestHandler, asynchronous
|
||||
from werkzeug.utils import redirect
|
||||
import json
|
||||
import urllib
|
||||
|
||||
api = Blueprint('api', __name__)
|
||||
api_docs = {}
|
||||
api_docs_missing = []
|
||||
api = {}
|
||||
api_nonblock = {}
|
||||
|
||||
api_docs = {}
|
||||
api_docs_missing = []
|
||||
|
||||
# NonBlock API handler
|
||||
class NonBlockHandler(RequestHandler):
|
||||
|
||||
stoppers = []
|
||||
|
||||
@asynchronous
|
||||
def get(self, route):
|
||||
def get(self, route, *args, **kwargs):
|
||||
route = route.strip('/')
|
||||
start, stop = api_nonblock[route]
|
||||
self.stoppers.append(stop)
|
||||
|
||||
@@ -32,14 +34,6 @@ class NonBlockHandler(RequestHandler):
|
||||
|
||||
self.stoppers = []
|
||||
|
||||
|
||||
def addApiView(route, func, static = False, docs = None, **kwargs):
|
||||
api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func, **kwargs)
|
||||
if docs:
|
||||
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
|
||||
else:
|
||||
api_docs_missing.append(route)
|
||||
|
||||
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
||||
api_nonblock[route] = func_tuple
|
||||
|
||||
@@ -48,9 +42,43 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
|
||||
else:
|
||||
api_docs_missing.append(route)
|
||||
|
||||
""" Api view """
|
||||
def index():
|
||||
index_url = url_for('web.index')
|
||||
return redirect(index_url + 'docs/')
|
||||
# Blocking API handler
|
||||
class ApiHandler(RequestHandler):
|
||||
|
||||
addApiView('', index)
|
||||
def get(self, route, *args, **kwargs):
|
||||
route = route.strip('/')
|
||||
if not api.get(route):
|
||||
self.write('API call doesn\'t seem to exist')
|
||||
return
|
||||
|
||||
kwargs = {}
|
||||
for x in self.request.arguments:
|
||||
kwargs[x] = urllib.unquote(self.get_argument(x))
|
||||
|
||||
# Split array arguments
|
||||
kwargs = getParams(kwargs)
|
||||
|
||||
# Remove t random string
|
||||
try: del kwargs['t']
|
||||
except: pass
|
||||
|
||||
# Check JSONP callback
|
||||
result = api[route](**kwargs)
|
||||
jsonp_callback = self.get_argument('callback_func', default = None)
|
||||
|
||||
if jsonp_callback:
|
||||
self.write(str(jsonp_callback) + '(' + json.dumps(result) + ')')
|
||||
elif isinstance(result, (tuple)) and result[0] == 'redirect':
|
||||
self.redirect(result[1])
|
||||
else:
|
||||
self.write(result)
|
||||
|
||||
def addApiView(route, func, static = False, docs = None, **kwargs):
|
||||
|
||||
if static: func(route)
|
||||
else: api[route] = func
|
||||
|
||||
if docs:
|
||||
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
|
||||
else:
|
||||
api_docs_missing.append(route)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.helpers.variable import cleanHost, md5
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -68,12 +67,12 @@ class Core(Plugin):
|
||||
|
||||
return True
|
||||
|
||||
def available(self):
|
||||
return jsonified({
|
||||
def available(self, **kwargs):
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def shutdown(self):
|
||||
def shutdown(self, **kwargs):
|
||||
if self.shutdown_started:
|
||||
return False
|
||||
|
||||
@@ -83,7 +82,7 @@ class Core(Plugin):
|
||||
|
||||
return 'shutdown'
|
||||
|
||||
def restart(self):
|
||||
def restart(self, **kwargs):
|
||||
if self.shutdown_started:
|
||||
return False
|
||||
|
||||
@@ -156,10 +155,10 @@ class Core(Plugin):
|
||||
host = 'localhost'
|
||||
port = Env.setting('port')
|
||||
|
||||
return '%s:%d%s' % (cleanHost(host).rstrip('/'), int(port), '/' + Env.setting('url_base').lstrip('/') if Env.setting('url_base') else '')
|
||||
return '%s:%d%s' % (cleanHost(host).rstrip('/'), int(port), Env.get('web_base'))
|
||||
|
||||
def createApiUrl(self):
|
||||
return '%s/api/%s' % (self.createBaseUrl(), Env.setting('api_key'))
|
||||
return '%sapi/%s' % (self.createBaseUrl(), Env.setting('api_key'))
|
||||
|
||||
def version(self):
|
||||
ver = fireEvent('updater.info', single = True)
|
||||
@@ -170,10 +169,10 @@ class Core(Plugin):
|
||||
|
||||
return '%s - %s-%s - v2' % (platf, ver.get('version')['type'], ver.get('version')['hash'])
|
||||
|
||||
def versionView(self):
|
||||
return jsonified({
|
||||
def versionView(self, **kwargs):
|
||||
return {
|
||||
'version': self.version()
|
||||
})
|
||||
}
|
||||
|
||||
def signalHandler(self):
|
||||
if Env.get('daemonized'): return
|
||||
|
||||
@@ -8,7 +8,6 @@ from minify.cssmin import cssmin
|
||||
from minify.jsmin import jsmin
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -122,7 +121,7 @@ class ClientScript(Plugin):
|
||||
# Combine all files together with some comments
|
||||
data = ''
|
||||
for r in raw:
|
||||
data += self.comment.get(file_type) % (r.get('file'), r.get('date'))
|
||||
data += self.comment.get(file_type) % (ss(r.get('file')), r.get('date'))
|
||||
data += r.get('data') + '\n\n'
|
||||
|
||||
self.createFile(out, data.strip())
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
@@ -36,7 +35,7 @@ class Updater(Plugin):
|
||||
addEvent('app.load', self.setCrons)
|
||||
addEvent('updater.info', self.info)
|
||||
|
||||
addApiView('updater.info', self.getInfo, docs = {
|
||||
addApiView('updater.info', self.info, docs = {
|
||||
'desc': 'Get updater information',
|
||||
'return': {
|
||||
'type': 'object',
|
||||
@@ -86,25 +85,24 @@ class Updater(Plugin):
|
||||
|
||||
if self.updater.check():
|
||||
if not self.available_notified and self.conf('notification') and not self.conf('automatic'):
|
||||
fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
|
||||
info = self.updater.info()
|
||||
version_date = datetime.fromtimestamp(info['update_version']['date'])
|
||||
fireEvent('updater.available', message = 'A new update with hash "%s" is available, this version is from %s' % (info['update_version']['hash'], version_date), data = info)
|
||||
self.available_notified = True
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def info(self):
|
||||
def info(self, **kwargs):
|
||||
return self.updater.info()
|
||||
|
||||
def getInfo(self):
|
||||
return jsonified(self.updater.info())
|
||||
|
||||
def checkView(self):
|
||||
return jsonified({
|
||||
def checkView(self, **kwargs):
|
||||
return {
|
||||
'update_available': self.check(force = True),
|
||||
'info': self.updater.info()
|
||||
})
|
||||
}
|
||||
|
||||
def doUpdateView(self):
|
||||
def doUpdateView(self, **kwargs):
|
||||
|
||||
self.check()
|
||||
if not self.updater.update_version:
|
||||
@@ -119,9 +117,9 @@ class Updater(Plugin):
|
||||
if not success:
|
||||
success = True
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': success
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
class BaseUpdater(Plugin):
|
||||
@@ -138,9 +136,6 @@ class BaseUpdater(Plugin):
|
||||
def doUpdate(self):
|
||||
pass
|
||||
|
||||
def getInfo(self):
|
||||
return jsonified(self.info())
|
||||
|
||||
def info(self):
|
||||
return {
|
||||
'last_check': self.last_check,
|
||||
@@ -279,6 +274,7 @@ class SourceUpdater(BaseUpdater):
|
||||
if download_data.get('type') == 'zip':
|
||||
zip = zipfile.ZipFile(destination)
|
||||
zip.extractall(extracted_path)
|
||||
zip.close()
|
||||
else:
|
||||
tar = tarfile.open(destination)
|
||||
tar.extractall(path = extracted_path)
|
||||
|
||||
@@ -1,26 +1,40 @@
|
||||
from couchpotato.core.helpers.variable import md5
|
||||
from couchpotato.environment import Env
|
||||
from flask import request, Response
|
||||
from functools import wraps
|
||||
import base64
|
||||
|
||||
def check_auth(username, password):
|
||||
return username == Env.setting('username') and password == Env.setting('password')
|
||||
|
||||
def authenticate():
|
||||
return Response(
|
||||
'This is not the page you are looking for. *waves hand*', 401,
|
||||
{'WWW-Authenticate': 'Basic realm="CouchPotato Login"'}
|
||||
)
|
||||
def requires_auth(handler_class):
|
||||
|
||||
def requires_auth(f):
|
||||
def wrap_execute(handler_execute):
|
||||
|
||||
@wraps(f)
|
||||
def decorated(*args, **kwargs):
|
||||
auth = getattr(request, 'authorization')
|
||||
if Env.setting('username') and Env.setting('password'):
|
||||
if (not auth or not check_auth(auth.username.decode('latin1'), md5(auth.password.decode('latin1').encode(Env.get('encoding'))))):
|
||||
return authenticate()
|
||||
def require_basic_auth(handler, kwargs):
|
||||
if Env.setting('username') and Env.setting('password'):
|
||||
|
||||
return f(*args, **kwargs)
|
||||
auth_header = handler.request.headers.get('Authorization')
|
||||
auth_decoded = base64.decodestring(auth_header[6:]) if auth_header else None
|
||||
if auth_decoded:
|
||||
username, password = auth_decoded.split(':', 2)
|
||||
|
||||
return decorated
|
||||
if auth_header is None or not auth_header.startswith('Basic ') or (not check_auth(username.decode('latin'), md5(password.decode('latin')))):
|
||||
handler.set_status(401)
|
||||
handler.set_header('WWW-Authenticate', 'Basic realm="CouchPotato Login"')
|
||||
handler._transforms = []
|
||||
handler.finish()
|
||||
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _execute(self, transforms, *args, **kwargs):
|
||||
|
||||
if not require_basic_auth(self, kwargs):
|
||||
return False
|
||||
return handler_execute(self, transforms, *args, **kwargs)
|
||||
|
||||
return _execute
|
||||
|
||||
handler_class._execute = wrap_execute(handler_class._execute)
|
||||
|
||||
return handler_class
|
||||
|
||||
@@ -16,7 +16,7 @@ class Downloader(Provider):
|
||||
|
||||
torrent_sources = [
|
||||
'http://torrage.com/torrent/%s.torrent',
|
||||
'http://torcache.net/torrent/%s.torrent',
|
||||
'https://torcache.net/torrent/%s.torrent',
|
||||
]
|
||||
|
||||
torrent_trackers = [
|
||||
|
||||
@@ -104,12 +104,21 @@ class NZBGet(Downloader):
|
||||
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
|
||||
except:
|
||||
nzb_id = item['NZBID']
|
||||
|
||||
|
||||
timeleft = -1
|
||||
try:
|
||||
if item['ActiveDownloads'] > 0 and item['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
|
||||
timeleft = str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
|
||||
except:
|
||||
pass
|
||||
|
||||
statuses.append({
|
||||
'id': nzb_id,
|
||||
'name': item['NZBFilename'],
|
||||
'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED',
|
||||
# Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
|
||||
'timeleft': str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20)) if item['ActiveDownloads'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']) else -1,
|
||||
'timeleft': timeleft,
|
||||
})
|
||||
|
||||
for item in queue: # 'Parameters' is not passed in rpc.postqueue
|
||||
|
||||
@@ -11,7 +11,7 @@ config = [{
|
||||
'list': 'download_providers',
|
||||
'name': 'sabnzbd',
|
||||
'label': 'Sabnzbd',
|
||||
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> to download NZBs.',
|
||||
'description': 'Use <a href="http://sabnzbd.org/" target="_blank">SABnzbd</a> (0.7+) to download NZBs.',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ config = [{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
'radio_group': 'torrent',
|
||||
'radio_group': 'nzb,torrent',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
@@ -32,6 +32,13 @@ config = [{
|
||||
'name': 'password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'use_for',
|
||||
'label': 'Use for',
|
||||
'default': 'both',
|
||||
'type': 'dropdown',
|
||||
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
|
||||
},
|
||||
{
|
||||
'name': 'manual',
|
||||
'default': 0,
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.helpers.encoding import isInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
import httplib
|
||||
import json
|
||||
import urllib
|
||||
import urllib2
|
||||
|
||||
import requests
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Synology(Downloader):
|
||||
|
||||
type = ['torrent_magnet']
|
||||
type = ['nzb', 'torrent', 'torrent_magnet']
|
||||
log = CPLog(__name__)
|
||||
|
||||
def download(self, data, movie, filedata = None):
|
||||
|
||||
log.error('Sending "%s" (%s) to Synology.', (data.get('name'), data.get('type')))
|
||||
response = False
|
||||
log.error('Sending "%s" (%s) to Synology.', (data['name'], data['type']))
|
||||
|
||||
# Load host from config and split out port.
|
||||
host = self.conf('host').split(':')
|
||||
@@ -24,20 +23,41 @@ class Synology(Downloader):
|
||||
log.error('Config properties are not filled in correctly, port is missing.')
|
||||
return False
|
||||
|
||||
if data.get('type') == 'torrent':
|
||||
log.error('Can\'t add binary torrent file')
|
||||
return False
|
||||
|
||||
try:
|
||||
# Send request to Transmission
|
||||
# Send request to Synology
|
||||
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
|
||||
remote_torrent = srpc.add_torrent_uri(data.get('url'))
|
||||
log.info('Response: %s', remote_torrent)
|
||||
return remote_torrent['success']
|
||||
if data['type'] == 'torrent_magnet':
|
||||
log.info('Adding torrent URL %s', data['url'])
|
||||
response = srpc.create_task(url = data['url'])
|
||||
elif data['type'] in ['nzb', 'torrent']:
|
||||
log.info('Adding %s' % data['type'])
|
||||
if not filedata:
|
||||
log.error('No %s data found' % data['type'])
|
||||
else:
|
||||
filename = data['name'] + '.' + data['type']
|
||||
response = srpc.create_task(filename = filename, filedata = filedata)
|
||||
except Exception, err:
|
||||
log.error('Exception while adding torrent: %s', err)
|
||||
return False
|
||||
finally:
|
||||
return response
|
||||
|
||||
def getEnabledDownloadType(self):
|
||||
if self.conf('use_for') == 'both':
|
||||
return super(Synology, self).getEnabledDownloadType()
|
||||
elif self.conf('use_for') == 'torrent':
|
||||
return ['torrent', 'torrent_magnet']
|
||||
else:
|
||||
return ['nzb']
|
||||
|
||||
def isEnabled(self, manual, data = {}):
|
||||
for_type = ['both']
|
||||
if data and 'torrent' in data.get('type'):
|
||||
for_type.append('torrent')
|
||||
elif data:
|
||||
for_type.append(data.get('type'))
|
||||
|
||||
return super(Synology, self).isEnabled(manual, data) and\
|
||||
((self.conf('use_for') in for_type))
|
||||
|
||||
class SynologyRPC(object):
|
||||
|
||||
@@ -58,11 +78,13 @@ class SynologyRPC(object):
|
||||
args = {'api': 'SYNO.API.Auth', 'account': self.username, 'passwd': self.password, 'version': 2,
|
||||
'method': 'login', 'session': self.session_name, 'format': 'sid'}
|
||||
response = self._req(self.auth_url, args)
|
||||
if response['success'] == True:
|
||||
if response['success']:
|
||||
self.sid = response['data']['sid']
|
||||
log.debug('Sid=%s', self.sid)
|
||||
return response
|
||||
elif self.username or self.password:
|
||||
log.debug('sid=%s', self.sid)
|
||||
else:
|
||||
log.error('Couldn\'t login to Synology, %s', response)
|
||||
return response['success']
|
||||
else:
|
||||
log.error('User or password missing, not using authentication.')
|
||||
return False
|
||||
|
||||
@@ -70,36 +92,51 @@ class SynologyRPC(object):
|
||||
args = {'api':'SYNO.API.Auth', 'version':1, 'method':'logout', 'session':self.session_name, '_sid':self.sid}
|
||||
return self._req(self.auth_url, args)
|
||||
|
||||
def _req(self, url, args):
|
||||
req_url = url + '?' + urllib.urlencode(args)
|
||||
def _req(self, url, args, files = None):
|
||||
response = {'success': False}
|
||||
try:
|
||||
req_open = urllib2.urlopen(req_url)
|
||||
response = json.loads(req_open.read())
|
||||
req = requests.post(url, data = args, files = files)
|
||||
req.raise_for_status()
|
||||
response = json.loads(req.text)
|
||||
if response['success'] == True:
|
||||
log.info('Synology action successfull')
|
||||
return response
|
||||
except httplib.InvalidURL, err:
|
||||
log.error('Invalid Transmission host, check your config %s', err)
|
||||
return False
|
||||
except urllib2.HTTPError, err:
|
||||
except requests.ConnectionError, err:
|
||||
log.error('Synology connection error, check your config %s', err)
|
||||
except requests.HTTPError, err:
|
||||
log.error('SynologyRPC HTTPError: %s', err)
|
||||
return False
|
||||
except urllib2.URLError, err:
|
||||
log.error('Unable to connect to Synology %s', err)
|
||||
return False
|
||||
except Exception, err:
|
||||
log.error('Exception: %s', err)
|
||||
finally:
|
||||
return response
|
||||
|
||||
def add_torrent_uri(self, torrent):
|
||||
log.info('Adding torrent URL %s', torrent)
|
||||
response = {}
|
||||
def create_task(self, url = None, filename = None, filedata = None):
|
||||
''' Creates new download task in Synology DownloadStation. Either specify
|
||||
url or pair (filename, filedata).
|
||||
|
||||
Returns True if task was created, False otherwise
|
||||
'''
|
||||
result = False
|
||||
# login
|
||||
login = self._login()
|
||||
if len(login) > 0 and login['success'] == True:
|
||||
log.info('Login success, adding torrent')
|
||||
args = {'api':'SYNO.DownloadStation.Task', 'version':1, 'method':'create', 'uri':torrent, '_sid':self.sid}
|
||||
response = self._req(self.download_url, args)
|
||||
if self._login():
|
||||
args = {'api': 'SYNO.DownloadStation.Task',
|
||||
'version': '1',
|
||||
'method': 'create',
|
||||
'_sid': self.sid}
|
||||
if url:
|
||||
log.info('Login success, adding torrent URI')
|
||||
args['uri'] = url
|
||||
response = self._req(self.download_url, args = args)
|
||||
log.info('Response: %s', response)
|
||||
result = response['success']
|
||||
elif filename and filedata:
|
||||
log.info('Login success, adding torrent')
|
||||
files = {'file': (filename, filedata)}
|
||||
response = self._req(self.download_url, args = args, files = files)
|
||||
log.info('Response: %s', response)
|
||||
result = response['success']
|
||||
else:
|
||||
log.error('Invalid use of SynologyRPC.create_task: either url or filename+filedata must be specified')
|
||||
self._logout()
|
||||
else:
|
||||
log.error('Couldn\'t login to Synology, %s', login)
|
||||
return response
|
||||
|
||||
|
||||
return result
|
||||
|
||||
@@ -22,14 +22,22 @@ def addEvent(name, handler, priority = 100):
|
||||
def createHandle(*args, **kwargs):
|
||||
|
||||
try:
|
||||
parent = handler.im_self
|
||||
bc = hasattr(parent, 'beforeCall')
|
||||
if bc: parent.beforeCall(handler)
|
||||
# Open handler
|
||||
has_parent = hasattr(handler, 'im_self')
|
||||
if has_parent:
|
||||
parent = handler.im_self
|
||||
bc = hasattr(parent, 'beforeCall')
|
||||
if bc: parent.beforeCall(handler)
|
||||
|
||||
# Main event
|
||||
h = runHandler(name, handler, *args, **kwargs)
|
||||
ac = hasattr(parent, 'afterCall')
|
||||
if ac: parent.afterCall(handler)
|
||||
|
||||
# Close handler
|
||||
if has_parent:
|
||||
ac = hasattr(parent, 'afterCall')
|
||||
if ac: parent.afterCall(handler)
|
||||
except:
|
||||
h = runHandler(name, handler, *args, **kwargs)
|
||||
log.error('Failed creating handler %s %s: %s', (name, handler, traceback.format_exc()))
|
||||
|
||||
return h
|
||||
|
||||
@@ -43,7 +51,7 @@ def removeEvent(name, handler):
|
||||
e -= handler
|
||||
|
||||
def fireEvent(name, *args, **kwargs):
|
||||
if not events.get(name): return
|
||||
if not events.has_key(name): return
|
||||
|
||||
e = Event(name = name, threads = 10, asynch = kwargs.get('async', False), exc_info = True, traceback = True, lock = threading.RLock())
|
||||
|
||||
@@ -133,8 +141,6 @@ def fireEvent(name, *args, **kwargs):
|
||||
options['on_complete']()
|
||||
|
||||
return results
|
||||
except KeyError, e:
|
||||
pass
|
||||
except Exception:
|
||||
log.error('%s: %s', (name, traceback.format_exc()))
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import natcmp
|
||||
from flask.globals import current_app
|
||||
from flask.helpers import json, make_response
|
||||
from urllib import unquote
|
||||
from werkzeug.urls import url_decode
|
||||
import flask
|
||||
import re
|
||||
|
||||
def getParams():
|
||||
|
||||
params = url_decode(getattr(flask.request, 'environ').get('QUERY_STRING', ''))
|
||||
def getParams(params):
|
||||
|
||||
reg = re.compile('^[a-z0-9_\.]+$')
|
||||
|
||||
current = temp = {}
|
||||
@@ -36,6 +32,8 @@ def getParams():
|
||||
current = current[item]
|
||||
else:
|
||||
temp[param] = toUnicode(unquote(value))
|
||||
if temp[param].lower() in ['true', 'false']:
|
||||
temp[param] = temp[param].lower() != 'false'
|
||||
|
||||
return dictToList(temp)
|
||||
|
||||
@@ -54,29 +52,3 @@ def dictToList(params):
|
||||
new = params
|
||||
|
||||
return new
|
||||
|
||||
def getParam(attr, default = None):
|
||||
try:
|
||||
return getParams().get(attr, default)
|
||||
except:
|
||||
return default
|
||||
|
||||
def padded_jsonify(callback, *args, **kwargs):
|
||||
content = str(callback) + '(' + json.dumps(dict(*args, **kwargs)) + ')'
|
||||
return getattr(current_app, 'response_class')(content, mimetype = 'text/javascript')
|
||||
|
||||
def jsonify(mimetype, *args, **kwargs):
|
||||
content = json.dumps(dict(*args, **kwargs))
|
||||
return getattr(current_app, 'response_class')(content, mimetype = mimetype)
|
||||
|
||||
def jsonified(*args, **kwargs):
|
||||
callback = getParam('callback_func', None)
|
||||
if callback:
|
||||
content = padded_jsonify(callback, *args, **kwargs)
|
||||
else:
|
||||
content = jsonify('application/json', *args, **kwargs)
|
||||
|
||||
response = make_response(content)
|
||||
response.cache_control.no_cache = True
|
||||
|
||||
return response
|
||||
|
||||
@@ -181,5 +181,6 @@ def possibleTitles(raw_title):
|
||||
def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
|
||||
return ''.join(random.choice(chars) for x in range(size))
|
||||
|
||||
def splitString(str, split_on = ','):
|
||||
return [x.strip() for x in str.split(split_on)] if str else []
|
||||
def splitString(str, split_on = ',', clean = True):
|
||||
list = [x.strip() for x in str.split(split_on)] if str else []
|
||||
return filter(None, list) if clean else list
|
||||
|
||||
@@ -4,7 +4,7 @@ import re
|
||||
class CPLog(object):
|
||||
|
||||
context = ''
|
||||
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key']
|
||||
replace_private = ['api', 'apikey', 'api_key', 'password', 'username', 'h', 'uid', 'key', 'passkey']
|
||||
|
||||
def __init__(self, context = ''):
|
||||
if context.endswith('.main'):
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.base import Provider
|
||||
from couchpotato.environment import Env
|
||||
@@ -50,7 +49,7 @@ class Notification(Provider):
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
pass
|
||||
|
||||
def test(self):
|
||||
def test(self, **kwargs):
|
||||
|
||||
test_type = self.testNotifyName()
|
||||
|
||||
@@ -62,7 +61,9 @@ class Notification(Provider):
|
||||
listener = 'test'
|
||||
)
|
||||
|
||||
return jsonified({'success': success})
|
||||
return {
|
||||
'success': success
|
||||
}
|
||||
|
||||
def testNotifyName(self):
|
||||
return 'notify.%s.test' % self.getName().lower()
|
||||
|
||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView, addNonBlockApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
@@ -11,6 +10,7 @@ from couchpotato.environment import Env
|
||||
from sqlalchemy.sql.expression import or_
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
import uuid
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -62,11 +62,9 @@ class CoreNotifier(Notification):
|
||||
db.commit()
|
||||
|
||||
|
||||
def markAsRead(self):
|
||||
def markAsRead(self, ids = None, **kwargs):
|
||||
|
||||
ids = None
|
||||
if getParam('ids'):
|
||||
ids = splitString(getParam('ids'))
|
||||
ids = splitString(ids) if ids else None
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -79,14 +77,13 @@ class CoreNotifier(Notification):
|
||||
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def listView(self):
|
||||
def listView(self, limit_offset = None, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
limit_offset = getParam('limit_offset', None)
|
||||
|
||||
q = db.query(Notif)
|
||||
|
||||
@@ -105,11 +102,11 @@ class CoreNotifier(Notification):
|
||||
ndict['type'] = 'notification'
|
||||
notifications.append(ndict)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(notifications) == 0,
|
||||
'notifications': notifications
|
||||
})
|
||||
}
|
||||
|
||||
def checkMessages(self):
|
||||
|
||||
@@ -150,6 +147,8 @@ class CoreNotifier(Notification):
|
||||
|
||||
def frontend(self, type = 'notification', data = {}, message = None):
|
||||
|
||||
log.debug('Notifying frontend')
|
||||
|
||||
self.m_lock.acquire()
|
||||
notification = {
|
||||
'message_id': str(uuid.uuid4()),
|
||||
@@ -168,11 +167,13 @@ class CoreNotifier(Notification):
|
||||
'result': [notification],
|
||||
})
|
||||
except:
|
||||
break
|
||||
log.debug('Failed sending to listener: %s', traceback.format_exc())
|
||||
|
||||
self.m_lock.release()
|
||||
self.cleanMessages()
|
||||
|
||||
log.debug('Done notifying frontend')
|
||||
|
||||
def addListener(self, callback, last_id = None):
|
||||
|
||||
if last_id:
|
||||
@@ -194,9 +195,11 @@ class CoreNotifier(Notification):
|
||||
if listener == callback:
|
||||
self.listeners.remove(list_tuple)
|
||||
except:
|
||||
pass
|
||||
log.debug('Failed removing listener: %s', traceback.format_exc())
|
||||
|
||||
def cleanMessages(self):
|
||||
|
||||
log.debug('Cleaning messages')
|
||||
self.m_lock.acquire()
|
||||
|
||||
for message in self.messages:
|
||||
@@ -204,8 +207,11 @@ class CoreNotifier(Notification):
|
||||
self.messages.remove(message)
|
||||
|
||||
self.m_lock.release()
|
||||
log.debug('Done cleaning messages')
|
||||
|
||||
def getMessages(self, last_id):
|
||||
|
||||
log.debug('Getting messages with id: %s', last_id)
|
||||
self.m_lock.acquire()
|
||||
|
||||
recent = []
|
||||
@@ -216,15 +222,16 @@ class CoreNotifier(Notification):
|
||||
recent = self.messages[index:]
|
||||
|
||||
self.m_lock.release()
|
||||
log.debug('Returning for %s %s messages', (last_id, len(recent or [])))
|
||||
|
||||
return recent or []
|
||||
|
||||
def listener(self):
|
||||
def listener(self, init = False, **kwargs):
|
||||
|
||||
messages = []
|
||||
|
||||
# Get unread
|
||||
if getParam('init'):
|
||||
if init:
|
||||
db = get_session()
|
||||
|
||||
notifications = db.query(Notif) \
|
||||
@@ -235,7 +242,7 @@ class CoreNotifier(Notification):
|
||||
ndict['type'] = 'notification'
|
||||
messages.append(ndict)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'result': messages,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.request import getParams, jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import re
|
||||
@@ -22,10 +21,7 @@ class NMJ(Notification):
|
||||
addApiView(self.testNotifyName(), self.test)
|
||||
addApiView('notify.nmj.auto_config', self.autoConfig)
|
||||
|
||||
def autoConfig(self):
|
||||
|
||||
params = getParams()
|
||||
host = params.get('host', 'localhost')
|
||||
def autoConfig(self, host = 'localhost', **kwargs):
|
||||
|
||||
database = ''
|
||||
mount = ''
|
||||
@@ -63,11 +59,11 @@ class NMJ(Notification):
|
||||
log.error('Detected a network share on the Popcorn Hour, but could not get the mounting url')
|
||||
return self.failed()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'database': database,
|
||||
'mount': mount,
|
||||
})
|
||||
}
|
||||
|
||||
def addToLibrary(self, message = None, group = {}):
|
||||
if self.isDisabled(): return
|
||||
@@ -113,9 +109,13 @@ class NMJ(Notification):
|
||||
return True
|
||||
|
||||
def failed(self):
|
||||
return jsonified({'success': False})
|
||||
return {
|
||||
'success': False
|
||||
}
|
||||
|
||||
def test(self):
|
||||
return jsonified({'success': self.addToLibrary()})
|
||||
def test(self, **kwargs):
|
||||
return {
|
||||
'success': self.addToLibrary()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from flask.helpers import json
|
||||
import base64
|
||||
import json
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
@@ -73,7 +72,7 @@ class Plex(Notification):
|
||||
log.info('Plex notification to %s successful.', host)
|
||||
return True
|
||||
|
||||
def test(self):
|
||||
def test(self, **kwargs):
|
||||
|
||||
test_type = self.testNotifyName()
|
||||
|
||||
@@ -86,4 +85,6 @@ class Plex(Notification):
|
||||
)
|
||||
success2 = self.addToLibrary()
|
||||
|
||||
return jsonified({'success': success or success2})
|
||||
return {
|
||||
'success': success or success2
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import os
|
||||
@@ -32,5 +31,7 @@ class Synoindex(Notification):
|
||||
|
||||
return True
|
||||
|
||||
def test(self):
|
||||
return jsonified({'success': os.path.isfile(self.index_path)})
|
||||
def test(self, **kwargs):
|
||||
return {
|
||||
'success': os.path.isfile(self.index_path)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from flask.helpers import url_for
|
||||
from couchpotato.environment import Env
|
||||
from pytwitter import Api, parse_qsl
|
||||
from werkzeug.utils import redirect
|
||||
import oauth2
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -70,10 +68,9 @@ class Twitter(Notification):
|
||||
|
||||
return True
|
||||
|
||||
def getAuthorizationUrl(self):
|
||||
def getAuthorizationUrl(self, host = None, **kwargs):
|
||||
|
||||
referer = getParam('host')
|
||||
callback_url = cleanHost(referer) + '%snotify.%s.credentials/' % (url_for('api.index').lstrip('/'), self.getName().lower())
|
||||
callback_url = cleanHost(host) + '%snotify.%s.credentials/' % (Env.get('api_base').lstrip('/'), self.getName().lower())
|
||||
|
||||
oauth_consumer = oauth2.Consumer(self.consumer_key, self.consumer_secret)
|
||||
oauth_client = oauth2.Client(oauth_consumer)
|
||||
@@ -82,31 +79,29 @@ class Twitter(Notification):
|
||||
|
||||
if resp['status'] != '200':
|
||||
log.error('Invalid response from Twitter requesting temp token: %s', resp['status'])
|
||||
return jsonified({
|
||||
return {
|
||||
'success': False,
|
||||
})
|
||||
}
|
||||
else:
|
||||
self.request_token = dict(parse_qsl(content))
|
||||
|
||||
auth_url = self.urls['authorize'] + ("?oauth_token=%s" % self.request_token['oauth_token'])
|
||||
|
||||
log.info('Redirecting to "%s"', auth_url)
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'url': auth_url,
|
||||
})
|
||||
}
|
||||
|
||||
def getCredentials(self):
|
||||
|
||||
key = getParam('oauth_verifier')
|
||||
def getCredentials(self, oauth_verifier, **kwargs):
|
||||
|
||||
token = oauth2.Token(self.request_token['oauth_token'], self.request_token['oauth_token_secret'])
|
||||
token.set_verifier(key)
|
||||
token.set_verifier(oauth_verifier)
|
||||
|
||||
oauth_consumer = oauth2.Consumer(key = self.consumer_key, secret = self.consumer_secret)
|
||||
oauth_client = oauth2.Client(oauth_consumer, token)
|
||||
|
||||
resp, content = oauth_client.request(self.urls['access'], method = 'POST', body = 'oauth_verifier=%s' % key)
|
||||
resp, content = oauth_client.request(self.urls['access'], method = 'POST', body = 'oauth_verifier=%s' % oauth_verifier)
|
||||
access_token = dict(parse_qsl(content))
|
||||
|
||||
if resp['status'] != '200':
|
||||
@@ -121,4 +116,4 @@ class Twitter(Notification):
|
||||
|
||||
self.request_token = None
|
||||
|
||||
return redirect(url_for('web.index') + 'settings/notifications/')
|
||||
return 'redirect', Env.get('web_base') + 'settings/notifications/'
|
||||
|
||||
@@ -31,6 +31,13 @@ config = [{
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'only_first',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Only update the first host when movie snatched, useful for synced XBMC',
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
'default': 0,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from couchpotato.core.helpers.variable import splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from flask.helpers import json
|
||||
from urllib2 import URLError
|
||||
import base64
|
||||
import json
|
||||
import socket
|
||||
import traceback
|
||||
import urllib
|
||||
|
||||
@@ -20,19 +22,30 @@ class XBMC(Notification):
|
||||
hosts = splitString(self.conf('host'))
|
||||
|
||||
successful = 0
|
||||
max_successful = 0
|
||||
for host in hosts:
|
||||
|
||||
if self.use_json_notifications.get(host) is None:
|
||||
self.getXBMCJSONversion(host, message = message)
|
||||
|
||||
if self.use_json_notifications.get(host):
|
||||
response = self.request(host, [
|
||||
calls = [
|
||||
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}),
|
||||
('VideoLibrary.Scan', {}),
|
||||
])
|
||||
]
|
||||
|
||||
if not self.conf('only_first') or hosts.index(host) == 0:
|
||||
calls.append(('VideoLibrary.Scan', {}))
|
||||
|
||||
max_successful += len(calls)
|
||||
response = self.request(host, calls)
|
||||
else:
|
||||
response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
|
||||
response += self.request(host, [('VideoLibrary.Scan', {})])
|
||||
|
||||
if not self.conf('only_first') or hosts.index(host) == 0:
|
||||
response += self.request(host, [('VideoLibrary.Scan', {})])
|
||||
max_successful += 1
|
||||
|
||||
max_successful += 1
|
||||
|
||||
try:
|
||||
for result in response:
|
||||
@@ -44,7 +57,7 @@ class XBMC(Notification):
|
||||
except:
|
||||
log.error('Failed parsing results: %s', traceback.format_exc())
|
||||
|
||||
return successful == len(hosts) * 2
|
||||
return successful == max_successful
|
||||
|
||||
def getXBMCJSONversion(self, host, message = ''):
|
||||
|
||||
@@ -53,7 +66,7 @@ class XBMC(Notification):
|
||||
# XBMC JSON-RPC version request
|
||||
response = self.request(host, [
|
||||
('JSONRPC.Version', {})
|
||||
])
|
||||
])
|
||||
for result in response:
|
||||
if (result.get('result') and type(result['result']['version']).__name__ == 'int'):
|
||||
# only v2 and v4 return an int object
|
||||
@@ -138,7 +151,7 @@ class XBMC(Notification):
|
||||
# <li>Error:<message>
|
||||
# </html>
|
||||
#
|
||||
response = self.urlopen(server, headers = headers)
|
||||
response = self.urlopen(server, headers = headers, timeout = 3, show_error = False)
|
||||
|
||||
if 'OK' in response:
|
||||
log.debug('Returned from non-JSON-type request %s: %s', (host, response))
|
||||
@@ -149,6 +162,13 @@ class XBMC(Notification):
|
||||
# manually fake expected response array
|
||||
return [{'result': 'Error'}]
|
||||
|
||||
except URLError, e:
|
||||
if isinstance(e.reason, socket.timeout):
|
||||
log.info('Couldn\'t send request to XBMC, assuming it\'s turned off')
|
||||
return [{'result': 'Error'}]
|
||||
else:
|
||||
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
|
||||
return [{'result': 'Error'}]
|
||||
except:
|
||||
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
|
||||
return [{'result': 'Error'}]
|
||||
@@ -177,11 +197,17 @@ class XBMC(Notification):
|
||||
|
||||
try:
|
||||
log.debug('Sending request to %s: %s', (host, data))
|
||||
rdata = self.urlopen(server, headers = headers, params = data, multipart = True)
|
||||
response = json.loads(rdata)
|
||||
response = self.getJsonData(server, headers = headers, params = data, timeout = 3, show_error = False)
|
||||
log.debug('Returned from request %s: %s', (host, response))
|
||||
|
||||
return response
|
||||
except URLError, e:
|
||||
if isinstance(e.reason, socket.timeout):
|
||||
log.info('Couldn\'t send request to XBMC, assuming it\'s turned off')
|
||||
return []
|
||||
else:
|
||||
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
|
||||
return []
|
||||
except:
|
||||
log.error('Failed sending request to XBMC: %s', traceback.format_exc())
|
||||
return []
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
from StringIO import StringIO
|
||||
from couchpotato import addView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode, ss, toSafeString, \
|
||||
toUnicode
|
||||
from couchpotato.core.helpers.variable import getExt, md5
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from flask.templating import render_template_string
|
||||
from multipartpost import MultipartPostHandler
|
||||
from tornado import template
|
||||
from tornado.web import StaticFileHandler
|
||||
from urlparse import urlparse
|
||||
import cookielib
|
||||
import glob
|
||||
@@ -37,6 +38,7 @@ class Plugin(object):
|
||||
def registerPlugin(self):
|
||||
addEvent('app.do_shutdown', self.doShutdown)
|
||||
addEvent('plugin.running', self.isRunning)
|
||||
self._running = []
|
||||
|
||||
def conf(self, attr, value = None, default = None):
|
||||
return Env.setting(attr, self.getName().lower(), value = value, default = default)
|
||||
@@ -44,35 +46,37 @@ class Plugin(object):
|
||||
def getName(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
def renderTemplate(self, parent_file, template, **params):
|
||||
def renderTemplate(self, parent_file, templ, **params):
|
||||
|
||||
template = open(os.path.join(os.path.dirname(parent_file), template), 'r').read()
|
||||
return render_template_string(template, **params)
|
||||
t = template.Template(open(os.path.join(os.path.dirname(parent_file), templ), 'r').read())
|
||||
return t.generate(**params)
|
||||
|
||||
def registerStatic(self, plugin_file, add_to_head = True):
|
||||
|
||||
# Register plugin path
|
||||
self.plugin_path = os.path.dirname(plugin_file)
|
||||
static_folder = toUnicode(os.path.join(self.plugin_path, 'static'))
|
||||
|
||||
if not os.path.isdir(static_folder):
|
||||
return
|
||||
|
||||
# Get plugin_name from PluginName
|
||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', self.__class__.__name__)
|
||||
class_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
||||
|
||||
# View path
|
||||
path = 'api/%s/static/%s/' % (Env.setting('api_key'), class_name)
|
||||
addView(path + '<path:filename>', self.showStatic, static = True)
|
||||
|
||||
# Add handler to Tornado
|
||||
Env.get('app').add_handlers(".*$", [(Env.get('web_base') + path + '(.*)', StaticFileHandler, {'path': static_folder})])
|
||||
|
||||
# Register for HTML <HEAD>
|
||||
if add_to_head:
|
||||
for f in glob.glob(os.path.join(self.plugin_path, 'static', '*')):
|
||||
ext = getExt(f)
|
||||
if ext in ['js', 'css']:
|
||||
fireEvent('register_%s' % ('script' if ext in 'js' else 'style'), path + os.path.basename(f), f)
|
||||
|
||||
def showStatic(self, filename):
|
||||
d = os.path.join(self.plugin_path, 'static')
|
||||
|
||||
from flask.helpers import send_from_directory
|
||||
return send_from_directory(d, filename)
|
||||
|
||||
def createFile(self, path, content, binary = False):
|
||||
path = ss(path)
|
||||
|
||||
@@ -106,12 +110,14 @@ class Plugin(object):
|
||||
|
||||
# Fill in some headers
|
||||
parsed_url = urlparse(url)
|
||||
host = parsed_url.hostname
|
||||
host = '%s%s' % (parsed_url.hostname, (':' + str(parsed_url.port) if parsed_url.port else ''))
|
||||
|
||||
headers['Referer'] = headers.get('Referer', '%s://%s' % (parsed_url.scheme, host))
|
||||
headers['Host'] = headers.get('Host', host)
|
||||
headers['User-Agent'] = headers.get('User-Agent', self.user_agent)
|
||||
headers['Accept-encoding'] = headers.get('Accept-encoding', 'gzip')
|
||||
headers['Connection'] = headers.get('Connection', 'keep-alive')
|
||||
headers['Cache-Control'] = headers.get('Cache-Control', 'max-age=0')
|
||||
|
||||
# Don't try for failed requests
|
||||
if self.http_failed_disabled.get(host, 0) > 0:
|
||||
@@ -128,6 +134,10 @@ class Plugin(object):
|
||||
self.wait(host)
|
||||
try:
|
||||
|
||||
# Make sure opener has the correct headers
|
||||
if opener:
|
||||
opener.add_headers = headers
|
||||
|
||||
if multipart:
|
||||
log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
|
||||
request = urllib2.Request(url, params, headers)
|
||||
@@ -141,7 +151,12 @@ class Plugin(object):
|
||||
response = opener.open(request, timeout = timeout)
|
||||
else:
|
||||
log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()] if isinstance(params, dict) else 'with data'))
|
||||
data = tryUrlencode(params) if len(params) > 0 else None
|
||||
|
||||
if isinstance(params, (str, unicode)) and len(params) > 0:
|
||||
data = params
|
||||
else:
|
||||
data = tryUrlencode(params) if len(params) > 0 else None
|
||||
|
||||
request = urllib2.Request(url, data, headers)
|
||||
|
||||
if opener:
|
||||
@@ -154,8 +169,10 @@ class Plugin(object):
|
||||
buf = StringIO(response.read())
|
||||
f = gzip.GzipFile(fileobj = buf)
|
||||
data = f.read()
|
||||
f.close()
|
||||
else:
|
||||
data = response.read()
|
||||
response.close()
|
||||
|
||||
self.http_failed_request[host] = 0
|
||||
except IOError:
|
||||
@@ -211,9 +228,6 @@ class Plugin(object):
|
||||
|
||||
def isRunning(self, value = None, boolean = True):
|
||||
|
||||
if not hasattr(self, '_running'):
|
||||
self._running = []
|
||||
|
||||
if value is None:
|
||||
return self._running
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.helpers.request import getParam, jsonified
|
||||
from couchpotato.core.helpers.variable import getUserDir
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
import ctypes
|
||||
@@ -63,16 +62,15 @@ class FileBrowser(Plugin):
|
||||
|
||||
return driveletters
|
||||
|
||||
def view(self):
|
||||
def view(self, path = '/', show_hidden = True, **kwargs):
|
||||
|
||||
path = getParam('path', '/')
|
||||
home = getUserDir()
|
||||
|
||||
if not path:
|
||||
path = home
|
||||
|
||||
try:
|
||||
dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True))
|
||||
dirs = self.getDirectories(path = path, show_hidden = show_hidden)
|
||||
except:
|
||||
dirs = []
|
||||
|
||||
@@ -82,14 +80,14 @@ class FileBrowser(Plugin):
|
||||
elif parent != '/' and parent[-2:] != ':\\':
|
||||
parent += os.path.sep
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'is_root': path == '/',
|
||||
'empty': len(dirs) == 0,
|
||||
'parent': parent,
|
||||
'home': home + os.path.sep,
|
||||
'platform': os.name,
|
||||
'dirs': dirs,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
def is_hidden(self, filepath):
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.request import jsonified, getParams
|
||||
from couchpotato.core.helpers.variable import splitString, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Movie
|
||||
from sqlalchemy.orm import joinedload_all
|
||||
import random
|
||||
import random as rndm
|
||||
import time
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -16,41 +15,10 @@ log = CPLog(__name__)
|
||||
class Dashboard(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
addApiView('dashboard.suggestions', self.suggestView)
|
||||
addApiView('dashboard.soon', self.getSoonView)
|
||||
|
||||
def newSuggestions(self):
|
||||
def getSoonView(self, limit_offset = None, random = False, late = False, **kwargs):
|
||||
|
||||
movies = fireEvent('movie.list', status = ['active', 'done'], limit_offset = (20, 0), single = True)
|
||||
movie_identifiers = [m['library']['identifier'] for m in movies[1]]
|
||||
|
||||
ignored_movies = fireEvent('movie.list', status = ['ignored', 'deleted'], limit_offset = (100, 0), single = True)
|
||||
ignored_identifiers = [m['library']['identifier'] for m in ignored_movies[1]]
|
||||
|
||||
suggestions = fireEvent('movie.suggest', movies = movie_identifiers, ignore = ignored_identifiers, single = True)
|
||||
suggest_status = fireEvent('status.get', 'suggest', single = True)
|
||||
|
||||
for suggestion in suggestions:
|
||||
fireEvent('movie.add', params = {'identifier': suggestion}, force_readd = False, search_after = False, status_id = suggest_status.get('id'))
|
||||
|
||||
def suggestView(self):
|
||||
|
||||
db = get_session()
|
||||
|
||||
movies = db.query(Movie).limit(20).all()
|
||||
identifiers = [m.library.identifier for m in movies]
|
||||
|
||||
suggestions = fireEvent('movie.suggest', movies = identifiers, single = True)
|
||||
|
||||
return jsonified({
|
||||
'result': True,
|
||||
'suggestions': suggestions
|
||||
})
|
||||
|
||||
def getSoonView(self):
|
||||
|
||||
params = getParams()
|
||||
db = get_session()
|
||||
now = time.time()
|
||||
|
||||
@@ -85,7 +53,6 @@ class Dashboard(Plugin):
|
||||
.options(joinedload_all('files'))
|
||||
|
||||
# Add limit
|
||||
limit_offset = params.get('limit_offset')
|
||||
limit = 12
|
||||
if limit_offset:
|
||||
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
|
||||
@@ -93,8 +60,8 @@ class Dashboard(Plugin):
|
||||
|
||||
all_movies = q.all()
|
||||
|
||||
if params.get('random', False):
|
||||
random.shuffle(all_movies)
|
||||
if random:
|
||||
rndm.shuffle(all_movies)
|
||||
|
||||
movies = []
|
||||
for movie in all_movies:
|
||||
@@ -103,9 +70,9 @@ class Dashboard(Plugin):
|
||||
coming_soon = False
|
||||
|
||||
# Theater quality
|
||||
if pp.get('theater') and fireEvent('searcher.could_be_released', True, eta, single = True):
|
||||
if pp.get('theater') and fireEvent('searcher.could_be_released', True, eta, movie.library.year, single = True):
|
||||
coming_soon = True
|
||||
if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, single = True):
|
||||
if pp.get('dvd') and fireEvent('searcher.could_be_released', False, eta, movie.library.year, single = True):
|
||||
coming_soon = True
|
||||
|
||||
# Skip if movie is snatched/downloaded/available
|
||||
@@ -126,18 +93,18 @@ class Dashboard(Plugin):
|
||||
})
|
||||
|
||||
# Don't list older movies
|
||||
if ((not params.get('late') and (not eta.get('dvd') or (eta.get('dvd') and eta.get('dvd') > (now - 2419200)))) or \
|
||||
(params.get('late') and eta.get('dvd') and eta.get('dvd') < (now - 2419200))):
|
||||
if ((not late and ((not eta.get('dvd') and not eta.get('theater')) or (eta.get('dvd') and eta.get('dvd') > (now - 2419200)))) or \
|
||||
(late and (eta.get('dvd', 0) > 0 or eta.get('theater')) and eta.get('dvd') < (now - 2419200))):
|
||||
movies.append(temp)
|
||||
|
||||
if len(movies) >= limit:
|
||||
break
|
||||
|
||||
db.expire_all()
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'movies': movies,
|
||||
})
|
||||
}
|
||||
|
||||
getLateView = getSoonView
|
||||
|
||||
@@ -2,15 +2,13 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.helpers.variable import md5, getExt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.plugins.scanner.main import Scanner
|
||||
from couchpotato.core.settings.model import FileType, File
|
||||
from couchpotato.environment import Env
|
||||
from flask.helpers import send_file
|
||||
from werkzeug.exceptions import NotFound
|
||||
from tornado.web import StaticFileHandler
|
||||
import os.path
|
||||
import time
|
||||
import traceback
|
||||
@@ -25,7 +23,7 @@ class FileManager(Plugin):
|
||||
addEvent('file.download', self.download)
|
||||
addEvent('file.types', self.getTypes)
|
||||
|
||||
addApiView('file.cache/<path:filename>', self.showCacheFile, static = True, docs = {
|
||||
addApiView('file.cache/(.*)', self.showCacheFile, static = True, docs = {
|
||||
'desc': 'Return a file from the cp_data/cache directory',
|
||||
'params': {
|
||||
'filename': {'desc': 'path/filename of the wanted file'}
|
||||
@@ -81,15 +79,9 @@ class FileManager(Plugin):
|
||||
except:
|
||||
log.error('Failed removing unused file: %s', traceback.format_exc())
|
||||
|
||||
def showCacheFile(self, filename = ''):
|
||||
def showCacheFile(self, route, **kwargs):
|
||||
Env.get('app').add_handlers(".*$", [('%s%s' % (Env.get('api_base'), route), StaticFileHandler, {'path': Env.get('cache_dir')})])
|
||||
|
||||
file_path = os.path.join(Env.get('cache_dir'), os.path.basename(filename))
|
||||
|
||||
if not os.path.isfile(file_path):
|
||||
log.error('File "%s" not found', file_path)
|
||||
raise NotFound()
|
||||
|
||||
return send_file(file_path, conditional = True)
|
||||
|
||||
def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}):
|
||||
|
||||
@@ -158,8 +150,8 @@ class FileManager(Plugin):
|
||||
|
||||
return types
|
||||
|
||||
def getTypesView(self):
|
||||
def getTypesView(self, **kwargs):
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'types': self.getTypes()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||
from couchpotato.core.helpers.variable import mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Library, LibraryTitle, File
|
||||
@@ -32,7 +31,8 @@ class LibraryPlugin(Plugin):
|
||||
identifier = attrs.get('identifier'),
|
||||
plot = toUnicode(attrs.get('plot')),
|
||||
tagline = toUnicode(attrs.get('tagline')),
|
||||
status_id = status.get('id')
|
||||
status_id = status.get('id'),
|
||||
info = {},
|
||||
)
|
||||
|
||||
title = LibraryTitle(
|
||||
@@ -87,7 +87,7 @@ class LibraryPlugin(Plugin):
|
||||
library.tagline = toUnicode(info.get('tagline', ''))
|
||||
library.year = info.get('year', 0)
|
||||
library.status_id = done_status.get('id')
|
||||
library.info = info
|
||||
library.info.update(info)
|
||||
db.commit()
|
||||
|
||||
# Titles
|
||||
@@ -148,7 +148,7 @@ class LibraryPlugin(Plugin):
|
||||
|
||||
if dates and dates.get('expires', 0) < time.time() or not dates:
|
||||
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
|
||||
library.info = mergeDicts(library.info, {'release_date': dates })
|
||||
library.info.update({'release_date': dates })
|
||||
db.commit()
|
||||
|
||||
db.expire_all()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified, getParam, getParams
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -47,9 +46,9 @@ class Logging(Plugin):
|
||||
}
|
||||
})
|
||||
|
||||
def get(self):
|
||||
def get(self, nr = 0, **kwargs):
|
||||
|
||||
nr = int(getParam('nr', 0))
|
||||
nr = tryInt(nr)
|
||||
current_path = None
|
||||
|
||||
total = 1
|
||||
@@ -71,16 +70,15 @@ class Logging(Plugin):
|
||||
f = open(current_path, 'r')
|
||||
log = f.read()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'log': toUnicode(log),
|
||||
'total': total,
|
||||
})
|
||||
}
|
||||
|
||||
def partial(self):
|
||||
def partial(self, type = 'all', lines = 30, **kwargs):
|
||||
|
||||
log_type = getParam('type', 'all')
|
||||
total_lines = tryInt(getParam('lines', 30))
|
||||
total_lines = tryInt(lines)
|
||||
|
||||
log_lines = []
|
||||
|
||||
@@ -100,7 +98,7 @@ class Logging(Plugin):
|
||||
brk = False
|
||||
for line in reversed_lines:
|
||||
|
||||
if log_type == 'all' or '%s ' % log_type.upper() in line:
|
||||
if type == 'all' or '%s ' % type.upper() in line:
|
||||
log_lines.append(line)
|
||||
|
||||
if len(log_lines) >= total_lines:
|
||||
@@ -111,12 +109,12 @@ class Logging(Plugin):
|
||||
break
|
||||
|
||||
log_lines.reverse()
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'log': '[0m\n'.join(log_lines),
|
||||
})
|
||||
}
|
||||
|
||||
def clear(self):
|
||||
def clear(self, **kwargs):
|
||||
|
||||
for x in range(0, 50):
|
||||
path = '%s%s' % (Env.get('log_path'), '.%s' % x if x > 0 else '')
|
||||
@@ -135,24 +133,21 @@ class Logging(Plugin):
|
||||
except:
|
||||
log.error('Couldn\'t delete file "%s": %s', (path, traceback.format_exc()))
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def log(self):
|
||||
|
||||
params = getParams()
|
||||
def log(self, type = 'error', **kwargs):
|
||||
|
||||
try:
|
||||
log_message = 'API log: %s' % params
|
||||
log_message = 'API log: %s' % kwargs
|
||||
try:
|
||||
getattr(log, params.get('type', 'error'))(log_message)
|
||||
getattr(log, type)(log_message)
|
||||
except:
|
||||
log.error(log_message)
|
||||
except:
|
||||
log.error('Couldn\'t log via API: %s', params)
|
||||
log.error('Couldn\'t log via API: %s', kwargs)
|
||||
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import splitString, getTitle
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -46,24 +45,23 @@ class Manage(Plugin):
|
||||
})
|
||||
|
||||
if not Env.get('dev'):
|
||||
def updateLibrary():
|
||||
self.updateLibrary(full = False)
|
||||
addEvent('app.load', updateLibrary)
|
||||
addEvent('app.load', self.updateLibraryQuick)
|
||||
|
||||
def getProgress(self):
|
||||
return jsonified({
|
||||
def getProgress(self, **kwargs):
|
||||
return {
|
||||
'progress': self.in_progress
|
||||
})
|
||||
}
|
||||
|
||||
def updateLibraryView(self):
|
||||
def updateLibraryView(self, full = 1, **kwargs):
|
||||
|
||||
full = getParam('full', default = 1)
|
||||
fireEventAsync('manage.update', full = True if full == '1' else False)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def updateLibraryQuick(self):
|
||||
return self.updateLibrary(full = False)
|
||||
|
||||
def updateLibrary(self, full = True):
|
||||
last_update = float(Env.prop('manage.last_update', default = 0))
|
||||
|
||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode, simplifyString
|
||||
from couchpotato.core.helpers.request import getParams, jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import getImdb, splitString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -125,15 +124,14 @@ class MoviePlugin(Plugin):
|
||||
|
||||
db.expire_all()
|
||||
|
||||
def getView(self):
|
||||
def getView(self, id = None, **kwargs):
|
||||
|
||||
movie_id = getParam('id')
|
||||
movie = self.get(movie_id) if movie_id else None
|
||||
movie = self.get(id) if id else None
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': movie is not None,
|
||||
'movie': movie,
|
||||
})
|
||||
}
|
||||
|
||||
def get(self, movie_id):
|
||||
|
||||
@@ -263,15 +261,14 @@ class MoviePlugin(Plugin):
|
||||
db.expire_all()
|
||||
return ''.join(sorted(chars, key = str.lower))
|
||||
|
||||
def listView(self):
|
||||
def listView(self, **kwargs):
|
||||
|
||||
params = getParams()
|
||||
status = splitString(params.get('status', None))
|
||||
release_status = splitString(params.get('release_status', None))
|
||||
limit_offset = params.get('limit_offset', None)
|
||||
starts_with = params.get('starts_with', None)
|
||||
search = params.get('search', None)
|
||||
order = params.get('order', None)
|
||||
status = splitString(kwargs.get('status', None))
|
||||
release_status = splitString(kwargs.get('release_status', None))
|
||||
limit_offset = kwargs.get('limit_offset', None)
|
||||
starts_with = kwargs.get('starts_with', None)
|
||||
search = kwargs.get('search', None)
|
||||
order = kwargs.get('order', None)
|
||||
|
||||
total_movies, movies = self.list(
|
||||
status = status,
|
||||
@@ -282,32 +279,31 @@ class MoviePlugin(Plugin):
|
||||
order = order
|
||||
)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'total': total_movies,
|
||||
'movies': movies,
|
||||
})
|
||||
}
|
||||
|
||||
def charView(self):
|
||||
def charView(self, **kwargs):
|
||||
|
||||
params = getParams()
|
||||
status = splitString(params.get('status', None))
|
||||
release_status = splitString(params.get('release_status', None))
|
||||
status = splitString(kwargs.get('status', None))
|
||||
release_status = splitString(kwargs.get('release_status', None))
|
||||
chars = self.availableChars(status, release_status)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(chars) == 0,
|
||||
'chars': chars,
|
||||
})
|
||||
}
|
||||
|
||||
def refresh(self):
|
||||
def refresh(self, id = '', **kwargs):
|
||||
|
||||
db = get_session()
|
||||
|
||||
for id in splitString(getParam('id')):
|
||||
movie = db.query(Movie).filter_by(id = id).first()
|
||||
for x in splitString(id):
|
||||
movie = db.query(Movie).filter_by(id = x).first()
|
||||
|
||||
if movie:
|
||||
|
||||
@@ -316,17 +312,16 @@ class MoviePlugin(Plugin):
|
||||
for title in movie.library.titles:
|
||||
if title.default: default_title = title.title
|
||||
|
||||
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True)
|
||||
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id))
|
||||
fireEvent('notify.frontend', type = 'movie.busy.%s' % x, data = True)
|
||||
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x))
|
||||
|
||||
db.expire_all()
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
})
|
||||
}
|
||||
|
||||
def search(self):
|
||||
def search(self, q = '', **kwargs):
|
||||
|
||||
q = getParam('q')
|
||||
cache_key = u'%s/%s' % (__name__, simplifyString(q))
|
||||
movies = Env.get('cache').get(cache_key)
|
||||
|
||||
@@ -338,11 +333,11 @@ class MoviePlugin(Plugin):
|
||||
movies = fireEvent('movie.search', q = q, merge = True)
|
||||
Env.get('cache').set(cache_key, movies)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0 if movies else 0,
|
||||
'movies': movies,
|
||||
})
|
||||
}
|
||||
|
||||
def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None):
|
||||
|
||||
@@ -432,33 +427,30 @@ class MoviePlugin(Plugin):
|
||||
return movie_dict
|
||||
|
||||
|
||||
def addView(self):
|
||||
def addView(self, **kwargs):
|
||||
|
||||
params = getParams()
|
||||
movie_dict = self.add(params = kwargs)
|
||||
|
||||
movie_dict = self.add(params)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'added': True if movie_dict else False,
|
||||
'movie': movie_dict,
|
||||
})
|
||||
}
|
||||
|
||||
def edit(self):
|
||||
def edit(self, id = '', **kwargs):
|
||||
|
||||
params = getParams()
|
||||
db = get_session()
|
||||
|
||||
available_status = fireEvent('status.get', 'available', single = True)
|
||||
|
||||
ids = splitString(params.get('id'))
|
||||
ids = splitString(id)
|
||||
for movie_id in ids:
|
||||
|
||||
m = db.query(Movie).filter_by(id = movie_id).first()
|
||||
if not m:
|
||||
continue
|
||||
|
||||
m.profile_id = params.get('profile_id')
|
||||
m.profile_id = kwargs.get('profile_id')
|
||||
|
||||
# Remove releases
|
||||
for rel in m.releases:
|
||||
@@ -467,9 +459,9 @@ class MoviePlugin(Plugin):
|
||||
db.commit()
|
||||
|
||||
# Default title
|
||||
if params.get('default_title'):
|
||||
if kwargs.get('default_title'):
|
||||
for title in m.library.titles:
|
||||
title.default = toUnicode(params.get('default_title', '')).lower() == toUnicode(title.title).lower()
|
||||
title.default = toUnicode(kwargs.get('default_title', '')).lower() == toUnicode(title.title).lower()
|
||||
|
||||
db.commit()
|
||||
|
||||
@@ -479,21 +471,19 @@ class MoviePlugin(Plugin):
|
||||
fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
|
||||
|
||||
db.expire_all()
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
})
|
||||
}
|
||||
|
||||
def deleteView(self):
|
||||
def deleteView(self, id = '', **kwargs):
|
||||
|
||||
params = getParams()
|
||||
|
||||
ids = splitString(params.get('id'))
|
||||
ids = splitString(id)
|
||||
for movie_id in ids:
|
||||
self.delete(movie_id, delete_from = params.get('delete_from', 'all'))
|
||||
self.delete(movie_id, delete_from = kwargs.get('delete_from', 'all'))
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
})
|
||||
}
|
||||
|
||||
def delete(self, movie_id, delete_from = None):
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
var MovieAction = new Class({
|
||||
|
||||
Implements: [Options],
|
||||
|
||||
class_name: 'action icon2',
|
||||
|
||||
initialize: function(movie){
|
||||
initialize: function(movie, options){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.movie = movie;
|
||||
|
||||
self.create();
|
||||
@@ -21,6 +25,32 @@ var MovieAction = new Class({
|
||||
this.el.removeClass('disable')
|
||||
},
|
||||
|
||||
getTitle: function(){
|
||||
var self = this;
|
||||
|
||||
try {
|
||||
return self.movie.getTitle();
|
||||
}
|
||||
catch(e){
|
||||
try {
|
||||
return self.movie.original_title ? self.movie.original_title : self.movie.titles[0];
|
||||
}
|
||||
catch(e){
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
get: function(key){
|
||||
var self = this;
|
||||
try {
|
||||
return self.movie.get(key)
|
||||
}
|
||||
catch(e){
|
||||
return self.movie[key]
|
||||
}
|
||||
},
|
||||
|
||||
createMask: function(){
|
||||
var self = this;
|
||||
self.mask = new Element('div.mask', {
|
||||
@@ -62,10 +92,10 @@ MA.IMDB = new Class({
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.id = self.movie.get('identifier');
|
||||
self.id = self.movie.get('imdb') || self.movie.get('identifier');
|
||||
|
||||
self.el = new Element('a.imdb', {
|
||||
'title': 'Go to the IMDB page of ' + self.movie.getTitle(),
|
||||
'title': 'Go to the IMDB page of ' + self.getTitle(),
|
||||
'href': 'http://www.imdb.com/title/'+self.id+'/',
|
||||
'target': '_blank'
|
||||
});
|
||||
@@ -83,7 +113,7 @@ MA.Release = new Class({
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.releases.download', {
|
||||
'title': 'Show the releases that are available for ' + self.movie.getTitle(),
|
||||
'title': 'Show the releases that are available for ' + self.getTitle(),
|
||||
'events': {
|
||||
'click': self.show.bind(self)
|
||||
}
|
||||
@@ -136,7 +166,7 @@ MA.Release = new Class({
|
||||
}
|
||||
|
||||
// Create release
|
||||
new Element('div', {
|
||||
var item = new Element('div', {
|
||||
'class': 'item '+status.identifier,
|
||||
'id': 'release_'+release.id
|
||||
}).adopt(
|
||||
@@ -165,11 +195,12 @@ MA.Release = new Class({
|
||||
'click': function(e){
|
||||
(e).preventDefault();
|
||||
self.ignore(release);
|
||||
this.getParent('.item').toggleClass('ignored')
|
||||
}
|
||||
}
|
||||
})
|
||||
).inject(self.release_container)
|
||||
).inject(self.release_container);
|
||||
|
||||
release['el'] = item;
|
||||
|
||||
if(status.identifier == 'ignored' || status.identifier == 'failed' || status.identifier == 'snatched'){
|
||||
if(!self.last_release || (self.last_release && self.last_release.status.identifier != 'snatched' && status.identifier == 'snatched'))
|
||||
@@ -189,7 +220,7 @@ MA.Release = new Class({
|
||||
}
|
||||
|
||||
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){
|
||||
|
||||
|
||||
self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top');
|
||||
|
||||
self.trynext_container.adopt(
|
||||
@@ -318,6 +349,17 @@ MA.Release = new Class({
|
||||
Api.request('release.ignore', {
|
||||
'data': {
|
||||
'id': release.id
|
||||
},
|
||||
'onComplete': function(){
|
||||
var el = release.el;
|
||||
if(el.hasClass('failed') || el.hasClass('ignored')){
|
||||
el.removeClass('failed').removeClass('ignored');
|
||||
el.getElement('.release_status').set('text', 'available');
|
||||
}
|
||||
else {
|
||||
el.addClass('ignored');
|
||||
el.getElement('.release_status').set('text', 'ignored');
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -355,7 +397,7 @@ MA.Trailer = new Class({
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.trailer', {
|
||||
'title': 'Watch the trailer of ' + self.movie.getTitle(),
|
||||
'title': 'Watch the trailer of ' + self.getTitle(),
|
||||
'events': {
|
||||
'click': self.watch.bind(self)
|
||||
}
|
||||
@@ -368,12 +410,12 @@ MA.Trailer = new Class({
|
||||
|
||||
var data_url = 'http://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'
|
||||
var url = data_url.substitute({
|
||||
'title': encodeURI(self.movie.getTitle()),
|
||||
'year': self.movie.get('year'),
|
||||
'title': encodeURI(self.getTitle()),
|
||||
'year': self.get('year'),
|
||||
'offset': offset || 1
|
||||
}),
|
||||
size = $(self.movie).getSize(),
|
||||
height = (size.x/16)*9,
|
||||
height = self.options.height || (size.x/16)*9,
|
||||
id = 'trailer-'+randomString();
|
||||
|
||||
self.player_container = new Element('div[id='+id+']');
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
.movies > div {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
|
||||
.movies > div .message {
|
||||
display: block;
|
||||
padding: 20px;
|
||||
@@ -20,11 +20,11 @@
|
||||
padding: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list > div:not(.description) {
|
||||
margin-right: -4px;
|
||||
}
|
||||
|
||||
|
||||
.movies .loading {
|
||||
display: block;
|
||||
padding: 20px 0 0 0;
|
||||
@@ -43,26 +43,26 @@
|
||||
margin-top: -20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.movies .loading .spinner {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
.movies .loading .message {
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.movies h2 {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.movies h2 {
|
||||
font-size: 25px;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.movies > .description {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
@@ -73,17 +73,17 @@
|
||||
.movies:hover > .description {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 860px) {
|
||||
.movies > .description {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list {
|
||||
padding: 20px 0 20px;
|
||||
}
|
||||
|
||||
|
||||
.home .movies {
|
||||
padding-top: 6px;
|
||||
}
|
||||
@@ -99,27 +99,27 @@
|
||||
transition-property: width, height;
|
||||
background: rgba(0,0,0,.2);
|
||||
}
|
||||
|
||||
|
||||
.movies.mass_edit_list .movie {
|
||||
padding-left: 22px;
|
||||
background: none;
|
||||
}
|
||||
|
||||
|
||||
.movies.details_list .movie {
|
||||
padding-left: 120px;
|
||||
}
|
||||
|
||||
|
||||
.movies.list_list .movie:not(.details_view),
|
||||
.movies.mass_edit_list .movie {
|
||||
height: 30px;
|
||||
border-bottom: 1px solid rgba(255,255,255,.15);
|
||||
}
|
||||
|
||||
|
||||
.movies.list_list .movie:last-child,
|
||||
.movies.mass_edit_list .movie:last-child {
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list .movie {
|
||||
width: 16.66667%;
|
||||
height: auto;
|
||||
@@ -128,7 +128,7 @@
|
||||
padding: 0;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
.movies.thumbs_list .movie {
|
||||
width: 25%;
|
||||
@@ -165,7 +165,7 @@
|
||||
.movies.mass_edit_list .movie .data {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list .data {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@@ -176,7 +176,7 @@
|
||||
background: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list .movie:hover .data {
|
||||
background: rgba(0,0,0,0.9);
|
||||
}
|
||||
@@ -218,7 +218,7 @@
|
||||
.movies.mass_edit_list .poster {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list .poster {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -243,7 +243,7 @@
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
.movies .info {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
@@ -272,7 +272,7 @@
|
||||
display: inline-block;
|
||||
padding-right: 55px;
|
||||
}
|
||||
|
||||
|
||||
.movies .info .title span {
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
@@ -282,14 +282,14 @@
|
||||
height: 100%;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list .info .title span {
|
||||
white-space: normal;
|
||||
overflow: auto;
|
||||
height: auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.movies.thumbs_list .movie .info .title span,
|
||||
.movies.thumbs_list .movie .info .year {
|
||||
@@ -298,21 +298,21 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.movies.list_list .movie:not(.details_view) .info .title,
|
||||
.movies.mass_edit_list .info .title {
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list .movie:not(.no_thumbnail) .info {
|
||||
display: none;
|
||||
}
|
||||
.movies.thumbs_list .movie:hover .info {
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list .info .title {
|
||||
font-size: 21px;
|
||||
word-wrap: break-word;
|
||||
@@ -334,7 +334,7 @@
|
||||
font-size: 1.25em;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
|
||||
.movies.thumbs_list .info .year {
|
||||
font-size: 23px;
|
||||
margin: 0;
|
||||
@@ -344,7 +344,7 @@
|
||||
right: auto;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
|
||||
.touch_enabled .movies.list_list .movie .info .year {
|
||||
font-size: 1em;
|
||||
}
|
||||
@@ -371,24 +371,24 @@
|
||||
display: block;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
|
||||
.movies.list_list .movie:hover .data .quality {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
.touch_enabled .movies.list_list .movie .data .quality {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
top: -4px;
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.movies .data .quality {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.movies .status_suggest .data .quality,
|
||||
.movies.thumbs_list .data .quality {
|
||||
display: none;
|
||||
@@ -417,13 +417,13 @@
|
||||
z-index: 1;
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
|
||||
.movies .data .quality .available,
|
||||
.movies .data .quality .snatched {
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.movies .data .quality .available { background-color: #578bc3; }
|
||||
.movies .data .quality .snatched { background-color: #369545; }
|
||||
.movies .data .quality .done {
|
||||
@@ -454,18 +454,18 @@
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.movies .movie:hover .data .actions,
|
||||
.touch_enabled .movies .movie .data .actions {
|
||||
opacity: 1;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
||||
.movies.details_list .data .actions {
|
||||
top: auto;
|
||||
bottom: 18px;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie:hover .actions {
|
||||
opacity: 1;
|
||||
display: inline-block;
|
||||
@@ -475,7 +475,7 @@
|
||||
right: 10px;
|
||||
top: auto;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie:hover .action { opacity: 0.6; }
|
||||
.movies .movie:hover .action:hover { opacity: 1; }
|
||||
|
||||
@@ -497,7 +497,7 @@
|
||||
.movies .data .action.delete { color: #e9b0b0; }
|
||||
.movies .data .action.directory { color: #ffed92; }
|
||||
.movies .data .action.readd { color: #c2fac5; }
|
||||
|
||||
|
||||
.movies.mass_edit_list .movie .data .actions {
|
||||
display: none;
|
||||
}
|
||||
@@ -558,13 +558,15 @@
|
||||
.movies .options .table .item {
|
||||
border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.movies .options .table .item.ignored span {
|
||||
.movies .options .table .item.ignored span,
|
||||
.movies .options .table .item.failed span {
|
||||
text-decoration: line-through;
|
||||
color: rgba(255,255,255,0.4);
|
||||
}
|
||||
.movies .options .table .item.ignored .delete:before {
|
||||
display: inline-block;
|
||||
content: "\e04b";
|
||||
.movies .options .table .item.ignored .delete:before,
|
||||
.movies .options .table .item.failed .delete:before {
|
||||
display: inline-block;
|
||||
content: "\e04b";
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
|
||||
@@ -616,7 +618,8 @@
|
||||
.movies .options .table a:hover { opacity: 1; }
|
||||
.movies .options .table a.download { color: #a7fbaf; }
|
||||
.movies .options .table a.delete { color: #fda3a3; }
|
||||
.movies .options .table .ignored a.delete { color: #b5fda3; }
|
||||
.movies .options .table .ignored a.delete,
|
||||
.movies .options .table .failed a.delete { color: #b5fda3; }
|
||||
|
||||
.movies .options .table .head > * {
|
||||
font-weight: bold;
|
||||
@@ -626,7 +629,7 @@
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.movies .movie .trailer_container {
|
||||
.trailer_container {
|
||||
width: 100%;
|
||||
background: #000;
|
||||
text-align: center;
|
||||
@@ -636,11 +639,11 @@
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
.movies .movie .trailer_container.hide {
|
||||
.trailer_container.hide {
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
.movies .movie .hide_trailer {
|
||||
.hide_trailer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
@@ -652,7 +655,7 @@
|
||||
transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s;
|
||||
z-index: 11;
|
||||
}
|
||||
.movies .movie .hide_trailer.hide {
|
||||
.hide_trailer.hide {
|
||||
top: -30px;
|
||||
}
|
||||
|
||||
@@ -695,7 +698,7 @@
|
||||
.touch_enabled .movies .movie .trynext {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
@media all and (max-width: 480px) {
|
||||
.movies .movie .trynext {
|
||||
display: none;
|
||||
@@ -709,14 +712,14 @@
|
||||
.touch_enabled .movies.details_list .movie .trynext {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
.movies.details_list .movie .trynext {
|
||||
background: #47515f;
|
||||
padding: 0;
|
||||
right: 0;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
|
||||
.movies .movie .trynext a {
|
||||
background-position: 5px center;
|
||||
padding: 0 5px 0 25px;
|
||||
@@ -761,7 +764,7 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.movies .alph_nav .menus {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
@@ -785,7 +788,7 @@
|
||||
border-right: 1px solid rgba(255,255,255,.07);
|
||||
}
|
||||
|
||||
.movies .alph_nav .numbers li,
|
||||
.movies .alph_nav .numbers li,
|
||||
.movies .alph_nav .actions li {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
@@ -795,7 +798,7 @@
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
.movies .alph_nav .numbers li {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
@@ -804,14 +807,14 @@
|
||||
.movies .alph_nav .numbers li.letter_all {
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
|
||||
.movies .alph_nav li.available {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
|
||||
}
|
||||
.movies .alph_nav li.active.available,
|
||||
.movies .alph_nav li.active.available,
|
||||
.movies .alph_nav li.available:hover {
|
||||
background: rgba(0,0,0,.1);
|
||||
}
|
||||
@@ -832,17 +835,17 @@
|
||||
.movies .alph_nav .search input:focus {
|
||||
background: rgba(0,0,0,.08);
|
||||
}
|
||||
|
||||
|
||||
.movies .alph_nav .search input::-webkit-input-placeholder {
|
||||
color: #444;
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
|
||||
.movies .alph_nav .search:before {
|
||||
font-family: 'Elusive-Icons';
|
||||
content: "\e03e";
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
height: 20px;
|
||||
line-height: 45px;
|
||||
font-size: 12px;
|
||||
margin: 0 0 0 10px;
|
||||
@@ -960,10 +963,10 @@
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
|
||||
.movies .alph_nav .more_menu.filter {
|
||||
}
|
||||
|
||||
|
||||
.movies .alph_nav .more_menu.filter > a:before {
|
||||
content: "\e0e8";
|
||||
font-family: 'Elusive-Icons';
|
||||
@@ -971,7 +974,7 @@
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.movies .alph_nav .more_menu.filter .wrapper {
|
||||
right: 88px;
|
||||
width: 300px;
|
||||
|
||||
@@ -193,7 +193,7 @@
|
||||
transition: all .4s cubic-bezier(0.9,0,0.1,1);
|
||||
}
|
||||
.movie_result .data.open {
|
||||
left: 100%;
|
||||
left: 100% !important;
|
||||
}
|
||||
|
||||
.movie_result:last-child .data { border-bottom: 0; }
|
||||
|
||||
@@ -185,8 +185,11 @@ Block.Search = new Class({
|
||||
|
||||
Block.Search.Item = new Class({
|
||||
|
||||
Implements: [Options, Events],
|
||||
|
||||
initialize: function(info, options){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.info = info;
|
||||
self.alternative_titles = [];
|
||||
@@ -208,17 +211,13 @@ Block.Search.Item = new Class({
|
||||
}) : null,
|
||||
self.options_el = new Element('div.options.inlay'),
|
||||
self.data_container = new Element('div.data', {
|
||||
'tween': {
|
||||
duration: 400,
|
||||
transition: 'quint:in:out'
|
||||
},
|
||||
'events': {
|
||||
'click': self.showOptions.bind(self)
|
||||
}
|
||||
}).adopt(
|
||||
new Element('div.info').adopt(
|
||||
self.title = new Element('h2', {
|
||||
'text': info.titles[0]
|
||||
'text': info.titles && info.titles.length > 0 ? info.titles[0] : 'Unknown'
|
||||
}).adopt(
|
||||
self.year = info.year ? new Element('span.year', {
|
||||
'text': info.year
|
||||
@@ -228,12 +227,12 @@ Block.Search.Item = new Class({
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
info.titles.each(function(title){
|
||||
self.alternativeTitle({
|
||||
'title': title
|
||||
});
|
||||
})
|
||||
if(info.titles)
|
||||
info.titles.each(function(title){
|
||||
self.alternativeTitle({
|
||||
'title': title
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
alternativeTitle: function(alternative){
|
||||
@@ -242,6 +241,20 @@ Block.Search.Item = new Class({
|
||||
self.alternative_titles.include(alternative);
|
||||
},
|
||||
|
||||
getTitle: function(){
|
||||
var self = this;
|
||||
try {
|
||||
return self.info.original_title ? self.info.original_title : self.info.titles[0];
|
||||
}
|
||||
catch(e){
|
||||
return 'Unknown';
|
||||
}
|
||||
},
|
||||
|
||||
get: function(key){
|
||||
return this.info[key]
|
||||
},
|
||||
|
||||
showOptions: function(){
|
||||
var self = this;
|
||||
|
||||
@@ -279,6 +292,8 @@ Block.Search.Item = new Class({
|
||||
})
|
||||
);
|
||||
self.mask.fade('out');
|
||||
|
||||
self.fireEvent('added');
|
||||
},
|
||||
'onFailure': function(){
|
||||
self.options_el.empty();
|
||||
|
||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified, getParams, getParam
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Profile, ProfileType, Movie
|
||||
@@ -46,12 +45,12 @@ class ProfilePlugin(Plugin):
|
||||
movie.profile_id = default_profile.get('id')
|
||||
db.commit()
|
||||
|
||||
def allView(self):
|
||||
def allView(self, **kwargs):
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'list': self.all()
|
||||
})
|
||||
}
|
||||
|
||||
def all(self):
|
||||
|
||||
@@ -65,30 +64,28 @@ class ProfilePlugin(Plugin):
|
||||
db.expire_all()
|
||||
return temp
|
||||
|
||||
def save(self):
|
||||
|
||||
params = getParams()
|
||||
def save(self, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
|
||||
p = db.query(Profile).filter_by(id = params.get('id')).first()
|
||||
p = db.query(Profile).filter_by(id = kwargs.get('id')).first()
|
||||
if not p:
|
||||
p = Profile()
|
||||
db.add(p)
|
||||
|
||||
p.label = toUnicode(params.get('label'))
|
||||
p.order = params.get('order', p.order if p.order else 0)
|
||||
p.core = params.get('core', False)
|
||||
p.label = toUnicode(kwargs.get('label'))
|
||||
p.order = kwargs.get('order', p.order if p.order else 0)
|
||||
p.core = kwargs.get('core', False)
|
||||
|
||||
#delete old types
|
||||
[db.delete(t) for t in p.types]
|
||||
|
||||
order = 0
|
||||
for type in params.get('types', []):
|
||||
for type in kwargs.get('types', []):
|
||||
t = ProfileType(
|
||||
order = order,
|
||||
finish = type.get('finish') if order > 0 else 1,
|
||||
wait_for = params.get('wait_for'),
|
||||
wait_for = kwargs.get('wait_for'),
|
||||
quality_id = type.get('quality_id')
|
||||
)
|
||||
p.types.append(t)
|
||||
@@ -99,10 +96,10 @@ class ProfilePlugin(Plugin):
|
||||
|
||||
profile_dict = p.to_dict(self.to_dict)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'profile': profile_dict
|
||||
})
|
||||
}
|
||||
|
||||
def default(self):
|
||||
|
||||
@@ -113,28 +110,25 @@ class ProfilePlugin(Plugin):
|
||||
db.expire_all()
|
||||
return default_dict
|
||||
|
||||
def saveOrder(self):
|
||||
def saveOrder(self, **kwargs):
|
||||
|
||||
params = getParams()
|
||||
db = get_session()
|
||||
|
||||
order = 0
|
||||
for profile in params.get('ids', []):
|
||||
for profile in kwargs.get('ids', []):
|
||||
p = db.query(Profile).filter_by(id = profile).first()
|
||||
p.hide = params.get('hidden')[order]
|
||||
p.hide = kwargs.get('hidden')[order]
|
||||
p.order = order
|
||||
|
||||
order += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def delete(self):
|
||||
|
||||
id = getParam('id')
|
||||
def delete(self, id = None, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
|
||||
@@ -154,10 +148,10 @@ class ProfilePlugin(Plugin):
|
||||
message = log.error('Failed deleting Profile: %s', e)
|
||||
|
||||
db.expire_all()
|
||||
return jsonified({
|
||||
return {
|
||||
'success': success,
|
||||
'message': message
|
||||
})
|
||||
}
|
||||
|
||||
def fill(self):
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified, getParams
|
||||
from couchpotato.core.helpers.variable import mergeDicts, md5, getExt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
@@ -18,8 +17,8 @@ class QualityPlugin(Plugin):
|
||||
|
||||
qualities = [
|
||||
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
|
||||
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
|
||||
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
|
||||
{'identifier': '1080p', 'hd': True, 'size': (4000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
|
||||
{'identifier': '720p', 'hd': True, 'size': (3000, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
|
||||
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']},
|
||||
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
|
||||
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
|
||||
@@ -51,12 +50,12 @@ class QualityPlugin(Plugin):
|
||||
def preReleases(self):
|
||||
return self.pre_releases
|
||||
|
||||
def allView(self):
|
||||
def allView(self, **kwargs):
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'list': self.all()
|
||||
})
|
||||
}
|
||||
|
||||
def all(self):
|
||||
|
||||
@@ -88,20 +87,18 @@ class QualityPlugin(Plugin):
|
||||
if identifier == q.get('identifier'):
|
||||
return q
|
||||
|
||||
def saveSize(self):
|
||||
|
||||
params = getParams()
|
||||
def saveSize(self, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
quality = db.query(Quality).filter_by(identifier = params.get('identifier')).first()
|
||||
quality = db.query(Quality).filter_by(identifier = kwargs.get('identifier')).first()
|
||||
|
||||
if quality:
|
||||
setattr(quality, params.get('value_type'), params.get('value'))
|
||||
setattr(quality, kwargs.get('value_type'), kwargs.get('value'))
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def fill(self):
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.request import getParam, jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.plugins.scanner.main import Scanner
|
||||
@@ -108,13 +107,11 @@ class Release(Plugin):
|
||||
# Check database and update/insert if necessary
|
||||
return fireEvent('file.add', path = filepath, part = fireEvent('scanner.partnumber', file, single = True), type_tuple = Scanner.file_types.get(type), properties = properties, single = True)
|
||||
|
||||
def deleteView(self):
|
||||
def deleteView(self, id = None, **kwargs):
|
||||
|
||||
release_id = getParam('id')
|
||||
|
||||
return jsonified({
|
||||
'success': self.delete(release_id)
|
||||
})
|
||||
return {
|
||||
'success': self.delete(id)
|
||||
}
|
||||
|
||||
def delete(self, id):
|
||||
|
||||
@@ -146,25 +143,23 @@ class Release(Plugin):
|
||||
|
||||
return False
|
||||
|
||||
def ignore(self):
|
||||
def ignore(self, id = None, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
id = getParam('id')
|
||||
|
||||
rel = db.query(Relea).filter_by(id = id).first()
|
||||
if rel:
|
||||
ignored_status, available_status = fireEvent('status.get', ['ignored', 'available'], single = True)
|
||||
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id')
|
||||
ignored_status, failed_status, available_status = fireEvent('status.get', ['ignored', 'failed', 'available'], single = True)
|
||||
rel.status_id = available_status.get('id') if rel.status_id in [ignored_status.get('id'), failed_status.get('id')] else ignored_status.get('id')
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def download(self):
|
||||
def download(self, id = None, **kwargs):
|
||||
|
||||
db = get_session()
|
||||
id = getParam('id')
|
||||
|
||||
snatched_status, done_status = fireEvent('status.get', ['snatched', 'done'], single = True)
|
||||
|
||||
@@ -199,12 +194,12 @@ class Release(Plugin):
|
||||
|
||||
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name'])
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': success
|
||||
})
|
||||
}
|
||||
else:
|
||||
log.error('Couldn\'t find release with id: %s', id)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': False
|
||||
})
|
||||
}
|
||||
|
||||
@@ -14,10 +14,14 @@ rename_options = {
|
||||
'year': 'Year (2011)',
|
||||
'first': 'First letter (M)',
|
||||
'quality': 'Quality (720p)',
|
||||
'quality_type': '(HD) or (SD)',
|
||||
'video': 'Video (x264)',
|
||||
'audio': 'Audio (DTS)',
|
||||
'group': 'Releasegroup name',
|
||||
'source': 'Source media (Bluray)',
|
||||
'resolution_width': 'resolution width (1280)',
|
||||
'resolution_height': 'resolution height (720)',
|
||||
'audio_channels': 'audio channels (7.1)',
|
||||
'original': 'Original filename',
|
||||
'original_folder': 'Original foldername',
|
||||
'imdb_id': 'IMDB id (tt0123456)',
|
||||
|
||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import toUnicode, ss
|
||||
from couchpotato.core.helpers.request import getParams, jsonified
|
||||
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
|
||||
getImdb, link, symlink, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
@@ -59,13 +58,12 @@ class Renamer(Plugin):
|
||||
|
||||
return True
|
||||
|
||||
def scanView(self):
|
||||
def scanView(self, **kwargs):
|
||||
|
||||
params = getParams()
|
||||
async = tryInt(params.get('async', None))
|
||||
movie_folder = params.get('movie_folder', None)
|
||||
downloader = params.get('downloader', None)
|
||||
download_id = params.get('download_id', None)
|
||||
async = tryInt(kwargs.get('async', None))
|
||||
movie_folder = kwargs.get('movie_folder', None)
|
||||
downloader = kwargs.get('downloader', None)
|
||||
download_id = kwargs.get('download_id', None)
|
||||
|
||||
fire_handle = fireEvent if not async else fireEventAsync
|
||||
|
||||
@@ -74,9 +72,9 @@ class Renamer(Plugin):
|
||||
download_info = {'id': download_id, 'downloader': downloader} if download_id else None
|
||||
)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True
|
||||
})
|
||||
}
|
||||
|
||||
def scan(self, movie_folder = None, download_info = None):
|
||||
|
||||
@@ -183,6 +181,7 @@ class Renamer(Plugin):
|
||||
'source': group['meta_data']['source'],
|
||||
'resolution_width': group['meta_data'].get('resolution_width'),
|
||||
'resolution_height': group['meta_data'].get('resolution_height'),
|
||||
'audio_channels': group['meta_data'].get('audio_channels'),
|
||||
'imdb_id': library['identifier'],
|
||||
'cd': '',
|
||||
'cd_nr': '',
|
||||
@@ -221,15 +220,15 @@ class Renamer(Plugin):
|
||||
replacements['cd_nr'] = cd if multiple else ''
|
||||
|
||||
# Naming
|
||||
final_folder_name = self.doReplace(folder_name, replacements).lstrip('. ')
|
||||
final_file_name = self.doReplace(file_name, replacements).lstrip('. ')
|
||||
final_folder_name = self.doReplace(folder_name, replacements)
|
||||
final_file_name = self.doReplace(file_name, replacements)
|
||||
replacements['filename'] = final_file_name[:-(len(getExt(final_file_name)) + 1)]
|
||||
|
||||
# Meta naming
|
||||
if file_type is 'trailer':
|
||||
final_file_name = self.doReplace(trailer_name, replacements, remove_multiple = True).lstrip('. ')
|
||||
final_file_name = self.doReplace(trailer_name, replacements, remove_multiple = True)
|
||||
elif file_type is 'nfo':
|
||||
final_file_name = self.doReplace(nfo_name, replacements, remove_multiple = True).lstrip('. ')
|
||||
final_file_name = self.doReplace(nfo_name, replacements, remove_multiple = True)
|
||||
|
||||
# Seperator replace
|
||||
if separator:
|
||||
@@ -283,7 +282,7 @@ class Renamer(Plugin):
|
||||
|
||||
# Don't add language if multiple languages in 1 subtitle file
|
||||
if len(sub_langs) == 1:
|
||||
sub_name = final_file_name.replace(replacements['ext'], '%s.%s' % (sub_langs[0], replacements['ext']))
|
||||
sub_name = sub_name.replace(replacements['ext'], '%s.%s' % (sub_langs[0], replacements['ext']))
|
||||
rename_files[current_file] = os.path.join(destination, final_folder_name, sub_name)
|
||||
|
||||
rename_files = mergeDicts(rename_files, rename_extras)
|
||||
@@ -557,7 +556,7 @@ Remove it if you want it to be renamed (again, or at least let it try again)
|
||||
replaced = re.sub(r"[\x00:\*\?\"<>\|]", '', replaced)
|
||||
|
||||
sep = self.conf('separator')
|
||||
return self.replaceDoubles(replaced).replace(' ', ' ' if not sep else sep)
|
||||
return self.replaceDoubles(replaced.lstrip('. ')).replace(' ', ' ' if not sep else sep)
|
||||
|
||||
def replaceDoubles(self, string):
|
||||
return string.replace(' ', ' ').replace(' .', '.')
|
||||
|
||||
@@ -432,6 +432,7 @@ class Scanner(Plugin):
|
||||
data['audio'] = meta.get('audio', self.getCodec(cur_file, self.codecs['audio']))
|
||||
data['resolution_width'] = meta.get('resolution_width', 720)
|
||||
data['resolution_height'] = meta.get('resolution_height', 480)
|
||||
data['audio_channels'] = meta.get('audio_channels', 2.0)
|
||||
data['aspect'] = meta.get('resolution_width', 720) / meta.get('resolution_height', 480)
|
||||
except:
|
||||
log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc()))
|
||||
@@ -476,6 +477,7 @@ class Scanner(Plugin):
|
||||
'audio': ac,
|
||||
'resolution_width': tryInt(p.video[0].width),
|
||||
'resolution_height': tryInt(p.video[0].height),
|
||||
'audio_channels': p.audio[0].channels,
|
||||
}
|
||||
except ParseError:
|
||||
log.debug('Failed to parse meta for %s', filename)
|
||||
@@ -582,7 +584,7 @@ class Scanner(Plugin):
|
||||
movie = fireEvent('movie.by_hash', file = cur_file, merge = True)
|
||||
|
||||
if len(movie) > 0:
|
||||
imdb_id = movie[0]['imdb']
|
||||
imdb_id = movie[0].get('imdb')
|
||||
if imdb_id:
|
||||
log.debug('Found movie via OpenSubtitleHash: %s', cur_file)
|
||||
break
|
||||
@@ -600,7 +602,7 @@ class Scanner(Plugin):
|
||||
movie = fireEvent('movie.search', q = '%(name)s %(year)s' % name_year, merge = True, limit = 1)
|
||||
|
||||
if len(movie) > 0:
|
||||
imdb_id = movie[0]['imdb']
|
||||
imdb_id = movie[0].get('imdb')
|
||||
log.debug('Found movie via search: %s', cur_file)
|
||||
if imdb_id: break
|
||||
else:
|
||||
|
||||
@@ -18,7 +18,7 @@ class Score(Plugin):
|
||||
def calculate(self, nzb, movie):
|
||||
''' Calculate the score of a NZB, used for sorting later '''
|
||||
|
||||
score = nameScore(toUnicode(nzb['name']), movie['library']['year'])
|
||||
score = nameScore(toUnicode(nzb['name'] + ' ' + nzb.get('name_extra', '')), movie['library']['year'])
|
||||
|
||||
for movie_title in movie['library']['titles']:
|
||||
score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title['title']))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.encoding import simplifyString
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.helpers.variable import tryInt, splitString
|
||||
from couchpotato.environment import Env
|
||||
import re
|
||||
|
||||
@@ -42,10 +42,8 @@ def nameScore(name, year):
|
||||
|
||||
# Contains preferred word
|
||||
nzb_words = re.split('\W+', simplifyString(name))
|
||||
preferred_words = [x.strip() for x in Env.setting('preferred_words', section = 'searcher').split(',')]
|
||||
for word in preferred_words:
|
||||
if word.strip() and word.strip().lower() in nzb_words:
|
||||
score = score + 100
|
||||
preferred_words = splitString(Env.setting('preferred_words', section = 'searcher'))
|
||||
score += 100 * len(list(set(nzb_words) & set(preferred_words)))
|
||||
|
||||
return score
|
||||
|
||||
|
||||
@@ -57,6 +57,14 @@ config = [{
|
||||
'advanced': True,
|
||||
'description': 'Cron settings for the searcher see: <a href="http://packages.python.org/APScheduler/cronschedule.html">APScheduler</a> for details.',
|
||||
'options': [
|
||||
{
|
||||
'name': 'run_on_launch',
|
||||
'label': 'Run on launch',
|
||||
'advanced': True,
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'description': 'Force run the searcher after (re)start.',
|
||||
},
|
||||
{
|
||||
'name': 'cron_day',
|
||||
'label': 'Day',
|
||||
|
||||
@@ -2,13 +2,13 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.variable import md5, getTitle, splitString, \
|
||||
possibleTitles
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
|
||||
from couchpotato.environment import Env
|
||||
from datetime import date
|
||||
from inspect import ismethod, isfunction
|
||||
from sqlalchemy.exc import InterfaceError
|
||||
import datetime
|
||||
@@ -50,6 +50,9 @@ class Searcher(Plugin):
|
||||
}"""},
|
||||
})
|
||||
|
||||
if self.conf('run_on_launch'):
|
||||
addEvent('app.load', self.allMovies)
|
||||
|
||||
addEvent('app.load', self.setCrons)
|
||||
addEvent('setting.save.searcher.cron_day.after', self.setCrons)
|
||||
addEvent('setting.save.searcher.cron_hour.after', self.setCrons)
|
||||
@@ -58,7 +61,7 @@ class Searcher(Plugin):
|
||||
def setCrons(self):
|
||||
fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
|
||||
|
||||
def allMoviesView(self):
|
||||
def allMoviesView(self, **kwargs):
|
||||
|
||||
in_progress = self.in_progress
|
||||
if not in_progress:
|
||||
@@ -67,15 +70,15 @@ class Searcher(Plugin):
|
||||
else:
|
||||
fireEvent('notify.frontend', type = 'searcher.already_started', data = True, message = 'Full search already in progress')
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': not in_progress
|
||||
})
|
||||
}
|
||||
|
||||
def getProgress(self):
|
||||
def getProgress(self, **kwargs):
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'progress': self.in_progress
|
||||
})
|
||||
}
|
||||
|
||||
def allMovies(self):
|
||||
|
||||
@@ -146,9 +149,10 @@ class Searcher(Plugin):
|
||||
|
||||
pre_releases = fireEvent('quality.pre_releases', single = True)
|
||||
release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True)
|
||||
available_status, ignored_status = fireEvent('status.get', ['available', 'ignored'], single = True)
|
||||
available_status, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], single = True)
|
||||
|
||||
found_releases = []
|
||||
too_early_to_search = []
|
||||
|
||||
default_title = getTitle(movie['library'])
|
||||
if not default_title:
|
||||
@@ -161,15 +165,15 @@ class Searcher(Plugin):
|
||||
|
||||
ret = False
|
||||
for quality_type in movie['profile']['types']:
|
||||
if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates):
|
||||
log.info('Too early to search for %s, %s', (quality_type['quality']['identifier'], default_title))
|
||||
if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']):
|
||||
too_early_to_search.append(quality_type['quality']['identifier'])
|
||||
continue
|
||||
|
||||
has_better_quality = 0
|
||||
|
||||
# See if better quality is available
|
||||
for release in movie['releases']:
|
||||
if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id')]:
|
||||
if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id'), failed_status.get('id')]:
|
||||
has_better_quality += 1
|
||||
|
||||
# Don't search for quality lower then already available.
|
||||
@@ -240,7 +244,7 @@ class Searcher(Plugin):
|
||||
log.info('Ignored, waiting %s days: %s', (quality_type.get('wait_for'), nzb['name']))
|
||||
continue
|
||||
|
||||
if nzb['status_id'] == ignored_status.get('id'):
|
||||
if nzb['status_id'] in [ignored_status.get('id'), failed_status.get('id')]:
|
||||
log.info('Ignored: %s', nzb['name'])
|
||||
continue
|
||||
|
||||
@@ -269,6 +273,9 @@ class Searcher(Plugin):
|
||||
if self.shuttingDown() or ret:
|
||||
break
|
||||
|
||||
if len(too_early_to_search) > 0:
|
||||
log.info2('Too early to search for %s, %s', (too_early_to_search, default_title))
|
||||
|
||||
fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
|
||||
|
||||
return ret
|
||||
@@ -552,11 +559,12 @@ class Searcher(Plugin):
|
||||
|
||||
return False
|
||||
|
||||
def couldBeReleased(self, is_pre_release, dates):
|
||||
def couldBeReleased(self, is_pre_release, dates, year = None):
|
||||
|
||||
now = int(time.time())
|
||||
now_year = date.today().year
|
||||
|
||||
if not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0):
|
||||
if (year is None or year < now_year - 1) and (not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0)):
|
||||
return True
|
||||
else:
|
||||
|
||||
@@ -586,18 +594,17 @@ class Searcher(Plugin):
|
||||
|
||||
return False
|
||||
|
||||
def tryNextReleaseView(self):
|
||||
def tryNextReleaseView(self, id = None, **kwargs):
|
||||
|
||||
trynext = self.tryNextRelease(getParam('id'))
|
||||
trynext = self.tryNextRelease(id)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': trynext
|
||||
})
|
||||
}
|
||||
|
||||
def tryNextRelease(self, movie_id, manual = False):
|
||||
|
||||
snatched_status = fireEvent('status.get', 'snatched', single = True)
|
||||
ignored_status = fireEvent('status.get', 'ignored', single = True)
|
||||
snatched_status, ignored_status = fireEvent('status.get', ['snatched', 'ignored'], single = True)
|
||||
|
||||
try:
|
||||
db = get_session()
|
||||
|
||||
@@ -2,7 +2,6 @@ from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Status
|
||||
@@ -42,12 +41,12 @@ class StatusPlugin(Plugin):
|
||||
}"""}
|
||||
})
|
||||
|
||||
def list(self):
|
||||
def list(self, **kwargs):
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
'list': self.all()
|
||||
})
|
||||
}
|
||||
|
||||
def getById(self, id):
|
||||
db = get_session()
|
||||
|
||||
@@ -1,22 +1,92 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.helpers.request import jsonified, getParam
|
||||
from couchpotato.core.helpers.encoding import ss
|
||||
from couchpotato.core.helpers.variable import splitString, md5
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Movie
|
||||
from couchpotato.environment import Env
|
||||
from sqlalchemy.sql.expression import or_
|
||||
|
||||
class Suggestion(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
|
||||
addApiView('suggestion.view', self.getView)
|
||||
addApiView('suggestion.view', self.suggestView)
|
||||
addApiView('suggestion.ignore', self.ignoreView)
|
||||
|
||||
def getView(self):
|
||||
def suggestView(self, **kwargs):
|
||||
|
||||
limit_offset = getParam('limit_offset', None)
|
||||
total_movies, movies = fireEvent('movie.list', status = 'suggest', limit_offset = limit_offset, single = True)
|
||||
movies = splitString(kwargs.get('movies', ''))
|
||||
ignored = splitString(kwargs.get('ignored', ''))
|
||||
limit = kwargs.get('limit', 6)
|
||||
|
||||
return jsonified({
|
||||
if not movies or len(movies) == 0:
|
||||
db = get_session()
|
||||
active_movies = db.query(Movie) \
|
||||
.filter(or_(*[Movie.status.has(identifier = s) for s in ['active', 'done']])).all()
|
||||
movies = [x.library.identifier for x in active_movies]
|
||||
|
||||
if not ignored or len(ignored) == 0:
|
||||
ignored = splitString(Env.prop('suggest_ignore', default = ''))
|
||||
|
||||
cached_suggestion = self.getCache('suggestion_cached')
|
||||
if cached_suggestion:
|
||||
suggestions = cached_suggestion
|
||||
else:
|
||||
suggestions = fireEvent('movie.suggest', movies = movies, ignore = ignored, single = True)
|
||||
self.setCache(md5(ss('suggestion_cached')), suggestions, timeout = 6048000) # Cache for 10 weeks
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'empty': len(movies) == 0,
|
||||
'total': total_movies,
|
||||
'movies': movies,
|
||||
})
|
||||
'count': len(suggestions),
|
||||
'suggestions': suggestions[:limit]
|
||||
}
|
||||
|
||||
def ignoreView(self, imdb = None, limit = 6, remove_only = False, **kwargs):
|
||||
|
||||
ignored = splitString(Env.prop('suggest_ignore', default = ''))
|
||||
|
||||
if imdb:
|
||||
if not remove_only:
|
||||
ignored.append(imdb)
|
||||
Env.prop('suggest_ignore', ','.join(set(ignored)))
|
||||
|
||||
new_suggestions = self.updateSuggestionCache(ignore_imdb = imdb, limit = limit, ignored = ignored)
|
||||
|
||||
return {
|
||||
'result': True,
|
||||
'ignore_count': len(ignored),
|
||||
'suggestions': new_suggestions[limit - 1:limit]
|
||||
}
|
||||
|
||||
def updateSuggestionCache(self, ignore_imdb = None, limit = 6, ignored = None):
|
||||
|
||||
# Combine with previous suggestion_cache
|
||||
cached_suggestion = self.getCache('suggestion_cached')
|
||||
new_suggestions = []
|
||||
|
||||
if ignore_imdb:
|
||||
for cs in cached_suggestion:
|
||||
if cs.get('imdb') != ignore_imdb:
|
||||
new_suggestions.append(cs)
|
||||
|
||||
# Get new results and add them
|
||||
if len(new_suggestions) - 1 < limit:
|
||||
|
||||
db = get_session()
|
||||
active_movies = db.query(Movie) \
|
||||
.filter(or_(*[Movie.status.has(identifier = s) for s in ['active', 'done']])).all()
|
||||
movies = [x.library.identifier for x in active_movies]
|
||||
|
||||
if ignored:
|
||||
ignored.extend([x.get('imdb') for x in new_suggestions])
|
||||
|
||||
suggestions = fireEvent('movie.suggest', movies = movies, ignore = list(set(ignored)), single = True)
|
||||
|
||||
if suggestions:
|
||||
new_suggestions.extend(suggestions)
|
||||
|
||||
self.setCache(md5(ss('suggestion_cached')), new_suggestions, timeout = 6048000)
|
||||
|
||||
return new_suggestions
|
||||
|
||||
84
couchpotato/core/plugins/suggestion/static/suggest.css
Normal file
84
couchpotato/core/plugins/suggestion/static/suggest.css
Normal file
@@ -0,0 +1,84 @@
|
||||
.suggestions {
|
||||
}
|
||||
|
||||
.suggestions > h2 {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.suggestions .movie_result {
|
||||
display: inline-block;
|
||||
width: 33.333%;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
@media all and (max-width: 960px) {
|
||||
.suggestions .movie_result {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 600px) {
|
||||
.suggestions .movie_result {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.suggestions .movie_result .data {
|
||||
left: 100px;
|
||||
background: #4e5969;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.suggestions .movie_result .data .info {
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.suggestions .movie_result .data .info h2 {
|
||||
white-space: normal;
|
||||
max-height: 120px;
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.suggestions .movie_result .data .info .year {
|
||||
position: static;
|
||||
display: block;
|
||||
margin: 5px 0 0;
|
||||
padding: 0;
|
||||
opacity: .6;
|
||||
}
|
||||
|
||||
.suggestions .movie_result .data {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.suggestions .movie_result .options {
|
||||
left: 100px;
|
||||
}
|
||||
|
||||
.suggestions .movie_result .thumbnail {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.suggestions .movie_result .actions {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
display: none;
|
||||
width: 120px;
|
||||
}
|
||||
.suggestions .movie_result:hover .actions {
|
||||
display: block;
|
||||
}
|
||||
.suggestions .movie_result .data.open .actions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.suggestions .movie_result .actions a {
|
||||
margin-left: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
||||
102
couchpotato/core/plugins/suggestion/static/suggest.js
Normal file
102
couchpotato/core/plugins/suggestion/static/suggest.js
Normal file
@@ -0,0 +1,102 @@
|
||||
var SuggestList = new Class({
|
||||
|
||||
Implements: [Options, Events],
|
||||
|
||||
initialize: function(options){
|
||||
var self = this;
|
||||
self.setOptions(options);
|
||||
|
||||
self.create();
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('div.suggestions', {
|
||||
'events': {
|
||||
'click:relay(a.delete)': function(e, el){
|
||||
(e).stop();
|
||||
|
||||
$(el).getParent('.movie_result').destroy();
|
||||
|
||||
Api.request('suggestion.ignore', {
|
||||
'data': {
|
||||
'imdb': el.get('data-ignore')
|
||||
},
|
||||
'onComplete': self.fill.bind(self)
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}).grab(
|
||||
new Element('h2', {
|
||||
'text': 'You might like these'
|
||||
})
|
||||
);
|
||||
|
||||
self.api_request = Api.request('suggestion.view', {
|
||||
'onComplete': self.fill.bind(self)
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
fill: function(json){
|
||||
|
||||
var self = this;
|
||||
|
||||
Object.each(json.suggestions, function(movie){
|
||||
|
||||
var m = new Block.Search.Item(movie, {
|
||||
'onAdded': function(){
|
||||
self.afterAdded(m, movie)
|
||||
}
|
||||
});
|
||||
m.data_container.grab(
|
||||
new Element('div.actions').adopt(
|
||||
new Element('a.add.icon2', {
|
||||
'title': 'Add movie with your default quality',
|
||||
'data-add': movie.imdb,
|
||||
'events': {
|
||||
'click': m.showOptions.bind(m)
|
||||
}
|
||||
}),
|
||||
$(new MA.IMDB(m)),
|
||||
$(new MA.Trailer(m, {
|
||||
'height': 150
|
||||
})),
|
||||
new Element('a.delete.icon2', {
|
||||
'title': 'Don\'t suggest this movie again',
|
||||
'data-ignore': movie.imdb
|
||||
})
|
||||
)
|
||||
);
|
||||
m.data_container.removeEvents('click');
|
||||
$(m).inject(self.el);
|
||||
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
afterAdded: function(m, movie){
|
||||
var self = this;
|
||||
|
||||
setTimeout(function(){
|
||||
$(m).destroy();
|
||||
|
||||
Api.request('suggestion.ignore', {
|
||||
'data': {
|
||||
'imdb': movie.imdb,
|
||||
'remove_only': true
|
||||
},
|
||||
'onComplete': self.fill.bind(self)
|
||||
});
|
||||
|
||||
}, 3000);
|
||||
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el;
|
||||
}
|
||||
|
||||
})
|
||||
@@ -1,5 +1,7 @@
|
||||
var includes = {{includes|tojson}};
|
||||
var excludes = {{excludes|tojson}};
|
||||
{% autoescape None %}
|
||||
|
||||
var includes = {{ json_encode(includes) }};
|
||||
var excludes = {{ json_encode(excludes) }};
|
||||
|
||||
var specialChars = '\\{}+.():-|^$';
|
||||
var makeRegex = function(pattern) {
|
||||
@@ -20,6 +22,8 @@ var makeRegex = function(pattern) {
|
||||
|
||||
var isCorrectUrl = function() {
|
||||
for(i in includes) {
|
||||
if(!includes.hasOwnProperty(i)) continue;
|
||||
|
||||
var reg = includes[i]
|
||||
if (makeRegex(reg).test(document.location.href))
|
||||
return true;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
from couchpotato import index
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent, addEvent
|
||||
from couchpotato.core.helpers.request import getParam, jsonified
|
||||
from couchpotato.core.helpers.variable import isDict
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
from flask.globals import request
|
||||
from flask.helpers import url_for
|
||||
from flask.templating import render_template
|
||||
from tornado.web import RequestHandler
|
||||
import os
|
||||
|
||||
log = CPLog(__name__)
|
||||
@@ -18,7 +16,8 @@ class Userscript(Plugin):
|
||||
version = 3
|
||||
|
||||
def __init__(self):
|
||||
addApiView('userscript.get/<random>/<path:filename>', self.getUserScript, static = True)
|
||||
addApiView('userscript.get/(.*)/(.*)', self.getUserScript, static = True)
|
||||
|
||||
addApiView('userscript', self.iFrame)
|
||||
addApiView('userscript.add_via_url', self.getViaUrl)
|
||||
addApiView('userscript.includes', self.getIncludes)
|
||||
@@ -26,38 +25,46 @@ class Userscript(Plugin):
|
||||
|
||||
addEvent('userscript.get_version', self.getVersion)
|
||||
|
||||
def bookmark(self):
|
||||
def bookmark(self, host = None, **kwargs):
|
||||
|
||||
params = {
|
||||
'includes': fireEvent('userscript.get_includes', merge = True),
|
||||
'excludes': fireEvent('userscript.get_excludes', merge = True),
|
||||
'host': getParam('host', None),
|
||||
'host': host,
|
||||
}
|
||||
|
||||
return self.renderTemplate(__file__, 'bookmark.js', **params)
|
||||
|
||||
def getIncludes(self):
|
||||
def getIncludes(self, **kwargs):
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'includes': fireEvent('userscript.get_includes', merge = True),
|
||||
'excludes': fireEvent('userscript.get_excludes', merge = True),
|
||||
})
|
||||
|
||||
def getUserScript(self, random = '', filename = ''):
|
||||
|
||||
params = {
|
||||
'includes': fireEvent('userscript.get_includes', merge = True),
|
||||
'excludes': fireEvent('userscript.get_excludes', merge = True),
|
||||
'version': self.getVersion(),
|
||||
'api': '%suserscript/' % url_for('api.index').lstrip('/'),
|
||||
'host': request.host_url,
|
||||
}
|
||||
|
||||
script = self.renderTemplate(__file__, 'template.js', **params)
|
||||
self.createFile(os.path.join(Env.get('cache_dir'), 'couchpotato.user.js'), script)
|
||||
def getUserScript(self, route, **kwargs):
|
||||
|
||||
klass = self
|
||||
|
||||
class UserscriptHandler(RequestHandler):
|
||||
|
||||
def get(self, random, route):
|
||||
|
||||
params = {
|
||||
'includes': fireEvent('userscript.get_includes', merge = True),
|
||||
'excludes': fireEvent('userscript.get_excludes', merge = True),
|
||||
'version': klass.getVersion(),
|
||||
'api': '%suserscript/' % Env.get('api_base'),
|
||||
'host': '%s://%s' % (self.request.protocol, self.request.host),
|
||||
}
|
||||
|
||||
script = klass.renderTemplate(__file__, 'template.js', **params)
|
||||
klass.createFile(os.path.join(Env.get('cache_dir'), 'couchpotato.user.js'), script)
|
||||
|
||||
self.redirect(Env.get('api_base') + 'file.cache/couchpotato.user.js')
|
||||
|
||||
Env.get('app').add_handlers(".*$", [('%s%s' % (Env.get('api_base'), route), UserscriptHandler)])
|
||||
|
||||
from flask.helpers import send_from_directory
|
||||
return send_from_directory(Env.get('cache_dir'), 'couchpotato.user.js')
|
||||
|
||||
def getVersion(self):
|
||||
|
||||
@@ -69,12 +76,12 @@ class Userscript(Plugin):
|
||||
|
||||
return version
|
||||
|
||||
def iFrame(self):
|
||||
return render_template('index.html', sep = os.sep, fireEvent = fireEvent, env = Env)
|
||||
def iFrame(self, **kwargs):
|
||||
return index()
|
||||
|
||||
def getViaUrl(self):
|
||||
def getViaUrl(self, url = None, **kwargs):
|
||||
|
||||
url = getParam('url')
|
||||
print url
|
||||
|
||||
params = {
|
||||
'url': url,
|
||||
@@ -84,4 +91,4 @@ class Userscript(Plugin):
|
||||
log.error('Failed adding movie via url: %s', url)
|
||||
params['error'] = params['movie'] if params['movie'] else 'Failed getting movie info'
|
||||
|
||||
return jsonified(params)
|
||||
return params
|
||||
|
||||
@@ -9,15 +9,16 @@
|
||||
// @grant none
|
||||
// @version {{version}}
|
||||
|
||||
// @match {{host}}*
|
||||
// @match {{host}}/*
|
||||
{% for include in includes %}
|
||||
// @match {{include}}{% endfor %}
|
||||
// @match {{include}}{% end %}
|
||||
{% for exclude in excludes %}
|
||||
// @exclude {{exclude}}{% endfor %}
|
||||
// @exclude {{exclude}}{% end %}
|
||||
// @exclude {{host}}{{api.rstrip('/')}}*
|
||||
|
||||
// ==/UserScript==
|
||||
|
||||
{% autoescape None %}
|
||||
if (window.top == window.self){ // Only run on top window
|
||||
|
||||
var version = {{version}},
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
from .main import V1Importer
|
||||
|
||||
def start():
|
||||
return V1Importer()
|
||||
|
||||
config = []
|
||||
@@ -1,30 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/main.css') }}" type="text/css">
|
||||
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/uniform.generic.css') }}" type="text/css">
|
||||
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/uniform.css') }}" type="text/css">
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('web.static', filename='scripts/library/mootools.js') }}"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
window.addEvent('domready', function(){
|
||||
if($('old_db'))
|
||||
$('old_db').addEvent('change', function(){
|
||||
$('form').submit();
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
{% if message: %}
|
||||
{{ message }}
|
||||
{% else: %}
|
||||
<form id="form" method="post" enctype="multipart/form-data">
|
||||
<input type="file" name="old_db" id="old_db" />
|
||||
</form>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,56 +0,0 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEventAsync
|
||||
from couchpotato.core.helpers.variable import getImdb
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
from flask.globals import request
|
||||
from flask.helpers import url_for
|
||||
import os
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class V1Importer(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
addApiView('v1.import', self.fromOld, methods = ['GET', 'POST'])
|
||||
|
||||
def fromOld(self):
|
||||
|
||||
if request.method != 'POST':
|
||||
return self.renderTemplate(__file__, 'form.html', url_for = url_for)
|
||||
|
||||
file = request.files['old_db']
|
||||
|
||||
uploaded_file = os.path.join(Env.get('cache_dir'), 'v1_database.db')
|
||||
|
||||
if os.path.isfile(uploaded_file):
|
||||
os.remove(uploaded_file)
|
||||
|
||||
file.save(uploaded_file)
|
||||
|
||||
try:
|
||||
import sqlite3
|
||||
conn = sqlite3.connect(uploaded_file)
|
||||
|
||||
wanted = []
|
||||
|
||||
t = ('want',)
|
||||
cur = conn.execute('SELECT status, imdb FROM Movie WHERE status=?', t)
|
||||
for row in cur:
|
||||
status, imdb = row
|
||||
if getImdb(imdb):
|
||||
wanted.append(imdb)
|
||||
conn.close()
|
||||
|
||||
wanted = set(wanted)
|
||||
for imdb in wanted:
|
||||
fireEventAsync('movie.add', {'identifier': imdb}, search_after = False)
|
||||
|
||||
message = 'Successfully imported %s movie(s)' % len(wanted)
|
||||
except Exception, e:
|
||||
message = 'Failed: %s' % e
|
||||
|
||||
return self.renderTemplate(__file__, 'form.html', url_for = url_for, message = message)
|
||||
|
||||
@@ -13,7 +13,7 @@ class Automation(Provider):
|
||||
enabled_option = 'automation_enabled'
|
||||
http_time_between_calls = 2
|
||||
|
||||
interval = 86400
|
||||
interval = 1800
|
||||
last_checked = 0
|
||||
|
||||
def __init__(self):
|
||||
@@ -51,6 +51,7 @@ class Automation(Provider):
|
||||
|
||||
def isMinimalMovie(self, movie):
|
||||
if not movie.get('rating'):
|
||||
log.info('ignoring %s as no rating is available for.', (movie['original_title']))
|
||||
return False
|
||||
|
||||
if movie['rating'] and movie['rating'].get('imdb'):
|
||||
|
||||
@@ -9,6 +9,8 @@ class Goodfilms(Automation):
|
||||
|
||||
url = 'http://goodfil.ms/%s/queue?page=%d&without_layout=1'
|
||||
|
||||
interval = 1800
|
||||
|
||||
def getIMDBids(self):
|
||||
|
||||
if not self.conf('automation_username'):
|
||||
|
||||
@@ -11,7 +11,7 @@ config = [{
|
||||
'list': 'watchlist_providers',
|
||||
'name': 'imdb_automation',
|
||||
'label': 'IMDB',
|
||||
'description': 'From any <strong>public</strong> IMDB watchlists. Url should be the RSS link.',
|
||||
'description': 'From any <strong>public</strong> IMDB watchlists. Url should be the CSV link.',
|
||||
'options': [
|
||||
{
|
||||
'name': 'automation_enabled',
|
||||
|
||||
@@ -12,6 +12,8 @@ class Letterboxd(Automation):
|
||||
url = 'http://letterboxd.com/%s/watchlist/'
|
||||
pattern = re.compile(r'(.*)\((\d*)\)')
|
||||
|
||||
interval = 1800
|
||||
|
||||
def getIMDBids(self):
|
||||
|
||||
urls = splitString(self.conf('automation_urls'))
|
||||
|
||||
@@ -86,6 +86,7 @@ class YarrProvider(Provider):
|
||||
sizeKb = ['kb', 'kib']
|
||||
|
||||
login_opener = None
|
||||
last_login_check = 0
|
||||
|
||||
def __init__(self):
|
||||
addEvent('provider.enabled_types', self.getEnabledProviderType)
|
||||
@@ -101,17 +102,29 @@ class YarrProvider(Provider):
|
||||
|
||||
def login(self):
|
||||
|
||||
# Check if we are still logged in every hour
|
||||
now = time.time()
|
||||
if self.login_opener and self.last_login_check < (now - 3600):
|
||||
try:
|
||||
output = self.urlopen(self.urls['login_check'], opener = self.login_opener)
|
||||
if self.loginCheckSuccess(output):
|
||||
self.last_login_check = now
|
||||
return True
|
||||
else:
|
||||
self.login_opener = None
|
||||
except:
|
||||
self.login_opener = None
|
||||
|
||||
if self.login_opener:
|
||||
return True
|
||||
|
||||
try:
|
||||
cookiejar = cookielib.CookieJar()
|
||||
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))
|
||||
opener.addheaders = [('User-Agent', self.user_agent)]
|
||||
urllib2.install_opener(opener)
|
||||
log.info2('Logging into %s', self.urls['login'])
|
||||
f = opener.open(self.urls['login'], self.getLoginParams())
|
||||
output = f.read()
|
||||
f.close()
|
||||
output = self.urlopen(self.urls['login'], params = self.getLoginParams(), opener = opener)
|
||||
|
||||
if self.loginSuccess(output):
|
||||
self.last_login_check = now
|
||||
self.login_opener = opener
|
||||
return True
|
||||
|
||||
@@ -119,15 +132,19 @@ class YarrProvider(Provider):
|
||||
except:
|
||||
error = traceback.format_exc()
|
||||
|
||||
self.login_opener = None
|
||||
log.error('Failed to login %s: %s', (self.getName(), error))
|
||||
return False
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return True
|
||||
|
||||
def loginCheckSuccess(self, output):
|
||||
return True
|
||||
|
||||
def loginDownload(self, url = '', nzb_id = ''):
|
||||
try:
|
||||
if not self.login_opener and not self.login():
|
||||
if not self.login():
|
||||
log.error('Failed downloading from %s', self.getName())
|
||||
return self.urlopen(url, opener = self.login_opener)
|
||||
except:
|
||||
@@ -150,7 +167,7 @@ class YarrProvider(Provider):
|
||||
return []
|
||||
|
||||
# Login if needed
|
||||
if self.urls.get('login') and (not self.login_opener and not self.login()):
|
||||
if self.urls.get('login') and not self.login():
|
||||
log.error('Failed to login to: %s', self.getName())
|
||||
return []
|
||||
|
||||
@@ -258,7 +275,7 @@ class ResultList(list):
|
||||
'id': 0,
|
||||
'type': self.provider.type,
|
||||
'provider': self.provider.getName(),
|
||||
'download': self.provider.download,
|
||||
'download': self.provider.loginDownload if self.provider.urls.get('login') else self.provider.download,
|
||||
'url': '',
|
||||
'name': '',
|
||||
'age': 0,
|
||||
|
||||
@@ -28,7 +28,6 @@ class MovieResultModifier(Plugin):
|
||||
'tagline': '',
|
||||
'imdb': '',
|
||||
'genres': [],
|
||||
'release_date': {}
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.request import jsonified, getParams
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.movie.base import MovieProvider
|
||||
from couchpotato.core.settings.model import Movie
|
||||
from couchpotato.environment import Env
|
||||
import time
|
||||
|
||||
@@ -29,7 +26,7 @@ class CouchPotatoApi(MovieProvider):
|
||||
addEvent('movie.info', self.getInfo, priority = 1)
|
||||
addEvent('movie.search', self.search, priority = 1)
|
||||
addEvent('movie.release_date', self.getReleaseDate)
|
||||
addEvent('movie.suggest', self.suggest)
|
||||
addEvent('movie.suggest', self.getSuggestions)
|
||||
addEvent('movie.is_movie', self.isMovie)
|
||||
|
||||
addEvent('cp.source_url', self.getSourceUrl)
|
||||
@@ -50,8 +47,8 @@ class CouchPotatoApi(MovieProvider):
|
||||
'branch': branch,
|
||||
}), headers = self.getRequestHeaders())
|
||||
|
||||
def search(self, q, limit = 12):
|
||||
return self.getJsonData(self.urls['search'] % tryUrlencode(q), headers = self.getRequestHeaders())
|
||||
def search(self, q, limit = 5):
|
||||
return self.getJsonData(self.urls['search'] % tryUrlencode(q) + ('?limit=%s' % limit), headers = self.getRequestHeaders())
|
||||
|
||||
def isMovie(self, identifier = None):
|
||||
|
||||
@@ -83,34 +80,15 @@ class CouchPotatoApi(MovieProvider):
|
||||
|
||||
return dates
|
||||
|
||||
def suggest(self, movies = [], ignore = []):
|
||||
def getSuggestions(self, movies = [], ignore = []):
|
||||
suggestions = self.getJsonData(self.urls['suggest'], params = {
|
||||
'movies': ','.join(movies),
|
||||
#'ignore': ','.join(ignore),
|
||||
})
|
||||
log.info('Found Suggestions for %s', (suggestions))
|
||||
'ignore': ','.join(ignore),
|
||||
}, headers = self.getRequestHeaders())
|
||||
log.info('Found suggestions for %s movies, %s ignored', (len(movies), len(ignore)))
|
||||
|
||||
return suggestions
|
||||
|
||||
def suggestView(self):
|
||||
|
||||
params = getParams()
|
||||
movies = params.get('movies')
|
||||
ignore = params.get('ignore', [])
|
||||
|
||||
if not movies:
|
||||
db = get_session()
|
||||
active_movies = db.query(Movie).filter(Movie.status.has(identifier = 'active')).all()
|
||||
movies = [x.library.identifier for x in active_movies]
|
||||
|
||||
suggestions = self.suggest(movies, ignore)
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'count': len(suggestions),
|
||||
'suggestions': suggestions
|
||||
})
|
||||
|
||||
def getRequestHeaders(self):
|
||||
return {
|
||||
'X-CP-Version': fireEvent('app.version', single = True),
|
||||
|
||||
@@ -2,7 +2,7 @@ from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.movie.base import MovieProvider
|
||||
from libs.themoviedb import tmdb
|
||||
from themoviedb import tmdb
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -16,6 +16,7 @@ class FTDWorld(NZBProvider):
|
||||
'detail': 'http://ftdworld.net/spotinfo.php?id=%s',
|
||||
'download': 'http://ftdworld.net/cgi-bin/nzbdown.pl?fileID=%s',
|
||||
'login': 'http://ftdworld.net/api/login.php',
|
||||
'login_check': 'http://ftdworld.net/api/login.php',
|
||||
}
|
||||
|
||||
http_time_between_calls = 3 #seconds
|
||||
@@ -58,7 +59,6 @@ class FTDWorld(NZBProvider):
|
||||
'age': self.calculateAge(tryInt(item.get('Created'))),
|
||||
'size': item.get('Size', 0),
|
||||
'url': self.urls['download'] % nzb_id,
|
||||
'download': self.loginDownload,
|
||||
'detail_url': self.urls['detail'] % nzb_id,
|
||||
'score': (tryInt(item.get('webPlus', 0)) - tryInt(item.get('webMin', 0))) * 3,
|
||||
})
|
||||
@@ -78,3 +78,6 @@ class FTDWorld(NZBProvider):
|
||||
return json.loads(output).get('goodToGo', False)
|
||||
except:
|
||||
return False
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
|
||||
@@ -53,11 +53,20 @@ class Newznab(NZBProvider, RSS):
|
||||
for nzb in nzbs:
|
||||
|
||||
date = None
|
||||
spotter = None
|
||||
for item in nzb:
|
||||
if date and spotter:
|
||||
break
|
||||
if item.attrib.get('name') == 'usenetdate':
|
||||
date = item.attrib.get('value')
|
||||
break
|
||||
|
||||
# Get the name of the person who posts the spot
|
||||
if item.attrib.get('name') == 'poster':
|
||||
if "@spot.net" in item.attrib.get('value'):
|
||||
spotter = item.attrib.get('value').split("@")[0]
|
||||
continue
|
||||
|
||||
if not date:
|
||||
date = self.getTextElement(nzb, 'pubDate')
|
||||
|
||||
@@ -67,10 +76,15 @@ class Newznab(NZBProvider, RSS):
|
||||
if not name:
|
||||
continue
|
||||
|
||||
name_extra = ''
|
||||
if spotter:
|
||||
name_extra = spotter
|
||||
|
||||
results.append({
|
||||
'id': nzb_id,
|
||||
'provider_extra': urlparse(host['host']).hostname or host['host'],
|
||||
'name': self.getTextElement(nzb, 'title'),
|
||||
'name': name,
|
||||
'name_extra': name_extra,
|
||||
'age': self.calculateAge(int(time.mktime(parse(date).timetuple()))),
|
||||
'size': int(self.getElement(nzb, 'enclosure').attrib['length']) / 1024 / 1024,
|
||||
'url': (self.getUrl(host['host'], self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host),
|
||||
@@ -81,17 +95,24 @@ class Newznab(NZBProvider, RSS):
|
||||
|
||||
def getHosts(self):
|
||||
|
||||
uses = splitString(str(self.conf('use')))
|
||||
hosts = splitString(self.conf('host'))
|
||||
api_keys = splitString(self.conf('api_key'))
|
||||
extra_score = splitString(self.conf('extra_score'))
|
||||
uses = splitString(str(self.conf('use')), clean = False)
|
||||
hosts = splitString(self.conf('host'), clean = False)
|
||||
api_keys = splitString(self.conf('api_key'), clean = False)
|
||||
extra_score = splitString(self.conf('extra_score'), clean = False)
|
||||
|
||||
list = []
|
||||
for nr in range(len(hosts)):
|
||||
|
||||
try: key = api_keys[nr]
|
||||
except: key = ''
|
||||
|
||||
try: host = hosts[nr]
|
||||
except: host = ''
|
||||
|
||||
list.append({
|
||||
'use': uses[nr],
|
||||
'host': hosts[nr],
|
||||
'api_key': api_keys[nr],
|
||||
'host': host,
|
||||
'api_key': key,
|
||||
'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0
|
||||
})
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class NzbIndex(NZBProvider, RSS):
|
||||
|
||||
def _searchOnTitle(self, title, movie, quality, results):
|
||||
|
||||
q = '"%s %s"' % (title, movie['library']['year'])
|
||||
q = '"%s %s" | "%s (%s)"' % (title, movie['library']['year'], title, movie['library']['year'])
|
||||
arguments = tryUrlencode({
|
||||
'q': q,
|
||||
'age': Env.setting('retention', 'nzb'),
|
||||
|
||||
59
couchpotato/core/providers/torrent/awesomehd/__init__.py
Normal file
59
couchpotato/core/providers/torrent/awesomehd/__init__.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from .main import AwesomeHD
|
||||
|
||||
def start():
|
||||
return AwesomeHD()
|
||||
|
||||
config = [{
|
||||
'name': 'awesomehd',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'Awesome-HD',
|
||||
'description': 'See <a href="https://awesome-hd.net">AHD</a>',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'passkey',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'only_internal',
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'default': 1,
|
||||
'description': 'Only search for internal releases.'
|
||||
},
|
||||
{
|
||||
'name': 'prefer_internal',
|
||||
'advanced': True,
|
||||
'type': 'bool',
|
||||
'default': 1,
|
||||
'description': 'Favors internal releases over non-internal releases.'
|
||||
},
|
||||
{
|
||||
'name': 'favor',
|
||||
'advanced': True,
|
||||
'default': 'both',
|
||||
'type': 'dropdown',
|
||||
'values': [('Encodes & Remuxes', 'both'), ('Encodes', 'encode'), ('Remuxes', 'remux'), ('None', 'none')],
|
||||
'description': 'Give extra scoring to encodes or remuxes.'
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'type': 'int',
|
||||
'default': 20,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
|
||||
64
couchpotato/core/providers/torrent/awesomehd/main.py
Normal file
64
couchpotato/core/providers/torrent/awesomehd/main.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||
import re
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class AwesomeHD(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test' : 'https://awesome-hd.net/',
|
||||
'detail' : 'https://awesome-hd.net/torrents.php?torrentid=%s',
|
||||
'search' : 'https://awesome-hd.net/searchapi.php?action=imdbsearch&passkey=%s&imdb=%s&internal=%s',
|
||||
'download' : 'https://awesome-hd.net/torrents.php?action=download&id=%s&authkey=%s&torrent_pass=%s',
|
||||
}
|
||||
http_time_between_calls = 1
|
||||
|
||||
def _search(self, movie, quality, results):
|
||||
|
||||
data = self.getHTMLData(self.urls['search'] % (self.conf('passkey'), movie['library']['identifier'], self.conf('only_internal')))
|
||||
|
||||
if data:
|
||||
try:
|
||||
soup = BeautifulSoup(data)
|
||||
authkey = soup.find('authkey').get_text()
|
||||
entries = soup.find_all('torrent')
|
||||
|
||||
for entry in entries:
|
||||
|
||||
torrentscore = 0
|
||||
torrent_id = entry.find('id').get_text()
|
||||
name = entry.find('name').get_text()
|
||||
year = entry.find('year').get_text()
|
||||
releasegroup = entry.find('releasegroup').get_text()
|
||||
resolution = entry.find('resolution').get_text()
|
||||
encoding = entry.find('encoding').get_text()
|
||||
freeleech = entry.find('freeleech').get_text()
|
||||
torrent_desc = '/ %s / %s / %s ' % (releasegroup, resolution, encoding)
|
||||
|
||||
if freeleech == '0.25' and self.conf('prefer_internal'):
|
||||
torrent_desc += '/ Internal'
|
||||
torrentscore += 200
|
||||
|
||||
if encoding == 'x264' and self.conf('favor') in ['encode', 'both']:
|
||||
torrentscore += 300
|
||||
if re.search('Remux', encoding) and self.conf('favor') in ['remux', 'both']:
|
||||
torrentscore += 200
|
||||
|
||||
results.append({
|
||||
'id': torrent_id,
|
||||
'name': re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) %s' % (name, year, torrent_desc)),
|
||||
'url': self.urls['download'] % (torrent_id, authkey, self.conf('passkey')),
|
||||
'detail_url': self.urls['detail'] % torrent_id,
|
||||
'size': self.parseSize(entry.find('size').get_text()),
|
||||
'seeders': tryInt(entry.find('seeders').get_text()),
|
||||
'leechers': tryInt(entry.find('leechers').get_text()),
|
||||
'score': torrentscore
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
@@ -16,6 +16,7 @@ class HDBits(TorrentProvider):
|
||||
'detail' : 'https://hdbits.org/details.php?id=%s&source=browse',
|
||||
'search' : 'https://hdbits.org/json_search.php?imdb=%s',
|
||||
'download' : 'https://hdbits.org/download.php/%s.torrent?id=%s&passkey=%s&source=details.browse',
|
||||
'login_check': 'http://hdbits.org/inbox.php',
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 #seconds
|
||||
@@ -30,7 +31,7 @@ class HDBits(TorrentProvider):
|
||||
results.append({
|
||||
'id': result['id'],
|
||||
'name': result['title'],
|
||||
'url': self.urls['download'] % (result['title'], result['id'], self.conf('passkey')),
|
||||
'url': self.urls['download'] % (result['id'], result['id'], self.conf('passkey')),
|
||||
'detail_url': self.urls['detail'] % result['id'],
|
||||
'size': self.parseSize(result['size']),
|
||||
'seeders': tryInt(result['seeder']),
|
||||
@@ -53,3 +54,5 @@ class HDBits(TorrentProvider):
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return '/logout.php' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
@@ -5,7 +5,6 @@ from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||
import traceback
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
@@ -15,7 +14,8 @@ class IPTorrents(TorrentProvider):
|
||||
'test' : 'http://www.iptorrents.com/',
|
||||
'base_url' : 'http://www.iptorrents.com',
|
||||
'login' : 'http://www.iptorrents.com/torrents/',
|
||||
'search' : 'http://www.iptorrents.com/torrents/?l%d=1%s&q=%s&qf=ti',
|
||||
'login_check': 'http://www.iptorrents.com/inbox.php',
|
||||
'search' : 'http://www.iptorrents.com/torrents/?l%d=1%s&q=%s&qf=ti&p=%d',
|
||||
}
|
||||
|
||||
cat_ids = [
|
||||
@@ -32,48 +32,62 @@ class IPTorrents(TorrentProvider):
|
||||
|
||||
freeleech = '' if not self.conf('freeleech') else '&free=on'
|
||||
|
||||
url = self.urls['search'] % (self.getCatId(quality['identifier'])[0], freeleech, tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])))
|
||||
data = self.getHTMLData(url, opener = self.login_opener)
|
||||
pages = 1
|
||||
current_page = 1
|
||||
while current_page <= pages and not self.shuttingDown():
|
||||
|
||||
if data:
|
||||
html = BeautifulSoup(data)
|
||||
url = self.urls['search'] % (self.getCatId(quality['identifier'])[0], freeleech, tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])), current_page)
|
||||
data = self.getHTMLData(url, opener = self.login_opener)
|
||||
|
||||
try:
|
||||
result_table = html.find('table', attrs = {'class' : 'torrents'})
|
||||
if data:
|
||||
html = BeautifulSoup(data)
|
||||
|
||||
if not result_table or 'nothing found!' in data.lower():
|
||||
return
|
||||
try:
|
||||
page_nav = html.find('span', attrs = {'class' : 'page_nav'})
|
||||
if page_nav:
|
||||
next_link = page_nav.find("a", text = "Next")
|
||||
if next_link:
|
||||
final_page_link = next_link.previous_sibling.previous_sibling
|
||||
pages = int(final_page_link.string)
|
||||
|
||||
entries = result_table.find_all('tr')
|
||||
result_table = html.find('table', attrs = {'class' : 'torrents'})
|
||||
|
||||
for result in entries[1:]:
|
||||
if not result_table or 'nothing found!' in data.lower():
|
||||
return
|
||||
|
||||
torrent = result.find_all('td')[1].find('a')
|
||||
entries = result_table.find_all('tr')
|
||||
|
||||
torrent_id = torrent['href'].replace('/details.php?id=', '')
|
||||
torrent_name = torrent.string
|
||||
torrent_download_url = self.urls['base_url'] + (result.find_all('td')[3].find('a'))['href'].replace(' ', '.')
|
||||
torrent_details_url = self.urls['base_url'] + torrent['href']
|
||||
torrent_size = self.parseSize(result.find_all('td')[5].string)
|
||||
torrent_seeders = tryInt(result.find('td', attrs = {'class' : 'ac t_seeders'}).string)
|
||||
torrent_leechers = tryInt(result.find('td', attrs = {'class' : 'ac t_leechers'}).string)
|
||||
for result in entries[1:]:
|
||||
|
||||
results.append({
|
||||
'id': torrent_id,
|
||||
'name': torrent_name,
|
||||
'url': torrent_download_url,
|
||||
'detail_url': torrent_details_url,
|
||||
'download': self.loginDownload,
|
||||
'size': torrent_size,
|
||||
'seeders': torrent_seeders,
|
||||
'leechers': torrent_leechers,
|
||||
})
|
||||
torrent = result.find_all('td')
|
||||
if len(torrent) <= 1:
|
||||
break
|
||||
|
||||
except:
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
torrent = torrent[1].find('a')
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return 'don\'t have an account' not in output.lower()
|
||||
torrent_id = torrent['href'].replace('/details.php?id=', '')
|
||||
torrent_name = torrent.string
|
||||
torrent_download_url = self.urls['base_url'] + (result.find_all('td')[3].find('a'))['href'].replace(' ', '.')
|
||||
torrent_details_url = self.urls['base_url'] + torrent['href']
|
||||
torrent_size = self.parseSize(result.find_all('td')[5].string)
|
||||
torrent_seeders = tryInt(result.find('td', attrs = {'class' : 'ac t_seeders'}).string)
|
||||
torrent_leechers = tryInt(result.find('td', attrs = {'class' : 'ac t_leechers'}).string)
|
||||
|
||||
results.append({
|
||||
'id': torrent_id,
|
||||
'name': torrent_name,
|
||||
'url': torrent_download_url,
|
||||
'detail_url': torrent_details_url,
|
||||
'size': torrent_size,
|
||||
'seeders': torrent_seeders,
|
||||
'leechers': torrent_leechers,
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
break
|
||||
|
||||
current_page += 1
|
||||
|
||||
def getLoginParams(self):
|
||||
return tryUrlencode({
|
||||
@@ -81,3 +95,9 @@ class IPTorrents(TorrentProvider):
|
||||
'password': self.conf('password'),
|
||||
'login': 'submit',
|
||||
})
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return 'don\'t have an account' not in output.lower()
|
||||
|
||||
def loginCheckSuccess(self, output):
|
||||
return '/logout.php' in output.lower()
|
||||
|
||||
@@ -11,9 +11,9 @@ log = CPLog(__name__)
|
||||
class KickAssTorrents(TorrentMagnetProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'https://kat.ph/',
|
||||
'detail': 'https://kat.ph/%s',
|
||||
'search': 'https://kat.ph/%s-i%s/',
|
||||
'test': 'https://kickass.to/',
|
||||
'detail': 'https://kickass.to/%s',
|
||||
'search': 'https://kickass.to/%s-i%s/',
|
||||
}
|
||||
|
||||
cat_ids = [
|
||||
|
||||
@@ -3,13 +3,11 @@ from couchpotato.core.helpers.variable import getTitle, tryInt, mergeDicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||
from dateutil.parser import parse
|
||||
import cookielib
|
||||
import htmlentitydefs
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
import urllib2
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -21,9 +19,12 @@ class PassThePopcorn(TorrentProvider):
|
||||
'detail': 'https://tls.passthepopcorn.me/torrents.php?torrentid=%s',
|
||||
'torrent': 'https://tls.passthepopcorn.me/torrents.php',
|
||||
'login': 'https://tls.passthepopcorn.me/ajax.php?action=login',
|
||||
'login_check': 'https://tls.passthepopcorn.me/ajax.php?action=login',
|
||||
'search': 'https://tls.passthepopcorn.me/search/%s/0/7/%d'
|
||||
}
|
||||
|
||||
http_time_between_calls = 2
|
||||
|
||||
quality_search_params = {
|
||||
'bd50': {'media': 'Blu-ray', 'format': 'BD50'},
|
||||
'1080p': {'resolution': '1080p'},
|
||||
@@ -52,18 +53,6 @@ class PassThePopcorn(TorrentProvider):
|
||||
'cam': {'Source': ['CAM']}
|
||||
}
|
||||
|
||||
class NotLoggedInHTTPError(urllib2.HTTPError):
|
||||
def __init__(self, url, code, msg, headers, fp):
|
||||
urllib2.HTTPError.__init__(self, url, code, msg, headers, fp)
|
||||
|
||||
class PTPHTTPRedirectHandler(urllib2.HTTPRedirectHandler):
|
||||
def http_error_302(self, req, fp, code, msg, headers):
|
||||
log.debug("302 detected; redirected to %s", headers['Location'])
|
||||
if (headers['Location'] != 'login.php'):
|
||||
return urllib2.HTTPRedirectHandler.http_error_302(self, req, fp, code, msg, headers)
|
||||
else:
|
||||
raise PassThePopcorn.NotLoggedInHTTPError(req.get_full_url(), code, msg, headers, fp)
|
||||
|
||||
def _search(self, movie, quality, results):
|
||||
|
||||
movie_title = getTitle(movie['library'])
|
||||
@@ -75,17 +64,8 @@ class PassThePopcorn(TorrentProvider):
|
||||
'searchstr': movie['library']['identifier']
|
||||
})
|
||||
|
||||
# Do login for the cookies
|
||||
if not self.login_opener and not self.login():
|
||||
return
|
||||
|
||||
try:
|
||||
url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params))
|
||||
txt = self.urlopen(url, opener = self.login_opener)
|
||||
res = json.loads(txt)
|
||||
except:
|
||||
log.error('Search on PassThePopcorn.me (%s) failed (could not decode JSON)', params)
|
||||
return
|
||||
url = '%s?json=noredirect&%s' % (self.urls['torrent'], tryUrlencode(params))
|
||||
res = self.getJsonData(url, opener = self.login_opener)
|
||||
|
||||
try:
|
||||
if not 'Movies' in res:
|
||||
@@ -136,40 +116,11 @@ class PassThePopcorn(TorrentProvider):
|
||||
'leechers': tryInt(torrent['Leechers']),
|
||||
'score': torrentscore,
|
||||
'extra_check': extra_check,
|
||||
'download': self.loginDownload,
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
def login(self):
|
||||
|
||||
cookieprocessor = urllib2.HTTPCookieProcessor(cookielib.CookieJar())
|
||||
opener = urllib2.build_opener(cookieprocessor, PassThePopcorn.PTPHTTPRedirectHandler())
|
||||
opener.addheaders = [
|
||||
('User-Agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/21.0.1180.75 Safari/537.1'),
|
||||
('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'),
|
||||
('Accept-Language', 'en-gb,en;q=0.5'),
|
||||
('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7'),
|
||||
('Keep-Alive', '115'),
|
||||
('Connection', 'keep-alive'),
|
||||
('Cache-Control', 'max-age=0'),
|
||||
]
|
||||
|
||||
try:
|
||||
response = opener.open(self.urls['login'], self.getLoginParams())
|
||||
except urllib2.URLError as e:
|
||||
log.error('Login to PassThePopcorn failed: %s', e)
|
||||
return False
|
||||
|
||||
if response.getcode() == 200:
|
||||
log.debug('Login HTTP status 200; seems successful')
|
||||
self.login_opener = opener
|
||||
return True
|
||||
else:
|
||||
log.error('Login to PassThePopcorn failed: returned code %d', response.getcode())
|
||||
return False
|
||||
|
||||
def torrentMeetsQualitySpec(self, torrent, quality):
|
||||
|
||||
if not quality in self.post_search_filters:
|
||||
@@ -186,7 +137,7 @@ class PassThePopcorn(TorrentProvider):
|
||||
seen_one = False
|
||||
|
||||
if not field in torrent:
|
||||
log.debug('Torrent with ID %s has no field "%s"; cannot apply post-search-filter for quality "%s"', (torrent['Id'], field, quality))
|
||||
log.debug('Torrent with ID %s has no field "%s"; cannot apply post-search-filter for quality "%s"', (torrent['id'], field, quality))
|
||||
continue
|
||||
|
||||
for spec in specs:
|
||||
@@ -244,3 +195,11 @@ class PassThePopcorn(TorrentProvider):
|
||||
'keeplogged': '1',
|
||||
'login': 'Login'
|
||||
})
|
||||
|
||||
def loginSuccess(self, output):
|
||||
try:
|
||||
return json.loads(output).get('Result', '').lower() == 'ok'
|
||||
except:
|
||||
return False
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
@@ -12,7 +12,8 @@ class SceneAccess(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'https://www.sceneaccess.eu/',
|
||||
'login' : 'https://www.sceneaccess.eu/login',
|
||||
'login': 'https://www.sceneaccess.eu/login',
|
||||
'login_check': 'https://www.sceneaccess.eu/inbox',
|
||||
'detail': 'https://www.sceneaccess.eu/details?id=%s',
|
||||
'search': 'https://www.sceneaccess.eu/browse?method=2&c%d=%d',
|
||||
'download': 'https://www.sceneaccess.eu/%s',
|
||||
@@ -39,9 +40,6 @@ class SceneAccess(TorrentProvider):
|
||||
})
|
||||
url = "%s&%s" % (url, arguments)
|
||||
|
||||
# Do login for the cookies
|
||||
if not self.login_opener and not self.login():
|
||||
return
|
||||
|
||||
data = self.getHTMLData(url, opener = self.login_opener)
|
||||
|
||||
@@ -69,7 +67,6 @@ class SceneAccess(TorrentProvider):
|
||||
'size': self.parseSize(result.find('td', attrs = {'class' : 'ttr_size'}).contents[0]),
|
||||
'seeders': tryInt(result.find('td', attrs = {'class' : 'ttr_seeders'}).find('a').string),
|
||||
'leechers': tryInt(leechers.string) if leechers else 0,
|
||||
'download': self.loginDownload,
|
||||
'get_more_info': self.getMoreInfo,
|
||||
})
|
||||
|
||||
@@ -91,3 +88,8 @@ class SceneAccess(TorrentProvider):
|
||||
|
||||
item['description'] = description
|
||||
return item
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return '/inbox' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
@@ -13,6 +13,7 @@ class SceneHD(TorrentProvider):
|
||||
urls = {
|
||||
'test': 'https://scenehd.org/',
|
||||
'login' : 'https://scenehd.org/takelogin.php',
|
||||
'login_check': 'https://scenehd.org/my.php',
|
||||
'detail': 'https://scenehd.org/details.php?id=%s',
|
||||
'search': 'https://scenehd.org/browse.php?ajax',
|
||||
'download': 'https://scenehd.org/download.php?id=%s',
|
||||
@@ -28,10 +29,6 @@ class SceneHD(TorrentProvider):
|
||||
})
|
||||
url = "%s&%s" % (self.urls['search'], arguments)
|
||||
|
||||
# Cookie login
|
||||
if not self.login_opener and not self.login():
|
||||
return
|
||||
|
||||
data = self.getHTMLData(url, opener = self.login_opener)
|
||||
|
||||
if data:
|
||||
@@ -61,7 +58,6 @@ class SceneHD(TorrentProvider):
|
||||
'seeders': tryInt(all_cells[10].find('a').string),
|
||||
'leechers': tryInt(leechers),
|
||||
'url': self.urls['download'] % torrent_id,
|
||||
'download': self.loginDownload,
|
||||
'description': all_cells[1].find('a')['href'],
|
||||
})
|
||||
|
||||
@@ -75,3 +71,9 @@ class SceneHD(TorrentProvider):
|
||||
'password': self.conf('password'),
|
||||
'ssl': 'yes',
|
||||
})
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return 'logout.php' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
|
||||
42
couchpotato/core/providers/torrent/torrentbytes/__init__.py
Normal file
42
couchpotato/core/providers/torrent/torrentbytes/__init__.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from .main import TorrentBytes
|
||||
|
||||
def start():
|
||||
return TorrentBytes()
|
||||
|
||||
config = [{
|
||||
'name': 'torrentbytes',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'TorrentBytes',
|
||||
'description': 'See <a href="http://torrentbytes.net">TorrentBytes</a>',
|
||||
'wizard': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': False,
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 20,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
82
couchpotato/core/providers/torrent/torrentbytes/main.py
Normal file
82
couchpotato/core/providers/torrent/torrentbytes/main.py
Normal file
@@ -0,0 +1,82 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.encoding import tryUrlencode
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class TorrentBytes(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test' : 'https://www.torrentbytes.net/',
|
||||
'login' : 'https://www.torrentbytes.net/takelogin.php',
|
||||
'login_check' : 'https://www.torrentbytes.net/inbox.php',
|
||||
'detail' : 'https://www.torrentbytes.net/details.php?id=%s',
|
||||
'search' : 'https://www.torrentbytes.net/browse.php?search=%s&cat=%d',
|
||||
'download' : 'https://www.torrentbytes.net/download.php?id=%s&name=%s',
|
||||
}
|
||||
|
||||
cat_ids = [
|
||||
([5], ['720p', '1080p']),
|
||||
([19], ['cam']),
|
||||
([19], ['ts', 'tc']),
|
||||
([19], ['r5', 'scr']),
|
||||
([19], ['dvdrip']),
|
||||
([5], ['brrip']),
|
||||
([20], ['dvdr']),
|
||||
]
|
||||
|
||||
http_time_between_calls = 1 #seconds
|
||||
cat_backup_id = None
|
||||
|
||||
def _searchOnTitle(self, title, movie, quality, results):
|
||||
|
||||
url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])), self.getCatId(quality['identifier'])[0])
|
||||
data = self.getHTMLData(url, opener = self.login_opener)
|
||||
|
||||
if data:
|
||||
html = BeautifulSoup(data)
|
||||
|
||||
try:
|
||||
result_table = html.find('table', attrs = {'border' : '1'})
|
||||
if not result_table:
|
||||
return
|
||||
|
||||
entries = result_table.find_all('tr')
|
||||
|
||||
for result in entries[1:]:
|
||||
cells = result.find_all('td')
|
||||
|
||||
link = cells[1].find('a', attrs = {'class' : 'index'})
|
||||
|
||||
full_id = link['href'].replace('details.php?id=', '')
|
||||
torrent_id = full_id[:6]
|
||||
|
||||
results.append({
|
||||
'id': torrent_id,
|
||||
'name': link.contents[0],
|
||||
'url': self.urls['download'] % (torrent_id, link.contents[0]),
|
||||
'detail_url': self.urls['detail'] % torrent_id,
|
||||
'size': self.parseSize(cells[6].contents[0] + cells[6].contents[2]),
|
||||
'seeders': tryInt(cells[8].find('span').contents[0]),
|
||||
'leechers': tryInt(cells[9].find('span').contents[0]),
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
def getLoginParams(self):
|
||||
return tryUrlencode({
|
||||
'username': self.conf('username'),
|
||||
'password': self.conf('password'),
|
||||
'login': 'submit',
|
||||
})
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return 'logout.php' in output.lower() or 'Welcome' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
@@ -10,7 +10,8 @@ class TorrentDay(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test': 'http://www.td.af/',
|
||||
'login' : 'http://www.td.af/torrents/',
|
||||
'login': 'http://www.td.af/torrents/',
|
||||
'login_check': 'http://www.torrentday.com/userdetails.php',
|
||||
'detail': 'http://www.td.af/details.php?id=%s',
|
||||
'search': 'http://www.td.af/V3/API/API.php',
|
||||
'download': 'http://www.td.af/download.php/%s/%s',
|
||||
@@ -50,7 +51,6 @@ class TorrentDay(TorrentProvider):
|
||||
'size': self.parseSize(torrent.get('size')),
|
||||
'seeders': tryInt(torrent.get('seed')),
|
||||
'leechers': tryInt(torrent.get('leech')),
|
||||
'download': self.loginDownload,
|
||||
})
|
||||
|
||||
def getLoginParams(self):
|
||||
@@ -62,3 +62,6 @@ class TorrentDay(TorrentProvider):
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return 'Password not correct' not in output
|
||||
|
||||
def loginCheckSuccess(self, output):
|
||||
return 'logout.php' in output.lower()
|
||||
|
||||
@@ -14,6 +14,7 @@ class TorrentLeech(TorrentProvider):
|
||||
urls = {
|
||||
'test' : 'http://www.torrentleech.org/',
|
||||
'login' : 'http://www.torrentleech.org/user/account/login/',
|
||||
'login_check': 'http://torrentleech.org/user/messages',
|
||||
'detail' : 'http://www.torrentleech.org/torrent/%s',
|
||||
'search' : 'http://www.torrentleech.org/torrents/browse/index/query/%s/categories/%d',
|
||||
'download' : 'http://www.torrentleech.org%s',
|
||||
@@ -58,7 +59,6 @@ class TorrentLeech(TorrentProvider):
|
||||
'name': link.string,
|
||||
'url': self.urls['download'] % url['href'],
|
||||
'detail_url': self.urls['download'] % details['href'],
|
||||
'download': self.loginDownload,
|
||||
'size': self.parseSize(result.find_all('td')[4].string),
|
||||
'seeders': tryInt(result.find('td', attrs = {'class' : 'seeders'}).string),
|
||||
'leechers': tryInt(result.find('td', attrs = {'class' : 'leechers'}).string),
|
||||
@@ -77,3 +77,5 @@ class TorrentLeech(TorrentProvider):
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return '/user/account/logout' in output.lower() or 'welcome back' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
@@ -13,6 +13,7 @@ class TorrentShack(TorrentProvider):
|
||||
urls = {
|
||||
'test' : 'http://www.torrentshack.net/',
|
||||
'login' : 'http://www.torrentshack.net/login.php',
|
||||
'login_check': 'http://www.torrentshack.net/inbox.php',
|
||||
'detail' : 'http://www.torrentshack.net/torrent/%s',
|
||||
'search' : 'http://www.torrentshack.net/torrents.php?searchstr=%s&filter_cat[%d]=1',
|
||||
'download' : 'http://www.torrentshack.net/%s',
|
||||
@@ -58,7 +59,6 @@ class TorrentShack(TorrentProvider):
|
||||
'name': unicode(link.span.string).translate({ord(u'\xad'): None}),
|
||||
'url': self.urls['download'] % url['href'],
|
||||
'detail_url': self.urls['download'] % link['href'],
|
||||
'download': self.loginDownload,
|
||||
'size': self.parseSize(result.find_all('td')[4].string),
|
||||
'seeders': tryInt(result.find_all('td')[6].string),
|
||||
'leechers': tryInt(result.find_all('td')[7].string),
|
||||
@@ -79,3 +79,5 @@ class TorrentShack(TorrentProvider):
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return 'logout.php' in output.lower()
|
||||
|
||||
loginCheckSuccess = loginSuccess
|
||||
|
||||
33
couchpotato/core/providers/torrent/yify/__init__.py
Normal file
33
couchpotato/core/providers/torrent/yify/__init__.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from main import Yify
|
||||
|
||||
def start():
|
||||
return Yify()
|
||||
|
||||
config = [{
|
||||
'name': 'yify',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'searcher',
|
||||
'subtab': 'providers',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'Yify',
|
||||
'description': 'Free provider, less accurate. Small HD movies, encoded by <a href="https://yify-torrents.com/">Yify</a>.',
|
||||
'wizard': False,
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'type': 'enabler',
|
||||
'default': 0
|
||||
},
|
||||
{
|
||||
'name': 'extra_score',
|
||||
'advanced': True,
|
||||
'label': 'Extra Score',
|
||||
'type': 'int',
|
||||
'default': 0,
|
||||
'description': 'Starting score for each release found via this provider.',
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}]
|
||||
53
couchpotato/core/providers/torrent/yify/main.py
Normal file
53
couchpotato/core/providers/torrent/yify/main.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.torrent.base import TorrentProvider
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Yify(TorrentProvider):
|
||||
|
||||
urls = {
|
||||
'test' : 'https://yify-torrents.com/api',
|
||||
'search' : 'https://yify-torrents.com/api/list.json?keywords=%s&quality=%s',
|
||||
'detail': 'https://yify-torrents.com/api/movie.json?id=%s'
|
||||
}
|
||||
|
||||
http_time_between_calls = 1 #seconds
|
||||
|
||||
def search(self, movie, quality):
|
||||
|
||||
if not quality.get('hd', False):
|
||||
return []
|
||||
|
||||
return super(Yify, self).search(movie, quality)
|
||||
|
||||
def _searchOnTitle(self, title, movie, quality, results):
|
||||
|
||||
data = self.getJsonData(self.urls['search'] % (title, quality['identifier']))
|
||||
|
||||
if data and data.get('MovieList'):
|
||||
try:
|
||||
for result in data.get('MovieList'):
|
||||
|
||||
try:
|
||||
title = result['TorrentUrl'].split('/')[-1][:-8].replace('_', '.').strip('._')
|
||||
title = title.replace('.-.', '-')
|
||||
title = title.replace('..', '.')
|
||||
except:
|
||||
continue
|
||||
|
||||
results.append({
|
||||
'id': result['MovieID'],
|
||||
'name': title,
|
||||
'url': result['TorrentUrl'],
|
||||
'detail_url': self.urls['detail'] % result['MovieID'],
|
||||
'size': self.parseSize(result['Size']),
|
||||
'seeders': tryInt(result['TorrentSeeds']),
|
||||
'leechers': tryInt(result['TorrentPeers'])
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
@@ -2,7 +2,6 @@ from __future__ import with_statement
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent, fireEvent
|
||||
from couchpotato.core.helpers.encoding import isInt, toUnicode
|
||||
from couchpotato.core.helpers.request import getParams, jsonified
|
||||
from couchpotato.core.helpers.variable import mergeDicts, tryInt
|
||||
from couchpotato.core.settings.model import Properties
|
||||
import ConfigParser
|
||||
@@ -169,19 +168,17 @@ class Settings(object):
|
||||
return self.options
|
||||
|
||||
|
||||
def view(self):
|
||||
return jsonified({
|
||||
def view(self, **kwargs):
|
||||
return {
|
||||
'options': self.getOptions(),
|
||||
'values': self.getValues()
|
||||
})
|
||||
}
|
||||
|
||||
def saveView(self):
|
||||
def saveView(self, **kwargs):
|
||||
|
||||
params = getParams()
|
||||
|
||||
section = params.get('section')
|
||||
option = params.get('name')
|
||||
value = params.get('value')
|
||||
section = kwargs.get('section')
|
||||
option = kwargs.get('name')
|
||||
value = kwargs.get('value')
|
||||
|
||||
# See if a value handler is attached, use that as value
|
||||
new_value = fireEvent('setting.save.%s.%s' % (section, option), value, single = True)
|
||||
@@ -192,9 +189,9 @@ class Settings(object):
|
||||
# After save (for re-interval etc)
|
||||
fireEvent('setting.save.%s.%s.after' % (section, option), single = True)
|
||||
|
||||
return jsonified({
|
||||
return {
|
||||
'success': True,
|
||||
})
|
||||
}
|
||||
|
||||
def getProperty(self, identifier):
|
||||
from couchpotato import get_session
|
||||
|
||||
@@ -3,6 +3,7 @@ from elixir.entity import Entity
|
||||
from elixir.fields import Field
|
||||
from elixir.options import options_defaults, using_options
|
||||
from elixir.relationships import ManyToMany, OneToMany, ManyToOne
|
||||
from sqlalchemy.ext.mutable import Mutable
|
||||
from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, String, \
|
||||
TypeDecorator
|
||||
import json
|
||||
@@ -39,6 +40,37 @@ class JsonType(TypeDecorator):
|
||||
def process_result_value(self, value, dialect):
|
||||
return json.loads(value if value else '{}')
|
||||
|
||||
class MutableDict(Mutable, dict):
|
||||
|
||||
@classmethod
|
||||
def coerce(cls, key, value):
|
||||
if not isinstance(value, MutableDict):
|
||||
if isinstance(value, dict):
|
||||
return MutableDict(value)
|
||||
return Mutable.coerce(key, value)
|
||||
else:
|
||||
return value
|
||||
|
||||
def __delitem(self, key):
|
||||
dict.__delitem__(self, key)
|
||||
self.changed()
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
dict.__setitem__(self, key, value)
|
||||
self.changed()
|
||||
|
||||
def __getstate__(self):
|
||||
return dict(self)
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.update(self)
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
super(MutableDict, self).update(*args, **kwargs)
|
||||
self.changed()
|
||||
|
||||
MutableDict.associate_with(JsonType)
|
||||
|
||||
|
||||
class Movie(Entity):
|
||||
"""Movie Resource a movie could have multiple releases
|
||||
|
||||
@@ -11,6 +11,7 @@ class Env(object):
|
||||
_appname = 'CouchPotato'
|
||||
|
||||
''' Environment variables '''
|
||||
_app = None
|
||||
_encoding = 'UTF-8'
|
||||
_debug = False
|
||||
_dev = False
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
from argparse import ArgumentParser
|
||||
from couchpotato import web
|
||||
from couchpotato.api import api, NonBlockHandler
|
||||
from cache import FileSystemCache
|
||||
from couchpotato import KeyHandler
|
||||
from couchpotato.api import NonBlockHandler, ApiHandler
|
||||
from couchpotato.core.event import fireEventAsync, fireEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.helpers.variable import getDataDir, tryInt
|
||||
from logging import handlers
|
||||
from tornado.httpserver import HTTPServer
|
||||
from tornado.web import Application, FallbackHandler
|
||||
from tornado.wsgi import WSGIContainer
|
||||
from werkzeug.contrib.cache import FileSystemCache
|
||||
from tornado.web import Application, StaticFileHandler, RedirectHandler
|
||||
import locale
|
||||
import logging
|
||||
import os.path
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
import warnings
|
||||
|
||||
def getOptions(base_path, args):
|
||||
@@ -75,23 +76,25 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
if not encoding or encoding in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
|
||||
encoding = 'UTF-8'
|
||||
|
||||
Env.set('encoding', encoding)
|
||||
|
||||
# Do db stuff
|
||||
db_path = os.path.join(data_dir, 'couchpotato.db')
|
||||
db_path = toUnicode(os.path.join(data_dir, 'couchpotato.db'))
|
||||
|
||||
# Backup before start and cleanup old databases
|
||||
new_backup = os.path.join(data_dir, 'db_backup', str(int(time.time())))
|
||||
new_backup = toUnicode(os.path.join(data_dir, 'db_backup', str(int(time.time()))))
|
||||
|
||||
# Create path and copy
|
||||
if not os.path.isdir(new_backup): os.makedirs(new_backup)
|
||||
src_files = [options.config_file, db_path, db_path + '-shm', db_path + '-wal']
|
||||
for src_file in src_files:
|
||||
if os.path.isfile(src_file):
|
||||
shutil.copy2(src_file, os.path.join(new_backup, os.path.basename(src_file)))
|
||||
shutil.copy2(src_file, toUnicode(os.path.join(new_backup, os.path.basename(src_file))))
|
||||
|
||||
# Remove older backups, keep backups 3 days or at least 3
|
||||
backups = []
|
||||
for directory in os.listdir(os.path.dirname(new_backup)):
|
||||
backup = os.path.join(os.path.dirname(new_backup), directory)
|
||||
backup = toUnicode(os.path.join(os.path.dirname(new_backup), directory))
|
||||
if os.path.isdir(backup):
|
||||
backups.append(backup)
|
||||
|
||||
@@ -100,7 +103,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
if total_backups > 3:
|
||||
if tryInt(os.path.basename(backup)) < time.time() - 259200:
|
||||
for src_file in src_files:
|
||||
b_file = os.path.join(backup, os.path.basename(src_file))
|
||||
b_file = toUnicode(os.path.join(backup, os.path.basename(src_file)))
|
||||
if os.path.isfile(b_file):
|
||||
os.remove(b_file)
|
||||
os.rmdir(backup)
|
||||
@@ -108,13 +111,12 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
|
||||
|
||||
# Register environment settings
|
||||
Env.set('encoding', encoding)
|
||||
Env.set('app_dir', base_path)
|
||||
Env.set('data_dir', data_dir)
|
||||
Env.set('log_path', os.path.join(log_dir, 'CouchPotato.log'))
|
||||
Env.set('db_path', 'sqlite:///' + db_path)
|
||||
Env.set('cache_dir', os.path.join(data_dir, 'cache'))
|
||||
Env.set('cache', FileSystemCache(os.path.join(Env.get('cache_dir'), 'python')))
|
||||
Env.set('app_dir', toUnicode(base_path))
|
||||
Env.set('data_dir', toUnicode(data_dir))
|
||||
Env.set('log_path', toUnicode(os.path.join(log_dir, 'CouchPotato.log')))
|
||||
Env.set('db_path', toUnicode('sqlite:///' + db_path))
|
||||
Env.set('cache_dir', toUnicode(os.path.join(data_dir, 'cache')))
|
||||
Env.set('cache', FileSystemCache(toUnicode(os.path.join(Env.get('cache_dir'), 'python'))))
|
||||
Env.set('console_log', options.console_log)
|
||||
Env.set('quiet', options.quiet)
|
||||
Env.set('desktop', desktop)
|
||||
@@ -170,12 +172,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
|
||||
# Check if database exists
|
||||
db = Env.get('db_path')
|
||||
db_exists = os.path.isfile(db_path)
|
||||
|
||||
# Load configs & plugins
|
||||
loader = Env.get('loader')
|
||||
loader.preload(root = base_path)
|
||||
loader.run()
|
||||
db_exists = os.path.isfile(toUnicode(db_path))
|
||||
|
||||
# Load migrations
|
||||
if db_exists:
|
||||
@@ -201,17 +198,16 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
from couchpotato.core.settings.model import setup
|
||||
setup()
|
||||
|
||||
# Fill database with needed stuff
|
||||
if not db_exists:
|
||||
fireEvent('app.initialize', in_order = True)
|
||||
|
||||
# Create app
|
||||
from couchpotato import app
|
||||
from couchpotato import WebHandler
|
||||
web_base = ('/' + Env.setting('url_base').lstrip('/') + '/') if Env.setting('url_base') else '/'
|
||||
Env.set('web_base', web_base)
|
||||
|
||||
api_key = Env.setting('api_key')
|
||||
url_base = '/' + Env.setting('url_base').lstrip('/') if Env.setting('url_base') else ''
|
||||
api_base = r'%sapi/%s/' % (web_base, api_key)
|
||||
Env.set('api_base', api_base)
|
||||
|
||||
# Basic config
|
||||
app.secret_key = api_key
|
||||
host = Env.setting('host', default = '0.0.0.0')
|
||||
# app.debug = development
|
||||
config = {
|
||||
@@ -222,36 +218,60 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
'ssl_key': Env.setting('ssl_key', default = None),
|
||||
}
|
||||
|
||||
# Static path
|
||||
app.static_folder = os.path.join(base_path, 'couchpotato', 'static')
|
||||
web.add_url_rule('api/%s/static/<path:filename>' % api_key,
|
||||
endpoint = 'static',
|
||||
view_func = app.send_static_file)
|
||||
|
||||
# Register modules
|
||||
app.register_blueprint(web, url_prefix = '%s/' % url_base)
|
||||
app.register_blueprint(api, url_prefix = '%s/api/%s/' % (url_base, api_key))
|
||||
# Load the app
|
||||
application = Application([],
|
||||
log_function = lambda x : None,
|
||||
debug = config['use_reloader'],
|
||||
gzip = True,
|
||||
)
|
||||
Env.set('app', application)
|
||||
|
||||
|
||||
# Request handlers
|
||||
application.add_handlers(".*$", [
|
||||
(r'%snonblock/(.*)(/?)' % api_base, NonBlockHandler),
|
||||
|
||||
# API handlers
|
||||
(r'%s(.*)(/?)' % api_base, ApiHandler), # Main API handler
|
||||
(r'%sgetkey(/?)' % web_base, KeyHandler), # Get API key
|
||||
(r'%s' % api_base, RedirectHandler, {"url": web_base + 'docs/'}), # API docs
|
||||
|
||||
# Catch all webhandlers
|
||||
(r'%s(.*)(/?)' % web_base, WebHandler),
|
||||
(r'(.*)', WebHandler),
|
||||
])
|
||||
|
||||
# Static paths
|
||||
static_path = '%sstatic/' % api_base
|
||||
for dir_name in ['fonts', 'images', 'scripts', 'style']:
|
||||
application.add_handlers(".*$", [
|
||||
('%s%s/(.*)' % (static_path, dir_name), StaticFileHandler, {'path': toUnicode(os.path.join(base_path, 'couchpotato', 'static', dir_name))})
|
||||
])
|
||||
Env.set('static_path', static_path);
|
||||
|
||||
|
||||
# Load configs & plugins
|
||||
loader = Env.get('loader')
|
||||
loader.preload(root = toUnicode(base_path))
|
||||
loader.run()
|
||||
|
||||
|
||||
# Fill database with needed stuff
|
||||
if not db_exists:
|
||||
fireEvent('app.initialize', in_order = True)
|
||||
|
||||
|
||||
# Go go go!
|
||||
from tornado.ioloop import IOLoop
|
||||
loop = IOLoop.current()
|
||||
|
||||
|
||||
# Some logging and fire load event
|
||||
try: log.info('Starting server on port %(port)s', config)
|
||||
except: pass
|
||||
fireEventAsync('app.load')
|
||||
|
||||
# Go go go!
|
||||
from tornado.ioloop import IOLoop
|
||||
web_container = WSGIContainer(app)
|
||||
web_container._log = _log
|
||||
loop = IOLoop.current()
|
||||
|
||||
|
||||
application = Application([
|
||||
(r'%s/api/%s/nonblock/(.*)/' % (url_base, api_key), NonBlockHandler),
|
||||
(r'.*', FallbackHandler, dict(fallback = web_container)),
|
||||
],
|
||||
log_function = lambda x : None,
|
||||
debug = config['use_reloader'],
|
||||
gzip = True,
|
||||
)
|
||||
|
||||
if config['ssl_cert'] and config['ssl_key']:
|
||||
server = HTTPServer(application, no_keep_alive = True, ssl_options = {
|
||||
@@ -269,10 +289,11 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
|
||||
server.listen(config['port'], config['host'])
|
||||
loop.start()
|
||||
except Exception, e:
|
||||
log.error('Failed starting: %s', traceback.format_exc())
|
||||
try:
|
||||
nr, msg = e
|
||||
if nr == 48:
|
||||
log.info('Already in use, try %s more time after few seconds', restart_tries)
|
||||
log.info('Port (%s) needed for CouchPotato is already in use, try %s more time after few seconds', (config.get('port'), restart_tries))
|
||||
time.sleep(1)
|
||||
restart_tries -= 1
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 29 KiB |
@@ -101,6 +101,9 @@ Page.Home = new Class({
|
||||
});
|
||||
});
|
||||
|
||||
// Suggest
|
||||
self.suggestion_list = new SuggestList();
|
||||
|
||||
// Still not available
|
||||
self.late_list = new MovieList({
|
||||
'navigation': false,
|
||||
@@ -121,25 +124,10 @@ Page.Home = new Class({
|
||||
self.el.adopt(
|
||||
$(self.available_list),
|
||||
$(self.soon_list),
|
||||
$(self.suggestion_list),
|
||||
$(self.late_list)
|
||||
);
|
||||
|
||||
// Suggest
|
||||
// self.suggestion_list = new MovieList({
|
||||
// 'navigation': false,
|
||||
// 'identifier': 'suggestions',
|
||||
// 'limit': 6,
|
||||
// 'load_more': false,
|
||||
// 'view': 'thumbs',
|
||||
// 'api_call': 'suggestion.suggest'
|
||||
// });
|
||||
// self.el.adopt(
|
||||
// new Element('h2', {
|
||||
// 'text': 'You might like'
|
||||
// }),
|
||||
// $(self.suggestion_list)
|
||||
// );
|
||||
|
||||
// Recent
|
||||
// Snatched
|
||||
// Renamed
|
||||
|
||||
@@ -168,6 +168,7 @@ body > .spinner, .mask{
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.icon2.add:before { content: "\e05a"; color: #c2fac5; }
|
||||
.icon2.cog:before { content: "\e109"; }
|
||||
.icon2.eye-open:before { content: "\e09d"; }
|
||||
.icon2.search:before { content: "\e03e"; }
|
||||
|
||||
@@ -1,27 +1,28 @@
|
||||
{% autoescape None %}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="{{ url_for('web.static', filename='style/api.css') }}" type="text/css">
|
||||
<link rel="stylesheet" href="{{ Env.get('static_path') }}style/api.css" type="text/css">
|
||||
<title>API documentation</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>CouchPotato API Documentation</h1>
|
||||
<div class="api">
|
||||
You can access the API via <pre>{{ fireEvent('app.api_url', single = True)|safe }}/</pre>
|
||||
You can access the API via <pre>{{ Env.get('api_base') }}</pre>
|
||||
To see it in action, have a look at the webinterface with Firebug (on firefox) or the development tools included in Chrome.
|
||||
All the data that you see there are from the API.
|
||||
<br />
|
||||
<br />
|
||||
A normal API call:
|
||||
<pre><a href="{{ fireEvent('app.api_url', single = True)|safe }}/updater.info/">{{ fireEvent('app.api_url', single = True)|safe }}/updater.info/</a></pre>
|
||||
<pre><a href="{{ Env.get('api_base') }}updater.info/">{{ Env.get('api_base') }}updater.info/</a></pre>
|
||||
<br />
|
||||
You can also use the API over another domain using JSONP, the callback function should be in 'callback_func'
|
||||
<pre><a href="{{ fireEvent('app.api_url', single = True)|safe }}/updater.info/?callback_func=myfunction">{{ fireEvent('app.api_url', single = True)|safe }}/updater.info/?callback_func=myfunction</a></pre>
|
||||
<pre><a href="{{ Env.get('api_base') }}updater.info/?callback_func=myfunction">{{ Env.get('api_base') }}updater.info/?callback_func=myfunction</a></pre>
|
||||
<br />
|
||||
<br />
|
||||
Get the API key:
|
||||
<pre><a href="{{ url_for('web.index') }}getkey/?p=md5(password)&u=md5(username)">{{ url_for('web.index') }}getkey/?p=md5(password)&u=md5(username)</a></pre>
|
||||
<pre><a href="{{ Env.get('web_base') }}getkey/?p=md5(password)&u=md5(username)">{{ Env.get('web_base') }}getkey/?p=md5(password)&u=md5(username)</a></pre>
|
||||
Will return {"api_key": "XXXXXXXXXX", "success": true}. When username or password is empty you don't need to md5 it.
|
||||
<br />
|
||||
</div>
|
||||
@@ -41,9 +42,9 @@
|
||||
<td class="type">{{ api_docs[route]['params'][param].get('type', 'string') }}</td>
|
||||
<td class="description">{{ api_docs[route]['params'][param]['desc'] }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% end %}
|
||||
</table>
|
||||
{% endif %}
|
||||
{% end %}
|
||||
|
||||
{% if api_docs[route].get('return') %}
|
||||
<h3>Return</h3>
|
||||
@@ -52,14 +53,14 @@
|
||||
{% if api_docs[route]['return'].get('example') %}
|
||||
<div class="example">
|
||||
<h4>Example</h4>
|
||||
<pre>{{ api_docs[route]['return'].get('example', '')|safe }}</pre>
|
||||
<pre>{{ api_docs[route]['return'].get('example', '') }}</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% end %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% end %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% end %}
|
||||
{% end %}
|
||||
|
||||
<div class="missing">
|
||||
<h1>Missing documentation</h1>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% autoescape None %}
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
@@ -5,38 +6,38 @@
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
|
||||
{% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'front', single = True) %}
|
||||
<link rel="stylesheet" href="{{ url_for('web.index') }}{{ url }}" type="text/css">{% endfor %}
|
||||
<link rel="stylesheet" href="{{ Env.get('web_base') }}{{ url }}" type="text/css">{% end %}
|
||||
{% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'front', single = True) %}
|
||||
<script type="text/javascript" src="{{ url_for('web.index') }}{{ url }}"></script>{% endfor %}
|
||||
<script type="text/javascript" src="{{ Env.get('web_base') }}{{ url }}"></script>{% end %}
|
||||
|
||||
{% for url in fireEvent('clientscript.get_scripts', as_html = True, location = 'head', single = True) %}
|
||||
<script type="text/javascript" src="{{ url_for('web.index') }}{{ url }}"></script>{% endfor %}
|
||||
<script type="text/javascript" src="{{ Env.get('web_base') }}{{ url }}"></script>{% end %}
|
||||
{% for url in fireEvent('clientscript.get_styles', as_html = True, location = 'head', single = True) %}
|
||||
<link rel="stylesheet" href="{{ url_for('web.index') }}{{ url }}" type="text/css">{% endfor %}
|
||||
<link rel="stylesheet" href="{{ Env.get('web_base') }}{{ url }}" type="text/css">{% end %}
|
||||
|
||||
<link href="{{ url_for('web.static', filename='images/favicon.ico') }}" rel="icon" type="image/x-icon" />
|
||||
<link rel="apple-touch-icon" href="{{ url_for('web.static', filename='images/homescreen.png') }}" />
|
||||
<link href="{{ Env.get('static_path') }}images/favicon.ico" rel="icon" type="image/x-icon" />
|
||||
<link rel="apple-touch-icon" href="{{ Env.get('static_path') }}images/homescreen.png" />
|
||||
|
||||
<script type="text/javascript" src="https://www.youtube.com/player_api" defer="defer"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
|
||||
if($(window).getSize().x <= 480)
|
||||
window.addEvent('load', function() {
|
||||
|
||||
|
||||
setTimeout(function(){
|
||||
window.scrollTo(0, 1);
|
||||
window.scrollTo(0, 0);
|
||||
}, 100);
|
||||
|
||||
|
||||
});
|
||||
|
||||
window.addEvent('domready', function() {
|
||||
new Uniform();
|
||||
|
||||
Api.setup({
|
||||
'url': {{ url_for('api.index')|tojson|safe }},
|
||||
'path_sep': {{ sep|tojson|safe }},
|
||||
'url': {{ json_encode(Env.get('api_base')) }},
|
||||
'path_sep': {{ json_encode(sep) }},
|
||||
'is_remote': false
|
||||
});
|
||||
|
||||
@@ -61,29 +62,29 @@
|
||||
}
|
||||
|
||||
Quality.setup({
|
||||
'profiles': {{ fireEvent('profile.all', single = True)|tojson|safe }},
|
||||
'qualities': {{ fireEvent('quality.all', single = True)|tojson|safe }}
|
||||
'profiles': {{ json_encode(fireEvent('profile.all', single = True)) }},
|
||||
'qualities': {{ json_encode(fireEvent('quality.all', single = True)) }}
|
||||
});
|
||||
|
||||
Status.setup({{ fireEvent('status.all', single = True)|tojson|safe }});
|
||||
Status.setup({{ json_encode(fireEvent('status.all', single = True)) }});
|
||||
|
||||
File.Type.setup({{ fireEvent('file.types', single = True)|tojson|safe }});
|
||||
File.Type.setup({{ json_encode(fireEvent('file.types', single = True)) }});
|
||||
|
||||
App.setup({
|
||||
'base_url': {{ url_for('web.index')|tojson|safe }},
|
||||
'args': {{ env.get('args')|tojson|safe }},
|
||||
'options': {{ ('%s' % env.get('options'))|tojson|safe }},
|
||||
'app_dir': {{ env.get('app_dir')|tojson|safe }},
|
||||
'data_dir': {{ env.get('data_dir')|tojson|safe }},
|
||||
'pid': {{ env.getPid()|tojson|safe }},
|
||||
'userscript_version': {{ fireEvent('userscript.get_version', single = True)|tojson|safe }}
|
||||
'base_url': {{ json_encode(Env.get('web_base')) }},
|
||||
'args': {{ json_encode(Env.get('args')) }},
|
||||
'options': {{ json_encode(('%s' % Env.get('options'))) }},
|
||||
'app_dir': {{ json_encode(Env.get('app_dir')) }},
|
||||
'data_dir': {{ json_encode(Env.get('data_dir')) }},
|
||||
'pid': {{ json_encode(Env.getPid()) }},
|
||||
'userscript_version': {{ json_encode(fireEvent('userscript.get_version', single = True)) }}
|
||||
});
|
||||
})
|
||||
|
||||
{% if env.setting('show_wizard') %}
|
||||
{% if Env.setting('show_wizard') %}
|
||||
if(!window.location.href.contains('wizard'))
|
||||
window.location = '{{ url_for('web.index') }}wizard/'
|
||||
{% endif %}
|
||||
window.location = '{{ Env.get('web_base') }}wizard/'
|
||||
{% end %}
|
||||
|
||||
</script>
|
||||
<title>CouchPotato</title>
|
||||
|
||||
@@ -45,6 +45,8 @@ test -x $CP_DAEMON || exit 0
|
||||
|
||||
set -e
|
||||
|
||||
. /lib/lsb/init-functions
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
echo "Starting $DESC"
|
||||
@@ -63,9 +65,13 @@ case "$1" in
|
||||
start-stop-daemon --stop --pidfile $CP_PID_FILE --retry 15
|
||||
start-stop-daemon -d $CP_APP_PATH -c $CP_RUN_AS --start --background --pidfile $CP_PID_FILE --exec $CP_DAEMON -- $CP_DAEMON_OPTS
|
||||
;;
|
||||
|
||||
status)
|
||||
status_of_proc -p $CP_PID_FILE "$CP_DAEMON" "$NAME"
|
||||
;;
|
||||
*)
|
||||
N=/etc/init.d/$NAME
|
||||
echo "Usage: $N {start|stop|restart|force-reload}" >&2
|
||||
echo "Usage: $N {start|stop|restart|force-reload|status}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
262
libs/cache/__init__.py
vendored
Normal file
262
libs/cache/__init__.py
vendored
Normal file
@@ -0,0 +1,262 @@
|
||||
"""
|
||||
copied from
|
||||
werkzeug.contrib.cache
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from cache.posixemulation import rename
|
||||
from itertools import izip
|
||||
from time import time
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
try:
|
||||
from hashlib import md5
|
||||
except ImportError:
|
||||
from md5 import new as md5
|
||||
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except ImportError:
|
||||
import pickle
|
||||
|
||||
|
||||
def _items(mappingorseq):
|
||||
"""Wrapper for efficient iteration over mappings represented by dicts
|
||||
or sequences::
|
||||
|
||||
>>> for k, v in _items((i, i*i) for i in xrange(5)):
|
||||
... assert k*k == v
|
||||
|
||||
>>> for k, v in _items(dict((i, i*i) for i in xrange(5))):
|
||||
... assert k*k == v
|
||||
|
||||
"""
|
||||
return mappingorseq.iteritems() if hasattr(mappingorseq, 'iteritems') \
|
||||
else mappingorseq
|
||||
|
||||
|
||||
class BaseCache(object):
|
||||
"""Baseclass for the cache systems. All the cache systems implement this
|
||||
API or a superset of it.
|
||||
|
||||
:param default_timeout: the default timeout that is used if no timeout is
|
||||
specified on :meth:`set`.
|
||||
"""
|
||||
|
||||
def __init__(self, default_timeout = 300):
|
||||
self.default_timeout = default_timeout
|
||||
|
||||
def delete(self, key):
|
||||
"""Deletes `key` from the cache. If it does not exist in the cache
|
||||
nothing happens.
|
||||
|
||||
:param key: the key to delete.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_many(self, *keys):
|
||||
"""Returns a list of values for the given keys.
|
||||
For each key a item in the list is created. Example::
|
||||
|
||||
foo, bar = cache.get_many("foo", "bar")
|
||||
|
||||
If a key can't be looked up `None` is returned for that key
|
||||
instead.
|
||||
|
||||
:param keys: The function accepts multiple keys as positional
|
||||
arguments.
|
||||
"""
|
||||
return map(self.get, keys)
|
||||
|
||||
def get_dict(self, *keys):
|
||||
"""Works like :meth:`get_many` but returns a dict::
|
||||
|
||||
d = cache.get_dict("foo", "bar")
|
||||
foo = d["foo"]
|
||||
bar = d["bar"]
|
||||
|
||||
:param keys: The function accepts multiple keys as positional
|
||||
arguments.
|
||||
"""
|
||||
return dict(izip(keys, self.get_many(*keys)))
|
||||
|
||||
def set(self, key, value, timeout = None):
|
||||
"""Adds a new key/value to the cache (overwrites value, if key already
|
||||
exists in the cache).
|
||||
|
||||
:param key: the key to set
|
||||
:param value: the value for the key
|
||||
:param timeout: the cache timeout for the key (if not specified,
|
||||
it uses the default timeout).
|
||||
"""
|
||||
pass
|
||||
|
||||
def add(self, key, value, timeout = None):
|
||||
"""Works like :meth:`set` but does not overwrite the values of already
|
||||
existing keys.
|
||||
|
||||
:param key: the key to set
|
||||
:param value: the value for the key
|
||||
:param timeout: the cache timeout for the key or the default
|
||||
timeout if not specified.
|
||||
"""
|
||||
pass
|
||||
|
||||
def set_many(self, mapping, timeout = None):
|
||||
"""Sets multiple keys and values from a mapping.
|
||||
|
||||
:param mapping: a mapping with the keys/values to set.
|
||||
:param timeout: the cache timeout for the key (if not specified,
|
||||
it uses the default timeout).
|
||||
"""
|
||||
for key, value in _items(mapping):
|
||||
self.set(key, value, timeout)
|
||||
|
||||
def delete_many(self, *keys):
|
||||
"""Deletes multiple keys at once.
|
||||
|
||||
:param keys: The function accepts multiple keys as positional
|
||||
arguments.
|
||||
"""
|
||||
for key in keys:
|
||||
self.delete(key)
|
||||
|
||||
def clear(self):
|
||||
"""Clears the cache. Keep in mind that not all caches support
|
||||
completely clearing the cache.
|
||||
"""
|
||||
pass
|
||||
|
||||
def inc(self, key, delta = 1):
|
||||
"""Increments the value of a key by `delta`. If the key does
|
||||
not yet exist it is initialized with `delta`.
|
||||
|
||||
For supporting caches this is an atomic operation.
|
||||
|
||||
:param key: the key to increment.
|
||||
:param delta: the delta to add.
|
||||
"""
|
||||
self.set(key, (self.get(key) or 0) + delta)
|
||||
|
||||
def dec(self, key, delta = 1):
|
||||
"""Decrements the value of a key by `delta`. If the key does
|
||||
not yet exist it is initialized with `-delta`.
|
||||
|
||||
For supporting caches this is an atomic operation.
|
||||
|
||||
:param key: the key to increment.
|
||||
:param delta: the delta to subtract.
|
||||
"""
|
||||
self.set(key, (self.get(key) or 0) - delta)
|
||||
|
||||
|
||||
class FileSystemCache(BaseCache):
|
||||
"""A cache that stores the items on the file system. This cache depends
|
||||
on being the only user of the `cache_dir`. Make absolutely sure that
|
||||
nobody but this cache stores files there or otherwise the cache will
|
||||
randomly delete files therein.
|
||||
|
||||
:param cache_dir: the directory where cache files are stored.
|
||||
:param threshold: the maximum number of items the cache stores before
|
||||
it starts deleting some.
|
||||
:param default_timeout: the default timeout that is used if no timeout is
|
||||
specified on :meth:`~BaseCache.set`.
|
||||
:param mode: the file mode wanted for the cache files, default 0600
|
||||
"""
|
||||
|
||||
#: used for temporary files by the FileSystemCache
|
||||
_fs_transaction_suffix = '.__wz_cache'
|
||||
|
||||
def __init__(self, cache_dir, threshold = 500, default_timeout = 300, mode = 0600):
|
||||
BaseCache.__init__(self, default_timeout)
|
||||
self._path = cache_dir
|
||||
self._threshold = threshold
|
||||
self._mode = mode
|
||||
if not os.path.exists(self._path):
|
||||
os.makedirs(self._path)
|
||||
|
||||
def _list_dir(self):
|
||||
"""return a list of (fully qualified) cache filenames
|
||||
"""
|
||||
return [os.path.join(self._path, fn) for fn in os.listdir(self._path)
|
||||
if not fn.endswith(self._fs_transaction_suffix)]
|
||||
|
||||
def _prune(self):
|
||||
entries = self._list_dir()
|
||||
if len(entries) > self._threshold:
|
||||
now = time()
|
||||
for idx, fname in enumerate(entries):
|
||||
remove = False
|
||||
f = None
|
||||
try:
|
||||
try:
|
||||
f = open(fname, 'rb')
|
||||
expires = pickle.load(f)
|
||||
remove = expires <= now or idx % 3 == 0
|
||||
finally:
|
||||
if f is not None:
|
||||
f.close()
|
||||
except Exception:
|
||||
pass
|
||||
if remove:
|
||||
try:
|
||||
os.remove(fname)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
|
||||
def clear(self):
|
||||
for fname in self._list_dir():
|
||||
try:
|
||||
os.remove(fname)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
|
||||
def _get_filename(self, key):
|
||||
hash = md5(key).hexdigest()
|
||||
return os.path.join(self._path, hash)
|
||||
|
||||
def get(self, key):
|
||||
filename = self._get_filename(key)
|
||||
try:
|
||||
f = open(filename, 'rb')
|
||||
try:
|
||||
if pickle.load(f) >= time():
|
||||
return pickle.load(f)
|
||||
finally:
|
||||
f.close()
|
||||
os.remove(filename)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def add(self, key, value, timeout = None):
|
||||
filename = self._get_filename(key)
|
||||
if not os.path.exists(filename):
|
||||
self.set(key, value, timeout)
|
||||
|
||||
def set(self, key, value, timeout = None):
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
filename = self._get_filename(key)
|
||||
self._prune()
|
||||
try:
|
||||
fd, tmp = tempfile.mkstemp(suffix = self._fs_transaction_suffix,
|
||||
dir = self._path)
|
||||
f = os.fdopen(fd, 'wb')
|
||||
try:
|
||||
pickle.dump(int(time() + timeout), f, 1)
|
||||
pickle.dump(value, f, pickle.HIGHEST_PROTOCOL)
|
||||
finally:
|
||||
f.close()
|
||||
rename(tmp, filename)
|
||||
os.chmod(filename, self._mode)
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
|
||||
def delete(self, key):
|
||||
try:
|
||||
os.remove(self._get_filename(key))
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
@@ -1,44 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask
|
||||
~~~~~
|
||||
|
||||
A microframework based on Werkzeug. It's extensively documented
|
||||
and follows best practice patterns.
|
||||
|
||||
:copyright: (c) 2011 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
__version__ = '0.9'
|
||||
|
||||
# utilities we import from Werkzeug and Jinja2 that are unused
|
||||
# in the module but are exported as public interface.
|
||||
from werkzeug.exceptions import abort
|
||||
from werkzeug.utils import redirect
|
||||
from jinja2 import Markup, escape
|
||||
|
||||
from .app import Flask, Request, Response
|
||||
from .config import Config
|
||||
from .helpers import url_for, jsonify, json_available, flash, \
|
||||
send_file, send_from_directory, get_flashed_messages, \
|
||||
get_template_attribute, make_response, safe_join, \
|
||||
stream_with_context
|
||||
from .globals import current_app, g, request, session, _request_ctx_stack, \
|
||||
_app_ctx_stack
|
||||
from .ctx import has_request_context, has_app_context, \
|
||||
after_this_request
|
||||
from .module import Module
|
||||
from .blueprints import Blueprint
|
||||
from .templating import render_template, render_template_string
|
||||
|
||||
# the signals
|
||||
from .signals import signals_available, template_rendered, request_started, \
|
||||
request_finished, got_request_exception, request_tearing_down
|
||||
|
||||
# only import json if it's available
|
||||
if json_available:
|
||||
from .helpers import json
|
||||
|
||||
# backwards compat, goes away in 1.0
|
||||
from .sessions import SecureCookieSession as Session
|
||||
1701
libs/flask/app.py
1701
libs/flask/app.py
File diff suppressed because it is too large
Load Diff
@@ -1,345 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.blueprints
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Blueprints are the recommended way to implement larger or more
|
||||
pluggable applications in Flask 0.7 and later.
|
||||
|
||||
:copyright: (c) 2011 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from functools import update_wrapper
|
||||
|
||||
from .helpers import _PackageBoundObject, _endpoint_from_view_func
|
||||
|
||||
|
||||
class BlueprintSetupState(object):
|
||||
"""Temporary holder object for registering a blueprint with the
|
||||
application. An instance of this class is created by the
|
||||
:meth:`~flask.Blueprint.make_setup_state` method and later passed
|
||||
to all register callback functions.
|
||||
"""
|
||||
|
||||
def __init__(self, blueprint, app, options, first_registration):
|
||||
#: a reference to the current application
|
||||
self.app = app
|
||||
|
||||
#: a reference to the blueprint that created this setup state.
|
||||
self.blueprint = blueprint
|
||||
|
||||
#: a dictionary with all options that were passed to the
|
||||
#: :meth:`~flask.Flask.register_blueprint` method.
|
||||
self.options = options
|
||||
|
||||
#: as blueprints can be registered multiple times with the
|
||||
#: application and not everything wants to be registered
|
||||
#: multiple times on it, this attribute can be used to figure
|
||||
#: out if the blueprint was registered in the past already.
|
||||
self.first_registration = first_registration
|
||||
|
||||
subdomain = self.options.get('subdomain')
|
||||
if subdomain is None:
|
||||
subdomain = self.blueprint.subdomain
|
||||
|
||||
#: The subdomain that the blueprint should be active for, `None`
|
||||
#: otherwise.
|
||||
self.subdomain = subdomain
|
||||
|
||||
url_prefix = self.options.get('url_prefix')
|
||||
if url_prefix is None:
|
||||
url_prefix = self.blueprint.url_prefix
|
||||
|
||||
#: The prefix that should be used for all URLs defined on the
|
||||
#: blueprint.
|
||||
self.url_prefix = url_prefix
|
||||
|
||||
#: A dictionary with URL defaults that is added to each and every
|
||||
#: URL that was defined with the blueprint.
|
||||
self.url_defaults = dict(self.blueprint.url_values_defaults)
|
||||
self.url_defaults.update(self.options.get('url_defaults', ()))
|
||||
|
||||
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
|
||||
"""A helper method to register a rule (and optionally a view function)
|
||||
to the application. The endpoint is automatically prefixed with the
|
||||
blueprint's name.
|
||||
"""
|
||||
if self.url_prefix:
|
||||
rule = self.url_prefix + rule
|
||||
options.setdefault('subdomain', self.subdomain)
|
||||
if endpoint is None:
|
||||
endpoint = _endpoint_from_view_func(view_func)
|
||||
defaults = self.url_defaults
|
||||
if 'defaults' in options:
|
||||
defaults = dict(defaults, **options.pop('defaults'))
|
||||
self.app.add_url_rule(rule, '%s.%s' % (self.blueprint.name, endpoint),
|
||||
view_func, defaults=defaults, **options)
|
||||
|
||||
|
||||
class Blueprint(_PackageBoundObject):
|
||||
"""Represents a blueprint. A blueprint is an object that records
|
||||
functions that will be called with the
|
||||
:class:`~flask.blueprint.BlueprintSetupState` later to register functions
|
||||
or other things on the main application. See :ref:`blueprints` for more
|
||||
information.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
"""
|
||||
|
||||
warn_on_modifications = False
|
||||
_got_registered_once = False
|
||||
|
||||
def __init__(self, name, import_name, static_folder=None,
|
||||
static_url_path=None, template_folder=None,
|
||||
url_prefix=None, subdomain=None, url_defaults=None):
|
||||
_PackageBoundObject.__init__(self, import_name, template_folder)
|
||||
self.name = name
|
||||
self.url_prefix = url_prefix
|
||||
self.subdomain = subdomain
|
||||
self.static_folder = static_folder
|
||||
self.static_url_path = static_url_path
|
||||
self.deferred_functions = []
|
||||
self.view_functions = {}
|
||||
if url_defaults is None:
|
||||
url_defaults = {}
|
||||
self.url_values_defaults = url_defaults
|
||||
|
||||
def record(self, func):
|
||||
"""Registers a function that is called when the blueprint is
|
||||
registered on the application. This function is called with the
|
||||
state as argument as returned by the :meth:`make_setup_state`
|
||||
method.
|
||||
"""
|
||||
if self._got_registered_once and self.warn_on_modifications:
|
||||
from warnings import warn
|
||||
warn(Warning('The blueprint was already registered once '
|
||||
'but is getting modified now. These changes '
|
||||
'will not show up.'))
|
||||
self.deferred_functions.append(func)
|
||||
|
||||
def record_once(self, func):
|
||||
"""Works like :meth:`record` but wraps the function in another
|
||||
function that will ensure the function is only called once. If the
|
||||
blueprint is registered a second time on the application, the
|
||||
function passed is not called.
|
||||
"""
|
||||
def wrapper(state):
|
||||
if state.first_registration:
|
||||
func(state)
|
||||
return self.record(update_wrapper(wrapper, func))
|
||||
|
||||
def make_setup_state(self, app, options, first_registration=False):
|
||||
"""Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState`
|
||||
object that is later passed to the register callback functions.
|
||||
Subclasses can override this to return a subclass of the setup state.
|
||||
"""
|
||||
return BlueprintSetupState(self, app, options, first_registration)
|
||||
|
||||
def register(self, app, options, first_registration=False):
|
||||
"""Called by :meth:`Flask.register_blueprint` to register a blueprint
|
||||
on the application. This can be overridden to customize the register
|
||||
behavior. Keyword arguments from
|
||||
:func:`~flask.Flask.register_blueprint` are directly forwarded to this
|
||||
method in the `options` dictionary.
|
||||
"""
|
||||
self._got_registered_once = True
|
||||
state = self.make_setup_state(app, options, first_registration)
|
||||
if self.has_static_folder:
|
||||
state.add_url_rule(self.static_url_path + '/<path:filename>',
|
||||
view_func=self.send_static_file,
|
||||
endpoint='static')
|
||||
|
||||
for deferred in self.deferred_functions:
|
||||
deferred(state)
|
||||
|
||||
def route(self, rule, **options):
|
||||
"""Like :meth:`Flask.route` but for a blueprint. The endpoint for the
|
||||
:func:`url_for` function is prefixed with the name of the blueprint.
|
||||
"""
|
||||
def decorator(f):
|
||||
endpoint = options.pop("endpoint", f.__name__)
|
||||
self.add_url_rule(rule, endpoint, f, **options)
|
||||
return f
|
||||
return decorator
|
||||
|
||||
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
|
||||
"""Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for
|
||||
the :func:`url_for` function is prefixed with the name of the blueprint.
|
||||
"""
|
||||
if endpoint:
|
||||
assert '.' not in endpoint, "Blueprint endpoint's should not contain dot's"
|
||||
self.record(lambda s:
|
||||
s.add_url_rule(rule, endpoint, view_func, **options))
|
||||
|
||||
def endpoint(self, endpoint):
|
||||
"""Like :meth:`Flask.endpoint` but for a blueprint. This does not
|
||||
prefix the endpoint with the blueprint name, this has to be done
|
||||
explicitly by the user of this method. If the endpoint is prefixed
|
||||
with a `.` it will be registered to the current blueprint, otherwise
|
||||
it's an application independent endpoint.
|
||||
"""
|
||||
def decorator(f):
|
||||
def register_endpoint(state):
|
||||
state.app.view_functions[endpoint] = f
|
||||
self.record_once(register_endpoint)
|
||||
return f
|
||||
return decorator
|
||||
|
||||
def app_template_filter(self, name=None):
|
||||
"""Register a custom template filter, available application wide. Like
|
||||
:meth:`Flask.template_filter` but for a blueprint.
|
||||
|
||||
:param name: the optional name of the filter, otherwise the
|
||||
function name will be used.
|
||||
"""
|
||||
def decorator(f):
|
||||
self.add_app_template_filter(f, name=name)
|
||||
return f
|
||||
return decorator
|
||||
|
||||
def add_app_template_filter(self, f, name=None):
|
||||
"""Register a custom template filter, available application wide. Like
|
||||
:meth:`Flask.add_template_filter` but for a blueprint. Works exactly
|
||||
like the :meth:`app_template_filter` decorator.
|
||||
|
||||
:param name: the optional name of the filter, otherwise the
|
||||
function name will be used.
|
||||
"""
|
||||
def register_template(state):
|
||||
state.app.jinja_env.filters[name or f.__name__] = f
|
||||
self.record_once(register_template)
|
||||
|
||||
def before_request(self, f):
|
||||
"""Like :meth:`Flask.before_request` but for a blueprint. This function
|
||||
is only executed before each request that is handled by a function of
|
||||
that blueprint.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.before_request_funcs
|
||||
.setdefault(self.name, []).append(f))
|
||||
return f
|
||||
|
||||
def before_app_request(self, f):
|
||||
"""Like :meth:`Flask.before_request`. Such a function is executed
|
||||
before each request, even if outside of a blueprint.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.before_request_funcs
|
||||
.setdefault(None, []).append(f))
|
||||
return f
|
||||
|
||||
def before_app_first_request(self, f):
|
||||
"""Like :meth:`Flask.before_first_request`. Such a function is
|
||||
executed before the first request to the application.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.before_first_request_funcs.append(f))
|
||||
return f
|
||||
|
||||
def after_request(self, f):
|
||||
"""Like :meth:`Flask.after_request` but for a blueprint. This function
|
||||
is only executed after each request that is handled by a function of
|
||||
that blueprint.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.after_request_funcs
|
||||
.setdefault(self.name, []).append(f))
|
||||
return f
|
||||
|
||||
def after_app_request(self, f):
|
||||
"""Like :meth:`Flask.after_request` but for a blueprint. Such a function
|
||||
is executed after each request, even if outside of the blueprint.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.after_request_funcs
|
||||
.setdefault(None, []).append(f))
|
||||
return f
|
||||
|
||||
def teardown_request(self, f):
|
||||
"""Like :meth:`Flask.teardown_request` but for a blueprint. This
|
||||
function is only executed when tearing down requests handled by a
|
||||
function of that blueprint. Teardown request functions are executed
|
||||
when the request context is popped, even when no actual request was
|
||||
performed.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.teardown_request_funcs
|
||||
.setdefault(self.name, []).append(f))
|
||||
return f
|
||||
|
||||
def teardown_app_request(self, f):
|
||||
"""Like :meth:`Flask.teardown_request` but for a blueprint. Such a
|
||||
function is executed when tearing down each request, even if outside of
|
||||
the blueprint.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.teardown_request_funcs
|
||||
.setdefault(None, []).append(f))
|
||||
return f
|
||||
|
||||
def context_processor(self, f):
|
||||
"""Like :meth:`Flask.context_processor` but for a blueprint. This
|
||||
function is only executed for requests handled by a blueprint.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.template_context_processors
|
||||
.setdefault(self.name, []).append(f))
|
||||
return f
|
||||
|
||||
def app_context_processor(self, f):
|
||||
"""Like :meth:`Flask.context_processor` but for a blueprint. Such a
|
||||
function is executed each request, even if outside of the blueprint.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.template_context_processors
|
||||
.setdefault(None, []).append(f))
|
||||
return f
|
||||
|
||||
def app_errorhandler(self, code):
|
||||
"""Like :meth:`Flask.errorhandler` but for a blueprint. This
|
||||
handler is used for all requests, even if outside of the blueprint.
|
||||
"""
|
||||
def decorator(f):
|
||||
self.record_once(lambda s: s.app.errorhandler(code)(f))
|
||||
return f
|
||||
return decorator
|
||||
|
||||
def url_value_preprocessor(self, f):
|
||||
"""Registers a function as URL value preprocessor for this
|
||||
blueprint. It's called before the view functions are called and
|
||||
can modify the url values provided.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.url_value_preprocessors
|
||||
.setdefault(self.name, []).append(f))
|
||||
return f
|
||||
|
||||
def url_defaults(self, f):
|
||||
"""Callback function for URL defaults for this blueprint. It's called
|
||||
with the endpoint and values and should update the values passed
|
||||
in place.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.url_default_functions
|
||||
.setdefault(self.name, []).append(f))
|
||||
return f
|
||||
|
||||
def app_url_value_preprocessor(self, f):
|
||||
"""Same as :meth:`url_value_preprocessor` but application wide.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.url_value_preprocessors
|
||||
.setdefault(None, []).append(f))
|
||||
return f
|
||||
|
||||
def app_url_defaults(self, f):
|
||||
"""Same as :meth:`url_defaults` but application wide.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.url_default_functions
|
||||
.setdefault(None, []).append(f))
|
||||
return f
|
||||
|
||||
def errorhandler(self, code_or_exception):
|
||||
"""Registers an error handler that becomes active for this blueprint
|
||||
only. Please be aware that routing does not happen local to a
|
||||
blueprint so an error handler for 404 usually is not handled by
|
||||
a blueprint unless it is caused inside a view function. Another
|
||||
special case is the 500 internal server error which is always looked
|
||||
up from the application.
|
||||
|
||||
Otherwise works as the :meth:`~flask.Flask.errorhandler` decorator
|
||||
of the :class:`~flask.Flask` object.
|
||||
"""
|
||||
def decorator(f):
|
||||
self.record_once(lambda s: s.app._register_error_handler(
|
||||
self.name, code_or_exception, f))
|
||||
return f
|
||||
return decorator
|
||||
@@ -1,168 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.config
|
||||
~~~~~~~~~~~~
|
||||
|
||||
Implements the configuration related objects.
|
||||
|
||||
:copyright: (c) 2011 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import imp
|
||||
import os
|
||||
import errno
|
||||
|
||||
from werkzeug.utils import import_string
|
||||
|
||||
|
||||
class ConfigAttribute(object):
|
||||
"""Makes an attribute forward to the config"""
|
||||
|
||||
def __init__(self, name, get_converter=None):
|
||||
self.__name__ = name
|
||||
self.get_converter = get_converter
|
||||
|
||||
def __get__(self, obj, type=None):
|
||||
if obj is None:
|
||||
return self
|
||||
rv = obj.config[self.__name__]
|
||||
if self.get_converter is not None:
|
||||
rv = self.get_converter(rv)
|
||||
return rv
|
||||
|
||||
def __set__(self, obj, value):
|
||||
obj.config[self.__name__] = value
|
||||
|
||||
|
||||
class Config(dict):
|
||||
"""Works exactly like a dict but provides ways to fill it from files
|
||||
or special dictionaries. There are two common patterns to populate the
|
||||
config.
|
||||
|
||||
Either you can fill the config from a config file::
|
||||
|
||||
app.config.from_pyfile('yourconfig.cfg')
|
||||
|
||||
Or alternatively you can define the configuration options in the
|
||||
module that calls :meth:`from_object` or provide an import path to
|
||||
a module that should be loaded. It is also possible to tell it to
|
||||
use the same module and with that provide the configuration values
|
||||
just before the call::
|
||||
|
||||
DEBUG = True
|
||||
SECRET_KEY = 'development key'
|
||||
app.config.from_object(__name__)
|
||||
|
||||
In both cases (loading from any Python file or loading from modules),
|
||||
only uppercase keys are added to the config. This makes it possible to use
|
||||
lowercase values in the config file for temporary values that are not added
|
||||
to the config or to define the config keys in the same file that implements
|
||||
the application.
|
||||
|
||||
Probably the most interesting way to load configurations is from an
|
||||
environment variable pointing to a file::
|
||||
|
||||
app.config.from_envvar('YOURAPPLICATION_SETTINGS')
|
||||
|
||||
In this case before launching the application you have to set this
|
||||
environment variable to the file you want to use. On Linux and OS X
|
||||
use the export statement::
|
||||
|
||||
export YOURAPPLICATION_SETTINGS='/path/to/config/file'
|
||||
|
||||
On windows use `set` instead.
|
||||
|
||||
:param root_path: path to which files are read relative from. When the
|
||||
config object is created by the application, this is
|
||||
the application's :attr:`~flask.Flask.root_path`.
|
||||
:param defaults: an optional dictionary of default values
|
||||
"""
|
||||
|
||||
def __init__(self, root_path, defaults=None):
|
||||
dict.__init__(self, defaults or {})
|
||||
self.root_path = root_path
|
||||
|
||||
def from_envvar(self, variable_name, silent=False):
|
||||
"""Loads a configuration from an environment variable pointing to
|
||||
a configuration file. This is basically just a shortcut with nicer
|
||||
error messages for this line of code::
|
||||
|
||||
app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS'])
|
||||
|
||||
:param variable_name: name of the environment variable
|
||||
:param silent: set to `True` if you want silent failure for missing
|
||||
files.
|
||||
:return: bool. `True` if able to load config, `False` otherwise.
|
||||
"""
|
||||
rv = os.environ.get(variable_name)
|
||||
if not rv:
|
||||
if silent:
|
||||
return False
|
||||
raise RuntimeError('The environment variable %r is not set '
|
||||
'and as such configuration could not be '
|
||||
'loaded. Set this variable and make it '
|
||||
'point to a configuration file' %
|
||||
variable_name)
|
||||
return self.from_pyfile(rv, silent=silent)
|
||||
|
||||
def from_pyfile(self, filename, silent=False):
|
||||
"""Updates the values in the config from a Python file. This function
|
||||
behaves as if the file was imported as module with the
|
||||
:meth:`from_object` function.
|
||||
|
||||
:param filename: the filename of the config. This can either be an
|
||||
absolute filename or a filename relative to the
|
||||
root path.
|
||||
:param silent: set to `True` if you want silent failure for missing
|
||||
files.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
`silent` parameter.
|
||||
"""
|
||||
filename = os.path.join(self.root_path, filename)
|
||||
d = imp.new_module('config')
|
||||
d.__file__ = filename
|
||||
try:
|
||||
execfile(filename, d.__dict__)
|
||||
except IOError, e:
|
||||
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
|
||||
return False
|
||||
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
|
||||
raise
|
||||
self.from_object(d)
|
||||
return True
|
||||
|
||||
def from_object(self, obj):
|
||||
"""Updates the values from the given object. An object can be of one
|
||||
of the following two types:
|
||||
|
||||
- a string: in this case the object with that name will be imported
|
||||
- an actual object reference: that object is used directly
|
||||
|
||||
Objects are usually either modules or classes.
|
||||
|
||||
Just the uppercase variables in that object are stored in the config.
|
||||
Example usage::
|
||||
|
||||
app.config.from_object('yourapplication.default_config')
|
||||
from yourapplication import default_config
|
||||
app.config.from_object(default_config)
|
||||
|
||||
You should not use this function to load the actual configuration but
|
||||
rather configuration defaults. The actual config should be loaded
|
||||
with :meth:`from_pyfile` and ideally from a location not within the
|
||||
package because the package might be installed system wide.
|
||||
|
||||
:param obj: an import name or object
|
||||
"""
|
||||
if isinstance(obj, basestring):
|
||||
obj = import_string(obj)
|
||||
for key in dir(obj):
|
||||
if key.isupper():
|
||||
self[key] = getattr(obj, key)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s %s>' % (self.__class__.__name__, dict.__repr__(self))
|
||||
@@ -1,295 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.ctx
|
||||
~~~~~~~~~
|
||||
|
||||
Implements the objects required to keep the context.
|
||||
|
||||
:copyright: (c) 2011 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
from .globals import _request_ctx_stack, _app_ctx_stack
|
||||
from .module import blueprint_is_module
|
||||
|
||||
|
||||
class _RequestGlobals(object):
|
||||
"""A plain object."""
|
||||
pass
|
||||
|
||||
|
||||
def after_this_request(f):
|
||||
"""Executes a function after this request. This is useful to modify
|
||||
response objects. The function is passed the response object and has
|
||||
to return the same or a new one.
|
||||
|
||||
Example::
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
@after_this_request
|
||||
def add_header(response):
|
||||
response.headers['X-Foo'] = 'Parachute'
|
||||
return response
|
||||
return 'Hello World!'
|
||||
|
||||
This is more useful if a function other than the view function wants to
|
||||
modify a response. For instance think of a decorator that wants to add
|
||||
some headers without converting the return value into a response object.
|
||||
|
||||
.. versionadded:: 0.9
|
||||
"""
|
||||
_request_ctx_stack.top._after_request_functions.append(f)
|
||||
return f
|
||||
|
||||
|
||||
def has_request_context():
|
||||
"""If you have code that wants to test if a request context is there or
|
||||
not this function can be used. For instance, you may want to take advantage
|
||||
of request information if the request object is available, but fail
|
||||
silently if it is unavailable.
|
||||
|
||||
::
|
||||
|
||||
class User(db.Model):
|
||||
|
||||
def __init__(self, username, remote_addr=None):
|
||||
self.username = username
|
||||
if remote_addr is None and has_request_context():
|
||||
remote_addr = request.remote_addr
|
||||
self.remote_addr = remote_addr
|
||||
|
||||
Alternatively you can also just test any of the context bound objects
|
||||
(such as :class:`request` or :class:`g` for truthness)::
|
||||
|
||||
class User(db.Model):
|
||||
|
||||
def __init__(self, username, remote_addr=None):
|
||||
self.username = username
|
||||
if remote_addr is None and request:
|
||||
remote_addr = request.remote_addr
|
||||
self.remote_addr = remote_addr
|
||||
|
||||
.. versionadded:: 0.7
|
||||
"""
|
||||
return _request_ctx_stack.top is not None
|
||||
|
||||
|
||||
def has_app_context():
|
||||
"""Works like :func:`has_request_context` but for the application
|
||||
context. You can also just do a boolean check on the
|
||||
:data:`current_app` object instead.
|
||||
|
||||
.. versionadded:: 0.9
|
||||
"""
|
||||
return _app_ctx_stack.top is not None
|
||||
|
||||
|
||||
class AppContext(object):
|
||||
"""The application context binds an application object implicitly
|
||||
to the current thread or greenlet, similar to how the
|
||||
:class:`RequestContext` binds request information. The application
|
||||
context is also implicitly created if a request context is created
|
||||
but the application is not on top of the individual application
|
||||
context.
|
||||
"""
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
self.url_adapter = app.create_url_adapter(None)
|
||||
|
||||
# Like request context, app contexts can be pushed multiple times
|
||||
# but there a basic "refcount" is enough to track them.
|
||||
self._refcnt = 0
|
||||
|
||||
def push(self):
|
||||
"""Binds the app context to the current context."""
|
||||
self._refcnt += 1
|
||||
_app_ctx_stack.push(self)
|
||||
|
||||
def pop(self, exc=None):
|
||||
"""Pops the app context."""
|
||||
self._refcnt -= 1
|
||||
if self._refcnt <= 0:
|
||||
if exc is None:
|
||||
exc = sys.exc_info()[1]
|
||||
self.app.do_teardown_appcontext(exc)
|
||||
rv = _app_ctx_stack.pop()
|
||||
assert rv is self, 'Popped wrong app context. (%r instead of %r)' \
|
||||
% (rv, self)
|
||||
|
||||
def __enter__(self):
|
||||
self.push()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, tb):
|
||||
self.pop(exc_value)
|
||||
|
||||
|
||||
class RequestContext(object):
|
||||
"""The request context contains all request relevant information. It is
|
||||
created at the beginning of the request and pushed to the
|
||||
`_request_ctx_stack` and removed at the end of it. It will create the
|
||||
URL adapter and request object for the WSGI environment provided.
|
||||
|
||||
Do not attempt to use this class directly, instead use
|
||||
:meth:`~flask.Flask.test_request_context` and
|
||||
:meth:`~flask.Flask.request_context` to create this object.
|
||||
|
||||
When the request context is popped, it will evaluate all the
|
||||
functions registered on the application for teardown execution
|
||||
(:meth:`~flask.Flask.teardown_request`).
|
||||
|
||||
The request context is automatically popped at the end of the request
|
||||
for you. In debug mode the request context is kept around if
|
||||
exceptions happen so that interactive debuggers have a chance to
|
||||
introspect the data. With 0.4 this can also be forced for requests
|
||||
that did not fail and outside of `DEBUG` mode. By setting
|
||||
``'flask._preserve_context'`` to `True` on the WSGI environment the
|
||||
context will not pop itself at the end of the request. This is used by
|
||||
the :meth:`~flask.Flask.test_client` for example to implement the
|
||||
deferred cleanup functionality.
|
||||
|
||||
You might find this helpful for unittests where you need the
|
||||
information from the context local around for a little longer. Make
|
||||
sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in
|
||||
that situation, otherwise your unittests will leak memory.
|
||||
"""
|
||||
|
||||
def __init__(self, app, environ):
|
||||
self.app = app
|
||||
self.request = app.request_class(environ)
|
||||
self.url_adapter = app.create_url_adapter(self.request)
|
||||
self.g = app.request_globals_class()
|
||||
self.flashes = None
|
||||
self.session = None
|
||||
|
||||
# Request contexts can be pushed multiple times and interleaved with
|
||||
# other request contexts. Now only if the last level is popped we
|
||||
# get rid of them. Additionally if an application context is missing
|
||||
# one is created implicitly so for each level we add this information
|
||||
self._implicit_app_ctx_stack = []
|
||||
|
||||
# indicator if the context was preserved. Next time another context
|
||||
# is pushed the preserved context is popped.
|
||||
self.preserved = False
|
||||
|
||||
# Functions that should be executed after the request on the response
|
||||
# object. These will be called before the regular "after_request"
|
||||
# functions.
|
||||
self._after_request_functions = []
|
||||
|
||||
self.match_request()
|
||||
|
||||
# XXX: Support for deprecated functionality. This is going away with
|
||||
# Flask 1.0
|
||||
blueprint = self.request.blueprint
|
||||
if blueprint is not None:
|
||||
# better safe than sorry, we don't want to break code that
|
||||
# already worked
|
||||
bp = app.blueprints.get(blueprint)
|
||||
if bp is not None and blueprint_is_module(bp):
|
||||
self.request._is_old_module = True
|
||||
|
||||
def match_request(self):
|
||||
"""Can be overridden by a subclass to hook into the matching
|
||||
of the request.
|
||||
"""
|
||||
try:
|
||||
url_rule, self.request.view_args = \
|
||||
self.url_adapter.match(return_rule=True)
|
||||
self.request.url_rule = url_rule
|
||||
except HTTPException, e:
|
||||
self.request.routing_exception = e
|
||||
|
||||
def push(self):
|
||||
"""Binds the request context to the current context."""
|
||||
# If an exception ocurrs in debug mode or if context preservation is
|
||||
# activated under exception situations exactly one context stays
|
||||
# on the stack. The rationale is that you want to access that
|
||||
# information under debug situations. However if someone forgets to
|
||||
# pop that context again we want to make sure that on the next push
|
||||
# it's invalidated otherwise we run at risk that something leaks
|
||||
# memory. This is usually only a problem in testsuite since this
|
||||
# functionality is not active in production environments.
|
||||
top = _request_ctx_stack.top
|
||||
if top is not None and top.preserved:
|
||||
top.pop()
|
||||
|
||||
# Before we push the request context we have to ensure that there
|
||||
# is an application context.
|
||||
app_ctx = _app_ctx_stack.top
|
||||
if app_ctx is None or app_ctx.app != self.app:
|
||||
app_ctx = self.app.app_context()
|
||||
app_ctx.push()
|
||||
self._implicit_app_ctx_stack.append(app_ctx)
|
||||
else:
|
||||
self._implicit_app_ctx_stack.append(None)
|
||||
|
||||
_request_ctx_stack.push(self)
|
||||
|
||||
# Open the session at the moment that the request context is
|
||||
# available. This allows a custom open_session method to use the
|
||||
# request context (e.g. flask-sqlalchemy).
|
||||
self.session = self.app.open_session(self.request)
|
||||
if self.session is None:
|
||||
self.session = self.app.make_null_session()
|
||||
|
||||
def pop(self, exc=None):
|
||||
"""Pops the request context and unbinds it by doing that. This will
|
||||
also trigger the execution of functions registered by the
|
||||
:meth:`~flask.Flask.teardown_request` decorator.
|
||||
|
||||
.. versionchanged:: 0.9
|
||||
Added the `exc` argument.
|
||||
"""
|
||||
app_ctx = self._implicit_app_ctx_stack.pop()
|
||||
|
||||
clear_request = False
|
||||
if not self._implicit_app_ctx_stack:
|
||||
self.preserved = False
|
||||
if exc is None:
|
||||
exc = sys.exc_info()[1]
|
||||
self.app.do_teardown_request(exc)
|
||||
clear_request = True
|
||||
|
||||
rv = _request_ctx_stack.pop()
|
||||
assert rv is self, 'Popped wrong request context. (%r instead of %r)' \
|
||||
% (rv, self)
|
||||
|
||||
# get rid of circular dependencies at the end of the request
|
||||
# so that we don't require the GC to be active.
|
||||
if clear_request:
|
||||
rv.request.environ['werkzeug.request'] = None
|
||||
|
||||
# Get rid of the app as well if necessary.
|
||||
if app_ctx is not None:
|
||||
app_ctx.pop(exc)
|
||||
|
||||
def __enter__(self):
|
||||
self.push()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, tb):
|
||||
# do not pop the request stack if we are in debug mode and an
|
||||
# exception happened. This will allow the debugger to still
|
||||
# access the request object in the interactive shell. Furthermore
|
||||
# the context can be force kept alive for the test client.
|
||||
# See flask.testing for how this works.
|
||||
if self.request.environ.get('flask._preserve_context') or \
|
||||
(tb is not None and self.app.preserve_context_on_exception):
|
||||
self.preserved = True
|
||||
else:
|
||||
self.pop(exc_value)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s \'%s\' [%s] of %s>' % (
|
||||
self.__class__.__name__,
|
||||
self.request.url,
|
||||
self.request.method,
|
||||
self.app.name
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user