Merge branch 'refs/heads/develop' into desktop

Conflicts:
	version.py
This commit is contained in:
Ruud committed 2013-07-05 22:43:54 +02:00
commit acda664686
512 files changed
+18469 -60698

No files matched your search

+47 -46
View File
@@ -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')
+48 -20
View File
@@ -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)
+10 -11
View File
@@ -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
+1 -2
View File
@@ -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())
+12 -16
View File
@@ -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)
+30 -16
View File
@@ -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
+1 -1
View File
@@ -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 = [
+10 -1
View File
@@ -104,12 +104,21 @@ class NZBGet(Downloader):
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
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,
+80 -43
View File
@@ -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
+15 -9
View File
@@ -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()))
+4 -32
View File
@@ -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
+3 -2
View File
@@ -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
+1 -1
View File
@@ -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'):
+4 -3
View File
@@ -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()
+24 -17
View File
@@ -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,
})
}
+10 -10
View File
@@ -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__)
+4 -3
View File
@@ -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)
}
+11 -16
View File
@@ -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,
+36 -10
View File
@@ -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 []
+32 -18
View File
@@ -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
+4 -6
View File
@@ -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):
+10 -43
View File
@@ -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
+7 -15
View File
@@ -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()
})
}
+4 -4
View File
@@ -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()
+18 -23
View File
@@ -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
})
}
+9 -11
View File
@@ -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))
+44 -54
View File
@@ -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+']');
+67 -64
View File
@@ -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; }
+26 -11
View File
@@ -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();
+20 -26
View File
@@ -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):
+10 -13
View File
@@ -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):
+14 -19
View File
@@ -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)',
+14 -15
View File
@@ -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(' .', '.')
+4 -2
View File
@@ -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:
+1 -1
View File
@@ -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']))
+3 -5
View File
@@ -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',
+27 -20
View File
@@ -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()
+3 -4
View File
@@ -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()
+80 -10
View File
@@ -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
@@ -0,0 +1,84 @@
.suggestions {
}
.suggestions > h2 {
height: 40px;
}
.suggestions .movie_result {
display: inline-block;
width: 33.333%;
height: 150px;
}
@media all and (max-width: 960px) {
.suggestions .movie_result {
width: 50%;
}
}
@media all and (max-width: 600px) {
.suggestions .movie_result {
width: 100%;
}
}
.suggestions .movie_result .data {
left: 100px;
background: #4e5969;
border: none;
}
.suggestions .movie_result .data .info {
top: 15px;
left: 15px;
right: 15px;
}
.suggestions .movie_result .data .info h2 {
white-space: normal;
max-height: 120px;
font-size: 18px;
line-height: 18px;
}
.suggestions .movie_result .data .info .year {
position: static;
display: block;
margin: 5px 0 0;
padding: 0;
opacity: .6;
}
.suggestions .movie_result .data {
cursor: default;
}
.suggestions .movie_result .options {
left: 100px;
}
.suggestions .movie_result .thumbnail {
width: 100px;
}
.suggestions .movie_result .actions {
position: absolute;
bottom: 10px;
right: 10px;
display: none;
width: 120px;
}
.suggestions .movie_result:hover .actions {
display: block;
}
.suggestions .movie_result .data.open .actions {
display: none;
}
.suggestions .movie_result .actions a {
margin-left: 10px;
vertical-align: middle;
}
@@ -0,0 +1,102 @@
var SuggestList = new Class({
Implements: [Options, Events],
initialize: function(options){
var self = this;
self.setOptions(options);
self.create();
},
create: function(){
var self = this;
self.el = new Element('div.suggestions', {
'events': {
'click:relay(a.delete)': function(e, el){
(e).stop();
$(el).getParent('.movie_result').destroy();
Api.request('suggestion.ignore', {
'data': {
'imdb': el.get('data-ignore')
},
'onComplete': self.fill.bind(self)
});
}
}
}).grab(
new Element('h2', {
'text': 'You might like these'
})
);
self.api_request = Api.request('suggestion.view', {
'onComplete': self.fill.bind(self)
});
},
fill: function(json){
var self = this;
Object.each(json.suggestions, function(movie){
var m = new Block.Search.Item(movie, {
'onAdded': function(){
self.afterAdded(m, movie)
}
});
m.data_container.grab(
new Element('div.actions').adopt(
new Element('a.add.icon2', {
'title': 'Add movie with your default quality',
'data-add': movie.imdb,
'events': {
'click': m.showOptions.bind(m)
}
}),
$(new MA.IMDB(m)),
$(new MA.Trailer(m, {
'height': 150
})),
new Element('a.delete.icon2', {
'title': 'Don\'t suggest this movie again',
'data-ignore': movie.imdb
})
)
);
m.data_container.removeEvents('click');
$(m).inject(self.el);
});
},
afterAdded: function(m, movie){
var self = this;
setTimeout(function(){
$(m).destroy();
Api.request('suggestion.ignore', {
'data': {
'imdb': movie.imdb,
'remove_only': true
},
'onComplete': self.fill.bind(self)
});
}, 3000);
},
toElement: function(){
return this.el;
}
})
@@ -1,5 +1,7 @@
var includes = {{includes|tojson}};
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;
+35 -28
View File
@@ -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'))
+26 -9
View File
@@ -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
+28 -7
View File
@@ -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'),
@@ -0,0 +1,59 @@
from .main import AwesomeHD
def start():
return AwesomeHD()
config = [{
'name': 'awesomehd',
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'list': 'torrent_providers',
'name': 'Awesome-HD',
'description': 'See <a href="https://awesome-hd.net">AHD</a>',
'wizard': True,
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'passkey',
'default': '',
},
{
'name': 'only_internal',
'advanced': True,
'type': 'bool',
'default': 1,
'description': 'Only search for internal releases.'
},
{
'name': 'prefer_internal',
'advanced': True,
'type': 'bool',
'default': 1,
'description': 'Favors internal releases over non-internal releases.'
},
{
'name': 'favor',
'advanced': True,
'default': 'both',
'type': 'dropdown',
'values': [('Encodes & Remuxes', 'both'), ('Encodes', 'encode'), ('Remuxes', 'remux'), ('None', 'none')],
'description': 'Give extra scoring to encodes or remuxes.'
},
{
'name': 'extra_score',
'advanced': True,
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
},
],
},
],
}]
@@ -0,0 +1,64 @@
from bs4 import BeautifulSoup
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider
import re
import traceback
log = CPLog(__name__)
class AwesomeHD(TorrentProvider):
urls = {
'test' : 'https://awesome-hd.net/',
'detail' : 'https://awesome-hd.net/torrents.php?torrentid=%s',
'search' : 'https://awesome-hd.net/searchapi.php?action=imdbsearch&passkey=%s&imdb=%s&internal=%s',
'download' : 'https://awesome-hd.net/torrents.php?action=download&id=%s&authkey=%s&torrent_pass=%s',
}
http_time_between_calls = 1
def _search(self, movie, quality, results):
data = self.getHTMLData(self.urls['search'] % (self.conf('passkey'), movie['library']['identifier'], self.conf('only_internal')))
if data:
try:
soup = BeautifulSoup(data)
authkey = soup.find('authkey').get_text()
entries = soup.find_all('torrent')
for entry in entries:
torrentscore = 0
torrent_id = entry.find('id').get_text()
name = entry.find('name').get_text()
year = entry.find('year').get_text()
releasegroup = entry.find('releasegroup').get_text()
resolution = entry.find('resolution').get_text()
encoding = entry.find('encoding').get_text()
freeleech = entry.find('freeleech').get_text()
torrent_desc = '/ %s / %s / %s ' % (releasegroup, resolution, encoding)
if freeleech == '0.25' and self.conf('prefer_internal'):
torrent_desc += '/ Internal'
torrentscore += 200
if encoding == 'x264' and self.conf('favor') in ['encode', 'both']:
torrentscore += 300
if re.search('Remux', encoding) and self.conf('favor') in ['remux', 'both']:
torrentscore += 200
results.append({
'id': torrent_id,
'name': re.sub('[^A-Za-z0-9\-_ \(\).]+', '', '%s (%s) %s' % (name, year, torrent_desc)),
'url': self.urls['download'] % (torrent_id, authkey, self.conf('passkey')),
'detail_url': self.urls['detail'] % torrent_id,
'size': self.parseSize(entry.find('size').get_text()),
'seeders': tryInt(entry.find('seeders').get_text()),
'leechers': tryInt(entry.find('leechers').get_text()),
'score': torrentscore
})
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
@@ -16,6 +16,7 @@ class HDBits(TorrentProvider):
'detail' : 'https://hdbits.org/details.php?id=%s&source=browse',
'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
@@ -0,0 +1,42 @@
from .main import TorrentBytes
def start():
return TorrentBytes()
config = [{
'name': 'torrentbytes',
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'list': 'torrent_providers',
'name': 'TorrentBytes',
'description': 'See <a href="http://torrentbytes.net">TorrentBytes</a>',
'wizard': True,
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
{
'name': 'username',
'default': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
}
],
},
],
}]
@@ -0,0 +1,82 @@
from bs4 import BeautifulSoup
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider
import traceback
log = CPLog(__name__)
class TorrentBytes(TorrentProvider):
urls = {
'test' : 'https://www.torrentbytes.net/',
'login' : 'https://www.torrentbytes.net/takelogin.php',
'login_check' : 'https://www.torrentbytes.net/inbox.php',
'detail' : 'https://www.torrentbytes.net/details.php?id=%s',
'search' : 'https://www.torrentbytes.net/browse.php?search=%s&cat=%d',
'download' : 'https://www.torrentbytes.net/download.php?id=%s&name=%s',
}
cat_ids = [
([5], ['720p', '1080p']),
([19], ['cam']),
([19], ['ts', 'tc']),
([19], ['r5', 'scr']),
([19], ['dvdrip']),
([5], ['brrip']),
([20], ['dvdr']),
]
http_time_between_calls = 1 #seconds
cat_backup_id = None
def _searchOnTitle(self, title, movie, quality, results):
url = self.urls['search'] % (tryUrlencode('%s %s' % (title.replace(':', ''), movie['library']['year'])), self.getCatId(quality['identifier'])[0])
data = self.getHTMLData(url, opener = self.login_opener)
if data:
html = BeautifulSoup(data)
try:
result_table = html.find('table', attrs = {'border' : '1'})
if not result_table:
return
entries = result_table.find_all('tr')
for result in entries[1:]:
cells = result.find_all('td')
link = cells[1].find('a', attrs = {'class' : 'index'})
full_id = link['href'].replace('details.php?id=', '')
torrent_id = full_id[:6]
results.append({
'id': torrent_id,
'name': link.contents[0],
'url': self.urls['download'] % (torrent_id, link.contents[0]),
'detail_url': self.urls['detail'] % torrent_id,
'size': self.parseSize(cells[6].contents[0] + cells[6].contents[2]),
'seeders': tryInt(cells[8].find('span').contents[0]),
'leechers': tryInt(cells[9].find('span').contents[0]),
})
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self):
return tryUrlencode({
'username': self.conf('username'),
'password': self.conf('password'),
'login': 'submit',
})
def loginSuccess(self, output):
return 'logout.php' in output.lower() or 'Welcome' in output.lower()
loginCheckSuccess = loginSuccess
@@ -10,7 +10,8 @@ class TorrentDay(TorrentProvider):
urls = {
'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
@@ -0,0 +1,33 @@
from main import Yify
def start():
return Yify()
config = [{
'name': 'yify',
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'list': 'torrent_providers',
'name': 'Yify',
'description': 'Free provider, less accurate. Small HD movies, encoded by <a href="https://yify-torrents.com/">Yify</a>.',
'wizard': False,
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': 0
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
],
}
]
}]
@@ -0,0 +1,53 @@
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider
import traceback
log = CPLog(__name__)
class Yify(TorrentProvider):
urls = {
'test' : 'https://yify-torrents.com/api',
'search' : 'https://yify-torrents.com/api/list.json?keywords=%s&quality=%s',
'detail': 'https://yify-torrents.com/api/movie.json?id=%s'
}
http_time_between_calls = 1 #seconds
def search(self, movie, quality):
if not quality.get('hd', False):
return []
return super(Yify, self).search(movie, quality)
def _searchOnTitle(self, title, movie, quality, results):
data = self.getJsonData(self.urls['search'] % (title, quality['identifier']))
if data and data.get('MovieList'):
try:
for result in data.get('MovieList'):
try:
title = result['TorrentUrl'].split('/')[-1][:-8].replace('_', '.').strip('._')
title = title.replace('.-.', '-')
title = title.replace('..', '.')
except:
continue
results.append({
'id': result['MovieID'],
'name': title,
'url': result['TorrentUrl'],
'detail_url': self.urls['detail'] % result['MovieID'],
'size': self.parseSize(result['Size']),
'seeders': tryInt(result['TorrentSeeds']),
'leechers': tryInt(result['TorrentPeers'])
})
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
+9 -12
View File
@@ -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
+32
View File
@@ -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
+1
View File
@@ -11,6 +11,7 @@ class Env(object):
_appname = 'CouchPotato'
''' Environment variables '''
_app = None
_encoding = 'UTF-8'
_debug = False
_dev = False
+75 -54
View File
@@ -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

+4 -16
View File
@@ -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
+1
View File
@@ -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"; }
+13 -12
View File
@@ -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)&amp;u=md5(username)">{{ url_for('web.index') }}getkey/?p=md5(password)&amp;u=md5(username)</a></pre>
<pre><a href="{{ Env.get('web_base') }}getkey/?p=md5(password)&amp;u=md5(username)">{{ Env.get('web_base') }}getkey/?p=md5(password)&amp;u=md5(username)</a></pre>
Will return {"api_key": "XXXXXXXXXX", "success": true}. When username or password is empty you don't need to md5 it.
<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>
+26 -25
View File
@@ -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>