Notifications

This commit is contained in:
Ruud committed 2011-04-09 20:18:51 +02:00
1 parent 288aa8c0a1
commit 85fc1c01ee
67 files changed
+3263 -393

No files matched your search

+1
View File
@@ -1,2 +1,3 @@
/settings.conf /settings.conf
/logs/*.log /logs/*.log
/_source/
+2 -1
View File
@@ -1,4 +1,5 @@
from couchpotato.core.auth import requires_auth from couchpotato.core.auth import requires_auth
from couchpotato.core.event import fireEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
from flask.app import Flask from flask.app import Flask
@@ -30,7 +31,7 @@ def get_engine():
@web.route('/') @web.route('/')
@requires_auth @requires_auth
def index(): def index():
return render_template('index.html', sep = os.sep) return render_template('index.html', sep = os.sep, fireEvent = fireEvent)
@app.errorhandler(404) @app.errorhandler(404)
def page_not_found(error): def page_not_found(error):
+4 -6
View File
@@ -1,6 +1,7 @@
from argparse import ArgumentParser from argparse import ArgumentParser
from couchpotato import web from couchpotato import web
from couchpotato.api import api from couchpotato.api import api
from couchpotato.core.event import fireEvent
from libs.daemon import createDaemon from libs.daemon import createDaemon
from logging import handlers from logging import handlers
from werkzeug.contrib.cache import FileSystemCache 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('data_dir', options.data_dir)
Env.set('db_path', 'sqlite:///' + os.path.join(options.data_dir, 'couchpotato.db')) 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_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('quiet', options.quiet)
Env.set('daemonize', options.daemonize) Env.set('daemonize', options.daemonize)
Env.set('args', args) 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 from migrate.versioning.api import version_control, db_version, version, upgrade
db = Env.get('db_path') db = Env.get('db_path')
repo = os.path.join('couchpotato', 'core', 'migration') repo = os.path.join('couchpotato', 'core', 'migration')
logging.getLogger('migrate').setLevel(logging.WARNING) # Disable logging for migration
latest_db_version = version(repo) latest_db_version = version(repo)
@@ -114,6 +116,7 @@ def cmd_couchpotato(base_path, args):
from couchpotato.core.settings.model import setup from couchpotato.core.settings.model import setup
setup() setup()
fireEvent('app.load')
# Create app # Create app
from couchpotato import app from couchpotato import app
@@ -128,11 +131,6 @@ def cmd_couchpotato(base_path, args):
app.secret_key = api_key app.secret_key = api_key
app.static_path = url_base + '/static' 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 # Register modules
app.register_module(web, url_prefix = '%s/' % url_base) 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')) app.register_module(api, url_prefix = '%s/%s/' % (url_base, api_key if not debug else 'api'))
+3 -13
View File
@@ -9,35 +9,28 @@ config = [{
{ {
'tab': 'general', 'tab': 'general',
'name': 'basics', 'name': 'basics',
'label': 'Basics',
'description': 'Needs restart before changes take effect.', 'description': 'Needs restart before changes take effect.',
'options': [ 'options': [
{ {
'name': 'username', 'name': 'username',
'default': '', 'default': '',
'type': 'string',
'label': 'Username',
}, },
{ {
'name': 'password', 'name': 'password',
'default': '', 'default': '',
'password': True, 'type': 'password',
'type': 'string',
'label': 'Password',
}, },
{ {
'name': 'host', 'name': 'host',
'advanced': True, 'advanced': True,
'default': '0.0.0.0', 'default': '0.0.0.0',
'type': 'string', 'label': 'IP',
'label': 'Host',
'description': 'Host that I should listen to. "0.0.0.0" listens to all ips.', 'description': 'Host that I should listen to. "0.0.0.0" listens to all ips.',
}, },
{ {
'name': 'port', 'name': 'port',
'default': 5000, 'default': 5000,
'type': 'int', 'type': 'int',
'label': 'Port',
'description': 'The port I should listen to.', 'description': 'The port I should listen to.',
}, },
{ {
@@ -52,14 +45,12 @@ config = [{
{ {
'tab': 'general', 'tab': 'general',
'name': 'advanced', 'name': 'advanced',
'label': 'Advanced',
'description': "For those who know what the're doing", 'description': "For those who know what the're doing",
'advanced': True, 'advanced': True,
'options': [ 'options': [
{ {
'name': 'api_key', 'name': 'api_key',
'default': uuid4().hex, 'default': uuid4().hex,
'type': 'string',
'readonly': True, 'readonly': True,
'label': 'Api Key', 'label': 'Api Key',
'description': "This is top-secret! Don't share this!", 'description': "This is top-secret! Don't share this!",
@@ -74,9 +65,8 @@ config = [{
{ {
'name': 'url_base', 'name': 'url_base',
'default': '', 'default': '',
'type': 'string',
'label': 'Url Base', '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.',
}, },
], ],
}, },
Whitespace-only changes.
+20
View File
@@ -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
+43 -9
View File
@@ -1,4 +1,5 @@
from axl.axel import Event from axl.axel import Event
from couchpotato.core.helpers.variable import merge_dicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import traceback import traceback
@@ -19,31 +20,64 @@ def removeEvent(name, handler):
e -= handler e -= handler
def fireEvent(name, *args, **kwargs): def fireEvent(name, *args, **kwargs):
log.debug('Firing "%s": %s, %s' % (name, args, kwargs))
try: 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 = events[name]
e.asynchronous = False e.asynchronous = False
result = e(*args, **kwargs) result = e(*args, **kwargs)
results = [] if single and not merge:
for r in result: results = result[0][1]
if r[0] == True: else:
results.append(r[1]) results = []
else: for r in result:
etype, value, tb = r[1] if r[0] == True:
log.debug(''.join(traceback.format_exception(etype, value, tb))) 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 return results
except Exception, e: except Exception, e:
log.debug(e) log.error('%s: %s' % (name, e))
def fireEventAsync(name, *args, **kwargs): def fireEventAsync(name, *args, **kwargs):
log.debug('Async "%s": %s, %s' % (name, args, kwargs))
try: try:
e = events[name] e = events[name]
e.asynchronous = True e.asynchronous = True
e.error_handler = errorHandler
e(*args, **kwargs) e(*args, **kwargs)
return True return True
except Exception, e: 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): def getEvent(name):
return events[name] return events[name]
+3
View File
@@ -5,16 +5,19 @@ import unicodedata
log = CPLog(__name__) log = CPLog(__name__)
def toSafeString(original): def toSafeString(original):
valid_chars = "-_.() %s%s" % (ascii_letters, digits) valid_chars = "-_.() %s%s" % (ascii_letters, digits)
cleanedFilename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore') cleanedFilename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
return ''.join(c for c in cleanedFilename if c in valid_chars) return ''.join(c for c in cleanedFilename if c in valid_chars)
def simplifyString(original): def simplifyString(original):
string = toSafeString(original) string = toSafeString(original)
split = re.split('\W+', string.lower()) split = re.split('\W+', string.lower())
return toUnicode(' '.join(split)) return toUnicode(' '.join(split))
def toUnicode(original, *args): def toUnicode(original, *args):
try: try:
if type(original) is unicode: if type(original) is unicode:
+1 -1
View File
@@ -42,7 +42,7 @@ def dictToList(params):
new = {} new = {}
for x, value in params.iteritems(): for x, value in params.iteritems():
try: try:
new_value = [dictToList(value2) for value2 in value.itervalues()] new_value = [dictToList(value[k]) for k in sorted(value.iterkeys())]
except: except:
new_value = value new_value = value
+38
View File
@@ -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
+14 -9
View File
@@ -1,4 +1,4 @@
from couchpotato.core.event import fireEvent, fireEventAsync from couchpotato.core.event import fireEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import glob import glob
import os import os
@@ -17,6 +17,8 @@ class Loader:
self.paths = { self.paths = {
'plugin' : ('couchpotato.core.plugins', os.path.join(root, 'couchpotato', 'core', 'plugins')), 'plugin' : ('couchpotato.core.plugins', os.path.join(root, 'couchpotato', 'core', 'plugins')),
'provider' : ('couchpotato.core.providers', os.path.join(root, 'couchpotato', 'core', 'providers')), '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(): for type, tuple in self.paths.iteritems():
@@ -28,14 +30,17 @@ class Loader:
for module_name, plugin in sorted(self.modules.iteritems()): for module_name, plugin in sorted(self.modules.iteritems()):
# Load module # 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 # Save default settings for plugin/provider
did_save += self.loadSettings(m, module_name, save = False) 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: if did_save:
fireEvent('settings.save') fireEvent('settings.save')
@@ -51,12 +56,12 @@ class Loader:
def loadSettings(self, module, name, save = True): def loadSettings(self, module, name, save = True):
try: try:
for section in module.config: for section in module.config:
fireEventAsync('settings.options', section['name'], section) fireEvent('settings.options', section['name'], section)
options = {} options = {}
for group in section['groups']: for group in section['groups']:
for option in group['options']: for option in group['options']:
options[option['name']] = option['default'] 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 return True
except Exception, e: except Exception, e:
log.debug("Failed loading settings for '%s': %s" % (name, e)) log.debug("Failed loading settings for '%s': %s" % (name, e))
@@ -67,7 +72,7 @@ class Loader:
module.start() module.start()
return True return True
except Exception, e: except Exception, e:
log.debug("Failed loading plugin '%s': %s" % (name, e)) log.error("Failed loading plugin '%s': %s" % (name, e))
return False return False
def addModule(self, type, module, name): def addModule(self, type, module, name):
Whitespace-only changes.
+29
View File
@@ -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': '',
},
],
}
],
}]
+131
View File
@@ -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 = []
+103
View File
@@ -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(): def start():
return FileBrowser() return FileBrowser()
+1 -1
View File
@@ -1,4 +1,4 @@
from couchpotato.core.plugins.library.main import LibraryPlugin from .main import LibraryPlugin
def start(): def start():
return LibraryPlugin() return LibraryPlugin()
+57 -8
View File
@@ -1,32 +1,81 @@
from couchpotato import get_session 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.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): class LibraryPlugin(Plugin):
def __init__(self): def __init__(self):
addEvent('library.add', self.add) addEvent('library.add', self.add)
addEvent('library.update', self.update)
def add(self, attrs = {}): def add(self, attrs = {}):
db = get_session(); db = get_session()
l = db.query(Library).filter_by(identifier = attrs.get('identifier')).first() l = db.query(Library).filter_by(identifier = attrs.get('identifier')).first()
if not l: if not l:
l = Library( l = Library(
name = attrs.get('name'),
year = attrs.get('year'), year = attrs.get('year'),
identifier = attrs.get('identifier'), 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.add(l)
db.commit() db.commit()
# Update library info
fireEventAsync('library.update', library = l, default_title = attrs.get('title', ''))
#db.remove()
return l 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 -1
View File
@@ -1,4 +1,4 @@
from couchpotato.core.plugins.movie.main import MoviePlugin from .main import MoviePlugin
def start(): def start():
return MoviePlugin() return MoviePlugin()
+71 -32
View File
@@ -1,9 +1,9 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent, fireEventAsync
from couchpotato.core.helpers.request import getParams, jsonified from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.plugins.base import Plugin 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 couchpotato.environment import Env
from urllib import urlencode from urllib import urlencode
@@ -13,30 +13,28 @@ class MoviePlugin(Plugin):
def __init__(self): def __init__(self):
addApiView('movie.search', self.search) addApiView('movie.search', self.search)
addApiView('movie.list', self.list) addApiView('movie.list', self.list)
addApiView('movie.refresh', self.refresh)
addApiView('movie.add', self.add) addApiView('movie.add', self.add)
addApiView('movie.edit', self.edit)
addApiView('movie.delete', self.delete)
def list(self): def list(self):
a = getParams() params = getParams()
db = get_session()
results = get_session().query(Movie).filter( results = db.query(Movie).filter(
Movie.releases.any( Movie.status.has(identifier = params.get('status', 'active'))
Release.status.has(identifier = 'wanted') ).all()
)
).all()
movies = [] movies = []
for movie in results: for movie in results:
temp = { temp = movie.to_dict(deep = {
'id': movie.id, 'releases': {'status': {}, 'quality': {}},
'name': movie.id, 'library': {'titles': {}, 'files':{}},
'releases': [], 'files': {}
} })
for release in movie.releases:
temp['releases'].append({
'status': release.status.label,
'quality': release.quality.label
})
movies.append(temp) movies.append(temp)
@@ -46,22 +44,41 @@ class MoviePlugin(Plugin):
'movies': movies, '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): def search(self):
a = getParams() params = getParams()
cache_key = '%s/%s' % (__name__, urlencode(a)) cache_key = '%s/%s' % (__name__, urlencode(params))
movies = Env.get('cache').get(cache_key) movies = Env.get('cache').get(cache_key)
if not movies: if not movies:
results = fireEvent('provider.movie.search', q = a.get('q')) results = fireEvent('provider.movie.search', q = params.get('q'))
# Combine movie results # Combine movie results
movies = [] movies = []
for r in results: for r in results:
movies += r movies += r
Env.get('cache').set(cache_key, movies, timeout = 10) Env.get('cache').set(cache_key, movies)
return jsonified({ return jsonified({
'success': True, 'success': True,
@@ -71,24 +88,46 @@ class MoviePlugin(Plugin):
def add(self): def add(self):
a = getParams() params = getParams()
db = get_session(); db = get_session();
library = fireEvent('library.add', attrs = a) library = fireEvent('library.add', single = True, attrs = params)
profile = db.query(Profile).filter_by(identifier = a.get('profile_identifier')) status = fireEvent('status.add', 'active', single = True)
m = db.query(Movie).filter_by(library = library).first()
m = db.query(Movie).filter_by(library_id = library.id).first()
if not m: if not m:
m = Movie( m = Movie(
library = library, library_id = library.id,
profile = profile, profile_id = params.get('profile_id')
) )
db.add(m) db.add(m)
db.commit()
m.status_id = status.id
db.commit()
return jsonified({ return jsonified({
'success': True, 'success': True,
'added': 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 -1
View File
@@ -1,4 +1,4 @@
from couchpotato.core.plugins.profile.main import ProfilePlugin from .main import ProfilePlugin
def start(): def start():
return ProfilePlugin() return ProfilePlugin()
+69 -7
View File
@@ -1,28 +1,90 @@
from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.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.plugins.base import Plugin
from couchpotato.core.settings.model import Profile, ProfileType
log = CPLog(__name__)
class ProfilePlugin(Plugin): class ProfilePlugin(Plugin):
def __init__(self): def __init__(self):
addEvent('profile.get', self.get) addEvent('profile.all', self.all)
addApiView('profile.save', self.save) addApiView('profile.save', self.save)
addApiView('profile.delete', self.delete) 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): 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({ return jsonified({
'success': True, 'success': True,
'a': a 'profile': p.to_dict(deep = {'types': {}})
}) })
def delete(self): 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 = []
+97
View File
@@ -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 = []
+68
View File
@@ -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()
+2 -1
View File
@@ -1,8 +1,9 @@
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
log = CPLog(__name__) log = CPLog(__name__)
class Provider(): class Provider(Plugin):
type = None # movie, nzb, torrent, subtitle, trailer type = None # movie, nzb, torrent, subtitle, trailer
timeout = 10 # Default timeout for url requests 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
-107
View File
@@ -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
+58 -21
View File
@@ -2,8 +2,10 @@ from elixir.entity import Entity
from elixir.fields import Field from elixir.fields import Field
from elixir.options import options_defaults from elixir.options import options_defaults
from elixir.relationships import OneToMany, ManyToOne from elixir.relationships import OneToMany, ManyToOne
from sqlalchemy.types import Integer, String, Unicode, UnicodeText, Boolean, \ from libs.elixir.options import using_options
Float from libs.elixir.relationships import ManyToMany
from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, Float, \
String
options_defaults["shortnames"] = True options_defaults["shortnames"] = True
@@ -20,24 +22,48 @@ class Movie(Entity):
The files belonging to the movie object are global for the whole movie The files belonging to the movie object are global for the whole movie
such as trailers, nfo, thumbnails""" such as trailers, nfo, thumbnails"""
library = ManyToOne('Library') last_edit = Field(Integer)
library = ManyToOne('Library')
status = ManyToOne('Status')
profile = ManyToOne('Profile') profile = ManyToOne('Profile')
releases = OneToMany('Release') releases = OneToMany('Release')
files = OneToMany('File') files = ManyToMany('File')
class Library(Entity): class Library(Entity):
""""""
title = Field(Unicode)
year = Field(Integer) year = Field(Integer)
identifier = Field(Unicode) identifier = Field(String(20))
rating = Field(Float) rating = Field(Float)
plot = Field(UnicodeText) plot = Field(UnicodeText)
tagline = Field(UnicodeText(255)) tagline = Field(UnicodeText(255))
status = ManyToOne('Status')
movie = OneToMany('Movie') 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): class Release(Entity):
@@ -47,7 +73,7 @@ class Release(Entity):
movie = ManyToOne('Movie') movie = ManyToOne('Movie')
status = ManyToOne('Status') status = ManyToOne('Status')
quality = ManyToOne('Quality') quality = ManyToOne('Quality')
files = OneToMany('File') files = ManyToMany('File')
history = OneToMany('History') history = OneToMany('History')
@@ -55,41 +81,49 @@ class Status(Entity):
"""The status of a release, such as Downloaded, Deleted, Wanted etc""" """The status of a release, such as Downloaded, Deleted, Wanted etc"""
identifier = Field(String(20), unique = True) identifier = Field(String(20), unique = True)
label = Field(String(20)) label = Field(Unicode(20))
releases = OneToMany('Release') releases = OneToMany('Release')
movies = OneToMany('Movie')
class Quality(Entity): class Quality(Entity):
"""Quality name of a release, DVD, 720P, DVD-Rip etc""" """Quality name of a release, DVD, 720P, DVD-Rip etc"""
using_options(order_by = 'order')
identifier = Field(String(20), unique = True) 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') releases = OneToMany('Release')
profile_types = ManyToOne('ProfileType') profile_types = OneToMany('ProfileType')
class Profile(Entity): class Profile(Entity):
"""""" """"""
using_options(order_by = 'order')
identifier = Field(String(20), unique = True)
label = Field(Unicode(50)) label = Field(Unicode(50))
order = Field(Integer) order = Field(Integer)
wait_for = Field(Integer) core = Field(Boolean)
hide = Field(Boolean)
movie = OneToMany('Movie') movie = OneToMany('Movie')
profile_type = OneToMany('ProfileType') types = OneToMany('ProfileType', cascade = 'all, delete-orphan')
class ProfileType(Entity): class ProfileType(Entity):
"""""" """"""
using_options(order_by = 'order')
order = Field(Integer) order = Field(Integer)
mark_completed = Field(Boolean) finish = Field(Boolean)
wait_for = Field(Integer) wait_for = Field(Integer)
type = OneToMany('Quality') quality = ManyToOne('Quality')
profile = ManyToOne('Profile') profile = ManyToOne('Profile')
@@ -97,19 +131,22 @@ class File(Entity):
"""File that belongs to a release.""" """File that belongs to a release."""
path = Field(Unicode(255), nullable = False, unique = True) 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') type = ManyToOne('FileType')
properties = OneToMany('FileProperty') properties = OneToMany('FileProperty')
history = OneToMany('RenameHistory')
movie = ManyToMany('Movie')
release = ManyToMany('Release')
library = ManyToMany('Library')
class FileType(Entity): class FileType(Entity):
"""Types could be trailer, subtitle, movie, partial movie etc.""" """Types could be trailer, subtitle, movie, partial movie etc."""
identifier = Field(String(20), unique = True) identifier = Field(String(20), unique = True)
type = Field(Unicode(20))
name = Field(Unicode(50), nullable = False) name = Field(Unicode(50), nullable = False)
files = OneToMany('File') files = OneToMany('File')
@@ -135,8 +172,8 @@ class History(Entity):
class RenameHistory(Entity): class RenameHistory(Entity):
"""Remembers from where to where files have been moved.""" """Remembers from where to where files have been moved."""
old = Field(String(255)) old = Field(Unicode(255))
new = Field(String(255)) new = Field(Unicode(255))
file = ManyToOne('File') 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

+37 -33
View File
@@ -34,6 +34,7 @@ Block.Search = new Class({
self.spinner = new Spinner(self.result_container); self.spinner = new Spinner(self.result_container);
self.OuterClickStack = new EventStack.OuterClick(); self.OuterClickStack = new EventStack.OuterClick();
History.addEvent('change', self.hideResults.bind(self, true));
//debug //debug
//self.input.set('value', 'kick ass') //self.input.set('value', 'kick ass')
@@ -126,16 +127,16 @@ Block.Search = new Class({
Object.each(json.movies, function(movie){ 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); var m = new Block.Search.Item(movie);
$(m).inject(self.results) $(m).inject(self.results)
self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m
} // }
else { // else {
self.movies[movie.imdb].alternativeName({ // self.movies[movie.imdb].alternativeTitle({
'name': movie.name // 'title': movie.title
}) // })
} // }
}); });
@@ -157,7 +158,7 @@ Block.Search.Item = new Class({
var self = this; var self = this;
self.info = info; self.info = info;
self.alternative_names = []; self.alternative_titles = [];
self.create(); self.create();
@@ -167,7 +168,7 @@ Block.Search.Item = new Class({
create: function(){ create: function(){
var self = this; var self = this;
var info = self.info var info = self.info;
self.el = new Element('div.movie', { self.el = new Element('div.movie', {
'id': info.imdb 'id': info.imdb
@@ -182,12 +183,12 @@ Block.Search.Item = new Class({
'click': self.showOptions.bind(self) 'click': self.showOptions.bind(self)
} }
}).adopt( }).adopt(
self.thumbnail = info.poster ? new Element('img.thumbnail', { self.thumbnail = info.images.posters.length > 0 ? new Element('img.thumbnail', {
'src': info.poster 'src': info.images.posters[0]
}) : null, }) : null,
new Element('div.info').adopt( new Element('div.info').adopt(
self.name = new Element('h2', { self.title = new Element('h2', {
'text': info.name 'text': info.titles[0]
}).adopt( }).adopt(
self.year = info.year ? new Element('span', { self.year = info.year ? new Element('span', {
'text': info.year '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; var self = this;
self.alternative_names.include(alternative); self.alternative_titles.include(alternative);
}, },
showOptions: function(){ showOptions: function(){
@@ -246,8 +250,8 @@ Block.Search.Item = new Class({
Api.request('movie.add', { Api.request('movie.add', {
'data': { 'data': {
'identifier': self.info.imdb, 'identifier': self.info.imdb,
'name': self.name_select.get('value'), 'title': self.title_select.get('value'),
'quality': self.quality_select.get('value') 'profile_id': self.profile_select.get('value')
}, },
'useSpinner': true, 'useSpinner': true,
'spinnerTarget': self.options, 'spinnerTarget': self.options,
@@ -277,14 +281,14 @@ Block.Search.Item = new Class({
self.options.adopt( self.options.adopt(
new Element('div').adopt( new Element('div').adopt(
self.info.poster ? new Element('img.thumbnail', { self.info.images.posters.length > 0 ? new Element('img.thumbnail', {
'src': self.info.poster 'src': self.info.images.posters[0]
}) : null, }) : null,
self.name_select = new Element('select', { self.title_select = new Element('select', {
'name': 'name' 'name': 'title'
}), }),
self.quality_select = new Element('select', { self.profile_select = new Element('select', {
'name': 'profile_identifier' 'name': 'profile'
}), }),
new Element('a.button', { new Element('a.button', {
'text': 'Add', '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', { new Element('option', {
'text': alt.name 'text': alt.title
}).inject(self.name_select) }).inject(self.title_select)
}) })
Array.each(Quality.profiles, function(q){ Array.each(Quality.profiles, function(profile){
new Element('option', { new Element('option', {
'value': q.indentifier, 'value': profile.id ? profile.id : profile.data.id,
'text': q.label 'text': profile.label ? profile.label : profile.data.label
}).inject(self.quality_select) }).inject(self.profile_select)
}); });
self.options.addClass('set'); self.options.addClass('set');
+13 -2
View File
@@ -114,8 +114,8 @@ var ApiClass = new Class({
}, options)).send() }, options)).send()
}, },
createUrl: function(action){ createUrl: function(action, params){
return this.options.url + (action || 'default') + '/' return this.options.url + (action || 'default') + '/' + (params ? '?'+Object.toQueryString(params) : '')
}, },
getOption: function(name){ getOption: function(name){
@@ -186,6 +186,17 @@ var p = function(){
console.log(arguments) 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(){ (function(){
var keyPaths = []; var keyPaths = [];
+77
View File
@@ -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();
+81 -2
View File
@@ -3,10 +3,10 @@
MooTools: the javascript framework MooTools: the javascript framework
web build: web build:
- http://mootools.net/core/bd6349d3fbc489736e5aefb01157c8a8 - http://mootools.net/core/c1215700e7dedaa9d48503126daf2111
packager build: 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. // MooTools: the javascript framework.
// Load this file's selection again by visiting: http://mootools.net/more/a6033a03c64d978cd0033a1d420e16e0 // 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/Request.JSONP More/Spinner // 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;
}
});
/* /*
--- ---
+47 -31
View File
@@ -11,6 +11,12 @@ Page.Settings = new Class({
}, },
'providers': { 'providers': {
'label': 'Providers' 'label': 'Providers'
},
'downloaders': {
'label': 'Downloaders'
},
'notifications': {
'label': 'Notifications'
} }
}, },
@@ -27,13 +33,13 @@ Page.Settings = new Class({
openTab: function(action){ openTab: function(action){
var self = this; var self = this;
action = action || self.action var action = action || self.action;
if(self.current) if(self.current)
self.toggleTab(self.current, true); self.toggleTab(self.current, true);
self.toggleTab(action) var tab = self.toggleTab(action)
self.current = action; self.current = tab == self.tabs.general ? 'general' : action;
}, },
@@ -47,6 +53,7 @@ Page.Settings = new Class({
t.tab[a](c); t.tab[a](c);
t.content[a](c); t.content[a](c);
return t
}, },
getData: function(onComplete){ getData: function(onComplete){
@@ -121,7 +128,7 @@ Page.Settings = new Class({
// Add options to group // Add options to group
group.options.sortBy('order').each(function(option){ 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); var input = new Option[class_name](self, section_name, option.name, option);
input.inject(group_el); input.inject(group_el);
}); });
@@ -145,13 +152,13 @@ Page.Settings = new Class({
var tab_el = new Element('li').adopt( var tab_el = new Element('li').adopt(
new Element('a', { new Element('a', {
'href': '/'+self.name+'/'+tab_name+'/', 'href': '/'+self.name+'/'+tab_name+'/',
'text': tab.label.capitalize() 'text': (tab.label || tab.name).capitalize()
}) })
).inject(self.tabs_container); ).inject(self.tabs_container);
if(!self.tabs[tab_name]) if(!self.tabs[tab_name])
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], { self.tabs[tab_name] = Object.merge(self.tabs[tab_name], {
@@ -171,7 +178,7 @@ Page.Settings = new Class({
'class': group.advanced ? 'inlineLabels advanced' : 'inlineLabels' 'class': group.advanced ? 'inlineLabels advanced' : 'inlineLabels'
}).adopt( }).adopt(
new Element('h2', { new Element('h2', {
'text': group.label 'text': group.label || group.name.capitalize()
}).adopt( }).adopt(
new Element('span.hint', { new Element('span.hint', {
'text': group.description 'text': group.description
@@ -223,6 +230,13 @@ var OptionBase = new Class({
create: function(){}, create: function(){},
createLabel: function(){
var self = this;
return new Element('label', {
'text': self.options.label || self.options.name.capitalize()
})
},
setAdvanced: function(){ setAdvanced: function(){
this.el.addClass(this.options.advanced ? 'advanced': '') this.el.addClass(this.options.advanced ? 'advanced': '')
}, },
@@ -319,9 +333,7 @@ Option.String = new Class({
var self = this var self = this
self.el.adopt( self.el.adopt(
new Element('label', { self.createLabel(),
'text': self.options.label
}),
self.input = new Element('input', { self.input = new Element('input', {
'type': 'text', 'type': 'text',
'name': self.postName(), 'name': self.postName(),
@@ -337,18 +349,17 @@ Option.Dropdown = new Class({
create: function(){ create: function(){
var self = this var self = this
new Element('label', { self.el.adopt(
'text': self.options.label self.createLabel(),
}).adopt(
self.input = new Element('select', { self.input = new Element('select', {
'name': self.postName() 'name': self.postName()
}) })
).inject(self.el) )
Object.each(self.options.values, function(label, value){ Object.each(self.options.values, function(value){
new Element('option', { new Element('option', {
'text': label, 'text': value[0],
'value': value 'value': value[1]
}).inject(self.input) }).inject(self.input)
}) })
@@ -366,24 +377,31 @@ Option.Checkbox = new Class({
var randomId = 'option-'+Math.floor(Math.random()*1000000) var randomId = 'option-'+Math.floor(Math.random()*1000000)
new Element('label', { self.el.adopt(
'text': self.options.label, self.createLabel().set('for', randomId),
'for': randomId self.input = new Element('input', {
}).inject(self.el); 'type': 'checkbox',
'value': self.getSettingValue(),
self.input = new Element('input', { 'checked': self.getSettingValue() !== undefined,
'type': 'checkbox', 'id': randomId
'value': self.getSettingValue(), })
'checked': self.getSettingValue() !== undefined, )
'id': randomId
}).inject(self.el);
} }
}); });
Option.Password = new Class({
Extends: Option.String,
type: 'password'
});
Option.Bool = new Class({ Option.Bool = new Class({
Extends: Option.Checkbox Extends: Option.Checkbox
}); });
Option.Enabler = new Class({
Extends: Option.Bool
});
Option.Int = new Class({ Option.Int = new Class({
Extends: Option.String Extends: Option.String
}); });
@@ -401,9 +419,7 @@ Option.Directory = new Class({
self.el.adopt( self.el.adopt(
new Element('label', { self.createLabel(),
'text': self.options.label
}),
self.input = new Element('span', { self.input = new Element('span', {
'text': self.getSettingValue(), 'text': self.getSettingValue(),
'events': { 'events': {
+276 -3
View File
@@ -24,13 +24,22 @@ Page.Wanted = new Class({
var m = new Movie(self, {}, info); var m = new Movie(self, {}, info);
$(m).inject(self.movie_container); $(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){ get: function(status, onComplete){
var self = this var self = this
if(self.movies.length == 0) if(self.movies.length == 0)
Api.request('movie', { Api.request('movie.list', {
'data': {}, 'data': {},
'onComplete': function(json){ 'onComplete': function(json){
self.store(json.movies); self.store(json.movies);
@@ -58,16 +67,280 @@ var Movie = new Class({
self.data = data; self.data = data;
self.profile = Quality.getProfile(data.profile_id);
self.parent(self, options); self.parent(self, options);
}, },
create: function(){ create: function(){
var self = this; var self = this;
self.el = new Element('div.movie', { self.el = new Element('div.movie').adopt(
'text': self.data.name 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();
} }
}) })
+90 -34
View File
@@ -6,13 +6,19 @@ var QualityBase = new Class({
setup: function(data){ setup: function(data){
var self = this; var self = this;
self.profiles = data.profiles;
self.qualities = data.qualities; self.qualities = data.qualities;
self.profiles = {}
Object.each(data.profiles, self.createProfilesClass.bind(self));
App.addEvent('load', self.addSettings.bind(self)) App.addEvent('load', self.addSettings.bind(self))
}, },
getProfile: function(id){
return this.profiles[id]
},
addSettings: function(){ addSettings: function(){
var self = this; var self = this;
@@ -47,22 +53,34 @@ var QualityBase = new Class({
new Element('a.add_new', { new Element('a.add_new', {
'text': 'Create a new quality profile', 'text': 'Create a new quality profile',
'events': { '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') 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; var self = this;
self.profiles[nr] = new Profile(data); if(data){
$(self.profiles[nr]).inject(self.profile_container) return self.profiles[data.id] = new Profile(data);
}
else {
var data = {
'id': randomString()
}
return self.profiles[data.id] = new Profile(data);
}
}, },
/** /**
@@ -119,12 +137,9 @@ var Profile = new Class({
var data = self.data; var data = self.data;
self.el = new Element('div', { self.el = new Element('div.profile').adopt(
'class': 'profile' self.header = new Element('h4', {'text': data.label}),
}).adopt( new Element('span.delete.icon', {
new Element('h4', {'text': data.label}),
new Element('span.delete', {
'html': 'del',
'events': { 'events': {
'click': self.del.bind(self) 'click': self.del.bind(self)
} }
@@ -135,25 +150,29 @@ var Profile = new Class({
new Element('label', {'text':'Name'}), new Element('label', {'text':'Name'}),
new Element('input.label.textInput.large', { new Element('input.label.textInput.large', {
'type':'text', '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('div.ctrlHolder').adopt(
new Element('label', {'text':'Wait'}), new Element('label', {'text':'Wait'}),
new Element('input.wait_for.textInput.xsmall', { new Element('input.wait_for.textInput.xsmall', {
'type':'text', '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('span', {'text':' day(s) for better quality.'})
), ),
new Element('div.ctrlHolder').adopt( new Element('div.ctrlHolder').adopt(
new Element('label', {'text': 'Qualities'}), new Element('label', {'text': 'Qualities'}),
self.type_container = new Element('div.types').adopt( new Element('div.head').adopt(
new Element('div.head').adopt( new Element('span.quality_type', {'text': 'Search for'}),
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('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', { new Element('a.addType', {
'text': 'Add another quality to search for.', 'text': 'Add another quality to search for.',
'href': '#', 'href': '#',
@@ -164,6 +183,8 @@ var Profile = new Class({
) )
); );
self.makeSortable()
if(data.types) if(data.types)
Object.each(data.types, self.addType.bind(self)) Object.each(data.types, self.addType.bind(self))
}, },
@@ -174,11 +195,19 @@ var Profile = new Class({
if(self.save_timer) clearTimeout(self.save_timer); if(self.save_timer) clearTimeout(self.save_timer);
self.save_timer = (function(){ self.save_timer = (function(){
var data = self.getData();
if(data.types.length < 2) return;
Api.request('profile.save', { Api.request('profile.save', {
'data': self.getData(), 'data': self.getData(),
'useSpinner': true, 'useSpinner': true,
'spinnerOptions': { 'spinnerOptions': {
'target': self.el 'target': self.el
},
'onComplete': function(json){
if(json.success){
self.data = json.profile
}
} }
}); });
}).delay(delay, self) }).delay(delay, self)
@@ -195,9 +224,12 @@ var Profile = new Class({
'types': [] 'types': []
} }
Object.each(self.types, function(type){ Array.each(self.type_container.getElements('.type'), function(type){
if(!type.deleted) if(!type.hasClass('deleted'))
data.types.include(type.getData()); data.types.include({
'quality_id': type.getElement('select').get('value'),
'finish': +type.getElement('input[type=checkbox]').checked
});
}) })
return data return data
@@ -208,6 +240,7 @@ var Profile = new Class({
var t = new Profile.Type(data); var t = new Profile.Type(data);
$(t).inject(self.type_container); $(t).inject(self.type_container);
self.sortable.addItems($(t));
self.types.include(t); self.types.include(t);
@@ -216,6 +249,8 @@ var Profile = new Class({
del: function(){ del: function(){
var self = this; var self = this;
if(!confirm('Are you sure you want to delete this profile?')) return
Api.request('profile.delete', { Api.request('profile.delete', {
'data': { 'data': {
'id': self.data.id 'id': self.data.id
@@ -224,12 +259,35 @@ var Profile = new Class({
'spinnerOptions': { 'spinnerOptions': {
'target': self.el 'target': self.el
}, },
'onComplete': function(){ 'onComplete': function(json){
self.el.destroy(); 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(){ toElement: function(){
return this.el return this.el
} }
@@ -252,7 +310,7 @@ Profile.Type = Class({
var self = this; var self = this;
var data = self.data; var data = self.data;
self.el = new Element('div.type').adopt( self.el = new Element('li.type').adopt(
new Element('span.quality_type').adopt( new Element('span.quality_type').adopt(
self.fillQualities() self.fillQualities()
), ),
@@ -263,15 +321,12 @@ Profile.Type = Class({
'checked': data.finish 'checked': data.finish
}) })
), ),
new Element('span.delete', { new Element('span.delete.icon', {
'html': 'del',
'events': { 'events': {
'click': self.del.bind(self) 'click': self.del.bind(self)
} }
}), }),
new Element('span', { new Element('span.handle')
'class':'handle'
})
) )
}, },
@@ -284,11 +339,11 @@ Profile.Type = Class({
Object.each(Quality.qualities, function(q){ Object.each(Quality.qualities, function(q){
new Element('option', { new Element('option', {
'text': q.label, 'text': q.label,
'value': q.identifier 'value': q.id
}).inject(self.qualities) }).inject(self.qualities)
}); });
self.qualities.set('value', self.data.quality); self.qualities.set('value', self.data.quality_id);
return self.qualities; return self.qualities;
@@ -298,7 +353,7 @@ Profile.Type = Class({
var self = this; var self = this;
return { return {
'quality': self.qualities.get('value'), 'quality_id': self.qualities.get('value'),
'finish': +self.finish.checked 'finish': +self.finish.checked
} }
}, },
@@ -306,6 +361,7 @@ Profile.Type = Class({
del: function(){ del: function(){
var self = this; var self = this;
self.el.addClass('deleted');
self.el.hide(); self.el.hide();
self.deleted = true; self.deleted = true;
}, },
+11
View File
@@ -0,0 +1,11 @@
var StatusBase = new Class({
setup: function(statuses){
var self = this;
self.statuses = statuses;
}
});
window.Status = new StatusBase();
+6
View File
@@ -121,6 +121,12 @@ form {
cursor: pointer; cursor: pointer;
} }
/*** Icons ***/
.icon.delete {
background: url('../images/delete.png') no-repeat;
display: inline-block;
}
/*** Navigation ***/ /*** Navigation ***/
.header { .header {
background: #f7f7f7; background: #f7f7f7;
@@ -24,7 +24,7 @@
margin: 0 0 -5px -20px; margin: 0 0 -5px -20px;
top: 4px; top: 4px;
right: 5px; right: 5px;
background: url('../images/close_button.png') 0 center no-repeat; background: url('../../images/close_button.png') 0 center no-repeat;
cursor: pointer; cursor: pointer;
} }
.search_form .input a:hover { background-position: -12px center; } .search_form .input a:hover { background-position: -12px center; }
@@ -46,7 +46,7 @@
-moz-border-radius: 3px; -moz-border-radius: 3px;
} }
.search_form .spinner { .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 { .search_form .pointer {
@@ -83,7 +83,7 @@
display: inline-block; display: inline-block;
margin-right: 10px; 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 select[name=quality] { width: 90px; }
.search_form .results .movie .options .button { .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;
}
+16 -33
View File
@@ -1,12 +1,13 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<link rel="stylesheet" href="{{ url_for('.static', filename='style/main.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') }}"> <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') }}"> <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/page/settings.css') }}" type="text/css">
<link rel="stylesheet" href="{{ url_for('.static', filename='style/movie_add.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.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/mootools_more.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/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/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 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"> <script type="text/javascript">
window.addEvent('domready', function() { window.addEvent('domready', function() {
@@ -44,34 +48,13 @@
}); });
Quality.setup({ Quality.setup({
'profiles': [ 'profiles': {{ fireEvent('profile.all', single = True)|tojson|safe }},
{ 'qualities': {{ fireEvent('quality.all', single = True)|tojson|safe }}
'identifier': 'hd', });
'label': 'HD',
'wait_for': 7, Status.setup({{ fireEvent('status.all', single = True)|tojson|safe }});
'types': [
{'quality': '720p', 'finish': true}, File.Type.setup({{ fireEvent('file.types', single = True)|tojson|safe }});
{'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'}
]
})
App.setup({ App.setup({
'base_url': '{{ request.path }}' 'base_url': '{{ request.path }}'
+3 -1
View File
@@ -179,10 +179,12 @@ class Event(object):
if not self.asynchronous: if not self.asynchronous:
self.result.append(tuple(r)) self.result.append(tuple(r))
except Exception: except Exception, e:
if not self.asynchronous: if not self.asynchronous:
self.result.append((False, self._error(sys.exc_info()), self.result.append((False, self._error(sys.exc_info()),
handler)) handler))
else:
self.error_handler(sys.exc_info())
finally: finally:
if isinstance(self.lock, threading._RLock): if isinstance(self.lock, threading._RLock):
self.lock.release() self.lock.release()
-30
View File
@@ -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
""")