Browse Source

Notifications

pull/1/merge
Ruud 14 years ago
parent
commit
85fc1c01ee
  1. 3
      .gitignore
  2. 3
      couchpotato/__init__.py
  3. 10
      couchpotato/cli.py
  4. 16
      couchpotato/core/__init__.py
  5. 0
      couchpotato/core/downloaders/__init__.py
  6. 20
      couchpotato/core/downloaders/base.py
  7. 32
      couchpotato/core/downloaders/blackhole/__init__.py
  8. 36
      couchpotato/core/downloaders/blackhole/main.py
  9. 39
      couchpotato/core/downloaders/sabnzbd/__init__.py
  10. 120
      couchpotato/core/downloaders/sabnzbd/main.py
  11. 52
      couchpotato/core/event.py
  12. 3
      couchpotato/core/helpers/encoding.py
  13. 2
      couchpotato/core/helpers/request.py
  14. 38
      couchpotato/core/helpers/variable.py
  15. 23
      couchpotato/core/loader.py
  16. 0
      couchpotato/core/notifications/__init__.py
  17. 29
      couchpotato/core/notifications/base.py
  18. 33
      couchpotato/core/notifications/growl/__init__.py
  19. 111
      couchpotato/core/notifications/growl/growl.py
  20. 45
      couchpotato/core/notifications/growl/main.py
  21. 37
      couchpotato/core/notifications/nmj/__init__.py
  22. 131
      couchpotato/core/notifications/nmj/main.py
  23. 32
      couchpotato/core/notifications/notifo/__init__.py
  24. 53
      couchpotato/core/notifications/notifo/main.py
  25. 33
      couchpotato/core/notifications/plex/__init__.py
  26. 43
      couchpotato/core/notifications/plex/main.py
  27. 35
      couchpotato/core/notifications/prowl/__init__.py
  28. 51
      couchpotato/core/notifications/prowl/main.py
  29. 38
      couchpotato/core/notifications/xbmc/__init__.py
  30. 47
      couchpotato/core/notifications/xbmc/main.py
  31. 6
      couchpotato/core/plugins/file/__init__.py
  32. 103
      couchpotato/core/plugins/file/main.py
  33. 2
      couchpotato/core/plugins/file_browser/__init__.py
  34. 2
      couchpotato/core/plugins/library/__init__.py
  35. 65
      couchpotato/core/plugins/library/main.py
  36. 2
      couchpotato/core/plugins/movie/__init__.py
  37. 103
      couchpotato/core/plugins/movie/main.py
  38. 2
      couchpotato/core/plugins/profile/__init__.py
  39. 76
      couchpotato/core/plugins/profile/main.py
  40. 6
      couchpotato/core/plugins/quality/__init__.py
  41. 97
      couchpotato/core/plugins/quality/main.py
  42. 6
      couchpotato/core/plugins/status/__init__.py
  43. 68
      couchpotato/core/plugins/status/main.py
  44. 3
      couchpotato/core/providers/base.py
  45. 0
      couchpotato/core/providers/themoviedb/__init__.py
  46. 116
      couchpotato/core/providers/themoviedb/main.py
  47. 107
      couchpotato/core/providers/tmdb/main.py
  48. 79
      couchpotato/core/settings/model.py
  49. BIN
      couchpotato/static/images/delete.png
  50. BIN
      couchpotato/static/images/favicon.ico
  51. BIN
      couchpotato/static/images/handle.png
  52. BIN
      couchpotato/static/images/homescreen.png
  53. 70
      couchpotato/static/scripts/block/search.js
  54. 15
      couchpotato/static/scripts/couchpotato.js
  55. 77
      couchpotato/static/scripts/file.js
  56. 83
      couchpotato/static/scripts/library/mootools.js
  57. 847
      couchpotato/static/scripts/library/mootools_more.js
  58. 78
      couchpotato/static/scripts/page/settings.js
  59. 281
      couchpotato/static/scripts/page/wanted.js
  60. 134
      couchpotato/static/scripts/quality.js
  61. 11
      couchpotato/static/scripts/status.js
  62. 6
      couchpotato/static/style/main.css
  63. 6
      couchpotato/static/style/plugin/movie_add.css
  64. 21
      couchpotato/static/style/plugin/quality.css
  65. 49
      couchpotato/templates/_desktop.html
  66. 4
      libs/axl/axel.py
  67. 30
      setup.py

3
.gitignore

