Notifications
This commit is contained in:
+2
-1
@@ -1,2 +1,3 @@
|
||||
/settings.conf
|
||||
/logs/*.log
|
||||
/logs/*.log
|
||||
/_source/
|
||||
@@ -1,4 +1,5 @@
|
||||
from couchpotato.core.auth import requires_auth
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from flask.app import Flask
|
||||
@@ -30,7 +31,7 @@ def get_engine():
|
||||
@web.route('/')
|
||||
@requires_auth
|
||||
def index():
|
||||
return render_template('index.html', sep = os.sep)
|
||||
return render_template('index.html', sep = os.sep, fireEvent = fireEvent)
|
||||
|
||||
@app.errorhandler(404)
|
||||
def page_not_found(error):
|
||||
|
||||
+4
-6
@@ -1,6 +1,7 @@
|
||||
from argparse import ArgumentParser
|
||||
from couchpotato import web
|
||||
from couchpotato.api import api
|
||||
from couchpotato.core.event import fireEvent
|
||||
from libs.daemon import createDaemon
|
||||
from logging import handlers
|
||||
from werkzeug.contrib.cache import FileSystemCache
|
||||
@@ -49,7 +50,7 @@ def cmd_couchpotato(base_path, args):
|
||||
Env.set('data_dir', options.data_dir)
|
||||
Env.set('db_path', 'sqlite:///' + os.path.join(options.data_dir, 'couchpotato.db'))
|
||||
Env.set('cache_dir', os.path.join(options.data_dir, 'cache'))
|
||||
Env.set('cache', FileSystemCache(Env.get('cache_dir')))
|
||||
Env.set('cache', FileSystemCache(os.path.join(Env.get('cache_dir'), 'python')))
|
||||
Env.set('quiet', options.quiet)
|
||||
Env.set('daemonize', options.daemonize)
|
||||
Env.set('args', args)
|
||||
@@ -97,6 +98,7 @@ def cmd_couchpotato(base_path, args):
|
||||
from migrate.versioning.api import version_control, db_version, version, upgrade
|
||||
db = Env.get('db_path')
|
||||
repo = os.path.join('couchpotato', 'core', 'migration')
|
||||
logging.getLogger('migrate').setLevel(logging.WARNING) # Disable logging for migration
|
||||
|
||||
latest_db_version = version(repo)
|
||||
|
||||
@@ -114,6 +116,7 @@ def cmd_couchpotato(base_path, args):
|
||||
from couchpotato.core.settings.model import setup
|
||||
setup()
|
||||
|
||||
fireEvent('app.load')
|
||||
|
||||
# Create app
|
||||
from couchpotato import app
|
||||
@@ -128,11 +131,6 @@ def cmd_couchpotato(base_path, args):
|
||||
app.secret_key = api_key
|
||||
app.static_path = url_base + '/static'
|
||||
|
||||
# Add static url with url_base
|
||||
app.add_url_rule(app.static_path + '/<path:filename>',
|
||||
endpoint = 'static',
|
||||
view_func = app.send_static_file)
|
||||
|
||||
# Register modules
|
||||
app.register_module(web, url_prefix = '%s/' % url_base)
|
||||
app.register_module(api, url_prefix = '%s/%s/' % (url_base, api_key if not debug else 'api'))
|
||||
|
||||
@@ -9,35 +9,28 @@ config = [{
|
||||
{
|
||||
'tab': 'general',
|
||||
'name': 'basics',
|
||||
'label': 'Basics',
|
||||
'description': 'Needs restart before changes take effect.',
|
||||
'options': [
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
'type': 'string',
|
||||
'label': 'Username',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'password': True,
|
||||
'type': 'string',
|
||||
'label': 'Password',
|
||||
'type': 'password',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'advanced': True,
|
||||
'default': '0.0.0.0',
|
||||
'type': 'string',
|
||||
'label': 'Host',
|
||||
'label': 'IP',
|
||||
'description': 'Host that I should listen to. "0.0.0.0" listens to all ips.',
|
||||
},
|
||||
{
|
||||
'name': 'port',
|
||||
'default': 5000,
|
||||
'type': 'int',
|
||||
'label': 'Port',
|
||||
'description': 'The port I should listen to.',
|
||||
},
|
||||
{
|
||||
@@ -52,14 +45,12 @@ config = [{
|
||||
{
|
||||
'tab': 'general',
|
||||
'name': 'advanced',
|
||||
'label': 'Advanced',
|
||||
'description': "For those who know what the're doing",
|
||||
'advanced': True,
|
||||
'options': [
|
||||
{
|
||||
'name': 'api_key',
|
||||
'default': uuid4().hex,
|
||||
'type': 'string',
|
||||
'readonly': True,
|
||||
'label': 'Api Key',
|
||||
'description': "This is top-secret! Don't share this!",
|
||||
@@ -74,9 +65,8 @@ config = [{
|
||||
{
|
||||
'name': 'url_base',
|
||||
'default': '',
|
||||
'type': 'string',
|
||||
'label': 'Url Base',
|
||||
'description': 'When using mod_proxy use this to prepend the url with this.',
|
||||
'description': 'When using mod_proxy use this to append the url with this.',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
|
||||
class Downloader(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('download', self.download)
|
||||
|
||||
def download(self, data = {}):
|
||||
pass
|
||||
|
||||
def conf(self, attr):
|
||||
return Env.setting(attr, self.__class__.__name__.lower())
|
||||
|
||||
def isDisabled(self):
|
||||
return not self.isEnabled()
|
||||
|
||||
def isEnabled(self):
|
||||
return self.conf('enabled', True)
|
||||
@@ -0,0 +1,32 @@
|
||||
from .main import Blackhole
|
||||
|
||||
def start():
|
||||
return Blackhole()
|
||||
|
||||
config = [{
|
||||
'name': 'blackhole',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'name': 'blackhole',
|
||||
'label': 'Black hole',
|
||||
'description': 'Fill in your Sabnzbd settings.',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': False,
|
||||
'type': 'bool',
|
||||
'label': 'Enabled',
|
||||
'description': 'Send snatched NZBs to Sabnzbd',
|
||||
},
|
||||
{
|
||||
'name': 'directory',
|
||||
'default': '',
|
||||
'type': 'directory',
|
||||
'label': 'Directory',
|
||||
'description': 'Directory where the .nzb (or .torrent) file is saved to.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -0,0 +1,36 @@
|
||||
from __future__ import with_statement
|
||||
from couchpotato.core.helpers.encoding import toSafeString
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
import os
|
||||
import urllib
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
class Blackhole(Downloader):
|
||||
|
||||
type = ['nzb', 'torrent']
|
||||
|
||||
def download(self, data = {}):
|
||||
|
||||
if self.isDisabled() or not self.isCorrectType(data.get('type')):
|
||||
return
|
||||
|
||||
directory = self.conf('directory')
|
||||
|
||||
if not directory or not os.path.isdir(directory):
|
||||
log.error('No directory set for blackhole %s download.' % data.get('type'))
|
||||
else:
|
||||
fullPath = os.path.join(directory, toSafeString(data.get('name')) + '.' + data)
|
||||
|
||||
if not os.path.isfile(fullPath):
|
||||
log.info('Downloading %s to %s.' % (data.get('type'), fullPath))
|
||||
file = urllib.urlopen(data.get('url')).read()
|
||||
with open(fullPath, 'wb') as f:
|
||||
f.write(file)
|
||||
|
||||
return True
|
||||
else:
|
||||
log.error('File %s already exists.' % fullPath)
|
||||
|
||||
return False
|
||||
@@ -0,0 +1,39 @@
|
||||
from .main import Sabnzbd
|
||||
|
||||
def start():
|
||||
return Sabnzbd()
|
||||
|
||||
config = [{
|
||||
'name': 'sabnzbd',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'downloaders',
|
||||
'name': 'sabnzbd',
|
||||
'label': 'Sabnzbd',
|
||||
'description': 'Fill in your Sabnzbd settings.',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': False,
|
||||
'type': 'bool',
|
||||
'label': 'Enabled',
|
||||
'description': 'Send snatched NZBs to Sabnzbd',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost:8080',
|
||||
'type': 'string',
|
||||
'label': 'Host',
|
||||
'description': 'Test',
|
||||
},
|
||||
{
|
||||
'name': 'api_key',
|
||||
'default': '',
|
||||
'type': 'string',
|
||||
'label': 'Api Key',
|
||||
'description': 'Used for all calls to Sabnzbd.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -0,0 +1,120 @@
|
||||
from couchpotato.core.downloaders.base import Downloader
|
||||
from couchpotato.core.helpers.variable import cleanHost
|
||||
from couchpotato.core.logger import CPLog
|
||||
from tempfile import mkstemp
|
||||
from urllib import urlencode
|
||||
import base64
|
||||
import os
|
||||
import re
|
||||
import urllib2
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
class Sabnzbd(Downloader):
|
||||
|
||||
type = ['nzb']
|
||||
|
||||
def download(self, data = {}):
|
||||
|
||||
if self.isDisabled() or not self.isCorrectType(data.get('type')):
|
||||
return
|
||||
|
||||
log.info("Sending '%s' to SABnzbd." % data.get('name'))
|
||||
|
||||
if self.conf('ppDir') and data.get('imdb_id'):
|
||||
try:
|
||||
pp_script_fn = self.buildPp(data.get('imdb_id'))
|
||||
except:
|
||||
log.info("Failed to create post-processing script.")
|
||||
pp_script_fn = False
|
||||
if not pp_script_fn:
|
||||
pp = False
|
||||
else:
|
||||
pp = True
|
||||
else:
|
||||
pp = False
|
||||
|
||||
params = {
|
||||
'apikey': self.conf('apikey'),
|
||||
'cat': self.conf('category'),
|
||||
'mode': 'addurl',
|
||||
'name': data.get('url')
|
||||
}
|
||||
|
||||
# sabNzbd complains about "invalid archive file" for newzbin urls
|
||||
# added using addurl, works fine with addid
|
||||
if data.get('addbyid'):
|
||||
params['mode'] = 'addid'
|
||||
|
||||
if pp:
|
||||
params['script'] = pp_script_fn
|
||||
|
||||
url = cleanHost(self.conf('host')) + "api?" + urlencode(params)
|
||||
log.info("URL: " + url)
|
||||
|
||||
try:
|
||||
r = urllib2.urlopen(url, timeout = 30)
|
||||
except:
|
||||
log.error("Unable to connect to SAB.")
|
||||
return False
|
||||
|
||||
result = r.read().strip()
|
||||
if not result:
|
||||
log.error("SABnzbd didn't return anything.")
|
||||
return False
|
||||
|
||||
log.debug("Result text from SAB: " + result)
|
||||
if result == "ok":
|
||||
log.info("NZB sent to SAB successfully.")
|
||||
return True
|
||||
elif result == "Missing authentication":
|
||||
log.error("Incorrect username/password.")
|
||||
return False
|
||||
else:
|
||||
log.error("Unknown error: " + result)
|
||||
return False
|
||||
|
||||
def buildPp(self, imdb_id):
|
||||
|
||||
pp_script_path = self.getPpFile()
|
||||
|
||||
scriptB64 = '''IyEvdXNyL2Jpbi9weXRob24KaW1wb3J0IG9zCmltcG9ydCBzeXMKcHJpbnQgIkNyZWF0aW5nIGNwLmNw
|
||||
bmZvIGZvciAlcyIgJSBzeXMuYXJndlsxXQppbWRiSWQgPSB7W0lNREJJREhFUkVdfQpwYXRoID0gb3Mu
|
||||
cGF0aC5qb2luKHN5cy5hcmd2WzFdLCAiY3AuY3BuZm8iKQp0cnk6CiBmID0gb3BlbihwYXRoLCAndycp
|
||||
CmV4Y2VwdCBJT0Vycm9yOgogcHJpbnQgIlVuYWJsZSB0byBvcGVuICVzIGZvciB3cml0aW5nIiAlIHBh
|
||||
dGgKIHN5cy5leGl0KDEpCnRyeToKIGYud3JpdGUob3MucGF0aC5iYXNlbmFtZShzeXMuYXJndlswXSkr
|
||||
IlxuIitpbWRiSWQpCmV4Y2VwdDoKIHByaW50ICJVbmFibGUgdG8gd3JpdGUgdG8gZmlsZTogJXMiICUg
|
||||
cGF0aAogc3lzLmV4aXQoMikKZi5jbG9zZSgpCnByaW50ICJXcm90ZSBpbWRiIGlkLCAlcywgdG8gZmls
|
||||
ZTogJXMiICUgKGltZGJJZCwgcGF0aCkK'''
|
||||
|
||||
script = re.sub(r"\{\[IMDBIDHERE\]\}", "'%s'" % imdb_id, base64.b64decode(scriptB64))
|
||||
|
||||
try:
|
||||
f = open(pp_script_path, 'wb')
|
||||
except:
|
||||
log.info("Unable to open post-processing script for writing. Check permissions: %s" % pp_script_path)
|
||||
return False
|
||||
|
||||
try:
|
||||
f.write(script)
|
||||
f.close()
|
||||
except:
|
||||
log.info("Unable to write to post-processing script. Check permissions: %s" % pp_script_path)
|
||||
return False
|
||||
|
||||
log.info("Wrote post-processing script to: %s" % pp_script_path)
|
||||
|
||||
return os.path.basename(pp_script_path)
|
||||
|
||||
def getPpFile(self):
|
||||
|
||||
pp_script_handle, pp_script_path = mkstemp(suffix = '.py', dir = self.conf('ppDir'))
|
||||
pp_sh = os.fdopen(pp_script_handle)
|
||||
pp_sh.close()
|
||||
|
||||
try:
|
||||
os.chmod(pp_script_path, int('777', 8))
|
||||
except:
|
||||
log.info("Unable to set post-processing script permissions to 777 (may still work correctly): %s" % pp_script_path)
|
||||
|
||||
return pp_script_path
|
||||
@@ -1,4 +1,5 @@
|
||||
from axl.axel import Event
|
||||
from couchpotato.core.helpers.variable import merge_dicts
|
||||
from couchpotato.core.logger import CPLog
|
||||
import traceback
|
||||
|
||||
@@ -19,31 +20,64 @@ def removeEvent(name, handler):
|
||||
e -= handler
|
||||
|
||||
def fireEvent(name, *args, **kwargs):
|
||||
log.debug('Firing "%s": %s, %s' % (name, args, kwargs))
|
||||
try:
|
||||
|
||||
# Return single handler
|
||||
single = False
|
||||
try:
|
||||
del kwargs['single']
|
||||
single = True
|
||||
except: pass
|
||||
|
||||
# Merge items
|
||||
merge = False
|
||||
try:
|
||||
del kwargs['merge']
|
||||
merge = True
|
||||
except: pass
|
||||
|
||||
e = events[name]
|
||||
e.asynchronous = False
|
||||
result = e(*args, **kwargs)
|
||||
|
||||
results = []
|
||||
for r in result:
|
||||
if r[0] == True:
|
||||
results.append(r[1])
|
||||
else:
|
||||
etype, value, tb = r[1]
|
||||
log.debug(''.join(traceback.format_exception(etype, value, tb)))
|
||||
if single and not merge:
|
||||
results = result[0][1]
|
||||
else:
|
||||
results = []
|
||||
for r in result:
|
||||
if r[0] == True:
|
||||
results.append(r[1])
|
||||
else:
|
||||
errorHandler(r[1])
|
||||
|
||||
# Merge the results
|
||||
if merge:
|
||||
merged = {}
|
||||
for result in results:
|
||||
merged = merge_dicts(merged, result)
|
||||
|
||||
results = merged
|
||||
|
||||
return results
|
||||
except Exception, e:
|
||||
log.debug(e)
|
||||
log.error('%s: %s' % (name, e))
|
||||
|
||||
def fireEventAsync(name, *args, **kwargs):
|
||||
log.debug('Async "%s": %s, %s' % (name, args, kwargs))
|
||||
try:
|
||||
e = events[name]
|
||||
e.asynchronous = True
|
||||
e.error_handler = errorHandler
|
||||
|
||||
e(*args, **kwargs)
|
||||
return True
|
||||
except Exception, e:
|
||||
log.debug(e)
|
||||
log.error('%s: %s' % (name, e))
|
||||
|
||||
def errorHandler(error):
|
||||
etype, value, tb = error
|
||||
log.error(''.join(traceback.format_exception(etype, value, tb)))
|
||||
|
||||
def getEvent(name):
|
||||
return events[name]
|
||||
|
||||
@@ -5,16 +5,19 @@ import unicodedata
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
def toSafeString(original):
|
||||
valid_chars = "-_.() %s%s" % (ascii_letters, digits)
|
||||
cleanedFilename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
|
||||
return ''.join(c for c in cleanedFilename if c in valid_chars)
|
||||
|
||||
|
||||
def simplifyString(original):
|
||||
string = toSafeString(original)
|
||||
split = re.split('\W+', string.lower())
|
||||
return toUnicode(' '.join(split))
|
||||
|
||||
|
||||
def toUnicode(original, *args):
|
||||
try:
|
||||
if type(original) is unicode:
|
||||
|
||||
@@ -42,7 +42,7 @@ def dictToList(params):
|
||||
new = {}
|
||||
for x, value in params.iteritems():
|
||||
try:
|
||||
new_value = [dictToList(value2) for value2 in value.itervalues()]
|
||||
new_value = [dictToList(value[k]) for k in sorted(value.iterkeys())]
|
||||
except:
|
||||
new_value = value
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import hashlib
|
||||
import os.path
|
||||
|
||||
def is_dict(object):
|
||||
return isinstance(object, dict)
|
||||
|
||||
|
||||
def merge_dicts(a, b):
|
||||
assert is_dict(a), is_dict(b)
|
||||
dst = a.copy()
|
||||
|
||||
stack = [(dst, b)]
|
||||
while stack:
|
||||
current_dst, current_src = stack.pop()
|
||||
for key in current_src:
|
||||
if key not in current_dst:
|
||||
current_dst[key] = current_src[key]
|
||||
else:
|
||||
if is_dict(current_src[key]) and is_dict(current_dst[key]) :
|
||||
stack.append((current_dst[key], current_src[key]))
|
||||
else:
|
||||
current_dst[key] = current_src[key]
|
||||
return dst
|
||||
|
||||
def md5(text):
|
||||
return hashlib.md5(text).hexdigest()
|
||||
|
||||
def getExt(filename):
|
||||
return os.path.splitext(filename)[1][1:]
|
||||
|
||||
def cleanHost(host):
|
||||
if not host.startswith(('http://', 'https://')):
|
||||
host = 'http://' + host
|
||||
|
||||
if not host.endswith('/'):
|
||||
host += '/'
|
||||
|
||||
return host
|
||||
@@ -1,4 +1,4 @@
|
||||
from couchpotato.core.event import fireEvent, fireEventAsync
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
import glob
|
||||
import os
|
||||
@@ -17,6 +17,8 @@ class Loader:
|
||||
self.paths = {
|
||||
'plugin' : ('couchpotato.core.plugins', os.path.join(root, 'couchpotato', 'core', 'plugins')),
|
||||
'provider' : ('couchpotato.core.providers', os.path.join(root, 'couchpotato', 'core', 'providers')),
|
||||
'notifications' : ('couchpotato.core.notifications', os.path.join(root, 'couchpotato', 'core', 'notifications')),
|
||||
'downloaders' : ('couchpotato.core.downloaders', os.path.join(root, 'couchpotato', 'core', 'downloaders')),
|
||||
}
|
||||
|
||||
for type, tuple in self.paths.iteritems():
|
||||
@@ -28,14 +30,17 @@ class Loader:
|
||||
for module_name, plugin in sorted(self.modules.iteritems()):
|
||||
|
||||
# Load module
|
||||
m = getattr(self.loadModule(module_name), plugin.get('name'))
|
||||
try:
|
||||
m = getattr(self.loadModule(module_name), plugin.get('name'))
|
||||
|
||||
log.info("Loading '%s'" % module_name)
|
||||
log.info("Loading %s: %s" % (plugin['type'], plugin['name']))
|
||||
|
||||
# Save default settings for plugin/provider
|
||||
did_save += self.loadSettings(m, module_name, save = False)
|
||||
# Save default settings for plugin/provider
|
||||
did_save += self.loadSettings(m, module_name, save = False)
|
||||
|
||||
self.loadPlugins(m, plugin.get('name'))
|
||||
self.loadPlugins(m, plugin.get('name'))
|
||||
except Exception, e:
|
||||
log.error(e)
|
||||
|
||||
if did_save:
|
||||
fireEvent('settings.save')
|
||||
@@ -51,12 +56,12 @@ class Loader:
|
||||
def loadSettings(self, module, name, save = True):
|
||||
try:
|
||||
for section in module.config:
|
||||
fireEventAsync('settings.options', section['name'], section)
|
||||
fireEvent('settings.options', section['name'], section)
|
||||
options = {}
|
||||
for group in section['groups']:
|
||||
for option in group['options']:
|
||||
options[option['name']] = option['default']
|
||||
fireEventAsync('settings.register', section_name = section['name'], options = options, save = save)
|
||||
fireEvent('settings.register', section_name = section['name'], options = options, save = save)
|
||||
return True
|
||||
except Exception, e:
|
||||
log.debug("Failed loading settings for '%s': %s" % (name, e))
|
||||
@@ -67,7 +72,7 @@ class Loader:
|
||||
module.start()
|
||||
return True
|
||||
except Exception, e:
|
||||
log.debug("Failed loading plugin '%s': %s" % (name, e))
|
||||
log.error("Failed loading plugin '%s': %s" % (name, e))
|
||||
return False
|
||||
|
||||
def addModule(self, type, module, name):
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.environment import Env
|
||||
|
||||
class Notification(Plugin):
|
||||
|
||||
default_title = 'CouchPotato'
|
||||
test_message = 'ZOMG Lazors Pewpewpew!'
|
||||
|
||||
def __init__(self):
|
||||
addEvent('notify', self.notify)
|
||||
|
||||
def notify(self, message = '', data = {}):
|
||||
pass
|
||||
|
||||
def conf(self, attr):
|
||||
return Env.setting(attr, self.__class__.__name__.lower())
|
||||
|
||||
def isDisabled(self):
|
||||
return not self.isEnabled()
|
||||
|
||||
def isEnabled(self):
|
||||
return self.conf('enabled', True)
|
||||
|
||||
def test(self):
|
||||
success = self.notify(message = self.test_message)
|
||||
|
||||
return jsonified({'success': success})
|
||||
@@ -0,0 +1,33 @@
|
||||
from .main import Growl
|
||||
|
||||
def start():
|
||||
return Growl()
|
||||
|
||||
config = [{
|
||||
'name': 'growl',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'name': 'growl',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': False,
|
||||
'type': 'enabler',
|
||||
'description': '',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost',
|
||||
'description': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
'description': '',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -0,0 +1,111 @@
|
||||
# Based on netprowl by the following authors.
|
||||
|
||||
# Altered 1st October 2010 - Tim Child.
|
||||
# Have added the ability for the command line arguments to take a password.
|
||||
|
||||
# Altered 1-17-2010 - Tanner Stokes - www.tannr.com
|
||||
# Added support for command line arguments
|
||||
|
||||
# ORIGINAL CREDITS
|
||||
# """Growl 0.6 Network Protocol Client for Python"""
|
||||
# __version__ = "0.6.3"
|
||||
# __author__ = "Rui Carmo (http://the.taoofmac.com)"
|
||||
# __copyright__ = "(C) 2004 Rui Carmo. Code under BSD License."
|
||||
# __contributors__ = "Ingmar J Stein (Growl Team), John Morrissey (hashlib patch)"
|
||||
|
||||
import struct
|
||||
|
||||
try:
|
||||
import hashlib
|
||||
md5_constructor = hashlib.md5
|
||||
except ImportError:
|
||||
import md5
|
||||
md5_constructor = md5.new
|
||||
|
||||
GROWL_UDP_PORT = 9887
|
||||
GROWL_PROTOCOL_VERSION = 1
|
||||
GROWL_TYPE_REGISTRATION = 0
|
||||
GROWL_TYPE_NOTIFICATION = 1
|
||||
|
||||
|
||||
class GrowlRegistrationPacket:
|
||||
"""Builds a Growl Network Registration packet.
|
||||
Defaults to emulating the command-line growlnotify utility."""
|
||||
|
||||
def __init__(self, application = "CouchPotato", password = None):
|
||||
self.notifications = []
|
||||
self.defaults = [] # array of indexes into notifications
|
||||
self.application = application.encode("utf-8")
|
||||
self.password = password
|
||||
|
||||
def addNotification(self, notification = "General Notification", enabled = True):
|
||||
"""Adds a notification type and sets whether it is enabled on the GUI"""
|
||||
|
||||
self.notifications.append(notification)
|
||||
if enabled:
|
||||
self.defaults.append(len(self.notifications) - 1)
|
||||
|
||||
def payload(self):
|
||||
"""Returns the packet payload."""
|
||||
self.data = struct.pack("!BBH",
|
||||
GROWL_PROTOCOL_VERSION,
|
||||
GROWL_TYPE_REGISTRATION,
|
||||
len(self.application)
|
||||
)
|
||||
self.data += struct.pack("BB",
|
||||
len(self.notifications),
|
||||
len(self.defaults)
|
||||
)
|
||||
self.data += self.application
|
||||
for notification in self.notifications:
|
||||
encoded = notification.encode("utf-8")
|
||||
self.data += struct.pack("!H", len(encoded))
|
||||
self.data += encoded
|
||||
for default in self.defaults:
|
||||
self.data += struct.pack("B", default)
|
||||
self.checksum = md5()
|
||||
self.checksum.update(self.data)
|
||||
if self.password:
|
||||
self.checksum.update(self.password)
|
||||
self.data += self.checksum.digest()
|
||||
return self.data
|
||||
|
||||
class GrowlNotificationPacket:
|
||||
"""Builds a Growl Network Notification packet.
|
||||
Defaults to emulating the command-line growlnotify utility."""
|
||||
|
||||
def __init__(self, application = "CouchPotato",
|
||||
notification = "General Notification", title = "Title",
|
||||
description = "Description", priority = 0, sticky = False, password = None):
|
||||
|
||||
self.application = application.encode("utf-8")
|
||||
self.notification = notification.encode("utf-8")
|
||||
self.title = title.encode("utf-8")
|
||||
self.description = description.encode("utf-8")
|
||||
flags = (priority & 0x07) * 2
|
||||
if priority < 0:
|
||||
flags |= 0x08
|
||||
if sticky:
|
||||
flags = flags | 0x0100
|
||||
self.data = struct.pack("!BBHHHHH",
|
||||
GROWL_PROTOCOL_VERSION,
|
||||
GROWL_TYPE_NOTIFICATION,
|
||||
flags,
|
||||
len(self.notification),
|
||||
len(self.title),
|
||||
len(self.description),
|
||||
len(self.application)
|
||||
)
|
||||
self.data += self.notification
|
||||
self.data += self.title
|
||||
self.data += self.description
|
||||
self.data += self.application
|
||||
self.checksum = md5_constructor()
|
||||
self.checksum.update(self.data)
|
||||
if password:
|
||||
self.checksum.update(password)
|
||||
self.data += self.checksum.digest()
|
||||
|
||||
def payload(self):
|
||||
"""Returns the packet payload."""
|
||||
return self.data
|
||||
@@ -0,0 +1,45 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from couchpotato.core.notifications.growl.growl import GROWL_UDP_PORT, \
|
||||
GrowlRegistrationPacket, GrowlNotificationPacket
|
||||
from couchpotato.environment import Env
|
||||
from socket import AF_INET, SOCK_DGRAM, socket
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Growl(Notification):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('notify', self.notify)
|
||||
addEvent('notify.growl', self.notify)
|
||||
|
||||
addApiView('notify.growl.test', self.test)
|
||||
|
||||
def conf(self, attr):
|
||||
return Env.setting(attr, 'growl')
|
||||
|
||||
def notify(self, message = '', data = {}):
|
||||
|
||||
if self.isDisabled():
|
||||
return
|
||||
|
||||
hosts = [x.strip() for x in self.conf('host').split(",")]
|
||||
password = self.conf('password')
|
||||
|
||||
for curHost in hosts:
|
||||
addr = (curHost, GROWL_UDP_PORT)
|
||||
|
||||
s = socket(AF_INET, SOCK_DGRAM)
|
||||
p = GrowlRegistrationPacket(password = password)
|
||||
p.addNotification()
|
||||
s.sendto(p.payload(), addr)
|
||||
|
||||
# send notification
|
||||
p = GrowlNotificationPacket(title = self.default_title, description = message, priority = 0, sticky = False, password = password)
|
||||
s.sendto(p.payload(), addr)
|
||||
s.close()
|
||||
|
||||
log.info('Growl notifications sent.')
|
||||
@@ -0,0 +1,37 @@
|
||||
from .main import NMJ
|
||||
|
||||
def start():
|
||||
return NMJ()
|
||||
|
||||
config = [{
|
||||
'name': 'nmj',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'name': 'nmj',
|
||||
'label': 'NMJ',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': False,
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost',
|
||||
'description': '',
|
||||
},
|
||||
{
|
||||
'name': 'database',
|
||||
'default': '',
|
||||
'description': '',
|
||||
},
|
||||
{
|
||||
'name': 'mount',
|
||||
'default': '',
|
||||
'description': '',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -0,0 +1,131 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.request import getParams, jsonified
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from couchpotato.environment import Env
|
||||
import re
|
||||
import telnetlib
|
||||
import urllib
|
||||
import urllib2
|
||||
|
||||
try:
|
||||
import xml.etree.cElementTree as etree
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class NMJ(Notification):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('notify', self.notify)
|
||||
addEvent('notify.nmj', self.notify)
|
||||
|
||||
addApiView('notify.nmj.test', self.test)
|
||||
addApiView('notify.nmj.auto_config', self.autoConfig)
|
||||
|
||||
def conf(self, attr):
|
||||
return Env.setting(attr, 'nmj')
|
||||
|
||||
def autoConfig(self):
|
||||
|
||||
params = getParams()
|
||||
host = params.get('host', 'localhost')
|
||||
|
||||
database = ''
|
||||
mount = ''
|
||||
|
||||
try:
|
||||
terminal = telnetlib.Telnet(host)
|
||||
except Exception:
|
||||
log.error('Warning: unable to get a telnet session to %s' % (host))
|
||||
return self.failed()
|
||||
|
||||
log.debug('Connected to %s via telnet' % (host))
|
||||
terminal.read_until('sh-3.00# ')
|
||||
terminal.write('cat /tmp/source\n')
|
||||
terminal.write('cat /tmp/netshare\n')
|
||||
terminal.write('exit\n')
|
||||
tnoutput = terminal.read_all()
|
||||
|
||||
match = re.search(r'(.+\.db)\r\n?(.+)(?=sh-3.00# cat /tmp/netshare)', tnoutput)
|
||||
|
||||
if match:
|
||||
database = match.group(1)
|
||||
device = match.group(2)
|
||||
log.info('Found NMJ database %s on device %s' % (database, device))
|
||||
else:
|
||||
log.error('Could not get current NMJ database on %s, NMJ is probably not running!' % (host))
|
||||
return self.failed()
|
||||
|
||||
if device.startswith('NETWORK_SHARE/'):
|
||||
match = re.search('.*(?=\r\n?%s)' % (re.escape(device[14:])), tnoutput)
|
||||
|
||||
if match:
|
||||
mount = match.group().replace('127.0.0.1', host)
|
||||
log.info('Found mounting url on the Popcorn Hour in configuration: %s' % (mount))
|
||||
else:
|
||||
log.error('Detected a network share on the Popcorn Hour, but could not get the mounting url')
|
||||
return self.failed()
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'database': database,
|
||||
'mount': mount,
|
||||
})
|
||||
|
||||
def notify(self, message = '', data = {}):
|
||||
|
||||
if self.isDisabled():
|
||||
return False
|
||||
|
||||
host = self.conf('host')
|
||||
mount = self.conf('mount')
|
||||
database = self.conf('database')
|
||||
|
||||
if self.mount:
|
||||
try:
|
||||
req = urllib2.Request(mount)
|
||||
log.debug('Try to mount network drive via url: %s' % (mount))
|
||||
handle = urllib2.urlopen(req)
|
||||
except IOError, e:
|
||||
log.error('Warning: Couldn\'t contact popcorn hour on host %s: %s' % (host, e))
|
||||
return False
|
||||
|
||||
params = {
|
||||
'arg0': 'scanner_start',
|
||||
'arg1': database,
|
||||
'arg2': 'background',
|
||||
'arg3': '',
|
||||
}
|
||||
params = urllib.urlencode(params)
|
||||
UPDATE_URL = 'http://%(host)s:8008/metadata_database?%(params)s'
|
||||
updateUrl = UPDATE_URL % {'host': host, 'params': params}
|
||||
|
||||
try:
|
||||
req = urllib2.Request(updateUrl)
|
||||
log.debug('Sending NMJ scan update command via url: %s' % (updateUrl))
|
||||
handle = urllib2.urlopen(req)
|
||||
response = handle.read()
|
||||
except IOError, e:
|
||||
log.error('Warning: Couldn\'t contact Popcorn Hour on host %s: %s' % (host, e))
|
||||
return False
|
||||
|
||||
try:
|
||||
et = etree.fromstring(response)
|
||||
result = et.findtext('returnValue')
|
||||
except SyntaxError, e:
|
||||
log.error('Unable to parse XML returned from the Popcorn Hour: %s' % (e))
|
||||
return False
|
||||
|
||||
if int(result) > 0:
|
||||
log.error('Popcorn Hour returned an errorcode: %s' % (result))
|
||||
return False
|
||||
else:
|
||||
log.info('NMJ started background scan')
|
||||
return True
|
||||
|
||||
def failed(self):
|
||||
return jsonified({'success': False})
|
||||
@@ -0,0 +1,32 @@
|
||||
from .main import Notifo
|
||||
|
||||
def start():
|
||||
return Notifo()
|
||||
|
||||
config = [{
|
||||
'name': 'notifo',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'name': 'notifo',
|
||||
'description': '',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': False,
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': '',
|
||||
'type': 'string',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -0,0 +1,53 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from couchpotato.environment import Env
|
||||
from flask.helpers import json
|
||||
import base64
|
||||
import urllib
|
||||
import urllib2
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Notifo(Notification):
|
||||
|
||||
url = 'https://api.notifo.com/v1/send_notification'
|
||||
|
||||
def __init__(self):
|
||||
addEvent('notify', self.notify)
|
||||
addEvent('notify.notifo', self.notify)
|
||||
|
||||
addApiView('notify.notifo.test', self.test)
|
||||
|
||||
def conf(self, attr):
|
||||
return Env.setting(attr, 'notifo')
|
||||
|
||||
def notify(self, message = '', data = {}):
|
||||
|
||||
if self.isDisabled():
|
||||
return False
|
||||
|
||||
try:
|
||||
data = urllib.urlencode({
|
||||
'msg': toUnicode(message),
|
||||
})
|
||||
|
||||
req = urllib2.Request(self.url)
|
||||
authHeader = "Basic %s" % base64.encodestring('%s:%s' % (self.conf('username'), self.conf('api_key')))[:-1]
|
||||
req.add_header("Authorization", authHeader)
|
||||
|
||||
handle = urllib2.urlopen(req, data)
|
||||
result = json.load(handle)
|
||||
|
||||
if result['status'] != 'success' or result['response_message'] != 'OK':
|
||||
raise Exception
|
||||
|
||||
except Exception, e:
|
||||
log.error('Notification failed: %s' % e)
|
||||
return False
|
||||
|
||||
log.info('Notifo notification successful.')
|
||||
return True
|
||||
@@ -0,0 +1,33 @@
|
||||
from .main import Plex
|
||||
|
||||
def start():
|
||||
return Plex()
|
||||
|
||||
config = [{
|
||||
'name': 'plex',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'name': 'plex',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': False,
|
||||
'type': 'enabler',
|
||||
'description': '',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost',
|
||||
'description': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': '',
|
||||
'type': 'password',
|
||||
'description': '',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -0,0 +1,43 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from xml.dom import minidom
|
||||
import urllib
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Plex(Notification):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('notify', self.notify)
|
||||
addEvent('notify.plex', self.notify)
|
||||
|
||||
addApiView('notify.plex.test', self.test)
|
||||
|
||||
def notify(self, message = '', data = {}):
|
||||
|
||||
if self.isDisabled():
|
||||
return
|
||||
|
||||
log.info('Sending notification to Plex')
|
||||
hosts = [x.strip() for x in self.conf('host').split(",")]
|
||||
|
||||
for host in hosts:
|
||||
|
||||
source_type = ['movie']
|
||||
base_url = 'http://%s/library/sections' % host
|
||||
refresh_url = '%s/%%s/refresh' % base_url
|
||||
|
||||
try:
|
||||
xml_sections = minidom.parse(urllib.urlopen(base_url))
|
||||
sections = xml_sections.getElementsByTagName('Directory')
|
||||
for s in sections:
|
||||
if s.getAttribute('type') in source_type:
|
||||
url = refresh_url % s.getAttribute('key')
|
||||
x = urllib.urlopen(url)
|
||||
except:
|
||||
log.error('Plex library update failed for %s.' % host)
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,35 @@
|
||||
from .main import Prowl
|
||||
|
||||
def start():
|
||||
return Prowl()
|
||||
|
||||
config = [{
|
||||
'name': 'prowl',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'name': 'prowl',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': False,
|
||||
'type': 'enabler',
|
||||
'description': '',
|
||||
},
|
||||
{
|
||||
'name': 'api_key',
|
||||
'default': '',
|
||||
'label': 'Api key',
|
||||
'description': '',
|
||||
},
|
||||
{
|
||||
'name': 'priority',
|
||||
'default': '0',
|
||||
'type': 'dropdown',
|
||||
'description': '',
|
||||
'values': [('Very Low', -2), ('Moderate', -1), ('Normal', 0), ('High', 1), ('Emergency', 2)]
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -0,0 +1,51 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
from httplib import HTTPSConnection
|
||||
from urllib import urlencode
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class Prowl(Notification):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('notify', self.notify)
|
||||
addEvent('notify.prowl', self.notify)
|
||||
|
||||
addApiView('notify.prowl.test', self.test)
|
||||
|
||||
def notify(self, message = '', data = {}):
|
||||
|
||||
if self.isDisabled():
|
||||
return
|
||||
|
||||
http_handler = HTTPSConnection('api.prowlapp.com')
|
||||
|
||||
data = {
|
||||
'apikey': self.conf('api_key'),
|
||||
'application': self.default_title,
|
||||
'event': self.default_title,
|
||||
'description': toUnicode(message),
|
||||
'priority': self.conf('priority'),
|
||||
}
|
||||
|
||||
http_handler.request('POST',
|
||||
'/publicapi/add',
|
||||
headers = {'Content-type': 'application/x-www-form-urlencoded'},
|
||||
body = urlencode(data)
|
||||
)
|
||||
response = http_handler.getresponse()
|
||||
request_status = response.status
|
||||
|
||||
if request_status == 200:
|
||||
log.info('Prowl notifications sent.')
|
||||
return True
|
||||
elif request_status == 401:
|
||||
log.error('Prowl auth failed: %s' % response.reason)
|
||||
return False
|
||||
else:
|
||||
log.error('Prowl notification failed.')
|
||||
return False
|
||||
@@ -0,0 +1,38 @@
|
||||
from .main import XBMC
|
||||
|
||||
def start():
|
||||
return XBMC()
|
||||
|
||||
config = [{
|
||||
'name': 'xbmc',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'name': 'xbmc',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': False,
|
||||
'type': 'enabler',
|
||||
'description': '',
|
||||
},
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost:8080',
|
||||
'description': '',
|
||||
},
|
||||
{
|
||||
'name': 'username',
|
||||
'default': 'xbmc',
|
||||
'description': '',
|
||||
},
|
||||
{
|
||||
'name': 'password',
|
||||
'default': 'xbmc',
|
||||
'type': 'password',
|
||||
'description': '',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -0,0 +1,47 @@
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import base64
|
||||
import urllib
|
||||
import urllib2
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class XBMC(Notification):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('notify', self.notify)
|
||||
addEvent('notify.xbmc', self.notify)
|
||||
|
||||
addApiView('notify.xbmc.test', self.test)
|
||||
|
||||
def notify(self, message = '', data = {}):
|
||||
|
||||
if self.isDisabled():
|
||||
return
|
||||
|
||||
for host in [x.strip() for x in self.conf('host').split(",")]:
|
||||
self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host)
|
||||
self.send({'command': 'ExecBuiltIn', 'parameter': 'XBMC.updatelibrary(video)'}, host)
|
||||
|
||||
return True
|
||||
|
||||
def send(self, command, host):
|
||||
|
||||
url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, urllib.urlencode(command))
|
||||
|
||||
try:
|
||||
req = urllib2.Request(url)
|
||||
if self.password:
|
||||
authHeader = "Basic %s" % base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password')))[:-1]
|
||||
req.add_header("Authorization", authHeader)
|
||||
|
||||
urllib2.urlopen(req, timeout = 10).read()
|
||||
except Exception, e:
|
||||
log.error("Couldn't sent command to XBMC. %s" % e)
|
||||
return False
|
||||
|
||||
log.info('XBMC notification to %s successful.' % host)
|
||||
return True
|
||||
@@ -0,0 +1,6 @@
|
||||
from .main import FileManager
|
||||
|
||||
def start():
|
||||
return FileManager()
|
||||
|
||||
config = []
|
||||
@@ -0,0 +1,103 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.variable import md5, getExt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import FileType, File
|
||||
from couchpotato.environment import Env
|
||||
from flask.helpers import send_from_directory
|
||||
import os.path
|
||||
import urllib2
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class FileManager(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('file.add', self.add)
|
||||
addEvent('file.download', self.download)
|
||||
addEvent('file.types', self.getTypes)
|
||||
|
||||
addApiView('file.cache/<path:file>', self.showImage)
|
||||
|
||||
def showImage(self, file = ''):
|
||||
|
||||
cache_dir = Env.get('cache_dir')
|
||||
filename = file.replace(cache_dir[1:] + '/', '')
|
||||
|
||||
return send_from_directory(cache_dir, filename)
|
||||
|
||||
def download(self, url = '', dest = None, overwrite = False):
|
||||
|
||||
try:
|
||||
file = urllib2.urlopen(url)
|
||||
|
||||
if not dest: # to Cache
|
||||
dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url)))
|
||||
|
||||
if overwrite or not os.path.exists(dest):
|
||||
log.debug('Writing file to: %s' % dest)
|
||||
output = open(dest, 'wb')
|
||||
output.write(file.read())
|
||||
output.close()
|
||||
else:
|
||||
log.debug('File already exists: %s' % dest)
|
||||
|
||||
return dest
|
||||
|
||||
except Exception, e:
|
||||
log.error('Unable to download file "%s": %s' % (url, e))
|
||||
|
||||
return False
|
||||
|
||||
def add(self, path = '', part = 1, type = (), properties = {}):
|
||||
|
||||
db = get_session()
|
||||
|
||||
f = db.query(File).filter_by(path = path).first()
|
||||
if not f:
|
||||
f = File()
|
||||
db.add(f)
|
||||
|
||||
f.path = path
|
||||
f.part = part
|
||||
f.type_id = self.getType(type).id
|
||||
|
||||
db.commit()
|
||||
|
||||
db.expunge(f)
|
||||
return f
|
||||
|
||||
def getType(self, type):
|
||||
|
||||
db = get_session()
|
||||
|
||||
type, identifier = type
|
||||
|
||||
ft = db.query(FileType).filter_by(identifier = identifier).first()
|
||||
if not ft:
|
||||
ft = FileType(
|
||||
type = type,
|
||||
identifier = identifier,
|
||||
name = identifier[0].capitalize() + identifier[1:]
|
||||
)
|
||||
|
||||
db.add(ft)
|
||||
db.commit()
|
||||
|
||||
return ft
|
||||
|
||||
def getTypes(self):
|
||||
|
||||
db = get_session()
|
||||
|
||||
results = db.query(FileType).all()
|
||||
|
||||
types = []
|
||||
for type in results:
|
||||
temp = type.to_dict()
|
||||
types.append(temp)
|
||||
|
||||
return types
|
||||
@@ -1,4 +1,4 @@
|
||||
from couchpotato.core.plugins.file_browser.main import FileBrowser
|
||||
from .main import FileBrowser
|
||||
|
||||
def start():
|
||||
return FileBrowser()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from couchpotato.core.plugins.library.main import LibraryPlugin
|
||||
from .main import LibraryPlugin
|
||||
|
||||
def start():
|
||||
return LibraryPlugin()
|
||||
|
||||
@@ -1,32 +1,81 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Library
|
||||
from couchpotato.core.settings.model import Library, LibraryTitle
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
class LibraryPlugin(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('library.add', self.add)
|
||||
addEvent('library.update', self.update)
|
||||
|
||||
def add(self, attrs = {}):
|
||||
|
||||
db = get_session();
|
||||
db = get_session()
|
||||
|
||||
l = db.query(Library).filter_by(identifier = attrs.get('identifier')).first()
|
||||
|
||||
if not l:
|
||||
l = Library(
|
||||
name = attrs.get('name'),
|
||||
year = attrs.get('year'),
|
||||
identifier = attrs.get('identifier'),
|
||||
description = attrs.get('description')
|
||||
plot = attrs.get('plot'),
|
||||
tagline = attrs.get('tagline')
|
||||
)
|
||||
|
||||
title = LibraryTitle(
|
||||
title = attrs.get('title')
|
||||
)
|
||||
|
||||
l.titles.append(title)
|
||||
|
||||
db.add(l)
|
||||
db.commit()
|
||||
|
||||
# Update library info
|
||||
fireEventAsync('library.update', library = l, default_title = attrs.get('title', ''))
|
||||
|
||||
#db.remove()
|
||||
return l
|
||||
|
||||
def update(self, item):
|
||||
def update(self, library, default_title = ''):
|
||||
|
||||
pass
|
||||
db = get_session()
|
||||
library = db.query(Library).filter_by(identifier = library.identifier).first()
|
||||
|
||||
info = fireEvent('provider.movie.info', merge = True, identifier = library.identifier)
|
||||
|
||||
# Main info
|
||||
library.plot = info.get('plot', '')
|
||||
library.tagline = info.get('tagline', '')
|
||||
library.year = info.get('year', 0)
|
||||
|
||||
# Titles
|
||||
[db.delete(title) for title in library.titles]
|
||||
titles = info.get('titles')
|
||||
|
||||
log.debug('Adding titles: %s' % titles)
|
||||
for title in titles:
|
||||
t = LibraryTitle(
|
||||
title = title,
|
||||
default = title.lower() == default_title.lower()
|
||||
)
|
||||
library.titles.append(t)
|
||||
|
||||
db.commit()
|
||||
|
||||
# Files
|
||||
images = info.get('images')
|
||||
for type in images:
|
||||
for image in images[type]:
|
||||
file_path = fireEvent('file.download', url = image, single = True)
|
||||
file = fireEvent('file.add', path = file_path, type = ('image', type[:-1]), single = True)
|
||||
try:
|
||||
library.files.append(file)
|
||||
db.commit()
|
||||
except:
|
||||
log.debug('File already attached to library')
|
||||
|
||||
fireEvent('library.update.after')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from couchpotato.core.plugins.movie.main import MoviePlugin
|
||||
from .main import MoviePlugin
|
||||
|
||||
def start():
|
||||
return MoviePlugin()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.event import fireEvent, fireEventAsync
|
||||
from couchpotato.core.helpers.request import getParams, jsonified
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
from couchpotato.core.settings.model import Movie, Release, Profile
|
||||
from couchpotato.core.settings.model import Movie
|
||||
from couchpotato.environment import Env
|
||||
from urllib import urlencode
|
||||
|
||||
@@ -13,30 +13,28 @@ class MoviePlugin(Plugin):
|
||||
def __init__(self):
|
||||
addApiView('movie.search', self.search)
|
||||
addApiView('movie.list', self.list)
|
||||
addApiView('movie.refresh', self.refresh)
|
||||
|
||||
addApiView('movie.add', self.add)
|
||||
addApiView('movie.edit', self.edit)
|
||||
addApiView('movie.delete', self.delete)
|
||||
|
||||
def list(self):
|
||||
|
||||
a = getParams()
|
||||
params = getParams()
|
||||
db = get_session()
|
||||
|
||||
results = get_session().query(Movie).filter(
|
||||
Movie.releases.any(
|
||||
Release.status.has(identifier = 'wanted')
|
||||
)
|
||||
).all()
|
||||
results = db.query(Movie).filter(
|
||||
Movie.status.has(identifier = params.get('status', 'active'))
|
||||
).all()
|
||||
|
||||
movies = []
|
||||
for movie in results:
|
||||
temp = {
|
||||
'id': movie.id,
|
||||
'name': movie.id,
|
||||
'releases': [],
|
||||
}
|
||||
for release in movie.releases:
|
||||
temp['releases'].append({
|
||||
'status': release.status.label,
|
||||
'quality': release.quality.label
|
||||
})
|
||||
temp = movie.to_dict(deep = {
|
||||
'releases': {'status': {}, 'quality': {}},
|
||||
'library': {'titles': {}, 'files':{}},
|
||||
'files': {}
|
||||
})
|
||||
|
||||
movies.append(temp)
|
||||
|
||||
@@ -46,22 +44,41 @@ class MoviePlugin(Plugin):
|
||||
'movies': movies,
|
||||
})
|
||||
|
||||
def refresh(self):
|
||||
|
||||
params = getParams()
|
||||
db = get_session()
|
||||
|
||||
movie = db.query(Movie).filter_by(id = params.get('id')).first()
|
||||
|
||||
# Get current selected title
|
||||
default_title = ''
|
||||
for title in movie.library.titles:
|
||||
if title.default: default_title = title.title
|
||||
|
||||
if movie:
|
||||
#addEvent('library.update.after', )
|
||||
fireEventAsync('library.update', library = movie.library, default_title = default_title)
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
})
|
||||
|
||||
def search(self):
|
||||
|
||||
a = getParams()
|
||||
cache_key = '%s/%s' % (__name__, urlencode(a))
|
||||
params = getParams()
|
||||
cache_key = '%s/%s' % (__name__, urlencode(params))
|
||||
movies = Env.get('cache').get(cache_key)
|
||||
|
||||
if not movies:
|
||||
results = fireEvent('provider.movie.search', q = a.get('q'))
|
||||
results = fireEvent('provider.movie.search', q = params.get('q'))
|
||||
|
||||
# Combine movie results
|
||||
movies = []
|
||||
for r in results:
|
||||
movies += r
|
||||
|
||||
Env.get('cache').set(cache_key, movies, timeout = 10)
|
||||
|
||||
Env.get('cache').set(cache_key, movies)
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
@@ -71,24 +88,46 @@ class MoviePlugin(Plugin):
|
||||
|
||||
def add(self):
|
||||
|
||||
a = getParams()
|
||||
params = getParams()
|
||||
db = get_session();
|
||||
|
||||
library = fireEvent('library.add', attrs = a)
|
||||
profile = db.query(Profile).filter_by(identifier = a.get('profile_identifier'))
|
||||
|
||||
m = db.query(Movie).filter_by(library = library).first()
|
||||
library = fireEvent('library.add', single = True, attrs = params)
|
||||
status = fireEvent('status.add', 'active', single = True)
|
||||
|
||||
m = db.query(Movie).filter_by(library_id = library.id).first()
|
||||
if not m:
|
||||
m = Movie(
|
||||
library = library,
|
||||
profile = profile,
|
||||
library_id = library.id,
|
||||
profile_id = params.get('profile_id')
|
||||
)
|
||||
db.add(m)
|
||||
db.commit()
|
||||
|
||||
m.status_id = status.id
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'added': True,
|
||||
'params': a,
|
||||
'movie': m.to_dict(deep = {
|
||||
'releases': {'status': {}, 'quality': {}},
|
||||
'library': {'titles': {}}
|
||||
})
|
||||
})
|
||||
|
||||
def edit(self):
|
||||
pass
|
||||
|
||||
def delete(self):
|
||||
|
||||
params = getParams()
|
||||
db = get_session()
|
||||
|
||||
status = fireEvent('status.add', 'deleted', single = True)
|
||||
|
||||
movie = db.query(Movie).filter_by(id = params.get('id')).first()
|
||||
movie.status_id = status.id
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from couchpotato.core.plugins.profile.main import ProfilePlugin
|
||||
from .main import ProfilePlugin
|
||||
|
||||
def start():
|
||||
return ProfilePlugin()
|
||||
|
||||
@@ -1,28 +1,90 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.api import addApiView
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.request import jsonified, getParams
|
||||
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
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class ProfilePlugin(Plugin):
|
||||
|
||||
def __init__(self):
|
||||
addEvent('profile.get', self.get)
|
||||
addEvent('profile.all', self.all)
|
||||
|
||||
addApiView('profile.save', self.save)
|
||||
addApiView('profile.delete', self.delete)
|
||||
|
||||
def get(self, key = ''):
|
||||
def all(self):
|
||||
|
||||
pass
|
||||
db = get_session()
|
||||
profiles = db.query(Profile).all()
|
||||
|
||||
temp = []
|
||||
for profile in profiles:
|
||||
temp.append(profile.to_dict(deep = {'types': {}}))
|
||||
|
||||
return temp
|
||||
|
||||
def save(self):
|
||||
|
||||
a = getParams()
|
||||
params = getParams()
|
||||
|
||||
db = get_session()
|
||||
|
||||
p = db.query(Profile).filter_by(id = params.get('id')).first()
|
||||
if not p:
|
||||
p = Profile()
|
||||
db.add(p)
|
||||
|
||||
p.label = params.get('label')
|
||||
p.order = params.get('order', p.order if p.order else 0)
|
||||
p.core = params.get('core', False)
|
||||
|
||||
#delete old types
|
||||
[db.delete(t) for t in p.types]
|
||||
|
||||
order = 0
|
||||
for type in params.get('types', []):
|
||||
t = ProfileType(
|
||||
order = order,
|
||||
finish = type.get('finish'),
|
||||
wait_for = params.get('wait_for'),
|
||||
quality_id = type.get('quality_id')
|
||||
)
|
||||
p.types.append(t)
|
||||
|
||||
order += 1
|
||||
|
||||
db.commit()
|
||||
|
||||
return jsonified({
|
||||
'success': True,
|
||||
'a': a
|
||||
'profile': p.to_dict(deep = {'types': {}})
|
||||
})
|
||||
|
||||
def delete(self):
|
||||
pass
|
||||
|
||||
id = getParam('id')
|
||||
|
||||
db = get_session()
|
||||
|
||||
success = False
|
||||
message = ''
|
||||
try:
|
||||
p = db.query(Profile).filter_by(id = id).first()
|
||||
|
||||
db.delete(p)
|
||||
db.commit()
|
||||
|
||||
success = True
|
||||
except Exception, e:
|
||||
message = 'Failed deleting Profile: %s' % e
|
||||
log.error(message)
|
||||
|
||||
return jsonified({
|
||||
'success': success,
|
||||
'message': message
|
||||
})
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
from .main import QualityPlugin
|
||||
|
||||
def start():
|
||||
return QualityPlugin()
|
||||
|
||||
config = []
|
||||
@@ -0,0 +1,97 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.settings.model import Quality, Profile, ProfileType
|
||||
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
class QualityPlugin:
|
||||
|
||||
qualities = [
|
||||
{'identifier': 'bd50', 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['1080p', 'bd25'], 'allow': [], 'ext':[], 'tags': ['x264', 'h264', 'blu ray']},
|
||||
{'identifier': '1080p', 'size': (5000, 20000), 'label': '1080P', 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']},
|
||||
{'identifier': '720p', 'size': (3500, 10000), 'label': '720P', 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']},
|
||||
{'identifier': 'brrip', 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['mkv', 'avi']},
|
||||
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc']},
|
||||
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['dvdscr'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
|
||||
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
|
||||
]
|
||||
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
|
||||
|
||||
def __init__(self):
|
||||
addEvent('quality.all', self.all)
|
||||
addEvent('app.load', self.fill)
|
||||
|
||||
def all(self):
|
||||
|
||||
db = get_session()
|
||||
|
||||
qualities = db.query(Quality).all()
|
||||
|
||||
temp = []
|
||||
for quality in qualities:
|
||||
q = dict(self.getQuality(quality.identifier), **quality.to_dict())
|
||||
temp.append(q)
|
||||
|
||||
return temp
|
||||
|
||||
def getQuality(self, identifier):
|
||||
|
||||
for q in self.qualities:
|
||||
if identifier == q.get('identifier'):
|
||||
return q
|
||||
|
||||
def fill(self):
|
||||
|
||||
db = get_session();
|
||||
|
||||
order = 0
|
||||
for q in self.qualities:
|
||||
|
||||
# Create quality
|
||||
quality = db.query(Quality).filter_by(identifier = q.get('identifier')).first()
|
||||
|
||||
if not quality:
|
||||
log.info('Creating quality: %s' % q.get('label'))
|
||||
quality = Quality()
|
||||
db.add(quality)
|
||||
|
||||
quality.order = order
|
||||
quality.identifier = q.get('identifier')
|
||||
quality.label = q.get('label')
|
||||
quality.size_min, quality.size_max = q.get('size')
|
||||
|
||||
# Create single quality profile
|
||||
profile = db.query(Profile).filter(
|
||||
Profile.core == True
|
||||
).filter(
|
||||
Profile.types.any(quality = quality)
|
||||
).all()
|
||||
|
||||
if not profile:
|
||||
log.info('Creating profile: %s' % q.get('label'))
|
||||
profile = Profile(
|
||||
core = True,
|
||||
label = toUnicode(quality.label),
|
||||
order = order
|
||||
)
|
||||
db.add(profile)
|
||||
|
||||
profile_type = ProfileType(
|
||||
quality = quality,
|
||||
profile = profile,
|
||||
finish = True,
|
||||
order = 0
|
||||
)
|
||||
profile.types.append(profile_type)
|
||||
|
||||
order += 1
|
||||
db.commit()
|
||||
|
||||
return True
|
||||
@@ -0,0 +1,6 @@
|
||||
from .main import StatusPlugin
|
||||
|
||||
def start():
|
||||
return StatusPlugin()
|
||||
|
||||
config = []
|
||||
@@ -0,0 +1,68 @@
|
||||
from couchpotato import get_session
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.settings.model import Status
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
class StatusPlugin:
|
||||
|
||||
statuses = {
|
||||
'active': 'Active',
|
||||
'done': 'Done',
|
||||
'downloaded': 'Downloaded',
|
||||
'wanted': 'Wanted',
|
||||
'deleted': 'Deleted',
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
addEvent('status.add', self.add)
|
||||
addEvent('status.all', self.all)
|
||||
addEvent('app.load', self.fill)
|
||||
|
||||
def all(self):
|
||||
|
||||
db = get_session()
|
||||
|
||||
statuses = db.query(Status).all()
|
||||
|
||||
temp = []
|
||||
for status in statuses:
|
||||
s = status.to_dict()
|
||||
temp.append(s)
|
||||
|
||||
return temp
|
||||
|
||||
def add(self, identifier):
|
||||
|
||||
db = get_session()
|
||||
|
||||
s = db.query(Status).filter_by(identifier = identifier).first()
|
||||
if not s:
|
||||
s = Status(
|
||||
identifier = identifier,
|
||||
label = identifier.capitalize()
|
||||
)
|
||||
db.add(s)
|
||||
db.commit()
|
||||
|
||||
#db.remove()
|
||||
return s
|
||||
|
||||
def fill(self):
|
||||
|
||||
db = get_session()
|
||||
|
||||
for identifier, label in self.statuses.iteritems():
|
||||
s = db.query(Status).filter_by(identifier = identifier).first()
|
||||
if not s:
|
||||
log.info('Creating status: %s' % label)
|
||||
s = Status(
|
||||
identifier = identifier,
|
||||
label = toUnicode(label)
|
||||
)
|
||||
db.add(s)
|
||||
|
||||
s.label = label
|
||||
db.commit()
|
||||
@@ -1,8 +1,9 @@
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.plugins.base import Plugin
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
class Provider():
|
||||
class Provider(Plugin):
|
||||
|
||||
type = None # movie, nzb, torrent, subtitle, trailer
|
||||
timeout = 10 # Default timeout for url requests
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
from __future__ import with_statement
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.base import Provider
|
||||
from couchpotato.environment import Env
|
||||
from libs.themoviedb import tmdb
|
||||
import copy
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
class TMDBWrapper(Provider):
|
||||
"""Api for theMovieDb"""
|
||||
|
||||
type = 'movie'
|
||||
apiUrl = 'http://api.themoviedb.org/2.1'
|
||||
imageUrl = 'http://hwcdn.themoviedb.org'
|
||||
|
||||
def __init__(self):
|
||||
addEvent('provider.movie.search', self.search)
|
||||
addEvent('provider.movie.info', self.getInfo)
|
||||
|
||||
# Use base wrapper
|
||||
tmdb.Config.api_key = self.conf('api_key')
|
||||
|
||||
def conf(self, attr):
|
||||
return Env.setting(attr, 'themoviedb')
|
||||
|
||||
def search(self, q, limit = 12):
|
||||
''' Find movie by name '''
|
||||
|
||||
if self.isDisabled():
|
||||
return False
|
||||
|
||||
log.debug('TheMovieDB - Searching for movie: %s' % q)
|
||||
raw = tmdb.search(simplifyString(q))
|
||||
|
||||
results = []
|
||||
if raw:
|
||||
try:
|
||||
nr = 0
|
||||
for movie in raw:
|
||||
|
||||
results.append(self.parseMovie(movie))
|
||||
|
||||
nr += 1
|
||||
if nr == limit:
|
||||
break
|
||||
|
||||
log.info('TheMovieDB - Found: %s' % [result['titles'][0] + ' (' + str(result['year']) + ')' for result in results])
|
||||
return results
|
||||
except SyntaxError, e:
|
||||
log.error('Failed to parse XML response: %s' % e)
|
||||
return False
|
||||
|
||||
return results
|
||||
|
||||
def getInfo(self, identifier = None):
|
||||
result = {}
|
||||
|
||||
movie = tmdb.imdbLookup(id = identifier)[0]
|
||||
|
||||
if movie:
|
||||
result = self.parseMovie(movie)
|
||||
|
||||
return result
|
||||
|
||||
def parseMovie(self, movie):
|
||||
|
||||
year = str(movie.get('released', 'none'))[:4]
|
||||
|
||||
# Poster url
|
||||
poster = self.getImage(movie, type = 'poster')
|
||||
backdrop = self.getImage(movie, type = 'backdrop')
|
||||
|
||||
# 1900 is the same as None
|
||||
if year == '1900' or year.lower() == 'none':
|
||||
year = None
|
||||
|
||||
movie_data = {
|
||||
'id': int(movie.get('id', 0)),
|
||||
'titles': [toUnicode(movie.get('name'))],
|
||||
'images': {
|
||||
'posters': [poster],
|
||||
'backdrops': [backdrop],
|
||||
},
|
||||
'imdb': movie.get('imdb_id'),
|
||||
'year': year,
|
||||
'plot': movie.get('overview', ''),
|
||||
'tagline': '',
|
||||
}
|
||||
|
||||
# Add alternative names
|
||||
for alt in ['original_name', 'alternative_name']:
|
||||
alt_name = toUnicode(movie.get(alt))
|
||||
if alt_name and not alt_name in movie_data['titles'] and alt_name.lower() != 'none' and alt_name != None:
|
||||
movie_data['titles'].append(alt_name)
|
||||
|
||||
return movie_data
|
||||
|
||||
def getImage(self, movie, type = 'poster'):
|
||||
|
||||
image = ''
|
||||
for image in movie.get('images', []):
|
||||
if(image.get('type') == type):
|
||||
image = image.get('thumb')
|
||||
break
|
||||
|
||||
return image
|
||||
|
||||
def isDisabled(self):
|
||||
if self.conf('api_key') == '':
|
||||
log.error('No API key provided.')
|
||||
True
|
||||
else:
|
||||
False
|
||||
@@ -1,107 +0,0 @@
|
||||
from __future__ import with_statement
|
||||
from couchpotato.core.event import addEvent
|
||||
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.base import Provider
|
||||
from couchpotato.environment import Env
|
||||
from libs.themoviedb import tmdb
|
||||
from urllib import quote_plus
|
||||
import copy
|
||||
import simplejson as json
|
||||
import urllib2
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
class TMDBWrapper(Provider):
|
||||
"""Api for theMovieDb"""
|
||||
|
||||
type = 'movie'
|
||||
apiUrl = 'http://api.themoviedb.org/2.1'
|
||||
imageUrl = 'http://hwcdn.themoviedb.org'
|
||||
|
||||
def __init__(self):
|
||||
addEvent('provider.movie.search', self.search)
|
||||
addEvent('provider.movie.info', self.getInfo)
|
||||
|
||||
# Use base wrapper
|
||||
tmdb.Config.api_key = self.conf('api_key')
|
||||
|
||||
def conf(self, attr):
|
||||
return Env.setting(attr, 'themoviedb')
|
||||
|
||||
def search(self, q, limit = 12, alternative = True):
|
||||
''' Find movie by name '''
|
||||
|
||||
if self.isDisabled():
|
||||
return False
|
||||
|
||||
log.debug('TheMovieDB - Searching for movie: %s' % q)
|
||||
|
||||
raw = tmdb.search(simplifyString(q))
|
||||
|
||||
#url = "%s/%s/%s/json/%s/%s" % (self.apiUrl, 'Movie.search', 'en', self.conf('api_key'), quote_plus(simplifyString(q)))
|
||||
|
||||
|
||||
# data = urllib2.urlopen(url)
|
||||
# jsn = json.load(data)
|
||||
|
||||
if raw:
|
||||
log.debug('TheMovieDB - Parsing RSS')
|
||||
try:
|
||||
results = []
|
||||
nr = 0
|
||||
for movie in raw:
|
||||
|
||||
for k, x in movie.iteritems():
|
||||
print k
|
||||
print x
|
||||
|
||||
year = str(movie.get('released', 'none'))[:4]
|
||||
|
||||
# Poster url
|
||||
poster = ''
|
||||
for p in movie.get('images'):
|
||||
if(p.get('type') == 'poster'):
|
||||
poster = p.get('thumb')
|
||||
break
|
||||
|
||||
# 1900 is the same as None
|
||||
if year == '1900' or year.lower() == 'none':
|
||||
year = None
|
||||
|
||||
movie_data = {
|
||||
'id': int(movie.get('id', 0)),
|
||||
'name': toUnicode(movie.get('name')),
|
||||
'poster': poster,
|
||||
'imdb': movie.get('imdb_id'),
|
||||
'year': year,
|
||||
'tagline': 'This is the tagline of the movie',
|
||||
}
|
||||
results.append(copy.deepcopy(movie_data))
|
||||
|
||||
alternativeName = movie.get('alternative_name')
|
||||
if alternativeName and alternative:
|
||||
if alternativeName.lower() != movie['name'].lower() and alternativeName.lower() != 'none' and alternativeName != None:
|
||||
movie_data['name'] = toUnicode(alternativeName)
|
||||
results.append(copy.deepcopy(movie_data))
|
||||
nr += 1
|
||||
if nr == limit:
|
||||
break
|
||||
|
||||
log.info('TheMovieDB - Found: %s' % [result['name'] + u' (' + str(result['year']) + ')' for result in results])
|
||||
return results
|
||||
except SyntaxError, e:
|
||||
log.error('TheMovieDB - Failed to parse XML response from TheMovieDb: %s' % e)
|
||||
return False
|
||||
|
||||
return results
|
||||
|
||||
def getInfo(self):
|
||||
pass
|
||||
|
||||
def isDisabled(self):
|
||||
if self.conf('api_key') == '':
|
||||
log.error('TheMovieDB - No API key provided for TheMovieDB')
|
||||
True
|
||||
else:
|
||||
False
|
||||
@@ -2,8 +2,10 @@ from elixir.entity import Entity
|
||||
from elixir.fields import Field
|
||||
from elixir.options import options_defaults
|
||||
from elixir.relationships import OneToMany, ManyToOne
|
||||
from sqlalchemy.types import Integer, String, Unicode, UnicodeText, Boolean, \
|
||||
Float
|
||||
from libs.elixir.options import using_options
|
||||
from libs.elixir.relationships import ManyToMany
|
||||
from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, Float, \
|
||||
String
|
||||
|
||||
options_defaults["shortnames"] = True
|
||||
|
||||
@@ -20,24 +22,48 @@ class Movie(Entity):
|
||||
The files belonging to the movie object are global for the whole movie
|
||||
such as trailers, nfo, thumbnails"""
|
||||
|
||||
library = ManyToOne('Library')
|
||||
last_edit = Field(Integer)
|
||||
|
||||
library = ManyToOne('Library')
|
||||
status = ManyToOne('Status')
|
||||
profile = ManyToOne('Profile')
|
||||
releases = OneToMany('Release')
|
||||
files = OneToMany('File')
|
||||
files = ManyToMany('File')
|
||||
|
||||
|
||||
class Library(Entity):
|
||||
""""""
|
||||
|
||||
title = Field(Unicode)
|
||||
year = Field(Integer)
|
||||
identifier = Field(Unicode)
|
||||
identifier = Field(String(20))
|
||||
rating = Field(Float)
|
||||
|
||||
plot = Field(UnicodeText)
|
||||
tagline = Field(UnicodeText(255))
|
||||
|
||||
status = ManyToOne('Status')
|
||||
movie = OneToMany('Movie')
|
||||
titles = OneToMany('LibraryTitle')
|
||||
files = ManyToMany('File')
|
||||
|
||||
|
||||
class LibraryTitle(Entity):
|
||||
""""""
|
||||
|
||||
title = Field(Unicode)
|
||||
default = Field(Boolean)
|
||||
|
||||
language = OneToMany('Language')
|
||||
libraries = ManyToOne('Library')
|
||||
|
||||
|
||||
class Language(Entity):
|
||||
""""""
|
||||
|
||||
identifier = Field(String(20))
|
||||
label = Field(Unicode)
|
||||
|
||||
titles = ManyToOne('LibraryTitle')
|
||||
|
||||
|
||||
class Release(Entity):
|
||||
@@ -47,7 +73,7 @@ class Release(Entity):
|
||||
movie = ManyToOne('Movie')
|
||||
status = ManyToOne('Status')
|
||||
quality = ManyToOne('Quality')
|
||||
files = OneToMany('File')
|
||||
files = ManyToMany('File')
|
||||
history = OneToMany('History')
|
||||
|
||||
|
||||
@@ -55,41 +81,49 @@ class Status(Entity):
|
||||
"""The status of a release, such as Downloaded, Deleted, Wanted etc"""
|
||||
|
||||
identifier = Field(String(20), unique = True)
|
||||
label = Field(String(20))
|
||||
label = Field(Unicode(20))
|
||||
|
||||
releases = OneToMany('Release')
|
||||
movies = OneToMany('Movie')
|
||||
|
||||
|
||||
class Quality(Entity):
|
||||
"""Quality name of a release, DVD, 720P, DVD-Rip etc"""
|
||||
using_options(order_by = 'order')
|
||||
|
||||
identifier = Field(String(20), unique = True)
|
||||
label = Field(String(20))
|
||||
label = Field(Unicode(20))
|
||||
order = Field(Integer)
|
||||
|
||||
size_min = Field(Integer)
|
||||
size_max = Field(Integer)
|
||||
|
||||
releases = OneToMany('Release')
|
||||
profile_types = ManyToOne('ProfileType')
|
||||
profile_types = OneToMany('ProfileType')
|
||||
|
||||
|
||||
class Profile(Entity):
|
||||
""""""
|
||||
using_options(order_by = 'order')
|
||||
|
||||
identifier = Field(String(20), unique = True)
|
||||
label = Field(Unicode(50))
|
||||
order = Field(Integer)
|
||||
wait_for = Field(Integer)
|
||||
core = Field(Boolean)
|
||||
hide = Field(Boolean)
|
||||
|
||||
movie = OneToMany('Movie')
|
||||
profile_type = OneToMany('ProfileType')
|
||||
types = OneToMany('ProfileType', cascade = 'all, delete-orphan')
|
||||
|
||||
|
||||
class ProfileType(Entity):
|
||||
""""""
|
||||
using_options(order_by = 'order')
|
||||
|
||||
order = Field(Integer)
|
||||
mark_completed = Field(Boolean)
|
||||
finish = Field(Boolean)
|
||||
wait_for = Field(Integer)
|
||||
|
||||
type = OneToMany('Quality')
|
||||
quality = ManyToOne('Quality')
|
||||
profile = ManyToOne('Profile')
|
||||
|
||||
|
||||
@@ -97,19 +131,22 @@ class File(Entity):
|
||||
"""File that belongs to a release."""
|
||||
|
||||
path = Field(Unicode(255), nullable = False, unique = True)
|
||||
part = Field(Integer)
|
||||
part = Field(Integer, default = 1)
|
||||
|
||||
history = OneToMany('RenameHistory')
|
||||
movie = ManyToOne('Movie')
|
||||
release = ManyToOne('Release')
|
||||
type = ManyToOne('FileType')
|
||||
properties = OneToMany('FileProperty')
|
||||
|
||||
history = OneToMany('RenameHistory')
|
||||
movie = ManyToMany('Movie')
|
||||
release = ManyToMany('Release')
|
||||
library = ManyToMany('Library')
|
||||
|
||||
|
||||
class FileType(Entity):
|
||||
"""Types could be trailer, subtitle, movie, partial movie etc."""
|
||||
|
||||
identifier = Field(String(20), unique = True)
|
||||
type = Field(Unicode(20))
|
||||
name = Field(Unicode(50), nullable = False)
|
||||
|
||||
files = OneToMany('File')
|
||||
@@ -135,8 +172,8 @@ class History(Entity):
|
||||
class RenameHistory(Entity):
|
||||
"""Remembers from where to where files have been moved."""
|
||||
|
||||
old = Field(String(255))
|
||||
new = Field(String(255))
|
||||
old = Field(Unicode(255))
|
||||
new = Field(Unicode(255))
|
||||
|
||||
file = ManyToOne('File')
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 228 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 206 B |
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
@@ -34,6 +34,7 @@ Block.Search = new Class({
|
||||
self.spinner = new Spinner(self.result_container);
|
||||
|
||||
self.OuterClickStack = new EventStack.OuterClick();
|
||||
History.addEvent('change', self.hideResults.bind(self, true));
|
||||
|
||||
//debug
|
||||
//self.input.set('value', 'kick ass')
|
||||
@@ -126,16 +127,16 @@ Block.Search = new Class({
|
||||
|
||||
Object.each(json.movies, function(movie){
|
||||
|
||||
if(!movie.imdb || (movie.imdb && !self.results.getElement('#'+movie.imdb))){
|
||||
// if(!movie.imdb || (movie.imdb && !self.results.getElement('#'+movie.imdb))){
|
||||
var m = new Block.Search.Item(movie);
|
||||
$(m).inject(self.results)
|
||||
self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m
|
||||
}
|
||||
else {
|
||||
self.movies[movie.imdb].alternativeName({
|
||||
'name': movie.name
|
||||
})
|
||||
}
|
||||
// }
|
||||
// else {
|
||||
// self.movies[movie.imdb].alternativeTitle({
|
||||
// 'title': movie.title
|
||||
// })
|
||||
// }
|
||||
|
||||
});
|
||||
|
||||
@@ -157,7 +158,7 @@ Block.Search.Item = new Class({
|
||||
var self = this;
|
||||
|
||||
self.info = info;
|
||||
self.alternative_names = [];
|
||||
self.alternative_titles = [];
|
||||
|
||||
self.create();
|
||||
|
||||
@@ -167,7 +168,7 @@ Block.Search.Item = new Class({
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
var info = self.info
|
||||
var info = self.info;
|
||||
|
||||
self.el = new Element('div.movie', {
|
||||
'id': info.imdb
|
||||
@@ -182,12 +183,12 @@ Block.Search.Item = new Class({
|
||||
'click': self.showOptions.bind(self)
|
||||
}
|
||||
}).adopt(
|
||||
self.thumbnail = info.poster ? new Element('img.thumbnail', {
|
||||
'src': info.poster
|
||||
self.thumbnail = info.images.posters.length > 0 ? new Element('img.thumbnail', {
|
||||
'src': info.images.posters[0]
|
||||
}) : null,
|
||||
new Element('div.info').adopt(
|
||||
self.name = new Element('h2', {
|
||||
'text': info.name
|
||||
self.title = new Element('h2', {
|
||||
'text': info.titles[0]
|
||||
}).adopt(
|
||||
self.year = info.year ? new Element('span', {
|
||||
'text': info.year
|
||||
@@ -214,15 +215,18 @@ Block.Search.Item = new Class({
|
||||
})
|
||||
}
|
||||
|
||||
self.alternativeName({
|
||||
'name': info.name
|
||||
});
|
||||
|
||||
info.titles.each(function(title){
|
||||
self.alternativeTitle({
|
||||
'title': title
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
alternativeName: function(alternative){
|
||||
alternativeTitle: function(alternative){
|
||||
var self = this;
|
||||
|
||||
self.alternative_names.include(alternative);
|
||||
self.alternative_titles.include(alternative);
|
||||
},
|
||||
|
||||
showOptions: function(){
|
||||
@@ -246,8 +250,8 @@ Block.Search.Item = new Class({
|
||||
Api.request('movie.add', {
|
||||
'data': {
|
||||
'identifier': self.info.imdb,
|
||||
'name': self.name_select.get('value'),
|
||||
'quality': self.quality_select.get('value')
|
||||
'title': self.title_select.get('value'),
|
||||
'profile_id': self.profile_select.get('value')
|
||||
},
|
||||
'useSpinner': true,
|
||||
'spinnerTarget': self.options,
|
||||
@@ -277,14 +281,14 @@ Block.Search.Item = new Class({
|
||||
|
||||
self.options.adopt(
|
||||
new Element('div').adopt(
|
||||
self.info.poster ? new Element('img.thumbnail', {
|
||||
'src': self.info.poster
|
||||
self.info.images.posters.length > 0 ? new Element('img.thumbnail', {
|
||||
'src': self.info.images.posters[0]
|
||||
}) : null,
|
||||
self.name_select = new Element('select', {
|
||||
'name': 'name'
|
||||
self.title_select = new Element('select', {
|
||||
'name': 'title'
|
||||
}),
|
||||
self.quality_select = new Element('select', {
|
||||
'name': 'profile_identifier'
|
||||
self.profile_select = new Element('select', {
|
||||
'name': 'profile'
|
||||
}),
|
||||
new Element('a.button', {
|
||||
'text': 'Add',
|
||||
@@ -295,17 +299,17 @@ Block.Search.Item = new Class({
|
||||
)
|
||||
);
|
||||
|
||||
Array.each(self.alternative_names, function(alt){
|
||||
Array.each(self.alternative_titles, function(alt){
|
||||
new Element('option', {
|
||||
'text': alt.name
|
||||
}).inject(self.name_select)
|
||||
'text': alt.title
|
||||
}).inject(self.title_select)
|
||||
})
|
||||
|
||||
Array.each(Quality.profiles, function(q){
|
||||
Array.each(Quality.profiles, function(profile){
|
||||
new Element('option', {
|
||||
'value': q.indentifier,
|
||||
'text': q.label
|
||||
}).inject(self.quality_select)
|
||||
'value': profile.id ? profile.id : profile.data.id,
|
||||
'text': profile.label ? profile.label : profile.data.label
|
||||
}).inject(self.profile_select)
|
||||
});
|
||||
|
||||
self.options.addClass('set');
|
||||
|
||||
@@ -114,8 +114,8 @@ var ApiClass = new Class({
|
||||
}, options)).send()
|
||||
},
|
||||
|
||||
createUrl: function(action){
|
||||
return this.options.url + (action || 'default') + '/'
|
||||
createUrl: function(action, params){
|
||||
return this.options.url + (action || 'default') + '/' + (params ? '?'+Object.toQueryString(params) : '')
|
||||
},
|
||||
|
||||
getOption: function(name){
|
||||
@@ -186,6 +186,17 @@ var p = function(){
|
||||
console.log(arguments)
|
||||
};
|
||||
|
||||
function randomString(length, extra) {
|
||||
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz" + (extra ? '-._!@#$%^&*()+=' : '');
|
||||
var stringLength = length || 8;
|
||||
var randomString = '';
|
||||
for (var i = 0; i < stringLength; i++) {
|
||||
var rnum = Math.floor(Math.random() * chars.length);
|
||||
randomString += chars.charAt(rnum);
|
||||
}
|
||||
return randomString;
|
||||
}
|
||||
|
||||
(function(){
|
||||
|
||||
var keyPaths = [];
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
var File = new Class({
|
||||
|
||||
initialize: function(file){
|
||||
var self = this;
|
||||
|
||||
self.data = file;
|
||||
self.type = File.Type.get(file.type_id);
|
||||
|
||||
self['create'+(self.type.type).capitalize()]()
|
||||
|
||||
},
|
||||
|
||||
createImage: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('div.type_image').adopt(
|
||||
new Element('img', {
|
||||
'src': Api.createUrl('file.cache') + self.data.path.substring(1) + '/'
|
||||
})
|
||||
)
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var FileSelect = new Class({
|
||||
|
||||
multiple: function(type, files, single){
|
||||
|
||||
var results = files.filter(function(file){
|
||||
return file.type_id == File.Type.get(type).id;
|
||||
});
|
||||
|
||||
if(single){
|
||||
results = new File(results.pop());
|
||||
}
|
||||
else {
|
||||
|
||||
}
|
||||
|
||||
return results;
|
||||
|
||||
},
|
||||
|
||||
single: function(type, files){
|
||||
return this.multiple(type, files, true);
|
||||
}
|
||||
|
||||
});
|
||||
window.File.Select = new FileSelect();
|
||||
|
||||
var FileTypeBase = new Class({
|
||||
|
||||
setup: function(types){
|
||||
var self = this;
|
||||
|
||||
self.typesById = {};
|
||||
self.typesByKey = {};
|
||||
Object.each(types, function(type){
|
||||
self.typesByKey[type.identifier] = type;
|
||||
self.typesById[type.id] = type;
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
get: function(identifier){
|
||||
if(typeOf(identifier) == 'number')
|
||||
return this.typesById[identifier]
|
||||
else
|
||||
return this.typesByKey[identifier]
|
||||
}
|
||||
|
||||
});
|
||||
window.File.Type = new FileTypeBase();
|
||||
@@ -3,10 +3,10 @@
|
||||
MooTools: the javascript framework
|
||||
|
||||
web build:
|
||||
- http://mootools.net/core/bd6349d3fbc489736e5aefb01157c8a8
|
||||
- http://mootools.net/core/c1215700e7dedaa9d48503126daf2111
|
||||
|
||||
packager build:
|
||||
- packager build Core/Class Core/Class.Extras Core/Element Core/Element.Style Core/Element.Dimensions Core/Fx.Tween Core/Fx.Transitions Core/Request.JSON Core/DOMReady
|
||||
- packager build Core/Class Core/Class.Extras Core/Element Core/Element.Style Core/Element.Dimensions Core/Fx.Tween Core/Fx.Morph Core/Fx.Transitions Core/Request.JSON Core/DOMReady
|
||||
|
||||
/*
|
||||
---
|
||||
@@ -4085,6 +4085,85 @@ Element.implement({
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
---
|
||||
|
||||
name: Fx.Morph
|
||||
|
||||
description: Formerly Fx.Styles, effect to transition any number of CSS properties for an element using an object of rules, or CSS based selector rules.
|
||||
|
||||
license: MIT-style license.
|
||||
|
||||
requires: Fx.CSS
|
||||
|
||||
provides: Fx.Morph
|
||||
|
||||
...
|
||||
*/
|
||||
|
||||
Fx.Morph = new Class({
|
||||
|
||||
Extends: Fx.CSS,
|
||||
|
||||
initialize: function(element, options){
|
||||
this.element = this.subject = document.id(element);
|
||||
this.parent(options);
|
||||
},
|
||||
|
||||
set: function(now){
|
||||
if (typeof now == 'string') now = this.search(now);
|
||||
for (var p in now) this.render(this.element, p, now[p], this.options.unit);
|
||||
return this;
|
||||
},
|
||||
|
||||
compute: function(from, to, delta){
|
||||
var now = {};
|
||||
for (var p in from) now[p] = this.parent(from[p], to[p], delta);
|
||||
return now;
|
||||
},
|
||||
|
||||
start: function(properties){
|
||||
if (!this.check(properties)) return this;
|
||||
if (typeof properties == 'string') properties = this.search(properties);
|
||||
var from = {}, to = {};
|
||||
for (var p in properties){
|
||||
var parsed = this.prepare(this.element, p, properties[p]);
|
||||
from[p] = parsed.from;
|
||||
to[p] = parsed.to;
|
||||
}
|
||||
return this.parent(from, to);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Element.Properties.morph = {
|
||||
|
||||
set: function(options){
|
||||
this.get('morph').cancel().setOptions(options);
|
||||
return this;
|
||||
},
|
||||
|
||||
get: function(){
|
||||
var morph = this.retrieve('morph');
|
||||
if (!morph){
|
||||
morph = new Fx.Morph(this, {link: 'cancel'});
|
||||
this.store('morph', morph);
|
||||
}
|
||||
return morph;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Element.implement({
|
||||
|
||||
morph: function(props){
|
||||
this.get('morph').start(props);
|
||||
return this;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// MooTools: the javascript framework.
|
||||
// Load this file's selection again by visiting: http://mootools.net/more/a6033a03c64d978cd0033a1d420e16e0
|
||||
// Or build this file again with packager using: packager build More/Element.Forms More/Element.Delegation More/Element.Shortcuts More/Request.JSONP More/Spinner
|
||||
// Load this file's selection again by visiting: http://mootools.net/more/2b832e45b9bf2f9e5fdbdafc9b16febf
|
||||
// Or build this file again with packager using: packager build More/Element.Forms More/Element.Delegation More/Element.Shortcuts More/Fx.Slide More/Sortables More/Request.JSONP More/Spinner
|
||||
/*
|
||||
---
|
||||
|
||||
@@ -767,6 +767,849 @@ Document.implement({
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
---
|
||||
|
||||
script: Fx.Slide.js
|
||||
|
||||
name: Fx.Slide
|
||||
|
||||
description: Effect to slide an element in and out of view.
|
||||
|
||||
license: MIT-style license
|
||||
|
||||
authors:
|
||||
- Valerio Proietti
|
||||
|
||||
requires:
|
||||
- Core/Fx
|
||||
- Core/Element.Style
|
||||
- /MooTools.More
|
||||
|
||||
provides: [Fx.Slide]
|
||||
|
||||
...
|
||||
*/
|
||||
|
||||
Fx.Slide = new Class({
|
||||
|
||||
Extends: Fx,
|
||||
|
||||
options: {
|
||||
mode: 'vertical',
|
||||
wrapper: false,
|
||||
hideOverflow: true,
|
||||
resetHeight: false
|
||||
},
|
||||
|
||||
initialize: function(element, options){
|
||||
element = this.element = this.subject = document.id(element);
|
||||
this.parent(options);
|
||||
options = this.options;
|
||||
|
||||
var wrapper = element.retrieve('wrapper'),
|
||||
styles = element.getStyles('margin', 'position', 'overflow');
|
||||
|
||||
if (options.hideOverflow) styles = Object.append(styles, {overflow: 'hidden'});
|
||||
if (options.wrapper) wrapper = document.id(options.wrapper).setStyles(styles);
|
||||
|
||||
if (!wrapper) wrapper = new Element('div', {
|
||||
styles: styles
|
||||
}).wraps(element);
|
||||
|
||||
element.store('wrapper', wrapper).setStyle('margin', 0);
|
||||
if (element.getStyle('overflow') == 'visible') element.setStyle('overflow', 'hidden');
|
||||
|
||||
this.now = [];
|
||||
this.open = true;
|
||||
this.wrapper = wrapper;
|
||||
|
||||
this.addEvent('complete', function(){
|
||||
this.open = (wrapper['offset' + this.layout.capitalize()] != 0);
|
||||
if (this.open && options.resetHeight) wrapper.setStyle('height', '');
|
||||
}, true);
|
||||
},
|
||||
|
||||
vertical: function(){
|
||||
this.margin = 'margin-top';
|
||||
this.layout = 'height';
|
||||
this.offset = this.element.offsetHeight;
|
||||
},
|
||||
|
||||
horizontal: function(){
|
||||
this.margin = 'margin-left';
|
||||
this.layout = 'width';
|
||||
this.offset = this.element.offsetWidth;
|
||||
},
|
||||
|
||||
set: function(now){
|
||||
this.element.setStyle(this.margin, now[0]);
|
||||
this.wrapper.setStyle(this.layout, now[1]);
|
||||
return this;
|
||||
},
|
||||
|
||||
compute: function(from, to, delta){
|
||||
return [0, 1].map(function(i){
|
||||
return Fx.compute(from[i], to[i], delta);
|
||||
});
|
||||
},
|
||||
|
||||
start: function(how, mode){
|
||||
if (!this.check(how, mode)) return this;
|
||||
this[mode || this.options.mode]();
|
||||
|
||||
var margin = this.element.getStyle(this.margin).toInt(),
|
||||
layout = this.wrapper.getStyle(this.layout).toInt(),
|
||||
caseIn = [[margin, layout], [0, this.offset]],
|
||||
caseOut = [[margin, layout], [-this.offset, 0]],
|
||||
start;
|
||||
|
||||
switch (how){
|
||||
case 'in': start = caseIn; break;
|
||||
case 'out': start = caseOut; break;
|
||||
case 'toggle': start = (layout == 0) ? caseIn : caseOut;
|
||||
}
|
||||
return this.parent(start[0], start[1]);
|
||||
},
|
||||
|
||||
slideIn: function(mode){
|
||||
return this.start('in', mode);
|
||||
},
|
||||
|
||||
slideOut: function(mode){
|
||||
return this.start('out', mode);
|
||||
},
|
||||
|
||||
hide: function(mode){
|
||||
this[mode || this.options.mode]();
|
||||
this.open = false;
|
||||
return this.set([-this.offset, 0]);
|
||||
},
|
||||
|
||||
show: function(mode){
|
||||
this[mode || this.options.mode]();
|
||||
this.open = true;
|
||||
return this.set([0, this.offset]);
|
||||
},
|
||||
|
||||
toggle: function(mode){
|
||||
return this.start('toggle', mode);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Element.Properties.slide = {
|
||||
|
||||
set: function(options){
|
||||
this.get('slide').cancel().setOptions(options);
|
||||
return this;
|
||||
},
|
||||
|
||||
get: function(){
|
||||
var slide = this.retrieve('slide');
|
||||
if (!slide){
|
||||
slide = new Fx.Slide(this, {link: 'cancel'});
|
||||
this.store('slide', slide);
|
||||
}
|
||||
return slide;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Element.implement({
|
||||
|
||||
slide: function(how, mode){
|
||||
how = how || 'toggle';
|
||||
var slide = this.get('slide'), toggle;
|
||||
switch (how){
|
||||
case 'hide': slide.hide(mode); break;
|
||||
case 'show': slide.show(mode); break;
|
||||
case 'toggle':
|
||||
var flag = this.retrieve('slide:flag', slide.open);
|
||||
slide[flag ? 'slideOut' : 'slideIn'](mode);
|
||||
this.store('slide:flag', !flag);
|
||||
toggle = true;
|
||||
break;
|
||||
default: slide.start(how, mode);
|
||||
}
|
||||
if (!toggle) this.eliminate('slide:flag');
|
||||
return this;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
---
|
||||
|
||||
script: Drag.js
|
||||
|
||||
name: Drag
|
||||
|
||||
description: The base Drag Class. Can be used to drag and resize Elements using mouse events.
|
||||
|
||||
license: MIT-style license
|
||||
|
||||
authors:
|
||||
- Valerio Proietti
|
||||
- Tom Occhinno
|
||||
- Jan Kassens
|
||||
|
||||
requires:
|
||||
- Core/Events
|
||||
- Core/Options
|
||||
- Core/Element.Event
|
||||
- Core/Element.Style
|
||||
- Core/Element.Dimensions
|
||||
- /MooTools.More
|
||||
|
||||
provides: [Drag]
|
||||
...
|
||||
|
||||
*/
|
||||
|
||||
var Drag = new Class({
|
||||
|
||||
Implements: [Events, Options],
|
||||
|
||||
options: {/*
|
||||
onBeforeStart: function(thisElement){},
|
||||
onStart: function(thisElement, event){},
|
||||
onSnap: function(thisElement){},
|
||||
onDrag: function(thisElement, event){},
|
||||
onCancel: function(thisElement){},
|
||||
onComplete: function(thisElement, event){},*/
|
||||
snap: 6,
|
||||
unit: 'px',
|
||||
grid: false,
|
||||
style: true,
|
||||
limit: false,
|
||||
handle: false,
|
||||
invert: false,
|
||||
preventDefault: false,
|
||||
stopPropagation: false,
|
||||
modifiers: {x: 'left', y: 'top'}
|
||||
},
|
||||
|
||||
initialize: function(){
|
||||
var params = Array.link(arguments, {
|
||||
'options': Type.isObject,
|
||||
'element': function(obj){
|
||||
return obj != null;
|
||||
}
|
||||
});
|
||||
|
||||
this.element = document.id(params.element);
|
||||
this.document = this.element.getDocument();
|
||||
this.setOptions(params.options || {});
|
||||
var htype = typeOf(this.options.handle);
|
||||
this.handles = ((htype == 'array' || htype == 'collection') ? $$(this.options.handle) : document.id(this.options.handle)) || this.element;
|
||||
this.mouse = {'now': {}, 'pos': {}};
|
||||
this.value = {'start': {}, 'now': {}};
|
||||
|
||||
this.selection = (Browser.ie) ? 'selectstart' : 'mousedown';
|
||||
|
||||
|
||||
if (Browser.ie && !Drag.ondragstartFixed){
|
||||
document.ondragstart = Function.from(false);
|
||||
Drag.ondragstartFixed = true;
|
||||
}
|
||||
|
||||
this.bound = {
|
||||
start: this.start.bind(this),
|
||||
check: this.check.bind(this),
|
||||
drag: this.drag.bind(this),
|
||||
stop: this.stop.bind(this),
|
||||
cancel: this.cancel.bind(this),
|
||||
eventStop: Function.from(false)
|
||||
};
|
||||
this.attach();
|
||||
},
|
||||
|
||||
attach: function(){
|
||||
this.handles.addEvent('mousedown', this.bound.start);
|
||||
return this;
|
||||
},
|
||||
|
||||
detach: function(){
|
||||
this.handles.removeEvent('mousedown', this.bound.start);
|
||||
return this;
|
||||
},
|
||||
|
||||
start: function(event){
|
||||
var options = this.options;
|
||||
|
||||
if (event.rightClick) return;
|
||||
|
||||
if (options.preventDefault) event.preventDefault();
|
||||
if (options.stopPropagation) event.stopPropagation();
|
||||
this.mouse.start = event.page;
|
||||
|
||||
this.fireEvent('beforeStart', this.element);
|
||||
|
||||
var limit = options.limit;
|
||||
this.limit = {x: [], y: []};
|
||||
|
||||
var styles = this.element.getStyles('left', 'right', 'top', 'bottom');
|
||||
this._invert = {
|
||||
x: options.modifiers.x == 'left' && styles.left == 'auto' && !isNaN(styles.right.toInt()) && (options.modifiers.x = 'right'),
|
||||
y: options.modifiers.y == 'top' && styles.top == 'auto' && !isNaN(styles.bottom.toInt()) && (options.modifiers.y = 'bottom')
|
||||
};
|
||||
|
||||
var z, coordinates;
|
||||
for (z in options.modifiers){
|
||||
if (!options.modifiers[z]) continue;
|
||||
|
||||
var style = this.element.getStyle(options.modifiers[z]);
|
||||
|
||||
// Some browsers (IE and Opera) don't always return pixels.
|
||||
if (style && !style.match(/px$/)){
|
||||
if (!coordinates) coordinates = this.element.getCoordinates(this.element.getOffsetParent());
|
||||
style = coordinates[options.modifiers[z]];
|
||||
}
|
||||
|
||||
if (options.style) this.value.now[z] = (style || 0).toInt();
|
||||
else this.value.now[z] = this.element[options.modifiers[z]];
|
||||
|
||||
if (options.invert) this.value.now[z] *= -1;
|
||||
if (this._invert[z]) this.value.now[z] *= -1;
|
||||
|
||||
this.mouse.pos[z] = event.page[z] - this.value.now[z];
|
||||
|
||||
if (limit && limit[z]){
|
||||
var i = 2;
|
||||
while (i--){
|
||||
var limitZI = limit[z][i];
|
||||
if (limitZI || limitZI === 0) this.limit[z][i] = (typeof limitZI == 'function') ? limitZI() : limitZI;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeOf(this.options.grid) == 'number') this.options.grid = {
|
||||
x: this.options.grid,
|
||||
y: this.options.grid
|
||||
};
|
||||
|
||||
var events = {
|
||||
mousemove: this.bound.check,
|
||||
mouseup: this.bound.cancel
|
||||
};
|
||||
events[this.selection] = this.bound.eventStop;
|
||||
this.document.addEvents(events);
|
||||
},
|
||||
|
||||
check: function(event){
|
||||
if (this.options.preventDefault) event.preventDefault();
|
||||
var distance = Math.round(Math.sqrt(Math.pow(event.page.x - this.mouse.start.x, 2) + Math.pow(event.page.y - this.mouse.start.y, 2)));
|
||||
if (distance > this.options.snap){
|
||||
this.cancel();
|
||||
this.document.addEvents({
|
||||
mousemove: this.bound.drag,
|
||||
mouseup: this.bound.stop
|
||||
});
|
||||
this.fireEvent('start', [this.element, event]).fireEvent('snap', this.element);
|
||||
}
|
||||
},
|
||||
|
||||
drag: function(event){
|
||||
var options = this.options;
|
||||
|
||||
if (options.preventDefault) event.preventDefault();
|
||||
this.mouse.now = event.page;
|
||||
|
||||
for (var z in options.modifiers){
|
||||
if (!options.modifiers[z]) continue;
|
||||
this.value.now[z] = this.mouse.now[z] - this.mouse.pos[z];
|
||||
|
||||
if (options.invert) this.value.now[z] *= -1;
|
||||
if (this._invert[z]) this.value.now[z] *= -1;
|
||||
|
||||
if (options.limit && this.limit[z]){
|
||||
if ((this.limit[z][1] || this.limit[z][1] === 0) && (this.value.now[z] > this.limit[z][1])){
|
||||
this.value.now[z] = this.limit[z][1];
|
||||
} else if ((this.limit[z][0] || this.limit[z][0] === 0) && (this.value.now[z] < this.limit[z][0])){
|
||||
this.value.now[z] = this.limit[z][0];
|
||||
}
|
||||
}
|
||||
|
||||
if (options.grid[z]) this.value.now[z] -= ((this.value.now[z] - (this.limit[z][0]||0)) % options.grid[z]);
|
||||
|
||||
if (options.style) this.element.setStyle(options.modifiers[z], this.value.now[z] + options.unit);
|
||||
else this.element[options.modifiers[z]] = this.value.now[z];
|
||||
}
|
||||
|
||||
this.fireEvent('drag', [this.element, event]);
|
||||
},
|
||||
|
||||
cancel: function(event){
|
||||
this.document.removeEvents({
|
||||
mousemove: this.bound.check,
|
||||
mouseup: this.bound.cancel
|
||||
});
|
||||
if (event){
|
||||
this.document.removeEvent(this.selection, this.bound.eventStop);
|
||||
this.fireEvent('cancel', this.element);
|
||||
}
|
||||
},
|
||||
|
||||
stop: function(event){
|
||||
var events = {
|
||||
mousemove: this.bound.drag,
|
||||
mouseup: this.bound.stop
|
||||
};
|
||||
events[this.selection] = this.bound.eventStop;
|
||||
this.document.removeEvents(events);
|
||||
if (event) this.fireEvent('complete', [this.element, event]);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Element.implement({
|
||||
|
||||
makeResizable: function(options){
|
||||
var drag = new Drag(this, Object.merge({
|
||||
modifiers: {
|
||||
x: 'width',
|
||||
y: 'height'
|
||||
}
|
||||
}, options));
|
||||
|
||||
this.store('resizer', drag);
|
||||
return drag.addEvent('drag', function(){
|
||||
this.fireEvent('resize', drag);
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
---
|
||||
|
||||
script: Drag.Move.js
|
||||
|
||||
name: Drag.Move
|
||||
|
||||
description: A Drag extension that provides support for the constraining of draggables to containers and droppables.
|
||||
|
||||
license: MIT-style license
|
||||
|
||||
authors:
|
||||
- Valerio Proietti
|
||||
- Tom Occhinno
|
||||
- Jan Kassens
|
||||
- Aaron Newton
|
||||
- Scott Kyle
|
||||
|
||||
requires:
|
||||
- Core/Element.Dimensions
|
||||
- /Drag
|
||||
|
||||
provides: [Drag.Move]
|
||||
|
||||
...
|
||||
*/
|
||||
|
||||
Drag.Move = new Class({
|
||||
|
||||
Extends: Drag,
|
||||
|
||||
options: {/*
|
||||
onEnter: function(thisElement, overed){},
|
||||
onLeave: function(thisElement, overed){},
|
||||
onDrop: function(thisElement, overed, event){},*/
|
||||
droppables: [],
|
||||
container: false,
|
||||
precalculate: false,
|
||||
includeMargins: true,
|
||||
checkDroppables: true
|
||||
},
|
||||
|
||||
initialize: function(element, options){
|
||||
this.parent(element, options);
|
||||
element = this.element;
|
||||
|
||||
this.droppables = $$(this.options.droppables);
|
||||
this.container = document.id(this.options.container);
|
||||
|
||||
if (this.container && typeOf(this.container) != 'element')
|
||||
this.container = document.id(this.container.getDocument().body);
|
||||
|
||||
if (this.options.style){
|
||||
if (this.options.modifiers.x == "left" && this.options.modifiers.y == "top"){
|
||||
var parentStyles,
|
||||
parent = element.getOffsetParent();
|
||||
var styles = element.getStyles('left', 'top');
|
||||
if (parent && (styles.left == 'auto' || styles.top == 'auto')){
|
||||
element.setPosition(element.getPosition(parent));
|
||||
}
|
||||
}
|
||||
|
||||
if (element.getStyle('position') == 'static') element.setStyle('position', 'absolute');
|
||||
}
|
||||
|
||||
this.addEvent('start', this.checkDroppables, true);
|
||||
this.overed = null;
|
||||
},
|
||||
|
||||
start: function(event){
|
||||
if (this.container) this.options.limit = this.calculateLimit();
|
||||
|
||||
if (this.options.precalculate){
|
||||
this.positions = this.droppables.map(function(el){
|
||||
return el.getCoordinates();
|
||||
});
|
||||
}
|
||||
|
||||
this.parent(event);
|
||||
},
|
||||
|
||||
calculateLimit: function(){
|
||||
var element = this.element,
|
||||
container = this.container,
|
||||
|
||||
offsetParent = document.id(element.getOffsetParent()) || document.body,
|
||||
containerCoordinates = container.getCoordinates(offsetParent),
|
||||
elementMargin = {},
|
||||
elementBorder = {},
|
||||
containerMargin = {},
|
||||
containerBorder = {},
|
||||
offsetParentPadding = {};
|
||||
|
||||
['top', 'right', 'bottom', 'left'].each(function(pad){
|
||||
elementMargin[pad] = element.getStyle('margin-' + pad).toInt();
|
||||
elementBorder[pad] = element.getStyle('border-' + pad).toInt();
|
||||
containerMargin[pad] = container.getStyle('margin-' + pad).toInt();
|
||||
containerBorder[pad] = container.getStyle('border-' + pad).toInt();
|
||||
offsetParentPadding[pad] = offsetParent.getStyle('padding-' + pad).toInt();
|
||||
}, this);
|
||||
|
||||
var width = element.offsetWidth + elementMargin.left + elementMargin.right,
|
||||
height = element.offsetHeight + elementMargin.top + elementMargin.bottom,
|
||||
left = 0,
|
||||
top = 0,
|
||||
right = containerCoordinates.right - containerBorder.right - width,
|
||||
bottom = containerCoordinates.bottom - containerBorder.bottom - height;
|
||||
|
||||
if (this.options.includeMargins){
|
||||
left += elementMargin.left;
|
||||
top += elementMargin.top;
|
||||
} else {
|
||||
right += elementMargin.right;
|
||||
bottom += elementMargin.bottom;
|
||||
}
|
||||
|
||||
if (element.getStyle('position') == 'relative'){
|
||||
var coords = element.getCoordinates(offsetParent);
|
||||
coords.left -= element.getStyle('left').toInt();
|
||||
coords.top -= element.getStyle('top').toInt();
|
||||
|
||||
left -= coords.left;
|
||||
top -= coords.top;
|
||||
if (container.getStyle('position') != 'relative'){
|
||||
left += containerBorder.left;
|
||||
top += containerBorder.top;
|
||||
}
|
||||
right += elementMargin.left - coords.left;
|
||||
bottom += elementMargin.top - coords.top;
|
||||
|
||||
if (container != offsetParent){
|
||||
left += containerMargin.left + offsetParentPadding.left;
|
||||
top += ((Browser.ie6 || Browser.ie7) ? 0 : containerMargin.top) + offsetParentPadding.top;
|
||||
}
|
||||
} else {
|
||||
left -= elementMargin.left;
|
||||
top -= elementMargin.top;
|
||||
if (container != offsetParent){
|
||||
left += containerCoordinates.left + containerBorder.left;
|
||||
top += containerCoordinates.top + containerBorder.top;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
x: [left, right],
|
||||
y: [top, bottom]
|
||||
};
|
||||
},
|
||||
|
||||
getDroppableCoordinates: function(element){
|
||||
var position = element.getCoordinates();
|
||||
if (element.getStyle('position') == 'fixed'){
|
||||
var scroll = window.getScroll();
|
||||
position.left += scroll.x;
|
||||
position.right += scroll.x;
|
||||
position.top += scroll.y;
|
||||
position.bottom += scroll.y;
|
||||
}
|
||||
return position;
|
||||
},
|
||||
|
||||
checkDroppables: function(){
|
||||
var overed = this.droppables.filter(function(el, i){
|
||||
el = this.positions ? this.positions[i] : this.getDroppableCoordinates(el);
|
||||
var now = this.mouse.now;
|
||||
return (now.x > el.left && now.x < el.right && now.y < el.bottom && now.y > el.top);
|
||||
}, this).getLast();
|
||||
|
||||
if (this.overed != overed){
|
||||
if (this.overed) this.fireEvent('leave', [this.element, this.overed]);
|
||||
if (overed) this.fireEvent('enter', [this.element, overed]);
|
||||
this.overed = overed;
|
||||
}
|
||||
},
|
||||
|
||||
drag: function(event){
|
||||
this.parent(event);
|
||||
if (this.options.checkDroppables && this.droppables.length) this.checkDroppables();
|
||||
},
|
||||
|
||||
stop: function(event){
|
||||
this.checkDroppables();
|
||||
this.fireEvent('drop', [this.element, this.overed, event]);
|
||||
this.overed = null;
|
||||
return this.parent(event);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Element.implement({
|
||||
|
||||
makeDraggable: function(options){
|
||||
var drag = new Drag.Move(this, options);
|
||||
this.store('dragger', drag);
|
||||
return drag;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
---
|
||||
|
||||
script: Sortables.js
|
||||
|
||||
name: Sortables
|
||||
|
||||
description: Class for creating a drag and drop sorting interface for lists of items.
|
||||
|
||||
license: MIT-style license
|
||||
|
||||
authors:
|
||||
- Tom Occhino
|
||||
|
||||
requires:
|
||||
- Core/Fx.Morph
|
||||
- /Drag.Move
|
||||
|
||||
provides: [Sortables]
|
||||
|
||||
...
|
||||
*/
|
||||
|
||||
var Sortables = new Class({
|
||||
|
||||
Implements: [Events, Options],
|
||||
|
||||
options: {/*
|
||||
onSort: function(element, clone){},
|
||||
onStart: function(element, clone){},
|
||||
onComplete: function(element){},*/
|
||||
opacity: 1,
|
||||
clone: false,
|
||||
revert: false,
|
||||
handle: false,
|
||||
dragOptions: {}
|
||||
},
|
||||
|
||||
initialize: function(lists, options){
|
||||
this.setOptions(options);
|
||||
|
||||
this.elements = [];
|
||||
this.lists = [];
|
||||
this.idle = true;
|
||||
|
||||
this.addLists($$(document.id(lists) || lists));
|
||||
|
||||
if (!this.options.clone) this.options.revert = false;
|
||||
if (this.options.revert) this.effect = new Fx.Morph(null, Object.merge({
|
||||
duration: 250,
|
||||
link: 'cancel'
|
||||
}, this.options.revert));
|
||||
},
|
||||
|
||||
attach: function(){
|
||||
this.addLists(this.lists);
|
||||
return this;
|
||||
},
|
||||
|
||||
detach: function(){
|
||||
this.lists = this.removeLists(this.lists);
|
||||
return this;
|
||||
},
|
||||
|
||||
addItems: function(){
|
||||
Array.flatten(arguments).each(function(element){
|
||||
this.elements.push(element);
|
||||
var start = element.retrieve('sortables:start', function(event){
|
||||
this.start.call(this, event, element);
|
||||
}.bind(this));
|
||||
(this.options.handle ? element.getElement(this.options.handle) || element : element).addEvent('mousedown', start);
|
||||
}, this);
|
||||
return this;
|
||||
},
|
||||
|
||||
addLists: function(){
|
||||
Array.flatten(arguments).each(function(list){
|
||||
this.lists.include(list);
|
||||
this.addItems(list.getChildren());
|
||||
}, this);
|
||||
return this;
|
||||
},
|
||||
|
||||
removeItems: function(){
|
||||
return $$(Array.flatten(arguments).map(function(element){
|
||||
this.elements.erase(element);
|
||||
var start = element.retrieve('sortables:start');
|
||||
(this.options.handle ? element.getElement(this.options.handle) || element : element).removeEvent('mousedown', start);
|
||||
|
||||
return element;
|
||||
}, this));
|
||||
},
|
||||
|
||||
removeLists: function(){
|
||||
return $$(Array.flatten(arguments).map(function(list){
|
||||
this.lists.erase(list);
|
||||
this.removeItems(list.getChildren());
|
||||
|
||||
return list;
|
||||
}, this));
|
||||
},
|
||||
|
||||
getClone: function(event, element){
|
||||
if (!this.options.clone) return new Element(element.tagName).inject(document.body);
|
||||
if (typeOf(this.options.clone) == 'function') return this.options.clone.call(this, event, element, this.list);
|
||||
var clone = element.clone(true).setStyles({
|
||||
margin: 0,
|
||||
position: 'absolute',
|
||||
visibility: 'hidden',
|
||||
width: element.getStyle('width')
|
||||
}).addEvent('mousedown', function(event){
|
||||
element.fireEvent('mousedown', event);
|
||||
});
|
||||
//prevent the duplicated radio inputs from unchecking the real one
|
||||
if (clone.get('html').test('radio')){
|
||||
clone.getElements('input[type=radio]').each(function(input, i){
|
||||
input.set('name', 'clone_' + i);
|
||||
if (input.get('checked')) element.getElements('input[type=radio]')[i].set('checked', true);
|
||||
});
|
||||
}
|
||||
|
||||
return clone.inject(this.list).setPosition(element.getPosition(element.getOffsetParent()));
|
||||
},
|
||||
|
||||
getDroppables: function(){
|
||||
var droppables = this.list.getChildren().erase(this.clone).erase(this.element);
|
||||
if (!this.options.constrain) droppables.append(this.lists).erase(this.list);
|
||||
return droppables;
|
||||
},
|
||||
|
||||
insert: function(dragging, element){
|
||||
var where = 'inside';
|
||||
if (this.lists.contains(element)){
|
||||
this.list = element;
|
||||
this.drag.droppables = this.getDroppables();
|
||||
} else {
|
||||
where = this.element.getAllPrevious().contains(element) ? 'before' : 'after';
|
||||
}
|
||||
this.element.inject(element, where);
|
||||
this.fireEvent('sort', [this.element, this.clone]);
|
||||
},
|
||||
|
||||
start: function(event, element){
|
||||
if (
|
||||
!this.idle ||
|
||||
event.rightClick ||
|
||||
['button', 'input', 'a'].contains(event.target.get('tag'))
|
||||
) return;
|
||||
|
||||
this.idle = false;
|
||||
this.element = element;
|
||||
this.opacity = element.get('opacity');
|
||||
this.list = element.getParent();
|
||||
this.clone = this.getClone(event, element);
|
||||
|
||||
this.drag = new Drag.Move(this.clone, Object.merge({
|
||||
|
||||
droppables: this.getDroppables()
|
||||
}, this.options.dragOptions)).addEvents({
|
||||
onSnap: function(){
|
||||
event.stop();
|
||||
this.clone.setStyle('visibility', 'visible');
|
||||
this.element.set('opacity', this.options.opacity || 0);
|
||||
this.fireEvent('start', [this.element, this.clone]);
|
||||
}.bind(this),
|
||||
onEnter: this.insert.bind(this),
|
||||
onCancel: this.end.bind(this),
|
||||
onComplete: this.end.bind(this)
|
||||
});
|
||||
|
||||
this.clone.inject(this.element, 'before');
|
||||
this.drag.start(event);
|
||||
},
|
||||
|
||||
end: function(){
|
||||
this.drag.detach();
|
||||
this.element.set('opacity', this.opacity);
|
||||
if (this.effect){
|
||||
var dim = this.element.getStyles('width', 'height'),
|
||||
clone = this.clone,
|
||||
pos = clone.computePosition(this.element.getPosition(this.clone.getOffsetParent()));
|
||||
|
||||
var destroy = function(){
|
||||
this.removeEvent('cancel', destroy);
|
||||
clone.destroy();
|
||||
};
|
||||
|
||||
this.effect.element = clone;
|
||||
this.effect.start({
|
||||
top: pos.top,
|
||||
left: pos.left,
|
||||
width: dim.width,
|
||||
height: dim.height,
|
||||
opacity: 0.25
|
||||
}).addEvent('cancel', destroy).chain(destroy);
|
||||
} else {
|
||||
this.clone.destroy();
|
||||
}
|
||||
this.reset();
|
||||
},
|
||||
|
||||
reset: function(){
|
||||
this.idle = true;
|
||||
this.fireEvent('complete', this.element);
|
||||
},
|
||||
|
||||
serialize: function(){
|
||||
var params = Array.link(arguments, {
|
||||
modifier: Type.isFunction,
|
||||
index: function(obj){
|
||||
return obj != null;
|
||||
}
|
||||
});
|
||||
var serial = this.lists.map(function(list){
|
||||
return list.getChildren().map(params.modifier || function(element){
|
||||
return element.get('id');
|
||||
}, this);
|
||||
}, this);
|
||||
|
||||
var index = params.index;
|
||||
if (this.lists.length == 1) index = 0;
|
||||
return (index || index === 0) && index >= 0 && index < this.lists.length ? serial[index] : serial;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
/*
|
||||
---
|
||||
|
||||
|
||||
@@ -11,6 +11,12 @@ Page.Settings = new Class({
|
||||
},
|
||||
'providers': {
|
||||
'label': 'Providers'
|
||||
},
|
||||
'downloaders': {
|
||||
'label': 'Downloaders'
|
||||
},
|
||||
'notifications': {
|
||||
'label': 'Notifications'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -27,13 +33,13 @@ Page.Settings = new Class({
|
||||
|
||||
openTab: function(action){
|
||||
var self = this;
|
||||
action = action || self.action
|
||||
var action = action || self.action;
|
||||
|
||||
if(self.current)
|
||||
self.toggleTab(self.current, true);
|
||||
|
||||
self.toggleTab(action)
|
||||
self.current = action;
|
||||
var tab = self.toggleTab(action)
|
||||
self.current = tab == self.tabs.general ? 'general' : action;
|
||||
|
||||
},
|
||||
|
||||
@@ -47,6 +53,7 @@ Page.Settings = new Class({
|
||||
t.tab[a](c);
|
||||
t.content[a](c);
|
||||
|
||||
return t
|
||||
},
|
||||
|
||||
getData: function(onComplete){
|
||||
@@ -121,7 +128,7 @@ Page.Settings = new Class({
|
||||
|
||||
// Add options to group
|
||||
group.options.sortBy('order').each(function(option){
|
||||
var class_name = (option.type || 'input').capitalize();
|
||||
var class_name = (option.type || 'string').capitalize();
|
||||
var input = new Option[class_name](self, section_name, option.name, option);
|
||||
input.inject(group_el);
|
||||
});
|
||||
@@ -145,13 +152,13 @@ Page.Settings = new Class({
|
||||
var tab_el = new Element('li').adopt(
|
||||
new Element('a', {
|
||||
'href': '/'+self.name+'/'+tab_name+'/',
|
||||
'text': tab.label.capitalize()
|
||||
'text': (tab.label || tab.name).capitalize()
|
||||
})
|
||||
).inject(self.tabs_container);
|
||||
|
||||
if(!self.tabs[tab_name])
|
||||
self.tabs[tab_name] = {
|
||||
'label': tab.label
|
||||
'label': tab.label || tab.name
|
||||
}
|
||||
|
||||
self.tabs[tab_name] = Object.merge(self.tabs[tab_name], {
|
||||
@@ -171,7 +178,7 @@ Page.Settings = new Class({
|
||||
'class': group.advanced ? 'inlineLabels advanced' : 'inlineLabels'
|
||||
}).adopt(
|
||||
new Element('h2', {
|
||||
'text': group.label
|
||||
'text': group.label || group.name.capitalize()
|
||||
}).adopt(
|
||||
new Element('span.hint', {
|
||||
'text': group.description
|
||||
@@ -222,6 +229,13 @@ var OptionBase = new Class({
|
||||
},
|
||||
|
||||
create: function(){},
|
||||
|
||||
createLabel: function(){
|
||||
var self = this;
|
||||
return new Element('label', {
|
||||
'text': self.options.label || self.options.name.capitalize()
|
||||
})
|
||||
},
|
||||
|
||||
setAdvanced: function(){
|
||||
this.el.addClass(this.options.advanced ? 'advanced': '')
|
||||
@@ -319,9 +333,7 @@ Option.String = new Class({
|
||||
var self = this
|
||||
|
||||
self.el.adopt(
|
||||
new Element('label', {
|
||||
'text': self.options.label
|
||||
}),
|
||||
self.createLabel(),
|
||||
self.input = new Element('input', {
|
||||
'type': 'text',
|
||||
'name': self.postName(),
|
||||
@@ -337,18 +349,17 @@ Option.Dropdown = new Class({
|
||||
create: function(){
|
||||
var self = this
|
||||
|
||||
new Element('label', {
|
||||
'text': self.options.label
|
||||
}).adopt(
|
||||
self.el.adopt(
|
||||
self.createLabel(),
|
||||
self.input = new Element('select', {
|
||||
'name': self.postName()
|
||||
})
|
||||
).inject(self.el)
|
||||
)
|
||||
|
||||
Object.each(self.options.values, function(label, value){
|
||||
Object.each(self.options.values, function(value){
|
||||
new Element('option', {
|
||||
'text': label,
|
||||
'value': value
|
||||
'text': value[0],
|
||||
'value': value[1]
|
||||
}).inject(self.input)
|
||||
})
|
||||
|
||||
@@ -366,24 +377,31 @@ Option.Checkbox = new Class({
|
||||
|
||||
var randomId = 'option-'+Math.floor(Math.random()*1000000)
|
||||
|
||||
new Element('label', {
|
||||
'text': self.options.label,
|
||||
'for': randomId
|
||||
}).inject(self.el);
|
||||
|
||||
self.input = new Element('input', {
|
||||
'type': 'checkbox',
|
||||
'value': self.getSettingValue(),
|
||||
'checked': self.getSettingValue() !== undefined,
|
||||
'id': randomId
|
||||
}).inject(self.el);
|
||||
self.el.adopt(
|
||||
self.createLabel().set('for', randomId),
|
||||
self.input = new Element('input', {
|
||||
'type': 'checkbox',
|
||||
'value': self.getSettingValue(),
|
||||
'checked': self.getSettingValue() !== undefined,
|
||||
'id': randomId
|
||||
})
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
Option.Password = new Class({
|
||||
Extends: Option.String,
|
||||
type: 'password'
|
||||
});
|
||||
|
||||
Option.Bool = new Class({
|
||||
Extends: Option.Checkbox
|
||||
});
|
||||
|
||||
Option.Enabler = new Class({
|
||||
Extends: Option.Bool
|
||||
});
|
||||
|
||||
Option.Int = new Class({
|
||||
Extends: Option.String
|
||||
});
|
||||
@@ -401,9 +419,7 @@ Option.Directory = new Class({
|
||||
|
||||
|
||||
self.el.adopt(
|
||||
new Element('label', {
|
||||
'text': self.options.label
|
||||
}),
|
||||
self.createLabel(),
|
||||
self.input = new Element('span', {
|
||||
'text': self.getSettingValue(),
|
||||
'events': {
|
||||
|
||||
@@ -18,19 +18,28 @@ Page.Wanted = new Class({
|
||||
|
||||
if(!self.movie_container)
|
||||
self.movie_container = new Element('div.movies').inject(self.el);
|
||||
|
||||
|
||||
self.movie_container.empty();
|
||||
Object.each(self.movies, function(info){
|
||||
var m = new Movie(self, {}, info);
|
||||
$(m).inject(self.movie_container);
|
||||
});
|
||||
|
||||
self.movie_container.addEvents({
|
||||
'mouseenter:relay(.movie)': function(e, el){
|
||||
el.addClass('hover')
|
||||
},
|
||||
'mouseleave:relay(.movie)': function(e, el){
|
||||
el.removeClass('hover')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
get: function(status, onComplete){
|
||||
var self = this
|
||||
|
||||
if(self.movies.length == 0)
|
||||
Api.request('movie', {
|
||||
Api.request('movie.list', {
|
||||
'data': {},
|
||||
'onComplete': function(json){
|
||||
self.store(json.movies);
|
||||
@@ -58,16 +67,280 @@ var Movie = new Class({
|
||||
|
||||
self.data = data;
|
||||
|
||||
self.profile = Quality.getProfile(data.profile_id);
|
||||
self.parent(self, options);
|
||||
},
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('div.movie', {
|
||||
'text': self.data.name
|
||||
self.el = new Element('div.movie').adopt(
|
||||
self.data_container = new Element('div').adopt(
|
||||
self.thumbnail = File.Select.single('poster', self.data.library.files),
|
||||
self.title = new Element('div.title', {
|
||||
'text': self.getTitle()
|
||||
}),
|
||||
self.description = new Element('div.description', {
|
||||
'text': self.data.library.plot
|
||||
}),
|
||||
self.rating = new Element('div.rating', {
|
||||
'text': self.data.library.rating || 10
|
||||
}),
|
||||
self.year = new Element('div.year', {
|
||||
'text': self.data.library.year || 'Unknown'
|
||||
}),
|
||||
self.quality = new Element('div.quality', {
|
||||
'text': self.profile.get('label')
|
||||
}),
|
||||
self.actions = new Element('div.actions').adopt(
|
||||
self.action_imdb = new Movie.Action.IMDB(self),
|
||||
self.action_edit = new Movie.Action.Edit(self),
|
||||
self.action_refresh = new Movie.Action.Refresh(self),
|
||||
self.action_delete = new Movie.Action.Delete(self)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
},
|
||||
|
||||
getTitle: function(){
|
||||
var self = this;
|
||||
|
||||
var titles = self.data.library.titles;
|
||||
|
||||
var title = titles.filter(function(title){
|
||||
return title['default']
|
||||
}).pop()
|
||||
|
||||
if(title)
|
||||
return title.title
|
||||
else if(titles.length > 0)
|
||||
return titles[0].title
|
||||
|
||||
return 'Unknown movie'
|
||||
},
|
||||
|
||||
get: function(attr){
|
||||
return this.data[attr] || this.data.library[attr]
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
var MovieAction = new Class({
|
||||
|
||||
class_name: 'action',
|
||||
|
||||
initialize: function(movie){
|
||||
var self = this;
|
||||
self.movie = movie;
|
||||
|
||||
self.create();
|
||||
self.el.addClass(self.class_name)
|
||||
},
|
||||
|
||||
create: function(){},
|
||||
|
||||
disable: function(){
|
||||
this.el.addClass('disable')
|
||||
},
|
||||
|
||||
enable: function(){
|
||||
this.el.removeClass('disable')
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
Movie.Action = {}
|
||||
|
||||
Movie.Action.Edit = new Class({
|
||||
|
||||
Extends: MovieAction,
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.edit', {
|
||||
'text': 'edit',
|
||||
'title': 'Refresh the movie info and do a forced search',
|
||||
'events': {
|
||||
'click': self.editMovie.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
editMovie: function(e){
|
||||
var self = this;
|
||||
(e).stop();
|
||||
|
||||
self.optionContainer = new Element('div.options').adopt(
|
||||
$(self.movie.thumbnail).clone(),
|
||||
self.title_select = new Element('select', {
|
||||
'name': 'title'
|
||||
}),
|
||||
self.profile_select = new Element('select', {
|
||||
'name': 'profile'
|
||||
}),
|
||||
new Element('a.button.edit', {
|
||||
'text': 'Save',
|
||||
'events': {
|
||||
'click': self.save.bind(self)
|
||||
}
|
||||
})
|
||||
).inject(self.movie, 'top');
|
||||
},
|
||||
|
||||
save: function(){
|
||||
var self = this;
|
||||
|
||||
Api.request('movie.edit', {
|
||||
'data': {
|
||||
'default_title': self.title_select.get('value'),
|
||||
'profile_id': self.profile_select.get('value')
|
||||
},
|
||||
'useSpinner': true,
|
||||
'spinnerTarget': self.movie
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
Movie.Action.IMDB = new Class({
|
||||
|
||||
Extends: MovieAction,
|
||||
id: null,
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.id = self.movie.get('identifier');
|
||||
|
||||
self.el = new Element('a.imdb', {
|
||||
'text': 'imdb',
|
||||
'title': 'Go to the IMDB page of ' + self.movie.getTitle(),
|
||||
'events': {
|
||||
'click': self.gotoIMDB.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
if(!self.id) self.disable();
|
||||
},
|
||||
|
||||
gotoIMDB: function(e){
|
||||
var self = this;
|
||||
(e).stop();
|
||||
|
||||
window.open('http://www.imdb.com/title/'+self.id+'/');
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
Movie.Action.Refresh = new Class({
|
||||
|
||||
Extends: MovieAction,
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.refresh', {
|
||||
'text': 'refresh',
|
||||
'title': 'Refresh the movie info and do a forced search',
|
||||
'events': {
|
||||
'click': self.doSearch.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
doSearch: function(e){
|
||||
var self = this;
|
||||
(e).stop();
|
||||
|
||||
Api.request('movie.refresh', {
|
||||
'data': {
|
||||
'id': self.movie.get('id')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
Movie.Action.Delete = new Class({
|
||||
|
||||
Extends: MovieAction,
|
||||
|
||||
Implements: [Chain],
|
||||
|
||||
create: function(){
|
||||
var self = this;
|
||||
|
||||
self.el = new Element('a.delete', {
|
||||
'text': 'delete',
|
||||
'title': 'Remove the movie from your wanted list',
|
||||
'events': {
|
||||
'click': self.showConfirm.bind(self)
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
showConfirm: function(e){
|
||||
var self = this;
|
||||
(e).stop();
|
||||
|
||||
self.mask = $(self.movie).mask({
|
||||
'destroyOnHide': true
|
||||
});
|
||||
|
||||
$(self.mask).adopt(
|
||||
new Element('a.button.delete', {
|
||||
'text': 'Delete movie',
|
||||
'events': {
|
||||
'click': self.del.bind(self)
|
||||
}
|
||||
}),
|
||||
new Element('span', {
|
||||
'text': 'or'
|
||||
}),
|
||||
new Element('a.button.cancel', {
|
||||
'text': 'Cancel',
|
||||
'events': {
|
||||
'click': self.mask.hide.bind(self.mask)
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
del: function(e){
|
||||
(e).stop()
|
||||
var self = this;
|
||||
|
||||
var movie = $(self.movie);
|
||||
|
||||
self.chain(
|
||||
function(){
|
||||
$(self.mask).empty().addClass('loading');
|
||||
self.callChain();
|
||||
},
|
||||
function(){
|
||||
Api.request('movie.delete', {
|
||||
'data': {
|
||||
'id': self.movie.get('id')
|
||||
},
|
||||
'onComplete': function(){
|
||||
p(movie, $(self.movie))
|
||||
movie.slide('in');
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
self.callChain();
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
@@ -6,13 +6,19 @@ var QualityBase = new Class({
|
||||
setup: function(data){
|
||||
var self = this;
|
||||
|
||||
self.profiles = data.profiles;
|
||||
self.qualities = data.qualities;
|
||||
|
||||
self.profiles = {}
|
||||
Object.each(data.profiles, self.createProfilesClass.bind(self));
|
||||
|
||||
App.addEvent('load', self.addSettings.bind(self))
|
||||
|
||||
},
|
||||
|
||||
getProfile: function(id){
|
||||
return this.profiles[id]
|
||||
},
|
||||
|
||||
addSettings: function(){
|
||||
var self = this;
|
||||
|
||||
@@ -47,22 +53,34 @@ var QualityBase = new Class({
|
||||
new Element('a.add_new', {
|
||||
'text': 'Create a new quality profile',
|
||||
'events': {
|
||||
'click': self.createNewProfile.bind(self)
|
||||
'click': function(){
|
||||
var profile = self.createProfilesClass();
|
||||
$(profile).inject(self.profile_container, 'top')
|
||||
}
|
||||
}
|
||||
}),
|
||||
self.profile_container = new Element('div.container')
|
||||
)
|
||||
|
||||
Object.each(self.profiles, self.createNewProfile.bind(self))
|
||||
Object.each(self.profiles, function(profile){
|
||||
if(!profile.isCore())
|
||||
$(profile).inject(self.profile_container, 'top')
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
createNewProfile: function(data, nr){
|
||||
createProfilesClass: function(data){
|
||||
var self = this;
|
||||
|
||||
self.profiles[nr] = new Profile(data);
|
||||
$(self.profiles[nr]).inject(self.profile_container)
|
||||
|
||||
if(data){
|
||||
return self.profiles[data.id] = new Profile(data);
|
||||
}
|
||||
else {
|
||||
var data = {
|
||||
'id': randomString()
|
||||
}
|
||||
return self.profiles[data.id] = new Profile(data);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -95,7 +113,7 @@ var QualityBase = new Class({
|
||||
window.Quality = new QualityBase();
|
||||
|
||||
var Profile = new Class({
|
||||
|
||||
|
||||
data: {},
|
||||
types: [],
|
||||
|
||||
@@ -119,12 +137,9 @@ var Profile = new Class({
|
||||
|
||||
var data = self.data;
|
||||
|
||||
self.el = new Element('div', {
|
||||
'class': 'profile'
|
||||
}).adopt(
|
||||
new Element('h4', {'text': data.label}),
|
||||
new Element('span.delete', {
|
||||
'html': 'del',
|
||||
self.el = new Element('div.profile').adopt(
|
||||
self.header = new Element('h4', {'text': data.label}),
|
||||
new Element('span.delete.icon', {
|
||||
'events': {
|
||||
'click': self.del.bind(self)
|
||||
}
|
||||
@@ -135,25 +150,29 @@ var Profile = new Class({
|
||||
new Element('label', {'text':'Name'}),
|
||||
new Element('input.label.textInput.large', {
|
||||
'type':'text',
|
||||
'value': data.label
|
||||
'value': data.label,
|
||||
'events': {
|
||||
'keyup': function(){
|
||||
self.header.set('text', this.get('value'))
|
||||
}
|
||||
}
|
||||
})
|
||||
),
|
||||
new Element('div.ctrlHolder').adopt(
|
||||
new Element('label', {'text':'Wait'}),
|
||||
new Element('input.wait_for.textInput.xsmall', {
|
||||
'type':'text',
|
||||
'value': data.wait_for
|
||||
'value': data.types && data.types.length > 0 ? data.types[0].wait_for : 0
|
||||
}),
|
||||
new Element('span', {'text':' day(s) for better quality.'})
|
||||
),
|
||||
new Element('div.ctrlHolder').adopt(
|
||||
new Element('label', {'text': 'Qualities'}),
|
||||
self.type_container = new Element('div.types').adopt(
|
||||
new Element('div.head').adopt(
|
||||
new Element('span.quality_type', {'text': 'Search for'}),
|
||||
new Element('span.finish', {'html': '<acronym title="Won\'t download anything else if it has found this quality.">Finish</acronym>'})
|
||||
)
|
||||
new Element('div.head').adopt(
|
||||
new Element('span.quality_type', {'text': 'Search for'}),
|
||||
new Element('span.finish', {'html': '<acronym title="Won\'t download anything else if it has found this quality.">Finish</acronym>'})
|
||||
),
|
||||
self.type_container = new Element('ol.types'),
|
||||
new Element('a.addType', {
|
||||
'text': 'Add another quality to search for.',
|
||||
'href': '#',
|
||||
@@ -164,6 +183,8 @@ var Profile = new Class({
|
||||
)
|
||||
);
|
||||
|
||||
self.makeSortable()
|
||||
|
||||
if(data.types)
|
||||
Object.each(data.types, self.addType.bind(self))
|
||||
},
|
||||
@@ -174,11 +195,19 @@ var Profile = new Class({
|
||||
if(self.save_timer) clearTimeout(self.save_timer);
|
||||
self.save_timer = (function(){
|
||||
|
||||
var data = self.getData();
|
||||
if(data.types.length < 2) return;
|
||||
|
||||
Api.request('profile.save', {
|
||||
'data': self.getData(),
|
||||
'useSpinner': true,
|
||||
'spinnerOptions': {
|
||||
'target': self.el
|
||||
},
|
||||
'onComplete': function(json){
|
||||
if(json.success){
|
||||
self.data = json.profile
|
||||
}
|
||||
}
|
||||
});
|
||||
}).delay(delay, self)
|
||||
@@ -194,12 +223,15 @@ var Profile = new Class({
|
||||
'wait_for' : self.el.getElement('.wait_for').get('value'),
|
||||
'types': []
|
||||
}
|
||||
|
||||
Object.each(self.types, function(type){
|
||||
if(!type.deleted)
|
||||
data.types.include(type.getData());
|
||||
|
||||
Array.each(self.type_container.getElements('.type'), function(type){
|
||||
if(!type.hasClass('deleted'))
|
||||
data.types.include({
|
||||
'quality_id': type.getElement('select').get('value'),
|
||||
'finish': +type.getElement('input[type=checkbox]').checked
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
return data
|
||||
},
|
||||
|
||||
@@ -208,6 +240,7 @@ var Profile = new Class({
|
||||
|
||||
var t = new Profile.Type(data);
|
||||
$(t).inject(self.type_container);
|
||||
self.sortable.addItems($(t));
|
||||
|
||||
self.types.include(t);
|
||||
|
||||
@@ -216,6 +249,8 @@ var Profile = new Class({
|
||||
del: function(){
|
||||
var self = this;
|
||||
|
||||
if(!confirm('Are you sure you want to delete this profile?')) return
|
||||
|
||||
Api.request('profile.delete', {
|
||||
'data': {
|
||||
'id': self.data.id
|
||||
@@ -224,12 +259,35 @@ var Profile = new Class({
|
||||
'spinnerOptions': {
|
||||
'target': self.el
|
||||
},
|
||||
'onComplete': function(){
|
||||
self.el.destroy();
|
||||
'onComplete': function(json){
|
||||
if(json.success)
|
||||
self.el.destroy();
|
||||
else
|
||||
alert(json.message)
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
makeSortable: function(){
|
||||
var self = this;
|
||||
|
||||
self.sortable = new Sortables(self.type_container, {
|
||||
'revert': true,
|
||||
//'clone': true,
|
||||
'handle': '.handle',
|
||||
'opacity': 0.5,
|
||||
'onComplete': self.save.bind(self, 300)
|
||||
});
|
||||
},
|
||||
|
||||
get: function(attr){
|
||||
return this.data[attr]
|
||||
},
|
||||
|
||||
isCore: function(){
|
||||
return this.data.core
|
||||
},
|
||||
|
||||
toElement: function(){
|
||||
return this.el
|
||||
}
|
||||
@@ -252,7 +310,7 @@ Profile.Type = Class({
|
||||
var self = this;
|
||||
var data = self.data;
|
||||
|
||||
self.el = new Element('div.type').adopt(
|
||||
self.el = new Element('li.type').adopt(
|
||||
new Element('span.quality_type').adopt(
|
||||
self.fillQualities()
|
||||
),
|
||||
@@ -263,15 +321,12 @@ Profile.Type = Class({
|
||||
'checked': data.finish
|
||||
})
|
||||
),
|
||||
new Element('span.delete', {
|
||||
'html': 'del',
|
||||
new Element('span.delete.icon', {
|
||||
'events': {
|
||||
'click': self.del.bind(self)
|
||||
}
|
||||
}),
|
||||
new Element('span', {
|
||||
'class':'handle'
|
||||
})
|
||||
new Element('span.handle')
|
||||
)
|
||||
|
||||
},
|
||||
@@ -284,21 +339,21 @@ Profile.Type = Class({
|
||||
Object.each(Quality.qualities, function(q){
|
||||
new Element('option', {
|
||||
'text': q.label,
|
||||
'value': q.identifier
|
||||
'value': q.id
|
||||
}).inject(self.qualities)
|
||||
});
|
||||
|
||||
self.qualities.set('value', self.data.quality);
|
||||
self.qualities.set('value', self.data.quality_id);
|
||||
|
||||
return self.qualities;
|
||||
|
||||
},
|
||||
|
||||
|
||||
getData: function(){
|
||||
var self = this;
|
||||
|
||||
|
||||
return {
|
||||
'quality': self.qualities.get('value'),
|
||||
'quality_id': self.qualities.get('value'),
|
||||
'finish': +self.finish.checked
|
||||
}
|
||||
},
|
||||
@@ -306,6 +361,7 @@ Profile.Type = Class({
|
||||
del: function(){
|
||||
var self = this;
|
||||
|
||||
self.el.addClass('deleted');
|
||||
self.el.hide();
|
||||
self.deleted = true;
|
||||
},
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
var StatusBase = new Class({
|
||||
|
||||
setup: function(statuses){
|
||||
var self = this;
|
||||
|
||||
self.statuses = statuses;
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
window.Status = new StatusBase();
|
||||
@@ -121,6 +121,12 @@ form {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*** Icons ***/
|
||||
.icon.delete {
|
||||
background: url('../images/delete.png') no-repeat;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/*** Navigation ***/
|
||||
.header {
|
||||
background: #f7f7f7;
|
||||
|
||||
+3
-3
@@ -24,7 +24,7 @@
|
||||
margin: 0 0 -5px -20px;
|
||||
top: 4px;
|
||||
right: 5px;
|
||||
background: url('../images/close_button.png') 0 center no-repeat;
|
||||
background: url('../../images/close_button.png') 0 center no-repeat;
|
||||
cursor: pointer;
|
||||
}
|
||||
.search_form .input a:hover { background-position: -12px center; }
|
||||
@@ -46,7 +46,7 @@
|
||||
-moz-border-radius: 3px;
|
||||
}
|
||||
.search_form .spinner {
|
||||
background: #fff url('../images/spinner.gif') no-repeat center 70px;
|
||||
background: #fff url('../../images/spinner.gif') no-repeat center 70px;
|
||||
}
|
||||
|
||||
.search_form .pointer {
|
||||
@@ -83,7 +83,7 @@
|
||||
display: inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.search_form .results .movie .options select[name=name] { width: 180px; }
|
||||
.search_form .results .movie .options select[name=title] { width: 180px; }
|
||||
.search_form .results .movie .options select[name=quality] { width: 90px; }
|
||||
|
||||
.search_form .results .movie .options .button {
|
||||
@@ -0,0 +1,21 @@
|
||||
/* @override http://localhost:5000/static/style/plugin/quality.css */
|
||||
|
||||
|
||||
.profile > .delete {
|
||||
background-position: center;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.profile .types .type .handle {
|
||||
background: url('../../images/handle.png') center;
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.profile .types .type .delete {
|
||||
background-position: center;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="{{ url_for('.static', filename='style/main.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('.static', filename='style/uniform.generic.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('.static', filename='style/uniform.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('.static', filename='style/main.css') }}" type="text/css">
|
||||
<link rel="stylesheet" href="{{ url_for('.static', filename='style/uniform.generic.css') }}" type="text/css">
|
||||
<link rel="stylesheet" href="{{ url_for('.static', filename='style/uniform.css') }}" type="text/css">
|
||||
|
||||
<link rel="stylesheet" href="{{ url_for('.static', filename='style/page/settings.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('.static', filename='style/movie_add.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('.static', filename='style/page/settings.css') }}" type="text/css">
|
||||
<link rel="stylesheet" href="{{ url_for('.static', filename='style/plugin/movie_add.css') }}" type="text/css">
|
||||
<link rel="stylesheet" href="{{ url_for('.static', filename='style/plugin/quality.css') }}" type="text/css">
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/mootools.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/mootools_more.js') }}"></script>
|
||||
@@ -30,8 +31,11 @@
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/page/manage.js') }}"></script>
|
||||
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/quality.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/status.js') }}"></script>
|
||||
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/file.js') }}"></script>
|
||||
|
||||
<link href="{{ url_for('.static', filename='images/favicon.ico') }}" rel="icon" type="image/x-icon" />
|
||||
<link rel="apple-touch-icon" href="{{ url_for('.static', filename='images/homescreen.png') }}" />
|
||||
|
||||
<script type="text/javascript">
|
||||
window.addEvent('domready', function() {
|
||||
@@ -44,34 +48,13 @@
|
||||
});
|
||||
|
||||
Quality.setup({
|
||||
'profiles': [
|
||||
{
|
||||
'identifier': 'hd',
|
||||
'label': 'HD',
|
||||
'wait_for': 7,
|
||||
'types': [
|
||||
{'quality': '720p', 'finish': true},
|
||||
{'quality': '1080p', 'finish': true}
|
||||
]
|
||||
},{
|
||||
'identifier': 'best',
|
||||
'label': 'Best',
|
||||
'wait_for': 7,
|
||||
'types': [
|
||||
{'quality': '720p', 'finish': true},
|
||||
{'quality': '1080p', 'finish': true},
|
||||
{'quality': 'brrip'},
|
||||
{'quality': 'dvdrip'}
|
||||
]
|
||||
}
|
||||
],
|
||||
'qualities': [
|
||||
{'identifier': '1080p', 'label': '1080P'},
|
||||
{'identifier': '720p', 'label': '720P'},
|
||||
{'identifier': 'brrip', 'label': 'BR-Rip'},
|
||||
{'identifier': 'dvdrip', 'label': 'DVD-Rip'}
|
||||
]
|
||||
})
|
||||
'profiles': {{ fireEvent('profile.all', single = True)|tojson|safe }},
|
||||
'qualities': {{ fireEvent('quality.all', single = True)|tojson|safe }}
|
||||
});
|
||||
|
||||
Status.setup({{ fireEvent('status.all', single = True)|tojson|safe }});
|
||||
|
||||
File.Type.setup({{ fireEvent('file.types', single = True)|tojson|safe }});
|
||||
|
||||
App.setup({
|
||||
'base_url': '{{ request.path }}'
|
||||
|
||||
+3
-1
@@ -179,10 +179,12 @@ class Event(object):
|
||||
if not self.asynchronous:
|
||||
self.result.append(tuple(r))
|
||||
|
||||
except Exception:
|
||||
except Exception, e:
|
||||
if not self.asynchronous:
|
||||
self.result.append((False, self._error(sys.exc_info()),
|
||||
handler))
|
||||
else:
|
||||
self.error_handler(sys.exc_info())
|
||||
finally:
|
||||
if isinstance(self.lock, threading._RLock):
|
||||
self.lock.release()
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
"""You need to have setuptools installed.
|
||||
|
||||
Usage:
|
||||
python setup.py develop
|
||||
|
||||
This will register the couchpotato package in your system and thereby make it
|
||||
available from anywhere.
|
||||
|
||||
Also, a script will be installed to control couchpotato from the shell.
|
||||
Try running:
|
||||
couchpotato --help
|
||||
|
||||
"""
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
setup(name="couchpotato",
|
||||
packages=['couchpotato'],
|
||||
install_requires=[
|
||||
'argparse',
|
||||
'elixir',
|
||||
'flask',
|
||||
'nose',
|
||||
'sqlalchemy'],
|
||||
entry_points="""
|
||||
[console_scripts]
|
||||
couchpotato = couchpotato.cli:cmd_couchpotato
|
||||
""")
|
||||
|
||||
Reference in New Issue
Block a user