@ -1,2 +1,3 @@
/settings.conf
/logs/*.log
/logs/*.log
/_source/

3
couchpotato/__init__.py

@ -1,4 +1,5 @@
from couchpotato.core.auth import requires_auth
from couchpotato.core.event import fireEvent
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
from flask.app import Flask
@ -30,7 +31,7 @@ def get_engine():
@web.route('/')
@requires_auth
def index():
return render_template('index.html', sep = os.sep)
return render_template('index.html', sep = os.sep, fireEvent = fireEvent)
@app.errorhandler(404)
def page_not_found(error):

10
couchpotato/cli.py

@ -1,6 +1,7 @@
from argparse import ArgumentParser
from couchpotato import web
from couchpotato.api import api
from couchpotato.core.event import fireEvent
from libs.daemon import createDaemon
from logging import handlers
from werkzeug.contrib.cache import FileSystemCache
@ -49,7 +50,7 @@ def cmd_couchpotato(base_path, args):
Env.set('data_dir', options.data_dir)
Env.set('db_path', 'sqlite:///' + os.path.join(options.data_dir, 'couchpotato.db'))
Env.set('cache_dir', os.path.join(options.data_dir, 'cache'))
Env.set('cache', FileSystemCache(Env.get('cache_dir')))
Env.set('cache', FileSystemCache(os.path.join(Env.get('cache_dir'), 'python')))
Env.set('quiet', options.quiet)
Env.set('daemonize', options.daemonize)
Env.set('args', args)
@ -97,6 +98,7 @@ def cmd_couchpotato(base_path, args):
from migrate.versioning.api import version_control, db_version, version, upgrade
db = Env.get('db_path')
repo = os.path.join('couchpotato', 'core', 'migration')
logging.getLogger('migrate').setLevel(logging.WARNING) # Disable logging for migration
latest_db_version = version(repo)
@ -114,6 +116,7 @@ def cmd_couchpotato(base_path, args):
from couchpotato.core.settings.model import setup
setup()
fireEvent('app.load')
# Create app
from couchpotato import app
@ -128,11 +131,6 @@ def cmd_couchpotato(base_path, args):
app.secret_key = api_key
app.static_path = url_base + '/static'
# Add static url with url_base
app.add_url_rule(app.static_path + '/<path:filename>',
endpoint = 'static',
view_func = app.send_static_file)
# Register modules
app.register_module(web, url_prefix = '%s/' % url_base)
app.register_module(api, url_prefix = '%s/%s/' % (url_base, api_key if not debug else 'api'))

16
couchpotato/core/__init__.py

@ -9,35 +9,28 @@ config = [{
{
'tab': 'general',
'name': 'basics',
'label': 'Basics',
'description': 'Needs restart before changes take effect.',
'options': [
{
'name': 'username',
'default': '',
'type': 'string',
'label': 'Username',
},
{
'name': 'password',
'default': '',
'password': True,
'type': 'string',
'label': 'Password',
'type': 'password',
},
{
'name': 'host',
'advanced': True,
'default': '0.0.0.0',
'type': 'string',
'label': 'Host',
'label': 'IP',
'description': 'Host that I should listen to. "0.0.0.0" listens to all ips.',
},
{
'name': 'port',
'default': 5000,
'type': 'int',
'label': 'Port',
'description': 'The port I should listen to.',
},
{
@ -52,14 +45,12 @@ config = [{
{
'tab': 'general',
'name': 'advanced',
'label': 'Advanced',
'description': "For those who know what the're doing",
'advanced': True,
'options': [
{
'name': 'api_key',
'default': uuid4().hex,
'type': 'string',
'readonly': True,
'label': 'Api Key',
'description': "This is top-secret! Don't share this!",
@ -74,9 +65,8 @@ config = [{
{
'name': 'url_base',
'default': '',
'type': 'string',
'label': 'Url Base',
'description': 'When using mod_proxy use this to prepend the url with this.',
'description': 'When using mod_proxy use this to append the url with this.',
},
],
},

0
couchpotato/core/downloaders/__init__.py

20
couchpotato/core/downloaders/base.py

@ -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)

32
couchpotato/core/downloaders/blackhole/__init__.py

@ -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.',
},
],
}
],
}]

36
couchpotato/core/downloaders/blackhole/main.py

@ -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

39
couchpotato/core/downloaders/sabnzbd/__init__.py

@ -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.',
},
],
}
],
}]

120
couchpotato/core/downloaders/sabnzbd/main.py

@ -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

52
couchpotato/core/event.py

@ -1,4 +1,5 @@
from axl.axel import Event
from couchpotato.core.helpers.variable import merge_dicts
from couchpotato.core.logger import CPLog
import traceback
@ -19,31 +20,64 @@ def removeEvent(name, handler):
e -= handler
def fireEvent(name, *args, **kwargs):
log.debug('Firing "%s": %s, %s' % (name, args, kwargs))
try:
# Return single handler
single = False
try:
del kwargs['single']
single = True
except: pass
# Merge items
merge = False
try:
del kwargs['merge']
merge = True
except: pass
e = events[name]
e.asynchronous = False
result = e(*args, **kwargs)
results = []
for r in result:
if r[0] == True:
results.append(r[1])
else:
etype, value, tb = r[1]
log.debug(''.join(traceback.format_exception(etype, value, tb)))
if single and not merge:
results = result[0][1]
else:
results = []
for r in result:
if r[0] == True:
results.append(r[1])
else:
errorHandler(r[1])
# Merge the results
if merge:
merged = {}
for result in results:
merged = merge_dicts(merged, result)
results = merged
return results
except Exception, e:
log.debug(e)
log.error('%s: %s' % (name, e))
def fireEventAsync(name, *args, **kwargs):
log.debug('Async "%s": %s, %s' % (name, args, kwargs))
try:
e = events[name]
e.asynchronous = True
e.error_handler = errorHandler
e(*args, **kwargs)
return True
except Exception, e:
log.debug(e)
log.error('%s: %s' % (name, e))
def errorHandler(error):
etype, value, tb = error
log.error(''.join(traceback.format_exception(etype, value, tb)))
def getEvent(name):
return events[name]

3
couchpotato/core/helpers/encoding.py

@ -5,16 +5,19 @@ import unicodedata
log = CPLog(__name__)
def toSafeString(original):
valid_chars = "-_.() %s%s" % (ascii_letters, digits)
cleanedFilename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
return ''.join(c for c in cleanedFilename if c in valid_chars)
def simplifyString(original):
string = toSafeString(original)
split = re.split('\W+', string.lower())
return toUnicode(' '.join(split))
def toUnicode(original, *args):
try:
if type(original) is unicode:

2
couchpotato/core/helpers/request.py

@ -42,7 +42,7 @@ def dictToList(params):
new = {}
for x, value in params.iteritems():
try:
new_value = [dictToList(value2) for value2 in value.itervalues()]
new_value = [dictToList(value[k]) for k in sorted(value.iterkeys())]
except:
new_value = value

38
couchpotato/core/helpers/variable.py

@ -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

23
couchpotato/core/loader.py

@ -1,4 +1,4 @@
from couchpotato.core.event import fireEvent, fireEventAsync
from couchpotato.core.event import fireEvent
from couchpotato.core.logger import CPLog
import glob
import os
@ -17,6 +17,8 @@ class Loader:
self.paths = {
'plugin' : ('couchpotato.core.plugins', os.path.join(root, 'couchpotato', 'core', 'plugins')),
'provider' : ('couchpotato.core.providers', os.path.join(root, 'couchpotato', 'core', 'providers')),
'notifications' : ('couchpotato.core.notifications', os.path.join(root, 'couchpotato', 'core', 'notifications')),
'downloaders' : ('couchpotato.core.downloaders', os.path.join(root, 'couchpotato', 'core', 'downloaders')),
}
for type, tuple in self.paths.iteritems():
@ -28,14 +30,17 @@ class Loader:
for module_name, plugin in sorted(self.modules.iteritems()):
# Load module
m = getattr(self.loadModule(module_name), plugin.get('name'))
try:
m = getattr(self.loadModule(module_name), plugin.get('name'))
log.info("Loading '%s'" % module_name)
log.info("Loading %s: %s" % (plugin['type'], plugin['name']))
# Save default settings for plugin/provider
did_save += self.loadSettings(m, module_name, save = False)
# Save default settings for plugin/provider
did_save += self.loadSettings(m, module_name, save = False)
self.loadPlugins(m, plugin.get('name'))
self.loadPlugins(m, plugin.get('name'))
except Exception, e:
log.error(e)
if did_save:
fireEvent('settings.save')
@ -51,12 +56,12 @@ class Loader:
def loadSettings(self, module, name, save = True):
try:
for section in module.config:
fireEventAsync('settings.options', section['name'], section)
fireEvent('settings.options', section['name'], section)
options = {}
for group in section['groups']:
for option in group['options']:
options[option['name']] = option['default']
fireEventAsync('settings.register', section_name = section['name'], options = options, save = save)
fireEvent('settings.register', section_name = section['name'], options = options, save = save)
return True
except Exception, e:
log.debug("Failed loading settings for '%s': %s" % (name, e))
@ -67,7 +72,7 @@ class Loader:
module.start()
return True
except Exception, e:
log.debug("Failed loading plugin '%s': %s" % (name, e))
log.error("Failed loading plugin '%s': %s" % (name, e))
return False
def addModule(self, type, module, name):

0
couchpotato/core/notifications/__init__.py

29
couchpotato/core/notifications/base.py

@ -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})

33
couchpotato/core/notifications/growl/__init__.py

@ -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': '',
},
],
}
],
}]

111
couchpotato/core/notifications/growl/growl.py

@ -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

45
couchpotato/core/notifications/growl/main.py

@ -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.')

37
couchpotato/core/notifications/nmj/__init__.py

@ -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
couchpotato/core/notifications/nmj/main.py

@ -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})

32
couchpotato/core/notifications/notifo/__init__.py

@ -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',
},
],
}
],
}]

53
couchpotato/core/notifications/notifo/main.py

@ -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

33
couchpotato/core/notifications/plex/__init__.py

@ -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': '',
},
],
}
],
}]

43
couchpotato/core/notifications/plex/main.py

@ -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

35
couchpotato/core/notifications/prowl/__init__.py

@ -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)]
},
],
}
],
}]

51
couchpotato/core/notifications/prowl/main.py

@ -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

38
couchpotato/core/notifications/xbmc/__init__.py

@ -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': '',
},
],
}
],
}]

47
couchpotato/core/notifications/xbmc/main.py

@ -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

6
couchpotato/core/plugins/file/__init__.py

@ -0,0 +1,6 @@
from .main import FileManager
def start():
return FileManager()
config = []

103
couchpotato/core/plugins/file/main.py

@ -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

2
couchpotato/core/plugins/file_browser/__init__.py

@ -1,4 +1,4 @@
from couchpotato.core.plugins.file_browser.main import FileBrowser
from .main import FileBrowser
def start():
return FileBrowser()

2
couchpotato/core/plugins/library/__init__.py

@ -1,4 +1,4 @@
from couchpotato.core.plugins.library.main import LibraryPlugin
from .main import LibraryPlugin
def start():
return LibraryPlugin()

65
couchpotato/core/plugins/library/main.py

@ -1,32 +1,81 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library
from couchpotato.core.settings.model import Library, LibraryTitle
log = CPLog(__name__)
class LibraryPlugin(Plugin):
def __init__(self):
addEvent('library.add', self.add)
addEvent('library.update', self.update)
def add(self, attrs = {}):
db = get_session();
db = get_session()
l = db.query(Library).filter_by(identifier = attrs.get('identifier')).first()
if not l:
l = Library(
name = attrs.get('name'),
year = attrs.get('year'),
identifier = attrs.get('identifier'),
description = attrs.get('description')
plot = attrs.get('plot'),
tagline = attrs.get('tagline')
)
title = LibraryTitle(
title = attrs.get('title')
)
l.titles.append(title)
db.add(l)
db.commit()
# Update library info
fireEventAsync('library.update', library = l, default_title = attrs.get('title', ''))
#db.remove()
return l
def update(self, item):
def update(self, library, default_title = ''):
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')
pass
fireEvent('library.update.after')

2
couchpotato/core/plugins/movie/__init__.py

@ -1,4 +1,4 @@
from couchpotato.core.plugins.movie.main import MoviePlugin
from .main import MoviePlugin
def start():
return MoviePlugin()

103
couchpotato/core/plugins/movie/main.py

@ -1,9 +1,9 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent
from couchpotato.core.event import fireEvent, fireEventAsync
from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie, Release, Profile
from couchpotato.core.settings.model import Movie
from couchpotato.environment import Env
from urllib import urlencode
@ -13,30 +13,28 @@ class MoviePlugin(Plugin):
def __init__(self):
addApiView('movie.search', self.search)
addApiView('movie.list', self.list)
addApiView('movie.refresh', self.refresh)
addApiView('movie.add', self.add)
addApiView('movie.edit', self.edit)
addApiView('movie.delete', self.delete)
def list(self):
a = getParams()
params = getParams()
db = get_session()
results = get_session().query(Movie).filter(
Movie.releases.any(
Release.status.has(identifier = 'wanted')
)
).all()
results = db.query(Movie).filter(
Movie.status.has(identifier = params.get('status', 'active'))
).all()
movies = []
for movie in results:
temp = {
'id': movie.id,
'name': movie.id,
'releases': [],
}
for release in movie.releases:
temp['releases'].append({
'status': release.status.label,
'quality': release.quality.label
})
temp = movie.to_dict(deep = {
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}},
'files': {}
})
movies.append(temp)
@ -46,22 +44,41 @@ class MoviePlugin(Plugin):
'movies': movies,
})
def refresh(self):
params = getParams()
db = get_session()
movie = db.query(Movie).filter_by(id = params.get('id')).first()
# Get current selected title
default_title = ''
for title in movie.library.titles:
if title.default: default_title = title.title
if movie:
#addEvent('library.update.after', )
fireEventAsync('library.update', library = movie.library, default_title = default_title)
return jsonified({
'success': True,
})
def search(self):
a = getParams()
cache_key = '%s/%s' % (__name__, urlencode(a))
params = getParams()
cache_key = '%s/%s' % (__name__, urlencode(params))
movies = Env.get('cache').get(cache_key)
if not movies:
results = fireEvent('provider.movie.search', q = a.get('q'))
results = fireEvent('provider.movie.search', q = params.get('q'))
# Combine movie results
movies = []
for r in results:
movies += r
Env.get('cache').set(cache_key, movies, timeout = 10)
Env.get('cache').set(cache_key, movies)
return jsonified({
'success': True,
@ -71,24 +88,46 @@ class MoviePlugin(Plugin):
def add(self):
a = getParams()
params = getParams()
db = get_session();
library = fireEvent('library.add', attrs = a)
profile = db.query(Profile).filter_by(identifier = a.get('profile_identifier'))
m = db.query(Movie).filter_by(library = library).first()
library = fireEvent('library.add', single = True, attrs = params)
status = fireEvent('status.add', 'active', single = True)
m = db.query(Movie).filter_by(library_id = library.id).first()
if not m:
m = Movie(
library = library,
profile = profile,
library_id = library.id,
profile_id = params.get('profile_id')
)
db.add(m)
db.commit()
m.status_id = status.id
db.commit()
return jsonified({
'success': True,
'added': True,
'params': a,
'movie': m.to_dict(deep = {
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}}
})
})
def edit(self):
pass
def delete(self):
params = getParams()
db = get_session()
status = fireEvent('status.add', 'deleted', single = True)
movie = db.query(Movie).filter_by(id = params.get('id')).first()
movie.status_id = status.id
db.commit()
return jsonified({
'success': True,
})

2
couchpotato/core/plugins/profile/__init__.py

@ -1,4 +1,4 @@
from couchpotato.core.plugins.profile.main import ProfilePlugin
from .main import ProfilePlugin
def start():
return ProfilePlugin()

76
couchpotato/core/plugins/profile/main.py

@ -1,28 +1,90 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.helpers.request import jsonified, getParams, getParam
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Profile, ProfileType
log = CPLog(__name__)
class ProfilePlugin(Plugin):
def __init__(self):
addEvent('profile.get', self.get)
addEvent('profile.all', self.all)
addApiView('profile.save', self.save)
addApiView('profile.delete', self.delete)
def get(self, key = ''):
def all(self):
db = get_session()
profiles = db.query(Profile).all()
pass
temp = []
for profile in profiles:
temp.append(profile.to_dict(deep = {'types': {}}))
return temp
def save(self):
a = getParams()
params = getParams()
db = get_session()
p = db.query(Profile).filter_by(id = params.get('id')).first()
if not p:
p = Profile()
db.add(p)
p.label = params.get('label')
p.order = params.get('order', p.order if p.order else 0)
p.core = params.get('core', False)
#delete old types
[db.delete(t) for t in p.types]
order = 0
for type in params.get('types', []):
t = ProfileType(
order = order,
finish = type.get('finish'),
wait_for = params.get('wait_for'),
quality_id = type.get('quality_id')
)
p.types.append(t)
order += 1
db.commit()
return jsonified({
'success': True,
'a': a
'profile': p.to_dict(deep = {'types': {}})
})
def delete(self):
pass
id = getParam('id')
db = get_session()
success = False
message = ''
try:
p = db.query(Profile).filter_by(id = id).first()
db.delete(p)
db.commit()
success = True
except Exception, e:
message = 'Failed deleting Profile: %s' % e
log.error(message)
return jsonified({
'success': success,
'message': message
})

6
couchpotato/core/plugins/quality/__init__.py

@ -0,0 +1,6 @@
from .main import QualityPlugin
def start():
return QualityPlugin()
config = []

97
couchpotato/core/plugins/quality/main.py

@ -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

6
couchpotato/core/plugins/status/__init__.py

@ -0,0 +1,6 @@
from .main import StatusPlugin
def start():
return StatusPlugin()
config = []

68
couchpotato/core/plugins/status/main.py

@ -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()

3
couchpotato/core/providers/base.py

@ -1,8 +1,9 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
log = CPLog(__name__)
class Provider():
class Provider(Plugin):
type = None # movie, nzb, torrent, subtitle, trailer
timeout = 10 # Default timeout for url requests

0
couchpotato/core/providers/tmdb/__init__.py → couchpotato/core/providers/themoviedb/__init__.py

116
couchpotato/core/providers/themoviedb/main.py

@ -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
couchpotato/core/providers/tmdb/main.py

@ -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

79
couchpotato/core/settings/model.py

@ -2,8 +2,10 @@ from elixir.entity import Entity
from elixir.fields import Field
from elixir.options import options_defaults
from elixir.relationships import OneToMany, ManyToOne
from sqlalchemy.types import Integer, String, Unicode, UnicodeText, Boolean, \
Float
from libs.elixir.options import using_options
from libs.elixir.relationships import ManyToMany
from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, Float, \
String
options_defaults["shortnames"] = True
@ -20,24 +22,48 @@ class Movie(Entity):
The files belonging to the movie object are global for the whole movie
such as trailers, nfo, thumbnails"""
library = ManyToOne('Library')
last_edit = Field(Integer)
library = ManyToOne('Library')
status = ManyToOne('Status')
profile = ManyToOne('Profile')
releases = OneToMany('Release')
files = OneToMany('File')
files = ManyToMany('File')
class Library(Entity):
""""""
title = Field(Unicode)
year = Field(Integer)
identifier = Field(Unicode)
identifier = Field(String(20))
rating = Field(Float)
plot = Field(UnicodeText)
tagline = Field(UnicodeText(255))
status = ManyToOne('Status')
movie = OneToMany('Movie')
titles = OneToMany('LibraryTitle')
files = ManyToMany('File')
class LibraryTitle(Entity):
""""""
title = Field(Unicode)
default = Field(Boolean)
language = OneToMany('Language')
libraries = ManyToOne('Library')
class Language(Entity):
""""""
identifier = Field(String(20))
label = Field(Unicode)
titles = ManyToOne('LibraryTitle')
class Release(Entity):
@ -47,7 +73,7 @@ class Release(Entity):
movie = ManyToOne('Movie')
status = ManyToOne('Status')
quality = ManyToOne('Quality')
files = OneToMany('File')
files = ManyToMany('File')
history = OneToMany('History')
@ -55,41 +81,49 @@ class Status(Entity):
"""The status of a release, such as Downloaded, Deleted, Wanted etc"""
identifier = Field(String(20), unique = True)
label = Field(String(20))
label = Field(Unicode(20))
releases = OneToMany('Release')
movies = OneToMany('Movie')
class Quality(Entity):
"""Quality name of a release, DVD, 720P, DVD-Rip etc"""
using_options(order_by = 'order')
identifier = Field(String(20), unique = True)
label = Field(String(20))
label = Field(Unicode(20))
order = Field(Integer)
size_min = Field(Integer)
size_max = Field(Integer)
releases = OneToMany('Release')
profile_types = ManyToOne('ProfileType')
profile_types = OneToMany('ProfileType')
class Profile(Entity):
""""""
using_options(order_by = 'order')
identifier = Field(String(20), unique = True)
label = Field(Unicode(50))
order = Field(Integer)
wait_for = Field(Integer)
core = Field(Boolean)
hide = Field(Boolean)
movie = OneToMany('Movie')
profile_type = OneToMany('ProfileType')
types = OneToMany('ProfileType', cascade = 'all, delete-orphan')
class ProfileType(Entity):
""""""
using_options(order_by = 'order')
order = Field(Integer)
mark_completed = Field(Boolean)
finish = Field(Boolean)
wait_for = Field(Integer)
type = OneToMany('Quality')
quality = ManyToOne('Quality')
profile = ManyToOne('Profile')
@ -97,19 +131,22 @@ class File(Entity):
"""File that belongs to a release."""
path = Field(Unicode(255), nullable = False, unique = True)
part = Field(Integer)
part = Field(Integer, default = 1)
history = OneToMany('RenameHistory')
movie = ManyToOne('Movie')
release = ManyToOne('Release')
type = ManyToOne('FileType')
properties = OneToMany('FileProperty')
history = OneToMany('RenameHistory')
movie = ManyToMany('Movie')
release = ManyToMany('Release')
library = ManyToMany('Library')
class FileType(Entity):
"""Types could be trailer, subtitle, movie, partial movie etc."""
identifier = Field(String(20), unique = True)
type = Field(Unicode(20))
name = Field(Unicode(50), nullable = False)
files = OneToMany('File')
@ -135,8 +172,8 @@ class History(Entity):
class RenameHistory(Entity):
"""Remembers from where to where files have been moved."""
old = Field(String(255))
new = Field(String(255))
old = Field(Unicode(255))
new = Field(Unicode(255))
file = ManyToOne('File')

BIN
couchpotato/static/images/delete.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

BIN
couchpotato/static/images/favicon.ico

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 28 KiB

BIN
couchpotato/static/images/handle.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

BIN
couchpotato/static/images/homescreen.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

70
couchpotato/static/scripts/block/search.js

@ -34,6 +34,7 @@ Block.Search = new Class({
self.spinner = new Spinner(self.result_container);
self.OuterClickStack = new EventStack.OuterClick();
History.addEvent('change', self.hideResults.bind(self, true));
//debug
//self.input.set('value', 'kick ass')
@ -126,16 +127,16 @@ Block.Search = new Class({
Object.each(json.movies, function(movie){
if(!movie.imdb || (movie.imdb && !self.results.getElement('#'+movie.imdb))){
// if(!movie.imdb || (movie.imdb && !self.results.getElement('#'+movie.imdb))){
var m = new Block.Search.Item(movie);
$(m).inject(self.results)
self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m
}
else {
self.movies[movie.imdb].alternativeName({
'name': movie.name
})
}
// }
// else {
// self.movies[movie.imdb].alternativeTitle({
// 'title': movie.title
// })
// }
});
@ -157,7 +158,7 @@ Block.Search.Item = new Class({
var self = this;
self.info = info;
self.alternative_names = [];
self.alternative_titles = [];
self.create();
@ -167,7 +168,7 @@ Block.Search.Item = new Class({
create: function(){
var self = this;
var info = self.info
var info = self.info;
self.el = new Element('div.movie', {
'id': info.imdb
@ -182,12 +183,12 @@ Block.Search.Item = new Class({
'click': self.showOptions.bind(self)
}
}).adopt(
self.thumbnail = info.poster ? new Element('img.thumbnail', {
'src': info.poster
self.thumbnail = info.images.posters.length > 0 ? new Element('img.thumbnail', {
'src': info.images.posters[0]
}) : null,
new Element('div.info').adopt(
self.name = new Element('h2', {
'text': info.name
self.title = new Element('h2', {
'text': info.titles[0]
}).adopt(
self.year = info.year ? new Element('span', {
'text': info.year
@ -214,15 +215,18 @@ Block.Search.Item = new Class({
})
}
self.alternativeName({
'name': info.name
});
info.titles.each(function(title){
self.alternativeTitle({
'title': title
});
})
},
alternativeName: function(alternative){
alternativeTitle: function(alternative){
var self = this;
self.alternative_names.include(alternative);
self.alternative_titles.include(alternative);
},
showOptions: function(){
@ -246,8 +250,8 @@ Block.Search.Item = new Class({
Api.request('movie.add', {
'data': {
'identifier': self.info.imdb,
'name': self.name_select.get('value'),
'quality': self.quality_select.get('value')
'title': self.title_select.get('value'),
'profile_id': self.profile_select.get('value')
},
'useSpinner': true,
'spinnerTarget': self.options,
@ -277,14 +281,14 @@ Block.Search.Item = new Class({
self.options.adopt(
new Element('div').adopt(
self.info.poster ? new Element('img.thumbnail', {
'src': self.info.poster
self.info.images.posters.length > 0 ? new Element('img.thumbnail', {
'src': self.info.images.posters[0]
}) : null,
self.name_select = new Element('select', {
'name': 'name'
self.title_select = new Element('select', {
'name': 'title'
}),
self.quality_select = new Element('select', {
'name': 'profile_identifier'
self.profile_select = new Element('select', {
'name': 'profile'
}),
new Element('a.button', {
'text': 'Add',
@ -295,17 +299,17 @@ Block.Search.Item = new Class({
)
);
Array.each(self.alternative_names, function(alt){
Array.each(self.alternative_titles, function(alt){
new Element('option', {
'text': alt.name
}).inject(self.name_select)
'text': alt.title
}).inject(self.title_select)
})
Array.each(Quality.profiles, function(q){
Array.each(Quality.profiles, function(profile){
new Element('option', {
'value': q.indentifier,
'text': q.label
}).inject(self.quality_select)
'value': profile.id ? profile.id : profile.data.id,
'text': profile.label ? profile.label : profile.data.label
}).inject(self.profile_select)
});
self.options.addClass('set');

15
couchpotato/static/scripts/couchpotato.js

@ -114,8 +114,8 @@ var ApiClass = new Class({
}, options)).send()
},
createUrl: function(action){
return this.options.url + (action || 'default') + '/'
createUrl: function(action, params){
return this.options.url + (action || 'default') + '/' + (params ? '?'+Object.toQueryString(params) : '')
},
getOption: function(name){
@ -186,6 +186,17 @@ var p = function(){
console.log(arguments)
};
function randomString(length, extra) {
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz" + (extra ? '-._!@#$%^&*()+=' : '');
var stringLength = length || 8;
var randomString = '';
for (var i = 0; i < stringLength; i++) {
var rnum = Math.floor(Math.random() * chars.length);
randomString += chars.charAt(rnum);
}
return randomString;
}
(function(){
var keyPaths = [];

77
couchpotato/static/scripts/file.js

@ -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();

83
couchpotato/static/scripts/library/mootools.js

@ -3,10 +3,10 @@
MooTools: the javascript framework
web build:
- http://mootools.net/core/bd6349d3fbc489736e5aefb01157c8a8
- http://mootools.net/core/c1215700e7dedaa9d48503126daf2111
packager build:
- packager build Core/Class Core/Class.Extras Core/Element Core/Element.Style Core/Element.Dimensions Core/Fx.Tween Core/Fx.Transitions Core/Request.JSON Core/DOMReady
- packager build Core/Class Core/Class.Extras Core/Element Core/Element.Style Core/Element.Dimensions Core/Fx.Tween Core/Fx.Morph Core/Fx.Transitions Core/Request.JSON Core/DOMReady
/*
---
@ -4088,6 +4088,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;
}
});
/*
---
name: Fx.Transitions
description: Contains a set of advanced transitions to be used with any of the Fx Classes.

847
couchpotato/static/scripts/library/mootools_more.js

@ -1,6 +1,6 @@
// MooTools: the javascript framework.
// Load this file's selection again by visiting: http://mootools.net/more/a6033a03c64d978cd0033a1d420e16e0
// Or build this file again with packager using: packager build More/Element.Forms More/Element.Delegation More/Element.Shortcuts More/Request.JSONP More/Spinner
// Load this file's selection again by visiting: http://mootools.net/more/2b832e45b9bf2f9e5fdbdafc9b16febf
// Or build this file again with packager using: packager build More/Element.Forms More/Element.Delegation More/Element.Shortcuts More/Fx.Slide More/Sortables More/Request.JSONP More/Spinner
/*
---
@ -770,6 +770,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;
}
});
/*
---
script: Request.JSONP.js
name: Request.JSONP

78
couchpotato/static/scripts/page/settings.js

@ -11,6 +11,12 @@ Page.Settings = new Class({
},
'providers': {
'label': 'Providers'
},
'downloaders': {
'label': 'Downloaders'
},
'notifications': {
'label': 'Notifications'
}
},
@ -27,13 +33,13 @@ Page.Settings = new Class({
openTab: function(action){
var self = this;
action = action || self.action
var action = action || self.action;
if(self.current)
self.toggleTab(self.current, true);
self.toggleTab(action)
self.current = action;
var tab = self.toggleTab(action)
self.current = tab == self.tabs.general ? 'general' : action;
},
@ -47,6 +53,7 @@ Page.Settings = new Class({
t.tab[a](c);
t.content[a](c);
return t
},
getData: function(onComplete){
@ -121,7 +128,7 @@ Page.Settings = new Class({
// Add options to group
group.options.sortBy('order').each(function(option){
var class_name = (option.type || 'input').capitalize();
var class_name = (option.type || 'string').capitalize();
var input = new Option[class_name](self, section_name, option.name, option);
input.inject(group_el);
});
@ -145,13 +152,13 @@ Page.Settings = new Class({
var tab_el = new Element('li').adopt(
new Element('a', {
'href': '/'+self.name+'/'+tab_name+'/',
'text': tab.label.capitalize()
'text': (tab.label || tab.name).capitalize()
})
).inject(self.tabs_container);
if(!self.tabs[tab_name])
self.tabs[tab_name] = {
'label': tab.label
'label': tab.label || tab.name
}
self.tabs[tab_name] = Object.merge(self.tabs[tab_name], {
@ -171,7 +178,7 @@ Page.Settings = new Class({
'class': group.advanced ? 'inlineLabels advanced' : 'inlineLabels'
}).adopt(
new Element('h2', {
'text': group.label
'text': group.label || group.name.capitalize()
}).adopt(
new Element('span.hint', {
'text': group.description
@ -222,6 +229,13 @@ var OptionBase = new Class({
},
create: function(){},
createLabel: function(){
var self = this;
return new Element('label', {
'text': self.options.label || self.options.name.capitalize()
})
},
setAdvanced: function(){
this.el.addClass(this.options.advanced ? 'advanced': '')
@ -319,9 +333,7 @@ Option.String = new Class({
var self = this
self.el.adopt(
new Element('label', {
'text': self.options.label
}),
self.createLabel(),
self.input = new Element('input', {
'type': 'text',
'name': self.postName(),
@ -337,18 +349,17 @@ Option.Dropdown = new Class({
create: function(){
var self = this
new Element('label', {
'text': self.options.label
}).adopt(
self.el.adopt(
self.createLabel(),
self.input = new Element('select', {
'name': self.postName()
})
).inject(self.el)
)
Object.each(self.options.values, function(label, value){
Object.each(self.options.values, function(value){
new Element('option', {
'text': label,
'value': value
'text': value[0],
'value': value[1]
}).inject(self.input)
})
@ -366,24 +377,31 @@ Option.Checkbox = new Class({
var randomId = 'option-'+Math.floor(Math.random()*1000000)
new Element('label', {
'text': self.options.label,
'for': randomId
}).inject(self.el);
self.input = new Element('input', {
'type': 'checkbox',
'value': self.getSettingValue(),
'checked': self.getSettingValue() !== undefined,
'id': randomId
}).inject(self.el);
self.el.adopt(
self.createLabel().set('for', randomId),
self.input = new Element('input', {
'type': 'checkbox',
'value': self.getSettingValue(),
'checked': self.getSettingValue() !== undefined,
'id': randomId
})
)
}
});
Option.Password = new Class({
Extends: Option.String,
type: 'password'
});
Option.Bool = new Class({
Extends: Option.Checkbox
});
Option.Enabler = new Class({
Extends: Option.Bool
});
Option.Int = new Class({
Extends: Option.String
});
@ -401,9 +419,7 @@ Option.Directory = new Class({
self.el.adopt(
new Element('label', {
'text': self.options.label
}),
self.createLabel(),
self.input = new Element('span', {
'text': self.getSettingValue(),
'events': {

281
couchpotato/static/scripts/page/wanted.js

@ -18,19 +18,28 @@ Page.Wanted = new Class({
if(!self.movie_container)
self.movie_container = new Element('div.movies').inject(self.el);
self.movie_container.empty();
Object.each(self.movies, function(info){
var m = new Movie(self, {}, info);
$(m).inject(self.movie_container);
});
self.movie_container.addEvents({
'mouseenter:relay(.movie)': function(e, el){
el.addClass('hover')
},
'mouseleave:relay(.movie)': function(e, el){
el.removeClass('hover')
}
})
},
get: function(status, onComplete){
var self = this
if(self.movies.length == 0)
Api.request('movie', {
Api.request('movie.list', {
'data': {},
'onComplete': function(json){
self.store(json.movies);
@ -58,16 +67,280 @@ var Movie = new Class({
self.data = data;
self.profile = Quality.getProfile(data.profile_id);
self.parent(self, options);
},
create: function(){
var self = this;
self.el = new Element('div.movie', {
'text': self.data.name
self.el = new Element('div.movie').adopt(
self.data_container = new Element('div').adopt(
self.thumbnail = File.Select.single('poster', self.data.library.files),
self.title = new Element('div.title', {
'text': self.getTitle()
}),
self.description = new Element('div.description', {
'text': self.data.library.plot
}),
self.rating = new Element('div.rating', {
'text': self.data.library.rating || 10
}),
self.year = new Element('div.year', {
'text': self.data.library.year || 'Unknown'
}),
self.quality = new Element('div.quality', {
'text': self.profile.get('label')
}),
self.actions = new Element('div.actions').adopt(
self.action_imdb = new Movie.Action.IMDB(self),
self.action_edit = new Movie.Action.Edit(self),
self.action_refresh = new Movie.Action.Refresh(self),
self.action_delete = new Movie.Action.Delete(self)
)
)
);
},
getTitle: function(){
var self = this;
var titles = self.data.library.titles;
var title = titles.filter(function(title){
return title['default']
}).pop()
if(title)
return title.title
else if(titles.length > 0)
return titles[0].title
return 'Unknown movie'
},
get: function(attr){
return this.data[attr] || this.data.library[attr]
}
});
var MovieAction = new Class({
class_name: 'action',
initialize: function(movie){
var self = this;
self.movie = movie;
self.create();
self.el.addClass(self.class_name)
},
create: function(){},
disable: function(){
this.el.addClass('disable')
},
enable: function(){
this.el.removeClass('disable')
},
toElement: function(){
return this.el
}
})
Movie.Action = {}
Movie.Action.Edit = new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.edit', {
'text': 'edit',
'title': 'Refresh the movie info and do a forced search',
'events': {
'click': self.editMovie.bind(self)
}
});
},
editMovie: function(e){
var self = this;
(e).stop();
self.optionContainer = new Element('div.options').adopt(
$(self.movie.thumbnail).clone(),
self.title_select = new Element('select', {
'name': 'title'
}),
self.profile_select = new Element('select', {
'name': 'profile'
}),
new Element('a.button.edit', {
'text': 'Save',
'events': {
'click': self.save.bind(self)
}
})
).inject(self.movie, 'top');
},
save: function(){
var self = this;
Api.request('movie.edit', {
'data': {
'default_title': self.title_select.get('value'),
'profile_id': self.profile_select.get('value')
},
'useSpinner': true,
'spinnerTarget': self.movie
})
}
})
Movie.Action.IMDB = new Class({
Extends: MovieAction,
id: null,
create: function(){
var self = this;
self.id = self.movie.get('identifier');
self.el = new Element('a.imdb', {
'text': 'imdb',
'title': 'Go to the IMDB page of ' + self.movie.getTitle(),
'events': {
'click': self.gotoIMDB.bind(self)
}
});
if(!self.id) self.disable();
},
gotoIMDB: function(e){
var self = this;
(e).stop();
window.open('http://www.imdb.com/title/'+self.id+'/');
}
})
Movie.Action.Refresh = new Class({
Extends: MovieAction,
create: function(){
var self = this;
self.el = new Element('a.refresh', {
'text': 'refresh',
'title': 'Refresh the movie info and do a forced search',
'events': {
'click': self.doSearch.bind(self)
}
});
},
doSearch: function(e){
var self = this;
(e).stop();
Api.request('movie.refresh', {
'data': {
'id': self.movie.get('id')
}
})
}
})
Movie.Action.Delete = new Class({
Extends: MovieAction,
Implements: [Chain],
create: function(){
var self = this;
self.el = new Element('a.delete', {
'text': 'delete',
'title': 'Remove the movie from your wanted list',
'events': {
'click': self.showConfirm.bind(self)
}
});
},
showConfirm: function(e){
var self = this;
(e).stop();
self.mask = $(self.movie).mask({
'destroyOnHide': true
});
$(self.mask).adopt(
new Element('a.button.delete', {
'text': 'Delete movie',
'events': {
'click': self.del.bind(self)
}
}),
new Element('span', {
'text': 'or'
}),
new Element('a.button.cancel', {
'text': 'Cancel',
'events': {
'click': self.mask.hide.bind(self.mask)
}
})
);
},
del: function(e){
(e).stop()
var self = this;
var movie = $(self.movie);
self.chain(
function(){
$(self.mask).empty().addClass('loading');
self.callChain();
},
function(){
Api.request('movie.delete', {
'data': {
'id': self.movie.get('id')
},
'onComplete': function(){
p(movie, $(self.movie))
movie.slide('in');
}
})
}
);
self.callChain();
}
})

134
couchpotato/static/scripts/quality.js

@ -6,13 +6,19 @@ var QualityBase = new Class({
setup: function(data){
var self = this;
self.profiles = data.profiles;
self.qualities = data.qualities;
self.profiles = {}
Object.each(data.profiles, self.createProfilesClass.bind(self));
App.addEvent('load', self.addSettings.bind(self))
},
getProfile: function(id){
return this.profiles[id]
},
addSettings: function(){
var self = this;
@ -47,22 +53,34 @@ var QualityBase = new Class({
new Element('a.add_new', {
'text': 'Create a new quality profile',
'events': {
'click': self.createNewProfile.bind(self)
'click': function(){
var profile = self.createProfilesClass();
$(profile).inject(self.profile_container, 'top')
}
}
}),
self.profile_container = new Element('div.container')
)
Object.each(self.profiles, self.createNewProfile.bind(self))
Object.each(self.profiles, function(profile){
if(!profile.isCore())
$(profile).inject(self.profile_container, 'top')
})
},
createNewProfile: function(data, nr){
createProfilesClass: function(data){
var self = this;
self.profiles[nr] = new Profile(data);
$(self.profiles[nr]).inject(self.profile_container)
if(data){
return self.profiles[data.id] = new Profile(data);
}
else {
var data = {
'id': randomString()
}
return self.profiles[data.id] = new Profile(data);
}
},
/**
@ -95,7 +113,7 @@ var QualityBase = new Class({
window.Quality = new QualityBase();
var Profile = new Class({
data: {},
types: [],
@ -119,12 +137,9 @@ var Profile = new Class({
var data = self.data;
self.el = new Element('div', {
'class': 'profile'
}).adopt(
new Element('h4', {'text': data.label}),
new Element('span.delete', {
'html': 'del',
self.el = new Element('div.profile').adopt(
self.header = new Element('h4', {'text': data.label}),
new Element('span.delete.icon', {
'events': {
'click': self.del.bind(self)
}
@ -135,25 +150,29 @@ var Profile = new Class({
new Element('label', {'text':'Name'}),
new Element('input.label.textInput.large', {
'type':'text',
'value': data.label
'value': data.label,
'events': {
'keyup': function(){
self.header.set('text', this.get('value'))
}
}
})
),
new Element('div.ctrlHolder').adopt(
new Element('label', {'text':'Wait'}),
new Element('input.wait_for.textInput.xsmall', {
'type':'text',
'value': data.wait_for
'value': data.types && data.types.length > 0 ? data.types[0].wait_for : 0
}),
new Element('span', {'text':' day(s) for better quality.'})
),
new Element('div.ctrlHolder').adopt(
new Element('label', {'text': 'Qualities'}),
self.type_container = new Element('div.types').adopt(
new Element('div.head').adopt(
new Element('span.quality_type', {'text': 'Search for'}),
new Element('span.finish', {'html': '<acronym title="Won\'t download anything else if it has found this quality.">Finish</acronym>'})
)
new Element('div.head').adopt(
new Element('span.quality_type', {'text': 'Search for'}),
new Element('span.finish', {'html': '<acronym title="Won\'t download anything else if it has found this quality.">Finish</acronym>'})
),
self.type_container = new Element('ol.types'),
new Element('a.addType', {
'text': 'Add another quality to search for.',
'href': '#',
@ -164,6 +183,8 @@ var Profile = new Class({
)
);
self.makeSortable()
if(data.types)
Object.each(data.types, self.addType.bind(self))
},
@ -174,11 +195,19 @@ var Profile = new Class({
if(self.save_timer) clearTimeout(self.save_timer);
self.save_timer = (function(){
var data = self.getData();
if(data.types.length < 2) return;
Api.request('profile.save', {
'data': self.getData(),
'useSpinner': true,
'spinnerOptions': {
'target': self.el
},
'onComplete': function(json){
if(json.success){
self.data = json.profile
}
}
});
}).delay(delay, self)
@ -194,12 +223,15 @@ var Profile = new Class({
'wait_for' : self.el.getElement('.wait_for').get('value'),
'types': []
}
Object.each(self.types, function(type){
if(!type.deleted)
data.types.include(type.getData());
Array.each(self.type_container.getElements('.type'), function(type){
if(!type.hasClass('deleted'))
data.types.include({
'quality_id': type.getElement('select').get('value'),
'finish': +type.getElement('input[type=checkbox]').checked
});
})
return data
},
@ -208,6 +240,7 @@ var Profile = new Class({
var t = new Profile.Type(data);
$(t).inject(self.type_container);
self.sortable.addItems($(t));
self.types.include(t);
@ -216,6 +249,8 @@ var Profile = new Class({
del: function(){
var self = this;
if(!confirm('Are you sure you want to delete this profile?')) return
Api.request('profile.delete', {
'data': {
'id': self.data.id
@ -224,12 +259,35 @@ var Profile = new Class({
'spinnerOptions': {
'target': self.el
},
'onComplete': function(){
self.el.destroy();
'onComplete': function(json){
if(json.success)
self.el.destroy();
else
alert(json.message)
}
});
},
makeSortable: function(){
var self = this;
self.sortable = new Sortables(self.type_container, {
'revert': true,
//'clone': true,
'handle': '.handle',
'opacity': 0.5,
'onComplete': self.save.bind(self, 300)
});
},
get: function(attr){
return this.data[attr]
},
isCore: function(){
return this.data.core
},
toElement: function(){
return this.el
}
@ -252,7 +310,7 @@ Profile.Type = Class({
var self = this;
var data = self.data;
self.el = new Element('div.type').adopt(
self.el = new Element('li.type').adopt(
new Element('span.quality_type').adopt(
self.fillQualities()
),
@ -263,15 +321,12 @@ Profile.Type = Class({
'checked': data.finish
})
),
new Element('span.delete', {
'html': 'del',
new Element('span.delete.icon', {
'events': {
'click': self.del.bind(self)
}
}),
new Element('span', {
'class':'handle'
})
new Element('span.handle')
)
},
@ -284,21 +339,21 @@ Profile.Type = Class({
Object.each(Quality.qualities, function(q){
new Element('option', {
'text': q.label,
'value': q.identifier
'value': q.id
}).inject(self.qualities)
});
self.qualities.set('value', self.data.quality);
self.qualities.set('value', self.data.quality_id);
return self.qualities;
},
getData: function(){
var self = this;
return {
'quality': self.qualities.get('value'),
'quality_id': self.qualities.get('value'),
'finish': +self.finish.checked
}
},
@ -306,6 +361,7 @@ Profile.Type = Class({
del: function(){
var self = this;
self.el.addClass('deleted');
self.el.hide();
self.deleted = true;
},

11
couchpotato/static/scripts/status.js

@ -0,0 +1,11 @@
var StatusBase = new Class({
setup: function(statuses){
var self = this;
self.statuses = statuses;
}
});
window.Status = new StatusBase();

6
couchpotato/static/style/main.css

@ -121,6 +121,12 @@ form {
cursor: pointer;
}
/*** Icons ***/
.icon.delete {
background: url('../images/delete.png') no-repeat;
display: inline-block;
}
/*** Navigation ***/
.header {
background: #f7f7f7;

6
couchpotato/static/style/movie_add.css → couchpotato/static/style/plugin/movie_add.css

@ -24,7 +24,7 @@
margin: 0 0 -5px -20px;
top: 4px;
right: 5px;
background: url('../images/close_button.png') 0 center no-repeat;
background: url('../../images/close_button.png') 0 center no-repeat;
cursor: pointer;
}
.search_form .input a:hover { background-position: -12px center; }
@ -46,7 +46,7 @@
-moz-border-radius: 3px;
}
.search_form .spinner {
background: #fff url('../images/spinner.gif') no-repeat center 70px;
background: #fff url('../../images/spinner.gif') no-repeat center 70px;
}
.search_form .pointer {
@ -83,7 +83,7 @@
display: inline-block;
margin-right: 10px;
}
.search_form .results .movie .options select[name=name] { width: 180px; }
.search_form .results .movie .options select[name=title] { width: 180px; }
.search_form .results .movie .options select[name=quality] { width: 90px; }
.search_form .results .movie .options .button {

21
couchpotato/static/style/plugin/quality.css

@ -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;
}

49
couchpotato/templates/_desktop.html

@ -1,12 +1,13 @@
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="{{ url_for('.static', filename='style/main.css') }}">
<link rel="stylesheet" href="{{ url_for('.static', filename='style/uniform.generic.css') }}">
<link rel="stylesheet" href="{{ url_for('.static', filename='style/uniform.css') }}">
<link rel="stylesheet" href="{{ url_for('.static', filename='style/main.css') }}" type="text/css">
<link rel="stylesheet" href="{{ url_for('.static', filename='style/uniform.generic.css') }}" type="text/css">
<link rel="stylesheet" href="{{ url_for('.static', filename='style/uniform.css') }}" type="text/css">
<link rel="stylesheet" href="{{ url_for('.static', filename='style/page/settings.css') }}">
<link rel="stylesheet" href="{{ url_for('.static', filename='style/movie_add.css') }}">
<link rel="stylesheet" href="{{ url_for('.static', filename='style/page/settings.css') }}" type="text/css">
<link rel="stylesheet" href="{{ url_for('.static', filename='style/plugin/movie_add.css') }}" type="text/css">
<link rel="stylesheet" href="{{ url_for('.static', filename='style/plugin/quality.css') }}" type="text/css">
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/mootools.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/library/mootools_more.js') }}"></script>
@ -30,8 +31,11 @@
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/page/manage.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/quality.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/status.js') }}"></script>
<script type="text/javascript" src="{{ url_for('.static', filename='scripts/file.js') }}"></script>
<link href="{{ url_for('.static', filename='images/favicon.ico') }}" rel="icon" type="image/x-icon" />
<link rel="apple-touch-icon" href="{{ url_for('.static', filename='images/homescreen.png') }}" />
<script type="text/javascript">
window.addEvent('domready', function() {
@ -44,34 +48,13 @@
});
Quality.setup({
'profiles': [
{
'identifier': 'hd',
'label': 'HD',
'wait_for': 7,
'types': [
{'quality': '720p', 'finish': true},
{'quality': '1080p', 'finish': true}
]
},{
'identifier': 'best',
'label': 'Best',
'wait_for': 7,
'types': [
{'quality': '720p', 'finish': true},
{'quality': '1080p', 'finish': true},
{'quality': 'brrip'},
{'quality': 'dvdrip'}
]
}
],
'qualities': [
{'identifier': '1080p', 'label': '1080P'},
{'identifier': '720p', 'label': '720P'},
{'identifier': 'brrip', 'label': 'BR-Rip'},
{'identifier': 'dvdrip', 'label': 'DVD-Rip'}
]
})
'profiles': {{ fireEvent('profile.all', single = True)|tojson|safe }},
'qualities': {{ fireEvent('quality.all', single = True)|tojson|safe }}
});
Status.setup({{ fireEvent('status.all', single = True)|tojson|safe }});
File.Type.setup({{ fireEvent('file.types', single = True)|tojson|safe }});
App.setup({
'base_url': '{{ request.path }}'

4
libs/axl/axel.py

@ -179,10 +179,12 @@ class Event(object):
if not self.asynchronous:
self.result.append(tuple(r))
except Exception:
except Exception, e:
if not self.asynchronous:
self.result.append((False, self._error(sys.exc_info()),
handler))
else:
self.error_handler(sys.exc_info())
finally:
if isinstance(self.lock, threading._RLock):
self.lock.release()

30
setup.py

@ -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
""")
Loading…
Cancel
Save